diff --git a/Doxyfile.in b/Doxyfile.in index 244de801..b643294c 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.5.0 +PROJECT_NUMBER = 2.5.1 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/README.md b/README.md index 58973237..8070ff8c 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,12 @@ target_link_libraries(${PROJECT_NAME} Then see the [C++ API Guide][api-guide] for usage examples. +### C++ Model Access Notes + +- `Simulation` owns models and supports index-based access via `operator[]` for mutating owned models (for example `sim[0]->RunTimesteps()`). +- `GetModels()` and `GetModel(...)` return cloned models for detached inspection/copy workflows. +- Use `GetModelIndexNameMap()` when you need to resolve model names to stable indices. + ### Building Documentation To build the Doxygen documentation: diff --git a/docs/src/api-guide.md b/docs/src/api-guide.md index 16af7938..91d59034 100644 --- a/docs/src/api-guide.md +++ b/docs/src/api-guide.md @@ -51,8 +51,14 @@ model->SetState(initial_state); // Build one timestep with transitions respond::Timestep step("logger_name"); -auto &transition = step.CreateTransition("behavior"); -transition->AddMatrix(some_matrix); +auto &behavior_transition = step.CreateTransition("behavior"); +behavior_transition->AddMatrix(some_matrix); + +auto migration_transition = respond::Transition::Create("migration"); +step.AddTransition(migration_transition); + +// Mutable index access to owned transition slots +step[0]->AddMatrix(some_other_matrix); model->AddTimestep(step); // Execute one simulation step @@ -96,6 +102,10 @@ auto model2 = respond::Model::Create("model2", "my_logger"); sim.AddModel(model1); sim.AddModel(model2); +// Mutate owned models directly via index +sim[0]->CreateDefaultHistories(); +sim[1]->CreateDefaultHistories(); + // Run 52 timesteps for all models sim.Run(52); @@ -112,15 +122,65 @@ auto history_names = sim.GetModelHistoryNames(0); - `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 +- `operator[](size_t idx)`: Mutable index access to owned model slot (`sim[idx]->Method()`) +- `operator[](size_t idx) const`: Const index access to owned model +- `GetModels() const`: Returns a deep-copied vector of models +- `GetModel(int idx) const`: Returns one deep-copied model by index (`-1` returns last) +- `GetModelIndexNameMap() const`: Returns map of model index to model name - `GetModelNames() const`: Returns all model names - `ClearModels()`: Removes all models - `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 + +## Timestep Class + +The Timestep class owns transitions for one model step and supports both +transition creation and clone-based insertion. + +```cpp +#include +#include + +respond::Timestep step("my_logger"); + +// Build transition in-place +auto &behavior = step.CreateTransition("behavior"); +behavior->AddMatrix(behavior_matrix); + +// Add an existing transition by clone +auto migration = respond::Transition::Create("migration"); +step.AddTransition(migration); + +// Mutable slot access (in-place edits) +step[0]->AddMatrix(another_behavior_matrix); + +// Replace a slot by cloning from another slot or transition pointer +step[1] = step[0]; +step[1] = migration; + +// Const slot access +const respond::Timestep &const_step = step; +const respond::Transition &t = const_step[0]; +``` + +### Key Methods + +- `CreateTransition(const std::string &transition_name)`: Creates and stores a transition by type +- `AddTransition(const std::unique_ptr &transition)`: Clones and stores caller-provided transition +- `operator[](size_t idx)`: Mutable slot access for transition mutation/replacement +- `operator[](size_t idx) const`: Const transition reference by index +- `GetTransition(const size_t &idx) const`: Gets transition pointer by index +- `GetTransition(const std::string &transition_name) const`: Gets transition pointer by name +- `GetTransitionNames() const`: Returns transition names in execution order +- `RemoveTransition(size_t idx)`: Removes and returns transition at index + +### Model Access Semantics + +- `sim[idx]` accesses the model owned by `Simulation` and can be used for in-place mutation. +- `sim[idx] = *other_model` replaces the model at `idx` by cloning `other_model`. +- `sim[idx] = other_model_ptr` replaces the model at `idx` by cloning the pointee (caller retains ownership). +- `GetModels()` and `GetModel(...)` return clones for safe detached access. +- Name-based retrieval is not provided; use `GetModelIndexNameMap()` to resolve names to indices. ## History Class @@ -260,6 +320,9 @@ int main() { // Add model to simulation sim.AddModel(model); + // Configure the owned model through Simulation indexing + sim[0]->CreateDefaultHistories(); + // Run simulation for 52 timesteps sim.Run(52); diff --git a/docs/src/run.md b/docs/src/run.md index 27d92d97..85ca6ef5 100644 --- a/docs/src/run.md +++ b/docs/src/run.md @@ -38,8 +38,14 @@ int main() { // Build one timestep and attach transitions respond::Timestep step("my_logger"); - auto &transition = step.CreateTransition("behavior"); - // transition->AddMatrix(...); + auto &behavior = step.CreateTransition("behavior"); + // behavior->AddMatrix(...); + + auto migration = respond::Transition::Create("migration"); + step.AddTransition(migration); + + // Mutable slot access to transitions owned by this timestep + // step[0]->AddMatrix(...); // Add timesteps to the model for (int t = 0; t < 52; ++t) { diff --git a/docs/src/uml.md b/docs/src/uml.md index 8f32d525..850ecd7b 100644 --- a/docs/src/uml.md +++ b/docs/src/uml.md @@ -20,18 +20,18 @@ classDiagram +operator=(const Simulation &) Simulation& +Simulation(Simulation &&other) +operator=(Simulation &&) Simulation& - +CreateNewModel(const string &) const string + +CreateNewModel(const string &) unique_ptr~Model~ +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~ & + +operator[](size_t idx) + +operator[](size_t idx) const + +GetModels() const vector~unique_ptr~Model~~ + +GetModel(int model_idx) const unique_ptr~Model~ + +GetModelIndexNameMap() const map~size_t, string~ +GetModelNames() vector~string~ +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 & } @@ -72,6 +72,7 @@ classDiagram +Timestep(const Timestep &&) +operator=(const Timestep &&) Timestep & +CreateTransition(const string &) const unique_ptr~Transition~ & + +AddTransition(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~ &) @@ -79,6 +80,8 @@ classDiagram +GetTransition(const string &) const unique_ptr~Transition~ & +GetTransitions() vector~unique_ptr~Transition~~ +GetTransitionNames() vector~string~ + +operator[](size_t) TransitionSlotProxy + +operator[](size_t) const Transition & +operator<<(ostream &os, const Timestep &obj) ostream & +operator==(const Timestep &, const Timestep &) bool +operator!=(const Timestep &, const Timestep &) bool diff --git a/include/respond/simulation.hpp b/include/respond/simulation.hpp index 637ea9db..bd0d0eaf 100644 --- a/include/respond/simulation.hpp +++ b/include/respond/simulation.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-05 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-13 // +// Last Modified: 2026-07-22 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -31,6 +31,54 @@ namespace respond { /// maintaining history records and providing access to simulation results. class Simulation { public: + /// @brief Proxy for mutable model slot access with clone-based assignment. + class ModelSlotProxy { + public: + ModelSlotProxy(Simulation &owner, size_t idx) + : _owner(&owner), _idx(idx) {} + + /// @brief Access the underlying model pointer for member access. + Model *operator->() { return &_owner->GetModelRefOrThrow(_idx); } + + /// @brief Dereference to the underlying model. + Model &operator*() { return _owner->GetModelRefOrThrow(_idx); } + + /// @brief Implicit conversion to underlying mutable model reference. + operator Model &() { return _owner->GetModelRefOrThrow(_idx); } + + /// @brief Replace this slot by cloning from another proxy's model. + ModelSlotProxy &operator=(const ModelSlotProxy &other) { + return *this = static_cast( + other._owner->GetModelRefOrThrow(other._idx)); + } + + /// @brief Replace this slot by cloning from a model reference. + ModelSlotProxy &operator=(const Model &model) { + _owner->GetModelRefOrThrow(_idx); + _owner->_models[_idx] = model.clone(); + return *this; + } + + /// @brief Replace this slot by cloning from a model unique_ptr. + /// @throws std::invalid_argument if model is nullptr. + ModelSlotProxy &operator=(const std::unique_ptr &model) { + if (!model) { + LogError(_owner->_log_name, + "Cannot assign null model pointer to simulation " + "slot."); + throw std::invalid_argument( + "Error attempting to assign null model pointer."); + } + _owner->GetModelRefOrThrow(_idx); + _owner->_models[_idx] = model->clone(); + return *this; + } + + private: + Simulation *_owner; + size_t _idx; + }; + //////////////////////////////////////////////////////////////////////////// // // Rule of Five: Copy and Move Semantics @@ -143,11 +191,11 @@ class Simulation { /// @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) { + /// @return A deep-copied model instance representing the newly created + /// model. + std::unique_ptr CreateNewModel(const std::string &model_name) { _models.push_back(Model::Create(model_name, _log_name)); - return std::to_string(_models.size()) + "_" + _models.back()->GetName(); + return _models.back()->clone(); } /// @brief Removes all models from the simulation. @@ -157,8 +205,6 @@ class Simulation { /// The model is cloned and managed by the simulation. /// @param model A unique_ptr to a Model instance to add. void AddModel(const std::unique_ptr &model) { - // because push_back is a move operation we're taking over ownership of - // the unique pointer _models.push_back(model->clone()); } @@ -183,33 +229,69 @@ class Simulation { //////////////////////////////////////////////////////////////////////////// /// @brief Retrieves all models in the simulation. - /// @return Const reference to the vector of Model unique_ptrs. - const std::vector> &GetModels() const { - return _models; + /// @return A deep-copied vector of Model unique_ptrs. + std::vector> GetModels() const { + std::vector> _models_copy; + for (const auto &model : _models) { + _models_copy.push_back(model->clone()); + } + return _models_copy; + } + + /// @brief Mutable index-based model access. + /// @details Returns a proxy that supports both model member access and + /// clone-based replacement assignment. + /// @param idx The index of the model to access. + /// @return A mutable proxy for the model slot. + /// @throws std::out_of_range if idx is out of range. + ModelSlotProxy operator[](size_t idx) { + GetModelRefOrThrow(idx); + return ModelSlotProxy(*this, idx); + } + + /// @brief Const index-based model access. + /// @param idx The index of the model to access. + /// @return Const reference to the model at the index. + /// @throws std::out_of_range if idx is out of range. + const Model &operator[](size_t idx) const { + return GetModelRefOrThrow(idx); + } + + /// @brief Provide a mapping of model indices to their names for all models + /// in the simulation. + /// @details This method returns a map where the keys are the indices of the + /// models in the simulation, and the values are the corresponding model + /// names. This allows for easy identification of models by their index in + /// the simulation. + /// @return The map of model indices to model names. + std::map GetModelIndexNameMap() const { + std::map ret; + for (size_t i = 0; i < _models.size(); ++i) { + ret[i] = _models[i]->GetName(); + } + return ret; } /// @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()) { + /// @return A deep-copied Model at the specified index. + /// @details If idx is -1, the last model is returned. + /// @throws std::out_of_range if no models exist or idx is out of range. + std::unique_ptr GetModel(int idx) const { + if (_models.empty()) { + LogError(_log_name, "No models available in GetModel."); + throw std::out_of_range("Error attempting to GetModel: no models."); + } + if (idx < -1 || idx >= static_cast(_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; - } + // Return the last model if idx is -1 + if (idx == -1) { + return _models.back()->clone(); } - LogError(_log_name, "Model name not found in GetModel: " + model_name); - throw std::invalid_argument("Error attempting to GetModel by name."); + return _models[idx]->clone(); } /// @brief Retrieves the names of all models in the simulation. @@ -225,8 +307,7 @@ class Simulation { /// @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. + /// @return Map of history names to history records for the selected model. const std::map &GetModelHistory(size_t idx) const { if (idx >= _models.size()) { LogError(_log_name, "Index out of range in GetModelHistory: " + @@ -237,21 +318,9 @@ class Simulation { return _models[idx]->GetHistories(); } - const std::map & - GetModelHistory(const std::string &model_name) const { - for (const auto &model : _models) { - if (model->GetName() == model_name) { - return model->GetHistories(); - } - } - 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. + /// @brief Retrieves history names for the model at the specified index. + /// @param idx The index of the model to retrieve history names for. + /// @return Vector of history names for the selected model. const std::vector GetModelHistoryNames(size_t idx) const { if (idx >= _models.size()) { LogError(_log_name, "Index out of range in GetModelHistoryNames: " + @@ -266,26 +335,29 @@ class Simulation { return ret; } - 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; - } + void SetDuration(int duration) { _duration = duration; } + +private: + Model &GetModelRefOrThrow(size_t idx) { + if (idx >= _models.size()) { + LogError(_log_name, "Index out of range in model access: " + + std::to_string(idx)); + throw std::out_of_range("Error attempting to access model by " + "index."); } - LogError(_log_name, - "Model name not found in GetModelHistoryNames: " + model_name); - throw std::invalid_argument( - "Error attempting to GetModelHistoryNames by name."); + return *_models[idx]; } - void SetDuration(int duration) { _duration = duration; } + const Model &GetModelRefOrThrow(size_t idx) const { + if (idx >= _models.size()) { + LogError(_log_name, "Index out of range in model access: " + + std::to_string(idx)); + throw std::out_of_range("Error attempting to access model by " + "index."); + } + return *_models[idx]; + } -private: std::string _log_name; std::vector> _models; diff --git a/include/respond/timestep.hpp b/include/respond/timestep.hpp index 590676ea..c8e3c177 100644 --- a/include/respond/timestep.hpp +++ b/include/respond/timestep.hpp @@ -4,7 +4,7 @@ // Created Date: 2026-06-30 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-16 // +// Last Modified: 2026-07-23 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -29,6 +29,61 @@ namespace respond { /// timestep also handles logging for its operations. class Timestep { public: + /// @brief Proxy for mutable model slot access with clone-based assignment. + class TransitionSlotProxy { + public: + TransitionSlotProxy(Timestep &owner, size_t idx) + : _owner(&owner), _idx(idx) {} + + /// @brief Access the underlying model pointer for member access. + Transition *operator->() { + return &_owner->GetTransitionRefOrThrow(_idx); + } + + /// @brief Dereference to the underlying model. + Transition &operator*() { + return _owner->GetTransitionRefOrThrow(_idx); + } + + /// @brief Implicit conversion to underlying mutable model reference. + operator Transition &() { + return _owner->GetTransitionRefOrThrow(_idx); + } + + /// @brief Replace this slot by cloning from another proxy's model. + TransitionSlotProxy &operator=(const TransitionSlotProxy &other) { + return *this = static_cast( + other._owner->GetTransitionRefOrThrow(other._idx)); + } + + /// @brief Replace this slot by cloning from a model reference. + TransitionSlotProxy &operator=(const Transition &transition) { + _owner->GetTransitionRefOrThrow(_idx); + _owner->_transitions[_idx] = transition.clone(); + return *this; + } + + /// @brief Replace this slot by cloning from a model unique_ptr. + /// @throws std::invalid_argument if model is nullptr. + TransitionSlotProxy & + operator=(const std::unique_ptr &transition) { + if (!transition) { + LogError(_owner->_log_name, + "Cannot assign null model pointer to simulation " + "slot."); + throw std::invalid_argument( + "Error attempting to assign null model pointer."); + } + _owner->GetTransitionRefOrThrow(_idx); + _owner->_transitions[_idx] = transition->clone(); + return *this; + } + + private: + Timestep *_owner; + size_t _idx; + }; + //////////////////////////////////////////////////////////////////////////// // // Rule of Five: Copy and Move Semantics @@ -126,6 +181,13 @@ class Timestep { return _transitions.back(); } + /// @brief Adds a transition to the timestep. + /// The transition is cloned and managed by the timestep. + /// @param transition A unique_ptr to a Transition instance to add. + void AddTransition(const std::unique_ptr &transition) { + _transitions.push_back(transition->clone()); + } + /// @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. @@ -247,6 +309,25 @@ class Timestep { // //////////////////////////////////////////////////////////////////////////// + /// @brief Mutable index-based transition access. + /// @details Returns a proxy that supports both transition member access and + /// clone-based replacement assignment. + /// @param idx The index of the transition to access. + /// @return A mutable proxy for the transition slot. + /// @throws std::out_of_range if idx is out of range. + TransitionSlotProxy operator[](size_t idx) { + GetTransitionRefOrThrow(idx); + return TransitionSlotProxy(*this, idx); + } + + /// @brief Const index-based transition access. + /// @param idx The index of the transition to access. + /// @return Const reference to the transition at the index. + /// @throws std::out_of_range if idx is out of range. + const Transition &operator[](size_t idx) const { + return GetTransitionRefOrThrow(idx); + } + /// @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 @@ -305,6 +386,26 @@ class Timestep { bool operator!=(const Timestep &other) const { return !(*this == other); } private: + Transition &GetTransitionRefOrThrow(size_t idx) { + if (idx >= _transitions.size()) { + LogError(_log_name, "Index out of range in transition access: " + + std::to_string(idx)); + throw std::out_of_range("Error attempting to access transition by " + "index."); + } + return *_transitions[idx]; + } + + const Transition &GetTransitionRefOrThrow(size_t idx) const { + if (idx >= _transitions.size()) { + LogError(_log_name, "Index out of range in transition access: " + + std::to_string(idx)); + throw std::out_of_range("Error attempting to access transition by " + "index."); + } + return *_transitions[idx]; + } + std::string _log_name; std::vector> _transitions; }; diff --git a/include/respond/version.hpp b/include/respond/version.hpp index 95cfc402..67ac8726 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-07-14 // +// Last Modified: 2026-07-22 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center // @@ -15,7 +15,7 @@ #define RESPOND_VER_MAJOR 2 #define RESPOND_VER_MINOR 5 -#define RESPOND_VER_PATCH 0 +#define RESPOND_VER_PATCH 1 #define RESPOND_TO_VERSION(major, minor, patch) \ (major * 10000 + minor * 100 + patch) diff --git a/tests/integration/respond_test.cpp b/tests/integration/respond_test.cpp index 5fd25941..eb832beb 100644 --- a/tests/integration/respond_test.cpp +++ b/tests/integration/respond_test.cpp @@ -55,8 +55,8 @@ class RespondTest : public ::testing::Test { tolerance << 1e-5, 1e-5, 1e-5; sim.CreateNewModel("markov"); - sim.GetModels()[0]->CreateDefaultHistories(); - sim.GetModels()[0]->SetState(init_state); + sim[0]->CreateDefaultHistories(); + sim[0]->SetState(init_state); } void TearDown() override { // Clean up loggers @@ -91,7 +91,7 @@ class RespondTest : public ::testing::Test { }; TEST_F(RespondTest, RunSingleTimestep) { - sim.GetModels()[0]->AddTimestep(CreateTestTimestep()); + sim[0]->AddTimestep(CreateTestTimestep()); sim.Run(); Eigen::VectorXd result = sim.GetModelHistory(0).at("state").GetStateAsVector().back(); @@ -102,8 +102,8 @@ TEST_F(RespondTest, RunSingleTimestep) { } TEST_F(RespondTest, RunSimulationTwoStep) { - sim.GetModels()[0]->AddTimestep(CreateTestTimestep()); - sim.GetModels()[0]->AddTimestep(CreateTestTimestep()); + sim[0]->AddTimestep(CreateTestTimestep()); + sim[0]->AddTimestep(CreateTestTimestep()); sim.Run(2); auto state_history = sim.GetModelHistory(0).at("state").GetStateAsVector(); @@ -119,11 +119,11 @@ TEST_F(RespondTest, RunSimulationTwoStep) { } 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[0]->AddTimestep(CreateTestTimestep()); + sim[0]->AddTimestep(CreateTestTimestep()); + sim[0]->AddTimestep(CreateTestTimestep()); + sim[0]->AddTimestep(CreateTestTimestep()); + sim[0]->AddTimestep(CreateTestTimestep()); sim.SetDuration(5); sim.Run(); @@ -140,11 +140,11 @@ TEST_F(RespondTest, RunSimulationFiveStep) { } 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[0]->AddTimestep(CreateTestTimestep()); + sim[0]->AddTimestep(CreateTestTimestep()); + sim[0]->AddTimestep(CreateTestTimestep()); + sim[0]->AddTimestep(CreateTestTimestep()); + sim[0]->AddTimestep(CreateTestTimestep()); sim.Run(5); auto state_history = sim.GetModelHistory(0).at("state").GetStateAsVector(); diff --git a/tests/unit/simulation_test.cpp b/tests/unit/simulation_test.cpp index 8e07b1c5..21b75bdb 100644 --- a/tests/unit/simulation_test.cpp +++ b/tests/unit/simulation_test.cpp @@ -97,8 +97,9 @@ TEST_F(SimulationTest, ConstructorWithLogNameAndLogFile) { TEST_F(SimulationTest, CreateNewModel) { Simulation s; std::string model_name = "test_model"; - std::string new_model_id = s.CreateNewModel(model_name); - ASSERT_EQ(new_model_id, "1_" + model_name); + auto new_model = s.CreateNewModel(model_name); + ASSERT_NE(new_model, nullptr); + ASSERT_EQ(new_model->GetName(), model_name); ASSERT_EQ(s.GetModels().size(), 1); } @@ -106,20 +107,24 @@ 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); + auto new_model1 = s.CreateNewModel(model_name1); + auto new_model2 = s.CreateNewModel(model_name2); + ASSERT_NE(new_model1, nullptr); + ASSERT_NE(new_model2, nullptr); + ASSERT_EQ(new_model1->GetName(), model_name1); + ASSERT_EQ(new_model2->GetName(), model_name2); ASSERT_EQ(s.GetModels().size(), 2); } TEST_F(SimulationTest, CreateModelWithExistingName) { Simulation s; std::string model_name = "test_model"; - std::string new_model_id1 = s.CreateNewModel(model_name); - std::string new_model_id2 = s.CreateNewModel(model_name); - ASSERT_EQ(new_model_id1, "1_" + model_name); - ASSERT_EQ(new_model_id2, "2_" + model_name); + auto new_model1 = s.CreateNewModel(model_name); + auto new_model2 = s.CreateNewModel(model_name); + ASSERT_NE(new_model1, nullptr); + ASSERT_NE(new_model2, nullptr); + ASSERT_EQ(new_model1->GetName(), model_name); + ASSERT_EQ(new_model2->GetName(), model_name); ASSERT_EQ(s.GetModels().size(), 2); } @@ -185,6 +190,146 @@ TEST_F(SimulationTest, GetModels) { ASSERT_EQ(models.size(), 1); } +TEST_F(SimulationTest, IndexOperatorMutableAccessCallsModelMethods) { + Simulation s; + + auto source_model_mock = std::make_unique>(); + auto *source_model_ptr = source_model_mock.get(); + std::unique_ptr source_model = std::move(source_model_mock); + auto stored_model = std::make_unique>(); + auto *stored_model_ptr = stored_model.get(); + EXPECT_CALL(*stored_model_ptr, SetFinalTimestep(52)).Times(1); + EXPECT_CALL(*source_model_ptr, clone()) + .WillOnce(Return(::testing::ByMove( + std::unique_ptr(std::move(stored_model))))); + + s.AddModel(source_model); + s[0]->SetFinalTimestep(52); +} + +TEST_F(SimulationTest, IndexOperatorConstAccessCallsModelMethods) { + Simulation s; + + auto source_model_mock = std::make_unique>(); + auto *source_model_ptr = source_model_mock.get(); + std::unique_ptr source_model = std::move(source_model_mock); + auto stored_model = std::make_unique>(); + auto *stored_model_ptr = stored_model.get(); + EXPECT_CALL(*stored_model_ptr, GetName()).WillOnce(Return("model_name")); + EXPECT_CALL(*source_model_ptr, clone()) + .WillOnce(Return(::testing::ByMove( + std::unique_ptr(std::move(stored_model))))); + + s.AddModel(source_model); + const Simulation &const_sim = s; + ASSERT_EQ(const_sim[0].GetName(), "model_name"); +} + +TEST_F(SimulationTest, IndexOperatorThrowsOutOfRangeForInvalidIndex) { + Simulation s; + EXPECT_THROW((void)s[0], std::out_of_range); +} + +TEST_F(SimulationTest, IndexOperatorAssignmentFromModelClonesSourceModel) { + Simulation s; + + auto initial_source_mock = std::make_unique>(); + auto *initial_source_ptr = initial_source_mock.get(); + std::unique_ptr initial_source = std::move(initial_source_mock); + auto initial_stored = std::make_unique>(); + EXPECT_CALL(*initial_source_ptr, clone()) + .WillOnce(Return(::testing::ByMove( + std::unique_ptr(std::move(initial_stored))))); + s.AddModel(initial_source); + + auto replacement_source = std::make_unique>(); + auto replacement_clone = std::make_unique>(); + auto *replacement_clone_ptr = replacement_clone.get(); + EXPECT_CALL(*replacement_clone_ptr, SetFinalTimestep(11)).Times(1); + EXPECT_CALL(*replacement_source, clone()) + .WillOnce(Return(::testing::ByMove(std::move(replacement_clone)))); + + s[0] = *replacement_source; + s[0]->SetFinalTimestep(11); +} + +TEST_F(SimulationTest, + IndexOperatorAssignmentFromUniquePtrClonesAndKeepsCallerOwnership) { + Simulation s; + + auto initial_source_mock = std::make_unique>(); + auto *initial_source_ptr = initial_source_mock.get(); + std::unique_ptr initial_source = std::move(initial_source_mock); + auto initial_stored = std::make_unique>(); + EXPECT_CALL(*initial_source_ptr, clone()) + .WillOnce(Return(::testing::ByMove( + std::unique_ptr(std::move(initial_stored))))); + s.AddModel(initial_source); + + auto replacement_source_mock = std::make_unique>(); + auto *replacement_source_ptr = replacement_source_mock.get(); + std::unique_ptr replacement_source = + std::move(replacement_source_mock); + auto replacement_clone = std::make_unique>(); + auto *replacement_clone_ptr = replacement_clone.get(); + EXPECT_CALL(*replacement_clone_ptr, SetFinalTimestep(17)).Times(1); + EXPECT_CALL(*replacement_source_ptr, clone()) + .WillOnce(Return(::testing::ByMove( + std::unique_ptr(std::move(replacement_clone))))); + + const std::unique_ptr &replacement_ref = replacement_source; + s[0] = replacement_ref; + ASSERT_NE(replacement_source, nullptr); + s[0]->SetFinalTimestep(17); +} + +TEST_F(SimulationTest, IndexOperatorAssignmentFromNullUniquePtrThrows) { + Simulation s; + + auto initial_source_mock = std::make_unique>(); + auto *initial_source_ptr = initial_source_mock.get(); + std::unique_ptr initial_source = std::move(initial_source_mock); + auto initial_stored = std::make_unique>(); + EXPECT_CALL(*initial_source_ptr, clone()) + .WillOnce(Return(::testing::ByMove( + std::unique_ptr(std::move(initial_stored))))); + s.AddModel(initial_source); + + std::unique_ptr null_model; + EXPECT_THROW(s[0] = null_model, std::invalid_argument); +} + +TEST_F(SimulationTest, IndexOperatorAssignmentFromProxyClonesSourceSlot) { + Simulation s; + + auto target_source_mock = std::make_unique>(); + auto *target_source_ptr = target_source_mock.get(); + std::unique_ptr target_source = std::move(target_source_mock); + auto target_stored = std::make_unique>(); + EXPECT_CALL(*target_source_ptr, clone()) + .WillOnce(Return(::testing::ByMove( + std::unique_ptr(std::move(target_stored))))); + s.AddModel(target_source); + + auto source_source_mock = std::make_unique>(); + auto *source_source_ptr = source_source_mock.get(); + std::unique_ptr source_source = std::move(source_source_mock); + auto source_stored = std::make_unique>(); + auto replacement_clone = std::make_unique>(); + auto *replacement_clone_ptr = replacement_clone.get(); + EXPECT_CALL(*replacement_clone_ptr, SetFinalTimestep(23)).Times(1); + EXPECT_CALL(*source_stored, clone()) + .WillOnce(Return(::testing::ByMove( + std::unique_ptr(std::move(replacement_clone))))); + EXPECT_CALL(*source_source_ptr, clone()) + .WillOnce(Return(::testing::ByMove( + std::unique_ptr(std::move(source_stored))))); + s.AddModel(source_source); + + s[0] = s[1]; + s[0]->SetFinalTimestep(23); +} + TEST_F(SimulationTest, GetModelNames) { Simulation s; std::string model_name1 = "test_model1"; diff --git a/tests/unit/timestep_test.cpp b/tests/unit/timestep_test.cpp index 55a128bc..b230e920 100644 --- a/tests/unit/timestep_test.cpp +++ b/tests/unit/timestep_test.cpp @@ -76,6 +76,30 @@ TEST_F(TimestepTest, CreateTransition) { ASSERT_EQ(transition->GetName(), "migration"); } +TEST_F(TimestepTest, AddTransitionClonesInputTransition) { + Timestep ts("test_log", test_log_file_); + + auto transition = Transition::Create("migration", "migration", "test_log", + test_log_file_); + + Eigen::MatrixXd m1(2, 2); + m1 << 0.5, 0.5, 0.5, 0.5; + transition->AddMatrix(m1); + + ts.AddTransition(transition); + + // Mutating the caller-owned transition should not affect timestep-owned + // clone. + Eigen::MatrixXd m2(2, 2); + m2 << 0.1, 0.9, 0.2, 0.8; + transition->AddMatrix(m2); + + const std::unique_ptr &stored = ts.GetTransition(0); + ASSERT_EQ(stored->GetName(), "migration"); + ASSERT_EQ(stored->GetMatrices().size(), 1); + ASSERT_TRUE(stored->GetMatrices()[0].isApprox(m1)); +} + TEST_F(TimestepTest, AddMatrixToTransitionByIndex) { Timestep ts("test_log", test_log_file_); const std::unique_ptr &transition = @@ -166,6 +190,42 @@ TEST_F(TimestepTest, CopyAssignment) { ASSERT_EQ(names[0], "migration"); } +TEST_F(TimestepTest, MutableOperatorIndexProvidesTransitionAccess) { + Timestep ts("test_log", test_log_file_); + ts.CreateTransition("migration"); + + Eigen::MatrixXd m(2, 2); + m << 0.4, 0.6, 0.1, 0.9; + + ts[0]->AddMatrix(m); + + ASSERT_EQ(ts.GetTransition(0)->GetMatrices().size(), 1); + ASSERT_TRUE(ts.GetTransition(0)->GetMatrices()[0].isApprox(m)); +} + +TEST_F(TimestepTest, MutableOperatorIndexSupportsSlotReplacementAssignment) { + Timestep ts("test_log", test_log_file_); + ts.CreateTransition("migration"); + ts.CreateTransition("behavior"); + + ts[0] = ts[1]; + ASSERT_EQ(ts.GetTransition(0)->GetName(), "behavior"); + + auto overdose = + Transition::Create("overdose", "overdose", "test_log", test_log_file_); + ts[1] = overdose; + ASSERT_EQ(ts.GetTransition(1)->GetName(), "overdose"); +} + +TEST_F(TimestepTest, ConstOperatorIndexReturnsConstTransitionReference) { + Timestep ts("test_log", test_log_file_); + ts.CreateTransition("migration"); + + const Timestep &const_ts = ts; + const Transition &transition = const_ts[0]; + ASSERT_EQ(transition.GetName(), "migration"); +} + TEST_F(TimestepTest, StreamOperatorOverload) { Timestep ts("test_log", test_log_file_); ts.CreateTransition("migration");