# Concurrency and Duration
Two features of a network's *timing* do more to shape an epidemic than almost anything else about who contacts whom: whether partnerships **overlap** (concurrency), and how long they **last** (duration). This tutorial changes each in turn, on the baseline network from the [previous tutorial](mod5-Movie.html), and closes with a single replicated-run comparison across all four specifications, so we can say not just how each mechanism works but how large its effect is.
::: {.callout-note}
Download the R script to follow along with this tutorial [here](mod5-Timing.R).
:::
## Setup
We load the packages, and define the animation settings and a small helper that wraps the four-step pipeline from Tutorial 1 (extract the network, color by status, compute the layout, render). From here on, one call to `animate(sim)` turns a simulation into a movie, so the network specification stays in the foreground.
```{r setup, message = FALSE, warning=FALSE}
library("EpiModel")
library("ndtv")
library("htmlwidgets")
slice.par <- list(start = 1, end = 25, interval = 1, aggregate.dur = 1, rule = "any")
render.par <- list(tween.frames = 10, show.time = FALSE)
plot.par <- list(mar = c(0, 0, 0, 0))
# The Tutorial 1 pipeline, wrapped so each model is a single call.
animate <- function(sim) {
nw <- get_network(sim)
nw <- color_tea(nw, verbose = FALSE)
compute.animation(nw, slice.par = slice.par, verbose = FALSE)
render.d3movie(
nw, render.par = render.par, plot.par = plot.par,
vertex.cex = 0.9, vertex.col = "ndtvcol",
edge.col = "darkgrey", vertex.border = "lightgrey",
displaylabels = FALSE, output.mode = "htmlWidget")
}
```
We also keep the epidemic fixed throughout, so that every difference we see comes from the network. It is the same poker-chip SI process as Tutorial 1.
```{r epi-params}
param <- param.net(inf.prob = 1)
init <- init.net(i.num = 10)
control <- control.net(type = "SI", nsteps = 25, nsims = 1)
```
## The Baseline
We re-fit the edges-only baseline so this page stands on its own. This is the reference every change below is measured against.
```{r base-netest, message=FALSE, results="hide"}
set.seed(1234)
nw <- network_initialize(n = 100)
coef.diss <- dissolution_coefs(dissolution = ~offset(edges), duration = 20)
est.base <- netest(nw, ~edges, 40, coef.diss)
sim.base <- netsim(est.base, param, init, control)
```
```{r base-movie, message=FALSE, fig.width=6}
animate(sim.base)
```
## Concurrency
Concurrency is the overlap of partnerships in time: a node with two or more partners at once. It speeds transmission, because an infection can pass across an overlapping partnership without waiting for one relationship to end before the next begins. We add the `concurrent` term, which counts the nodes with a momentary degree (the number of partners at a single instant) of two or more, and drive it to the extreme value of **zero**: no overlap at all, a network of strict serial monogamy.
You established this same result numerically in [Module 4's Lab 1](../4_basic_epimodel/mod4-Lab1.html), with diagnostics and replicates. Here we look at it from the other side, as a mechanism you can watch one contact at a time.
Before choosing a target, it helps to know how much concurrency chance alone would produce. Our mean degree of 0.8 means that if partnerships formed at random, degree would be roughly Poisson with mean 0.8, so the expected number of nodes with two or more partners is the upper tail of that distribution, times 100:
```{r conc-ppois}
ppois(1, lambda = 2 * 40 / 100, lower.tail = FALSE) * 100
```
So about 19 nodes would be concurrent by chance. A target of 0 asks for far less than that.
```{r conc-netest, message=FALSE, results="hide"}
set.seed(1234)
nw <- network_initialize(n = 100)
est.noconc <- netest(nw, ~edges + concurrent, c(40, 0), coef.diss)
sim.noconc <- netsim(est.noconc, param, init, control)
```
With no concurrency, the edges must form a **matching**: disjoint pairs, no node with more than one partner. That also sets a hard ceiling on mean degree of 1.0, since at most `N / 2` edges can fit among `N` nodes when none may share. At our target of 40 edges, roughly 80 of the 100 nodes are paired and about 20 sit isolated at any moment, though as always these are expected values: the realized edge count wanders (this movie typically shows anywhere from the mid-30s to the low-40s), and which nodes are paired changes step to step. Watch how this "serial monogamy" constrains spread: an infection has to wait for one tie to dissolve before it can travel along the next.
```{r conc-movie, message=FALSE, fig.width=6}
animate(sim.noconc)
```
The same constraint operates well beyond sexual partnerships. Wherever contacts happen strictly one at a time (a home care aide who finishes with one client before visiting the next, say), an infection must wait for one tie to end before the next opens.
## Duration
Duration is the other timing dial, and unlike concurrency it has no single "right" setting: what matters is duration *relative to two other timescales*.
::: {.callout-note .concept}
## Duration only means something relative to the time step and the infectious period
A duration is a number of time steps, but the number alone tells you little. Two comparisons give it meaning:
- **Against the time step.** A duration of one step is *memoryless*: every edge is redrawn each step, so partner identity carries no information from one step to the next. Longer durations carry memory, in that the same partners persist.
- **Against the infectious period.** If partnerships turn over fast compared with how long a person stays infectious, an infected person meets many *different* people while infectious, and the process approaches well-mixed mass action. If partnerships outlast the infectious period, transmission is confined to whoever you are currently partnered with.
This SI module has no recovery, so the infectious period is effectively the whole 25-step run, and we are really looking at duration against that horizon. In a disease *with* recovery, the comparison that transfers is duration against the infectious period: the same 20-step partnership is "long" for a 3-day infection and "short" for a 3-year one.
:::
We take duration to its two extremes. Each reproduces a modeling tradition you likely already know.
### Very long duration: a static network
We set the mean duration to 100,000 steps, which fixes every edge for the whole 25-step run. Everything else matches the baseline.
```{r long-netest, message=FALSE, results="hide"}
set.seed(1234)
nw <- network_initialize(n = 100)
coef.long <- dissolution_coefs(dissolution = ~offset(edges), duration = 1e5)
est.long <- netest(nw, ~edges, 40, coef.long)
sim.long <- netsim(est.long, param, init, control)
```
Very little happens, even at 100% transmission, and the reason is structural rather than epidemiological. A network that never dissolves an edge is frozen: whatever component a seed lands in at step 1 is the entire world it will ever reach. At a mean degree of 0.8 most components are tiny, so each seed saturates its own little cluster within a few steps and then stops. The infection does not slow down; it runs out of network.
```{r long-movie, message=FALSE, fig.width=6}
animate(sim.long)
```
::: {.callout-note .concept}
## You have just built a static network model
A frozen edge set is what a large and productive tradition in network epidemiology assumes by design: configuration models, percolation approaches, and many of the classic threshold results all run an epidemic on a single fixed graph. The assumption buys tractability and, for particular model families, elegant analytic results.
It also decides several things silently, and this model is showing them:
- **The reachable set is fixed at time zero.** Whoever shares your component is reachable; whoever does not, never will be. The epidemic's ceiling is set before it starts. This is the one you are watching.
- **Momentary, cumulative, and total degree collapse into one number**, because no edge ever ends. In a dynamic network these are three different quantities.
- **Every partnership coexists with every other**, so concurrency is maximal by construction. There is no serial monogamy in a frozen graph.
Two boundaries are worth stating so the lesson does not overreach. First, those last two points are properties of *this* idealization, a permanent, cross-sectional edge set, not of every study that uses a fixed graph; a static network can also be read as a cumulative or momentary contact network, with different meanings. Second, the clean analytic results, thresholds and final sizes from the degree distribution alone, hold for specific ensembles (large, locally tree-like configuration models); clustering, degree correlations, finite size, and the seeding all move them. [Keeling and Eames (*J R Soc Interface*, 2005)](https://pubmed.ncbi.nlm.nih.gov/16849187/) reviews this family and states plainly that real contact networks are dynamic and static results are an approximation.
:::
### One-step duration: rapid mixing
Now the opposite extreme: a mean duration of one step, so every edge lasts exactly one step and then dissolves. There is no persistence at all; each step is an independent draw of momentary contacts.
A duration of one is the shortest a partnership can be, and it is worth seeing what it does to the coefficient. Persistence probability is `1 - 1/duration`, which is zero at a duration of one, and the coefficient is the logit of zero:
```{r short-coef}
dissolution_coefs(dissolution = ~offset(edges), duration = 1)
```
Negative infinity is the correct answer, not an error: an edge persists to the next step with probability zero. This is the one place in the course where a dissolution coefficient is not a finite number.
```{r short-netest, message=FALSE, results="hide"}
set.seed(1234)
nw <- network_initialize(n = 100)
coef.short <- dissolution_coefs(dissolution = ~offset(edges), duration = 1)
est.short <- netest(nw, ~edges, 40, coef.short)
sim.short <- netsim(est.short, param, init, control)
```
The edges turn over completely every step, which makes the movie a jumble of moving nodes, and the epidemic saturates almost at once. Compare it against the very-long-duration run: same mean degree, same number of edges at any moment. All that differs is whether those edges persist. There the infection ran out of network; here the network is rebuilt from scratch every step, so it never runs out.
```{r short-movie, message=FALSE, fig.width=6}
animate(sim.short)
```
::: {.callout-note .concept}
## You have just built a compartmental model
If the frozen network was the static tradition, this is the other one, the one most people arrive with. A compartmental model writes the infection flow as $\beta S I / N$. Ask where the partnerships are in that expression, and the answer is that there are none: contact is instantaneous, each contact is a fresh independent draw from the population, nobody has a partner, only a contact rate.
That is close to what a one-step duration builds. Partner identity becomes memoryless: every tie is redrawn each step, so an infected person keeps meeting fresh susceptibles rather than re-encountering the same people. No local depletion (your contacts never become a pool of already-exposed people) and no repeated contact (real social contact is dominated by seeing the same people again, which this model does not do).
::: {.callout-note}
## The headline is a stylistic generalization, not an identity
"You have just built a compartmental model" is a useful slogan, but be precise about what it is: a *stochastic, discrete-time network* whose partner set is redrawn each step, an analogue of the rapid-mixing limit an ODE describes, not the ODE itself. Two things the slogan elides. Mass action does not require meeting a brand-new person every single step; it requires only that contacts are well mixed relative to the infectious period. And rapid mixing does not necessarily *overstate* spread: whether erasing partnership memory raises or lowers the final size depends on the disease, the degree distribution, and the outcome you measure. Here, with certain transmission and no recovery, it saturates. That is this specification, not a universal law.
:::
:::
### The dial, and the replicated evidence
Put the two extremes side by side. Same population, same mean degree, same number of edges at any instant, same transmission probability. Opposite epidemics. And each is a whole modeling tradition:
| specification | contacts | what it reproduces |
|---|---|---|
| **duration 1** | shortest possible, redrawn every step | what a compartmental (ODE) model assumes |
| durations in between (Tutorials 1 and 3) | persist for a while, then end | a contact process with genuine duration |
| **duration 100,000** | permanent | what a static network model assumes |
Neither tradition chose its assumption because contacts really work that way; each chose it for tractability. Both are compressions of a contact process that has duration, and the compression is not free: it changes who is reachable, when, and how much a contact is worth. The point of the framework is that **duration is a parameter you set, not an assumption baked into your model class**. You can sit anywhere on the dial, and reproduce either endpoint on purpose, as a comparison, which is what we just did.
This dial, duration measured against the epidemic's own timescale, is worth holding onto. An [optional appendix](mod5-Timescale.html) after the lab develops it into a single organizing idea, the network's clock against the epidemic's, under which the two endpoints become the slow- and fast-network limits of one ratio and concurrency turns out to be the feature that matters in one regime rather than a universal law. That is why we have treated concurrency here as one setting on the dial rather than the point of the module.
None of this makes the dynamic model automatically the right one. A compartmental model is transparent, cheap, and often exactly right for a question defined at the group level, and its assumptions hide nothing. Match the model to the question; what this module gives you is the ability to see what each choice costs, so the match is deliberate.
Finally, the replicated evidence. Everything above described *single* movies, which show mechanisms but cannot measure them. Here we run each specification 200 times and report the mean number infected at the final step. These numbers are specific to *these* specifications (this network size, this SI process, this seeding, this horizon); read them as replicated evidence for the directions the movies suggested, not as universal effects of concurrency or duration.
```{r replicated, message=FALSE, warning=FALSE, results="hide"}
final_prev <- function(formation, target.stats, duration, nsims = 200) {
set.seed(1234)
nw <- network_initialize(n = 100)
cd <- dissolution_coefs(~offset(edges), duration = duration)
est <- netest(nw, formation, target.stats, cd, verbose = FALSE)
sim <- netsim(est, param, init,
control.net(type = "SI", nsteps = 25, nsims = nsims,
ncores = 5, verbose = FALSE))
d <- as.data.frame(sim, out = "mean")
round(d$i.num[d$time == 25], 1)
}
evidence <- data.frame(
specification = c("Baseline (edges only, duration 20)",
"No concurrency (duration 20)",
"Very long duration (100,000)",
"One-step duration (1)"),
mean_infected_of_100 = c(
final_prev(~edges, 40, 20),
final_prev(~edges + concurrent, c(40, 0), 20),
final_prev(~edges, 40, 1e5),
final_prev(~edges, 40, 1)))
```
```{r replicated-show}
evidence
```
Across 200 runs the directions the movies suggested hold, each measured against the same baseline: removing concurrency roughly halves the final size; freezing the network (very long duration) lowers it even more, for a different reason (seeds run out of reachable network rather than losing concurrency, since a frozen graph is in fact maximally concurrent); and one-step mixing saturates the whole population. The [next tutorial](mod5-Structure.html) turns from timing to structure: clustering, and contact patterns that depend on who people are.