Skip to content

Commit a45fce4

Browse files
authored
Merge pull request #309 from iiasa/issue/304
Adapt to modified "Emissions|*|Aviation|*" variable names in `.ssp.transport`
2 parents 5ecc763 + 4fd6446 commit a45fce4

File tree

7 files changed

+13250
-164
lines changed

7 files changed

+13250
-164
lines changed

doc/project/ssp.rst

+15
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,22 @@ Transport
8080

8181
2. To process an existing :class:`pandas.DataFrame` from other code, call :func:`.process_df`, passing the input dataframe and the `method` parameter.
8282

83+
As of 2025-03-07 / :pull:`309`, the set of required "variable" codes handled includes::
84+
85+
Emissions|.*|Energy|Bunkers
86+
Emissions|.*|Energy|Bunkers|International Aviation
87+
Emissions|.*|Energy|Demand|Transportation
88+
Emissions|.*|Energy|Demand|Transportation|Road Rail and Domestic Shipping
89+
90+
The previous set is no longer supported.
91+
8392
As of 2025-01-25:
8493

94+
- The set of "variable" codes modified includes::
95+
96+
Emissions|.*|Energy|Demand|Transportation|Aviation
97+
Emissions|.*|Energy|Demand|Transportation|Aviation|International
98+
Emissions|.*|Energy|Demand|Transportation|Road Rail and Domestic Shipping
99+
85100
- Method 'B' (that is, :func:`.prepare_method_B`; see its documentation) is the preferred method.
86101
- The code is tested on :file:`.xlsx` files in the (internal) directories under `SharePoint > ECE > Documents > SharedSocioeconomicPathways2023 > Scenario_Vetting <https://iiasahub.sharepoint.com/sites/eceprog/Shared%20Documents/Forms/AllItems.aspx?csf=1&web=1&e=APKv0Z&CID=23fa0a51%2Dc303%2D4381%2D8c6d%2D143305cbc5a1&FolderCTID=0x012000AA9481BF7BE9264E85B14105F7F082FF&id=%2Fsites%2Feceprog%2FShared%20Documents%2FSharedSocioEconomicPathways2023%2FScenario%5FVetting&viewid=956acd8a%2De1e7%2D4ae9%2Dab1b%2D0506911bae11>`_, for example :file:`v2.1_Internal_version_Dec13_2024/Reporting_output/SSP_SSP2_v2.1_baseline.xlsx`.

doc/whatsnew.rst

+2-1
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ SSP :ref:`ssp-2024`/ScenarioMIP
2626

2727
Improve :mod:`.ssp.transport`:
2828

29-
- Add :func:`.prepare_method_B` and make this the default1 (:pull:`259`).
29+
- Add :func:`.prepare_method_B` and make this the default (:pull:`259`).
3030
- Add :func:`~.ssp.transport.process_df` (:pull:`303`).
31+
- Adapt to revised ‘variable’ codes (:pull:`309`, :issue:`304`).
3132

3233
Transport
3334
---------

message_ix_models/data/test/report/SSP_LED_v2.3.1_baseline.csv

+13,027
Large diffs are not rendered by default.

message_ix_models/project/ssp/transport.py

+75-99
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
import logging
44
import re
55
from collections.abc import Hashable
6-
from typing import TYPE_CHECKING
6+
from functools import cache
7+
from typing import TYPE_CHECKING, Literal, Optional
78

89
import genno
910
import pandas as pd
10-
import xarray as xr
1111
from genno import Key
1212
from genno.core.key import single_key
1313

@@ -28,9 +28,10 @@
2828
#: Dimensions of several quantities.
2929
DIMS = "e n t y UNIT".split()
3030

