Scenario definitions (v2 schema)
Status: design specification. No code implements this yet. The canonical parser and run-step iterator will live in hazelbean (next to project_flow.py); seals, gtappy, gtap_invest and global_invest will import it and delete their local copies of assign_df_row_to_object_attributes.
This document specifies version 2 of the scenario definitions CSV that drives SEALS and the models built around it (gtap_invest, global_invest, gtappy, ngfs_pnas). It replaces the v1 standard_scenarios.csv schema and its per-project variants.
Example files live in examples/scenario_definitions_examples/:
single_stage_seals.csv— a SEALS-only project (migrated from the v1 SEALS template), including a hindcast year.multi_stage_ngfs.csv— a GTAP+SEALS project (migrated from the NGFS/PNAS variant): a 2020-anchored canonical clock with agtapstage clock (2023 base, annual viagtap_timestep=1).hindcast_example.csv— minimal demonstration of backward years.
Why v2
The v1 schema accumulated five structural problems as projects grew:
- Per-model time columns multiplied ad hoc. The NGFS variant needed
key_base_year/yearsfor the CGE model (2023→2050) andseals_key_base_year/seals_yearsfor SEALS (2020→2050). Every new model stage added more one-off columns. - “baseline” conflated two different things: the observed historical anchor (base-year LULC data, calibration inputs) and the business-as-usual counterfactual trajectory. One is data; the other is a scenario.
scenario_typewas overloaded with workflow control. Values likebaseline_ignore_dependenciessmuggled a label and a dependency flag into the type field, and pseudo-baseline rows were wired together throughcomparison_counterfactual_labels.- Years only ran forward from the base year; hindcasting (simulating years before the anchor) was impossible to express.
- Model clocks had no defined meeting point. GTAP’s database anchors at 2023 and solves annually; SEALS anchors on observed LULC (ESACCI ends at 2022) and allocates in 5–10-year steps; MAgPIE delivers 5-year steps anchored at 2020. Nothing specified how these timelines exchange data, so harmonization lived in ad hoc task code: the
seals_yearsoverride inseals_utils.py:598-607, three duplicated linear-interpolation blocks ingtappy_tasks.py, and “POSSIBLE HACK” year-filter fallbacks inseals_process_coarse_timeseries.py.
v2 keeps a single flat, Excel-editable CSV — duplication across rows is accepted — and fixes the semantics.
Core concepts
A v2 file is a table where every row is either an observed anchor or a runnable scenario:
- An observed row defines the historical anchor: the base year, the base-year LULC map, calibration inputs, and any additional observed years. It runs no projection years. It is data preparation, not a counterfactual.
- A bau or policy row is a trajectory to simulate. It points at its anchor (
observed_reference_label), optionally at scenarios that must run first (depends_on), and at scenarios it is compared against in reporting (compared_to).
Real computations that were previously disguised as baseline rows (e.g. an initial CGE equilibrium run) become ordinary bau rows that other rows depends_on. Observed anchors get observed rows; real runs get bau/policy rows.
Every scenario also defines exactly one canonical clock — one base_year and one years list that the whole project exchanges data on. Model stages may run on their own native clocks (GTAP annually from 2023, SEALS decadally from an observed-LULC anchor), but anything that crosses a model boundary is expressed on the canonical clock, with declared interpolation for the gaps. See The canonical clock and model-clock harmonization.
Column reference
Identity
| column | type | required | semantics |
|---|---|---|---|
scenario_label |
label | yes | Primary key, unique within the file. Rows whose label starts with # are comments and are skipped entirely. |
scenario_type |
enum | yes | Closed vocabulary: observed, bau, or policy. Anything else is a load-time error. |
tags |
label list | no | Free-form grouping (e.g. stress_test, ngfs_phase4) for filtering and reporting. Never read by run logic. |
Comparison and dependency DAG
| column | type | required | semantics |
|---|---|---|---|
observed_reference_label |
label | on bau/policy rows |
Which observed row anchors this scenario (base-year data and calibration lookup). Replaces v1 baseline_reference_label. |
depends_on |
label list | no | Scenarios that must complete (all stages) before this row runs. Defines the execution DAG; the parser topologically sorts, and cycles are a load-time error. |
compared_to |
label list | required on policy rows |
Scenario(s) this row is differenced against in reporting/comparison tasks. Replaces v1 comparison_counterfactual_labels. |
These are three separate concerns that v1 mixed together: where is my anchor data, what must run before me, and what am I compared against.
Time
Time is specified at two levels: the canonical clock (the plain columns) and optional stage clocks (stage-prefixed columns) for models whose data availability or cadence differs. The canonical clock is the project’s single source of truth — cross-stage data exchange, comparison tasks and reporting all speak canonical years. Stage time columns are declarations of native data availability, and the framework — not task code — owns the mapping between each stage clock and the canonical clock (see the harmonization section).
| column | type | required | semantics |
|---|---|---|---|
base_year |
int | yes | The single canonical anchor year. Must be a year the anchoring observed row can supply data for — either truly observed (with ESACCI LULC that means ≤ 2022) or constructed by an upstream row (see Constructed anchors). Replaces v1 key_base_year. Must never appear in years. |
observed_years |
int list | no | Additional observed/historical years available for calibration or validation. Replaces v1 base_years. Defaults to just base_year. |
years |
int list | on bau/policy rows |
Canonical years to simulate and report. May contain years before and/or after base_year (see Hindcasting). Empty on observed rows, and may be empty on a bau row that is a base-year-preparation run only (e.g. an initial CGE equilibrium). |
<stage>_base_year |
int | no | The stage’s native anchor (e.g. gtap_base_year=2023, the GTAP database year). Empty → canonical base_year. |
<stage>_years |
int list | no | The stage’s native run years. Empty → canonical years. Must never contain the stage’s resolved base year. Mutually exclusive with <stage>_timestep. |
<stage>_timestep |
int | no | Excel-friendly alternative to <stage>_years: expands to every <stage>_timestep-th year after the stage base year, through max(years) (e.g. gtap_timestep=1 with gtap_base_year=2023 → 2024 2025 … 2050). |
<stage>_interpolation |
enum | no | How the stage’s published outputs are filled at canonical years its native clock skips: linear (default), nearest, hold (carry the previous native value), compound (for growth-rate variables: aggregate by compounding, split by geometric root — never linearly; see Exogenous shocks), none (a skipped canonical year is a load-time error). |
<stage>_extrapolation |
enum | no | How canonical years outside the stage’s native range are filled: hold (default), linear, error. |
coarse_interpolation |
enum | no | Same vocabulary as <stage>_interpolation, applied when extracting coarse-projection NetCDF bands (MAgPIE, LUH2) at years the source lacks. Default linear. Replaces the hardcoded decadal interpolation in the BtC extraction path and the silent year-skipping in extract_global_netcdf. |
Derived at load, never written in the CSV: previous_years — for the canonical clock and for each stage clock, a dict mapping each year to the adjacent year toward the base year in its chain (see Hindcasting and the ProjectFlow attribute contract).
Per-stage overrides
When a project chains multiple model stages with different timesteps (e.g. GTAP runs 2023→2050 while SEALS runs 2020→2050), any time or config field can be overridden per stage by prefixing the stage name:
<stage>_<field> e.g. gtap_base_year, gtap_years, gtap_model_label
Because single underscores are ambiguous (regional_projections_input_path is not a “regional” stage override), the parser requires a registered stage list, declared in the run file before the CSV is loaded:
p.scenario_stages = ['gtap', 'seals']Resolution rule when executing stage s, field f: use the <s>_<f> cell if it is non-empty, otherwise fall back to the plain <f> column. The plain columns are the single-stage default — a SEALS-only project never writes a prefix. The type of a prefixed column is inherited from its base field name (gtap_shocks_path is a path because shocks_path would be).
For time fields the fallback rule is the same (empty stage cell → canonical column), but the semantics are stronger than a value swap: stage time columns declare a native clock that the framework validates against and resamples to the canonical clock (see the harmonization section). For every other config field the override is a simple per-stage substitution.
Naming and provenance labels
Unchanged from v1; these compose output directories as exogenous_label/climate_label/model_label/counterfactual_label/year:
exogenous_label, climate_label, model_label (the coarse-projection source model, e.g. luh2-message, magpie), counterfactual_label (the trajectory name, e.g. bau, below_2c).
Spatial extent
aoi (ISO3 code, global, or a vector path), regions_vector_path, regions_column_label.
Coarse projection sources
Unchanged names from v1: coarse_projections_input_path, coarse_src_label, coarse_simplification_label, coarse_correspondence_path, lc_class_varname, dimensions, time_dim_adjustment (e.g. add2015, arithmetic applied to NetCDF time coordinates).
Base-year LULC and calibration
Primarily meaningful on observed rows: base_year_lulc_path, lulc_src_label, lulc_simplification_label, lulc_correspondence_path, calibration_parameters_path (renamed from v1 calibration_parameters_source so the _path suffix types it correctly).
Regional / vector stage
regional_projections_input_path (templatable, see Templating), region_to_coarse_algorithm (e.g. covariate_sum_shift).
GTAP/CGE stage config
All stage-prefixed in multi-stage projects: gtap_model_label (was v1 cge_model_label), gtap_aggregation_label (was aggregation_label), gtap_cmf_template, gtap_shocks_path, gtap_shock_config. In v2, gtap_shock_config additionally carries the per-header time semantics (rate / level / difference) that drive shock harmonization — see Exogenous shocks in the harmonization section.
Workflow control
| column | type | default | semantics |
|---|---|---|---|
run |
bool | 1 | Include this row in execution. Replaces commenting-out or deleting rows. |
ignore_dependencies |
bool | 0 | Skip validation of / waiting on cross-scenario upstream artifacts (assume present or regenerate). This is the boolean that v1 smuggled into scenario_type='baseline_ignore_dependencies'. |
Unknown columns
Columns not in this spec are allowed: they are typed by the suffix rules below and set as ProjectFlow attributes. This is the extensibility valve so projects extend the schema instead of forking it.
The canonical clock and model-clock harmonization
Every scenario has exactly one canonical clock: p.base_year (a single int), p.years (a sorted int list), and the derived p.previous_years (a dict mapping each canonical year to the adjacent year toward base_year in its chain). These live on the ProjectFlow object, are set once at scenario load, and are never mutated by stages or iterators. Everything project-wide — cross-stage data exchange, output-directory years, comparison and reporting tasks — speaks this clock.
Any model wired into the project may have different data availability or a different natural cadence. Each such model declares a stage clock (<stage>_base_year, <stage>_years or <stage>_timestep) plus a harmonization policy (<stage>_interpolation, <stage>_extrapolation). A stage with no time columns runs on the canonical clock — the common case.
The motivating configuration (an NGFS-style GTAP+SEALS project):
| clock | base year | run years | why |
|---|---|---|---|
| canonical | 2020 | 2030 2040 2050 | anchored to observed ESA LULC; ESACCI ends 2022, so a directly observed canonical anchor must be ≤ 2022 (a later anchor is possible via Constructed anchors) |
gtap |
2023 | 2024 … 2050 (gtap_timestep=1) |
the GTAP database year is 2023; the recursive-dynamic solve is annual |
seals |
(canonical) | (canonical) | SEALS is the keeper of the land state; its clock is the canonical clock |
| MAgPIE (coarse source) | 2020 | 5-year bands | harmonized at NetCDF extraction via coarse_interpolation |
The exchange rule
Cross-stage artifacts live on the canonical clock. A stage runs internally on its native clock — GTAP’s annual year-on-year .upd chain is untouched — and anything it publishes for consumption by another stage or by reporting is resampled to canonical years using its declared policy, through one shared hazelbean utility (planned hb.resample_years(values_by_year, target_years, method, extrapolation)). Symmetrically, a stage that needs an input at a native year the canonical grid skips resamples the canonical artifact the same way. The same machinery runs at every clock boundary, in both directions: stage outputs → canonical (publishing), canonical inputs → stage-native (consumption), and exogenous sources → stage-native (shock files, coarse NetCDFs — see below). Task code never hand-rolls year interpolation: the three duplicated linear-interpolation blocks in gtappy_tasks.py (~2898, ~3313, ~3825) and the hardcoded decadal interpolation in the BtC extraction path collapse into this utility. Native-clock artifacts stay in the stage’s own output tree — canonical resampling adds files, it never deletes the annual detail.
How the awkward anchors resolve:
- GTAP 2023 vs canonical 2020. GTAP’s base year is a stage fact, not the project’s. Canonical years above 2023 are sampled directly from its annual grid (a validated superset). If a task requests GTAP values at canonical years at or below 2023, the default
gtap_extrapolation=holdsupplies the 2023 base values — an explicit, declared assumption of no change over 2020→2023, instead of a silent one. - MAgPIE 2020 / LUH2 2015. Calendar alignment of the NetCDF time axis stays with the existing
time_dim_adjustmentmechanism (orthogonal to harmonization). Missing years on the aligned axis are filled percoarse_interpolation— default linear between the neighboring bands. This also fixesextract_global_netcdf, which today silently skips requested years the source lacks. - ESACCI ends 2022. Not hardcoded anywhere: the canonical
base_yearmust be a year the anchoringobservedrow can supply (base_year_lulc_pathmust exist), which is where the ≤ 2022 constraint enforces itself. A future ESA release moves the anchor by editing one CSV cell — or the project constructs a later anchor and makes it canonical (see Constructed anchors).
Exogenous shocks (GTAP baseline drivers)
The GTAP baseline requires exogenous driver shocks (GDP, population, labor) supplied in a .har whose headers come in distinct time families — annual growth rates (OGP/POP/LAB), decadal (DGP/DYP/DYL), cumulative-from-base, and duplicates. Today a run year missing from the shocks file freezes the solve mid-run, and single-step runs flip between header families by special case (gtappy_tasks.py:988, enabled only for len(years)==1 and year ∈ {2030, 2040, 2050}). Under v2 the shocks file is treated like any other clocked source:
- A shock-preparation step, part of the stage’s base-year preparation, resamples the source shocks onto the stage’s resolved chain and writes a derived
.harthat matchesgtap_yearsexactly. After that, the CGE loop can never request a year the file lacks. - Growth rates compound; they never interpolate linearly. Splitting a decadal shock into annual steps takes the geometric root
(1+G)^(1/10) − 1; aggregating annual shocks into a multi-year step (including a singleshot 2023→2050 jump) takes the running product of(1+g_t)minus one. This is thecompoundmethod in the interpolation vocabulary. - The method is per header, not per file: most drivers are ratio growth rates, some are levels, and at least one (
gdppc) is a difference rather than a ratio.gtap_shock_configmaps header → semantics (rate/level/difference), and the resampler picks compound, linear, or difference-scaling accordingly. - Coverage validation includes shock sources: at load, the years obtainable from
gtap_shocks_pathunder the declared per-header semantics are checked against the resolved stage chain. A gap is a load-time error naming the header and the year — replacing today’s silent freeze.
Coupled stages: canonical years are the barriers
For one-way pipelines the scheduler may run stage by stage. For two-way coupled runs — e.g. GTAP solving annually while SEALS and InVEST recompute ecosystem-service shocks every fifth year — it iterates canonical years outermost: between consecutive canonical years, each stage runs its native sub-chain (GTAP: five annual steps; SEALS/InVEST: one step), then exchange artifacts are published and consumed at the barrier before any stage proceeds. The canonical clock is therefore not just the reporting grid; it is the synchronization grid.
Execution is realized through ProjectFlow’s existing nested-iterator machinery rather than a bespoke scheduler: the nesting order of the iterators is what distinguishes one-way (stage-outer) from coupled (canonical-year-outer), the barrier is the outer iteration boundary, and the feedback is task order plus a file on disk. The iter_stage_runs sketch below describes what one iterator level yields, not a standalone scheduler. See Model specs and iterator-driven staging for the full execution design.
A fast test case with years=2050 degenerates to a single barrier — GTAP takes one step on a compounded 2023→2050 shock, SEALS downscales the 2050 land use, InVEST scores it — with no special-cased code path. Note that shrinking years does not silently shrink a declared stage cadence: a project with gtap_timestep=1 must also blank that cell to make GTAP single-step, because a declared native clock is never discarded implicitly. The idiomatic way to keep the full run and the fast test side by side is two rows with run flags, not cell surgery.
Constructed anchors
The canonical anchor does not have to be a directly observed year — it can be produced by an upstream row. The motivating case: GTAP’s database year is 2023, GTAP data is hard to move, but ESACCI ends at 2022 and ESA is easy to extend. Rather than carrying a permanent gtap stage clock, the project can construct a 2023 LULC anchor and make 2023 canonical:
observed_esa_2022— a trueobservedrow (base 2022, real ESA map,observed_years=2020 2022). Calibration and validation reference only these genuinely observed years.extend_anchor_to_2023— an ordinarybaurow anchored on it withyears=2023: a mini-SEALS run downscaling the assumed-baseline coarse trajectory’s 2022→2023 change onto ESA (the 2023 slice of 2020/2025 bands comes fromcoarse_interpolation).anchor_2023— anobservedrow withbase_year=2023,depends_on=extend_anchor_to_2023, andbase_year_lulc_pathtemplated (cat-ears) onto row 2’s output.
Real scenarios then anchor on anchor_2023, and gtap_base_year empties out — GTAP simply runs the canonical clock. Two validation relaxations make this legal: observed rows may carry depends_on, and base_year_lulc_path must exist at load or be a templated product of a depends_on row (checked once that row completes).
Provenance stays visible instead of buried in a side script: use lulc_src_label=esa-extended (or tags=constructed_anchor), and put the assumed trajectory in the extend row’s counterfactual_label so the construction assumption appears in output paths. Two consequences to state honestly: every scenario, including policy rows, embeds the extension-trajectory assumption in its anchor (it cancels in compared_to differences but not in absolute levels), and a constructed anchor must not depend on stages it feeds — the DAG validator rejects the cycle; here the construction uses only the coarse trajectory and ESA.
To cache the artifact instead of re-running it, provision the raster to base data and set the extend row run=0 — path resolution through get_path() plus ignore_dependencies semantics make caching a policy choice, not an architecture fork. When a real observed year ships, swap one row and delete two.
ProjectFlow attribute contract
Canonical — set once at scenario load, immutable thereafter:
| attribute | type | meaning |
|---|---|---|
p.base_year |
int | canonical anchor (renamed from p.key_base_year; always a bare scalar) |
p.years |
list of int | canonical simulated/reported years |
p.previous_years |
dict int→int | each canonical year → the adjacent year toward base_year in its chain; the first year of each chain maps to base_year. A dict rather than a parallel list so hindcast chains and sparse grids cannot drift out of alignment. |
p.observed_years |
list of int | observed/historical years from the anchoring row |
Stage-resolved — set by the iterator when entering a stage, equal to the canonical values when the stage declares no clock:
p.stage_label, p.stage_base_year, p.stage_years, p.stage_previous_years
Per run step — names unchanged from today, so the bulk of per-year task code (path composition from str(p.year), ..._{year}_{previous_year}_ha_diff filenames) does not change:
p.year, p.previous_year, p.time_direction
The scattered idiom previous_year = p.key_base_year if c == 0 else p.years[c - 1] — recomputed today in at least six places across seals_main, seals_process_coarse_timeseries, gtappy_tasks and ecosystem_services_tasks — becomes a read of p.previous_years[year] (or p.stage_previous_years[year] inside a stage). The key_base_years_as_list() coercion helper and the scalar/list special-casing of key_base_year in the parser are deleted outright: *_year columns are always scalar ints, *_years columns always int lists.
Load-time clock validation
<stage>_yearsnever contains the stage’s resolved base year;<stage>_yearsand<stage>_timestepare mutually exclusive.- Coverage: every canonical year must be obtainable from every enabled stage under that stage’s declared
interpolation/extrapolationpolicy, and every resolved stage year must be obtainable from the stage’s declared exogenous sources (e.g. everygtapyear fromgtap_shocks_pathunder its per-header semantics). Withnone/errorpolicies an uncoverable year is a hard load-time failure naming the stage (or header) and the year — instead of a mid-run KeyError, a silently missing band, or a frozen GEMPACK solve. interpolation/extrapolation/coarse_interpolationvalues are a closed vocabulary; unknown methods are a load-time error (new methods get added to hazelbean, not to project task code).- A
baurow may have emptyyearsonly when it resolves to base-year preparation work alone (e.g. an initial CGE equilibrium atgtap_base_year); such rows exist to bedepends_ontargets. observedrows may carrydepends_on;base_year_lulc_pathmust exist at load or be a cat-ears-templated product of adepends_onrow (constructed anchors), in which case its existence is checked when that row completes.
Hindcasting (backward years)
years may contain any integers other than base_year (listing base_year itself is a load-time error — it is the anchor, never a simulated step). The parser splits years into two chains:
- forward chain: years greater than
base_year, ascending; - backward chain: years less than
base_year, descending (closest-to-base first).
previous_year is defined as the adjacent year toward base_year in this chain — base_year for the first element of each chain, otherwise the prior chain element. The p.previous_years dict is exactly this mapping materialized, computed once per clock (canonical and per stage). Allocation still computes state(year) − state(previous_year) exactly as today; in the backward chain previous_year is chronologically later than year, and the signs of the changes fall out naturally.
Each run step carries a derived time_direction ∈ {forward, backward} so tasks that genuinely care (discounting, time-dimension slicing) can branch. Canonical serial order per scenario per stage is the full forward chain, then the full backward chain; the two chains are independent and may be parallelized. Cross-stage ordering of these runs (one-way pipelines vs. coupled feedback) is defined by the barrier rule in the harmonization section — canonical years are the synchronization points.
The iteration contract the hazelbean implementation must satisfy:
def iter_stage_runs(scenario_set, stage):
for s in scenario_set.enabled_topo_sorted(): # run==1, ordered by depends_on DAG
base, years = s.stage_clock(stage) # stage columns else canonical clock;
# <stage>_timestep expanded here
fwd = sorted(y for y in years if y > base)
bwd = sorted((y for y in years if y < base), reverse=True)
for chain, direction in ((fwd, 'forward'), (bwd, 'backward')):
prev = base
for y in chain:
yield RunStep(scenario=s, stage=stage, year=y,
previous_year=prev, base_year=base,
direction=direction)
prev = yBase-year preparation jobs run once per unique anchor before any RunStep — from observed rows for the canonical anchor, and per stage for a stage anchor that differs from it (e.g. the initial GTAP equilibrium solve at 2023).
Parsing rules (implementation contract)
- Read with
pd.read_csv(path, encoding='utf-8-sig', dtype=str, keep_default_na=False). An empty string means None; the empty cell is the only null representation (nonone/NA/nanliterals — this kills the'nan'-string checks in the v1 parsers). - Comment rows: a
scenario_labelstarting with#skips the whole row. - Lists are single-space-separated within a cell (Excel-safe, never commas). Repeated spaces collapse; leading/trailing whitespace is stripped.
- Typing is by column-name suffix, never value sniffing (applied after stripping a registered stage prefix):
*_path→ path;*_paths→ path listyears,*_years→ int list;*_year→ int (always a bare scalar — the v1key_base_yearscalar/list asymmetry is gone)*_timestep→ int*_interpolation,*_extrapolation→ enum string (closed vocabulary, validated at load)run,ignore_dependencies,*_enabled→ bool (1/0/true/false, case-insensitive)depends_on,compared_to,tags,*_labels,dimensions→ string list- everything else → string.
- Stage prefixes are resolved only against the registered stage list (
p.scenario_stages). - Paths use forward slashes, always (see the slashes rule in conventions; v1 files containing
lulc\esa\...are normalized by migration). Paths resolve through ProjectFlow’sget_path()search order (project input dir → base_data → …). - Templating uses hazelbean cat-ears tokens,
<^token^>, in two phases: tokens naming already-defined project attributes substitute at row load; run-namespace tokens (year,previous_year,base_year,direction,stage,scenario_label, plus any row field) substitute per run step, just before use. A token still unresolved when its value is consumed is a hard error — no silent pass-through (this replaces v1’sleave_ref_path_if_fail=Truefallback). - Load-time validation: unique labels; closed
scenario_typevocabulary;observed_reference_labelpoints at anobservedrow;depends_on/compared_toreference existing labels; the dependency DAG is acyclic;base_yeardoes not appear inyears;observedrows have emptyyears;policyrows have non-emptycompared_to; plus the clock validations in the harmonization section (stage years exclude the stage base year,<stage>_years/<stage>_timestepmutual exclusion, canonical-coverage check under declared policies). - Legacy detection: the presence of a
key_base_yearcolumn or anyscenario_type='baseline'row means the file is v1 — the parser refuses it and points at the one-shot migrator (planned ashb.scenarios.migrate_v1_to_v2(csv_path)). There is deliberately no dual-mode parsing.
Migration from v1
SEALS standard_scenarios.csv (27 columns)
| v1 | v2 |
|---|---|
scenario_type=baseline row |
scenario_type=observed, years emptied |
key_base_year |
base_year |
base_years |
observed_years |
baseline_reference_label |
observed_reference_label |
comparison_counterfactual_labels |
compared_to |
calibration_parameters_source |
calibration_parameters_path |
backslash paths (lulc\esa\...) |
forward slashes |
| (new) | run=1, ignore_dependencies=0, empty depends_on and tags |
| everything else | unchanged |
NGFS/PNAS variant (35 columns)
All of the above, plus:
| v1 | v2 |
|---|---|
aggregation_label |
gtap_aggregation_label |
cge_model_label |
gtap_model_label |
cmf_template, shocks_path, shock_config |
gtap_cmf_template, gtap_shocks_path, gtap_shock_config |
key_base_year (CGE anchor, 2023), years (annual CGE years) |
gtap_base_year=2023, gtap_timestep=1 (or an explicit gtap_years list) |
seals_key_base_year (2020 observed anchor) |
plain base_year (the canonical anchor) |
seals_years |
plain years (the canonical clock; SEALS declares no stage clock) |
shock_config=none literal |
empty cell |
Row surgery:
- Add one
observedrow (anchor 2020, ESA LULC) — v1 had no row representing the actual observed anchor. baseline_ignore_dependencies_initial(typebaseline) → ordinarybaurowgtap_initial— it is a real CGE run, not data. It carries emptyyearsand an emptygtapclock: a base-year-only equilibrium anchored atgtap_base_year.baseline_ignore_dependencies(typebaseline_ignore_dependencies) →baurow withdepends_on=gtap_initial,ignore_dependencies=1.- Policy rows:
baseline_reference_label/comparison_counterfactual_labelspointing at the pseudo-baseline →depends_on=bau,compared_to=bau,observed_reference_label=<the observed row>. - Any
scenario_type=stress_test-style values →policyplustags=stress_test.
gtappy template variant
| v1 | v2 |
|---|---|
scenario_index |
dropped (label is the key) |
*_short_label columns |
dropped, or folded into the canonical label columns at migration |
baseline_reference_year |
base_year |
| variable-extraction / output-filter columns | out of scope for scenario definitions — they stay in gtappy’s outputs.csv; a genuinely per-scenario override becomes a gtap_-prefixed column |
Breaking changes
- The
p.key_base_yearattribute disappears (→p.base_year); task code reading it must rename (mechanical: e.g.seals_main.py:109, 540, 562, 592). scenario_typeoutside{observed, bau, policy}is rejected.- Stage-override columns require stage registration (
p.scenario_stages). - Backslash paths are invalid.
- The duplicated v1 parsers (
seals_utils.assign_df_row_to_object_attributes,gtappy_utils.assign_df_row_to_object_attributes) are slated for deletion once the hazelbean parser exists. p.seals_years/p.seals_key_base_yearand the override tail inseals_utils.py:598-607(which clobbersp.yearsfor non-baseline rows) disappear — SEALS either runs the canonical clock (typical) or declares asealsstage clock.- Inside stage task code, reads of
p.years/p.key_base_yearthat mean “the clock I am currently iterating” becomep.stage_years/p.stage_base_year(set by the iterator);p.yearandp.previous_yearkeep their names.key_base_years_as_list()(seals_process_coarse_timeseries.py:22) and the scalar/list special case forkey_base_yearinhazelbean/assign_to_object.py:_parse_yearare deleted. - Hand-rolled year interpolation in task code is disallowed: the three duplicated blocks in
gtappy_tasks.pyand the hardcoded decadal BtC interpolation are replaced byhb.resample_years, and the singleshot header-family switch (gtappy_tasks.py:988) is absorbed by the shock-preparation step.
Future implementation
The canonical parser, validator, migrator and RunStep iterator belong in hazelbean next to project_flow.py, reusing cat_ears.py for templating, plus three harmonization pieces: a semantics-aware hb.resample_years (the single temporal resampling utility — levels interpolate, rates compound, differences scale; hazelbean currently has spatial interpolation only, nothing temporal), a coarse_interpolation-aware extract_global_netcdf (which today silently skips requested years the source lacks), and the shock-preparation step that writes a stage-chain-aligned .har from Erwin-style driver files. Key v1 code to replace when implementing: seals/seals_utils.py:586-724 (row→attributes parsing, including the seals_years override tail), seals/seals_main.py:1400-1560 (scenario/year iteration), the gtappy/gtap_invest copies, and the interpolation blocks in gtappy_tasks.py.