diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 19c9b45f7..128851018 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,11 @@ Change Log [upcoming release] - 2025-..-.. ------------------------------- - [ADDED] enabling connection of valves directly to pipes besides connection between two junctions +- [ADDED] mapbox_plot module for visualizing networks using Mapbox +- [ADDED] colormaps.py (adapted from pandapower) to enable future colormap support for plotting +- [ADDED] mapbox_plotting tutorial notebook demonstrating new Mapbox-based visualization +- [CHANGED] simple_plot extended to accept Mapbox-related parameters +- [ADDED] test_mapbox.py with unit tests for Mapbox-based plotting functionality - [ADDED] tutorial for combining the DHNx package and pandapipes - [CHANGED] Improve readability of setting pipeflow options - [FIXED] Default compressibility model in STANET converter now set to "linear" to avoid Exceptions diff --git a/src/pandapipes/plotting/colormaps.py b/src/pandapipes/plotting/colormaps.py new file mode 100644 index 000000000..95c53f0be --- /dev/null +++ b/src/pandapipes/plotting/colormaps.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016-2023 by University of Kassel and Fraunhofer Institute for Energy Economics +# and Energy System Technology (IEE), Kassel. All rights reserved. + + +try: + from matplotlib.colors import ListedColormap, BoundaryNorm, LinearSegmentedColormap, Normalize, LogNorm + MATPLOTLIB_INSTALLED = True +except ImportError: + MATPLOTLIB_INSTALLED = False + +import numpy as np + + +def cmap_discrete(cmap_list): + """ + Can be used to create a discrete colormap. + + INPUT: + - cmap_list (list) - list of tuples, where each tuple represents one range. Each tuple has + the form of ((from, to), color). + + OUTPUT: + - cmap - matplotlib colormap + + - norm - matplotlib norm object + + EXAMPLE: + >>> from pandapower.plotting import cmap_discrete, create_line_collection, draw_collections + >>> from pandapower.networks import mv_oberrhein + >>> net = mv_oberrhein("generation") + >>> cmap_list = [((0, 10), "green"), ((10, 30), "yellow"), ((30, 100), "red")] + >>> cmap, norm = cmap_discrete(cmap_list) + >>> lc = create_line_collection(net, cmap=cmap, norm=norm) + >>> draw_collections([lc]) + """ + if not MATPLOTLIB_INSTALLED: + raise UserWarning("install matplotlib to use this function") + cmap_colors = [] + boundaries = [] + last_upper = None + for (lower, upper), color in cmap_list: + if last_upper is not None and lower != last_upper: + raise ValueError("Ranges for colormap must be continuous") + cmap_colors.append(color) + boundaries.append(lower) + last_upper = upper + boundaries.append(upper) + cmap = ListedColormap(cmap_colors) + norm = BoundaryNorm(boundaries, cmap.N) + return cmap, norm + + +def cmap_continuous(cmap_list): + """ + Can be used to create a continuous colormap. + + INPUT: + - cmap_list (list) - list of tuples, where each tuple represents one color. Each tuple has + the form of (center, color). The colorbar is a linear segmentation of + the colors between the centers. + + OUTPUT: + - cmap - matplotlib colormap + + - norm - matplotlib norm object + + EXAMPLE: + >>> from pandapower.plotting import cmap_continuous, create_bus_collection, draw_collections + >>> from pandapower.networks import mv_oberrhein + >>> net = mv_oberrhein("generation") + >>> cmap_list = [(0.97, "blue"), (1.0, "green"), (1.03, "red")] + >>> cmap, norm = cmap_continuous(cmap_list) + >>> bc = create_bus_collection(net, size=70, cmap=cmap, norm=norm) + >>> draw_collections([bc]) + """ + if not MATPLOTLIB_INSTALLED: + raise UserWarning("install matplotlib to use this function") + min_loading = cmap_list[0][0] + max_loading = cmap_list[-1][0] + cmap_colors = [((loading-min_loading)/(max_loading - min_loading), color) for + (loading, color) in cmap_list] + cmap = LinearSegmentedColormap.from_list('name', cmap_colors) + norm = Normalize(min_loading, max_loading) + return cmap, norm + + +def cmap_logarithmic(min_value, max_value, colors): + """ + Can be used to create a logarithmic colormap. The colormap itself has a linear segmentation of + the given colors. The values however will be matched to the colors based on a logarithmic + normalization (c.f. matplotlib.colors.LogNorm for more information on how the logarithmic + normalization works). + + \nPlease note: {There are numerous ways of how a logarithmic scale might + be created, the intermediate values on the scale are created automatically based on the minimum + and maximum given values in analogy to the LogNorm. Also, the logarithmic colormap can only be + used with at least 3 colors and increasing values which all have to be above 0.} + + INPUT: + **min_value** (float) - the minimum value of the colorbar + + **max_value** (float) - the maximum value for the colorbar + + **colors** (list) - list of colors to be used for the colormap + + OUTPUT: + **cmap** - matplotlib colormap + + **norm** - matplotlib norm object + + EXAMPLE: + + >>> from pandapower.plotting import cmap_logarithmic, create_bus_collection, draw_collections + >>> from pandapower.networks import mv_oberrhein + >>> net = mv_oberrhein("generation") + >>> min_value, max_value = 1.0, 1.03 + >>> colors = ["blue", "green", "red"] + >>> cmap, norm = cmap_logarithmic(min_value, max_value, colors) + >>> bc = create_bus_collection(net, size=70, cmap=cmap, norm=norm) + >>> draw_collections([bc]) + + """ + + num_values = len(colors) + if num_values < 2: + raise UserWarning("Cannot create a logarithmic colormap less than 2 colors.") + if min_value <= 0: + raise UserWarning("The minimum value must be above 0.") + if max_value <= min_value: + raise UserWarning("The upper bound must be larger than the lower bound.") + values = np.arange(num_values + 1) + diff = (max_value - min_value) / (num_values - 1) + values = (np.log(min_value + values * diff) - np.log(min_value)) \ + / (np.log(max_value) - np.log(min_value)) + cmap = LinearSegmentedColormap.from_list("name", list(zip(values, colors))) + norm = LogNorm(min_value, max_value) + return cmap, norm diff --git a/src/pandapipes/plotting/mapbox_plot.py b/src/pandapipes/plotting/mapbox_plot.py new file mode 100644 index 000000000..001ee17fc --- /dev/null +++ b/src/pandapipes/plotting/mapbox_plot.py @@ -0,0 +1,491 @@ +""" +mapbox_plot.py + +This module adds Mapbox‐based (Plotly) plotting functionality to pandapipes. +It provides helper functions for setting/retrieving the Mapbox token and a +main function to create an interactive Plotly figure that plots all network +components on a real map. + +Inspired by the pandapower implementation, it supports: + • Junctions (from net.junction_geodata) + • Pipes – if net.pipe_geodata exists, its “coords” are used; + otherwise, the from/to junctions (via net.pipe) are used. + • External Grids, Sources, and Sinks – using the junction reference. + • Valves – if a dedicated “junction” column exists use it; otherwise compute + the midpoint from “from_junction” and “to_junction”. + • Pumps – drawn as lines between from/to junctions. + • Line‐type components (Heat exchangers, Press controls, Compressors, + Flow controls, Heat consumers) – drawn as lines using “from_junction” and “to_junction”. + +New functionality: + • Optionally color pipes based on (e.g.) temperature values (from net.res_pipe) + and display a colorbar. +""" +from pandapipes.plotting import colormaps + +import os +import plotly.graph_objects as go +import matplotlib.colors as mcolors + +# Module‐level variable to hold a user‐defined Mapbox access token. +_MAPBOX_TOKEN = None + + +def set_mapbox_token(token): + """ + Save the user's Mapbox API token for authenticated access. + + Parameters + ---------- + token : str + A valid Mapbox access token. + """ + global _MAPBOX_TOKEN + _MAPBOX_TOKEN = token + + +def _get_mapbox_token(): + """ + Retrieve the stored Mapbox token. If none has been set, + try reading from the environment variable MAPBOX_ACCESS_TOKEN. + + Returns + ------- + token : str + The Mapbox token (or an empty string if none is available). + """ + global _MAPBOX_TOKEN + if _MAPBOX_TOKEN is None: + _MAPBOX_TOKEN = os.getenv("MAPBOX_ACCESS_TOKEN", "") + return _MAPBOX_TOKEN + + +def _on_map_test(x, y): + """ + Test whether given coordinates (x, y) are plausible geographic coordinates. + (Assuming x is longitude and y is latitude.) + + Parameters + ---------- + x : float + Longitude. + y : float + Latitude. + + Returns + ------- + bool + True if x is between -180 and 180 and y between -90 and 90. + """ + return (-180 <= x <= 180) and (-90 <= y <= 90) + + +def create_mapbox_figure(net, mapbox_access_token=None, map_style="streets", zoom=10, + pipe_temperature_coloring=False, pipe_temperature_field="t_to_k", + pipe_color=None, show_colorbar=False, + pipe_temperature_colorscale=[[0, "blue"], [1, "red"]], + renderer='browser'): # Keep parameter for documentation + """ + Create an interactive Plotly Mapbox figure for a pandapipes network. + + The function gathers geodata from all network components. For components that + do not have dedicated geodata (e.g. sinks, sources, heat exchangers, valves, pumps, + compressors), the referenced junction geodata (or midpoints between "from" and "to" nodes) + are used. + + Parameters + ---------- + net : pandapipes network + The network object. It is expected that: + - net.junction_geodata is a DataFrame containing the junction coordinates. + - net.pipe_geodata is optional (if provided, each row should have a "coords" column, + a list of coordinate pairs). + - Other component tables (net.pipe, net.ext_grid, net.source, net.sink, net.valve, + net.pump, net.heat_exchanger, net.press_control, net.compressor, net.flow_control, + net.heat_consumer) are available. + mapbox_access_token : str, optional + A valid Mapbox access token. If None, _get_mapbox_token() is used. + map_style : str, optional + The Mapbox style (e.g. "streets", "light", "dark", "satellite"). Default is "streets". + zoom : int, optional + The initial zoom level. Default is 10. + + Additional parameters for pipe temperature-based coloring: + pipe_temperature_coloring : bool, optional + If True, pipes are colored based on the temperature value taken from net.res_pipe. + (Default: False) + pipe_temperature_field : str, optional + The field name in net.res_pipe to use for temperature (default: "t_to_k"). + pipe_color : list or str, optional + If provided (and if pipe_temperature_coloring is False), these colors are used for pipes. + If a list, it is assumed the ordering corresponds to the pipe indices. + show_colorbar : bool, optional + If True (and pipe_temperature_coloring is True), a colorbar is added to the figure. + pipe_temperature_colorscale : list, optional + The colorscale to use for the colorbar (default: [[0, "blue"], [1, "red"]]). + renderer : str, optional + The Plotly renderer to use for display (default: 'browser'). Options include + 'browser', 'notebook', 'json', etc. + + Returns + ------- + fig : plotly.graph_objects.Figure + The interactive Plotly figure with the network overlaid on a Mapbox basemap. + """ + token = mapbox_access_token if mapbox_access_token is not None else _get_mapbox_token() + if not token: + print("Warning: No Mapbox access token provided. The map may not render correctly.") + + traces = [] + center = {"lat": 0, "lon": 0} + + # --- Junctions --- + if hasattr(net, "junction_geodata") and net.junction_geodata is not None and not net.junction_geodata.empty: + df = net.junction_geodata + if "lat" in df.columns and "lon" in df.columns: + lat = df["lat"] + lon = df["lon"] + elif "y" in df.columns and "x" in df.columns: + lat = df["y"] + lon = df["x"] + else: + raise ValueError("Junction geodata must have columns 'lat'/'lon' or 'x'/'y'.") + center = {"lat": float(lat.mean()), "lon": float(lon.mean())} + traces.append(go.Scattermapbox( + lon=lon, + lat=lat, + mode="markers", + marker=dict(size=8, color="red"), + text=["Junction {}".format(i) for i in df.index], + name="Junctions" + )) + else: + print("Warning: No junction geodata available.") + + # --- Pipes --- + # If temperature-based coloring is enabled and pipe results are available, + # compute the min/max and a colormap. + if pipe_temperature_coloring: + if hasattr(net, "res_pipe") and net.res_pipe is not None and not net.res_pipe.empty: + pipe_temps = net.res_pipe[pipe_temperature_field].values + min_temp = float(pipe_temps.min()) + max_temp = float(pipe_temps.max()) + from pandapipes.plotting import colormaps + cmap, norm = colormaps.cmap_continuous([(min_temp, "blue"), (max_temp, "red")]) + else: + print("Warning: pipe_temperature_coloring is enabled but no pipe results are available. Defaulting to gray.") + cmap = None + norm = None + min_temp = None + max_temp = None + else: + cmap = None + norm = None + min_temp = None + max_temp = None + + # Prefer pipe_geodata if available + if hasattr(net, "pipe_geodata") and net.pipe_geodata is not None and not net.pipe_geodata.empty: + for idx, row in net.pipe_geodata.iterrows(): + coords = row.get("coords") + if coords is None: + continue + if isinstance(coords, list) and len(coords) > 0: + # Assume each coordinate pair is (x, y) which represents (lon, lat) + lon_list, lat_list = zip(*coords) + # Determine color for this pipe trace + if pipe_temperature_coloring: + if cmap is not None: + try: + temp = net.res_pipe.loc[idx, pipe_temperature_field] + except Exception: + temp = min_temp + color = mcolors.rgb2hex(cmap(norm(temp))) + else: + color = "gray" + elif pipe_color is not None: + if isinstance(pipe_color, (list, tuple)): + color = pipe_color[idx] if idx < len(pipe_color) else "gray" + else: + color = pipe_color + else: + color = "gray" + traces.append(go.Scattermapbox( + lon=lon_list, + lat=lat_list, + mode="lines", + line=dict(width=3, color=color), + name="Pipe {}".format(idx) + )) + elif hasattr(net, "pipe") and net.pipe is not None and not net.pipe.empty: + # Fallback: use junction_geodata via from/to indices + for idx, row in net.pipe.iterrows(): + from_idx = row["from_junction"] + to_idx = row["to_junction"] + try: + if "lat" in net.junction_geodata.columns and "lon" in net.junction_geodata.columns: + lat0 = net.junction_geodata.loc[from_idx, "lat"] + lon0 = net.junction_geodata.loc[from_idx, "lon"] + lat1 = net.junction_geodata.loc[to_idx, "lat"] + lon1 = net.junction_geodata.loc[to_idx, "lon"] + elif "y" in net.junction_geodata.columns and "x" in net.junction_geodata.columns: + lat0 = net.junction_geodata.loc[from_idx, "y"] + lon0 = net.junction_geodata.loc[from_idx, "x"] + lat1 = net.junction_geodata.loc[to_idx, "y"] + lon1 = net.junction_geodata.loc[to_idx, "x"] + else: + continue + except Exception: + continue + if pipe_temperature_coloring: + if cmap is not None: + try: + temp = net.res_pipe.loc[idx, pipe_temperature_field] + except Exception: + temp = min_temp + color = mcolors.rgb2hex(cmap(norm(temp))) + else: + color = "gray" + elif pipe_color is not None: + if isinstance(pipe_color, (list, tuple)): + color = pipe_color[idx] if idx < len(pipe_color) else "gray" + else: + color = pipe_color + else: + color = "gray" + traces.append(go.Scattermapbox( + lon=[lon0, lon1], + lat=[lat0, lat1], + mode="lines", + line=dict(width=3, color=color), + name="Pipe {}".format(idx) + )) + + # --- External Grids --- + if hasattr(net, "ext_grid") and net.ext_grid is not None and not net.ext_grid.empty: + eg_indices = net.ext_grid.junction.values + try: + coords = net.junction_geodata.loc[eg_indices] + except Exception: + coords = None + if coords is not None and not coords.empty: + if "lat" in coords.columns and "lon" in coords.columns: + lat = coords["lat"] + lon = coords["lon"] + elif "y" in coords.columns and "x" in coords.columns: + lat = coords["y"] + lon = coords["x"] + else: + lat, lon = [], [] + traces.append(go.Scattermapbox( + lon=lon, + lat=lat, + mode="markers", + marker=dict(size=10, color="orange"), + text=["Ext Grid {}".format(i) for i in coords.index], + name="External Grids" + )) + + # --- Sources --- + if hasattr(net, "source") and net.source is not None and not net.source.empty: + src_indices = net.source.junction.values + try: + coords = net.junction_geodata.loc[src_indices] + except Exception: + coords = None + if coords is not None and not coords.empty: + if "lat" in coords.columns and "lon" in coords.columns: + lat = coords["lat"] + lon = coords["lon"] + elif "y" in coords.columns and "x" in coords.columns: + lat = coords["y"] + lon = coords["x"] + else: + lat, lon = [], [] + traces.append(go.Scattermapbox( + lon=lon, + lat=lat, + mode="markers", + marker=dict(size=10, color="blue"), + text=["Source {}".format(i) for i in coords.index], + name="Sources" + )) + + # --- Sinks --- + if hasattr(net, "sink") and net.sink is not None and not net.sink.empty: + sink_indices = net.sink.junction.values + try: + coords = net.junction_geodata.loc[sink_indices] + except Exception: + coords = None + if coords is not None and not coords.empty: + if "lat" in coords.columns and "lon" in coords.columns: + lat = coords["lat"] + lon = coords["lon"] + elif "y" in coords.columns and "x" in coords.columns: + lat = coords["y"] + lon = coords["x"] + else: + lat, lon = [], [] + traces.append(go.Scattermapbox( + lon=lon, + lat=lat, + mode="markers", + marker=dict(size=10, color="green"), + text=["Sink {}".format(i) for i in coords.index], + name="Sinks" + )) + + # --- Valves --- + if hasattr(net, "valve") and net.valve is not None and not net.valve.empty: + if "junction" in net.valve.columns: + valve_indices = net.valve.junction.values + try: + coords = net.junction_geodata.loc[valve_indices] + except Exception: + coords = None + if coords is not None and not coords.empty: + if "lat" in coords.columns and "lon" in coords.columns: + lat = coords["lat"] + lon = coords["lon"] + elif "y" in coords.columns and "x" in coords.columns: + lat = coords["y"] + lon = coords["x"] + else: + lat, lon = [], [] + traces.append(go.Scattermapbox( + lon=lon, + lat=lat, + mode="markers", + marker=dict(size=8, color="cyan"), + text=["Valve {}".format(i) for i in coords.index], + name="Valves" + )) + elif "from_junction" in net.valve.columns and "to_junction" in net.valve.columns: + for idx, row in net.valve.iterrows(): + from_idx = row["from_junction"] + to_idx = row["to_junction"] + try: + if "lat" in net.junction_geodata.columns and "lon" in net.junction_geodata.columns: + lat0 = net.junction_geodata.loc[from_idx, "lat"] + lon0 = net.junction_geodata.loc[from_idx, "lon"] + lat1 = net.junction_geodata.loc[to_idx, "lat"] + lon1 = net.junction_geodata.loc[to_idx, "lon"] + elif "y" in net.junction_geodata.columns and "x" in net.junction_geodata.columns: + lat0 = net.junction_geodata.loc[from_idx, "y"] + lon0 = net.junction_geodata.loc[from_idx, "x"] + lat1 = net.junction_geodata.loc[to_idx, "y"] + lon1 = net.junction_geodata.loc[to_idx, "x"] + else: + continue + except Exception: + continue + mid_lat = (lat0 + lat1) / 2.0 + mid_lon = (lon0 + lon1) / 2.0 + traces.append(go.Scattermapbox( + lon=[mid_lon], + lat=[mid_lat], + mode="markers", + marker=dict(size=8, color="cyan"), + text="Valve {}".format(idx), + name="Valves" + )) + + # --- Pumps --- + if hasattr(net, "pump") and net.pump is not None and not net.pump.empty: + for idx, row in net.pump.iterrows(): + from_idx = row["from_junction"] + to_idx = row["to_junction"] + try: + if "lat" in net.junction_geodata.columns and "lon" in net.junction_geodata.columns: + lat0 = net.junction_geodata.loc[from_idx, "lat"] + lon0 = net.junction_geodata.loc[from_idx, "lon"] + lat1 = net.junction_geodata.loc[to_idx, "lat"] + lon1 = net.junction_geodata.loc[to_idx, "lon"] + elif "y" in net.junction_geodata.columns and "x" in net.junction_geodata.columns: + lat0 = net.junction_geodata.loc[from_idx, "y"] + lon0 = net.junction_geodata.loc[from_idx, "x"] + lat1 = net.junction_geodata.loc[to_idx, "y"] + lon1 = net.junction_geodata.loc[to_idx, "x"] + else: + continue + except Exception: + continue + traces.append(go.Scattermapbox( + lon=[lon0, lon1], + lat=[lat0, lat1], + mode="lines", + line=dict(width=3, color="purple", dash="dot"), + name="Pump {}".format(idx) + )) + + # --- Other line-type components --- + def add_line_component(component_name, color): + if hasattr(net, component_name) and getattr(net, component_name) is not None and not getattr(net, component_name).empty: + comp = getattr(net, component_name) + for idx, row in comp.iterrows(): + if "from_junction" in row and "to_junction" in row: + from_idx = row["from_junction"] + to_idx = row["to_junction"] + try: + if "lat" in net.junction_geodata.columns and "lon" in net.junction_geodata.columns: + lat0 = net.junction_geodata.loc[from_idx, "lat"] + lon0 = net.junction_geodata.loc[from_idx, "lon"] + lat1 = net.junction_geodata.loc[to_idx, "lat"] + lon1 = net.junction_geodata.loc[to_idx, "lon"] + elif "y" in net.junction_geodata.columns and "x" in net.junction_geodata.columns: + lat0 = net.junction_geodata.loc[from_idx, "y"] + lon0 = net.junction_geodata.loc[from_idx, "x"] + lat1 = net.junction_geodata.loc[to_idx, "y"] + lon1 = net.junction_geodata.loc[to_idx, "x"] + else: + continue + except Exception: + continue + traces.append(go.Scattermapbox( + lon=[lon0, lon1], + lat=[lat0, lat1], + mode="lines", + line=dict(width=3, color=color), + name="{} {}".format(component_name.capitalize(), idx) + )) + add_line_component("heat_exchanger", color="brown") + add_line_component("press_control", color="pink") + add_line_component("compressor", color="black") + add_line_component("flow_control", color="orange") + add_line_component("heat_consumer", color="yellow") + + if pipe_temperature_coloring and show_colorbar and min_temp is not None and max_temp is not None: + traces.append(go.Scattermapbox( + lon=[center["lon"]], + lat=[center["lat"]], + mode="markers", + marker=dict( + size=0, + color=[min_temp], # Dummy value + colorscale=pipe_temperature_colorscale, + cmin=min_temp, + cmax=max_temp, + colorbar=dict( + title="Pipe Temperature (K)" + + ), + showscale=True + ), + showlegend=False, + hoverinfo="none" + )) + + layout = go.Layout( + mapbox=dict( + accesstoken=token, + style=map_style, + center=center, + zoom=zoom + ), + margin={"l": 0, "r": 0, "t": 0, "b": 0}, + showlegend=True + ) + + fig = go.Figure(data=traces, layout=layout) + return fig \ No newline at end of file diff --git a/src/pandapipes/plotting/simple_plot.py b/src/pandapipes/plotting/simple_plot.py index e51c0f3da..5c658c66f 100644 --- a/src/pandapipes/plotting/simple_plot.py +++ b/src/pandapipes/plotting/simple_plot.py @@ -3,7 +3,7 @@ # Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. from itertools import chain - +import plotly.io as pio import matplotlib.pyplot as plt from pandapower.plotting import draw_collections @@ -32,84 +32,61 @@ def simple_plot(net, respect_valves=False, respect_in_service=True, pipe_width=2 heat_consumer_size=1.0, scale_size=True, junction_color="r", pipe_color='silver', ext_grid_color='orange', valve_color='silver', pump_color='silver', heat_exchanger_color='silver', pressure_control_color='silver', compressor_color='silver', flow_control_color='silver', - heat_consumer_color='silver',library="igraph", show_plot=True, ax=None, **kwargs): + heat_consumer_color='silver', library="igraph", show_plot=True, ax=None, + use_mapbox=False, mapbox_access_token=None, map_style="streets", zoom=10, + # New parameters for pipe temperature-based coloring: + pipe_temperature_coloring=False, pipe_temperature_field="t_to_k", + show_colorbar=False, pipe_temperature_colorscale=[[0, "blue"], [1, "red"]], + renderer='browser', # Add renderer parameter with browser default + **kwargs): """ Plots a pandapipes network as simple as possible. If no geodata is available, artificial - geodata is generated. For advanced plotting see - the `tutorial `_. - - :param net: The pandapipes format network. - :type net: pandapipesNet - :param respect_valves: Respect valves if artificial geodata is created. \ - Note: This Flag is ignored if plot_line_switches is True - :type respect_valves: bool default False - :param respect_in_service: Respect only components which are in service. - :type respect_in_service: bool default True - :param pipe_width: Width of pipes - :type pipe_width: float, default 5.0 - :param junction_size: Relative size of junctions to plot. The value junction_size is multiplied\ - with mean_distance_between_buses, which equals the distance between the max geoocord\ - and the min divided by 200 - :type junction_size: float, default 1.0 - :param ext_grid_size: Relative size of ext_grids to plot. See bus sizes for details. Note: \ - ext_grids are plottet as rectangles - :type ext_grid_size: float, default 1.0 - :param plot_sinks: Flag to decide whether sink symbols should be drawn. - :type plot_sinks: bool, default False - :param plot_sources: Flag to decide whether source symbols should be drawn. - :type plot_sources: bool, default False - :param sink_size: Relative size of sinks to plot. - :type sink_size: float, default 1.0 - :param source_size: Relative size of sources to plot. - :type source_size: float, default 1.0 - :param valve_size: Relative size of valves to plot. - :type valve_size: float, default 1.0 - :param pump_size: Relative size of pumps to plot. - :type pump_size: float, default 1.0 - :param heat_exchanger_size: Relative size of heat_exchanger to plot. - :type heat_exchanger_size: float, default 1.0 - :param pressure_control_size: Relative size of pres_control to plot. - :type pressure_control_size: float, default 1.0 - :param compressor_size: Relative size of compressor to plot. - :type compressor_size: float, default 1.0 - :param flow_control_size: Relative size of flow_control to plot. - :type flow_control_size: float, default 1.0 - :param heat_consumer_size: Relative size of heat_consumer to plot. - :type heat_consumer_size: float, default 1.0 - :param scale_size: Flag if junction_size, ext_grid_size, valve_size- and distance will be \ - scaled with respect to grid mean distances - :type scale_size: bool, default True - :param junction_color: Junction Color. See also matplotlib or seaborn documentation on how to\ - choose colors. - :type junction_color: str, tuple, default "r" - :param pipe_color: Pipe color - :type pipe_color: str, tuple, default "silver" - :param ext_grid_color: External grid color - :type ext_grid_color: str, tuple, default "orange" - :param valve_color: Valve Color. - :type valve_color: str, tuple, default "silver" - :param pump_color: Pump Color. - :type pump_color: str, tuple, default "silver" - :param heat_exchanger_color: Heat Exchanger Color. - :type heat_exchanger_color: str, tuple, default "silver" - :param pressure_control_color: Pressure Control Color. - :type pressure_control_color: str, tuple, default "silver" - :param compressor_color: Compressor Color. - :type compressor_color: str, tuple, default "silver" - :param flow_control_color: Flow Control Color. - :type flow_control_color: str, tuple, default "silver" - :param heat_consumer_color: heat_consumer Color. - :type heat_consumer_color: str, tuple, default "silver" - :param library: Library name to create generic coordinates (case of missing geodata). Choose\ - "igraph" to use igraph package or "networkx" to use networkx package. - :type library: str, default "igraph" - :param show_plot: If True, show plot at the end of plotting - :type show_plot: bool, default True - :param ax: matplotlib axis to plot to - :type ax: object, default None - :return: ax - Axes of figure - + geodata is generated. For advanced plotting see the tutorial. + + Additional parameters: + - use_mapbox : bool + If True, plot the network on an interactive Mapbox map using Plotly. + - mapbox_access_token : str + Mapbox token to use. If None, the environment variable or stored token is used. + - map_style : str + Mapbox style (e.g. "streets", "light", "dark", "satellite"). + - zoom : int + Initial zoom level for the Mapbox plot. + - pipe_temperature_coloring : bool + If True, pipes are colored based on the temperature values in net.res_pipe. + - pipe_temperature_field : str + The field in net.res_pipe to use for temperature (default: "t_to_k"). + - show_colorbar : bool + If True and pipe_temperature_coloring is enabled, a colorbar is shown. + - pipe_temperature_colorscale : list + The colorscale for the colorbar (default: [[0, "blue"], [1, "red"]]). + - renderer : str + The Plotly renderer to use for display (default: 'browser'). Options include + 'browser', 'notebook', 'json', etc. + + All other parameters remain as in the original implementation. """ + if use_mapbox: + from pandapipes.plotting import mapbox_plot + fig = mapbox_plot.create_mapbox_figure( + net, + mapbox_access_token=mapbox_access_token, + map_style=map_style, + zoom=zoom, + pipe_temperature_coloring=pipe_temperature_coloring, + pipe_temperature_field=pipe_temperature_field, + pipe_color=pipe_color, + show_colorbar=show_colorbar, + pipe_temperature_colorscale=pipe_temperature_colorscale, + renderer=renderer # Pass renderer to mapbox_plot + ) + if show_plot: + + pio.renderers.default = renderer # Set the renderer + fig.show() # Show with the specified renderer + return fig + + # --- Original matplotlib-based plotting --- collections = create_simple_collections(net, respect_valves=respect_valves, respect_in_service=respect_in_service, @@ -141,7 +118,7 @@ def simple_plot(net, respect_valves=False, respect_in_service=True, pipe_width=2 library=library, as_dict=False, **kwargs) ax = draw_collections(collections, ax=ax) - + if show_plot: plt.show() return ax diff --git a/src/pandapipes/test/plotting/test_mapbox.py b/src/pandapipes/test/plotting/test_mapbox.py new file mode 100644 index 000000000..58e1098bf --- /dev/null +++ b/src/pandapipes/test/plotting/test_mapbox.py @@ -0,0 +1,153 @@ +# test_mapbox_plot.py + +import copy +import pandas as pd +import pytest +import pandapipes as pp +import plotly.graph_objects as go + +from pandapipes.plotting.mapbox_plot import ( + set_mapbox_token, + _get_mapbox_token, + _on_map_test, + create_mapbox_figure +) +from pandapipes.plotting.simple_plot import simple_plot + +def create_test_network(): + """ + Build the "2-Consumer Loop" network with geodata, pipes, HX, ext_grid, and a pump. + """ + # Coordinates + coords_main = [ + (13.377704, 52.509669), # j0: Plant + (13.380000, 52.509700), # j1 + (13.382000, 52.509720), # j2 + (13.384000, 52.509740), # j3 + ] + coords_h = { + 'h1_feed': (13.380500, 52.510500), + 'h1_return': (13.380300, 52.510300), + 'h2_feed': (13.382500, 52.510500), + 'h2_return': (13.382300, 52.510300), + } + + # Build network + net = pp.create_empty_network(fluid='water', name='2-Consumer Loop') + main_j = [pp.create_junction(net, pn_bar=3, tfluid_k=353.15) for _ in coords_main] + j_feed1 = pp.create_junction(net, pn_bar=3, tfluid_k=353.15) + j_ret1 = pp.create_junction(net, pn_bar=3, tfluid_k=353.15) + j_feed2 = pp.create_junction(net, pn_bar=3, tfluid_k=353.15) + j_ret2 = pp.create_junction(net, pn_bar=3, tfluid_k=353.15) + + # Assign geodata + all_coords = coords_main + list(coords_h.values()) + all_juncs = main_j + [j_feed1, j_ret1, j_feed2, j_ret2] + net.junction_geodata = pd.DataFrame({ + 'lon': [lon for lon, lat in all_coords], + 'lat': [lat for lon, lat in all_coords], + }, index=all_juncs) + + # Pipes + for i in range(len(main_j)-1): + pp.create_pipe_from_parameters(net, + main_j[i], main_j[i+1], length_km=0.1, diameter_m=0.1, + k_mm=0.02, alpha_w_per_m2k=10, text_k=293.15, + name=f'loop_{i}' + ) + pp.create_pipe_from_parameters(net, main_j[1], j_feed1, 0.02, 0.05, + k_mm=0.02, alpha_w_per_m2k=10, text_k=293.15, name='h1_supp' + ) + pp.create_pipe_from_parameters(net, j_ret1, main_j[2], 0.02, 0.05, + k_mm=0.02, alpha_w_per_m2k=10, text_k=293.15, name='h1_ret' + ) + pp.create_pipe_from_parameters(net, main_j[2], j_feed2, 0.02, 0.05, + k_mm=0.02, alpha_w_per_m2k=10, text_k=293.15, name='h2_supp' + ) + pp.create_pipe_from_parameters(net, j_ret2, main_j[3], 0.02, 0.05, + k_mm=0.02, alpha_w_per_m2k=10, text_k=293.15, name='h2_ret' + ) + + # Heat exchangers + pp.create_heat_exchanger(net, from_junction=j_feed1, to_junction=j_ret1, + qext_w=15e3, diameter_m=0.05, + length_km=0.005, sections=2, name='HX1') + pp.create_heat_exchanger(net, from_junction=j_feed2, to_junction=j_ret2, + qext_w=25e3, diameter_m=0.05, + length_km=0.005, sections=2, name='HX2') + + # External grid and pump + pp.create_ext_grid(net, junction=main_j[0], p_bar=3.0, t_k=353.15, name='Plant') + pp.create_circ_pump_const_mass_flow(net, + return_junction=main_j[-1], flow_junction=main_j[0], + mdot_flow_kg_per_s=0.1, p_flow_bar=3.5, t_flow_k=353.15, + name='LoopPump' + ) + + # Run a pipeflow to generate res_pipe + pp.pipeflow(net, mode='sequential', friction_model='swamee-jain') + return net + +def test_set_and_get_token(): + token = "test-token-123" + set_mapbox_token(token) + assert _get_mapbox_token() == token + +def test_on_map_test_behavior(): + # valid bounds + assert _on_map_test(0, 0) + assert _on_map_test(180, 90) + assert _on_map_test(-180, -90) + # out of bounds + assert not _on_map_test(200, 0) + assert not _on_map_test(0, -100) + +def test_create_mapbox_figure_basic(): + net = create_test_network() + fig = create_mapbox_figure(net, mapbox_access_token="dummy-token") + assert isinstance(fig, go.Figure) + # must contain at least the junctions layer + names = [trace.name for trace in fig.data] + assert "Junctions" in names + +def test_pipe_color_override(): + net = create_test_network() + fig = create_mapbox_figure(net, mapbox_access_token="dummy", pipe_color="purple") + # all pipe traces should use the overridden 'purple' color + pipe_traces = [t for t in fig.data if t.name.startswith("Pipe")] + assert pipe_traces, "No pipe traces found" + for t in pipe_traces: + assert t.line.color == "purple" + +def test_temperature_coloring_and_colorbar(): + net = create_test_network() + # override res_pipe to guarantee a t_to_k field + net.res_pipe = net.pipe.copy() + net.res_pipe["t_to_k"] = 300 + net.res_pipe.index + fig = create_mapbox_figure( + net, + mapbox_access_token="dummy", + pipe_temperature_coloring=True, + show_colorbar=True + ) + # exactly one trace should carry the colorbar + cb_traces = [ + t for t in fig.data + if hasattr(t.marker, "showscale") and t.marker.showscale + ] + assert len(cb_traces) == 1 + +def test_simple_plot_returns_plotly_figure(monkeypatch): + net = create_test_network() + # ensure token comes from env if not passed explicitly + monkeypatch.setenv("MAPBOX_ACCESS_TOKEN", "env-token") + fig = simple_plot( + net, + use_mapbox=True, + mapbox_access_token=None, + show_plot=False + ) + assert isinstance(fig, go.Figure) + +if __name__ == '__main__': + pytest.main(["test_mapbox.py"]) diff --git a/tutorials/circular_flow_in_a_district_heating_grid.ipynb b/tutorials/circular_flow_in_a_district_heating_grid.ipynb index 0a57948ea..e3cf5fac6 100644 --- a/tutorials/circular_flow_in_a_district_heating_grid.ipynb +++ b/tutorials/circular_flow_in_a_district_heating_grid.ipynb @@ -10,11 +10,11 @@ "\n", "\n", "\n", - "In this example, we will not only calculate the pressure and velocity distribution in the network, but also determine the temperature levels. The pump feeds fluid of a given temperature into the grid. Due to losses, the temperature will fall. The heat exchanger removes more heat from the network. On its way back to the pump, the temperature will fall further. \n", + "In this example, we will not only calculate the pressure and velocity distribution in the network, but also determine the temperature levels. The pump feeds fluid of a given temperature into the grid. Due to losses, the temperature will fall. The heat exchanger removes more heat from the network. On its way back to the pump, the temperature will fall further.\n", "\n", "The network is based on the topology of a district heating grid, where the fluid returns to the pump after the consumers (heat exchangers) have been supplied.\n", "\n", - "To set up this network, at first, the pandapipes package has to be imported. Additionally, a net container is created and, at the same time, water as a fluid is chosen." + "To set up this network, at first, the pandapipes package has to be imported. Additionally, a net container is created and, at the same time, water as a fluid is chosen.\n" ] }, { @@ -37,7 +37,7 @@ "The parameters `pn_bar` and `tfluid_k` that have to be set in the `create_junction`-function\n", "are\n", "only used as starting points for the network simulation. The fix pressure and fluid temperature is\n", - "being determined by the circular pump component which will be created afterwards." + "being determined by the circular pump component which will be created afterwards.\n" ] }, { @@ -62,7 +62,7 @@ "connected to the junction specified via the to_junction-parameter.\n", "\n", "However, the internal structure is not visible to the user, so that the circular pump component\n", - "supplies a fluid flow with the specified properties." + "supplies a fluid flow with the specified properties.\n" ] }, { @@ -83,7 +83,7 @@ "The most important parameter for this component is the heat flux `qext_w`. A positive value of\n", "`qext_w` means that heat is withdrawn from the network and supplied to a consumer.\n", "A negative value of `qext_w` corresponds to a heat source, i. e. thermal energy is being transfered\n", - "from the heat exchanger into the network." + "from the heat exchanger into the network.\n" ] }, { @@ -102,7 +102,7 @@ "The following commands defines the pipes between the components. Each pipe will consist of five\n", "internal sections in order to improve the spatial resolution for the temperature calculation.\n", "The parameter `text_k` specifies the ambient temperature on the outside of the pipe. It is used to\n", - "calculate energy losses." + "calculate energy losses.\n" ] }, { @@ -121,7 +121,6 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "\n", "We now run a pipe flow.\n" ] }, @@ -141,7 +140,7 @@ "By default, only the pressure and velocity distribution is calculated by the pipeflow function. If\n", "the `mode`-parameter is set to \"all\", the heat transfer calculation is started automatically\n", "after the hydraulics computation. Computed mass flows are used as an input for the temperature\n", - "calculation. After the computation, you can check the results for junctions and pipes:" + "calculation. After the computation, you can check the results for junctions and pipes:\n" ] }, { @@ -162,7 +161,7 @@ "the pipe components. This also means that - if the extracted heat flow is large enough - the\n", "temperature level behind the heat exchanger might be lower than the ambient temperature level. A\n", "way to avoid this behaviour would be to create a controller which defines a function for the\n", - "extracted heat in dependence of the ambient temperature." + "extracted heat in dependence of the ambient temperature.\n" ] }, { @@ -182,7 +181,7 @@ "junctions are displayed. Due to heat losses, the temperatures at the to-nodes is lower than the\n", "temperatures at the from-nodes. Note also that the junctions are not equal to the internal nodes,\n", "introduced by the pipe sections we defined. To display the temperatures at the internal nodes, we\n", - "can retrieve the internal node values with the following commands:" + "can retrieve the internal node values with the following commands:\n" ] }, { @@ -221,7 +220,7 @@ "source": [ "We can see that the pressure level falls due to friction. As the fluid is incompressible, the\n", "velocity remains constant over the pipe length. Because the temperature level at the pipe entry is\n", - "higher than the ambient temperature, the temperature level decreases." + "higher than the ambient temperature, the temperature level decreases.\n" ] } ], @@ -241,7 +240,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.16" + "version": "3.11.4" } }, "nbformat": 4, diff --git a/tutorials/creating_a_simple_network.ipynb b/tutorials/creating_a_simple_network.ipynb index 5057d4a13..ed1fa4e97 100644 --- a/tutorials/creating_a_simple_network.ipynb +++ b/tutorials/creating_a_simple_network.ipynb @@ -43,7 +43,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": { "scrolled": true }, @@ -54,7 +54,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -70,9 +70,34 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "This pandapipes network includes the following parameter tables:.\n", + "It contains the following fluid: \n", + "Fluid lgas (gas) with properties:\n", + " - density (InterExtra)\n", + " - viscosity (InterExtra)\n", + " - heat_capacity (InterExtra)\n", + " - molar_mass (Constant)\n", + " - der_compressibility (Constant)\n", + " - compressibility (Linear)\n", + " - lhv (Constant)\n", + " - hhv (Constant)\n", + "and uses the following component models:\n", + " - Junction\n", + " - Pipe\n", + " - ExtGrid" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "net" ] @@ -102,7 +127,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -123,9 +148,120 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
namepn_bartfluid_kheight_min_servicetype
0Connection to External Grid1.0293.150.0Truejunction
1Junction 21.0293.150.0Truejunction
2Junction 31.0293.150.0Truejunction
3Junction 41.0293.150.0Truejunction
4Junction 51.0293.150.0Truejunction
5Junction 61.0293.150.0Truejunction
\n", + "
" + ], + "text/plain": [ + " name pn_bar tfluid_k height_m in_service \\\n", + "0 Connection to External Grid 1.0 293.15 0.0 True \n", + "1 Junction 2 1.0 293.15 0.0 True \n", + "2 Junction 3 1.0 293.15 0.0 True \n", + "3 Junction 4 1.0 293.15 0.0 True \n", + "4 Junction 5 1.0 293.15 0.0 True \n", + "5 Junction 6 1.0 293.15 0.0 True \n", + "\n", + " type \n", + "0 junction \n", + "1 junction \n", + "2 junction \n", + "3 junction \n", + "4 junction \n", + "5 junction " + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "net.junction # show junction table" ] @@ -139,18 +275,46 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "np.int64(0)" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "junction1" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "name Connection to External Grid\n", + "pn_bar 1.0\n", + "tfluid_k 293.15\n", + "height_m 0.0\n", + "in_service True\n", + "type junction\n", + "Name: 0, dtype: object" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "net.junction.loc[junction1]" ] @@ -180,9 +344,62 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
namejunctionp_bart_kin_servicetype
0Grid Connection01.1293.15Truept
\n", + "
" + ], + "text/plain": [ + " name junction p_bar t_k in_service type\n", + "0 Grid Connection 0 1.1 293.15 True pt" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "medium_pressure_grid = pp.create_ext_grid(net, junction=junction1, p_bar=1.1, t_k=293.15, name=\"Grid Connection\")\n", "\n", @@ -209,7 +426,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -229,9 +446,157 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
namefrom_junctionto_junctionstd_typelength_kmdiameter_mk_mmloss_coefficientu_w_per_m2ktext_kqext_wsectionsin_servicetype
0Pipe 101None10.00.30.20.00.0NaN0.01Truepipe
1Pipe 212None2.00.30.20.00.0NaN0.01Truepipe
2Pipe 313None2.50.30.20.00.0NaN0.01Truepipe
3Pipe 424None1.00.30.20.00.0NaN0.01Truepipe
4Pipe 535None1.00.30.20.00.0NaN0.01Truepipe
\n", + "
" + ], + "text/plain": [ + " name from_junction to_junction std_type length_km diameter_m k_mm \\\n", + "0 Pipe 1 0 1 None 10.0 0.3 0.2 \n", + "1 Pipe 2 1 2 None 2.0 0.3 0.2 \n", + "2 Pipe 3 1 3 None 2.5 0.3 0.2 \n", + "3 Pipe 4 2 4 None 1.0 0.3 0.2 \n", + "4 Pipe 5 3 5 None 1.0 0.3 0.2 \n", + "\n", + " loss_coefficient u_w_per_m2k text_k qext_w sections in_service type \n", + "0 0.0 0.0 NaN 0.0 1 True pipe \n", + "1 0.0 0.0 NaN 0.0 1 True pipe \n", + "2 0.0 0.0 NaN 0.0 1 True pipe \n", + "3 0.0 0.0 NaN 0.0 1 True pipe \n", + "4 0.0 0.0 NaN 0.0 1 True pipe " + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "net.pipe # show pipe table" ] @@ -260,11 +625,13 @@ ] }, { - "metadata": {}, "cell_type": "code", + "execution_count": 11, + "metadata": {}, "outputs": [], - "execution_count": null, - "source": "valve = pp.create_valve(net, junction=junction5, element=junction6, et='ju', diameter_m=0.3, opened=True)" + "source": [ + "valve = pp.create_valve(net, junction=junction5, element=junction6, et='ju', diameter_m=0.3, opened=True)" + ] }, { "cell_type": "markdown", @@ -275,9 +642,66 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
namejunctionelementetdiameter_mopenedloss_coefficienttype
0None45ju0.3True0.0valve
\n", + "
" + ], + "text/plain": [ + " name junction element et diameter_m opened loss_coefficient type\n", + "0 None 4 5 ju 0.3 True 0.0 valve" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "net.valve # show valve table" ] @@ -300,9 +724,62 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
namejunctionmdot_kg_per_sscalingin_servicetype
0Sink 130.5451.0Truesink
\n", + "
" + ], + "text/plain": [ + " name junction mdot_kg_per_s scaling in_service type\n", + "0 Sink 1 3 0.545 1.0 True sink" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "sink = pp.create_sink(net, junction=junction4, mdot_kg_per_s=0.545, name=\"Sink 1\")\n", "net.sink" @@ -327,7 +804,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ @@ -336,9 +813,62 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
namejunctionmdot_kg_per_sscalingin_servicetype
0Source 120.2341.0Truesource
\n", + "
" + ], + "text/plain": [ + " name junction mdot_kg_per_s scaling in_service type\n", + "0 Source 1 2 0.234 1.0 True source" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "net.source" ] @@ -352,9 +882,43 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "This pandapipes network includes the following parameter tables:\n", + " - junction (6 elements)\n", + " - pipe (5 elements)\n", + " - ext_grid (1 elements)\n", + " - valve (1 elements)\n", + " - sink (1 elements)\n", + " - source (1 elements).\n", + "It contains the following fluid: \n", + "Fluid lgas (gas) with properties:\n", + " - density (InterExtra)\n", + " - viscosity (InterExtra)\n", + " - heat_capacity (InterExtra)\n", + " - molar_mass (Constant)\n", + " - der_compressibility (Constant)\n", + " - compressibility (Linear)\n", + " - lhv (Constant)\n", + " - hhv (Constant)\n", + "and uses the following component models:\n", + " - Junction\n", + " - Pipe\n", + " - ExtGrid\n", + " - Valve\n", + " - Sink\n", + " - Source" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "net" ] @@ -368,7 +932,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "metadata": {}, "outputs": [], "source": [ @@ -384,27 +948,304 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "This pandapipes network includes the following parameter tables:\n", + " - junction (6 elements)\n", + " - pipe (5 elements)\n", + " - ext_grid (1 elements)\n", + " - valve (1 elements)\n", + " - sink (1 elements)\n", + " - source (1 elements)\n", + "and the following results tables:\n", + " - res_junction (6 elements)\n", + " - res_pipe (5 elements)\n", + " - res_ext_grid (1 elements)\n", + " - res_valve (1 elements)\n", + " - res_sink (1 elements)\n", + " - res_source (1 elements).\n", + "It contains the following fluid: \n", + "Fluid lgas (gas) with properties:\n", + " - density (InterExtra)\n", + " - viscosity (InterExtra)\n", + " - heat_capacity (InterExtra)\n", + " - molar_mass (Constant)\n", + " - der_compressibility (Constant)\n", + " - compressibility (Linear)\n", + " - lhv (Constant)\n", + " - hhv (Constant)\n", + "and uses the following component models:\n", + " - Junction\n", + " - Pipe\n", + " - ExtGrid\n", + " - Valve\n", + " - Sink\n", + " - Source" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "net # result tables have been added to the net " ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
p_bart_k
01.100000293.15
11.061641293.15
21.061396293.15
31.054845293.15
41.058123293.15
51.058123293.15
\n", + "
" + ], + "text/plain": [ + " p_bar t_k\n", + "0 1.100000 293.15\n", + "1 1.061641 293.15\n", + "2 1.061396 293.15\n", + "3 1.054845 293.15\n", + "4 1.058123 293.15\n", + "5 1.058123 293.15" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "net.res_junction # calculated pressure and temperature at junctions" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
v_from_m_per_sv_to_m_per_sv_mean_m_per_sp_from_barp_to_bart_from_kt_to_kt_outlet_kmdot_from_kg_per_smdot_to_kg_per_svdot_norm_m3_per_sreynoldslambdanormfactor_fromnormfactor_to
02.8186612.8710132.8445181.1000001.061641293.15293.15293.150.311000-0.3110000.388996111395.9550820.0183890.5121890.521703
10.4746500.4747060.4746781.0616411.061396293.15293.15293.150.051416-0.0514160.06431118416.6539500.0212900.5217030.521764
22.3963642.4042742.4003101.0616411.054845293.15293.15293.150.259584-0.2595840.32468592979.3011320.0185030.5217030.523425
32.6351462.6393292.6372351.0613961.058123293.15293.15293.150.285416-0.2854160.356995102232.2600240.0184410.5217640.522593
4-2.643532-2.639329-2.6414281.0548451.058123293.15293.15293.15-0.2854160.285416-0.356995102232.2600240.0184410.5234250.522593
\n", + "
" + ], + "text/plain": [ + " v_from_m_per_s v_to_m_per_s v_mean_m_per_s p_from_bar p_to_bar \\\n", + "0 2.818661 2.871013 2.844518 1.100000 1.061641 \n", + "1 0.474650 0.474706 0.474678 1.061641 1.061396 \n", + "2 2.396364 2.404274 2.400310 1.061641 1.054845 \n", + "3 2.635146 2.639329 2.637235 1.061396 1.058123 \n", + "4 -2.643532 -2.639329 -2.641428 1.054845 1.058123 \n", + "\n", + " t_from_k t_to_k t_outlet_k mdot_from_kg_per_s mdot_to_kg_per_s \\\n", + "0 293.15 293.15 293.15 0.311000 -0.311000 \n", + "1 293.15 293.15 293.15 0.051416 -0.051416 \n", + "2 293.15 293.15 293.15 0.259584 -0.259584 \n", + "3 293.15 293.15 293.15 0.285416 -0.285416 \n", + "4 293.15 293.15 293.15 -0.285416 0.285416 \n", + "\n", + " vdot_norm_m3_per_s reynolds lambda normfactor_from normfactor_to \n", + "0 0.388996 111395.955082 0.018389 0.512189 0.521703 \n", + "1 0.064311 18416.653950 0.021290 0.521703 0.521764 \n", + "2 0.324685 92979.301132 0.018503 0.521703 0.523425 \n", + "3 0.356995 102232.260024 0.018441 0.521764 0.522593 \n", + "4 -0.356995 102232.260024 0.018441 0.523425 0.522593 " + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "net.res_pipe # velocities, mass flows through pipes and other results\n", "\n" @@ -427,7 +1268,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.3" + "version": "3.11.4" } }, "nbformat": 4, diff --git a/tutorials/mapbox_plotting.ipynb b/tutorials/mapbox_plotting.ipynb new file mode 100644 index 000000000..5fa6cede2 --- /dev/null +++ b/tutorials/mapbox_plotting.ipynb @@ -0,0 +1,207 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a84ab265", + "metadata": {}, + "source": [ + "# Pandapipes Mapbox Plotting Tutorial\n", + "\n", + "This notebook demonstrates how to use the Mapbox-based plotting functionality in pandapipes’s `simple_plot` function.\n" + ] + }, + { + "cell_type": "markdown", + "id": "a90b0888", + "metadata": {}, + "source": [ + "## 1. Setup\n", + "\n", + "Ensure you have set the `MAPBOX_ACCESS_TOKEN` environment variable before running these cells. You can also override it in code if needed.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6a0a694c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mapbox token is set.\n" + ] + } + ], + "source": [ + "import os\n", + "token = os.getenv('MAPBOX_ACCESS_TOKEN')\n", + "if not token:\n", + " print(\"Skipping Mapbox plotting: no MAPBOX_ACCESS_TOKEN found.\")\n", + " raise SystemExit\n", + "from pandapipes.plotting.mapbox_plot import set_mapbox_token\n", + "set_mapbox_token(token)\n", + "print('Mapbox token is set.')" + ] + }, + { + "cell_type": "markdown", + "id": "8d317f7d", + "metadata": {}, + "source": [ + "## 2. Create a Simple Network\n", + "\n", + "We’ll build a two-consumer loop network for demonstration.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "7c434b62", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "diameter_m is deprecated as it has no effect on the calculation and results. Nonetheless, it will be stored in the compoent table for postprocessing purposes by you if required.\n", + "diameter_m is deprecated as it has no effect on the calculation and results. Nonetheless, it will be stored in the compoent table for postprocessing purposes by you if required.\n" + ] + } + ], + "source": [ + "import pandapipes as pp\n", + "import pandas as pd\n", + "\n", + "# Coordinates\n", + "coords_main = [\n", + " (13.377704, 52.509669), # j0: Plant\n", + " (13.380000, 52.509700), # j1\n", + " (13.382000, 52.509720), # j2\n", + " (13.384000, 52.509740), # j3\n", + "]\n", + "coords_h = {\n", + " 'h1_feed': (13.380500, 52.510500),\n", + " 'h1_return': (13.380300, 52.510300),\n", + " 'h2_feed': (13.382500, 52.510500),\n", + " 'h2_return': (13.382300, 52.510300),\n", + "}\n", + "\n", + "# Build network\n", + "net = pp.create_empty_network(fluid='water', name='2-Consumer Loop')\n", + "main_j = [pp.create_junction(net, pn_bar=3, tfluid_k=353.15) for _ in coords_main]\n", + "j_feed1, j_ret1 = pp.create_junction(net, pn_bar=3, tfluid_k=353.15), pp.create_junction(net, pn_bar=3, tfluid_k=353.15)\n", + "j_feed2, j_ret2 = pp.create_junction(net, pn_bar=3, tfluid_k=353.15), pp.create_junction(net, pn_bar=3, tfluid_k=353.15)\n", + "\n", + "# Assign geodata\n", + "all_coords = coords_main + list(coords_h.values())\n", + "all_juncs = main_j + [j_feed1, j_ret1, j_feed2, j_ret2]\n", + "net.junction_geodata = pd.DataFrame({\n", + " 'lon': [lon for lon, lat in all_coords],\n", + " 'lat': [lat for lon, lat in all_coords],\n", + "}, index=all_juncs)\n", + "\n", + "# Pipes and devices\n", + "for i in range(len(main_j)-1):\n", + " pp.create_pipe_from_parameters(net, main_j[i], main_j[i+1], length_km=0.1, diameter_m=0.1,\n", + " k_mm=0.02, alpha_w_per_m2k=10, text_k=293.15, name=f'loop_{i}')\n", + "pp.create_pipe_from_parameters(net, main_j[1], j_feed1, 0.02, 0.05, k_mm=0.02, alpha_w_per_m2k=10, text_k=293.15, name='h1_supp')\n", + "pp.create_pipe_from_parameters(net, j_ret1, main_j[2], 0.02, 0.05, k_mm=0.02, alpha_w_per_m2k=10, text_k=293.15, name='h1_ret')\n", + "pp.create_pipe_from_parameters(net, main_j[2], j_feed2, 0.02, 0.05, k_mm=0.02, alpha_w_per_m2k=10, text_k=293.15, name='h2_supp')\n", + "pp.create_pipe_from_parameters(net, j_ret2, main_j[3], 0.02, 0.05, k_mm=0.02, alpha_w_per_m2k=10, text_k=293.15, name='h2_ret')\n", + "pp.create_heat_exchanger(net, from_junction=j_feed1, to_junction=j_ret1, qext_w=15e3, diameter_m=0.05,\n", + " length_km=0.005, sections=2, name='HX1')\n", + "pp.create_heat_exchanger(net, from_junction=j_feed2, to_junction=j_ret2, qext_w=25e3, diameter_m=0.05,\n", + " length_km=0.005, sections=2, name='HX2')\n", + "pp.create_ext_grid(net, junction=main_j[0], p_bar=3.0, t_k=353.15, name='Plant')\n", + "pp.create_circ_pump_const_mass_flow(net, return_junction=main_j[-1], flow_junction=main_j[0],\n", + " mdot_flow_kg_per_s=0.1, p_flow_bar=3.5, t_flow_k=353.15, name='LoopPump')\n", + "pp.pipeflow(net, mode='sequential', friction_model='swamee-jain')" + ] + }, + { + "cell_type": "markdown", + "id": "31e04482", + "metadata": {}, + "source": [ + "## 3. Plotting with Mapbox\n", + "\n", + "Use `simple_plot` with `use_mapbox=True` and customize parameters:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "e47a2c8e", + "metadata": {}, + "outputs": [], + "source": [ + "from pandapipes.plotting.simple_plot import simple_plot\n", + "\n", + "fig = simple_plot(\n", + " net,\n", + " use_mapbox=True,\n", + " map_style='light', # e.g. 'streets', 'dark', 'satellite'\n", + " zoom=15,\n", + " pipe_temperature_coloring=True,\n", + " show_colorbar=True,\n", + " pipe_temperature_colorscale=[\n", + " [0.0, 'blue'],\n", + " [0.5, 'yellow'],\n", + " [1.0, 'red'],\n", + " ],\n", + " renderer='browser'\n", + ")\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "id": "f46725c8", + "metadata": {}, + "source": [ + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "4e6692cf", + "metadata": {}, + "source": [ + "### Parameters Reference\n", + "\n", + "- **use_mapbox** (bool): Switch to Mapbox backend (default: False)\n", + "- **mapbox_access_token** (str): Your Mapbox token (env var or override)\n", + "- **map_style** (str): Mapbox style, e.g., 'streets', 'light', 'dark'\n", + "- **zoom** (int): Initial zoom level\n", + "- **pipe_temperature_coloring** (bool): Color pipes by temperature\n", + "- **pipe_temperature_field** (str): Column in `net.res_pipe` (default: 't_to_k')\n", + "- **pipe_temperature_colorscale** (list): Color stops for temperature scale\n", + "- **show_colorbar** (bool): Display the colorbar\n", + "- **renderer** (str): Plotly renderer, e.g., 'browser', 'notebook'\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.11.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/pics/mapbox_plot_sample.png b/tutorials/pics/mapbox_plot_sample.png new file mode 100644 index 000000000..f1404c701 Binary files /dev/null and b/tutorials/pics/mapbox_plot_sample.png differ