Skip to content

Commit d8d009d

Browse files
committed
Merge partner notification example
2 parents 175c27d + 074d4b5 commit d8d009d

5 files changed

Lines changed: 1615 additions & 0 deletions

File tree

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# Partner Notification for an Endemic STI
2+
3+
## Description
4+
5+
This example demonstrates how to build a **partner notification (PN) intervention** for an endemic bacterial STI (chlamydia-like), modeled as an SIS process on a dynamic sexual contact network. Indices are detected by routine screening; a custom `partner_services` module looks each index's recent partners up in the cumulative edgelist and either Patient Referral (PR) or Expedited Partner Therapy (EPT) routes them through treatment.
6+
7+
The pedagogical core is the **cumulative-edgelist API**: `get_partners()`, `get_posit_ids()`, `get_unique_ids()`, and the three `control.net()` flags that turn the engine on. Once a custom module can ask "who were this person's partners in the last 60 weeks?", any partner-based intervention is a matter of choosing what to do with the returned set.
8+
9+
Five scenarios share the same network and disease parameters and differ only in the PN configuration:
10+
11+
| Scenario | `pn.arm` | `pn.trace.prob` | `pn.lookback` |
12+
|----------|----------|-----------------|---------------|
13+
| Screening only | `"none"` | 0 | n/a |
14+
| Patient Referral | `"PR"` | 0.5 | 60 |
15+
| EPT | `"EPT"` | 0.5 | 60 |
16+
| EPT, longer lookback | `"EPT"` | 0.5 | 120 |
17+
| EPT, high trace + long lookback | `"EPT"` | 0.8 | 120 |
18+
19+
## Model Structure
20+
21+
### Disease Compartments
22+
23+
| Compartment | Label | Description |
24+
|-------------|-------|-------------|
25+
| Susceptible | **S** | Not infected; at risk and reinfectable |
26+
| Infectious | **I** | Infected and transmitting |
27+
28+
### Flow Diagram
29+
30+
```mermaid
31+
flowchart LR
32+
S["<b>S</b><br/>Susceptible"] -->|"infection<br/>(si.flow)"| I
33+
I["<b>I</b><br/>Infectious"] -->|"natural clearance<br/>(is.flow.natural)"| S
34+
I -->|"screen + treat index<br/>(n.index.tx)"| S
35+
I -->|"partner notification<br/>(n.partner.cleared.pn)"| S
36+
37+
style S fill:#3498db,color:#fff
38+
style I fill:#e74c3c,color:#fff
39+
```
40+
41+
Three return paths from `I` to `S`: slow natural clearance, treatment of screened-positive indices, and treatment of notified partners under the active PN arm.
42+
43+
### Auxiliary Node Attributes
44+
45+
| Attribute | Type | Purpose |
46+
|-----------|------|---------|
47+
| `diag.status` | 0/1 | Sticky: 1 if the node was ever diagnosed |
48+
| `dx.time` | int | Step of most recent positive diagnosis |
49+
| `dx.this.step` | 0/1 | Trigger for partner notification and index treatment in the current step |
50+
| `tx.this.step` | 0/1 | Whether the node received any treatment this step (index or notified partner) |
51+
| `pn.notified` | int | Step at which the node was most recently notified as a partner |
52+
| `infections` | int | Running count of S to I transitions for reinfection analysis |
53+
54+
## The Cumulative Edgelist Pattern
55+
56+
Three flags on `control.net()`:
57+
58+
```r
59+
control.net(
60+
...,
61+
cumulative.edgelist = TRUE, # turn the engine on
62+
truncate.el.cuml = max.lookback,# drop edges older than this
63+
save.cumulative.edgelist = TRUE # attach to returned sim
64+
)
65+
```
66+
67+
Inside `partner_services()`:
68+
69+
```r
70+
idsIndex <- which(active == 1 & dx.this.step == 1)
71+
part_df <- get_partners(dat, idsIndex,
72+
truncate = pn.lookback,
73+
only.active.nodes = TRUE)
74+
75+
# get_partners returns UNIQUE IDs in the `partner` column. Convert to
76+
# positional IDs before indexing into per-node vectors.
77+
partner_pid <- get_posit_ids(dat, part_df$partner)
78+
partner_pid <- partner_pid[!is.na(partner_pid)]
79+
```
80+
81+
The unique-vs-positional ID round-trip is the most common stumbling block of the cumulative-edgelist API. Per-node vectors in `dat` (status, active, all attributes) are indexed by **positional ID**. The `partner` column of `get_partners()` is in **unique-ID** space, because past partners may have departed the population and no longer have a positional ID. Convert before doing anything else.
82+
83+
## Modules
84+
85+
### Screening Module (`screen`)
86+
87+
Routine screening of infected actives at rate `screen.rate` per timestep. A positive sets `dx.this.step = 1` (the PN trigger), stamps `dx.time = at`, and sets `diag.status = 1`. The `diag.status` flag is sticky across the run, persisting after later clearance. Initializes all custom attributes at `at == 2`.
88+
89+
### Partner Notification Module (`partner_services`)
90+
91+
The headline module. Identifies fresh-positive indices, calls `get_partners()` with the configured lookback, converts unique IDs to positional IDs, applies a Bernoulli trace at `pn.trace.prob`, and stamps reached partners with `pn.notified = at`. Records `n.partners.elig` and `n.partners.reached` per step.
92+
93+
### Treatment Module (`treat`)
94+
95+
Two pathways. Indices (`dx.this.step == 1`) are always treated at `tx.efficacy`. Notified partners (`pn.notified == at`) are handled per `pn.arm`:
96+
97+
- `"none"`: nothing.
98+
- `"PR"`: test the partner at sensitivity `pn.test.prob`, then treat at `tx.efficacy` if positive. Uninfected partners are not treated.
99+
- `"EPT"`: dispense medication directly at `ept.efficacy`. Treats both infected and uninfected partners; uninfected treatment is counted as a wasted dose but has no epidemic effect.
100+
101+
Records `n.index.tx`, `n.partner.tx`, `n.partner.tx.wasted`, `n.partner.cleared.pn` per step.
102+
103+
### Recovery Module (`recov`)
104+
105+
Slow natural clearance I to S at `rec.rate`. Records `is.flow.natural`.
106+
107+
### Infection Module (`infect`)
108+
109+
Standard discordant-edge transmission. Increments a per-node `infections` counter on every fresh S to I for reinfection analysis. Records `si.flow`.
110+
111+
## Parameters
112+
113+
### Transmission and Recovery
114+
115+
| Parameter | Description | Default |
116+
|-----------|-------------|---------|
117+
| `inf.prob` | Per-act transmission probability | 0.18 |
118+
| `act.rate` | Acts per partnership per week | 1 |
119+
| `rec.rate` | Weekly natural clearance rate | 0.02 (mean ~50 wk) |
120+
121+
### Screening and Treatment
122+
123+
| Parameter | Description | Default |
124+
|-----------|-------------|---------|
125+
| `screen.rate` | Weekly probability of screening for an infected | 0.025 |
126+
| `tx.efficacy` | Probability a treated index clears | 0.95 |
127+
| `ept.efficacy` | Probability a partner takes EPT meds and clears | 0.85 |
128+
| `pn.test.prob` | Sensitivity of the PR returning-partner test | 0.85 |
129+
130+
### Partner Notification
131+
132+
| Parameter | Description | Varied |
133+
|-----------|-------------|--------|
134+
| `pn.arm` | `"none"`, `"PR"`, `"EPT"` | Yes |
135+
| `pn.trace.prob` | Bernoulli probability a partner is reached | 0, 0.5, 0.8 |
136+
| `pn.lookback` | Weeks of lookback on the cumulative edgelist | 60 or 120 |
137+
| `pn.start` | Step at which PN switches on after burn-in | 300 |
138+
139+
### Network
140+
141+
| Parameter | Description | Default |
142+
|-----------|-------------|---------|
143+
| Population size | Number of nodes | 1000 |
144+
| Mean degree | Edges per node | 1.2 |
145+
| Concurrency | Nodes with degree > 1 | ~18% |
146+
| Partnership duration | Mean edge duration (weeks) | 100 |
147+
148+
## Module Execution Order
149+
150+
```
151+
resim_nets -> summary_nets -> infection -> screen -> partner_services ->
152+
treat -> recovery -> nwupdate -> prevalence
153+
```
154+
155+
`screen` runs first so this step's fresh indices are visible to the downstream modules. `partner_services` reads them, queries the cumulative edgelist, and stamps notifications. `treat` reads both `dx.this.step` (indices) and `pn.notified == at` (partners) and applies the arm-specific cascade. `recovery` handles slow natural clearance.
156+
157+
## Caveats
158+
159+
`pn.lookback = 60` weeks is much longer than the real CDC guidance (60 days for chlamydia/gonorrhea). The partnership durations and act rates here are tuned for clean endemic-equilibrium teaching, not for calibrating to U.S. chlamydia surveillance data. Treat all numbers as illustrative.
160+
161+
## Next Steps
162+
163+
- Add a clinic-visit delay between index diagnosis and partner outreach.
164+
- Stratify trace rate by partnership type (main vs. casual) via a multilayer network and the `networks` argument of `get_partners()`.
165+
- Allow re-notification of partners who were notified earlier but never treated.
166+
- Replace EPT efficacy with a recency-dependent stochastic uptake decision, modeled from the `start` column of `get_partners()`.
167+
- Pair with a contact-tracing example for a respiratory pathogen sharing the same API.
168+
169+
## Author
170+
171+
Samuel M. Jenness, Emory University (http://samueljenness.org/)

0 commit comments

Comments
 (0)