Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add .util.sankey and tutorial #770

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3407131
init sankey
mabudz Jan 7, 2024
7ee572e
Apply formatting and fix import from pyam
glatterf42 Jan 10, 2024
1d193b5
Move sankey_mapper to own file
glatterf42 Mar 14, 2024
295fda5
Add plotly as optional dependency
glatterf42 Mar 14, 2024
fb3ea19
Update tutorial
glatterf42 Mar 14, 2024
6d89e5a
Remove forgotten temp output
glatterf42 Mar 14, 2024
cd74331
Add test for sankey_mapper
glatterf42 Mar 14, 2024
29f8137
init sankey
mabudz Jan 7, 2024
372adff
Apply formatting and fix import from pyam
glatterf42 Jan 10, 2024
a733d19
Move sankey_mapper to own file
glatterf42 Mar 14, 2024
03f371d
Extract sankey functionality from init and update the westeros_sankey…
daymontas1 Jun 5, 2024
aff53b8
Extract sankey functionality from init and update the westeros_sankey…
daymontas1 Jun 5, 2024
244e583
Extract sankey functionality from init and update the westeros_sankey…
daymontas1 Jun 5, 2024
dba4d29
Clean up report/sankey after rebase
glatterf42 Sep 23, 2024
a3743e1
Clean up westeros_sankey after rebase
glatterf42 Sep 23, 2024
df93425
Restore mysteriously deleted line
glatterf42 Sep 24, 2024
9c3d4f6
Allow adding sankey-computations as Reporter. function
glatterf42 Sep 24, 2024
155f530
Test reporter.add_sankey
glatterf42 Sep 24, 2024
153fbe1
Refactor map_for_sankey and corresponding test
glatterf42 Sep 24, 2024
0c869c5
Clean up tutorial
glatterf42 Sep 24, 2024
604ed30
Add new tutorial to test suite
glatterf42 Sep 24, 2024
53fa17e
Exclude submodules of pyam from mypy, too
glatterf42 Sep 24, 2024
6bf6eba
Fix LiteralString import for old Python versions
glatterf42 Sep 24, 2024
d32fe75
Fix List type hint for Python 3.8
glatterf42 Sep 24, 2024
c595c93
Fix Dict type hint for Python 3.8
glatterf42 Sep 24, 2024
2dd4a83
Add PR to release notes
glatterf42 Sep 24, 2024
6e086bd
Mention new tutorial in docs
glatterf42 Sep 24, 2024
c1da066
Add new functionality to docs
glatterf42 Sep 24, 2024
f48ca83
Fix typo in docs
glatterf42 Sep 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions RELEASE_NOTES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ All changes

- :mod:`message_ix` is tested and compatible with `Python 3.13 <https://www.python.org/downloads/release/python-3130/>`__ (:pull:`881`).
- Support for Python 3.8 is dropped (:pull:`881`), as it has reached end-of-life.
- Add functionality to create Sankey diagrams from :class:`.Reporter` together with a new tutorial showcase (:pull:`770`).
- Add option to :func:`.util.copy_model` from a non-default location of model files (:pull:`877`).

.. _v3.9.0:
Expand Down
2 changes: 2 additions & 0 deletions doc/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ Utility methods
.. automodule:: message_ix.util
:members: expand_dims, copy_model, make_df

.. automodule:: message_ix.util.sankey
:members: map_for_sankey

Testing utilities
-----------------
Expand Down
1 change: 1 addition & 0 deletions doc/reporting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ These automatic contents are prepared using:
.. autosummary::
add
add_queue
add_sankey
add_single
apply
check_keys
Expand Down
21 changes: 21 additions & 0 deletions message_ix/report/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,24 @@ def add_tasks(self, fail_action: Union[int, str] = "raise") -> None:

# Use a queue pattern via Reporter.add_queue()
self.add_queue(get_tasks(), fail=fail_action)

def add_sankey(self, fail_action: Union[int, str] = "raise") -> None:
"""Add the calculations required to produce Sankey plots.

Parameters
----------
fail_action : "raise" or int
:mod:`logging` level or level name, passed to the `fail` argument of
:meth:`.Reporter.add_queue`.
"""
# NOTE This includes just one task for the base version, but could later be
# expanded.
self.add_queue(
[
(
("message::sankey", "concat", "out::pyam", "in::pyam"),
dict(strict=True),
)
],
fail=fail_action,
)
17 changes: 17 additions & 0 deletions message_ix/tests/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,3 +272,20 @@ def add_tm(df, name="Activity"):
# Results have the expected units
assert all(df5["unit"] == "centiUSD / case")
assert_series_equal(df4["value"], df5["value"] / 100.0)


def test_reporter_add_sankey(test_mp, request):
scen = make_westeros(
test_mp, emissions=True, solve=True, quiet=True, request=request
)

# Reporter.from_scenario can handle Westeros example model
rep = Reporter.from_scenario(scen)

# Westeros-specific configuration: '-' is a reserved character in pint
configure(units={"replace": {"-": ""}})

# Add Sankey calculation(s)
rep.add_sankey()

assert rep.check_keys("message::sankey")
1 change: 1 addition & 0 deletions message_ix/tests/test_tutorials.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def _t(group: Union[str, None], basename: str, *, check=None, marks=None):
_t("w0", f"{W}_addon_technologies"),
_t("w0", f"{W}_historical_new_capacity"),
_t("w0", f"{W}_multinode_energy_trade"),
_t("w0", f"{W}_sankey"),
# NB this is the same value as in test_reporter()
_t(None, f"{W}_report", check=[("len-rep-graph", 13724)]),
_t("at0", "austria", check=[("solve-objective-value", 206321.90625)]),
Expand Down
45 changes: 45 additions & 0 deletions message_ix/tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import pytest

