An interactive, single-file simulator of the birth-death-sampling (BDS) process used in phylodynamics. Open it in a browser and explore how the epidemiological parameters shape both the epidemic and the tree you could reconstruct from it.
Live version: https://plemey.github.io/bds-simulator/
Each infected host is a lineage that, while infectious, undergoes three competing exponential processes:
| Event | Rate | Effect |
|---|---|---|
| Birth | λ | transmission — a new lineage starts |
| Death | μ | becomes uninfectious, unobserved |
| Sampling | ψ | becomes uninfectious and is observed |
A sampled host is removed from the infectious population with probability r, the treatment probability, and remains infectious otherwise. The true removal rate is therefore μ + ψr, not μ + ψ. The simulation uses the Gillespie algorithm: exponential waiting times to the next event across all active lineages, then a draw for which lineage and which event.
Three quantities are derived from the rates and shown in the interface:
- Re = λ / (μ + ψr) — expected onward infections per case. Above 1 the epidemic grows.
- D = μ + ψr — total rate of becoming uninfectious.
- S = ψr / (μ + ψr) — the fraction of removals that are actually observed.
These match BEAST, whose effectiveReproductiveNumber is computed as
birthRate / (samplingRate * treatmentProbability + deathRate). At r = 1 they reduce to
the familiar λ/(μ+ψ), μ+ψ and ψ/(μ+ψ) quoted in most write-ups of the model, which assume
sampling always removes.
The same simulated epidemic is shown three ways, which is the point of the tool.
Full process — every lineage the process ever produced, including the many that died without ever being sampled.
Sub process — the sampled lineages plus the unsampled ancestors that carried the infection between them. Side branches that led nowhere are dropped, but unsampled ancestors remain: they are real hosts who were never observed.
Observed phylogeny — the reconstructed tree, i.e. what you could infer from the samples alone. Built by taking the event tree (each birth a bifurcation, each non-removing sampling event a unary node), pruning every subtree with no sampled descendant, and then suppressing each unsampled node left with a single surviving child so its two branches merge into one. Unsampled ancestors are exactly those degree-2 nodes, so they all dissolve.
Sampled unary nodes are not suppressed: a host sampled while remaining infectious, whose descendants are also sampled, survives as a sampled ancestor sitting partway along a branch rather than at its tip (Gavryushkina et al., 2014). Whether a given sample ends up a tip or an ancestor is decided by the pruning, not at simulation time — if its continuation leaves no further samples, it collapses to an ordinary tip.
At r = 1 there are no sampled ancestors and the tree is strictly bifurcating with n tips and exactly n−1 internal nodes. Below 1 the identity becomes bifurcations = tips − 1, with sampled ancestors counted separately, and every sampling event still appears somewhere in the tree.
The gap between the first and third views is the phylodynamic inference problem: the "of process retained" statistic reports how little of the epidemic the data actually sees. At low ψ it is routinely a few percent.
No build, no dependencies, no server. Open index.html in any modern browser, or visit the
live URL above.
Push this directory to a GitHub repository, then in Settings → Pages set the source to
the main branch, root folder. The page appears at
https://<username>.github.io/<repo>/ within a minute or so.