From 213ddd9d498b40142c0ed01cd8346fadba4b3b2d Mon Sep 17 00:00:00 2001 From: Dimitri Baptiste <55843498+ddbaptiste@users.noreply.github.com> Date: Wed, 16 Sep 2026 12:39:00 -0400 Subject: [PATCH 1/4] Use the overloaded `Model::Create` method in bindings --- CMakeLists.txt | 2 +- src/register_model.cpp | 57 ++++++++++++++++++++++++++---------------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f76f82d..c76a7fc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,7 +62,7 @@ set(SPDLOG_INSTALL ON) FetchContent_Declare( respond GIT_REPOSITORY https://github.com/SyndemicsLab/respond.git - GIT_TAG 2bc260ef749a76b7eba73a4d64e9d38c48ed2bdc # v2.5.1 + GIT_TAG d540a9948adbfcee1c207cb16e7eae0fed93d5f5 # feature/configurable-thread-count OVERRIDE_FIND_PACKAGE ) set(RESPOND_BUILD_DOCS OFF) diff --git a/src/register_model.cpp b/src/register_model.cpp index 7490dae..41a1a33 100644 --- a/src/register_model.cpp +++ b/src/register_model.cpp @@ -4,8 +4,8 @@ // Created Date: 2026-01-08 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-20 // -// Modified By: Matthew Carroll // +// Last Modified: 2026-09-16 // +// Modified By: Dimitri Baptiste // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // //////////////////////////////////////////////////////////////////////////////// @@ -21,12 +21,25 @@ using namespace respond; // NOLINTNEXTLINE(misc-use-internal-linkage) void register_model(py::module &m) { py::class_(m, "Model") - .def(py::init(&Model::Create), py::arg("name"), + .def( + py::init(py::overload_cast(&Model::Create)), + py::arg("name"), py::arg("log_name") = "respond", + py::arg("log_filepath") = "respond.log", + "Factory method to create a Model instance. Initializes logging " + "for the model and returns a unique_ptr to the created instance. " + "Throws an exception if the model name is unsupported.") + .def(py::init( + py::overload_cast( + &Model::Create)), + py::arg("name"), py::arg("processor_count"), py::arg("log_name") = "respond", py::arg("log_filepath") = "respond.log", - "Factory method to create a Model instance. Initializes logging " - "for the model and returns a unique_ptr to the created instance. " - "Throws an exception if the model name is unsupported.") + "Factory method to create a Model instance when the number of " + "processors must be specified. Initializes logging for the model " + "and returns a unique_ptr to the created instance. Throws an " + "exception if the model name is unsupported.") .def("__copy__", [](const Model &self) { return self.clone(); }) .def( "__deepcopy__", @@ -54,14 +67,14 @@ void register_model(py::module &m) { .def("get_timestep_at_index", &Model::GetTimestepAtIndex, py::arg("idx"), "Get the timestep at the specified index in the model's sequence.") - .def( - "get_state", - [](const Model &self) { - // Return a concrete vector copy to avoid exposing Eigen::Ref - // lifetimes across the Python boundary. - return Eigen::VectorXd(self.GetState()); - }, - "Get the current state vector of the model.") + .def( + "get_state", + [](const Model &self) { + // Return a concrete vector copy to avoid exposing Eigen::Ref + // lifetimes across the Python boundary. + return Eigen::VectorXd(self.GetState()); + }, + "Get the current state vector of the model.") .def("get_name", &Model::GetName, "Get the name of the model.") .def("get_histories", &Model::GetHistories, "Get the list of histories associated with the model.") @@ -74,14 +87,14 @@ void register_model(py::module &m) { "Get the configured final simulation timestep, or -1 if unset.") .def("get_initial_history_recorded", &Model::GetInitialHistoryRecorded, "Check if the initial history has been recorded.") - .def( - "set_state", - [](Model &self, const Eigen::VectorXd &state) { - // Copy into a concrete Eigen vector first, then pass by Ref. - // This avoids temporary-map lifetime issues on some platforms. - self.SetState(state); - }, - py::arg("state"), "Set the current state vector of the model.") + .def( + "set_state", + [](Model &self, const Eigen::VectorXd &state) { + // Copy into a concrete Eigen vector first, then pass by Ref. + // This avoids temporary-map lifetime issues on some platforms. + self.SetState(state); + }, + py::arg("state"), "Set the current state vector of the model.") .def("set_history_capture_interval", &Model::SetHistoryCaptureInterval, py::arg("interval"), "Set the global history capture interval. Records every " From 6b6f2db9012f9f6eb2a4568e56e6f4596ea643a4 Mon Sep 17 00:00:00 2001 From: Dimitri Baptiste <55843498+ddbaptiste@users.noreply.github.com> Date: Wed, 16 Sep 2026 14:48:42 -0400 Subject: [PATCH 2/4] Add the processor count argument to `build` functions --- src/respondpy/build.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/respondpy/build.py b/src/respondpy/build.py index 01cf29c..78d6420 100644 --- a/src/respondpy/build.py +++ b/src/respondpy/build.py @@ -4,8 +4,8 @@ # Created Date: 2026-07-23 # # Author: Matthew Carroll # # ----- # -# Last Modified: 2026-08-04 # -# Modified By: Matthew Carroll # +# Last Modified: 2026-09-16 # +# Modified By: Dimitri Baptiste # # ----- # # Copyright (c) 2026 Syndemics Lab at Boston Medical Center # ################################################################################ @@ -24,6 +24,7 @@ def build_simulation( input_data: Input, *, + processor_count: int | None = None, cohort_ids: Sequence[int] | None = None, log_name: str = "respond", log_file: str = "respond.log" @@ -34,6 +35,9 @@ def build_simulation( ---------- input_data : Input Loaded input data and simulation configuration. + processor_count: int, optional + The number of processors (threads) to use when a model runs. When + omitted, the maximum number of available processors is used. cohort_ids : Sequence of int, optional Cohort identifiers to include in the simulation. When omitted, all cohort identifiers present in ``input_data`` are used. @@ -65,7 +69,10 @@ def build_simulation( duration = int(input_data.config.get("simulation", "duration")) s.set_duration(duration) for cohort_id in cohort_ids: - s.add_model(build_model(input_data, cohort_id)) + if processor_count: + s.add_model(build_model(input_data, cohort_id, processor_count)) + else: + s.add_model(build_model(input_data, cohort_id)) return s @@ -74,6 +81,7 @@ def build_model( input_data: Input, cohort_id: int, *, + processor_count: int | None = None, log_name: str = "respond", log_file: str = "respond.log" ) -> Model: @@ -86,6 +94,9 @@ def build_model( cohort_id : int Cohort identifier used to select the initial state and parameter values. + processor_count : int, optional + The number of processors (threads) to use when a model runs. When + omitted, the maximum number of available processors is used. log_name : str, default="respond" Logger name used by the underlying model. log_file : str, default="respond.log" @@ -97,7 +108,13 @@ def build_model( A model configured with the cohort initial state and timestep transitions. """ - model = Model("markov", log_name, log_file) + # if the number of threads to use is specified, use the correct model + # constructor + if processor_count: + model = Model("markov", processor_count, log_name, log_file) + else: + model = Model("markov", log_name, log_file) + initial_state = input_data.select_parameter( Parameter(ParameterType.INITIAL_COHORT), cohort_id, From 7709115be13f1373e9b4ec2003ce4ab983853336 Mon Sep 17 00:00:00 2001 From: Dimitri Baptiste <55843498+ddbaptiste@users.noreply.github.com> Date: Wed, 16 Sep 2026 15:31:08 -0400 Subject: [PATCH 3/4] Add kwarg for `build_model` when `processor_count` is specified --- src/respondpy/build.py | 108 ++++++++++++++++++++++++++++------------- 1 file changed, 73 insertions(+), 35 deletions(-) diff --git a/src/respondpy/build.py b/src/respondpy/build.py index 78d6420..ea1bde6 100644 --- a/src/respondpy/build.py +++ b/src/respondpy/build.py @@ -22,12 +22,12 @@ def build_simulation( - input_data: Input, - *, - processor_count: int | None = None, - cohort_ids: Sequence[int] | None = None, - log_name: str = "respond", - log_file: str = "respond.log" + input_data: Input, + *, + processor_count: int | None = None, + cohort_ids: Sequence[int] | None = None, + log_name: str = "respond", + log_file: str = "respond.log", ) -> Simulation: """Build a simulation populated with one model per cohort. @@ -70,7 +70,11 @@ def build_simulation( s.set_duration(duration) for cohort_id in cohort_ids: if processor_count: - s.add_model(build_model(input_data, cohort_id, processor_count)) + s.add_model( + build_model( + input_data, cohort_id, processor_count=processor_count + ) + ) else: s.add_model(build_model(input_data, cohort_id)) @@ -83,7 +87,7 @@ def build_model( *, processor_count: int | None = None, log_name: str = "respond", - log_file: str = "respond.log" + log_file: str = "respond.log", ) -> Model: """Build a model for a single cohort. @@ -126,7 +130,8 @@ def build_model( map( int, input_data.config.get( - "simulation", "parameter_change_times").split(), + "simulation", "parameter_change_times" + ).split(), ) ) ) @@ -134,15 +139,17 @@ def build_model( duration = int(input_data.config.get("simulation", "duration")) schedule_times = [1, *change_times] - for model_timestep in range(1, duration+1): + for model_timestep in range(1, duration + 1): parameter_time = max(t for t in schedule_times if t <= model_timestep) - model.add_timestep(build_timestep( - input_data, - cohort_id, - parameter_time, - log_name=log_name, - log_file=log_file, - )) + model.add_timestep( + build_timestep( + input_data, + cohort_id, + parameter_time, + log_name=log_name, + log_file=log_file, + ) + ) return model @@ -152,7 +159,7 @@ def build_timestep( tstep: int = 1, *, log_name: str = "respond", - log_file: str = "respond.log" + log_file: str = "respond.log", ) -> Timestep: """Build a timestep containing the cohort transitions for a time point. @@ -177,7 +184,8 @@ def build_timestep( timestep = Timestep(log_name, log_file) transitions = build_default_transitions( - input_data, cohort_id, time=tstep, log_name=log_name, log_file=log_file) + input_data, cohort_id, time=tstep, log_name=log_name, log_file=log_file + ) for transition in transitions: timestep.add_transition(transition) @@ -192,7 +200,7 @@ def build_transition( *, time: int = 1, log_name: str = "respond", - log_file: str = "respond.log" + log_file: str = "respond.log", ) -> Transition: """Build a transition for a single parameter and cohort. @@ -221,19 +229,19 @@ def build_transition( param.get_parameter_name(), param.get_parameter_name(), log_name, - log_file + log_file, ) transition.add_matrix(input_data.select_parameter(param, cohort_id, time)) return transition def add_matrix_to_transition( - transition: Transition, - input_data: Input, - cohort_id: int, - param: Parameter, - *, - time: int = 1 + transition: Transition, + input_data: Input, + cohort_id: int, + param: Parameter, + *, + time: int = 1, ) -> Transition: """Add another parameter matrix to an existing transition. @@ -265,7 +273,7 @@ def build_default_transitions( *, time: int = 1, log_name: str = "respond", - log_file: str = "respond.log" + log_file: str = "respond.log", ) -> list[Transition]: """Build the default transitions used by each timestep. @@ -288,25 +296,55 @@ def build_default_transitions( The default transition set for a timestep, in model order. """ m = build_transition( - input_data, cohort_id, Parameter(ParameterType.MIGRATION_COHORT), time=time, log_name=log_name, log_file=log_file + input_data, + cohort_id, + Parameter(ParameterType.MIGRATION_COHORT), + time=time, + log_name=log_name, + log_file=log_file, ) b = build_transition( - input_data, cohort_id, Parameter(ParameterType.BEHAVIOR_TRANSITION_PROBABILITY), time=time, log_name=log_name, log_file=log_file + input_data, + cohort_id, + Parameter(ParameterType.BEHAVIOR_TRANSITION_PROBABILITY), + time=time, + log_name=log_name, + log_file=log_file, ) i = build_transition( - input_data, cohort_id, Parameter(ParameterType.INTERVENTION_TRANSITION_PROBABILITY), time=time, log_name=log_name, log_file=log_file + input_data, + cohort_id, + Parameter(ParameterType.INTERVENTION_TRANSITION_PROBABILITY), + time=time, + log_name=log_name, + log_file=log_file, ) o = build_transition( - input_data, cohort_id, Parameter(ParameterType.OVERDOSE_PROBABILITY), time=time, log_name=log_name, log_file=log_file + input_data, + cohort_id, + Parameter(ParameterType.OVERDOSE_PROBABILITY), + time=time, + log_name=log_name, + log_file=log_file, + ) + o = add_matrix_to_transition( + o, + input_data, + cohort_id, + Parameter(ParameterType.OVERDOSE_FATALITY_PROBABILITY), + time=time, ) - o = add_matrix_to_transition(o, input_data, cohort_id, Parameter( - ParameterType.OVERDOSE_FATALITY_PROBABILITY), time=time) d = build_transition( - input_data, cohort_id, Parameter(ParameterType.BACKGROUND_DEATH_PROBABILITY), time=time, log_name=log_name, log_file=log_file + input_data, + cohort_id, + Parameter(ParameterType.BACKGROUND_DEATH_PROBABILITY), + time=time, + log_name=log_name, + log_file=log_file, ) d.get_matrices()[0] = d.get_matrices()[0] * input_data.select_parameter( From a3acc06fb406511ad2a0f954d05e9b62ad5868f3 Mon Sep 17 00:00:00 2001 From: Dimitri Baptiste <55843498+ddbaptiste@users.noreply.github.com> Date: Mon, 21 Sep 2026 09:49:05 -0400 Subject: [PATCH 4/4] Update commit hash for `respond` --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c76a7fc..4d6a6bc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,7 +62,7 @@ set(SPDLOG_INSTALL ON) FetchContent_Declare( respond GIT_REPOSITORY https://github.com/SyndemicsLab/respond.git - GIT_TAG d540a9948adbfcee1c207cb16e7eae0fed93d5f5 # feature/configurable-thread-count + GIT_TAG 738f5b5534b0c87a9b02d2c62c03f4b86ff9bd5f # v2.5.2 OVERRIDE_FIND_PACKAGE ) set(RESPOND_BUILD_DOCS OFF)