from message_ix import Scenario, make_df
from message_ix.report import Reporter
from message_ix.testing import make_dantzig, make_westeros
from message_ix.util.sankey import map_for_sankey


def test_make_df():
Expand Down Expand Up @@ -59,3 +61,46 @@ def test_testing_make_scenario(test_mp, request):
# Westeros model can be created
scen = make_westeros(test_mp, solve=True, request=request)
assert isinstance(scen, Scenario)


def test_map_for_sankey(test_mp, request):
# NB: we actually only need a pyam.IamDataFrame that has the same form as the result
# of these setup steps, so maybe this can be simplified
scen = make_westeros(test_mp, solve=True, request=request)
rep = Reporter.from_scenario(scen)
rep.configure(units={"replace": {"-": ""}})
rep.add_sankey()
df = rep.get("message::sankey")

# Set expectations
expected_all = {
"in|final|electricity|bulb|standard": ("final|electricity", "bulb|standard"),
"in|secondary|electricity|grid|standard": (
"secondary|electricity",
"grid|standard",
),
"out|final|electricity|grid|standard": ("grid|standard", "final|electricity"),
"out|secondary|electricity|coal_ppl|standard": (
"coal_ppl|standard",
"secondary|electricity",
),
"out|secondary|electricity|wind_ppl|standard": (
"wind_ppl|standard",
"secondary|electricity",
),
"out|useful|light|bulb|standard": ("bulb|standard", "useful|light"),
}
expected_without_final_electricity = {
key: value
for (key, value) in expected_all.items()
if "final|electricity" not in value
}

# Load all variables
mapping_all = map_for_sankey(df, year=700, region="Westeros")
assert mapping_all == expected_all

mapping_without_final_electricity = map_for_sankey(
df, year=700, region="Westeros", exclude=["final|electricity"]
)
assert mapping_without_final_electricity == expected_without_final_electricity
75 changes: 75 additions & 0 deletions message_ix/util/sankey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from typing import Any, Dict, List, Optional, Tuple, Union

from pyam import IamDataFrame

try:
from pyam.str import get_variable_components
except ImportError: # Python < 3.10, pandas < 2.0
from pyam.utils import get_variable_components

try:
from typing import LiteralString
except ImportError: # Python < 3.11
from typing_extensions import LiteralString


def map_for_sankey(
iam_df: IamDataFrame,
year: int,
region: str,
exclude: List[Optional[str]] = [],
) -> Dict[str, Tuple[Union[List, Any, LiteralString], Union[List, Any, LiteralString]]]:
"""Maps input to output flows to enable Sankey plots.

Parameters
----------
iam_df: :class:`pyam.IamDataframe`
The IAMC-format DataFrame holding the data to plot as Sankey diagrams.
year: int
The year to display in the Sankey diagram.
region: str
The region to display in the Sankey diagram.
exclude: list[str], optional
If provided, exclude these keys from the Sankey diagram. Defaults to an empty
list, i.e. showing all flows.

Returns
-------
mapping: dict
A mapping from variable names to their inputs and outputs.
"""
return {
var: get_source_and_target(var)
for var in iam_df.filter(region=region + "*", year=year).variable
if not exclude_flow(get_source_and_target(var), exclude)
}


def get_source_and_target(
variable: str,
) -> Tuple[Union[List, Any, LiteralString], Union[List, Any, LiteralString]]:
"""Get source and target for the `variable` flow."""
start_idx, end_idx = set_start_and_end_index(variable)
return (
get_variable_components(variable, start_idx, join=True),
get_variable_components(variable, end_idx, join=True),
)


def set_start_and_end_index(variable: str) -> Tuple[List[int], List[int]]:
"""Get indices of source and target in variable name."""
return (
([1, 2], [3, 4])
if get_variable_components(variable, 0) == "in"
else ([3, 4], [1, 2])
)


def exclude_flow(
flow: Tuple[Union[List, Any, LiteralString], Union[List, Any, LiteralString]],
exclude: List[Optional[str]],
) -> bool:
"""Exclude sources or targets of variable flow if requested."""
if flow[0] in exclude or flow[1] in exclude:
return True
return False
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ docs = [
"sphinx_rtd_theme",
"sphinxcontrib-bibtex",
]
tutorial = ["jupyter", "matplotlib", "message_ix[report]"]
tutorial = ["jupyter", "matplotlib", "message_ix[report]", "plotly"]
report = ["ixmp[report]"]
tests = [
"asyncssh",
Expand Down Expand Up @@ -96,7 +96,7 @@ local_partial_types = true
[[tool.mypy.overrides]]
# Packages/modules for which no type hints are available.
module = [
"pyam",
"pyam.*",
"scipy.*",
# Indirectly via ixmp; this should be a subset of the list in ixmp's pyproject.toml
"jpype",
Expand Down
4 changes: 4 additions & 0 deletions tutorial/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ framework, such as used in global research applications of |MESSAGEix|.
module to ‘report’ results, e.g. do post-processing, plotting, and other
calculations (:tut:`westeros/westeros_report.ipynb`).

#. After familiarizing yourself with ‘reporting’, learn how to quickly assess
variable flows by plotting Sankey diagrams
(:tut:`westeros/westeros_sankey.ipynb`).

#. Build the baseline scenario using data stored in Excel files to
populate sets and parameters:

Expand Down
Loading
Loading