Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
680bb1e
Merge branch 'main' of https://github.com/CDCgov/PyRenew
cdc-mitzimorris Sep 15, 2025
2cb876b
update
cdc-mitzimorris Sep 18, 2025
60db8df
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Sep 22, 2025
32a5314
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Oct 5, 2025
d6213f2
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Oct 8, 2025
96f27c9
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Nov 17, 2025
1cb6fa2
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Nov 24, 2025
f62e1e4
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Dec 4, 2025
0c6785d
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Dec 22, 2025
1ee62b9
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Jan 29, 2026
0629461
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Feb 4, 2026
efeadee
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Feb 5, 2026
371ba98
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Feb 5, 2026
0304bed
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Feb 6, 2026
ffeea65
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Feb 9, 2026
50e7261
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Feb 9, 2026
dae6af8
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Feb 10, 2026
5cb3097
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Feb 11, 2026
1d80ccc
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Feb 11, 2026
e73b401
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Feb 12, 2026
b1473b5
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Feb 18, 2026
0b929b5
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Feb 18, 2026
3ee00a7
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Feb 24, 2026
307982a
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Feb 24, 2026
b862bc6
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Feb 26, 2026
2c665a5
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Mar 11, 2026
60d6458
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Mar 12, 2026
ec8c464
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Mar 19, 2026
c018bf7
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Mar 24, 2026
d0207dd
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Apr 4, 2026
f3c706a
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Apr 9, 2026
684c6c5
Merge branch 'main' of github-bf06:CDCgov/PyRenew
cdc-mitzimorris Apr 10, 2026
6b52cfe
updated files; unit tests passing
cdc-mitzimorris Apr 11, 2026
c9ef607
consistent names across docs
cdc-mitzimorris Apr 11, 2026
e7b6ec0
Update test/test_interface_coverage.py
cdc-mitzimorris Apr 11, 2026
c8732a0
copilot fix
cdc-mitzimorris Apr 11, 2026
a709d14
Merge branch 'mem_778_rename' of github-bf06:CDCgov/PyRenew into mem_…
cdc-mitzimorris Apr 11, 2026
6290cff
mathcal Rt fixes
cdc-mitzimorris Apr 13, 2026
de9e80a
docstring comment cleanup
cdc-mitzimorris Apr 13, 2026
f0de42f
docs cleanup
cdc-mitzimorris Apr 13, 2026
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
5 changes: 3 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,10 @@ pip install git+https://github.com/CDCgov/PyRenew@main

- [The RandomVariable abstract base class](tutorials/random_variables.md) -- PyRenew's core abstraction and its concrete implementations.
- [Building multi-signal models](tutorials/building_multisignal_models.md) -- composing a renewal model from PyRenew components using `PyrenewBuilder`.
- [Latent infections](tutorials/latent_infections.md) -- modeling latent infection trajectories over time.
- [Latent subpopulation infections](tutorials/latent_subpopulation_infections.md) -- modeling latent infections with subpopulation structure.
- [Observation processes: count data](tutorials/observation_processes_counts.md) -- connecting latent infections to observed counts.
- [Observation processes: measurements](tutorials/observation_processes_measurements.md) -- connecting latent infections to continuous measurements.
- [Latent hierarchical infections](tutorials/latent_hierarchical_infections.md) -- modeling infections with subpopulation structure.

## Resources

Expand All @@ -115,4 +116,4 @@ pip install git+https://github.com/CDCgov/PyRenew@main
### Further reading

