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/Doxyfile.in b/Doxyfile.in index d568dfaa..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 @@ -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/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 841b09ef..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::TransitionFactory::CreateTransition("behavior", "logger_name"); -transition->AddTransitionMatrix(some_matrix); -model->AddTransition(transition); +// Build one timestep with transitions +respond::Timestep step("logger_name"); +auto &transition = step.CreateTransition("behavior"); +transition->AddMatrix(some_matrix); +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 -- `GetModelName() const`: Returns model name -- `GetLogName() const`: Returns associated logger name +- `ClearHistories()`: Clears history records and resets history tracking +- `GetName() const`: Returns model 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 -auto transition = respond::TransitionFactory::CreateTransition( - "behavior", // Type: migration, behavior, intervention, overdose, background_death - "my_logger" // Logger name +// Create a transition +auto transition = respond::Transition::Create( + "behavior", // Type + "behavior_name", // Instance name + "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) +// 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::TransitionFactory::CreateTransition( - "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::TransitionFactory::CreateTransition( - "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 672dd77e..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) @@ -117,7 +110,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**: @@ -125,14 +118,15 @@ auto transition = TransitionFactory::CreateTransition("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,9 +197,9 @@ 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 `TransitionFactory::CreateTransition()` +3. Add factory entry in `Transition::Create()` Example: ```cpp 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 779f5394..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::TransitionFactory::CreateTransition( - "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 91869cf8..8f32d525 100644 --- a/docs/src/uml.md +++ b/docs/src/uml.md @@ -13,87 +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() * - +RunTransitions() * + +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 { - +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~ + +Timestep() + +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 { @@ -154,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) @@ -176,7 +233,7 @@ classDiagram } class Markov { - -vector~shared_ptr~Transition~~ _transition_vector + -vector~Timestep~ _timestep_vector -VectorXd _state -string _name -string _log_name @@ -198,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 @@ -218,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 - +GetTransitionName() string override - +ClearTransitionMatrices() override - +GetLogName() string override + +GetMatrices() const vector~MatrixXd~ + +AddMatrix(const Eigen::Ref~const MatrixXd~ &) override + +GetName() string override + +ClearMatrices() override + +Serialize(ostream &) override } class Migration { @@ -276,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/extras/benchmark/src/benchmark_respond.cpp b/extras/benchmark/src/benchmark_respond.cpp index e484bb67..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-04-27 // +// 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,58 +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::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"); - - 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->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)); + 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(); @@ -423,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); } @@ -442,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 new file mode 100644 index 00000000..a00e8aaf --- /dev/null +++ b/include/respond/constants.hpp @@ -0,0 +1,20 @@ +//////////////////////////////////////////////////////////////////////////////// +// File: constants.hpp // +// 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 // +//////////////////////////////////////////////////////////////////////////////// + +#ifndef RESPOND_CONSTANTS_HPP_ +#define RESPOND_CONSTANTS_HPP_ + +#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/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 e92598d7..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-25 // +// 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,32 +50,75 @@ inline HistoryMode GetDefaultHistoryMode(const std::string &name) { /// are filled with zero vectors). class History { public: - /// @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(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. + //////////////////////////////////////////////////////////////////////////// + // + // 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") {} + + /// @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, 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(); } @@ -72,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; @@ -86,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); } @@ -99,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. @@ -147,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; } @@ -159,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(); @@ -166,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. @@ -179,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(); @@ -187,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++; @@ -200,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(); - } + //////////////////////////////////////////////////////////////////////////// + // + // Operator Comparisons and Stream Output + // + //////////////////////////////////////////////////////////////////////////// - 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; - } - - 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 ee12217f..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-06-25 // +// Last Modified: 2026-07-14 // // 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,99 +29,163 @@ 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 Eigen::VectorXd 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 + // + //////////////////////////////////////////////////////////////////////////// + + /// @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. - /// @param t A unique_ptr to a Transition object. The model assumes - /// ownership. - virtual void AddTransition(const std::unique_ptr &t) = 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 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; - /// @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; + + /// @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/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 b9b7fb55..637ea9db 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-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,34 +25,134 @@ #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("console") {} + //////////////////////////////////////////////////////////////////////////// + // + // 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"). - Simulation(const std::string &log_name) : _log_name(log_name) {} + /// @param log_name The name of the logger to use for simulation output. + Simulation(const std::string &log_name) + : Simulation(log_name, RESPOND_DEFAULT_LOG_FILE) {} + + /// @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); + } /// @brief Virtual destructor for polymorphic cleanup. ~Simulation() = default; - /// @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->RunTransitions(); + /// @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 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. /// @param model A unique_ptr to a Model instance to add. @@ -57,101 +162,141 @@ 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 { 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 { std::vector ret; for (auto &m : _models) { - ret.push_back(m->GetModelName()); + ret.push_back(m->GetName()); } return ret; } - /// @brief Removes all models from the simulation. - void ClearModels() { _models.clear(); } - - /// @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; - for (const auto &model : _models) { - for (const auto &kv : model->GetHistories()) { - std::pair p = {model->GetModelName(), - kv.first}; - ret.push_back(p); - } + 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."); } - 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()); + std::vector ret; + for (const auto &kv : _models[idx]->GetHistories()) { + ret.push_back(kv.first); } + return ret; } - /// @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()); + const std::vector + GetModelHistoryNames(const std::string &model_name) const { + for (const auto &model : _models) { + if (model->GetName() == model_name) { + std::vector ret; + for (const auto &kv : model->GetHistories()) { + ret.push_back(kv.first); + } + return ret; } } - return *this; + 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; } + 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..097693f9 --- /dev/null +++ b/include/respond/timestep.hpp @@ -0,0 +1,316 @@ +//////////////////////////////////////////////////////////////////////////////// +// File: timestep.hpp // +// Project: respond // +// Created Date: 2026-06-30 // +// Author: Matthew Carroll // +// ----- // +// Last Modified: 2026-07-14 // +// 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 { + +/// @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, 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); + } + + /// @brief Destructor for Timestep. Default implementation. + ~Timestep() = default; + + /// @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(std::move(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(std::move(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, transition_name, _log_name)); + 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: " + + 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 + /// 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()) { + 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); + } + + /// @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) { + if (_transitions[i]->GetName() == transition_name) { + _transitions[i]->AddMatrix(m); + return; + } + } + LogWarning(_log_name, + "Transition not found in AddMatrixToTransition: " + + 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()) { + 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) { + if (t->GetName() == transition_name) { + return t; + } + } + 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) { + _ret.push_back(t->clone()); + } + return _ret; + } + + /// @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()); + } + return names; + } + + //////////////////////////////////////////////////////////////////////////// + // + // Logging and Output Methods + // + //////////////////////////////////////////////////////////////////////////// + + /// @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) { + os << " - " << t->GetName() << "\n"; + } + return os; + } + + /// @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; + } + 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 true; + } + + /// @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: + 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..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-06-25 // +// Last Modified: 2026-07-14 // // 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 { @@ -46,19 +48,18 @@ 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(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 GetTransitionName() const = 0; + virtual std::string GetName() const = 0; /// @brief Clears all stored transition matrices. - virtual void ClearTransitionMatrices() = 0; - - /// @brief Retrieves the logger name used by this transition. - /// @return The associated logger's name. - virtual std::string GetLogName() const = 0; + virtual void ClearMatrices() = 0; /// @brief Deleted copy constructor (transitions are non-copyable by public /// API). @@ -71,11 +72,52 @@ 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 &name = RESPOND_DEFAULT_TRANSITION_NAME, + 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/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/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 10fd08f7..46c49061 100644 --- a/src/background.cpp +++ b/src/background.cpp @@ -4,51 +4,35 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-25 // +// Last Modified: 2026-07-13 // // 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 (GetTransitionMatrices().size() != 1) { - std::string error_msg = - "Background death error: Expected 1 transition matrix, got " + - std::to_string(GetTransitionMatrices().size()); - LogError(GetLogName(), error_msg); - throw std::runtime_error(error_msg); - } - auto deaths = - state.cwiseProduct(GetTransitionMatrices()[0]); // calculate the deaths + TestCorrectNumberMatrices(1); + TestMatrixSizes(state, GetMatrices()[0]); + Eigen::VectorXd deaths = state.cwiseProduct(GetMatrices()[0]); + 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); } - 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; } - -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..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-06-25 // +// Last Modified: 2026-07-13 // // 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,30 +23,9 @@ namespace respond { Eigen::VectorXd Behavior::Execute(const Eigen::Ref &state, std::map &h) const { - if (GetTransitionMatrices().size() != 1) { - std::string error_msg = - "Behavior error: Expected 1 transition matrix, got " + - std::to_string(GetTransitionMatrices().size()); - LogError(GetLogName(), error_msg); - throw std::runtime_error(error_msg); - } - if (state.rows() != GetTransitionMatrices()[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() << ")"; - std::string error_msg = ss.str(); - LogError(GetLogName(), error_msg); - throw std::runtime_error(error_msg); - } - auto new_state = GetTransitionMatrices()[0] * state; + TestCorrectNumberMatrices(1); + TestRowColDimensions(state, GetMatrices()[0]); + Eigen::VectorXd 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/background.hpp b/src/internals/background.hpp index 1ff631e6..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-06-25 // +// Last Modified: 2026-07-13 // // 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,19 +42,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(), _log_name); + 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..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-06-25 // +// Last Modified: 2026-07-13 // // 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,19 +41,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(), _log_name); + 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..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-06-25 // +// Last Modified: 2026-07-13 // // 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,19 +42,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(), _log_name); + 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..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-06-25 // +// Last Modified: 2026-07-14 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -20,72 +20,230 @@ #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.. + 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); } + + /// @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); + } + 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 { + SetupHistory(); + if (!_initial_history_recorded) { + RecordHistoryAtCurrentTimestep(); + } + 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(); + } + } + + void ClearTimesteps() override { _timestep_vector.clear(); } + + void ClearHistories() override { + _histories.clear(); + ResetHistoryTracking(); } /// @brief The default histories are: @@ -97,48 +255,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", HistoryMode::kSnapshot, _log_name); ret["total_overdose"] = - History("total_overdose", GetLogName(), HistoryMode::Accumulated); + History("total_overdose", HistoryMode::kAccumulated, _log_name); ret["fatal_overdose"] = - History("fatal_overdose", GetLogName(), HistoryMode::Accumulated); + History("fatal_overdose", HistoryMode::kAccumulated, _log_name); ret["intervention_admission"] = History( - "intervention_admission", GetLogName(), HistoryMode::Accumulated); + "intervention_admission", HistoryMode::kAccumulated, _log_name); ret["background_death"] = - History("background_death", GetLogName(), HistoryMode::Accumulated); - SetHistories(ret); - } - - // manipulate the state vector - void RunTransitions() override { - SetupHistory(); - if (!_initial_history_recorded) { - RecordHistoryAtCurrentTimestep(); - } - 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()); - } - // 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", HistoryMode::kAccumulated, _log_name); + _histories = ret; if (_histories.empty()) { ResetHistoryTracking(); return; @@ -153,36 +279,29 @@ 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; + 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"; + } } - // 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; @@ -223,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 c63821e5..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-06-25 // +// Last Modified: 2026-07-13 // // 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,19 +41,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(), _log_name); + 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..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-06-25 // +// Last Modified: 2026-07-13 // // 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,19 +41,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(), _log_name); + 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..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-06-25 // +// Last Modified: 2026-07-14 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -12,38 +12,134 @@ #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) // and can accept the const type. - void - AddTransitionMatrix(const Eigen::Ref &m) override { + void AddMatrix(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 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; } + void Serialize(std::ostream &os) const override { + os << "Transition(name=" << _name + << ", num_matrices=" << _transition_matrices.size() << ")"; + } protected: - const std::vector &GetTransitionMatrices() const { - return _transition_matrices; + 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, + 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); + } } private: std::string _name; - std::string _log_name; std::vector _transition_matrices; }; diff --git a/src/intervention.cpp b/src/intervention.cpp index 70e50b9c..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-06-25 // +// Last Modified: 2026-07-13 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -22,27 +22,13 @@ namespace respond { Eigen::VectorXd Intervention::Execute(const Eigen::Ref &state, std::map &h) const { - if (GetTransitionMatrices().size() != 1) { - std::string error_msg = - "Intervention error: Expected 1 transition matrix, got " + - std::to_string(GetTransitionMatrices().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() != GetTransitionMatrices()[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() << ")"; - std::string error_msg = ss.str(); - LogError(GetLogName(), error_msg); - throw std::runtime_error(error_msg); - } - auto moved = GetTransitionMatrices()[0] * state; + TestRowColDimensions(state, trans_matrix); + Eigen::VectorXd moved = trans_matrix * state; // Add intervention_admissions to history if avaliable Eigen::VectorXd admissions = moved - state; @@ -53,9 +39,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/logging.cpp b/src/logging.cpp index 032364ca..74fcd724 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-07-09 // // Modified By: Matthew Carroll // // ----- // -// Copyright (c) 2025 Syndemics Lab at Boston Medical Center // +// Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center // //////////////////////////////////////////////////////////////////////////////// #include @@ -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 { @@ -36,6 +35,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/migration.cpp b/src/migration.cpp index 0ee5e4df..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-06-25 // +// Last Modified: 2026-07-13 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -22,29 +22,11 @@ namespace respond { Eigen::VectorXd Migration::Execute(const Eigen::Ref &state, std::map &h) const { - if (GetTransitionMatrices().size() != 1) { - std::string error_msg = - "Migration error: Expected 1 transition matrix, got " + - std::to_string(GetTransitionMatrices().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()) + ")"; - LogError(GetLogName(), error_msg); - throw std::runtime_error(error_msg); - } - auto subtracted = state + GetTransitionMatrices()[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; } - -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/markov.cpp b/src/model_factory.cpp similarity index 79% rename from src/markov.cpp rename to src/model_factory.cpp index 48bfaf9e..d2a57d35 100644 --- a/src/markov.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..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-06-25 // +// Last Modified: 2026-07-13 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -22,57 +22,27 @@ namespace respond { Eigen::VectorXd Overdose::Execute(const Eigen::Ref &state, std::map &h) const { - if (GetTransitionMatrices().size() != 2) { - std::string error_msg = - "Overdose error: Expected 2 transition matrices, got " + - std::to_string(GetTransitionMatrices().size()); - LogError(GetLogName(), error_msg); - throw std::runtime_error(error_msg); - } + TestCorrectNumberMatrices(2); + auto matrices = GetMatrices(); - 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()) + ")"; - LogError(GetLogName(), error_msg); - throw std::runtime_error(error_msg); - } - Eigen::VectorXd overdoses = - state.cwiseProduct(GetTransitionMatrices()[0]); // overdose - // Add total overdoses to stamp + 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); } - 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()) + ")"; - LogError(GetLogName(), error_msg); - throw std::runtime_error(error_msg); - } - auto fods = overdoses.cwiseProduct(GetTransitionMatrices()[1]); // negatives + TestMatrixSizes(overdoses, GetMatrices()[1]); + 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); } - 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); - } - 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/src/transition_factory.cpp b/src/transition_factory.cpp index 267d383e..eb87e726 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-14 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // //////////////////////////////////////////////////////////////////////////////// -#include - #include #include #include @@ -26,23 +24,24 @@ #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 &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 Migration::Create(type, log_name); + return std::make_unique(name, log_name, log_file); } else if (type_copy == "behavior") { - return Behavior::Create(type, log_name); + return std::make_unique(name, log_name, log_file); } else if (type_copy == "intervention") { - return Intervention::Create(type, log_name); + return std::make_unique(name, log_name, log_file); } else if (type_copy == "overdose") { - return Overdose::Create(type, log_name); + return std::make_unique(name, log_name, log_file); } else if (type_copy == "background_death") { - return BackgroundDeath::Create(type, log_name); + return std::make_unique(name, log_name, log_file); } // Invalid transition type @@ -50,6 +49,6 @@ TransitionFactory::CreateTransition(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/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 ce576615..5fd25941 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-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 = TransitionFactory::CreateTransition(name, "test_log"); - migr->AddTransitionMatrix(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,110 +30,132 @@ 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; + + tolerance << 1e-5, 1e-5, 1e-5; + + sim.CreateNewModel("markov"); + sim.GetModels()[0]->CreateDefaultHistories(); + sim.GetModels()[0]->SetState(init_state); } - void TearDown() override { markov.reset(); } -}; + void TearDown() override { + // Clean up loggers + spdlog::drop_all(); -TEST_F(RespondTest, RunTransitionsInModel) { - markov->SetState(init_state); + // Remove test files + std::remove(default_log_file_.c_str()); + } - auto migr = MakeTestTransition("migration", migration_pop); - markov->AddTransition(migr); + std::string default_log_file_; - auto beha = MakeTestTransition("behavior", behavior_trans); - markov->AddTransition(beha); + Timestep CreateTestTimestep() { - auto inte = MakeTestTransition("intervention", intervention_trans); - markov->AddTransition(inte); + Timestep ts; + ts.CreateTransition("migration"); + ts.AddMatrixToTransition("migration", migration_pop); - auto over = MakeTestTransition("overdose", overdose_prob); - over->AddTransitionMatrix(fod_prob); - markov->AddTransition(over); + ts.CreateTransition("behavior"); + ts.AddMatrixToTransition("behavior", behavior_trans); - auto back = MakeTestTransition("background_death", background_death_prob); - markov->AddTransition(back); + ts.CreateTransition("intervention"); + ts.AddMatrixToTransition("intervention", intervention_trans); - markov->RunTransitions(); + ts.CreateTransition("overdose"); + ts.AddMatrixToTransition("overdose", overdose_prob); + ts.AddMatrixToTransition("overdose", fod_prob); - auto t_names = markov->GetTransitionNames(); - std::vector expected = {"migration", "behavior", - "intervention", "overdose", - "background_death"}; - ASSERT_EQ(t_names, expected); + ts.CreateTransition("background_death"); + ts.AddMatrixToTransition("background_death", background_death_prob); + return ts; + } +}; + +TEST_F(RespondTest, RunSingleTimestep) { + sim.GetModels()[0]->AddTimestep(CreateTestTimestep()); + sim.Run(); + Eigen::VectorXd result = + sim.GetModelHistory(0).at("state").GetStateAsVector().back(); Eigen::Vector3d final_state; final_state << 0.76715528791564891, 0.72320370216816077, 1.037712429738102; - ASSERT_TRUE(markov->GetState().isApprox(final_state)); + ASSERT_TRUE(result.isApprox(final_state)); } -TEST_F(RespondTest, RunSimulationOneStep) { - markov->CreateDefaultHistories(); +TEST_F(RespondTest, RunSimulationTwoStep) { + sim.GetModels()[0]->AddTimestep(CreateTestTimestep()); + sim.GetModels()[0]->AddTimestep(CreateTestTimestep()); + sim.Run(2); - markov->SetState(init_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); - 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; + ASSERT_TRUE(state_history[0].isApprox(init_state)); - auto back = MakeTestTransition("background_death", background_death_prob); - markov->AddTransition(back); + final_state << 0.46999281, 0.44109648, 0.631613324; + Eigen::Vector3d diff = (state_history[2] - final_state).cwiseAbs(); + ASSERT_TRUE((diff.array() <= tolerance.array()).all()); +} - Simulation sim("test_logger"); - sim.AddModel(markov); +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."; - } - - auto state_history = mm_histories.at("state"); - // 2 because it carries the initial state and 1 step - ASSERT_EQ(state_history.size(), 2); + 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); 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)); + + final_state << 0.10714013, 0.10056269, 0.14400034; + Eigen::Vector3d diff = (state_history[5] - final_state).cwiseAbs(); + ASSERT_TRUE((diff.array() <= tolerance.array()).all()); } -TEST_F(RespondTest, CreateDefaultHistories) { - std::vector expected = { - "state", "total_overdose", "fatal_overdose", "intervention_admission", - "background_death"}; +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); - std::sort(expected.begin(), expected.end()); + 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); - 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 diff --git a/tests/mocks/model_mock.hpp b/tests/mocks/model_mock.hpp index 57c82850..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-06-25 // +// Last Modified: 2026-07-14 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center // @@ -14,9 +14,11 @@ #include #include +#include #include #include +#include #include #include @@ -24,34 +26,34 @@ #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)); + 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 1e375741..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-06-25 // +// Last Modified: 2026-07-14 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -12,17 +12,17 @@ #ifndef RESPOND_TESTS_MARKOVMOCK_HPP_ #define RESPOND_TESTS_MARKOVMOCK_HPP_ +#include #include #include #include +#include #include #include #include -#include - namespace respond { namespace testing { class MockTransition : public virtual Transition { @@ -31,12 +31,14 @@ 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(std::string, GetLogName, (), (const, override)); + MOCK_METHOD(void, AddMatrix, (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::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 92aaca2c..1077f787 100644 --- a/tests/unit/background_test.cpp +++ b/tests/unit/background_test.cpp @@ -4,74 +4,172 @@ // Created Date: 2026-02-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-02-06 // +// Last Modified: 2026-07-13 // // 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 +#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 = TransitionFactory::CreateTransition("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, TooManyTransitionMatrices) { - tran->AddTransitionMatrix(state); - tran->AddTransitionMatrix(state); - EXPECT_THROW(tran->Execute(state, histories), std::runtime_error); +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, GoodExecuteNoHistory) { - tran->AddTransitionMatrix(tran_matrix); - auto result = tran->Execute(state, histories); - auto expected = state - state.cwiseProduct(tran_matrix); - EXPECT_TRUE(result.isApprox(expected)); +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, 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, 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->AddTransitionMatrix(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 ffc547f7..e89b71f1 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-02-06 // +// Last Modified: 2026-07-13 // // 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 = TransitionFactory::CreateTransition("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->AddTransitionMatrix(tran_matrix); - tran->AddTransitionMatrix(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->AddTransitionMatrix(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->AddTransitionMatrix(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/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 192449e1..94744991 100644 --- a/tests/unit/intervention_test.cpp +++ b/tests/unit/intervention_test.cpp @@ -4,80 +4,182 @@ // Created Date: 2026-02-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-05-06 // +// Last Modified: 2026-07-13 // // 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 = - TransitionFactory::CreateTransition("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->AddTransitionMatrix(tran_matrix); - tran->AddTransitionMatrix(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->AddTransitionMatrix(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->AddTransitionMatrix(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->AddTransitionMatrix(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 6cc1627c..7a172219 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-14 // // Modified By: Matthew Carroll // // ----- // -// Copyright (c) 2025 Syndemics Lab at Boston Medical Center // +// Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center // //////////////////////////////////////////////////////////////////////////////// #include @@ -22,7 +22,7 @@ #include #include -#include +#include namespace respond { namespace testing { @@ -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::TransitionFactory::CreateTransition( - "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); } @@ -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..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-02-06 // +// Last Modified: 2026-07-09 // // 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,345 @@ 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(); + EXPECT_EQ(markov.GetTimestepAtIndex(0), timestep1); + EXPECT_EQ(markov.GetTimestepAtIndex(1), timestep2); +} - const auto histories = markov->GetHistories(); - ASSERT_EQ(histories.size(), 5u); +TEST_F(MarkovTest, GetAndSetState) { + Markov markov("markov", RESPOND_DEFAULT_LOG); + markov.SetState(state); + auto result = markov.GetState(); + EXPECT_TRUE(result.isApprox(state)); +} - 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, GetName) { + Markov markov("markov_test", RESPOND_DEFAULT_LOG); + EXPECT_EQ(markov.GetName(), "markov_test"); +} - 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, 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()); } -TEST_F(MarkovTest, SparseHistoryCaptureRecordsRequestedAndFinalTimesteps) { - markov->SetHistoryCaptureInterval(2); - markov->SetFinalTimestep(5); - markov->SetState(state); +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); +} - for (int step = 0; step < 5; ++step) { - markov->RunTransitions(); - } +TEST_F(MarkovTest, GetAndSetHistoryCaptureInterval) { + Markov markov("markov", RESPOND_DEFAULT_LOG); + EXPECT_EQ(markov.GetHistoryCaptureInterval(), 1); + markov.SetHistoryCaptureInterval(5); + EXPECT_EQ(markov.GetHistoryCaptureInterval(), 5); +} - 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, GetAndSetFinalTimestep) { + Markov markov("markov", RESPOND_DEFAULT_LOG); + EXPECT_EQ(markov.GetFinalTimestep(), -1); + markov.SetFinalTimestep(10); + EXPECT_EQ(markov.GetFinalTimestep(), 10); } -TEST_F(MarkovTest, ClearHistoriesResetsTrackingState) { - markov->SetHistoryCaptureInterval(2); - markov->SetFinalTimestep(4); - markov->SetState(state); +TEST_F(MarkovTest, GetAndSetInitialHistoryRecorded) { + Markov markov("markov", RESPOND_DEFAULT_LOG); + EXPECT_FALSE(markov.GetInitialHistoryRecorded()); + markov.SetInitialHistoryRecorded(true); + EXPECT_TRUE(markov.GetInitialHistoryRecorded()); +} - markov->RunTransitions(); - markov->RunTransitions(); - markov->ClearHistories(); +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)); +} - Eigen::VectorXd next_state = state * 3.0; - markov->SetState(next_state); - markov->RunTransitions(); +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 histories = markov->GetHistories(); - const auto ×teps = histories.at("state").GetRecordedTimesteps(); - std::vector expected = {0}; - ASSERT_EQ(timesteps, expected); +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); +} - const auto &states = histories.at("state").GetRecordedStates(); - ASSERT_EQ(states.size(), 1u); - EXPECT_TRUE(states[0].isApprox(next_state)); +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")); } -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, 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 +} - markov->AddTransition(std::move(upmt)); - markov->ClearTransitions(); - auto names = markov->GetTransitionNames(); - ASSERT_EQ(names.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, EmptyHistories) { - auto result = markov->GetHistories(); - ASSERT_EQ(result.size(), 0); +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, 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, 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, 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..4d4d0889 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-02-06 // +// Last Modified: 2026-07-13 // // 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 = TransitionFactory::CreateTransition("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->AddTransitionMatrix(state); - tran->AddTransitionMatrix(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->AddTransitionMatrix(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->AddTransitionMatrix(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 6dcd488f..0023fd60 100644 --- a/tests/unit/overdose_test.cpp +++ b/tests/unit/overdose_test.cpp @@ -4,129 +4,208 @@ // Created Date: 2026-02-06 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-05-06 // +// Last Modified: 2026-07-13 // // 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/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 = TransitionFactory::CreateTransition("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->AddTransitionMatrix(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->AddTransitionMatrix(state); - tran->AddTransitionMatrix(state); - tran->AddTransitionMatrix(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->AddTransitionMatrix(bad_t_matrix); - tran->AddTransitionMatrix(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->AddTransitionMatrix(tran_matrix); - tran->AddTransitionMatrix(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); -TEST_F(OverdoseTest, GoodExecuteWriteTotalOverdoseHistory) { - History h("total_overdose", "test_logger"); - histories["total_overdose"] = h; - tran->AddTransitionMatrix(tran_matrix); - tran->AddTransitionMatrix(tran_matrix); - auto result = tran->Execute(state, histories); + histories["state"] = History("state"); + Eigen::VectorXd result = overdose.Execute(state, histories); + Eigen::VectorXd expected_overdoses = state.cwiseProduct(tran_matrix); - auto overdoses = state.cwiseProduct(tran_matrix); - auto fods = overdoses.cwiseProduct(tran_matrix); - auto expected_return = state - fods; + Eigen::VectorXd expected_fods = + expected_overdoses.cwiseProduct(tran_matrix); + Eigen::VectorXd expected_new_state = state - expected_fods; - EXPECT_TRUE(result.isApprox(expected_return)); - EXPECT_TRUE(histories["total_overdose"].HasPendingState()); - EXPECT_TRUE( - histories["total_overdose"].GetPendingState().isApprox(overdoses)); + EXPECT_TRUE(result.isApprox(expected_new_state)); } -TEST_F(OverdoseTest, GoodExecuteWriteFatalOverdoseHistory) { - History h("fatal_overdose", "test_logger"); - histories["fatal_overdose"] = h; - tran->AddTransitionMatrix(tran_matrix); - tran->AddTransitionMatrix(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, 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, GoodExecuteWriteAllHistory) { - History h1("fatal_overdose", "test_logger"); - histories["fatal_overdose"] = h1; - History h2("total_overdose", "test_logger"); - histories["total_overdose"] = h2; - tran->AddTransitionMatrix(tran_matrix); - tran->AddTransitionMatrix(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, 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, 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 06b3140f..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-02-12 // +// Last Modified: 2026-07-15 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -13,156 +13,254 @@ #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) { +TEST_F(SimulationTest, DefaultConstructor) { Simulation s; - ASSERT_EQ(s.GetLogName(), "console"); + ASSERT_EQ(CreateFileLogger(RESPOND_DEFAULT_LOG, default_log_file_), + CreationStatus::kExists); } -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)))); +TEST_F(SimulationTest, ConstructorWithLogName) { + Simulation s("custom_log"); + ASSERT_EQ(CreateFileLogger("custom_log", default_log_file_), + CreationStatus::kExists); +} - std::unique_ptr upmm = std::move(mock); +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; - s.AddModel(upmm); + 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, ClearModels) { - auto mock = std::make_unique>(); - auto cloned = std::make_unique>(); - EXPECT_CALL(*mock, clone()) - .WillOnce(Return(::testing::ByMove(std::move(cloned)))); +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); +} - std::unique_ptr upmm = std::move(mock); +TEST_F(SimulationTest, CreateModelWithExistingName) { Simulation s; - s.AddModel(upmm); + 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, 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); +TEST_F(SimulationTest, AddModel) { Simulation s; - s.AddModel(upmm); - auto result = s.GetModelNames(); - ASSERT_EQ(result.size(), 1); - ASSERT_EQ(result[0], expected); + 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, GetModelHistories) { - auto mock = std::make_unique>(); +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(); +} - 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; +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)); - std::map> h_map; - h_map["temp"] = h.GetStateAsVector(); + 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)); - std::vector>> expected; - expected.push_back(h_map); + s.Run(); +} - ON_CALL(*cloned, GetModelName()).WillByDefault(Return("temp_model")); +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)); - EXPECT_CALL(*cloned, GetHistories()).WillOnce(Return(hv)); - ON_CALL(*mock, clone()) - .WillByDefault(Return(::testing::ByMove(std::move(cloned)))); + const auto &models = s.GetModels(); + ASSERT_EQ(models.size(), 1); +} - std::unique_ptr upmm = std::move(mock); +TEST_F(SimulationTest, GetModelNames) { Simulation s; - s.AddModel(upmm); - ASSERT_EQ(s.GetModelHistories(), expected); + 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, 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; +TEST_F(SimulationTest, GetModelHistories) { + Simulation s; - std::vector> expected = { - {model_name, history_name}}; + 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}}; - ON_CALL(*cloned, GetModelName()).WillByDefault(Return(model_name)); + 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)); - EXPECT_CALL(*cloned, GetHistories()).WillOnce(Return(hv)); - ON_CALL(*mock, clone()) - .WillByDefault(Return(::testing::ByMove(std::move(cloned)))); + const auto model_histories = s.GetModelHistory(0); + ASSERT_EQ(model_histories.size(), 1); + ASSERT_EQ(model_histories.size(), 1); - std::unique_ptr upmm = std::move(mock); - Simulation s; - s.AddModel(upmm); - ASSERT_EQ(s.GetModelHistoryNames(), expected); + 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, 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); +TEST_F(SimulationTest, GetModelHistoryNames) { 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)); + + 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(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 diff --git a/tests/unit/timestep_test.cpp b/tests/unit/timestep_test.cpp new file mode 100644 index 00000000..55a128bc --- /dev/null +++ b/tests/unit/timestep_test.cpp @@ -0,0 +1,179 @@ +//////////////////////////////////////////////////////////////////////////////// +// File: timestep_test.cpp // +// Project: respond // +// Created Date: 2026-07-06 // +// Author: Matthew Carroll // +// ----- // +// Last Modified: 2026-07-09 // +// 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, 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 = + 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