Author a teaching scenario
A Windgram scenario is a deterministic recipe over source-shaped atmospheric columns. Generation applies declared transforms, calls the same Python derivation used by publishers, validates the resulting profile contract and lesson assertions, and commits the output for reproducible figures.
The authority chain
Section titled “The authority chain”scenario.schema.json +definitions/<id>.json ──→ source baseline ──→ declared transforms │ ▼ Python profile derivation │ ▼ contract + lesson assertions │ ▼ generated profile(s) + scenarios/index.jsonThe inputs are:
scenarios/scenario.schema.json: the closed definition vocabulary;scenarios/definitions/*.json: one discoverable recipe per top-level file;- source files named by each definition’s
baseline.path: input values, neverderived.*; and pipeline/src/windgram/scenarios.py: validation, transform, generation, assertion, and deterministic serialization behaviour.
The outputs are scenarios/generated/*.profile.json and
scenarios/index.json. They are committed so generator checks and every
package, renderer, or site integration consume identical bytes. Never edit an
output or its hash by hand.
Start from a working recipe
Section titled “Start from a working recipe”- Choose one lesson. State a relationship that a reader can see and a test can establish, such as a cap eroding after heating or a wider member spread producing a wider percentile band.
- Copy the nearest definition. Use
scenarios/definitions/minimal-valid.jsonfor a deterministic starting point, anensemble-*.jsondefinition for member output, ormodel-timing-disagreement.jsonfor a controlled comparison. - Give it synthetic identity. The filename and
iduse the same lowercase hyphenated slug. It must not equal a production slug frommodels.json. Keepsite.synthetic: trueand use a synthetic site name. - Fix time and randomness. Set UTC
referenceTime,generatedAt, andstartAt, an allowedstepHours, an explicit IANA timezone,hourCount, and integerseed. No value may depend on the wall-clock date, machine-local time, or ambient randomness. - Select a local baseline. A synthetic baseline is preferred. A calibrated baseline requires the calibration provenance.
- Change source inputs with declared transforms. Do not write a derived output to force the desired picture.
- Declare semantics and capabilities honestly. Field presence after all
transforms must match the declaration. Gust semantics, when present, match
capabilities.gust; precipitation semantics are always explicit. - Assert the lesson. Add machine-readable assertions that name exact hours, fields, and comparison targets where needed. Assertions observe output; they do not author it.
- Generate, inspect, and test. Commit the recipe and all regenerated outputs together.
The complete schema is the authority. This excerpt shows the identity and clock properties that should be obvious in review:
{ "schemaVersion": 1, "id": "synthetic-example", "title": "A controlled teaching relationship", "lesson": "A fixed input change creates one observable and asserted output relationship.", "kind": "deterministic", "modelShape": "hourly-core", "timeZone": "Etc/UTC", "site": { "id": "synthetic-ridge", "name": "Synthetic Ridge", "synthetic": true, "latitude": 49, "longitude": -123, "altitudeM": 1050, "modelElevationM": 900 }, "clock": { "referenceTime": "2000-01-01T06:00:00Z", "generatedAt": "2000-01-01T06:00:00Z", "startAt": "2000-01-01T12:00:00Z", "stepHours": 1, "hourCount": 6, "seed": 520 }}This excerpt omits required fields. Copy a committed valid recipe so required baseline, transform, semantics, capability, and assertion fields are not guessed.
Model shape and capabilities
Section titled “Model shape and capabilities”modelShape is an abstract transport shape, not a claim that a named provider
produced the values.
| Shape | Structural promise |
|---|---|
hourly-rich |
Hourly levels, heat fluxes, gust, CAPE/CIN, model PBL height, and cloud layers |
hourly-core |
Hourly levels and heat fluxes without optional science fields |
three-hourly-regional |
Three-hour steps and a reduced pressure-level set |
ensemble-five-level |
Member-derived percentile output on exactly five pressure levels |
The capabilities object must describe the transformed source, including exact
pressure and vertical-velocity levels. Omission is meaningful. If gust, CAPE,
CIN, PBL height, cloud layers, or the cloud profile is declared unavailable,
the corresponding source field must be absent—not zero.
The scenario’s semantics maps its synthetic transport into the same public
v0.3 vocabulary used by profiles. It does not borrow semantics from a
production model name.
Smoke follows the same discipline: a baseline hour may carry a smoke
block (surfaceUgm3, columnMgm2, aot), and every hour must agree
with capabilities.smoke — "passive" for a synthetic profile whose
smoke should accept the renderer’s smoke-adjusted view,
"radiativelyCoupled" to teach the already-smoke-aware case, false
(or omission) with no smoke blocks anywhere. semantics.smoke must
exactly match, like gust. Assertions can reference smoke.surfaceUgm3,
smoke.columnMgm2, and smoke.aot; the smoke-over-thermals recipe is
the working example.
A smoke recipe’s clock is physics, not convention. The smoke-adjusted
view derives slant-path transmittance from the sun’s position at the
site coordinates and each hour’s validAt, so the smoky convective
hours must be genuinely daytime at the site’s longitude, in a plausible
fire season — smoke-over-thermals runs an August afternoon in
America/Vancouver for exactly this reason. A clock that puts the
thermals at local night makes the adjustment a silent no-op: the
correction correctly finds no sun to attenuate, and the “adjusted” view
renders identical to the base.
Author source changes
Section titled “Author source changes”The transform vocabulary is closed. It supports surface-field curves, level temperature and dew-point-depression offsets, wind speed and direction changes, pressure tendency, explicit optional capability fields, time shifts, and elevation adjustments.
Transforms may write source quantities such as
surface.sensibleHeatFluxWm2 or levels.temperatureC. They may not name
derived.boundaryLayerTopM, derived.thermalVelocityMs,
derived.cloudBaseM, or derived.usableLiftTopM. Those values require the
pipeline’s inputs and therefore have one authority: the Python profile
derivation.
If the lesson needs a new transform type, that is a generator change rather than a one-off escape hatch. Update the scenario schema, implementation, positive and invalid fixtures, and tests together. Physical exceptions are narrow, explicit, reasoned schema entries; they are not a general bypass for plausibility checks.
Ensemble and comparison recipes
Section titled “Ensemble and comparison recipes”An ensemble recipe declares its member count and seeded perturbations. Each member source column is derived independently before the normal production aggregator computes percentile objects. Preserve these rules:
membersat a numeric position counts contributors to that position and can be lower thanrun.memberswhen null member values are excluded;- percentiles remain ordered from p10 through p90;
ceiledMembersis used only where the column ceiling censored a height; and- wind direction uses the shared circular aggregation rather than a linear percentile across north.
Ensemble assertions address one position inside a percentile block with a
trailing key, such as surface.temperatureC.p90 or
derived.usableLiftTopM.members, so a recipe can assert band width and member
accounting, not just presence. Nearest-height level selection uses the
ensemble median (p50) height.
A comparison declares neutral variant ids and labels. Variant transforms use
their target; generated filenames and registry entries retain that identity.
Describe a controlled difference such as earlier and later development. Do not
call one model correct, probable, safer, or a majority result.
Calibrated baselines and attribution
Section titled “Calibrated baselines and attribution”Real provider output may be used offline to calibrate plausible magnitudes and vertical relationships. It does not become a site dependency or a public forecast example.
A calibrated definition names a sibling provenance JSON record. Its required
fields are listed in
scenarios/README.md,
next to the baselines they describe.
The public definition replaces baseline identity and time with its synthetic site and fixed clock. Website code must not import the calibration baseline directly. Credit the provider adjacent to any discussion of the calibration method.
Generate and test
Section titled “Generate and test”From the repository root:
uv run --project pipeline windgram scenarios generateuv run --project pipeline windgram scenarios checkuv run --project pipeline pytest pipeline/tests/test_scenarios.py pipeline/tests/test_scenario_ensembles.pyuv run --project pipeline pytest pipeline/tests/test_scenario_calibration.pypnpm --dir toolkit testpnpm --dir site checkpnpm --dir site buildgenerate discovers only top-level definition JSON files, removes unmanaged
generated profile files, writes canonical JSON, and rebuilds the registry with
SHA-256 hashes. check generates expected bytes in memory and fails on stale,
missing, or unmanaged output without modifying the worktree.
Review all of these in the same change:
- the definition and any intentional baseline, schema, or generator change;
- each generated profile named by the scenario;
- the scenario’s registry metadata and hashes;
- assertion coverage of the stated lesson; and
- any figure or prose that the changed relationship makes stale.
If a selected scenario also owns an SVG golden, follow the deliberate golden review policy in Tests by change. A new hash or screenshot is not itself proof that the scenario still teaches the right relationship.