Skip to content

Commit 175dedc

Browse files
authored
Merge pull request #325 from iiasa/issue/305
Derive aviation final energy share from solved MESSAGEix-Transport scenarios
2 parents dadf8d8 + 0d563fc commit 175dedc

File tree

16 files changed

+559
-310
lines changed

16 files changed

+559
-310
lines changed

.github/workflows/pytest.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ jobs:
137137
138138
version_string = "${{ matrix.version.upstream }}"
139139
140-
if version_string != "main":
140+
if version_string != "main":
141141
v, result = parse(version_string), []
142142
for condition, dependency in (
143143
(v <= Version("v3.7.0"), "genno < 1.25"), # Upstream versions < 3.8.0 import genno.computations, removed in 1.25.0 (#156)
@@ -174,7 +174,7 @@ jobs:
174174
-m "not (ece_db or snapshot)" \
175175
--color=yes --durations=20 -rA --verbose \
176176
--cov-report=term-missing --cov-report=xml \
177-
--numprocesses=auto \
177+
--numprocesses=auto --dist=loadscope \
178178
--local-cache --jvmargs="-Xmx6G"
179179
180180
- name: Upload test coverage to Codecov.io

doc/project/ssp.rst

+11-10
Original file line numberDiff line numberDiff line change
@@ -72,18 +72,21 @@ Transport
7272
1. To process data from file, use :program:`mix-models ssp transport --help in.xlsx out.xlsx` to invoke :func:`.process_file`.
7373
Data are read from PATH_IN, in :file:`.xlsx` or :file:`.csv` format.
7474
If :file:`.xlsx`, the data are first temporarily converted to :file:`.csv`.
75-
Data are written to PATH_OUT; if not given, this defaults to the same path and suffix as PATH_IN, with "_out" added to the stem.
75+
Data are written to PATH_OUT;
76+
if not given, this defaults to the same path and suffix as PATH_IN with "_out" added to the stem.
7677

7778
For example:
7879

7980
.. code-block:: shell
8081
81-
mix-models ssp transport --method=B \
82+
mix-models --platform=ixmp-dev \
83+
ssp transport --method=C \
8284
SSP_SSP2_v2.1_baseline.xlsx
8385
8486
…produces a file :file:`SSP_SSP2_v2.1_baseline_out.xlsx` in the same directory.
8587

86-
2. To process an existing :class:`pandas.DataFrame` from other code, call :func:`.process_df`, passing the input dataframe and the `method` parameter.
88+
2. To process an existing :class:`pandas.DataFrame` from other code, call :func:`.process_df`,
89+
passing the input dataframe and the `method` parameter.
8790

8891
As of 2025-03-07 / :pull:`309`, the set of required "variable" codes handled includes::
8992

@@ -92,15 +95,13 @@ Transport
9295
Emissions|.*|Energy|Demand|Transportation
9396
Emissions|.*|Energy|Demand|Transportation|Road Rail and Domestic Shipping
9497

95-
The previous set is no longer supported.
96-
97-
As of 2025-01-25:
98-
99-
- The set of "variable" codes modified includes::
98+
The previous set, supported as of 2025-01-25 but no longer supported, included::
10099

101100
Emissions|.*|Energy|Demand|Transportation|Aviation
102101
Emissions|.*|Energy|Demand|Transportation|Aviation|International
103102
Emissions|.*|Energy|Demand|Transportation|Road Rail and Domestic Shipping
104103

105-
- Method 'B' (that is, :func:`.prepare_method_B`; see its documentation) is the preferred method.
106-
- 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`.
104+
- Method 'C' (that is, :func:`.method_C`; see its documentation) is the preferred method.
105+
- 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>`_,
106+
for example :file:`v2.1_Internal_version_Dec13_2024/Reporting_output/SSP_SSP2_v2.1_baseline.xlsx`
107+
or :file:`v2.3_v2.4_Submission_Mar01_2025/Scenario_Reporting_Files/SSP_LED_v2.3.1_baseline.xlsx`

doc/whatsnew.rst

+2-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ SSP :ref:`ssp-2024`/ScenarioMIP
4444

4545
Improve :mod:`.ssp.transport`:
4646

47-
- Add :func:`.prepare_method_B` and make this the default (:pull:`259`).
47+
- Add :func:`~.ssp.transport.method_B` and make this the default (:pull:`259`).
48+
- Add :func:`~.ssp.transport.method_C` (:issue:`305`, :pull:`325`).
4849
- Add :func:`~.ssp.transport.process_df` (:pull:`303`).
4950
- Adapt to revised ‘variable’ codes (:pull:`309`, :issue:`304`).
5051

message_ix_models/cli.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def main(click_ctx, **kwargs):
7474
# NB this can't be Context.only(). When click.testing.CliRunner is used, there may
7575
# already be ≥2 Context instances created elsewhere in the test session before
7676
# this function is called to run CLI commands within the test session.
77-
click_ctx.obj = Context.get_instance(-1)
77+
click_ctx.obj = Context()
7878

7979
# Handle command-line parameters
8080
click_ctx.obj.core.handle_cli_args(**kwargs)

message_ix_models/model/transport/base.py

+16-16
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import genno
99
import numpy as np
1010
import pandas as pd
11-
from genno import Computer, Key, KeySeq, quote
11+
from genno import Computer, Key, quote
1212
from genno.core.key import single_key
1313

1414
from message_ix_models.util import minimum_version
@@ -80,7 +80,7 @@ def smooth(c: Computer, key: "genno.Key", *, dim: str = "ya") -> "genno.Key":
8080
from itertools import pairwise
8181

8282
assert key.tag != "2"
83-
ks = KeySeq(key.remove_tag(key.tag or ""))
83+
ks = Key(key.remove_tag(key.tag or ""))
8484

8585
def first_block_false(column: pd.Series) -> pd.Series:
8686
"""Modify `column` to contain at most one contiguous block of :data:`.False`."""
@@ -181,9 +181,9 @@ def prepare_reporter(rep: "message_ix.Reporter") -> str:
181181

182182
# Keys for starting quantities
183183
e_iea = Key("energy:n-y-product-flow:iea")
184-
e_fnp = KeySeq(e_iea.drop("y"))
184+
e_fnp = Key(e_iea.drop("y"))
185185
e_cnlt = Key("energy:c-nl-t:iea+0")
186-
k = KeySeq("in:nl-t-ya-c-l-h:transport+units") # MESSAGE solution values
186+
k = Key("in:nl-t-ya-c-l-h:transport+units") # MESSAGE solution values
187187

188188
# First period
189189
y0 = rep.get("y0")
@@ -205,18 +205,18 @@ def prepare_reporter(rep: "message_ix.Reporter") -> str:
205205
)
206206

