Scenario documents

A scenario binds shocks to a country and system. It is a plain dict, validated against a JSON Schema shipped with the package (schemas/scenario.v1.schema.json), and it is deliberately thin: it selects data, never modelling.

scenario = {
    "country_code": "BE",
    "system_name": "BE_2025",
    "shocks": {"inline": [
        {"channel": "align", "metric": "employment", "group": "deh=0-3",
         "period": "1", "op": "grow", "value": 0.0655},
    ]},
    "params": {"period": "1"},
}

Where shocks come from

shocks takes exactly one of two forms:

{"inline": [...]}

Records written out in the document, as above.

{"file": "projections.xlsx", "mapping": "regional_projections"}

An external model’s output file, read through a declarative mapping spec.

Both are reproducible: the resulting table’s id is derived from its contents, so the same shocks give the same shock_table_id whichever form supplied them. The result reports that id under plan["shock_table_id"].

Two kinds of constants, and the difference matters

This is the part that is easy to get wrong, so the package refuses the ambiguous case.

Scenario constants are context. They apply to both runs:

scenario["constants"] = [{"name": "$f_cpi", "group": "2023", "value": 1.058}]

Use them for the things that are true of the world you are simulating in, not of the reform: an uprating factor, a price level. Because both halves get them, the difference between the runs still isolates the shock.

Shock records with channel: constant are part of the shock. They apply to the counterfactual onlymetric is the constant name, period is the constant group, and op must be set.

A constant that appears in both places is rejected rather than silently resolved, since the two readings give opposite answers about what the baseline is.

A scenario whose shocks are all channel: constant needs no methodology at all: the input microdata is untouched and the two runs differ only in their overrides.

params

params carries scenario-semantics parameters, validated against the resolved methodology’s own params_schema with additionalProperties: false — so a typo is an error, not a silently ignored setting. Every shipped methodology takes period, selecting which of the external model’s periods to apply; lma_labour_alignment also takes tolerance_pct. The full list per methodology is in the Methods section.

A constants-only scenario takes no params.

methodology

There is a methodology field, and you should normally leave it out. It exists to pin a run for exact reproduction, or to disambiguate on the day two methodologies claim the same channel. Under normal use the methodology is resolved from the shocks and echoed back to you — see Methods for why.

Validating cheaply

check_scenario(scenario) validates everything that needs neither the model nor the data: structure, shock resolution, methodology dispatch, declared params, and the methodology’s contract. It raises ScenarioError carrying .problems, a list of every problem found rather than the first.

from euromod_linking.scenarios import check_scenario

try:
    check_scenario(scenario)
except ScenarioError as e:
    for problem in e.problems:
        print(problem)

The next step up is apply_scenario(..., validate_only=True), which additionally consults the model and the data — cell populations, income-list expansion, model compatibility, and the methodology’s own preview of what it would target — and still transforms nothing. That preview is what makes a mis-sized shock visible while it is still cheap to fix.

Fingerprints

scenario.fingerprint() reduces a scenario to a canonical hash. It folds in the shock table’s content id and the resolved methodology’s code_fingerprint — a hash of the method’s own source. So editing the modelling invalidates cached results instead of serving answers computed by a previous version of it.