pyR0compute 0.1.0__tar.gz
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.
- pyr0compute-0.1.0/.gitignore +2 -0
- pyr0compute-0.1.0/CHANGELOG.md +29 -0
- pyr0compute-0.1.0/LICENSE +21 -0
- pyr0compute-0.1.0/PKG-INFO +146 -0
- pyr0compute-0.1.0/README.md +121 -0
- pyr0compute-0.1.0/pyproject.toml +48 -0
- pyr0compute-0.1.0/src/pyr0compute/__init__.py +39 -0
- pyr0compute-0.1.0/src/pyr0compute/compat.py +79 -0
- pyr0compute-0.1.0/src/pyr0compute/exceptions.py +17 -0
- pyr0compute-0.1.0/src/pyr0compute/model.py +867 -0
- pyr0compute-0.1.0/src/pyr0compute/parsing.py +202 -0
- pyr0compute-0.1.0/tests/conftest.py +10 -0
- pyr0compute-0.1.0/tests/helpers.py +19 -0
- pyr0compute-0.1.0/tests/test_errors.py +99 -0
- pyr0compute-0.1.0/tests/test_literature.py +84 -0
- pyr0compute-0.1.0/tests/test_seir.py +109 -0
- pyr0compute-0.1.0/tests/test_sir.py +102 -0
- pyr0compute-0.1.0/tests/test_vector.py +113 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (unreleased)
|
|
4
|
+
|
|
5
|
+
First packaged release. The code of the original notebook was turned into an installable library.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `R0Model`: models can be entered as text (`dS/dt = ...`), as a dict or as SymPy symbols.
|
|
9
|
+
- Automatic detection of parameters: every symbol that is not a state variable.
|
|
10
|
+
- Equations in any order; infected compartments given by name, symbol or index.
|
|
11
|
+
- Safe parsing of names that are special in SymPy (`I`, `S`, `E`, `N`, `beta`, `gamma`, `lambda`) and `^` for powers.
|
|
12
|
+
- Auxiliary definitions in text models (`N = S + I + R`).
|
|
13
|
+
- Partial user-given disease-free equilibrium (`dfe={"S": "N"}`), list of all non-negative DFEs (`dfe_candidates`).
|
|
14
|
+
- `report_latex()`: step-by-step report in LaTeX (document or Markdown for notebooks).
|
|
15
|
+
- `R0_numeric`, `sensitivity_indices`, `report`, `latex`, `eigenvalues`, `next_generation_matrix_small`.
|
|
16
|
+
- Informative exceptions (`ModelSpecificationError`, `DiseaseFreeEquilibriumError`, `NextGenerationError`).
|
|
17
|
+
- Test suite (SIR, SEIR, vector-borne, literature models) and continuous integration.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Terms with a division (e.g. `beta*S*I/N`) were never recognised as new infections, so R0 was silently returned as 0. The sign test `term.has(-1)` matched the exponent of `1/N`.
|
|
21
|
+
- Coefficients such as `(1 - p)*beta*S*I` were split into a new-infection part and a spurious transition part, giving a wrong R0.
|
|
22
|
+
- For vector-borne models the negative eigenvalue `-sqrt(...)` could be returned as R0; the Perron root is now used.
|
|
23
|
+
- When the dominant eigenvalue depends on the parameters (competing strains), R0 is returned as `Max(...)` instead of an arbitrary branch.
|
|
24
|
+
- Undetermined DFE values were replaced by unexplained symbols such as `S_eq`; an error now says which value must be given.
|
|
25
|
+
- Several DFEs: the first solution returned by SymPy was taken silently.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- `GeneralEpidemiologicalModel` keeps the original interface; `parameters` is now optional and the infected equations no longer need to come first.
|
|
29
|
+
- Output is silent by default (`verbose=True` or `report()` for the step-by-step description).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yvan Baldera-Moreno
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pyR0compute
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Symbolic computation of the basic reproduction number R0 via the next-generation matrix method
|
|
5
|
+
Project-URL: Homepage, https://github.com/Yvan-BM-Git/pyR0compute
|
|
6
|
+
Project-URL: Issues, https://github.com/Yvan-BM-Git/pyR0compute/issues
|
|
7
|
+
Author: Yvan Baldera-Moreno
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: R0,basic reproduction number,compartmental models,epidemiology,mathematical immunology,next-generation matrix,symbolic computation,sympy
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Education
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Requires-Dist: numpy>=1.22
|
|
21
|
+
Requires-Dist: sympy>=1.12
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: pytest>=7; extra == 'test'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# pyR0compute
|
|
27
|
+
|
|
28
|
+
Symbolic computation of the basic reproduction number $R_0$ of compartmental ODE models by the next-generation matrix method (van den Driessche & Watmough, 2002).
|
|
29
|
+
|
|
30
|
+
Write the model, say which compartments are infected, and pyR0compute does the rest: every symbol that is not a state variable is treated as a parameter, the new-infection terms $\mathcal{F}$ and transitions $\mathcal{V}$ are identified, the disease-free equilibrium (DFE) is solved, and $R_0 = \rho(FV^{-1})$ is returned as a SymPy expression.
|
|
31
|
+
|
|
32
|
+
[](https://colab.research.google.com/github/Yvan-BM-Git/pyR0compute/blob/main/examples/pyR0compute_examples.ipynb)
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install pyR0compute
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Until the first release is on PyPI, install it from GitHub:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install git+https://github.com/Yvan-BM-Git/pyR0compute
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Requires Python ≥ 3.9, SymPy and NumPy.
|
|
47
|
+
|
|
48
|
+
## Quick start
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from pyr0compute import R0Model
|
|
52
|
+
|
|
53
|
+
model = R0Model("""
|
|
54
|
+
dS/dt = Lambda - beta*S*I - mu*S
|
|
55
|
+
dE/dt = beta*S*I - (sigma + mu)*E
|
|
56
|
+
dI/dt = sigma*E - (gamma + mu)*I
|
|
57
|
+
dR/dt = gamma*I - mu*R
|
|
58
|
+
""", infected=["E", "I"])
|
|
59
|
+
|
|
60
|
+
model.parameters # (Lambda, beta, gamma, mu, sigma) <- detected automatically
|
|
61
|
+
model.dfe # {S: Lambda/mu, E: 0, I: 0, R: 0}
|
|
62
|
+
model.R0 # Lambda*beta*sigma/(mu*(gamma + mu)*(mu + sigma))
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The equations can be written in any order; there is no need to list the infected compartments first.
|
|
66
|
+
|
|
67
|
+
### Three ways to enter a model
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# 1. Text: one "dX/dt = ..." (or "X' = ...") line per variable.
|
|
71
|
+
# Auxiliary lines such as "N = S + I + R" are substituted.
|
|
72
|
+
R0Model("""
|
|
73
|
+
N = S + I + R
|
|
74
|
+
dS/dt = Lambda - beta*S*I/N - mu*S
|
|
75
|
+
dI/dt = beta*S*I/N - (gamma + mu)*I
|
|
76
|
+
dR/dt = gamma*I - mu*R
|
|
77
|
+
""", infected=["I"])
|
|
78
|
+
|
|
79
|
+
# 2. A dict {variable: right-hand side} (strings or SymPy expressions)
|
|
80
|
+
R0Model({"S": "Lambda - beta*S*I - mu*S",
|
|
81
|
+
"I": "beta*S*I - (gamma + mu)*I"}, infected=["I"])
|
|
82
|
+
|
|
83
|
+
# 3. SymPy symbols
|
|
84
|
+
import sympy as sp
|
|
85
|
+
S, I = sp.symbols("S I")
|
|
86
|
+
beta, gamma, mu, Lam = sp.symbols("beta gamma mu Lambda")
|
|
87
|
+
R0Model({S: Lam - beta*S*I - mu*S, I: beta*S*I - (gamma + mu)*I}, infected=[I])
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Use subscripts in names for clean LaTeX: `mu_h`, `alpha_hv`, `Delta_h` are written $\mu_h$, $\alpha_{hv}$, $\Delta_h$. Names that are special in SymPy (`I`, `S`, `E`, `N`, `beta`, `gamma`, `Lambda`, even `lambda`) are plain symbols inside text models, and `^` means a power.
|
|
91
|
+
|
|
92
|
+
### What you get
|
|
93
|
+
|
|
94
|
+
| Attribute / method | Content |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `R0` | Basic reproduction number (spectral radius of $FV^{-1}$) |
|
|
97
|
+
| `parameters` | Parameters detected automatically |
|
|
98
|
+
| `new_infections`, `transitions` | Terms $\mathcal{F}_i$ and $\mathcal{V}_i$ of each infected compartment |
|
|
99
|
+
| `dfe`, `dfe_candidates` | Disease-free equilibrium used, and all non-negative ones found |
|
|
100
|
+
| `F`, `V`, `K` | Jacobians at the DFE and next-generation matrix $K = FV^{-1}$ |
|
|
101
|
+
| `next_generation_matrix_small` | $K$ restricted to compartments receiving new infections |
|
|
102
|
+
| `eigenvalues` | Eigenvalues of $K$ |
|
|
103
|
+
| `R0_numeric(values)` | Numerical spectral radius (for large models without a closed form) |
|
|
104
|
+
| `sensitivity_indices(values=None)` | Normalized forward sensitivity indices $\Upsilon_p = \frac{\partial R_0}{\partial p}\frac{p}{R_0}$ |
|
|
105
|
+
| `report()` | Step-by-step description of the whole computation |
|
|
106
|
+
| `report_latex(style, standalone, mat_str)` | The same report in LaTeX (`"document"`, compilable with `standalone=True`) or Markdown for notebooks (`"markdown"`) |
|
|
107
|
+
| `latex()` | LaTeX code of $R_0$ |
|
|
108
|
+
|
|
109
|
+
### When the automatic choices need help
|
|
110
|
+
|
|
111
|
+
* **Closed populations** (no births), e.g. the classic SIR: the DFE is not unique, so give it, `dfe={"S": "N"}`. The error message says which value is missing.
|
|
112
|
+
* **Several DFEs** (e.g. logistic vector populations): the one with the most non-zero compartments is used and a warning is shown. Choose another with `dfe={...}`; all are in `model.dfe_candidates`.
|
|
113
|
+
* **Custom decompositions**: the split $\mathcal{F}$/$\mathcal{V}$ is not unique. Override the automatic one with `new_infections={"E": "beta*S*I/N"}`.
|
|
114
|
+
|
|
115
|
+
### How new infections are detected
|
|
116
|
+
|
|
117
|
+
In the equation of an infected compartment, a positive term is a new infection when it involves an infected compartment and either (i) it is lost from an uninfected compartment (a transfer such as $S \to E$), or (ii) it involves an uninfected compartment and is not a transfer between infected compartments. Progression ($E \to I$), treatment failure, superinfection between strains, deaths and recoveries go to $\mathcal{V}$. `model.report()` shows the classification of every term and the reason.
|
|
118
|
+
|
|
119
|
+
## Validation
|
|
120
|
+
|
|
121
|
+
The test suite (`pytest`) reproduces known results: SIR (with and without vital dynamics, mass-action and frequency-dependent), SEIR, a vaccination model, Ross-Macdonald, a host-vector SEIR/SEI model with human-to-human transmission and logistic vectors, the treatment model of van den Driessche & Watmough (2002, §4.1), within-host target cell-infected cell-virus models and a two-strain model with superinfection. Symbolic results are also checked against the numerical spectral radius.
|
|
122
|
+
|
|
123
|
+
## Previous interface
|
|
124
|
+
|
|
125
|
+
Code written for the original notebook keeps working:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from pyr0compute import GeneralEpidemiologicalModel
|
|
129
|
+
model = GeneralEpidemiologicalModel(variables, parameters, equations, infected_indices)
|
|
130
|
+
model.calculate_R0()
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Citation
|
|
134
|
+
|
|
135
|
+
If you use pyR0compute in your research, please cite it (see `CITATION.cff`) together with:
|
|
136
|
+
|
|
137
|
+
* van den Driessche, P., & Watmough, J. (2002). Reproduction numbers and sub-threshold endemic equilibria for compartmental models of disease transmission. *Mathematical Biosciences*, 180(1-2), 29-48.
|
|
138
|
+
* Diekmann, O., Heesterbeek, J. A. P., & Roberts, M. G. (2010). The construction of next-generation matrices for compartmental epidemic models. *Journal of the Royal Society Interface*, 7(47), 873-885.
|
|
139
|
+
|
|
140
|
+
## Resumen en español
|
|
141
|
+
|
|
142
|
+
pyR0compute calcula simbólicamente el número básico de reproducción $R_0$ mediante el método de la matriz de próxima generación. Basta escribir el sistema de EDO e indicar los compartimentos infectados: todos los demás símbolos se reconocen automáticamente como parámetros, se identifican los términos de nuevas infecciones, se calcula el equilibrio libre de enfermedad y se obtiene $R_0$. El notebook `examples/pyR0compute_examples.ipynb` contiene ejemplos listos para Google Colab.
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT, see `LICENSE`.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# pyR0compute
|
|
2
|
+
|
|
3
|
+
Symbolic computation of the basic reproduction number $R_0$ of compartmental ODE models by the next-generation matrix method (van den Driessche & Watmough, 2002).
|
|
4
|
+
|
|
5
|
+
Write the model, say which compartments are infected, and pyR0compute does the rest: every symbol that is not a state variable is treated as a parameter, the new-infection terms $\mathcal{F}$ and transitions $\mathcal{V}$ are identified, the disease-free equilibrium (DFE) is solved, and $R_0 = \rho(FV^{-1})$ is returned as a SymPy expression.
|
|
6
|
+
|
|
7
|
+
[](https://colab.research.google.com/github/Yvan-BM-Git/pyR0compute/blob/main/examples/pyR0compute_examples.ipynb)
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install pyR0compute
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Until the first release is on PyPI, install it from GitHub:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install git+https://github.com/Yvan-BM-Git/pyR0compute
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires Python ≥ 3.9, SymPy and NumPy.
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from pyr0compute import R0Model
|
|
27
|
+
|
|
28
|
+
model = R0Model("""
|
|
29
|
+
dS/dt = Lambda - beta*S*I - mu*S
|
|
30
|
+
dE/dt = beta*S*I - (sigma + mu)*E
|
|
31
|
+
dI/dt = sigma*E - (gamma + mu)*I
|
|
32
|
+
dR/dt = gamma*I - mu*R
|
|
33
|
+
""", infected=["E", "I"])
|
|
34
|
+
|
|
35
|
+
model.parameters # (Lambda, beta, gamma, mu, sigma) <- detected automatically
|
|
36
|
+
model.dfe # {S: Lambda/mu, E: 0, I: 0, R: 0}
|
|
37
|
+
model.R0 # Lambda*beta*sigma/(mu*(gamma + mu)*(mu + sigma))
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The equations can be written in any order; there is no need to list the infected compartments first.
|
|
41
|
+
|
|
42
|
+
### Three ways to enter a model
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
# 1. Text: one "dX/dt = ..." (or "X' = ...") line per variable.
|
|
46
|
+
# Auxiliary lines such as "N = S + I + R" are substituted.
|
|
47
|
+
R0Model("""
|
|
48
|
+
N = S + I + R
|
|
49
|
+
dS/dt = Lambda - beta*S*I/N - mu*S
|
|
50
|
+
dI/dt = beta*S*I/N - (gamma + mu)*I
|
|
51
|
+
dR/dt = gamma*I - mu*R
|
|
52
|
+
""", infected=["I"])
|
|
53
|
+
|
|
54
|
+
# 2. A dict {variable: right-hand side} (strings or SymPy expressions)
|
|
55
|
+
R0Model({"S": "Lambda - beta*S*I - mu*S",
|
|
56
|
+
"I": "beta*S*I - (gamma + mu)*I"}, infected=["I"])
|
|
57
|
+
|
|
58
|
+
# 3. SymPy symbols
|
|
59
|
+
import sympy as sp
|
|
60
|
+
S, I = sp.symbols("S I")
|
|
61
|
+
beta, gamma, mu, Lam = sp.symbols("beta gamma mu Lambda")
|
|
62
|
+
R0Model({S: Lam - beta*S*I - mu*S, I: beta*S*I - (gamma + mu)*I}, infected=[I])
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Use subscripts in names for clean LaTeX: `mu_h`, `alpha_hv`, `Delta_h` are written $\mu_h$, $\alpha_{hv}$, $\Delta_h$. Names that are special in SymPy (`I`, `S`, `E`, `N`, `beta`, `gamma`, `Lambda`, even `lambda`) are plain symbols inside text models, and `^` means a power.
|
|
66
|
+
|
|
67
|
+
### What you get
|
|
68
|
+
|
|
69
|
+
| Attribute / method | Content |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `R0` | Basic reproduction number (spectral radius of $FV^{-1}$) |
|
|
72
|
+
| `parameters` | Parameters detected automatically |
|
|
73
|
+
| `new_infections`, `transitions` | Terms $\mathcal{F}_i$ and $\mathcal{V}_i$ of each infected compartment |
|
|
74
|
+
| `dfe`, `dfe_candidates` | Disease-free equilibrium used, and all non-negative ones found |
|
|
75
|
+
| `F`, `V`, `K` | Jacobians at the DFE and next-generation matrix $K = FV^{-1}$ |
|
|
76
|
+
| `next_generation_matrix_small` | $K$ restricted to compartments receiving new infections |
|
|
77
|
+
| `eigenvalues` | Eigenvalues of $K$ |
|
|
78
|
+
| `R0_numeric(values)` | Numerical spectral radius (for large models without a closed form) |
|
|
79
|
+
| `sensitivity_indices(values=None)` | Normalized forward sensitivity indices $\Upsilon_p = \frac{\partial R_0}{\partial p}\frac{p}{R_0}$ |
|
|
80
|
+
| `report()` | Step-by-step description of the whole computation |
|
|
81
|
+
| `report_latex(style, standalone, mat_str)` | The same report in LaTeX (`"document"`, compilable with `standalone=True`) or Markdown for notebooks (`"markdown"`) |
|
|
82
|
+
| `latex()` | LaTeX code of $R_0$ |
|
|
83
|
+
|
|
84
|
+
### When the automatic choices need help
|
|
85
|
+
|
|
86
|
+
* **Closed populations** (no births), e.g. the classic SIR: the DFE is not unique, so give it, `dfe={"S": "N"}`. The error message says which value is missing.
|
|
87
|
+
* **Several DFEs** (e.g. logistic vector populations): the one with the most non-zero compartments is used and a warning is shown. Choose another with `dfe={...}`; all are in `model.dfe_candidates`.
|
|
88
|
+
* **Custom decompositions**: the split $\mathcal{F}$/$\mathcal{V}$ is not unique. Override the automatic one with `new_infections={"E": "beta*S*I/N"}`.
|
|
89
|
+
|
|
90
|
+
### How new infections are detected
|
|
91
|
+
|
|
92
|
+
In the equation of an infected compartment, a positive term is a new infection when it involves an infected compartment and either (i) it is lost from an uninfected compartment (a transfer such as $S \to E$), or (ii) it involves an uninfected compartment and is not a transfer between infected compartments. Progression ($E \to I$), treatment failure, superinfection between strains, deaths and recoveries go to $\mathcal{V}$. `model.report()` shows the classification of every term and the reason.
|
|
93
|
+
|
|
94
|
+
## Validation
|
|
95
|
+
|
|
96
|
+
The test suite (`pytest`) reproduces known results: SIR (with and without vital dynamics, mass-action and frequency-dependent), SEIR, a vaccination model, Ross-Macdonald, a host-vector SEIR/SEI model with human-to-human transmission and logistic vectors, the treatment model of van den Driessche & Watmough (2002, §4.1), within-host target cell-infected cell-virus models and a two-strain model with superinfection. Symbolic results are also checked against the numerical spectral radius.
|
|
97
|
+
|
|
98
|
+
## Previous interface
|
|
99
|
+
|
|
100
|
+
Code written for the original notebook keeps working:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from pyr0compute import GeneralEpidemiologicalModel
|
|
104
|
+
model = GeneralEpidemiologicalModel(variables, parameters, equations, infected_indices)
|
|
105
|
+
model.calculate_R0()
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Citation
|
|
109
|
+
|
|
110
|
+
If you use pyR0compute in your research, please cite it (see `CITATION.cff`) together with:
|
|
111
|
+
|
|
112
|
+
* van den Driessche, P., & Watmough, J. (2002). Reproduction numbers and sub-threshold endemic equilibria for compartmental models of disease transmission. *Mathematical Biosciences*, 180(1-2), 29-48.
|
|
113
|
+
* Diekmann, O., Heesterbeek, J. A. P., & Roberts, M. G. (2010). The construction of next-generation matrices for compartmental epidemic models. *Journal of the Royal Society Interface*, 7(47), 873-885.
|
|
114
|
+
|
|
115
|
+
## Resumen en español
|
|
116
|
+
|
|
117
|
+
pyR0compute calcula simbólicamente el número básico de reproducción $R_0$ mediante el método de la matriz de próxima generación. Basta escribir el sistema de EDO e indicar los compartimentos infectados: todos los demás símbolos se reconocen automáticamente como parámetros, se identifican los términos de nuevas infecciones, se calcula el equilibrio libre de enfermedad y se obtiene $R_0$. El notebook `examples/pyR0compute_examples.ipynb` contiene ejemplos listos para Google Colab.
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT, see `LICENSE`.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.21"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyR0compute"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Symbolic computation of the basic reproduction number R0 via the next-generation matrix method"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Yvan Baldera-Moreno" }]
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
dependencies = ["sympy>=1.12", "numpy>=1.22"]
|
|
15
|
+
keywords = [
|
|
16
|
+
"basic reproduction number", "R0", "next-generation matrix", "epidemiology",
|
|
17
|
+
"mathematical immunology", "compartmental models", "symbolic computation", "sympy",
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Intended Audience :: Science/Research",
|
|
22
|
+
"Intended Audience :: Education",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
26
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
27
|
+
"Topic :: Scientific/Engineering :: Medical Science Apps.",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
test = ["pytest>=7"]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/Yvan-BM-Git/pyR0compute"
|
|
35
|
+
Issues = "https://github.com/Yvan-BM-Git/pyR0compute/issues"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.version]
|
|
38
|
+
path = "src/pyr0compute/__init__.py"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/pyr0compute"]
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.sdist]
|
|
44
|
+
include = ["src/pyr0compute", "tests", "README.md", "LICENSE", "CHANGELOG.md"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
testpaths = ["tests"]
|
|
48
|
+
addopts = "-ra"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""pyR0compute: symbolic computation of the basic reproduction number R0.
|
|
2
|
+
|
|
3
|
+
Implements the next-generation matrix method of van den Driessche & Watmough
|
|
4
|
+
(2002) for compartmental ODE models. Write the model, say which compartments
|
|
5
|
+
are infected, and every other symbol is treated as a parameter::
|
|
6
|
+
|
|
7
|
+
from pyr0compute import R0Model
|
|
8
|
+
|
|
9
|
+
model = R0Model('''
|
|
10
|
+
dS/dt = Lambda - beta*S*I - mu*S
|
|
11
|
+
dI/dt = beta*S*I - (gamma + mu)*I
|
|
12
|
+
dR/dt = gamma*I - mu*R
|
|
13
|
+
''', infected=["I"])
|
|
14
|
+
|
|
15
|
+
model.R0 # Lambda*beta/(mu*(gamma + mu))
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .compat import GeneralEpidemiologicalModel
|
|
19
|
+
from .exceptions import (
|
|
20
|
+
DiseaseFreeEquilibriumError,
|
|
21
|
+
ModelSpecificationError,
|
|
22
|
+
NextGenerationError,
|
|
23
|
+
R0ComputeError,
|
|
24
|
+
)
|
|
25
|
+
from .model import R0Model
|
|
26
|
+
from .parsing import parse_expression
|
|
27
|
+
|
|
28
|
+
__version__ = "0.1.0"
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"R0Model",
|
|
32
|
+
"GeneralEpidemiologicalModel",
|
|
33
|
+
"parse_expression",
|
|
34
|
+
"R0ComputeError",
|
|
35
|
+
"ModelSpecificationError",
|
|
36
|
+
"DiseaseFreeEquilibriumError",
|
|
37
|
+
"NextGenerationError",
|
|
38
|
+
"__version__",
|
|
39
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Backward-compatible interface of the original notebook."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional, Sequence
|
|
6
|
+
|
|
7
|
+
import sympy as sp
|
|
8
|
+
|
|
9
|
+
from .model import R0Model
|
|
10
|
+
|
|
11
|
+
__all__ = ["GeneralEpidemiologicalModel"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GeneralEpidemiologicalModel(R0Model):
|
|
15
|
+
"""Interface of the original ``pyR0compute_examples.ipynb`` notebook.
|
|
16
|
+
|
|
17
|
+
Existing code keeps working, with two differences: the equations no longer
|
|
18
|
+
need to list the infected compartments first, and ``parameters`` is
|
|
19
|
+
optional (it is detected automatically). New code should use
|
|
20
|
+
:class:`R0Model`.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
variables, parameters, equations, infected_indices
|
|
25
|
+
As in the original notebook; ``infected_indices`` are positions in
|
|
26
|
+
``variables``.
|
|
27
|
+
new_infection_terms
|
|
28
|
+
Optional list aligned with ``variables`` (zeros for uninfected).
|
|
29
|
+
equilibrium_point
|
|
30
|
+
Optional disease-free equilibrium aligned with ``variables``.
|
|
31
|
+
total_population
|
|
32
|
+
Accepted for compatibility; not needed.
|
|
33
|
+
verbose
|
|
34
|
+
Print the step-by-step report when :meth:`calculate_R0` is called
|
|
35
|
+
(default ``True``, like the original).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
variables: Sequence[sp.Symbol],
|
|
41
|
+
parameters: Optional[Sequence[sp.Symbol]],
|
|
42
|
+
equations: Sequence[sp.Expr],
|
|
43
|
+
infected_indices: Sequence[int],
|
|
44
|
+
new_infection_terms: Optional[Sequence[sp.Expr]] = None,
|
|
45
|
+
equilibrium_point: Optional[Sequence[sp.Expr]] = None,
|
|
46
|
+
total_population=None,
|
|
47
|
+
verbose: bool = True,
|
|
48
|
+
**kwargs,
|
|
49
|
+
) -> None:
|
|
50
|
+
dfe = None
|
|
51
|
+
if equilibrium_point is not None:
|
|
52
|
+
dfe = dict(zip(variables, list(equilibrium_point)))
|
|
53
|
+
self._legacy_verbose = verbose
|
|
54
|
+
super().__init__(
|
|
55
|
+
equations=list(equations),
|
|
56
|
+
infected=list(infected_indices),
|
|
57
|
+
variables=list(variables),
|
|
58
|
+
parameters=list(parameters) if parameters else None,
|
|
59
|
+
new_infections=list(new_infection_terms) if new_infection_terms is not None else None,
|
|
60
|
+
dfe=dfe,
|
|
61
|
+
**kwargs,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def identify_F_V_terms(self):
|
|
65
|
+
"""Return the vectors of new-infection and transition terms."""
|
|
66
|
+
F = sp.Matrix([self.new_infections[v] for v in self.infected])
|
|
67
|
+
V = sp.Matrix([self.transitions[v] for v in self.infected])
|
|
68
|
+
return F, V
|
|
69
|
+
|
|
70
|
+
def calculate_next_generation_matrices(self):
|
|
71
|
+
"""Return ``(F, V)`` evaluated at the disease-free equilibrium."""
|
|
72
|
+
return self.F, self.V
|
|
73
|
+
|
|
74
|
+
def calculate_R0(self):
|
|
75
|
+
"""Return R0 (printing the report if ``verbose``)."""
|
|
76
|
+
R0 = self.R0
|
|
77
|
+
if self._legacy_verbose:
|
|
78
|
+
print(self.report())
|
|
79
|
+
return R0
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Exceptions raised by pyR0compute."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class R0ComputeError(ValueError):
|
|
5
|
+
"""Base class for all pyR0compute errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ModelSpecificationError(R0ComputeError):
|
|
9
|
+
"""The model (equations, variables, infected compartments...) is ill-defined."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DiseaseFreeEquilibriumError(R0ComputeError):
|
|
13
|
+
"""The disease-free equilibrium could not be determined unambiguously."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NextGenerationError(R0ComputeError):
|
|
17
|
+
"""The next-generation matrix or its spectral radius could not be computed."""
|