RESPOND 2.5.1
Researching Effective Strategies to Prevent Opioid Death
Loading...
Searching...
No Matches
simulation.hpp
1
2// File: simulation.hpp //
3// Project: respond //
4// Created Date: 2026-02-05 //
5// Author: Matthew Carroll //
6// ----- //
7// Last Modified: 2026-10-06 //
8// Modified By: Matthew Carroll //
9// ----- //
10// Copyright (c) 2026 Syndemics Lab at Boston Medical Center //
12#ifndef RESPOND_SIMULATION_HPP_
13#define RESPOND_SIMULATION_HPP_
14
15#include <respond/constants.hpp>
16#include <respond/eigen_config.hpp>
17#include <respond/history.hpp>
18#include <respond/logging.hpp>
19#include <respond/model.hpp>
20#include <respond/runtime_config.hpp>
21
22#include <atomic>
23#include <exception>
24#include <map>
25#include <memory>
26#include <mutex>
27#include <stdexcept>
28#include <string>
29#include <thread>
30#include <utility>
31#include <vector>
32
33#include <Eigen/Dense>
34
35namespace respond {
40public:
43 public:
44 ModelSlotProxy(Simulation &owner, size_t idx)
45 : _owner(&owner), _idx(idx) {}
46
48 Model *operator->() { return &_owner->GetModelRefOrThrow(_idx); }
49
51 Model &operator*() { return _owner->GetModelRefOrThrow(_idx); }
52
54 operator Model &() { return _owner->GetModelRefOrThrow(_idx); }
55
58 return *this = static_cast<const Model &>(
59 other._owner->GetModelRefOrThrow(other._idx));
60 }
61
64 _owner->GetModelRefOrThrow(_idx);
65 _owner->_models[_idx] = model.clone();
66 return *this;
67 }
68
71 ModelSlotProxy &operator=(const std::unique_ptr<Model> &model) {
72 if (!model) {
73 LogError(_owner->_runtime_config.logging.logger_name,
74 "Cannot assign null model pointer to simulation "
75 "slot.");
76 throw std::invalid_argument(
77 "Error attempting to assign null model pointer.");
78 }
79 _owner->GetModelRefOrThrow(_idx);
80 _owner->_models[_idx] = model->clone();
81 return *this;
82 }
83
84 private:
85 Simulation *_owner;
86 size_t _idx;
87 };
88
90 //
91 // Rule of Five: Copy and Move Semantics
92 //
94
98
101 [[deprecated("Use Simulation(RuntimeConfig) instead")]]
102 Simulation(const std::string &log_name)
105 LoggingConfig{log_name, RESPOND_DEFAULT_LOG_FILE, false}}) {}
106
110 [[deprecated("Use Simulation(RuntimeConfig) instead")]]
111 Simulation(const std::string &log_name, const std::string &log_filepath)
112 : Simulation(
114 LoggingConfig{log_name, log_filepath, false}}) {}
115
120 [[deprecated("Use Simulation(RuntimeConfig) instead")]]
121 Simulation(const std::string &log_name, const std::string &log_filepath,
122 const ExecutionConfig &execution_config)
124 execution_config, LoggingConfig{log_name, log_filepath, false}}) {
125 }
126
128 explicit Simulation(const RuntimeConfig &runtime_config)
129 : _runtime_config(runtime_config) {
130 if (ConfigureLogger(_runtime_config.logging) ==
131 CreationStatus::kError) {
132 throw std::runtime_error(
133 "Error attempting to initialize simulation logger.");
134 }
135 }
136
138 ~Simulation() = default;
139
144 Simulation(const Simulation &other) {
145 _runtime_config = other._runtime_config;
146 for (const auto &m : other._models) {
147 _models.push_back(m->clone());
148 }
149 _duration = other._duration;
150 _parameter_change_times = other._parameter_change_times;
151 _stratify_entering_cohort = other._stratify_entering_cohort;
152 _build_summary_stats = other._build_summary_stats;
153 _save_state_history = other._save_state_history;
154 _timesteps_to_report = other._timesteps_to_report;
155 _pivot_long = other._pivot_long;
156 }
157
162 if (this != &other) {
163 std::vector<std::unique_ptr<Model>> models;
164 for (const auto &m : other._models) {
165 models.push_back(m->clone());
166 }
167 _runtime_config = other._runtime_config;
168 _models = std::move(models);
169 _duration = other._duration;
170 _parameter_change_times = other._parameter_change_times;
171 _stratify_entering_cohort = other._stratify_entering_cohort;
172 _build_summary_stats = other._build_summary_stats;
173 _save_state_history = other._save_state_history;
174 _timesteps_to_report = other._timesteps_to_report;
175 _pivot_long = other._pivot_long;
176 }
177 return *this;
178 }
179
182 Simulation(Simulation &&other) noexcept
183 : _runtime_config(std::move(other._runtime_config)),
184 _models(std::move(other._models)), _duration(other._duration),
185 _parameter_change_times(std::move(other._parameter_change_times)),
186 _stratify_entering_cohort(other._stratify_entering_cohort),
187 _build_summary_stats(other._build_summary_stats),
188 _save_state_history(other._save_state_history),
189 _timesteps_to_report(std::move(other._timesteps_to_report)),
190 _pivot_long(other._pivot_long) {}
191
195 Simulation &operator=(Simulation &&other) noexcept {
196 if (this != &other) {
197 _runtime_config = std::move(other._runtime_config);
198 _models = std::move(other._models);
199 _duration = other._duration;
200 _parameter_change_times = std::move(other._parameter_change_times);
201 _stratify_entering_cohort = other._stratify_entering_cohort;
202 _build_summary_stats = other._build_summary_stats;
203 _save_state_history = other._save_state_history;
204 _timesteps_to_report = std::move(other._timesteps_to_report);
205 _pivot_long = other._pivot_long;
206 }
207 return *this;
208 }
209
211 //
212 // Simulation Behavior Methods: Model Management
213 //
215
223 std::unique_ptr<Model> CreateNewModel(const std::string &model_name) {
224 _models.push_back(Model::Create(model_name, _runtime_config));
225 return _models.back()->clone();
226 }
227
229 void ClearModels() { _models.clear(); }
230
234 void AddModel(const std::unique_ptr<Model> &model) {
235 if (!model) {
236 LogError(_runtime_config.logging.logger_name,
237 "Cannot add a null model to the simulation.");
238 throw std::invalid_argument(
239 "Error attempting to add a null model to simulation.");
240 }
241 _models.push_back(model->clone());
242 }
243
253 void Run(int duration = -1) {
254 if (duration == 0 || duration < -1) {
255 throw std::invalid_argument(
256 "Simulation duration must be positive or -1.");
257 }
258 if (_models.empty()) {
259 LogError(_runtime_config.logging.logger_name,
260 "Cannot run a simulation with no models.");
261 throw std::invalid_argument(
262 "Error attempting to run simulation with no models.");
263 }
264 std::lock_guard<std::mutex> execution_lock(
265 detail::GetEigenExecutionMutex());
266 if (duration > 0) {
267 _duration = duration;
268 }
269 const auto &execution = _runtime_config.execution;
270 const unsigned int thread_limit = std::thread::hardware_concurrency();
271 const unsigned int eigen_threads =
272 thread_limit == 0 ? execution.eigen_threads
273 : std::min(execution.eigen_threads, thread_limit);
274 Eigen::setNbThreads(eigen_threads);
275
276 LogInfo(_runtime_config.logging.logger_name,
277 "Running simulation for duration of " +
278 std::to_string(_duration) + " timesteps.");
279 const auto run_model = [this](const std::unique_ptr<Model> &model) {
280 model->SetFinalTimestep(_duration);
281 model->RunTimesteps();
282 };
283 if (!execution.run_models_concurrently || _models.size() < 2) {
284 for (const auto &model : _models) {
285 run_model(model);
286 }
287 return;
288 }
289
290 if (execution.eigen_threads > 1) {
291 throw std::invalid_argument(
292 "Concurrent model execution requires eigen_threads == 1.");
293 }
294
295 unsigned int worker_limit = execution.total_threads;
296 if (worker_limit == 0) {
297 worker_limit = std::thread::hardware_concurrency();
298 if (worker_limit == 0) {
299 worker_limit = 1;
300 }
301 }
302 const auto worker_count =
303 std::min<size_t>(worker_limit, _models.size());
304 if (worker_count <= 1) {
305 for (const auto &model : _models) {
306 run_model(model);
307 }
308 return;
309 }
310
311 std::atomic<size_t> next_model{0};
312 std::exception_ptr first_exception;
313 std::mutex exception_mutex;
314 std::vector<std::thread> workers;
315 workers.reserve(worker_count);
316
317 for (size_t worker = 0; worker < worker_count; ++worker) {
318 workers.emplace_back([&]() {
319 while (true) {
320 const size_t index = next_model.fetch_add(1);
321 if (index >= _models.size()) {
322 return;
323 }
324 try {
325 run_model(_models[index]);
326 } catch (...) {
327 std::lock_guard<std::mutex> lock(exception_mutex);
328 if (!first_exception) {
329 first_exception = std::current_exception();
330 }
331 return;
332 }
333 }
334 });
335 }
336
337 for (auto &worker : workers) {
338 worker.join();
339 }
340 if (first_exception) {
341 std::rethrow_exception(first_exception);
342 }
343 }
344
346 //
347 // Getters and Setters for Transitions and Metadata
348 //
350
353 std::vector<std::unique_ptr<Model>> GetModels() const {
354 std::vector<std::unique_ptr<Model>> _models_copy;
355 for (const auto &model : _models) {
356 _models_copy.push_back(model->clone());
357 }
358 return _models_copy;
359 }
360
368 GetModelRefOrThrow(idx);
369 return ModelSlotProxy(*this, idx);
370 }
371
376 const Model &operator[](size_t idx) const {
377 return GetModelRefOrThrow(idx);
378 }
379
387 std::map<size_t, std::string> GetModelIndexNameMap() const {
388 std::map<size_t, std::string> ret;
389 for (size_t i = 0; i < _models.size(); ++i) {
390 ret[i] = _models[i]->GetName();
391 }
392 return ret;
393 }
394
400 std::unique_ptr<Model> GetModel(int idx) const {
401 if (_models.empty()) {
402 LogError(_runtime_config.logging.logger_name,
403 "No models available in GetModel.");
404 throw std::out_of_range("Error attempting to GetModel: no models.");
405 }
406 if (idx < -1 || idx >= static_cast<int>(_models.size())) {
407 LogError(_runtime_config.logging.logger_name,
408 "Index out of range in GetModel: " + std::to_string(idx));
409 throw std::out_of_range("Error attempting to GetModel by index.");
410 }
411 // Return the last model if idx is -1
412 if (idx == -1) {
413 return _models.back()->clone();
414 }
415 return _models[idx]->clone();
416 }
417
420 std::vector<std::string> GetModelNames() const {
421 std::vector<std::string> ret;
422 for (auto &m : _models) {
423 ret.push_back(m->GetName());
424 }
425 return ret;
426 }
427
432 const std::map<std::string, History> &GetModelHistory(size_t idx) const {
433 if (idx >= _models.size()) {
434 LogError(_runtime_config.logging.logger_name,
435 "Index out of range in GetModelHistory: " +
436 std::to_string(idx));
437 throw std::out_of_range(
438 "Error attempting to GetModelHistory by index.");
439 }
440 return _models[idx]->GetHistories();
441 }
442
446 const std::vector<std::string> GetModelHistoryNames(size_t idx) const {
447 if (idx >= _models.size()) {
448 LogError(_runtime_config.logging.logger_name,
449 "Index out of range in GetModelHistoryNames: " +
450 std::to_string(idx));
451 throw std::out_of_range(
452 "Error attempting to GetModelHistoryNames by index.");
453 }
454 std::vector<std::string> ret;
455 for (const auto &kv : _models[idx]->GetHistories()) {
456 ret.push_back(kv.first);
457 }
458 return ret;
459 }
460
464 void SetDuration(int duration) {
465 if (duration <= 0) {
466 throw std::invalid_argument(
467 "Simulation duration must be positive.");
468 }
469 _duration = duration;
470 }
471
475 return _runtime_config.execution;
476 }
477
480 void SetExecutionConfig(const ExecutionConfig &execution_config) {
481 _runtime_config.execution = execution_config;
482 }
483
484 const RuntimeConfig &GetRuntimeConfig() const { return _runtime_config; }
485
486 void SetRuntimeConfig(const RuntimeConfig &runtime_config) {
487 if (ConfigureLogger(runtime_config.logging) == CreationStatus::kError) {
488 LogError(_runtime_config.logging.logger_name,
489 "Unable to apply simulation runtime logging config.");
490 throw std::invalid_argument(
491 "Error attempting to apply simulation runtime logging "
492 "configuration.");
493 }
494 _runtime_config = runtime_config;
495 }
496
497private:
498 Model &GetModelRefOrThrow(size_t idx) {
499 if (idx >= _models.size()) {
500 LogError(_runtime_config.logging.logger_name,
501 "Index out of range in model access: " +
502 std::to_string(idx));
503 throw std::out_of_range("Error attempting to access model by "
504 "index.");
505 }
506 return *_models[idx];
507 }
508
509 const Model &GetModelRefOrThrow(size_t idx) const {
510 if (idx >= _models.size()) {
511 LogError(_runtime_config.logging.logger_name,
512 "Index out of range in model access: " +
513 std::to_string(idx));
514 throw std::out_of_range("Error attempting to access model by "
515 "index.");
516 }
517 return *_models[idx];
518 }
519
520 RuntimeConfig _runtime_config;
521 std::vector<std::unique_ptr<Model>> _models;
522
523 int _duration = 1; // Default simulation duration in timesteps
524 std::vector<int> _parameter_change_times;
525 bool _stratify_entering_cohort = false;
526
527 bool _build_summary_stats = false;
528 bool _save_state_history = false;
529 std::vector<int> _timesteps_to_report;
530 bool _pivot_long = false;
531};
532} // namespace respond
533
534#endif // RESPOND_SIMULATION_HPP_
Abstract base class representing a state transition model. Models manage a state vector,...
Definition: model.hpp:31
virtual std::unique_ptr< Model > clone() const =0
Creates a deep copy of this model.
static std::unique_ptr< Model > Create(const std::string &name, const std::string &log_name=RESPOND_DEFAULT_LOG, const std::string &log_filepath=RESPOND_DEFAULT_LOG_FILE)
Factory method to create a Model instance.
Proxy for mutable model slot access with clone-based assignment.
Definition: simulation.hpp:42
Model * operator->()
Access the underlying model pointer for member access.
Definition: simulation.hpp:48
Model & operator*()
Dereference to the underlying model.
Definition: simulation.hpp:51
ModelSlotProxy & operator=(const ModelSlotProxy &other)
Replace this slot by cloning from another proxy's model.
Definition: simulation.hpp:57
ModelSlotProxy & operator=(const Model &model)
Replace this slot by cloning from a model reference.
Definition: simulation.hpp:63
ModelSlotProxy & operator=(const std::unique_ptr< Model > &model)
Replace this slot by cloning from a model unique_ptr.
Definition: simulation.hpp:71
Manages and executes multiple models in a coordinated simulation. A Simulation aggregates Model insta...
Definition: simulation.hpp:39
std::vector< std::string > GetModelNames() const
Retrieves the names of all models in the simulation.
Definition: simulation.hpp:420
void SetExecutionConfig(const ExecutionConfig &execution_config)
Sets the simulation execution settings.
Definition: simulation.hpp:480
Simulation(Simulation &&other) noexcept
Move constructor for transferring simulation ownership.
Definition: simulation.hpp:182
std::vector< std::unique_ptr< Model > > GetModels() const
Retrieves all models in the simulation.
Definition: simulation.hpp:353
~Simulation()=default
Virtual destructor for polymorphic cleanup.
std::map< size_t, std::string > GetModelIndexNameMap() const
Provide a mapping of model indices to their names for all models in the simulation.
Definition: simulation.hpp:387
void Run(int duration=-1)
Executes the simulation for all registered models. Models run sequentially by default....
Definition: simulation.hpp:253
Simulation(const std::string &log_name)
Constructs a Simulation with a specified logger.
Definition: simulation.hpp:102
void ClearModels()
Removes all models from the simulation.
Definition: simulation.hpp:229
Simulation(const RuntimeConfig &runtime_config)
Constructs a Simulation with shared runtime settings.
Definition: simulation.hpp:128
const std::map< std::string, History > & GetModelHistory(size_t idx) const
Retrieves the complete state histories for the model at the index.
Definition: simulation.hpp:432
Simulation(const Simulation &other)
Copy constructor creating an independent deep copy of the simulation. All models are cloned; modifica...
Definition: simulation.hpp:144
void SetDuration(int duration)
Sets the default duration used by Run().
Definition: simulation.hpp:464
Simulation(const std::string &log_name, const std::string &log_filepath, const ExecutionConfig &execution_config)
Constructs a Simulation with logger and execution settings.
Definition: simulation.hpp:121
std::unique_ptr< Model > GetModel(int idx) const
Retrieves a specific model by index in the simulation.
Definition: simulation.hpp:400
const ExecutionConfig & GetExecutionConfig() const
Retrieves the simulation execution settings.
Definition: simulation.hpp:474
std::unique_ptr< Model > CreateNewModel(const std::string &model_name)
Creates a new model instance and adds it to the simulation.
Definition: simulation.hpp:223
Simulation()
Default constructor for a Simulation instance. Initializes the simulation with the default logger.
Definition: simulation.hpp:97
Simulation & operator=(const Simulation &other)
Copy assignment operator for deep copying simulation state.
Definition: simulation.hpp:161
const Model & operator[](size_t idx) const
Const index-based model access.
Definition: simulation.hpp:376
Simulation(const std::string &log_name, const std::string &log_filepath)
Constructs a Simulation with a specified logger and log file.
Definition: simulation.hpp:111
Simulation & operator=(Simulation &&other) noexcept
Move assignment operator for transferring simulation ownership.
Definition: simulation.hpp:195
void AddModel(const std::unique_ptr< Model > &model)
Adds a model to the simulation. The model is cloned and managed by the simulation.
Definition: simulation.hpp:234
const std::vector< std::string > GetModelHistoryNames(size_t idx) const
Retrieves history names for the model at the specified index.
Definition: simulation.hpp:446
ModelSlotProxy operator[](size_t idx)
Mutable index-based model access.
Definition: simulation.hpp:367
Controls how a Simulation distributes execution resources.
Definition: execution_config.hpp:14
Describes the logging destination used by a library object.
Definition: logging_config.hpp:21
Groups execution and logging settings shared by runtime objects.
Definition: runtime_config.hpp:17