|
| 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