From d84dcb5c86480ae121dafcc4a4d31a85118f2ccd Mon Sep 17 00:00:00 2001 From: Matthew Carroll <28577806+MJC598@users.noreply.github.com> Date: Wed, 22 Jul 2026 08:38:13 -0400 Subject: [PATCH 1/6] [Update] Correcting Bindings and Stubs (#27) * fixing respondpy and respond linking * fixing overloads --- CMakeLists.txt | 3 +- src/module.cpp | 6 +- src/register_history.cpp | 103 ++++++++------- src/register_logging.cpp | 29 ++--- src/register_model.cpp | 93 +++++++++----- src/register_simulation.cpp | 124 ++++++++++-------- src/register_timestep.cpp | 95 ++++++++++++++ src/register_transition.cpp | 78 +++++++----- src/respondpy/__init__.py | 18 +-- src/respondpy/_core/__init__.pyi | 11 +- src/respondpy/_core/history.pyi | 193 +++++++++++++++++++++++------ src/respondpy/_core/model.pyi | 62 +++++---- src/respondpy/_core/simulation.pyi | 56 ++++++--- src/respondpy/_core/timestep.pyi | 85 +++++++++++++ src/respondpy/_core/transition.pyi | 33 +++-- src/respondpy/_core/types.pyi | 20 +++ src/respondpy/history.py | 6 +- src/respondpy/model.py | 115 +---------------- src/respondpy/simulation.py | 18 ++- src/respondpy/timestep.py | 17 +++ src/respondpy/transition.py | 119 +----------------- tests/test_smoke.py | 68 +++------- 22 files changed, 776 insertions(+), 576 deletions(-) create mode 100644 src/register_timestep.cpp create mode 100644 src/respondpy/_core/timestep.pyi create mode 100644 src/respondpy/_core/types.pyi create mode 100644 src/respondpy/timestep.py diff --git a/CMakeLists.txt b/CMakeLists.txt index b2ca0df..ad8ef15 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,8 +62,7 @@ set(SPDLOG_INSTALL ON) FetchContent_Declare( respond GIT_REPOSITORY https://github.com/SyndemicsLab/respond.git - GIT_TAG e9a452e7082b785978e66907b17af4db8b9bed53 # v2.4.1 - #dcba9320f04dcac0fd161bb86ed3f04a1c016b65 # v2.5.0 + GIT_TAG 1ec829cedee7e33f3cd505312f1e93e7e87115e9 # main OVERRIDE_FIND_PACKAGE ) set(RESPOND_BUILD_DOCS OFF) diff --git a/src/module.cpp b/src/module.cpp index b1e9421..4db73fa 100644 --- a/src/module.cpp +++ b/src/module.cpp @@ -4,7 +4,7 @@ // Created Date: 2025-08-01 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-02-09 // +// Last Modified: 2026-07-16 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center // @@ -20,6 +20,7 @@ void register_logging(py::module &m); void register_model(py::module &m); void register_simulation(py::module &m); void register_transition(py::module &m); +void register_timestep(py::module &m); PYBIND11_MODULE(_core, m, py::mod_gil_not_used()) { py::module history_mod = m.def_submodule("history"); @@ -37,6 +38,9 @@ PYBIND11_MODULE(_core, m, py::mod_gil_not_used()) { py::module sim_mod = m.def_submodule("simulation"); register_simulation(sim_mod); + py::module timestep_mod = m.def_submodule("timestep"); + register_timestep(timestep_mod); + py::module t_mod = m.def_submodule("transition"); register_transition(t_mod); } \ No newline at end of file diff --git a/src/register_history.cpp b/src/register_history.cpp index 79b91bd..55a86b7 100644 --- a/src/register_history.cpp +++ b/src/register_history.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-09 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-05-06 // +// Last Modified: 2026-07-16 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -22,69 +22,84 @@ using namespace respond; // NOLINTNEXTLINE(misc-use-internal-linkage) void register_history(py::module &m) { py::enum_(m, "HistoryMode") - .value("Snapshot", HistoryMode::Snapshot) - .value("Accumulated", HistoryMode::Accumulated) + .value("Snapshot", HistoryMode::kSnapshot) + .value("Accumulated", HistoryMode::kAccumulated) .export_values(); - m.def("get_default_history_mode", &GetDefaultHistoryMode, - py::arg("name"), + m.def("get_default_history_mode", &GetDefaultHistoryMode, py::arg("name"), "Return the default HistoryMode for a named history. Accumulated " "histories track intervention_admission, total_overdose, " "fatal_overdose, and background_death; all others are Snapshot."); py::class_(m, "History") - .def(py::init(), - py::arg("name") = "state", - py::arg("log_name") = "console") - .def(py::init(), - py::arg("name"), - py::arg("log_name"), - py::arg("mode"), - "Construct a History with an explicit recording mode.") + .def(py::init<>(), "Default constructor for a History with name " + "'state' and default mode.") + .def(py::init(), py::arg("name"), + "Construct a History with a specified name and default mode.") + .def(py::init(), + py::arg("name"), py::arg("mode"), + "Construct a History with a specified name and " + "explicit recording mode.") + .def(py::init(), + py::arg("name"), py::arg("mode"), py::arg("log_name"), + "Construct a History with a specified name, mode, and logger.") + .def(py::init(), + py::arg("name"), py::arg("log_name"), + "Construct a History with a specified name " + "and logger, using default mode.") + .def(py::init(), + py::arg("name"), py::arg("log_name"), py::arg("log_filepath"), + "Construct a History with a specified name, logger, and log file " + "path, using default mode.") + .def(py::init(), + py::arg("name"), py::arg("mode"), py::arg("log_name"), + py::arg("log_filepath"), + "Construct a History with a specified name, mode, logger, and log " + "file path.") .def("__copy__", [](const History &self) { return History(self); }) + .def( + "__deepcopy__", + [](const History &self, py::dict) { return History(self); }, "memo") + .def("add_state", &History::AddState, py::arg("state"), + py::arg("timestep") = -1, + "Add a state vector at a given timestep (-1 for auto-increment).") + .def("accumulate_state", &History::AccumulateState, py::arg("state"), + "Add a per-step contribution to an accumulated history.") + .def("flush_pending_state", &History::FlushPendingState, + py::arg("timestep"), py::arg("state_size"), + "Flush the pending accumulated state into a recorded timestep. " + "Records a zero vector of state_size if nothing is pending.") + .def("clear", &History::Clear, "Clear all stored state history.") .def("get_state_map", &History::GetStateMap, "Get the state map (timestep -> state vector).") - .def("get_history_name", &History::GetHistoryName, - "Get the name of the history object.") - .def("get_log_name", &History::GetLogName, - "Get the log name used for logging.") - .def("get_history_mode", &History::GetHistoryMode, - "Get the recording mode (Snapshot or Accumulated).") - .def("get_state_as_vector", &History::GetStateAsVector, - "Get the state as a dense vector, padding missing timesteps with " - "zero vectors.") .def("get_recorded_timesteps", &History::GetRecordedTimesteps, "Get the raw recorded timestep indices without gap-filling.") .def("get_recorded_states", &History::GetRecordedStates, "Get the raw recorded state vectors without gap-filling.") - .def("has_pending_state", &History::HasPendingState, - "Return True when an accumulated history has a pending aggregate " - "not yet flushed.") + .def("get_history_mode", &History::GetHistoryMode, + "Get the recording mode (Snapshot or Accumulated).") .def("get_pending_state", &History::GetPendingState, "Get the pending accumulated state vector, or an empty vector if " "none exists.") - .def("get_latest_recorded_timestep", &History::GetLatestRecordedTimestep, + .def("get_patest_recorded_timestep", + &History::GetLatestRecordedTimestep, "Get the largest recorded timestep, or -1 if history is empty.") - .def("add_state", &History::AddState, - py::arg("state"), - py::arg("timestep") = -1, - "Add a state vector at a given timestep (-1 for auto-increment).") - .def("record_snapshot", &History::RecordSnapshot, - py::arg("state"), - py::arg("timestep"), - "Record a snapshot state vector at a concrete timestep.") - .def("accumulate_state", &History::AccumulateState, - py::arg("state"), - "Add a per-step contribution to an accumulated history.") - .def("flush_pending_state", &History::FlushPendingState, - py::arg("timestep"), - py::arg("state_size") = 0, - "Flush the pending accumulated state into a recorded timestep. " - "Records a zero vector of state_size if nothing is pending.") - .def("clear", &History::Clear, "Clear all stored state history.") + .def("get_name", &History::GetName, + "Get the name of the history object.") + .def("get_state_as_vector", &History::GetStateAsVector, + "Get the state as a dense vector, padding missing timesteps with " + "zero vectors.") .def("__eq__", &History::operator==, "Check equality of History objects (name, log_name, mode, state, " "and pending state).") .def("__ne__", &History::operator!=, - "Check inequality of History objects."); + "Check inequality of History objects.") + .def("__repr__", [](const History &self) { + std::ostringstream ss; + ss << self; + return ss.str(); + }); } \ No newline at end of file diff --git a/src/register_logging.cpp b/src/register_logging.cpp index 41bc8c1..3abdc9d 100644 --- a/src/register_logging.cpp +++ b/src/register_logging.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-01-08 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-05-06 // +// Last Modified: 2026-07-16 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -38,6 +38,7 @@ void register_logging(py::module &m) { .export_values(); m.def("create_file_logger", &respond::CreateFileLogger, + py::arg("logger_name"), py::arg("filepath"), "Creates a File Logger for use with RESPOND."); m.def("create_shared_file_sink", &respond::CreateSharedFileSink, py::arg("filepath"), @@ -49,14 +50,12 @@ void register_logging(py::module &m) { "Create a logger that writes to the shared file sink. Requires " "create_shared_file_sink() to be called first."); - m.def("set_log_pattern", &respond::SetLogPattern, - py::arg("pattern"), + m.def("set_log_pattern", &respond::SetLogPattern, py::arg("pattern"), "Set the logging pattern template for all subsequent logger " "creations."); m.def("get_log_pattern", &respond::GetLogPattern, "Get the current logging pattern template."); - m.def("set_flush_interval", &respond::SetFlushInterval, - py::arg("seconds"), + m.def("set_flush_interval", &respond::SetFlushInterval, py::arg("seconds"), "Set the global flush interval in seconds (0 to disable " "auto-flush)."); m.def("flush_all_loggers", &respond::FlushAllLoggers, @@ -66,18 +65,20 @@ void register_logging(py::module &m) { py::arg("logger_name"), "Check if a logger with the given name exists. Returns " "CreationStatus.kExists or kNotCreated."); - m.def("get_logger_info", &respond::GetLoggerInfo, - py::arg("logger_name"), + m.def("get_logger_info", &respond::GetLoggerInfo, py::arg("logger_name"), "Retrieve a string with details about a logger (name, file path, " "level, thread info)."); - m.def("set_logger_level", &respond::SetLoggerLevel, - py::arg("logger_name"), py::arg("level"), + m.def("set_logger_level", &respond::SetLoggerLevel, py::arg("logger_name"), + py::arg("level"), "Set the logging level for a specific logger. level: 0=trace, " "1=debug, 2=info, 3=warn, 4=error, 5=critical."); - m.def("log_info", &respond::LogInfo, "Logs an info message to the log."); - m.def("log_warning", &respond::LogWarning, - "Logs a warning message to the log."); - m.def("log_error", &respond::LogError, "Logs an error message to the log."); - m.def("log_debug", &respond::LogDebug, "Logs a debug message to the log."); + m.def("log_info", &respond::LogInfo, py::arg("logger_name"), + py::arg("message"), "Logs an info message to the log."); + m.def("log_warning", &respond::LogWarning, py::arg("logger_name"), + py::arg("message"), "Logs a warning message to the log."); + m.def("log_error", &respond::LogError, py::arg("logger_name"), + py::arg("message"), "Logs an error message to the log."); + m.def("log_debug", &respond::LogDebug, py::arg("logger_name"), + py::arg("message"), "Logs a debug message to the log."); } diff --git a/src/register_model.cpp b/src/register_model.cpp index 1a64b61..b1e9c9b 100644 --- a/src/register_model.cpp +++ b/src/register_model.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-01-08 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-25 // +// Last Modified: 2026-07-20 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -12,6 +12,7 @@ #include +#include #include namespace py = pybind11; @@ -21,45 +22,69 @@ using namespace respond; void register_model(py::module &m) { py::class_(m, "Model") .def(py::init(&Model::Create), py::arg("name"), - py::arg("log_name") = "console") - .def("set_state", - [](Model &m, const py::EigenDRef &vec) { - return m.SetState(vec); - }) - .def("get_state", &Model::GetState) - .def("run_transitions", &Model::RunTransitions) - .def("add_transition", &Model::AddTransition) - .def("get_transition_names", &Model::GetTransitionNames) - .def("clear_transitions", &Model::ClearTransitions) - .def("get_histories", &Model::GetHistories) - .def("set_histories", &Model::SetHistories) - .def("get_model_name", &Model::GetModelName) - .def("get_log_name", &Model::GetLogName) - .def("create_default_histories", &Model::CreateDefaultHistories) + 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("__copy__", [](const Model &self) { return self.clone(); }) + .def( + "__deepcopy__", + [](const Model &self, py::dict) { return self.clone(); }, + "memo") // memo argument is required by Python's deepcopy protocol; + .def("add_timestep", &Model::AddTimestep, py::arg("timestep"), + "Add a single timestep to the model. The model gains an ownership " + "reference to this timestep and will manage its lifecycle.") + .def("run_timestep", py::overload_cast<>(&Model::RunTimestep), + "Execute the next timestep in the model's sequence.") + .def("run_timestep", py::overload_cast(&Model::RunTimestep), + py::arg("idx"), + "Execute the timestep at the specified index in the model's " + "sequence.") + .def("run_timesteps", &Model::RunTimesteps, + "Execute all registered timesteps in sequence, applying their " + "transitions to the model's state.") + .def("clear_timesteps", &Model::ClearTimesteps, + "Clear all timesteps from the model.") .def("clear_histories", &Model::ClearHistories, - "Clear all history records and reset history tracking state.") + "Clear all history records and reset the history tracking state.") + .def("create_default_histories", &Model::CreateDefaultHistories, + "Create default history tracking for the model. Initializes " + "standard history records based on the model's state.") + .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", &Model::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.") + .def("get_timestep", &Model::GetTimestep, + "Get the current timestep index.") + .def( + "get_history_capture_interval", &Model::GetHistoryCaptureInterval, + "Get the active capture interval. A value of 1 means full capture.") + .def("get_final_timestep", &Model::GetFinalTimestep, + "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::SetState, 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 " "interval timesteps; values less than 1 default to full capture.") - .def("get_history_capture_interval", &Model::GetHistoryCaptureInterval, - "Get the active capture interval. A value of 1 means full " - "capture.") .def("set_final_timestep", &Model::SetFinalTimestep, py::arg("final_timestep"), "Set the final timestep that must always be recorded.") - .def("get_final_timestep", &Model::GetFinalTimestep, - "Get the configured final simulation timestep, or -1 if unset.") - .def("__repr__", - [](const Model &m) { - return ""; - }) - .def("__copy__", [](const Model &self) { return self.clone(); }) - .def( - "__deepcopy__", - [](const Model &self, py::dict) { return self.clone(); }, - "memo"); // memo argument is required by Python's deepcopy protocol; + .def("set_initial_history_recorded", &Model::SetInitialHistoryRecorded, + py::arg("recorded"), + "Set whether the initial history has been recorded.") + .def("serialize", &Model::Serialize, + "Serialize the model's state and history into a string.") + .def("__repr__", [](const Model &m) { + std::stringstream ss; + ss << m; + return ss.str(); + }); } diff --git a/src/register_simulation.cpp b/src/register_simulation.cpp index 83cf669..5116ce7 100644 --- a/src/register_simulation.cpp +++ b/src/register_simulation.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-09 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-06-17 // +// Last Modified: 2026-07-16 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -20,56 +20,78 @@ using namespace respond; // NOLINTNEXTLINE(misc-use-internal-linkage) void register_simulation(py::module &m) { py::class_(m, "Simulation") - .def(py::init<>()) - .def(py::init()) - .def("run", &Simulation::Run) - .def("add_model", &Simulation::AddModel) - .def("get_models", &Simulation::GetModels) - .def("get_model_names", &Simulation::GetModelNames) - .def("clear_models", &Simulation::ClearModels) + .def(py::init<>(), + "Default constructor for a Simulation instance. Initializes the " + "simulation with the default logger.") + .def(py::init(), py::arg("log_name"), + "Constructs a Simulation with a specified logger.") + .def(py::init(), + py::arg("log_name"), py::arg("log_filepath"), + "Constructs a Simulation with a specified logger and log file.") + .def("__copy__", + [](const Simulation &self) { return Simulation(self); }) .def( - "get_model_histories", - [](const Simulation &self) { - py::dict result; - for (const auto &model : self.GetModels()) { - py::dict model_hist; - for (const auto &kv : model->GetHistories()) { - model_hist[py::str(kv.first)] = - kv.second.GetStateAsVector(); - } - result[py::str(model->GetModelName())] = model_hist; - } - return result; - }, - "Get densified state histories for all models. Returns " - "dict[model_name, dict[history_name, list[ndarray]]].") + "__deepcopy__", + [](const Simulation &self, py::dict) { return Simulation(self); }, + "memo") + .def("create_new_model", &Simulation::CreateNewModel, + py::arg("model_name"), + "Create a new model instance and add it to the simulation. " + "Initializes logging for the model and returns a unique_ptr to " + "the created instance. Throws an exception if the model name is " + "unsupported.") + .def("clear_models", &Simulation::ClearModels, + "Clear all models from the simulation.") + .def("add_model", &Simulation::AddModel, py::arg("model"), + "Add an existing model instance to the simulation. The simulation " + "takes ownership of the model.") + .def("run", &Simulation::Run, py::arg("duration") = -1, + "Run the simulation for a specified duration. Executes all " + "registered timesteps for each model in sequence.") + .def("get_models", &Simulation::GetModels, + "Get the list of models in the simulation.") + .def("get_model", + py::overload_cast(&Simulation::GetModel, py::const_), + py::arg("model_index"), + "Get a model instance by its index in the simulation. Throws an " + "exception if the index is out of bounds.") + .def("get_model", + py::overload_cast(&Simulation::GetModel, + py::const_), + py::arg("model_name"), + "Get a model instance by its name. Throws an exception if the " + "model name is not found.") + .def("get_model_names", &Simulation::GetModelNames, + "Get the list of model names in the simulation.") .def( - "get_model_sparse_histories", - [](const Simulation &self) { - py::dict result; - for (const auto &model : self.GetModels()) { - py::dict model_hist; - for (const auto &kv : model->GetHistories()) { - model_hist[py::str(kv.first)] = kv.second; - } - if (result.contains(py::str(model->GetModelName()))) { - throw std::runtime_error( - "Duplicate model name found: " + - model->GetModelName()); - } - result[py::str(model->GetModelName())] = model_hist; - } - return result; - }, - "Get sparse History objects for all models. Returns " - "dict[model_name, dict[history_name, History]].") - .def("get_model_history_names", &Simulation::GetModelHistoryNames) - .def("get_log_name", &Simulation::GetLogName) - .def("__repr__", - [](const Simulation &m) { - return ""; - }) - .def("__copy__", - [](const Simulation &self) { return Simulation(self); }); + "get_model_history", + py::overload_cast(&Simulation::GetModelHistory, + py::const_), + py::arg("idx"), + "Get the history of a model by its index in the simulation. Throws " + "an exception if the index is out of bounds.") + .def("get_model_history", + py::overload_cast( + &Simulation::GetModelHistory, py::const_), + py::arg("model_name"), + "Get the history of a model by its name. Throws an exception if " + "the model name is not found.") + .def("get_model_history_names", + py::overload_cast(&Simulation::GetModelHistoryNames, + py::const_), + py::arg("idx"), + "Get the list of history names for a model by its index in the " + "simulation. Throws an exception if the index is out of bounds.") + .def("get_model_history_names", + py::overload_cast( + &Simulation::GetModelHistoryNames, py::const_), + py::arg("model_name"), + "Get the list of history names for a model by its name. Throws an " + "exception if the model name is not found.") + .def("set_duration", &Simulation::SetDuration, py::arg("duration"), + "Set the duration for which the simulation should run.") + .def("__repr__", [](const Simulation &m) { + return ""; + }); } \ No newline at end of file diff --git a/src/register_timestep.cpp b/src/register_timestep.cpp new file mode 100644 index 0000000..2462f0b --- /dev/null +++ b/src/register_timestep.cpp @@ -0,0 +1,95 @@ +//////////////////////////////////////////////////////////////////////////////// +// File: register_timestep.cpp // +// Project: respondpy // +// Created Date: 2026-07-16 // +// Author: Matthew Carroll // +// ----- // +// Last Modified: 2026-07-16 // +// Modified By: Matthew Carroll // +// ----- // +// Copyright (c) 2026 Syndemics Lab at Boston Medical Center // +//////////////////////////////////////////////////////////////////////////////// + +#include + +#include + +namespace py = pybind11; +using namespace respond; + +// NOLINTNEXTLINE(misc-use-internal-linkage) +void register_timestep(py::module &m) { + py::class_(m, "Timestep") + .def(py::init<>(), "Default constructor for a Timestep instance.") + .def(py::init(), py::arg("log_name"), + "Constructs a Timestep with a specified log_name.") + .def( + py::init(), + py::arg("log_name"), py::arg("log_filepath"), + "Constructs a Timestep with a specified log_name and log_filepath.") + .def("__copy__", [](const Timestep &self) { return Timestep(self); }) + .def( + "__deepcopy__", + [](const Timestep &self, py::dict) { return Timestep(self); }, + "memo") + .def( + "create_transition", + [](Timestep &self, + const std::string &transition_name) -> const Transition * { + return self.CreateTransition(transition_name).get(); + }, + py::arg("transition_name"), + py::return_value_policy::reference_internal, + "Create a new transition instance and add it to the timestep. " + "Returns a reference to the created transition.") + .def("remove_transition", &Timestep::RemoveTransition, py::arg("idx"), + "Remove a transition from the timestep by its idx. Throws an " + "exception if the idx is out of bounds.") + .def( + "add_matrix_to_transition", + py::overload_cast &>( + &Timestep::AddMatrixToTransition), + py::arg("idx"), py::arg("matrix"), + "Add a matrix to a transition in the timestep by its index. Throws " + "an exception if the index is out of bounds.") + .def("add_matrix_to_transition", + py::overload_cast &>( + &Timestep::AddMatrixToTransition), + py::arg("transition_name"), py::arg("matrix"), + "Add a matrix to a transition in the timestep by its name. Throws " + "an exception if the transition name is not found.") + .def( + "get_transition", + [](const Timestep &self, const size_t &idx) -> const Transition * { + return self.GetTransition(idx).get(); + }, + py::arg("idx"), py::return_value_policy::reference_internal, + "Get a transition from the timestep by its index. Throws an " + "exception if the index is out of bounds.") + .def( + "get_transition", + [](const Timestep &self, + const std::string &transition_name) -> const Transition * { + return self.GetTransition(transition_name).get(); + }, + py::arg("transition_name"), + py::return_value_policy::reference_internal, + "Get a transition from the timestep by its name. Throws an " + "exception if the transition name is not found.") + .def("get_transitions", &Timestep::GetTransitions, + "Get the list of transitions in the timestep.") + .def("get_transition_names", &Timestep::GetTransitionNames, + "Get the list of transition names in the timestep.") + .def("__repr__", + [](const Timestep &t) { + std::stringstream ss; + ss << t; + return ss.str(); + }) + .def("__eq__", &Timestep::operator==, + "Check if two Timestep instances are equal.") + .def("__ne__", &Timestep::operator!=, + "Check if two Timestep instances are not equal."); +} \ No newline at end of file diff --git a/src/register_transition.cpp b/src/register_transition.cpp index 86ab2aa..8ed6c80 100644 --- a/src/register_transition.cpp +++ b/src/register_transition.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-02 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-05-06 // +// Last Modified: 2026-07-20 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -18,7 +18,6 @@ #include #include #include -#include namespace py = pybind11; using namespace respond; @@ -26,36 +25,55 @@ using namespace respond; // NOLINTNEXTLINE(misc-use-internal-linkage) void register_transition(py::module &m) { py::class_ t(m, "Transition"); - t.def(py::init(&TransitionFactory::CreateTransition), py::arg("type"), - py::arg("log_name") = "console") - .def("execute", - [](const Transition &self, const Eigen::VectorXd &state, - py::object hist_obj) { - std::map h; - if (hist_obj.is_none()) { - LogWarning(self.GetLogName(), - "execute() called without a history map. " - "History will not be recorded. Pass the " - "model's history map for expected behavior."); - } else { - h = hist_obj.cast>(); - } - auto result = self.Execute(state, h); - return py::make_tuple(result, h); - }, - py::arg("state"), py::arg("history") = py::none(), - "Execute the transition on the given state. Returns " - "(state_result, history_map). Pass the model's history map for " - "expected behavior; omitting it will issue a warning and the " - "returned history map will be empty.") - .def("add_transition_matrix", &Transition::AddTransitionMatrix) - .def("get_transition_name", &Transition::GetTransitionName) - .def("clear_transition_matrices", &Transition::ClearTransitionMatrices) - .def("get_log_name", &Transition::GetLogName) + t.def(py::init([](const std::string &type, const std::string &log_name, + const std::string &log_file) { + return Transition::Create(type, RESPOND_DEFAULT_TRANSITION_NAME, + log_name, log_file); + }), + py::arg("type"), py::arg("log_name") = RESPOND_DEFAULT_LOG, + py::arg("log_file") = RESPOND_DEFAULT_LOG_FILE, + "Factory method to create a Transition instance of the specified " + "type. Uses the default transition name and initializes logging.") + .def(py::init([](const std::string &type, const std::string &name, + const std::string &log_name, + const std::string &log_file) { + return Transition::Create(type, name, log_name, log_file); + }), + py::arg("type"), py::arg("name"), + py::arg("log_name") = RESPOND_DEFAULT_LOG, + py::arg("log_file") = RESPOND_DEFAULT_LOG_FILE, + "Factory method to create a named Transition instance.") + .def( + "execute", + [](const Transition &self, const Eigen::VectorXd &state, + py::object hist_obj) { + std::map h; + if (hist_obj.is_none()) { + LogWarning(RESPOND_DEFAULT_LOG, + "execute() called without a history map. " + "History will not be recorded. Pass the " + "model's history map for expected behavior."); + } else { + h = hist_obj.cast>(); + } + auto result = self.Execute(state, h); + return py::make_tuple(result, h); + }, + py::arg("state"), py::arg("history") = py::none(), + "Execute the transition on the given state. Returns " + "(state_result, history_map). Pass the model's history map for " + "expected behavior; omitting it will issue a warning and the " + "returned history map will be empty.") + .def("add_matrix", &Transition::AddMatrix, py::arg("matrix")) + .def("get_name", &Transition::GetName) + .def("clear_matrices", &Transition::ClearMatrices) + .def("add_matrix", &Transition::AddMatrix, py::arg("matrix")) + .def("get_name", &Transition::GetName) + .def("clear_matrices", &Transition::ClearMatrices) + .def("get_matrices", &Transition::GetMatrices) .def("__repr__", [](const Transition &m) { - return ""; + return ""; }) .def("__copy__", [](const Transition &self) { return self.clone(); }) .def( diff --git a/src/respondpy/__init__.py b/src/respondpy/__init__.py index 103c55a..438ac31 100644 --- a/src/respondpy/__init__.py +++ b/src/respondpy/__init__.py @@ -4,7 +4,7 @@ # Created Date: 2025-08-04 # # Author: Matthew Carroll # # ----- # -# Last Modified: 2026-06-26 # +# Last Modified: 2026-07-20 # # Modified By: Matthew Carroll # # ----- # # Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center # @@ -21,17 +21,15 @@ ) from .history import History -from .model import ( - Model, build_model, add_transitions_to_model, build_model_transitions -) +from .model import Model from .simulation import ( Simulation, build_simulation ) -from .transition import ( - Transition, transition_factory, build_timestep_transition -) +from .timestep import Timestep + +from .transition import Transition __all__ = [ "data", @@ -41,14 +39,10 @@ "calculate_life_years", "History", "Model", - "build_model", - "add_transitions_to_model", - "build_model_transitions", "Simulation", "build_simulation", + "Timestep", "Transition", - "transition_factory", - "build_timestep_transition" ] diff --git a/src/respondpy/_core/__init__.pyi b/src/respondpy/_core/__init__.pyi index 509178a..cca209d 100644 --- a/src/respondpy/_core/__init__.pyi +++ b/src/respondpy/_core/__init__.pyi @@ -4,7 +4,7 @@ # Created Date: 2026-02-13 # # Author: Matthew Carroll # # ----- # -# Last Modified: 2026-06-05 # +# Last Modified: 2026-07-20 # # Modified By: Matthew Carroll # # ----- # # Copyright (c) 2026 Syndemics Lab at Boston Medical Center # @@ -14,14 +14,16 @@ from __future__ import annotations from .cost_effectiveness import discount, cwise_product, cwise_min, calculate_life_years # pylint: disable=E0611,E0401 # type: ignore[reportMissingModuleSource] -from .history import History # pylint: disable=E0611,E0401 # type: ignore[reportMissingModuleSource] - from .logging import CreationStatus, LogType, create_file_logger, kDebug, kError, kExists, kInfo, kNotCreated, kSuccess, kWarn, log_debug, log_error, log_info, log_warning # pylint: disable=E0611,E0401 # type: ignore[reportMissingModuleSource] +from .history import HistoryMode, get_default_history_mode, History # pylint: disable=E0611,E0401 # type: ignore[reportMissingModuleSource] + from .model import Model # pylint: disable=E0611,E0401 # type: ignore[reportMissingModuleSource] from .simulation import Simulation # pylint: disable=E0611,E0401 # type: ignore[reportMissingModuleSource] +from .timestep import Timestep # pylint: disable=E0611,E0401 # type: ignore[reportMissingModuleSource] + from .transition import Transition # pylint: disable=E0611,E0401 # type: ignore[reportMissingModuleSource] __all__: list[str] = [ @@ -43,9 +45,12 @@ __all__: list[str] = [ 'log_error', 'log_info', 'log_warning', + 'HistoryMode', + 'get_default_history_mode', 'History', 'Model', 'Simulation', + 'Timestep', 'Transition' ] diff --git a/src/respondpy/_core/history.pyi b/src/respondpy/_core/history.pyi index d76dad7..4bfee37 100644 --- a/src/respondpy/_core/history.pyi +++ b/src/respondpy/_core/history.pyi @@ -4,7 +4,7 @@ # Created Date: 2026-02-09 # # Author: Matthew Carroll # # ----- # -# Last Modified: 2026-02-10 # +# Last Modified: 2026-07-20 # # Modified By: Matthew Carroll # # ----- # # Copyright (c) 2026 Syndemics Lab at Boston Medical Center # @@ -12,58 +12,169 @@ from __future__ import annotations -import numpy -import numpy.typing import typing -__all__: list[str] = ['History'] +from .types import StateVector + +__all__: list[str] = ['HistoryMode', 'get_default_history_mode', 'History'] + + +class HistoryMode: + """ + Members: + + kSnapshot + + kAccumulated + """ + __members__: typing.ClassVar[ + dict[str, HistoryMode] + # value = {'kError': , 'kSuccess': } + ] + # value = + kSnapshot: typing.ClassVar[HistoryMode] + # value = + kAccumulated: typing.ClassVar[HistoryMode] + + def __eq__(self, other: typing.Any) -> bool: + ... + + def __getstate__(self) -> int: + ... + + def __hash__(self) -> int: + ... + + def __index__(self) -> int: + ... + + def __init__(self, value: typing.SupportsInt) -> None: + ... + + def __int__(self) -> int: + ... + + def __ne__(self, other: typing.Any) -> bool: + ... + + def __repr__(self) -> str: + ... + + def __setstate__(self, state: typing.SupportsInt) -> None: + ... + + def __str__(self) -> str: + ... + + @property + def name(self) -> str: + ... + + @property + def value(self) -> int: + ... + + +def get_default_history_mode(name: str) -> HistoryMode: + ... class History: - __hash__: typing.ClassVar[None] = None + @typing.overload + def __init__(self) -> None: + ... + + @typing.overload + def __init__( + self, + name: str + ) -> None: + ... + + @typing.overload + def __init__(self, name: str, mode: HistoryMode) -> None: + ... + + @typing.overload + def __init__(self, name: str, mode: HistoryMode, log_name: str) -> None: + ... + + @typing.overload + def __init__(self, name: str, log_name: str) -> None: + ... + + @typing.overload + def __init__(self, name: str, log_name: str, log_file: str) -> None: + ... + + @typing.overload + def __init__( + self, + name: str, + mode: HistoryMode, + log_name: str, + log_file: str + ) -> None: + ... def __copy__(self) -> History: ... - def __eq__(self, arg0: History) -> bool: - """ - Check equality of History objects (name, log_name, and state). - """ + def __deepcopy__(self, arg0: dict) -> History: + ... - def __init__(self, name: str = 'state', log_name: str = 'console') -> None: + def add_state( + self, + state: StateVector, + timestep: typing.SupportsInt = -1 + ) -> None: ... - def __ne__(self, arg0: History) -> bool: - """ - Check inequality of History objects. - """ + def accumulate_state(self, state: StateVector) -> None: + ... - def add_state(self, state: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[m, 1]"], timestep: typing.SupportsInt = -1) -> None: - """ - Add a state vector at a given timestep (-1 for auto-increment). - """ + def flush_pending_state( + self, + timestep: typing.SupportsInt, + state_size: typing.SupportsInt + ) -> None: + ... def clear(self) -> None: - """ - Clear all stored state history. - """ - - def get_history_name(self) -> str: - """ - Get the name of the history object. - """ - - def get_log_name(self) -> str: - """ - Get the log name used for logging. - """ - - def get_state_as_vector(self) -> list[typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 1]"]]: - """ - Get the state as a vector, padding missing timesteps with zero vectors. - """ - - def get_state_map(self) -> dict[int, typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 1]"]]: - """ - Get the state map (timestep -> state vector). - """ + ... + + def has_pending_state(self) -> bool: + ... + + def get_state_map(self) -> typing.Mapping[int, StateVector]: + ... + + def get_recorded_timesteps(self) -> typing.Sequence[int]: + ... + + def get_recorded_states(self) -> typing.Sequence[StateVector]: + ... + + def get_history_mode(self) -> HistoryMode: + ... + + def get_pending_state(self) -> StateVector: + ... + + def get_latest_recorded_timestep(self) -> typing.SupportsInt: + ... + + def get_name(self) -> str: + ... + + def get_state_as_vector(self) -> typing.Sequence[StateVector]: + ... + + def __eq__(self, other: object) -> bool: + ... + + def __ne__(self, other: object) -> bool: + ... + + def __repr__(self) -> str: + ... diff --git a/src/respondpy/_core/model.pyi b/src/respondpy/_core/model.pyi index 2e61ac5..5c26139 100644 --- a/src/respondpy/_core/model.pyi +++ b/src/respondpy/_core/model.pyi @@ -4,7 +4,7 @@ # Created Date: 2026-02-09 # # Author: Matthew Carroll # # ----- # -# Last Modified: 2026-06-25 # +# Last Modified: 2026-07-20 # # Modified By: Matthew Carroll # # ----- # # Copyright (c) 2026 Syndemics Lab at Boston Medical Center # @@ -12,62 +12,78 @@ from __future__ import annotations -import collections.abc - -import numpy -import numpy.typing -import respondpy._core.history import typing -from .transition import Transition +from .types import StateVector +from .history import History +from .timestep import Timestep __all__: list[str] = ['Model'] class Model: + def __init__(self, name: str, log_name: str = "respond", log_file: str = "respond.log") -> None: + ... + def __copy__(self) -> Model: ... def __deepcopy__(self, arg0: dict) -> Model: - """ - memo - """ + ... - def __init__(self, name: str, log_name: str = 'console') -> None: + def add_timestep(self, timestep: Timestep) -> None: ... - def __repr__(self) -> str: + def run_timestep(self, idx: typing.SupportsInt = -1) -> None: ... - def add_transition(self, transition: Transition) -> None: + def run_timesteps(self) -> None: ... - def clear_transitions(self) -> None: + def clear_timesteps(self) -> None: ... - def get_histories(self) -> dict[str, respondpy._core.history.History]: + def clear_histories(self) -> None: ... - def get_log_name(self) -> str: + def create_default_histories(self) -> None: ... - def get_model_name(self) -> str: + def get_timestep_at_index(self, idx: typing.SupportsInt) -> Timestep: ... - def get_state(self) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 1]"]: + def get_state(self) -> StateVector: ... - def get_transition_names(self) -> list[str]: + def get_name(self) -> str: ... - def run_transitions(self) -> None: + def get_histories(self) -> typing.Mapping[str, History]: ... - def set_histories(self, histories: collections.abc.Mapping[str, respondpy._core.history.History]) -> None: + def get_timestep(self) -> int: ... - def set_state(self, state: numpy.typing.NDArray[numpy.float64]) -> None: + def get_history_capture_interval(self) -> typing.SupportsInt: ... - def create_default_histories(self) -> None: + def get_final_timestep(self) -> typing.SupportsInt: + ... + + def get_initial_history_recorded(self) -> bool: + ... + + def set_state(self, state: StateVector) -> None: + ... + + def set_history_capture_interval(self, interval: typing.SupportsInt) -> None: + ... + + def set_final_timestep(self, timestep: typing.SupportsInt) -> None: + ... + + def set_initial_history_recorded(self, recorded: bool) -> None: + ... + + def __repr__(self) -> str: ... diff --git a/src/respondpy/_core/simulation.pyi b/src/respondpy/_core/simulation.pyi index 36d75cb..4efc41f 100644 --- a/src/respondpy/_core/simulation.pyi +++ b/src/respondpy/_core/simulation.pyi @@ -4,7 +4,7 @@ # Created Date: 2026-02-09 # # Author: Matthew Carroll # # ----- # -# Last Modified: 2026-06-29 # +# Last Modified: 2026-07-20 # # Modified By: Matthew Carroll # # ----- # # Copyright (c) 2026 Syndemics Lab at Boston Medical Center # @@ -12,20 +12,15 @@ from __future__ import annotations -from collections.abc import Sequence - -import numpy -import numpy.typing -import respondpy._core.model import typing +from .model import Model +from .history import History + __all__: list[str] = ['Simulation'] class Simulation: - def __copy__(self) -> Simulation: - ... - @typing.overload def __init__(self) -> None: ... @@ -34,32 +29,57 @@ class Simulation: def __init__(self, log_name: str) -> None: ... - def __repr__(self) -> str: + @typing.overload + def __init__(self, log_name: str, log_file: str) -> None: ... - def add_model(self, model: respondpy._core.model.Model) -> None: + def __copy__(self) -> Simulation: + ... + + def __deepcopy__(self, arg0: dict) -> Simulation: + ... + + def create_new_model(self, model_name: str) -> str: ... def clear_models(self) -> None: ... - def get_log_name(self) -> str: + def add_model(self, model: Model) -> None: ... - def get_model_histories(self) -> dict[str, dict[str, Sequence[typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 1]"]]]]: + def run(self, duration: typing.SupportsInt = -1) -> None: ... - def get_model_sparse_histories(self) -> dict[str, dict[str, typing.Any]]: + def get_models(self) -> list[Model]: ... - def get_model_history_names(self) -> Sequence[tuple[str, str]]: + @typing.overload + def get_model(self, idx: typing.SupportsInt) -> Model: ... - def get_model_names(self) -> Sequence[str]: + @typing.overload + def get_model(self, model_name: str) -> Model: ... - def get_models(self) -> Sequence[respondpy._core.model.Model]: + def get_model_names(self) -> list[str]: + ... + + @typing.overload + def get_model_history(self, idx: typing.SupportsInt) -> typing.Mapping[str, History]: + ... + + @typing.overload + def get_model_history(self, model_name: str) -> typing.Mapping[str, History]: + ... + + @typing.overload + def get_model_history_names(self, idx: typing.SupportsInt) -> list[str]: + ... + + @typing.overload + def get_model_history_names(self, model_name: str) -> list[str]: ... - def run(self) -> None: + def set_duration(self, duration: typing.SupportsInt) -> None: ... diff --git a/src/respondpy/_core/timestep.pyi b/src/respondpy/_core/timestep.pyi new file mode 100644 index 0000000..ca52b0b --- /dev/null +++ b/src/respondpy/_core/timestep.pyi @@ -0,0 +1,85 @@ +################################################################################ +# File: timestep.pyi # +# Project: respondpy # +# Created Date: 2026-07-20 # +# Author: Matthew Carroll # +# ----- # +# Last Modified: 2026-07-20 # +# Modified By: Matthew Carroll # +# ----- # +# Copyright (c) 2026 Syndemics Lab at Boston Medical Center # +################################################################################ + +from __future__ import annotations + +import typing + +from .transition import Transition +from .types import StateVector, TransitionMatrix + +__all__: list[str] = ['Timestep'] + + +class Timestep: + @typing.overload + def __init__(self) -> None: + ... + + @typing.overload + def __init__(self, log_name: str) -> None: + ... + + @typing.overload + def __init__(self, log_name: str, log_file: str) -> None: + ... + + def __copy__(self) -> Timestep: + ... + + def __deepcopy__(self, arg0: dict) -> Timestep: + ... + + def create_transition(self, transition_name: str) -> Transition: + ... + + def remove_transition(self, idx: typing.SupportsInt) -> Transition: + ... + + @typing.overload + def add_matrix_to_transition( + self, + idx: typing.SupportsInt, + mat: StateVector | TransitionMatrix + ) -> None: + ... + + @typing.overload + def add_matrix_to_transition( + self, + transition_name: str, + mat: StateVector | TransitionMatrix + ) -> None: + ... + + @typing.overload + def get_transition(self, idx: typing.SupportsInt) -> Transition: + ... + + @typing.overload + def get_transition(self, transition_name: str) -> Transition: + ... + + def get_transitions(self) -> typing.Sequence[Transition]: + ... + + def get_transition_names(self) -> typing.Sequence[str]: + ... + + def __repr__(self) -> str: + ... + + def __eq__(self, other: object) -> bool: + ... + + def __ne__(self, other: object) -> bool: + ... diff --git a/src/respondpy/_core/transition.pyi b/src/respondpy/_core/transition.pyi index 5243aaf..07ebe70 100644 --- a/src/respondpy/_core/transition.pyi +++ b/src/respondpy/_core/transition.pyi @@ -4,49 +4,48 @@ # Created Date: 2026-02-09 # # Author: Matthew Carroll # # ----- # -# Last Modified: 2026-02-10 # +# Last Modified: 2026-07-20 # # Modified By: Matthew Carroll # # ----- # # Copyright (c) 2026 Syndemics Lab at Boston Medical Center # ################################################################################ from __future__ import annotations -import collections.abc -import numpy -import numpy.typing -import respondpy._core.history import typing +from .history import History +from .types import StateVector, TransitionMatrix + __all__: list[str] = ['Transition'] class Transition: - def __copy__(self) -> Transition: + def __init__( + self, type: str, name: str = 'transition', log_name: str = 'respond', log_file: str = 'respond.log' + ) -> None: ... - def __deepcopy__(self, arg0: dict) -> Transition: - """ - memo - """ + def execute(self, state: StateVector, history: typing.Mapping[str, History]) -> StateVector: + ... - def __init__(self, type: str, log_name: str = 'console') -> None: + def add_matrix(self, mat: TransitionMatrix) -> None: ... - def __repr__(self) -> str: + def get_matrices(self) -> list[TransitionMatrix]: ... - def add_transition_matrix(self, mat: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[m, n]"]) -> None: + def get_name(self) -> str: ... - def clear_transition_matrices(self) -> None: + def clear_matrices(self) -> None: ... - def execute(self, state: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[m, 1]"], history: collections.abc.Mapping[str, respondpy._core.history.History]) -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[m, 1]"]: + def __copy__(self) -> Transition: ... - def get_log_name(self) -> str: + def __deepcopy__(self, arg0: dict) -> Transition: ... - def get_transition_name(self) -> str: + def __repr__(self) -> str: ... diff --git a/src/respondpy/_core/types.pyi b/src/respondpy/_core/types.pyi new file mode 100644 index 0000000..0837870 --- /dev/null +++ b/src/respondpy/_core/types.pyi @@ -0,0 +1,20 @@ +################################################################################ +# File: types.pyi # +# Project: respondpy # +# Created Date: 2026-07-20 # +# Author: Matthew Carroll # +# ----- # +# Last Modified: 2026-07-20 # +# Modified By: Matthew Carroll # +# ----- # +# Copyright (c) 2026 Syndemics Lab at Boston Medical Center # +################################################################################ + +from __future__ import annotations + +import numpy as np +import numpy.typing as npt +import typing + +StateVector = typing.Annotated[npt.NDArray[np.float64], "[m, 1]"] +TransitionMatrix = typing.Annotated[npt.NDArray[np.float64], "[m, m]"] diff --git a/src/respondpy/history.py b/src/respondpy/history.py index 6a870ac..f4f53e1 100644 --- a/src/respondpy/history.py +++ b/src/respondpy/history.py @@ -4,7 +4,7 @@ # Created Date: 2026-06-05 # # Author: Matthew Carroll # # ----- # -# Last Modified: 2026-06-10 # +# Last Modified: 2026-07-20 # # Modified By: Matthew Carroll # # ----- # # Copyright (c) 2026 Syndemics Lab at Boston Medical Center # @@ -12,6 +12,6 @@ from __future__ import annotations -from ._core.history import History # pylint: disable=E0611,E0401 # type: ignore[reportMissingModuleSource] +from ._core.history import HistoryMode, get_default_history_mode, History # pylint: disable=E0611,E0401 # type: ignore[reportMissingModuleSource] -__all__: list[str] = ['History'] +__all__: list[str] = ['HistoryMode', 'get_default_history_mode', 'History'] diff --git a/src/respondpy/model.py b/src/respondpy/model.py index 157c33d..1ead297 100644 --- a/src/respondpy/model.py +++ b/src/respondpy/model.py @@ -4,124 +4,13 @@ # Created Date: 2026-06-05 # # Author: Matthew Carroll # # ----- # -# Last Modified: 2026-06-25 # +# Last Modified: 2026-07-20 # # Modified By: Matthew Carroll # # ----- # # Copyright (c) 2026 Syndemics Lab at Boston Medical Center # ################################################################################ from __future__ import annotations -from .data.input import Input -from .data.logic_conditions import validate_time_list -from .data.parameters import Parameter, ParameterType -from .transition import Transition, build_timestep_transition from ._core.model import Model # pylint: disable=E0611,E0401 # type: ignore[reportMissingModuleSource] -from ._utils import str_to_int_list -__all__: list[str] = [ - 'Model', 'build_model', - 'add_transitions_to_model', 'build_model_transitions' -] - - -def build_model( - input_data: Input, - cohort_id: int = 1, - *, - name: str = "markov", - log_name: str = "console" -) -> Model: - """Build a Model with initialized state and configured transitions. - - Parameters - ---------- - input_data : Input - Loaded input data and simulation configuration. - cohort_id : int, default=1 - Cohort identifier used to resolve sampled parameters. - name : str, default="markov" - Model name passed to the core model constructor. - log_name : str, default="console" - Logger name used by the underlying core model. - - Returns - ------- - Model - A model ready to be added to a simulation. - """ - m = Model(name, log_name) - init_pop = input_data.select_parameter( - Parameter(ParameterType.INITIAL_COHORT), cohort_id, time=1) - m.set_state(init_pop) - m = build_model_transitions(m, input_data, cohort_id) - return m - - -def build_model_transitions( - model: Model, - input_data: Input, - cohort_id: int -) -> Model: - """Populate a model with per-timestep transitions for full duration. - - The first transition block is built from timestep 1. Additional timestep - transition blocks are either copied or rebuilt at configured - ``parameter_change_times`` values. - - Parameters - ---------- - model : Model - Model instance to mutate. - input_data : Input - Loaded input data and simulation configuration. - cohort_id : int - Cohort identifier used to resolve sampled parameters. - - Returns - ------- - Model - The same model instance, with transitions appended. - """ - # Add the first timestep - ct_val = 1 - transition = build_timestep_transition(ct_val, input_data, cohort_id) - add_transitions_to_model(model, transition) - duration = input_data.config.getint('simulation', 'duration') - change_times = validate_time_list( - str_to_int_list(input_data.config.get( - 'simulation', 'parameter_change_times')) - ) - - # we start at 2 because 0 is in the initial state, 1 is the first transition (added above), and now we look for more transitions. If there is no other change times then we just make copies. - for i in range(1, duration): - if change_times and i == change_times[-1]: - ct_val = change_times.pop() - transition = build_timestep_transition( - ct_val, input_data, cohort_id) - add_transitions_to_model(model, transition) - else: - add_transitions_to_model(model, transition.copy()) - return model - - -def add_transitions_to_model( - model: Model, - t_transition: list[Transition] -) -> Model: - """Append one timestep's transitions to a model. - - Parameters - ---------- - model : Model - Model to update. - t_transition : list of Transition - Transition objects for one simulation timestep. - - Returns - ------- - Model - The same model instance, for chaining. - """ - for t in t_transition: - model.add_transition(t) - return model +__all__: list[str] = ['Model'] diff --git a/src/respondpy/simulation.py b/src/respondpy/simulation.py index 383bf73..27f863f 100644 --- a/src/respondpy/simulation.py +++ b/src/respondpy/simulation.py @@ -4,7 +4,7 @@ # Created Date: 2026-06-05 # # Author: Matthew Carroll # # ----- # -# Last Modified: 2026-06-11 # +# Last Modified: 2026-07-20 # # Modified By: Matthew Carroll # # ----- # # Copyright (c) 2026 Syndemics Lab at Boston Medical Center # @@ -13,7 +13,7 @@ from __future__ import annotations from collections.abc import Sequence -from .model import build_model +from .model import Model from .data import Input from ._core.simulation import Simulation # pylint: disable=E0611,E0401 # type: ignore[reportMissingModuleSource] @@ -24,7 +24,8 @@ def build_simulation( input_data: Input, *, cohort_ids: Sequence[int] | None = None, - log_name: str = "console" + log_name: str = "respond", + log_file: str = "respond.log" ) -> Simulation: """Build a simulation containing one model per cohort id. @@ -58,6 +59,15 @@ def build_simulation( ) s = Simulation(log_name) for cohort_id in cohort_ids: - s.add_model(build_model(input_data, cohort_id, log_name=log_name)) + s.create_new_model("markov") + _fill_model(s.get_model(cohort_id), input_data, cohort_id) return s + + +def _fill_model( + model: Model, + input_data: Input, + cohort_id: int, +) -> None: + pass diff --git a/src/respondpy/timestep.py b/src/respondpy/timestep.py new file mode 100644 index 0000000..0f955c1 --- /dev/null +++ b/src/respondpy/timestep.py @@ -0,0 +1,17 @@ +################################################################################ +# File: timestep.py # +# Project: respondpy # +# Created Date: 2026-07-20 # +# Author: Matthew Carroll # +# ----- # +# Last Modified: 2026-07-20 # +# Modified By: Matthew Carroll # +# ----- # +# Copyright (c) 2026 Syndemics Lab at Boston Medical Center # +################################################################################ + +from __future__ import annotations + +from ._core.timestep import Timestep # pylint: disable=E0611,E0401 # type: ignore[reportMissingModuleSource] + +__all__: list[str] = ['Timestep'] diff --git a/src/respondpy/transition.py b/src/respondpy/transition.py index 3bdcb52..15ce326 100644 --- a/src/respondpy/transition.py +++ b/src/respondpy/transition.py @@ -4,7 +4,7 @@ # Created Date: 2026-06-05 # # Author: Matthew Carroll # # ----- # -# Last Modified: 2026-06-09 # +# Last Modified: 2026-07-20 # # Modified By: Matthew Carroll # # ----- # # Copyright (c) 2026 Syndemics Lab at Boston Medical Center # @@ -12,121 +12,6 @@ from __future__ import annotations -import numpy as np - -from .data.parameters import Parameter, ParameterType -from .data.input import Input from ._core.transition import Transition # pylint: disable=E0611,E0401 # type: ignore[reportMissingModuleSource] -__all__: list[str] = [ - 'Transition', 'transition_factory', 'build_timestep_transition' -] - - -def transition_factory( - name: str, - tran_matrices: list[np.ndarray], - *, - log_name: str = "console" -) -> Transition: - """Create a transition and load its ordered transition matrices. - - Parameters - ---------- - name : str - Transition name used by the core model. - tran_matrices : list of numpy.ndarray - Matrix/vector operands consumed in execution order. - log_name : str, default="console" - Logger name used by the underlying core transition. - - Returns - ------- - Transition - A transition ready to be attached to a model. - """ - t = Transition(name, log_name) - for tm in tran_matrices: - t.add_transition_matrix(tm) - return t - - -def build_timestep_transition( - timestep: int, - input_data: Input, - cohort_id: int -) -> list[Transition]: - """Build the full ordered transition set for one timestep. - - The returned transitions are: migration, intervention change, behavior - change, overdose, and background mortality. - - Parameters - ---------- - timestep : int - Simulation timestep to sample. - input_data : Input - Loaded input data and simulation configuration. - cohort_id : int - Cohort identifier used to resolve sampled parameters. - - Returns - ------- - list of Transition - Transition list for exactly one model timestep. - """ - - migration = transition_factory( - "migration", [ - input_data.select_parameter( - Parameter(ParameterType.MIGRATION_COHORT), cohort_id, timestep) - ]) - - inter = transition_factory( - "intervention", [ - input_data.select_parameter( - Parameter(ParameterType.INTERVENTION_TRANSITION_PROBABILITY), - cohort_id, - timestep - ).T] - ) - - behav = transition_factory( - "behavior", [ - input_data.select_parameter( - Parameter(ParameterType.BEHAVIOR_TRANSITION_PROBABILITY), - cohort_id, - timestep - ).T] - ) - - overd = transition_factory( - "overdose", [ - input_data.select_parameter( - Parameter(ParameterType.OVERDOSE_PROBABILITY), - cohort_id, - timestep - ).squeeze(), - input_data.select_parameter( - Parameter(ParameterType.OVERDOSE_FATALITY_PROBABILITY), - cohort_id, - timestep - ).squeeze() - ] - ) - - morta = transition_factory( - "background_death", [ - input_data.select_parameter( - Parameter(ParameterType.BACKGROUND_DEATH_PROBABILITY), - cohort_id, - timestep - ).squeeze() * input_data.select_parameter( - Parameter(ParameterType.STANDARD_MORTALITY_RATIO), - cohort_id, - timestep - ).squeeze() - ] - ) - - return [migration, inter, behav, overd, morta] +__all__: list[str] = ['Transition'] diff --git a/tests/test_smoke.py b/tests/test_smoke.py index 509d911..f9b2567 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -4,7 +4,7 @@ # Created Date: 2026-01-08 # # Author: Matthew Carroll # # ----- # -# Last Modified: 2026-06-25 # +# Last Modified: 2026-07-20 # # Modified By: Matthew Carroll # # ----- # # Copyright (c) 2026 Syndemics Lab at Boston Medical Center # @@ -40,7 +40,7 @@ def test_data_import() -> None: @pytest.mark.smoke def test_model_Nx0() -> None: state = np.array([10.0, 20.0, 30.0]).squeeze() - model = rpy.Model("markov", "console") + model = rpy.Model("markov") model.set_state(state) np.testing.assert_array_equal(state, model.get_state()) @@ -48,7 +48,7 @@ def test_model_Nx0() -> None: @pytest.mark.smoke def test_model_1xN() -> None: state = np.array([[10.0, 20.0, 30.0]]).squeeze() - model = rpy.Model("markov", "console") + model = rpy.Model("markov", "respond") print(state.shape) model.set_state(state) np.testing.assert_array_equal(state, model.get_state()) @@ -57,59 +57,34 @@ def test_model_1xN() -> None: @pytest.mark.smoke def test_model_Nx1() -> None: state = np.array([[10.0], [20.0], [30.0]]) - model = rpy.Model("markov", "console") + model = rpy.Model("markov", "respond", "respond.log") model.set_state(state) np.testing.assert_array_equal(state.squeeze(), model.get_state()) @pytest.mark.smoke def test_one_step() -> None: - state = np.array([[1.3], [1.1], [1.8]]) - migra = np.array([[0.0], [0.0], [0.0]]) - inter = np.array([[0.1, 0.2, 0.5], [0.3, 0.2, 0.3], [0.7, 0.2, 0.3]]) - behav = np.array([[0.3, 0.2, 0.1], [0.4, 0.2, 0.1], [0.3, 0.4, 0.1]]) - overd = np.array([[0.01], [0.01], [0.02]]) - fatal = np.array([[0.01], [0.01], [0.01]]) - backg = np.array([[0.001], [0.001], [0.002]]) - - model = rpy.Model("markov", "console") - model.set_state(state) - - migr = rpy.Transition("migration", "console") - migr.add_transition_matrix(migra) - model.add_transition(migr) - - beha = rpy.Transition("behavior", "console") - beha.add_transition_matrix(behav) - model.add_transition(beha) - - inte = rpy.Transition("intervention", "console") - inte.add_transition_matrix(inter) - model.add_transition(inte) + state = np.array([1.3, 1.1, 1.8]) + migra = np.zeros((3, 1)) - over = rpy.Transition("overdose", "console") - over.add_transition_matrix(overd) - over.add_transition_matrix(fatal) - model.add_transition(over) + model = rpy.Model("markov") + model.set_state(state) - back = rpy.Transition("background_death", "console") - back.add_transition_matrix(backg) - model.add_transition(back) + timestep = rpy.Timestep("console") + timestep.create_transition("migration") + timestep.add_matrix_to_transition("migration", migra) - model.run_transitions() + model.add_timestep(timestep) + model.run_timesteps() - assert model.get_transition_names( - ) == ["migration", "behavior", "intervention", "overdose", "background_death"] - print(model.get_state()) - expected = [0.76715528791564891, 0.72320370216816077, 1.037712429738102] - np.testing.assert_almost_equal(model.get_state(), expected) + np.testing.assert_equal(model.get_state().shape, (3,)) @pytest.mark.smoke def test_simulation_sparse_histories() -> None: - """Verify get_model_sparse_histories returns a name-keyed dict of History objects.""" + """Verify simulation exposes model histories as name-keyed dict of History objects.""" state = np.array([10.0, 20.0, 30.0]) - model = rpy.Model("markov", "console") + model = rpy.Model("markov") model.set_state(state) model.create_default_histories() @@ -117,15 +92,10 @@ def test_simulation_sparse_histories() -> None: sim.add_model(model) sim.run() - sparse = sim.get_model_sparse_histories() - - assert isinstance(sparse, dict), "Expected a dict keyed by model name" - assert "markov" in sparse, "Expected 'markov' model name as key" - - model_histories = sparse["markov"] + model_histories = sim.get_model_history("markov") assert isinstance( - model_histories, dict), "Expected inner dict keyed by history name" + model_histories, dict), "Expected dict keyed by history name" for hist in model_histories.values(): assert isinstance( - hist, rpy.History), "Expected History values in inner dict" + hist, rpy.History), "Expected History values in dict" From 4843d2b6f385bc019a0575ff7695570a2298b773 Mon Sep 17 00:00:00 2001 From: Matthew Carroll <28577806+MJC598@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:19:36 -0400 Subject: [PATCH 2/6] Feature/22 07 26/documentation and testing (#28) * smoke test updates * updating bindings for newest respond push with simulation index operator overload * codeowners file * Update README.md Co-authored-by: Dimitri Baptiste <55843498+ddbaptiste@users.noreply.github.com> Signed-off-by: Matthew Carroll <28577806+MJC598@users.noreply.github.com> --------- Signed-off-by: Matthew Carroll <28577806+MJC598@users.noreply.github.com> Co-authored-by: Dimitri Baptiste <55843498+ddbaptiste@users.noreply.github.com> --- .github/CODEOWNERS | 23 ++ CMakeLists.txt | 2 +- README.md | 19 ++ pyproject.toml | 1 + src/register_history.cpp | 8 +- src/register_simulation.cpp | 84 +++++--- src/respondpy/_core/simulation.pyi | 23 +- src/respondpy/_core/transition.pyi | 6 +- src/respondpy/simulation.py | 12 +- tests/test_integration.py | 4 +- tests/test_model.py | 78 ++----- ...e.py => test_smoke_basic_functionality.py} | 6 +- tests/test_smoke_bindings_runtime.py | 159 ++++++++++++++ tests/test_smoke_stubs_mypy.py | 115 ++++++++++ tests/test_transition.py | 16 +- uv.lock | 198 +++++++++++++++++- 16 files changed, 621 insertions(+), 133 deletions(-) create mode 100644 .github/CODEOWNERS rename tests/{test_smoke.py => test_smoke_basic_functionality.py} (94%) create mode 100644 tests/test_smoke_bindings_runtime.py create mode 100644 tests/test_smoke_stubs_mypy.py diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..131e01e --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,23 @@ +# This file contains the CODEOWNERS of hep-ce. It is used to determine +# required reviews on Pull Requests. Each line is a file pattern followed +# by one or more owners. + +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, +# @MJC598 and @ddbaptiste will be requested for +# review when someone opens a pull request. +* @MJC598 @ddbaptiste + +# Order is important; the last matching pattern takes the most +# precedence. When someone opens a pull request that only +# modifies R or C++ files, only @the specified teams and not the global +# owner(s) will be requested for a review. + +# Teams can be specified as code owners as well. Teams should +# be identified in the format @org/team-name. Teams must have +# explicit write access to the repository. +*.R @SyndemicsLab/Analysts @SyndemicsLab/Developers +*.py @SyndemicsLab/Developers +*.cpp @SyndemicsLab/Developers +*.hpp @SyndemicsLab/Developers +*.sh @SyndemicsLab/Developers \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index ad8ef15..840652b 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 1ec829cedee7e33f3cd505312f1e93e7e87115e9 # main + GIT_TAG 6653edc7aacece2713a98085e85a450b306ac216 # dev OVERRIDE_FIND_PACKAGE ) set(RESPOND_BUILD_DOCS OFF) diff --git a/README.md b/README.md index 493f142..e94bca8 100644 --- a/README.md +++ b/README.md @@ -32,3 +32,22 @@ This results in a wheel and `tar.gz` being placed in a `dist/` directory. From h ## Supported OSes We are currently working on supporting as many OSes as possible. As these are bindings for a C++ project, we are limited in our capacity. For the moment, we are generating many linux builds for python versions >= 3.10. We do not have a Windows or Mac build at the present. + +## Binding Parity Checklist + +Use this checklist when updating bindings, stubs, or API docs. + +- Run smoke tests: `uv run pytest -m smoke` +- [ ] Runtime vs stubs parity: + - [ ] Confirm pybind runtime signatures and return shapes match `.pyi` annotations. + - [ ] Confirm enum members and bound method names match stub names exactly. + - [ ] Confirm overload behavior matches typed expectations. +- [ ] Runtime vs docs parity: + - [ ] Confirm binding docstrings describe actual runtime return types and side effects. + - [ ] Confirm parameter defaults in docs match bound defaults. +- [ ] Expected-failure messaging parity: + - [ ] Confirm incorrect arguments raise errors with stable, informative message patterns. + - [ ] Prefer regex pattern assertions in tests over exact full-message equality. +- [ ] Smoke coverage scope: + - [ ] Runtime binding smoke lives in `tests/test_smoke_bindings_runtime.py`. + - [ ] Stub smoke via `mypy` in pytest lives in `tests/test_smoke_stubs_mypy.py`. diff --git a/pyproject.toml b/pyproject.toml index a3e87e9..21fb170 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ docs = [ # pytest-xdist has no processes on iOS test = [ "cloudpickle", + "mypy>=1.17.1", "pylint>=4.0.5", "pytest-cov>=7.0.0", "pytest>=9.0.2", diff --git a/src/register_history.cpp b/src/register_history.cpp index 55a86b7..c80a41a 100644 --- a/src/register_history.cpp +++ b/src/register_history.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-09 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-16 // +// Last Modified: 2026-07-22 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -22,8 +22,8 @@ using namespace respond; // NOLINTNEXTLINE(misc-use-internal-linkage) void register_history(py::module &m) { py::enum_(m, "HistoryMode") - .value("Snapshot", HistoryMode::kSnapshot) - .value("Accumulated", HistoryMode::kAccumulated) + .value("kSnapshot", HistoryMode::kSnapshot) + .value("kAccumulated", HistoryMode::kAccumulated) .export_values(); m.def("get_default_history_mode", &GetDefaultHistoryMode, py::arg("name"), @@ -84,7 +84,7 @@ void register_history(py::module &m) { .def("get_pending_state", &History::GetPendingState, "Get the pending accumulated state vector, or an empty vector if " "none exists.") - .def("get_patest_recorded_timestep", + .def("get_latest_recorded_timestep", &History::GetLatestRecordedTimestep, "Get the largest recorded timestep, or -1 if history is empty.") .def("get_name", &History::GetName, diff --git a/src/register_simulation.cpp b/src/register_simulation.cpp index 5116ce7..57fee40 100644 --- a/src/register_simulation.cpp +++ b/src/register_simulation.cpp @@ -4,7 +4,7 @@ // Created Date: 2026-02-09 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-07-16 // +// Last Modified: 2026-07-22 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2026 Syndemics Lab at Boston Medical Center // @@ -37,57 +37,73 @@ void register_simulation(py::module &m) { .def("create_new_model", &Simulation::CreateNewModel, py::arg("model_name"), "Create a new model instance and add it to the simulation. " - "Initializes logging for the model and returns a unique_ptr to " - "the created instance. Throws an exception if the model name is " - "unsupported.") + "Initializes logging for the model and returns a cloned model. " + "Throws an exception if the model name is unsupported.") .def("clear_models", &Simulation::ClearModels, "Clear all models from the simulation.") - .def("add_model", &Simulation::AddModel, py::arg("model"), - "Add an existing model instance to the simulation. The simulation " - "takes ownership of the model.") + .def( + "add_model", + [](Simulation &self, const Model &model) { + const auto cloned_model = model.clone(); + self.AddModel(cloned_model); + }, + py::arg("model"), + "Add an existing model instance to the simulation. The simulation " + "takes ownership of the model.") .def("run", &Simulation::Run, py::arg("duration") = -1, "Run the simulation for a specified duration. Executes all " "registered timesteps for each model in sequence.") .def("get_models", &Simulation::GetModels, "Get the list of models in the simulation.") - .def("get_model", - py::overload_cast(&Simulation::GetModel, py::const_), - py::arg("model_index"), - "Get a model instance by its index in the simulation. Throws an " - "exception if the index is out of bounds.") - .def("get_model", - py::overload_cast(&Simulation::GetModel, - py::const_), - py::arg("model_name"), - "Get a model instance by its name. Throws an exception if the " - "model name is not found.") + .def( + "get_model", + [](Simulation &self, size_t model_index) -> Model & { + return self[model_index]; + }, + py::arg("model_index"), py::return_value_policy::reference_internal, + "Get a model instance by its index in the simulation. Throws an " + "exception if the index is out of bounds.") + .def( + "set_model", + [](Simulation &self, size_t model_index, + const Model &replacement_model) { + self[model_index] = replacement_model; + }, + py::arg("model_index"), py::arg("model"), + "Replace a model instance by index with a cloned copy of the " + "provided model. Throws an exception if the index is out of " + "bounds.") + .def( + "__getitem__", + [](Simulation &self, size_t model_index) -> Model & { + return self[model_index]; + }, + py::arg("model_index"), py::return_value_policy::reference_internal, + "Get a model using index access semantics.") + .def( + "__setitem__", + [](Simulation &self, size_t model_index, + const Model &replacement_model) { + self[model_index] = replacement_model; + }, + py::arg("model_index"), py::arg("model"), + "Set a model using index access semantics.") .def("get_model_names", &Simulation::GetModelNames, "Get the list of model names in the simulation.") + .def("get_model_index_name_map", &Simulation::GetModelIndexNameMap, + "Get a mapping from model indices to model names.") .def( "get_model_history", - py::overload_cast(&Simulation::GetModelHistory, - py::const_), + py::overload_cast(&Simulation::GetModelHistory, py::const_), py::arg("idx"), "Get the history of a model by its index in the simulation. Throws " "an exception if the index is out of bounds.") - .def("get_model_history", - py::overload_cast( - &Simulation::GetModelHistory, py::const_), - py::arg("model_name"), - "Get the history of a model by its name. Throws an exception if " - "the model name is not found.") .def("get_model_history_names", - py::overload_cast(&Simulation::GetModelHistoryNames, - py::const_), + py::overload_cast(&Simulation::GetModelHistoryNames, + py::const_), py::arg("idx"), "Get the list of history names for a model by its index in the " "simulation. Throws an exception if the index is out of bounds.") - .def("get_model_history_names", - py::overload_cast( - &Simulation::GetModelHistoryNames, py::const_), - py::arg("model_name"), - "Get the list of history names for a model by its name. Throws an " - "exception if the model name is not found.") .def("set_duration", &Simulation::SetDuration, py::arg("duration"), "Set the duration for which the simulation should run.") .def("__repr__", [](const Simulation &m) { diff --git a/src/respondpy/_core/simulation.pyi b/src/respondpy/_core/simulation.pyi index 4efc41f..7f5c444 100644 --- a/src/respondpy/_core/simulation.pyi +++ b/src/respondpy/_core/simulation.pyi @@ -39,7 +39,7 @@ class Simulation: def __deepcopy__(self, arg0: dict) -> Simulation: ... - def create_new_model(self, model_name: str) -> str: + def create_new_model(self, model_name: str) -> Model: ... def clear_models(self) -> None: @@ -54,31 +54,28 @@ class Simulation: def get_models(self) -> list[Model]: ... - @typing.overload def get_model(self, idx: typing.SupportsInt) -> Model: ... - @typing.overload - def get_model(self, model_name: str) -> Model: + def set_model(self, idx: typing.SupportsInt, model: Model) -> None: ... - def get_model_names(self) -> list[str]: + def __getitem__(self, idx: typing.SupportsInt) -> Model: ... - @typing.overload - def get_model_history(self, idx: typing.SupportsInt) -> typing.Mapping[str, History]: + def __setitem__(self, idx: typing.SupportsInt, model: Model) -> None: ... - @typing.overload - def get_model_history(self, model_name: str) -> typing.Mapping[str, History]: + def get_model_names(self) -> list[str]: ... - @typing.overload - def get_model_history_names(self, idx: typing.SupportsInt) -> list[str]: + def get_model_index_name_map(self) -> dict[int, str]: ... - @typing.overload - def get_model_history_names(self, model_name: str) -> list[str]: + def get_model_history(self, idx: typing.SupportsInt) -> typing.Mapping[str, History]: + ... + + def get_model_history_names(self, idx: typing.SupportsInt) -> list[str]: ... def set_duration(self, duration: typing.SupportsInt) -> None: diff --git a/src/respondpy/_core/transition.pyi b/src/respondpy/_core/transition.pyi index 07ebe70..1d6ac6f 100644 --- a/src/respondpy/_core/transition.pyi +++ b/src/respondpy/_core/transition.pyi @@ -26,7 +26,11 @@ class Transition: ) -> None: ... - def execute(self, state: StateVector, history: typing.Mapping[str, History]) -> StateVector: + def execute( + self, + state: StateVector, + history: typing.Mapping[str, History] | None = None + ) -> tuple[StateVector, dict[str, History]]: ... def add_matrix(self, mat: TransitionMatrix) -> None: diff --git a/src/respondpy/simulation.py b/src/respondpy/simulation.py index 27f863f..b31d28b 100644 --- a/src/respondpy/simulation.py +++ b/src/respondpy/simulation.py @@ -4,7 +4,7 @@ # Created Date: 2026-06-05 # # Author: Matthew Carroll # # ----- # -# Last Modified: 2026-07-20 # +# Last Modified: 2026-07-22 # # Modified By: Matthew Carroll # # ----- # # Copyright (c) 2026 Syndemics Lab at Boston Medical Center # @@ -13,7 +13,6 @@ from __future__ import annotations from collections.abc import Sequence -from .model import Model from .data import Input from ._core.simulation import Simulation # pylint: disable=E0611,E0401 # type: ignore[reportMissingModuleSource] @@ -57,17 +56,16 @@ def build_simulation( raise ValueError( f"Cohort IDs {missing_cohorts} not found in input data." ) - s = Simulation(log_name) + s = Simulation(log_name, log_file) for cohort_id in cohort_ids: - s.create_new_model("markov") - _fill_model(s.get_model(cohort_id), input_data, cohort_id) + _fill_model(s, input_data, cohort_id) return s def _fill_model( - model: Model, + sim: Simulation, input_data: Input, cohort_id: int, ) -> None: - pass + sim.create_new_model("markov") diff --git a/tests/test_integration.py b/tests/test_integration.py index b0c1cc7..e2158e0 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -78,7 +78,7 @@ def test_simulation_run(setup_data): inp = rpy.data.Input(db_path=db_path, conf_path=config_path) sim = rpy.build_simulation(inp) sim.run() - histories = sim.get_model_histories()['markov'] + histories = sim.get_model_history(0) # state, admissions, ODs, FODs, background death assert len(histories) == 5 - assert len(histories['state']) == 2 + assert len(histories['state'].get_state_map()) >= 1 diff --git a/tests/test_model.py b/tests/test_model.py index e8697fc..f26bd3c 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -10,43 +10,16 @@ # Copyright (c) 2026 Syndemics Lab at Boston Medical Center # ################################################################################ -from __future__ import annotations - - import sqlite3 from configparser import ConfigParser +import numpy as np + import pytest import respondpy as rpy -class DummyTransition: - def __init__(self, label: str) -> None: - self.label = label - - def copy(self) -> DummyTransition: - return DummyTransition(self.label) - - -class DummyModel: - def __init__(self) -> None: - self.transitions: list[DummyTransition] = [] - - def add_transition(self, transition: DummyTransition) -> None: - self.transitions.append(transition) - - -class DummyInput: - def __init__(self, duration: int, parameter_change_times: str) -> None: - cfg = ConfigParser() - cfg["simulation"] = { - "duration": str(duration), - "parameter_change_times": parameter_change_times, - } - self.config = cfg - - @pytest.fixture def setup_db(tmp_path_factory, db_schema, insert_complete_sample): """Fixture to execute before all tests to setup_db the dummy database @@ -106,45 +79,28 @@ def setup_data(setup_db, setup_config): def test_build_model(setup_data) -> None: db, cfg = setup_data inp = rpy.data.Input(db_path=db, conf_path=cfg) - m = rpy.build_model(inp, 1) + sim = rpy.build_simulation(inp, cohort_ids=[1]) + m = sim.get_model(0) assert isinstance(m, rpy.Model) @pytest.mark.unit -def test_build_model_transitions_rebuilds_on_change_times( - monkeypatch: pytest.MonkeyPatch, -) -> None: - calls: list[int] = [] - - def fake_build_timestep_transition( - timestep: int, - _input_data: DummyInput, - _cohort_id: int, - ) -> list[DummyTransition]: - calls.append(timestep) - return [DummyTransition(f"t{timestep}")] - - monkeypatch.setattr(rpy.model, "build_timestep_transition", - fake_build_timestep_transition) - - model = DummyModel() - data = DummyInput(duration=4, parameter_change_times="1,3") - - out = rpy.model.build_model_transitions(model, data, cohort_id=1) +def test_model_default_histories_can_be_created() -> None: + model = rpy.Model("markov") + model.set_state(np.array([1.0, 2.0, 3.0])) + model.create_default_histories() - assert out is model - # Initial build at timestep 1, then rebuild when i == 3. - assert calls == [1, 3] - # One transition for each timestep in duration. - assert len(model.transitions) == 4 + histories = model.get_histories() + assert isinstance(histories, dict) + assert "state" in histories + assert isinstance(histories["state"], rpy.History) @pytest.mark.unit -def test_add_transitions_to_model_returns_same_instance() -> None: - model = DummyModel() - transitions = [DummyTransition("a"), DummyTransition("b")] +def test_model_set_and_get_state_roundtrip() -> None: + model = rpy.Model("markov") + expected = np.array([3.0, 2.0, 1.0]) - out = rpy.model.add_transitions_to_model(model, transitions) + model.set_state(expected) - assert out is model - assert [t.label for t in model.transitions] == ["a", "b"] + np.testing.assert_array_equal(model.get_state(), expected) diff --git a/tests/test_smoke.py b/tests/test_smoke_basic_functionality.py similarity index 94% rename from tests/test_smoke.py rename to tests/test_smoke_basic_functionality.py index f9b2567..29b6f60 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke_basic_functionality.py @@ -1,10 +1,10 @@ ################################################################################ -# File: test_smoke.py # +# File: test_smoke_basic_functionality.py # # Project: respondpy # # Created Date: 2026-01-08 # # Author: Matthew Carroll # # ----- # -# Last Modified: 2026-07-20 # +# Last Modified: 2026-07-22 # # Modified By: Matthew Carroll # # ----- # # Copyright (c) 2026 Syndemics Lab at Boston Medical Center # @@ -92,7 +92,7 @@ def test_simulation_sparse_histories() -> None: sim.add_model(model) sim.run() - model_histories = sim.get_model_history("markov") + model_histories = sim.get_model_history(0) assert isinstance( model_histories, dict), "Expected dict keyed by history name" diff --git a/tests/test_smoke_bindings_runtime.py b/tests/test_smoke_bindings_runtime.py new file mode 100644 index 0000000..2bc027c --- /dev/null +++ b/tests/test_smoke_bindings_runtime.py @@ -0,0 +1,159 @@ +################################################################################ +# File: test_smoke_bindings_runtime.py # +# Project: respondpy # +# Created Date: 2026-07-22 # +# Author: Matthew Carroll # +# ----- # +# Last Modified: 2026-07-22 # +# Modified By: Matthew Carroll # +# ----- # +# Copyright (c) 2026 Syndemics Lab at Boston Medical Center # +################################################################################ + +"""Smoke tests validating pybind runtime contracts for core bindings.""" + +from __future__ import annotations + +import numpy as np +import pytest + +import respondpy as rpy +from respondpy.history import HistoryMode + + +@pytest.mark.smoke +def test_transition_execute_returns_state_and_history_tuple() -> None: + """Transition.execute should return both the updated state and history map.""" + transition = rpy.Transition("migration") + transition.add_matrix(np.zeros((3, 1))) + input_state = np.array([1.0, 2.0, 3.0]) + + result = transition.execute(input_state, {}) + + assert isinstance(result, tuple), ( + "Expected Transition.execute to return a tuple of " + "(StateVector, history_map)." + ) + assert len(result) == 2, ( + "Expected Transition.execute tuple to have exactly 2 elements: " + "state and history map." + ) + + output_state, output_history = result + np.testing.assert_equal( + output_state.shape, + input_state.shape, + err_msg="Expected output state shape to match input state shape.", + ) + assert isinstance(output_history, dict), ( + "Expected Transition.execute second return value to be a dict-like " + "history mapping." + ) + + +@pytest.mark.smoke +def test_history_mode_members_and_latest_timestep_method_are_exposed() -> None: + """History bindings should expose enum members and latest timestep accessor.""" + assert hasattr(HistoryMode, "kSnapshot"), ( + "Expected HistoryMode to expose enum member 'kSnapshot'." + ) + assert hasattr(HistoryMode, "kAccumulated"), ( + "Expected HistoryMode to expose enum member 'kAccumulated'." + ) + + history = rpy.History("state") + assert hasattr(history, "get_latest_recorded_timestep"), ( + "Expected History to expose method 'get_latest_recorded_timestep'." + ) + + +@pytest.mark.smoke +def test_simulation_create_new_model_returns_model_and_registers_model() -> None: + """Simulation.create_new_model should return a cloned Model instance.""" + simulation = rpy.Simulation() + created_model = simulation.create_new_model("markov") + + assert isinstance(created_model, rpy.Model), ( + "Expected Simulation.create_new_model to return a Model instance." + ) + assert created_model.get_name() == "markov", ( + "Expected returned model to use the requested model type name." + ) + assert simulation.get_model_names() == ["markov"], ( + "Expected simulation to register one canonical model name: 'markov'." + ) + assert isinstance(simulation.get_model(0), rpy.Model), ( + "Expected get_model(0) to return a Model instance after creation." + ) + + +@pytest.mark.smoke +def test_simulation_get_model_returns_live_mutable_model_reference() -> None: + """Mutating a model from get_model should update the simulation-owned model.""" + simulation = rpy.Simulation() + simulation.create_new_model("markov") + + expected_state = np.array([3.0, 2.0, 1.0]) + model = simulation.get_model(0) + model.set_state(expected_state) + + np.testing.assert_array_equal( + simulation.get_model(0).get_state(), + expected_state, + err_msg="Expected get_model to expose a live simulation-owned model.", + ) + + +@pytest.mark.smoke +def test_simulation_set_model_replaces_model_by_index() -> None: + """set_model(index, model) should replace the stored model state.""" + simulation = rpy.Simulation() + simulation.create_new_model("markov") + + replacement = rpy.Model("markov") + replacement_state = np.array([7.0, 8.0, 9.0]) + replacement.set_state(replacement_state) + + simulation.set_model(0, replacement) + + np.testing.assert_array_equal( + simulation.get_model(0).get_state(), + replacement_state, + err_msg="Expected set_model to replace the simulation model at index.", + ) + + +@pytest.mark.smoke +def test_simulation_index_setitem_replaces_model_by_index() -> None: + """Simulation[index] assignment should replace the stored model.""" + simulation = rpy.Simulation() + simulation.create_new_model("markov") + + replacement = rpy.Model("markov") + replacement_state = np.array([4.0, 5.0, 6.0]) + replacement.set_state(replacement_state) + + simulation[0] = replacement + + np.testing.assert_array_equal( + simulation[0].get_state(), + replacement_state, + err_msg="Expected __setitem__ to replace the simulation model at index.", + ) + + +@pytest.mark.smoke +def test_binding_failure_messages_follow_expected_patterns() -> None: + """Binding exceptions should surface informative message patterns.""" + simulation = rpy.Simulation() + simulation.create_new_model("markov") + + with pytest.raises(Exception, match=r"(?i)(out of bounds|index)"): + simulation.get_model(999) + + timestep = rpy.Timestep() + with pytest.raises(Exception, match=r"(?i)(not found|transition)"): + timestep.get_transition("missing") + + with pytest.raises(TypeError, match=r"(?i)incompatible constructor arguments"): + _ = rpy.Simulation(1) # type: ignore[arg-type] diff --git a/tests/test_smoke_stubs_mypy.py b/tests/test_smoke_stubs_mypy.py new file mode 100644 index 0000000..9bfd92f --- /dev/null +++ b/tests/test_smoke_stubs_mypy.py @@ -0,0 +1,115 @@ +################################################################################ +# File: test_smoke_stubs_mypy.py # +# Project: respondpy # +# Created Date: 2026-07-22 # +# Author: Matthew Carroll # +# ----- # +# Last Modified: 2026-07-22 # +# Modified By: Matthew Carroll # +# ----- # +# Copyright (c) 2026 Syndemics Lab at Boston Medical Center # +################################################################################ + +"""Smoke tests that validate stub behavior via mypy through uv.""" + +from __future__ import annotations + +from pathlib import Path +import subprocess + +import pytest + + +def _run_mypy(path: Path) -> subprocess.CompletedProcess[str]: + """Run mypy through uv for a single smoke snippet file.""" + repo_root = Path(__file__).resolve().parent.parent + return subprocess.run( + [ + "uv", + "run", + "mypy", + "--no-color-output", + "--hide-error-context", + str(path), + ], + cwd=repo_root, + capture_output=True, + text=True, + check=False, + ) + + +@pytest.mark.smoke +def test_mypy_smoke_positive_stub_usage(tmp_path: Path) -> None: + """Typed usage matching stubs should pass mypy.""" + test_file = tmp_path / "stub_smoke_positive.py" + test_file.write_text( + """from __future__ import annotations + +import numpy as np +import respondpy as rpy +from respondpy.history import HistoryMode + +state = np.array([1.0, 2.0, 3.0], dtype=float) +transition = rpy.Transition("migration") +transition.add_matrix(np.zeros((3, 1))) +next_state, history_map = transition.execute(state, {}) + +created_model: rpy.Model = rpy.Simulation().create_new_model("markov") +mode: HistoryMode = HistoryMode.kSnapshot +latest_timestep: int = int(rpy.History("state").get_latest_recorded_timestep()) + +assert isinstance(created_model, rpy.Model) +assert latest_timestep >= -1 +assert next_state.shape == state.shape +assert isinstance(history_map, dict) +assert mode.name == "kSnapshot" +""", + encoding="utf-8", + ) + + result = _run_mypy(test_file) + assert result.returncode == 0, ( + "Expected mypy smoke positive snippet to pass.\n" + f"stdout:\n{result.stdout}\n" + f"stderr:\n{result.stderr}" + ) + + +@pytest.mark.smoke +def test_mypy_smoke_negative_stub_misuse(tmp_path: Path) -> None: + """Typed misuse should fail mypy with informative message patterns.""" + test_file = tmp_path / "stub_smoke_negative.py" + test_file.write_text( + """from __future__ import annotations + +import numpy as np +import respondpy as rpy + +bad_model: int = rpy.Simulation().create_new_model("markov") +state = np.array([1.0, 2.0, 3.0], dtype=float) +transition = rpy.Transition("migration") +transition.add_matrix(np.zeros((3, 1))) +_ = transition.execute(state, 123) +_ = rpy.History("state").get_latest_recorded_timestep("oops") +""", + encoding="utf-8", + ) + + result = _run_mypy(test_file) + combined = f"{result.stdout}\n{result.stderr}" + + assert result.returncode != 0, ( + "Expected mypy smoke negative snippet to fail, but it passed.\n" + f"stdout:\n{result.stdout}\n" + f"stderr:\n{result.stderr}" + ) + assert "Incompatible types in assignment" in combined, ( + "Expected mypy to report assignment type mismatch for create_new_model." + ) + assert "execute" in combined and "incompatible type" in combined.lower(), ( + "Expected mypy to report incompatible type for Transition.execute history argument." + ) + assert "Too many arguments" in combined and "get_latest_recorded_timestep" in combined, ( + "Expected mypy to report incorrect argument count for latest timestep accessor." + ) diff --git a/tests/test_transition.py b/tests/test_transition.py index edbb261..d9cbcd2 100644 --- a/tests/test_transition.py +++ b/tests/test_transition.py @@ -13,6 +13,8 @@ import sqlite3 from configparser import ConfigParser +import numpy as np + import pytest import respondpy as rpy @@ -75,8 +77,12 @@ def setup_data(setup_db, setup_config): @pytest.mark.unit def test_build_timestep_transition(setup_data) -> None: - db, cfg = setup_data - inp = rpy.data.Input(db_path=db, conf_path=cfg) - transitions = rpy.build_timestep_transition(1, inp, 1) - assert isinstance(transitions, list) - assert all(isinstance(t, rpy.Transition) for t in transitions) + transition = rpy.Transition("migration") + matrix = np.zeros((3, 1)) + + transition.add_matrix(matrix) + + matrices = transition.get_matrices() + assert isinstance(matrices, list) + assert len(matrices) == 1 + np.testing.assert_array_equal(matrices[0], matrix) diff --git a/uv.lock b/uv.lock index f1bd214..d06f42d 100644 --- a/uv.lock +++ b/uv.lock @@ -2,7 +2,8 @@ version = 1 revision = 3 requires-python = ">=3.11" resolution-markers = [ - "python_full_version >= '3.12'", + "python_full_version >= '3.15'", + "python_full_version >= '3.12' and python_full_version < '3.15'", "python_full_version < '3.12'", ] @@ -36,6 +37,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, ] +[[package]] +name = "ast-serialize" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/ad/0d70a3a2d6e01968d985415259e8ec7ad3f777903f9b1c1f3c8c44642c60/ast_serialize-0.6.0.tar.gz", hash = "sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe", size = 61489, upload-time = "2026-06-30T20:02:55.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/12/3e5f575f156555547c250a8b0d1347517a3a20fc7f4492e9703a69d4f45e/ast_serialize-0.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264", size = 1177640, upload-time = "2026-06-30T20:02:06.708Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a4/921a9e27951627983b0f368859ea00f8330a551dc0bf4c2fdcb11855a98b/ast_serialize-0.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc", size = 1168111, upload-time = "2026-06-30T20:02:08.366Z" }, + { url = "https://files.pythonhosted.org/packages/00/69/950cf404de7b8782cf95e5c1237e25e2aa46177b287f39f9eeddf481fd6f/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516", size = 1227656, upload-time = "2026-06-30T20:02:09.843Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a8/46f8f6a6479d9d2273980957bb091a506c55f5b95d3c029ee58518a78407/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089", size = 1227706, upload-time = "2026-06-30T20:02:11.367Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/9ac415bda0a40e49eab8fea3b2741c19c98bb84d57d62c4cfc6230eb67be/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c", size = 1431705, upload-time = "2026-06-30T20:02:12.737Z" }, + { url = "https://files.pythonhosted.org/packages/e5/06/8807115d441444879f7561b5eede5ac18fc80392f11826d61ccf31f503b1/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8", size = 1249533, upload-time = "2026-06-30T20:02:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c0/c2ba82ef9618650357d9421a1fdb27ffec862a7f57e8e2de82a3ccd11e12/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f", size = 1252619, upload-time = "2026-06-30T20:02:16.219Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a7/fa31d52dd4102cede29fb9634e98d214129b2783b4f95528c6dc6a8f6587/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66", size = 1242983, upload-time = "2026-06-30T20:02:17.813Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/ddf742b5ad3c4bafd3466f2265037cfd99bc1b9a5ee46a5d58c90d523242/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b", size = 1296148, upload-time = "2026-06-30T20:02:19.146Z" }, + { url = "https://files.pythonhosted.org/packages/24/cb/9f6f217cce8b3b632c5568b478d195a35e79dce4dbe309438cb89ba6ea4f/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a", size = 1403826, upload-time = "2026-06-30T20:02:20.696Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f8/9d16d4f0107a183924425cc0e7618d8bf76f96b45afa9ff19f924ed1ad57/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554", size = 1502943, upload-time = "2026-06-30T20:02:22.034Z" }, + { url = "https://files.pythonhosted.org/packages/80/dd/bbc1c38756350dddf7e24acae1c9482ef42051c267417e019aecc1ed4075/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f", size = 1497632, upload-time = "2026-06-30T20:02:23.517Z" }, + { url = "https://files.pythonhosted.org/packages/42/7e/9daffefcf5b97e6bb4c3e0b3c024c1aee9722f23d3cf7cd2ff80d6fb4a40/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec", size = 1448858, upload-time = "2026-06-30T20:02:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1f/f9baaab81a677ea0af7d2458cac2f94ebcc85958f8a3c15ba9d9e5dab653/ast_serialize-0.6.0-cp314-cp314t-win32.whl", hash = "sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a", size = 1052600, upload-time = "2026-06-30T20:02:26.263Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1f/41b535866519512d8cf6669cb2cff7823b7672bb6279c0333b4ff89d7d9f/ast_serialize-0.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24", size = 1095570, upload-time = "2026-06-30T20:02:27.639Z" }, + { url = "https://files.pythonhosted.org/packages/50/64/e472fe3e3a2d33d874b987e8518aedf24562919e3b6161a4fa1797e89c0f/ast_serialize-0.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14", size = 1067267, upload-time = "2026-06-30T20:02:28.949Z" }, + { url = "https://files.pythonhosted.org/packages/52/19/ac8348ae8711c9b5ae834634f635780cab62a0f5e6f988882e048b89c2ae/ast_serialize-0.6.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596", size = 1185367, upload-time = "2026-06-30T20:02:30.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f6/ec7ec652c51db77c2f61d8573338e13e4704303265ccc658cb4031d9f354/ast_serialize-0.6.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad", size = 1178657, upload-time = "2026-06-30T20:02:31.964Z" }, + { url = "https://files.pythonhosted.org/packages/6f/02/613a7534a41d0122f37d1e0c64aa8ac78bfb831f8c92f6db057a311abb3c/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a", size = 1238620, upload-time = "2026-06-30T20:02:33.664Z" }, + { url = "https://files.pythonhosted.org/packages/4d/21/087957bba486242afc52f49b2d9e21c9dad00289356cf9efe67084015a9d/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081", size = 1236075, upload-time = "2026-06-30T20:02:34.936Z" }, + { url = "https://files.pythonhosted.org/packages/82/04/78128bbb170071c2c72a210a181f1c00e11cc1cec60a8beef747b07f9201/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77", size = 1441348, upload-time = "2026-06-30T20:02:36.245Z" }, + { url = "https://files.pythonhosted.org/packages/64/64/62fb99d6faf199b4c3e5b08a07136e9a0d7664bb249c6de3670e5b63e9b6/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790", size = 1258580, upload-time = "2026-06-30T20:02:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/ca/87/b4d6c38e0ccd5e85dc54cecdf933a152c60b28fe5d993a6d8a72fa6d5896/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7", size = 1261693, upload-time = "2026-06-30T20:02:39.123Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/3676ca2191f39bafb75f93f99b2f429ec464586158fece2165f3572805dc/ast_serialize-0.6.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b", size = 1252517, upload-time = "2026-06-30T20:02:40.511Z" }, + { url = "https://files.pythonhosted.org/packages/f3/58/494ef8c4b4acb2f4a265ac934caf45f792a08fe27d6b853de35ad991941a/ast_serialize-0.6.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b", size = 1304843, upload-time = "2026-06-30T20:02:41.961Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f2/13736d920ab3d49bbee80ef1a277dd7b7aaf3b3545efd9d2a8114fe05525/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32", size = 1413698, upload-time = "2026-06-30T20:02:44.179Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5a/e046f3899e2acba4677d7427b76431443a1aa1a0e583dfb05b55b69d55cf/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b", size = 1512209, upload-time = "2026-06-30T20:02:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c7/e42aaca7bb2d22a7c06d5a8c7930086c5a334e93d716e6fa5e6647a4515f/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a", size = 1508464, upload-time = "2026-06-30T20:02:46.942Z" }, + { url = "https://files.pythonhosted.org/packages/95/93/5524a3dc6c3f593de3228ed9cbef73afa047625b7000ec21b7f58e6eb4d4/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e", size = 1457164, upload-time = "2026-06-30T20:02:48.294Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c0/36a6ffb4d653cf621427b4c4928671f53ad800c453474de2b82564a44ad9/ast_serialize-0.6.0-cp39-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b", size = 863014, upload-time = "2026-06-30T20:02:49.742Z" }, + { url = "https://files.pythonhosted.org/packages/09/c7/7d5ad8b49e1278e1c2a1e0274bd7850560b3f09313aa00c13bc8d5544792/ast_serialize-0.6.0-cp39-abi3-win32.whl", hash = "sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1", size = 1063165, upload-time = "2026-06-30T20:02:50.98Z" }, + { url = "https://files.pythonhosted.org/packages/47/ae/6710c14ecb276031cf10249f6adf5a59e2d3fdb3b5183bd59f70524067ee/ast_serialize-0.6.0-cp39-abi3-win_amd64.whl", hash = "sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e", size = 1101444, upload-time = "2026-06-30T20:02:52.554Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/c53deb2cd0c9b0fb636d24d9f40924cf2e65028e6b20b10cd5c1eeb2c730/ast_serialize-0.6.0-cp39-abi3-win_arm64.whl", hash = "sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e", size = 1072965, upload-time = "2026-06-30T20:02:54.097Z" }, +] + [[package]] name = "astroid" version = "4.0.4" @@ -730,6 +772,81 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, ] +[[package]] +name = "librt" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/2f/3908645ddddab7120b46295e541ead308109fa48dbec7d67d7a778870d60/librt-0.13.0.tar.gz", hash = "sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781", size = 211402, upload-time = "2026-07-08T12:26:29.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/25/a6498964cfeec270c468cffdc118f69c29b412593610d55fa1327ca51ff4/librt-0.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082", size = 148029, upload-time = "2026-07-08T12:24:45.961Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/dc86d1bffd8e0c2818bace29d9f7783cfbb8e0673bf3673b5bbd5bbe0420/librt-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14", size = 153036, upload-time = "2026-07-08T12:24:47.257Z" }, + { url = "https://files.pythonhosted.org/packages/29/3f/b923826660f02f286186cd9303d52bb05ced0a13708edc104dc8480920e3/librt-0.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79", size = 493062, upload-time = "2026-07-08T12:24:48.483Z" }, + { url = "https://files.pythonhosted.org/packages/88/87/6c0980a9c9b1302cb68d108906697b89eceb55889bb1dcf77c109aa56ca5/librt-0.13.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176", size = 485510, upload-time = "2026-07-08T12:24:49.727Z" }, + { url = "https://files.pythonhosted.org/packages/32/81/795ae3b9df5dd94079fb807e38191855e023e8c6249014ae6bc3f0d9a490/librt-0.13.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89", size = 515909, upload-time = "2026-07-08T12:24:51.135Z" }, + { url = "https://files.pythonhosted.org/packages/20/e5/182de15abce8907108a6fdb41487de65beb5099b74dc5841b19b099168db/librt-0.13.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f", size = 508620, upload-time = "2026-07-08T12:24:52.358Z" }, + { url = "https://files.pythonhosted.org/packages/32/03/33978d32db76e1f66377e8f78e42a2ca3c162143331677d1f50bbad36cfb/librt-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d", size = 530363, upload-time = "2026-07-08T12:24:53.503Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f5/b291fbd2d00f7d8287bcbf67b5aa0c6afed4bc26cef23e079629c47a2c04/librt-0.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd", size = 534209, upload-time = "2026-07-08T12:24:55.138Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/6f41f17939d191bc21609f220da8509316bc62797f078545fe83be522e78/librt-0.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588", size = 514254, upload-time = "2026-07-08T12:24:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/af/c2/2e4befa5410a7443019c14abccc94ff619797171f6b72013635fb87f31d7/librt-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1", size = 557611, upload-time = "2026-07-08T12:24:57.561Z" }, + { url = "https://files.pythonhosted.org/packages/ab/54/8b69f81448417adbc040a2185f4e2eece1e1994b7dcfaeed4662b30f98a5/librt-0.13.0-cp311-cp311-win32.whl", hash = "sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21", size = 104906, upload-time = "2026-07-08T12:24:58.806Z" }, + { url = "https://files.pythonhosted.org/packages/76/5a/f4aaf37b50f2fde12c8c663b83fdd499cdc24f957f19543d7414bfcc9e25/librt-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b", size = 125852, upload-time = "2026-07-08T12:25:00.065Z" }, + { url = "https://files.pythonhosted.org/packages/f2/99/bf1820e6feeabc2f218c24450ec0c995d6a91e8ba0fd3caf042c9e8adb2a/librt-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c", size = 111832, upload-time = "2026-07-08T12:25:01.148Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/b2933ddae222dac338476abb872641169a5cfed2c2bb5444a5b07b32b0c3/librt-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0", size = 150990, upload-time = "2026-07-08T12:25:02.42Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/db98f744ca50e6efc9c95c70ee49b77aefac31f6a3fc7c83754a42d6a74f/librt-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5", size = 155238, upload-time = "2026-07-08T12:25:03.681Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/a197e7bc72baf2c61ce7fdc6906a5054dc05bd8da0819aa894e4857bf87e/librt-0.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9", size = 503073, upload-time = "2026-07-08T12:25:05.049Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e7/7887712e27da7c1ab80fcabb1de6eb24243964f6557cae530d4b70706dbd/librt-0.13.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b", size = 496528, upload-time = "2026-07-08T12:25:06.26Z" }, + { url = "https://files.pythonhosted.org/packages/94/f0/f2283385bb6b950b26a1410f4ce51ec27231e0b3a4b925c46366d218b198/librt-0.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03", size = 531786, upload-time = "2026-07-08T12:25:07.658Z" }, + { url = "https://files.pythonhosted.org/packages/36/11/69ac3b54766ffba5fd7e5acebfb048d66dbe1f9f2d14516c2b3edc59cf87/librt-0.13.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e", size = 524393, upload-time = "2026-07-08T12:25:09.121Z" }, + { url = "https://files.pythonhosted.org/packages/61/5f/d72f95fd444a926a3c14b4e24979474116988dd57a45be242077c45d3c22/librt-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd", size = 543026, upload-time = "2026-07-08T12:25:10.459Z" }, + { url = "https://files.pythonhosted.org/packages/c4/08/dcd9993ad192737a004ba263d549f8ea605b326b952e7d6205c7d4170b76/librt-0.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348", size = 546829, upload-time = "2026-07-08T12:25:11.716Z" }, + { url = "https://files.pythonhosted.org/packages/96/d5/6d9bb2f54e4109a956b7128836529653eb9d740f784bc47ed10a02c1000e/librt-0.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7", size = 535700, upload-time = "2026-07-08T12:25:13.144Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f2/10946922503858a359492fa27f13e86228bde702116a740ac7b3cd185f24/librt-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82", size = 573566, upload-time = "2026-07-08T12:25:14.336Z" }, + { url = "https://files.pythonhosted.org/packages/48/a8/94f00e3c99479a18088af3685ea016c42f3c7d5d1964d8dbb40c08d7f1aa/librt-0.13.0-cp312-cp312-win32.whl", hash = "sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3", size = 106099, upload-time = "2026-07-08T12:25:16.159Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7b/2da9c74c1ed25a89cc4e1c8e007ea2eb4a0f1fafa3e70d757fe3242c5c5c/librt-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa", size = 126934, upload-time = "2026-07-08T12:25:17.275Z" }, + { url = "https://files.pythonhosted.org/packages/d0/65/aead61bbf3b5358593f9d4779d2a0e88eaf6ec191a6342dde36dd1df6371/librt-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1", size = 112236, upload-time = "2026-07-08T12:25:18.425Z" }, + { url = "https://files.pythonhosted.org/packages/67/3b/18e7b63255297a2bdc9c25c8d6d4ca8eca9f63aceb1252c0f7427ac7099e/librt-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3", size = 151027, upload-time = "2026-07-08T12:25:19.638Z" }, + { url = "https://files.pythonhosted.org/packages/4d/68/e2248452c00d1a03b45fee1752cdc8f790a476efd2402b75181da88a9e61/librt-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c", size = 155152, upload-time = "2026-07-08T12:25:20.851Z" }, + { url = "https://files.pythonhosted.org/packages/0e/16/52b1c99bf19057a062aac39c900cbb81499f6f75d6c537c14463d247ba78/librt-0.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c", size = 502499, upload-time = "2026-07-08T12:25:22.055Z" }, + { url = "https://files.pythonhosted.org/packages/9f/54/b811151805c795f55e0dedee6ec687b75f9982a8105d240ea3910737a77b/librt-0.13.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b", size = 496108, upload-time = "2026-07-08T12:25:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f8/094d6b2bd93f3fdaa54db54cc788c4a365333bddad65ab02e04da0b1d004/librt-0.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9", size = 531576, upload-time = "2026-07-08T12:25:24.648Z" }, + { url = "https://files.pythonhosted.org/packages/2e/40/541733d5755824f968f7ec39d78ffbd75d145964157ae5e69a09ec6d7326/librt-0.13.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db", size = 524390, upload-time = "2026-07-08T12:25:25.898Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b5/255673cfdbf5ba663339d36cd863c897289ab4337577e19f9405ce059f36/librt-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6", size = 543053, upload-time = "2026-07-08T12:25:27.436Z" }, + { url = "https://files.pythonhosted.org/packages/9e/11/ab5005e9c9850710f21e354201bf090646349d3fabf5f951eaf70235729e/librt-0.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7", size = 546387, upload-time = "2026-07-08T12:25:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/a2/04/a5d7ce1d1df1afd15ca283dcdf7530ac073e12d69ae8c40879dda96f7868/librt-0.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1", size = 535970, upload-time = "2026-07-08T12:25:30.171Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/927e267a6daa290174ac281b23c9804c8829b042ade9c6f24a065f540958/librt-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a", size = 573582, upload-time = "2026-07-08T12:25:31.507Z" }, + { url = "https://files.pythonhosted.org/packages/10/24/b6c5213efe39c19f9e13605644d0cf063b4ddaa33ac2e45b088e23a70e2e/librt-0.13.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628", size = 82189, upload-time = "2026-07-08T12:25:32.675Z" }, + { url = "https://files.pythonhosted.org/packages/4c/00/d29736be177a906ac0b84a5b04b4fbfa22c776dc2f366de4172b0f968c08/librt-0.13.0-cp313-cp313-win32.whl", hash = "sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927", size = 106193, upload-time = "2026-07-08T12:25:33.692Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ac/aff6fb45393cb8912f39dfb156ef6b2d1cadb207ff465fc8f66141054be8/librt-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650", size = 126962, upload-time = "2026-07-08T12:25:34.769Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3a/d68cb2b334d53fd30fac81d3a489ce4ba0d9506f4df43fcf676b68352b19/librt-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566", size = 112127, upload-time = "2026-07-08T12:25:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/7b/66/f49ae0d592bd45b6941e9a8bafcb6a87cddcd501ee7874707e767f01b585/librt-0.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71", size = 149818, upload-time = "2026-07-08T12:25:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/3d/50/51c76d74014d04fb95b6506d286808984b78a2f7a41039094e6b2194ac48/librt-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6", size = 154071, upload-time = "2026-07-08T12:25:39.399Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fe/f19b0f5f82d5a1f2da736586bc840abd00ce07d6388136ae80b7333883fc/librt-0.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f", size = 494168, upload-time = "2026-07-08T12:25:40.641Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/b8550c75775127fd31a5f20e8775997f7b527ad661fc8ddccd7497c064f7/librt-0.13.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180", size = 491054, upload-time = "2026-07-08T12:25:41.905Z" }, + { url = "https://files.pythonhosted.org/packages/30/14/4d0204867623df3f33f86efd3d3692ba5e01321443f4d6eab35a22697618/librt-0.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6", size = 523006, upload-time = "2026-07-08T12:25:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/19/0a/c45fc9a260934696bace1ac5df1e148ac92bd71767aee3bf7cd7a4534f4c/librt-0.13.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9", size = 515058, upload-time = "2026-07-08T12:25:44.541Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/50c5ce45b326854ef8fa6ae4c36cf5142e5c55315eaf9e51d0ae73ac4da3/librt-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007", size = 534025, upload-time = "2026-07-08T12:25:45.825Z" }, + { url = "https://files.pythonhosted.org/packages/89/2d/08c413c8f93fc13b8103624fce38e5caa86cd08cbbc8465870ab287af54b/librt-0.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0", size = 540557, upload-time = "2026-07-08T12:25:47.059Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/93af71fb4a364952210051811dd4e40174e79656b050c89cacac18af3330/librt-0.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1", size = 523201, upload-time = "2026-07-08T12:25:48.392Z" }, + { url = "https://files.pythonhosted.org/packages/c1/6e/9766f07b676a4889d9f8bc2864e9ba5fff165653143ef4dda7df6aa34d16/librt-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d", size = 565740, upload-time = "2026-07-08T12:25:49.678Z" }, + { url = "https://files.pythonhosted.org/packages/a2/1e/664e3472ce2b6e10e9b83f29d4a36eb982ff6b5a169ae7567bba3a4c4ff5/librt-0.13.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16", size = 81611, upload-time = "2026-07-08T12:25:50.857Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d4/8582a4d65e2234673685e07309d02c230b28a85724eb0acbf13f019b7f6e/librt-0.13.0-cp314-cp314-win32.whl", hash = "sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37", size = 100106, upload-time = "2026-07-08T12:25:52.03Z" }, + { url = "https://files.pythonhosted.org/packages/63/ce/0cb99efe6086b46cd985dc26672166fae312a239690e75871f7fafbd3fc5/librt-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39", size = 121209, upload-time = "2026-07-08T12:25:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/26/85/4f3ccb083a3c9b0d42e223acdb3c3f507953324a59cdcab4826e8e2e3b89/librt-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6", size = 106404, upload-time = "2026-07-08T12:25:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/b2/77/333191499538c8e8189de7a4cba8e6f49ee949fd6d6e6324b21fd1522466/librt-0.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5", size = 159231, upload-time = "2026-07-08T12:25:55.432Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9e/2aa83758f22c278b837a1d8025898434ce2b8bff36678d5330ecaef56dff/librt-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46", size = 161300, upload-time = "2026-07-08T12:25:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c0/86791e936553ca763d6b3c2fb4d31d596cd00e14fa631c283a40ba01559a/librt-0.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e", size = 582056, upload-time = "2026-07-08T12:25:58.144Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d3/a9ec15984a185e000c4d2a16ba28bd623124ad4c38a10974c7ff78e3a893/librt-0.13.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a", size = 562758, upload-time = "2026-07-08T12:25:59.544Z" }, + { url = "https://files.pythonhosted.org/packages/3c/af/dbe36b78b19c06a55097f99305e4ea9458e2273e6ae16a3cbecaad7ee978/librt-0.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22", size = 602095, upload-time = "2026-07-08T12:26:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a8/2966891b4dd2830f5203fbee92ac2c4947653a2390ba73dfa44244fad025/librt-0.13.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c", size = 593452, upload-time = "2026-07-08T12:26:02.352Z" }, + { url = "https://files.pythonhosted.org/packages/61/f5/4df8bfc8405ecf8c0d525b4d69636f694bdd8620b313ec8b76e54a5926cc/librt-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0", size = 623729, upload-time = "2026-07-08T12:26:04.294Z" }, + { url = "https://files.pythonhosted.org/packages/d6/13/9ac202dffc8db06f75d06c08c2f9f6ff054be67d21272dcc078fa1cc0c57/librt-0.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04", size = 617077, upload-time = "2026-07-08T12:26:05.845Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/ebe38610716aee5cb28efd95089bb90192096179802779381e1c5dcf239c/librt-0.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61", size = 599561, upload-time = "2026-07-08T12:26:07.21Z" }, + { url = "https://files.pythonhosted.org/packages/4f/5c/c2e72e236fff7abc716d5b1753b8b8cd3ea85ac46fe17d2e7c51d4e1c723/librt-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9", size = 645511, upload-time = "2026-07-08T12:26:08.562Z" }, + { url = "https://files.pythonhosted.org/packages/0c/99/6203ce619dee940d6bfbe099ec3fe4be00a68e9d60f70abf906cf124fe66/librt-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18", size = 104357, upload-time = "2026-07-08T12:26:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/52/dd/843b6314087c41657c7036d7914d8f294bdf9b580aa8513ea0588c8e9a3d/librt-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259", size = 126998, upload-time = "2026-07-08T12:26:10.975Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/3dcec2884ba1b0806d1408612555c38dd5d68e90156b59f75f6e36435c3a/librt-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99", size = 110771, upload-time = "2026-07-08T12:26:12.303Z" }, +] + [[package]] name = "markdown-it-py" version = "4.2.0" @@ -867,6 +984,67 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/7f/a946aa4f8752b37102b41e64dca18a1976ac705c3a0d1dfe74d820a02552/mistune-3.2.1-py3-none-any.whl", hash = "sha256:78cdb0ba5e938053ccf63651b352508d2efa9411dc8810bfb05f2dc5140c0048", size = 53749, upload-time = "2026-05-03T14:33:20.551Z" }, ] +[[package]] +name = "mypy" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/af/4e516a05d3ca2eb9283e9ec45b2c02225c1514dd6da49fd3c9eaa6639370/mypy-2.3.0.tar.gz", hash = "sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e", size = 3988104, upload-time = "2026-07-13T11:34:53.387Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/b9/d75b3082b05f1b3028828aeb18e74ae5ab0a0936051bbf1f32f59f654747/mypy-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3419d00717afbc5265b50dd14b1278f29ea4884dd398ab67873489ac093fd329", size = 14838725, upload-time = "2026-07-13T11:32:44.655Z" }, + { url = "https://files.pythonhosted.org/packages/a9/50/79a65c6ea6e115bc73296038a4543b2d5c91f07912b918a2c616a2514bba/mypy-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cfca8ee88544090f86b6dcce05ec55d66eb48a762412ac2507810ba4bd793b6f", size = 13911128, upload-time = "2026-07-13T11:32:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/90/48/e11ed7716c26953ca321f726e452e374dbf81a6f2b8b212ec02af29b6b8f/mypy-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75cbb4b9ef04a0c84a957f07abc4504fbf64b8dcc145675101f2d3a78a4b1d6a", size = 14146742, upload-time = "2026-07-13T11:33:03.313Z" }, + { url = "https://files.pythonhosted.org/packages/06/72/6807565b1c4861ef66f7fdd98b51c61556356eab80235717b46c53bb8627/mypy-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:982e3d53dd23d0a4cef67dd66791fdbede0cf38f9eb617bf47663554c51e1e36", size = 15081418, upload-time = "2026-07-13T11:31:13.899Z" }, + { url = "https://files.pythonhosted.org/packages/00/80/1ea14c5d80e589e415973db3e47c78c2219a305b808b2b506395342c1d79/mypy-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85c5385b93012ffa3b31479ab579aef5415f4f3a32c6cf1ae07a984d2a0ff461", size = 15328164, upload-time = "2026-07-13T11:31:35.723Z" }, + { url = "https://files.pythonhosted.org/packages/37/28/8223157404a3d51920078459c37f80fbdc590e1d8ea049dc5ce48643022a/mypy-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:13b1b16e2fa39f3b2e33fb1c468abc7a69369fa2e886b4b87b5afc81472325cd", size = 11136472, upload-time = "2026-07-13T11:27:37.018Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cc/ea27e5959c5f258585a756b252031f3b313583d81b5064b2bebc41d3706b/mypy-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:b5cd2f027a972a4a5f2278a11fac9747f5f81a53a30b714d74950b6807e55568", size = 10135800, upload-time = "2026-07-13T11:30:08.92Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/0e7e592619e2133596a47cdd642534b0456545c218430bd3b9d8fefdd1b1/mypy-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d53fc67b9d28a43c6199077f49fea0f05839e36cf6158500331c9549225e5a5", size = 15026523, upload-time = "2026-07-13T11:34:49.206Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/1e1731df090a857df2807177a4626863e5ac0f0256513c35780efe53986f/mypy-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fbc00cee7bdbb9291979ddc9d08034a29dfcda4932628c9bbc28c1edd589df0c", size = 14032189, upload-time = "2026-07-13T11:33:57.168Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/cab921f4a806e171f34113e6181dd23c55358ccf6a80741269ef594a410e/mypy-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04e617030eca5221909c8b7d8d7fd1c637948199aa2100b2ad9813feb07e1491", size = 14198696, upload-time = "2026-07-13T11:32:12.767Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/e6d008bb19fe446e3662d85e0e2717bf9f2d611a2164fb29d6e067dbf46c/mypy-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56c184d2c20ca6b6378d58d1960270a767f41f5e44acbbd27f05effef4f4e1d7", size = 15286904, upload-time = "2026-07-13T11:34:27.594Z" }, + { url = "https://files.pythonhosted.org/packages/db/83/94397c9293608a364aa03e8084fb34ede4ae976a260384b9b52929308135/mypy-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3961a4a34b05f7c74b0f05aa51fbfe99a2d1e126038df40318d15c8f558b7ef3", size = 15528342, upload-time = "2026-07-13T11:34:07.819Z" }, + { url = "https://files.pythonhosted.org/packages/cf/96/d8b37d819adec6cfccfb1fd3afc1735d94717ddeafb45536db9c6943e09b/mypy-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b1942b9314d4c784b8ea1dbab4972603290e5dd5630f06675f13aec97526bc4c", size = 11218346, upload-time = "2026-07-13T11:28:27.745Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cd/cd9f725b19b19e5b530a154cf9bcf9e94279c5d55b3c34fb42b3aa48ea1b/mypy-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:be51653d7669d7d7955d613b8d0bb57d5b652eaf71a873ddf65ac87254dd2595", size = 10204525, upload-time = "2026-07-13T11:31:02.552Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ae/f7d056eb0294586a572d0d0d89580ec633c064db520f11d37d5a2fb833bd/mypy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97", size = 14947298, upload-time = "2026-07-13T11:27:47.734Z" }, + { url = "https://files.pythonhosted.org/packages/32/d5/db3e7af01e7844d21662c6ddc1f7825ec7cb4053f0391ac02faf3638396f/mypy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac", size = 13950768, upload-time = "2026-07-13T11:27:57.726Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fb/43c031f0190513d1ec248ed037eceb742ddd2a4d74bbf406658a28173837/mypy-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6", size = 14151586, upload-time = "2026-07-13T11:29:18.615Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c3/f8b2ffc60883084da91be51af58e88a7ffd4ff9795acb7d902ff88d31eb1/mypy-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b", size = 15227411, upload-time = "2026-07-13T11:30:29.904Z" }, + { url = "https://files.pythonhosted.org/packages/83/2e/16b917fc7adcf03f1aadddfc93aab804ffb234b1ab09c0ffd6d92a5d34a2/mypy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2", size = 15478790, upload-time = "2026-07-13T11:33:14.686Z" }, + { url = "https://files.pythonhosted.org/packages/c0/88/aaa65a93c73d0cdae7e42f8adb302bf6885bb281302084f99d0290a35347/mypy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757", size = 11234919, upload-time = "2026-07-13T11:33:39.28Z" }, + { url = "https://files.pythonhosted.org/packages/35/19/b40de63f1a80e63bc2d40f0679a6a8dbd34e95176c8122119bdf406aa552/mypy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1", size = 10201510, upload-time = "2026-07-13T11:31:52.619Z" }, + { url = "https://files.pythonhosted.org/packages/a4/58/fa0ae047da911f540284009b4f44b96fe09d83c076d7c103e9d645f46303/mypy-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db", size = 14941909, upload-time = "2026-07-13T11:32:34.332Z" }, + { url = "https://files.pythonhosted.org/packages/15/14/2ba1d61452d7c2a7fe12741e8d374e52b183476b07aa7f9e2a0d02b0720a/mypy-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762", size = 13967581, upload-time = "2026-07-13T11:30:00.587Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5a/483fb9e5ffbbb1a28dccc7b0a13d141b17ac769b6c9f488c0a0c63698962/mypy-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef", size = 14168807, upload-time = "2026-07-13T11:28:48.6Z" }, + { url = "https://files.pythonhosted.org/packages/ae/77/70d7a10732063beb74ad713682cf871e88f5c5fa39bfc8beff8a524bf9cb/mypy-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b", size = 15200144, upload-time = "2026-07-13T11:31:25.283Z" }, + { url = "https://files.pythonhosted.org/packages/56/72/766218ac783be4fdfcd699b90037b63017348a3e86fb2c1fbfb18302637d/mypy-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff", size = 15460389, upload-time = "2026-07-13T11:29:29.077Z" }, + { url = "https://files.pythonhosted.org/packages/38/4e/8a9db7411ecb8ec0cb1fd05dba432f28bafffcd38b4e887714a4a0506689/mypy-2.3.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4", size = 7753664, upload-time = "2026-07-13T11:29:08.147Z" }, + { url = "https://files.pythonhosted.org/packages/65/4c/c3f8bfd6ed0e5e38b5a244403b27f821d433443df5a15a278417c10a3a3c/mypy-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f", size = 11417237, upload-time = "2026-07-13T11:33:47.467Z" }, + { url = "https://files.pythonhosted.org/packages/3c/00/89a32eaf5ccf174bc4f90db0eaea5d70636c01b8d49f384bdab2e8834390/mypy-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7", size = 10389252, upload-time = "2026-07-13T11:31:43.81Z" }, + { url = "https://files.pythonhosted.org/packages/31/56/104f93d69aa9f339b6b9d3b0a7faa699b8b466c942cf3ae86cc2a2ec0915/mypy-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373", size = 16385495, upload-time = "2026-07-13T11:29:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/d2/03/f1d2123313f55efafdd27706960f43a771c62f1b68426c76043f3ab9ebf3/mypy-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556", size = 15098155, upload-time = "2026-07-13T11:30:40.301Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5d/d5f9200399b445e81726c4f23becee33f233aee81c72680b1ef3a258b641/mypy-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88", size = 15514155, upload-time = "2026-07-13T11:34:38.569Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ce/69977c555f08faa3190cfde44189b89dbd56861b1ab97aa18fc5f3a2e4a3/mypy-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe", size = 16766351, upload-time = "2026-07-13T11:33:29.195Z" }, + { url = "https://files.pythonhosted.org/packages/bc/92/6648b6caa3ab9e00f9ac0c2a78307805f873dd48139b24a6f6f7c3667bbf/mypy-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60", size = 17043490, upload-time = "2026-07-13T11:30:53.927Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ab/0dc91d80f3f016634c68d451f294a97320fe903a9b6f90b9e57b3f7f1717/mypy-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654", size = 12146869, upload-time = "2026-07-13T11:29:38.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/b5/4c964d02634ba81f4d1c84838e5c5b18ab06d13ed568960f5d6318495ccc/mypy-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5", size = 10965113, upload-time = "2026-07-13T11:28:07.056Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/fdc54fe583ba3cafbcedfb70eeeaf03849f75b1827a07096c7bd996f582d/mypy-2.3.0-py3-none-any.whl", hash = "sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88", size = 2753292, upload-time = "2026-07-13T11:33:18.48Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + [[package]] name = "myst-parser" version = "5.1.0" @@ -1082,6 +1260,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" }, ] +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + [[package]] name = "pexpect" version = "4.9.0" @@ -1521,6 +1708,7 @@ dev = [ { name = "cloudpickle" }, { name = "hypothesis", marker = "sys_platform != 'ios'" }, { name = "ipython" }, + { name = "mypy" }, { name = "myst-parser" }, { name = "nbsphinx" }, { name = "nox" }, @@ -1546,6 +1734,7 @@ docs = [ github = [ { name = "cloudpickle" }, { name = "hypothesis", marker = "sys_platform != 'ios'" }, + { name = "mypy" }, { name = "nox" }, { name = "pylint" }, { name = "pytest" }, @@ -1557,6 +1746,7 @@ github = [ test = [ { name = "cloudpickle" }, { name = "hypothesis", marker = "sys_platform != 'ios'" }, + { name = "mypy" }, { name = "nox" }, { name = "pylint" }, { name = "pytest" }, @@ -1577,6 +1767,7 @@ dev = [ { name = "cloudpickle" }, { name = "hypothesis", marker = "sys_platform != 'ios'", specifier = ">=6.0" }, { name = "ipython" }, + { name = "mypy", specifier = ">=1.17.1" }, { name = "myst-parser", specifier = ">=0.13" }, { name = "nbsphinx" }, { name = "nox", specifier = ">=2026.2.9" }, @@ -1600,6 +1791,7 @@ docs = [ github = [ { name = "cloudpickle" }, { name = "hypothesis", marker = "sys_platform != 'ios'", specifier = ">=6.0" }, + { name = "mypy", specifier = ">=1.17.1" }, { name = "nox", specifier = ">=2026.2.9" }, { name = "pylint", specifier = ">=4.0.5" }, { name = "pytest", specifier = ">=9.0.2" }, @@ -1611,6 +1803,7 @@ github = [ test = [ { name = "cloudpickle" }, { name = "hypothesis", marker = "sys_platform != 'ios'", specifier = ">=6.0" }, + { name = "mypy", specifier = ">=1.17.1" }, { name = "nox", specifier = ">=2026.2.9" }, { name = "pylint", specifier = ">=4.0.5" }, { name = "pytest", specifier = ">=9.0.2" }, @@ -1837,7 +2030,8 @@ name = "sphinx" version = "9.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.12'", + "python_full_version >= '3.15'", + "python_full_version >= '3.12' and python_full_version < '3.15'", ] dependencies = [ { name = "alabaster", marker = "python_full_version >= '3.12'" }, From c7a4af6d922302254ea13875f91b6e57221a5f34 Mon Sep 17 00:00:00 2001 From: Matthew Carroll <28577806+MJC598@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:40:04 -0400 Subject: [PATCH 3/6] [Feature] Building Models and Documentation (#29) * updating for new dev branch * adding a new integration to test parameter changes * documentation updates * removing SMR matrix from final death * multiplying background mortality transition by SMR * new integration tests * really mermaid? --- CMakeLists.txt | 2 +- docs/source/conf.py | 5 +- docs/source/explanations/architecture.md | 122 +++--- docs/source/explanations/data-flow.md | 56 +++ docs/source/explanations/object-lifecycle.md | 69 ++++ docs/source/explanations/public-api.md | 83 ++++ docs/source/explanations/runtime-execution.md | 68 ++++ .../how_to/cohort_subset_and_logging.md | 61 +++ docs/source/how_to/data_loading.md | 31 +- docs/source/how_to/load_input_data.md | 55 +++ docs/source/how_to/run_simulation.md | 62 +++ docs/source/how_to/single_model_build.md | 55 +++ docs/source/how_to/troubleshooting.md | 88 +++++ docs/source/index.rst | 41 +- docs/source/references/build.md | 26 ++ docs/source/references/cost_effectiveness.md | 23 ++ docs/source/references/data.md | 36 ++ docs/source/references/package.md | 28 ++ docs/source/references/runtime_objects.md | 57 +++ docs/source/references/wrapper_typing.md | 31 +- docs/source/tutorials/base_respond.md | 27 +- docs/source/tutorials/first_run.md | 73 ++++ .../tutorials/history_interpretation.md | 55 +++ .../tutorials/parameter_change_experiment.md | 87 ++++ pyproject.toml | 1 + src/register_timestep.cpp | 22 ++ src/respondpy/__init__.py | 20 +- src/respondpy/_core/timestep.pyi | 9 + src/respondpy/build.py | 299 ++++++++++++++ src/respondpy/data/parameters.py | 32 +- src/respondpy/simulation.py | 58 +-- tests/test_integration.py | 371 ++++++++++++++++++ tests/test_smoke_bindings_runtime.py | 40 ++ uv.lock | 19 + 34 files changed, 1956 insertions(+), 156 deletions(-) create mode 100644 docs/source/explanations/data-flow.md create mode 100644 docs/source/explanations/object-lifecycle.md create mode 100644 docs/source/explanations/public-api.md create mode 100644 docs/source/explanations/runtime-execution.md create mode 100644 docs/source/how_to/cohort_subset_and_logging.md create mode 100644 docs/source/how_to/load_input_data.md create mode 100644 docs/source/how_to/run_simulation.md create mode 100644 docs/source/how_to/single_model_build.md create mode 100644 docs/source/how_to/troubleshooting.md create mode 100644 docs/source/references/build.md create mode 100644 docs/source/references/cost_effectiveness.md create mode 100644 docs/source/references/data.md create mode 100644 docs/source/references/package.md create mode 100644 docs/source/references/runtime_objects.md create mode 100644 docs/source/tutorials/first_run.md create mode 100644 docs/source/tutorials/history_interpretation.md create mode 100644 docs/source/tutorials/parameter_change_experiment.md create mode 100644 src/respondpy/build.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 840652b..646e5d7 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 6653edc7aacece2713a98085e85a450b306ac216 # dev + GIT_TAG 01c44f2e4475f8e6e1b694a39563fc002c6fd5bb # dev OVERRIDE_FIND_PACKAGE ) set(RESPOND_BUILD_DOCS OFF) diff --git a/docs/source/conf.py b/docs/source/conf.py index a3eaa05..092a301 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -22,11 +22,14 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.napoleon', - 'myst_parser' + 'myst_parser', + 'sphinxcontrib.mermaid' ] templates_path = ['_templates'] exclude_patterns = [] +source_suffix = {".md": "markdown", ".rst": "restructuredtext"} +myst_fence_as_directive = ["mermaid"] # -- Options for HTML output ------------------------------------------------- diff --git a/docs/source/explanations/architecture.md b/docs/source/explanations/architecture.md index 315b398..3c8db4f 100644 --- a/docs/source/explanations/architecture.md +++ b/docs/source/explanations/architecture.md @@ -1,97 +1,67 @@ # Architecture -This page summarizes the public surface and runtime flow of respondpy. +This section explains the public API and runtime behavior of respondpy. +It is intentionally conceptual: there are no references pages here and no +how-to recipes. -## Public API +If you need task-oriented steps, use [How-To Guides](../how_to/data_loading.md). +If you need API signatures and symbol-level details, use +[References](../references/wrapper_typing.md). +If you want guided learning exercises, use +[Tutorials](../tutorials/base_respond.md). + +```{toctree} +:maxdepth: 1 + +public-api +object-lifecycle +data-flow +runtime-execution +``` + +## Overview ```mermaid flowchart LR - subgraph Pkg[respondpy package] + subgraph Public[respondpy public surface] data[data] - discount[discount] - cwise_product[cwise_product] - cwise_min[cwise_min] - calculate_life_years[calculate_life_years] - + cost[cost_effectiveness] History[History] Model[Model] - build_model[build_model] - add_transitions_to_model[add_transitions_to_model] - build_model_transitions[build_model_transitions] - Simulation[Simulation] - build_simulation[build_simulation] - + Timestep[Timestep] Transition[Transition] - transition_factory[transition_factory] - build_timestep_transition[build_timestep_transition] + build_simulation[build_simulation] + build_model[build_model] + build_timestep[build_timestep] + build_default_transitions[build_default_transitions] + build_transition[build_transition] + add_matrix_to_transition[add_matrix_to_transition] end build_simulation --> Simulation build_model --> Model - add_transitions_to_model --> Model - build_model_transitions --> Model - transition_factory --> Transition - build_timestep_transition --> Transition -``` - -## Execution Diagram - -```mermaid -flowchart LR - A[Input initialized with DB + sim.conf] --> B["build_simulation(input_data, cohort_ids)"] - B --> C{Iterate cohort ids} - C --> D["build_model(input_data, cohort_id)"] - D --> E["input_data.select_parameter(INITIAL_COHORT, cohort_id, time=1)"] - E --> F["Model.set_state(initial_population)"] - F --> G["build_model_transitions(model, input_data, cohort_id)"] - G --> H["build_timestep_transition(timestep, input_data, cohort_id)"] - H --> I["migration transition"] - H --> J["intervention transition"] - H --> K["behavior transition"] - H --> L["overdose transition"] - H --> M["background death transition"] - I --> N["add_transitions_to_model"] - J --> N - K --> N - L --> N - M --> N - N --> O["Simulation.add_model(model)"] - O --> P["Simulation.run()"] - P --> Q["get_model_sparse_histories() -> History objects"] + build_timestep --> Timestep + build_default_transitions --> Transition + build_transition --> Transition + add_matrix_to_transition --> Transition + Simulation --> History + data --> Model + data --> Transition ``` -## UML Library Flow +## Scope ```mermaid -sequenceDiagram - autonumber - actor User - participant In as Input - participant BS as build_simulation() - participant BM as build_model() - participant BMT as build_model_transitions() - participant BTT as build_timestep_transition() - participant Sim as Simulation - participant Mod as Model - participant Tr as Transition - participant Hist as History +flowchart TB + A[Public API] + B[Core runtime objects] + C[Data access and validation] + D[Simulation assembly helpers] + E[Execution and output flow] - User->>In: create Input(path or db_path/conf_path) - User->>BS: build_simulation(In, cohort_ids) - loop for each cohort_id - BS->>BM: build_model(In, cohort_id) - BM->>In: select_parameter(INITIAL_COHORT, cohort_id, time=1) - BM->>Mod: set_state(initial_population) - BM->>BMT: build_model_transitions(Mod, In, cohort_id) - loop for each timestep - BMT->>BTT: build_timestep_transition(timestep, In, cohort_id) - BTT-->>BMT: [migration, intervention, behavior, overdose, mortality] - BMT->>Mod: add_transition(Transition...) - end - BS->>Sim: add_model(Mod) - end - User->>Sim: run() - User->>Sim: get_model_sparse_histories() - Sim-->>Hist: return per-model History objects + A --> B + A --> C + A --> D + A --> E ``` diff --git a/docs/source/explanations/data-flow.md b/docs/source/explanations/data-flow.md new file mode 100644 index 0000000..d25ff37 --- /dev/null +++ b/docs/source/explanations/data-flow.md @@ -0,0 +1,56 @@ +# Explanation: Data Flow + +This page follows the path from persisted RESPOND inputs to model-ready arrays +and transition matrices. + +See also: +- [How-To: Load RESPOND Input Data](../how_to/load_input_data.md) +- [References: respondpy.data](../references/data.md) +- [Tutorial: Parameter Change-Time Experiment](../tutorials/parameter_change_experiment.md) + +## Input Processing + +```mermaid +flowchart LR + A[SQLite database] --> B[Input] + C[sim.conf] --> B + B --> D[Parameter] + D --> E[select_parameter] + E --> F[Raw rows or numpy arrays] +``` + +## Parameter Mapping + +```mermaid +flowchart TB + A[ParameterType] + A --> B[INITIAL_COHORT] + A --> C[MIGRATION_COHORT] + A --> D[INTERVENTION_TRANSITION_PROBABILITY] + A --> E[BEHAVIOR_TRANSITION_PROBABILITY] + A --> F[OVERDOSE_PROBABILITY] + A --> G[OVERDOSE_FATALITY_PROBABILITY] + A --> H[BACKGROUND_DEATH_PROBABILITY] + A --> I[STANDARD_MORTALITY_RATIO] + + D --> J[transition matrix columns] + E --> J + F --> K[probability column] + G --> K + H --> K + I --> L[ratio column] + B --> M[count column] + C --> M +``` + +## Validation and Normalization + +```mermaid +flowchart LR + A[change-time values] --> B[validate_time_list] + B --> C[normalized ascending times] + D[transition rows] --> E[verify_no_nulls] + D --> F[verify_no_duplicates] + D --> G[update_retention_probability] + G --> H[normalized transitions] +``` \ No newline at end of file diff --git a/docs/source/explanations/object-lifecycle.md b/docs/source/explanations/object-lifecycle.md new file mode 100644 index 0000000..5230d89 --- /dev/null +++ b/docs/source/explanations/object-lifecycle.md @@ -0,0 +1,69 @@ +# Explanation: Object Lifecycle + +This page describes the main runtime objects and how ownership flows through +the simulation assembly process. + +See also: +- [How-To: Build a Single Cohort Model](../how_to/single_model_build.md) +- [References: Runtime Objects](../references/runtime_objects.md) +- [Tutorial: First End-to-End Simulation Run](../tutorials/first_run.md) + +## Runtime Ownership + +```mermaid +flowchart TB + User[User code] --> In[Input] + In --> BS[build_simulation] + BS --> Sim[Simulation] + BS --> BM[build_model] + BM --> Mod[Model] + BM --> BT[build_timestep] + BT --> TS[Timestep] + BT --> Tr[Transition] + TS --> Mod + Mod --> Sim + Sim --> Hist[History] +``` + +## Class Roles + +```mermaid +classDiagram + class Input { + +config + +select_parameter() + +get_cohort_ids() + } + + class Model + class Simulation { + +set_duration() + +add_model() + +run() + } + class Timestep { + +add_transition() + } + class Transition { + +add_matrix() + } + class History + + Simulation "1" o-- "many" Model + Model "1" o-- "many" Timestep + Timestep "1" o-- "many" Transition + Simulation ..> History : returns + Input ..> Model : initial state lookup + Input ..> Transition : parameter lookup +``` + +## Assembly Boundaries + +```mermaid +flowchart LR + A[Data access] --> B[Model construction] + B --> C[Timestep assembly] + C --> D[Transition population] + D --> E[Simulation execution] + E --> F[History output] +``` \ No newline at end of file diff --git a/docs/source/explanations/public-api.md b/docs/source/explanations/public-api.md new file mode 100644 index 0000000..59da801 --- /dev/null +++ b/docs/source/explanations/public-api.md @@ -0,0 +1,83 @@ +# Explanation: Public API + +This page summarizes the top-level modules and classes that make up the +library surface exposed by `respondpy`. + +See also: +- [How-To Guides](../how_to/data_loading.md) +- [References](../references/wrapper_typing.md) +- [Tutorials](../tutorials/base_respond.md) + +## Package Surface + +```mermaid +flowchart LR + subgraph respondpy + data[data] + cost_effectiveness[cost_effectiveness] + History[History] + Model[Model] + Simulation[Simulation] + Timestep[Timestep] + Transition[Transition] + build_simulation[build_simulation] + build_model[build_model] + build_timestep[build_timestep] + build_default_transitions[build_default_transitions] + build_transition[build_transition] + add_matrix_to_transition[add_matrix_to_transition] + end + + data --> data_api[data API] + cost_effectiveness --> cost_api[discount, cwise_product, cwise_min, calculate_life_years] + build_simulation --> Simulation + build_model --> Model + build_timestep --> Timestep + build_default_transitions --> Transition + build_transition --> Transition + add_matrix_to_transition --> Transition +``` + +## Data Namespace + +```mermaid +flowchart TB + subgraph respondpy.data + ParameterType[ParameterType] + Parameter[Parameter] + Input[Input] + build_constant_state_vector[build_constant_state_vector] + build_constant_transition[build_constant_transition] + update_retention_probability[update_retention_probability] + verify_transition_probability[verify_transition_probability] + verify_no_nulls[verify_no_nulls] + verify_no_duplicates[verify_no_duplicates] + validate_time_list[validate_time_list] + end + + ParameterType --> Parameter + Parameter --> Input + Parameter --> build_constant_transition + Parameter --> build_constant_state_vector + Parameter --> update_retention_probability + Parameter --> validate_time_list +``` + +## Public Classes + +```mermaid +classDiagram + class Input + class Model + class Simulation + class Timestep + class Transition + class History + + Simulation "1" o-- "many" Model + Model "1" o-- "many" Timestep + Timestep "1" o-- "many" Transition + Simulation ..> History : produces + Input ..> Model : supplies state data + Input ..> Transition : supplies matrices +``` \ No newline at end of file diff --git a/docs/source/explanations/runtime-execution.md b/docs/source/explanations/runtime-execution.md new file mode 100644 index 0000000..11d135e --- /dev/null +++ b/docs/source/explanations/runtime-execution.md @@ -0,0 +1,68 @@ +# Explanation: Runtime Execution + +This page shows the construction and execution sequence of a simulation run. + +See also: +- [How-To: Build and Run a Simulation](../how_to/run_simulation.md) +- [References: respondpy.build](../references/build.md) +- [Tutorial: First End-to-End Simulation Run](../tutorials/first_run.md) + +## Build Sequence + +```mermaid +sequenceDiagram + autonumber + actor User + participant In as Input + participant BS as build_simulation() + participant BM as build_model() + participant BT as build_timestep() + participant BDT as build_default_transitions() + participant Tr as Transition + participant TS as Timestep + participant Mod as Model + participant Sim as Simulation + + User->>In: create Input(path or db/config files) + User->>BS: build_simulation(In, cohort_ids) + loop each cohort id + BS->>BM: build_model(In, cohort_id) + BM->>In: select_parameter(INITIAL_COHORT, cohort_id, time=1) + BM->>Mod: set_state(initial_state) + loop each model timestep + BM->>BT: build_timestep(In, cohort_id, tstep) + BT->>BDT: build_default_transitions(In, cohort_id, time=tstep) + BDT-->>BT: migration, behavior, intervention, overdose, mortality + BT->>TS: add_transition(Transition) + TS-->>BM: timestep + end + BS->>Sim: add_model(Model) + end +``` + +## Execution Sequence + +```mermaid +sequenceDiagram + autonumber + actor User + participant Sim as Simulation + participant Mod as Model + participant TS as Timestep + participant Tr as Transition + participant Hist as History + + User->>Sim: run() + loop for each model + Sim->>Mod: advance state over time + loop for each timestep + Mod->>TS: apply transitions + loop for each transition + TS->>Tr: evaluate matrix + end + end + Sim->>Hist: collect sparse history + end + User->>Sim: get_model_history(index) + Sim-->>User: mapping of history names to History objects +``` \ No newline at end of file diff --git a/docs/source/how_to/cohort_subset_and_logging.md b/docs/source/how_to/cohort_subset_and_logging.md new file mode 100644 index 0000000..635ea44 --- /dev/null +++ b/docs/source/how_to/cohort_subset_and_logging.md @@ -0,0 +1,61 @@ +# How-To: Select Cohorts and Configure Logging + +Use this guide to scope a run to a cohort subset and set deterministic logging +settings for embedded workflows. + +See also: +- [How-To: Build and Run a Simulation](run_simulation.md) +- [Tutorial: First End-to-End Simulation Run](../tutorials/first_run.md) +- [References: respondpy.build](../references/build.md) + +## Discover valid cohort IDs + +```python +from pathlib import Path + +from respondpy.data import Input + +input_data = Input(path=Path("/path/to/respond-input")) +cohort_ids = input_data.get_cohort_ids() + +print(cohort_ids) +``` + +## Build a subset simulation safely + +```python +from pathlib import Path + +from respondpy.build import build_simulation +from respondpy.data import Input + +input_data = Input(path=Path("/path/to/respond-input")) + +target_cohorts = [cohort_id for cohort_id in input_data.get_cohort_ids()[:2]] + +simulation = build_simulation( + input_data, + cohort_ids=target_cohorts, + log_name="respond_subset", + log_file="respond_subset.log", +) + +simulation.run() +print(len(simulation.get_model_names())) +``` + +## Fail fast on unknown cohorts + +```python +from pathlib import Path + +from respondpy.build import build_simulation +from respondpy.data import Input + +input_data = Input(path=Path("/path/to/respond-input")) + +try: + build_simulation(input_data, cohort_ids=[999999]) +except ValueError as exc: + print(exc) +``` \ No newline at end of file diff --git a/docs/source/how_to/data_loading.md b/docs/source/how_to/data_loading.md index bc8e39c..cd99712 100644 --- a/docs/source/how_to/data_loading.md +++ b/docs/source/how_to/data_loading.md @@ -1,3 +1,30 @@ -# How-To Load Data +# How-To Guides -Loading data +These guides are task-oriented workflows for engineers embedding RESPOND via +respondpy. They use high-level build helpers first and keep examples runnable. + +For guided learning paths, use [Tutorials](../tutorials/base_respond.md). +For conceptual runtime background, use +[Explanations](../explanations/architecture.md). +For API-level symbol details, use [References](../references/wrapper_typing.md). + +```{toctree} +:maxdepth: 1 + +load_input_data +run_simulation +cohort_subset_and_logging +single_model_build +troubleshooting +``` + +## Workflow Map + +```mermaid +flowchart LR + A[Load Input] --> B[Build Simulation] + B --> C[Run Simulation] + C --> D[Collect Histories] + B --> E[Debug with single model] + A --> F[Troubleshoot data/config] +``` diff --git a/docs/source/how_to/load_input_data.md b/docs/source/how_to/load_input_data.md new file mode 100644 index 0000000..927a4d0 --- /dev/null +++ b/docs/source/how_to/load_input_data.md @@ -0,0 +1,55 @@ +# How-To: Load RESPOND Input Data + +Use this guide to initialize an `Input` object from RESPOND database and +configuration files. + +See also: +- [Tutorial: First End-to-End Simulation Run](../tutorials/first_run.md) +- [References: respondpy.data](../references/data.md) +- [Explanation: Data Flow](../explanations/data-flow.md) + +## Load from a shared directory + +```python +from pathlib import Path + +from respondpy.data import Input + +base_path = Path("/path/to/respond-input") +input_data = Input(path=base_path) + +print(input_data) +print(input_data.get_cohort_ids()) +``` + +## Load from explicit file paths + +```python +from pathlib import Path + +from respondpy.data import Input + +db_file = Path("/path/to/respond-input/input.db") +conf_file = Path("/path/to/respond-input/sim.conf") + +input_data = Input(db_path=db_file, conf_path=conf_file) + +duration = int(input_data.config.get("simulation", "duration")) +print(duration) +``` + +## Verify required simulation config values + +```python +from pathlib import Path + +from respondpy.data import Input + +input_data = Input(path=Path("/path/to/respond-input")) + +duration = input_data.config.get("simulation", "duration") +change_times = input_data.config.get("simulation", "parameter_change_times") + +print("duration:", duration) +print("parameter_change_times:", change_times) +``` \ No newline at end of file diff --git a/docs/source/how_to/run_simulation.md b/docs/source/how_to/run_simulation.md new file mode 100644 index 0000000..acd2f42 --- /dev/null +++ b/docs/source/how_to/run_simulation.md @@ -0,0 +1,62 @@ +# How-To: Build and Run a Simulation + +Use this guide when you want the shortest path from RESPOND input files to a +simulation run with history outputs. + +See also: +- [Tutorial: First End-to-End Simulation Run](../tutorials/first_run.md) +- [References: respondpy.build](../references/build.md) +- [Explanation: Runtime Execution](../explanations/runtime-execution.md) + +## Build and run all cohorts + +```python +from pathlib import Path + +from respondpy.build import build_simulation +from respondpy.data import Input + +input_data = Input(path=Path("/path/to/respond-input")) + +simulation = build_simulation(input_data) +simulation.run() + +model_names = simulation.get_model_names() +print(len(model_names)) +``` + +## Build and run selected cohorts + +```python +from pathlib import Path + +from respondpy.build import build_simulation +from respondpy.data import Input + +input_data = Input(path=Path("/path/to/respond-input")) + +simulation = build_simulation(input_data, cohort_ids=[1, 3, 5]) +simulation.run() + +for idx, _ in enumerate(simulation.get_model_names()): + history_names = simulation.get_model_history_names(idx) + print(history_names) +``` + +## Use run-specific logging + +```python +from pathlib import Path + +from respondpy.build import build_simulation +from respondpy.data import Input + +input_data = Input(path=Path("/path/to/respond-input")) + +simulation = build_simulation( + input_data, + log_name="respond_embed", + log_file="respond_embed.log", +) +simulation.run() +``` \ No newline at end of file diff --git a/docs/source/how_to/single_model_build.md b/docs/source/how_to/single_model_build.md new file mode 100644 index 0000000..f16675f --- /dev/null +++ b/docs/source/how_to/single_model_build.md @@ -0,0 +1,55 @@ +# How-To: Build a Single Cohort Model + +Use this guide when you need to inspect one cohort model before integrating it +into a multi-cohort simulation run. + +See also: +- [How-To: Build and Run a Simulation](run_simulation.md) +- [References: Runtime Objects](../references/runtime_objects.md) +- [Explanation: Object Lifecycle](../explanations/object-lifecycle.md) + +## Build one model from input data + +```python +from pathlib import Path + +from respondpy.build import build_model +from respondpy.data import Input + +input_data = Input(path=Path("/path/to/respond-input")) + +cohort_id = input_data.get_cohort_ids()[0] +model = build_model(input_data, cohort_id) + +print(type(model).__name__) +``` + +## Build one timestep explicitly + +```python +from pathlib import Path + +from respondpy.build import build_timestep +from respondpy.data import Input + +input_data = Input(path=Path("/path/to/respond-input")) +cohort_id = input_data.get_cohort_ids()[0] + +timestep = build_timestep(input_data, cohort_id, tstep=1) +print(type(timestep).__name__) +``` + +## Build default transitions for a time point + +```python +from pathlib import Path + +from respondpy.build import build_default_transitions +from respondpy.data import Input + +input_data = Input(path=Path("/path/to/respond-input")) +cohort_id = input_data.get_cohort_ids()[0] + +transitions = build_default_transitions(input_data, cohort_id, time=1) +print([type(t).__name__ for t in transitions]) +``` \ No newline at end of file diff --git a/docs/source/how_to/troubleshooting.md b/docs/source/how_to/troubleshooting.md new file mode 100644 index 0000000..146d07a --- /dev/null +++ b/docs/source/how_to/troubleshooting.md @@ -0,0 +1,88 @@ +# How-To: Troubleshoot Common Embedding Issues + +Use this guide when a high-level build workflow fails during setup or runtime. + +See also: +- [How-To: Load RESPOND Input Data](load_input_data.md) +- [How-To: Build and Run a Simulation](run_simulation.md) +- [References: respondpy.data](../references/data.md) +- [Explanation: Data Flow](../explanations/data-flow.md) + +## Database or config file not found + +```python +from pathlib import Path + +from respondpy.data import Input + +try: + Input(path=Path("/bad/path")) +except FileNotFoundError as exc: + print(exc) +``` + +## Missing required input arguments + +```python +from respondpy.data import Input + +try: + Input() +except ValueError as exc: + print(exc) +``` + +## Invalid cohort IDs in build_simulation + +```python +from pathlib import Path + +from respondpy.build import build_simulation +from respondpy.data import Input + +input_data = Input(path=Path("/path/to/respond-input")) + +try: + build_simulation(input_data, cohort_ids=[-1]) +except ValueError as exc: + print(exc) +``` + +## Invalid parameter change times in config + +```python +from respondpy.data import validate_time_list + +try: + validate_time_list([0, 12]) +except ValueError as exc: + print(exc) +``` + +## Unsupported parameter in low-level calls + +```python +from pathlib import Path + +from respondpy.data import Input, Parameter, ParameterType + +input_data = Input(path=Path("/path/to/respond-input")) + +parameter = Parameter(ParameterType.MIGRATION_COHORT) +print(input_data.select_parameter(parameter, cohort_id=1, time=1).shape) +``` + +## Troubleshooting Flow + +```mermaid +flowchart TD + A[Failure during load/build/run] --> B{Failure type} + B -->|Path or file error| C[Check Input path/db_path/conf_path] + B -->|Cohort error| D[Check input_data.get_cohort_ids] + B -->|Config error| E[Check simulation.duration and parameter_change_times] + B -->|Data shape/value error| F[Check select_parameter outputs and parameter type] + C --> G[Retry build_simulation] + D --> G + E --> G + F --> G +``` \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 8dc977f..5f2140f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -4,7 +4,7 @@ contain the root `toctree` directive. Welcome to respondpy's documentation! -======================= +===================================== **respondpy** is a Python library for wrapping and interacting with the `RESPOND simulation model`_ C++ API. It provides a convenient and Pythonic interface for users to access the functionality of RESPOND, enabling seamless integration with Python applications. @@ -13,10 +13,39 @@ Welcome to respondpy's documentation! .. note:: This project is under active development, and the API may change in future releases. Users are encouraged to check the documentation for updates and refer to the source code for the latest features. +Documentation flow +------------------ + +The docs are organized into four sections with distinct goals: + +- Tutorials: guided learning paths for researchers. +- How-To Guides: task-oriented workflows for engineers embedding RESPOND. +- Explanations: conceptual architecture and runtime behavior. +- References: API and symbol-level technical detail. + +Start here based on your goal: + +- Learn by doing: Tutorials. +- Complete a concrete task: How-To Guides. +- Understand system design: Explanations. +- Look up precise behavior or signatures: References. + .. toctree:: - :maxdepth: 2 - - explanations/architecture + :maxdepth: 1 + + tutorials/base_respond + +.. toctree:: + :maxdepth: 1 + how_to/data_loading - references/wrapper_typing - tutorials/base_respond \ No newline at end of file + +.. toctree:: + :maxdepth: 1 + + explanations/architecture + +.. toctree:: + :maxdepth: 1 + + references/wrapper_typing \ No newline at end of file diff --git a/docs/source/references/build.md b/docs/source/references/build.md new file mode 100644 index 0000000..a19b2ea --- /dev/null +++ b/docs/source/references/build.md @@ -0,0 +1,26 @@ +# Reference: respondpy.build + +API reference for high-level simulation and model assembly helpers. + +See also: +- [Explanation: Runtime Execution](../explanations/runtime-execution.md) +- [How-To: Build and Run a Simulation](../how_to/run_simulation.md) +- [Tutorial: First End-to-End Simulation Run](../tutorials/first_run.md) + +```{automodule} respondpy.build +:members: +:undoc-members: +:show-inheritance: +``` + +## Helper Relationships + +```mermaid +flowchart TB + build_simulation --> Simulation + build_model --> Model + build_timestep --> Timestep + build_default_transitions --> Transition + build_transition --> Transition + add_matrix_to_transition --> Transition +``` \ No newline at end of file diff --git a/docs/source/references/cost_effectiveness.md b/docs/source/references/cost_effectiveness.md new file mode 100644 index 0000000..d0ca258 --- /dev/null +++ b/docs/source/references/cost_effectiveness.md @@ -0,0 +1,23 @@ +# Reference: respondpy.cost_effectiveness + +API reference for cost-effectiveness helper functions exported by respondpy. + +See also: +- [Explanations](../explanations/architecture.md) +- [How-To Guides](../how_to/data_loading.md) +- [Tutorials](../tutorials/base_respond.md) + +```{automodule} respondpy.cost_effectiveness +:members: +:undoc-members: +:show-inheritance: +``` + +## Function Surface + +```mermaid +flowchart LR + discount --> cwise_product + discount --> cwise_min + discount --> calculate_life_years +``` \ No newline at end of file diff --git a/docs/source/references/data.md b/docs/source/references/data.md new file mode 100644 index 0000000..850fcdb --- /dev/null +++ b/docs/source/references/data.md @@ -0,0 +1,36 @@ +# Reference: respondpy.data + +API reference for the data namespace, including input access and validation +helpers. + +See also: +- [Explanation: Data Flow](../explanations/data-flow.md) +- [How-To: Load RESPOND Input Data](../how_to/load_input_data.md) +- [Tutorial: Parameter Change-Time Experiment](../tutorials/parameter_change_experiment.md) + +```{automodule} respondpy.data +:members: +:undoc-members: +:show-inheritance: +``` + +## Public Symbols + +```mermaid +classDiagram + class ParameterType + class Parameter + class Input + class build_constant_state_vector + class build_constant_transition + class update_retention_probability + class verify_transition_probability + class verify_no_nulls + class verify_no_duplicates + class validate_time_list + + ParameterType --> Parameter + Parameter --> Input + Input ..> build_constant_state_vector + Input ..> build_constant_transition +``` \ No newline at end of file diff --git a/docs/source/references/package.md b/docs/source/references/package.md new file mode 100644 index 0000000..30e3bf8 --- /dev/null +++ b/docs/source/references/package.md @@ -0,0 +1,28 @@ +# Reference: respondpy Package + +API reference for top-level symbols exported by `respondpy`. + +See also: +- [Explanations](../explanations/architecture.md) +- [How-To Guides](../how_to/data_loading.md) +- [Tutorials](../tutorials/base_respond.md) + +```{automodule} respondpy +:members: +:undoc-members: +:show-inheritance: +``` + +## Export Map + +```mermaid +flowchart LR + respondpy[respondpy] --> data[data] + respondpy --> cost[cost_effectiveness] + respondpy --> history[History] + respondpy --> model[Model] + respondpy --> simulation[Simulation] + respondpy --> timestep[Timestep] + respondpy --> transition[Transition] + respondpy --> build[build helpers] +``` \ No newline at end of file diff --git a/docs/source/references/runtime_objects.md b/docs/source/references/runtime_objects.md new file mode 100644 index 0000000..839156d --- /dev/null +++ b/docs/source/references/runtime_objects.md @@ -0,0 +1,57 @@ +# Reference: Runtime Objects + +API reference for runtime classes exposed through the core wrapper modules. + +See also: +- [Explanation: Object Lifecycle](../explanations/object-lifecycle.md) +- [How-To: Build a Single Cohort Model](../how_to/single_model_build.md) +- [Tutorial: Interpret Model Histories](../tutorials/history_interpretation.md) + +```{automodule} respondpy.history +:members: +:undoc-members: +:show-inheritance: +``` + +```{automodule} respondpy.model +:members: +:undoc-members: +:show-inheritance: +``` + +```{automodule} respondpy.simulation +:members: +:undoc-members: +:show-inheritance: +``` + +```{automodule} respondpy.timestep +:members: +:undoc-members: +:show-inheritance: +``` + +```{automodule} respondpy.transition +:members: +:undoc-members: +:show-inheritance: +``` + +## Runtime Graph + +```mermaid +classDiagram + class Input + class History + class Model + class Simulation + class Timestep + class Transition + + Simulation "1" o-- "many" Model + Model "1" o-- "many" Timestep + Timestep "1" o-- "many" Transition + Simulation ..> History : produces + Input ..> Model : initializes + Input ..> Transition : populates +``` \ No newline at end of file diff --git a/docs/source/references/wrapper_typing.md b/docs/source/references/wrapper_typing.md index 06fada1..a06eddb 100644 --- a/docs/source/references/wrapper_typing.md +++ b/docs/source/references/wrapper_typing.md @@ -1,3 +1,30 @@ -# Wrapper Typing +# References -How the wrapper typing works between Python and C++ +This section contains technical reference material for the public API and +runtime-facing modules exposed by respondpy. + +For conceptual architecture and execution behavior, use +[Explanations](../explanations/architecture.md). +For operational workflows, use [How-To Guides](../how_to/data_loading.md). +For stepwise onboarding exercises, use [Tutorials](../tutorials/base_respond.md). + +```{toctree} +:maxdepth: 1 + +package +data +build +cost_effectiveness +runtime_objects +``` + +## Scope + +```mermaid +flowchart LR + A[respondpy package] --> B[Package reference] + A --> C[data namespace] + A --> D[build helpers] + A --> E[cost_effectiveness] + A --> F[runtime objects] +``` diff --git a/docs/source/tutorials/base_respond.md b/docs/source/tutorials/base_respond.md index 90c3c5b..431060c 100644 --- a/docs/source/tutorials/base_respond.md +++ b/docs/source/tutorials/base_respond.md @@ -1,3 +1,26 @@ -# Building and Running Base RESPOND +# Tutorials -Tutorial 1 \ No newline at end of file +These tutorials are guided learning paths for general researchers using +respondpy. + +For direct problem-solving recipes, use [How-To Guides](../how_to/data_loading.md). +For conceptual architecture and internals, use +[Explanations](../explanations/architecture.md). +For API signatures and object-level detail, use +[References](../references/wrapper_typing.md). + +```{toctree} +:maxdepth: 1 + +first_run +history_interpretation +parameter_change_experiment +``` + +## Learning Path + +```mermaid +flowchart LR + A[First Run] --> B[Interpret Histories] + B --> C[Parameter Change-Time Experiment] +``` \ No newline at end of file diff --git a/docs/source/tutorials/first_run.md b/docs/source/tutorials/first_run.md new file mode 100644 index 0000000..5e74bb4 --- /dev/null +++ b/docs/source/tutorials/first_run.md @@ -0,0 +1,73 @@ +# Tutorial: First End-to-End Simulation Run + +This tutorial walks through a complete run: load RESPOND input files, +construct a simulation from all cohorts, execute it, and inspect model-level +history handles. + +See also: +- [How-To: Build and Run a Simulation](../how_to/run_simulation.md) +- [References: respondpy.build](../references/build.md) +- [Explanation: Runtime Execution](../explanations/runtime-execution.md) + +## Step 1: Initialize Input + +```python +from pathlib import Path + +from respondpy.data import Input + +input_data = Input(path=Path("/path/to/respond-input")) +cohort_ids = input_data.get_cohort_ids() + +print("Cohorts:", cohort_ids) +``` + +## Step 2: Build and run the simulation + +```python +from respondpy.build import build_simulation + +simulation = build_simulation(input_data) +simulation.run() +``` + +## Step 3: Inspect model names and history names + +```python +model_names = simulation.get_model_names() +index_name_map = simulation.get_model_index_name_map() + +print("Model names:", model_names) +print("Index map:", index_name_map) + +if model_names: + first_model_history_names = simulation.get_model_history_names(0) + print("First model history names:", first_model_history_names) +``` + +## Step 4: Inspect one model history object + +```python +model_history = simulation.get_model_history(0) + +for history_name, history in model_history.items(): + print(history_name, type(history).__name__, history.get_name()) +``` + +## Run Flow + +```mermaid +sequenceDiagram + autonumber + participant U as User + participant I as Input + participant B as build_simulation + participant S as Simulation + + U->>I: create Input + U->>B: build_simulation(input_data) + B-->>S: configured Simulation + U->>S: run() + U->>S: get_model_names() + U->>S: get_model_history(...) +``` \ No newline at end of file diff --git a/docs/source/tutorials/history_interpretation.md b/docs/source/tutorials/history_interpretation.md new file mode 100644 index 0000000..951d059 --- /dev/null +++ b/docs/source/tutorials/history_interpretation.md @@ -0,0 +1,55 @@ +# Tutorial: Interpret Model Histories + +This tutorial demonstrates how to inspect model histories after a simulation +run and extract recorded timestep/state information using the history API. + +See also: +- [How-To: Build and Run a Simulation](../how_to/run_simulation.md) +- [References: Runtime Objects](../references/runtime_objects.md) +- [Explanation: Runtime Execution](../explanations/runtime-execution.md) + +## Step 1: Build and run a simulation subset + +```python +from pathlib import Path + +from respondpy.build import build_simulation +from respondpy.data import Input + +input_data = Input(path=Path("/path/to/respond-input")) +subset = input_data.get_cohort_ids()[:2] + +simulation = build_simulation(input_data, cohort_ids=subset) +simulation.run() +``` + +## Step 2: Read history names for each model + +```python +for idx, model_name in enumerate(simulation.get_model_names()): + history_names = simulation.get_model_history_names(idx) + print(idx, model_name, history_names) +``` + +## Step 3: Inspect recorded timesteps and state counts + +```python +model_history = simulation.get_model_history(0) + +for history_name, history in model_history.items(): + timesteps = history.get_recorded_timesteps() + states = history.get_recorded_states() + mode = history.get_history_mode() + + print("history:", history_name) + print("mode:", mode.name) + print("n_timesteps:", len(timesteps)) + print("n_states:", len(states)) +``` + +## Step 4: Access the latest recorded timestep + +```python +for history_name, history in model_history.items(): + print(history_name, history.get_latest_recorded_timestep()) +``` diff --git a/docs/source/tutorials/parameter_change_experiment.md b/docs/source/tutorials/parameter_change_experiment.md new file mode 100644 index 0000000..c805964 --- /dev/null +++ b/docs/source/tutorials/parameter_change_experiment.md @@ -0,0 +1,87 @@ +# Tutorial: Parameter Change-Time Experiment + +This tutorial compares runs under different `parameter_change_times` +configurations by using separate RESPOND config files that point to the same +database. + +See also: +- [How-To: Load RESPOND Input Data](../how_to/load_input_data.md) +- [References: respondpy.data](../references/data.md) +- [Explanation: Data Flow](../explanations/data-flow.md) + +## Step 1: Create two Input objects with different config files + +```python +from pathlib import Path + +from respondpy.data import Input + +db_file = Path("/path/to/respond-input/input.db") + +baseline_conf = Path("/path/to/respond-input/sim_baseline.conf") +experiment_conf = Path("/path/to/respond-input/sim_experiment.conf") + +baseline_input = Input(db_path=db_file, conf_path=baseline_conf) +experiment_input = Input(db_path=db_file, conf_path=experiment_conf) + +print(baseline_input.config.get("simulation", "parameter_change_times")) +print(experiment_input.config.get("simulation", "parameter_change_times")) +``` + +## Step 2: Build and run one simulation per config + +```python +from respondpy.build import build_simulation + +baseline_sim = build_simulation(baseline_input) +experiment_sim = build_simulation(experiment_input) + +baseline_sim.run() +experiment_sim.run() +``` + +## Step 3: Compare model-level history availability + +```python +baseline_model_names = baseline_sim.get_model_names() +experiment_model_names = experiment_sim.get_model_names() + +print("baseline models:", baseline_model_names) +print("experiment models:", experiment_model_names) + +if baseline_model_names and experiment_model_names: + baseline_history_names = baseline_sim.get_model_history_names(0) + experiment_history_names = experiment_sim.get_model_history_names(0) + + print("baseline history names:", baseline_history_names) + print("experiment history names:", experiment_history_names) +``` + +## Step 4: Compare latest recorded timestep in each run + +```python +baseline_history = baseline_sim.get_model_history(0) +experiment_history = experiment_sim.get_model_history(0) + +for history_name in baseline_history: + if history_name in experiment_history: + baseline_latest = baseline_history[history_name].get_latest_recorded_timestep() + experiment_latest = experiment_history[history_name].get_latest_recorded_timestep() + print(history_name, baseline_latest, experiment_latest) +``` + +## Experiment Flow + +```mermaid +flowchart LR + A[Shared input.db] --> B[sim_baseline.conf] + A --> C[sim_experiment.conf] + B --> D[baseline Input] + C --> E[experiment Input] + D --> F[baseline Simulation] + E --> G[experiment Simulation] + F --> H[baseline histories] + G --> I[experiment histories] + H --> J[compare timing and coverage] + I --> J +``` \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 21fb170..89aee2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ docs = [ "sphinx-book-theme>=0.0.33", "sphinx>=4.0", "sphinx_copybutton", + "sphinxcontrib-mermaid>=2.1.0", ] # hypothesis is slow on iOS, can fail health check # pytest-xdist has no processes on iOS diff --git a/src/register_timestep.cpp b/src/register_timestep.cpp index 2462f0b..967acbf 100644 --- a/src/register_timestep.cpp +++ b/src/register_timestep.cpp @@ -42,6 +42,15 @@ void register_timestep(py::module &m) { py::return_value_policy::reference_internal, "Create a new transition instance and add it to the timestep. " "Returns a reference to the created transition.") + .def( + "add_transition", + [](Timestep &self, const Transition &transition) { + const auto cloned_transition = transition.clone(); + self.AddTransition(cloned_transition); + }, + py::arg("transition"), + "Add a transition by cloning the provided transition instance " + "into this timestep.") .def("remove_transition", &Timestep::RemoveTransition, py::arg("idx"), "Remove a transition from the timestep by its idx. Throws an " "exception if the idx is out of bounds.") @@ -82,6 +91,19 @@ void register_timestep(py::module &m) { "Get the list of transitions in the timestep.") .def("get_transition_names", &Timestep::GetTransitionNames, "Get the list of transition names in the timestep.") + .def( + "__getitem__", + [](Timestep &self, size_t idx) -> Transition & { return self[idx]; }, + py::arg("idx"), py::return_value_policy::reference_internal, + "Get a transition using index access semantics.") + .def( + "__setitem__", + [](Timestep &self, size_t idx, const Transition &transition) { + self[idx] = transition; + }, + py::arg("idx"), py::arg("transition"), + "Replace a transition slot by index with a clone of the provided " + "transition.") .def("__repr__", [](const Timestep &t) { std::stringstream ss; diff --git a/src/respondpy/__init__.py b/src/respondpy/__init__.py index 438ac31..e14f739 100644 --- a/src/respondpy/__init__.py +++ b/src/respondpy/__init__.py @@ -4,7 +4,7 @@ # Created Date: 2025-08-04 # # Author: Matthew Carroll # # ----- # -# Last Modified: 2026-07-20 # +# Last Modified: 2026-07-28 # # Modified By: Matthew Carroll # # ----- # # Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center # @@ -20,17 +20,14 @@ discount, cwise_product, cwise_min, calculate_life_years ) from .history import History - from .model import Model - -from .simulation import ( - Simulation, build_simulation -) - +from .simulation import Simulation from .timestep import Timestep - from .transition import Transition +from .build import build_simulation, build_model, build_timestep, build_default_transitions, build_transition, add_matrix_to_transition + + __all__ = [ "data", "discount", @@ -40,9 +37,14 @@ "History", "Model", "Simulation", - "build_simulation", "Timestep", "Transition", + "build_simulation", + "build_model", + "build_timestep", + "build_default_transitions", + "build_transition", + "add_matrix_to_transition" ] diff --git a/src/respondpy/_core/timestep.pyi b/src/respondpy/_core/timestep.pyi index ca52b0b..4714815 100644 --- a/src/respondpy/_core/timestep.pyi +++ b/src/respondpy/_core/timestep.pyi @@ -42,6 +42,9 @@ class Timestep: def create_transition(self, transition_name: str) -> Transition: ... + def add_transition(self, transition: Transition) -> None: + ... + def remove_transition(self, idx: typing.SupportsInt) -> Transition: ... @@ -75,6 +78,12 @@ class Timestep: def get_transition_names(self) -> typing.Sequence[str]: ... + def __getitem__(self, idx: typing.SupportsInt) -> Transition: + ... + + def __setitem__(self, idx: typing.SupportsInt, transition: Transition) -> None: + ... + def __repr__(self) -> str: ... diff --git a/src/respondpy/build.py b/src/respondpy/build.py new file mode 100644 index 0000000..160b871 --- /dev/null +++ b/src/respondpy/build.py @@ -0,0 +1,299 @@ +################################################################################ +# File: build.py # +# Project: respondpy # +# Created Date: 2026-07-23 # +# Author: Matthew Carroll # +# ----- # +# Last Modified: 2026-07-29 # +# Modified By: Matthew Carroll # +# ----- # +# Copyright (c) 2026 Syndemics Lab at Boston Medical Center # +################################################################################ + +from __future__ import annotations + +from collections.abc import Sequence + +from .data import Input, Parameter, ParameterType, validate_time_list +from .simulation import Simulation +from .model import Model +from .timestep import Timestep +from .transition import Transition + + +def build_simulation( + input_data: Input, + *, + 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. + cohort_ids : Sequence of int, optional + Cohort identifiers to include in the simulation. When omitted, all + cohort identifiers present in ``input_data`` are used. + log_name : str, default="respond" + Logger name used by the underlying simulation objects. + log_file : str, default="respond.log" + File name used by the underlying simulation objects for logging. + + Returns + ------- + Simulation + A simulation object populated with cohort-specific models. + + Raises + ------ + ValueError + If any requested cohort id is not present in ``input_data``. + """ + input_cohort_ids = input_data.get_cohort_ids() + if cohort_ids is None: + cohort_ids = input_cohort_ids + else: + missing_cohorts = set(cohort_ids) - set(input_cohort_ids) + if missing_cohorts: + raise ValueError( + f"Cohort IDs {missing_cohorts} not found in input data." + ) + s = Simulation(log_name, log_file) + 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)) + + return s + + +def build_model( + input_data: Input, + cohort_id: int, + *, + log_name: str = "respond", + log_file: str = "respond.log" +) -> Model: + """Build a model for a single cohort. + + Parameters + ---------- + input_data : Input + Loaded input data and simulation configuration. + cohort_id : int + Cohort identifier used to select the initial state and parameter + values. + log_name : str, default="respond" + Logger name used by the underlying model. + log_file : str, default="respond.log" + File name used by the underlying model for logging. + + Returns + ------- + Model + A model configured with the cohort initial state and timestep + transitions. + """ + model = Model("markov", log_name, log_file) + initial_state = input_data.select_parameter( + Parameter(ParameterType.INITIAL_COHORT), + cohort_id, + ) + model.set_state(initial_state) + + change_times = validate_time_list( + list( + map( + int, + input_data.config.get( + "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): + 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, + )) + return model + + +def build_timestep( + input_data: Input, + cohort_id: int, + tstep: int = 1, + *, + log_name: str = "respond", + log_file: str = "respond.log" +) -> Timestep: + """Build a timestep containing the cohort transitions for a time point. + + Parameters + ---------- + input_data : Input + Loaded input data and simulation configuration. + cohort_id : int + Cohort identifier used to select timestep-specific parameters. + tstep : int, default=1 + Simulation time point represented by the timestep. + log_name : str, default="respond" + Logger name used by the underlying timestep. + log_file : str, default="respond.log" + File name used by the underlying timestep for logging. + + Returns + ------- + Timestep + A timestep populated with the default transitions for ``tstep``. + """ + timestep = Timestep(log_name, log_file) + + transitions = build_default_transitions( + input_data, cohort_id, time=tstep, log_name=log_name, log_file=log_file) + + for transition in transitions: + timestep.add_transition(transition) + + return timestep + + +def build_transition( + input_data: Input, + cohort_id: int, + param: Parameter, + *, + time: int = 1, + log_name: str = "respond", + log_file: str = "respond.log" +) -> Transition: + """Build a transition for a single parameter and cohort. + + Parameters + ---------- + input_data : Input + Loaded input data containing the parameter matrix. + cohort_id : int + Cohort identifier used to select the parameter values. + param : Parameter + Parameter descriptor used to identify the transition and look up the + corresponding data. + time : int, default=1 + Time point used when selecting the parameter values. + log_name : str, default="respond" + Logger name used by the underlying transition. + log_file : str, default="respond.log" + File name used by the underlying transition for logging. + + Returns + ------- + Transition + A transition containing the selected parameter matrix. + """ + transition = Transition( + param.get_parameter_name(), + param.get_parameter_name(), + log_name, + 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: + """Add another parameter matrix to an existing transition. + + Parameters + ---------- + transition : Transition + Transition to update in place. + input_data : Input + Loaded input data containing the parameter matrix. + cohort_id : int + Cohort identifier used to select the parameter values. + param : Parameter + Parameter descriptor used to look up the additional matrix. + time : int, default=1 + Time point used when selecting the parameter values. + + Returns + ------- + Transition + The same transition instance after the matrix has been added. + """ + transition.add_matrix(input_data.select_parameter(param, cohort_id, time)) + return transition + + +def build_default_transitions( + input_data: Input, + cohort_id: int, + *, + time: int = 1, + log_name: str = "respond", + log_file: str = "respond.log" +) -> list[Transition]: + """Build the default transitions used by each timestep. + + Parameters + ---------- + input_data : Input + Loaded input data and simulation configuration. + cohort_id : int + Cohort identifier used to select transition matrices. + time : int, default=1 + Time point used when selecting parameter values. + log_name : str, default="respond" + Logger name used by the underlying transitions. + log_file : str, default="respond.log" + File name used by the underlying transitions for logging. + + Returns + ------- + list[Transition] + 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 + ) + + b = build_transition( + 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 + ) + + o = build_transition( + 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) + + d = build_transition( + 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( + Parameter(ParameterType.STANDARD_MORTALITY_RATIO), cohort_id, time=time + ) + + return [m, b, i, o, d] diff --git a/src/respondpy/data/parameters.py b/src/respondpy/data/parameters.py index 0905ec1..90889cd 100644 --- a/src/respondpy/data/parameters.py +++ b/src/respondpy/data/parameters.py @@ -4,7 +4,7 @@ # Created Date: 2026-01-15 # # Author: Matthew Carroll # # ----- # -# Last Modified: 2026-07-16 # +# Last Modified: 2026-07-28 # # Modified By: Matthew Carroll # # ----- # # Copyright (c) 2026 Syndemics Lab at Boston Medical Center # @@ -73,6 +73,36 @@ def __repr__(self) -> str: """ return f"Parameter(parameter_type={self.__parameter_type})" + def get_parameter_name(self) -> str: + """Return the parameter type name. + + Returns + ------- + str + The parameter type name. + """ + match self.__parameter_type: + case ParameterType.INITIAL_COHORT: + return "initial_cohort" + case ParameterType.MIGRATION_COHORT: + return "migration" + case ParameterType.INTERVENTION_TRANSITION_PROBABILITY: + return "intervention" + case ParameterType.BEHAVIOR_TRANSITION_PROBABILITY: + return "behavior" + case ParameterType.OVERDOSE_PROBABILITY: + return "overdose" + case ParameterType.OVERDOSE_FATALITY_PROBABILITY: + return "fatal_overdose" + case ParameterType.BACKGROUND_DEATH_PROBABILITY: + return "background_death" + case ParameterType.STANDARD_MORTALITY_RATIO: + return "smr" + case _: + raise ValueError( + f"ParameterType value has a non-standard name! ParameterType: {self.__parameter_type}." + ) + def get_parameter_type(self) -> ParameterType: """Return the wrapped parameter type. diff --git a/src/respondpy/simulation.py b/src/respondpy/simulation.py index b31d28b..c80876a 100644 --- a/src/respondpy/simulation.py +++ b/src/respondpy/simulation.py @@ -4,68 +4,14 @@ # Created Date: 2026-06-05 # # Author: Matthew Carroll # # ----- # -# Last Modified: 2026-07-22 # +# Last Modified: 2026-07-23 # # Modified By: Matthew Carroll # # ----- # # Copyright (c) 2026 Syndemics Lab at Boston Medical Center # ################################################################################ from __future__ import annotations -from collections.abc import Sequence -from .data import Input from ._core.simulation import Simulation # pylint: disable=E0611,E0401 # type: ignore[reportMissingModuleSource] -__all__: list[str] = ['Simulation', 'build_simulation'] - - -def build_simulation( - input_data: Input, - *, - cohort_ids: Sequence[int] | None = None, - log_name: str = "respond", - log_file: str = "respond.log" -) -> Simulation: - """Build a simulation containing one model per cohort id. - - Parameters - ---------- - input_data : Input - Loaded input data and simulation configuration. - cohort_ids : Sequence of int, optional - Cohort identifiers to include in the simulation. - log_name : str, default="console" - Logger name used by the underlying core simulation/model. - - Returns - ------- - Simulation - A simulation object populated with cohort-specific models. - - Raises - ------ - ValueError - If any requested cohort id is not present in ``input_data``. - """ - input_cohort_ids = input_data.get_cohort_ids() - if cohort_ids is None: - cohort_ids = input_cohort_ids - else: - missing_cohorts = set(cohort_ids) - set(input_cohort_ids) - if missing_cohorts: - raise ValueError( - f"Cohort IDs {missing_cohorts} not found in input data." - ) - s = Simulation(log_name, log_file) - for cohort_id in cohort_ids: - _fill_model(s, input_data, cohort_id) - - return s - - -def _fill_model( - sim: Simulation, - input_data: Input, - cohort_id: int, -) -> None: - sim.create_new_model("markov") +__all__: list[str] = ['Simulation'] diff --git a/tests/test_integration.py b/tests/test_integration.py index e2158e0..76a6397 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -12,6 +12,7 @@ import sqlite3 from configparser import ConfigParser +import numpy as np import pytest import respondpy as rpy @@ -72,6 +73,136 @@ def setup_data(setup_db, setup_config): yield setup_db, setup_config +@pytest.fixture +def setup_db_with_midrun_change(tmp_path_factory, db_schema, insert_complete_sample): + """Build a DB with additional time-52 rows and higher migration inflow.""" + temp_dir = tmp_path_factory.mktemp("test-data") + mem_str = temp_dir / "input_104.db" + conn = sqlite3.connect(mem_str) + cursor = conn.cursor() + cursor.executescript(db_schema) + cursor.executescript(insert_complete_sample) + + # Add time-52 parameter rows. Keep all non-migration parameters identical + # to time 1, and increase migration counts so post-52 growth is higher. + cursor.executescript(""" +INSERT INTO population_change (sample, intervention, behavior, time, count) +SELECT sample, intervention, behavior, 52, count * 3.0 +FROM population_change +WHERE time = 1; + +INSERT INTO intervention_transition (sample, behavior, time, initial_intervention, new_intervention, probability) +SELECT sample, behavior, 52, initial_intervention, new_intervention, probability +FROM intervention_transition +WHERE time = 1; + +INSERT INTO behavior_transition (sample, intervention, time, initial_behavior, new_behavior, probability) +SELECT sample, intervention, 52, initial_behavior, new_behavior, probability +FROM behavior_transition +WHERE time = 1; + +INSERT INTO smr (sample, intervention, behavior, time, ratio) +SELECT sample, intervention, behavior, 52, ratio +FROM smr +WHERE time = 1; + +INSERT INTO background_mortality (sample, time, probability) +SELECT sample, 52, probability +FROM background_mortality +WHERE time = 1; + +INSERT INTO overdose (sample, intervention, behavior, time, probability) +SELECT sample, intervention, behavior, 52, probability +FROM overdose +WHERE time = 1; + +INSERT INTO overdose_fatality (sample, intervention, behavior, time, probability) +SELECT sample, intervention, behavior, 52, probability +FROM overdose_fatality +WHERE time = 1; +""") + + conn.commit() + conn.close() + yield mem_str + + +@pytest.fixture +def setup_db_with_flat_midrun_change(tmp_path_factory, db_schema, insert_complete_sample): + """Build a DB with time-52 rows that match time-1 values exactly.""" + temp_dir = tmp_path_factory.mktemp("test-data") + mem_str = temp_dir / "input_104_flat.db" + conn = sqlite3.connect(mem_str) + cursor = conn.cursor() + cursor.executescript(db_schema) + cursor.executescript(insert_complete_sample) + + cursor.executescript(""" +INSERT INTO population_change (sample, intervention, behavior, time, count) +SELECT sample, intervention, behavior, 52, count +FROM population_change +WHERE time = 1; + +INSERT INTO intervention_transition (sample, behavior, time, initial_intervention, new_intervention, probability) +SELECT sample, behavior, 52, initial_intervention, new_intervention, probability +FROM intervention_transition +WHERE time = 1; + +INSERT INTO behavior_transition (sample, intervention, time, initial_behavior, new_behavior, probability) +SELECT sample, intervention, 52, initial_behavior, new_behavior, probability +FROM behavior_transition +WHERE time = 1; + +INSERT INTO smr (sample, intervention, behavior, time, ratio) +SELECT sample, intervention, behavior, 52, ratio +FROM smr +WHERE time = 1; + +INSERT INTO background_mortality (sample, time, probability) +SELECT sample, 52, probability +FROM background_mortality +WHERE time = 1; + +INSERT INTO overdose (sample, intervention, behavior, time, probability) +SELECT sample, intervention, behavior, 52, probability +FROM overdose +WHERE time = 1; + +INSERT INTO overdose_fatality (sample, intervention, behavior, time, probability) +SELECT sample, intervention, behavior, 52, probability +FROM overdose_fatality +WHERE time = 1; +""") + + conn.commit() + conn.close() + yield mem_str + + +@pytest.fixture +def setup_config_104_change_52(tmp_path_factory): + """Config for a 104-step run with a parameter switch at step 52.""" + temp_dir = tmp_path_factory.mktemp("test-data") + mem_str = temp_dir / "sim_104.conf" + cfg = ConfigParser() + cfg['simulation'] = { + 'duration': '104', + 'parameter_change_times': '52', + 'stratify_entering_cohort': 'false' + } + + cfg['output'] = { + 'build_summary_stats': 'true', + 'save_state_history': 'true', + 'timesteps_to_report': '104', + } + + with mem_str.open('w') as configfile: + cfg.write(configfile) + + yield mem_str + + @pytest.mark.integration def test_simulation_run(setup_data): db_path, config_path = setup_data @@ -82,3 +213,243 @@ def test_simulation_run(setup_data): # state, admissions, ODs, FODs, background death assert len(histories) == 5 assert len(histories['state'].get_state_map()) >= 1 + + +@pytest.mark.integration +def test_simulation_run_104_midrun_parameter_increase( + setup_db_with_flat_midrun_change, + setup_db_with_midrun_change, + setup_config_104_change_52, +): + """Higher migration at t=52 should produce a larger final state than flat mid-run params.""" + flat_inp = rpy.data.Input( + db_path=setup_db_with_flat_midrun_change, + conf_path=setup_config_104_change_52, + ) + inp = rpy.data.Input( + db_path=setup_db_with_midrun_change, + conf_path=setup_config_104_change_52, + ) + + flat_migration_t52 = flat_inp.select_parameter( + rpy.data.Parameter(rpy.data.ParameterType.MIGRATION_COHORT), + cohort_id=1, + time=52, + ) + flat_migration_t1 = flat_inp.select_parameter( + rpy.data.Parameter(rpy.data.ParameterType.MIGRATION_COHORT), + cohort_id=1, + time=1, + ) + assert float(np.sum(flat_migration_t52)) == float( + np.sum(flat_migration_t1)) + + migration_t1 = inp.select_parameter( + rpy.data.Parameter(rpy.data.ParameterType.MIGRATION_COHORT), + cohort_id=1, + time=1, + ) + migration_t52 = inp.select_parameter( + rpy.data.Parameter(rpy.data.ParameterType.MIGRATION_COHORT), + cohort_id=1, + time=52, + ) + assert float(np.sum(migration_t52)) > float(np.sum(migration_t1)) + + sim_flat = rpy.build_simulation(flat_inp) + model_flat = sim_flat.get_model(0) + model_flat.create_default_histories() + sim_flat.run() + final_state_flat = model_flat.get_state() + + sim = rpy.build_simulation(inp) + model = sim.get_model(0) + model.create_default_histories() + sim.run() + final_state_changed = model.get_state() + + assert float(np.sum(final_state_changed)) > float(np.sum(final_state_flat)), ( + "Expected larger final population when migration inflow increases at " + "timestep 52." + ) + + +@pytest.fixture +def setup_db_for_numerical_check(tmp_path_factory, db_schema): + """Build deterministic dummy data for math-focused integration checks.""" + temp_dir = tmp_path_factory.mktemp("test-data") + db_path = temp_dir / "input_numerical.db" + + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + cursor.executescript(db_schema) + cursor.executescript(""" +INSERT INTO cohort (id, description, background_mortality_sample, behavior_transition_sample, initial_population_sample, intervention_transition_sample, overdose_sample, overdose_fatality_sample, population_change_sample, smr_sample) +VALUES (1, "Numerical Cohort", 1, 1, 1, 1, 1, 1, 1, 1); + +INSERT INTO intervention (id, name) +VALUES (1, "i1"), (2, "i2"); + +INSERT INTO behavior (id, name) +VALUES (1, "b1"), (2, "b2"); + +INSERT INTO initial_population (sample, intervention, behavior, count) +VALUES + (1, 1, 1, 100.0), + (1, 1, 2, 80.0), + (1, 2, 1, 60.0), + (1, 2, 2, 40.0); + +INSERT INTO population_change (sample, intervention, behavior, time, count) +VALUES + (1, 1, 1, 1, 1.0), + (1, 1, 2, 1, -2.0), + (1, 2, 1, 1, 3.0), + (1, 2, 2, 1, -1.0); + +INSERT INTO behavior_transition (sample, intervention, time, initial_behavior, new_behavior, probability) +VALUES + (1, 1, 1, 1, 1, 0.9), + (1, 1, 1, 1, 2, 0.1), + (1, 1, 1, 2, 1, 0.2), + (1, 1, 1, 2, 2, 0.8), + (1, 2, 1, 1, 1, 0.85), + (1, 2, 1, 1, 2, 0.15), + (1, 2, 1, 2, 1, 0.25), + (1, 2, 1, 2, 2, 0.75); + +INSERT INTO intervention_transition (sample, behavior, time, initial_intervention, new_intervention, probability) +VALUES + (1, 1, 1, 1, 1, 0.95), + (1, 1, 1, 1, 2, 0.05), + (1, 1, 1, 2, 1, 0.10), + (1, 1, 1, 2, 2, 0.90), + (1, 2, 1, 1, 1, 0.92), + (1, 2, 1, 1, 2, 0.08), + (1, 2, 1, 2, 1, 0.15), + (1, 2, 1, 2, 2, 0.85); + +INSERT INTO overdose (sample, intervention, behavior, time, probability) +VALUES + (1, 1, 1, 1, 0.05), + (1, 1, 2, 1, 0.04), + (1, 2, 1, 1, 0.03), + (1, 2, 2, 1, 0.02); + +INSERT INTO overdose_fatality (sample, intervention, behavior, time, probability) +VALUES + (1, 1, 1, 1, 0.20), + (1, 1, 2, 1, 0.10), + (1, 2, 1, 1, 0.15), + (1, 2, 2, 1, 0.25); + +INSERT INTO background_mortality (sample, time, probability) +VALUES (1, 1, 0.01); + +INSERT INTO smr (sample, intervention, behavior, time, ratio) +VALUES + (1, 1, 1, 1, 1.2), + (1, 1, 2, 1, 1.0), + (1, 2, 1, 1, 1.1), + (1, 2, 2, 1, 0.9); +""") + conn.commit() + conn.close() + + yield db_path + + +@pytest.fixture +def setup_config_one_executed_timestep(tmp_path_factory): + """Config for exactly one executed timestep. + + RESPOND records timestep 0 as the initial state, so duration 2 runs one + transition step. + """ + temp_dir = tmp_path_factory.mktemp("test-data") + config_path = temp_dir / "sim_one_step.conf" + cfg = ConfigParser() + cfg['simulation'] = { + 'duration': '2', + 'parameter_change_times': '2', + 'stratify_entering_cohort': 'false', + } + cfg['output'] = { + 'build_summary_stats': 'true', + 'save_state_history': 'true', + 'timesteps_to_report': '2', + } + + with config_path.open('w') as configfile: + cfg.write(configfile) + + yield config_path + + +@pytest.fixture +def setup_config_fifty_two_executed_timesteps(tmp_path_factory): + """Config for exactly fifty-two executed timesteps. + + RESPOND records timestep 0 as the initial state, so duration 53 runs + fifty-two transition steps. + """ + temp_dir = tmp_path_factory.mktemp("test-data") + config_path = temp_dir / "sim_52_steps.conf" + cfg = ConfigParser() + cfg['simulation'] = { + 'duration': '53', + 'parameter_change_times': '53', + 'stratify_entering_cohort': 'false', + } + cfg['output'] = { + 'build_summary_stats': 'true', + 'save_state_history': 'true', + 'timesteps_to_report': '53', + } + + with config_path.open('w') as configfile: + cfg.write(configfile) + + yield config_path + + +@pytest.mark.integration +def test_single_timestep_numerical_state_matches_expected( + setup_db_for_numerical_check, + setup_config_one_executed_timestep, +): + """Single-step run should match manually computed transition math.""" + inp = rpy.data.Input( + db_path=setup_db_for_numerical_check, + conf_path=setup_config_one_executed_timestep, + ) + sim = rpy.build_simulation(inp) + sim.run() + + final_state = sim.get_model(0).get_state() + expected_state = np.array([216.2933685, 0.0, 0.0, 210.24611685000002]) + + np.testing.assert_allclose( + final_state, expected_state, rtol=1e-10, atol=1e-10) + + +@pytest.mark.integration +def test_fifty_two_timestep_numerical_state_matches_expected( + setup_db_for_numerical_check, + setup_config_fifty_two_executed_timesteps, +): + """Fifty-two-step run should match manually computed transition math.""" + inp = rpy.data.Input( + db_path=setup_db_for_numerical_check, + conf_path=setup_config_fifty_two_executed_timesteps, + ) + sim = rpy.build_simulation(inp) + sim.run() + + final_state = sim.get_model(0).get_state() + expected_state = np.array( + [5.764271919791796e26, 0.0, 0.0, 5.6072656482224475e26] + ) + + np.testing.assert_allclose( + final_state, expected_state, rtol=1e-9, atol=1e-9) diff --git a/tests/test_smoke_bindings_runtime.py b/tests/test_smoke_bindings_runtime.py index 2bc027c..a39ee68 100644 --- a/tests/test_smoke_bindings_runtime.py +++ b/tests/test_smoke_bindings_runtime.py @@ -157,3 +157,43 @@ def test_binding_failure_messages_follow_expected_patterns() -> None: with pytest.raises(TypeError, match=r"(?i)incompatible constructor arguments"): _ = rpy.Simulation(1) # type: ignore[arg-type] + + +@pytest.mark.smoke +def test_timestep_add_transition_clones_input_transition() -> None: + """Timestep.add_transition should clone, not alias, the input transition.""" + timestep = rpy.Timestep() + source_transition = rpy.Transition("migration") + first_matrix = np.array([[0.5], [0.5], [0.0]]) + source_transition.add_matrix(first_matrix) + + timestep.add_transition(source_transition) + + second_matrix = np.array([[0.2], [0.3], [0.5]]) + source_transition.add_matrix(second_matrix) + + stored_transition = timestep.get_transition(0) + stored_matrices = stored_transition.get_matrices() + assert len(stored_matrices) == 1, ( + "Expected timestep-owned transition to remain independent after " + "mutating the caller-owned transition." + ) + np.testing.assert_allclose(stored_matrices[0], first_matrix) + + +@pytest.mark.smoke +def test_timestep_index_access_supports_get_and_set() -> None: + """Timestep index operators should expose mutable get/set semantics.""" + timestep = rpy.Timestep() + timestep.create_transition("migration") + timestep.create_transition("behavior") + + replacement = rpy.Transition("overdose", "overdose") + timestep[1] = replacement + + assert timestep[0].get_name() == "migration", ( + "Expected __getitem__ to return transition by index." + ) + assert timestep[1].get_name() == replacement.get_name(), ( + "Expected __setitem__ to replace transition slot by index." + ) diff --git a/uv.lock b/uv.lock index d06f42d..b193197 100644 --- a/uv.lock +++ b/uv.lock @@ -1721,6 +1721,7 @@ dev = [ { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx-book-theme" }, { name = "sphinx-copybutton" }, + { name = "sphinxcontrib-mermaid" }, ] docs = [ { name = "ipython" }, @@ -1730,6 +1731,7 @@ docs = [ { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx-book-theme" }, { name = "sphinx-copybutton" }, + { name = "sphinxcontrib-mermaid" }, ] github = [ { name = "cloudpickle" }, @@ -1779,6 +1781,7 @@ dev = [ { name = "sphinx", specifier = ">=4.0" }, { name = "sphinx-book-theme", specifier = ">=0.0.33" }, { name = "sphinx-copybutton" }, + { name = "sphinxcontrib-mermaid", specifier = ">=2.1.0" }, ] docs = [ { name = "ipython" }, @@ -1787,6 +1790,7 @@ docs = [ { name = "sphinx", specifier = ">=4.0" }, { name = "sphinx-book-theme", specifier = ">=0.0.33" }, { name = "sphinx-copybutton" }, + { name = "sphinxcontrib-mermaid", specifier = ">=2.1.0" }, ] github = [ { name = "cloudpickle" }, @@ -2120,6 +2124,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, ] +[[package]] +name = "sphinxcontrib-mermaid" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "pyyaml" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/9d/bf3a48a657682c7e0445c71d916bca7ab8194454276cc22b16e7f974e502/sphinxcontrib_mermaid-2.1.0.tar.gz", hash = "sha256:13c5f9ac395cb6abf403eca34e228dc9fb3a30c9d960dbf3e40e9a8cef969549", size = 21695, upload-time = "2026-07-18T23:08:11.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/bd/d3348d62296e73a91420f0edf671450c9003ecd8704da40b1e3d9b868459/sphinxcontrib_mermaid-2.1.0-py3-none-any.whl", hash = "sha256:417cd144ec4b28852f46ba653f02ce8e538881c812111671a4c30344e87f2112", size = 16190, upload-time = "2026-07-18T23:08:10.098Z" }, +] + [[package]] name = "sphinxcontrib-qthelp" version = "2.0.0" From b14afd0a367c75616870299ec9c758dbc190d37a Mon Sep 17 00:00:00 2001 From: Matthew Carroll <28577806+MJC598@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:19:03 -0400 Subject: [PATCH 4/6] fixing cibuildwheel and bumping to respond release v2.5.1 --- CMakeLists.txt | 2 +- pyproject.toml | 6 +++--- ...t_smoke_stubs_mypy.py => test_typing_stubs_mypy.py} | 10 +++++----- uv.lock | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) rename tests/{test_smoke_stubs_mypy.py => test_typing_stubs_mypy.py} (94%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 646e5d7..f76f82d 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 01c44f2e4475f8e6e1b694a39563fc002c6fd5bb # dev + GIT_TAG 2bc260ef749a76b7eba73a4d64e9d38c48ed2bdc # v2.5.1 OVERRIDE_FIND_PACKAGE ) set(RESPOND_BUILD_DOCS OFF) diff --git a/pyproject.toml b/pyproject.toml index 89aee2d..b77ad3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build" [project] name = "respondpy" -version = "0.2.3" +version = "0.3.0" description = "The Syndemic Lab's RESPOND Simulation Python Extension Module." readme = "README.md" requires-python = ">=3.11" @@ -121,7 +121,7 @@ filterwarnings = [ "default:could not create cache path:pytest.PytestCacheWarning", ] log_level = "INFO" -markers = ["smoke", "unit", "integration", "benchmark"] +markers = ["smoke", "unit", "integration", "benchmark", "typing"] required_plugins = ["pytest-benchmark"] [tool.coverage.run] @@ -245,7 +245,7 @@ build-frontend = "build[uv]" # Test Step Details test-groups = ["test"] test-sources = ["pyproject.toml", "tests"] -test-command = "python -m pytest tests" +test-command = "python -m pytest tests -m 'smoke or unit or integration'" test-skip = [ "cp31*-musllinux_*", # Threading test crashes on musllinux "cp313*", # polars-runtime-32 has issues with abi3 on 3.13 and above diff --git a/tests/test_smoke_stubs_mypy.py b/tests/test_typing_stubs_mypy.py similarity index 94% rename from tests/test_smoke_stubs_mypy.py rename to tests/test_typing_stubs_mypy.py index 9bfd92f..2216d5a 100644 --- a/tests/test_smoke_stubs_mypy.py +++ b/tests/test_typing_stubs_mypy.py @@ -4,7 +4,7 @@ # Created Date: 2026-07-22 # # Author: Matthew Carroll # # ----- # -# Last Modified: 2026-07-22 # +# Last Modified: 2026-07-29 # # Modified By: Matthew Carroll # # ----- # # Copyright (c) 2026 Syndemics Lab at Boston Medical Center # @@ -39,8 +39,8 @@ def _run_mypy(path: Path) -> subprocess.CompletedProcess[str]: ) -@pytest.mark.smoke -def test_mypy_smoke_positive_stub_usage(tmp_path: Path) -> None: +@pytest.mark.typing +def test_mypy_typing_positive_stub_usage(tmp_path: Path) -> None: """Typed usage matching stubs should pass mypy.""" test_file = tmp_path / "stub_smoke_positive.py" test_file.write_text( @@ -76,8 +76,8 @@ def test_mypy_smoke_positive_stub_usage(tmp_path: Path) -> None: ) -@pytest.mark.smoke -def test_mypy_smoke_negative_stub_misuse(tmp_path: Path) -> None: +@pytest.mark.typing +def test_mypy_typing_negative_stub_misuse(tmp_path: Path) -> None: """Typed misuse should fail mypy with informative message patterns.""" test_file = tmp_path / "stub_smoke_negative.py" test_file.write_text( diff --git a/uv.lock b/uv.lock index b193197..eb42421 100644 --- a/uv.lock +++ b/uv.lock @@ -1695,7 +1695,7 @@ wheels = [ [[package]] name = "respondpy" -version = "0.2.3" +version = "0.3.0" source = { editable = "." } dependencies = [ { name = "numpy" }, From 3701e50b70653dd87315184999814b5c0e6c4d91 Mon Sep 17 00:00:00 2001 From: Matthew Carroll <28577806+MJC598@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:26:22 -0400 Subject: [PATCH 5/6] Removing codecov because we don't want to buy seats --- .github/workflows/testing.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index ef24c23..8792b27 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -87,9 +87,3 @@ jobs: - name: Run tests with coverage run: uv run pytest --cov=respondpy --cov-report=term-missing --cov-report=xml tests - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} - slug: SyndemicsLab/respondpy From 23920284ff31d743aa6a73c81502cbf483639271 Mon Sep 17 00:00:00 2001 From: Matthew Carroll <28577806+MJC598@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:43:09 -0400 Subject: [PATCH 6/6] enforcing we pass by Eigen hard types so Refs don't create problems moving through the python boundary on OSes --- src/register_model.cpp | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/register_model.cpp b/src/register_model.cpp index b1e9c9b..7490dae 100644 --- a/src/register_model.cpp +++ b/src/register_model.cpp @@ -54,8 +54,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", &Model::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.") @@ -68,8 +74,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::SetState, 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 "