Skip to content

Commit 074d4b5

Browse files
committed
Add partner notification example for endemic STI
Demonstrates partner notification (partner services) for chlamydia-like endemic STIs using the cumulative-edgelist API: get_partners() with a lookback window to find sexual partners of newly-diagnosed indices, get_posit_ids() to round-trip unique IDs back to positional IDs, and downstream treatment via either Patient Referral (test then treat) or Expedited Partner Therapy (EPT, treat without testing). Compares five scenarios spanning PR vs EPT, trace probability, and lookback window length.
1 parent 6f004fe commit 074d4b5

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)