epidemics: the default SEIR-V by age
The model of epidemics’ “Get started” vignette: the package’s default model (model_default()), an SEIR-V in the UK in three age groups (0–19, 20–39, and 40+), mixing by a contact matrix from the POLYMOD survey, with R0 = 1.3, a latent period of 2 days, and an infectious period of 7, run for 600 days from one infectious person in a million. There is no vaccination, so V stays empty. See the review.
model.py is the Starsim version, and compare.py checks it against reference/, a run of the vignette.
Comparison
| Measure | Original | Starsim | |
|---|---|---|---|
| Lines of model code | 36 | 12 | Both count their data: 16 of the original’s lines make the contact matrix and demography from the survey, and 2 of Starsim’s give them as numbers |
| One run | first 0.02 s, rerun 0.009 s | first 0.59 s, rerun 0.0011 s (JAX) | The vignette’s one call of model_default(), which includes checking its inputs and making its table of results; the odin model was compiled when the package was installed. Starsim’s first run includes building the sim |
| Results | Agree to 1.4e-7 of each state’s largest value, in total and in each age group, on every day. I peaks at 1,231,054 on day 369 in both, and 23,438,153 people are infected in both. compare.py allows 1e-6 |
Measured on 9 November 2026 with Starsim 4.0.0rc3 (the v4 branch, commit be38b9f6), on a 20-core Linux machine.
What was declared and what was not
Everything is declared: four states, three transitions, a stratification by age with the population of each group, and the contact matrix as ss.contacts(). The original’s transmission rate is R0 over the infectious period, with the contact matrix scaled to a leading eigenvalue of 1; Starsim’s is the same, with the eigenvalue written out in the rate. Starsim prints R0 = 1.3 at the start of the run, from the next-generation matrix, which confirms the scaling.
| Original | Starsim |
|---|---|
socialmixr::contact_matrix(polymod, ...) and wpp2024, 16 lines |
matrix and N, as the vignette prints them |
initial_conditions, a row of proportions for each age group |
init_n=dict(I=1e-6*sum(N)), which is split in proportion to the groups |
population(...) |
seir.stratify(age=[0, 20, 40], fractions=N) and networks=ss.contacts(matrix) |
model_default(population, time_end = 600), with its defaults transmission_rate = 1.3/7, infectiousness_rate = 1/2, recovery_rate = 1/7 |
The three transitions, with the defaults written in |
V, a compartment of the fixed model |
Left out, since nothing reaches it |
Findings
- The contact matrix needs no conversion here. epidemics transposes the matrix, divides it by its leading eigenvalue, and divides each row by the size of its group, so that
lambda[i] = beta * sum(C[j, i]/N[i] * I[j]). Starsim’s frequency-dependent mixing islambda[i] = beta * sum(C[i, j] * I[j]/N[j]). The two are the same when the matrix is reciprocal (C[i, j]*N[i] = C[j, i]*N[j]), as socialmixr’s is withsymmetric = TRUE, as here. A matrix that is not reciprocal would give different epidemics in the two, and a translation would have to transpose and rescale it. This is a difference of convention, not a Starsim issue, and the vignette’s note on the two conventions says the same. - R0 is reported, not given. The original’s parameter is R0, through the scaling of the matrix. Starsim works out R0 from the next-generation matrix and prints it, but the rate is still written as
1.3/7/max(eigenvalues), as in the epydemix models. A way to give the transmission rate by its R0 (for examplebeta=ss.R0(1.3), solved at the start from the same next-generation matrix) would state what the vignette means directly. - Only the states that are used. epidemics’ models are fixed, so this one has a
Vcompartment that stays empty. Starsim states the model the vignette uses, without it; the next model, seasonal_scenarios, adds it with the vaccination.
Why this model
It is epidemics’ “Get started” example, and the base of every other vignette of the default model: the population, contact matrix, and initial conditions here are shared by them. It is the simple one of the two epidemics models; seasonal_scenarios is the other.
What it exercises
- An SEIR stratified by age, mixing by a contact matrix from a survey
- A transmission rate given by R0, through the leading eigenvalue of the matrix
- Initial conditions as a share of each group
Files
model.py: the Starsim versioncompare.py: checks it againstreference/original/epidemics.R: the vignette’s code (vignettes/epidemics.Rmdat thev0.5.0tag), asknitr::purl()writes it, unchangedrun.py: runs it in the epidemics environment, giving the contact matrix and demography as the vignette prints them in place of the 16 lines that load them (see the framework’s README), and leaving out the plot; it writesreference/reference/results.csv:S,E,I,R, andVin each age group (I_0-19,I_20-39,I_40+) and in total, on each day from 0 to 600reference/timing.json: the time of the vignette’s call ofmodel_default(), and of a rerunmeta.yaml: where it came fromLICENSE: the upstream license (MIT)
The model
For each age group i of 3:
- Infection:
lambda[i] = beta * sum(C[j, i]/N[i] * I[j]), withbeta = 1.3/7per day andCthe contact matrix divided by its leading eigenvalue (14.1, 12.2, and 9.2 contacts a day in the three groups, before scaling; the leading eigenvalue is 11.8) - Latency of 2 days and recovery after 7, both exponential
- Population: 14,865,748, 16,978,469, and 29,438,434 people aged 0–19, 20–39, and 40+ (the UK in 2006, from
wpp2024), with POLYMOD’s UK contacts by the same groups - Initial conditions: one in a million infectious in each group, so 61 in all
- Time: days 0 to 600, solved by deSolve’s
lsodathrough odin, with results once a day
Results
The epidemic grows slowly (R0 is 1.3), peaks on day 369 with 1.23 million infectious, and infects 23.4 million people, 38% of the population, by day 600.
Lines of model code
36: the lines of epidemics.R that are not blank or comments, without library(), knitr’s options, the plot, and the lines that only print an object (contact_matrix, demography_vector, initial_conditions, uk_population). 16 of them make the contact matrix and the demography. No line is over 100 characters. Starsim’s 12 are counted the same way.