FAQ
Basics
How does this API work?
It's a stateless REST API built on top of epydemix. You send a single JSON POST to /api/v1/simulations describing the model, population, and run settings, and you get back compartment trajectories and transition counts for every simulation run. No server-side state between requests, and no client-side simulation work, all the heavy process (e.g., stochastic runs) happens on the API. See the Introduction for the request/response flow and a list of endpoints.
Do I need to install Python?
No. The API runs Python on the server; the client just speaks HTTP+JSON. Anything that can send a POST works (curl, JavaScript fetch, Python requests, R httr, Postman, a browser via the playground, etc.).
How can I try it without writing code?
Use the Playground: pick a template (SIR, SEIR, V-SEIHR, etc.), edit the JSON in the browser, hit Run, and see the response inline.
What kind of simulation does it do?
It runs a discrete-time stochastic compartmental simulation. At each step the simulator draws a multinomial for the people leaving every source compartment, splitting them across the possible target compartments according to the per-step transition probabilities 1 - exp(-rate × dt). The same request runs Nsim independent trajectories so you can quantify variability across stochastic realizations.
Population structure is built in, and by default, every run uses an age-stratified population with a contact matrix governing who mixes with whom.
See Model Presets › Overview for the full set of compartmental presets and the math notation, and Populations › Overview for built-in vs custom population options.
What is dt?
The simulation time step, in days. Set on the request as simulation.dt (default: 1.0). The stochastic engine advances the system in chunks of dt and computes per-step transition probabilities as 1 - exp(-rate × dt), then draws multinomial outcomes against the source population.
Why you might change it:
dt = 1.0(default): one step per day. Fast, and accurate when no per-step probability is large (rate × dtmuch less than 1). For most respiratory-disease parameter sets this is fine.dt < 1.0(e.g.0.5,0.25): sub-daily steps. Slower but more accurate when per-step probabilities approach saturation. Smallerdtbrings the discrete-time dynamics closer to the underlying continuous-time rates.
The response is always resampled to daily by default (resample_frequency: "D"), so the dates returned in responses are the same regardless of dt.
Models
How do I define a custom model?
Skip model.preset and provide three fields under model: a list of compartments, a parameters dict, and a list of transitions. Each transition has a source compartment, a target, a kind (spontaneous or mediated), and params referencing entries from parameters.
"model": {
"compartments": ["S", "E", "I", "R"],
"parameters": {
"transmission_rate": 0.3,
"incubation_rate": 0.2,
"recovery_rate": 0.1
},
"transitions": [
{"source": "S", "target": "E", "kind": "mediated", "params": ["transmission_rate", "I"]},
{"source": "E", "target": "I", "kind": "spontaneous", "params": "incubation_rate"},
{"source": "I", "target": "R", "kind": "spontaneous", "params": "recovery_rate"}
]
}
See Model › Custom Models for the full structure and a worked SEIRH example.
How do I make simulations reproducible?
Pass an integer seed under simulation. The same request with the same seed returns identical trajectories.
"simulation": {
"start_date": "2024-01-01",
"end_date": "2024-06-01",
"Nsim": 10,
"seed": 42
}
Populations
How do I run a homogeneous (single-group) model?
Skip the built-in populations and pass a custom population with one age group and a 1×1 contact matrix. This collapses the contact structure to "everyone mixes with everyone at rate 1" and is useful for sanity checks, theory comparisons, and unit-test-style scenarios.
"population": {
"source": "custom",
"name": "Homogeneous 1M",
"age_groups": {"all": 1000000},
"contact_matrices": {"all": [[1.0]]}
}
The group label ("all" here) is arbitrary; pick anything readable. Any transmission_rate / per-capita rate parameter you set then applies uniformly across the whole population.
See Populations › Custom Populations for multi-group custom setups (e.g. children vs adults with a 2×2 contact matrix).
Parameters
How do I set a rate instead of a period for a transition?
Preset disease-history parameters accept either a period (in days) or its corresponding rate (per day). Pass whichever form is more convenient and the resolver injects the other automatically as rate = 1 / period.
| Period (days) | Rate (1/day) |
|---|---|
incubation_period | incubation_rate |
infectious_period | recovery_rate |
hosp_duration | hosp_recovery_rate |
immunity_duration | waning_rate |
For example, to set the recovery rate directly instead of the infectious period:
"parameters": {
"R0": 2.5,
"incubation_period": 3.0,
"recovery_rate": 0.4
}
Pass either the period or the rate, not both. If both are sent the rate form wins and the period is dropped silently. Conversions are preset-scoped; see each preset's Parameters table for which conversions it accepts.
How do I set the transmission rate directly instead of R0?
R0 and transmission_rate are parameters for transmission, and are associated with each other. By default, the preset takes R0 and derives transmission_rate from it via a preset-specific formula, which depends on the next-generation matrix of the model's infectious compartments. You can also directly set transmission_rate to bypass the conversion.
"parameters": {
"transmission_rate": 0.4,
}
If both are sent, transmission_rate wins and R0 is dropped silently. See each preset's Parameters table for the conversion formula it uses, and Model › Parameters › Calculated parameters for the resolver machinery.
How do I make a parameter age-varying?
Pass a list under model.parameters instead of a scalar. The list length must equal the number of resolved age groups (after age_group_mapping is applied).
"parameters": {
"transmission_rate": [0.35, 0.35, 0.30, 0.25, 0.20]
}
See Model › Parameters.
How do I apply seasonality?
Add parameter_transforms with balcan method targeting the parameter you want to modulate. Set max_date / min_date to the seasonal peak / trough and max_value / min_value to the bounds. The existing parameter value is multiplied by a sinusoidal factor in [min_value/max_value, 1].
"parameter_transforms": [
{
"target_parameter": "transmission_rate",
"method": "balcan",
"max_date": "2024-01-15",
"min_date": "2024-07-15",
"max_value": 1,
"min_value": 0.1
}
]
See Parameter Transforms › Seasonality for the math.
How do I override a parameter for a date range?
Use a parameter_transforms entry with method: "override". The replacement is absolute (not multiplicative) and always wins for its window. Pass a scalar or a per-age-group list as value.
"parameter_transforms": [
{
"target_parameter": "transmission_rate",
"method": "override",
"start_date": "2024-03-01",
"end_date": "2024-04-01",
"value": 0.05
}
]
See Parameter Transforms › Override.
What's the difference between scale and override?
scale | override | |
|---|---|---|
| Operation | Multiplicative (baseline × factor) | Absolute replacement (ignores baseline) |
| Composition | Stacks with other scale and balcan transforms | Always wins for its date window |
| Outside window | Multiplier is 1.0 (no change) | Original baseline applies |
Use scale when you want a relative change (e.g. "halve transmission during this window") and override when you want a known absolute value (e.g. "set transmission to exactly 0.05 during this window").
How do I see the effective parameter values?
Set output.include_parameters: true. The response gains a results.parameters section with the per-step value of every model parameter after all transforms and overrides have been applied, so the arrays reflect what actually drove the simulation.
"output": { "include_parameters": true }
Response shape:
{
"results": {
"parameters": {
"dates": ["2024-01-01", "2024-01-02", "..."],
"data": {
"transmission_rate": {
"0-4": [0.296, 0.297, "..."],
"5-19": [0.296, 0.297, "..."]
},
"recovery_rate": {
"0-4": [0.10, 0.10, "..."]
}
}
}
}
}
datesaligns 1-to-1 withresults.compartments.datesandresults.transitions.dates.- Every entry under
datais a{age_group: [per-step values]}map. Scalars and age-varying parameters are broadcast to the time axis; time-varying transforms (e.g.balcan) keep their per-step shape. - Parameters are deterministic inputs, so this section is shared across
Nsimruns; there's one array per parameter regardless of how many stochastic runs you requested. - Reserved names (e.g.
CONTACT_MATRIX_EIGENVALUE_ALL) only exist in the expression namespace and do not appear here.
Off by default to keep responses small. See Parameter Transforms › Inspecting the effective parameter values for a worked example with a balcan seasonality transform.
Vaccination
How do I model a disease with vaccination?
Pick a vaccination model preset (V-SEIR or V-SEIHR) and add a top-level vaccination block with one or more campaigns. The preset gives you a parallel _vax branch for every compartment, and the vaccination block drives doses from Susceptible into Susceptible_vax over a campaign window. Vaccine efficacy on the vaccinated branch is controlled by VE_S (susceptibility, both presets) and VE_H (hospitalization, V-SEIHR only); see the preset pages linked below for the exact semantics.
Minimal example:
{
"model": {
"preset": "V-SEIHR",
"parameters": {"R0": 2.5, "VE_S": 0.7, "VE_H": 0.85}
},
"population": {"name": "United_States"},
"simulation": {"start_date": "2025-01-01", "end_date": "2025-06-30", "Nsim": 10},
"vaccination": {
"campaigns": [
{
"start_date": "2025-02-01",
"end_date": "2025-04-30",
"rollout": {"type": "flat_count", "daily_doses": 100000}
}
]
}
}
Which preset to pick:
- V-SEIR when you only care about infections and recoveries (no hospitalization).
- V-SEIHR when you also want to track hospitalizations and apply a separate efficacy against severe outcomes (
VE_H).
What you get back in the response: every flow surfaces as a regular transition under results.transitions.data (e.g. Susceptible_to_Susceptible_vax, Exposed_vax_to_Infected_vax, Infected_vax_to_Hospitalized_vax). Sum the unvaccinated and vaccinated counterparts to get totals (e.g. total incidence = Exposed_to_Infected + Exposed_vax_to_Infected_vax).
See the Vaccination overview for the dose-allocation math, the Campaigns page for age targeting and multi-campaign stacking, and the V-SEIR / V-SEIHR preset pages for compartments, transitions, and parameter references.
How do I disable vaccination rollout in a vaccination-preset run?
Omit the vaccination block entirely from the request. The Susceptible → Susceptible_vax flow is added only when vaccination is present; without it the vaccinated branch stays at zero for the whole run and the preset behaves like its non-vaccinated counterpart (plain SEIR / SEIHR).
An empty "vaccination": {"campaigns": []} is rejected as a 422 (vaccination.campaigns must contain at least one entry), so just leave the block out instead. See V-SEIR › Without vaccination or V-SEIHR › Without vaccination for full request examples.