From 90a8a133195ee1b8998f144c7cb36e773f8a90ba Mon Sep 17 00:00:00 2001 From: Matthew Carroll <28577806+MJC598@users.noreply.github.com> Date: Mon, 6 Jul 2026 16:02:16 -0400 Subject: [PATCH 1/4] [Feature] Timestep (#142) * uml and some basic constructors Updating public diagram updating UML reverting index and uml * Updating timestep interface to match UML and missing parts of UML * updating model and timestep to reflect new flow * including UML in doxyfile * Addressing PR comments --- Doxyfile.in | 1 + docs/src/uml.md | 12 +- extras/benchmark/src/benchmark_respond.cpp | 2 +- include/respond/constants.hpp | 18 +++ include/respond/history.hpp | 8 +- include/respond/model.hpp | 16 ++- include/respond/simulation.hpp | 33 ++++- include/respond/timestep.hpp | 156 +++++++++++++++++++++ include/respond/transition.hpp | 23 ++- include/respond/transition_factory.hpp | 41 ------ src/internals/background.hpp | 15 +- src/internals/behavior.hpp | 15 +- src/internals/intervention.hpp | 15 +- src/internals/markov.hpp | 13 +- src/internals/migration.hpp | 15 +- src/internals/overdose.hpp | 15 +- src/internals/transition_base.hpp | 13 +- src/logging.cpp | 6 +- src/{markov.cpp => model_factory.cpp} | 0 src/transition_factory.cpp | 19 ++- tests/integration/respond_test.cpp | 49 ++++++- 21 files changed, 344 insertions(+), 141 deletions(-) create mode 100644 include/respond/constants.hpp create mode 100644 include/respond/timestep.hpp delete mode 100644 include/respond/transition_factory.hpp rename src/{markov.cpp => model_factory.cpp} (100%) diff --git a/Doxyfile.in b/Doxyfile.in index d568dfaa..3332584e 100644 --- a/Doxyfile.in +++ b/Doxyfile.in @@ -927,6 +927,7 @@ INPUT = docs/src/index.md \ docs/src/faq.md \ docs/src/architecture.md \ docs/src/api-guide.md \ + docs/src/uml.md \ include # This tag can be used to specify the character encoding of the source files diff --git a/docs/src/uml.md b/docs/src/uml.md index 91869cf8..0f6625d1 100644 --- a/docs/src/uml.md +++ b/docs/src/uml.md @@ -40,13 +40,19 @@ classDiagram } class Timestep { + +Timestep() + +Timestep(const string &log_name) + +Timestep(const string &log_name, const string &log_filepath) + +Timestep(const Timestep &other) + +operator=(const Timestep &other) Timestep & + +Timestep(const Timestep &&other) + +operator=(const Timestep &&other) Timestep & +CreateTransition(type) const Transition & +AddMatrixToTransition(size_t index, MatrixXd mat) +GetTransition(size_t idx) const Transition & +GetTransition(string name) const Transition & +GetTransitions() const vector~const Transition &~ +GetTransitionNames() vector~const string~ - +clone() unique_ptr~Timestep~ +operator<<(ostream &os, const Timestep &obj) ostream & } @@ -220,8 +226,8 @@ classDiagram -vector~MatrixXd~ _transition_matrices -GetMatrices() const vector & +AddMatrix(const Eigen::Ref~const MatrixXd~ &matrix, size_t idx) override - +GetTransitionName() string override - +ClearTransitionMatrices() override + +GetName() string override + +ClearMatrices() override +GetLogName() string override } diff --git a/extras/benchmark/src/benchmark_respond.cpp b/extras/benchmark/src/benchmark_respond.cpp index e484bb67..deb95a89 100644 --- a/extras/benchmark/src/benchmark_respond.cpp +++ b/extras/benchmark/src/benchmark_respond.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-04-27 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-04-27 // +// Last Modified: 2026-06-29 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // diff --git a/include/respond/constants.hpp b/include/respond/constants.hpp new file mode 100644 index 00000000..bc7b84a4 --- /dev/null +++ b/include/respond/constants.hpp @@ -0,0 +1,18 @@ +//////////////////////////////////////////////////////////////////////////////// +// File: constants.hpp // +// Project: respond // +// Created Date: 2026-07-06 // +// Author: Matthew Carroll // +// ----- // +// Last Modified: 2026-07-06 // +// Modified By: Matthew Carroll // +// ----- // +// Copyright (c) 2026 Syndemics Lab at Boston Medical Center // +//////////////////////////////////////////////////////////////////////////////// + +#ifndef RESPOND_CONSTANTS_HPP_ +#define RESPOND_CONSTANTS_HPP_ + +#define RESPOND_DEFAULT_LOG "respond" + +#endif \ No newline at end of file diff --git a/include/respond/history.hpp b/include/respond/history.hpp index e92598d7..34eaedf3 100644 --- a/include/respond/history.hpp +++ b/include/respond/history.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-25 // +// Last Modified: 2026-06-30 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -36,12 +36,14 @@ inline HistoryMode GetDefaultHistoryMode(const std::string &name) { /// are filled with zero vectors). class History { public: + History() : History("state") {} + + History(const std::string &name) : History(name, "console") {} /// @brief Constructs a History tracker. /// @param name The identifier for this history (default: "state"). /// @param log_name The logger name for error reporting (default: /// "console"). - History(const std::string &name = "state", - const std::string &log_name = "console") + History(const std::string &name, const std::string &log_name) : History(name, log_name, GetDefaultHistoryMode(name)) {} /// @brief Constructs a History tracker with an explicit recording mode. diff --git a/include/respond/model.hpp b/include/respond/model.hpp index ee12217f..6ac33171 100644 --- a/include/respond/model.hpp +++ b/include/respond/model.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-25 // +// Last Modified: 2026-07-02 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -37,17 +37,21 @@ class Model { /// @brief Retrieves the current state of the model. /// @return A copy of the current state vector (limited to observation). - virtual Eigen::VectorXd GetState() const = 0; + virtual const Eigen::Ref &GetState() const = 0; + + virtual void AddTimestep(const std::shared_ptr &transition) = 0; /// @brief Executes all registered transitions on the current state. /// Transitions are applied in the order they were added and may modify /// history. virtual void RunTransitions() = 0; - /// @brief Adds a transition to the model. - /// @param t A unique_ptr to a Transition object. The model assumes - /// ownership. - virtual void AddTransition(const std::unique_ptr &t) = 0; + /// @brief Adds a transition to the model. The model takes ownership of the + /// transition via cloning. + /// @param transitions A vector of unique_ptrs to a Transition instances to + /// add. + virtual void AddTimestep( + const std::vector> &transitions) = 0; /// @brief Retrieves the names of all registered transitions. /// @return Vector of transition names in the order they were added. diff --git a/include/respond/simulation.hpp b/include/respond/simulation.hpp index b9b7fb55..d6c2fbb9 100644 --- a/include/respond/simulation.hpp +++ b/include/respond/simulation.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-02-12 // +// Last Modified: 2026-07-02 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -21,6 +21,7 @@ #include #include +#include #include namespace respond { @@ -30,20 +31,37 @@ namespace respond { class Simulation { public: /// @brief Default constructor initializing with "console" logger. - Simulation() : Simulation("console") {} + Simulation() : Simulation("respond") {} /// @brief Constructs a Simulation with a specified logger. /// @param log_name Name of the logger for this simulation (default: /// "console"). - Simulation(const std::string &log_name) : _log_name(log_name) {} + Simulation(const std::string &log_name) + : Simulation(log_name, log_name + ".log") {} + + /// @brief Constructs a Simulation with a specified logger and log file + /// path. + /// @param log_name + /// @param log_filepath + Simulation(const std::string &log_name, const std::string &log_filepath) + : _log_name(log_name) { + CreateFileLogger(log_name, log_filepath); + } /// @brief Virtual destructor for polymorphic cleanup. ~Simulation() = default; + const std::string CreateNewModel(const std::string &model_name) { + _models.push_back(Model::Create(model_name, _log_name)); + return std::to_string(_models.size()) + "_" + + _models.back()->GetModelName(); + } + /// @brief Executes one step of the simulation for all models. /// Calls RunTransitions() on each registered model in sequence. void Run() { for (const auto &model : _models) { + model->SetFinalTimestep(_duration); model->RunTransitions(); } } @@ -152,6 +170,15 @@ class Simulation { private: std::string _log_name; std::vector> _models; + + int _duration = 1; // Default simulation duration in timesteps + std::vector _parameter_change_times; + bool _stratify_entering_cohort; + + bool _build_summary_stats; + bool _save_state_history; + std::vector _timesteps_to_report; + bool _pivot_long; }; } // namespace respond diff --git a/include/respond/timestep.hpp b/include/respond/timestep.hpp new file mode 100644 index 00000000..4e414ff0 --- /dev/null +++ b/include/respond/timestep.hpp @@ -0,0 +1,156 @@ +//////////////////////////////////////////////////////////////////////////////// +// File: timestep.hpp // +// Project: respond // +// Created Date: 2026-06-30 // +// Author: Matthew Carroll // +// ----- // +// Last Modified: 2026-07-06 // +// Modified By: Matthew Carroll // +// ----- // +// Copyright (c) 2026 Syndemics Lab at Boston Medical Center // +//////////////////////////////////////////////////////////////////////////////// +#ifndef RESPOND_TIMESTEP_HPP_ +#define RESPOND_TIMESTEP_HPP_ + +#include +#include +#include + +#include +#include +#include + +namespace respond { +class Timestep { +public: + Timestep() : Timestep(RESPOND_DEFAULT_LOG) {} + Timestep(const std::string &log_name) + : Timestep(log_name, log_name + ".log") {} + Timestep(const std::string &log_name, const std::string &log_filepath) + : _log_name(log_name) { + CreateFileLogger(log_name, log_filepath); + _transitions = {}; + } + ~Timestep() = default; + + const Transition & + CreateTransition(const std::string &transition_name) const { + auto transition = Transition::Create(transition_name, _log_name); + return *transition; + } + + void AddMatrixToTransition(const size_t &idx, + const Eigen::Ref &m) { + if (idx >= _transitions.size()) { + throw std::out_of_range( + "Index out of range in AddMatrixToTransition"); + } + _transitions[idx]->AddMatrix(m); + } + + void AddMatrixToTransition(const std::string &transition_name, + const Eigen::Ref &m) { + for (size_t i = 0; i < _transitions.size(); ++i) { + if (_transitions[i]->GetName() == transition_name) { + _transitions[i]->AddMatrix(m); + return; + } + } + LogWarning(_log_name, + "Transition not found in AddMatrixToTransition: " + + transition_name); + } + + const std::unique_ptr &GetTransition(const size_t &idx) const { + if (idx >= _transitions.size()) { + LogWarning(_log_name, "Index out of range in GetTransition: " + + std::to_string(idx)); + } + return _transitions[idx]; + } + + const std::unique_ptr & + GetTransition(const std::string &transition_name) const { + for (const auto &t : _transitions) { + if (t->GetName() == transition_name) { + return t; + } + } + throw std::invalid_argument("Transition not found in GetTransition: " + + transition_name); + } + + std::vector> GetTransitions() const { + std::vector> _ret; + for (const auto &t : _transitions) { + _ret.push_back(t->clone()); + } + return _ret; + } + + const std::unique_ptr & + GetTransitionAtIndex(size_t index) const { + if (index >= _transitions.size()) { + throw std::out_of_range( + "Index out of range in GetTransitionAtIndex"); + } + return _transitions[index]; + } + + const std::vector &GetTransitionNames() const { + std::vector names; + for (const auto &t : _transitions) { + names.push_back(t->GetName()); + } + return names; + } + + // Copy Constructor and Assignment + + Timestep(const Timestep &other) { + _transitions.clear(); + for (const auto &t : other._transitions) { + _transitions.push_back(t->clone()); + } + } + + Timestep &operator=(const Timestep &other) { + if (this != &other) { + _transitions.clear(); + for (const auto &t : other._transitions) { + _transitions.push_back(t->clone()); + } + } + return *this; + } + + // Move Constructor and Assignment + + Timestep(Timestep &&other) noexcept + : _transitions(std::move(other._transitions)) { + other._transitions.clear(); + } + + Timestep &operator=(Timestep &&other) noexcept { + if (this != &other) { + _transitions = std::move(other._transitions); + other._transitions.clear(); + } + return *this; + } + + friend std::ostream &operator<<(std::ostream &os, Timestep &other) { + os << "Timestep with the following transitions:\n"; + for (const auto &t : other._transitions) { + os << " - " << t->GetName() << "\n"; + } + return os; + } + +private: + std::string _log_name; + std::vector> _transitions; +}; +} // namespace respond + +#endif // RESPOND_TIMESTEP_HPP_ \ No newline at end of file diff --git a/include/respond/transition.hpp b/include/respond/transition.hpp index 9c2fd0dd..1fcb5334 100644 --- a/include/respond/transition.hpp +++ b/include/respond/transition.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-02 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-25 // +// Last Modified: 2026-07-06 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -46,15 +46,14 @@ class Transition { /// @brief Adds a transformation matrix to this transition. /// The matrix is stored for use during Execute() calls. /// @param m The transition matrix to add (not modified by this transition). - virtual void - AddTransitionMatrix(const Eigen::Ref &m) = 0; + virtual void AddMatrix(const Eigen::Ref &m) = 0; /// @brief Retrieves the name/type of this transition. /// @return The transition's identifier as a string. - virtual std::string GetTransitionName() const = 0; + virtual std::string GetName() const = 0; /// @brief Clears all stored transition matrices. - virtual void ClearTransitionMatrices() = 0; + virtual void ClearMatrices() = 0; /// @brief Retrieves the logger name used by this transition. /// @return The associated logger's name. @@ -71,6 +70,20 @@ class Transition { /// @return A unique_ptr to an independent copy of this transition. virtual std::unique_ptr clone() const = 0; + /// @brief Creates a transition of the specified type. + /// @param type The type of transition to create. Supported types + /// (case-insensitive): + /// - "migration": Population migration transitions + /// - "behavior": Behavioral state transitions + /// - "intervention": Intervention-driven transitions + /// - "overdose": Overdose-related transitions + /// - "background_death": Background mortality transitions + /// @param log_name The logger name for error reporting (e.g., "console"). + /// @return A unique_ptr to the created Transition, or nullptr if type is + /// unsupported. + static std::unique_ptr Create(const std::string &type, + const std::string &log_name); + protected: /// @brief Protected default constructor for subclass initialization. /// Not intended for direct public use. diff --git a/include/respond/transition_factory.hpp b/include/respond/transition_factory.hpp deleted file mode 100644 index 87fd8fc8..00000000 --- a/include/respond/transition_factory.hpp +++ /dev/null @@ -1,41 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// File: transition_factory.hpp // -// Project: respond // -// Created Date: 2026-02-05 // -// Author: Matthew Carroll // -// ----- // -// Last Modified: 2026-02-06 // -// Modified By: Matthew Carroll // -// ----- // -// Copyright (c) 2026 Syndemics Lab at Boston Medical Center // -//////////////////////////////////////////////////////////////////////////////// -#ifndef RESPOND_TRANSITION_FACTORY_HPP_ -#define RESPOND_TRANSITION_FACTORY_HPP_ - -#include - -#include - -namespace respond { -/// @brief Factory for creating concrete Transition instances. -/// This factory supports creation of various transition types used in the -/// RESPOND model. -class TransitionFactory { -public: - /// @brief Creates a transition of the specified type. - /// @param type The type of transition to create. Supported types - /// (case-insensitive): - /// - "migration": Population migration transitions - /// - "behavior": Behavioral state transitions - /// - "intervention": Intervention-driven transitions - /// - "overdose": Overdose-related transitions - /// - "background_death": Background mortality transitions - /// @param log_name The logger name for error reporting (e.g., "console"). - /// @return A unique_ptr to the created Transition, or nullptr if type is - /// unsupported. - static std::unique_ptr - CreateTransition(const std::string &type, const std::string &log_name); -}; -} // namespace respond - -#endif // RESPOND_TRANSITION_FACTORY_HPP_ \ No newline at end of file diff --git a/src/internals/background.hpp b/src/internals/background.hpp index 1ff631e6..28cbf822 100644 --- a/src/internals/background.hpp +++ b/src/internals/background.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-25 // +// Last Modified: 2026-07-02 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -30,19 +30,12 @@ class BackgroundDeath : public virtual TransitionBase { // Clone std::unique_ptr clone() const override { - auto ret = std::make_unique(GetTransitionName(), - GetLogName()); - for (const auto &t : GetTransitionMatrices()) { - ret->AddTransitionMatrix(t); + auto ret = std::make_unique(GetName(), GetLogName()); + for (const auto &t : GetMatrices()) { + ret->AddMatrix(t); } return ret; } - - /// @brief Factory method to create a Markov instance. - /// @param log_name Name of the logger to write errors to. - /// @return An instance of Markov. - static std::unique_ptr - Create(const std::string &name, const std::string &log_name = "console"); }; } // namespace respond diff --git a/src/internals/behavior.hpp b/src/internals/behavior.hpp index 555dbe19..44d2b4d5 100644 --- a/src/internals/behavior.hpp +++ b/src/internals/behavior.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-25 // +// Last Modified: 2026-07-02 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -30,19 +30,12 @@ class Behavior : public virtual TransitionBase { // Clone std::unique_ptr clone() const override { - auto ret = - std::make_unique(GetTransitionName(), GetLogName()); - for (const auto &t : GetTransitionMatrices()) { - ret->AddTransitionMatrix(t); + auto ret = std::make_unique(GetName(), GetLogName()); + for (const auto &t : GetMatrices()) { + ret->AddMatrix(t); } return ret; } - - /// @brief Factory method to create a Markov instance. - /// @param log_name Name of the logger to write errors to. - /// @return An instance of Markov. - static std::unique_ptr - Create(const std::string &name, const std::string &log_name = "console"); }; } // namespace respond diff --git a/src/internals/intervention.hpp b/src/internals/intervention.hpp index 39659849..6fd9a908 100644 --- a/src/internals/intervention.hpp +++ b/src/internals/intervention.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-25 // +// Last Modified: 2026-07-02 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -30,19 +30,12 @@ class Intervention : public virtual TransitionBase { // Clone std::unique_ptr clone() const override { - auto ret = - std::make_unique(GetTransitionName(), GetLogName()); - for (const auto &t : GetTransitionMatrices()) { - ret->AddTransitionMatrix(t); + auto ret = std::make_unique(GetName(), GetLogName()); + for (const auto &t : GetMatrices()) { + ret->AddMatrix(t); } return ret; } - - /// @brief Factory method to create a Markov instance. - /// @param log_name Name of the logger to write errors to. - /// @return An instance of Markov. - static std::unique_ptr - Create(const std::string &name, const std::string &log_name = "console"); }; } // namespace respond diff --git a/src/internals/markov.hpp b/src/internals/markov.hpp index a3f03cad..e2e7d780 100644 --- a/src/internals/markov.hpp +++ b/src/internals/markov.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-25 // +// Last Modified: 2026-06-29 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -115,15 +115,22 @@ class Markov : public virtual Model { if (!_initial_history_recorded) { RecordHistoryAtCurrentTimestep(); } + int transitions_per_timestep = + static_cast(_transition_vector.size()) / _final_timestep; for (const auto &t : _transition_vector) { _state = t->Execute(_state, _histories); } _current_timestep++; RecordHistoryAtCurrentTimestep(); } + // assume ownership of the Transition - void AddTransition(const std::unique_ptr &t) override { - _transition_vector.push_back(t->clone()); + void AddTimestep( + const std::vector> &transitions) override { + for (const auto &t : transitions) { + _transition_vector.push_back(t->clone()); + } + _final_timestep++; } // get the names of each transition we own std::vector GetTransitionNames() const override { diff --git a/src/internals/migration.hpp b/src/internals/migration.hpp index c63821e5..b7d37c4a 100644 --- a/src/internals/migration.hpp +++ b/src/internals/migration.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-25 // +// Last Modified: 2026-07-02 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -30,19 +30,12 @@ class Migration : public virtual TransitionBase { // Clone std::unique_ptr clone() const override { - auto ret = - std::make_unique(GetTransitionName(), GetLogName()); - for (const auto &t : GetTransitionMatrices()) { - ret->AddTransitionMatrix(t); + auto ret = std::make_unique(GetName(), GetLogName()); + for (const auto &t : GetMatrices()) { + ret->AddMatrix(t); } return ret; } - - /// @brief Factory method to create a Markov instance. - /// @param log_name Name of the logger to write errors to. - /// @return An instance of Markov. - static std::unique_ptr - Create(const std::string &name, const std::string &log_name = "console"); }; } // namespace respond diff --git a/src/internals/overdose.hpp b/src/internals/overdose.hpp index b1187754..4e446146 100644 --- a/src/internals/overdose.hpp +++ b/src/internals/overdose.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-25 // +// Last Modified: 2026-07-02 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -30,19 +30,12 @@ class Overdose : public virtual TransitionBase { // Clone std::unique_ptr clone() const override { - auto ret = - std::make_unique(GetTransitionName(), GetLogName()); - for (const auto &t : GetTransitionMatrices()) { - ret->AddTransitionMatrix(t); + auto ret = std::make_unique(GetName(), GetLogName()); + for (const auto &t : GetMatrices()) { + ret->AddMatrix(t); } return ret; } - - /// @brief Factory method to create a Markov instance. - /// @param log_name Name of the logger to write errors to. - /// @return An instance of Markov. - static std::unique_ptr - Create(const std::string &name, const std::string &log_name = "console"); }; } // namespace respond diff --git a/src/internals/transition_base.hpp b/src/internals/transition_base.hpp index aca34ca0..7ea64c1d 100644 --- a/src/internals/transition_base.hpp +++ b/src/internals/transition_base.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-25 // +// Last Modified: 2026-07-02 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -24,27 +24,26 @@ class TransitionBase : public virtual Transition { // Add a Transition Matrix to the set. We have no need to edit it once it's // been added, just use it. Thus, we don't need full ownership (reference) // and can accept the const type. - void - AddTransitionMatrix(const Eigen::Ref &m) override { + void AddMatrix(const Eigen::Ref &m) override { _transition_matrices.push_back(m); } // Get the name of the Transition. No need to edit the object and do not // need user to edit the name. - std::string GetTransitionName() const override { return _name; } + std::string GetName() const override { return _name; } // Clear out all the stored Eigen::MatrixXd values - void ClearTransitionMatrices() override { _transition_matrices.clear(); } + void ClearMatrices() override { _transition_matrices.clear(); } std::string GetLogName() const override { return _log_name; } protected: - const std::vector &GetTransitionMatrices() const { + const std::vector> &GetMatrices() const { return _transition_matrices; } private: std::string _name; std::string _log_name; - std::vector _transition_matrices; + std::vector> _transition_matrices; }; } // namespace respond diff --git a/src/logging.cpp b/src/logging.cpp index 032364ca..f3da2a3b 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -4,10 +4,10 @@ // Created Date: 2025-06-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2025-07-30 // +// Last Modified: 2026-06-30 // // Modified By: Matthew Carroll // // ----- // -// Copyright (c) 2025 Syndemics Lab at Boston Medical Center // +// Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center // //////////////////////////////////////////////////////////////////////////////// #include @@ -36,6 +36,8 @@ CreationStatus CreateFileLogger(const std::string &logger_name, std::cerr << error_msg << std::endl; return CreationStatus::kError; } + std::cout << "Initialized logger (" << logger_name << ") to log file (" + << filepath << ")" << std::endl; return CreationStatus::kSuccess; } diff --git a/src/markov.cpp b/src/model_factory.cpp similarity index 100% rename from src/markov.cpp rename to src/model_factory.cpp diff --git a/src/transition_factory.cpp b/src/transition_factory.cpp index 267d383e..60b5ef72 100644 --- a/src/transition_factory.cpp +++ b/src/transition_factory.cpp @@ -4,14 +4,12 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-02-05 // +// Last Modified: 2026-07-02 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // //////////////////////////////////////////////////////////////////////////////// -#include - #include #include #include @@ -26,23 +24,22 @@ #include "internals/overdose.hpp" namespace respond { -std::unique_ptr -TransitionFactory::CreateTransition(const std::string &type, - const std::string &log_name) { +std::unique_ptr Transition::Create(const std::string &type, + const std::string &log_name) { std::string type_copy = type; std::transform(type_copy.begin(), type_copy.end(), type_copy.begin(), [](unsigned char c) { return std::tolower(c); }); if (type_copy == "migration") { - return Migration::Create(type, log_name); + return std::make_unique(type, log_name); } else if (type_copy == "behavior") { - return Behavior::Create(type, log_name); + return std::make_unique(type, log_name); } else if (type_copy == "intervention") { - return Intervention::Create(type, log_name); + return std::make_unique(type, log_name); } else if (type_copy == "overdose") { - return Overdose::Create(type, log_name); + return std::make_unique(type, log_name); } else if (type_copy == "background_death") { - return BackgroundDeath::Create(type, log_name); + return std::make_unique(type, log_name); } // Invalid transition type diff --git a/tests/integration/respond_test.cpp b/tests/integration/respond_test.cpp index ce576615..33b9602e 100644 --- a/tests/integration/respond_test.cpp +++ b/tests/integration/respond_test.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-02-13 // +// Last Modified: 2026-06-29 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -128,6 +128,53 @@ TEST_F(RespondTest, RunSimulationOneStep) { ASSERT_TRUE(state_history[1].isApprox(final_state)); } +TEST_F(RespondTest, RunSimulationTwoStep) { + markov->CreateDefaultHistories(); + + markov->SetState(init_state); + + auto migr = MakeTestTransition("migration", migration_pop); + auto beha = MakeTestTransition("behavior", behavior_trans); + auto inte = MakeTestTransition("intervention", intervention_trans); + auto over = MakeTestTransition("overdose", overdose_prob); + over->AddTransitionMatrix(fod_prob); + + auto back = MakeTestTransition("background_death", background_death_prob); + + markov->AddTransition(migr); + markov->AddTransition(beha); + markov->AddTransition(inte); + markov->AddTransition(over); + markov->AddTransition(back); + + markov->AddTransition(migr); + markov->AddTransition(beha); + markov->AddTransition(inte); + markov->AddTransition(over); + markov->AddTransition(back); + + Simulation sim("test_logger"); + sim.AddModel(markov); + sim.Run(); + + auto histories = sim.GetModelHistories(); + ASSERT_EQ(histories.size(), 1); + + auto mm_histories = histories[0]; + if (mm_histories.find("state") == mm_histories.end()) { + FAIL() << "Unable to find the 'state' history."; + } + + auto state_history = mm_histories.at("state"); + // 2 because it carries the initial state and 1 step + ASSERT_EQ(state_history.size(), 3); + + Eigen::Vector3d final_state; + ASSERT_TRUE(state_history[0].isApprox(init_state)); + final_state << 0.76715528791564891, 0.72320370216816077, 1.037712429738102; + ASSERT_TRUE(state_history[1].isApprox(final_state)); +} + TEST_F(RespondTest, CreateDefaultHistories) { std::vector expected = { "state", "total_overdose", "fatal_overdose", "intervention_admission", From 59245b8016254418dfdb7c5872a8857966be7a4f Mon Sep 17 00:00:00 2001 From: Matthew Carroll <28577806+MJC598@users.noreply.github.com> Date: Thu, 9 Jul 2026 08:51:22 -0400 Subject: [PATCH 2/4] [Feature] Model Ownership (#143) * uml and some basic constructors Updating public diagram updating UML reverting index and uml * thinking through Model updates * Making repo wide changes to match transition and model syntax so project compiles. Tests commented out, no expectation simulation works yet * Updating model and unit tests --- CMakeLists.txt | 33 +- docs/src/api-guide.md | 14 +- docs/src/architecture.md | 4 +- docs/src/run.md | 2 +- docs/src/uml.md | 2 +- extras/benchmark/src/benchmark_respond.cpp | 25 +- include/respond/constants.hpp | 3 +- include/respond/model.hpp | 163 +++++---- include/respond/simulation.hpp | 11 +- include/respond/timestep.hpp | 248 ++++++++++--- include/respond/transition.hpp | 5 + src/background.cpp | 14 +- src/behavior.cpp | 20 +- src/internals/markov.hpp | 290 +++++++++------ src/internals/transition_base.hpp | 11 +- src/intervention.cpp | 20 +- src/migration.cpp | 23 +- src/model_factory.cpp | 9 +- src/overdose.cpp | 36 +- tests/CMakeLists.txt | 18 +- tests/integration/respond_test.cpp | 228 ++++++------ tests/mocks/model_mock.hpp | 40 +- tests/mocks/transition_mock.hpp | 15 +- tests/unit/background_test.cpp | 17 +- tests/unit/behavior_test.cpp | 12 +- tests/unit/intervention_test.cpp | 15 +- tests/unit/logging_test.cpp | 37 +- tests/unit/markov_test.cpp | 403 +++++++++++++++------ tests/unit/migration_test.cpp | 12 +- tests/unit/overdose_test.cpp | 32 +- tests/unit/simulation_test.cpp | 266 +++++++------- tests/unit/timestep_test.cpp | 168 +++++++++ 32 files changed, 1390 insertions(+), 806 deletions(-) create mode 100644 tests/unit/timestep_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ac8cf8b6..ae62eb39 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,20 +73,39 @@ endif() add_library(respond_model) add_library(respond::respond_model ALIAS respond_model) -file(GLOB_RECURSE RESPOND_HEADERS include/respond/*.hpp include/respond/*.h) -file(GLOB RESPOND_INTERNAL_HEADERS CONFIGURE_DEPENDS src/internals/*.hpp) -file(GLOB RESPOND_SOURCE_FILES CONFIGURE_DEPENDS src/*.cpp) - target_sources(respond_model PRIVATE - ${RESPOND_INTERNAL_HEADERS} - ${RESPOND_SOURCE_FILES} + src/internals/background.hpp + src/internals/behavior.hpp + src/internals/intervention.hpp + src/internals/logging_internals.hpp + src/internals/markov.hpp + src/internals/migration.hpp + src/internals/overdose.hpp + src/internals/transition_base.hpp + src/background.cpp + src/behavior.cpp + src/intervention.cpp + src/logging.cpp + src/migration.cpp + src/model_factory.cpp + src/overdose.cpp + src/transition_factory.cpp PUBLIC FILE_SET HEADERS BASE_DIRS include FILES - ${RESPOND_HEADERS} + include/respond/constants.hpp + include/respond/cost_effectiveness.hpp + include/respond/history.hpp + include/respond/logging.hpp + include/respond/model.hpp + include/respond/respond.hpp + include/respond/simulation.hpp + include/respond/timestep.hpp + include/respond/transition.hpp + include/respond/version.hpp ) #------------------------------------------------------------------------------- diff --git a/docs/src/api-guide.md b/docs/src/api-guide.md index 841b09ef..b9b24866 100644 --- a/docs/src/api-guide.md +++ b/docs/src/api-guide.md @@ -48,8 +48,8 @@ initial_state.setZero(); model->SetState(initial_state); // Add transitions -auto transition = respond::TransitionFactory::CreateTransition("behavior", "logger_name"); -transition->AddTransitionMatrix(some_matrix); +auto transition = respond::Transition::Create("behavior", "logger_name"); +transition->AddMatrix(some_matrix); model->AddTransition(transition); // Execute one simulation step @@ -73,7 +73,7 @@ auto histories = model->GetHistories(); - `GetHistories() const`: Returns map of history name to History objects - `CreateDefaultHistories()`: Initializes default history tracking - `SetHistories(const std::map &h)`: Sets history records -- `GetModelName() const`: Returns model name +- `GetName() const`: Returns model name - `GetLogName() const`: Returns associated logger name - `clone() const`: Creates a deep copy of the model @@ -167,14 +167,14 @@ The Transition class is abstract; use TransitionFactory to create concrete insta #include // Create a transition using the factory -auto transition = respond::TransitionFactory::CreateTransition( +auto transition = respond::Transition::Create( "behavior", // Type: migration, behavior, intervention, overdose, background_death "my_logger" // Logger name ); // Add transformation matrices Eigen::MatrixXd trans_matrix = ...; -transition->AddTransitionMatrix(trans_matrix); +transition->AddMatrix(trans_matrix); // Execute the transition (typically done via Model::RunTransitions) auto histories_map = ...; // From model @@ -235,12 +235,12 @@ int main() { model->SetState(initial_state); // Add transitions - auto behavior_transition = respond::TransitionFactory::CreateTransition( + auto behavior_transition = respond::Transition::Create( "behavior", "app"); // Add matrices... model->AddTransition(behavior_transition); - auto migration_transition = respond::TransitionFactory::CreateTransition( + auto migration_transition = respond::Transition::Create( "migration", "app"); // Add matrices... model->AddTransition(migration_transition); diff --git a/docs/src/architecture.md b/docs/src/architecture.md index 672dd77e..a514af6b 100644 --- a/docs/src/architecture.md +++ b/docs/src/architecture.md @@ -117,7 +117,7 @@ RESPOND follows the **inversion of control** principle, abstracting the model to Encapsulates object creation for transitions: ```cpp -auto transition = TransitionFactory::CreateTransition("behavior", "logger"); +auto transition = Transition::Create("behavior", "logger"); ``` **Benefits**: @@ -205,7 +205,7 @@ RESPOND minimizes shared state. History objects are the exception—they're: 1. Create a new header in `include/respond/internals/` 2. Implement concrete Transition subclass -3. Add factory entry in `TransitionFactory::CreateTransition()` +3. Add factory entry in `Transition::Create()` Example: ```cpp diff --git a/docs/src/run.md b/docs/src/run.md index 779f5394..c5e81be1 100644 --- a/docs/src/run.md +++ b/docs/src/run.md @@ -35,7 +35,7 @@ int main() { model->SetState(initial_state); // Add transitions - auto transition = respond::TransitionFactory::CreateTransition( + auto transition = respond::Transition::Create( "behavior", "my_logger"); model->AddTransition(transition); diff --git a/docs/src/uml.md b/docs/src/uml.md index 0f6625d1..b27baacb 100644 --- a/docs/src/uml.md +++ b/docs/src/uml.md @@ -28,7 +28,7 @@ classDiagram +GetState() VectorXd * +AddTimestep(shared_ptr~timestep~) * +GetTimesteps() * - +RunTransitions() * + +RunTimesteps() * +ClearTimesteps() * +GetHistories() map~string, History~ * +ClearHistories() * diff --git a/extras/benchmark/src/benchmark_respond.cpp b/extras/benchmark/src/benchmark_respond.cpp index deb95a89..70520654 100644 --- a/extras/benchmark/src/benchmark_respond.cpp +++ b/extras/benchmark/src/benchmark_respond.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-04-27 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-29 // +// Last Modified: 2026-07-07 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -277,24 +277,21 @@ std::unique_ptr BuildModel(std::size_t state_size, model->SetHistoryCaptureInterval(history_capture_interval); model->SetFinalTimestep(final_timestep); - auto behavior = - respond::TransitionFactory::CreateTransition("behavior", "console"); - auto intervention = - respond::TransitionFactory::CreateTransition("intervention", "console"); - auto overdose = - respond::TransitionFactory::CreateTransition("overdose", "console"); - auto background = respond::TransitionFactory::CreateTransition( - "background_death", "console"); + auto behavior = respond::Transition::Create("behavior", "console"); + auto intervention = respond::Transition::Create("intervention", "console"); + auto overdose = respond::Transition::Create("overdose", "console"); + auto background = + respond::Transition::Create("background_death", "console"); if (!behavior || !intervention || !overdose || !background) { throw std::runtime_error("Failed to create one or more transitions"); } - behavior->AddTransitionMatrix(MakeShiftMatrix(state_size, 0.985, 1)); - intervention->AddTransitionMatrix(MakeShiftMatrix(state_size, 0.990, -1)); - overdose->AddTransitionMatrix(MakeRateVector(state_size, 0.0020, 0.0005)); - overdose->AddTransitionMatrix(MakeRateVector(state_size, 0.0800, 0.0200)); - background->AddTransitionMatrix(MakeRateVector(state_size, 0.0008, 0.0004)); + behavior->AddMatrix(MakeShiftMatrix(state_size, 0.985, 1)); + intervention->AddMatrix(MakeShiftMatrix(state_size, 0.990, -1)); + overdose->AddMatrix(MakeRateVector(state_size, 0.0020, 0.0005)); + overdose->AddMatrix(MakeRateVector(state_size, 0.0800, 0.0200)); + background->AddMatrix(MakeRateVector(state_size, 0.0008, 0.0004)); model->AddTransition(behavior); model->AddTransition(intervention); diff --git a/include/respond/constants.hpp b/include/respond/constants.hpp index bc7b84a4..97ba83d2 100644 --- a/include/respond/constants.hpp +++ b/include/respond/constants.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-07-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-06 // +// Last Modified: 2026-07-07 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -14,5 +14,6 @@ #define RESPOND_CONSTANTS_HPP_ #define RESPOND_DEFAULT_LOG "respond" +#define RESPOND_DEFAULT_LOG_FILE "respond.log" #endif \ No newline at end of file diff --git a/include/respond/model.hpp b/include/respond/model.hpp index 6ac33171..f92ede7b 100644 --- a/include/respond/model.hpp +++ b/include/respond/model.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-02 // +// Last Modified: 2026-07-07 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -18,8 +18,9 @@ #include +#include #include -#include +#include namespace respond { /// @brief Abstract base class representing a state transition model. @@ -28,97 +29,135 @@ namespace respond { /// execution, and history tracking. class Model { public: + //////////////////////////////////////////////////////////////////////////// + // + // Rule of Five: Copy and Move Semantics + // + //////////////////////////////////////////////////////////////////////////// + + /// @brief Factory method to create a Model instance. + /// @details This method creates a new instance of a Model subclass based on + /// the provided name. It initializes logging for the model and returns a + /// unique_ptr to the created instance. Throws an exception if the model + /// name is unsupported. + /// @param name The name identifier for the model to create. + /// @param log_name Name of the logger for this model (default: "console"). + /// @param log_filepath File path for the log file (default: "respond.log"). + /// @return A unique_ptr to the newly created Model instance. + static std::unique_ptr + Create(const std::string &name, + const std::string &log_name = RESPOND_DEFAULT_LOG, + const std::string &log_filepath = RESPOND_DEFAULT_LOG_FILE); + /// @brief Virtual destructor for proper polymorphic cleanup. virtual ~Model() = default; - /// @brief Sets the current state of the model. - /// @param state The state vector to set. A copy is made internally. - virtual void SetState(const Eigen::Ref &state) = 0; + /// @brief Deleted copy constructor (models are non-copyable by public API). + Model(const Model &) = delete; + /// @brief Deleted copy assignment operator (models are non-copyable by + /// public API). + Model &operator=(const Model &) = delete; - /// @brief Retrieves the current state of the model. - /// @return A copy of the current state vector (limited to observation). - virtual const Eigen::Ref &GetState() const = 0; + /// @brief Creates a deep copy of this model. + /// @return A unique_ptr to an independent copy of this model. + virtual std::unique_ptr clone() const = 0; + + //////////////////////////////////////////////////////////////////////////// + // + // Model Behavior Methods: Timestep Execution and History Management + // + //////////////////////////////////////////////////////////////////////////// - virtual void AddTimestep(const std::shared_ptr &transition) = 0; + /// @brief Helper function to add a single timestep to the model. + /// @param timestep A shared pointer to a Timestep instance. The model gains + /// an ownership reference to this timestep and will manage its lifecycle. + virtual void AddTimestep(const Timestep ×tep) = 0; - /// @brief Executes all registered transitions on the current state. - /// Transitions are applied in the order they were added and may modify - /// history. - virtual void RunTransitions() = 0; + /// @brief Executes the next timestep in the model's sequence. + virtual void RunTimestep() = 0; - /// @brief Adds a transition to the model. The model takes ownership of the - /// transition via cloning. - /// @param transitions A vector of unique_ptrs to a Transition instances to - /// add. - virtual void AddTimestep( - const std::vector> &transitions) = 0; + /// @brief Executes the timestep in the model's sequence. + virtual void RunTimestep(size_t idx) = 0; - /// @brief Retrieves the names of all registered transitions. - /// @return Vector of transition names in the order they were added. - virtual std::vector GetTransitionNames() const = 0; + /// @brief Executes all registered timesteps in sequence, applying their + /// transitions to the model's state. + virtual void RunTimesteps() = 0; - /// @brief Clears all registered transitions. - /// Deletes all stored Transition unique_ptrs. - virtual void ClearTransitions() = 0; + /// @brief Clears all timesteps from the model. + virtual void ClearTimesteps() = 0; - /// @brief Retrieves the history records for all state variables. - /// @return A map of history names to History objects containing state - /// trajectories. - virtual std::map GetHistories() const = 0; + /// @brief Clear all history records and reset the history tracking state. + virtual void ClearHistories() = 0; /// @brief Creates default history tracking for the model. /// This method initializes standard history records based on the model's /// state. virtual void CreateDefaultHistories() = 0; - /// @brief Sets the history records for the model. - /// @param h A map of history names to History objects. - virtual void SetHistories(const std::map &h) = 0; + //////////////////////////////////////////////////////////////////////////// + // + // Getters and Setters for Model State and Metadata + // + //////////////////////////////////////////////////////////////////////////// - /// @brief Clears all history records and resets history tracking state. - virtual void ClearHistories() = 0; + /// @brief Retrieve an immutable reference to a specific timestep by index. + /// @param index The zero-based index of the timestep to retrieve. + /// @return A constant reference to the Timestep at the specified index. + virtual Timestep GetTimestepAtIndex(size_t index) const = 0; - /// @brief Sets the global history capture interval for this model. - /// @param interval Record every interval timesteps. Values less than 1 - /// default to full capture. - virtual void SetHistoryCaptureInterval(int interval) = 0; + /// @brief Retrieves the current state of the model. + /// @return A reference to the model's internal state. It is limited to + /// observation and changes cannot be made to it directly. + virtual const Eigen::Ref GetState() const = 0; + + /// @brief Retrieves the name identifier for this model. + /// @return The model's name as a string. + virtual std::string GetName() const = 0; + + /// @brief Retrieves a reference to the map of all registered histories. The + /// histories cannot be edited directly and must be saved to their own space + /// by users before being consumed. + /// @return A constant reference to a map of history names to History + /// objects. + virtual const std::map &GetHistories() const = 0; + + /// @brief Retrieves the current simulation timestep. + /// @return The current timestep index. Returns -1 if no timesteps have been + /// executed yet. + virtual int GetTimestep() const = 0; /// @brief Retrieves the global history capture interval. /// @return The active capture interval. A value of 1 means full capture. virtual int GetHistoryCaptureInterval() const = 0; - /// @brief Sets the final timestep that must always be recorded. - /// @param final_timestep The final simulation timestep. - virtual void SetFinalTimestep(int final_timestep) = 0; - /// @brief Retrieves the final timestep forced into history output. /// @return The configured final simulation timestep, or -1 if unset. virtual int GetFinalTimestep() const = 0; - /// @brief Retrieves the name identifier for this model. - /// @return The model's name as a string. - virtual std::string GetModelName() const = 0; + /// @brief Checks if the initial history has been recorded for this model. + /// @return True if the initial state has been recorded in history, false + /// otherwise. + virtual bool GetInitialHistoryRecorded() const = 0; - /// @brief Retrieves the logger name used by this model. - /// @return The name of the associated logger. - virtual std::string GetLogName() const = 0; + /// @brief Sets the current state of the model. + /// @param state A constant reference to a vector. This vector is then + /// applied to the model's internal state. The model may copy or reference + /// this vector as needed. + virtual void SetState(const Eigen::Ref &state) = 0; - /// @brief Factory method to create a Model instance. - /// @param name The name identifier for the model to create. - /// @param log_name Name of the logger for this model (default: "console"). - /// @return A unique_ptr to the newly created Model instance. - static std::unique_ptr - Create(const std::string &name, const std::string &log_name = "console"); + /// @brief Sets the global history capture interval for this model. + /// @param interval Record every interval timesteps. Values less than 1 + /// default to full capture. + virtual void SetHistoryCaptureInterval(int interval) = 0; - /// @brief Deleted copy constructor (models are non-copyable by public API). - Model(const Model &) = delete; - /// @brief Deleted copy assignment operator (models are non-copyable by - /// public API). - Model &operator=(const Model &) = delete; + /// @brief Sets the final timestep that must always be recorded. + /// @param final_timestep The final simulation timestep. + virtual void SetFinalTimestep(int final_timestep) = 0; - /// @brief Creates a deep copy of this model. - /// @return A unique_ptr to an independent copy of this model. - virtual std::unique_ptr clone() const = 0; + /// @brief Sets whether the initial state has been recorded in history. + /// @param recorded True if the initial state has been recorded, false + /// otherwise. + virtual void SetInitialHistoryRecorded(bool recorded) = 0; protected: /// @brief Protected default constructor for subclass initialization. diff --git a/include/respond/simulation.hpp b/include/respond/simulation.hpp index d6c2fbb9..07adb3c8 100644 --- a/include/respond/simulation.hpp +++ b/include/respond/simulation.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-02 // +// Last Modified: 2026-07-07 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -53,8 +53,7 @@ class Simulation { const std::string CreateNewModel(const std::string &model_name) { _models.push_back(Model::Create(model_name, _log_name)); - return std::to_string(_models.size()) + "_" + - _models.back()->GetModelName(); + return std::to_string(_models.size()) + "_" + _models.back()->GetName(); } /// @brief Executes one step of the simulation for all models. @@ -62,7 +61,7 @@ class Simulation { void Run() { for (const auto &model : _models) { model->SetFinalTimestep(_duration); - model->RunTransitions(); + // model->RunTransitions(); } } @@ -86,7 +85,7 @@ class Simulation { std::vector GetModelNames() const { std::vector ret; for (auto &m : _models) { - ret.push_back(m->GetModelName()); + ret.push_back(m->GetName()); } return ret; } @@ -131,7 +130,7 @@ class Simulation { std::vector> ret; for (const auto &model : _models) { for (const auto &kv : model->GetHistories()) { - std::pair p = {model->GetModelName(), + std::pair p = {model->GetName(), kv.first}; ret.push_back(p); } diff --git a/include/respond/timestep.hpp b/include/respond/timestep.hpp index 4e414ff0..4a006613 100644 --- a/include/respond/timestep.hpp +++ b/include/respond/timestep.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-06-30 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-06 // +// Last Modified: 2026-07-07 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -21,33 +21,130 @@ #include namespace respond { + +/// @brief Represents a single timestep in a simulation, managing a collection +/// of transitions. Each timestep can execute its transitions in sequence, +/// applying their effects to a state vector and updating history records. +/// Transitions can be added, retrieved, and managed within the timestep. The +/// timestep also handles logging for its operations. class Timestep { public: + //////////////////////////////////////////////////////////////////////////// + // + // Rule of Five: Copy and Move Semantics + // + //////////////////////////////////////////////////////////////////////////// + + /// @brief Default constructor for Timestep. Initializes with default + /// logger. Timestep() : Timestep(RESPOND_DEFAULT_LOG) {} + + /// @brief Default constructor for Timestep with specified logger name. + /// Initializes with default log file path. + /// @param log_name String name for the logger to be used by this timestep. Timestep(const std::string &log_name) - : Timestep(log_name, log_name + ".log") {} + : Timestep(log_name, RESPOND_DEFAULT_LOG_FILE) {} + + /// @brief Default constructor for Timestep with specified logger name and + /// log file path. + /// @param log_name String name for the logger to be used by this timestep. + /// @param log_filepath String path for the log file to be used by this + /// timestep. Timestep(const std::string &log_name, const std::string &log_filepath) : _log_name(log_name) { CreateFileLogger(log_name, log_filepath); - _transitions = {}; } + + /// @brief Destructor for Timestep. Default implementation. ~Timestep() = default; - const Transition & - CreateTransition(const std::string &transition_name) const { - auto transition = Transition::Create(transition_name, _log_name); - return *transition; + /// @brief Copy constructor for Timestep. Creates a deep copy of the + /// transitions. + /// @param other The Timestep instance to copy from. + Timestep(const Timestep &other) { + _transitions.clear(); + for (const auto &t : other._transitions) { + _transitions.push_back(t->clone()); + } } + /// @brief Copy assignment operator for Timestep. Creates a deep copy of the + /// transitions. + /// @param other The Timestep instance to copy from. + /// @return Reference to this Timestep instance after assignment. + Timestep &operator=(const Timestep &other) { + if (this != &other) { + _transitions.clear(); + for (const auto &t : other._transitions) { + _transitions.push_back(t->clone()); + } + } + return *this; + } + + /// @brief Move constructor for Timestep. Transfers ownership of + /// transitions. + /// @param other The Timestep instance to move from. + Timestep(Timestep &&other) noexcept + : _transitions(std::move(other._transitions)) { + other._transitions.clear(); + } + + /// @brief Move assignment operator for Timestep. Transfers ownership of + /// transitions. + /// @param other The Timestep instance to move from. + /// @return Reference to this Timestep instance after assignment. + Timestep &operator=(Timestep &&other) noexcept { + if (this != &other) { + _transitions = std::move(other._transitions); + other._transitions.clear(); + } + return *this; + } + + //////////////////////////////////////////////////////////////////////////// + // + // Timestep Behavior Methods: Transition Management + // + //////////////////////////////////////////////////////////////////////////// + + /// @brief Creates a transition of the specified type and adds it to this + /// timestep. + /// @param transition_name The type of transition to create. Supported types + /// (case-insensitive): + /// - "migration": Population migration transitions + /// - "behavior": Behavioral state transitions + /// - "intervention": Intervention-driven transitions + /// - "overdose": Overdose-related transitions + /// - "background_death": Background mortality transitions + /// @return A constant reference to the created Transition. Throws an + /// exception if the transition type is unsupported. + const std::unique_ptr & + CreateTransition(const std::string &transition_name) { + _transitions.push_back(Transition::Create(transition_name, _log_name)); + return _transitions.back(); + } + + /// @brief Adds a matrix to an existing transition in this timestep by + /// index. + /// @param idx The index of the transition to which the matrix will be + /// added. + /// @param m The transition matrix to add (not modified by this transition). void AddMatrixToTransition(const size_t &idx, const Eigen::Ref &m) { if (idx >= _transitions.size()) { - throw std::out_of_range( - "Index out of range in AddMatrixToTransition"); + LogWarning(_log_name, + "Index out of range in AddMatrixToTransition: " + + std::to_string(idx)); } _transitions[idx]->AddMatrix(m); } + /// @brief Adds a matrix to an existing transition in this timestep by + /// name. + /// @param transition_name The name of the transition to which the matrix + /// will be added. + /// @param m The transition matrix to add (not modified by this transition). void AddMatrixToTransition(const std::string &transition_name, const Eigen::Ref &m) { for (size_t i = 0; i < _transitions.size(); ++i) { @@ -61,14 +158,32 @@ class Timestep { transition_name); } + //////////////////////////////////////////////////////////////////////////// + // + // Getters and Setters for Transitions and Metadata + // + //////////////////////////////////////////////////////////////////////////// + + /// @brief Retrieves a constant reference to a transition in this timestep + /// by index. + /// @param idx The index of the transition to retrieve. + /// @return A constant reference to the Transition at the specified index. + /// Throws an error if the index is out of range. const std::unique_ptr &GetTransition(const size_t &idx) const { if (idx >= _transitions.size()) { - LogWarning(_log_name, "Index out of range in GetTransition: " + - std::to_string(idx)); + LogError(_log_name, "Index out of range in GetTransition: " + + std::to_string(idx)); + throw std::out_of_range( + "Error attempting to GetTransition by index."); } return _transitions[idx]; } + /// @brief Gets a constant reference to a transition in this timestep by + /// name. + /// @param transition_name The name of the transition to retrieve. + /// @return A constant reference to the Transition with the specified name. + /// Throws an error if the transition is not found. const std::unique_ptr & GetTransition(const std::string &transition_name) const { for (const auto &t : _transitions) { @@ -76,10 +191,17 @@ class Timestep { return t; } } - throw std::invalid_argument("Transition not found in GetTransition: " + - transition_name); + LogError(_log_name, + "Transition not found in GetTransition: " + transition_name); + throw std::invalid_argument( + "Error attempting to GetTransition by name."); } + /// @brief Gets a vector of unique_ptrs to all transitions in this timestep. + /// The returned vector contains deep copies of the transitions, ensuring + /// that modifications to the returned transitions do not affect the + /// original transitions in the timestep. + /// @return A vector of unique_ptrs to the transitions in this timestep. std::vector> GetTransitions() const { std::vector> _ret; for (const auto &t : _transitions) { @@ -88,16 +210,10 @@ class Timestep { return _ret; } - const std::unique_ptr & - GetTransitionAtIndex(size_t index) const { - if (index >= _transitions.size()) { - throw std::out_of_range( - "Index out of range in GetTransitionAtIndex"); - } - return _transitions[index]; - } - - const std::vector &GetTransitionNames() const { + /// @brief Gets a vector of the names of all transitions in this timestep. + /// @return A vector of strings containing the names of the transitions in + /// this timestep. + std::vector GetTransitionNames() const { std::vector names; for (const auto &t : _transitions) { names.push_back(t->GetName()); @@ -105,46 +221,70 @@ class Timestep { return names; } - // Copy Constructor and Assignment + //////////////////////////////////////////////////////////////////////////// + // + // Logging and Output Methods + // + //////////////////////////////////////////////////////////////////////////// - Timestep(const Timestep &other) { - _transitions.clear(); + /// @brief Overloaded stream insertion operator for Timestep. Outputs the + /// names of all transitions in the timestep to the provided output stream. + /// @details This operator allows for easy logging and debugging of the + /// transitions contained within a Timestep instance. It outputs a list of + /// transition names, each prefixed with a dash for clarity. + /// @param os The output stream to which the transition names will be + /// written. + /// @param other The Timestep instance whose transitions are to be output. + /// @return A reference to the output stream after writing the transition + /// names. + friend std::ostream &operator<<(std::ostream &os, Timestep &other) { + os << "Timestep with the following transitions:\n"; for (const auto &t : other._transitions) { - _transitions.push_back(t->clone()); + os << " - " << t->GetName() << "\n"; } + return os; } - Timestep &operator=(const Timestep &other) { - if (this != &other) { - _transitions.clear(); - for (const auto &t : other._transitions) { - _transitions.push_back(t->clone()); - } + /// @brief Overloaded equality operator for Timestep. Compares two Timestep + /// instances for equality based on their transitions and transition + /// matrices. + /// @param lhs The left-hand side Timestep instance to compare. + /// @param rhs The right-hand side Timestep instance to compare. + /// @return True if the two Timestep instances are equal (same transitions + /// and transition matrices), false otherwise. + friend bool operator==(const Timestep &lhs, const Timestep &rhs) { + if (lhs._transitions.size() != rhs._transitions.size()) { + return false; } - return *this; - } - - // Move Constructor and Assignment - - Timestep(Timestep &&other) noexcept - : _transitions(std::move(other._transitions)) { - other._transitions.clear(); - } - - Timestep &operator=(Timestep &&other) noexcept { - if (this != &other) { - _transitions = std::move(other._transitions); - other._transitions.clear(); + for (size_t i = 0; i < lhs._transitions.size(); ++i) { + if (lhs._transitions[i]->GetName() != + rhs._transitions[i]->GetName()) { + return false; + } + if (lhs._transitions[i]->GetMatrices().size() != + rhs._transitions[i]->GetMatrices().size()) { + return false; + } + for (size_t j = 0; j < lhs._transitions[i]->GetMatrices().size(); + ++j) { + if (!lhs._transitions[i]->GetMatrices()[j].isApprox( + rhs._transitions[i]->GetMatrices()[j])) { + return false; + } + } } - return *this; + return true; } - friend std::ostream &operator<<(std::ostream &os, Timestep &other) { - os << "Timestep with the following transitions:\n"; - for (const auto &t : other._transitions) { - os << " - " << t->GetName() << "\n"; - } - return os; + /// @brief Overloaded inequality operator for Timestep. Compares two + /// Timestep instances for inequality based on their transitions and + /// transition matrices. + /// @param lhs The left-hand side Timestep instance to compare. + /// @param rhs The right-hand side Timestep instance to compare. + /// @return True if the two Timestep instances are not equal (different + /// transitions or transition matrices), false otherwise. + friend bool operator!=(const Timestep &lhs, const Timestep &rhs) { + return !(lhs == rhs); } private: diff --git a/include/respond/transition.hpp b/include/respond/transition.hpp index 1fcb5334..19d74171 100644 --- a/include/respond/transition.hpp +++ b/include/respond/transition.hpp @@ -48,6 +48,11 @@ class Transition { /// @param m The transition matrix to add (not modified by this transition). virtual void AddMatrix(const Eigen::Ref &m) = 0; + /// @brief Retrieves the stored transition matrices for this transition. + /// @return A vector of references to the stored transition matrices. + virtual std::vector> + GetMatrices() const = 0; + /// @brief Retrieves the name/type of this transition. /// @return The transition's identifier as a string. virtual std::string GetName() const = 0; diff --git a/src/background.cpp b/src/background.cpp index 10fd08f7..b9f2befa 100644 --- a/src/background.cpp +++ b/src/background.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-25 // +// Last Modified: 2026-07-07 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -22,15 +22,14 @@ namespace respond { Eigen::VectorXd BackgroundDeath::Execute(const Eigen::Ref &state, std::map &h) const { - if (GetTransitionMatrices().size() != 1) { + if (GetMatrices().size() != 1) { std::string error_msg = "Background death error: Expected 1 transition matrix, got " + - std::to_string(GetTransitionMatrices().size()); + std::to_string(GetMatrices().size()); LogError(GetLogName(), error_msg); throw std::runtime_error(error_msg); } - auto deaths = - state.cwiseProduct(GetTransitionMatrices()[0]); // calculate the deaths + auto deaths = state.cwiseProduct(GetMatrices()[0]); // calculate the deaths if (h.find("background_death") != h.end()) { h["background_death"].AccumulateState(deaths); } @@ -46,9 +45,4 @@ BackgroundDeath::Execute(const Eigen::Ref &state, auto new_state = state - deaths; // remove deaths from state return new_state; } - -std::unique_ptr -BackgroundDeath::Create(const std::string &name, const std::string &log_name) { - return std::make_unique(name, log_name); -} } // namespace respond \ No newline at end of file diff --git a/src/behavior.cpp b/src/behavior.cpp index 29a85cc3..6eb43d02 100644 --- a/src/behavior.cpp +++ b/src/behavior.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-25 // +// Last Modified: 2026-07-07 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -22,30 +22,24 @@ namespace respond { Eigen::VectorXd Behavior::Execute(const Eigen::Ref &state, std::map &h) const { - if (GetTransitionMatrices().size() != 1) { + if (GetMatrices().size() != 1) { std::string error_msg = "Behavior error: Expected 1 transition matrix, got " + - std::to_string(GetTransitionMatrices().size()); + std::to_string(GetMatrices().size()); LogError(GetLogName(), error_msg); throw std::runtime_error(error_msg); } - if (state.rows() != GetTransitionMatrices()[0].cols()) { + if (state.rows() != GetMatrices()[0].cols()) { std::stringstream ss; ss << "Behavior error: State dimension mismatch. State size is (" << state.rows() << ", " << state.cols() - << ") but transition matrix expects (" - << GetTransitionMatrices()[0].rows() << ", " - << GetTransitionMatrices()[0].cols() << ")"; + << ") but transition matrix expects (" << GetMatrices()[0].rows() + << ", " << GetMatrices()[0].cols() << ")"; std::string error_msg = ss.str(); LogError(GetLogName(), error_msg); throw std::runtime_error(error_msg); } - auto new_state = GetTransitionMatrices()[0] * state; + auto new_state = GetMatrices()[0] * state; return new_state; } - -std::unique_ptr Behavior::Create(const std::string &name, - const std::string &log_name) { - return std::make_unique(name, log_name); -} } // namespace respond \ No newline at end of file diff --git a/src/internals/markov.hpp b/src/internals/markov.hpp index e2e7d780..2ae830b7 100644 --- a/src/internals/markov.hpp +++ b/src/internals/markov.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-29 // +// Last Modified: 2026-07-07 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -20,72 +20,216 @@ #include +#include #include #include namespace respond { class Markov : public virtual Model { public: - Markov() : Markov("markov", "console") {} + //////////////////////////////////////////////////////////////////////////// + // + // Rule of Five: Copy and Move Semantics + // + //////////////////////////////////////////////////////////////////////////// + + /// @brief Default constructor for Markov model. Initializes with default + /// name "markov" and logger "console". + Markov() : Markov("markov", RESPOND_DEFAULT_LOG) {} + + /// @brief Constructs a Markov model with specified name and logger. + /// @param name The identifier for this model. + /// @param log_name The logger name for error reporting. Markov(const std::string &name, const std::string &log_name) + : Markov(name, log_name, RESPOND_DEFAULT_LOG_FILE) {} + + /// @brief Constructs a Markov model with specified name, logger, and log + /// file path. + /// @param name The identifier for this model. + /// @param log_name The logger name for error reporting. + /// @param log_filepath The file path for the log file to be used by this + /// model. + Markov(const std::string &name, const std::string &log_name, + const std::string &log_filepath) : _name(name), _log_name(log_name), _current_timestep(0), _history_capture_interval(1), _final_timestep(-1), _initial_history_recorded(false) { + CreateFileLogger(log_name, log_filepath); const auto processor_count = std::thread::hardware_concurrency(); Eigen::setNbThreads(processor_count); } - // Rule of Five + /// @brief Destructor for Markov model. Default implementation. ~Markov() = default; - // Copy + Markov(Markov &&other) noexcept { + _state = other._state; + _name = other._name; + _log_name = other._log_name; + _current_timestep = other._current_timestep; + _history_capture_interval = other._history_capture_interval; + _final_timestep = other._final_timestep; + _initial_history_recorded = other._initial_history_recorded; + for (const auto &h : other._histories) { + _histories[h.first] = h.second; + } + other._histories.clear(); + for (const auto &t : other._timestep_vector) { + _timestep_vector.push_back(std::move(t)); + } + other.ClearTimesteps(); + } + Markov &operator=(Markov &&other) noexcept { + if (this != &other) { + _state = other._state; + _name = other._name; + _log_name = other._log_name; + _current_timestep = other._current_timestep; + _history_capture_interval = other._history_capture_interval; + _final_timestep = other._final_timestep; + _initial_history_recorded = other._initial_history_recorded; + for (const auto &h : other._histories) { + _histories[h.first] = h.second; + } + other._histories.clear(); + for (const auto &t : other._timestep_vector) { + _timestep_vector.push_back(std::move(t)); + } + other.ClearTimesteps(); + } + return *this; + } + + /// @brief Function to provide a deep copy operation of the Model. Copy + /// constructor and assignment operator are deleted to prevent copying of + /// Markov instances. Instead we prefer to use `clone()` for deep copying. + /// @return A unique_ptr to a new Markov instance that is a deep copy of + /// this instance. std::unique_ptr clone() const override { - auto np = Model::Create(GetModelName(), GetLogName()); + auto np = Model::Create(_name, _log_name); np->SetState(GetState()); - np->SetHistories(GetHistories()); np->SetHistoryCaptureInterval(GetHistoryCaptureInterval()); np->SetFinalTimestep(GetFinalTimestep()); if (auto *markov = dynamic_cast(np.get())) { + markov->_histories = _histories; markov->_current_timestep = _current_timestep; markov->_initial_history_recorded = _initial_history_recorded; } - for (const auto &t : GetTransitions()) { - np->AddTransition(t->clone()); + + int timesteps = static_cast(_timestep_vector.size()); + for (int i = 0; i < timesteps; ++i) { + np->AddTimestep(_timestep_vector[i]); } return np; } - // Move - Markov(Markov &&other) noexcept { - _state = other.GetState(); - _name = other.GetModelName(); - _log_name = other.GetLogName(); - for (const auto &t : other.GetTransitions()) { - AddTransition(std::move(t)); + + //////////////////////////////////////////////////////////////////////////// + // + // Getters and Setters for Model State and Metadata + // + //////////////////////////////////////////////////////////////////////////// + + Timestep GetTimestepAtIndex(size_t index) const override { + if (index >= _timestep_vector.size()) { + throw std::out_of_range("Index out of range in GetTimestepAtIndex"); } - other.ClearTransitions(); + return _timestep_vector[index]; } - Markov &operator=(Markov &&other) noexcept { - if (this != &other) { - _state = other.GetState(); - _name = other.GetModelName(); - _log_name = other.GetLogName(); - for (const auto &t : other.GetTransitions()) { - AddTransition(std::move(t)); - } - other.ClearTransitions(); - } - return *this; + + const Eigen::Ref GetState() const override { + return _state; + } + + std::string GetName() const override { return _name; } + + const std::map &GetHistories() const override { + return _histories; + } + + int GetTimestep() const override { return _current_timestep; } + + int GetHistoryCaptureInterval() const override { + return _history_capture_interval; + } + + int GetFinalTimestep() const override { return _final_timestep; } + + bool GetInitialHistoryRecorded() const override { + return _initial_history_recorded; } - // anticipate making a copy of the vector void SetState(const Eigen::Ref &s) override { _state = s; } - // return const & to limit to observation of the state - Eigen::VectorXd GetState() const override { return _state; } - // return the transitions - const std::vector> &GetTransitions() const { - return _transition_vector; + + void SetHistoryCaptureInterval(int interval) override { + _history_capture_interval = (interval < 1) ? 1 : interval; + } + + void SetFinalTimestep(int final_timestep) override { + _final_timestep = final_timestep; + } + + void SetInitialHistoryRecorded(bool recorded) override { + _initial_history_recorded = recorded; + } + + //////////////////////////////////////////////////////////////////////////// + // + // Model Behavior Methods: Timestep Execution and History Management + // + //////////////////////////////////////////////////////////////////////////// + + void AddTimestep(const Timestep ×tep) override { + _timestep_vector.push_back(timestep); + if (static_cast(_timestep_vector.size()) > _final_timestep) { + LogWarning(_log_name, "Final timestep exceeded by added timestep."); + } + } + + void RunTimestep() override { + RunTimestep(_current_timestep); + _current_timestep++; + } + + /// @brief Executes the timestep in the model's sequence. + void RunTimestep(size_t idx) override { + if (_timestep_vector.empty()) { + LogWarning(_log_name, + "No timesteps available to run for model: " + _name); + return; + } + + if (idx >= static_cast(_timestep_vector.size())) { + LogWarning( + _log_name, + "Current timestep exceeds available timesteps for model: " + + _name); + return; + } + + auto transitions = _timestep_vector[idx].GetTransitions(); + for (const auto &t : transitions) { + _state = t->Execute(_state, _histories); + } + } + + void RunTimesteps() override { + SetupHistory(); + if (!_initial_history_recorded) { + RecordHistoryAtCurrentTimestep(); + } + for (size_t i = 0; i < _timestep_vector.size(); ++i) { + RunTimestep(); + RecordHistoryAtCurrentTimestep(); + } + } + + void ClearTimesteps() override { _timestep_vector.clear(); } + + void ClearHistories() override { + _histories.clear(); + ResetHistoryTracking(); } /// @brief The default histories are: @@ -97,55 +241,16 @@ class Markov : public virtual Model { /// @return A vector of the default history objects. void CreateDefaultHistories() override { std::map ret; - ret["state"] = History("state", GetLogName(), HistoryMode::Snapshot); + ret["state"] = History("state", _log_name, HistoryMode::Snapshot); ret["total_overdose"] = - History("total_overdose", GetLogName(), HistoryMode::Accumulated); + History("total_overdose", _log_name, HistoryMode::Accumulated); ret["fatal_overdose"] = - History("fatal_overdose", GetLogName(), HistoryMode::Accumulated); + History("fatal_overdose", _log_name, HistoryMode::Accumulated); ret["intervention_admission"] = History( - "intervention_admission", GetLogName(), HistoryMode::Accumulated); + "intervention_admission", _log_name, HistoryMode::Accumulated); ret["background_death"] = - History("background_death", GetLogName(), HistoryMode::Accumulated); - SetHistories(ret); - } - - // manipulate the state vector - void RunTransitions() override { - SetupHistory(); - if (!_initial_history_recorded) { - RecordHistoryAtCurrentTimestep(); - } - int transitions_per_timestep = - static_cast(_transition_vector.size()) / _final_timestep; - for (const auto &t : _transition_vector) { - _state = t->Execute(_state, _histories); - } - _current_timestep++; - RecordHistoryAtCurrentTimestep(); - } - - // assume ownership of the Transition - void AddTimestep( - const std::vector> &transitions) override { - for (const auto &t : transitions) { - _transition_vector.push_back(t->clone()); - } - _final_timestep++; - } - // get the names of each transition we own - std::vector GetTransitionNames() const override { - std::vector t_names; - for (const auto &n : _transition_vector) { - t_names.push_back(n->GetTransitionName()); - } - return t_names; - } - // delete all the Transition unique_ptrs by clearing the vector - void ClearTransitions() override { _transition_vector.clear(); } - - virtual void - SetHistories(const std::map &h) override { - _histories = h; + History("background_death", _log_name, HistoryMode::Accumulated); + _histories = ret; if (_histories.empty()) { ResetHistoryTracking(); return; @@ -160,36 +265,9 @@ class Markov : public virtual Model { _initial_history_recorded = true; _current_timestep = latest_timestep; } - void ClearHistories() override { - _histories.clear(); - ResetHistoryTracking(); - } - - void SetHistoryCaptureInterval(int interval) override { - _history_capture_interval = (interval < 1) ? 1 : interval; - } - - int GetHistoryCaptureInterval() const override { - return _history_capture_interval; - } - - void SetFinalTimestep(int final_timestep) override { - _final_timestep = final_timestep; - } - - int GetFinalTimestep() const override { return _final_timestep; } - - // return const & to limit to observation of the state. Need copy ability of - // History, but let that be the History's responsibility - std::map GetHistories() const override { - return _histories; - } - // getter for model name - std::string GetModelName() const override { return _name; } - std::string GetLogName() const override { return _log_name; } private: - std::vector> _transition_vector; + std::vector _timestep_vector; Eigen::VectorXd _state; std::string _name; std::string _log_name; diff --git a/src/internals/transition_base.hpp b/src/internals/transition_base.hpp index 7ea64c1d..f9e15547 100644 --- a/src/internals/transition_base.hpp +++ b/src/internals/transition_base.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-02 // +// Last Modified: 2026-07-06 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -27,6 +27,10 @@ class TransitionBase : public virtual Transition { void AddMatrix(const Eigen::Ref &m) override { _transition_matrices.push_back(m); } + std::vector> + GetMatrices() const override { + return _transition_matrices; + } // Get the name of the Transition. No need to edit the object and do not // need user to edit the name. std::string GetName() const override { return _name; } @@ -35,11 +39,6 @@ class TransitionBase : public virtual Transition { std::string GetLogName() const override { return _log_name; } -protected: - const std::vector> &GetMatrices() const { - return _transition_matrices; - } - private: std::string _name; std::string _log_name; diff --git a/src/intervention.cpp b/src/intervention.cpp index 70e50b9c..ec64cbb8 100644 --- a/src/intervention.cpp +++ b/src/intervention.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-25 // +// Last Modified: 2026-07-07 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -22,27 +22,26 @@ namespace respond { Eigen::VectorXd Intervention::Execute(const Eigen::Ref &state, std::map &h) const { - if (GetTransitionMatrices().size() != 1) { + if (GetMatrices().size() != 1) { std::string error_msg = "Intervention error: Expected 1 transition matrix, got " + - std::to_string(GetTransitionMatrices().size()); + std::to_string(GetMatrices().size()); LogError(GetLogName(), error_msg); throw std::runtime_error(error_msg); } Eigen::VectorXd zero_matrix = Eigen::VectorXd::Zero(state.size()); - if (state.rows() != GetTransitionMatrices()[0].cols()) { + if (state.rows() != GetMatrices()[0].cols()) { std::stringstream ss; ss << "Intervention error: State dimension mismatch. State size is (" << state.rows() << ", " << state.cols() - << ") but transition matrix expects (" - << GetTransitionMatrices()[0].rows() << ", " - << GetTransitionMatrices()[0].cols() << ")"; + << ") but transition matrix expects (" << GetMatrices()[0].rows() + << ", " << GetMatrices()[0].cols() << ")"; std::string error_msg = ss.str(); LogError(GetLogName(), error_msg); throw std::runtime_error(error_msg); } - auto moved = GetTransitionMatrices()[0] * state; + auto moved = GetMatrices()[0] * state; // Add intervention_admissions to history if avaliable Eigen::VectorXd admissions = moved - state; @@ -53,9 +52,4 @@ Intervention::Execute(const Eigen::Ref &state, return moved; } - -std::unique_ptr Intervention::Create(const std::string &name, - const std::string &log_name) { - return std::make_unique(name, log_name); -} } // namespace respond \ No newline at end of file diff --git a/src/migration.cpp b/src/migration.cpp index 0ee5e4df..3a6aca29 100644 --- a/src/migration.cpp +++ b/src/migration.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-25 // +// Last Modified: 2026-07-07 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -22,29 +22,24 @@ namespace respond { Eigen::VectorXd Migration::Execute(const Eigen::Ref &state, std::map &h) const { - if (GetTransitionMatrices().size() != 1) { + if (GetMatrices().size() != 1) { std::string error_msg = "Migration error: Expected 1 transition matrix, got " + - std::to_string(GetTransitionMatrices().size()); + std::to_string(GetMatrices().size()); LogError(GetLogName(), error_msg); throw std::runtime_error(error_msg); } - if (state.size() != GetTransitionMatrices()[0].size()) { - std::string error_msg = - "Migration error: State size (" + std::to_string(state.size()) + - ") does not match transition matrix size (" + - std::to_string(GetTransitionMatrices()[0].size()) + ")"; + if (state.size() != GetMatrices()[0].size()) { + std::string error_msg = "Migration error: State size (" + + std::to_string(state.size()) + + ") does not match transition matrix size (" + + std::to_string(GetMatrices()[0].size()) + ")"; LogError(GetLogName(), error_msg); throw std::runtime_error(error_msg); } - auto subtracted = state + GetTransitionMatrices()[0]; + auto subtracted = state + GetMatrices()[0]; auto zero_stop = subtracted.array().max( Eigen::VectorXd::Zero(subtracted.size()).array()); return zero_stop; } - -std::unique_ptr Migration::Create(const std::string &name, - const std::string &log_name) { - return std::make_unique(name, log_name); -} } // namespace respond \ No newline at end of file diff --git a/src/model_factory.cpp b/src/model_factory.cpp index 48bfaf9e..d2a57d35 100644 --- a/src/model_factory.cpp +++ b/src/model_factory.cpp @@ -1,10 +1,10 @@ //////////////////////////////////////////////////////////////////////////////// -// File: markov.cpp // +// File: model_factory.cpp // // Project: respond // // Created Date: 2025-07-07 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-02-05 // +// Last Modified: 2026-07-07 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center // @@ -21,7 +21,8 @@ namespace respond { std::unique_ptr Model::Create(const std::string &name, - const std::string &log_name) { - return std::make_unique(name, log_name); + const std::string &log_name, + const std::string &log_filepath) { + return std::make_unique(name, log_name, log_filepath); } } // namespace respond diff --git a/src/overdose.cpp b/src/overdose.cpp index 794d59d0..bc700a41 100644 --- a/src/overdose.cpp +++ b/src/overdose.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-25 // +// Last Modified: 2026-07-07 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -22,39 +22,38 @@ namespace respond { Eigen::VectorXd Overdose::Execute(const Eigen::Ref &state, std::map &h) const { - if (GetTransitionMatrices().size() != 2) { + if (GetMatrices().size() != 2) { std::string error_msg = "Overdose error: Expected 2 transition matrices, got " + - std::to_string(GetTransitionMatrices().size()); + std::to_string(GetMatrices().size()); LogError(GetLogName(), error_msg); throw std::runtime_error(error_msg); } - if (state.size() != GetTransitionMatrices()[0].size()) { - std::string error_msg = - "Overdose error: State size (" + std::to_string(state.size()) + - ") does not match transition matrix size (" + - std::to_string(GetTransitionMatrices()[0].size()) + ")"; + if (state.size() != GetMatrices()[0].size()) { + std::string error_msg = "Overdose error: State size (" + + std::to_string(state.size()) + + ") does not match transition matrix size (" + + std::to_string(GetMatrices()[0].size()) + ")"; LogError(GetLogName(), error_msg); throw std::runtime_error(error_msg); } Eigen::VectorXd overdoses = - state.cwiseProduct(GetTransitionMatrices()[0]); // overdose + state.cwiseProduct(GetMatrices()[0]); // overdose // Add total overdoses to stamp if (h.find("total_overdose") != h.end()) { h["total_overdose"].AccumulateState(overdoses); } - if (overdoses.size() != GetTransitionMatrices()[1].size()) { - std::string error_msg = - "Overdose error: Fatal overdose vector size (" + - std::to_string(overdoses.size()) + - ") does not match transition matrix size (" + - std::to_string(GetTransitionMatrices()[1].size()) + ")"; + if (overdoses.size() != GetMatrices()[1].size()) { + std::string error_msg = "Overdose error: Fatal overdose vector size (" + + std::to_string(overdoses.size()) + + ") does not match transition matrix size (" + + std::to_string(GetMatrices()[1].size()) + ")"; LogError(GetLogName(), error_msg); throw std::runtime_error(error_msg); } - auto fods = overdoses.cwiseProduct(GetTransitionMatrices()[1]); // negatives + auto fods = overdoses.cwiseProduct(GetMatrices()[1]); // negatives if (h.find("fatal_overdose") != h.end()) { h["fatal_overdose"].AccumulateState(fods); } @@ -70,9 +69,4 @@ Overdose::Execute(const Eigen::Ref &state, auto new_state = state - fods; // remove fods from state return new_state; } - -std::unique_ptr Overdose::Create(const std::string &name, - const std::string &log_name) { - return std::make_unique(name, log_name); -} } // namespace respond \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 34bc349c..84aba9c2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -22,16 +22,22 @@ if(PROJECT_IS_TOP_LEVEL OR NOT RESPOND_BUILD_TESTS) return() endif() -file(GLOB RESPOND_TEST_MOCKS mocks/*.hpp) -file(GLOB RESPOND_UNIT_TESTS CONFIGURE_DEPENDS unit/*.cpp) -file(GLOB RESPOND_INTEGRATION_TESTS CONFIGURE_DEPENDS integration/*.cpp) - add_executable(respond_tests) target_sources(respond_tests PRIVATE - ${RESPOND_UNIT_TESTS} - ${RESPOND_INTEGRATION_TESTS} + unit/background_test.cpp + unit/behavior_test.cpp + unit/cost_effectiveness_test.cpp + unit/history_test.cpp + unit/intervention_test.cpp + unit/logging_test.cpp + unit/markov_test.cpp + unit/migration_test.cpp + unit/overdose_test.cpp + unit/simulation_test.cpp + unit/timestep_test.cpp + integration/respond_test.cpp ) target_include_directories(respond_tests diff --git a/tests/integration/respond_test.cpp b/tests/integration/respond_test.cpp index 33b9602e..bc6ec4a3 100644 --- a/tests/integration/respond_test.cpp +++ b/tests/integration/respond_test.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-29 // +// Last Modified: 2026-07-07 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -21,8 +21,8 @@ namespace testing { std::unique_ptr MakeTestTransition(const std::string &name, Eigen::MatrixXd matrix) { - auto migr = TransitionFactory::CreateTransition(name, "test_log"); - migr->AddTransitionMatrix(matrix); + auto migr = Transition::Create(name, "test_log"); + migr->AddMatrix(matrix); return migr; } @@ -53,142 +53,146 @@ class RespondTest : public ::testing::Test { void TearDown() override { markov.reset(); } }; -TEST_F(RespondTest, RunTransitionsInModel) { - markov->SetState(init_state); +// TEST_F(RespondTest, RunTransitionsInModel) { +// markov->SetState(init_state); - auto migr = MakeTestTransition("migration", migration_pop); - markov->AddTransition(migr); +// auto migr = MakeTestTransition("migration", migration_pop); +// markov->AddTransition(migr); - auto beha = MakeTestTransition("behavior", behavior_trans); - markov->AddTransition(beha); +// auto beha = MakeTestTransition("behavior", behavior_trans); +// markov->AddTransition(beha); - auto inte = MakeTestTransition("intervention", intervention_trans); - markov->AddTransition(inte); +// auto inte = MakeTestTransition("intervention", intervention_trans); +// markov->AddTransition(inte); - auto over = MakeTestTransition("overdose", overdose_prob); - over->AddTransitionMatrix(fod_prob); - markov->AddTransition(over); +// auto over = MakeTestTransition("overdose", overdose_prob); +// over->AddMatrix(fod_prob); +// markov->AddTransition(over); - auto back = MakeTestTransition("background_death", background_death_prob); - markov->AddTransition(back); +// auto back = MakeTestTransition("background_death", +// background_death_prob); markov->AddTransition(back); - markov->RunTransitions(); +// markov->RunTransitions(); - auto t_names = markov->GetTransitionNames(); - std::vector expected = {"migration", "behavior", - "intervention", "overdose", - "background_death"}; - ASSERT_EQ(t_names, expected); +// auto t_names = markov->GetTransitionNames(); +// std::vector expected = {"migration", "behavior", +// "intervention", "overdose", +// "background_death"}; +// ASSERT_EQ(t_names, expected); - Eigen::Vector3d final_state; - final_state << 0.76715528791564891, 0.72320370216816077, 1.037712429738102; - ASSERT_TRUE(markov->GetState().isApprox(final_state)); -} - -TEST_F(RespondTest, RunSimulationOneStep) { - markov->CreateDefaultHistories(); - - markov->SetState(init_state); - - auto migr = MakeTestTransition("migration", migration_pop); - markov->AddTransition(migr); - - auto beha = MakeTestTransition("behavior", behavior_trans); - markov->AddTransition(beha); - - auto inte = MakeTestTransition("intervention", intervention_trans); - markov->AddTransition(inte); - - auto over = MakeTestTransition("overdose", overdose_prob); - over->AddTransitionMatrix(fod_prob); - markov->AddTransition(over); +// Eigen::Vector3d final_state; +// final_state << 0.76715528791564891, +// 0.72320370216816077, 1.037712429738102; +// ASSERT_TRUE(markov->GetState().isApprox(final_state)); +// } - auto back = MakeTestTransition("background_death", background_death_prob); - markov->AddTransition(back); +// TEST_F(RespondTest, RunSimulationOneStep) { +// markov->CreateDefaultHistories(); - Simulation sim("test_logger"); - sim.AddModel(markov); - sim.Run(); +// markov->SetState(init_state); - auto histories = sim.GetModelHistories(); - ASSERT_EQ(histories.size(), 1); +// auto migr = MakeTestTransition("migration", migration_pop); +// markov->AddTransition(migr); - auto mm_histories = histories[0]; - if (mm_histories.find("state") == mm_histories.end()) { - FAIL() << "Unable to find the 'state' history."; - } +// auto beha = MakeTestTransition("behavior", behavior_trans); +// markov->AddTransition(beha); - auto state_history = mm_histories.at("state"); - // 2 because it carries the initial state and 1 step - ASSERT_EQ(state_history.size(), 2); +// auto inte = MakeTestTransition("intervention", intervention_trans); +// markov->AddTransition(inte); - Eigen::Vector3d final_state; - ASSERT_TRUE(state_history[0].isApprox(init_state)); - final_state << 0.76715528791564891, 0.72320370216816077, 1.037712429738102; - ASSERT_TRUE(state_history[1].isApprox(final_state)); -} +// auto over = MakeTestTransition("overdose", overdose_prob); +// over->AddMatrix(fod_prob); +// markov->AddTransition(over); -TEST_F(RespondTest, RunSimulationTwoStep) { - markov->CreateDefaultHistories(); +// auto back = MakeTestTransition("background_death", +// background_death_prob); markov->AddTransition(back); - markov->SetState(init_state); +// Simulation sim("test_logger"); +// sim.AddModel(markov); +// sim.Run(); - auto migr = MakeTestTransition("migration", migration_pop); - auto beha = MakeTestTransition("behavior", behavior_trans); - auto inte = MakeTestTransition("intervention", intervention_trans); - auto over = MakeTestTransition("overdose", overdose_prob); - over->AddTransitionMatrix(fod_prob); +// auto histories = sim.GetModelHistories(); +// ASSERT_EQ(histories.size(), 1); - auto back = MakeTestTransition("background_death", background_death_prob); +// auto mm_histories = histories[0]; +// if (mm_histories.find("state") == mm_histories.end()) { +// FAIL() << "Unable to find the 'state' history."; +// } - markov->AddTransition(migr); - markov->AddTransition(beha); - markov->AddTransition(inte); - markov->AddTransition(over); - markov->AddTransition(back); +// auto state_history = mm_histories.at("state"); +// // 2 because it carries the initial state and 1 step +// ASSERT_EQ(state_history.size(), 2); - markov->AddTransition(migr); - markov->AddTransition(beha); - markov->AddTransition(inte); - markov->AddTransition(over); - markov->AddTransition(back); +// Eigen::Vector3d final_state; +// ASSERT_TRUE(state_history[0].isApprox(init_state)); +// final_state << 0.76715528791564891, +// 0.72320370216816077, 1.037712429738102; +// ASSERT_TRUE(state_history[1].isApprox(final_state)); +// } - Simulation sim("test_logger"); - sim.AddModel(markov); - sim.Run(); +// TEST_F(RespondTest, RunSimulationTwoStep) { +// markov->CreateDefaultHistories(); - auto histories = sim.GetModelHistories(); - ASSERT_EQ(histories.size(), 1); +// markov->SetState(init_state); - auto mm_histories = histories[0]; - if (mm_histories.find("state") == mm_histories.end()) { - FAIL() << "Unable to find the 'state' history."; - } +// auto migr = MakeTestTransition("migration", migration_pop); +// auto beha = MakeTestTransition("behavior", behavior_trans); +// auto inte = MakeTestTransition("intervention", intervention_trans); +// auto over = MakeTestTransition("overdose", overdose_prob); +// over->AddMatrix(fod_prob); - auto state_history = mm_histories.at("state"); - // 2 because it carries the initial state and 1 step - ASSERT_EQ(state_history.size(), 3); +// auto back = MakeTestTransition("background_death", +// background_death_prob); - Eigen::Vector3d final_state; - ASSERT_TRUE(state_history[0].isApprox(init_state)); - final_state << 0.76715528791564891, 0.72320370216816077, 1.037712429738102; - ASSERT_TRUE(state_history[1].isApprox(final_state)); -} +// markov->AddTransition(migr); +// markov->AddTransition(beha); +// markov->AddTransition(inte); +// markov->AddTransition(over); +// markov->AddTransition(back); -TEST_F(RespondTest, CreateDefaultHistories) { - std::vector expected = { - "state", "total_overdose", "fatal_overdose", "intervention_admission", - "background_death"}; +// markov->AddTransition(migr); +// markov->AddTransition(beha); +// markov->AddTransition(inte); +// markov->AddTransition(over); +// markov->AddTransition(back); - std::sort(expected.begin(), expected.end()); - - markov->CreateDefaultHistories(); - std::vector results; - for (const auto &kv : markov->GetHistories()) { - results.push_back(kv.first); - } - ASSERT_EQ(results, expected); -} +// Simulation sim("test_logger"); +// sim.AddModel(markov); +// sim.Run(); + +// auto histories = sim.GetModelHistories(); +// ASSERT_EQ(histories.size(), 1); + +// auto mm_histories = histories[0]; +// if (mm_histories.find("state") == mm_histories.end()) { +// FAIL() << "Unable to find the 'state' history."; +// } + +// auto state_history = mm_histories.at("state"); +// // 2 because it carries the initial state and 1 step +// ASSERT_EQ(state_history.size(), 3); + +// Eigen::Vector3d final_state; +// ASSERT_TRUE(state_history[0].isApprox(init_state)); +// final_state << 0.76715528791564891, +// 0.72320370216816077, 1.037712429738102; +// ASSERT_TRUE(state_history[1].isApprox(final_state)); +// } + +// TEST_F(RespondTest, CreateDefaultHistories) { +// std::vector expected = { +// "state", "total_overdose", "fatal_overdose", +// "intervention_admission", "background_death"}; + +// std::sort(expected.begin(), expected.end()); + +// markov->CreateDefaultHistories(); +// std::vector results; +// for (const auto &kv : markov->GetHistories()) { +// results.push_back(kv.first); +// } +// ASSERT_EQ(results, expected); +// } } // namespace testing } // namespace respond \ No newline at end of file diff --git a/tests/mocks/model_mock.hpp b/tests/mocks/model_mock.hpp index 57c82850..e80809e9 100644 --- a/tests/mocks/model_mock.hpp +++ b/tests/mocks/model_mock.hpp @@ -4,7 +4,7 @@ // Created Date: 2025-08-01 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-25 // +// Last Modified: 2026-07-07 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center // @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -24,34 +25,33 @@ #include #include -#include - namespace respond { namespace testing { class MockModel : public virtual Model { public: - MOCK_METHOD(void, SetState, (const Eigen::Ref &), - (override)); - MOCK_METHOD(Eigen::VectorXd, GetState, (), (const, override)); - MOCK_METHOD(void, RunTransitions, (), (override)); - MOCK_METHOD(void, AddTransition, (const std::unique_ptr &), - (override)); - MOCK_METHOD((std::vector), GetTransitionNames, (), + MOCK_METHOD((std::unique_ptr), clone, (), (const, override)); + MOCK_METHOD(void, AddTimestep, (const Timestep &), (override)); + MOCK_METHOD(void, RunTimestep, (), (override)); + MOCK_METHOD(void, RunTimestep, (size_t), (override)); + MOCK_METHOD(void, RunTimesteps, (), (override)); + MOCK_METHOD(void, ClearTimesteps, (), (override)); + MOCK_METHOD(void, ClearHistories, (), (override)); + MOCK_METHOD(void, CreateDefaultHistories, (), (override)); + MOCK_METHOD(Timestep, GetTimestepAtIndex, (size_t), (const, override)); + MOCK_METHOD((const Eigen::Ref), GetState, (), (const, override)); - MOCK_METHOD(void, ClearTransitions, (), (override)); - MOCK_METHOD((std::map), GetHistories, (), + MOCK_METHOD(std::string, GetName, (), (const, override)); + MOCK_METHOD((const std::map &), GetHistories, (), (const, override)); - MOCK_METHOD(void, SetHistories, ((const std::map &)), + MOCK_METHOD(int, GetTimestep, (), (const, override)); + MOCK_METHOD(int, GetHistoryCaptureInterval, (), (const, override)); + MOCK_METHOD(int, GetFinalTimestep, (), (const, override)); + MOCK_METHOD(bool, GetInitialHistoryRecorded, (), (const, override)); + MOCK_METHOD(void, SetState, (const Eigen::Ref &), (override)); - MOCK_METHOD(void, ClearHistories, (), (override)); MOCK_METHOD(void, SetHistoryCaptureInterval, (int), (override)); - MOCK_METHOD(int, GetHistoryCaptureInterval, (), (const, override)); MOCK_METHOD(void, SetFinalTimestep, (int), (override)); - MOCK_METHOD(int, GetFinalTimestep, (), (const, override)); - MOCK_METHOD(std::string, GetModelName, (), (const, override)); - MOCK_METHOD(std::string, GetLogName, (), (const, override)); - MOCK_METHOD((std::unique_ptr), clone, (), (const, override)); - MOCK_METHOD(void, CreateDefaultHistories, (), (override)); + MOCK_METHOD(void, SetInitialHistoryRecorded, (bool), (override)); }; } // namespace testing } // namespace respond diff --git a/tests/mocks/transition_mock.hpp b/tests/mocks/transition_mock.hpp index 1e375741..64fc6af2 100644 --- a/tests/mocks/transition_mock.hpp +++ b/tests/mocks/transition_mock.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-25 // +// Last Modified: 2026-07-07 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -12,6 +12,7 @@ #ifndef RESPOND_TESTS_MARKOVMOCK_HPP_ #define RESPOND_TESTS_MARKOVMOCK_HPP_ +#include #include #include @@ -21,8 +22,6 @@ #include #include -#include - namespace respond { namespace testing { class MockTransition : public virtual Transition { @@ -31,10 +30,12 @@ class MockTransition : public virtual Transition { ((const Eigen::Ref &), (std::map &)), (const, override)); - MOCK_METHOD(void, AddTransitionMatrix, - (const Eigen::Ref &), (override)); - MOCK_METHOD(std::string, GetTransitionName, (), (const, override)); - MOCK_METHOD(void, ClearTransitionMatrices, (), (override)); + MOCK_METHOD(void, AddMatrix, (const Eigen::Ref &), + (override)); + MOCK_METHOD((std::vector>), GetMatrices, + (), (const, override)); + MOCK_METHOD(void, ClearMatrices, (), (override)); + MOCK_METHOD(std::string, GetName, (), (const, override)); MOCK_METHOD(std::string, GetLogName, (), (const, override)); MOCK_METHOD(std::unique_ptr, clone, (), (const, override)); }; diff --git a/tests/unit/background_test.cpp b/tests/unit/background_test.cpp index 92aaca2c..ca746270 100644 --- a/tests/unit/background_test.cpp +++ b/tests/unit/background_test.cpp @@ -4,12 +4,13 @@ // Created Date: 2026-02-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-02-06 // +// Last Modified: 2026-07-07 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // //////////////////////////////////////////////////////////////////////////////// +#include #include #include @@ -17,9 +18,6 @@ #include #include -#include -#include - namespace respond { namespace testing { @@ -32,8 +30,7 @@ class BackgroundDeathTest : public ::testing::Test { protected: void SetUp() override { - tran = TransitionFactory::CreateTransition("background_death", - "test_logger"); + tran = Transition::Create("background_death", "test_logger"); state = Eigen::VectorXd(3); state << 1.0f, 2.0f, 3.0f; @@ -48,13 +45,13 @@ TEST_F(BackgroundDeathTest, NoTransitionMatrices) { } TEST_F(BackgroundDeathTest, TooManyTransitionMatrices) { - tran->AddTransitionMatrix(state); - tran->AddTransitionMatrix(state); + tran->AddMatrix(state); + tran->AddMatrix(state); EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); } TEST_F(BackgroundDeathTest, GoodExecuteNoHistory) { - tran->AddTransitionMatrix(tran_matrix); + tran->AddMatrix(tran_matrix); auto result = tran->Execute(state, histories); auto expected = state - state.cwiseProduct(tran_matrix); EXPECT_TRUE(result.isApprox(expected)); @@ -63,7 +60,7 @@ TEST_F(BackgroundDeathTest, GoodExecuteNoHistory) { TEST_F(BackgroundDeathTest, GoodExecuteWriteHistory) { History h("background_death", "test_logger"); histories["background_death"] = h; - tran->AddTransitionMatrix(tran_matrix); + tran->AddMatrix(tran_matrix); auto result = tran->Execute(state, histories); auto expected_deaths = state.cwiseProduct(tran_matrix); auto expected_return = state - expected_deaths; diff --git a/tests/unit/behavior_test.cpp b/tests/unit/behavior_test.cpp index ffc547f7..b59969bb 100644 --- a/tests/unit/behavior_test.cpp +++ b/tests/unit/behavior_test.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-02-06 // +// Last Modified: 2026-07-07 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -32,7 +32,7 @@ class BehaviorTest : public ::testing::Test { protected: void SetUp() override { - tran = TransitionFactory::CreateTransition("behavior", "test_logger"); + tran = Transition::Create("behavior", "test_logger"); state = Eigen::VectorXd(3); state << 1.0f, 2.0f, 3.0f; @@ -47,18 +47,18 @@ TEST_F(BehaviorTest, NoTransitionMatrices) { } TEST_F(BehaviorTest, TooManyTransitionMatrices) { - tran->AddTransitionMatrix(tran_matrix); - tran->AddTransitionMatrix(tran_matrix); + tran->AddMatrix(tran_matrix); + tran->AddMatrix(tran_matrix); EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); } TEST_F(BehaviorTest, NotSquareTransitionMatrix) { - tran->AddTransitionMatrix(state); + tran->AddMatrix(state); EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); } TEST_F(BehaviorTest, GoodExecuteNoHistory) { - tran->AddTransitionMatrix(tran_matrix); + tran->AddMatrix(tran_matrix); auto result = tran->Execute(state, histories); auto expected = tran_matrix * state; EXPECT_TRUE(result.isApprox(expected)); diff --git a/tests/unit/intervention_test.cpp b/tests/unit/intervention_test.cpp index 192449e1..932cad46 100644 --- a/tests/unit/intervention_test.cpp +++ b/tests/unit/intervention_test.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-05-06 // +// Last Modified: 2026-07-07 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -32,8 +32,7 @@ class InterventionTest : public ::testing::Test { protected: void SetUp() override { - tran = - TransitionFactory::CreateTransition("intervention", "test_logger"); + tran = Transition::Create("intervention", "test_logger"); state = Eigen::VectorXd(3); state << 1.0f, 2.0f, 3.0f; @@ -48,18 +47,18 @@ TEST_F(InterventionTest, NoTransitionMatrices) { } TEST_F(InterventionTest, TooManyTransitionMatrices) { - tran->AddTransitionMatrix(tran_matrix); - tran->AddTransitionMatrix(tran_matrix); + tran->AddMatrix(tran_matrix); + tran->AddMatrix(tran_matrix); EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); } TEST_F(InterventionTest, NotSquareTransitionMatrix) { - tran->AddTransitionMatrix(state); + tran->AddMatrix(state); EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); } TEST_F(InterventionTest, GoodExecuteNoHistory) { - tran->AddTransitionMatrix(tran_matrix); + tran->AddMatrix(tran_matrix); auto result = tran->Execute(state, histories); auto expected = tran_matrix * state; EXPECT_TRUE(result.isApprox(expected)); @@ -68,7 +67,7 @@ TEST_F(InterventionTest, GoodExecuteNoHistory) { TEST_F(InterventionTest, GoodExecuteWriteHistory) { History h("intervention_admission", "test_logger"); histories["intervention_admission"] = h; - tran->AddTransitionMatrix(tran_matrix); + tran->AddMatrix(tran_matrix); auto result = tran->Execute(state, histories); auto expected_return = tran_matrix * state; auto expected_admissions = diff --git a/tests/unit/logging_test.cpp b/tests/unit/logging_test.cpp index 6cc1627c..cc2da2c0 100644 --- a/tests/unit/logging_test.cpp +++ b/tests/unit/logging_test.cpp @@ -4,10 +4,10 @@ // Created Date: 2025-03-18 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-04-16 // +// Last Modified: 2026-07-07 // // Modified By: Matthew Carroll // // ----- // -// Copyright (c) 2025 Syndemics Lab at Boston Medical Center // +// Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center // //////////////////////////////////////////////////////////////////////////////// #include @@ -496,8 +496,8 @@ TEST_F(LoggingTest, TransitionFactoryInvalidType) { CreateFileLogger("factory_test", test_log_file_); // Create transition with invalid type should log error and return nullptr - auto transition = respond::TransitionFactory::CreateTransition( - "invalid_type", "factory_test"); + auto transition = + respond::Transition::Create("invalid_type", "factory_test"); EXPECT_EQ(transition, nullptr); EXPECT_EQ(CheckLoggerExists("factory_test"), CreationStatus::kExists); @@ -507,24 +507,21 @@ TEST_F(LoggingTest, TransitionFactoryValidTypes) { CreateFileLogger("factory_test", test_log_file_); // Test all valid transition types - auto migration = respond::TransitionFactory::CreateTransition( - "migration", "factory_test"); + auto migration = respond::Transition::Create("migration", "factory_test"); EXPECT_NE(migration, nullptr); - auto behavior = respond::TransitionFactory::CreateTransition( - "behavior", "factory_test"); + auto behavior = respond::Transition::Create("behavior", "factory_test"); EXPECT_NE(behavior, nullptr); - auto intervention = respond::TransitionFactory::CreateTransition( - "intervention", "factory_test"); + auto intervention = + respond::Transition::Create("intervention", "factory_test"); EXPECT_NE(intervention, nullptr); - auto overdose = respond::TransitionFactory::CreateTransition( - "overdose", "factory_test"); + auto overdose = respond::Transition::Create("overdose", "factory_test"); EXPECT_NE(overdose, nullptr); - auto background = respond::TransitionFactory::CreateTransition( - "background_death", "factory_test"); + auto background = + respond::Transition::Create("background_death", "factory_test"); EXPECT_NE(background, nullptr); } @@ -532,20 +529,16 @@ TEST_F(LoggingTest, TransitionFactoryCaseInsensitivity) { CreateFileLogger("factory_test", test_log_file_); // Test case-insensitive matching - auto trans1 = respond::TransitionFactory::CreateTransition("MIGRATION", - "factory_test"); + auto trans1 = respond::Transition::Create("MIGRATION", "factory_test"); EXPECT_NE(trans1, nullptr); - auto trans2 = respond::TransitionFactory::CreateTransition("Behavior", - "factory_test"); + auto trans2 = respond::Transition::Create("Behavior", "factory_test"); EXPECT_NE(trans2, nullptr); - auto trans3 = respond::TransitionFactory::CreateTransition("INTERVENTION", - "factory_test"); + auto trans3 = respond::Transition::Create("INTERVENTION", "factory_test"); EXPECT_NE(trans3, nullptr); - auto trans4 = respond::TransitionFactory::CreateTransition("OverDose", - "factory_test"); + auto trans4 = respond::Transition::Create("OverDose", "factory_test"); EXPECT_NE(trans4, nullptr); } diff --git a/tests/unit/markov_test.cpp b/tests/unit/markov_test.cpp index 635969e1..065b3d09 100644 --- a/tests/unit/markov_test.cpp +++ b/tests/unit/markov_test.cpp @@ -4,7 +4,7 @@ // Created Date: 2025-06-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-02-06 // +// Last Modified: 2026-07-07 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center // @@ -12,13 +12,14 @@ #include +#include #include #include #include +#include -#include - +#include "../../src/internals/markov.hpp" #include "../mocks/transition_mock.hpp" using ::testing::_; @@ -30,163 +31,329 @@ namespace testing { class MarkovTest : public ::testing::Test { public: - std::unique_ptr markov; Eigen::VectorXd state; protected: void SetUp() override { - markov = Model::Create("markov", "test_logger"); + // Clear any existing loggers from previous tests + spdlog::drop_all(); + + // Create temporary log files for testing + test_log_file_ = "/tmp/respond_test.log"; + shared_log_file_ = "/tmp/respond_shared.log"; + default_log_file_ = RESPOND_DEFAULT_LOG_FILE; + + // Remove test files if they exist + std::remove(default_log_file_.c_str()); + std::remove(test_log_file_.c_str()); + std::remove(shared_log_file_.c_str()); + state = Eigen::VectorXd(3); state << 1.0f, 2.0f, 3.0f; } - void TearDown() override { markov.reset(); } + void TearDown() override { + // Clean up loggers + spdlog::drop_all(); + + // Remove test files + std::remove(default_log_file_.c_str()); + std::remove(test_log_file_.c_str()); + std::remove(shared_log_file_.c_str()); + } + + std::string test_log_file_; + std::string shared_log_file_; + std::string default_log_file_; + + // Helper to check if file contains a string + bool FileContains(const std::string &filepath, const std::string &search) { + std::ifstream file(filepath); + if (!file.is_open()) + return false; + + std::string line; + while (std::getline(file, line)) { + if (line.find(search) != std::string::npos) { + return true; + } + } + return false; + } }; -TEST_F(MarkovTest, GetEmptyState) { - auto result = markov->GetState(); - SUCCEED(); +TEST_F(MarkovTest, CreateMarkovModel) { + auto markov = Model::Create("markov"); + ASSERT_NE(markov, nullptr); + ASSERT_EQ(CreateFileLogger(RESPOND_DEFAULT_LOG, ""), + CreationStatus::kExists); } -TEST_F(MarkovTest, GetAndSetState) { - markov->SetState(state); - auto result = markov->GetState(); - EXPECT_TRUE(result.isApprox(state)); +TEST_F(MarkovTest, MoveConstructor) { + Markov markov("markov_source", RESPOND_DEFAULT_LOG); + markov.SetState(state); + markov.SetHistoryCaptureInterval(3); + markov.SetFinalTimestep(7); + markov.SetInitialHistoryRecorded(true); + markov.CreateDefaultHistories(); + Timestep timestep(RESPOND_DEFAULT_LOG); + markov.AddTimestep(timestep); + + const auto expected_name = markov.GetName(); + const auto expected_interval = markov.GetHistoryCaptureInterval(); + const auto expected_final_timestep = markov.GetFinalTimestep(); + const auto expected_initial_history = markov.GetInitialHistoryRecorded(); + const auto expected_history_count = markov.GetHistories().size(); + + Markov moved_markov(std::move(markov)); + + EXPECT_TRUE(moved_markov.GetState().isApprox(state)); + EXPECT_EQ(moved_markov.GetName(), expected_name); + EXPECT_EQ(moved_markov.GetHistoryCaptureInterval(), expected_interval); + EXPECT_EQ(moved_markov.GetFinalTimestep(), expected_final_timestep); + EXPECT_EQ(moved_markov.GetInitialHistoryRecorded(), + expected_initial_history); + EXPECT_EQ(moved_markov.GetHistories().size(), expected_history_count); + EXPECT_NO_THROW((void)moved_markov.GetTimestepAtIndex(0)); + + EXPECT_TRUE(markov.GetHistories().empty()); + EXPECT_THROW((void)markov.GetTimestepAtIndex(0), std::out_of_range); } -TEST_F(MarkovTest, GetEmptyTransitionName) { - auto names = markov->GetTransitionNames(); - ASSERT_EQ(names.size(), 0); +TEST_F(MarkovTest, MoveOperator) { + Markov markov("markov_source", RESPOND_DEFAULT_LOG); + markov.SetState(state); + markov.SetHistoryCaptureInterval(3); + markov.SetFinalTimestep(7); + markov.SetInitialHistoryRecorded(true); + markov.CreateDefaultHistories(); + Timestep timestep(RESPOND_DEFAULT_LOG); + markov.AddTimestep(timestep); + + const auto expected_name = markov.GetName(); + const auto expected_interval = markov.GetHistoryCaptureInterval(); + const auto expected_final_timestep = markov.GetFinalTimestep(); + const auto expected_initial_history = markov.GetInitialHistoryRecorded(); + const auto expected_history_count = markov.GetHistories().size(); + + Markov moved_markov = std::move(markov); + + EXPECT_TRUE(moved_markov.GetState().isApprox(state)); + EXPECT_EQ(moved_markov.GetName(), expected_name); + EXPECT_EQ(moved_markov.GetHistoryCaptureInterval(), expected_interval); + EXPECT_EQ(moved_markov.GetFinalTimestep(), expected_final_timestep); + EXPECT_EQ(moved_markov.GetInitialHistoryRecorded(), + expected_initial_history); + EXPECT_EQ(moved_markov.GetHistories().size(), expected_history_count); + EXPECT_NO_THROW((void)moved_markov.GetTimestepAtIndex(0)); + + EXPECT_TRUE(markov.GetHistories().empty()); + EXPECT_THROW((void)markov.GetTimestepAtIndex(0), std::out_of_range); } -TEST_F(MarkovTest, TransitionNames) { - // When Markov::AddTransition copies the transition it calls `clone()` on - // the provided object. Make the mock return a heap-allocated mock that - // will receive the `GetTransitionName()` call later. - auto upmt = std::make_unique>(); - auto clone = std::make_unique>(); - EXPECT_CALL(*clone, GetTransitionName()) - .WillOnce(Return(std::string("test_transition"))); - EXPECT_CALL(*upmt, clone()) - .WillOnce(::testing::Return(::testing::ByMove(std::move(clone)))); - - markov->AddTransition(std::move(upmt)); - auto names = markov->GetTransitionNames(); - ASSERT_EQ(names.size(), 1u); - EXPECT_EQ(names[0], "test_transition"); +TEST_F(MarkovTest, Clone) { + Markov markov("markov_source", RESPOND_DEFAULT_LOG); + markov.SetState(state); + markov.SetHistoryCaptureInterval(3); + markov.SetFinalTimestep(7); + markov.SetInitialHistoryRecorded(true); + markov.CreateDefaultHistories(); + Timestep timestep(RESPOND_DEFAULT_LOG); + markov.AddTimestep(timestep); + + const auto expected_name = markov.GetName(); + const auto expected_interval = markov.GetHistoryCaptureInterval(); + const auto expected_final_timestep = markov.GetFinalTimestep(); + const auto expected_initial_history = markov.GetInitialHistoryRecorded(); + const auto expected_history_count = markov.GetHistories().size(); + + auto cloned_markov = markov.clone(); + + EXPECT_TRUE(cloned_markov->GetState().isApprox(state)); + EXPECT_EQ(cloned_markov->GetName(), expected_name); + EXPECT_EQ(cloned_markov->GetHistoryCaptureInterval(), expected_interval); + EXPECT_EQ(cloned_markov->GetFinalTimestep(), expected_final_timestep); + EXPECT_EQ(cloned_markov->GetInitialHistoryRecorded(), + expected_initial_history); + EXPECT_EQ(cloned_markov->GetHistories().size(), expected_history_count); + EXPECT_NO_THROW((void)cloned_markov->GetTimestepAtIndex(0)); + + EXPECT_FALSE(markov.GetHistories().empty()); + EXPECT_NO_THROW((void)markov.GetTimestepAtIndex(0)); } -TEST_F(MarkovTest, RunTransitions) { - // When Markov::AddTransition copies the transition it calls `clone()` on - // the provided object. Make the mock return a heap-allocated mock that - // will receive the `GetTransitionName()` call later. - auto upmt = std::make_unique>(); - auto clone = std::make_unique>(); - EXPECT_CALL(*clone, Execute(_, _)).Times(1); - EXPECT_CALL(*upmt, clone()) - .WillOnce(::testing::Return(::testing::ByMove(std::move(clone)))); - - markov->AddTransition(std::move(upmt)); - markov->RunTransitions(); +TEST_F(MarkovTest, GetTimestepAtIndexOutOfRange) { + Markov markov("markov", RESPOND_DEFAULT_LOG); + EXPECT_THROW((void)markov.GetTimestepAtIndex(0), std::out_of_range); + Timestep timestep(RESPOND_DEFAULT_LOG); + markov.AddTimestep(timestep); + EXPECT_NO_THROW((void)markov.GetTimestepAtIndex(0)); + EXPECT_THROW((void)markov.GetTimestepAtIndex(1), std::out_of_range); } -TEST_F(MarkovTest, RunTransitionsAccumulatesDefaultHistories) { - markov->SetState(state); - markov->RunTransitions(); +TEST_F(MarkovTest, GetTimestepAtIndex) { + Markov markov("markov", RESPOND_DEFAULT_LOG); + Timestep timestep1(RESPOND_DEFAULT_LOG); + Timestep timestep2(RESPOND_DEFAULT_LOG); + markov.AddTimestep(timestep1); + markov.AddTimestep(timestep2); - Eigen::VectorXd next_state = state * 2.0; - markov->SetState(next_state); - markov->RunTransitions(); - - const auto histories = markov->GetHistories(); - ASSERT_EQ(histories.size(), 5u); + EXPECT_EQ(markov.GetTimestepAtIndex(0), timestep1); + EXPECT_EQ(markov.GetTimestepAtIndex(1), timestep2); +} - const auto state_history = histories.at("state").GetStateAsVector(); - ASSERT_EQ(state_history.size(), 3u); - EXPECT_TRUE(state_history[0].isApprox(state)); - EXPECT_TRUE(state_history[1].isApprox(state)); - EXPECT_TRUE(state_history[2].isApprox(next_state)); +TEST_F(MarkovTest, GetAndSetState) { + Markov markov("markov", RESPOND_DEFAULT_LOG); + markov.SetState(state); + auto result = markov.GetState(); + EXPECT_TRUE(result.isApprox(state)); +} - const auto overdose_history = - histories.at("total_overdose").GetStateAsVector(); - ASSERT_EQ(overdose_history.size(), 3u); - EXPECT_TRUE(overdose_history[0].isZero()); - EXPECT_TRUE(overdose_history[1].isZero()); - EXPECT_TRUE(overdose_history[2].isZero()); +TEST_F(MarkovTest, GetName) { + Markov markov("markov_test", RESPOND_DEFAULT_LOG); + EXPECT_EQ(markov.GetName(), "markov_test"); } -TEST_F(MarkovTest, SparseHistoryCaptureRecordsRequestedAndFinalTimesteps) { - markov->SetHistoryCaptureInterval(2); - markov->SetFinalTimestep(5); - markov->SetState(state); +TEST_F(MarkovTest, GetHistoriesCreateDefaultHistories) { + Markov markov("markov", RESPOND_DEFAULT_LOG); + markov.CreateDefaultHistories(); + const auto &histories = markov.GetHistories(); + EXPECT_EQ(histories.size(), 5u); + EXPECT_TRUE(histories.find("state") != histories.end()); + EXPECT_TRUE(histories.find("total_overdose") != histories.end()); + EXPECT_TRUE(histories.find("fatal_overdose") != histories.end()); + EXPECT_TRUE(histories.find("intervention_admission") != histories.end()); + EXPECT_TRUE(histories.find("background_death") != histories.end()); +} - for (int step = 0; step < 5; ++step) { - markov->RunTransitions(); - } +TEST_F(MarkovTest, GetTimestep) { + Markov markov("markov", RESPOND_DEFAULT_LOG); + EXPECT_EQ(markov.GetTimestep(), 0); + Timestep timestep1(RESPOND_DEFAULT_LOG); + markov.AddTimestep(timestep1); + EXPECT_EQ(markov.GetTimestep(), 0); + markov.RunTimestep(); + EXPECT_EQ(markov.GetTimestep(), 1); +} - const auto histories = markov->GetHistories(); - const auto ×teps = histories.at("state").GetRecordedTimesteps(); - std::vector expected = {0, 2, 4, 5}; - ASSERT_EQ(timesteps, expected); +TEST_F(MarkovTest, GetAndSetHistoryCaptureInterval) { + Markov markov("markov", RESPOND_DEFAULT_LOG); + EXPECT_EQ(markov.GetHistoryCaptureInterval(), 1); + markov.SetHistoryCaptureInterval(5); + EXPECT_EQ(markov.GetHistoryCaptureInterval(), 5); } -TEST_F(MarkovTest, ClearHistoriesResetsTrackingState) { - markov->SetHistoryCaptureInterval(2); - markov->SetFinalTimestep(4); - markov->SetState(state); +TEST_F(MarkovTest, GetAndSetFinalTimestep) { + Markov markov("markov", RESPOND_DEFAULT_LOG); + EXPECT_EQ(markov.GetFinalTimestep(), -1); + markov.SetFinalTimestep(10); + EXPECT_EQ(markov.GetFinalTimestep(), 10); +} - markov->RunTransitions(); - markov->RunTransitions(); - markov->ClearHistories(); +TEST_F(MarkovTest, GetAndSetInitialHistoryRecorded) { + Markov markov("markov", RESPOND_DEFAULT_LOG); + EXPECT_FALSE(markov.GetInitialHistoryRecorded()); + markov.SetInitialHistoryRecorded(true); + EXPECT_TRUE(markov.GetInitialHistoryRecorded()); +} - Eigen::VectorXd next_state = state * 3.0; - markov->SetState(next_state); - markov->RunTransitions(); +TEST_F(MarkovTest, AddTimestep) { + Markov markov("markov", RESPOND_DEFAULT_LOG); + Timestep timestep(RESPOND_DEFAULT_LOG); + markov.AddTimestep(timestep); + EXPECT_NO_THROW((void)markov.GetTimestepAtIndex(0)); +} - const auto histories = markov->GetHistories(); - const auto ×teps = histories.at("state").GetRecordedTimesteps(); - std::vector expected = {0}; - ASSERT_EQ(timesteps, expected); +TEST_F(MarkovTest, AddTimestepBeyondFinalTimestep) { + Markov markov("markov", RESPOND_DEFAULT_LOG); + markov.SetFinalTimestep(1); + Timestep timestep1(RESPOND_DEFAULT_LOG); + Timestep timestep2(RESPOND_DEFAULT_LOG); + markov.AddTimestep(timestep1); + markov.AddTimestep(timestep2); + FlushAllLoggers(); + EXPECT_TRUE(FileContains(RESPOND_DEFAULT_LOG_FILE, + "Final timestep exceeded by added timestep.")); +} - const auto &states = histories.at("state").GetRecordedStates(); - ASSERT_EQ(states.size(), 1u); - EXPECT_TRUE(states[0].isApprox(next_state)); +TEST_F(MarkovTest, RunTimestep) { + Markov markov("markov", RESPOND_DEFAULT_LOG); + Timestep timestep(RESPOND_DEFAULT_LOG); + markov.AddTimestep(timestep); + EXPECT_EQ(markov.GetTimestep(), 0); + markov.RunTimestep(); + EXPECT_EQ(markov.GetTimestep(), 1); } -TEST_F(MarkovTest, ClearTransitions) { - // When Markov::AddTransition copies the transition it calls `clone()` on - // the provided object. Make the mock return a heap-allocated mock that - // will receive the `GetTransitionName()` call later. - auto upmt = std::make_unique>(); - auto clone = std::make_unique>(); - ON_CALL(*clone, GetTransitionName()) - .WillByDefault(Return(std::string("test_transition"))); - EXPECT_CALL(*upmt, clone()) - .WillOnce(::testing::Return(::testing::ByMove(std::move(clone)))); +TEST_F(MarkovTest, RunTimestepEmptyTimestepVector) { + Markov markov("markov", RESPOND_DEFAULT_LOG); + markov.RunTimestep(); + FlushAllLoggers(); + EXPECT_TRUE( + FileContains(RESPOND_DEFAULT_LOG_FILE, + "No timesteps available to run for model: markov")); +} - markov->AddTransition(std::move(upmt)); - markov->ClearTransitions(); - auto names = markov->GetTransitionNames(); - ASSERT_EQ(names.size(), 0); +TEST_F(MarkovTest, RunTimestepIndex) { + Markov markov("markov", RESPOND_DEFAULT_LOG); + Timestep timestep1(RESPOND_DEFAULT_LOG); + Timestep timestep2(RESPOND_DEFAULT_LOG); + markov.AddTimestep(timestep1); + markov.AddTimestep(timestep2); + EXPECT_EQ(markov.GetTimestep(), 0); + markov.RunTimestep(1); + EXPECT_EQ(markov.GetTimestep(), 0); // Current timestep does not change } -TEST_F(MarkovTest, EmptyHistories) { - auto result = markov->GetHistories(); - ASSERT_EQ(result.size(), 0); +TEST_F(MarkovTest, RunTimestepIndexOutOfRange) { + Markov markov("markov", RESPOND_DEFAULT_LOG); + Timestep timestep(RESPOND_DEFAULT_LOG); + markov.AddTimestep(timestep); + markov.RunTimestep(2); + FlushAllLoggers(); + EXPECT_TRUE(FileContains( + RESPOND_DEFAULT_LOG_FILE, + "Current timestep exceeds available timesteps for model: markov")); } -TEST_F(MarkovTest, Histories) { - std::map hv; - History h("temp", "test_logger"); - hv["temp"] = h; - markov->SetHistories(hv); - auto result = markov->GetHistories(); - ASSERT_EQ(result.size(), 1u); - EXPECT_EQ(result["temp"], h); +TEST_F(MarkovTest, RunTimesteps) { + Markov markov("markov", RESPOND_DEFAULT_LOG); + markov.SetInitialHistoryRecorded(true); + Timestep timestep1(RESPOND_DEFAULT_LOG); + Timestep timestep2(RESPOND_DEFAULT_LOG); + markov.AddTimestep(timestep1); + markov.AddTimestep(timestep2); + EXPECT_EQ(markov.GetTimestep(), 0); + markov.RunTimesteps(); + EXPECT_EQ(markov.GetTimestep(), 2); } -TEST_F(MarkovTest, ModelName) { ASSERT_EQ(markov->GetModelName(), "markov"); } +TEST_F(MarkovTest, RunTimestepsRecordInitialHistory) { + Markov markov("markov", RESPOND_DEFAULT_LOG); + markov.SetInitialHistoryRecorded(false); + Timestep timestep1(RESPOND_DEFAULT_LOG); + Timestep timestep2(RESPOND_DEFAULT_LOG); + markov.AddTimestep(timestep1); + markov.AddTimestep(timestep2); + EXPECT_EQ(markov.GetTimestep(), 0); + markov.RunTimesteps(); + EXPECT_EQ(markov.GetTimestep(), 2); +} -TEST_F(MarkovTest, LogName) { ASSERT_EQ(markov->GetLogName(), "test_logger"); } +TEST_F(MarkovTest, ClearTimesteps) { + Markov markov("markov", RESPOND_DEFAULT_LOG); + Timestep timestep(RESPOND_DEFAULT_LOG); + markov.AddTimestep(timestep); + EXPECT_NO_THROW((void)markov.GetTimestepAtIndex(0)); + markov.ClearTimesteps(); + EXPECT_THROW((void)markov.GetTimestepAtIndex(0), std::out_of_range); +} +TEST_F(MarkovTest, ClearHistories) { + Markov markov("markov", RESPOND_DEFAULT_LOG); + markov.CreateDefaultHistories(); + EXPECT_FALSE(markov.GetHistories().empty()); + markov.ClearHistories(); + EXPECT_TRUE(markov.GetHistories().empty()); +} } // namespace testing } // namespace respond diff --git a/tests/unit/migration_test.cpp b/tests/unit/migration_test.cpp index d66413c8..5fdf02ae 100644 --- a/tests/unit/migration_test.cpp +++ b/tests/unit/migration_test.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-02-06 // +// Last Modified: 2026-07-07 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -32,7 +32,7 @@ class MigrationTest : public ::testing::Test { protected: void SetUp() override { - tran = TransitionFactory::CreateTransition("migration", "test_logger"); + tran = Transition::Create("migration", "test_logger"); state = Eigen::VectorXd(3); state << 1.0f, 2.0f, 3.0f; @@ -47,8 +47,8 @@ TEST_F(MigrationTest, NoTransitionMatrices) { } TEST_F(MigrationTest, TooManyTransitionMatrices) { - tran->AddTransitionMatrix(state); - tran->AddTransitionMatrix(state); + tran->AddMatrix(state); + tran->AddMatrix(state); EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); } @@ -56,12 +56,12 @@ TEST_F(MigrationTest, WrongSizeTransitionMatrix) { Eigen::VectorXd bad_t_matrix; bad_t_matrix = Eigen::VectorXd(6); bad_t_matrix << 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f; - tran->AddTransitionMatrix(bad_t_matrix); + tran->AddMatrix(bad_t_matrix); EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); } TEST_F(MigrationTest, GoodExecuteNoHistory) { - tran->AddTransitionMatrix(tran_matrix); + tran->AddMatrix(tran_matrix); auto result = tran->Execute(state, histories); auto expected = state + tran_matrix; EXPECT_TRUE(result.isApprox(expected)); diff --git a/tests/unit/overdose_test.cpp b/tests/unit/overdose_test.cpp index 6dcd488f..947abfc0 100644 --- a/tests/unit/overdose_test.cpp +++ b/tests/unit/overdose_test.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-05-06 // +// Last Modified: 2026-07-07 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -32,7 +32,7 @@ class OverdoseTest : public ::testing::Test { protected: void SetUp() override { - tran = TransitionFactory::CreateTransition("overdose", "test_logger"); + tran = Transition::Create("overdose", "test_logger"); state = Eigen::VectorXd(3); state << 1.0f, 2.0f, 3.0f; @@ -47,14 +47,14 @@ TEST_F(OverdoseTest, NoTransitionMatrices) { } TEST_F(OverdoseTest, TooFewTransitionMatrices) { - tran->AddTransitionMatrix(state); + tran->AddMatrix(state); EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); } TEST_F(OverdoseTest, TooManyTransitionMatrices) { - tran->AddTransitionMatrix(state); - tran->AddTransitionMatrix(state); - tran->AddTransitionMatrix(state); + tran->AddMatrix(state); + tran->AddMatrix(state); + tran->AddMatrix(state); EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); } @@ -62,14 +62,14 @@ TEST_F(OverdoseTest, WrongSizeTransitionMatrix) { Eigen::VectorXd bad_t_matrix; bad_t_matrix = Eigen::VectorXd(6); bad_t_matrix << 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f; - tran->AddTransitionMatrix(bad_t_matrix); - tran->AddTransitionMatrix(bad_t_matrix); + tran->AddMatrix(bad_t_matrix); + tran->AddMatrix(bad_t_matrix); EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); } TEST_F(OverdoseTest, GoodExecuteNoHistory) { - tran->AddTransitionMatrix(tran_matrix); - tran->AddTransitionMatrix(tran_matrix); + tran->AddMatrix(tran_matrix); + tran->AddMatrix(tran_matrix); auto result = tran->Execute(state, histories); auto overdoses = state.cwiseProduct(tran_matrix); auto fods = overdoses.cwiseProduct(tran_matrix); @@ -80,8 +80,8 @@ TEST_F(OverdoseTest, GoodExecuteNoHistory) { TEST_F(OverdoseTest, GoodExecuteWriteTotalOverdoseHistory) { History h("total_overdose", "test_logger"); histories["total_overdose"] = h; - tran->AddTransitionMatrix(tran_matrix); - tran->AddTransitionMatrix(tran_matrix); + tran->AddMatrix(tran_matrix); + tran->AddMatrix(tran_matrix); auto result = tran->Execute(state, histories); auto overdoses = state.cwiseProduct(tran_matrix); @@ -97,8 +97,8 @@ TEST_F(OverdoseTest, GoodExecuteWriteTotalOverdoseHistory) { TEST_F(OverdoseTest, GoodExecuteWriteFatalOverdoseHistory) { History h("fatal_overdose", "test_logger"); histories["fatal_overdose"] = h; - tran->AddTransitionMatrix(tran_matrix); - tran->AddTransitionMatrix(tran_matrix); + tran->AddMatrix(tran_matrix); + tran->AddMatrix(tran_matrix); auto result = tran->Execute(state, histories); auto overdoses = state.cwiseProduct(tran_matrix); @@ -115,8 +115,8 @@ TEST_F(OverdoseTest, GoodExecuteWriteAllHistory) { histories["fatal_overdose"] = h1; History h2("total_overdose", "test_logger"); histories["total_overdose"] = h2; - tran->AddTransitionMatrix(tran_matrix); - tran->AddTransitionMatrix(tran_matrix); + tran->AddMatrix(tran_matrix); + tran->AddMatrix(tran_matrix); auto result = tran->Execute(state, histories); auto overdoses = state.cwiseProduct(tran_matrix); diff --git a/tests/unit/simulation_test.cpp b/tests/unit/simulation_test.cpp index 06b3140f..9cd973bc 100644 --- a/tests/unit/simulation_test.cpp +++ b/tests/unit/simulation_test.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-09 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-02-12 // +// Last Modified: 2026-07-07 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -32,138 +32,138 @@ class SimulationTest : public ::testing::Test { void TearDown() override {} }; -TEST_F(SimulationTest, ConstructGetLogger) { - Simulation s; - ASSERT_EQ(s.GetLogName(), "console"); -} - -TEST_F(SimulationTest, GetSetModel) { - auto mock = std::make_unique>(); - auto cloned = std::make_unique>(); - EXPECT_CALL(*mock, clone()) - .WillOnce(Return(::testing::ByMove(std::move(cloned)))); - - std::unique_ptr upmm = std::move(mock); - Simulation s; - s.AddModel(upmm); - ASSERT_EQ(s.GetModels().size(), 1); -} - -TEST_F(SimulationTest, ClearModels) { - auto mock = std::make_unique>(); - auto cloned = std::make_unique>(); - EXPECT_CALL(*mock, clone()) - .WillOnce(Return(::testing::ByMove(std::move(cloned)))); - - std::unique_ptr upmm = std::move(mock); - Simulation s; - s.AddModel(upmm); - s.ClearModels(); - ASSERT_EQ(s.GetModels().size(), 0); -} - -TEST_F(SimulationTest, GetModelNames) { - auto mock = std::make_unique>(); - auto cloned = std::make_unique>(); - auto expected = "test_model_name"; - EXPECT_CALL(*cloned, GetModelName()).WillOnce(Return(expected)); - EXPECT_CALL(*mock, clone()) - .WillOnce(Return(::testing::ByMove(std::move(cloned)))); - - std::unique_ptr upmm = std::move(mock); - Simulation s; - s.AddModel(upmm); - auto result = s.GetModelNames(); - ASSERT_EQ(result.size(), 1); - ASSERT_EQ(result[0], expected); -} - -TEST_F(SimulationTest, GetModelHistories) { - auto mock = std::make_unique>(); - auto cloned = std::make_unique>(); - - std::map hv; - History h("temp", "test_logger"); - Eigen::VectorXd state = Eigen::VectorXd(3); - state << 1.0f, 2.0f, 3.0f; - h.AddState(state); - hv["temp"] = h; - - std::map> h_map; - h_map["temp"] = h.GetStateAsVector(); - - std::vector>> expected; - expected.push_back(h_map); - - ON_CALL(*cloned, GetModelName()).WillByDefault(Return("temp_model")); - - EXPECT_CALL(*cloned, GetHistories()).WillOnce(Return(hv)); - ON_CALL(*mock, clone()) - .WillByDefault(Return(::testing::ByMove(std::move(cloned)))); - - std::unique_ptr upmm = std::move(mock); - Simulation s; - s.AddModel(upmm); - ASSERT_EQ(s.GetModelHistories(), expected); -} - -TEST_F(SimulationTest, GetHistoryNames) { - auto mock = std::make_unique>(); - auto cloned = std::make_unique>(); - - std::string model_name = "temp_model"; - std::string history_name = "temp_history"; - - std::map hv; - History h("temp", "test_logger"); - hv[history_name] = h; - - std::vector> expected = { - {model_name, history_name}}; - - ON_CALL(*cloned, GetModelName()).WillByDefault(Return(model_name)); - - EXPECT_CALL(*cloned, GetHistories()).WillOnce(Return(hv)); - ON_CALL(*mock, clone()) - .WillByDefault(Return(::testing::ByMove(std::move(cloned)))); - - std::unique_ptr upmm = std::move(mock); - Simulation s; - s.AddModel(upmm); - ASSERT_EQ(s.GetModelHistoryNames(), expected); -} - -TEST_F(SimulationTest, GetModelSparseHistories) { - auto mock = std::make_unique>(); - auto cloned = std::make_unique>(); - - std::map hv; - History h("temp", "test_logger"); - Eigen::VectorXd state0 = Eigen::VectorXd(2); - state0 << 1.0f, 2.0f; - Eigen::VectorXd state2 = Eigen::VectorXd(2); - state2 << 3.0f, 4.0f; - h.AddState(state0, 0); - h.AddState(state2, 2); - hv["temp"] = h; - - EXPECT_CALL(*cloned, GetHistories()).WillOnce(Return(hv)); - ON_CALL(*mock, clone()) - .WillByDefault(Return(::testing::ByMove(std::move(cloned)))); - - std::unique_ptr upmm = std::move(mock); - Simulation s; - s.AddModel(upmm); - - const auto histories = s.GetModelSparseHistories(); - ASSERT_EQ(histories.size(), 1u); - const auto &history = histories[0].at("temp"); - std::vector expected_timesteps = {0, 2}; - ASSERT_EQ(history.GetRecordedTimesteps(), expected_timesteps); - ASSERT_EQ(history.GetRecordedStates().size(), 2u); - EXPECT_TRUE(history.GetRecordedStates()[0].isApprox(state0)); - EXPECT_TRUE(history.GetRecordedStates()[1].isApprox(state2)); -} +// TEST_F(SimulationTest, ConstructGetLogger) { +// Simulation s; +// ASSERT_EQ(s.GetLogName(), "console"); +// } + +// TEST_F(SimulationTest, GetSetModel) { +// auto mock = std::make_unique>(); +// auto cloned = std::make_unique>(); +// EXPECT_CALL(*mock, clone()) +// .WillOnce(Return(::testing::ByMove(std::move(cloned)))); + +// std::unique_ptr upmm = std::move(mock); +// Simulation s; +// s.AddModel(upmm); +// ASSERT_EQ(s.GetModels().size(), 1); +// } + +// TEST_F(SimulationTest, ClearModels) { +// auto mock = std::make_unique>(); +// auto cloned = std::make_unique>(); +// EXPECT_CALL(*mock, clone()) +// .WillOnce(Return(::testing::ByMove(std::move(cloned)))); + +// std::unique_ptr upmm = std::move(mock); +// Simulation s; +// s.AddModel(upmm); +// s.ClearModels(); +// ASSERT_EQ(s.GetModels().size(), 0); +// } + +// TEST_F(SimulationTest, GetModelNames) { +// auto mock = std::make_unique>(); +// auto cloned = std::make_unique>(); +// auto expected = "test_model_name"; +// EXPECT_CALL(*cloned, GetName()).WillOnce(Return(expected)); +// EXPECT_CALL(*mock, clone()) +// .WillOnce(Return(::testing::ByMove(std::move(cloned)))); + +// std::unique_ptr upmm = std::move(mock); +// Simulation s; +// s.AddModel(upmm); +// auto result = s.GetModelNames(); +// ASSERT_EQ(result.size(), 1); +// ASSERT_EQ(result[0], expected); +// } + +// TEST_F(SimulationTest, GetModelHistories) { +// auto mock = std::make_unique>(); +// auto cloned = std::make_unique>(); + +// std::map hv; +// History h("temp", "test_logger"); +// Eigen::VectorXd state = Eigen::VectorXd(3); +// state << 1.0f, 2.0f, 3.0f; +// h.AddState(state); +// hv["temp"] = h; + +// std::map> h_map; +// h_map["temp"] = h.GetStateAsVector(); + +// std::vector>> +// expected; expected.push_back(h_map); + +// ON_CALL(*cloned, GetName()).WillByDefault(Return("temp_model")); + +// EXPECT_CALL(*cloned, GetHistories()).WillOnce(Return(hv)); +// ON_CALL(*mock, clone()) +// .WillByDefault(Return(::testing::ByMove(std::move(cloned)))); + +// std::unique_ptr upmm = std::move(mock); +// Simulation s; +// s.AddModel(upmm); +// ASSERT_EQ(s.GetModelHistories(), expected); +// } + +// TEST_F(SimulationTest, GetHistoryNames) { +// auto mock = std::make_unique>(); +// auto cloned = std::make_unique>(); + +// std::string model_name = "temp_model"; +// std::string history_name = "temp_history"; + +// std::map hv; +// History h("temp", "test_logger"); +// hv[history_name] = h; + +// std::vector> expected = { +// {model_name, history_name}}; + +// ON_CALL(*cloned, GetName()).WillByDefault(Return(model_name)); + +// EXPECT_CALL(*cloned, GetHistories()).WillOnce(Return(hv)); +// ON_CALL(*mock, clone()) +// .WillByDefault(Return(::testing::ByMove(std::move(cloned)))); + +// std::unique_ptr upmm = std::move(mock); +// Simulation s; +// s.AddModel(upmm); +// ASSERT_EQ(s.GetModelHistoryNames(), expected); +// } + +// TEST_F(SimulationTest, GetModelSparseHistories) { +// auto mock = std::make_unique>(); +// auto cloned = std::make_unique>(); + +// std::map hv; +// History h("temp", "test_logger"); +// Eigen::VectorXd state0 = Eigen::VectorXd(2); +// state0 << 1.0f, 2.0f; +// Eigen::VectorXd state2 = Eigen::VectorXd(2); +// state2 << 3.0f, 4.0f; +// h.AddState(state0, 0); +// h.AddState(state2, 2); +// hv["temp"] = h; + +// EXPECT_CALL(*cloned, GetHistories()).WillOnce(Return(hv)); +// ON_CALL(*mock, clone()) +// .WillByDefault(Return(::testing::ByMove(std::move(cloned)))); + +// std::unique_ptr upmm = std::move(mock); +// Simulation s; +// s.AddModel(upmm); + +// const auto histories = s.GetModelSparseHistories(); +// ASSERT_EQ(histories.size(), 1u); +// const auto &history = histories[0].at("temp"); +// std::vector expected_timesteps = {0, 2}; +// ASSERT_EQ(history.GetRecordedTimesteps(), expected_timesteps); +// ASSERT_EQ(history.GetRecordedStates().size(), 2u); +// EXPECT_TRUE(history.GetRecordedStates()[0].isApprox(state0)); +// EXPECT_TRUE(history.GetRecordedStates()[1].isApprox(state2)); +// } } // namespace testing } // namespace respond \ No newline at end of file diff --git a/tests/unit/timestep_test.cpp b/tests/unit/timestep_test.cpp new file mode 100644 index 00000000..f81fa27f --- /dev/null +++ b/tests/unit/timestep_test.cpp @@ -0,0 +1,168 @@ +//////////////////////////////////////////////////////////////////////////////// +// File: timestep_test.cpp // +// Project: respond // +// Created Date: 2026-07-06 // +// Author: Matthew Carroll // +// ----- // +// Last Modified: 2026-07-07 // +// Modified By: Matthew Carroll // +// ----- // +// Copyright (c) 2026 Syndemics Lab at Boston Medical Center // +//////////////////////////////////////////////////////////////////////////////// + +#include + +#include +#include + +namespace respond::testing { + +class TimestepTest : public ::testing::Test { +public: +protected: + void SetUp() override { + // Clear any existing loggers from previous tests + spdlog::drop_all(); + + // Create temporary log files for testing + test_log_file_ = "/tmp/respond_test.log"; + shared_log_file_ = "/tmp/respond_shared.log"; + default_log_file_ = RESPOND_DEFAULT_LOG_FILE; + + // Remove test files if they exist + std::remove(default_log_file_.c_str()); + std::remove(test_log_file_.c_str()); + std::remove(shared_log_file_.c_str()); + } + void TearDown() override { + // Clean up loggers + spdlog::drop_all(); + + // Remove test files + std::remove(default_log_file_.c_str()); + std::remove(test_log_file_.c_str()); + std::remove(shared_log_file_.c_str()); + } + + std::string test_log_file_; + std::string shared_log_file_; + std::string default_log_file_; +}; + +TEST_F(TimestepTest, DefaultConstructor) { + Timestep ts; + ASSERT_EQ(CreateFileLogger(RESPOND_DEFAULT_LOG, ""), + CreationStatus::kExists); +} + +TEST_F(TimestepTest, DefaultConstructorWithLogName) { + std::string log_name = "temp"; + Timestep ts(log_name); + ASSERT_EQ(CreateFileLogger(log_name, ""), CreationStatus::kExists); +} + +TEST_F(TimestepTest, DefaultConstructorWithLogNameAndFile) { + std::string log_name = "temp2"; + Timestep ts(log_name, test_log_file_); + ASSERT_EQ(CreateFileLogger(log_name, test_log_file_), + CreationStatus::kExists); +} + +TEST_F(TimestepTest, CreateTransition) { + Timestep ts("test_log", test_log_file_); + const std::unique_ptr &transition = + ts.CreateTransition("migration"); + ASSERT_NE(transition, nullptr); + ASSERT_EQ(transition->GetName(), "migration"); +} + +TEST_F(TimestepTest, AddMatrixToTransitionByIndex) { + Timestep ts("test_log", test_log_file_); + const std::unique_ptr &transition = + ts.CreateTransition("migration"); + Eigen::MatrixXd m(2, 2); + m << 0.5, 0.5, 0.5, 0.5; + ts.AddMatrixToTransition(0, m); + ASSERT_EQ(transition->GetMatrices().size(), 1); + ASSERT_TRUE(transition->GetMatrices()[0].isApprox(m)); +} + +TEST_F(TimestepTest, AddMatrixToTransitionByName) { + Timestep ts("test_log", test_log_file_); + const std::unique_ptr &transition = + ts.CreateTransition("migration"); + Eigen::MatrixXd m(2, 2); + m << 0.5, 0.5, 0.5, 0.5; + ts.AddMatrixToTransition("migration", m); + ASSERT_EQ(transition->GetMatrices().size(), 1); + ASSERT_TRUE(transition->GetMatrices()[0].isApprox(m)); +} + +TEST_F(TimestepTest, GetTransitionByIndex) { + Timestep ts("test_log", test_log_file_); + const std::unique_ptr &transition = + ts.CreateTransition("migration"); + const std::unique_ptr &retrieved_transition = + ts.GetTransition(0); + ASSERT_EQ(retrieved_transition->GetName(), "migration"); +} + +TEST_F(TimestepTest, GetTransitionByName) { + Timestep ts("test_log", test_log_file_); + const std::unique_ptr &transition = + ts.CreateTransition("migration"); + const std::unique_ptr &retrieved_transition = + ts.GetTransition("migration"); + ASSERT_EQ(retrieved_transition->GetName(), "migration"); +} + +TEST_F(TimestepTest, GetTransitions) { + Timestep ts("test_log", test_log_file_); + ts.CreateTransition("migration"); + ts.CreateTransition("behavior"); + std::vector> transitions = ts.GetTransitions(); + ASSERT_EQ(transitions.size(), 2); + ASSERT_EQ(transitions[0]->GetName(), "migration"); + ASSERT_EQ(transitions[1]->GetName(), "behavior"); +} + +TEST_F(TimestepTest, GetTransitionNames) { + Timestep ts("test_log", test_log_file_); + ts.CreateTransition("migration"); + ts.CreateTransition("behavior"); + std::vector names = ts.GetTransitionNames(); + ASSERT_EQ(names.size(), 2); + ASSERT_EQ(names[0], "migration"); + ASSERT_EQ(names[1], "behavior"); +} + +TEST_F(TimestepTest, CopyConstructor) { + Timestep ts1("test_log", test_log_file_); + ts1.CreateTransition("migration"); + Timestep ts2(ts1); + std::vector names = ts2.GetTransitionNames(); + ASSERT_EQ(names.size(), 1); + ASSERT_EQ(names[0], "migration"); +} + +TEST_F(TimestepTest, CopyAssignment) { + Timestep ts1("test_log", test_log_file_); + ts1.CreateTransition("migration"); + Timestep ts2; + ts2 = ts1; + std::vector names = ts2.GetTransitionNames(); + ASSERT_EQ(names.size(), 1); + ASSERT_EQ(names[0], "migration"); +} + +TEST_F(TimestepTest, StreamOperatorOverload) { + Timestep ts("test_log", test_log_file_); + ts.CreateTransition("migration"); + std::stringstream ss; + ss << ts; + std::string output = ss.str(); + ASSERT_NE( + output.find("Timestep with the following transitions:\n - migration\n"), + std::string::npos); +} +} // namespace respond::testing \ No newline at end of file From c3ac4a303632f36d080ba00448e1078e1c70cea2 Mon Sep 17 00:00:00 2001 From: Matthew Carroll <28577806+MJC598@users.noreply.github.com> Date: Mon, 13 Jul 2026 12:54:35 -0400 Subject: [PATCH 3/4] [Feature] Intervention Ownership (#144) * uml and some basic constructors Updating public diagram updating UML reverting index and uml * thinking through Model updates * Making repo wide changes to match transition and model syntax so project compiles. Tests commented out, no expectation simulation works yet * Removing GetLogName() function from transitions and replacing with protected member. Additional background death tests * cleaning up imports and adding behavior tests * Adding protected functions to transition base to help with testing dimensions * overdose tests * simulation test update * intervention clone function testing * Adding integration tests * RemoveTransition function added * Addressing PR Comments #143 * Fixing logging test dependency * Ahhhhh * Addressing PR comments * fixing respond tests to have the correct final state --- extras/benchmark/src/benchmark_respond.cpp | 102 +++--- include/respond/constants.hpp | 1 + include/respond/model.hpp | 4 +- include/respond/respond.hpp | 3 +- include/respond/simulation.hpp | 183 ++++++--- include/respond/timestep.hpp | 17 +- include/respond/transition.hpp | 15 +- src/background.cpp | 30 +- src/behavior.cpp | 24 +- src/internals/background.hpp | 18 +- src/internals/behavior.hpp | 17 +- src/internals/intervention.hpp | 18 +- src/internals/markov.hpp | 17 +- src/internals/migration.hpp | 17 +- src/internals/overdose.hpp | 17 +- src/internals/transition_base.hpp | 100 ++++- src/intervention.cpp | 25 +- src/migration.cpp | 23 +- src/overdose.cpp | 42 +-- src/transition_factory.cpp | 16 +- tests/integration/respond_test.cpp | 245 ++++++------- tests/mocks/model_mock.hpp | 2 +- tests/mocks/transition_mock.hpp | 1 - tests/unit/background_test.cpp | 157 ++++++-- tests/unit/behavior_test.cpp | 122 ++++-- tests/unit/intervention_test.cpp | 171 +++++++-- tests/unit/logging_test.cpp | 4 +- tests/unit/markov_test.cpp | 18 +- tests/unit/migration_test.cpp | 125 +++++-- tests/unit/overdose_test.cpp | 230 ++++++++---- tests/unit/simulation_test.cpp | 407 ++++++++++++++------- tests/unit/timestep_test.cpp | 13 +- 32 files changed, 1473 insertions(+), 711 deletions(-) diff --git a/extras/benchmark/src/benchmark_respond.cpp b/extras/benchmark/src/benchmark_respond.cpp index 70520654..fbc49e51 100644 --- a/extras/benchmark/src/benchmark_respond.cpp +++ b/extras/benchmark/src/benchmark_respond.cpp @@ -4,12 +4,15 @@ // Created Date: 2026-04-27 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-07 // +// Last Modified: 2026-07-09 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // //////////////////////////////////////////////////////////////////////////////// +#include +#include + #include #include #include @@ -30,9 +33,6 @@ #include -#include -#include - namespace { using Clock = std::chrono::steady_clock; using Nanoseconds = std::chrono::duration; @@ -270,55 +270,65 @@ Eigen::VectorXd MakeRateVector(std::size_t n, double base_rate, return v; } -std::unique_ptr BuildModel(std::size_t state_size, - int history_capture_interval, - int final_timestep) { - auto model = respond::Model::Create("benchmark_model", "console"); - model->SetHistoryCaptureInterval(history_capture_interval); - model->SetFinalTimestep(final_timestep); - - auto behavior = respond::Transition::Create("behavior", "console"); - auto intervention = respond::Transition::Create("intervention", "console"); - auto overdose = respond::Transition::Create("overdose", "console"); - auto background = - respond::Transition::Create("background_death", "console"); - - if (!behavior || !intervention || !overdose || !background) { - throw std::runtime_error("Failed to create one or more transitions"); - } +respond::Timestep CreateTestTimestep(std::size_t state_size) { + + respond::Timestep ts; + ts.CreateTransition("migration"); + ts.AddMatrixToTransition("migration", + MakeRateVector(state_size, 10.0, 0.0)); + + ts.CreateTransition("behavior"); + ts.AddMatrixToTransition("behavior", MakeShiftMatrix(state_size, 0.985, 1)); + + auto temp = ts.GetTransition("behavior")->clone(); - behavior->AddMatrix(MakeShiftMatrix(state_size, 0.985, 1)); - intervention->AddMatrix(MakeShiftMatrix(state_size, 0.990, -1)); - overdose->AddMatrix(MakeRateVector(state_size, 0.0020, 0.0005)); - overdose->AddMatrix(MakeRateVector(state_size, 0.0800, 0.0200)); - background->AddMatrix(MakeRateVector(state_size, 0.0008, 0.0004)); + ts.CreateTransition("intervention"); + ts.AddMatrixToTransition("intervention", + MakeShiftMatrix(state_size, 0.990, -1)); - model->AddTransition(behavior); - model->AddTransition(intervention); - model->AddTransition(overdose); - model->AddTransition(background); + ts.CreateTransition("overdose"); + ts.AddMatrixToTransition("overdose", + MakeRateVector(state_size, 0.0020, 0.0005)); - return model; + ts.AddMatrixToTransition("overdose", + MakeRateVector(state_size, 0.0800, 0.0200)); + + ts.CreateTransition("background_death"); + ts.AddMatrixToTransition("background_death", + MakeRateVector(state_size, 0.0008, 0.0004)); + return ts; } -TimedRunResult TimeOneSample(respond::Model &model, +respond::Simulation BuildSimulation(std::size_t state_size, + int history_capture_interval, + size_t duration) { + respond::Simulation sim; + sim.CreateNewModel("markov"); + sim.GetModels()[0]->SetHistoryCaptureInterval(history_capture_interval); + sim.GetModels()[0]->SetFinalTimestep(duration); + auto timestep = CreateTestTimestep(state_size); + + for (size_t t = 0; t < duration; ++t) { + sim.GetModels()[0]->AddTimestep(timestep); + } + return sim; +} + +TimedRunResult TimeOneSample(respond::Simulation sim, const Eigen::VectorXd &initial_state, int steps) { - model.SetState(initial_state); - model.ClearHistories(); - model.SetFinalTimestep(steps); + sim.GetModels()[0]->SetState(initial_state); + sim.GetModels()[0]->CreateDefaultHistories(); const auto start = Clock::now(); - for (int i = 0; i < steps; ++i) { - model.RunTransitions(); - } + sim.Run(steps); const auto end = Clock::now(); - const double checksum = model.GetState().sum(); + const double checksum = sim.GetModels()[0]->GetState().sum(); std::size_t recorded_points = 0; - const auto histories = model.GetHistories(); + const auto histories = sim.GetModelHistories()[0]; const auto state_history = histories.find("state"); if (state_history != histories.end()) { - recorded_points = state_history->second.GetRecordedTimesteps().size(); + recorded_points = state_history->second.size(); } DoNotOptimize(checksum); ClobberMemory(); @@ -420,16 +430,16 @@ int main(int argc, char **argv) { for (int repetition = 0; repetition < config.repetitions; ++repetition) { - auto model = - BuildModel(config.state_size, config.history_capture_interval, - config.steps); + auto sim = + BuildSimulation(config.state_size, + config.history_capture_interval, config.steps); Eigen::VectorXd initial_state = Eigen::VectorXd::Constant( - static_cast(config.state_size), 1'000.0); + static_cast(config.state_size), 1000.0); for (int i = 0; i < config.warmup_iterations; ++i) { const auto warmup = - TimeOneSample(*model, initial_state, config.steps); + TimeOneSample(sim, initial_state, config.steps); DoNotOptimize(warmup.checksum); } @@ -439,7 +449,7 @@ int main(int argc, char **argv) { for (int i = 0; i < config.sample_iterations; ++i) { const auto sample = - TimeOneSample(*model, initial_state, config.steps); + TimeOneSample(sim, initial_state, config.steps); sample_ns.push_back(sample.elapsed_ns); all_samples_ns.push_back(sample.elapsed_ns); final_checksum = sample.checksum; diff --git a/include/respond/constants.hpp b/include/respond/constants.hpp index 97ba83d2..a00e8aaf 100644 --- a/include/respond/constants.hpp +++ b/include/respond/constants.hpp @@ -15,5 +15,6 @@ #define RESPOND_DEFAULT_LOG "respond" #define RESPOND_DEFAULT_LOG_FILE "respond.log" +#define RESPOND_DEFAULT_TRANSITION_NAME "transition" #endif \ No newline at end of file diff --git a/include/respond/model.hpp b/include/respond/model.hpp index f92ede7b..c4e07667 100644 --- a/include/respond/model.hpp +++ b/include/respond/model.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-07 // +// Last Modified: 2026-07-09 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -100,7 +100,7 @@ class Model { // //////////////////////////////////////////////////////////////////////////// - /// @brief Retrieve an immutable reference to a specific timestep by index. + /// @brief Retrieve a copy of a specific timestep by index. /// @param index The zero-based index of the timestep to retrieve. /// @return A constant reference to the Timestep at the specified index. virtual Timestep GetTimestepAtIndex(size_t index) const = 0; diff --git a/include/respond/respond.hpp b/include/respond/respond.hpp index 13c233b6..e0b20aad 100644 --- a/include/respond/respond.hpp +++ b/include/respond/respond.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-05-07 // +// Last Modified: 2026-07-09 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -18,7 +18,6 @@ #include #include #include -#include #include #endif // RESPOND_RESPOND_HPP_ \ No newline at end of file diff --git a/include/respond/simulation.hpp b/include/respond/simulation.hpp index 07adb3c8..deda1041 100644 --- a/include/respond/simulation.hpp +++ b/include/respond/simulation.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-07 // +// Last Modified: 2026-07-13 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -12,6 +12,11 @@ #ifndef RESPOND_SIMULATION_HPP_ #define RESPOND_SIMULATION_HPP_ +#include +#include +#include +#include + #include #include #include @@ -20,29 +25,30 @@ #include -#include -#include -#include - namespace respond { /// @brief Manages and executes multiple models in a coordinated simulation. /// A Simulation aggregates Model instances and coordinates their execution, /// maintaining history records and providing access to simulation results. class Simulation { public: - /// @brief Default constructor initializing with "console" logger. - Simulation() : Simulation("respond") {} + //////////////////////////////////////////////////////////////////////////// + // + // Rule of Five: Copy and Move Semantics + // + //////////////////////////////////////////////////////////////////////////// + + /// @brief Default constructor for a Simulation instance. + /// Initializes the simulation with the default logger. + Simulation() : Simulation(RESPOND_DEFAULT_LOG) {} /// @brief Constructs a Simulation with a specified logger. - /// @param log_name Name of the logger for this simulation (default: - /// "console"). + /// @param log_name The name of the logger to use for simulation output. Simulation(const std::string &log_name) - : Simulation(log_name, log_name + ".log") {} + : Simulation(log_name, RESPOND_DEFAULT_LOG_FILE) {} - /// @brief Constructs a Simulation with a specified logger and log file - /// path. - /// @param log_name - /// @param log_filepath + /// @brief Constructs a Simulation with a specified logger and log file. + /// @param log_name The name of the logger to use for simulation output. + /// @param log_filepath The file path for the logger output. Simulation(const std::string &log_name, const std::string &log_filepath) : _log_name(log_name) { CreateFileLogger(log_name, log_filepath); @@ -51,19 +57,101 @@ class Simulation { /// @brief Virtual destructor for polymorphic cleanup. ~Simulation() = default; + /// @brief Copy constructor creating an independent deep copy of the + /// simulation. All models are cloned; modifications to the copy do not + /// affect the original. + /// @param other The Simulation instance to copy from. + Simulation(const Simulation &other) { + _log_name = other._log_name; + for (const auto &m : other._models) { + _models.push_back(m->clone()); + } + _duration = other._duration; + _parameter_change_times = other._parameter_change_times; + _stratify_entering_cohort = other._stratify_entering_cohort; + _build_summary_stats = other._build_summary_stats; + _save_state_history = other._save_state_history; + _timesteps_to_report = other._timesteps_to_report; + _pivot_long = other._pivot_long; + } + + /// @brief Copy assignment operator for deep copying simulation state. + /// @param other The simulation to copy from. + /// @return Reference to this simulation after assignment. + Simulation &operator=(const Simulation &other) { + if (this != &other) { + _log_name = other._log_name; + _models.clear(); + for (const auto &m : other._models) { + _models.push_back(m->clone()); + } + _duration = other._duration; + _parameter_change_times = other._parameter_change_times; + _stratify_entering_cohort = other._stratify_entering_cohort; + _build_summary_stats = other._build_summary_stats; + _save_state_history = other._save_state_history; + _timesteps_to_report = other._timesteps_to_report; + _pivot_long = other._pivot_long; + } + return *this; + } + + /// @brief Move constructor for transferring simulation ownership. + /// @param other The simulation to move from. + Simulation(Simulation &&other) noexcept + : _log_name(std::move(other._log_name)), _duration(other._duration), + _parameter_change_times(std::move(other._parameter_change_times)), + _stratify_entering_cohort(other._stratify_entering_cohort), + _build_summary_stats(other._build_summary_stats), + _save_state_history(other._save_state_history), + _timesteps_to_report(std::move(other._timesteps_to_report)), + _pivot_long(other._pivot_long) { + for (const auto &m : other._models) { + _models.push_back(m->clone()); + } + other._models.clear(); + } + + /// @brief Move assignment operator for transferring simulation ownership. + /// @param other The simulation to move from. + /// @return Reference to this simulation after assignment. + Simulation &operator=(Simulation &&other) noexcept { + if (this != &other) { + _log_name = std::move(other._log_name); + _duration = other._duration; + _parameter_change_times = std::move(other._parameter_change_times); + _stratify_entering_cohort = other._stratify_entering_cohort; + _build_summary_stats = other._build_summary_stats; + _save_state_history = other._save_state_history; + _timesteps_to_report = std::move(other._timesteps_to_report); + _pivot_long = other._pivot_long; + + for (const auto &m : other._models) { + _models.push_back(m->clone()); + } + other._models.clear(); + } + return *this; + } + + //////////////////////////////////////////////////////////////////////////// + // + // Simulation Behavior Methods: Model Management + // + //////////////////////////////////////////////////////////////////////////// + + /// @brief Creates a new model instance and adds it to the simulation. + /// @param model_name The name identifier for the model to create. This name + /// is used to identify the model type and initialize it accordingly. + /// @return The unique identifier for the newly created model, combining its + /// index and name. const std::string CreateNewModel(const std::string &model_name) { _models.push_back(Model::Create(model_name, _log_name)); return std::to_string(_models.size()) + "_" + _models.back()->GetName(); } - /// @brief Executes one step of the simulation for all models. - /// Calls RunTransitions() on each registered model in sequence. - void Run() { - for (const auto &model : _models) { - model->SetFinalTimestep(_duration); - // model->RunTransitions(); - } - } + /// @brief Removes all models from the simulation. + void ClearModels() { _models.clear(); } /// @brief Adds a model to the simulation. /// The model is cloned and managed by the simulation. @@ -74,6 +162,26 @@ class Simulation { _models.push_back(model->clone()); } + /// @brief Executes one step of the simulation for all models. + /// Calls RunTransitions() on each registered model in sequence. + void Run(int duration = -1) { + if (duration > 0) { + _duration = duration; + } + LogInfo(_log_name, "Running simulation for duration of " + + std::to_string(_duration) + " timesteps."); + for (const auto &model : _models) { + model->SetFinalTimestep(_duration); + model->RunTimesteps(); + } + } + + //////////////////////////////////////////////////////////////////////////// + // + // Getters and Setters for Transitions and Metadata + // + //////////////////////////////////////////////////////////////////////////// + /// @brief Retrieves all models in the simulation. /// @return Const reference to the vector of Model unique_ptrs. const std::vector> &GetModels() const { @@ -90,9 +198,6 @@ class Simulation { return ret; } - /// @brief Removes all models from the simulation. - void ClearModels() { _models.clear(); } - /// @brief Retrieves the complete state histories for all models. /// @return Vector of maps (one per model) mapping history names to state /// vector trajectories. @@ -138,33 +243,7 @@ class Simulation { return ret; } - /// @brief Retrieves the logger name used by this simulation. - /// @return The name of the associated logger. - std::string GetLogName() const { return _log_name; } - - /// @brief Copy constructor creating an independent deep copy of the - /// simulation. All models are cloned; modifications to the copy do not - /// affect the original. - Simulation(const Simulation &other) : _log_name(other.GetLogName()) { - ClearModels(); - for (const auto &m : other.GetModels()) { - _models.push_back(m->clone()); - } - } - - /// @brief Copy assignment operator for deep copying simulation state. - /// @param other The simulation to copy from. - /// @return Reference to this simulation after assignment. - Simulation &operator=(const Simulation &other) { - if (this != &other) { - ClearModels(); - _log_name = other.GetLogName(); - for (const auto &m : other.GetModels()) { - _models.push_back(m->clone()); - } - } - return *this; - } + void SetDuration(int duration) { _duration = duration; } private: std::string _log_name; diff --git a/include/respond/timestep.hpp b/include/respond/timestep.hpp index 4a006613..80881bfd 100644 --- a/include/respond/timestep.hpp +++ b/include/respond/timestep.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-06-30 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-07 // +// Last Modified: 2026-07-09 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -121,10 +121,23 @@ class Timestep { /// exception if the transition type is unsupported. const std::unique_ptr & CreateTransition(const std::string &transition_name) { - _transitions.push_back(Transition::Create(transition_name, _log_name)); + _transitions.push_back( + Transition::Create(transition_name, transition_name, _log_name)); return _transitions.back(); } + std::unique_ptr RemoveTransition(size_t idx) { + if (idx >= _transitions.size()) { + LogWarning(_log_name, "Index out of range in RemoveTransition: " + + std::to_string(idx)); + throw std::out_of_range( + "Error attempting to RemoveTransition by index."); + } + auto removed_transition = std::move(_transitions[idx]); + _transitions.erase(_transitions.begin() + idx); + return removed_transition; + } + /// @brief Adds a matrix to an existing transition in this timestep by /// index. /// @param idx The index of the transition to which the matrix will be diff --git a/include/respond/transition.hpp b/include/respond/transition.hpp index 19d74171..309427e1 100644 --- a/include/respond/transition.hpp +++ b/include/respond/transition.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-02 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-06 // +// Last Modified: 2026-07-08 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -16,9 +16,11 @@ #include #include #include +#include #include +#include #include namespace respond { @@ -60,10 +62,6 @@ class Transition { /// @brief Clears all stored transition matrices. virtual void ClearMatrices() = 0; - /// @brief Retrieves the logger name used by this transition. - /// @return The associated logger's name. - virtual std::string GetLogName() const = 0; - /// @brief Deleted copy constructor (transitions are non-copyable by public /// API). Transition(const Transition &) = delete; @@ -86,8 +84,11 @@ class Transition { /// @param log_name The logger name for error reporting (e.g., "console"). /// @return A unique_ptr to the created Transition, or nullptr if type is /// unsupported. - static std::unique_ptr Create(const std::string &type, - const std::string &log_name); + static std::unique_ptr + Create(const std::string &type, + const std::string &name = RESPOND_DEFAULT_TRANSITION_NAME, + const std::string &log_name = RESPOND_DEFAULT_LOG, + const std::string &log_file = RESPOND_DEFAULT_LOG_FILE); protected: /// @brief Protected default constructor for subclass initialization. diff --git a/src/background.cpp b/src/background.cpp index b9f2befa..7bdfde7e 100644 --- a/src/background.cpp +++ b/src/background.cpp @@ -4,44 +4,32 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-07 // +// Last Modified: 2026-07-09 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // //////////////////////////////////////////////////////////////////////////////// -#include "internals/background.hpp" +#include #include #include -#include -#include +#include + +#include "internals/background.hpp" namespace respond { Eigen::VectorXd BackgroundDeath::Execute(const Eigen::Ref &state, std::map &h) const { - if (GetMatrices().size() != 1) { - std::string error_msg = - "Background death error: Expected 1 transition matrix, got " + - std::to_string(GetMatrices().size()); - LogError(GetLogName(), error_msg); - throw std::runtime_error(error_msg); - } - auto deaths = state.cwiseProduct(GetMatrices()[0]); // calculate the deaths + TestCorrectNumberMatrices(1); + TestMatrixSizes(state, GetMatrices()[0]); + Eigen::VectorXd deaths = state.cwiseProduct(GetMatrices()[0]); + TestLessThanState(state, deaths); if (h.find("background_death") != h.end()) { h["background_death"].AccumulateState(deaths); } - if (!(state.array() >= deaths.array()).all()) { - std::string error_msg = - "Background death error: State values are less than estimated " - "deaths. " + - std::to_string((state.array() < deaths.array()).count()) + - " elements affected"; - LogError(GetLogName(), error_msg); - throw std::runtime_error(error_msg); - } auto new_state = state - deaths; // remove deaths from state return new_state; } diff --git a/src/behavior.cpp b/src/behavior.cpp index 6eb43d02..7ec865a0 100644 --- a/src/behavior.cpp +++ b/src/behavior.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-07 // +// Last Modified: 2026-07-08 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -13,6 +13,7 @@ #include "internals/behavior.hpp" #include +#include #include #include @@ -22,24 +23,9 @@ namespace respond { Eigen::VectorXd Behavior::Execute(const Eigen::Ref &state, std::map &h) const { - if (GetMatrices().size() != 1) { - std::string error_msg = - "Behavior error: Expected 1 transition matrix, got " + - std::to_string(GetMatrices().size()); - LogError(GetLogName(), error_msg); - throw std::runtime_error(error_msg); - } - if (state.rows() != GetMatrices()[0].cols()) { - std::stringstream ss; - ss << "Behavior error: State dimension mismatch. State size is (" - << state.rows() << ", " << state.cols() - << ") but transition matrix expects (" << GetMatrices()[0].rows() - << ", " << GetMatrices()[0].cols() << ")"; - std::string error_msg = ss.str(); - LogError(GetLogName(), error_msg); - throw std::runtime_error(error_msg); - } - auto new_state = GetMatrices()[0] * state; + TestCorrectNumberMatrices(1); + TestRowColDimensions(state, GetMatrices()[0]); + Eigen::VectorXd new_state = GetMatrices()[0] * state; return new_state; } } // namespace respond \ No newline at end of file diff --git a/src/internals/background.hpp b/src/internals/background.hpp index 28cbf822..2db92b28 100644 --- a/src/internals/background.hpp +++ b/src/internals/background.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-02 // +// Last Modified: 2026-07-09 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -12,15 +12,27 @@ #ifndef RESPOND_INTERNALS_BACKGROUND_HPP_ #define RESPOND_INTERNALS_BACKGROUND_HPP_ +#include + +#include #include +#include + +#include #include "transition_base.hpp" namespace respond { class BackgroundDeath : public virtual TransitionBase { public: + BackgroundDeath() : BackgroundDeath("background_death") {} + BackgroundDeath(const std::string &name) + : BackgroundDeath(name, RESPOND_DEFAULT_LOG) {} BackgroundDeath(const std::string &name, const std::string &log_name) - : TransitionBase(name, log_name) {} + : BackgroundDeath(name, log_name, RESPOND_DEFAULT_LOG_FILE) {} + BackgroundDeath(const std::string &name, const std::string &log_name, + const std::string &log_file) + : TransitionBase(name, log_name, log_file) {} // Run the execute function and return the final state. Do not edit the // parameter state, but do edit the history provided. Nothing in the @@ -30,7 +42,7 @@ class BackgroundDeath : public virtual TransitionBase { // Clone std::unique_ptr clone() const override { - auto ret = std::make_unique(GetName(), GetLogName()); + auto ret = std::make_unique(GetName(), _log_name); for (const auto &t : GetMatrices()) { ret->AddMatrix(t); } diff --git a/src/internals/behavior.hpp b/src/internals/behavior.hpp index 44d2b4d5..54dd1ba3 100644 --- a/src/internals/behavior.hpp +++ b/src/internals/behavior.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-02 // +// Last Modified: 2026-07-09 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -12,15 +12,26 @@ #ifndef RESPOND_INTERNALS_BEHAVIOR_HPP_ #define RESPOND_INTERNALS_BEHAVIOR_HPP_ +#include + +#include #include +#include + +#include #include "transition_base.hpp" namespace respond { class Behavior : public virtual TransitionBase { public: + Behavior() : Behavior("behavior") {} + Behavior(const std::string &name) : Behavior(name, RESPOND_DEFAULT_LOG) {} Behavior(const std::string &name, const std::string &log_name) - : TransitionBase(name, log_name) {} + : Behavior(name, log_name, RESPOND_DEFAULT_LOG_FILE) {} + Behavior(const std::string &name, const std::string &log_name, + const std::string &log_file) + : TransitionBase(name, log_name, log_file) {} // Run the execute function and return the final state. Do not edit the // parameter state, but do edit the history provided. Nothing in the @@ -30,7 +41,7 @@ class Behavior : public virtual TransitionBase { // Clone std::unique_ptr clone() const override { - auto ret = std::make_unique(GetName(), GetLogName()); + auto ret = std::make_unique(GetName(), _log_name); for (const auto &t : GetMatrices()) { ret->AddMatrix(t); } diff --git a/src/internals/intervention.hpp b/src/internals/intervention.hpp index 6fd9a908..4e748e2a 100644 --- a/src/internals/intervention.hpp +++ b/src/internals/intervention.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-02 // +// Last Modified: 2026-07-09 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -12,15 +12,27 @@ #ifndef RESPOND_INTERNALS_INTERVENTION_HPP_ #define RESPOND_INTERNALS_INTERVENTION_HPP_ +#include + +#include #include +#include + +#include #include "transition_base.hpp" namespace respond { class Intervention : public virtual TransitionBase { public: + Intervention() : Intervention("intervention") {} + Intervention(const std::string &name) + : Intervention(name, RESPOND_DEFAULT_LOG) {} Intervention(const std::string &name, const std::string &log_name) - : TransitionBase(name, log_name) {} + : Intervention(name, log_name, RESPOND_DEFAULT_LOG_FILE) {} + Intervention(const std::string &name, const std::string &log_name, + const std::string &log_file) + : TransitionBase(name, log_name, log_file) {} // Run the execute function and return the final state. Do not edit the // parameter state, but do edit the history provided. Nothing in the @@ -30,7 +42,7 @@ class Intervention : public virtual TransitionBase { // Clone std::unique_ptr clone() const override { - auto ret = std::make_unique(GetName(), GetLogName()); + auto ret = std::make_unique(GetName(), _log_name); for (const auto &t : GetMatrices()) { ret->AddMatrix(t); } diff --git a/src/internals/markov.hpp b/src/internals/markov.hpp index 2ae830b7..be9dfe8e 100644 --- a/src/internals/markov.hpp +++ b/src/internals/markov.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-07 // +// Last Modified: 2026-07-09 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -33,8 +33,7 @@ class Markov : public virtual Model { // //////////////////////////////////////////////////////////////////////////// - /// @brief Default constructor for Markov model. Initializes with default - /// name "markov" and logger "console". + /// @brief Default constructor for Markov model. Initializes with default.. Markov() : Markov("markov", RESPOND_DEFAULT_LOG) {} /// @brief Constructs a Markov model with specified name and logger. @@ -219,7 +218,17 @@ class Markov : public virtual Model { if (!_initial_history_recorded) { RecordHistoryAtCurrentTimestep(); } - for (size_t i = 0; i < _timestep_vector.size(); ++i) { + size_t duration = _timestep_vector.size(); + if (_timestep_vector.size() > static_cast(_final_timestep) && + _final_timestep >= 0) { + std::string warning_msg = + "Duration is less than available timesteps for model: " + + _name + ".\nOnly running timesteps up to duration value."; + LogWarning(_log_name, warning_msg); + duration = static_cast(_final_timestep); + } + + for (size_t i = 0; i < duration; ++i) { RunTimestep(); RecordHistoryAtCurrentTimestep(); } diff --git a/src/internals/migration.hpp b/src/internals/migration.hpp index b7d37c4a..63245977 100644 --- a/src/internals/migration.hpp +++ b/src/internals/migration.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-02 // +// Last Modified: 2026-07-09 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -12,15 +12,26 @@ #ifndef RESPOND_INTERNALS_MIGRATION_HPP_ #define RESPOND_INTERNALS_MIGRATION_HPP_ +#include + +#include #include +#include + +#include #include "transition_base.hpp" namespace respond { class Migration : public virtual TransitionBase { public: + Migration() : Migration("migration") {} + Migration(const std::string &name) : Migration(name, RESPOND_DEFAULT_LOG) {} Migration(const std::string &name, const std::string &log_name) - : TransitionBase(name, log_name) {} + : Migration(name, log_name, RESPOND_DEFAULT_LOG_FILE) {} + Migration(const std::string &name, const std::string &log_name, + const std::string &log_file) + : TransitionBase(name, log_name, log_file) {} // Run the execute function and return the final state. Do not edit the // parameter state, but do edit the history provided. Nothing in the @@ -30,7 +41,7 @@ class Migration : public virtual TransitionBase { // Clone std::unique_ptr clone() const override { - auto ret = std::make_unique(GetName(), GetLogName()); + auto ret = std::make_unique(GetName(), _log_name); for (const auto &t : GetMatrices()) { ret->AddMatrix(t); } diff --git a/src/internals/overdose.hpp b/src/internals/overdose.hpp index 4e446146..c6236b78 100644 --- a/src/internals/overdose.hpp +++ b/src/internals/overdose.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-02 // +// Last Modified: 2026-07-09 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -12,15 +12,26 @@ #ifndef RESPOND_INTERNALS_OVERDOSE_HPP_ #define RESPOND_INTERNALS_OVERDOSE_HPP_ +#include + +#include #include +#include + +#include #include "transition_base.hpp" namespace respond { class Overdose : public virtual TransitionBase { public: + Overdose() : Overdose("overdose") {} + Overdose(const std::string &name) : Overdose(name, RESPOND_DEFAULT_LOG) {} Overdose(const std::string &name, const std::string &log_name) - : TransitionBase(name, log_name) {} + : Overdose(name, log_name, RESPOND_DEFAULT_LOG_FILE) {} + Overdose(const std::string &name, const std::string &log_name, + const std::string &log_file) + : TransitionBase(name, log_name, log_file) {} // Run the execute function and return the final state. Do not edit the // parameter state, but do edit the history provided. Nothing in the @@ -30,7 +41,7 @@ class Overdose : public virtual TransitionBase { // Clone std::unique_ptr clone() const override { - auto ret = std::make_unique(GetName(), GetLogName()); + auto ret = std::make_unique(GetName(), _log_name); for (const auto &t : GetMatrices()) { ret->AddMatrix(t); } diff --git a/src/internals/transition_base.hpp b/src/internals/transition_base.hpp index f9e15547..9cb5a857 100644 --- a/src/internals/transition_base.hpp +++ b/src/internals/transition_base.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-06 // +// Last Modified: 2026-07-08 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -12,14 +12,23 @@ #ifndef RESPOND_INTERNALS_TRANSITION_BASE_HPP_ #define RESPOND_INTERNALS_TRANSITION_BASE_HPP_ +#include #include +#include +#include + +#include + namespace respond { class TransitionBase : public virtual Transition { public: - TransitionBase(const std::string &name, const std::string &log_name) - : _name(name), _log_name(log_name) {} + TransitionBase(const std::string &name, const std::string &log_name, + const std::string &log_file) + : _name(name), _log_name(log_name) { + CreateFileLogger(log_name, log_file); + } virtual ~TransitionBase() = default; // Add a Transition Matrix to the set. We have no need to edit it once it's // been added, just use it. Thus, we don't need full ownership (reference) @@ -37,11 +46,92 @@ class TransitionBase : public virtual Transition { // Clear out all the stored Eigen::MatrixXd values void ClearMatrices() override { _transition_matrices.clear(); } - std::string GetLogName() const override { return _log_name; } +protected: + const std::string _log_name; + + void TestMatrixSizes(const Eigen::Ref &m1, + const Eigen::Ref &m2) const { + if (m1.size() != m2.size()) { + std::string error_msg = "Transition error - matrix size mismatch. " + "Matrix 1 size is (" + + std::to_string(m1.rows()) + ", " + + std::to_string(m1.cols()) + + ") but Matrix 2 " + "size is (" + + std::to_string(m2.rows()) + ", " + + std::to_string(m2.cols()) + ")"; + LogError(_log_name, error_msg); + throw std::runtime_error(error_msg); + } + } + + void TestSquareMatrix(const Eigen::Ref &m) const { + if (m.rows() != m.cols()) { + std::string error_msg = "Transition error - matrix is not " + "square. Matrix size is (" + + std::to_string(m.rows()) + ", " + + std::to_string(m.cols()) + ")"; + LogError(_log_name, error_msg); + throw std::runtime_error(error_msg); + } + } + + void + TestRowColDimensions(const Eigen::Ref &m1, + const Eigen::Ref &m2) const { + if (m1.rows() != m2.cols()) { + std::stringstream ss; + ss << "Transition error - Dimension mismatch, m1 rows do not match " + "m2 columns. m1 size is (" + << m1.rows() << ", " << m1.cols() << ") m2 size is (" + << m2.rows() << ", " << m2.cols() << ")"; + std::string error_msg = ss.str(); + LogError(_log_name, error_msg); + throw std::runtime_error(error_msg); + } + } + + void + TestColRowDimensions(const Eigen::Ref &m1, + const Eigen::Ref &m2) const { + if (m1.cols() != m2.rows()) { + std::stringstream ss; + ss << "Transition error - Dimension mismatch, m1 columns do not " + "match m2 rows. m1 size is (" + << m1.rows() << ", " << m1.cols() << ") m2 size is (" + << m2.rows() << ", " << m2.cols() << ")"; + std::string error_msg = ss.str(); + LogError(_log_name, error_msg); + throw std::runtime_error(error_msg); + } + } + + void TestCorrectNumberMatrices(const size_t &expected = 1) const { + if (_transition_matrices.size() != expected) { + std::string error_msg = + "Transition error - Wrong number of matrices. Expected " + + std::to_string(expected) + " transition matrix, got " + + std::to_string(_transition_matrices.size()); + LogError(_log_name, error_msg); + throw std::runtime_error(error_msg); + } + } + + void TestLessThanState(const Eigen::Ref &state, + const Eigen::Ref &m1) const { + if (!(state.array() >= m1.array()).all()) { + std::string error_msg = + "Transition error - State contains values less than m1! " + + std::to_string((state.array() < m1.array()).count()) + + " elements affected. Verify that the transition matrix is " + "correct and that the state vector is valid."; + LogError(_log_name, error_msg); + throw std::runtime_error(error_msg); + } + } private: std::string _name; - std::string _log_name; std::vector> _transition_matrices; }; diff --git a/src/intervention.cpp b/src/intervention.cpp index ec64cbb8..17af5f12 100644 --- a/src/intervention.cpp +++ b/src/intervention.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-07 // +// Last Modified: 2026-07-08 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -22,26 +22,13 @@ namespace respond { Eigen::VectorXd Intervention::Execute(const Eigen::Ref &state, std::map &h) const { - if (GetMatrices().size() != 1) { - std::string error_msg = - "Intervention error: Expected 1 transition matrix, got " + - std::to_string(GetMatrices().size()); - LogError(GetLogName(), error_msg); - throw std::runtime_error(error_msg); - } + TestCorrectNumberMatrices(1); + auto trans_matrix = GetMatrices()[0]; + TestSquareMatrix(trans_matrix); Eigen::VectorXd zero_matrix = Eigen::VectorXd::Zero(state.size()); - if (state.rows() != GetMatrices()[0].cols()) { - std::stringstream ss; - ss << "Intervention error: State dimension mismatch. State size is (" - << state.rows() << ", " << state.cols() - << ") but transition matrix expects (" << GetMatrices()[0].rows() - << ", " << GetMatrices()[0].cols() << ")"; - std::string error_msg = ss.str(); - LogError(GetLogName(), error_msg); - throw std::runtime_error(error_msg); - } - auto moved = GetMatrices()[0] * state; + TestRowColDimensions(state, trans_matrix); + Eigen::VectorXd moved = trans_matrix * state; // Add intervention_admissions to history if avaliable Eigen::VectorXd admissions = moved - state; diff --git a/src/migration.cpp b/src/migration.cpp index 3a6aca29..22999090 100644 --- a/src/migration.cpp +++ b/src/migration.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-07 // +// Last Modified: 2026-07-08 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -22,23 +22,10 @@ namespace respond { Eigen::VectorXd Migration::Execute(const Eigen::Ref &state, std::map &h) const { - if (GetMatrices().size() != 1) { - std::string error_msg = - "Migration error: Expected 1 transition matrix, got " + - std::to_string(GetMatrices().size()); - LogError(GetLogName(), error_msg); - throw std::runtime_error(error_msg); - } - if (state.size() != GetMatrices()[0].size()) { - std::string error_msg = "Migration error: State size (" + - std::to_string(state.size()) + - ") does not match transition matrix size (" + - std::to_string(GetMatrices()[0].size()) + ")"; - LogError(GetLogName(), error_msg); - throw std::runtime_error(error_msg); - } - auto subtracted = state + GetMatrices()[0]; - auto zero_stop = subtracted.array().max( + TestCorrectNumberMatrices(1); + TestMatrixSizes(state, GetMatrices()[0]); + Eigen::VectorXd subtracted = state + GetMatrices()[0]; + Eigen::VectorXd zero_stop = subtracted.array().max( Eigen::VectorXd::Zero(subtracted.size()).array()); return zero_stop; } diff --git a/src/overdose.cpp b/src/overdose.cpp index bc700a41..0dabaeae 100644 --- a/src/overdose.cpp +++ b/src/overdose.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-07 // +// Last Modified: 2026-07-08 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -22,50 +22,20 @@ namespace respond { Eigen::VectorXd Overdose::Execute(const Eigen::Ref &state, std::map &h) const { - if (GetMatrices().size() != 2) { - std::string error_msg = - "Overdose error: Expected 2 transition matrices, got " + - std::to_string(GetMatrices().size()); - LogError(GetLogName(), error_msg); - throw std::runtime_error(error_msg); - } + TestCorrectNumberMatrices(2); - if (state.size() != GetMatrices()[0].size()) { - std::string error_msg = "Overdose error: State size (" + - std::to_string(state.size()) + - ") does not match transition matrix size (" + - std::to_string(GetMatrices()[0].size()) + ")"; - LogError(GetLogName(), error_msg); - throw std::runtime_error(error_msg); - } - Eigen::VectorXd overdoses = - state.cwiseProduct(GetMatrices()[0]); // overdose - // Add total overdoses to stamp + TestMatrixSizes(state, GetMatrices()[0]); + Eigen::VectorXd overdoses = state.cwiseProduct(GetMatrices()[0]); if (h.find("total_overdose") != h.end()) { h["total_overdose"].AccumulateState(overdoses); } - if (overdoses.size() != GetMatrices()[1].size()) { - std::string error_msg = "Overdose error: Fatal overdose vector size (" + - std::to_string(overdoses.size()) + - ") does not match transition matrix size (" + - std::to_string(GetMatrices()[1].size()) + ")"; - LogError(GetLogName(), error_msg); - throw std::runtime_error(error_msg); - } + TestMatrixSizes(overdoses, GetMatrices()[1]); auto fods = overdoses.cwiseProduct(GetMatrices()[1]); // negatives if (h.find("fatal_overdose") != h.end()) { h["fatal_overdose"].AccumulateState(fods); } - if (!(state.array() >= fods.array()).all()) { - std::string error_msg = - "Overdose error: State values are less than estimated fatal " - "overdoses. " + - std::to_string((state.array() < fods.array()).count()) + - " elements affected"; - LogError(GetLogName(), error_msg); - throw std::runtime_error(error_msg); - } + TestLessThanState(state, fods); auto new_state = state - fods; // remove fods from state return new_state; } diff --git a/src/transition_factory.cpp b/src/transition_factory.cpp index 60b5ef72..52dc8d4b 100644 --- a/src/transition_factory.cpp +++ b/src/transition_factory.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-02 // +// Last Modified: 2026-07-07 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -25,21 +25,23 @@ namespace respond { std::unique_ptr Transition::Create(const std::string &type, - const std::string &log_name) { + const std::string &name, + const std::string &log_name, + const std::string &log_file) { std::string type_copy = type; std::transform(type_copy.begin(), type_copy.end(), type_copy.begin(), [](unsigned char c) { return std::tolower(c); }); if (type_copy == "migration") { - return std::make_unique(type, log_name); + return std::make_unique(name, log_name, log_file); } else if (type_copy == "behavior") { - return std::make_unique(type, log_name); + return std::make_unique(name, log_name, log_file); } else if (type_copy == "intervention") { - return std::make_unique(type, log_name); + return std::make_unique(name, log_name, log_file); } else if (type_copy == "overdose") { - return std::make_unique(type, log_name); + return std::make_unique(name, log_name, log_file); } else if (type_copy == "background_death") { - return std::make_unique(type, log_name); + return std::make_unique(name, log_name, log_file); } // Invalid transition type diff --git a/tests/integration/respond_test.cpp b/tests/integration/respond_test.cpp index bc6ec4a3..c206741f 100644 --- a/tests/integration/respond_test.cpp +++ b/tests/integration/respond_test.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-07 // +// Last Modified: 2026-07-13 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -15,21 +15,14 @@ #include #include +#include namespace respond { namespace testing { -std::unique_ptr MakeTestTransition(const std::string &name, - Eigen::MatrixXd matrix) { - auto migr = Transition::Create(name, "test_log"); - migr->AddMatrix(matrix); - return migr; -} - class RespondTest : public ::testing::Test { public: - std::unique_ptr markov; - std::unique_ptr transition; + Simulation sim; Eigen::Vector3d init_state; Eigen::Vector3d migration_pop; Eigen::Matrix3d intervention_trans; @@ -37,162 +30,156 @@ class RespondTest : public ::testing::Test { Eigen::Vector3d overdose_prob; Eigen::Vector3d fod_prob; Eigen::Vector3d background_death_prob; + Eigen::Vector3d tolerance; protected: void SetUp() override { - markov = Model::Create("markov", "test_logger"); + // Clear any existing loggers from previous tests + spdlog::drop_all(); + + // Create temporary log files for testing + default_log_file_ = RESPOND_DEFAULT_LOG_FILE; + + // Remove test files if they exist + std::remove(default_log_file_.c_str()); + init_state << 1.3f, 1.1f, 1.8f; migration_pop << 0.0f, 0.0f, 0.0f; + behavior_trans << 0.3f, 0.2f, 0.1f, 0.4f, 0.2f, 0.1f, 0.3f, 0.4f, 0.1f; intervention_trans << 0.1f, 0.2f, 0.5f, 0.3f, 0.2f, 0.3f, 0.7f, 0.2f, 0.3f; - behavior_trans << 0.3f, 0.2f, 0.1f, 0.4f, 0.2f, 0.1f, 0.3f, 0.4f, 0.1f; overdose_prob << 0.01f, 0.01f, 0.02f; fod_prob << 0.01f, 0.01f, 0.01f; background_death_prob << 0.001f, 0.001f, 0.002f; - } - void TearDown() override { markov.reset(); } -}; -// TEST_F(RespondTest, RunTransitionsInModel) { -// markov->SetState(init_state); + tolerance << 1e-5, 1e-5, 1e-5; -// auto migr = MakeTestTransition("migration", migration_pop); -// markov->AddTransition(migr); - -// auto beha = MakeTestTransition("behavior", behavior_trans); -// markov->AddTransition(beha); + sim.CreateNewModel("markov"); + sim.GetModels()[0]->CreateDefaultHistories(); + sim.GetModels()[0]->SetState(init_state); + } + void TearDown() override { + // Clean up loggers + spdlog::drop_all(); -// auto inte = MakeTestTransition("intervention", intervention_trans); -// markov->AddTransition(inte); + // Remove test files + std::remove(default_log_file_.c_str()); + } -// auto over = MakeTestTransition("overdose", overdose_prob); -// over->AddMatrix(fod_prob); -// markov->AddTransition(over); + std::string default_log_file_; -// auto back = MakeTestTransition("background_death", -// background_death_prob); markov->AddTransition(back); + Timestep CreateTestTimestep() { -// markov->RunTransitions(); + Timestep ts; + ts.CreateTransition("migration"); + ts.AddMatrixToTransition("migration", migration_pop); -// auto t_names = markov->GetTransitionNames(); -// std::vector expected = {"migration", "behavior", -// "intervention", "overdose", -// "background_death"}; -// ASSERT_EQ(t_names, expected); + ts.CreateTransition("behavior"); + ts.AddMatrixToTransition("behavior", behavior_trans); -// Eigen::Vector3d final_state; -// final_state << 0.76715528791564891, -// 0.72320370216816077, 1.037712429738102; -// ASSERT_TRUE(markov->GetState().isApprox(final_state)); -// } + ts.CreateTransition("intervention"); + ts.AddMatrixToTransition("intervention", intervention_trans); -// TEST_F(RespondTest, RunSimulationOneStep) { -// markov->CreateDefaultHistories(); + ts.CreateTransition("overdose"); + ts.AddMatrixToTransition("overdose", overdose_prob); + ts.AddMatrixToTransition("overdose", fod_prob); -// markov->SetState(init_state); + ts.CreateTransition("background_death"); + ts.AddMatrixToTransition("background_death", background_death_prob); + return ts; + } +}; -// auto migr = MakeTestTransition("migration", migration_pop); -// markov->AddTransition(migr); +TEST_F(RespondTest, RunSingleTimestep) { + sim.GetModels()[0]->AddTimestep(CreateTestTimestep()); + sim.Run(); + Eigen::VectorXd result = sim.GetModelHistories()[0].at("state").back(); -// auto beha = MakeTestTransition("behavior", behavior_trans); -// markov->AddTransition(beha); + Eigen::Vector3d final_state; + final_state << 0.76715528791564891, 0.72320370216816077, 1.037712429738102; + ASSERT_TRUE(result.isApprox(final_state)); +} -// auto inte = MakeTestTransition("intervention", intervention_trans); -// markov->AddTransition(inte); +TEST_F(RespondTest, RunSimulationTwoStep) { + sim.GetModels()[0]->AddTimestep(CreateTestTimestep()); + sim.GetModels()[0]->AddTimestep(CreateTestTimestep()); + sim.Run(2); -// auto over = MakeTestTransition("overdose", overdose_prob); -// over->AddMatrix(fod_prob); -// markov->AddTransition(over); + auto histories = sim.GetModelHistories(); + ASSERT_EQ(histories.size(), 1); -// auto back = MakeTestTransition("background_death", -// background_death_prob); markov->AddTransition(back); + auto mm_histories = histories[0]; + if (mm_histories.find("state") == mm_histories.end()) { + FAIL() << "Unable to find the 'state' history."; + } -// Simulation sim("test_logger"); -// sim.AddModel(markov); -// sim.Run(); + auto state_history = mm_histories.at("state"); + // 2 because it carries the initial state and 2 steps + ASSERT_EQ(state_history.size(), 3); -// auto histories = sim.GetModelHistories(); -// ASSERT_EQ(histories.size(), 1); + Eigen::Vector3d final_state; + ASSERT_TRUE(state_history[0].isApprox(init_state)); -// auto mm_histories = histories[0]; -// if (mm_histories.find("state") == mm_histories.end()) { -// FAIL() << "Unable to find the 'state' history."; -// } + final_state << 0.46999281, 0.44109648, 0.631613324; + Eigen::Vector3d diff = (state_history[2] - final_state).cwiseAbs(); + ASSERT_TRUE((diff.array() <= tolerance.array()).all()); +} -// auto state_history = mm_histories.at("state"); -// // 2 because it carries the initial state and 1 step -// ASSERT_EQ(state_history.size(), 2); +TEST_F(RespondTest, RunSimulationFiveStep) { + sim.GetModels()[0]->AddTimestep(CreateTestTimestep()); + sim.GetModels()[0]->AddTimestep(CreateTestTimestep()); + sim.GetModels()[0]->AddTimestep(CreateTestTimestep()); + sim.GetModels()[0]->AddTimestep(CreateTestTimestep()); + sim.GetModels()[0]->AddTimestep(CreateTestTimestep()); + sim.SetDuration(5); + sim.Run(); + + auto histories = sim.GetModelHistories(); + ASSERT_EQ(histories.size(), 1); + + auto mm_histories = histories[0]; + if (mm_histories.find("state") == mm_histories.end()) { + FAIL() << "Unable to find the 'state' history."; + } -// Eigen::Vector3d final_state; -// ASSERT_TRUE(state_history[0].isApprox(init_state)); -// final_state << 0.76715528791564891, -// 0.72320370216816077, 1.037712429738102; -// ASSERT_TRUE(state_history[1].isApprox(final_state)); -// } + auto state_history = mm_histories.at("state"); + // 6 because it carries the initial state and 5 timesteps + ASSERT_EQ(state_history.size(), 6); -// TEST_F(RespondTest, RunSimulationTwoStep) { -// markov->CreateDefaultHistories(); + Eigen::Vector3d final_state; + ASSERT_TRUE(state_history[0].isApprox(init_state)); -// markov->SetState(init_state); + final_state << 0.10714013, 0.10056269, 0.14400034; + Eigen::Vector3d diff = (state_history[5] - final_state).cwiseAbs(); + ASSERT_TRUE((diff.array() <= tolerance.array()).all()); +} -// auto migr = MakeTestTransition("migration", migration_pop); -// auto beha = MakeTestTransition("behavior", behavior_trans); -// auto inte = MakeTestTransition("intervention", intervention_trans); -// auto over = MakeTestTransition("overdose", overdose_prob); -// over->AddMatrix(fod_prob); +TEST_F(RespondTest, RunSimulationFiveStepWithDurationParameter) { + sim.GetModels()[0]->AddTimestep(CreateTestTimestep()); + sim.GetModels()[0]->AddTimestep(CreateTestTimestep()); + sim.GetModels()[0]->AddTimestep(CreateTestTimestep()); + sim.GetModels()[0]->AddTimestep(CreateTestTimestep()); + sim.GetModels()[0]->AddTimestep(CreateTestTimestep()); + sim.Run(5); -// auto back = MakeTestTransition("background_death", -// background_death_prob); + auto histories = sim.GetModelHistories(); + ASSERT_EQ(histories.size(), 1); -// markov->AddTransition(migr); -// markov->AddTransition(beha); -// markov->AddTransition(inte); -// markov->AddTransition(over); -// markov->AddTransition(back); + auto mm_histories = histories[0]; + if (mm_histories.find("state") == mm_histories.end()) { + FAIL() << "Unable to find the 'state' history."; + } -// markov->AddTransition(migr); -// markov->AddTransition(beha); -// markov->AddTransition(inte); -// markov->AddTransition(over); -// markov->AddTransition(back); + auto state_history = mm_histories.at("state"); + // 6 because it carries the initial state and 5 timesteps + ASSERT_EQ(state_history.size(), 6); -// Simulation sim("test_logger"); -// sim.AddModel(markov); -// sim.Run(); - -// auto histories = sim.GetModelHistories(); -// ASSERT_EQ(histories.size(), 1); - -// auto mm_histories = histories[0]; -// if (mm_histories.find("state") == mm_histories.end()) { -// FAIL() << "Unable to find the 'state' history."; -// } - -// auto state_history = mm_histories.at("state"); -// // 2 because it carries the initial state and 1 step -// ASSERT_EQ(state_history.size(), 3); - -// Eigen::Vector3d final_state; -// ASSERT_TRUE(state_history[0].isApprox(init_state)); -// final_state << 0.76715528791564891, -// 0.72320370216816077, 1.037712429738102; -// ASSERT_TRUE(state_history[1].isApprox(final_state)); -// } - -// TEST_F(RespondTest, CreateDefaultHistories) { -// std::vector expected = { -// "state", "total_overdose", "fatal_overdose", -// "intervention_admission", "background_death"}; - -// std::sort(expected.begin(), expected.end()); - -// markov->CreateDefaultHistories(); -// std::vector results; -// for (const auto &kv : markov->GetHistories()) { -// results.push_back(kv.first); -// } -// ASSERT_EQ(results, expected); -// } + Eigen::Vector3d final_state; + ASSERT_TRUE(state_history[0].isApprox(init_state)); + final_state << 0.10714013, 0.10056269, 0.14400034; + Eigen::Vector3d diff = (state_history[5] - final_state).cwiseAbs(); + ASSERT_TRUE((diff.array() <= tolerance.array()).all()); +} } // namespace testing } // namespace respond \ No newline at end of file diff --git a/tests/mocks/model_mock.hpp b/tests/mocks/model_mock.hpp index e80809e9..93ed3295 100644 --- a/tests/mocks/model_mock.hpp +++ b/tests/mocks/model_mock.hpp @@ -4,7 +4,7 @@ // Created Date: 2025-08-01 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-07 // +// Last Modified: 2026-07-09 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center // diff --git a/tests/mocks/transition_mock.hpp b/tests/mocks/transition_mock.hpp index 64fc6af2..f516af93 100644 --- a/tests/mocks/transition_mock.hpp +++ b/tests/mocks/transition_mock.hpp @@ -36,7 +36,6 @@ class MockTransition : public virtual Transition { (), (const, override)); MOCK_METHOD(void, ClearMatrices, (), (override)); MOCK_METHOD(std::string, GetName, (), (const, override)); - MOCK_METHOD(std::string, GetLogName, (), (const, override)); MOCK_METHOD(std::unique_ptr, clone, (), (const, override)); }; } // namespace testing diff --git a/tests/unit/background_test.cpp b/tests/unit/background_test.cpp index ca746270..12dcc599 100644 --- a/tests/unit/background_test.cpp +++ b/tests/unit/background_test.cpp @@ -4,71 +4,172 @@ // Created Date: 2026-02-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-07 // +// Last Modified: 2026-07-08 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // //////////////////////////////////////////////////////////////////////////////// +#include #include -#include +#include +#include +#include #include +#include #include #include +#include + +#include "../../src/internals/background.hpp" namespace respond { namespace testing { class BackgroundDeathTest : public ::testing::Test { public: - std::unique_ptr tran; Eigen::VectorXd state; Eigen::VectorXd tran_matrix; std::map histories; protected: void SetUp() override { - tran = Transition::Create("background_death", "test_logger"); state = Eigen::VectorXd(3); state << 1.0f, 2.0f, 3.0f; tran_matrix = Eigen::VectorXd(3); tran_matrix << 0.5f, 0.1f, 0.8f; + + // Clear any existing loggers from previous tests + spdlog::drop_all(); + + // Create temporary log files for testing + test_log_file_ = "/tmp/respond_test.log"; + shared_log_file_ = "/tmp/respond_shared.log"; + default_log_file_ = RESPOND_DEFAULT_LOG_FILE; + + // Remove test files if they exist + std::remove(default_log_file_.c_str()); + std::remove(test_log_file_.c_str()); + std::remove(shared_log_file_.c_str()); + } + void TearDown() override { + // Clean up loggers + spdlog::drop_all(); + + // Remove test files + std::remove(default_log_file_.c_str()); + std::remove(test_log_file_.c_str()); + std::remove(shared_log_file_.c_str()); + } + + std::string test_log_file_; + std::string shared_log_file_; + std::string default_log_file_; + + // Helper to check if file contains a string + bool FileContains(const std::string &filepath, const std::string &search) { + std::ifstream file(filepath); + if (!file.is_open()) + return false; + + std::string line; + while (std::getline(file, line)) { + if (line.find(search) != std::string::npos) { + return true; + } + } + return false; } - void TearDown() override { tran.reset(); } }; -TEST_F(BackgroundDeathTest, NoTransitionMatrices) { - EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); +TEST_F(BackgroundDeathTest, DefaultConstructor) { + BackgroundDeath background_death; + EXPECT_EQ(background_death.GetName(), "background_death"); + ASSERT_EQ(CreateFileLogger(RESPOND_DEFAULT_LOG, default_log_file_), + CreationStatus::kExists); +} + +TEST_F(BackgroundDeathTest, ConstructorWithName) { + BackgroundDeath background_death("custom_name"); + EXPECT_EQ(background_death.GetName(), "custom_name"); + ASSERT_EQ(CreateFileLogger(RESPOND_DEFAULT_LOG, default_log_file_), + CreationStatus::kExists); +} + +TEST_F(BackgroundDeathTest, ConstructorWithNameAndLogName) { + BackgroundDeath background_death("custom_name", "custom_log"); + EXPECT_EQ(background_death.GetName(), "custom_name"); + ASSERT_EQ(CreateFileLogger("custom_log", default_log_file_), + CreationStatus::kExists); } -TEST_F(BackgroundDeathTest, TooManyTransitionMatrices) { - tran->AddMatrix(state); - tran->AddMatrix(state); - EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); +TEST_F(BackgroundDeathTest, ConstructorWithNameLogNameAndLogFile) { + BackgroundDeath background_death("custom_name", "custom_log", + test_log_file_); + EXPECT_EQ(background_death.GetName(), "custom_name"); + ASSERT_EQ(CreateFileLogger("custom_log", test_log_file_), + CreationStatus::kExists); } -TEST_F(BackgroundDeathTest, GoodExecuteNoHistory) { - tran->AddMatrix(tran_matrix); - auto result = tran->Execute(state, histories); - auto expected = state - state.cwiseProduct(tran_matrix); - EXPECT_TRUE(result.isApprox(expected)); +TEST_F(BackgroundDeathTest, ExecuteNoMatrices) { + BackgroundDeath background_death; + histories["state"] = History("state"); + EXPECT_THROW((void)background_death.Execute(state, histories), + std::runtime_error); + FlushAllLoggers(); + EXPECT_TRUE(FileContains(RESPOND_DEFAULT_LOG_FILE, + "Transition error - Wrong number of matrices. " + "Expected 1 transition matrix, got 0")); +} + +TEST_F(BackgroundDeathTest, ExecuteTooManyMatrices) { + BackgroundDeath background_death; + background_death.AddMatrix(tran_matrix); + background_death.AddMatrix(tran_matrix); + histories["state"] = History("state"); + EXPECT_THROW((void)background_death.Execute(state, histories), + std::runtime_error); + FlushAllLoggers(); + EXPECT_TRUE(FileContains(RESPOND_DEFAULT_LOG_FILE, + "Transition error - Wrong number of matrices. " + "Expected 1 transition matrix, got 2")); +} + +TEST_F(BackgroundDeathTest, ExecuteNoBackgroundDeathHistory) { + BackgroundDeath background_death; + background_death.AddMatrix(tran_matrix); + histories["state"] = History("state"); + + auto deaths = state.cwiseProduct(tran_matrix); + Eigen::VectorXd expected_state = state - deaths; + Eigen::VectorXd result = background_death.Execute(state, histories); + EXPECT_TRUE(result.isApprox(expected_state)); +} + +TEST_F(BackgroundDeathTest, ExecuteWithBackgroundDeathHistory) { + BackgroundDeath background_death; + background_death.AddMatrix(tran_matrix); + histories["state"] = History("state"); + histories["background_death"] = History("background_death"); + + auto deaths = state.cwiseProduct(tran_matrix); + Eigen::VectorXd expected_state = state - deaths; + Eigen::VectorXd result = background_death.Execute(state, histories); + EXPECT_TRUE(result.isApprox(expected_state)); } -TEST_F(BackgroundDeathTest, GoodExecuteWriteHistory) { - History h("background_death", "test_logger"); - histories["background_death"] = h; - tran->AddMatrix(tran_matrix); - auto result = tran->Execute(state, histories); - auto expected_deaths = state.cwiseProduct(tran_matrix); - auto expected_return = state - expected_deaths; - - EXPECT_TRUE(result.isApprox(expected_return)); - EXPECT_TRUE(histories["background_death"].HasPendingState()); - EXPECT_TRUE(histories["background_death"].GetPendingState().isApprox( - expected_deaths)); +TEST_F(BackgroundDeathTest, Clone) { + BackgroundDeath background_death; + background_death.AddMatrix(tran_matrix); + std::unique_ptr cloned_bgd = background_death.clone(); + EXPECT_EQ(cloned_bgd->GetName(), background_death.GetName()); + EXPECT_EQ(cloned_bgd->GetMatrices().size(), + background_death.GetMatrices().size()); + EXPECT_TRUE(cloned_bgd->GetMatrices()[0].isApprox( + background_death.GetMatrices()[0])); } } // namespace testing } // namespace respond diff --git a/tests/unit/behavior_test.cpp b/tests/unit/behavior_test.cpp index b59969bb..695d8e68 100644 --- a/tests/unit/behavior_test.cpp +++ b/tests/unit/behavior_test.cpp @@ -4,64 +4,142 @@ // Created Date: 2026-02-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-07 // +// Last Modified: 2026-07-08 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // //////////////////////////////////////////////////////////////////////////////// +#include +#include #include +#include +#include #include +#include #include #include +#include -#include -#include +#include "../../src/internals/behavior.hpp" namespace respond { namespace testing { class BehaviorTest : public ::testing::Test { public: - std::unique_ptr tran; Eigen::VectorXd state; Eigen::MatrixXd tran_matrix; std::map histories; protected: void SetUp() override { - tran = Transition::Create("behavior", "test_logger"); state = Eigen::VectorXd(3); state << 1.0f, 2.0f, 3.0f; - tran_matrix = Eigen::MatrixXd(3, 3); - tran_matrix << 0.5f, 0.1f, 0.8f, 0.5f, 0.1f, 0.8f, 0.5f, 0.1f, 0.8f; + tran_matrix = Eigen::MatrixXd(1, 3); + tran_matrix << 0.5f, 0.1f, 0.8f; + + // Clear any existing loggers from previous tests + spdlog::drop_all(); + + // Create temporary log files for testing + test_log_file_ = "/tmp/respond_test.log"; + shared_log_file_ = "/tmp/respond_shared.log"; + default_log_file_ = RESPOND_DEFAULT_LOG_FILE; + + // Remove test files if they exist + std::remove(default_log_file_.c_str()); + std::remove(test_log_file_.c_str()); + std::remove(shared_log_file_.c_str()); + } + void TearDown() override { + // Clean up loggers + spdlog::drop_all(); + + // Remove test files + std::remove(default_log_file_.c_str()); + std::remove(test_log_file_.c_str()); + std::remove(shared_log_file_.c_str()); + } + + std::string test_log_file_; + std::string shared_log_file_; + std::string default_log_file_; + + // Helper to check if file contains a string + bool FileContains(const std::string &filepath, const std::string &search) { + std::ifstream file(filepath); + if (!file.is_open()) + return false; + + std::string line; + while (std::getline(file, line)) { + if (line.find(search) != std::string::npos) { + return true; + } + } + return false; } - void TearDown() override { tran.reset(); } }; -TEST_F(BehaviorTest, NoTransitionMatrices) { - EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); +TEST_F(BehaviorTest, ExecuteNoMatrices) { + Behavior behavior; + histories["state"] = History("state"); + EXPECT_THROW((void)behavior.Execute(state, histories), std::runtime_error); + FlushAllLoggers(); + EXPECT_TRUE(FileContains(RESPOND_DEFAULT_LOG_FILE, + "Transition error - Wrong number of matrices. " + "Expected 1 transition matrix, got 0")); } -TEST_F(BehaviorTest, TooManyTransitionMatrices) { - tran->AddMatrix(tran_matrix); - tran->AddMatrix(tran_matrix); - EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); +TEST_F(BehaviorTest, ExecuteTooManyMatrices) { + Behavior behavior; + behavior.AddMatrix(tran_matrix); + behavior.AddMatrix(tran_matrix); + histories["state"] = History("state"); + EXPECT_THROW((void)behavior.Execute(state, histories), std::runtime_error); + FlushAllLoggers(); + EXPECT_TRUE(FileContains(RESPOND_DEFAULT_LOG_FILE, + "Transition error - Wrong number of matrices. " + "Expected 1 transition matrix, got 2")); } -TEST_F(BehaviorTest, NotSquareTransitionMatrix) { - tran->AddMatrix(state); - EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); +TEST_F(BehaviorTest, ExecuteDimensionMismatch) { + Behavior behavior; + Eigen::MatrixXd wrong_dim_matrix(3, 2); // 3 rows, 2 cols + wrong_dim_matrix << 0.5f, 0.1f, 0.8f, 0.2f, 0.3f, 0.4f; + behavior.AddMatrix(wrong_dim_matrix); + histories["state"] = History("state"); + EXPECT_THROW((void)behavior.Execute(state, histories), std::runtime_error); + FlushAllLoggers(); + EXPECT_TRUE( + FileContains(RESPOND_DEFAULT_LOG_FILE, + "Transition error - Dimension mismatch, m1 rows do not " + "match m2 columns. m1 size is (3, 1) m2 size is (3, 2)")); } -TEST_F(BehaviorTest, GoodExecuteNoHistory) { - tran->AddMatrix(tran_matrix); - auto result = tran->Execute(state, histories); - auto expected = tran_matrix * state; - EXPECT_TRUE(result.isApprox(expected)); +TEST_F(BehaviorTest, ExecuteValid) { + Behavior behavior; + behavior.AddMatrix(tran_matrix); + histories["state"] = History("state"); + Eigen::VectorXd new_state = behavior.Execute(state, histories); + Eigen::VectorXd expected_state = tran_matrix * state; + EXPECT_TRUE(new_state.isApprox(expected_state)); } + +TEST_F(BehaviorTest, Clone) { + Behavior behavior; + behavior.AddMatrix(tran_matrix); + std::unique_ptr cloned_behavior = behavior.clone(); + EXPECT_EQ(cloned_behavior->GetName(), behavior.GetName()); + EXPECT_EQ(cloned_behavior->GetMatrices().size(), + behavior.GetMatrices().size()); + EXPECT_TRUE( + cloned_behavior->GetMatrices()[0].isApprox(behavior.GetMatrices()[0])); +} + } // namespace testing } // namespace respond \ No newline at end of file diff --git a/tests/unit/intervention_test.cpp b/tests/unit/intervention_test.cpp index 932cad46..33f1d734 100644 --- a/tests/unit/intervention_test.cpp +++ b/tests/unit/intervention_test.cpp @@ -4,79 +4,182 @@ // Created Date: 2026-02-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-07 // +// Last Modified: 2026-07-08 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // //////////////////////////////////////////////////////////////////////////////// +#include +#include #include +#include +#include #include +#include #include #include +#include -#include -#include +#include "../../src/internals/intervention.hpp" namespace respond { namespace testing { class InterventionTest : public ::testing::Test { public: - std::unique_ptr tran; Eigen::VectorXd state; Eigen::MatrixXd tran_matrix; std::map histories; protected: void SetUp() override { - tran = Transition::Create("intervention", "test_logger"); state = Eigen::VectorXd(3); state << 1.0f, 2.0f, 3.0f; tran_matrix = Eigen::MatrixXd(3, 3); - tran_matrix << 0.5f, 0.1f, 0.8f, 0.5f, 0.1f, 0.8f, 0.5f, 0.1f, 0.8f; + tran_matrix << 0.5f, 0.1f, 0.8f, 0.6f, 0.1f, 0.7f, 0.7f, 0.1f, 0.6f; + + // Clear any existing loggers from previous tests + spdlog::drop_all(); + + // Create temporary log files for testing + test_log_file_ = "/tmp/respond_test.log"; + shared_log_file_ = "/tmp/respond_shared.log"; + default_log_file_ = RESPOND_DEFAULT_LOG_FILE; + + // Remove test files if they exist + std::remove(default_log_file_.c_str()); + std::remove(test_log_file_.c_str()); + std::remove(shared_log_file_.c_str()); + } + void TearDown() override { + // Clean up loggers + spdlog::drop_all(); + + // Remove test files + std::remove(default_log_file_.c_str()); + std::remove(test_log_file_.c_str()); + std::remove(shared_log_file_.c_str()); + } + + std::string test_log_file_; + std::string shared_log_file_; + std::string default_log_file_; + + // Helper to check if file contains a string + bool FileContains(const std::string &filepath, const std::string &search) { + std::ifstream file(filepath); + if (!file.is_open()) + return false; + + std::string line; + while (std::getline(file, line)) { + if (line.find(search) != std::string::npos) { + return true; + } + } + return false; } - void TearDown() override { tran.reset(); } }; -TEST_F(InterventionTest, NoTransitionMatrices) { - EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); +TEST_F(InterventionTest, ExecuteNoMatrices) { + Intervention intervention; + histories["state"] = History("state"); + EXPECT_THROW((void)intervention.Execute(state, histories), + std::runtime_error); + FlushAllLoggers(); + EXPECT_TRUE(FileContains(RESPOND_DEFAULT_LOG_FILE, + "Transition error - Wrong number of matrices. " + "Expected 1 transition matrix, got 0")); } -TEST_F(InterventionTest, TooManyTransitionMatrices) { - tran->AddMatrix(tran_matrix); - tran->AddMatrix(tran_matrix); - EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); +TEST_F(InterventionTest, ExecuteTooManyMatrices) { + Intervention intervention; + intervention.AddMatrix(tran_matrix); + intervention.AddMatrix(tran_matrix); // Add a second matrix + histories["state"] = History("state"); + EXPECT_THROW((void)intervention.Execute(state, histories), + std::runtime_error); + FlushAllLoggers(); + EXPECT_TRUE(FileContains(RESPOND_DEFAULT_LOG_FILE, + "Transition error - Wrong number of matrices. " + "Expected 1 transition matrix, got 2")); } -TEST_F(InterventionTest, NotSquareTransitionMatrix) { - tran->AddMatrix(state); - EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); +TEST_F(InterventionTest, ExecuteNonSquareMatrix) { + Intervention intervention; + Eigen::MatrixXd non_square_matrix(3, 2); // Non-square matrix + non_square_matrix << 0.5f, 0.1f, 0.8f, 0.6f, 0.1f, 0.7f; + intervention.AddMatrix(non_square_matrix); + histories["state"] = History("state"); + + EXPECT_THROW((void)intervention.Execute(state, histories), + std::runtime_error); + FlushAllLoggers(); + EXPECT_TRUE(FileContains(RESPOND_DEFAULT_LOG_FILE, + "Transition error - matrix is not square. Matrix " + "size is (3, 2)")); +} + +TEST_F(InterventionTest, ExecuteDimensionMismatch) { + Intervention intervention; + intervention.AddMatrix(tran_matrix); + histories["state"] = History("state"); + + // Create a state vector with a different size to trigger dimension mismatch + Eigen::VectorXd mismatched_state(2); + mismatched_state << 1.0f, 2.0f; + + EXPECT_THROW((void)intervention.Execute(mismatched_state, histories), + std::runtime_error); + FlushAllLoggers(); + EXPECT_TRUE( + FileContains(RESPOND_DEFAULT_LOG_FILE, + "Transition error - Dimension mismatch, m1 rows do not " + "match m2 columns. m1 size is (2, 1) m2 size is (3, 3)")); } -TEST_F(InterventionTest, GoodExecuteNoHistory) { - tran->AddMatrix(tran_matrix); - auto result = tran->Execute(state, histories); - auto expected = tran_matrix * state; - EXPECT_TRUE(result.isApprox(expected)); +TEST_F(InterventionTest, ExecuteValid) { + Intervention intervention; + intervention.AddMatrix(tran_matrix); + histories["state"] = History("state"); + Eigen::VectorXd new_state = intervention.Execute(state, histories); + Eigen::VectorXd expected_state = tran_matrix * state; + EXPECT_TRUE(new_state.isApprox(expected_state)); +} + +TEST_F(InterventionTest, ExecuteValidWithHistory) { + Intervention intervention; + intervention.AddMatrix(tran_matrix); + histories["state"] = History("state"); + histories["intervention_admission"] = History("intervention_admission"); + + Eigen::VectorXd new_state = intervention.Execute(state, histories); + Eigen::VectorXd expected_state = tran_matrix * state; + EXPECT_TRUE(new_state.isApprox(expected_state)); + + histories["intervention_admission"].FlushPendingState(0, state.size()); + + // Check that the intervention_admission history has been updated correctly + Eigen::VectorXd expected_admissions = + (expected_state - state).cwiseMax(Eigen::VectorXd::Zero(state.size())); + EXPECT_TRUE( + histories["intervention_admission"].GetStateAsVector()[0].isApprox( + expected_admissions)); } -TEST_F(InterventionTest, GoodExecuteWriteHistory) { - History h("intervention_admission", "test_logger"); - histories["intervention_admission"] = h; - tran->AddMatrix(tran_matrix); - auto result = tran->Execute(state, histories); - auto expected_return = tran_matrix * state; - auto expected_admissions = - (expected_return - state).cwiseMax(Eigen::VectorXd::Zero(3)); - - EXPECT_TRUE(result.isApprox(expected_return)); - EXPECT_TRUE(histories["intervention_admission"].HasPendingState()); - EXPECT_TRUE(histories["intervention_admission"].GetPendingState().isApprox( - expected_admissions)); +TEST_F(InterventionTest, Clone) { + Intervention intervention; + intervention.AddMatrix(tran_matrix); + std::unique_ptr cloned_intervention = intervention.clone(); + EXPECT_EQ(cloned_intervention->GetName(), intervention.GetName()); + EXPECT_EQ(cloned_intervention->GetMatrices().size(), + intervention.GetMatrices().size()); + EXPECT_TRUE(cloned_intervention->GetMatrices()[0].isApprox( + intervention.GetMatrices()[0])); } } // namespace testing } // namespace respond \ No newline at end of file diff --git a/tests/unit/logging_test.cpp b/tests/unit/logging_test.cpp index cc2da2c0..9aaca62c 100644 --- a/tests/unit/logging_test.cpp +++ b/tests/unit/logging_test.cpp @@ -4,7 +4,7 @@ // Created Date: 2025-03-18 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-07 // +// Last Modified: 2026-07-09 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center // @@ -22,7 +22,7 @@ #include #include -#include +#include namespace respond { namespace testing { diff --git a/tests/unit/markov_test.cpp b/tests/unit/markov_test.cpp index 065b3d09..02619a26 100644 --- a/tests/unit/markov_test.cpp +++ b/tests/unit/markov_test.cpp @@ -4,7 +4,7 @@ // Created Date: 2025-06-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-07 // +// Last Modified: 2026-07-09 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center // @@ -327,6 +327,22 @@ TEST_F(MarkovTest, RunTimesteps) { EXPECT_EQ(markov.GetTimestep(), 2); } +TEST_F(MarkovTest, RunTimestepsWithFinalTimestep) { + Markov markov; + markov.SetInitialHistoryRecorded(true); + markov.SetFinalTimestep(1); + Timestep timestep1(RESPOND_DEFAULT_LOG); + Timestep timestep2(RESPOND_DEFAULT_LOG); + markov.AddTimestep(timestep1); + markov.AddTimestep(timestep2); + EXPECT_EQ(markov.GetTimestep(), 0); + markov.RunTimesteps(); + EXPECT_EQ(markov.GetTimestep(), 1); + FlushAllLoggers(); + EXPECT_TRUE(FileContains(RESPOND_DEFAULT_LOG_FILE, + "Only running timesteps up to duration value.")); +} + TEST_F(MarkovTest, RunTimestepsRecordInitialHistory) { Markov markov("markov", RESPOND_DEFAULT_LOG); markov.SetInitialHistoryRecorded(false); diff --git a/tests/unit/migration_test.cpp b/tests/unit/migration_test.cpp index 5fdf02ae..4ce8e0ad 100644 --- a/tests/unit/migration_test.cpp +++ b/tests/unit/migration_test.cpp @@ -4,67 +4,142 @@ // Created Date: 2026-02-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-07 // +// Last Modified: 2026-07-08 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // //////////////////////////////////////////////////////////////////////////////// -#include +#include +#include +#include #include +#include #include #include +#include -#include -#include +#include "../../src/internals/migration.hpp" namespace respond { namespace testing { class MigrationTest : public ::testing::Test { public: - std::unique_ptr tran; Eigen::VectorXd state; - Eigen::VectorXd tran_matrix; + Eigen::MatrixXd tran_matrix; std::map histories; protected: void SetUp() override { - tran = Transition::Create("migration", "test_logger"); state = Eigen::VectorXd(3); state << 1.0f, 2.0f, 3.0f; tran_matrix = Eigen::VectorXd(3); - tran_matrix << 1.0f, 2.0f, 3.0f; + tran_matrix << 0.5f, 0.1f, 0.8f; + + // Clear any existing loggers from previous tests + spdlog::drop_all(); + + // Create temporary log files for testing + test_log_file_ = "/tmp/respond_test.log"; + shared_log_file_ = "/tmp/respond_shared.log"; + default_log_file_ = RESPOND_DEFAULT_LOG_FILE; + + // Remove test files if they exist + std::remove(default_log_file_.c_str()); + std::remove(test_log_file_.c_str()); + std::remove(shared_log_file_.c_str()); + } + void TearDown() override { + // Clean up loggers + spdlog::drop_all(); + + // Remove test files + std::remove(default_log_file_.c_str()); + std::remove(test_log_file_.c_str()); + std::remove(shared_log_file_.c_str()); + } + + std::string test_log_file_; + std::string shared_log_file_; + std::string default_log_file_; + + // Helper to check if file contains a string + bool FileContains(const std::string &filepath, const std::string &search) { + std::ifstream file(filepath); + if (!file.is_open()) + return false; + + std::string line; + while (std::getline(file, line)) { + if (line.find(search) != std::string::npos) { + return true; + } + } + return false; } - void TearDown() override { tran.reset(); } }; -TEST_F(MigrationTest, NoTransitionMatrices) { - EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); +TEST_F(MigrationTest, ExecuteNoMatrices) { + Migration migration; + histories["state"] = History("state"); + EXPECT_THROW((void)migration.Execute(state, histories), std::runtime_error); + FlushAllLoggers(); + EXPECT_TRUE(FileContains(RESPOND_DEFAULT_LOG_FILE, + "Transition error - Wrong number of matrices. " + "Expected 1 transition matrix, got 0")); } -TEST_F(MigrationTest, TooManyTransitionMatrices) { - tran->AddMatrix(state); - tran->AddMatrix(state); - EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); +TEST_F(MigrationTest, ExecuteTooManyMatrices) { + Migration migration; + migration.AddMatrix(tran_matrix); + migration.AddMatrix(tran_matrix); + histories["state"] = History("state"); + EXPECT_THROW((void)migration.Execute(state, histories), std::runtime_error); + FlushAllLoggers(); + EXPECT_TRUE(FileContains(RESPOND_DEFAULT_LOG_FILE, + "Transition error - Wrong number of matrices. " + "Expected 1 transition matrix, got 2")); } -TEST_F(MigrationTest, WrongSizeTransitionMatrix) { - Eigen::VectorXd bad_t_matrix; - bad_t_matrix = Eigen::VectorXd(6); - bad_t_matrix << 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f; - tran->AddMatrix(bad_t_matrix); - EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); +TEST_F(MigrationTest, ExecuteSizeMismatch) { + Migration migration; + Eigen::MatrixXd wrong_size_matrix(2, 2); + wrong_size_matrix << 0.5f, 0.1f, 0.8f, 0.6f; + migration.AddMatrix(wrong_size_matrix); + histories["state"] = History("state"); + EXPECT_THROW((void)migration.Execute(state, histories), std::runtime_error); + FlushAllLoggers(); + EXPECT_TRUE( + FileContains(RESPOND_DEFAULT_LOG_FILE, + "Transition error - matrix size mismatch. " + "Matrix 1 size is (3, 1) but Matrix 2 size is (2, 2)")); } -TEST_F(MigrationTest, GoodExecuteNoHistory) { - tran->AddMatrix(tran_matrix); - auto result = tran->Execute(state, histories); - auto expected = state + tran_matrix; +TEST_F(MigrationTest, ExecuteValid) { + Migration migration; + migration.AddMatrix(tran_matrix); + histories["state"] = History("state"); + Eigen::VectorXd result = migration.Execute(state, histories); + Eigen::VectorXd expected = state + tran_matrix; + expected = + expected.array().max(Eigen::VectorXd::Zero(expected.size()).array()); EXPECT_TRUE(result.isApprox(expected)); } + +TEST_F(MigrationTest, Clone) { + Migration migration; + migration.AddMatrix(tran_matrix); + std::unique_ptr cloned_migration = migration.clone(); + EXPECT_EQ(cloned_migration->GetName(), migration.GetName()); + EXPECT_EQ(cloned_migration->GetMatrices().size(), + migration.GetMatrices().size()); + EXPECT_TRUE(cloned_migration->GetMatrices()[0].isApprox( + migration.GetMatrices()[0])); +} + } // namespace testing } // namespace respond \ No newline at end of file diff --git a/tests/unit/overdose_test.cpp b/tests/unit/overdose_test.cpp index 947abfc0..7c3e0d0e 100644 --- a/tests/unit/overdose_test.cpp +++ b/tests/unit/overdose_test.cpp @@ -4,129 +4,205 @@ // Created Date: 2026-02-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-07 // +// Last Modified: 2026-07-08 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // //////////////////////////////////////////////////////////////////////////////// +#include #include +#include +#include #include +#include #include #include +#include -#include -#include +#include "../../src/internals/overdose.hpp" namespace respond { namespace testing { class OverdoseTest : public ::testing::Test { public: - std::unique_ptr tran; Eigen::VectorXd state; - Eigen::VectorXd tran_matrix; + Eigen::MatrixXd tran_matrix; std::map histories; protected: void SetUp() override { - tran = Transition::Create("overdose", "test_logger"); state = Eigen::VectorXd(3); state << 1.0f, 2.0f, 3.0f; tran_matrix = Eigen::VectorXd(3); tran_matrix << 0.5f, 0.1f, 0.8f; + + // Clear any existing loggers from previous tests + spdlog::drop_all(); + + // Create temporary log files for testing + test_log_file_ = "/tmp/respond_test.log"; + shared_log_file_ = "/tmp/respond_shared.log"; + default_log_file_ = RESPOND_DEFAULT_LOG_FILE; + + // Remove test files if they exist + std::remove(default_log_file_.c_str()); + std::remove(test_log_file_.c_str()); + std::remove(shared_log_file_.c_str()); + } + void TearDown() override { + // Clean up loggers + spdlog::drop_all(); + + // Remove test files + std::remove(default_log_file_.c_str()); + std::remove(test_log_file_.c_str()); + std::remove(shared_log_file_.c_str()); + } + + std::string test_log_file_; + std::string shared_log_file_; + std::string default_log_file_; + + // Helper to check if file contains a string + bool FileContains(const std::string &filepath, const std::string &search) { + std::ifstream file(filepath); + if (!file.is_open()) + return false; + + std::string line; + while (std::getline(file, line)) { + if (line.find(search) != std::string::npos) { + return true; + } + } + return false; } - void TearDown() override { tran.reset(); } }; -TEST_F(OverdoseTest, NoTransitionMatrices) { - EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); +TEST_F(OverdoseTest, ExecuteNoMatrices) { + Overdose overdose; + histories["state"] = History("state"); + EXPECT_THROW((void)overdose.Execute(state, histories), std::runtime_error); + FlushAllLoggers(); + EXPECT_TRUE(FileContains(RESPOND_DEFAULT_LOG_FILE, + "Transition error - Wrong number of matrices. " + "Expected 2 transition matrix, got 0")); } -TEST_F(OverdoseTest, TooFewTransitionMatrices) { - tran->AddMatrix(state); - EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); +TEST_F(OverdoseTest, ExecuteTooManyMatrices) { + Overdose overdose; + overdose.AddMatrix(tran_matrix); + overdose.AddMatrix(tran_matrix); + overdose.AddMatrix(tran_matrix); + histories["state"] = History("state"); + EXPECT_THROW((void)overdose.Execute(state, histories), std::runtime_error); + FlushAllLoggers(); + EXPECT_TRUE(FileContains(RESPOND_DEFAULT_LOG_FILE, + "Transition error - Wrong number of matrices. " + "Expected 2 transition matrix, got 3")); } -TEST_F(OverdoseTest, TooManyTransitionMatrices) { - tran->AddMatrix(state); - tran->AddMatrix(state); - tran->AddMatrix(state); - EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); +TEST_F(OverdoseTest, ExecuteSizeMismatch) { + Overdose overdose; + Eigen::MatrixXd wrong_size_matrix(2, 2); + wrong_size_matrix << 0.5f, 0.1f, 0.8f, 0.6f; + overdose.AddMatrix(wrong_size_matrix); + overdose.AddMatrix(wrong_size_matrix); + histories["state"] = History("state"); + EXPECT_THROW((void)overdose.Execute(state, histories), std::runtime_error); + FlushAllLoggers(); + EXPECT_TRUE( + FileContains(RESPOND_DEFAULT_LOG_FILE, + "Transition error - matrix size mismatch. " + "Matrix 1 size is (3, 1) but Matrix 2 size is (2, 2)")); } -TEST_F(OverdoseTest, WrongSizeTransitionMatrix) { - Eigen::VectorXd bad_t_matrix; - bad_t_matrix = Eigen::VectorXd(6); - bad_t_matrix << 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f; - tran->AddMatrix(bad_t_matrix); - tran->AddMatrix(bad_t_matrix); - EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); +TEST_F(OverdoseTest, BadSecondMatrixSize) { + Overdose overdose; + overdose.AddMatrix(tran_matrix); + Eigen::MatrixXd wrong_size_matrix(2, 2); + wrong_size_matrix << 0.5f, 0.1f, 0.8f, 0.6f; + overdose.AddMatrix(wrong_size_matrix); + histories["state"] = History("state"); + EXPECT_THROW((void)overdose.Execute(state, histories), std::runtime_error); + FlushAllLoggers(); + EXPECT_TRUE( + FileContains(RESPOND_DEFAULT_LOG_FILE, + "Transition error - matrix size mismatch. " + "Matrix 1 size is (3, 1) but Matrix 2 size is (2, 2)")); } -TEST_F(OverdoseTest, GoodExecuteNoHistory) { - tran->AddMatrix(tran_matrix); - tran->AddMatrix(tran_matrix); - auto result = tran->Execute(state, histories); - auto overdoses = state.cwiseProduct(tran_matrix); - auto fods = overdoses.cwiseProduct(tran_matrix); - auto expected_return = state - fods; - EXPECT_TRUE(result.isApprox(expected_return)); +TEST_F(OverdoseTest, ExecuteValid) { + Overdose overdose; + overdose.AddMatrix(tran_matrix); + overdose.AddMatrix(tran_matrix); + histories["state"] = History("state"); + Eigen::VectorXd result = overdose.Execute(state, histories); + + Eigen::VectorXd expected_overdoses = state.cwiseProduct(tran_matrix); + Eigen::VectorXd expected_fods = + expected_overdoses.cwiseProduct(tran_matrix); + Eigen::VectorXd expected_new_state = state - expected_fods; + EXPECT_TRUE(result.isApprox(expected_new_state)); } -TEST_F(OverdoseTest, GoodExecuteWriteTotalOverdoseHistory) { - History h("total_overdose", "test_logger"); - histories["total_overdose"] = h; - tran->AddMatrix(tran_matrix); - tran->AddMatrix(tran_matrix); - auto result = tran->Execute(state, histories); - - auto overdoses = state.cwiseProduct(tran_matrix); - auto fods = overdoses.cwiseProduct(tran_matrix); - auto expected_return = state - fods; - - EXPECT_TRUE(result.isApprox(expected_return)); - EXPECT_TRUE(histories["total_overdose"].HasPendingState()); - EXPECT_TRUE( - histories["total_overdose"].GetPendingState().isApprox(overdoses)); +TEST_F(OverdoseTest, ExecuteValidWithTotalOverdoseHistory) { + Overdose overdose; + overdose.AddMatrix(tran_matrix); + overdose.AddMatrix(tran_matrix); + histories["state"] = History("state"); + histories["total_overdose"] = History("total_overdose"); + Eigen::VectorXd result = overdose.Execute(state, histories); + + Eigen::VectorXd expected_overdoses = state.cwiseProduct(tran_matrix); + Eigen::VectorXd expected_fods = + expected_overdoses.cwiseProduct(tran_matrix); + Eigen::VectorXd expected_new_state = state - expected_fods; + EXPECT_TRUE(result.isApprox(expected_new_state)); + + histories["total_overdose"].FlushPendingState(0, state.size()); + + // Check that the intervention_admission history has been updated correctly + EXPECT_TRUE(histories["total_overdose"].GetStateAsVector()[0].isApprox( + expected_overdoses)); } -TEST_F(OverdoseTest, GoodExecuteWriteFatalOverdoseHistory) { - History h("fatal_overdose", "test_logger"); - histories["fatal_overdose"] = h; - tran->AddMatrix(tran_matrix); - tran->AddMatrix(tran_matrix); - auto result = tran->Execute(state, histories); - - auto overdoses = state.cwiseProduct(tran_matrix); - auto fods = overdoses.cwiseProduct(tran_matrix); - auto expected_return = state - fods; - - EXPECT_TRUE(histories["fatal_overdose"].HasPendingState()); - EXPECT_TRUE(histories["fatal_overdose"].GetPendingState().isApprox(fods)); - EXPECT_TRUE(result.isApprox(expected_return)); +TEST_F(OverdoseTest, ExecuteValidWithFatalOverdoseHistory) { + Overdose overdose; + overdose.AddMatrix(tran_matrix); + overdose.AddMatrix(tran_matrix); + histories["state"] = History("state"); + histories["fatal_overdose"] = History("fatal_overdose"); + Eigen::VectorXd result = overdose.Execute(state, histories); + + Eigen::VectorXd expected_overdoses = state.cwiseProduct(tran_matrix); + Eigen::VectorXd expected_fods = + expected_overdoses.cwiseProduct(tran_matrix); + Eigen::VectorXd expected_new_state = state - expected_fods; + EXPECT_TRUE(result.isApprox(expected_new_state)); + + histories["fatal_overdose"].FlushPendingState(0, state.size()); + + // Check that the intervention_admission history has been updated correctly + EXPECT_TRUE(histories["fatal_overdose"].GetStateAsVector()[0].isApprox( + expected_fods)); } -TEST_F(OverdoseTest, GoodExecuteWriteAllHistory) { - History h1("fatal_overdose", "test_logger"); - histories["fatal_overdose"] = h1; - History h2("total_overdose", "test_logger"); - histories["total_overdose"] = h2; - tran->AddMatrix(tran_matrix); - tran->AddMatrix(tran_matrix); - auto result = tran->Execute(state, histories); - - auto overdoses = state.cwiseProduct(tran_matrix); - auto fods = overdoses.cwiseProduct(tran_matrix); - auto expected_return = state - fods; - +TEST_F(OverdoseTest, Clone) { + Overdose overdose; + overdose.AddMatrix(tran_matrix); + std::unique_ptr cloned_overdose = overdose.clone(); + EXPECT_EQ(cloned_overdose->GetName(), overdose.GetName()); + EXPECT_EQ(cloned_overdose->GetMatrices().size(), + overdose.GetMatrices().size()); EXPECT_TRUE( - histories["total_overdose"].GetPendingState().isApprox(overdoses)); - EXPECT_TRUE(histories["fatal_overdose"].GetPendingState().isApprox(fods)); - EXPECT_TRUE(result.isApprox(expected_return)); + cloned_overdose->GetMatrices()[0].isApprox(overdose.GetMatrices()[0])); } + } // namespace testing } // namespace respond diff --git a/tests/unit/simulation_test.cpp b/tests/unit/simulation_test.cpp index 9cd973bc..a2b6c4a4 100644 --- a/tests/unit/simulation_test.cpp +++ b/tests/unit/simulation_test.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-09 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-07 // +// Last Modified: 2026-07-08 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -13,157 +13,294 @@ #include #include +#include #include +#include #include +#include #include "../mocks/model_mock.hpp" using ::testing::_; using ::testing::NiceMock; using ::testing::Return; +using ::testing::ReturnRef; namespace respond { namespace testing { class SimulationTest : public ::testing::Test { public: protected: - void SetUp() override {} - void TearDown() override {} + void SetUp() override { + // Clear any existing loggers from previous tests + spdlog::drop_all(); + + // Create temporary log files for testing + test_log_file_ = "/tmp/respond_test.log"; + shared_log_file_ = "/tmp/respond_shared.log"; + default_log_file_ = RESPOND_DEFAULT_LOG_FILE; + + // Remove test files if they exist + std::remove(default_log_file_.c_str()); + std::remove(test_log_file_.c_str()); + std::remove(shared_log_file_.c_str()); + } + void TearDown() override { + // Clean up loggers + spdlog::drop_all(); + + // Remove test files + std::remove(default_log_file_.c_str()); + std::remove(test_log_file_.c_str()); + std::remove(shared_log_file_.c_str()); + } + + std::string test_log_file_; + std::string shared_log_file_; + std::string default_log_file_; + + // Helper to check if file contains a string + bool FileContains(const std::string &filepath, const std::string &search) { + std::ifstream file(filepath); + if (!file.is_open()) + return false; + + std::string line; + while (std::getline(file, line)) { + if (line.find(search) != std::string::npos) { + return true; + } + } + return false; + } }; -// TEST_F(SimulationTest, ConstructGetLogger) { -// Simulation s; -// ASSERT_EQ(s.GetLogName(), "console"); -// } - -// TEST_F(SimulationTest, GetSetModel) { -// auto mock = std::make_unique>(); -// auto cloned = std::make_unique>(); -// EXPECT_CALL(*mock, clone()) -// .WillOnce(Return(::testing::ByMove(std::move(cloned)))); - -// std::unique_ptr upmm = std::move(mock); -// Simulation s; -// s.AddModel(upmm); -// ASSERT_EQ(s.GetModels().size(), 1); -// } - -// TEST_F(SimulationTest, ClearModels) { -// auto mock = std::make_unique>(); -// auto cloned = std::make_unique>(); -// EXPECT_CALL(*mock, clone()) -// .WillOnce(Return(::testing::ByMove(std::move(cloned)))); - -// std::unique_ptr upmm = std::move(mock); -// Simulation s; -// s.AddModel(upmm); -// s.ClearModels(); -// ASSERT_EQ(s.GetModels().size(), 0); -// } - -// TEST_F(SimulationTest, GetModelNames) { -// auto mock = std::make_unique>(); -// auto cloned = std::make_unique>(); -// auto expected = "test_model_name"; -// EXPECT_CALL(*cloned, GetName()).WillOnce(Return(expected)); -// EXPECT_CALL(*mock, clone()) -// .WillOnce(Return(::testing::ByMove(std::move(cloned)))); - -// std::unique_ptr upmm = std::move(mock); -// Simulation s; -// s.AddModel(upmm); -// auto result = s.GetModelNames(); -// ASSERT_EQ(result.size(), 1); -// ASSERT_EQ(result[0], expected); -// } - -// TEST_F(SimulationTest, GetModelHistories) { -// auto mock = std::make_unique>(); -// auto cloned = std::make_unique>(); - -// std::map hv; -// History h("temp", "test_logger"); -// Eigen::VectorXd state = Eigen::VectorXd(3); -// state << 1.0f, 2.0f, 3.0f; -// h.AddState(state); -// hv["temp"] = h; - -// std::map> h_map; -// h_map["temp"] = h.GetStateAsVector(); - -// std::vector>> -// expected; expected.push_back(h_map); - -// ON_CALL(*cloned, GetName()).WillByDefault(Return("temp_model")); - -// EXPECT_CALL(*cloned, GetHistories()).WillOnce(Return(hv)); -// ON_CALL(*mock, clone()) -// .WillByDefault(Return(::testing::ByMove(std::move(cloned)))); - -// std::unique_ptr upmm = std::move(mock); -// Simulation s; -// s.AddModel(upmm); -// ASSERT_EQ(s.GetModelHistories(), expected); -// } - -// TEST_F(SimulationTest, GetHistoryNames) { -// auto mock = std::make_unique>(); -// auto cloned = std::make_unique>(); - -// std::string model_name = "temp_model"; -// std::string history_name = "temp_history"; - -// std::map hv; -// History h("temp", "test_logger"); -// hv[history_name] = h; - -// std::vector> expected = { -// {model_name, history_name}}; - -// ON_CALL(*cloned, GetName()).WillByDefault(Return(model_name)); - -// EXPECT_CALL(*cloned, GetHistories()).WillOnce(Return(hv)); -// ON_CALL(*mock, clone()) -// .WillByDefault(Return(::testing::ByMove(std::move(cloned)))); - -// std::unique_ptr upmm = std::move(mock); -// Simulation s; -// s.AddModel(upmm); -// ASSERT_EQ(s.GetModelHistoryNames(), expected); -// } - -// TEST_F(SimulationTest, GetModelSparseHistories) { -// auto mock = std::make_unique>(); -// auto cloned = std::make_unique>(); - -// std::map hv; -// History h("temp", "test_logger"); -// Eigen::VectorXd state0 = Eigen::VectorXd(2); -// state0 << 1.0f, 2.0f; -// Eigen::VectorXd state2 = Eigen::VectorXd(2); -// state2 << 3.0f, 4.0f; -// h.AddState(state0, 0); -// h.AddState(state2, 2); -// hv["temp"] = h; - -// EXPECT_CALL(*cloned, GetHistories()).WillOnce(Return(hv)); -// ON_CALL(*mock, clone()) -// .WillByDefault(Return(::testing::ByMove(std::move(cloned)))); - -// std::unique_ptr upmm = std::move(mock); -// Simulation s; -// s.AddModel(upmm); - -// const auto histories = s.GetModelSparseHistories(); -// ASSERT_EQ(histories.size(), 1u); -// const auto &history = histories[0].at("temp"); -// std::vector expected_timesteps = {0, 2}; -// ASSERT_EQ(history.GetRecordedTimesteps(), expected_timesteps); -// ASSERT_EQ(history.GetRecordedStates().size(), 2u); -// EXPECT_TRUE(history.GetRecordedStates()[0].isApprox(state0)); -// EXPECT_TRUE(history.GetRecordedStates()[1].isApprox(state2)); -// } +TEST_F(SimulationTest, DefaultConstructor) { + Simulation s; + ASSERT_EQ(CreateFileLogger(RESPOND_DEFAULT_LOG, default_log_file_), + CreationStatus::kExists); +} + +TEST_F(SimulationTest, ConstructorWithLogName) { + Simulation s("custom_log"); + ASSERT_EQ(CreateFileLogger("custom_log", default_log_file_), + CreationStatus::kExists); +} + +TEST_F(SimulationTest, ConstructorWithLogNameAndLogFile) { + Simulation s("custom_log", test_log_file_); + ASSERT_EQ(CreateFileLogger("custom_log", test_log_file_), + CreationStatus::kExists); +} + +TEST_F(SimulationTest, CreateNewModel) { + Simulation s; + std::string model_name = "test_model"; + std::string new_model_id = s.CreateNewModel(model_name); + ASSERT_EQ(new_model_id, "1_" + model_name); + ASSERT_EQ(s.GetModels().size(), 1); +} + +TEST_F(SimulationTest, CreateMultipleModels) { + Simulation s; + std::string model_name1 = "test_model1"; + std::string model_name2 = "test_model2"; + std::string new_model_id1 = s.CreateNewModel(model_name1); + std::string new_model_id2 = s.CreateNewModel(model_name2); + ASSERT_EQ(new_model_id1, "1_" + model_name1); + ASSERT_EQ(new_model_id2, "2_" + model_name2); + ASSERT_EQ(s.GetModels().size(), 2); +} + +TEST_F(SimulationTest, CreateModelWithExistingName) { + Simulation s; + std::string model_name = "test_model"; + std::string new_model_id1 = s.CreateNewModel(model_name); + std::string new_model_id2 = s.CreateNewModel(model_name); + ASSERT_EQ(new_model_id1, "1_" + model_name); + ASSERT_EQ(new_model_id2, "2_" + model_name); + ASSERT_EQ(s.GetModels().size(), 2); +} + +TEST_F(SimulationTest, ClearModels) { + Simulation s; + std::string model_name = "test_model"; + s.CreateNewModel(model_name); + ASSERT_EQ(s.GetModels().size(), 1); + s.ClearModels(); + ASSERT_EQ(s.GetModels().size(), 0); +} + +TEST_F(SimulationTest, AddModel) { + Simulation s; + auto mock_model = std::make_unique>(); + auto cloned_model = std::make_unique>(); + EXPECT_CALL(*mock_model, clone()) + .WillOnce(Return(::testing::ByMove(std::move(cloned_model)))); + + s.AddModel(std::move(mock_model)); + ASSERT_EQ(s.GetModels().size(), 1); +} + +TEST_F(SimulationTest, Run) { + Simulation s; + auto mock_model = std::make_unique>(); + auto cloned = std::make_unique>(); + EXPECT_CALL(*cloned, RunTimesteps()).Times(1); + EXPECT_CALL(*mock_model, clone()) + .WillOnce(Return(::testing::ByMove(std::move(cloned)))); + s.AddModel(std::move(mock_model)); + s.Run(); +} + +TEST_F(SimulationTest, RunMultipleModels) { + Simulation s; + auto mock_model = std::make_unique>(); + auto cloned = std::make_unique>(); + EXPECT_CALL(*cloned, RunTimesteps()).Times(1); + EXPECT_CALL(*mock_model, clone()) + .WillOnce(Return(::testing::ByMove(std::move(cloned)))); + s.AddModel(std::move(mock_model)); + + auto mock_model2 = std::make_unique>(); + auto cloned2 = std::make_unique>(); + EXPECT_CALL(*cloned2, RunTimesteps()).Times(1); + EXPECT_CALL(*mock_model2, clone()) + .WillOnce(Return(::testing::ByMove(std::move(cloned2)))); + s.AddModel(std::move(mock_model2)); + + s.Run(); +} + +TEST_F(SimulationTest, GetModels) { + Simulation s; + auto mock_model = std::make_unique>(); + auto cloned = std::make_unique>(); + EXPECT_CALL(*mock_model, clone()) + .WillOnce(Return(::testing::ByMove(std::move(cloned)))); + s.AddModel(std::move(mock_model)); + + const auto &models = s.GetModels(); + ASSERT_EQ(models.size(), 1); +} + +TEST_F(SimulationTest, GetModelNames) { + Simulation s; + std::string model_name1 = "test_model1"; + std::string model_name2 = "test_model2"; + s.CreateNewModel(model_name1); + s.CreateNewModel(model_name2); + + const auto &model_names = s.GetModelNames(); + ASSERT_EQ(model_names.size(), 2); + ASSERT_EQ(model_names[0], model_name1); + ASSERT_EQ(model_names[1], model_name2); +} + +TEST_F(SimulationTest, GetModelHistories) { + Simulation s; + + History history("history1"); + Eigen::VectorXd state0(2); + state0 << 1.0, 2.0; + Eigen::VectorXd state2(2); + state2 << 3.0, 4.0; + history.AddState(state0, 0); + history.AddState(state2, 2); + auto histories = std::map{{"history1", history}}; + + auto mock_model = std::make_unique>(); + auto cloned = std::make_unique>(); + auto *cloned_ptr = cloned.get(); + EXPECT_CALL(*cloned_ptr, GetHistories()).WillOnce(ReturnRef(histories)); + EXPECT_CALL(*mock_model, clone()) + .WillOnce(Return(::testing::ByMove(std::move(cloned)))); + s.AddModel(std::move(mock_model)); + + const auto model_histories = s.GetModelHistories(); + ASSERT_EQ(model_histories.size(), 1); + ASSERT_EQ(model_histories[0].size(), 1); + + const auto history_it = model_histories[0].find("history1"); + ASSERT_NE(history_it, model_histories[0].end()); + ASSERT_EQ(history_it->second.size(), 3); + EXPECT_TRUE(history_it->second[0].isApprox(state0)); + EXPECT_TRUE(history_it->second[1].isApprox(Eigen::VectorXd::Zero(2))); + EXPECT_TRUE(history_it->second[2].isApprox(state2)); +} + +TEST_F(SimulationTest, GetModelSparseHistories) { + Simulation s; + + History history1("history1"); + Eigen::VectorXd state1(2); + state1 << 5.0, 6.0; + history1.AddState(state1, 1); + + History history2("history2"); + Eigen::VectorXd state2(2); + state2 << 7.0, 8.0; + history2.AddState(state2, 3); + + auto histories = std::map{{"history1", history1}, + {"history2", history2}}; + + auto mock_model = std::make_unique>(); + auto cloned = std::make_unique>(); + auto *cloned_ptr = cloned.get(); + EXPECT_CALL(*cloned_ptr, GetHistories()).WillOnce(ReturnRef(histories)); + EXPECT_CALL(*mock_model, clone()) + .WillOnce(Return(::testing::ByMove(std::move(cloned)))); + s.AddModel(std::move(mock_model)); + + const auto sparse_histories = s.GetModelSparseHistories(); + ASSERT_EQ(sparse_histories.size(), 1); + ASSERT_EQ(sparse_histories[0].size(), 2); + + const auto history1_it = sparse_histories[0].find("history1"); + ASSERT_NE(history1_it, sparse_histories[0].end()); + EXPECT_EQ(history1_it->second, history1); + + const auto history2_it = sparse_histories[0].find("history2"); + ASSERT_NE(history2_it, sparse_histories[0].end()); + EXPECT_EQ(history2_it->second, history2); +} + +TEST_F(SimulationTest, GetModelHistoryNames) { + Simulation s; + + auto histories1 = std::map{ + {"history1", History("history1")}, {"history2", History("history2")}}; + auto mock_model1 = std::make_unique>(); + auto cloned1 = std::make_unique>(); + auto *cloned1_ptr = cloned1.get(); + EXPECT_CALL(*cloned1_ptr, GetName()).WillRepeatedly(Return("model1")); + EXPECT_CALL(*cloned1_ptr, GetHistories()).WillOnce(ReturnRef(histories1)); + EXPECT_CALL(*mock_model1, clone()) + .WillOnce(Return(::testing::ByMove(std::move(cloned1)))); + s.AddModel(std::move(mock_model1)); + + auto histories2 = + std::map{{"history3", History("history3")}}; + auto mock_model2 = std::make_unique>(); + auto cloned2 = std::make_unique>(); + auto *cloned2_ptr = cloned2.get(); + EXPECT_CALL(*cloned2_ptr, GetName()).WillRepeatedly(Return("model2")); + EXPECT_CALL(*cloned2_ptr, GetHistories()).WillOnce(ReturnRef(histories2)); + EXPECT_CALL(*mock_model2, clone()) + .WillOnce(Return(::testing::ByMove(std::move(cloned2)))); + s.AddModel(std::move(mock_model2)); + + const auto history_names = s.GetModelHistoryNames(); + const std::vector> expected = { + {"model1", "history1"}, + {"model1", "history2"}, + {"model2", "history3"}, + }; + + ASSERT_EQ(history_names, expected); +} } // namespace testing } // namespace respond \ No newline at end of file diff --git a/tests/unit/timestep_test.cpp b/tests/unit/timestep_test.cpp index f81fa27f..55a128bc 100644 --- a/tests/unit/timestep_test.cpp +++ b/tests/unit/timestep_test.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-07-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-07 // +// Last Modified: 2026-07-09 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -98,6 +98,17 @@ TEST_F(TimestepTest, AddMatrixToTransitionByName) { ASSERT_TRUE(transition->GetMatrices()[0].isApprox(m)); } +TEST_F(TimestepTest, RemoveTransition) { + Timestep ts("test_log", test_log_file_); + ts.CreateTransition("migration"); + ts.CreateTransition("behavior"); + const std::unique_ptr &removed_transition = + ts.RemoveTransition(0); + ASSERT_EQ(removed_transition->GetName(), "migration"); + ASSERT_EQ(ts.GetTransitions().size(), 1); + ASSERT_EQ(ts.GetTransitions()[0]->GetName(), "behavior"); +} + TEST_F(TimestepTest, GetTransitionByIndex) { Timestep ts("test_log", test_log_file_); const std::unique_ptr &transition = From 0b1ca2f0e743ee0b8eb3eaee6ff5009d4d657ca8 Mon Sep 17 00:00:00 2001 From: Matthew Carroll <28577806+MJC598@users.noreply.github.com> Date: Wed, 15 Jul 2026 15:31:11 -0400 Subject: [PATCH 4/4] [Feature] History and UML Update (#146) * thinking through Model updates * Making repo wide changes to match transition and model syntax so project compiles. Tests commented out, no expectation simulation works yet * Removing GetLogName() function from transitions and replacing with protected member. Additional background death tests * cleaning up imports and adding behavior tests * Adding protected functions to transition base to help with testing dimensions * overdose tests * simulation test update * cleaning up copy issues discovered during benchmarking * history changes * fixing sim and history integration * UML updates * doc updates and some identified high risk functional breaks addressed * version bumps * Addressing PR comments --- Doxyfile.in | 2 +- README.md | 21 +- docs/src/api-guide.md | 153 ++++----- docs/src/architecture.md | 68 ++-- docs/src/index.md | 4 +- docs/src/limitations.md | 27 +- docs/src/run.md | 23 +- docs/src/uml.md | 176 +++++++---- extras/benchmark/README.md | 8 +- include/respond/cost_effectiveness.hpp | 4 +- include/respond/history.hpp | 324 ++++++++++++-------- include/respond/model.hpp | 24 +- include/respond/simulation.hpp | 98 ++++-- include/respond/timestep.hpp | 19 +- include/respond/transition.hpp | 31 +- include/respond/version.hpp | 4 +- src/background.cpp | 8 +- src/behavior.cpp | 2 +- src/internals/background.hpp | 2 +- src/internals/behavior.hpp | 2 +- src/internals/intervention.hpp | 2 +- src/internals/markov.hpp | 47 ++- src/internals/migration.hpp | 2 +- src/internals/overdose.hpp | 2 +- src/internals/transition_base.hpp | 20 +- src/intervention.cpp | 2 +- src/logging.cpp | 3 +- src/migration.cpp | 2 +- src/overdose.cpp | 14 +- src/transition_factory.cpp | 4 +- tests/integration/respond_test.cpp | 33 +- tests/mocks/model_mock.hpp | 4 +- tests/mocks/transition_mock.hpp | 10 +- tests/unit/background_test.cpp | 2 +- tests/unit/behavior_test.cpp | 2 +- tests/unit/history_test.cpp | 409 +++++++++++++++++++++---- tests/unit/intervention_test.cpp | 2 +- tests/unit/logging_test.cpp | 12 +- tests/unit/migration_test.cpp | 2 +- tests/unit/overdose_test.cpp | 7 +- tests/unit/simulation_test.cpp | 67 +--- 41 files changed, 1074 insertions(+), 574 deletions(-) diff --git a/Doxyfile.in b/Doxyfile.in index 3332584e..244de801 100644 --- a/Doxyfile.in +++ b/Doxyfile.in @@ -48,7 +48,7 @@ PROJECT_NAME = RESPOND # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2.4.0 +PROJECT_NUMBER = 2.5.0 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/README.md b/README.md index fbe37f55..58973237 100644 --- a/README.md +++ b/README.md @@ -22,21 +22,14 @@ The [original RESPOND model](https://github.com/SyndemicsLab/RESPONDv1/tree/main RESPOND makes full use of the CMake build system. It is a common tool used throughout the C++ user-base and we utilize it for dependency management, linking, and testing. As C++ has poor package management, we intentionally decided to move our focus away from tools such as conan and vcpkg and stay with pure CMake. Not to say we would never publish with such package managers, but it is not a core focus of the refactor/engineering team. -We natively support 5 different build workflows with the `CMakePresets.json` file. They are: - -1. `test-debug-gcc-linux-shared-workflow` -2. `test-debug-gcc-linux-static-workflow` -3. `package-release-gcc-linux-shared-workflow` -4. `package-release-gcc-linux-static-workflow` -5. `benchmark-linux-static-workflow` - -These workflows follow the pattern `{function}-{build}-gcc-linux-{library}-workflow` and have corresponding presets for build, test, and package. As we adopt more operating systems and compilers we will expand beyond gcc and linux. +The project provides configure/build/test/package presets in `CMakePresets.json`. +Common configure presets include `test-debug`, `test-release`, `test-debug-static`, `test-release-static`, `coverage`, `benchmark`, `package-shared`, `package-static`, and `docs`. Overall, we make use of 10 CMake variables. They are found in the [options.cmake file](cmake/options.cmake) and all are set accordingly in the `CMakePresets.json`. ## Dependencies -We make abundant use of the CMake `FetchContent` feature released in CMake 3.11. We utilize features added in CMake 3.24 to check if the package is previously installed, so the minimum required version of CMake is **3.24**. +We make abundant use of the CMake `FetchContent` feature released in CMake 3.11, plus newer preset and dependency features. The minimum required version of CMake is **3.27**. The required dependencies are: @@ -54,7 +47,9 @@ If you would like to clone and build this locally, it is a relatively straightfo ```shell git clone https://github.com/SyndemicsLab/respond.git cd respond -cmake --workflow --preset test-debug-gcc-linux-shared-workflow +cmake --preset test-debug +cmake --build --preset test-debug +ctest --preset test-debug ``` And then the model is build and installed. Our default location is a build directory in the repository, but the CMake Install Directory can be pointed to wherever the user desires. @@ -167,7 +162,9 @@ Open `build/static/docs/doxygen/html/index.html` in a browser. After building with tests enabled: ```shell -cmake --workflow --preset test-debug-gcc-linux-static-workflow +cmake --preset test-debug-static +cmake --build --preset test-debug-static +ctest --preset test-debug-static ``` Tests verify all core components (models, transitions, history tracking). diff --git a/docs/src/api-guide.md b/docs/src/api-guide.md index b9b24866..16af7938 100644 --- a/docs/src/api-guide.md +++ b/docs/src/api-guide.md @@ -8,9 +8,9 @@ The RESPOND library provides a flexible framework for building opioid use disord - **Model**: Abstract base class representing a state transition system - **Simulation**: Aggregates and coordinates multiple models +- **Timestep**: Owns and sequences transitions for one simulation step - **Transition**: Abstract base for specific transition types - **History**: Tracks state vectors over time -- **TransitionFactory**: Creates concrete transition instances ## Core Concepts @@ -20,7 +20,7 @@ Models operate on state vectors (Eigen::VectorXd) representing the population di ### Transitions -Transitions apply transformations to state vectors using transition matrices. The RESPOND model supports several transition types: +Transitions apply transformations to state vectors using transition matrices or vectors. The RESPOND model supports several transition types: - **Migration**: Population movement between states - **Behavior**: Behavioral state changes @@ -30,7 +30,7 @@ Transitions apply transformations to state vectors using transition matrices. Th ### History Tracking -History objects record state vectors at each timestep, enabling analysis of state trajectories over time. Histories support sparse timesteps—gaps are automatically filled with zero vectors. +History objects record state vectors at timesteps, enabling analysis of state trajectories over time. Histories support sparse timesteps and can return contiguous vectors with zero-filled gaps. ## Model Class @@ -38,22 +38,25 @@ The Model class is the abstract base for all models in RESPOND. ```cpp #include +#include +#include // Create a model -auto model = respond::Model::Create("model_name", "logger_name"); +auto model = respond::Model::Create("markov", "logger_name"); // Set the initial state Eigen::VectorXd initial_state(50); initial_state.setZero(); model->SetState(initial_state); -// Add transitions -auto transition = respond::Transition::Create("behavior", "logger_name"); +// Build one timestep with transitions +respond::Timestep step("logger_name"); +auto &transition = step.CreateTransition("behavior"); transition->AddMatrix(some_matrix); -model->AddTransition(transition); +model->AddTimestep(step); // Execute one simulation step -model->RunTransitions(); +model->RunTimestep(); // Retrieve current state Eigen::VectorXd current_state = model->GetState(); @@ -64,17 +67,17 @@ auto histories = model->GetHistories(); ### Key Methods -- `SetState(const Eigen::VectorXd &state)`: Sets the model's state vector (copied internally) -- `GetState() const`: Returns a copy of the current state -- `RunTransitions()`: Executes all registered transitions -- `AddTransition(const std::unique_ptr &t)`: Adds a transition (assumes ownership) -- `GetTransitionNames() const`: Returns names of all transitions -- `ClearTransitions()`: Removes all transitions +- `SetState(const Eigen::Ref &state)`: Sets the model's state vector +- `GetState() const`: Returns a const Eigen ref to the current state +- `AddTimestep(const Timestep ×tep)`: Adds a timestep (deep-copied) +- `RunTimestep()`: Runs the current timestep and advances time +- `RunTimestep(size_t idx)`: Runs a specific timestep index +- `RunTimesteps()`: Runs all registered timesteps (bounded by final timestep when set) +- `ClearTimesteps()`: Removes all timesteps - `GetHistories() const`: Returns map of history name to History objects - `CreateDefaultHistories()`: Initializes default history tracking -- `SetHistories(const std::map &h)`: Sets history records +- `ClearHistories()`: Clears history records and resets history tracking - `GetName() const`: Returns model name -- `GetLogName() const`: Returns associated logger name - `clone() const`: Creates a deep copy of the model ## Simulation Class @@ -93,28 +96,31 @@ auto model2 = respond::Model::Create("model2", "my_logger"); sim.AddModel(model1); sim.AddModel(model2); -// Run one step (executes all model transitions) -sim.Run(); +// Run 52 timesteps for all models +sim.Run(52); // Retrieve results -auto all_histories = sim.GetModelHistories(); +auto model_0_histories = sim.GetModelHistory(0); auto model_names = sim.GetModelNames(); -// Get detailed history mapping -auto history_names = sim.GetModelHistoryNames(); -// Returns vector of (model_name, history_name) pairs +// Get history names for one model +auto history_names = sim.GetModelHistoryNames(0); ``` ### Key Methods -- `Run()`: Executes one simulation step for all models +- `Run(int duration = -1)`: Runs all models for the configured duration +- `SetDuration(int duration)`: Sets default duration used by `Run()` when no argument is provided - `AddModel(const std::unique_ptr &model)`: Adds a model (cloned internally) - `GetModels() const`: Returns const reference to model vector +- `GetModel(size_t idx) const`: Returns one model by index +- `GetModel(const std::string &name) const`: Returns one model by name - `GetModelNames() const`: Returns all model names - `ClearModels()`: Removes all models -- `GetModelHistories() const`: Returns state histories for all models -- `GetModelHistoryNames() const`: Returns (model_name, history_name) pairs -- `GetLogName() const`: Returns logger name +- `GetModelHistory(size_t idx) const`: Returns one model's history map +- `GetModelHistory(const std::string &name) const`: Returns one model's history map +- `GetModelHistoryNames(size_t idx) const`: Returns history names for one model +- `GetModelHistoryNames(const std::string &name) const`: Returns history names for one model ## History Class @@ -139,8 +145,8 @@ auto state_at_t0 = hist.GetStateMap()[0]; auto all_states = hist.GetStateAsVector(); // Contiguous vector, fills gaps // Query history properties -std::string name = hist.GetHistoryName(); -std::string log_name = hist.GetLogName(); +std::string name = hist.GetName(); +respond::HistoryMode mode = hist.GetHistoryMode(); // Clear history hist.Clear(); @@ -152,40 +158,43 @@ hist.Clear(); - If timestep < 0, automatically assigns next available timestep - If timestep already exists, currently overwrites - `GetStateMap() const`: Returns map of timestep → state vector +- `GetRecordedTimesteps() const`: Returns stored timesteps without densifying +- `GetRecordedStates() const`: Returns stored states without densifying - `GetStateAsVector() const`: Returns contiguous vector of states (fills gaps with zeros) -- `GetHistoryName() const`: Returns history identifier -- `GetLogName() const`: Returns logger name +- `GetName() const`: Returns history identifier +- `GetLatestRecordedTimestep() const`: Returns latest recorded timestep +- `GetPendingState() const`: Returns pending aggregate for accumulated histories +- `HasPendingState() const`: Indicates pending aggregate state - `Clear()`: Removes all recorded states - `operator==`, `operator!=`: Comparison operators ## Transition Class -The Transition class is abstract; use TransitionFactory to create concrete instances. +The Transition class is abstract; use `Transition::Create(...)` to create concrete instances. ```cpp #include -#include -// Create a transition using the factory +// Create a transition auto transition = respond::Transition::Create( - "behavior", // Type: migration, behavior, intervention, overdose, background_death - "my_logger" // Logger name + "behavior", // Type + "behavior_name", // Instance name + "my_logger" // Logger name ); // Add transformation matrices Eigen::MatrixXd trans_matrix = ...; transition->AddMatrix(trans_matrix); -// Execute the transition (typically done via Model::RunTransitions) +// Execute the transition (typically done by model timesteps) auto histories_map = ...; // From model Eigen::VectorXd result = transition->Execute(current_state, histories_map); // Get transition properties -std::string name = transition->GetTransitionName(); -std::string log = transition->GetLogName(); +std::string name = transition->GetName(); // Clear matrices -transition->ClearTransitionMatrices(); +transition->ClearMatrices(); ``` ### Supported Transition Types @@ -216,7 +225,7 @@ respond::CreateFileLogger("my_logger", "path/to/logfile.log"); ```cpp #include #include -#include +#include #include int main() { @@ -227,35 +236,36 @@ int main() { respond::Simulation sim("app"); // Create and configure a model - auto model = respond::Model::Create("population_model", "app"); + auto model = respond::Model::Create("markov", "app"); // Set initial state (e.g., 1000 individuals across 50 states) Eigen::VectorXd initial_state = Eigen::VectorXd::Zero(50); initial_state(0) = 1000; // All in first state model->SetState(initial_state); - // Add transitions - auto behavior_transition = respond::Transition::Create( - "behavior", "app"); - // Add matrices... - model->AddTransition(behavior_transition); + // Create a reusable timestep with transitions + respond::Timestep step("app"); + + auto &behavior_transition = step.CreateTransition("behavior"); + // behavior_transition->AddMatrix(...); + + auto &migration_transition = step.CreateTransition("migration"); + // migration_transition->AddMatrix(...); - auto migration_transition = respond::Transition::Create( - "migration", "app"); - // Add matrices... - model->AddTransition(migration_transition); + // Register timesteps on the model + for (int t = 0; t < 52; ++t) { + model->AddTimestep(step); + } // Add model to simulation sim.AddModel(model); // Run simulation for 52 timesteps - for (int t = 0; t < 52; ++t) { - sim.Run(); - } + sim.Run(52); // Extract results - auto histories = sim.GetModelHistories(); - auto history_names = sim.GetModelHistoryNames(); + auto histories = sim.GetModelHistory(0); + auto history_names = sim.GetModelHistoryNames(0); // Process results... @@ -269,17 +279,16 @@ RESPOND uses `std::unique_ptr` for ownership management: - Models and Transitions are typically managed by Simulation or parent objects - History objects are copyable and can be freely copied -- All models are cloned when added to a Simulation (ownership transfer) -- Clearing containers (ClearModels, ClearTransitions) deletes contained objects +- All models are cloned when added to a Simulation +- Clearing containers (for example `ClearModels`, `ClearTimesteps`) deletes contained objects ## Best Practices -1. **Use TransitionFactory** to create transitions—it handles type dispatch -2. **Let Simulation manage models** for automatic cloning and lifecycle management -3. **Reuse History objects** for multiple runs to accumulate results -4. **Use const references** where available (GetState returns a copy for safety) -5. **Initialize loggers early** before creating models to enable error tracking -6. **Validate matrix dimensions** before adding to transitions (not checked by API) +1. **Use `Transition::Create`** to create transitions by type. +2. **Build timesteps explicitly** and add them to models in execution order. +3. **Set simulation duration intentionally** (`SetDuration` or `Run(duration)`) to match timestep plans. +4. **Initialize loggers early** before creating models and transitions. +5. **Validate matrix dimensions and ranges** before adding matrices. ## Common Patterns @@ -289,13 +298,11 @@ RESPOND uses `std::unique_ptr` for ownership management: for (int run = 0; run < num_runs; ++run) { respond::Simulation sim("logger_" + std::to_string(run)); - auto model = respond::Model::Create("model", "logger_" + std::to_string(run)); + auto model = respond::Model::Create("markov", "logger_" + std::to_string(run)); // Configure model... sim.AddModel(model); - for (int t = 0; t < duration; ++t) { - sim.Run(); - } + sim.Run(duration); // Store results... } @@ -308,8 +315,8 @@ for (int run = 0; run < num_runs; ++run) { Eigen::VectorXd initial_state = ...; model->SetState(initial_state); -// To also clear history -model->ClearTransitions(); +// To also clear history and timesteps +model->ClearTimesteps(); model->CreateDefaultHistories(); ``` @@ -368,20 +375,18 @@ void RunSimulation(int id, const std::string& log_file) { // Create and run simulation respond::Simulation sim(logger_name); - auto model = respond::Model::Create("model", logger_name); + auto model = respond::Model::Create("markov", logger_name); // Configure model... Eigen::VectorXd initial_state = Eigen::VectorXd::Zero(50); initial_state(0) = 1000; model->SetState(initial_state); - // Add transitions... + // Add timesteps... sim.AddModel(model); // Run simulation - for (int t = 0; t < 52; ++t) { - sim.Run(); - } + sim.Run(52); // Flush logs for this model respond::FlushAllLoggers(); diff --git a/docs/src/architecture.md b/docs/src/architecture.md index a514af6b..75ce7ce6 100644 --- a/docs/src/architecture.md +++ b/docs/src/architecture.md @@ -13,32 +13,25 @@ RESPOND follows the **inversion of control** principle, abstracting the model to ## Component Architecture -``` -┌─────────────────────────────────────────────────┐ -│ Simulation │ -│ (aggregates and coordinates Models) │ -└──────────────────┬──────────────────────────────┘ - │ - ┌──────────┴──────────┬──────────────┐ - │ │ │ - ┌───▼────┐ ┌───▼────┐ ┌──▼────┐ - │ Model │ │ Model │ │ Model │ - │ (PopA) │ │ (PopB) │ │(PopC) │ - └───┬────┘ └───┬────┘ └──┬────┘ - │ │ │ - ├─ Transitions ────┐ │ │ - │ - Migration │ │ │ - │ - Behavior │ │ │ - │ - Intervention │ │ │ - │ - Overdose │ │ │ - │ - Background │ │ │ - └──────────────────┘ │ │ - │ │ │ - └─ Histories ────┐ │ │ - - State │ │ │ - - Outcomes │ │ │ - - Costs │ │ │ - └────────────┘ └─────────────┘ +```mermaid +flowchart TB + S[Simulation\naggregates and coordinates models] + + M1[Model PopA] + M2[Model PopB] + M3[Model PopC] + + S --> M1 + S --> M2 + S --> M3 + + TS1[Timesteps\n- Step 0\n- Step 1\n- ...] + TR1[Transitions per step\n- Migration\n- Behavior\n- Intervention\n- Overdose\n- BackgroundDeath] + H1[Histories\n- State\n- Outcomes\n- Costs] + + M1 --> TS1 + TS1 --> TR1 + M1 --> H1 ``` ## Core Classes @@ -48,13 +41,13 @@ RESPOND follows the **inversion of control** principle, abstracting the model to - **Role**: Represents a state transition system - **Responsibilities**: - Manages state vector - - Owns and executes transitions + - Owns and executes timesteps - Tracks history - Provides cloning capability - **Key Design Decisions**: - Non-copyable by assignment (enforces `clone()` usage for clarity) - - Owns transitions (unique_ptr for memory safety) - - Read-only GetState() (returns copy to prevent external state modification) + - Owns timesteps by value (timestep owns transition instances) + - Read-only GetState() (returns a const Eigen ref for observation) ### Simulation @@ -106,7 +99,7 @@ RESPOND follows the **inversion of control** principle, abstracting the model to - Encapsulates type dispatch logic - Provides single point of extensibility for new transitions - **Key Design Decisions**: - - Static factory method (no factory state needed) + - Static creation on `Transition::Create(...)` (no factory state needed) - String-based type identification (simple, extensible) - Case-insensitive type matching (user-friendly) @@ -125,14 +118,15 @@ auto transition = Transition::Create("behavior", "logger"); - Centralizes type dispatch logic - Easy to add new transition types -### Template Method Pattern (Model → Transitions) +### Template Method Pattern (Model → Timesteps → Transitions) -Model delegates to transitions in RunTransitions(): +Model delegates execution through timesteps, and each timestep applies its transitions in sequence: ```cpp -void Model::RunTransitions() { - for (const auto& transition : _transitions) { - _state = transition->Execute(_state, _histories); +void Model::RunTimestep(size_t idx) { + auto transitions = _timestep_vector[idx].GetTransitions(); + for (const auto& transition : transitions) { + _state = transition->Execute(_state, _histories); } } ``` @@ -178,7 +172,7 @@ RESPOND uses modern C++ memory management practices: ### Unique Ownership (unique_ptr) Used for objects with clear ownership: -- Model owns its Transitions +- Timestep owns its Transitions - Simulation owns its Models (via cloning) ### Shared Ownership (None by default) @@ -203,7 +197,7 @@ RESPOND minimizes shared state. History objects are the exception—they're: ### Adding a New Transition Type -1. Create a new header in `include/respond/internals/` +1. Create a new header in `src/internals/` 2. Implement concrete Transition subclass 3. Add factory entry in `Transition::Create()` diff --git a/docs/src/index.md b/docs/src/index.md index 5776959d..e0c01388 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -16,7 +16,7 @@ The [original RESPOND model](https://github.com/SyndemicsLab/RESPONDv1/tree/main RESPOND makes full use of the CMake build system. It is a common tool used throughout the C++ user-base and we utilize it for dependency management, linking, and testing. As C++ has poor package management, we intentionally decided to move our focus away from tools such as conan and vcpkg and stay with pure CMake. Not to say we would never publish with such package managers, but it is not a core focus of the refactor/engineering team. -Currently our CMake supports 10 different workflows. They are named using the following convention: `----workflow`. Please consult the CMakePresets.json or run `cmake --workflow --list-presets` to see the entire list. +Current CMake support is provided through configure/build/test/package presets in `CMakePresets.json` (for example `test-debug`, `test-release`, `test-debug-static`, `benchmark`, `package-shared`, `package-static`, and `docs`). Please consult `CMakePresets.json` or run `cmake --list-presets` to see the available options. ### Fetch Content @@ -36,7 +36,7 @@ FetchContent_MakeAvailable(respond) ## Dependencies -As mentioned above, we utilize the CMake `FetchContent` feature released in CMake 3.11. Additionally, we utilize features added in CMake 3.24 to check if the package is previously installed, so the minimum required version of CMake is **3.24**. +As mentioned above, we utilize the CMake `FetchContent` feature released in CMake 3.11. We also rely on newer preset and dependency features, so the minimum required version of CMake is **3.27**. The required dependencies are: diff --git a/docs/src/limitations.md b/docs/src/limitations.md index 455369e1..79a986b5 100644 --- a/docs/src/limitations.md +++ b/docs/src/limitations.md @@ -1,29 +1,24 @@ # Limitations -Currently the RESPOND model and overall Simdemics Modeling Library are in heavy development. This means they have A LOT of current limitations. Any requests should be directed to the issues page on the github repository and the maintainers will work on them as permits. That being said, some readily identified limitations that have been identified are: +RESPOND is under active development. The following limitations reflect the current C++ library behavior. ## Library -The code base started as a specific model and many of the library limitations are due to old code and notations being maintained. That being said, this is an area of continuous improvement so watch for updates: +The core API is stable enough for integration, but there are important constraints: -- `DataLoader` currently only supports RESPOND -- The RESPOND model currently requires `DataLoader` -- `DataLoader` makes use of the csv file structure -- Specific file names are required for `DataLoader` -- Column names must be exact -- Behavior and intervention names must be underscored instead of spaces or dashes -- The model cannot be run on a GPU -- Windows hangs during unit tests -- There is no generalized linux build -- Installation requires adding the respond cmake folder path to the `CMAKE_PREFIX_PATH` +- `Model::Create(...)` currently returns the Markov implementation; additional model families are not yet exposed through the public factory. +- Execution is timestep-driven; users must construct timesteps and transitions explicitly. +- Transition creation is string-based (`Transition::Create(...)`), so invalid type names fail at runtime. +- The library is not internally synchronized for shared mutable use across threads. +- GPU execution is not supported. +- Legacy standalone executable workflows are maintained separately from the modern library API. ## Data -Currently, there are a lot of limitations in the required data structure. +RESPOND C++ focuses on simulation primitives (state vectors, transitions, histories) rather than built-in dataset ingestion. -- `sim.conf` timestep lists must contain the duration as the final value in the list -- All oud columns should be renamed to behaviors -- The Demographic structure still exists in the data without any impact to the model +- Users are responsible for preparing and validating transition inputs (matrices/vectors) before simulation. +- Schema conventions from legacy tooling (for example `sim.conf` and CSV pipelines) are not part of the required core C++ API. Previous: [Under the Hood](math.md) diff --git a/docs/src/run.md b/docs/src/run.md index c5e81be1..27d92d97 100644 --- a/docs/src/run.md +++ b/docs/src/run.md @@ -20,13 +20,15 @@ For C++ developers, RESPOND can be integrated directly into projects. See the [C ```cpp #include #include +#include +#include int main() { // Create a simulation respond::Simulation sim("my_logger"); // Create and configure a model - auto model = respond::Model::Create("population_model", "my_logger"); + auto model = respond::Model::Create("markov", "my_logger"); // Set initial state Eigen::VectorXd initial_state(50); @@ -34,21 +36,24 @@ int main() { initial_state(0) = 1000; // 1000 individuals in state 0 model->SetState(initial_state); - // Add transitions - auto transition = respond::Transition::Create( - "behavior", "my_logger"); - model->AddTransition(transition); + // Build one timestep and attach transitions + respond::Timestep step("my_logger"); + auto &transition = step.CreateTransition("behavior"); + // transition->AddMatrix(...); + + // Add timesteps to the model + for (int t = 0; t < 52; ++t) { + model->AddTimestep(step); + } // Add model to simulation sim.AddModel(model); // Run simulation for 52 timesteps - for (int t = 0; t < 52; ++t) { - sim.Run(); - } + sim.Run(52); // Extract results - auto histories = sim.GetModelHistories(); + auto histories = sim.GetModelHistory(0); return 0; } diff --git a/docs/src/uml.md b/docs/src/uml.md index b27baacb..8f32d525 100644 --- a/docs/src/uml.md +++ b/docs/src/uml.md @@ -13,93 +13,144 @@ classDiagram direction LR class Simulation { - +CreateNewModel(type) shared_ptr~Model~ - +AddNewModel(shared_ptr~Model~) bool - +Run() - +GetModel(size_t model_idx) const Model & + +Simulation() + +Simulation(const string &) + +Simulation(const string &, const string &) + +Simulation(const Simulation &) + +operator=(const Simulation &) Simulation& + +Simulation(Simulation &&other) + +operator=(Simulation &&) Simulation& + +CreateNewModel(const string &) const string + +ClearModels() + +AddModel(const unique_ptr~Model~) + +Run(int=-1) + +GetModels() const vector~unique_ptr~Model~~ & + +GetModel(size_t model_idx) const unique_ptr~Model~ & + +GetModel(const string &) const unique_ptr~Model~ & +GetModelNames() vector~string~ - +GetModelHistory(size_t model_idx) map~string, History~ + +GetModelHistory(size_t model_idx) const map~string, History~ & + +GetModelHistory(const string &) const map~string, History~ & + +GetModelHistoryNames(size_t idx) vector~string~ + +GetModelHistoryNames(const string &) vector~string~ + +SetDuration(int) +operator<<(ostream &os, const Simulation &obj) ostream & } class Model { <> - +SetState(state) * - +GetState() VectorXd * - +AddTimestep(shared_ptr~timestep~) * - +GetTimesteps() * + +Create(const string &, const string &, const string &) unique_ptr~Model~ + +clone() unique_ptr~Model~ * + +AddTimestep(const Timestep &) * + +RunTimestep() * + +RunTimestep(size_t) * +RunTimesteps() * +ClearTimesteps() * - +GetHistories() map~string, History~ * +ClearHistories() * +CreateDefaultHistories() * - +SetFinalTimestep(final_timestep) * - +clone() unique_ptr~Model~ * - +Create(name, log_name) unique_ptr~Model~ + +GetTimestepAtIndex(size_t) Timestep * + +GetState() Ref~const VectorXd~ * + +GetName() string * + +GetHistories() map~string, History~ * + +GetTimestep() int * + +GetHistoryCaptureInterval() int * + +GetFinalTimestep() int * + +GetInitialHistoryRecorded() bool * + +SetState(const Ref~const VectorXd~ &) * + +SetHistoryCaptureInterval(int) * + +SetFinalTimestep(int) * + +SetInitialHistoryRecorded(bool) * + +Serialize(ostream &) * +operator<<(ostream &os, const Model &obj) ostream & } class Timestep { +Timestep() - +Timestep(const string &log_name) - +Timestep(const string &log_name, const string &log_filepath) - +Timestep(const Timestep &other) - +operator=(const Timestep &other) Timestep & - +Timestep(const Timestep &&other) - +operator=(const Timestep &&other) Timestep & - +CreateTransition(type) const Transition & - +AddMatrixToTransition(size_t index, MatrixXd mat) - +GetTransition(size_t idx) const Transition & - +GetTransition(string name) const Transition & - +GetTransitions() const vector~const Transition &~ - +GetTransitionNames() vector~const string~ + +Timestep(const string &) + +Timestep(const string &, const string &) + +Timestep(const Timestep &) + +operator=(const Timestep &) Timestep & + +Timestep(const Timestep &&) + +operator=(const Timestep &&) Timestep & + +CreateTransition(const string &) const unique_ptr~Transition~ & + +RemoveTransition(size_t) unique_ptr~Transition~ + +AddMatrixToTransition(const size_t &, const Ref~const MatrixXd~ &) + +AddMatrixToTransition(const string &, const Ref~const MatrixXd~ &) + +GetTransition(const size_t &) const unique_ptr~Transition~ & + +GetTransition(const string &) const unique_ptr~Transition~ & + +GetTransitions() vector~unique_ptr~Transition~~ + +GetTransitionNames() vector~string~ +operator<<(ostream &os, const Timestep &obj) ostream & + +operator==(const Timestep &, const Timestep &) bool + +operator!=(const Timestep &, const Timestep &) bool } class Transition { <> - +Execute(state, histories) VectorXd * - +AddMatrix(const Eigen::Ref~const MatrixXd~ &matrix, size_t idx) * - +GetMatrix(size_t idx) Eigen::Ref~MatrixXd~ * + +Execute(const Ref Vector &, map~string, History~ &) VectorXd * + +AddMatrix(Eigen::Ref~const MatrixXd~) * + +GetMatrices() vector~MatrixXd~ * +GetName() string * +ClearMatrices() * +clone() unique_ptr~Transition~ * - +Create(type, log_name) unique_ptr~Transition~ - +operator<<(ostream &os, const Transition &obj) ostream & + +Create(const string &, const string &, const string &, const string &) unique_ptr~Transition~ + +Serialize(ostream &) * + +operator<<(ostream &, const Transition &) ostream & } class History { - +AddState(state, timestep) - +RecordSnapshot(state, timestep) - +AccumulateState(state) - +FlushPendingState(timestep, state_size) + +History() + +History(const string &) + +History(const string &, const HistoryMode &) + +History(const string &, const HistoryMode &, const string &) + +History(const string &, const string &) + +History(const string &, const string &, const string &) + +History(const string &, const HistoryMode &, const string &, const string &) + +History(const History &) + +operator=(const History &) History & + +History(History &&) + +operator=(History &&) History & + +AddState(const Ref~const VectorXd~ &, int) + +AccumulateState(const Ref~const VectorXd~ &) + +FlushPendingState(int, Index) + +Clear() + +HasPendingState() bool +GetStateMap() map~int, VectorXd~ + +GetRecordedTimesteps() const vector~int~ & + +GetRecordedStates() const vector~VectorXd~ & + +GetHistoryMode() HistoryMode + +GetPendingState() VectorXd + +GetLatestRecordedTimestep() int + +GetName() string +GetStateAsVector() vector~VectorXd~ + +operator==(const History &) bool + +operator!=(const History &) bool +operator<<(ostream &os, const History &obj) ostream & + -GetNextTimestep() int + -GetZeroVector(const int &) VectorXd } class HistoryMode { <> - Snapshot - Accumulated + kSnapshot + kAccumulated } class LoggingAPI { <> - +CreateFileLogger(name, filepath) - +CreateSharedFileSink(filepath) - +CreateSharedLogger(name) - +SetLogPattern(pattern) - +GetLogPattern() - +SetFlushInterval(seconds) + +CreateFileLogger(const string &, const string &) + +CreateSharedFileSink(const string &) + +CreateSharedLogger(const string &) + +SetLogPattern(LogPattern) + +GetLogPattern() LogPattern + +SetFlushInterval(int) +FlushAllLoggers() - +LogInfo(name, message) - +LogWarning(name, message) - +LogError(name, message) - +LogDebug(name, message) - +CheckLoggerExists(name) - +GetLoggerInfo(name) - +SetLoggerLevel(name, level) + +LogInfo(const string &, const string &) + +LogWarning(const string &, const string &) + +LogError(const string &, const string &) + +LogDebug(const string &, const string &) + +CheckLoggerExists(const string &) + +GetLoggerInfo(const string &) + +SetLoggerLevel(const string &, int) } class LogType { @@ -160,13 +211,13 @@ classDiagram class Simulation { -string _log_name - -vector~shared_ptr~Model~~ _models - -size_t _duration - -vector~size_t~ _parameter_change_times + -vector~unique_ptr~Model~~ _models + -int _duration + -vector~int~ _parameter_change_times -bool _stratify_entering_cohort -bool _build_summary_stats -bool _save_state_history - -vector~size_t~ _timesteps_to_report + -vector~int~ _timesteps_to_report -bool _pivot_long +Simulation() +Simulation(const string log_name) @@ -182,7 +233,7 @@ classDiagram } class Markov { - -vector~shared_ptr~Transition~~ _transition_vector + -vector~Timestep~ _timestep_vector -VectorXd _state -string _name -string _log_name @@ -204,10 +255,12 @@ classDiagram +operator=(Markov &&other) Markov & +SetState(state) override +GetState() VectorXd override - +AddTimestep(shared_ptr~timestep~) override - +GetTimesteps() override + +AddTimestep(const Timestep &) override + +GetTimestepAtIndex(size_t) Timestep override +ClearTimesteps() override - +RunTransitions() override + +RunTimestep() override + +RunTimestep(size_t) override + +RunTimesteps() override +GetHistories() map~string, History~ override +ClearHistories() override +CreateDefaultHistories() override @@ -224,11 +277,11 @@ classDiagram -string _name -string _log_name -vector~MatrixXd~ _transition_matrices - -GetMatrices() const vector & - +AddMatrix(const Eigen::Ref~const MatrixXd~ &matrix, size_t idx) override + +GetMatrices() const vector~MatrixXd~ + +AddMatrix(const Eigen::Ref~const MatrixXd~ &) override +GetName() string override +ClearMatrices() override - +GetLogName() string override + +Serialize(ostream &) override } class Migration { @@ -282,7 +335,8 @@ classDiagram TransitionBase <|-- Overdose : implements TransitionBase <|-- BackgroundDeath : implements - Markov *-- "0..*" Transition : owns + Markov *-- "0..*" Timestep : owns + Timestep *-- "0..*" Transition : owns Markov *-- "0..*" History : owns Migration <.. History : uses Behavior <.. History : uses diff --git a/extras/benchmark/README.md b/extras/benchmark/README.md index 308c9297..e34f1b0e 100644 --- a/extras/benchmark/README.md +++ b/extras/benchmark/README.md @@ -14,7 +14,7 @@ This benchmark target is designed for reproducible performance measurement of co The benchmark constructs one RESPOND model and measures repeated execution of: -- `Model::RunTransitions()` for a fixed number of timesteps +- `Simulation::Run(steps)` / `Model::RunTimesteps()` for a fixed number of timesteps - Transition mix: behavior, intervention, overdose, background death - Deterministic transition matrices/vectors and deterministic initial state @@ -25,14 +25,14 @@ Warm-up runs are excluded from reported timings. Enable benchmark builds in CMake: ```bash -cmake -S . -B build/bench -DRESPOND_BUILD_BENCH=ON -cmake --build build/bench --target respond_benchmark +cmake --preset benchmark +cmake --build --preset benchmark ``` ## Run ```bash -./build/bench/bin/respond_benchmark \ +./build/shared/bin/respond_benchmark \ --state-size 64 \ --steps 52 \ --history-capture-interval 1 \ diff --git a/include/respond/cost_effectiveness.hpp b/include/respond/cost_effectiveness.hpp index f7e49442..c5a2f8a9 100644 --- a/include/respond/cost_effectiveness.hpp +++ b/include/respond/cost_effectiveness.hpp @@ -4,7 +4,7 @@ // Created Date: 2025-08-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-25 // +// Last Modified: 2026-07-09 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center // @@ -13,8 +13,6 @@ #ifndef RESPOND_COSTEFFECTIVENESS_HPP_ #define RESPOND_COSTEFFECTIVENESS_HPP_ -#include - #include #include diff --git a/include/respond/history.hpp b/include/respond/history.hpp index 34eaedf3..9a73ab7e 100644 --- a/include/respond/history.hpp +++ b/include/respond/history.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-30 // +// Last Modified: 2026-07-13 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -12,6 +12,9 @@ #ifndef RESPOND_HISTORY_HPP_ #define RESPOND_HISTORY_HPP_ +#include +#include + #include #include #include @@ -20,14 +23,25 @@ #include namespace respond { -enum class HistoryMode { Snapshot, Accumulated }; +/// @brief Defines the mode of history recording for state vectors in a +/// simulation. +enum class HistoryMode : int { + kSnapshot = 0, // Snapshot of state at each timestep + kAccumulated = 1, // Accumulated contributions over timesteps + kCount = 2 // Enum Counter +}; + +/// @brief Determines the default history mode based on the history name. +/// @param name The name of the history to evaluate. +/// @return HistoryMode::kAccumulated for specific names, otherwise +/// HistoryMode::kSnapshot. inline HistoryMode GetDefaultHistoryMode(const std::string &name) { if (name == "intervention_admission" || name == "total_overdose" || name == "fatal_overdose" || name == "background_death") { - return HistoryMode::Accumulated; + return HistoryMode::kAccumulated; } - return HistoryMode::Snapshot; + return HistoryMode::kSnapshot; } /// @brief Tracks and manages state vector history over time. @@ -36,34 +50,75 @@ inline HistoryMode GetDefaultHistoryMode(const std::string &name) { /// are filled with zero vectors). class History { public: + //////////////////////////////////////////////////////////////////////////// + // + // Rule of Five: Copy and Move Semantics + // + //////////////////////////////////////////////////////////////////////////// + + /// @brief Default constructor initializing a history with the default name + /// "state" and default mode based on that name. History() : History("state") {} - History(const std::string &name) : History(name, "console") {} - /// @brief Constructs a History tracker. - /// @param name The identifier for this history (default: "state"). - /// @param log_name The logger name for error reporting (default: - /// "console"). + /// @brief Constructs a history with a specified name, using the default + /// mode based on that name. + /// @param name The identifier name for this history instance. + History(const std::string &name) + : History(name, GetDefaultHistoryMode(name)) {} + + /// @brief Constructs a history with a specified name and mode. + /// @param name The identifier name for this history instance. + /// @param mode The history recording mode (snapshot or accumulated). + History(const std::string &name, const HistoryMode &mode) + : History(name, mode, RESPOND_DEFAULT_LOG, RESPOND_DEFAULT_LOG_FILE) {} + + /// @brief Constructs a history with a specified name, mode, and logger. + /// @param name The identifier name for this history instance. + /// @param mode The history recording mode (snapshot or accumulated). + /// @param log_name The name of the logger to use for history output. + History(const std::string &name, const HistoryMode &mode, + const std::string &log_name) + : History(name, mode, log_name, RESPOND_DEFAULT_LOG_FILE) {} + + /// @brief Constructs a history with a specified name and logger. + /// @param name The identifier name for this history instance. + /// @param log_name The name of the logger to use for history output. History(const std::string &name, const std::string &log_name) - : History(name, log_name, GetDefaultHistoryMode(name)) {} - - /// @brief Constructs a History tracker with an explicit recording mode. - /// @param name The identifier for this history. - /// @param log_name The logger name for error reporting. - /// @param mode Whether the history stores snapshots or accumulations. + : History(name, GetDefaultHistoryMode(name), log_name, + RESPOND_DEFAULT_LOG_FILE) {} + + /// @brief Constructs a history with a specified name, logger, and log + /// file path. + /// @param name The identifier name for this history instance. + /// @param log_name The name of the logger to use for history output. + /// @param log_filepath The file path for the logger output. History(const std::string &name, const std::string &log_name, - HistoryMode mode) - : _log_name(log_name), _name(name), _mode(mode) {} + const std::string &log_filepath) + : History(name, GetDefaultHistoryMode(name), log_name, log_filepath) {} + + /// @brief Constructs a history with a specified name, mode, logger, and + /// log file path. + /// @param name The identifier name for this history instance. + /// @param mode The history recording mode (snapshot or accumulated). + /// @param log_name The name of the logger to use for history output. + /// @param log_filepath The file path for the logger output. + History(const std::string &name, const HistoryMode &mode, + const std::string &log_name, const std::string &log_filepath) + : _name(name), _mode(mode), _log_name(log_name) { + CreateFileLogger(log_name, log_filepath); + } /// @brief Destructor (default). ~History() = default; + /// @brief Copy constructor implementing the Rule of Five. /// Creates an independent copy of the history state and metadata. History(const History &other) { _timesteps = other.GetRecordedTimesteps(); _states = other.GetRecordedStates(); - _name = other.GetHistoryName(); - _log_name = other.GetLogName(); - _mode = other.GetHistoryMode(); + _name = other._name; + _log_name = other._log_name; + _mode = other._mode; _pending_state = other.GetPendingState(); } @@ -74,9 +129,9 @@ class History { if (this != &other) { _timesteps = other.GetRecordedTimesteps(); _states = other.GetRecordedStates(); - _name = other.GetHistoryName(); - _log_name = other.GetLogName(); - _mode = other.GetHistoryMode(); + _name = other._name; + _log_name = other._log_name; + _mode = other._mode; _pending_state = other.GetPendingState(); } return *this; @@ -88,9 +143,9 @@ class History { History(History &&other) noexcept { _timesteps = std::move(other._timesteps); _states = std::move(other._states); - _name = other.GetHistoryName(); - _log_name = other.GetLogName(); - _mode = other.GetHistoryMode(); + _name = other._name; + _log_name = other._log_name; + _mode = other._mode; _pending_state = std::move(other._pending_state); } @@ -101,29 +156,106 @@ class History { if (this != &other) { _timesteps = std::move(other._timesteps); _states = std::move(other._states); - _name = other.GetHistoryName(); - _log_name = other.GetLogName(); - _mode = other.GetHistoryMode(); + _name = other._name; + _log_name = other._log_name; + _mode = other._mode; _pending_state = std::move(other._pending_state); } return *this; } - /// @brief Equality comparison operator. - /// @param other The history to compare with. - /// @return True if all history properties and state are identical. - bool operator==(const History &other) const { - return GetHistoryName() == other.GetHistoryName() && - GetLogName() == other.GetLogName() && - GetHistoryMode() == other.GetHistoryMode() && - GetStateMap() == other.GetStateMap() && - GetPendingState().isApprox(other.GetPendingState()); + //////////////////////////////////////////////////////////////////////////// + // + // History Methods: State Vector Management + // + //////////////////////////////////////////////////////////////////////////// + + /// @brief Records a state vector at a specific or automatic timestep. + /// @param state The state vector to record. + /// @param timestep The timestep index for this state (default: -1 for + /// automatic next timestep). If timestep is negative, the next sequential + /// timestep is used automatically. If timestep already exists, it is + /// considered invalid but is currently overwritten. + void AddState(const Eigen::Ref &state, + int timestep = -1) { + if (timestep < 0) { + timestep = GetNextTimestep(); + } + + const auto existing = + std::find(_timesteps.begin(), _timesteps.end(), timestep); + if (existing != _timesteps.end()) { + const auto index = + static_cast(existing - _timesteps.begin()); + _states[index] = state; + return; + } + + _timesteps.push_back(timestep); + _states.push_back(state); } - /// @brief Inequality comparison operator. - /// @param other The history to compare with. - /// @return True if histories differ in any aspect. - bool operator!=(const History &other) const { return !(*this == other); } + /// @brief Adds a contribution to an accumulated history. + /// @param state The per-step contribution to accumulate. + void AccumulateState(const Eigen::Ref &state) { + if (_mode != HistoryMode::kAccumulated) { + LogWarning(_log_name, "AccumulateState called on non-accumulated " + "history, adding state instead: " + + _name); + AddState(state); + return; + } + + if (_pending_state.size() == 0) { + _pending_state = state; + return; + } + _pending_state += state; + } + + /// @brief Flushes pending accumulated state into a recorded timestep. + /// @param timestep The simulation timestep to record. + /// @param state_size Size of a zero vector to record if nothing is pending. + void FlushPendingState(int timestep, Eigen::Index state_size) { + if (_mode != HistoryMode::kAccumulated) { + LogInfo(_log_name, + "FlushPendingState called on non-accumulated history, " + "no pending state to flush: " + + _name); + return; + } + + Eigen::VectorXd value; + if (_pending_state.size() > 0) { + value = _pending_state; + } else { + LogInfo(_log_name, + "FlushPendingState called with no pending state, " + "recording zero vector: " + + _name); + value = Eigen::VectorXd::Zero(state_size); + } + + AddState(value, timestep); + _pending_state.resize(0); + } + + /// @brief Clears all recorded state history. + void Clear() { + _timesteps.clear(); + _states.clear(); + _pending_state.resize(0); + } + + /// @brief Indicates whether an accumulated history has pending state. + /// @return True when a pending aggregate exists. + bool HasPendingState() const { return _pending_state.size() > 0; } + + //////////////////////////////////////////////////////////////////////////// + // + // Getters and Setters for History Vectors + // + //////////////////////////////////////////////////////////////////////////// /// @brief Retrieves the complete state map (timestep -> state vector). /// @return Map of integer timesteps to Eigen vectors representing states. @@ -149,10 +281,6 @@ class History { /// @return Snapshot or accumulated history mode. HistoryMode GetHistoryMode() const { return _mode; } - /// @brief Indicates whether an accumulated history has pending state. - /// @return True when a pending aggregate exists. - bool HasPendingState() const { return _pending_state.size() > 0; } - /// @brief Retrieves the pending accumulated state. /// @return The pending aggregate vector, or an empty vector if none. Eigen::VectorXd GetPendingState() const { return _pending_state; } @@ -161,6 +289,9 @@ class History { /// @return Largest recorded timestep, or -1 if history is empty. int GetLatestRecordedTimestep() const { if (_timesteps.empty()) { + LogWarning(_log_name, + "GetLatestRecordedTimestep called on empty history: " + + _name); return -1; } return _timesteps.back(); @@ -168,11 +299,7 @@ class History { /// @brief Retrieves the identifier name of this history. /// @return The history's name string. - std::string GetHistoryName() const { return _name; } - - /// @brief Retrieves the logger name for this history. - /// @return The associated logger's name. - std::string GetLogName() const { return _log_name; } + std::string GetName() const { return _name; } /// @brief Converts the sparse history map to a contiguous vector of states. /// Gaps in timesteps are filled with zero vectors of appropriate dimension. @@ -181,7 +308,8 @@ class History { std::vector GetStateAsVector() const { std::vector ret; if (_states.empty()) { - // warn empty state vector - no states recorded + LogWarning(_log_name, + "GetStateAsVector called on empty history: " + _name); return {}; } int default_size = _states.front().size(); @@ -189,9 +317,6 @@ class History { for (size_t index = 0; index < _timesteps.size(); ++index) { const int recorded_timestep = _timesteps[index]; const auto &recorded_state = _states[index]; - if (recorded_timestep > tstep) { - // Fill gap: raise error if timestep mapping is invalid - } while (recorded_timestep > tstep) { ret.push_back(GetZeroVector(default_size)); tstep++; @@ -202,78 +327,33 @@ class History { return ret; } - /// @brief Records a state vector at a specific or automatic timestep. - /// @param state The state vector to record. - /// @param timestep The timestep index for this state (default: -1 for - /// automatic next timestep). If timestep is negative, the next sequential - /// timestep is used automatically. If timestep already exists, it is - /// considered invalid but is currently overwritten. - void AddState(const Eigen::Ref &state, - int timestep = -1) { - if (timestep < 0) { - timestep = GetNextTimestep(); - } - - const auto existing = - std::find(_timesteps.begin(), _timesteps.end(), timestep); - if (existing != _timesteps.end()) { - const auto index = - static_cast(existing - _timesteps.begin()); - _states[index] = state; - return; - } - - _timesteps.push_back(timestep); - _states.push_back(state); - } - - /// @brief Records a snapshot value at a concrete timestep. - /// @param state The snapshot value to record. - /// @param timestep The simulation timestep for this snapshot. - void RecordSnapshot(const Eigen::Ref &state, - int timestep) { - AddState(state, timestep); - } - - /// @brief Adds a contribution to an accumulated history. - /// @param state The per-step contribution to accumulate. - void AccumulateState(const Eigen::Ref &state) { - if (_mode != HistoryMode::Accumulated) { - AddState(state); - return; - } + //////////////////////////////////////////////////////////////////////////// + // + // Operator Comparisons and Stream Output + // + //////////////////////////////////////////////////////////////////////////// - if (_pending_state.size() == 0) { - _pending_state = state; - return; - } - _pending_state += state; + /// @brief Equality comparison operator. + /// @param other The history to compare with. + /// @return True if all history properties and state are identical. + bool operator==(const History &other) const { + return _name == other._name && _log_name == other._log_name && + _mode == other._mode && GetStateMap() == other.GetStateMap() && + GetPendingState().isApprox(other.GetPendingState()); } - /// @brief Flushes pending accumulated state into a recorded timestep. - /// @param timestep The simulation timestep to record. - /// @param state_size Size of a zero vector to record if nothing is pending. - void FlushPendingState(int timestep, Eigen::Index state_size) { - if (_mode != HistoryMode::Accumulated) { - return; - } + /// @brief Inequality comparison operator. + /// @param other The history to compare with. + /// @return True if histories differ in any aspect. + bool operator!=(const History &other) const { return !(*this == other); } - Eigen::VectorXd value; - if (_pending_state.size() > 0) { - value = _pending_state; - } else { - value = Eigen::VectorXd::Zero(state_size); + friend std::ostream &operator<<(std::ostream &os, const History &history) { + os << "History(name=" << history._name << ", timesteps=["; + for (const auto &t : history._timesteps) { + os << t << ","; } - - AddState(value, timestep); - _pending_state.resize(0); - } - - /// @brief Clears all recorded state history. - void Clear() { - _timesteps.clear(); - _states.clear(); - _pending_state.resize(0); + os << "], pending_state=" << history._pending_state.transpose() << ")"; + return os; } private: diff --git a/include/respond/model.hpp b/include/respond/model.hpp index c4e07667..6be742b7 100644 --- a/include/respond/model.hpp +++ b/include/respond/model.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-09 // +// Last Modified: 2026-07-14 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -159,11 +159,33 @@ class Model { /// otherwise. virtual void SetInitialHistoryRecorded(bool recorded) = 0; + /// @brief Function to serialize the model's state and metadata to an output + /// stream. + /// @details This function is intended to be overridden by subclasses to + /// provide custom serialization logic. It should write the model's state, + /// metadata, and any relevant information to the provided output stream. + /// @note The output format is implementation-defined and may vary between + /// subclasses. Users should refer to the specific subclass documentation + /// for details on the serialization format. + /// @param os The output stream to which the model's serialized data will be + /// written. + virtual void Serialize(std::ostream &os) const = 0; + protected: /// @brief Protected default constructor for subclass initialization. /// Not intended for direct public use. Model() = default; }; + +/// @brief Overloaded stream insertion operator for Model serialization. +/// @param os The output stream to write to. +/// @param model The Model instance to serialize. +/// @return The output stream after writing the model's serialized data. +inline std::ostream &operator<<(std::ostream &os, const Model &model) { + model.Serialize(os); + return os; +} + } // namespace respond #endif // RESPOND_MODEL_HPP_ \ No newline at end of file diff --git a/include/respond/simulation.hpp b/include/respond/simulation.hpp index deda1041..637ea9db 100644 --- a/include/respond/simulation.hpp +++ b/include/respond/simulation.hpp @@ -188,6 +188,30 @@ class Simulation { return _models; } + /// @brief Retrieves a specific model by index in the simulation. + /// @param idx The index of the model to retrieve. + /// @return A const reference to the Model unique_ptr at the specified + /// index. Throws an exception if the index is out of range. + const std::unique_ptr &GetModel(size_t idx) const { + if (idx >= _models.size()) { + LogError(_log_name, + "Index out of range in GetModel: " + std::to_string(idx)); + throw std::out_of_range("Error attempting to GetModel by index."); + } + return _models[idx]; + } + + const std::unique_ptr & + GetModel(const std::string &model_name) const { + for (const auto &model : _models) { + if (model->GetName() == model_name) { + return model; + } + } + LogError(_log_name, "Model name not found in GetModel: " + model_name); + throw std::invalid_argument("Error attempting to GetModel by name."); + } + /// @brief Retrieves the names of all models in the simulation. /// @return Vector of model names in the order they were added. std::vector GetModelNames() const { @@ -198,49 +222,65 @@ class Simulation { return ret; } - /// @brief Retrieves the complete state histories for all models. + /// @brief Retrieves the complete state histories for the model at the + /// index. + /// @param idx The index of the model to retrieve histories for. /// @return Vector of maps (one per model) mapping history names to state /// vector trajectories. - const std::vector>> - GetModelHistories() const { - std::vector>> ret; - int model_idx = 0; - for (const auto &model : _models) { - std::map> inner_ret; - for (const auto &kv : model->GetHistories()) { - inner_ret[kv.first] = kv.second.GetStateAsVector(); - } - ret.push_back(inner_ret); - model_idx++; + const std::map &GetModelHistory(size_t idx) const { + if (idx >= _models.size()) { + LogError(_log_name, "Index out of range in GetModelHistory: " + + std::to_string(idx)); + throw std::out_of_range( + "Error attempting to GetModelHistory by index."); } - return ret; + return _models[idx]->GetHistories(); } - /// @brief Retrieves sparse history objects for all models. - /// @return Vector of maps (one per model) mapping history names to sparse - /// History objects. - const std::vector> - GetModelSparseHistories() const { - std::vector> ret; + const std::map & + GetModelHistory(const std::string &model_name) const { for (const auto &model : _models) { - ret.push_back(model->GetHistories()); + if (model->GetName() == model_name) { + return model->GetHistories(); + } } - return ret; + LogError(_log_name, + "Model name not found in GetModelHistory: " + model_name); + throw std::invalid_argument( + "Error attempting to GetModelHistory by name."); } /// @brief Retrieves pairs of (model name, history name) for all histories. /// @return Vector of pairs associating each history with its parent model. - const std::vector> - GetModelHistoryNames() const { - std::vector> ret; + const std::vector GetModelHistoryNames(size_t idx) const { + if (idx >= _models.size()) { + LogError(_log_name, "Index out of range in GetModelHistoryNames: " + + std::to_string(idx)); + throw std::out_of_range( + "Error attempting to GetModelHistoryNames by index."); + } + std::vector ret; + for (const auto &kv : _models[idx]->GetHistories()) { + ret.push_back(kv.first); + } + return ret; + } + + const std::vector + GetModelHistoryNames(const std::string &model_name) const { for (const auto &model : _models) { - for (const auto &kv : model->GetHistories()) { - std::pair p = {model->GetName(), - kv.first}; - ret.push_back(p); + if (model->GetName() == model_name) { + std::vector ret; + for (const auto &kv : model->GetHistories()) { + ret.push_back(kv.first); + } + return ret; } } - return ret; + LogError(_log_name, + "Model name not found in GetModelHistoryNames: " + model_name); + throw std::invalid_argument( + "Error attempting to GetModelHistoryNames by name."); } void SetDuration(int duration) { _duration = duration; } diff --git a/include/respond/timestep.hpp b/include/respond/timestep.hpp index 80881bfd..097693f9 100644 --- a/include/respond/timestep.hpp +++ b/include/respond/timestep.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-06-30 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-09 // +// Last Modified: 2026-07-14 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -64,7 +64,7 @@ class Timestep { Timestep(const Timestep &other) { _transitions.clear(); for (const auto &t : other._transitions) { - _transitions.push_back(t->clone()); + _transitions.push_back(std::move(t->clone())); } } @@ -76,7 +76,7 @@ class Timestep { if (this != &other) { _transitions.clear(); for (const auto &t : other._transitions) { - _transitions.push_back(t->clone()); + _transitions.push_back(std::move(t->clone())); } } return *this; @@ -126,6 +126,11 @@ class Timestep { return _transitions.back(); } + /// @brief Removes a transition from this timestep by index and returns it. + /// @param idx The index of the transition to remove. Must be within the + /// range of existing transitions. + /// @return A unique_ptr to the removed Transition. Throws an exception if + /// the index is out of range. std::unique_ptr RemoveTransition(size_t idx) { if (idx >= _transitions.size()) { LogWarning(_log_name, "Index out of range in RemoveTransition: " + @@ -146,9 +151,11 @@ class Timestep { void AddMatrixToTransition(const size_t &idx, const Eigen::Ref &m) { if (idx >= _transitions.size()) { - LogWarning(_log_name, - "Index out of range in AddMatrixToTransition: " + - std::to_string(idx)); + LogError(_log_name, + "Index out of range in AddMatrixToTransition: " + + std::to_string(idx)); + throw std::out_of_range( + "Error attempting to AddMatrixToTransition by index."); } _transitions[idx]->AddMatrix(m); } diff --git a/include/respond/transition.hpp b/include/respond/transition.hpp index 309427e1..1bed2c06 100644 --- a/include/respond/transition.hpp +++ b/include/respond/transition.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-02 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-08 // +// Last Modified: 2026-07-14 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -48,12 +48,11 @@ class Transition { /// @brief Adds a transformation matrix to this transition. /// The matrix is stored for use during Execute() calls. /// @param m The transition matrix to add (not modified by this transition). - virtual void AddMatrix(const Eigen::Ref &m) = 0; + virtual void AddMatrix(Eigen::Ref m) = 0; /// @brief Retrieves the stored transition matrices for this transition. /// @return A vector of references to the stored transition matrices. - virtual std::vector> - GetMatrices() const = 0; + virtual std::vector GetMatrices() const = 0; /// @brief Retrieves the name/type of this transition. /// @return The transition's identifier as a string. @@ -90,11 +89,35 @@ class Transition { const std::string &log_name = RESPOND_DEFAULT_LOG, const std::string &log_file = RESPOND_DEFAULT_LOG_FILE); + /// @brief Helper function to overload to the stream insertion operator for + /// Transition serialization. + /// @details This function is intended to be overridden by subclasses to + /// provide custom serialization logic. It should write the transition's + /// state, metadata, and any relevant information to the provided output + /// stream. + /// @note The output format is implementation-defined and may vary between + /// subclasses. Users should refer to the specific subclass documentation + /// for details on the serialization format. + /// @param os The output stream to which the transition's serialized data + /// will be written. + virtual void Serialize(std::ostream &os) const = 0; + protected: /// @brief Protected default constructor for subclass initialization. /// Not intended for direct public use. Transition() = default; }; + +/// @brief Overloaded stream insertion operator for Model serialization. +/// @param os The output stream to write to. +/// @param model The Model instance to serialize. +/// @return The output stream after writing the model's serialized data. +inline std::ostream &operator<<(std::ostream &os, + const Transition &transition) { + transition.Serialize(os); + return os; +} + } // namespace respond #endif \ No newline at end of file diff --git a/include/respond/version.hpp b/include/respond/version.hpp index 4221aeca..95cfc402 100644 --- a/include/respond/version.hpp +++ b/include/respond/version.hpp @@ -4,7 +4,7 @@ // Created Date: 2025-03-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-04-16 // +// Last Modified: 2026-07-14 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center // @@ -14,7 +14,7 @@ #define RESPOND_VERSION_HPP_ #define RESPOND_VER_MAJOR 2 -#define RESPOND_VER_MINOR 4 +#define RESPOND_VER_MINOR 5 #define RESPOND_VER_PATCH 0 #define RESPOND_TO_VERSION(major, minor, patch) \ diff --git a/src/background.cpp b/src/background.cpp index 7bdfde7e..46c49061 100644 --- a/src/background.cpp +++ b/src/background.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-09 // +// Last Modified: 2026-07-13 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -26,11 +26,13 @@ BackgroundDeath::Execute(const Eigen::Ref &state, TestCorrectNumberMatrices(1); TestMatrixSizes(state, GetMatrices()[0]); Eigen::VectorXd deaths = state.cwiseProduct(GetMatrices()[0]); - TestLessThanState(state, deaths); + TestLessThanState(state, deaths, + "BackgroundDeath transition produced more deaths than " + "available in state."); + auto new_state = state - deaths; if (h.find("background_death") != h.end()) { h["background_death"].AccumulateState(deaths); } - auto new_state = state - deaths; // remove deaths from state return new_state; } } // namespace respond \ No newline at end of file diff --git a/src/behavior.cpp b/src/behavior.cpp index 7ec865a0..6f98214a 100644 --- a/src/behavior.cpp +++ b/src/behavior.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-08 // +// Last Modified: 2026-07-13 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // diff --git a/src/internals/background.hpp b/src/internals/background.hpp index 2db92b28..5845f317 100644 --- a/src/internals/background.hpp +++ b/src/internals/background.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-09 // +// Last Modified: 2026-07-13 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // diff --git a/src/internals/behavior.hpp b/src/internals/behavior.hpp index 54dd1ba3..d1e4c2da 100644 --- a/src/internals/behavior.hpp +++ b/src/internals/behavior.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-09 // +// Last Modified: 2026-07-13 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // diff --git a/src/internals/intervention.hpp b/src/internals/intervention.hpp index 4e748e2a..7f6da082 100644 --- a/src/internals/intervention.hpp +++ b/src/internals/intervention.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-09 // +// Last Modified: 2026-07-13 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // diff --git a/src/internals/markov.hpp b/src/internals/markov.hpp index be9dfe8e..08e63993 100644 --- a/src/internals/markov.hpp +++ b/src/internals/markov.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-09 // +// Last Modified: 2026-07-14 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -186,10 +186,7 @@ class Markov : public virtual Model { } } - void RunTimestep() override { - RunTimestep(_current_timestep); - _current_timestep++; - } + void RunTimestep() override { RunTimestep(_current_timestep); } /// @brief Executes the timestep in the model's sequence. void RunTimestep(size_t idx) override { @@ -211,6 +208,14 @@ class Markov : public virtual Model { for (const auto &t : transitions) { _state = t->Execute(_state, _histories); } + if (idx != _current_timestep) { + LogWarning(_log_name, + "Ran timestep out of order. Current timestep: " + + std::to_string(_current_timestep) + + ", run timestep: " + std::to_string(idx)); + return; + } + _current_timestep++; } void RunTimesteps() override { @@ -250,15 +255,15 @@ class Markov : public virtual Model { /// @return A vector of the default history objects. void CreateDefaultHistories() override { std::map ret; - ret["state"] = History("state", _log_name, HistoryMode::Snapshot); + ret["state"] = History("state", HistoryMode::kSnapshot, _log_name); ret["total_overdose"] = - History("total_overdose", _log_name, HistoryMode::Accumulated); + History("total_overdose", HistoryMode::kAccumulated, _log_name); ret["fatal_overdose"] = - History("fatal_overdose", _log_name, HistoryMode::Accumulated); + History("fatal_overdose", HistoryMode::kAccumulated, _log_name); ret["intervention_admission"] = History( - "intervention_admission", _log_name, HistoryMode::Accumulated); + "intervention_admission", HistoryMode::kAccumulated, _log_name); ret["background_death"] = - History("background_death", _log_name, HistoryMode::Accumulated); + History("background_death", HistoryMode::kAccumulated, _log_name); _histories = ret; if (_histories.empty()) { ResetHistoryTracking(); @@ -275,6 +280,26 @@ class Markov : public virtual Model { _current_timestep = latest_timestep; } + void Serialize(std::ostream &os) const override { + os << "Model Name: " << _name << "\n"; + os << "Current Timestep: " << _current_timestep << "\n"; + os << "History Capture Interval: " << _history_capture_interval << "\n"; + os << "Final Timestep: " << _final_timestep << "\n"; + os << "Initial History Recorded: " + << (_initial_history_recorded ? "true" : "false") << "\n"; + os << "State Vector: " << _state.transpose() << "\n"; + os << "Histories:\n"; + for (const auto &kv : _histories) { + os << " - " << kv.first << "\n"; + } + os << "Timesteps:\n"; + for (size_t i = 0; i < _timestep_vector.size(); ++i) { + os << " Timestep Index: " << i << "\n"; + os << " Number of Transitions: " + << _timestep_vector[i].GetTransitions().size() << "\n"; + } + } + private: std::vector _timestep_vector; Eigen::VectorXd _state; @@ -317,7 +342,7 @@ class Markov : public virtual Model { return; } - _histories["state"].RecordSnapshot(_state, _current_timestep); + _histories["state"].AddState(_state, _current_timestep); const auto size = _state.size(); _histories["intervention_admission"].FlushPendingState( _current_timestep, size); diff --git a/src/internals/migration.hpp b/src/internals/migration.hpp index 63245977..4c220cf0 100644 --- a/src/internals/migration.hpp +++ b/src/internals/migration.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-09 // +// Last Modified: 2026-07-13 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // diff --git a/src/internals/overdose.hpp b/src/internals/overdose.hpp index c6236b78..a2128e93 100644 --- a/src/internals/overdose.hpp +++ b/src/internals/overdose.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-09 // +// Last Modified: 2026-07-13 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // diff --git a/src/internals/transition_base.hpp b/src/internals/transition_base.hpp index 9cb5a857..baf213ef 100644 --- a/src/internals/transition_base.hpp +++ b/src/internals/transition_base.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-08 // +// Last Modified: 2026-07-14 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -33,11 +33,10 @@ class TransitionBase : public virtual Transition { // Add a Transition Matrix to the set. We have no need to edit it once it's // been added, just use it. Thus, we don't need full ownership (reference) // and can accept the const type. - void AddMatrix(const Eigen::Ref &m) override { + void AddMatrix(Eigen::Ref m) override { _transition_matrices.push_back(m); } - std::vector> - GetMatrices() const override { + std::vector GetMatrices() const override { return _transition_matrices; } // Get the name of the Transition. No need to edit the object and do not @@ -46,6 +45,11 @@ class TransitionBase : public virtual Transition { // Clear out all the stored Eigen::MatrixXd values void ClearMatrices() override { _transition_matrices.clear(); } + void Serialize(std::ostream &os) const override { + os << "Transition(name=" << _name + << ", num_matrices=" << _transition_matrices.size() << ")"; + } + protected: const std::string _log_name; @@ -118,13 +122,17 @@ class TransitionBase : public virtual Transition { } void TestLessThanState(const Eigen::Ref &state, - const Eigen::Ref &m1) const { + const Eigen::Ref &m1, + std::string extra_msg = "") const { if (!(state.array() >= m1.array()).all()) { std::string error_msg = "Transition error - State contains values less than m1! " + std::to_string((state.array() < m1.array()).count()) + " elements affected. Verify that the transition matrix is " "correct and that the state vector is valid."; + if (!extra_msg.empty()) { + error_msg += " " + extra_msg; + } LogError(_log_name, error_msg); throw std::runtime_error(error_msg); } @@ -132,7 +140,7 @@ class TransitionBase : public virtual Transition { private: std::string _name; - std::vector> _transition_matrices; + std::vector _transition_matrices; }; } // namespace respond diff --git a/src/intervention.cpp b/src/intervention.cpp index 17af5f12..d9e6e1c2 100644 --- a/src/intervention.cpp +++ b/src/intervention.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-08 // +// Last Modified: 2026-07-13 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // diff --git a/src/logging.cpp b/src/logging.cpp index f3da2a3b..74fcd724 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -4,7 +4,7 @@ // Created Date: 2025-06-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-30 // +// Last Modified: 2026-07-09 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center // @@ -21,7 +21,6 @@ namespace respond { CreationStatus CreateFileLogger(const std::string &logger_name, const std::string &filepath) { if (CheckIfExists(logger_name) == CreationStatus::kExists) { - std::cout << "Logger " << logger_name << " already exists" << std::endl; return CreationStatus::kExists; } try { diff --git a/src/migration.cpp b/src/migration.cpp index 22999090..d588c454 100644 --- a/src/migration.cpp +++ b/src/migration.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-08 // +// Last Modified: 2026-07-13 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // diff --git a/src/overdose.cpp b/src/overdose.cpp index 0dabaeae..2d59b8b4 100644 --- a/src/overdose.cpp +++ b/src/overdose.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-08 // +// Last Modified: 2026-07-13 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -23,20 +23,26 @@ Eigen::VectorXd Overdose::Execute(const Eigen::Ref &state, std::map &h) const { TestCorrectNumberMatrices(2); + auto matrices = GetMatrices(); TestMatrixSizes(state, GetMatrices()[0]); Eigen::VectorXd overdoses = state.cwiseProduct(GetMatrices()[0]); + TestLessThanState(state, overdoses, + "Overdose transition produced more total overdoses than " + "available in state."); if (h.find("total_overdose") != h.end()) { h["total_overdose"].AccumulateState(overdoses); } TestMatrixSizes(overdoses, GetMatrices()[1]); - auto fods = overdoses.cwiseProduct(GetMatrices()[1]); // negatives + Eigen::VectorXd fods = overdoses.cwiseProduct(GetMatrices()[1]); + TestLessThanState(state, fods, + "Overdose transition produced more fatal overdoses than " + "available in state."); + auto new_state = state - fods; if (h.find("fatal_overdose") != h.end()) { h["fatal_overdose"].AccumulateState(fods); } - TestLessThanState(state, fods); - auto new_state = state - fods; // remove fods from state return new_state; } } // namespace respond \ No newline at end of file diff --git a/src/transition_factory.cpp b/src/transition_factory.cpp index 52dc8d4b..eb87e726 100644 --- a/src/transition_factory.cpp +++ b/src/transition_factory.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-07 // +// Last Modified: 2026-07-14 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -49,6 +49,6 @@ std::unique_ptr Transition::Create(const std::string &type, "'. Supported types: migration, behavior, " "intervention, overdose, background_death"; LogError(log_name, error_msg); - return nullptr; + throw std::invalid_argument(error_msg); } } // namespace respond \ No newline at end of file diff --git a/tests/integration/respond_test.cpp b/tests/integration/respond_test.cpp index c206741f..5fd25941 100644 --- a/tests/integration/respond_test.cpp +++ b/tests/integration/respond_test.cpp @@ -93,7 +93,8 @@ class RespondTest : public ::testing::Test { TEST_F(RespondTest, RunSingleTimestep) { sim.GetModels()[0]->AddTimestep(CreateTestTimestep()); sim.Run(); - Eigen::VectorXd result = sim.GetModelHistories()[0].at("state").back(); + Eigen::VectorXd result = + sim.GetModelHistory(0).at("state").GetStateAsVector().back(); Eigen::Vector3d final_state; final_state << 0.76715528791564891, 0.72320370216816077, 1.037712429738102; @@ -105,15 +106,7 @@ TEST_F(RespondTest, RunSimulationTwoStep) { sim.GetModels()[0]->AddTimestep(CreateTestTimestep()); sim.Run(2); - auto histories = sim.GetModelHistories(); - ASSERT_EQ(histories.size(), 1); - - auto mm_histories = histories[0]; - if (mm_histories.find("state") == mm_histories.end()) { - FAIL() << "Unable to find the 'state' history."; - } - - auto state_history = mm_histories.at("state"); + auto state_history = sim.GetModelHistory(0).at("state").GetStateAsVector(); // 2 because it carries the initial state and 2 steps ASSERT_EQ(state_history.size(), 3); @@ -134,15 +127,7 @@ TEST_F(RespondTest, RunSimulationFiveStep) { sim.SetDuration(5); sim.Run(); - auto histories = sim.GetModelHistories(); - ASSERT_EQ(histories.size(), 1); - - auto mm_histories = histories[0]; - if (mm_histories.find("state") == mm_histories.end()) { - FAIL() << "Unable to find the 'state' history."; - } - - auto state_history = mm_histories.at("state"); + auto state_history = sim.GetModelHistory(0).at("state").GetStateAsVector(); // 6 because it carries the initial state and 5 timesteps ASSERT_EQ(state_history.size(), 6); @@ -162,15 +147,7 @@ TEST_F(RespondTest, RunSimulationFiveStepWithDurationParameter) { sim.GetModels()[0]->AddTimestep(CreateTestTimestep()); sim.Run(5); - auto histories = sim.GetModelHistories(); - ASSERT_EQ(histories.size(), 1); - - auto mm_histories = histories[0]; - if (mm_histories.find("state") == mm_histories.end()) { - FAIL() << "Unable to find the 'state' history."; - } - - auto state_history = mm_histories.at("state"); + auto state_history = sim.GetModelHistory(0).at("state").GetStateAsVector(); // 6 because it carries the initial state and 5 timesteps ASSERT_EQ(state_history.size(), 6); diff --git a/tests/mocks/model_mock.hpp b/tests/mocks/model_mock.hpp index 93ed3295..0ab2b043 100644 --- a/tests/mocks/model_mock.hpp +++ b/tests/mocks/model_mock.hpp @@ -4,7 +4,7 @@ // Created Date: 2025-08-01 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-09 // +// Last Modified: 2026-07-14 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center // @@ -18,6 +18,7 @@ #include #include +#include #include #include @@ -52,6 +53,7 @@ class MockModel : public virtual Model { MOCK_METHOD(void, SetHistoryCaptureInterval, (int), (override)); MOCK_METHOD(void, SetFinalTimestep, (int), (override)); MOCK_METHOD(void, SetInitialHistoryRecorded, (bool), (override)); + MOCK_METHOD(void, Serialize, (std::ostream &), (const, override)); }; } // namespace testing } // namespace respond diff --git a/tests/mocks/transition_mock.hpp b/tests/mocks/transition_mock.hpp index f516af93..93eb7625 100644 --- a/tests/mocks/transition_mock.hpp +++ b/tests/mocks/transition_mock.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-07 // +// Last Modified: 2026-07-14 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -30,13 +31,14 @@ class MockTransition : public virtual Transition { ((const Eigen::Ref &), (std::map &)), (const, override)); - MOCK_METHOD(void, AddMatrix, (const Eigen::Ref &), + MOCK_METHOD(void, AddMatrix, (Eigen::Ref), (override)); - MOCK_METHOD((std::vector>), GetMatrices, - (), (const, override)); + MOCK_METHOD((std::vector), GetMatrices, (), + (const, override)); MOCK_METHOD(void, ClearMatrices, (), (override)); MOCK_METHOD(std::string, GetName, (), (const, override)); MOCK_METHOD(std::unique_ptr, clone, (), (const, override)); + MOCK_METHOD(void, Serialize, (std::ostream &), (const, override)); }; } // namespace testing } // namespace respond diff --git a/tests/unit/background_test.cpp b/tests/unit/background_test.cpp index 12dcc599..1077f787 100644 --- a/tests/unit/background_test.cpp +++ b/tests/unit/background_test.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-08 // +// Last Modified: 2026-07-13 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // diff --git a/tests/unit/behavior_test.cpp b/tests/unit/behavior_test.cpp index 695d8e68..e89b71f1 100644 --- a/tests/unit/behavior_test.cpp +++ b/tests/unit/behavior_test.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-08 // +// Last Modified: 2026-07-13 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // diff --git a/tests/unit/history_test.cpp b/tests/unit/history_test.cpp index 36123c79..21002e1f 100644 --- a/tests/unit/history_test.cpp +++ b/tests/unit/history_test.cpp @@ -2,89 +2,211 @@ // File: history_test.cpp // // Project: respond // // Created Date: 2026-05-05 // -// Author: GitHub Copilot // +// Author: Matthew Carroll // // ----- // -// Last Modified: 2026-05-05 // -// Modified By: GitHub Copilot // +// Last Modified: 2026-07-13 // +// Modified By: Matthew Carroll // // ----- // //////////////////////////////////////////////////////////////////////////////// #include +#include #include #include #include +#include namespace respond { namespace testing { -TEST(HistoryTest, SparseStoragePreservesRecordedTimesteps) { - History history("state", "test_logger"); - Eigen::VectorXd state0(2); - state0 << 1.0f, 2.0f; - Eigen::VectorXd state2(2); - state2 << 3.0f, 4.0f; +class HistoryTest : public ::testing::Test { +public: +protected: + void SetUp() override { + // Clear any existing loggers from previous tests + spdlog::drop_all(); + + // Create temporary log files for testing + default_log_file_ = RESPOND_DEFAULT_LOG_FILE; + + // Remove test files if they exist + std::remove(default_log_file_.c_str()); + } + void TearDown() override { + // Clean up loggers + spdlog::drop_all(); + + // Remove test files + std::remove(default_log_file_.c_str()); + } - history.AddState(state0, 0); - history.AddState(state2, 2); + std::string default_log_file_; + + // Helper to check if file contains a string + bool FileContains(const std::string &filepath, const std::string &search) { + std::ifstream file(filepath); + if (!file.is_open()) + return false; + + std::string line; + while (std::getline(file, line)) { + if (line.find(search) != std::string::npos) { + return true; + } + } + return false; + } +}; + +TEST_F(HistoryTest, GetDefaultHistoryModeReturnsAccumulatedForSpecificNames) { + EXPECT_EQ(GetDefaultHistoryMode("intervention_admission"), + HistoryMode::kAccumulated); + EXPECT_EQ(GetDefaultHistoryMode("total_overdose"), + HistoryMode::kAccumulated); + EXPECT_EQ(GetDefaultHistoryMode("fatal_overdose"), + HistoryMode::kAccumulated); + EXPECT_EQ(GetDefaultHistoryMode("background_death"), + HistoryMode::kAccumulated); +} - std::vector expected_timesteps = {0, 2}; +TEST_F(HistoryTest, GetDefaultHistoryModeReturnsSnapshotForOtherNames) { + EXPECT_EQ(GetDefaultHistoryMode("state"), HistoryMode::kSnapshot); + EXPECT_EQ(GetDefaultHistoryMode("custom_history"), HistoryMode::kSnapshot); + EXPECT_EQ(GetDefaultHistoryMode(""), HistoryMode::kSnapshot); +} + +TEST_F(HistoryTest, DefaultConstructor) { + History history; + EXPECT_EQ(history.GetName(), "state"); + EXPECT_EQ(history.GetHistoryMode(), HistoryMode::kSnapshot); + EXPECT_TRUE(history.GetRecordedTimesteps().empty()); + EXPECT_TRUE(history.GetRecordedStates().empty()); +} + +TEST_F(HistoryTest, ConstructorWithName) { + History history("custom_history"); + EXPECT_EQ(history.GetName(), "custom_history"); + EXPECT_EQ(history.GetHistoryMode(), HistoryMode::kSnapshot); + EXPECT_TRUE(history.GetRecordedTimesteps().empty()); + EXPECT_TRUE(history.GetRecordedStates().empty()); +} + +TEST_F(HistoryTest, ConstructorWithNameAndMode) { + History history("total_overdose", HistoryMode::kAccumulated); + EXPECT_EQ(history.GetName(), "total_overdose"); + EXPECT_EQ(history.GetHistoryMode(), HistoryMode::kAccumulated); + EXPECT_TRUE(history.GetRecordedTimesteps().empty()); + EXPECT_TRUE(history.GetRecordedStates().empty()); +} + +TEST_F(HistoryTest, ConstructorWithNameModeAndLogger) { + History history("fatal_overdose", HistoryMode::kAccumulated, "test_logger"); + EXPECT_EQ(history.GetName(), "fatal_overdose"); + EXPECT_EQ(history.GetHistoryMode(), HistoryMode::kAccumulated); + EXPECT_TRUE(history.GetRecordedTimesteps().empty()); + EXPECT_TRUE(history.GetRecordedStates().empty()); +} + +TEST_F(HistoryTest, ConstructorWithNameAndLogger) { + History history("background_death", "test_logger"); + EXPECT_EQ(history.GetName(), "background_death"); + EXPECT_EQ(history.GetHistoryMode(), HistoryMode::kAccumulated); + EXPECT_TRUE(history.GetRecordedTimesteps().empty()); + EXPECT_TRUE(history.GetRecordedStates().empty()); +} + +TEST_F(HistoryTest, ConstructorWithNameModeLoggerAndLogFile) { + History history("background_death", HistoryMode::kAccumulated, + "test_logger", default_log_file_); + EXPECT_EQ(history.GetName(), "background_death"); + EXPECT_EQ(history.GetHistoryMode(), HistoryMode::kAccumulated); + EXPECT_TRUE(history.GetRecordedTimesteps().empty()); + EXPECT_TRUE(history.GetRecordedStates().empty()); +} + +TEST_F(HistoryTest, EqualityOperator) { + History history("state"); + History history_copy("state"); + + history.AddState(Eigen::VectorXd::Ones(2), 0); + history_copy.AddState(Eigen::VectorXd::Ones(2), 0); + EXPECT_EQ(history, history_copy); +} + +TEST_F(HistoryTest, InequalityOperator) { + History history("state"); + History history_copy("state"); + + history.AddState(Eigen::VectorXd::Ones(2), 0); + history_copy.AddState(Eigen::VectorXd::Zero(2), 0); + EXPECT_NE(history, history_copy); +} + +TEST_F(HistoryTest, AddStateRecordsStateAtSpecifiedTimestep) { + History history("state"); + Eigen::VectorXd state(2); + state << 1.0f, 2.0f; + + history.AddState(state, 5); + + std::vector expected_timesteps = {5}; ASSERT_EQ(history.GetRecordedTimesteps(), expected_timesteps); - ASSERT_EQ(history.GetRecordedStates().size(), 2u); - EXPECT_TRUE(history.GetRecordedStates()[0].isApprox(state0)); - EXPECT_TRUE(history.GetRecordedStates()[1].isApprox(state2)); + ASSERT_EQ(history.GetRecordedStates().size(), 1u); + EXPECT_TRUE(history.GetRecordedStates()[0].isApprox(state)); } -TEST(HistoryTest, GetStateAsVectorFillsSparseGapsWithZeros) { - History history("state", "test_logger"); - Eigen::VectorXd state0(2); - state0 << 1.0f, 2.0f; +TEST_F(HistoryTest, AddStateRecordsStateAtNextSequentialTimestep) { + History history("state"); + Eigen::VectorXd state1(2); + state1 << 1.0f, 2.0f; Eigen::VectorXd state2(2); state2 << 3.0f, 4.0f; - history.AddState(state0, 0); - history.AddState(state2, 2); + history.AddState(state1); // Should be at timestep 0 + history.AddState(state2); // Should be at timestep 1 - const auto dense_states = history.GetStateAsVector(); - ASSERT_EQ(dense_states.size(), 3u); - EXPECT_TRUE(dense_states[0].isApprox(state0)); - EXPECT_TRUE(dense_states[1].isZero()); - EXPECT_TRUE(dense_states[2].isApprox(state2)); + std::vector expected_timesteps = {0, 1}; + ASSERT_EQ(history.GetRecordedTimesteps(), expected_timesteps); + ASSERT_EQ(history.GetRecordedStates().size(), 2u); + EXPECT_TRUE(history.GetRecordedStates()[0].isApprox(state1)); + EXPECT_TRUE(history.GetRecordedStates()[1].isApprox(state2)); } -TEST(HistoryTest, GetStateMapBuildsSparseMapOnDemand) { - History history("state", "test_logger"); - Eigen::VectorXd state0(1); - state0 << 5.0f; - Eigen::VectorXd state3(1); - state3 << 7.0f; +TEST_F(HistoryTest, AccumulateStateAddsToPendingState) { + History history("total_overdose", HistoryMode::kAccumulated); + Eigen::VectorXd first(2); + first << 1.0f, 2.0f; + Eigen::VectorXd second(2); + second << 3.0f, 4.0f; - history.AddState(state0, 0); - history.AddState(state3, 3); + history.AccumulateState(first); + history.AccumulateState(second); - const auto state_map = history.GetStateMap(); - ASSERT_EQ(state_map.size(), 2u); - EXPECT_TRUE(state_map.at(0).isApprox(state0)); - EXPECT_TRUE(state_map.at(3).isApprox(state3)); + Eigen::VectorXd expected(2); + expected << 4.0f, 6.0f; + EXPECT_TRUE(history.GetPendingState().isApprox(expected)); } -TEST(HistoryTest, ClearRemovesRecordedStatesAndTimesteps) { - History history("state", "test_logger"); - Eigen::VectorXd state(1); - state << 1.0f; - history.AddState(state, 0); +TEST_F(HistoryTest, AccumulateStateOnNonAccumulatedHistoryAddsState) { + History history("state", HistoryMode::kSnapshot); + Eigen::VectorXd state(2); + state << 1.0f, 2.0f; - history.Clear(); + history.AccumulateState(state); - EXPECT_TRUE(history.GetRecordedTimesteps().empty()); - EXPECT_TRUE(history.GetRecordedStates().empty()); - EXPECT_TRUE(history.GetStateAsVector().empty()); - EXPECT_TRUE(history.GetStateMap().empty()); + std::vector expected_timesteps = {0}; + ASSERT_EQ(history.GetRecordedTimesteps(), expected_timesteps); + ASSERT_EQ(history.GetRecordedStates().size(), 1u); + EXPECT_TRUE(history.GetRecordedStates()[0].isApprox(state)); + FlushAllLoggers(); + EXPECT_TRUE(FileContains(RESPOND_DEFAULT_LOG_FILE, + "AccumulateState called on non-accumulated")); } -TEST(HistoryTest, AccumulatedHistoryFlushesPendingState) { - History history("total_overdose", "test_logger", HistoryMode::Accumulated); +TEST_F(HistoryTest, FlushPendingStateRecordsPendingStateAtSpecifiedTimestep) { + History history("total_overdose", HistoryMode::kAccumulated); Eigen::VectorXd first(2); first << 1.0f, 2.0f; Eigen::VectorXd second(2); @@ -92,10 +214,10 @@ TEST(HistoryTest, AccumulatedHistoryFlushesPendingState) { history.AccumulateState(first); history.AccumulateState(second); - history.FlushPendingState(4, 2); + history.FlushPendingState(10, 2); ASSERT_FALSE(history.HasPendingState()); - std::vector expected_timesteps = {4}; + std::vector expected_timesteps = {10}; ASSERT_EQ(history.GetRecordedTimesteps(), expected_timesteps); Eigen::VectorXd expected(2); @@ -104,16 +226,189 @@ TEST(HistoryTest, AccumulatedHistoryFlushesPendingState) { EXPECT_TRUE(history.GetRecordedStates()[0].isApprox(expected)); } -TEST(HistoryTest, AccumulatedHistoryFlushesZeroWhenNoPendingStateExists) { - History history("background_death", "test_logger", - HistoryMode::Accumulated); +TEST_F(HistoryTest, FlushPendingStateOnNonAccumulatedHistoryDoesNothing) { + History history("state", HistoryMode::kSnapshot); + Eigen::VectorXd state(2); + state << 1.0f, 2.0f; - history.FlushPendingState(0, 3); + history.AccumulateState(state); // Should add state instead + history.FlushPendingState(5, 2); // Should do nothing std::vector expected_timesteps = {0}; ASSERT_EQ(history.GetRecordedTimesteps(), expected_timesteps); ASSERT_EQ(history.GetRecordedStates().size(), 1u); - EXPECT_TRUE(history.GetRecordedStates()[0].isZero()); + EXPECT_TRUE(history.GetRecordedStates()[0].isApprox(state)); + FlushAllLoggers(); + EXPECT_TRUE( + FileContains(RESPOND_DEFAULT_LOG_FILE, + "FlushPendingState called on non-accumulated history")); +} + +TEST_F(HistoryTest, FlushPendingStateWithNoPendingStateRecordsZeroVector) { + History history("total_overdose", HistoryMode::kAccumulated); + history.FlushPendingState(3, 2); + + std::vector expected_timesteps = {3}; + ASSERT_EQ(history.GetRecordedTimesteps(), expected_timesteps); + ASSERT_EQ(history.GetRecordedStates().size(), 1u); + + Eigen::VectorXd expected = Eigen::VectorXd::Zero(2); + EXPECT_TRUE(history.GetRecordedStates()[0].isApprox(expected)); +} + +TEST_F(HistoryTest, ClearEmptiesHistory) { + History history("state"); + Eigen::VectorXd state(2); + state << 1.0f, 2.0f; + + history.AddState(state, 0); + ASSERT_FALSE(history.GetRecordedTimesteps().empty()); + ASSERT_FALSE(history.GetRecordedStates().empty()); + + history.Clear(); + EXPECT_TRUE(history.GetRecordedTimesteps().empty()); + EXPECT_TRUE(history.GetRecordedStates().empty()); +} + +TEST_F(HistoryTest, HasPendingStateReturnsTrueWhenPending) { + History history("total_overdose", HistoryMode::kAccumulated); + Eigen::VectorXd state(2); + state << 1.0f, 2.0f; + + EXPECT_FALSE(history.HasPendingState()); + history.AccumulateState(state); + EXPECT_TRUE(history.HasPendingState()); +} + +TEST_F(HistoryTest, HasPendingStateReturnsFalseWhenNoPending) { + History history("total_overdose", HistoryMode::kAccumulated); + EXPECT_FALSE(history.HasPendingState()); +} + +TEST_F(HistoryTest, GetStateMap) { + History history("state"); + Eigen::VectorXd state1(2); + state1 << 1.0f, 2.0f; + Eigen::VectorXd state2(2); + state2 << 3.0f, 4.0f; + + history.AddState(state1, 5); + history.AddState(state2, 10); + + std::map expected_map = {{5, state1}, {10, state2}}; + + auto state_map = history.GetStateMap(); + ASSERT_EQ(state_map.size(), expected_map.size()); + for (const auto &[timestep, expected_state] : expected_map) { + ASSERT_TRUE(state_map.find(timestep) != state_map.end()); + EXPECT_TRUE(state_map[timestep].isApprox(expected_state)); + } +} + +TEST_F(HistoryTest, GetRecordedTimesteps) { + History history("state"); + Eigen::VectorXd state1(2); + state1 << 1.0f, 2.0f; + Eigen::VectorXd state2(2); + state2 << 3.0f, 4.0f; + + history.AddState(state1, 5); + history.AddState(state2, 10); + + std::vector expected_timesteps = {5, 10}; + EXPECT_EQ(history.GetRecordedTimesteps(), expected_timesteps); +} + +TEST_F(HistoryTest, GetRecordedStates) { + History history("state"); + Eigen::VectorXd state1(2); + state1 << 1.0f, 2.0f; + Eigen::VectorXd state2(2); + state2 << 3.0f, 4.0f; + + history.AddState(state1, 5); + history.AddState(state2, 10); + + const auto &recorded_states = history.GetRecordedStates(); + ASSERT_EQ(recorded_states.size(), 2u); + EXPECT_TRUE(recorded_states[0].isApprox(state1)); + EXPECT_TRUE(recorded_states[1].isApprox(state2)); +} + +TEST_F(HistoryTest, GetHistoryMode) { + History history("state", HistoryMode::kSnapshot); + EXPECT_EQ(history.GetHistoryMode(), HistoryMode::kSnapshot); + + History history2("total_overdose", HistoryMode::kAccumulated); + EXPECT_EQ(history2.GetHistoryMode(), HistoryMode::kAccumulated); +} + +TEST_F(HistoryTest, GetPendingState) { + History history("total_overdose", HistoryMode::kAccumulated); + Eigen::VectorXd state(2); + state << 1.0f, 2.0f; + + EXPECT_TRUE(history.GetPendingState().size() == 0); + history.AccumulateState(state); + EXPECT_TRUE(history.GetPendingState().isApprox(state)); +} + +TEST_F(HistoryTest, GetLatestRecordedTimestep) { + History history("state"); + EXPECT_EQ(history.GetLatestRecordedTimestep(), -1); + + Eigen::VectorXd state1(2); + state1 << 1.0f, 2.0f; + Eigen::VectorXd state2(2); + state2 << 3.0f, 4.0f; + + history.AddState(state1, 5); + history.AddState(state2, 10); + + EXPECT_EQ(history.GetLatestRecordedTimestep(), 10); +} + +TEST_F(HistoryTest, GetLatestRecordedTimestepOnEmptyHistoryReturnsNegativeOne) { + History history("state"); + EXPECT_EQ(history.GetLatestRecordedTimestep(), -1); + FlushAllLoggers(); + EXPECT_TRUE( + FileContains(RESPOND_DEFAULT_LOG_FILE, + "GetLatestRecordedTimestep called on empty history")); +} + +TEST_F(HistoryTest, GetName) { + History history("custom_history"); + EXPECT_EQ(history.GetName(), "custom_history"); +} + +TEST_F(HistoryTest, GetStateAsVectorFillsGapsWithZeroVectors) { + History history("state"); + Eigen::VectorXd state1(2); + state1 << 1.0f, 2.0f; + Eigen::VectorXd state2(2); + state2 << 3.0f, 4.0f; + + history.AddState(state1, 0); + history.AddState(state2, 3); + + std::vector expected_states = { + state1, Eigen::VectorXd::Zero(2), Eigen::VectorXd::Zero(2), state2}; + + auto state_vector = history.GetStateAsVector(); + ASSERT_EQ(state_vector.size(), expected_states.size()); + for (size_t i = 0; i < expected_states.size(); ++i) { + EXPECT_TRUE(state_vector[i].isApprox(expected_states[i])); + } +} + +TEST_F(HistoryTest, GetStateAsVectorOnEmptyHistoryReturnsEmptyVector) { + History history("state"); + auto state_vector = history.GetStateAsVector(); + EXPECT_TRUE(state_vector.empty()); + FlushAllLoggers(); + EXPECT_TRUE(FileContains(RESPOND_DEFAULT_LOG_FILE, + "GetStateAsVector called on empty history:")); } } // namespace testing diff --git a/tests/unit/intervention_test.cpp b/tests/unit/intervention_test.cpp index 33f1d734..94744991 100644 --- a/tests/unit/intervention_test.cpp +++ b/tests/unit/intervention_test.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-08 // +// Last Modified: 2026-07-13 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // diff --git a/tests/unit/logging_test.cpp b/tests/unit/logging_test.cpp index 9aaca62c..7a172219 100644 --- a/tests/unit/logging_test.cpp +++ b/tests/unit/logging_test.cpp @@ -4,7 +4,7 @@ // Created Date: 2025-03-18 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-09 // +// Last Modified: 2026-07-14 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center // @@ -495,11 +495,11 @@ TEST_F(LoggingTest, MixedFileAndSharedLoggers) { TEST_F(LoggingTest, TransitionFactoryInvalidType) { CreateFileLogger("factory_test", test_log_file_); - // Create transition with invalid type should log error and return nullptr - auto transition = - respond::Transition::Create("invalid_type", "factory_test"); - - EXPECT_EQ(transition, nullptr); + // Create transition with invalid type should log error and throw error + EXPECT_THROW( + (void)respond::Transition::Create("invalid_type", "factory_test"), + std::invalid_argument); + FlushAllLoggers(); EXPECT_EQ(CheckLoggerExists("factory_test"), CreationStatus::kExists); } diff --git a/tests/unit/migration_test.cpp b/tests/unit/migration_test.cpp index 4ce8e0ad..4d4d0889 100644 --- a/tests/unit/migration_test.cpp +++ b/tests/unit/migration_test.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-08 // +// Last Modified: 2026-07-13 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // diff --git a/tests/unit/overdose_test.cpp b/tests/unit/overdose_test.cpp index 7c3e0d0e..0023fd60 100644 --- a/tests/unit/overdose_test.cpp +++ b/tests/unit/overdose_test.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-08 // +// Last Modified: 2026-07-13 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -141,13 +142,15 @@ TEST_F(OverdoseTest, ExecuteValid) { Overdose overdose; overdose.AddMatrix(tran_matrix); overdose.AddMatrix(tran_matrix); + histories["state"] = History("state"); Eigen::VectorXd result = overdose.Execute(state, histories); - Eigen::VectorXd expected_overdoses = state.cwiseProduct(tran_matrix); + Eigen::VectorXd expected_fods = expected_overdoses.cwiseProduct(tran_matrix); Eigen::VectorXd expected_new_state = state - expected_fods; + EXPECT_TRUE(result.isApprox(expected_new_state)); } diff --git a/tests/unit/simulation_test.cpp b/tests/unit/simulation_test.cpp index a2b6c4a4..8e07b1c5 100644 --- a/tests/unit/simulation_test.cpp +++ b/tests/unit/simulation_test.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-09 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-08 // +// Last Modified: 2026-07-15 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -218,53 +218,15 @@ TEST_F(SimulationTest, GetModelHistories) { .WillOnce(Return(::testing::ByMove(std::move(cloned)))); s.AddModel(std::move(mock_model)); - const auto model_histories = s.GetModelHistories(); + const auto model_histories = s.GetModelHistory(0); + ASSERT_EQ(model_histories.size(), 1); ASSERT_EQ(model_histories.size(), 1); - ASSERT_EQ(model_histories[0].size(), 1); - - const auto history_it = model_histories[0].find("history1"); - ASSERT_NE(history_it, model_histories[0].end()); - ASSERT_EQ(history_it->second.size(), 3); - EXPECT_TRUE(history_it->second[0].isApprox(state0)); - EXPECT_TRUE(history_it->second[1].isApprox(Eigen::VectorXd::Zero(2))); - EXPECT_TRUE(history_it->second[2].isApprox(state2)); -} - -TEST_F(SimulationTest, GetModelSparseHistories) { - Simulation s; - - History history1("history1"); - Eigen::VectorXd state1(2); - state1 << 5.0, 6.0; - history1.AddState(state1, 1); - - History history2("history2"); - Eigen::VectorXd state2(2); - state2 << 7.0, 8.0; - history2.AddState(state2, 3); - - auto histories = std::map{{"history1", history1}, - {"history2", history2}}; - - auto mock_model = std::make_unique>(); - auto cloned = std::make_unique>(); - auto *cloned_ptr = cloned.get(); - EXPECT_CALL(*cloned_ptr, GetHistories()).WillOnce(ReturnRef(histories)); - EXPECT_CALL(*mock_model, clone()) - .WillOnce(Return(::testing::ByMove(std::move(cloned)))); - s.AddModel(std::move(mock_model)); - - const auto sparse_histories = s.GetModelSparseHistories(); - ASSERT_EQ(sparse_histories.size(), 1); - ASSERT_EQ(sparse_histories[0].size(), 2); - - const auto history1_it = sparse_histories[0].find("history1"); - ASSERT_NE(history1_it, sparse_histories[0].end()); - EXPECT_EQ(history1_it->second, history1); - const auto history2_it = sparse_histories[0].find("history2"); - ASSERT_NE(history2_it, sparse_histories[0].end()); - EXPECT_EQ(history2_it->second, history2); + const auto history_it = model_histories.at("history1").GetStateAsVector(); + ASSERT_EQ(history_it.size(), 3); + EXPECT_TRUE(history_it[0].isApprox(state0)); + EXPECT_TRUE(history_it[1].isApprox(Eigen::VectorXd::Zero(2))); + EXPECT_TRUE(history_it[2].isApprox(state2)); } TEST_F(SimulationTest, GetModelHistoryNames) { @@ -292,14 +254,13 @@ TEST_F(SimulationTest, GetModelHistoryNames) { .WillOnce(Return(::testing::ByMove(std::move(cloned2)))); s.AddModel(std::move(mock_model2)); - const auto history_names = s.GetModelHistoryNames(); - const std::vector> expected = { - {"model1", "history1"}, - {"model1", "history2"}, - {"model2", "history3"}, - }; - + const auto history_names = s.GetModelHistoryNames(0); + const std::vector expected = {"history1", "history2"}; ASSERT_EQ(history_names, expected); + + const auto history_names_two = s.GetModelHistoryNames(1); + const std::vector expected_two = {"history3"}; + ASSERT_EQ(history_names_two, expected_two); } } // namespace testing