diff --git a/CMakeLists.txt b/CMakeLists.txt index 363650b..b2ca0df 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,6 +63,7 @@ FetchContent_Declare( respond GIT_REPOSITORY https://github.com/SyndemicsLab/respond.git GIT_TAG e9a452e7082b785978e66907b17af4db8b9bed53 # v2.4.1 + #dcba9320f04dcac0fd161bb86ed3f04a1c016b65 # v2.5.0 OVERRIDE_FIND_PACKAGE ) set(RESPOND_BUILD_DOCS OFF) diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..747ffb7 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..a3eaa05 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,36 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +import sys +from pathlib import Path + +# Add the src directory to the Python path +sys.path.insert(0, str(Path('../..', 'src').resolve())) + +project = 'respondpy' +copyright = '2026, Syndemics Lab at Boston Medical Center ' +author = 'Matthew Carroll, Dimitri Baptiste' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.napoleon', + 'myst_parser' +] + +templates_path = ['_templates'] +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'alabaster' +html_static_path = ['_static'] diff --git a/docs/source/explanations/architecture.md b/docs/source/explanations/architecture.md new file mode 100644 index 0000000..315b398 --- /dev/null +++ b/docs/source/explanations/architecture.md @@ -0,0 +1,97 @@ +# Architecture + +This page summarizes the public surface and runtime flow of respondpy. + +## Public API + +```mermaid +flowchart LR + subgraph Pkg[respondpy package] + data[data] + discount[discount] + cwise_product[cwise_product] + cwise_min[cwise_min] + calculate_life_years[calculate_life_years] + + 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] + + Transition[Transition] + transition_factory[transition_factory] + build_timestep_transition[build_timestep_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"] +``` + +## UML Library Flow + +```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 + + 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 +``` diff --git a/docs/source/how_to/data_loading.md b/docs/source/how_to/data_loading.md new file mode 100644 index 0000000..bc8e39c --- /dev/null +++ b/docs/source/how_to/data_loading.md @@ -0,0 +1,3 @@ +# How-To Load Data + +Loading data diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..8dc977f --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,22 @@ +.. respondpy documentation master file, created by + sphinx-quickstart on Wed Jul 15 14:57:47 2026. + You can adapt this file completely to your liking, but it should at least + 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. + +.. _RESPOND simulation model: https://github.com/SyndemicsLab/respond.git + +.. 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. + +.. toctree:: + :maxdepth: 2 + + explanations/architecture + how_to/data_loading + references/wrapper_typing + tutorials/base_respond \ No newline at end of file diff --git a/docs/source/references/wrapper_typing.md b/docs/source/references/wrapper_typing.md new file mode 100644 index 0000000..06fada1 --- /dev/null +++ b/docs/source/references/wrapper_typing.md @@ -0,0 +1,3 @@ +# Wrapper Typing + +How the wrapper typing works between Python and C++ diff --git a/docs/source/tutorials/base_respond.md b/docs/source/tutorials/base_respond.md new file mode 100644 index 0000000..90c3c5b --- /dev/null +++ b/docs/source/tutorials/base_respond.md @@ -0,0 +1,3 @@ +# Building and Running Base RESPOND + +Tutorial 1 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 6e776c5..a3e87e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ Issues = "https://github.com/SyndemicsLab/respond/issues" [dependency-groups] docs = [ "ipython", - "myst_parser>=0.13", + "myst-parser>=0.13", "nbsphinx", "sphinx-book-theme>=0.0.33", "sphinx>=4.0", @@ -78,6 +78,7 @@ build.verbose = true logging.level = "INFO" minimum-version = "build-system.requires" cmake.version = ">=3.27.0" +cmake.build-type = "Release" ninja.version = ">=1.11" sdist.exclude = [ ".github/*", @@ -87,7 +88,6 @@ sdist.exclude = [ sdist.include = [ "src/respondpy/_version.py" ] -cmake.build-type = "Release" messages.after-success = "{green}Wheel successfully built" messages.after-failure = """ {bold.red}Sorry{normal}, build failed. Your platform is {platform.platform}. @@ -119,11 +119,11 @@ filterwarnings = [ "default:could not create cache path:pytest.PytestCacheWarning", ] log_level = "INFO" -markers = ["smoke", "unit", "benchmark"] +markers = ["smoke", "unit", "integration", "benchmark"] required_plugins = ["pytest-benchmark"] [tool.coverage.run] -omit = ["tests/*"] +omit = ["tests/*", "docs/*", "benchmarks/*", "src/respondpy/_version.py"] [tool.coverage.report] exclude_also = [ diff --git a/src/respondpy/_core/simulation.pyi b/src/respondpy/_core/simulation.pyi index bf784df..36d75cb 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-16 # +# Last Modified: 2026-06-29 # # Modified By: Matthew Carroll # # ----- # # Copyright (c) 2026 Syndemics Lab at Boston Medical Center # diff --git a/src/respondpy/_utils.py b/src/respondpy/_utils.py index a975885..c943529 100644 --- a/src/respondpy/_utils.py +++ b/src/respondpy/_utils.py @@ -17,9 +17,21 @@ def str_to_int_list(config_string: str, *, delimiter: str = ',') -> list[int]: Whitespace around each token is stripped before conversion. - :param config_string: Delimited string of integer-like values. - :param delimiter: Token delimiter used to split ``config_string``. - :returns: Parsed integers in input order. - :raises ValueError: If any token cannot be converted to ``int``. + Parameters + ---------- + config_string : str + Delimited string of integer-like values. + delimiter : str, default="," + Token delimiter used to split ``config_string``. + + Returns + ------- + list of int + Parsed integers in input order. + + Raises + ------ + ValueError + If any token cannot be converted to ``int``. """ return [int(x.strip()) for x in config_string.split(delimiter)] diff --git a/src/respondpy/data/database_helpers.py b/src/respondpy/data/database_helpers.py index 9a5033c..3e08014 100644 --- a/src/respondpy/data/database_helpers.py +++ b/src/respondpy/data/database_helpers.py @@ -20,11 +20,24 @@ def _sort_state_vector( ) -> pl.LazyFrame: """Sort a state-vector dataframe by intervention and behavior ids. - :param lf: State-vector LazyFrame with ``intervention`` and ``behavior``. - :param behaviors: Ordered ``(id, name)`` behavior tuples. - :param interventions: Ordered ``(id, name)`` intervention tuples. - :returns: Sorted LazyFrame in deterministic state order. - :raises ValueError: If required state columns are missing. + Parameters + ---------- + lf : polars.LazyFrame + State-vector LazyFrame with ``intervention`` and ``behavior``. + behaviors : list of tuple of (int, str) + Ordered ``(id, name)`` behavior tuples. + interventions : list of tuple of (int, str) + Ordered ``(id, name)`` intervention tuples. + + Returns + ------- + polars.LazyFrame + Sorted LazyFrame in deterministic state order. + + Raises + ------ + ValueError + If required state columns are missing. """ s = lf.collect_schema().names() if 'intervention' not in s or 'behavior' not in s: @@ -51,11 +64,24 @@ def _sort_transition_matrix( ) -> pl.LazyFrame: """Sort a transition-matrix dataframe into deterministic state order. - :param lf: Transition LazyFrame with initial and next state columns. - :param behaviors: Ordered ``(id, name)`` behavior tuples. - :param interventions: Ordered ``(id, name)`` intervention tuples. - :returns: Sorted LazyFrame for stable downstream reshaping/comparison. - :raises ValueError: If required transition columns are missing. + Parameters + ---------- + lf : polars.LazyFrame + Transition LazyFrame with initial and next state columns. + behaviors : list of tuple of (int, str) + Ordered ``(id, name)`` behavior tuples. + interventions : list of tuple of (int, str) + Ordered ``(id, name)`` intervention tuples. + + Returns + ------- + polars.LazyFrame + Sorted LazyFrame for stable downstream reshaping/comparison. + + Raises + ------ + ValueError + If required transition columns are missing. """ if 'intervention' not in lf.columns or 'behavior' not in lf.columns or 'next_intervention' not in lf.columns or 'next_behavior' not in lf.columns: raise ValueError( @@ -90,10 +116,19 @@ def sort_dataframes( Dataframes with 3 columns are treated as state vectors, and dataframes with 4 columns as transition matrices. - :param lf: LazyFrame to sort. - :param behaviors: Ordered ``(id, name)`` behavior tuples. - :param interventions: Ordered ``(id, name)`` intervention tuples. - :returns: Sorted LazyFrame when shape is recognized, otherwise input. + Parameters + ---------- + lf : polars.LazyFrame + LazyFrame to sort. + behaviors : list of tuple of (int, str) + Ordered ``(id, name)`` behavior tuples. + interventions : list of tuple of (int, str) + Ordered ``(id, name)`` intervention tuples. + + Returns + ------- + polars.LazyFrame + Sorted LazyFrame when shape is recognized, otherwise input. """ if len(lf.collect_schema().names()) == 3: return _sort_state_vector(lf, behaviors, interventions) @@ -105,9 +140,17 @@ def sort_dataframes( def get_column_order(col_name: str, values: list[str]) -> str: """Get the SQL ORDER BY clause for a given column and list of values. - :param col_name: Column expression used in generated CASE conditions. - :param values: Ordered values representing desired sort priority. - :returns: SQL CASE expression suitable for ORDER BY clauses. + Parameters + ---------- + col_name : str + Column expression used in generated CASE conditions. + values : list of str + Ordered values representing desired sort priority. + + Returns + ------- + str + SQL CASE expression suitable for ORDER BY clauses. """ order_clause = "CASE\n" for idx, v in enumerate(values): diff --git a/src/respondpy/data/input.py b/src/respondpy/data/input.py index 381725d..d325968 100644 --- a/src/respondpy/data/input.py +++ b/src/respondpy/data/input.py @@ -4,7 +4,7 @@ # Created Date: 2026-06-05 # # Author: Matthew Carroll # # ----- # -# Last Modified: 2026-06-25 # +# Last Modified: 2026-07-16 # # Modified By: Matthew Carroll # # ----- # # Copyright (c) 2026 Syndemics Lab at Boston Medical Center # @@ -44,13 +44,25 @@ def __init__( ) -> None: """Create an Input data source from a base path or explicit files. - :param path: Directory containing both database and config files. - :param db_name: Database filename used when ``path`` is provided. - :param conf_name: Config filename used when ``path`` is provided. - :param db_path: Explicit database file path. - :param conf_path: Explicit config file path. - :raises ValueError: If required path arguments are incomplete. - :raises FileNotFoundError: If database or config files do not exist. + Parameters + ---------- + path : str or pathlib.Path, optional + Directory containing both database and config files. + db_name : str, default="input.db" + Database filename used when ``path`` is provided. + conf_name : str, default="sim.conf" + Config filename used when ``path`` is provided. + db_path : str or pathlib.Path, optional + Explicit database file path. + conf_path : str or pathlib.Path, optional + Explicit config file path. + + Raises + ------ + ValueError + If required path arguments are incomplete. + FileNotFoundError + If database or config files do not exist. """ if path is not None: if isinstance(path, str): @@ -216,15 +228,28 @@ def _extract_values( param: Parameter, lf: pl.LazyFrame, *, - n: int = 64 # 16 interventions * 4 behaviors + n: int = 64 ) -> Annotated[npt.NDArray[np.float64], "[m, 1] | [m, m]"]: """Convert extracted rows to a model-ready state vector or matrix. - :param param: Parameter descriptor controlling output shape. - :param lf: LazyFrame containing extracted values. - :param n: Number of states in the model. - :returns: ``(n, 1)`` state vector or ``(n, n)`` transition matrix. - :raises ValueError: If ``param`` cannot be mapped to either shape. + Parameters + ---------- + param : Parameter + Parameter descriptor controlling output shape. + lf : polars.LazyFrame + LazyFrame containing extracted values. + n : int, default=64 + Number of states in the model. (Default is 64 which corresponds to 16 interventions * 4 behaviors) + + Returns + ------- + numpy.typing.NDArray[numpy.float64] + ``(n, 1)`` state vector or ``(n, n)`` transition matrix. + + Raises + ------ + ValueError + If ``param`` cannot be mapped to either shape. """ val_col_name = param.get_value_column_name() if param.is_state_vector_operation(): @@ -247,9 +272,17 @@ def _zero_invalid_transitions( For intervention transitions, behavior changes are invalid. For behavior transitions, intervention changes are invalid. - :param param: Parameter descriptor identifying transition type. - :param transition_matrix: Transition rows to sanitize. - :returns: Transition dataframe with invalid rows forced to zero. + Parameters + ---------- + param : Parameter + Parameter descriptor identifying transition type. + transition_matrix : polars.DataFrame + Transition rows to sanitize. + + Returns + ------- + polars.DataFrame + Transition dataframe with invalid rows forced to zero. """ if param == ParameterType.INTERVENTION_TRANSITION_PROBABILITY: m = transition_matrix.with_columns( @@ -288,11 +321,24 @@ def _get_parameter_filled( Transition parameters are completed from a constant transition matrix, then normalized for retention probabilities. - :param param: Parameter descriptor to extract. - :param sample_id: Sample id selected from the cohort table. - :param time: Timestep used for time-varying parameters. - :returns: Complete and consistently ordered parameter rows. - :raises ValueError: If required transition state columns are missing. + Parameters + ---------- + param : Parameter + Parameter descriptor to extract. + sample_id : int, default=1 + Sample id selected from the cohort table. + time : int, default=1 + Timestep used for time-varying parameters. + + Returns + ------- + polars.LazyFrame + Complete and consistently ordered parameter rows. + + Raises + ------ + ValueError + If required transition state columns are missing. """ lf = self._select_parameter_raw(param, sample_id, time) if param == ParameterType.INTERVENTION_TRANSITION_PROBABILITY: @@ -390,7 +436,10 @@ def get_behaviors(self) -> list[str]: def get_state_names(self) -> list[tuple[str, str]]: """Return ordered state-name tuples as ``(intervention, behavior)``. - :returns: State label pairs sorted by intervention id then behavior id. + Returns + ------- + list of tuple of str + State label pairs sorted by intervention id then behavior id. """ if "combination" in self.states: return self.states["combination"] @@ -409,7 +458,10 @@ def get_state_names(self) -> list[tuple[str, str]]: def get_cohorts(self) -> tuple[list[str], list]: """Return raw cohort table data. - :returns: Tuple of column names and row tuples from ``cohort``. + Returns + ------- + tuple of (list of str, list) + Tuple of column names and row tuples from ``cohort``. """ stmt = "SELECT * FROM cohort;" col_names, results = self._connect_and_fetchall(stmt) @@ -424,7 +476,10 @@ def get_cohort_ids(self) -> list[int]: def insert_cohorts(self, data: list) -> None: """Insert cohort rows into the cohort table. - :param data: Row tuples matching cohort insert statement order. + Parameters + ---------- + data : list + Row tuples matching cohort insert statement order. """ sql_stmt = """ INSERT INTO cohort(description, background_mortality_sample, behavior_transition_sample, initial_population_sample, intervention_transition_sample, overdose_sample, overdose_fatality_sample, population_change_sample, smr_sample) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?) @@ -440,11 +495,21 @@ def select_parameter( ) -> np.ndarray: """Select parameter data for a cohort and optional timestep. - :param param: Parameter descriptor to extract. - :param cohort_id: Cohort id used to resolve sample ids. - :param time: Timestep for time-varying parameters. - :param raw: When ``True``, return raw table rows as numpy values. - :returns: Raw rows or model-ready shaped numpy array. + Parameters + ---------- + param : Parameter + Parameter descriptor to extract. + cohort_id : int, default=1 + Cohort id used to resolve sample ids. + time : int, default=1 + Timestep for time-varying parameters. + raw : bool, default=False + When ``True``, return raw table rows as numpy values. + + Returns + ------- + numpy.ndarray + Raw rows or model-ready shaped numpy array. """ sample_id = self._get_sample_id_for_parameter(param, cohort_id) if raw: @@ -464,7 +529,11 @@ def insert_parameter( ) -> None: """Insert parameter rows using parameter-specific SQL. - :param param: Parameter descriptor choosing target table and schema. - :param data: Row tuples matching ``param`` insert statement order. + Parameters + ---------- + param : Parameter + Parameter descriptor choosing target table and schema. + data : list + Row tuples matching ``param`` insert statement order. """ return self._connect_and_executemany(data, param.get_insert_statement()) diff --git a/src/respondpy/data/logic_conditions.py b/src/respondpy/data/logic_conditions.py index 84cb187..02bc82a 100644 --- a/src/respondpy/data/logic_conditions.py +++ b/src/respondpy/data/logic_conditions.py @@ -24,11 +24,21 @@ def verify_no_nulls( ) -> None: """Validate that a required value column contains no null values. - :param df: Data to validate. - :param sample_id: Sample id used only for error context. - :param p: Parameter type used only for error context. - :param col_to_check: Column expected to be fully populated. - :raises ValueError: If any null value is found. + Parameters + ---------- + df : polars.DataFrame + Data to validate. + sample_id : int + Sample id used only for error context. + p : ParameterType + Parameter type used only for error context. + col_to_check : str, default="probability" + Column expected to be fully populated. + + Raises + ------ + ValueError + If any null value is found. """ if df.select(pl.col(col_to_check).is_null().any()).item(): raise ValueError( @@ -44,11 +54,21 @@ def verify_no_duplicates( ) -> None: """Validate that key columns uniquely identify transition rows. - :param df: Data to validate. - :param key_columns: Columns that must uniquely identify each row. - :param sample_id: Sample id used only for error context. - :param p: Parameter type used only for error context. - :raises ValueError: If duplicated keys are found. + Parameters + ---------- + df : polars.DataFrame + Data to validate. + key_columns : list of str + Columns that must uniquely identify each row. + sample_id : int + Sample id used only for error context. + p : ParameterType + Parameter type used only for error context. + + Raises + ------ + ValueError + If duplicated keys are found. """ if df.select( pl.struct(key_columns).is_duplicated().any() @@ -64,10 +84,20 @@ def validate_time_list(ct_list: list[int]) -> list[int]: The value ``1`` is removed because timestep 1 is always explicitly built in model construction. - :param ct_list: Parsed integer values from - ``simulation.parameter_change_times``. - :returns: Validated and ascending change-time list. - :raises ValueError: If any value is less than or equal to zero. + Parameters + ---------- + ct_list : list of int + Parsed integer values from ``simulation.parameter_change_times``. + + Returns + ------- + list of int + Validated and ascending change-time list. + + Raises + ------ + ValueError + If any value is less than or equal to zero. """ if any(num <= 0 for num in ct_list): raise ValueError( diff --git a/src/respondpy/data/parameters.py b/src/respondpy/data/parameters.py index d66f40a..0905ec1 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-06-17 # +# Last Modified: 2026-07-16 # # Modified By: Matthew Carroll # # ----- # # Copyright (c) 2026 Syndemics Lab at Boston Medical Center # @@ -37,11 +37,26 @@ def __init__( ) -> None: """Create a parameter descriptor. - :param parameter_type: Enumerated parameter family. + Parameters + ---------- + parameter_type : ParameterType + Enumerated parameter family. """ self.__parameter_type = parameter_type def __eq__(self, other) -> bool: + """Equal comparison operator comparing parameter types. + + Parameters + ---------- + other : Parameter + The other parameter to compare against. + + Returns + ------- + bool + True if the parameter types are equal, False otherwise. + """ if isinstance(other, Parameter): return self.__parameter_type == other.get_parameter_type() if isinstance(other, ParameterType): @@ -49,33 +64,71 @@ def __eq__(self, other) -> bool: return False def __repr__(self) -> str: + """The string representation of the object. + + Returns + ------- + str + The string representation of the object, including the parameter type. + """ return f"Parameter(parameter_type={self.__parameter_type})" def get_parameter_type(self) -> ParameterType: - """Return the wrapped parameter type.""" + """Return the wrapped parameter type. + + Returns + ------- + ParameterType + The enumerated parameter family. + """ return self.__parameter_type def is_time_varying(self) -> bool: - """Return whether this parameter is indexed by timestep.""" + """Return whether this parameter is indexed by timestep. + + Returns + ------- + bool + True if the parameter is time-varying, False otherwise. + """ if self.__parameter_type == ParameterType.INITIAL_COHORT: return False return True def is_transition_matrix_operation(self) -> bool: - """Return whether this parameter maps to transition-matrix data.""" + """Return whether this parameter maps to transition-matrix data. + + Returns + ------- + bool + True if the parameter is a transition-matrix operation, False otherwise. + """ if self.__parameter_type in [ParameterType.INTERVENTION_TRANSITION_PROBABILITY, ParameterType.BEHAVIOR_TRANSITION_PROBABILITY]: return True return False def is_state_vector_operation(self) -> bool: - """Return whether this parameter maps to state-vector data.""" + """Return whether this parameter maps to state-vector data. + + Returns + ------- + bool + True if the parameter is a state-vector operation, False otherwise. + """ return not self.is_transition_matrix_operation() def get_value_column_name(self) -> str: """Return the numeric value column name used for this parameter. - :returns: One of ``count``, ``probability``, or ``ratio``. - :raises ValueError: If the parameter type is not implemented. + Returns + ------- + str + One of ``count``, ``probability``, or ``ratio``. + + Raises + ------ + ValueError + If the parameter type is not implemented. """ match self.__parameter_type: case ParameterType.STANDARD_MORTALITY_RATIO: @@ -92,7 +145,10 @@ def get_value_column_name(self) -> str: def get_initial_state_column_name(self) -> str | None: """Return the origin-state column name for transition parameters. - :returns: Origin-state column name, or ``None`` for non-transition + Returns + ------- + str or None + Origin-state column name, or ``None`` for non-transition parameters. """ match self.__parameter_type: @@ -106,7 +162,10 @@ def get_initial_state_column_name(self) -> str | None: def get_next_state_column_name(self) -> str | None: """Return the destination-state column name for transition parameters. - :returns: Destination-state column name, or ``None`` for non-transition + Returns + ------- + str or None + Destination-state column name, or ``None`` for non-transition parameters. """ match self.__parameter_type: @@ -120,8 +179,15 @@ def get_next_state_column_name(self) -> str | None: def get_cohort_column_name(self) -> str: """Return cohort-table sample column used by this parameter. - :returns: Column name in the ``cohort`` table. - :raises ValueError: If the parameter type is not implemented. + Returns + ------- + str + Column name in the ``cohort`` table. + + Raises + ------ + ValueError + If the parameter type is not implemented. """ match self.__parameter_type: case ParameterType.INITIAL_COHORT: @@ -148,8 +214,15 @@ def get_cohort_column_name(self) -> str: def get_insert_statement(self) -> str: """Return the INSERT SQL template for this parameter table. - :returns: Parameter-specific SQL INSERT statement. - :raises ValueError: If the parameter type is not implemented. + Returns + ------- + str + Parameter-specific SQL INSERT statement. + + Raises + ------ + ValueError + If the parameter type is not implemented. """ match self.__parameter_type: case ParameterType.INITIAL_COHORT: @@ -178,10 +251,22 @@ def get_select_statement( ) -> str: """Return parameter-specific SELECT SQL with deterministic ordering. - :param interventions: Ordered intervention names used in ``ORDER BY``. - :param behaviors: Ordered behavior names used in ``ORDER BY``. - :returns: SQL query string for parameter extraction. - :raises ValueError: If SELECT generation is not implemented. + Parameters + ---------- + interventions : list of str + Ordered intervention names used in ``ORDER BY``. + behaviors : list of str + Ordered behavior names used in ``ORDER BY``. + + Returns + ------- + str + SQL query string for parameter extraction. + + Raises + ------ + ValueError + If SELECT generation is not implemented. """ match self.__parameter_type: case ParameterType.INITIAL_COHORT: diff --git a/src/respondpy/data/state_vectors.py b/src/respondpy/data/state_vectors.py index 9ed516c..4310351 100644 --- a/src/respondpy/data/state_vectors.py +++ b/src/respondpy/data/state_vectors.py @@ -24,14 +24,25 @@ def build_constant_state_vector( ) -> pl.DataFrame: """Build a fully populated constant-valued state vector table. - :param interventions: Ordered intervention names. - :param behaviors: Ordered behavior names. - :param sample_id: Sample identifier written into output rows. - :param time: Timestep written into output rows. - :param value_column: Name of the generated value column. - :param constant: Constant value assigned to every state row. - :returns: Cross-product dataframe with one row per intervention-behavior - state. + Parameters + ---------- + interventions : list + Ordered intervention names. + behaviors : list + Ordered behavior names. + sample_id : int, default=1 + Sample identifier written into output rows. + time : int, default=1 + Timestep written into output rows. + value_column : str, default="count" + Name of the generated value column. + constant : float, default=0.0 + Constant value assigned to every state row. + + Returns + ------- + polars.DataFrame + Cross-product dataframe with one row per intervention-behavior state. """ inter = pl.DataFrame({"intervention": interventions}) diff --git a/src/respondpy/data/transition_matrices.py b/src/respondpy/data/transition_matrices.py index 9120e97..d37c922 100644 --- a/src/respondpy/data/transition_matrices.py +++ b/src/respondpy/data/transition_matrices.py @@ -13,6 +13,7 @@ import polars as pl import numpy as np + def _require_columns( frame: pl.LazyFrame | pl.DataFrame, columns: list[str] ) -> None: @@ -93,13 +94,28 @@ def build_constant_transition( ) -> pl.LazyFrame: """Build a complete constant-valued transition matrix table. - :param interventions: Ordered intervention names. - :param behaviors: Ordered behavior names. - :param sample_id: Sample identifier written into output rows. - :param time: Timestep written into output rows. - :param constant: Constant probability assigned before normalization. - :returns: LazyFrame containing all from/to state combinations. - :raises ValueError: If the generated matrix is not square. + Parameters + ---------- + interventions : list of str + Ordered intervention names. + behaviors : list of str + Ordered behavior names. + sample_id : int, default=1 + Sample identifier written into output rows. + time : int, default=1 + Timestep written into output rows. + constant : float, default=0.0 + Constant probability assigned before normalization. + + Returns + ------- + polars.LazyFrame + LazyFrame containing all from/to state combinations. + + Raises + ------ + ValueError + If the generated matrix is not square. """ init_behav = pl.LazyFrame({"initial_behavior": behaviors}) new_behav = pl.LazyFrame({"new_behavior": behaviors}) @@ -149,11 +165,24 @@ def combine_dataframes( Values in ``raw_data_df`` take precedence over template values where keys match. - :param complete_df: Fully enumerated dataframe used as fallback. - :param raw_data_df: Observed values to merge into template. - :param value_col: Name of numeric value column to collapse. - :returns: Combined LazyFrame with a single ``value_col``. - :raises ValueError: If join key columns are incompatible. + Parameters + ---------- + complete_df : polars.LazyFrame + Fully enumerated dataframe used as fallback. + raw_data_df : polars.LazyFrame + Observed values to merge into template. + value_col : str, default="probability" + Name of numeric value column to collapse. + + Returns + ------- + polars.LazyFrame + Combined LazyFrame with a single ``value_col``. + + Raises + ------ + ValueError + If join key columns are incompatible. """ join_cols = raw_data_df.collect_schema().names() join_cols.remove(value_col) @@ -194,17 +223,34 @@ def update_retention_probability( Retention rows are those where origin-state columns equal destination-state columns. Their values are replaced by ``1 - sum(non_retention)`` per group. - :param transition_matrix: Transition rows to normalize. - :param transition_columns: Origin-state column(s). - :param new_columns: Destination-state column(s). - :param probability_column: Probability column to update. - :param group_columns: Grouping keys defining one origin state/time/sample. - :param unique_key_columns: Columns expected to uniquely identify rows. - :param tolerance: Floating-point tolerance for validation checks. - :param forbid_negative_retention: If ``True``, reject negative retention. - :returns: Transition dataframe with updated retention probabilities. - :raises ValueError: If schema validation fails or probabilities cannot be - normalized. + Parameters + ---------- + transition_matrix : polars.LazyFrame or polars.DataFrame + Transition rows to normalize. + transition_columns : str or list of str + Origin-state column(s). + new_columns : str or list of str + Destination-state column(s). + probability_column : str, default="probability" + Probability column to update. + group_columns : list of str, optional + Grouping keys defining one origin state/time/sample. + unique_key_columns : list of str, optional + Columns expected to uniquely identify rows. + tolerance : float, default=1e-12 + Floating-point tolerance for validation checks. + forbid_negative_retention : bool, default=True + If ``True``, reject negative retention. + + Returns + ------- + polars.DataFrame + Transition dataframe with updated retention probabilities. + + Raises + ------ + ValueError + If schema validation fails or probabilities cannot be normalized. """ if group_columns is None or unique_key_columns is None: group_cols, from_cols, to_cols, constraints = _default_transition_shape( @@ -334,15 +380,25 @@ def verify_transition_probability( ) -> bool: """Check that transition probabilities sum to one within each group. - :param transition_matrix: Transition rows to verify. - :param transition_columns: Origin-state column(s), used with grouping. - :param probability_column: Probability column to sum. - :param group_columns: Explicit grouping keys. If omitted, defaults are - inferred. - :param unique_key_columns: Explicit unique-key columns. If omitted, - defaults are inferred. - :param tolerance: Absolute tolerance for checking sums against ``1.0``. - :returns: ``True`` when all transition groups sum to one. + Parameters + ---------- + transition_matrix : polars.DataFrame + Transition rows to verify. + transition_columns : str or list of str + Origin-state column(s), used with grouping. + probability_column : str, default="probability" + Probability column to sum. + group_columns : list of str, optional + Explicit grouping keys. If omitted, defaults are inferred. + unique_key_columns : list of str, optional + Explicit unique-key columns. If omitted, defaults are inferred. + tolerance : float, default=1e-12 + Absolute tolerance for checking sums against ``1.0``. + + Returns + ------- + bool + ``True`` when all transition groups sum to one. """ if group_columns is None or unique_key_columns is None: group_cols, _, _, constraints = _default_transition_shape( diff --git a/src/respondpy/model.py b/src/respondpy/model.py index 19589a1..157c33d 100644 --- a/src/respondpy/model.py +++ b/src/respondpy/model.py @@ -33,11 +33,21 @@ def build_model( ) -> Model: """Build a Model with initialized state and configured transitions. - :param input_data: Loaded input data and simulation configuration. - :param cohort_id: Cohort identifier used to resolve sampled parameters. - :param name: Model name passed to the core model constructor. - :param log_name: Logger name used by the underlying core model. - :returns: A model ready to be added to a simulation. + 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( @@ -58,10 +68,19 @@ def build_model_transitions( transition blocks are either copied or rebuilt at configured ``parameter_change_times`` values. - :param model: Model instance to mutate. - :param input_data: Loaded input data and simulation configuration. - :param cohort_id: Cohort identifier used to resolve sampled parameters. - :returns: The same model instance, with transitions appended. + 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 @@ -91,9 +110,17 @@ def add_transitions_to_model( ) -> Model: """Append one timestep's transitions to a model. - :param model: Model to update. - :param t_transition: Transition objects for one simulation timestep. - :returns: The same model instance, for chaining. + 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) diff --git a/src/respondpy/simulation.py b/src/respondpy/simulation.py index bbce8cf..383bf73 100644 --- a/src/respondpy/simulation.py +++ b/src/respondpy/simulation.py @@ -28,10 +28,24 @@ def build_simulation( ) -> Simulation: """Build a simulation containing one model per cohort id. - :param cohort_ids: Cohort identifiers to include in the simulation. - :param input_data: Loaded input data and simulation configuration. - :param log_name: Logger name used by the underlying core simulation/model. - :returns: A simulation object populated with cohort-specific models. + 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: diff --git a/src/respondpy/transition.py b/src/respondpy/transition.py index 376efff..3bdcb52 100644 --- a/src/respondpy/transition.py +++ b/src/respondpy/transition.py @@ -31,10 +31,19 @@ def transition_factory( ) -> Transition: """Create a transition and load its ordered transition matrices. - :param name: Transition name used by the core model. - :param tran_matrices: Matrix/vector operands consumed in execution order. - :param log_name: Logger name used by the underlying core transition. - :returns: A transition ready to be attached to a model. + 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: @@ -52,10 +61,19 @@ def build_timestep_transition( The returned transitions are: migration, intervention change, behavior change, overdose, and background mortality. - :param timestep: Simulation timestep to sample. - :param input_data: Loaded input data and simulation configuration. - :param cohort_id: Cohort identifier used to resolve sampled parameters. - :returns: Transition list for exactly one model timestep. + 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( diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..b0c1cc7 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,84 @@ +################################################################################ +# File: test_integration.py # +# Project: respondpy # +# Created Date: 2026-06-29 # +# Author: Matthew Carroll # +# ----- # +# Last Modified: 2026-07-16 # +# Modified By: Matthew Carroll # +# ----- # +# Copyright (c) 2026 Syndemics Lab at Boston Medical Center # +################################################################################ + +import sqlite3 +from configparser import ConfigParser +import pytest + +import respondpy as rpy + + +@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 + + Yields: + _type_: _description_ + """ + temp_dir = tmp_path_factory.mktemp("test-data") + mem_str = temp_dir / "input.db" + conn = sqlite3.connect(mem_str) + cursor = conn.cursor() + cursor.executescript(db_schema) + cursor.executescript(insert_complete_sample) + conn.commit() + conn.close() + yield mem_str + + +@pytest.fixture +def setup_config(tmp_path_factory): + temp_dir = tmp_path_factory.mktemp("test-data") + mem_str = temp_dir / "sim.conf" + cfg = ConfigParser() + cfg['simulation'] = { + 'duration': '52', + 'parameter_change_times': '52', + 'stratify_entering_cohort': 'false' + } + + cfg['output'] = { + 'build_summary_stats': 'true', + 'save_state_history': 'true', + 'timesteps_to_report': '52', + } + + with mem_str.open('w') as configfile: + cfg.write(configfile) + + yield mem_str + + +@pytest.fixture +def setup_data(setup_db, setup_config): + """Pytest fixture to setup the data + + Args: + setup_db (_type_): _description_ + setup_config (_type_): _description_ + + Yields: + _type_: path to database and a ConfigParser + """ + yield setup_db, setup_config + + +@pytest.mark.integration +def test_simulation_run(setup_data): + db_path, config_path = 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'] + # state, admissions, ODs, FODs, background death + assert len(histories) == 5 + assert len(histories['state']) == 2 diff --git a/uv.lock b/uv.lock index 256a5e3..f1bd214 100644 --- a/uv.lock +++ b/uv.lock @@ -1508,7 +1508,7 @@ wheels = [ [[package]] name = "respondpy" -version = "0.2.2" +version = "0.2.3" source = { editable = "." } dependencies = [ { name = "numpy" },