Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Comment thread
MJC598 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -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 738f5b5534b0c87a9b02d2c62c03f4b86ff9bd5f # v2.5.2
OVERRIDE_FIND_PACKAGE
)
set(RESPOND_BUILD_DOCS OFF)
Expand Down
57 changes: 35 additions & 22 deletions src/register_model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 //
////////////////////////////////////////////////////////////////////////////////
Expand All @@ -21,12 +21,25 @@ using namespace respond;
// NOLINTNEXTLINE(misc-use-internal-linkage)
void register_model(py::module &m) {
py::class_<Model, py::smart_holder>(m, "Model")
.def(py::init(&Model::Create), py::arg("name"),
.def(
py::init(py::overload_cast<const std::string &, const std::string &,
const std::string &>(&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<const std::string &, unsigned int,
const std::string &, const std::string &>(
&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__",
Expand Down Expand Up @@ -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.")
Expand All @@ -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 "
Expand Down
129 changes: 92 additions & 37 deletions src/respondpy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 #
################################################################################
Expand All @@ -22,18 +22,22 @@


def build_simulation(
input_data: Input,
*,
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.

Parameters
----------
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.
Expand Down Expand Up @@ -65,7 +69,14 @@ 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=processor_count
)
)
else:
s.add_model(build_model(input_data, cohort_id))

return s

Expand All @@ -74,8 +85,9 @@ def build_model(
input_data: Input,
cohort_id: int,
*,
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.

Expand All @@ -86,6 +98,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"
Expand All @@ -97,7 +112,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,
Expand All @@ -109,23 +130,26 @@ def build_model(
map(
int,
input_data.config.get(
"simulation", "parameter_change_times").split(),
"simulation", "parameter_change_times"
).split(),
)
)
)

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


Expand All @@ -135,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.

Expand All @@ -160,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)
Expand All @@ -175,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.

Expand Down Expand Up @@ -204,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.

Expand Down Expand Up @@ -248,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.

Expand All @@ -271,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(
Expand Down
Loading