import numpy as np
from summer2 import CompartmentalModel, Stratification, Multiply
from summer2.parameters import Data
def build_model():
"""Returns a model for the mixing matrix examples"""
model = CompartmentalModel(
times=[0, 20],
compartments=["S", "I", "R"],
infectious_compartments=["I"],
timestep=0.1
)
model.set_initial_population(distribution={"S": 990, "I": 10})
model.add_infection_frequency_flow(name="infection", contact_rate=2, source="S", dest="I")
model.add_transition_flow(name="recovery", fractional_rate=1/3, source="I", dest="R")
model.add_death_flow(name="infection_death", death_rate=0.05, source="I")
return model
# Age stratification with young/old mixing
age_strata = ["young", "old"]
age_strat = Stratification(name="age", strata=age_strata, compartments=["S", "I", "R"])
age_mixing_matrix = Data(np.array([
[0.2, 0.3],
[0.5, 0.7],
]) )
age_mixing_matrix.node_name = "age_mixing"
age_strat.set_mixing_matrix(age_mixing_matrix)
# Location stratification with urban/rural mixing
loc_strata = ["urban", "rural"]
loc_strat = Stratification(name="location", strata=loc_strata, compartments=["S", "I", "R"])
# Rural people have worse health care, higher mortality rates,
loc_strat.set_flow_adjustments("infection_death", {
"urban": None,
"rural": Multiply(3),
})
loc_strat.set_population_split({"urban": 0.7, "rural": 0.3})
loc_mixing_matrix = Data(np.array([
[0.8, 0.2],
[0.2, 0.8],
]))
loc_mixing_matrix.node_name = "loc_mixing"
loc_strat.set_mixing_matrix(loc_mixing_matrix)
model = build_model()
model.stratify_with(age_strat)
model.stratify_with(loc_strat)
model.run()
model.get_outputs_df().plot()summer2
This page is for people who build compartmental models with summer2 in Python, often in or near the AuTuMN group: a base model stratified by age, clinical status, immunity, strain, or place, each stratification with flow adjustments, infectiousness adjustments, and a mixing matrix; parameters that vary in time; derived outputs; and calibration through JAX. It shows a summer2 model next to the same model in Starsim, how their results agree, how long each takes, and what works and what does not yet, including what fails.
The same model in both
summer2’s mixing-matrix example: an SIR with deaths, stratified by age and by location, with a mixing matrix for each:
In Starsim, a stratification is one call, with its split; an adjustment is a value by group in the rate (ss.by()), and the two mixing matrices are one ss.contacts():
import starsim as ss
S, I, R = ss.states('S I R')
sir = ss.Disease(
infection = ss.Transition(S, I, rate=2 * I),
recovery = ss.Transition(I, R, rate=1/3),
infection_death = ss.death(I, rate=0.05*ss.by(location=[1, 3])), # Rural people have worse health care
init_n = dict(I=10),
)
sir.stratify(age=['young', 'old'], location={'urban': 0.7, 'rural': 0.3})
mixing = ss.contacts(age=[[0.2, 0.3], [0.5, 0.7]], location=[[0.8, 0.2], [0.2, 0.8]])
sim = ss.Sim(sir, networks=mixing, total_pop=1000, dt=0.1, stop=20, verbose=0)
sim.run()
sim.plot()
Every flow and state is a result, by group, so summer2’s output requests are selections: sim.results.sir.infection.select(age='old', location='rural') is request_output_for_flow(..., dest_strata=...). The same model runs as a stochastic compartmental model or as agents with solver='binomial' or solver='abm', which summer2 cannot do.
How the results agree
| Model | Agreement |
|---|---|
| The mixing-matrix example (above) | Agree: every compartment within 0.08 people of 1,000. The stochastic and agent-based runs give the same number recovered, within 1% (means of 100 runs) |
| An SEIR with clinical status entered by age, infectiousness and death adjusted by clinical status, and seven derived outputs | Agree: every compartment within 0.0006 people of 1,000, and every derived output within 0.005% of its peak |
| A TB model in 36 compartments (six ages with aging and a mixing matrix, reinfection, births and deaths, and case detection scaled up over time by summer2’s sigmoid times a fitted maximum) | Agree: every compartment within 8 people of 1.24 million (6e-6); active TB within 1 person in 1990, 2010, and 2025, and notifications within 0.5% |
Timings
Lines are lines of model code (not counting blank lines, comments, imports, or plotting, and counting a line over 100 characters twice); times are for one run, the first in a new process and a rerun of the same model. Both were measured as described on the comparison page. summer2 1.3.6’s times were recorded on the same machine, for model.run() on a model that is already built and compiled.
| Model | Lines: summer2 | Lines: Starsim | One run: summer2 | One run: Starsim | |
|---|---|---|---|---|---|
| The mixing-matrix example | 35 | 12 | first 0.8 s, rerun 0.00023 s, the first with building and compiling | first 0.012 s, rerun 0.0004 s | Pass |
| The SEIR with clinical status | 134 (55 for the model alone) | 16 | first 2.9 s, rerun 0.0003 s, the first with compiling all seven models | first 0.014 s, rerun 0.00052 s | Pass |
| A COVID model in 384 compartments (16 age groups, five clinical levels, three levels of protection) | 3.6 to 4.3 s the first time; 0.23 to 0.41 s after that | 0.2 to 0.56 s the first time; 0.08 to 0.5 s after that |
The last row was measured on 17 October 2026, on a machine shared with other jobs. Starsim’s compiled code is the same for every model, and is compiled once, so there is no wait for each new model.
What works and what does not yet
These are from Starsim’s switching demo for summer2 users (tests/switching/test_summer2.py in this repo), which checks each task against summer2’s numbers, and from the review it was built from. “Fails” means the demo’s check does not pass today.
| Task | Today |
|---|---|
| The TB model above, written by hand in about 25 lines | Works: agreement as above |
| Fit by NUTS when a fitted parameter multiplies a time series (case detection as a fitted maximum times a scale-up) | Works: the log-density in JAX equals the sim’s, and NUTS runs on the JAX path with no divergences and recovers both values within 5% |
| The log-density as a JAX function, for a sampler of your own (numpyro, estival) | Works: sim.log_density(), with .grad() by reverse mode |
| Calibration by optimization | Works: sim.fit() chose L-BFGS-B with JAX gradients for the TB model, and recovered the two values from summer2’s output to 0.2% |
| Import a live summer2 model, with its parameters and its time functions | Not in the same process: summer2 1.3.6 needs JAX 0.4, so it cannot be installed with Starsim. The same model, written to JSON by starsim/converters/summer2.py in summer2’s environment, imports with nothing missing and matches the hand-written model to 1e-13 |
| summer2’s sigmoidal scale-up | Not yet: ss.timeseries has constant, linear, and cosine interpolation, so the sigmoid is sampled into a time series |
A plain number times a time series (1.5 * ss.timeseries(...)) |
Works. A parameter with no name inline (ss.par(1.5) * ss.timeseries(...)) raises a ValueError asking for one: give it as ss.par(1.5, name='k'), or assign it to a variable first |
| The same model as a stochastic model or as agents | Works: solver='binomial', 'gillespie', or 'abm'; at a yearly step the stochastic solvers can differ a lot from the ODE, and sim.check_dt() shows how much a smaller step changes the results |
| Interventions with costs (active case finding) | Works: ss.treat(cure=0.7, moves='detection', cost=5, who=I, when=[2026, 2027], coverage=0.5) |
| Contact matrices and populations by country | Works: ss.contacts('Philippines') and ss.population('Philippines', year=2021) are bundled |
Bringing your model in
ss.import_from(model, pars=params) reads a live summer2 CompartmentalModel (if summer2 is installed alongside Starsim), and ss.import_from('model.json', format='summer2') reads the JSON written by starsim/converters/summer2.py run as a script, which runs in summer2’s own environment. Each flow becomes one transition with the matching kind of rate, and each stratify_with() a stratification, with its population split, flow adjustments, infectiousness adjustments, and mixing matrix. ss.converters.summer2.outputs(sim) works out summer2’s derived outputs from the results. What is not translated is listed (sim.imported.incomplete), and sim.imported.prompt is a prompt for an AI assistant with Starsim’s convert-model skill to finish the job.
See Import/export for importing in general: checking an import, keeping it as code, and finishing one that is incomplete.
Further reading
- summer2: documentation: building, stratifying, and calibrating compartmental models
- summer2: repository: the code and its example notebooks