simtrial 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
simtrial/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """
2
+ Public API for the simtrial package.
3
+ """
4
+
5
+ from .piecewise_exponential import PiecewiseExponential, set_random_seed
6
+
7
+ __all__ = ["PiecewiseExponential", "set_random_seed"]
@@ -0,0 +1,154 @@
1
+ """
2
+ Tools for working with the piecewise exponential distribution.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import math
8
+ from dataclasses import dataclass
9
+ from typing import Sequence, cast
10
+
11
+ import numpy as np
12
+ from numpy.typing import NDArray
13
+
14
+ _RANDOM_STATE: np.random.Generator | None = None
15
+
16
+
17
+ def _default_rng() -> np.random.Generator:
18
+ """
19
+ Return a module-level random number generator.
20
+
21
+ Returns:
22
+ The reusable random number generator for piecewise exponential sampling.
23
+ """
24
+
25
+ global _RANDOM_STATE
26
+ if _RANDOM_STATE is None:
27
+ _reset_rng()
28
+ return cast(np.random.Generator, _RANDOM_STATE)
29
+
30
+
31
+ def _reset_rng(seed: int | None = None) -> None:
32
+ """
33
+ Initialize the module-level random number generator.
34
+
35
+ Args:
36
+ seed: Seed used to reset the generator.
37
+
38
+ Returns:
39
+ Nothing.
40
+ """
41
+
42
+ global _RANDOM_STATE
43
+ _RANDOM_STATE = np.random.default_rng(seed)
44
+
45
+
46
+ def set_random_seed(seed: int) -> None:
47
+ """
48
+ Set the seed for the module-level random number generator.
49
+
50
+ Args:
51
+ seed: Seed value that controls reproducibility.
52
+
53
+ Returns:
54
+ Nothing.
55
+ """
56
+
57
+ _reset_rng(seed=seed)
58
+
59
+
60
+ @dataclass
61
+ class PiecewiseExponential:
62
+ """
63
+ Piecewise exponential sampler based on the inverse cumulative distribution.
64
+
65
+ Args:
66
+ durations: Interval durations for each hazard rate segment; all but the
67
+ final duration must be finite and strictly positive, while the last
68
+ entry may be set to ``math.inf`` to represent an open-ended tail.
69
+ rates: Hazard rates aligned with the provided durations.
70
+ rng: Optional random number generator.
71
+
72
+ Raises:
73
+ ValueError: Raised when inputs are invalid.
74
+ """
75
+
76
+ durations: Sequence[float]
77
+ rates: Sequence[float]
78
+ rng: np.random.Generator | None = None
79
+
80
+ def __post_init__(self) -> None:
81
+ """
82
+ Validate inputs and precompute cumulative quantities.
83
+
84
+ Returns:
85
+ Nothing.
86
+ """
87
+
88
+ self._durations = np.asarray(self.durations, dtype=float)
89
+ self._rates = np.asarray(self.rates, dtype=float)
90
+
91
+ if self._durations.ndim != 1:
92
+ raise ValueError("durations must be one-dimensional")
93
+ if self._rates.ndim != 1:
94
+ raise ValueError("rates must be one-dimensional")
95
+ if self._durations.size == 0:
96
+ raise ValueError("durations must contain at least one interval")
97
+ if self._durations.size != self._rates.size:
98
+ raise ValueError("durations and rates must have the same length")
99
+ if not np.all(np.isfinite(self._durations[:-1])):
100
+ raise ValueError(
101
+ "durations must be finite except possibly the last interval, "
102
+ "which may extend to infinity"
103
+ )
104
+ if np.any(self._durations[:-1] <= 0):
105
+ raise ValueError("durations before the final interval must be positive")
106
+ last_duration = float(self._durations[-1])
107
+ if math.isnan(last_duration):
108
+ raise ValueError("final duration must be finite or math.inf")
109
+ if last_duration <= 0:
110
+ raise ValueError("final duration must be positive")
111
+ if np.any(~np.isfinite(self._rates)):
112
+ raise ValueError("rates must be finite")
113
+ if np.any(self._rates <= 0):
114
+ raise ValueError("rates must be strictly positive")
115
+
116
+ self._cum_time = np.concatenate(
117
+ (np.array([0.0]), np.cumsum(self._durations[:-1]))
118
+ )
119
+ self._cum_hazard = np.concatenate(
120
+ (np.array([0.0]), np.cumsum(self._durations[:-1] * self._rates[:-1]))
121
+ )
122
+
123
+ def sample(
124
+ self,
125
+ size: int | tuple[int, ...] | None = None,
126
+ rng: np.random.Generator | None = None,
127
+ ) -> float | NDArray[np.float64]:
128
+ """
129
+ Draw samples using the inverse cumulative distribution function.
130
+
131
+ Args:
132
+ size: Requested sample size or shape.
133
+ rng: Optional random number generator overriding the stored generator.
134
+
135
+ Returns:
136
+ A scalar when `size` is `None`, otherwise an array of samples.
137
+ """
138
+
139
+ generator = rng or self.rng or _default_rng()
140
+
141
+ if size is None:
142
+ uniform = float(generator.uniform())
143
+ hazard = -math.log(uniform)
144
+ index = int(np.searchsorted(self._cum_hazard, hazard, side="right") - 1)
145
+ base_time = float(self._cum_time[index])
146
+ return base_time + (hazard - float(self._cum_hazard[index])) / float(
147
+ self._rates[index]
148
+ )
149
+
150
+ uniforms = generator.uniform(size=size)
151
+ hazards = -np.log(uniforms)
152
+ indices = np.searchsorted(self._cum_hazard, hazards, side="right") - 1
153
+ base_times = self._cum_time[indices]
154
+ return base_times + (hazards - self._cum_hazard[indices]) / self._rates[indices]
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.4
2
+ Name: simtrial
3
+ Version: 0.0.1
4
+ Summary: Clinical trial simulation
5
+ Project-URL: Repository, https://github.com/nanxstats/simtrial-python
6
+ Project-URL: Issues, https://github.com/nanxstats/simtrial-python/issues
7
+ Project-URL: Changelog, https://github.com/nanxstats/simtrial-python/blob/main/CHANGELOG.md
8
+ Author-email: Nan Xiao <me@nanx.me>
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: numpy>=2.0.0
23
+ Description-Content-Type: text/markdown
24
+
25
+ # simtrial-python
26
+
27
+ [![PyPI version](https://img.shields.io/pypi/v/simtrial)](https://pypi.org/project/simtrial/)
28
+ ![Python versions](https://img.shields.io/pypi/pyversions/simtrial)
29
+ [![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
30
+ [![CI Tests](https://github.com/nanxstats/simtrial-python/actions/workflows/ci-tests.yml/badge.svg)](https://github.com/nanxstats/simtrial-python/actions/workflows/ci-tests.yml)
31
+ ![License](https://img.shields.io/pypi/l/simtrial)
32
+
33
+ simtrial-python is an experimental Python package for clinical trial
34
+ simulation with time-to-event endpoints.
35
+
36
+ ## Installation
37
+
38
+ You can install simtrial-python from PyPI:
39
+
40
+ ```bash
41
+ pip install simtrial
42
+ ```
43
+
44
+ Or install the development version from GitHub:
45
+
46
+ ```bash
47
+ git clone https://github.com/nanxstats/simtrial-python.git
48
+ cd simtrial-python
49
+ python3 -m pip install -e .
50
+ ```
@@ -0,0 +1,6 @@
1
+ simtrial/__init__.py,sha256=9Jimypg2M2w_Lkc-VAbhsI44-DZ0Romcc5w-_KxKL8E,174
2
+ simtrial/piecewise_exponential.py,sha256=vVxILWC7iwXQghnS4e2gsu3Ed0jVIwpoJRqfqmnF_Ts,4871
3
+ simtrial-0.0.1.dist-info/METADATA,sha256=SetPmMShHLtJZr87vpgcYMgxU65A7MbUF9lmUgnW00I,1884
4
+ simtrial-0.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
+ simtrial-0.0.1.dist-info/licenses/LICENSE,sha256=fbue7RMrTYyNN6h6aYvgHX0k4Gpkd6h9bhk1XSVgAuI,1068
6
+ simtrial-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2025, simtrial-python authors
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.