Skip to main content
🧠0% 🤖100%

Parameters and distributions

Recommendation

  1. Declare parameters; refuse unknown names. A misspelled parameter should be an error at build time, not a silent behavioral difference.
  2. A parameter may be a number, a typed quantity, a distribution, a time series, a seasonal curve, a per-group table, or a function. Every slot accepts all of them.
  3. Keep values with the model by default, and make it possible to separate them. camdl shows the value of separating a model’s structure from its values; here that is available, not required.
  4. Record what data a model uses, with its documentation. epymorph’s requirement contract is one of the most useful ideas in the review. An early version here (sim.requires()) was removed on 2026-10-21 because nothing enforced it and no model used it; doc= and source= on ss.par do this job.
  5. Record where a value came from. One optional field, source=, answers the question every reviewer asks.
  6. Parameters have shapes, and the shape is checked.

Why

Declaration catches typos; no framework without it does

EpiModel: “param.net(inf.prob = 0.4, act.rate = 2, my.new.param = 0.7) is legal… There is no parameter schema and no declaration step; a misspelled parameter name is not an error until something looks for it.” Its own review lists “No declaration step of any kind — parameters, attributes, and outputs all spring into existence on first use. Typos become silent behavioral differences” as a limitation.

By contrast, MEmilio gets compile-time checking from C++ tag types (parameters.set<TimeInfected>(2) — “the strongest parameter-name checking in the review”); camdl gets it from its compiler; Starsim gets a runtime version from define_pars: passing a parameter that was not declared (ss.SIR(foo=3)) raises an error naming it. Starsim’s mechanism is already the right one for Python. Keep it.

Where values live

camdl takes the strongest position in the review:

parameter values are never declared in the parameters block. That block carries names, kinds, dimensions, and optional priors only. Values come from a params.toml, a CLI --param, or a named scenario’s set = { ... }.

with a factored run identity (model hash, params hash, config hash, scenario hash, fit hash) so that “a reviewer asking ‘is this a structural change or a parameter sweep?’ reads the level hashes rather than diffing files.”

The mechanism is excellent, and the tradeoff is that the separation is mandatory. An epidemiologist writing an SIR should not need two files. So values live inline by default, and the split is available. Comparing runs is left to the code and the commit (stochasticity-and-reproducibility.md).

Requirements as data

epymorph’s AttributeDef is the standout:

requirements = [
    AttributeDef("beta", type=float, shape=Shapes.TxN, comment="infectivity"),
]

From its review: “A model states its data interface as data, so it can be checked before anything runs… Data provenance is explicit… Shape checking catches an entire class of broadcasting bug at bind time. Nothing else in the review makes a model’s data dependencies declarative in this way. Most frameworks have the model reach out and get what it needs.”

Atomica does the same thing in a different medium: the databook, a data-entry spreadsheet with exactly the fields the model needs, per population, with the right units and labels, is generated from the framework.

Distributions do double duty

A distribution in a parameter slot means one of two things, and the slot says which: in dur= or beta= it is variation between people (each agent draws its own value), and in prior= it is uncertainty for fitting (camdl’s ~ prior). Keeping the two in separate slots is what avoids the ambiguity EpiModel has, where “a vector-valued parameter means a time series here and a sensitivity sweep there”.

Shapes

epymorph’s Shapes.Scalar / N / T / TxN / NxN are checked when data is attached. Its review states the complement precisely: “Dimensional types say what a number means; shapes say how many of them there are and along which axis. A language spanning agents, patches, strata, and time needs both.” SimInf’s gdata versus ldata (global versus per-node parameters) and MetaCast’s universal_params versus subpop_params are two independent arrivals at a coarse version of the same thing. Here the shape is taken from the value (ss.by(age=[...]) has one value per age group) and checked against the model’s stratification, with an error (ss.ShapeError) that names the dimension and its levels.

Named data, not just values

Epydemix loads a country’s population and contact matrices by name (load_epydemix_population("United_States", contacts_source="mistry_2021", ...)). Its review: “A lingua franca that can express a contact matrix but cannot say which contact matrix, from which source, for which place, has moved the problem rather than solved it.” epymorph’s ADRIOs are the pull-based version with caching and provenance; camdl has read("data/lga_pop.tsv", column="patch"); LASER ships a library of migration models. All four say the same thing: a reference to data is part of the model, and where it came from belongs in the description of the run.

The specification

04-parameters.md specifies ss.par (with bounds=, prior=, source=, and doc=), the forms a value can take (a number, a typed quantity, a distribution, ss.timeseries, ss.seasonal, ss.by, or a function), values kept outside the model (ss.Sim(pars=values), with values read from a file), and named data (ss.people, ss.contacts, ss.geo.geography, from starsim_data). The distributions and the units of bare numbers are in 02-types.md, and fitting with bounds and priors in 13-inference.md. The docs show how to use these in Parameters and Distributions.

Trade-offs

  • Declaration means adding a parameter to a custom module is two edits (declare, use). Starsim already works this way and it is worth it. Most parameters live in their transition and are never named at all.
  • Nothing lists a model’s data needs for the user. Enforcing a list (Vivarium’s PopulationView, where a module sees only what it declared) would give one, but people forget declarations and they fall out of step with the code, so a missing value shows up as an error when the model is built.
  • A function in a parameter slot is only known to work under the solvers it has been tried with. The sim reports what happened when it is built; nothing is promised in advance.
  • Bundled data goes stale and is large; pulled data needs a network and can change underneath you. So do some of each: bundle what is small and common, pull everything else with a cache (epymorph’s approach), and version both. starsim_data keeps each UN revision as a separate store, so a new release adds data and does not change what an existing model loads.
  • Optional source= will mostly be empty. True, and it costs nothing when unused, and the models that matter most are the ones where someone bothered.

Rejected

  • Open parameter lists (EpiModel’s ...). The cost is documented.
  • Mandatory value/model separation (camdl). The capability is valuable; two files for an SIR is more than this audience needs.
  • Parameter names joined as strings (MetaCast’s parameters['p' + subpop_suffix], 'beta_[high,vaccinated]'). Its review: “the lesson is in the split, not the mechanism.” Shapes do this job.
  • Use_Defaults: 1 (EMOD), where “much of a model’s behavior is not visible in its files”. We guess defaults aggressively and print every one, which is the opposite policy despite sounding similar.
  • New distribution names, for now. Starsim’s names are used (ss.lognormal, not a new ss.lognorm; ss.nbinom, not ss.neg_binomial), with their parameters named by their moments where they have them (ss.normal(mean, std), ss.poisson(mean)), and loc= a shift, as in SciPy. There was at first no separate ss.erlang: an Erlang distribution is exactly a gamma distribution with an integer shape (ss.gamma(shape=3, scale=...)), and its main use in this field, splitting a state into sub-stages, is stages=3 on the transition (ss.erlang() has since been added, as a friendlier spelling). Starsim’s review flags “~20 distributions” as part of a large vocabulary, so the documentation teaches the dozen listed in 17-vocabulary.md; the others, and any SciPy distribution via ss.Dist, remain available. The list itself is not fixed (see open questions).
  • Declared and enforced data requirements (ss.Requires, removed; Vivarium’s views). See the trade-off above.
  • Repeating the unit on every distribution parameter. Once is enough.

Open questions

The open questions from this document are in 19-open-questions.md.