207207
# Transport outputs for comparison
208-
rep.add(k[0], "select", k.base, indexers=dict(ya=y0), drop=True)
209-
rep.add(k[1], "aggregate", k[0], "groups::transport to iea", keep=False)
208+
rep.add(k[0], "aggregate", k, "groups::transport to iea", keep=False, sums=True)
209+
rep.add(k[1] / "ya", "select", k[0], indexers=dict(ya=y0), drop=True)
210210

211211
# Scaling factor 1: ratio of MESSAGEix-Transport outputs to IEA data
212-
tmp = rep.add("scale 1", "div", k[1], e_cnlt)
213-
s1 = KeySeq(tmp)
214-
rep.add(s1[1], "convert_units", s1.base, units="1 / a")
212+
tmp = rep.add("scale 1", "div", k[1] / "ya", e_cnlt)
213+
s1 = Key(single_key(tmp))
214+
rep.add(s1[1], "convert_units", s1, units="1 / a")
215215
rep.add(s1[2], "mul", s1[1], genno.Quantity(1.0, units="a"))
216216
# Replace ~0 and ∞ values with 1.0; this avoids x / 0 = inf
217217
rep.add(s1[3], "where", s1[2], cond=lambda v: (v > 1e-3) & (v != np.inf), other=1.0)
218218
# Ensure no values are dropped versus the numerator (= MESSAGE outputs)
219-
rep.add(s1[4], align_and_fill, s1[3], k[1])
219+
rep.add(s1[4], align_and_fill, s1[3], k[1] / "ya")
220220