31-
#: Expression for IAMC ‘variable’ names used in :func:`main`.
32-
EXPR_EMI = r"^Emissions\|(?P<e>[^\|]+)\|Energy\|Demand\|Transportation(?:\|(?P<t>.*))?$"
33-
EXPR_FE = r"^Final Energy\|Transportation\|(?P<c>Liquids\|Oil)$"
31+
EXPR_EMI = re.compile(
32+
r"^Emissions\|(?P<e>[^\|]+)\|Energy\|Demand\|(?P<t>(Bunkers|Transportation).*)$"
33+
)
34+
EXPR_FE = re.compile(r"^Final Energy\|Transportation\|(?P<c>Liquids\|Oil)$")
3435

3536
#: :class:`.IEA_EWEB` flow codes used in the current file.
3637
FLOWS = ["AVBUNK", "DOMESAIR", "TOTTRANS"]
@@ -63,27 +64,53 @@ def aviation_share(ref: "TQuantity") -> "TQuantity":
6364
)
6465

6566

66-
def broadcast_t(include_international: bool) -> "AnyQuantity":
67+
def broadcast_t(version: Literal[1, 2], include_international: bool) -> "AnyQuantity":
6768
"""Quantity to re-add the |t| dimension.
6869
6970
Parameters
7071
----------
71-
include_international
72-
If :any:`True`, include "Aviation|International" with magnitude 1.0. Otherwise,
73-
omit
72+
version :
73+
Version of ‘variable’ names supported by the current module.
74+
include_international :
75+
If :any:`True`, include "Transportation|Aviation|International" with magnitude
76+
1.0. Otherwise, omit.
7477
7578
Return
7679
------
7780
genno.Quantity
78-
with dimension "t" and the values:
81+
with dimension "t".
7982
80-
- +1.0 for t="Aviation", a label with missing data.
81-
- -1.0 for t="Road Rail and Domestic Shipping", a label with existing data from
82-
which the aviation total should be subtracted.
83+
If :py:`version=1`, the values include:
84+
85+
- +1.0 for t="Transportation|Aviation", a label with missing data.
86+
- -1.0 for t="Transportation|Road Rail and Domestic Shipping", a label with
87+
existing data from which the aviation total must be subtracted.
88+
89+
If :py:`version=2`, the values include:
90+
91+
- +1.0 for t="Bunkers" and t="Bunkers|International Aviation", labels with zeros
92+
in the input data file.
93+
- -1.0 for t="Transportation" and t="Transportation|Road Rail and Domestic
94+
Shipping", labels with existing data from which the aviation total must be
95+
subtracted.
8396
"""
84-
value = [1, -1, 1]
85-
t = ["Aviation", "Road Rail and Domestic Shipping", "Aviation|International"]
86-
idx = slice(None) if include_international else slice(-1)
97+
if version == 1:
98+
value = [1, -1, 1]
99+
t = [
100+
"Transportation|Aviation",
101+
"Transportation|Road Rail and Domestic Shipping",
102+
"Transportation|Aviation|International",
103+
]
104+
idx = slice(None) if include_international else slice(-1)
105+
elif version == 2:
106+
value = [1, 1, -1, -1]
107+
t = [
108+
"Bunkers",
109+
"Bunkers|International Aviation",
110+
"Transportation",
111+
"Transportation|Road Rail and Domestic Shipping",
112+
]
113+
idx = slice(None)
87114

88115
return genno.Quantity(value[idx], coords={"t": t[idx]})
89116

@@ -119,59 +146,6 @@ def e_UNIT(cl_emission: "sdmx.model.common.Codelist") -> "AnyQuantity":
119146
)
120147

121148

