Architecture¶
iampypsa is the shared layer of the IAM ↔ PyPSA soft-coupling. It provides backend
functions that are needed by all PyPSA models that couple to an IAM (reading IAM
output, unit conversion, downscaling, the demand/cost/capacity/CO2 transforms) behind a small,
stable interface: Coupler.
Guiding rule:
IAM-side logic lives in the package. PyPSA-side logic lives in the model.
Coupleris the seam where the two meet.
Symbols
"Symbol" is the GAMS name for sets, scalars, parameters and variables — these are the
REMIND outputs that iampypsa reads.
Workflow¶
flowchart TB
subgraph inputs["IAM outputs"]
direction LR
src[".gdx / .mif / .csv"]
cfg["data/remind_symbols_gdx.yaml<br/>data/remind_symbols_mif.yaml"]
end
subgraph pkg["iampypsa — shared package"]
subgraph building_blocks["io / transforms / downscale — IAM-side building blocks"]
direction LR
io["io/<br/>RemindLoader · remind_symbols"] --> tf["transforms/<br/>co2 · loads · capacities · costs"] --> ds["downscale/<br/>region → country (SSP)"]
units["units.py"] -.-> io
end
base(["Coupler (base)<br/>shared concrete builders"])
gdx(["★ RemindGdxCoupler"])
iamc(["★ RemindIamcCoupler"])
building_blocks ==> base
base --> gdx
base --> iamc
end
subgraph models["PyPSA models — your code"]
direction LR
eur["pypsa-eur-iam<br/>thin Snakemake scripts"]
chn["PyPSA-China-PIK<br/>thin Snakemake scripts<br/>(+ optional model-specific subclass)"]
end
inputs --> building_blocks
gdx -.->|constructed directly by| eur
iamc -.->|constructed directly by| eur
gdx -.->|constructed / subclassed by| chn
building_blocks -.->|imported directly for a single step| eur
classDef pub fill:#26a69a,stroke:#00564d,color:#ffffff,stroke-width:3px;
class gdx,iamc,io,tf,ds,units pub
Everything in iampypsa is public — io, transforms, and downscale are not hidden
implementation details. Coupler is the recommended entry point for a full pipeline step (it
orchestrates loading, converting, and transforming in one call), but real integrations also
import io/transforms/downscale functions directly for a single step — see
Integrating a PyPSA model's step list, or
pypsa-eur-iam's downscale_REMIND_demand.py, which calls iampypsa.downscale functions
directly rather than going through a Coupler. Data generally flows load → convert →
transform → (downscale) → hand to the model, whether that's orchestrated by a Coupler call
or assembled step-by-step in the model's own Snakemake rules.
What lives where¶
In the package (iampypsa) — shared, model-agnostic¶
| Subpackage | Component | Responsibility |
|---|---|---|
io/ |
loader.RemindLoader |
Open an IAM source and resolve/read symbols. Backend (gdx via gamspy, or iamc .mif/.csv) is auto-detected; lru-cached. |
io/ |
remind_symbols (+ data/remind_symbols_gdx.yaml / data/remind_symbols_mif.yaml) |
Map coupling names — iampypsa's own stable names for a quantity (co2_price, capacity, tech_data, …) → the actual IAM symbol name(s), plus the unit each carries. load_frame() / load_set() read a symbol and apply the declared unit conversion. |
io/ |
technology_mapping |
Parse the model's technology-mapping YAML into a {parameter: source} map per technology — see Technology mapping. |
io/ |
ssp |
Fetch / read the SSP population & GDP proxy datasets used by downscaling. |
units |
units.py |
Unit conversions (from_unit, to_unit) → factor. |
transforms/ |
co2_prices, loads, capacities, costs |
Pure functions on already-loaded long-format tables. They never read files and never know IAM symbol names. |
downscale/ |
demand, proxy, base |
Region → country disaggregation via SSP/degree-day proxy shares. |
couplers/ |
base.Coupler |
The interface + shared concrete builders (CO2 prices, country demand, discount rates). |
couplers/ |
remind.RemindGdxCoupler, remind.RemindIamcCoupler, remind.read_region_map |
REMIND's two Coupler subclasses (GDX / IAMC backends), and the REMIND region↔country CSV reader. |
| (root) | validate |
Check the config's declared scenario (regions/years) actually exists in the IAM source before a run. |
In the PyPSA repo — Snakemake glue (and, optionally, a subclass)¶
The model's own thin Snakemake rules, which construct a Coupler subclass and call its methods.
Most of what used to require a per-model adapter subclass is now just constructor arguments:
- The paths, scenario logic, sector definitions — these stay entirely in the model's own Snakemake/config and never leak into the package.
- Model-specific tweaks, when a model genuinely needs one (e.g. an extra demand-calibration
step), go into a further
Couplersubclass in the model repo — optional, not required.
The Coupler interface¶
Coupler.__init__ binds the shared inputs:
coupler = RemindGdxCoupler(
loader=RemindLoader(remind_gdx_path), # the REMIND source
symbols=load_symbol_specs(backend="gdx"), # coupling-name → REMIND symbol map (+ region overrides)
region_map=read_region_map(), # REMIND region → [country, ...]
config=coupling_config, # the model's coupling config dict
model_regions=[...], # REMIND regions in scope
reference_data={"population": pop_df, "gdp": gdp_df}, # proxies for downscaling
)
Methods, grouped by whether they're IAM-specific or shared:
| Method | Kind | Override when… |
|---|---|---|
build_regional_demand() |
hook — must implement per IAM backend | always, for a new IAM/backend. RemindGdxCoupler/RemindIamcCoupler already implement this for REMIND. |
extract_cost_parameters(year) |
hook — must implement per IAM backend | always, for a new IAM/backend. Same as above. |
build_co2_prices(years=None) |
concrete, inherited | rarely — only if the model's CO2 handling diverges. |
downscale_country_demand(regional=None) |
concrete, inherited | the model needs extra steps (e.g. a historical-calibration adjustment). |
discount_rates(year) |
concrete, inherited | rarely. |
Symbols & units: configure, don't code¶
data/remind_symbols_gdx.yaml / data/remind_symbols_mif.yaml decouple the package from
the IAM's symbol names and units, one file per backend. These can be overwritten in the PyPSA
model's thin coupling layer.
Each top-level key in the YAML (co2_price, capacity, tech_data, …) is a coupling
name: iampypsa's own stable name for a quantity. The coupling code only ever refers to a
quantity by its coupling name (self.symbols["co2_price"]); the YAML maps that name to the
actual IAM symbol name(s). This is not the PyPSA carrier name — that mapping happens
later, in the technology mapping. The indirection means
a symbol can be renamed or reversioned by the IAM (REMIND's v32_taxCO2eq → p_priceCO2 is one
real example), or a region can expose a different one entirely, without touching any code — only
the YAML changes.
A single-quantity symbol — candidate list (first present wins), column renames, source unit(s) and target unit:
co2_price:
symbol: [v32_taxCO2eq, p_priceCO2] # try v32_… first, fall back to p_priceCO2
rename: {tall: year, all_regi: region}
units: [$/tC, $/tC] # per-candidate source unit
to_unit: $/tCO2 # load_frame() applies the (units, to_unit) factor
A mixed-unit set — one IAM symbol whose index column selects several quantities
with different units:
tech_data:
symbol: pm_data
rename: {all_regi: region, all_te: technology}
index: char
schema:
lifetime: {parameter: lifetime, unit: yr, to_unit: yr}
omf: {parameter: FOM, unit: p.u., to_unit: "%/yr"}
omv: {parameter: VOM, unit: T$/TWa, to_unit: $/MWh}
Per-region differences go under overrides: (e.g. CHA:) and need to list only the entries
that differ — everything else is inherited from default:. Resolve with
load_symbol_specs(region="CHA", backend="gdx").
Two layering mechanisms let a model adjust symbols without forking the package:
load_symbol_specs(path=…)or theIAMPYPSA_SYMBOLSenv var — overlay a model-local YAML on top of the packaged default.- the
overrides:block — per-IAM-region deltas inside one config.
The unit conversion contract: load_frame/load_set apply the declared conversion at
the moment of loading (the "Coupler seam"). The downstream transforms are therefore called
with conversion disabled so units are never applied twice. Add a new conversion by adding one
row to UNIT_CONVERSIONS in units.py — never as a literal in a transform or a rule.
Transforms¶
transforms/ is the stateless compute layer. Each function takes an already-loaded,
already-unit-converted long-format table (canonical columns region, year, value, …) and
returns one. They never touch the filesystem and never reference an IAM symbol name — that
is the loader's job — which makes them trivially unit-testable and reusable across models.
Because conversion happens at the load seam (above), transforms are invoked with conversion already applied — they don't re-scale a quantity.
| Module | Key functions | Does |
|---|---|---|
co2_prices |
extract_co2_prices, convert_co2_prices |
Filter/reindex the CO2 price pathway to the coupled regions × years grid (missing → 0); apply the currency factor. |
loads |
convert_loads |
Reduce IAM demand to one tidy row per (year, region, sector) in annual MWh. |
capacities |
prepare_capacities, apply_consolidation, adjust_link_capacities_to_input, aggregate_capacities_to_carriers, build_capacity_targets |
Read + consolidate (VRE-variant merge, battery scaling) tidy capacities; divide link-like techs by efficiency (output→input basis); map IAM techs to PyPSA carriers and sum to capacity targets. |
costs |
build_iam_techdata, build_pypsa_techdata, build_set_value_overrides, apply_overrides, add_discount_rate, convert_investment_to_input_capacity_basis |
Split cost values by technology-mapping source, merge IAM values onto the PyPSA baseline, convert investment from per-output to per-input capacity (× efficiency ** exp), add discount-rate rows. |
The Coupler's build_* / extract_* methods and the transforms/capacities entry point
(build_capacity_targets) are thin orchestrations over these functions; each function is
documented individually in the Reference section of the nav.
Downscaling¶
downscale/ turns the IAM's regional demand into country-level demand — spatial
downscaling only; capacities aren't downscaled this way (see
Harmonising capacities for how those are handled
instead). The split uses SSP population/GDP proxy shares (and degree-day-weighted proxies for
heating-sensitive sectors), applied per (region, year, sector) row via
Coupler.downscale_country_demand().
See Downscaling demand for the full walkthrough — why it's needed, the proxy mechanism, edge cases, and how it relates to the temporal downscaling (annual → hourly) that happens on the model side, outside this package.