221221
rep.apply(to_csv, s1[4], name=s1.name, header_comment=SCALE_1_HEADER)
222222

@@ -241,7 +241,7 @@ def prepare_reporter(rep: "message_ix.Reporter") -> str:
241241

242242
# Correct MESSAGEix-Transport outputs for the MESSAGEix-base model using the high-
243243
# resolution scaling factor
244-
rep.add(k["s1"], "div", k.base, s1[11])
244+
rep.add(k["s1"], "div", k, s1[11])
245245

246246
# Scaling factor 2: ratio of total of scaled data to IEA total
247247
rep.add(k[2] / "ya", "select", k["s1"], indexers=dict(ya=y0), drop=True, sums=True)
@@ -253,8 +253,8 @@ def prepare_reporter(rep: "message_ix.Reporter") -> str:
253253
drop=True,
254254
)
255255
tmp = rep.add("scale 2", "div", k[2] / ("c", "t", "ya"), "energy:nl:iea+transport")
256-
s2 = KeySeq(tmp)
257-
rep.add(s2[1], "convert_units", s2.base, units="1 / a")
256+
s2 = Key(single_key(tmp))
257+
rep.add(s2[1], "convert_units", s2, units="1 / a")
258258
rep.add(s2[2], "mul", s2[1], genno.Quantity(1.0, units="a"))
259259

260260
rep.apply(to_csv, s2[2], name=s2.name, header_comment=SCALE_2_HEADER)
@@ -283,13 +283,13 @@ def prepare_reporter(rep: "message_ix.Reporter") -> str:
283283
# - Sum across the "t" dimension of `k` to avoid conflict with "t" labels introduced
284284
# by the data from file.
285285
tmp = rep.add("ue", "div", k["s2"] / "t", "input:t-c-h:base")
286-
ue = KeySeq(tmp)
286+
ue = Key(single_key(tmp))
287287

288-
rep.apply(share_constraints, k["s2"], ue.base)
288+
rep.apply(share_constraints, k["s2"], ue)
289289

290290
# Ensure units: in::transport+units [=] GWa/a and input::base [=] GWa; their ratio
291291
# gives units 1/a. The base model expects "GWa" for all 3 parameters.
292-
rep.add(ue[1], "mul", ue.base, genno.Quantity(1.0, units="GWa * a"))
292+
rep.add(ue[1], "mul", ue, genno.Quantity(1.0, units="GWa * a"))
293293
rep.apply(to_csv, ue[1] / ("c", "t"), name="demand no fill")
294294

295295
# 'Smooth' ue[1] data by interpolating any dip below the base year value

