From 5b608a980acac7f03e83a27a47d3069a7ad78fae Mon Sep 17 00:00:00 2001 From: Sebastian Troitzsch Date: Wed, 18 Oct 2023 05:20:30 +0200 Subject: [PATCH] plots: Add electric grid graph layout plot --- examples/development/entrypoint_plotting.py | 11 +- mesmo/data_models/results.py | 1 + mesmo/plots/__init__.py | 1 + mesmo/plots/constants.py | 1 + mesmo/plots/graph.py | 181 ++++++++++++++++++++ mesmo/problems.py | 3 +- poetry.lock | 86 +++++++++- pyproject.toml | 1 + 8 files changed, 270 insertions(+), 15 deletions(-) create mode 100644 mesmo/plots/graph.py diff --git a/examples/development/entrypoint_plotting.py b/examples/development/entrypoint_plotting.py index b4b90fd9a..1ad422ea1 100644 --- a/examples/development/entrypoint_plotting.py +++ b/examples/development/entrypoint_plotting.py @@ -14,13 +14,7 @@ def main(): results_raw = mesmo.api.run_nominal_operation_problem( scenario_name, results_path=results_path, store_results=False, recreate_database=False ) - results = results_raw.get_run_results() - - # # Roundtrip save/load to/from JSON, just for demonstration - # print("Writing results to file") - # results.to_json_file(results_path / "results.json") - # print("Loading results from file") - # results = mesmo.data_models.RunResults.from_json_file(results_path / "results.json") + results = results_raw.get_run_results(scenario_name=scenario_name) # Write results to compressed JSON print("Writing results to file") @@ -32,7 +26,7 @@ def main(): mesmo.config.base_path / "results" / "results.json.bz2", decompress=True ) - # Sample plotting to file, just for demonstration + # Sample plotting to file plots.plot_to_file(plots.der_active_power_time_series, results=results, results_path=results_path) plots.plot_to_file(plots.der_reactive_power_time_series, results=results, results_path=results_path) plots.plot_to_file(plots.der_apparent_power_time_series, results=results, results_path=results_path) @@ -41,6 +35,7 @@ def main(): plots.plot_to_file(plots.der_aggregated_apparent_power_time_series, results=results, results_path=results_path) plots.plot_to_file(plots.node_voltage_per_unit_time_series, results=results, results_path=results_path) plots.plot_to_file(plots.node_aggregated_voltage_per_unit_time_series, results=results, results_path=results_path) + plots.plot_to_file(plots.electric_grid_asset_layout, results=results, results_path=results_path) # Sample JSON return print(plots.plot_to_json(plots.der_active_power_time_series, results=results)) diff --git a/mesmo/data_models/results.py b/mesmo/data_models/results.py index b87ad2b8e..a0df08fd5 100644 --- a/mesmo/data_models/results.py +++ b/mesmo/data_models/results.py @@ -259,6 +259,7 @@ class ThermalGridDLMPRunResults(base_model.BaseModel): class RunResults(base_model.BaseModel): + scenario_name: str electric_grid_model_index: Optional[model_index.ElectricGridModelIndex] thermal_grid_model_index: Optional[model_index.ThermalGridModelIndex] der_model_set_index: model_index.DERModelSetIndex diff --git a/mesmo/plots/__init__.py b/mesmo/plots/__init__.py index ab1181686..099295241 100644 --- a/mesmo/plots/__init__.py +++ b/mesmo/plots/__init__.py @@ -1,5 +1,6 @@ """Plotting function collection.""" +from .graph import electric_grid_asset_layout from .plots import plot_to_figure, plot_to_file, plot_to_json from .time_series import ( der_active_power_time_series, diff --git a/mesmo/plots/constants.py b/mesmo/plots/constants.py index c3de0ec22..bfa44a2fb 100644 --- a/mesmo/plots/constants.py +++ b/mesmo/plots/constants.py @@ -17,3 +17,4 @@ class ValueLabels(str, enum.Enum): REACTIVE_POWER = "Reactive power" APPARENT_POWER = "Apparent power" VOLTAGE = "Voltage" + ASSETS = "Assets" diff --git a/mesmo/plots/graph.py b/mesmo/plots/graph.py new file mode 100644 index 000000000..c88e75932 --- /dev/null +++ b/mesmo/plots/graph.py @@ -0,0 +1,181 @@ +"""Graph-based plotting functions.""" + +import networkx as nx +import pandas as pd +import plotly.graph_objects as go +from multimethod import multimethod +from netgraph import Graph + +import mesmo.data_interface +from mesmo import data_models +from mesmo.plots import constants + + +class ElectricGridGraph(nx.DiGraph): + """Electric grid graph object.""" + + line_edges: dict[str, tuple[str, str]] + transformer_edges: dict[str, tuple[str, str]] + node_positions: dict[str, tuple[float, float]] + + @multimethod + def __init__(self, scenario_name: str): + # Obtain electric grid data. + electric_grid_data = mesmo.data_interface.ElectricGridData(scenario_name) + + self.__init__(electric_grid_data) + + @multimethod + def __init__(self, electric_grid_data: mesmo.data_interface.ElectricGridData): + # Create graph + super().__init__() + self.add_nodes_from(electric_grid_data.electric_grid_nodes.loc[:, "node_name"].tolist(), layer=0) + self.add_edges_from( + electric_grid_data.electric_grid_lines.loc[:, ["node_1_name", "node_2_name"]].itertuples(index=False) + ) + self.add_edges_from( + electric_grid_data.electric_grid_transformers.loc[:, ["node_1_name", "node_2_name"]].itertuples(index=False) + ) + + # Obtain edges indexed by line name + self.line_edges = pd.Series( + electric_grid_data.electric_grid_lines.loc[:, ["node_1_name", "node_2_name"]].itertuples(index=False), + index=electric_grid_data.electric_grid_lines.loc[:, "line_name"], + ).to_dict() + + # Obtain edges indexed by transformer name + self.transformer_edges = pd.Series( + electric_grid_data.electric_grid_transformers.loc[:, ["node_1_name", "node_2_name"]].itertuples( + index=False + ), + index=electric_grid_data.electric_grid_transformers.loc[:, "transformer_name"], + ).to_dict() + + # Apply graph layout for node positions + graph_layout = Graph(self, node_layout="dot") + self.node_positions = graph_layout.node_positions + + +def electric_grid_asset_layout(figure: go.Figure, results: data_models.RunResults) -> go.Figure: + graph = ElectricGridGraph(results.scenario_name) + + # Plot lines + line_edge_x = [] + line_edge_y = [] + line_edge_x_label = [] + line_edge_y_label = [] + line_name = [] + for name, edge in graph.line_edges.items(): + x0, y0 = graph.node_positions[edge[0]] + x1, y1 = graph.node_positions[edge[1]] + line_edge_x.append(x0) + line_edge_x.append(x1) + line_edge_x.append(None) + line_edge_y.append(y0) + line_edge_y.append(y1) + line_edge_y.append(None) + line_edge_x_label.append((x0 + x1) / 2) + line_edge_y_label.append((y0 + y1) / 2) + line_name.append(name) + figure.add_trace( + go.Scatter( + x=line_edge_x, + y=line_edge_y, + line=go.scatter.Line(width=2, color="grey"), + mode="lines", + name="lines", + ) + ) + figure.add_trace( + go.Scatter( + x=line_edge_x_label, + y=line_edge_y_label, + hoverinfo="text", + hovertext=line_name, + mode="markers", + showlegend=False, + marker=go.scatter.Marker(opacity=0), + ) + ) + + # Plot transformers + transformer_edge_x = [] + transformer_edge_y = [] + transformer_edge_x_label = [] + transformer_edge_y_label = [] + transformer_name = [] + for name, edge in graph.transformer_edges.items(): + x0, y0 = graph.node_positions[edge[0]] + x1, y1 = graph.node_positions[edge[1]] + transformer_edge_x.append(x0) + transformer_edge_x.append(x1) + transformer_edge_x.append(None) + transformer_edge_y.append(y0) + transformer_edge_y.append(y1) + transformer_edge_y.append(None) + transformer_edge_x_label.append((x0 + x1) / 2) + transformer_edge_y_label.append((y0 + y1) / 2) + transformer_name.append(name) + figure.add_trace( + go.Scatter( + x=transformer_edge_x, + y=transformer_edge_y, + line=go.scatter.Line(width=2, color="black", dash="dot"), + mode="lines", + name="transformers", + ) + ) + figure.add_trace( + go.Scatter( + x=transformer_edge_x_label, + y=transformer_edge_y_label, + hoverinfo="text", + hovertext=transformer_name, + mode="markers", + showlegend=False, + marker=go.scatter.Marker(opacity=0), + ) + ) + + node_x = [] + node_y = [] + node_name = [] + for node in graph.nodes(): + x, y = graph.node_positions[node] + node_x.append(x) + node_y.append(y) + node_name.append(node) + figure.add_trace( + go.Scatter( + x=node_x, + y=node_y, + mode="markers", + hoverinfo="text", + hovertext=node_name, + marker=go.scatter.Marker( + # showscale=True, + # colorscale options + #'Greys' | 'YlGnBu' | 'Greens' | 'YlOrRd' | 'Bluered' | 'RdBu' | + #'Reds' | 'Blues' | 'Picnic' | 'Rainbow' | 'Portland' | 'Jet' | + #'Hot' | 'Blackbody' | 'Earth' | 'Electric' | 'Viridis' | + # colorscale="YlGnBu", + # reversescale=True, + color="teal", + size=10, + # colorbar=dict(thickness=15, title="Node Connections", xanchor="left", titleside="right"), + # line_width=2, + ), + name="nodes", + ) + ) + + title = "Electric grid asset layout" + legend_title = constants.ValueLabels.ASSETS + + figure.update_layout( + title=title, + xaxis=go.layout.XAxis(showgrid=False, visible=False), + yaxis=go.layout.YAxis(showgrid=False, visible=False), + legend=go.layout.Legend(title=legend_title, x=0.99, xanchor="auto", y=0.01, yanchor="auto"), + ) + return figure diff --git a/mesmo/problems.py b/mesmo/problems.py index 23b2be198..1209c8227 100644 --- a/mesmo/problems.py +++ b/mesmo/problems.py @@ -31,8 +31,9 @@ class Results( price_data: mesmo.data_interface.PriceData - def get_run_results(self) -> mesmo.data_models.RunResults: + def get_run_results(self, scenario_name: str) -> mesmo.data_models.RunResults: return mesmo.data_models.RunResults( + scenario_name=scenario_name, electric_grid_model_index=getattr(self, "electric_grid_model_index", None), thermal_grid_model_index=getattr(self, "thermal_grid_model_index", None), der_model_set_index=getattr(self, "der_model_set_index", None), diff --git a/poetry.lock b/poetry.lock index d620d2766..5eed9f123 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -738,6 +738,23 @@ ufo = ["fs (>=2.2.0,<3)"] unicode = ["unicodedata2 (>=15.0.0)"] woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] +[[package]] +name = "grandalf" +version = "0.8" +description = "Graph and drawing algorithms framework" +optional = false +python-versions = "*" +files = [ + {file = "grandalf-0.8-py3-none-any.whl", hash = "sha256:793ca254442f4a79252ea9ff1ab998e852c1e071b863593e5383afee906b4185"}, + {file = "grandalf-0.8.tar.gz", hash = "sha256:2813f7aab87f0d20f334a3162ccfbcbf085977134a17a5b516940a93a77ea974"}, +] + +[package.dependencies] +pyparsing = "*" + +[package.extras] +full = ["numpy", "ply"] + [[package]] name = "gurobipy" version = "10.0.3" @@ -1301,6 +1318,28 @@ files = [ fast = ["fastnumbers (>=2.0.0)"] icu = ["PyICU (>=1.0.0)"] +[[package]] +name = "netgraph" +version = "4.12.11" +description = "Python drawing utilities for publication quality plots of networks." +optional = false +python-versions = ">=3" +files = [ + {file = "netgraph-4.12.11-py3-none-any.whl", hash = "sha256:fc2dbccecc3bee87a768fce6e291e68bb378ff837833c7a904333e7bcd4fd252"}, + {file = "netgraph-4.12.11.tar.gz", hash = "sha256:4a10168b226b4bc745b73e092f45694c0e7bc2b06d95748f43a873670b927b5d"}, +] + +[package.dependencies] +grandalf = "*" +matplotlib = ">=1.5" +numpy = "*" +rectangle-packer = "*" +scipy = "*" + +[package.extras] +doc = ["Pillow", "networkx", "numpydoc", "sphinx", "sphinx-gallery", "sphinx-rtd-theme"] +tests = ["pytest", "pytest-mpl"] + [[package]] name = "networkx" version = "3.1" @@ -1378,11 +1417,8 @@ files = [ [package.dependencies] numpy = [ - {version = ">=1.21.2", markers = "python_version >= \"3.10\""}, {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\""}, - {version = ">=1.19.3", markers = "python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\" or python_version >= \"3.9\""}, - {version = ">=1.17.0", markers = "python_version >= \"3.7\""}, - {version = ">=1.17.3", markers = "python_version >= \"3.8\""}, + {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\""}, ] [[package]] @@ -2119,6 +2155,44 @@ files = [ numpy = ">=1.7" scipy = ">=0.13.2" +[[package]] +name = "rectangle-packer" +version = "2.0.2" +description = "Pack a set of rectangles into a bounding box with minimum area" +optional = false +python-versions = "*" +files = [ + {file = "rectangle-packer-2.0.2.tar.gz", hash = "sha256:34e450029255f726c4a8e6e939a18cad5879f0d9fe588c1878fe85c872dcbe41"}, + {file = "rectangle_packer-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:41fcb471bea91950beaaccb6672197c6e1f63bba25b4b933dfd02aa470c2fcbe"}, + {file = "rectangle_packer-2.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f630d690859f41ef860420defc889a610367e69956b87164ef66feb814281478"}, + {file = "rectangle_packer-2.0.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:386c95cf1255555bd9b16b759332e8139aaf5e3e3bd4ed9b7117802703b4192d"}, + {file = "rectangle_packer-2.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:26ca5e23e9980a25aa2641c55ee863c5c15e99659604b085e0dcbb7b4ebcaa19"}, + {file = "rectangle_packer-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d7d2b7df8d0a3cc8d3e85643018d23737a29cca968553c9a7cb57c63f3be2f78"}, + {file = "rectangle_packer-2.0.2-cp310-cp310-win32.whl", hash = "sha256:0b0327373fc0a0e71dc510e7d8178718427141adfae2790a456c2a8d9251d1c3"}, + {file = "rectangle_packer-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:e4515ce755c1afa49e4cce3612ddd7a0d92770cbbd54a692948d634bbe0d8d90"}, + {file = "rectangle_packer-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9af27dfb75b964ff7d40f57812ee6e6ffbeff4c50e8c8021a50c1720ce184846"}, + {file = "rectangle_packer-2.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c060aed842e072a16b853d9260cff3d0a147f66d84f675e64505e193d0087438"}, + {file = "rectangle_packer-2.0.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d036f45576589b54aa133e6fd0e47cc3f45598cb3d6562b5215271ef42f59775"}, + {file = "rectangle_packer-2.0.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f9672b5d8b685405c98de71b00b3ce0e963c6195ea479325ebab219dbb98d328"}, + {file = "rectangle_packer-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ed01302499bba7cfd9b8aaf91b8d964344f1f6d74cd966e53f9616fae399fd33"}, + {file = "rectangle_packer-2.0.2-cp311-cp311-win32.whl", hash = "sha256:10deaf700cc71f95e7e7bce701a8cc5392ea2ec11dd02b1c2288990f4b613d30"}, + {file = "rectangle_packer-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:83993b093fed7bac0bfb678a4bb0d31cad7042a8601a462d3bbe519b94980180"}, + {file = "rectangle_packer-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6e834844eb25b91af02f2f166f2dadb4a7920d82c06f3e52b6e6a19c8f04e704"}, + {file = "rectangle_packer-2.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7b983f687db7eef1c4280fb8a9010cbc766459697ec5e689456a55133971573"}, + {file = "rectangle_packer-2.0.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac5bb2cb181d77d85246bbf52da885dc33cc58de03b66e73791d64912814b333"}, + {file = "rectangle_packer-2.0.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6547557d48b385f272533b7c62f4de49f9f6884e9c496d6d26ea28d799f9a1a6"}, + {file = "rectangle_packer-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:340cc9002aa1d4df795f24ef5f516535256bd40dc9568d5bcce94861277bfec6"}, + {file = "rectangle_packer-2.0.2-cp312-cp312-win32.whl", hash = "sha256:7d239749aea7bace64a5052caa6cca84ee558799e0e47348d1ff4e992426a4a1"}, + {file = "rectangle_packer-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:e282a38fae460107806991539905ffe8d6719595f791a049e3622a03b41acbb8"}, + {file = "rectangle_packer-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d41730069bed5bc7f5da8566282112d20a3f24e6e723e2fad4ca7a3eb61d2f2f"}, + {file = "rectangle_packer-2.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ead71062ae26306573479ac38c599dfae6d60c67f25c39987e4c4fb25aa8510"}, + {file = "rectangle_packer-2.0.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfa6b9d24925dc54f07b966ae8c17b76e6870924bf5e9597ab4ab4cbccc0fc1f"}, + {file = "rectangle_packer-2.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:233f22e39ba185ffce0b91503ae7133312adefef32a23736d21065067c49262a"}, + {file = "rectangle_packer-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:796b7a62be8efd1b114eb26301f57e4270807745025d1eae9d956f3b96bc988d"}, + {file = "rectangle_packer-2.0.2-cp39-cp39-win32.whl", hash = "sha256:197195574c1c163df2b76e3803148a97d9f46949f048a0a40e8c2d049c194bee"}, + {file = "rectangle_packer-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:13f89cf33ac9f3d11d67a59c8be6755c5926f552cd823cabece23a8caad2bc22"}, +] + [[package]] name = "requests" version = "2.31.0" @@ -2559,4 +2633,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "~3.10" -content-hash = "dc687cf891ed2b9850698f5cecc96ccfa870dd60b7d77ee978de63e1f2576cf7" +content-hash = "ea24e8d4dc284caddbc31a009e8c69981b57d9e02936d2d35ade3b55b6bbf6c1" diff --git a/pyproject.toml b/pyproject.toml index cad4770e9..a14bbf179 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ requests = "^2.31.0" # For HiGHS installation. scipy = "^1.11.3" tqdm = "^4.66.1" pandera = "^0.17.2" +netgraph = "^4.12.11" [tool.poetry.group.test.dependencies] coverage = { extras = ["toml"], version = "^7.3.2" }