- [Semi-mechanistic Bayesian modelling of COVID-19 with renewal processes](https://academic.oup.com/jrsssa/article-pdf/186/4/601/54770289/qnad030.pdf) (Bhatt et al., 2023)
- [Unifying incidence and prevalence under a time-varying general branching process](https://link.springer.com/content/pdf/10.1007/s00285-023-01958-w.pdf)
- [Unifying incidence and prevalence under a time-varying general branching process](https://link.springer.com/content/pdf/10.1007/s00285-023-01958-w.pdf) (Pakkanen et al., 2023)
4 changes: 2 additions & 2 deletions docs/tutorials/.pages
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
nav:
- random_variables.md
- building_multisignal_models.md
- latent_infections.md
- latent_subpopulation_infections.md
- observation_processes_counts.md
- observation_processes_measurements.md
- latent_infections.md
- latent_hierarchical_infections.md
- right_truncation.md
- day_of_week_effects.md
46 changes: 23 additions & 23 deletions docs/tutorials/building_multisignal_models.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ from pyrenew.metaclass import RandomVariable
from pyrenew.randomvariable import DistributionalVariable

from pyrenew.latent import (
HierarchicalInfections,
SubpopulationInfections,
AR1,
RandomWalk,
GammaGroupSdPrior,
Expand Down Expand Up @@ -97,7 +97,7 @@ A **multi-signal model** combines multiple observation processes—each represen

The `PyrenewBuilder` class handles this plumbing. You specify:

1. A single **latent process** (e.g., `HierarchicalInfections`) that defines how infections evolve.
1. A single **latent process** (e.g., `SubpopulationInfections`) that defines how infections evolve.
2. One or more **observation processes** (e.g., `Counts`, `Measurements`) that define how infections become data.

The builder computes initialization requirements, wires components together, and produces a model ready for inference.
Expand All @@ -106,7 +106,7 @@ The builder computes initialization requirements, wires components together, and

Before diving into multi-signal models, you may want to review these foundational tutorials:

- **[Hierarchical Latent Infections](latent_hierarchical_infections.md)**: Understanding temporal process choices for $\mathcal{R}(t)$
- **[Latent Infections](latent_infections.md)** and **[Latent Subpopulation Infections](latent_subpopulation_infections.md)**: Understanding temporal process choices for $\mathcal{R}(t)$
- **[Observation Processes: Counts](observation_processes_counts.md)**: Modeling count data (admissions, deaths)
- **[Observation Processes: Measurements](observation_processes_measurements.md)**: Modeling continuous data (wastewater)

Expand All @@ -116,7 +116,7 @@ This tutorial shows how to combine these components into a complete multi-signal

This tutorial demonstrates building a multi-signal renewal model using:

- `HierarchicalInfections` — subpopulations share a jurisdiction-level baseline $\mathcal{R}(t)$ with subpopulation-specific deviations
- `SubpopulationInfections` — subpopulations share a jurisdiction-level baseline $\mathcal{R}(t)$ with subpopulation-specific deviations
- `Counts` — hospital admissions (jurisdiction-level)
- A custom `Wastewater` class — viral concentrations (subpopulation-level)

Expand All @@ -133,7 +133,7 @@ The diagram below shows how data flows through the model. The latent process gen
flowchart TB

subgraph Latent["Latent Infection Process"]
L["Renewal equation<br/>(HierarchicalInfections)"]
L["Renewal equation<br/>(SubpopulationInfections)"]
end

subgraph Infections["Infection Trajectories"]
Expand Down Expand Up @@ -208,14 +208,14 @@ I0_rv = DistributionalVariable("I0", dist.Beta(1, 100))



### Initial Log Rt
### Log Rt at time $0$

We place a prior on the initial log(Rt), centered at 0.0 (Rt = 1.0) with moderate uncertainty:
We place a prior on the log(Rt) at time $0$, centered at 0.0 (Rt = 1.0) with moderate uncertainty:

```{python}
# | label: initial-log-rt
initial_log_rt_rv = DistributionalVariable(
"initial_log_rt", dist.Normal(0.0, 0.5)
# | label: log-rt-time-0
log_rt_time_0_rv = DistributionalVariable(
"log_rt_time_0", dist.Normal(0.0, 0.5)
)
```

Expand Down Expand Up @@ -530,10 +530,10 @@ print("Latent process configuration:")
print(f" Generation interval length: {len(gen_int_rv())} days")

builder.configure_latent(
HierarchicalInfections,
SubpopulationInfections,
gen_int_rv=gen_int_rv,
I0_rv=I0_rv,
initial_log_rt_rv=initial_log_rt_rv,
log_rt_time_0_rv=log_rt_time_0_rv,
baseline_rt_process=baseline_rt_process,
subpop_rt_deviation_process=subpop_rt_deviation_process,
)
Expand Down Expand Up @@ -824,11 +824,11 @@ idata_90 = az.from_numpyro(
model.mcmc,
dims={
"latent_infections": ["time"],
"HierarchicalInfections::infections_aggregate": ["time"],
"HierarchicalInfections::log_rt_baseline": ["time", "dummy"],
"HierarchicalInfections::rt_baseline": ["time", "dummy"],
"HierarchicalInfections::rt_subpop": ["time", "subpop"],
"HierarchicalInfections::subpop_deviations": ["time", "subpop"],
"SubpopulationInfections::infections_aggregate": ["time"],
"SubpopulationInfections::log_rt_baseline": ["time", "dummy"],
"SubpopulationInfections::rt_baseline": ["time", "dummy"],
"SubpopulationInfections::rt_subpop": ["time", "subpop"],
"SubpopulationInfections::subpop_deviations": ["time", "subpop"],
"latent_infections_by_subpop": ["time", "subpop"],
"hospital_predicted": ["time"],
"wastewater_predicted": ["time", "subpop"],
Expand Down Expand Up @@ -973,11 +973,11 @@ idata_180 = az.from_numpyro(
model.mcmc,
dims={
"latent_infections": ["time"],
"HierarchicalInfections::infections_aggregate": ["time"],
"HierarchicalInfections::log_rt_baseline": ["time", "dummy"],
"HierarchicalInfections::rt_baseline": ["time", "dummy"],
"HierarchicalInfections::rt_subpop": ["time", "subpop"],
"HierarchicalInfections::subpop_deviations": ["time", "subpop"],
"SubpopulationInfections::infections_aggregate": ["time"],
"SubpopulationInfections::log_rt_baseline": ["time", "dummy"],
"SubpopulationInfections::rt_baseline": ["time", "dummy"],
"SubpopulationInfections::rt_subpop": ["time", "subpop"],
"SubpopulationInfections::subpop_deviations": ["time", "subpop"],
"latent_infections_by_subpop": ["time", "subpop"],
"hospital_predicted": ["time"],
"wastewater_predicted": ["time", "subpop"],
Expand Down Expand Up @@ -1139,6 +1139,6 @@ This tutorial demonstrated composing a multi-signal renewal model using `Pyrenew

### Next Steps

- Explore different temporal processes for $\mathcal{R}(t)$ in the [Hierarchical Latent Infections](latent_hierarchical_infections.md) tutorial
- Explore different temporal processes for $\mathcal{R}(t)$ in the [Latent Infections](latent_infections.md) and [Latent Subpopulation Infections](latent_subpopulation_infections.md) tutorials
- Learn about count-based observation models in [Observation Processes: Counts](observation_processes_counts.md)
- Learn about continuous measurement models in [Observation Processes: Measurements](observation_processes_measurements.md)
69 changes: 34 additions & 35 deletions docs/tutorials/latent_infections.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,6 @@ title: Latent Infection Processes
format:
gfm:
code-fold: true
html:
toc: true
embed-resources: true
self-contained-math: true
code-fold: true
code-tools: true
engine: jupyter
jupyter:
jupytext:
Expand Down Expand Up @@ -44,7 +38,12 @@ from _tutorial_theme import theme_tutorial

```{python}
# | label: imports
from pyrenew.latent import SharedInfections, AR1, DifferencedAR1, RandomWalk
from pyrenew.latent import (
PopulationInfections,
AR1,
DifferencedAR1,
RandomWalk,
)
from pyrenew.deterministic import DeterministicPMF, DeterministicVariable
from pyrenew.randomvariable import DistributionalVariable
```
Expand Down Expand Up @@ -72,17 +71,17 @@ Here, $\tau$ indexes lags in the generation interval.

PyRenew provides two latent infection classes:

- **`SharedInfections`**: A single $\mathcal{R}(t)$ drives one renewal equation. Appropriate when modeling one jurisdiction as a single population with one or more observation streams. This tutorial covers `SharedInfections`.
- **`HierarchicalInfections`**: A baseline $\mathcal{R}(t)$ with per-subpopulation deviations. See [Hierarchical Latent Infections](latent_hierarchical_infections.md).
- **`PopulationInfections`**: A single $\mathcal{R}(t)$ drives one renewal equation. Appropriate when modeling one jurisdiction as a single population with one or more observation streams. This tutorial covers `PopulationInfections`.
- **`SubpopulationInfections`**: A baseline $\mathcal{R}(t)$ with per-subpopulation deviations. See [Latent Subpopulation Infections](latent_subpopulation_infections.md).

## Model Inputs

`SharedInfections` requires four inputs:
`PopulationInfections` requires four inputs:

1. Generation interval distribution $w_\tau$ (`gen_int_rv`)
2. Infection prevalence at time $0$ as a proportion of the population (`I0_rv`)
3. Value for $log(\mathcal{R}(t))$ at time $0$ (`initial_log_rt_rv`)
4. A temporal process for $\mathcal{R}(t)$ dynamics (`shared_rt_process`)
3. Value for $log(\mathcal{R}(t))$ at time $0$ (`log_rt_time_0_rv`)
4. A temporal process for $\mathcal{R}(t)$ dynamics (`single_rt_process`)

All inputs are **RandomVariables**, a quantity that is either known (observed, conditioned on) or unknown (to be inferred). See [PyRenew's RandomVariable abstract base class](random_variables.md). In this tutorial, we use `DeterministicVariable` and `DeterministicPMF` (fixed values) for illustration. In real inference, you would use `DistributionalVariable` with priors for quantities you want to estimate:

Expand Down Expand Up @@ -127,7 +126,7 @@ gi_df = pd.DataFrame({"day": days, "probability": np.array(gen_int_pmf)})

The generation interval length determines the minimum initialization period: with a $G$-point generation interval distribution, the renewal equation at time $0$ needs an infection-history vector long enough to supply the previous $G$ infection values used in the convolution.

### Initial Conditions: `I0` and `initial_log_rt`
### Initial Conditions: `I0` and `log_rt_time_0`

These two parameters jointly define the infection history before the observation
period begins. Understanding their interaction requires knowing how the latent
Expand All @@ -146,7 +145,7 @@ $$I_{\text{init}}(\tau) = I_0 \cdot e^{r \cdot \tau}, \quad \tau = 0, 1,

where $r$ is the asymptotic growth rate implied by the reproduction number at
the start of the observation period, $\mathcal{R}(t=0) =
e^{\text{initial\_log\_rt}}$, and the generation interval. The function
e^{\text{log\_rt\_time\_0}}$, and the generation interval. The function
`r_approx_from_R` converts $\mathcal{R}(t=0)$ and the generation interval into
$r$ using Newton's method.

Expand All @@ -156,17 +155,17 @@ period, $n_{\text{init}} - 1$ time points before $t = 0$. It sets the scale of
the entire initialization vector: $I_{\text{init}}(0) = I_0$, with subsequent
entries growing or declining exponentially toward $t = 0$.

* **The shape is set by `initial_log_rt`**.<br>
`initial_log_rt` enters the model in two places: it is the starting point of
* **The shape is set by `log_rt_time_0`**.<br>
`log_rt_time_0` enters the model in two places: it is the starting point of
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latex explanation below still uses the old name $e^{\text{initial\_log\_rt}}$. Perhaps search the codebase for any latex versions of the olde names.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch. thanks!

the $\mathcal{R}(t)$ trajectory ($\mathcal{R}(t=0) =
e^{\text{initial\_log\_rt}}$), and it determines the exponential growth rate
$r$ used to construct the initialization vector. When `initial_log_rt = 0`,
e^{\text{log\_rt\_time\_0}}$), and it determines the exponential growth rate
$r$ used to construct the initialization vector. When `log_rt_time_0 = 0`,
$r = 0$ and the initialization vector is flat at level `I0`. When
`initial_log_rt > 0`, infections are growing exponentially at $t = 0$; when
`initial_log_rt < 0`, they are declining.
`log_rt_time_0 > 0`, infections are growing exponentially at $t = 0$; when
`log_rt_time_0 < 0`, they are declining.


The initialization vector is what the renewal equation "sees" as recent infection history at time $0$. We can compute it directly for three values of `initial_log_rt`:
The initialization vector is what the renewal equation "sees" as recent infection history at time $0$. We can compute it directly for three values of `log_rt_time_0`:

```{python}
# | label: backprojection-compute
Expand All @@ -191,11 +190,11 @@ for label, log_rt in init_rt_values.items():
{
"day": int(t) - n_init,
"infections": float(I0_init[t]),
"config": f"initial_log_rt = {log_rt} ({label})",
"config": f"log_rt_time_0 = {log_rt} ({label})",
}
)
print(
f"initial_log_rt = {log_rt:+.1f}: Rt(0) = {float(Rt0):.2f}, r = {float(r):.4f}, "
f"log_rt_time_0 = {log_rt:+.1f}: Rt(0) = {float(Rt0):.2f}, r = {float(r):.4f}, "
f"I_init range = [{float(I0_init[0]):.6f}, {float(I0_init[-1]):.6f}]"
)

Expand All @@ -204,7 +203,7 @@ init_df = pd.DataFrame(init_data)

```{python}
# | label: fig-backprojection
# | fig-cap: Initialization vectors for three values of initial_log_rt. Days are numbered relative to day 0, which is when the temporal process and renewal equation take over. When initial_log_rt = 0 (stable), the vector is flat. Nonzero values produce exponential growth or decay in the pre-observation period.
# | fig-cap: Initialization vectors for three values of log_rt_time_0. Days are numbered relative to day 0, which is when the temporal process and renewal equation take over. When log_rt_time_0 = 0 (stable), the vector is flat. Nonzero values produce exponential growth or decay in the pre-observation period.
(
p9.ggplot(init_df, p9.aes(x="day", y="infections", color="config"))
+ p9.geom_line(size=1)
Expand Down Expand Up @@ -233,7 +232,7 @@ After day 0, the temporal process takes over. How quickly the trajectory departs
The temporal process governs how $\log \mathcal{R}(t)$ evolves day to day.

To evaluate what a given process implies, we use **prior predictive checks**: drawing many samples from the model *before seeing any data* and examining the distribution of trajectories. A single sample tells you little (the trajectory depends on the random seed), but the envelope of many samples reveals the structural constraints built into the process.
We fix the initial conditions to a growing epidemic (`initial_log_rt = 0.5`, so $\mathcal{R}(0) \approx 1.65$) with `I0 = 0.001`. Starting well above equilibrium rather than near it makes the behavioral differences between temporal processes visible: the median trajectory of a mean-reverting process drifts back toward $\mathcal{R} = 1$, while a non-reverting process does not.
We fix the initial conditions to a growing epidemic (`log_rt_time_0 = 0.5`, so $\mathcal{R}(0) \approx 1.65$) with `I0 = 0.001`. Starting well above equilibrium rather than near it makes the behavioral differences between temporal processes visible: the median trajectory of a mean-reverting process drifts back toward $\mathcal{R} = 1$, while a non-reverting process does not.

This section is primarily **modeling guidance for prior specification in PyRenew**, not a claim that epidemiologic theory uniquely determines one temporal process choice. The appropriate process depends on the scientific setting, time horizon, and how strongly you want the prior to regularize latent transmission dynamics.

Expand All @@ -242,21 +241,19 @@ This section is primarily **modeling guidance for prior specification in PyRenew
n_days = 28
n_init = len(gen_int_pmf)
n_samples = 200
initial_log_rt = 0.5
log_rt_time_0 = 0.5
I0_val = 0.001
rt_cap = 3.0


def sample_process(rt_process, label):
"""Draw prior predictive samples for a given temporal process."""
model = SharedInfections(
name="SharedInfections",
model = PopulationInfections(
name="PopulationInfections",
gen_int_rv=gen_int_rv,
I0_rv=DeterministicVariable("I0", I0_val),
initial_log_rt_rv=DeterministicVariable(
"initial_log_rt", initial_log_rt
),
shared_rt_process=rt_process,
log_rt_time_0_rv=DeterministicVariable("log_rt_time_0", log_rt_time_0),
single_rt_process=rt_process,
n_initialization_points=n_init,
)

Expand All @@ -266,9 +263,11 @@ def sample_process(rt_process, label):

samples = Predictive(sampler, num_samples=n_samples)(random.PRNGKey(42))
return {
"rt": np.array(samples["SharedInfections::rt_shared"])[:, n_init:, 0],
"rt": np.array(samples["PopulationInfections::rt_single"])[
:, n_init:, 0
],
"infections": np.array(
samples["SharedInfections::infections_aggregate"]
samples["PopulationInfections::infections_aggregate"]
)[:, n_init:],
}

Expand Down Expand Up @@ -686,7 +685,7 @@ The latent infection trajectory is not observed directly. To connect it to data,

The `PyrenewBuilder` handles the wiring:

1. **`configure_latent()`** sets the shared infection process (called once)
1. **`configure_latent()`** sets the single infection process (called once)
2. **`add_observation()`** adds an observation process (called once per data stream)
3. **`build()`** computes `n_initialization_points` from all delay distributions and produces a model ready for inference

Expand Down
Loading
Loading