message_ix_models/model/transport/build.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,7 @@ def get_computer(
570570
if visualize and HAS_GRAPHVIZ:
571571
path = context.get_local_path("transport", "build.svg")
572572
path.parent.mkdir(exist_ok=True)
573-
c.visualize(filename=path, key="add transport data")
573+
c.visualize(filename=path, key="add transport data", rankdir="LR")
574574
log.info(f"Visualization written to {path}")
575575

576576
return c

message_ix_models/model/transport/config.py

+26-2
Original file line numberDiff line numberDiff line change
@@ -405,8 +405,21 @@ def set_navigate_scenario(self, value: Optional[str]) -> None:
405405
self.project.update(navigate=s)
406406
self.check()
407407

408-
def use_scenario_code(self, code: "common.Code") -> None:
409-
"""Update settings given a `code` with :class:`ScenarioCodeAnnotations`."""
408+
def use_scenario_code(self, code: "common.Code") -> tuple[str, str]:
409+
"""Update settings given a `code` with :class:`ScenarioCodeAnnotations`.
410+
411+
Returns
412+
-------
413+
tuple of str
414+
The entries are:
415+
416+
1. A short label suitable for a :class:`.Workflow` step name, for instance
417+
"SSP3 policy" or "SSP5", where the first part is :py:`code.id`. See
418+
:func:`.transport.workflow.generate`.
419+
2. A longer, more explicity label suitable for (part of) a
420+
:attr:`message_ix.Scenario.scenario` name in an :mod:`ixmp` database, for
421+
instance "SSP_2024.3".
422+
"""
410423
sca = ScenarioCodeAnnotations.from_obj(code)
411424

412425
# Look up the SSP_2024 Enum
@@ -418,6 +431,17 @@ def use_scenario_code(self, code: "common.Code") -> None:
418431

419432
self.base_scenario_url = sca.base_scenario_URL
420433

434+
# Construct labels including the SSP code and policy identifier
435+
# ‘Short’ label used for workflow steps
436+
label = f"{code.id}{' policy' if self.policy else ''}"
437+
# ‘Full’ label used in the scenario name
438+
if not sca.is_LED_scenario and sca.EDITS_activity_id is None:
439+
label_full = f"SSP_2024.{self.ssp.name}"
440+
else:
441+
label_full = label
442+
443+
return label, label_full
444+
421445

422446
@dataclass
423447
class ScenarioCodeAnnotations(AnnotationsMixIn):

message_ix_models/model/transport/structure.py

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def _leaf_ids(node) -> list[str]:
6262
for tech in filter(lambda t: len(t.child), t_list):
6363
result[tech.id] = _leaf_ids(tech)
6464
# Store non-LDV technologies
65+
# FIXME This is actually "P ex LDV"; label as such
6566
if tech.id != "LDV":
6667
result["non-ldv"].extend(result[tech.id])
6768

message_ix_models/model/transport/workflow.py

+1-10
Original file line numberDiff line numberDiff line change
@@ -210,22 +210,13 @@ def generate(
210210
config = deepcopy(context.transport)
211211

212212
# Update the .transport.Config from the `scenario_code` and `policy`
213-
config.use_scenario_code(scenario_code)
214213
config.policy = policy
214+
label, label_full = config.use_scenario_code(scenario_code)
215215

216216
# Retrieve updated values
217217
is_LED = config.project["LED"]
218218
EDITS_activity = config.project["EDITS"]["activity"]
219219

220-
# Construct labels including the SSP code and policy identifier
221-
# ‘Short’ label used for workflow steps
222-
label = f"{scenario_code.id}{' policy' if policy else ''}"
223-
# ‘Full’ label used in the scenario name
224-
if not is_LED and EDITS_activity is None:
225-
label_full = f"SSP_2024.{config.ssp.name}"
226-
else:
227-
label_full = label
228-
229220
if config.policy and (is_LED or EDITS_activity is not None): # TEMPORARY
230221
log.info(f"({label_full}, {config.policy=}) → skip")
231222
continue

message_ix_models/project/ssp/cli.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def gen_structures(context, **kwargs):
2525

2626

2727
@cli.command("transport")
28-
@click.option("--method", type=click.Choice(["A", "B"]), required=True)
28+
@click.option("--method", type=click.Choice(["A", "B", "C"]), required=True)
2929
@click.argument("path_in", type=click.Path(exists=True, dir_okay=False, path_type=Path))
3030
@click.argument(
3131
"path_out",
@@ -39,11 +39,14 @@ def transport_cmd(context: "Context", method, path_in: Path, path_out: Optional[
3939
Data are read from PATH_IN, in .xlsx or .csv format. If .xlsx, the data are first
4040
temporarily converted to .csv. Data are written to PATH_OUT; if not given, this
4141
defaults to the same path and suffix as PATH_IN, with "_out" added to the stem.
42+
43+
For --method=C, the top-level option --platform=ixmp-dev (for example) may be used
44+
to specify the Platform on which to locate solved MESSAGEix-Transport scenarios.
4245
"""
4346
import pandas as pd
4447
from platformdirs import user_cache_path
4548

46-
from .transport import process_file
49+
from .transport import METHOD, process_file
4750

4851
if path_in.suffix == ".xlsx":
4952
path_in_user = path_in
@@ -65,7 +68,12 @@ def transport_cmd(context: "Context", method, path_in: Path, path_out: Optional[
6568
else:
6669
path_out_user = path_out
6770

68-
process_file(path_in, path_out, method=method)
71+
process_file(
72+
path_in,
73+
path_out,
74+
method=METHOD[method],
75+
platform_name=context.core.platform_info.get("name", None),
76+
)
6977

7078
if path_out_user != path_out:
7179
print(f"Convert CSV output to {path_out_user}")

0 commit comments

Comments
 (0)