122-
def extract_dims(
123-
qty: "TQuantity", dim_expr: dict, *, drop: bool = True, fillna: str = "_T"
124-
) -> "TQuantity":
125-
"""Extract dimensions from IAMC-like ‘variable’ names using regular expressions."""
126-
dims = list(qty.dims)
127-
128-
dfs = [qty.to_series().rename("value").reset_index()]
129-
for dim, expr in dim_expr.items():
130-
pattern = re.compile(expr)
131-
dfs.append(dfs[0][dim].str.extract(pattern).fillna(fillna))
132-
dims.extend(pattern.groupindex)
133-
if drop:
134-
dims.remove(dim)
135-
136-
return genno.Quantity(pd.concat(dfs, axis=1).set_index(dims)["value"])
137-
138-
139-
def extract_dims1(qty: "TQuantity", dim: dict) -> "TQuantity": # pragma: no cover
140-
"""Extract dimensions from IAMC-like ‘variable’ names expressions.
141-
142-
.. note:: This incomplete, non-working version of :func:`extract_dims` uses
143-
:mod:`xarray` semantics.
144-
"""
145-
from collections import defaultdict
146-
147-
result = qty
148-
for d0, expr in dim.items():
149-
d0_new = f"{d0}_new"
150-
pattern = re.compile(expr)
151-
152-
indexers: dict[Hashable, list[Hashable]] = {g: [] for g in pattern.groupindex}
153-
indexers[d0_new] = []
154-
155-
coords = qty.coords[d0].data.astype(str)
156-
for coord in coords:
157-
if match := pattern.match(coord):
158-
groupdict = match.groupdict()
159-
coord_new = coord[match.span()[1] :]
160-
else:
161-
groupdict = defaultdict(None)
162-
coord_new = coord
163-
164-
for g in pattern.groupindex:
165-
indexers[g].append(groupdict[g])
166-
indexers[d0_new].append(coord_new)
167-
168-
for d1, labels in indexers.items():
169-
i2 = {d0: xr.DataArray(coords, coords={d1: labels})}
170-
result = result.sel(i2)
171-
172-
return result
173-
174-
175149
def finalize(
176150
q_all: "TQuantity", q_update: "TQuantity", model_name: str, scenario_name: str
177151
) -> pd.DataFrame:
@@ -210,9 +184,7 @@ def _expand(qty):
210184
.to_frame()
211185
.reset_index()
212186
.assign(
213-
Variable=lambda df: (
214-
"Emissions|" + df["e"] + "|Energy|Demand|Transportation|" + df["t"]
215-
).str.replace("|_T", ""),
187+
Variable=lambda df: "Emissions|" + df["e"] + "|Energy|Demand|" + df["t"]
216188
)
217189
.drop(["e", "t"], axis=1)
218190
.set_index(s_all.index.names)[0]
@@ -239,17 +211,19 @@ def prepare_computer(c: "Computer", k_input: Key, method: str) -> "KeyLike":
239211
str
240212
"target". Calling :py:`c.get("target")` triggers the calculation.
241213
"""
214+
c.require_compat("message_ix_models.report.operator")
215+
242216
# Common structure and utility quantities used by prepare_method_[AB]
243-
c.add(f"broadcast:t:{L}", broadcast_t, include_international=method == "A")
217+
c.add(
218+
f"broadcast:t:{L}", broadcast_t, version=2, include_international=method == "A"
219+
)
244220

245-
k_emi_in, e_t = Key(L, DIMS, "input"), tuple("et")
221+
k_emi_in = Key(L, DIMS, "input")
246222

247223
# Select and transform data matching EXPR_EMI
248-
# Filter on "VARIABLE"
249-
c.add(k_emi_in[0] / e_t, select_re, k_input, indexers={"VARIABLE": EXPR_EMI})
250-
# Extract the "e" and "t" dimensions from "VARIABLE"
251-
c.add(k_emi_in[1], extract_dims, k_emi_in[0] / e_t, dim_expr={"VARIABLE": EXPR_EMI})
252-
c.add(k_emi_in[2], "assign_units", k_emi_in[1], units="Mt/year")
224+
# Filter on "VARIABLE", expand the (e, t) dimensions from "VARIABLE"
225+
c.add(k_emi_in[0], "select_expand", k_input, dim_cb={"VARIABLE": v_to_emi_coords})
226+
c.add(k_emi_in[1], "assign_units", k_emi_in[0], units="Mt/year")
253227

254228
# Call a function to prepare the remaining calculations
255229
prepare_func = {"A": prepare_method_A, "B": prepare_method_B}[method]
@@ -318,11 +292,8 @@ def prepare_method_B(
318292
is, final energy use by aviation.
319293
6. Load emissions intensity of aviation final energy use from the file
320294
:ref:`transport-input-emi-intensity`.
321-
7. Multiply (4) × (5) × (6) to compute the estimate of
322-
``Emissions|*|Energy|Demand|Transportation|Aviation``.
323-
8. Estimate an additive adjustment to
324-
``Emissions|*|Energy|Demand|Transportation|Road Rail and Domestic Shipping`` as
325-
the negative of (7).
295+
7. Multiply (4) × (5) × (6) to compute the estimate of aviation emissions.
296+
8. Estimate adjustments according to :func:`broadcast_t`.
326297
9. Adjust `k_emi_in` by adding (7) and (8).
327298
"""
328299
from message_ix_models.model.transport import build
@@ -379,14 +350,11 @@ def prepare_method_B(
379350

380351
### Prepare data from the input data file: total transport consumption of light oil
381352

382-
# Filter on "VARIABLE"
383-
c.add(k_fe_in[0] / "c", select_re, k_input, indexers={"VARIABLE": EXPR_FE})
384-
385-
# Extract the "e" dimensions from "VARIABLE"
386-
c.add(k_fe_in[1], extract_dims, k_fe_in[0] / "c", dim_expr={"VARIABLE": EXPR_FE})
353+
# Filter on "VARIABLE", extract (e) dimension
354+
c.add(k_fe_in[0], "select_expand", k_input, dim_cb={"VARIABLE": v_to_fe_coords})
387355

388356
# Convert "UNIT" dim labels to Quantity.units
389-
c.add(k_fe_in[2] / "UNIT", "unique_units_from_dim", k_fe_in[1], dim="UNIT")
357+
c.add(k_fe_in[1] / "UNIT", "unique_units_from_dim", k_fe_in[0], dim="UNIT")
390358

391359
# Relabel:
392360
# - c[ommodity]: 'Liquids|Oil' (IAMC 'variable' component) → 'lightoil'
@@ -395,7 +363,7 @@ def prepare_method_B(
395363
c={"Liquids|Oil": "lightoil"},
396364
n={n.id.partition("_")[2]: n.id for n in get_codelist("node/R12")},
397365
)
398-
c.add(k_fe_in[3] / "UNIT", "relabel", k_fe_in[2] / "UNIT", labels=labels)
366+
c.add(k_fe_in[2] / "UNIT", "relabel", k_fe_in[1] / "UNIT", labels=labels)
399367

400368
### Compute estimate of emissions
401369
# Product of aviation share and FE of total transport → FE of aviation
@@ -500,11 +468,19 @@ def process_file(
500468
c.get("target").to_csv(path_out, index=False)
501469

502470

503-
def select_re(qty: "AnyQuantity", indexers: dict) -> "AnyQuantity":
504-
"""Select from `qty` using regular expressions for each dimension."""
505-
new_indexers = dict()
506-
for dim, expr in indexers.items():
507-
new_indexers[dim] = list(
508-
map(str, filter(re.compile(expr).match, qty.coords[dim].data.astype(str)))
509-
)
510-
return qty.sel(new_indexers)
471+
@cache
472+
def v_to_fe_coords(value: Hashable) -> Optional[dict[str, str]]:
473+
"""Match ‘variable’ names used in :func:`main`."""
474+
if match := EXPR_FE.fullmatch(str(value)):
475+
return match.groupdict()
476+
else:
477+
return None
478+
479+
480+
@cache
481+
def v_to_emi_coords(value: Hashable) -> Optional[dict[str, str]]:
482+
"""Match ‘variable’ names used in :func:`main`."""
483+
if match := EXPR_EMI.fullmatch(str(value)):
484+
return match.groupdict()
485+
else:
486+
return None

0 commit comments

Comments
 (0)