From ecd2fc42ed21ddccd91f46ad317b6ae9efd226c9 Mon Sep 17 00:00:00 2001 From: Matthew Carroll <28577806+MJC598@users.noreply.github.com> Date: Tue, 4 Aug 2026 12:37:08 -0400 Subject: [PATCH 1/5] duration issues --- src/respondpy/build.py | 4 ++-- src/respondpy/data/input.py | 11 ++++++++-- tests/test_integration.py | 24 ++++++++++----------- tests/test_model.py | 43 ++++++++++++++++++++++++++++++++++--- tests/test_simulation.py | 6 +++--- uv.lock | 2 +- 6 files changed, 67 insertions(+), 23 deletions(-) diff --git a/src/respondpy/build.py b/src/respondpy/build.py index 160b871..01cf29c 100644 --- a/src/respondpy/build.py +++ b/src/respondpy/build.py @@ -4,7 +4,7 @@ # Created Date: 2026-07-23 # # Author: Matthew Carroll # # ----- # -# Last Modified: 2026-07-29 # +# Last Modified: 2026-08-04 # # Modified By: Matthew Carroll # # ----- # # Copyright (c) 2026 Syndemics Lab at Boston Medical Center # @@ -117,7 +117,7 @@ def build_model( duration = int(input_data.config.get("simulation", "duration")) schedule_times = [1, *change_times] - for model_timestep in range(1, duration): + for model_timestep in range(1, duration+1): parameter_time = max(t for t in schedule_times if t <= model_timestep) model.add_timestep(build_timestep( input_data, diff --git a/src/respondpy/data/input.py b/src/respondpy/data/input.py index acaa36e..04f5ebe 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-07-16 # +# Last Modified: 2026-08-04 # # Modified By: Matthew Carroll # # ----- # # Copyright (c) 2026 Syndemics Lab at Boston Medical Center # @@ -88,7 +88,6 @@ def __init__( raise FileNotFoundError( f"Config file not found at {self._conf_path}!") - self._db_path = db_path self._connection = sqlite3.connect(str(self._db_path)) self._config = ConfigParser() @@ -214,6 +213,14 @@ def _select_parameter_raw( else: cols, vals = self._connect_and_fetchall( stmt, (str(sample_id), str(time))) + if len(vals) == 0: + + raise ValueError( + "Missing time-varying parameter rows in database: " + f"parameter={param.get_parameter_name()}, " + f"sample_id={sample_id}, time={time}. " + "Expected rows for this configured timestep but found none." + ) lzdf = pl.LazyFrame( vals, schema=cols, orient='row' ).with_columns( diff --git a/tests/test_integration.py b/tests/test_integration.py index fa9ed21..70c37d2 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -4,7 +4,7 @@ # Created Date: 2026-06-29 # # Author: Matthew Carroll # # ----- # -# Last Modified: 2026-07-16 # +# Last Modified: 2026-08-04 # # Modified By: Matthew Carroll # # ----- # # Copyright (c) 2026 Syndemics Lab at Boston Medical Center # @@ -42,15 +42,15 @@ def setup_config(tmp_path_factory): mem_str = temp_dir / "sim.conf" cfg = ConfigParser() cfg['simulation'] = { - 'duration': '52', - 'parameter_change_times': '52', + 'duration': '1', + 'parameter_change_times': '1', 'stratify_entering_cohort': 'false' } cfg['output'] = { 'build_summary_stats': 'true', 'save_state_history': 'true', - 'timesteps_to_report': '52', + 'timesteps_to_report': '1', } with mem_str.open('w') as configfile: @@ -363,21 +363,21 @@ def setup_db_for_numerical_check(tmp_path_factory, db_schema): 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 + RESPOND records timestep 0 as the initial state, so duration 1 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', + 'duration': '1', + 'parameter_change_times': '1', 'stratify_entering_cohort': 'false', } cfg['output'] = { 'build_summary_stats': 'true', 'save_state_history': 'true', - 'timesteps_to_report': '2', + 'timesteps_to_report': '1', } with config_path.open('w') as configfile: @@ -390,21 +390,21 @@ def setup_config_one_executed_timestep(tmp_path_factory): 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 + RESPOND records timestep 0 as the initial state, so duration 52 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', + 'duration': '52', + 'parameter_change_times': '1', 'stratify_entering_cohort': 'false', } cfg['output'] = { 'build_summary_stats': 'true', 'save_state_history': 'true', - 'timesteps_to_report': '53', + 'timesteps_to_report': '52', } with config_path.open('w') as configfile: diff --git a/tests/test_model.py b/tests/test_model.py index f26bd3c..41b64ab 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -44,15 +44,15 @@ def setup_config(tmp_path_factory): mem_str = temp_dir / "sim.conf" cfg = ConfigParser() cfg['simulation'] = { - 'duration': '52', - 'parameter_change_times': '52', + 'duration': '1', + 'parameter_change_times': '1', 'stratify_entering_cohort': 'false' } cfg['output'] = { 'build_summary_stats': 'true', 'save_state_history': 'true', - 'timesteps_to_report': '52', + 'timesteps_to_report': '1', } with mem_str.open('w') as configfile: @@ -75,6 +75,29 @@ def setup_data(setup_db, setup_config): yield setup_db, setup_config +@pytest.fixture +def setup_config_missing_change_time(tmp_path_factory): + temp_dir = tmp_path_factory.mktemp("test-data") + mem_str = temp_dir / "sim_missing_change_time.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.mark.unit def test_build_model(setup_data) -> None: db, cfg = setup_data @@ -84,6 +107,20 @@ def test_build_model(setup_data) -> None: assert isinstance(m, rpy.Model) +@pytest.mark.unit +def test_build_model_raises_when_change_time_rows_missing( + setup_db, + setup_config_missing_change_time, +) -> None: + inp = rpy.data.Input(db_path=setup_db, conf_path=setup_config_missing_change_time) + + with pytest.raises( + ValueError, + match=r"Missing time-varying parameter rows in database: .*time=52", + ): + rpy.build_simulation(inp, cohort_ids=[1]) + + @pytest.mark.unit def test_model_default_histories_can_be_created() -> None: model = rpy.Model("markov") diff --git a/tests/test_simulation.py b/tests/test_simulation.py index 8bac6e7..9e20bdf 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -46,15 +46,15 @@ def setup_config(tmp_path_factory): mem_str = temp_dir / "sim.conf" cfg = ConfigParser() cfg['simulation'] = { - 'duration': '52', - 'parameter_change_times': '52', + 'duration': '1', + 'parameter_change_times': '1', 'stratify_entering_cohort': 'false' } cfg['output'] = { 'build_summary_stats': 'true', 'save_state_history': 'true', - 'timesteps_to_report': '52', + 'timesteps_to_report': '1', } with mem_str.open('w') as configfile: diff --git a/uv.lock b/uv.lock index eb42421..c92003a 100644 --- a/uv.lock +++ b/uv.lock @@ -1695,7 +1695,7 @@ wheels = [ [[package]] name = "respondpy" -version = "0.3.0" +version = "0.3.1" source = { editable = "." } dependencies = [ { name = "numpy" }, From f612dd7aae493e49dbcb179295453975d3fc332e Mon Sep 17 00:00:00 2001 From: Matthew Carroll <28577806+MJC598@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:53:39 -0400 Subject: [PATCH 2/5] updating logging for inputs to log to the same file as the model --- README.md | 35 ++++ .../how_to/cohort_subset_and_logging.md | 37 +++++ docs/source/references/logging.md | 16 ++ docs/source/references/wrapper_typing.md | 1 + src/respondpy/__init__.py | 2 + src/respondpy/_core/logging.pyi | 156 +++++++++++++++--- src/respondpy/data/input.py | 23 ++- src/respondpy/logging.py | 53 ++++++ tests/test_data_input.py | 24 +++ 9 files changed, 326 insertions(+), 21 deletions(-) create mode 100644 docs/source/references/logging.md create mode 100644 src/respondpy/logging.py diff --git a/README.md b/README.md index e94bca8..dd16c38 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,41 @@ RESPOND is a simulation model developed by the Syndemics Lab at Boston Medical C This tool makes use of the popular tool [Pybind11](https://pybind11.readthedocs.io/en/stable/index.html). From here, we expose bindings for users to connect to via Python. +## Logging Across C++ and Python + +`respondpy` now exposes RESPOND logging through a top-level module, +`respondpy.logging`, so Python and C++ can write to the same logger/file +without reimplementing logging behavior. + +```python +from pathlib import Path + +import respondpy as rpy +from respondpy.data import Input + +base = Path("/path/to/respond-input") +log_name = "respond" +log_file = "respond.log" + +# Input can initialize the logger through the C++ backend. +input_data = Input(path=base, log_name=log_name, log_file=log_file) + +# Python-side logging uses the same RESPOND logger backend. +rpy.logging.log_info(log_name, "Loading input complete") +rpy.logging.log_warning(log_name, "Using fallback parameter for cohort 3") +rpy.logging.flush_all_loggers() +``` + +To support concurrent logging from multiple models/threads to one file, prefer +the shared sink APIs exposed by the same module: + +```python +import respondpy as rpy + +rpy.logging.create_shared_file_sink("respond.log") +rpy.logging.create_shared_logger("respond") +``` + ## Building and Installing The bindings are available on PyPI! They can be installed via `pip install respondpy`. diff --git a/docs/source/how_to/cohort_subset_and_logging.md b/docs/source/how_to/cohort_subset_and_logging.md index 635ea44..42590f2 100644 --- a/docs/source/how_to/cohort_subset_and_logging.md +++ b/docs/source/how_to/cohort_subset_and_logging.md @@ -44,6 +44,43 @@ simulation.run() print(len(simulation.get_model_names())) ``` +## Write Python logs to the same RESPOND log file + +```python +from pathlib import Path + +import respondpy as rpy +from respondpy.build import build_simulation +from respondpy.data import Input + +input_data = Input( + path=Path("/path/to/respond-input"), + log_name="respond", + log_file="respond.log", +) + +simulation = build_simulation( + input_data, + log_name="respond", + log_file="respond.log", +) + +rpy.logging.log_info("respond", "Starting simulation run from Python") +simulation.run() +rpy.logging.log_info("respond", "Simulation run complete") +rpy.logging.flush_all_loggers() +``` + +For concurrent logging to one file, use shared sink helpers before creating +runtime objects: + +```python +import respondpy as rpy + +rpy.logging.create_shared_file_sink("respond.log") +rpy.logging.create_shared_logger("respond") +``` + ## Fail fast on unknown cohorts ```python diff --git a/docs/source/references/logging.md b/docs/source/references/logging.md new file mode 100644 index 0000000..77b9e92 --- /dev/null +++ b/docs/source/references/logging.md @@ -0,0 +1,16 @@ +# Reference: Logging + +API reference for RESPOND logging functions exposed to Python. + +This module forwards directly to the C++ RESPOND logging backend, so Python and +C++ can write to the same logger and output file. + +See also: +- [How-To: Select Cohorts and Configure Logging](../how_to/cohort_subset_and_logging.md) +- [Reference: respondpy Package](package.md) + +```{automodule} respondpy.logging +:members: +:undoc-members: +:show-inheritance: +``` diff --git a/docs/source/references/wrapper_typing.md b/docs/source/references/wrapper_typing.md index a06eddb..47d7eef 100644 --- a/docs/source/references/wrapper_typing.md +++ b/docs/source/references/wrapper_typing.md @@ -12,6 +12,7 @@ For stepwise onboarding exercises, use [Tutorials](../tutorials/base_respond.md) :maxdepth: 1 package +logging data build cost_effectiveness diff --git a/src/respondpy/__init__.py b/src/respondpy/__init__.py index e14f739..36865d6 100644 --- a/src/respondpy/__init__.py +++ b/src/respondpy/__init__.py @@ -15,6 +15,7 @@ from ._version import version as __version__ # pylint: disable=E0611,E0401 # type: ignore[reportMissingModuleSource] from . import data +from . import logging from .cost_effectiveness import ( discount, cwise_product, cwise_min, calculate_life_years @@ -30,6 +31,7 @@ __all__ = [ "data", + "logging", "discount", "cwise_product", "cwise_min", diff --git a/src/respondpy/_core/logging.pyi b/src/respondpy/_core/logging.pyi index 78e47d7..9168d69 100644 --- a/src/respondpy/_core/logging.pyi +++ b/src/respondpy/_core/logging.pyi @@ -4,7 +4,7 @@ # Created Date: 2026-02-09 # # Author: Matthew Carroll # # ----- # -# Last Modified: 2026-06-01 # +# Last Modified: 2026-08-04 # # Modified By: Matthew Carroll # # ----- # # Copyright (c) 2026 Syndemics Lab at Boston Medical Center # @@ -20,6 +20,65 @@ __all__: list[str] = [ ] +class LogType: + """ + Members: + + kInfo + + kWarn + + kError + + kDebug + """ + __members__: typing.ClassVar[dict[str, LogType] + # value = {'kInfo': , 'kWarn': , 'kError': , 'kDebug': } + ] + kDebug: typing.ClassVar[LogType] # value = + kError: typing.ClassVar[LogType] # value = + kInfo: typing.ClassVar[LogType] # value = + kWarn: typing.ClassVar[LogType] # value = + + 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: + ... + + class CreationStatus: """ Members: @@ -38,12 +97,12 @@ class CreationStatus: ] # value = kError: typing.ClassVar[CreationStatus] + # value = + kSuccess: typing.ClassVar[CreationStatus] # value = kExists: typing.ClassVar[CreationStatus] # value = kNotCreated: typing.ClassVar[CreationStatus] - # value = - kSuccess: typing.ClassVar[CreationStatus] def __eq__(self, other: typing.Any) -> bool: ... @@ -84,25 +143,30 @@ class CreationStatus: ... -class LogType: +class LogPattern: """ Members: - kInfo + kSimple - kWarn + kStandard - kError + kDetailed - kDebug + kThreadSafe """ - __members__: typing.ClassVar[dict[str, LogType] - # value = {'kInfo': , 'kWarn': , 'kError': , 'kDebug': } - ] - kDebug: typing.ClassVar[LogType] # value = - kError: typing.ClassVar[LogType] # value = - kInfo: typing.ClassVar[LogType] # value = - kWarn: typing.ClassVar[LogType] # value = + __members__: typing.ClassVar[ + dict[str, LogPattern] + # value = {'kSimple': , 'kStandard': , 'kDetailed': , 'kThreadSafe': } + ] + # value = + kSimple: typing.ClassVar[LogPattern] + # value = + kStandard: typing.ClassVar[LogPattern] + # value = + kDetailed: typing.ClassVar[LogPattern] + # value = + kThreadSafe: typing.ClassVar[LogPattern] def __eq__(self, other: typing.Any) -> bool: ... @@ -149,15 +213,57 @@ def create_file_logger(arg0: str, arg1: str) -> CreationStatus: """ -def log_debug(arg0: str, arg1: str) -> None: +def create_shared_file_sink(arg0: str) -> CreationStatus: """ - Logs a debug message to the log. + Creates a shared file sink for use with RESPOND. """ -def log_error(arg0: str, arg1: str) -> None: +def create_shared_logger(arg0: str, arg1: str) -> CreationStatus: """ - Logs an error message to the log. + Creates a shared logger for use with RESPOND. + """ + + +def set_log_pattern(arg0: LogPattern) -> None: + """ + Sets the log pattern for all loggers. + """ + + +def get_log_pattern() -> LogPattern: + """ + Gets the log pattern for all loggers. + """ + + +def set_flush_interval(arg0: int) -> None: + """ + Sets the flush interval for all loggers. + """ + + +def flush_all_loggers() -> None: + """ + Flushes all loggers. + """ + + +def check_logger_exists(arg0: str) -> bool: + """ + Checks if a logger exists. + """ + + +def get_logger_info(arg0: str) -> tuple[LogType, str]: + """ + Gets the logger info for a logger. + """ + + +def set_logger_level(arg0: str, arg1: LogType) -> None: + """ + Sets the logger level for a logger. """ @@ -173,6 +279,18 @@ def log_warning(arg0: str, arg1: str) -> None: """ +def log_error(arg0: str, arg1: str) -> None: + """ + Logs an error message to the log. + """ + + +def log_debug(arg0: str, arg1: str) -> None: + """ + Logs a debug message to the log. + """ + + kDebug: LogType # value = kError: CreationStatus # value = kExists: CreationStatus # value = diff --git a/src/respondpy/data/input.py b/src/respondpy/data/input.py index 04f5ebe..e32599b 100644 --- a/src/respondpy/data/input.py +++ b/src/respondpy/data/input.py @@ -12,7 +12,7 @@ import sqlite3 from pathlib import Path -from typing import Literal, Annotated +from typing import Annotated, Literal from operator import itemgetter from configparser import ConfigParser @@ -20,6 +20,7 @@ import numpy.typing as npt import polars as pl +from .. import logging as rpy_logging from .database_helpers import sort_dataframes from .parameters import Parameter, ParameterType from .transition_matrices import build_constant_transition, update_retention_probability, combine_dataframes @@ -40,7 +41,9 @@ def __init__( db_name: str = "input.db", conf_name: str = "sim.conf", db_path: str | Path | None = None, - conf_path: str | Path | None = None + conf_path: str | Path | None = None, + log_name: str | None = "respond", + log_file: str | Path | None = None ) -> None: """Create an Input data source from a base path or explicit files. @@ -56,6 +59,12 @@ def __init__( Explicit database file path. conf_path : str or pathlib.Path, optional Explicit config file path. + log_name : str, optional + RESPOND logger name used for Python-side logging. Set to ``None`` to + disable logging from this Input instance. + log_file : str or pathlib.Path, optional + Optional logger output file. If provided, attempts to create the + logger through RESPOND's C++ logging backend. Raises ------ @@ -96,10 +105,20 @@ def __init__( self.states: dict[str, list] = {} self.interventions: list[str] | None = None self.behaviors: list[str] | None = None + self._log_name = log_name + self._log_file = str(log_file) if log_file is not None else None + + if self._log_name is not None and self._log_file is not None: + rpy_logging.create_file_logger(self._log_name, self._log_file) def __repr__(self) -> str: return f"Input(db_path={self._db_path}, conf_path={self._conf_path})" + @property + def log_name(self) -> str | None: + """Return the configured RESPOND logger name for this Input.""" + return self._log_name + @property def config(self) -> ConfigParser: """Return parsed simulation configuration.""" diff --git a/src/respondpy/logging.py b/src/respondpy/logging.py new file mode 100644 index 0000000..ff75712 --- /dev/null +++ b/src/respondpy/logging.py @@ -0,0 +1,53 @@ +################################################################################ +# File: logging.py # +# Project: respondpy # +# Created Date: 2026-08-04 # +# Author: Matthew Carroll # +# ----- # +# Last Modified: 2026-08-04 # +# Modified By: Matthew Carroll # +# ----- # +# Copyright (c) 2026 Syndemics Lab at Boston Medical Center # +################################################################################ + +from __future__ import annotations + +from ._core.logging import ( # pylint: disable=E0611,E0401 # type: ignore[reportMissingModuleSource] + LogType, + CreationStatus, + LogPattern, + create_file_logger, + create_shared_file_sink, + create_shared_logger, + set_log_pattern, + get_log_pattern, + set_flush_interval, + flush_all_loggers, + check_logger_exists, + get_logger_info, + set_logger_level, + log_info, + log_warning, + log_error, + log_debug, +) + +__all__: list[str] = [ + "LogType", + "CreationStatus", + "LogPattern", + "create_file_logger", + "create_shared_file_sink", + "create_shared_logger", + "set_log_pattern", + "get_log_pattern", + "set_flush_interval", + "flush_all_loggers", + "check_logger_exists", + "get_logger_info", + "set_logger_level", + "log_info", + "log_warning", + "log_error", + "log_debug", +] diff --git a/tests/test_data_input.py b/tests/test_data_input.py index a9b258e..009b9b9 100644 --- a/tests/test_data_input.py +++ b/tests/test_data_input.py @@ -11,6 +11,7 @@ ################################################################################ import sqlite3 +import uuid from configparser import ConfigParser from pathlib import Path @@ -18,6 +19,7 @@ import numpy as np import polars as pl +import respondpy as rpy import respondpy.data as rpydata @@ -368,3 +370,25 @@ def test_insert_parameter_adds_sample_row(input_data): sample_ids = input_data._get_sample_ids_by_table("initial_population") assert 2 in sample_ids + + +@pytest.mark.unit +def test_input_logging_writes_to_backend_file(setup_data) -> None: + db_path, config_path = setup_data + log_file = Path(db_path).parent / "respond-input.log" + logger_name = f"respond-input-{uuid.uuid4()}" + + input_data = rpydata.Input( + db_path=db_path, + conf_path=config_path, + log_name=logger_name, + log_file=log_file, + ) + rpy.logging.log_info(logger_name, "python-input-info-message") + rpy.logging.log_warning(logger_name, "python-input-warning-message") + rpy.logging.flush_all_loggers() + assert log_file.exists() + + contents = log_file.read_text() + assert "python-input-info-message" in contents + assert "python-input-warning-message" in contents From 9b308ba9865abc2b2e76184c52e1aa91305cbada Mon Sep 17 00:00:00 2001 From: Matthew Carroll <28577806+MJC598@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:59:58 -0400 Subject: [PATCH 3/5] adding logging to inputs --- src/respondpy/data/input.py | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/src/respondpy/data/input.py b/src/respondpy/data/input.py index e32599b..96d15f6 100644 --- a/src/respondpy/data/input.py +++ b/src/respondpy/data/input.py @@ -42,7 +42,7 @@ def __init__( conf_name: str = "sim.conf", db_path: str | Path | None = None, conf_path: str | Path | None = None, - log_name: str | None = "respond", + log_name: str = "respond", log_file: str | Path | None = None ) -> None: """Create an Input data source from a base path or explicit files. @@ -115,7 +115,7 @@ def __repr__(self) -> str: return f"Input(db_path={self._db_path}, conf_path={self._conf_path})" @property - def log_name(self) -> str | None: + def log_name(self) -> str: """Return the configured RESPOND logger name for this Input.""" return self._log_name @@ -141,9 +141,9 @@ def _check_valid_list(self, l: list[tuple], tuple_items: int) -> bool: def _connect_and_executemany(self, data: list[tuple], stmt: str) -> None: n_question_marks = stmt.count("?") if not self._check_valid_list(data, n_question_marks): - raise ValueError( - f"Data provided does not match the expected format for the SQL statement! Expected list of tuples with {n_question_marks} items each. Provided data: {data}" - ) + msg = f"Data provided does not match the expected format for the SQL statement! Expected list of tuples with {n_question_marks} items each. Provided data: {data}" + rpy_logging.log_error(self.log_name, msg) + raise ValueError(msg) con = self._get_connection() cur = con.cursor() cur.executemany(stmt, data) @@ -208,9 +208,9 @@ def _get_sample_id_for_parameter( stmt = f"SELECT {col_name} FROM cohort WHERE id = ?" _, result = self._connect_and_fetchall(stmt, (str(cohort_id),)) if len(result) == 0 or len(result[0]) == 0: - raise ValueError( - f"No sample ID found for parameter {param} and cohort ID {cohort_id}!" - ) + msg = f"No sample ID found for parameter {param} and cohort ID {cohort_id}!" + rpy_logging.log_error(self.log_name, msg) + raise ValueError(msg) return result[0][0] def _select_parameter_raw( @@ -233,13 +233,9 @@ def _select_parameter_raw( cols, vals = self._connect_and_fetchall( stmt, (str(sample_id), str(time))) if len(vals) == 0: - - raise ValueError( - "Missing time-varying parameter rows in database: " - f"parameter={param.get_parameter_name()}, " - f"sample_id={sample_id}, time={time}. " - "Expected rows for this configured timestep but found none." - ) + msg = f"Missing time-varying parameter rows in database: parameter={param.get_parameter_name()}, sample_id={sample_id}, time={time}. Expected rows for this configured timestep but found none." + rpy_logging.log_error(self.log_name, msg) + raise ValueError(msg) lzdf = pl.LazyFrame( vals, schema=cols, orient='row' ).with_columns( @@ -285,8 +281,9 @@ def _extract_values( return lf.select( pl.col(val_col_name) ).collect().to_numpy().reshape(n, n) - raise ValueError( - "Invalid parameter applied when attempting to extract parameters!") + msg = f"Invalid parameter applied when attempting to extract parameters! Parameter: {param}" + rpy_logging.log_error(self.log_name, msg) + raise ValueError(msg) def _zero_invalid_transitions( self, @@ -423,9 +420,9 @@ def _get_parameter_filled( init_col = param.get_initial_state_column_name() next_col = param.get_next_state_column_name() if init_col is None or next_col is None: - raise ValueError( - f"Parameter {param} is missing the initial or next state column names required for transition matrix operations!" - ) + msg = f"Parameter {param} is missing the initial or next state column names required for transition matrix operations!" + rpy_logging.log_error(self.log_name, msg) + raise ValueError(msg) res = self._zero_invalid_transitions(param, res.collect()) From 0c23d1dff649a8bdcfef28ec0448de33ccb5fbc4 Mon Sep 17 00:00:00 2001 From: Matthew Carroll <28577806+MJC598@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:05:27 -0400 Subject: [PATCH 4/5] bumping patch version --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 35f2220..c68e428 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build" [project] name = "respondpy" -version = "0.3.1" +version = "0.3.2" description = "The Syndemic Lab's RESPOND Simulation Python Extension Module." readme = "README.md" requires-python = ">=3.11" diff --git a/uv.lock b/uv.lock index c92003a..1174a3b 100644 --- a/uv.lock +++ b/uv.lock @@ -1695,7 +1695,7 @@ wheels = [ [[package]] name = "respondpy" -version = "0.3.1" +version = "0.3.2" source = { editable = "." } dependencies = [ { name = "numpy" }, From 42577f882ab0eda3392f6819ab6c1df467a05a6b Mon Sep 17 00:00:00 2001 From: Matthew Carroll <28577806+MJC598@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:08:17 -0400 Subject: [PATCH 5/5] nox please --- src/respondpy/data/input.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/respondpy/data/input.py b/src/respondpy/data/input.py index 96d15f6..72beb8b 100644 --- a/src/respondpy/data/input.py +++ b/src/respondpy/data/input.py @@ -34,6 +34,10 @@ class Input: and returns either raw rows or model-ready numpy arrays. """ + # pylint: disable=too-many-instance-attributes + # Nine attributes are reasonable here: database connection, config parser, + # state cache, intervention list, behavior list, and logging attributes. + def __init__( self, *,