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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
37 changes: 37 additions & 0 deletions docs/source/how_to/cohort_subset_and_logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions docs/source/references/logging.md
Original file line number Diff line number Diff line change
@@ -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:
```
1 change: 1 addition & 0 deletions docs/source/references/wrapper_typing.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ For stepwise onboarding exercises, use [Tutorials](../tutorials/base_respond.md)
:maxdepth: 1

package
logging
data
build
cost_effectiveness
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions src/respondpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,6 +31,7 @@

__all__ = [
"data",
"logging",
"discount",
"cwise_product",
"cwise_min",
Expand Down
156 changes: 137 additions & 19 deletions src/respondpy/_core/logging.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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 #
Expand All @@ -20,6 +20,65 @@ __all__: list[str] = [
]


class LogType:
"""
Members:

kInfo

kWarn

kError

kDebug
"""
__members__: typing.ClassVar[dict[str, LogType]
# value = {'kInfo': <LogType.kInfo: 0>, 'kWarn': <LogType.kWarn: 1>, 'kError': <LogType.kError: 2>, 'kDebug': <LogType.kDebug: 3>}
]
kDebug: typing.ClassVar[LogType] # value = <LogType.kDebug: 3>
kError: typing.ClassVar[LogType] # value = <LogType.kError: 2>
kInfo: typing.ClassVar[LogType] # value = <LogType.kInfo: 0>
kWarn: typing.ClassVar[LogType] # value = <LogType.kWarn: 1>

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:
Expand All @@ -38,12 +97,12 @@ class CreationStatus:
]
# value = <CreationStatus.kError: -1>
kError: typing.ClassVar[CreationStatus]
# value = <CreationStatus.kSuccess: 0>
kSuccess: typing.ClassVar[CreationStatus]
# value = <CreationStatus.kExists: 1>
kExists: typing.ClassVar[CreationStatus]
# value = <CreationStatus.kNotCreated: 2>
kNotCreated: typing.ClassVar[CreationStatus]
# value = <CreationStatus.kSuccess: 0>
kSuccess: typing.ClassVar[CreationStatus]

def __eq__(self, other: typing.Any) -> bool:
...
Expand Down Expand Up @@ -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': <LogType.kInfo: 0>, 'kWarn': <LogType.kWarn: 1>, 'kError': <LogType.kError: 2>, 'kDebug': <LogType.kDebug: 3>}
]
kDebug: typing.ClassVar[LogType] # value = <LogType.kDebug: 3>
kError: typing.ClassVar[LogType] # value = <LogType.kError: 2>
kInfo: typing.ClassVar[LogType] # value = <LogType.kInfo: 0>
kWarn: typing.ClassVar[LogType] # value = <LogType.kWarn: 1>
__members__: typing.ClassVar[
dict[str, LogPattern]
# value = {'kSimple': <LogPattern.kSimple: 0>, 'kStandard': <LogPattern.kStandard: 1>, 'kDetailed': <LogPattern.kDetailed: 2>, 'kThreadSafe': <LogPattern.kThreadSafe: 3>}
]
# value = <LogPattern.kSimple: 0>
kSimple: typing.ClassVar[LogPattern]
# value = <LogPattern.kStandard: 1>
kStandard: typing.ClassVar[LogPattern]
# value = <LogPattern.kDetailed: 2>
kDetailed: typing.ClassVar[LogPattern]
# value = <LogPattern.kThreadSafe: 3>
kThreadSafe: typing.ClassVar[LogPattern]

def __eq__(self, other: typing.Any) -> bool:
...
Expand Down Expand Up @@ -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.
"""


Expand All @@ -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 = <LogType.kDebug: 3>
kError: CreationStatus # value = <CreationStatus.kError: -1>
kExists: CreationStatus # value = <CreationStatus.kExists: 1>
Expand Down
4 changes: 2 additions & 2 deletions src/respondpy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 #
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading