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

init sankey #770

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions message_ix/report/__init__.py
Expand Up @@ -141,6 +141,7 @@
"message::costs",
"message::emissions",
),
("message::sankey", "concat", "out::pyam", "in::pyam"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would get rid of this, and simply do the concatenation outside of reporter (e.g., in the jupyter notebook here)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept this for now as I don't know what the Reporter actually does and it seems useful to just get the dataframe format we need from the Reporter. But please elaborate :)

Copy link

@daymontas1 daymontas1 May 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have created a new file, sankey.py, within the report directory, which extracts Sankey diagram functionality from init.py. Fridolin (@glatterf42), could you please confirm if this is what we want? Additionally, I have updated the westeros_sankey.ipynb file to ensure compatibility with the new sankey.py file in the report directory. I have annotated these changes in the westeros_sankey.ipynb below. Also, in this case, the following line inside the init.py file must be either removed or commented out:
("message::sankey", "concat", "out::pyam", "in::pyam")

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @daymontas1, these changes sound good to me. For the line that has become redundant, please remove it rather than commenting it out, this keeps the code clean.
Could you please push your changes to the branch so that we can take a look and see how the tests are doing?
If you don't have write access to @mabudz's fork, please let us know so that we can figure out a solution :)

Copy link

@daymontas1 daymontas1 May 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#''Sankey''
import logging
from functools import partial
from typing import Tuple, Mapping, List

from genno.operator import broadcast_map
from ixmp.report import Key
from .pyam import collapse_message_cols

# Assuming TASKS1 was where Sankey tasks were defined:
TASKS1 = (
    ("message::sankey", "concat", "out::pyam", "in::pyam"),
)

def get_sankey_tasks() -> List[Tuple[Tuple, Mapping]]:
    """Return a list of tasks for Sankey diagram reporting."""
    to_add: List[Tuple[Tuple, Mapping]] = []
    strict = dict(strict=True)

    # This might include specific Sankey diagram configuration or additional tasks.
    for t in TASKS1:
        to_add.append((t, strict))

    return to_add

class SankeyReporter:
    """A specialized reporter for generating Sankey diagrams."""

    @staticmethod
    def add_tasks(reporter, fail_action: str = "raise") -> None:
        """Add Sankey-related tasks to a given reporter."""
        reporter.add_queue(get_sankey_tasks(), fail=fail_action)

```suggestion
    ("message::sankey", "concat", "out::pyam", "in::pyam"),
```

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this suggestion, @daymontas1 :)
To clarify here: are these the contents of the new sankey.py file you mentioned above? They probably don't need to include the suggestion part, then.
If I understand correctly, what you want to suggest is deleting this line that is no longer needed here:

Suggested change
("message::sankey", "concat", "out::pyam", "in::pyam"),

)


Expand Down
54 changes: 54 additions & 0 deletions message_ix/tests/test_util.py
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 sankey_mapper


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


def test_sankey_mapper(test_mp):
# NB: we actually only need a pd.DataFrame 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)
rep = Reporter.from_scenario(scen)
rep.configure(units={"replace": {"-": ""}})
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 = {
"in|secondary|electricity|grid|standard": (
"secondary|electricity",
"grid|standard",
),
"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"),
}

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

mapping_without_final_electricity = sankey_mapper(
df, year=700, region="Westeros", exclude=["final|electricity"]
)
assert mapping_without_final_electricity == expected_without_final_electricity
28 changes: 28 additions & 0 deletions message_ix/util/sankey.py
@@ -0,0 +1,28 @@
from typing import Any, Optional

import pandas as pd

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


def sankey_mapper(
df: pd.DataFrame,
year: int,
region: str,
exclude: list[Optional[str]] = [],
) -> dict[str, Any]:
mapping = {}

for var in df.filter(region=region + "*", year=year).variable:
is_input = gvc(var, 0) == "in"
(start_idx, end_idx) = ([1, 2], [3, 4]) if is_input else ([3, 4], [1, 2])
source = gvc(var, start_idx, join=True)
target = gvc(var, end_idx, join=True)
if source in exclude or target in exclude:
continue
mapping[var] = (source, target)

return mapping
2 changes: 1 addition & 1 deletion pyproject.toml
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
199 changes: 199 additions & 0 deletions tutorial/westeros/westeros_sankey.ipynb
@@ -0,0 +1,199 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Westeros Tutorial - Introducing Sankey diagrams\n",
"\n",
"Sankey diagrams are a useful technique to visualize energy flow accounts.\n",
"\n",
"This tutorial introduces the sankey feature provided by the ``pyam`` packages.\n",
"\n",
"\n",
"**Pre-requisites**\n",
"- You have the *MESSAGEix* framework installed and working\n",
" In particular, you should have installed ``message_ix``, ``pyam``, and ``plotly``\n",
"- Complete tutorial Part 1 (``westeros_baseline.ipynb``) and Introducing Reporting (``westeros_report.ipynb``)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import ixmp\n",
"\n",
"from message_ix import Scenario\n",
"\n",
"mp = ixmp.Platform()\n",
"scenario = Scenario(mp, model=\"Westeros Electrified\", scenario=\"baseline\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Remove any existing solution\n",
"try:\n",
" scenario.remove_solution()\n",
"except ValueError:\n",
" pass\n",
"\n",
"scenario.solve()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Create the reporter object. (Since ``\"-\"`` is not a unit, we replace it by ``\"\"``.)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from message_ix.report import Reporter\n",
"\n",
"rep = Reporter.from_scenario(scenario)\n",
"\n",
"rep.configure(units={\"replace\": {\"-\": \"\"}})"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Use the `message::sankey` reporter option to generate a pyam.dataframe including the reqiured input (`in::pyam`) and output flows (`out::pyam`) in iamc format.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"df = rep.get(\"message::sankey\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The utility function `sankey_mapper(df, year, region, exclude=[])` can be used to create the required mapping for the `figures.sankey()` function of the `pyam` package.\n",
"\n",
"In some models it might be necessary to exclude variables and flow to get meaningful sankey diagrams. But let´s try with all!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from message_ix.util.sankey import sankey_mapper\n",
"\n",
"mapping = sankey_mapper(df, year=700, region=\"Westeros\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The pyam function `plot.sankey(mapping)`returns a plotly sankey figure object that can be further modified.\n",
"\n",
"To plot it as an interactive diagram in your web browser, you can do the following."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from pyam.figures import sankey\n",
"\n",
"fig = sankey(df=df.filter(year=700), mapping=mapping)\n",
"fig.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"mapping_without_final_electricity = sankey_mapper(\n",
" df, year=700, region=\"Westeros\", exclude=[\"final|electricity\"]\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fig = sankey(df=df.filter(year=700), mapping=mapping_without_final_electricity)\n",
"fig.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"mapping_without_wind_ppl_standard = sankey_mapper(\n",
" df, year=700, region=\"Westeros\", exclude=[\"wind_ppl|standard\"]\n",
")\n",
"fig = sankey(df=df.filter(year=700), mapping=mapping_without_wind_ppl_standard)\n",
"fig.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Do not forget to close the database ;-) "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"mp.close_db()"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "messageix",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.12"
},
"orig_nbformat": 4
},
"nbformat": 4,
"nbformat_minor": 2
}