diff --git a/getting_started/08_PlottingCapabilities.ipynb b/getting_started/08_PlottingCapabilities.ipynb index d2a2da9a..2bc45c4d 100644 --- a/getting_started/08_PlottingCapabilities.ipynb +++ b/getting_started/08_PlottingCapabilities.ipynb @@ -61,7 +61,9 @@ "metadata": {}, "outputs": [], "source": [ + "import numpy as np\n", "import matplotlib\n", + "import matplotlib as mpl\n", "import shutil\n", "%matplotlib inline\n", "import matplotlib.pyplot as plt # pip install matplotlib\n", @@ -69,7 +71,7 @@ "import plotly.graph_objects as go # pip install plotly\n", "import imageio # pip install imageio\n", "\n", - "max_iter = 5 # to save time we only assess performance on 30 iterations" + "max_iter = 5 # To save time we only assess performance on 30 iterations" ] }, { @@ -87,7 +89,7 @@ "source": [ "import grid2op\n", "env_name = \"l2rpn_case14_sandbox\"\n", - "env = grid2op.make(env_name, test=True)" + "env = grid2op.make(env_name, test=True, n_busbar=3)" ] }, { @@ -174,7 +176,7 @@ "metadata": {}, "outputs": [], "source": [ - "fig_obs2 = plot_helper.plot_obs(obs, line_info=\"p\", load_info=\"v\")" + "fig_obs2 = plot_helper.plot_obs(obs, line_info=\"p\", load_info=\"v\", gen_info=None)" ] }, { @@ -231,6 +233,85 @@ "This plotting utility is a very useful tool to detect what happened, especially just before a game over." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Matplotlib - Full Customization\n", + "In PlotMatplot it is also possible to customize:\n", + "* The size of image (width and height)\n", + "* The resolution of the image (dpi)\n", + "* The size of the markers representing loads, generators, energy storage and busbars\n", + "* The color map used to color markers and/or powerlines\n", + "* The normalization of the color map\n", + "* The attribute used to color markers and/or powerlines (e.g. \"rho\" for ratio of thermal limit)\n", + "* The attribute used to display information next to the markers / powerlines (e.g. \"v\" for voltage and \"p\" for active power)\n", + "\n", + "You can either do so when first instantiating the helper PlotMatplot(), which will ensure the same process is applied each time you plot an observation. Otherwise, you can pass it as a kwarg to .plot_obs(...) for a specific observation to customize that one." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Example: Double the size of markers\n", + "plot_helper = PlotMatplot(env.observation_space, sub_radius=16, gen_radius=18, load_radius=18, bus_radius=12)\n", + "plot_helper.plot_obs(obs);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Example: Use built-in matplotlib colormap (as str)\n", + "print(f\"Available Colormaps: {[k for k in mpl.colormaps.keys() if not k.endswith('_r')]}\")\n", + "plot_helper = PlotMatplot(env.observation_space, load_color=\"inferno\", load_color_attr=\"load_p\", gen_id=True, gen_color=\"berlin\", gen_color_attr=\"v\", bus_color=\"inferno_r\")\n", + "plot_helper.plot_obs(obs, gen_info=\"v\");" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Example: Changing the normalization on the colormaps (BoundaryNorm)\n", + "# Note: Using a list of numbers with a continuous cmap name will NOT work, since BoundaryNorm expects a discrete cmap\n", + "plot_helper = PlotMatplot(env.observation_space, line_color_norm=[0.0, 0.25, 0.5, 0.75, 1.0], \n", + " line_color=[\"darkgreen\", \"green\", \"yellow\", \"orange\", \"red\"], \n", + " line_color_attr=\"rho\")\n", + "plot_helper.plot_obs(obs);\n", + "\n", + "# Example: Changing the normalization on the colormaps (Custom Norm: PowerNorm)\n", + "plot_helper = PlotMatplot(env.observation_space, line_color_norm=mpl.colors.PowerNorm(vmin=0.0, vmax=np.max(obs.p_or), gamma=0.5),\n", + " line_color=\"cividis\", \n", + " line_color_attr=\"p\")\n", + "plot_helper.plot_obs(obs);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Example: Changing the normalization on the colormaps (Normalize (linear))\n", + "plot_helper = PlotMatplot(env.observation_space, load_color_norm=[0.0, np.max(obs.load_p)], \n", + " load_color=\"twilight\", \n", + " load_color_attr=\"p\")\n", + "plot_helper.plot_obs(obs);\n", + "\n", + "# Example: Changing the normalization on the colormaps (BoundaryNorm)\n", + "plot_helper = PlotMatplot(env.observation_space, gen_color_norm=mpl.colors.PowerNorm(vmin=np.min(obs.gen_pmin), vmax=np.max(obs.gen_pmax), gamma=0.5), \n", + " gen_color=\"twilight\", \n", + " gen_color_attr=\"p\")\n", + "plot_helper.plot_obs(obs);" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -510,7 +591,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "venv_grid2op", "language": "python", "name": "python3" }, @@ -524,7 +605,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.10" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/grid2op/PlotGrid/BasePlot.py b/grid2op/PlotGrid/BasePlot.py index 707c8d34..fb9834b7 100644 --- a/grid2op/PlotGrid/BasePlot.py +++ b/grid2op/PlotGrid/BasePlot.py @@ -11,8 +11,9 @@ import numpy as np from abc import ABC, abstractmethod +from typing import Iterable, Callable, Union -from grid2op.Observation import BaseObservation +from grid2op.Observation import BaseObservation, ObservationSpace from grid2op.Exceptions import PlotError from grid2op.PlotGrid.LayoutUtil import layout_obs_sub_load_and_gen from grid2op.PlotGrid.PlotUtil import PlotUtil as pltu @@ -43,15 +44,9 @@ class BasePlot(ABC): """ - def __init__( - self, - observation_space, - width=800, - height=600, - scale=2000.0, - grid_layout=None, - parallel_spacing=3.0, - ): + def __init__(self, observation_space:ObservationSpace, width:int=800, + height:int=600, scale:float=2000.0, grid_layout:Union[dict,None]=None, + parallel_spacing:float=3.0): self.observation_space = observation_space self.width = width @@ -76,10 +71,7 @@ def __init__( ) self.observation_space.rho = np.full(self.observation_space.n_line, 0.0) self.observation_space.p_or = np.ones(self.observation_space.n_line) - - # TODO storage: display the storage units too - # TODO storage doc (yes also the documentation) - + @abstractmethod def create_figure(self): """ @@ -105,7 +97,8 @@ def convert_figure_to_numpy_HWC(self, figure): """ pass - def compute_grid_layout(self, observation_space, grid_layout=None): + def compute_grid_layout(self, observation_space:ObservationSpace, + grid_layout:dict=None): """ Compute the grid layout from the observation space @@ -155,7 +148,9 @@ def compute_grid_layout(self, observation_space, grid_layout=None): ) @abstractmethod - def draw_substation(self, figure, observation, sub_id, sub_name, pos_x, pos_y): + def draw_substation(self, figure:object, observation:BaseObservation, + sub_id:int, sub_name:str, + pos_x:int, pos_y:int): """ Draws a substation into the figure @@ -178,7 +173,9 @@ def draw_substation(self, figure, observation, sub_id, sub_name, pos_x, pos_y): """ pass - def update_substation(self, figure, observation, sub_id, sub_name, pos_x, pos_y): + def update_substation(self, figure:object, observation:BaseObservation, + sub_id:int, sub_name:str, + pos_x:int, pos_y:int): """ INTERNAL @@ -189,20 +186,10 @@ def update_substation(self, figure, observation, sub_id, sub_name, pos_x, pos_y) pass @abstractmethod - def draw_load( - self, - figure, - observation, - load_name, - load_id, - load_bus, - load_value, - load_unit, - pos_x, - pos_y, - sub_x, - sub_y, - ): + def draw_load(self, figure:object, observation:BaseObservation, + load_id:int, load_name:str, load_bus:int, + load_value:float, load_unit:str, + pos_x:int, pos_y:int, sub_x:int, sub_y:int): """ Draws a load into the figure @@ -235,20 +222,10 @@ def draw_load( """ pass - def update_load( - self, - figure, - observation, - load_name, - load_id, - load_bus, - load_value, - load_unit, - pos_x, - pos_y, - sub_x, - sub_y, - ): + def update_load(self, figure:object, observation:BaseObservation, + load_id:int, load_name:str, load_bus:int, + load_value:float, load_unit:str, + pos_x:int, pos_y:int, sub_x:int, sub_y:int): """ INTERNAL @@ -259,20 +236,10 @@ def update_load( pass @abstractmethod - def draw_gen( - self, - figure, - observation, - gen_name, - gen_id, - gen_bus, - gen_value, - gen_unit, - pos_x, - pos_y, - sub_x, - sub_y, - ): + def draw_gen(self, figure:object, observation:BaseObservation, + gen_id:int, gen_name:str, gen_bus:int, + gen_value:float, gen_unit:str, + pos_x:int, pos_y:int, sub_x:int, sub_y:int): """ Draws a generator into the figure @@ -306,20 +273,10 @@ def draw_gen( """ pass - def update_gen( - self, - figure, - observation, - gen_name, - gen_id, - gen_bus, - gen_value, - gen_unit, - pos_x, - pos_y, - sub_x, - sub_y, - ): + def update_gen(self, figure:object, observation:BaseObservation, + gen_id:int, gen_name:str, gen_bus:int, + gen_value:float, gen_unit:str, + pos_x:int, pos_y:int, sub_x:int, sub_y:int): """ INTERNAL @@ -330,22 +287,11 @@ def update_gen( pass @abstractmethod - def draw_powerline( - self, - figure, - observation, - line_id, - line_name, - connected, - line_value, - line_unit, - or_bus, - pos_or_x, - pos_or_y, - ex_bus, - pos_ex_x, - pos_ex_y, - ): + def draw_powerline(self, figure:object, observation:BaseObservation, + line_id:int, line_name:str, connected:bool, + line_value:float, line_unit:str, + or_bus:int, pos_or_x:int, pos_or_y:int, + ex_bus:int, pos_ex_x:int, pos_ex_y:int): """ Draws a powerline into the figure @@ -382,46 +328,25 @@ def draw_powerline( """ pass - def update_powerline( - self, - figure, - observation, - line_id, - line_name, - connected, - line_value, - line_unit, - or_bus, - pos_or_x, - pos_or_y, - ex_bus, - pos_ex_x, - pos_ex_y, - ): + def update_powerline(self, figure:object, observation:BaseObservation, + line_id:int, line_name:str, connected:bool, + line_value:float, line_unit:str, + or_bus:int, pos_or_x:int, pos_or_y:int, + ex_bus:int, pos_ex_x:int, pos_ex_y:int): """ INTERNAL .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ - Draws a powerline into the figure + Updates a powerline into the figure """ pass @abstractmethod - def draw_storage( - self, - figure, - observation, - storage_name, - storage_id, - storage_bus, - storage_value, - storage_unit, - pos_x, - pos_y, - sub_x, - sub_y, - ): + def draw_storage(self, figure:object, observation:BaseObservation, + storage_id:int, storage_name:str, storage_bus:int, + storage_value:float, storage_unit:str, + pos_x:int, pos_y:int, sub_x:int, sub_y:int): """ Draws a storage unit into the figure @@ -434,15 +359,15 @@ def draw_storage( observation: :grid2op.Observation.BaseObservation: Current state of the grid being drawn - storage_name: ``str`` Name of the load + storage_id: ``int`` Id of the ESS, Index in the observation + + storage_name: ``str`` Name of the ESS - storage_id: ``int`` Id of the load, Index in the observation + storage_bus: ``int`` Id of bus the ESS is connected to. - storage_bus: ``int`` Id of bus the load is connected to. + storage_value: ``float`` An informative value of the ESS current state - storage_value: ``float`` An informative value of the load current state - - storage_unit: ``str`` The unit of the `load_value` argument as a string + storage_unit: ``str`` The unit of the `stprage_value` argument as a string pos_x: ``int`` x position from the layout @@ -454,20 +379,10 @@ def draw_storage( """ pass - def update_storage( - self, - figure, - observation, - storage_name, - storage_id, - storage_bus, - storage_value, - storage_unit, - pos_x, - pos_y, - sub_x, - sub_y, - ): + def update_storage(self, figure:object, observation:BaseObservation, + storage_id:int, storage_name:str, storage_bus:int, + storage_value:float, storage_unit:str, + pos_x:int, pos_y:int, sub_x:int, sub_y:int): """ INTERNAL @@ -478,7 +393,7 @@ def update_storage( pass @abstractmethod - def draw_legend(self, figure, observation): + def draw_legend(self, figure:object, observation:BaseObservation): """ Setup the legend for the given figure. @@ -492,7 +407,7 @@ def draw_legend(self, figure, observation): """ pass - def update_legend(self, figure, observation): + def update_legend(self, figure:object, observation:BaseObservation): """ INTERNAL @@ -502,14 +417,22 @@ def update_legend(self, figure, observation): """ pass - def plot_postprocess(self, figure, observation, is_update): + def plot_postprocess(self, figure:object, observation:BaseObservation, update:bool): """ Some implementations may need post-processing. This is called at the end of plot. """ pass - def _plot_subs(self, figure, observation, redraw): + def _plot_subs(self, figure:object, observation:BaseObservation, redraw:bool): + """ + INTERNAL + + .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ + + Plots the substations + """ + draw_fn = self.draw_substation if not redraw: draw_fn = self.update_substation @@ -520,104 +443,80 @@ def _plot_subs(self, figure, observation, redraw): sub_y = self._grid_layout[sub_name][1] draw_fn(figure, observation, sub_idx, sub_name, sub_x, sub_y) - def _aux_draw_elements( - self, - figure, - observation, - load_values, - load_unit, - draw_fn, - el_names, - el_to_subid, - el_pos_topo_vect, - ): + def _aux_draw_elements(self, figure:object, observation:BaseObservation, + el_values:Iterable[float], el_unit:str, draw_fn:Callable, + el_names:Iterable[str], el_to_subid:Iterable[int], + el_pos_topo_vect:Iterable[int]): """ generic method to loop through all elements of a given type and call the draw function on them """ topo = observation.topo_vect topo_pos = el_pos_topo_vect - for stor_idx, stor_name in enumerate(el_names): - if stor_name not in self._grid_layout: + for el_idx, el_name in enumerate(el_names): + if el_name not in self._grid_layout: continue - load_value = None - if load_values is not None: - if load_values[stor_idx] is not None: - load_value = np.round(float(load_values[stor_idx]), 2) + el_value = None + if el_values is not None: + if el_values[el_idx] is not None: + el_value = np.round(float(el_values[el_idx]), 2) else: - load_value = None - sto_x = self._grid_layout[stor_name][0] - sto_y = self._grid_layout[stor_name][1] - sto_subid = el_to_subid[stor_idx] - subname = observation.name_sub[sto_subid] - sto_bus = topo[topo_pos[stor_idx]] - sto_bus = sto_bus if sto_bus > 0 else 0 + el_value = None + el_x = self._grid_layout[el_name][0] + el_y = self._grid_layout[el_name][1] + el_subid = el_to_subid[el_idx] + subname = observation.name_sub[el_subid] + el_bus = topo[topo_pos[el_idx]] + el_bus = el_bus if el_bus > 0 else 0 sub_x = self._grid_layout[subname][0] sub_y = self._grid_layout[subname][1] - draw_fn( - figure, - observation, - stor_idx, - stor_name, - sto_bus, - load_value, - load_unit, - sto_x, - sto_y, - sub_x, - sub_y, - ) + draw_fn(figure, observation, + el_idx, el_name, el_bus, + el_value, el_unit, + el_x, el_y, sub_x, sub_y) - def _plot_loads(self, figure, observation, load_values, load_unit, redraw): + def _plot_loads(self, figure:object, observation:BaseObservation, load_values:Iterable[float], + load_unit:str, redraw:bool): draw_fn = self.draw_load if not redraw: draw_fn = self.update_load - self._aux_draw_elements( - figure, - observation, - load_values, - load_unit, - draw_fn, - observation.name_load, - observation.load_to_subid, - observation.load_pos_topo_vect, - ) + self._aux_draw_elements(figure, observation, + load_values, load_unit, + draw_fn, + observation.name_load, + observation.load_to_subid, + observation.load_pos_topo_vect) - def _plot_storages(self, figure, observation, storage_values, storage_unit, redraw): + def _plot_storages(self, figure:object, observation:BaseObservation, storage_values:Iterable[float], + storage_unit:str, redraw:bool): if observation.n_storage == 0: return draw_fn = self.draw_storage if not redraw: draw_fn = self.update_storage - self._aux_draw_elements( - figure, - observation, - storage_values, - storage_unit, - draw_fn, - observation.name_storage, - observation.storage_to_subid, - observation.storage_pos_topo_vect, - ) - - def _plot_gens(self, figure, observation, gen_values, gen_unit, redraw): + self._aux_draw_elements(figure, observation, + storage_values, storage_unit, + draw_fn, + observation.name_storage, + observation.storage_to_subid, + observation.storage_pos_topo_vect) + + def _plot_gens(self, figure:object, observation:BaseObservation, gen_values:Iterable[float], + gen_unit:str, redraw:bool): draw_fn = self.draw_gen if not redraw: draw_fn = self.update_gen - self._aux_draw_elements( - figure, - observation, - gen_values, - gen_unit, - draw_fn, - observation.name_gen, - observation.gen_to_subid, - observation.gen_pos_topo_vect, - ) + self._aux_draw_elements(figure, observation, + gen_values, gen_unit, + draw_fn, + observation.name_gen, + observation.gen_to_subid, + observation.gen_pos_topo_vect) - def _plot_lines(self, figure, observation, line_values, line_unit, redraw): + def _plot_lines(self, figure:object, observation:BaseObservation, line_values:Iterable[float], + line_unit:str, redraw:bool): draw_fn = self.draw_powerline if not redraw: draw_fn = self.update_powerline @@ -672,28 +571,18 @@ def _plot_lines(self, figure, observation, line_values, line_unit, redraw): line_ex_x -= ox * self._parallel_spacing line_ex_y -= oy * self._parallel_spacing - draw_fn( - figure, - observation, - line_idx, - line_name, - line_status, - line_value, - line_unit, - line_or_bus, - line_or_x, - line_or_y, - line_ex_bus, - line_ex_x, - line_ex_y, - ) + draw_fn(figure, observation, + line_idx, line_name, line_status, + line_value, line_unit, + line_or_bus, line_or_x, line_or_y, + line_ex_bus, line_ex_x, line_ex_y) - def _plot_legend(self, fig, observation, redraw): + def _plot_legend(self, figure:object, observation:BaseObservation, redraw:bool): draw_fn = self.draw_legend if not redraw: draw_fn = self.update_legend - draw_fn(fig, observation) + draw_fn(figure, observation) def plot_layout(self): """ @@ -704,16 +593,8 @@ def plot_layout(self): observation=self.observation_space, figure=None, redraw=True ) - def plot_obs( - self, - observation, - figure=None, - redraw=True, - line_info="rho", - load_info="p", - gen_info="p", - storage_info="p", - ): + def plot_obs(self, observation:BaseObservation, figure:Union[object,None]=None, redraw:bool=True, + line_info:str="rho", load_info:str="p", gen_info:str="p", storage_info:str="p"): """ Plot an observation. @@ -841,35 +722,18 @@ def plot_obs( f'provided as "storage_info"' ) - return self.plot_info( - observation=observation, - figure=figure, - redraw=redraw, - line_values=line_values, - line_unit=line_unit, - load_values=load_values, - load_unit=load_unit, - gen_values=gen_values, - gen_unit=gen_unit, - storage_values=storage_values, - storage_unit=storage_unit, - ) + return self.plot_info(figure=figure, observation=observation, redraw=redraw, + line_values=line_values, line_unit=line_unit, + load_values=load_values, load_unit=load_unit, + gen_values=gen_values, gen_unit=gen_unit, + storage_values=storage_values, storage_unit=storage_unit) - def plot_info( - self, - figure=None, - redraw=True, - line_values=None, - line_unit="", - load_values=None, - load_unit="", - storage_values=None, - storage_unit="", - gen_values=None, - gen_unit="", - observation=None, - coloring=None, - ): + def plot_info(self, figure:Union[object,None]=None, redraw:bool=True, + line_values:Union[Iterable[float],None]=None, line_unit:str="", + load_values:Union[Iterable[float],None]=None, load_unit:str="", + storage_values:Union[Iterable[float],None]=None, storage_unit:str="", + gen_values:Union[Iterable[float],None]=None, gen_unit:str="", + observation:BaseObservation=None, coloring:Union[str,None]=None): """ Plot an observation with custom values @@ -881,7 +745,7 @@ def plot_info( line_values: ``list`` information to be displayed for the powerlines - [must have the same size as observation.n_line and convertible to float] + [must have the same size as observation.n_line and convertible to float] line_unit: ``str`` Unit string for the :line_values: argument, displayed after the line value @@ -1066,4 +930,4 @@ def plot_info( self.plot_postprocess(fig, observation, not redraw) # Return updated figure - return fig + return fig \ No newline at end of file diff --git a/grid2op/PlotGrid/PlotMatplot.py b/grid2op/PlotGrid/PlotMatplot.py index ca584dd9..6d288953 100644 --- a/grid2op/PlotGrid/PlotMatplot.py +++ b/grid2op/PlotGrid/PlotMatplot.py @@ -6,22 +6,26 @@ # SPDX-License-Identifier: MPL-2.0 # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. -import copy +import copy, re import io import numpy as np - +import matplotlib as mpl import matplotlib.pyplot as plt +from dataclasses import dataclass, fields +from typing import Tuple, Literal, Callable, Union, List +from matplotlib.colors import Colormap, LinearSegmentedColormap, Normalize, is_color_like, BoundaryNorm +from matplotlib.figure import Figure from matplotlib.path import Path +from matplotlib.patches import Patch +from grid2op.Observation import ObservationSpace, BaseObservation from grid2op.PlotGrid.BasePlot import BasePlot from grid2op.PlotGrid.PlotUtil import PlotUtil as pltu +from grid2op.PlotGrid.config import * # all colors import matplotlib.patches as patches from matplotlib.lines import Line2D -from grid2op.PlotGrid.config import * # all colors - - -class GenDraw(patches.CirclePolygon): +class LoadPatch(patches.RegularPolygon): """ INTERNAL @@ -30,11 +34,11 @@ class GenDraw(patches.CirclePolygon): Empty class to handle the legend """ - def __init__(self, *args, resolution=5, **kwargs): - patches.CirclePolygon.__init__(self, *args, resolution=resolution, **kwargs) + def __init__(self, *args, n_vertices:int=3, **kwargs): + patches.RegularPolygon.__init__(self, *args, numVertices=n_vertices, **kwargs) -class LoadDraw(patches.CirclePolygon): +class StoragePatch(patches.RegularPolygon): """ INTERNAL @@ -43,11 +47,10 @@ class LoadDraw(patches.CirclePolygon): Empty class to handle the legend """ - def __init__(self, *args, resolution=3, **kwargs): - patches.CirclePolygon.__init__(self, *args, resolution=resolution, **kwargs) - + def __init__(self, *args, n_vertices:int=4, **kwargs): + patches.RegularPolygon.__init__(self, *args, numVertices=n_vertices, **kwargs) -class StorageDraw(patches.CirclePolygon): +class GenPatch(patches.RegularPolygon): """ INTERNAL @@ -56,21 +59,107 @@ class StorageDraw(patches.CirclePolygon): Empty class to handle the legend """ - def __init__(self, *args, resolution=4, **kwargs): - patches.CirclePolygon.__init__(self, *args, resolution=resolution, **kwargs) - + def __init__(self, *args, n_vertices:int=5, **kwargs): + patches.RegularPolygon.__init__(self, *args, numVertices=n_vertices, **kwargs) -# TODO refactor this class to make possible some calls like -# plotmatplot.plot_info(...).plot_gentype(...) is possible +# TODO Refactor this class to make possible some calls like +# plotmatplot.plot_info(...).plot_gentype(...) is possible # TODO add some transparency when coloring=... is used in plot_info # TODO code the load part in the plot_info +@dataclass +class PatchSettings: + radius:int + name:bool + id:bool + color_attr:Union[str,None] + face_color:str + cmap:Colormap + cnorm:Union[Normalize,None] + active_cnorm:Union[Normalize,None] + vertices:Union[int,None] # No. of Points on Polygon + txt_color:str + line_color:Union[str,None] + line_width:Union[float,None] + display_value:bool + display_name:bool + +@dataclass +class LineSettings: + busbar_radius:int + name:str + id:bool + color_attr:Union[str,None] + width:Union[float,None] + cmap:Colormap + cnorm:Union[Normalize,None] + active_cnorm:Union[Normalize,None] + busbar_cmap:Colormap + arrow_len:int = 10 + arrow_width:float = 10.0 + +@dataclass +class Settings: + sub:PatchSettings + load:PatchSettings + gen:PatchSettings + storage:PatchSettings + line:LineSettings class PlotMatplot(BasePlot): """ This class uses the python library "matplotlib" to draw the powergrid. + Arguments + ---------- + observation_space: ``grid2op.Observation.ObservationSpace`` + width: ``int`` (default: 1280) + height: ``int`` (default: 720) + grid_layout ``dict`` (default: None) + dpi: ``int`` (default: 96) + scale: ``float`` (default: 2000.0) + + # >> Substation << + sub_radius: ``int`` (default: 15) + sub_color: ``str`` (default: "blue") + + # >> Load << + load_radius: ``int`` (default: 8) + load_name: ``bool`` (default: False) + load_id: ``bool`` (default: False) + load_vertices: ``int`` (default: 3) # No. edges load polygon + load_color_attr: ``str|None`` (default: None) + load_color: ``Colormap|str|list`` (default: "orange") + + # >> Generator << + gen_radius: ``int`` (default: 8) + gen_name: ``bool`` (default: False) + gen_id: ``bool`` (default: False) + gen_vertices: ``int`` (default: 5) # No. edges gen polygon + gen_color_attr: ``Literal["gen_type", "prod_p"]`` (default: "prod_p") + gen_color: ``Colormap|str|list`` (default: "green") + + # >> Storage << + storage_radius: ``int`` (default: 8) + storage_name: ``bool`` (default: False) + storage_id: ``bool`` (default: False) + storage_vertices: ``int`` (default: 4) # No. edges ESS polygon + storage_color_attr: ``str|None`` (default: None) + storage_color: ``Colormap|str|list`` (default: "purple") + + # >> Line << + line_name: ``bool`` (default: False) + line_id: ``bool`` (default: False) + line_color_norm: ``Normalize`` (default: None) # BoundaryNorm([0.0, 0.5, 1.0], ncolors=3, extend="max"), + line_color_attr: ``Literal["rho", "a", "p", "v", "a_or", "p_or", "v_or"]`` (default: "rho") + line_color: ``Colormap|str|list`` (default: ["blue", "orange", "red"]) # Cmap name, or list of colours + + # >> Busbar << + bus_radius: ``int`` (default: 6) + bus_color: ``Colormap|str|list`` (default: ["black", "red", "lime"]) + + Attributes ---------- @@ -80,57 +169,9 @@ class PlotMatplot(BasePlot): Height of the figure in pixel dpi: ``int`` Dots per inch, to convert pixels dimensions into inches - _scale: ``float`` - Scale of the drawing in arbitrary units - _sub_radius: ``int`` - Substation circle size - _sub_face_color: ``str`` - Substation circle fill color - _sub_edge_color: ``str`` - Substation circle edge color - _sub_txt_color: ``str`` - Substation info text color - _load_radius: ``int`` - Load circle size - _load_name: ``bool`` - Show load names (default True) - _load_face_color: ``str`` - Load circle fill color - _load_edge_color: ``str`` - Load circle edge color - _load_txt_color: ``str`` - Load info text color - _load_line_color: ``str`` - Color of the line from load to substation - _load_line_width: ``int`` - Width of the line from load to substation - _gen_radius: ``int`` - Generators circle size - _gen_name: ``bool`` - Show generators names (default True) - _gen_face_color: ``str`` - Generators circle fill color - _gen_edge_color: ``str`` - Generators circle edge color - _gen_txt_color: ``str`` - Generators info txt color - _gen_line_color: ``str`` - Color of the line form generator to substation - _gen_line_width: ``str`` - Width of the line from generator to substation - _line_color_scheme: ``list`` - List of color strings to color powerlines based on rho values - _line_color_width: ``int`` - Width of the powerlines lines - _line_bus_radius: ``int`` - Size of the bus display circle - _line_bus_face_colors: ``list`` - List of 3 colors strings, each corresponding to the fill color of the bus circle - _line_arrow_len: ``int`` - Length of the arrow on the powerlines - _line_arrow_width: ``int`` - Width of the arrow on the powerlines - + settings: ``Settings`` + Settings for the size, coloring and information displayed for loads, generators, storage, and powerlines. + Examples -------- You can use it this way: @@ -150,144 +191,250 @@ class PlotMatplot(BasePlot): # more information about it on the `getting_started/8_PlottingCapabilities.ipynb` notebook of grid2op """ - - def __init__( - self, - observation_space, - width=1280, - height=720, - grid_layout=None, - dpi=96, - scale=2000.0, - bus_radius=6, - sub_radius=15, - load_radius=8, - load_name=False, - load_id=False, - load_resolution=3, # number of edges of the polygon representing the generator - gen_radius=8, - gen_name=False, - gen_id=False, - gen_resolution=5, # number of edges of the polygon representing the generator - storage_resolution=4, # number of edges of the polygon representing the generator - line_name=False, - line_id=False, - ): + + line_attr_map = {"a":"a_or", "p":"p_or", "v":"v_or"} + load_attr_map = {"p":"load_p", "v":"load_v", "q":"load_q"} + gen_attr_map = {"p":"prod_p", "v":"prod_v", "q":"prod_q", "type":"gen_type"} + storage_attr_map = {"p":"storage_power", "c":"storage_charge", "theta":"storage_theta"} + + def __init__(self, observation_space:ObservationSpace, + width:int=1280, height:int=720, + grid_layout:dict=None, + dpi:int=96, scale:float=2000.0, + + # >> Substation << + sub_radius:int=15, sub_color:str = "blue", + + # >> Load << + load_radius:int=8, load_name:bool=False, + load_id:bool=False, load_vertices:int=3, # No. edges load polygon + load_color_norm:Union[Normalize,List[float]] = None, + load_color_attr:Literal["p","v"] = "p", + load_color:Union[Colormap,str,list] = "orange", + + # >> Generator << + gen_radius:int=8, gen_name:bool=False, + gen_id:bool=False, gen_vertices:int=5, # No. edges gen polygon + gen_color_norm:Union[Normalize,List[float]] = None, + gen_color_attr:Literal["type", "p", "q", "v"]= "p", + gen_color:Union[Colormap,str,list] = "green", + + # >> Storage << + storage_radius:int=8, storage_name:bool=False, + storage_id:bool=False, storage_vertices:int=4, # No. edges ESS polygon + storage_color_norm:Union[Normalize,List[float]] = None, + storage_color_attr:Literal["p", "theta", "charge"] = "p", + storage_color:Union[Colormap,str,list] = "purple", + + # >> Line << + line_name:bool=False, line_id:bool=False, + line_width:float=1.0, + line_color_norm:Union[Normalize,List[float]] = None, + line_color_attr:Literal["rho", "p", "a", "v"]="rho", + line_color:Union[Colormap,str,list]=["blue", "orange", "red"], # Cmap name, or list of colours + + # >> Busbar << + bus_radius:int=6, + bus_color:Union[Colormap,str,list]=["black", "red", "lime"], + ): self.dpi = dpi super().__init__(observation_space, width, height, scale, grid_layout) - - self._sub_radius = sub_radius - self._sub_face_color = "w" - self._sub_edge_color = "blue" - self._sub_txt_color = "black" - self._display_sub_name = True - - self._load_radius = load_radius - self._load_name = load_name - self._load_id = load_id - self._load_face_color = "w" - self._load_edge_color = "orange" - self._load_resolution = load_resolution + + _sub_settings = PatchSettings(sub_radius, False, False, None, + "w", self._convert_colors_to_cmap(sub_color), + None, None, + None, "black", "black", 1, + False, True) + + load_color_attr = PlotMatplot.load_attr_map.get(load_color_attr, load_color_attr) + _load_settings = PatchSettings(load_radius, load_name, load_id, load_color_attr, + "w", self._convert_colors_to_cmap(load_color), + self._convert_values_to_norm(load_color_norm), None, + load_vertices, + "black", "black", 1, + False, True) self._load_patch = self._load_patch_default - self._load_txt_color = "black" - self._load_line_color = "black" - self._load_line_width = 1 - self._display_load_name = True - - self._gen_radius_orig = gen_radius - self._gen_radius = None # init in self.restore_gen_palette() - self._gen_resolution = gen_resolution + + gen_color_attr = PlotMatplot.gen_attr_map.get(gen_color_attr, gen_color_attr) + _gen_settings = PatchSettings(gen_radius, gen_name, gen_id, gen_color_attr, + "w", self._convert_colors_to_cmap(gen_color), + self._convert_values_to_norm(gen_color_norm), None, + gen_vertices, + "black", "black", 1, + True, True) self._gen_patch = self._gen_patch_default - self._gen_name = gen_name - self._gen_id = gen_id - self._gen_face_color = "w" - self._gen_edge_color_orig = "green" - self._gen_edge_color = None - self._gen_txt_color = "black" - self._gen_line_color = "black" - self._gen_line_width_orig = 1 - self._gen_line_width = None - self._display_gen_value = True - self._display_gen_name = True - self.restore_gen_palette() - - self._storage_radius = load_radius - self._storage_name = load_name - self._storage_id = load_id # bool : do i plot the id - self._storage_face_color = "w" - self._storage_edge_color = "purple" - self._storage_resolution = storage_resolution + + storage_color_attr = PlotMatplot.storage_attr_map.get(storage_color_attr, storage_color_attr) + _storage_settings = PatchSettings(storage_radius, storage_name, storage_id, storage_color_attr, + "w", self._convert_colors_to_cmap(storage_color), + self._convert_values_to_norm(storage_color_norm), None, + storage_vertices, + "black", "black", 1, + False, True) self._storage_patch = self._storage_patch_default - self._storage_txt_color = "black" - self._storage_line_color = "black" - self._storage_line_width = 1 - self._display_storage_name = True - - self._line_name = line_name - self._line_id = line_id - self._line_color_scheme_orig = ["blue", "orange", "red"] - self._line_color_scheme = None - self.restore_line_palette() - - self._line_color_width = 1 - self._line_bus_radius = bus_radius - self._line_bus_face_colors = ["black", "red", "lime"] - self._line_arrow_len = 10 - self._line_arrow_width = 10.0 + + # Line Color Normalizaion (if None, will normalize by attr when obs is passed) + line_color_attr = PlotMatplot.line_attr_map.get(line_color_attr, line_color_attr) + if line_color_attr == "rho" and line_color_norm is None: # 1.11.0: Backwards compatability + line_color_norm = BoundaryNorm([0.0, 0.5, 1.0], ncolors=3, extend="max") + + _line_settings = LineSettings(bus_radius, line_name, line_id, + line_color_attr, line_width, + self._convert_colors_to_cmap(line_color), + self._convert_values_to_norm(line_color_norm), None, + self._convert_colors_to_cmap(bus_color)) + + self.settings = Settings(_sub_settings, _load_settings, _gen_settings, + _storage_settings, _line_settings) + self.orig_settings = copy.deepcopy(self.settings) self.xlim = [0, 0] self.xpad = 5 self.ylim = [0, 0] self.ypad = 5 - # for easize manipulation + # For easize manipulation self.legend = None - self.figure = None - - def _gen_patch_default(self, xy, radius, edgecolor, facecolor): - """default patch used to draw generator""" - # TODO maybe make a better version of this - patch = GenDraw( - xy, - radius=radius, - edgecolor=edgecolor, - facecolor=facecolor, - resolution=self._gen_resolution, - linewidth=self._gen_line_width, + self.figure:Union[Figure,None] = None + self.blitting:bool = False + + def _convert_colors_to_cmap(self, color_scheme:Union[str,list,Colormap], nb_color:int=-1) -> Colormap: + """ + Converts a colormap name (as str) or a list of color-like objects to a + 'matpolitlib.colors.Colormap'. + + Optionally will refit the colormap into a Discretized linear segemented color-map + if nb-color is provided. + + Args: + color_scheme (str | list | Colormap): Specific color, name of colormap, or list of color-like objects. + nb_color (int, optional): Number of discrete color categories. Defaults to -1 (no affect). + + Raises: + ValueError: Colour scheme is not valid + + Returns: + Colormap: A Matplotlib Colormap which when called converts a float to a color 4-tuple (r,g,b,a). + """ + cmap = None + if isinstance(color_scheme, Colormap): + cmap = color_scheme + elif isinstance(color_scheme, str): + if color_scheme in mpl.colormaps: # String is a colormap + cmap = mpl.colormaps[color_scheme] + elif is_color_like(color_scheme): # String is a color + cmap = LinearSegmentedColormap.from_list("", [color_scheme]*2, N=2) + else: + raise ValueError(f"String: '{color_scheme}' is not a valid color nor colormap name.") + elif isinstance(color_scheme, list): + if len(color_scheme) == 1: # Single Value + color_scheme.append(color_scheme[0]) + cmap = LinearSegmentedColormap.from_list("", color_scheme, N=len(color_scheme)) + else: + raise ValueError(f"Colour Scheme not found: '{color_scheme}'") + + if nb_color > 0: + return LinearSegmentedColormap.from_list("", + [cmap(i / nb_color) for i in range(nb_color)], N=nb_color) + else: + return cmap + + def _convert_values_to_norm(self, color_norm:Union[List[float],Normalize,None]) -> Normalize: + """ + Converts a list of floats to a color Normalizer in Matplotlib. If two valeus are provided + the normalization is done linearly between them. If more are provided, a Discete boundary + norm is used instead. If the normalization is defined manually, it is used as is. + + Args: + color_norm (List[float]|Normalize|None): Specific color normalization. + + Returns: + Normalize: A Matplotlib Normalizer which when called converts a float to its normalized value. + """ + if isinstance(color_norm, list): + if len(color_norm) == 2: # Continuous between [min, max] + color_norm = Normalize(vmin=np.min(np.abs(color_norm)), + vmax= np.max(np.abs(color_norm))) + else: # Linear Discrete + color_norm= BoundaryNorm(np.sort(np.abs(color_norm)), ncolors=len(color_norm), extend="max") + return color_norm + + def _set_active_color_norm(self, obs:Union[BaseObservation,ObservationSpace], norm:Union[Normalize,None], + attr:Union[str,None], settings:Union[PatchSettings,LineSettings]): + """ + Set the currently active color normalization, which overwrites (in-place) the settings for a specific patch. + + Args: + obs (BaseObservation|ObservationSpace): Current observation (to be visualized) or Observation Space (static) + norm (Normalize | None): Color normalization process in Matplotlib + attr (str | None): Name of attribute to color by the patches by + settings (PatchSettings | Line Settings): Settings to modify, belonging to a specific patch category. + """ + attr = attr if attr is not None else settings.color_attr + if isinstance(obs, ObservationSpace): + norm = None + elif norm is None and settings.cnorm is None and attr is not None: + try: + values = getattr(obs, attr) + except Exception: + values = getattr(obs, f"{attr}_or") + values = np.abs(values) # Direction does not matter + if values.size > 0: + norm = Normalize(vmin=np.min(values), vmax=np.max(values), clip=True) + elif norm is None: + norm = copy.deepcopy(settings.cnorm) + settings.active_cnorm = norm + + def _set_active_color_norms(self, obs:Union[BaseObservation,ObservationSpace], **kwargs): + """ + Set the active color normalization for all patches (load, gen, storage, and lines). Behaviour depends + on whether kwargs overwrites any of the normalizations for this specific observation. + """ + for field in fields(Settings): + self._set_active_color_norm(obs, kwargs.get(f"{field.name}_color_norm", None), + kwargs.get(f"{field.name}_color_attr", None), getattr(self.settings, field.name)) + + def _gen_patch_default(self, xy:Tuple[float, float], radius:float, + edgecolor:Union[object,None], facecolor:Union[object,None]): + """Default patch used to draw generators""" + return GenPatch( + xy, radius=radius, + edgecolor=edgecolor, facecolor=facecolor, + n_vertices=self.settings.gen.vertices, + linewidth=self.settings.gen.line_width, ) - return patch - - def _load_patch_default(self, xy, radius, edgecolor, facecolor): - """default patch used to draw generator""" - # TODO maybe make a better version of this - patch = LoadDraw( - xy, - radius=radius, + + def _load_patch_default(self, xy:Tuple[float, float], radius:float, + edgecolor:Union[object,None], + facecolor:Union[object,None]) -> Patch: + """Default patch used to draw loads""" + return LoadPatch( + xy, radius=radius, edgecolor=edgecolor, facecolor=facecolor, - resolution=self._load_resolution, + linewidth=self.settings.load.line_width, + n_vertices=self.settings.load.vertices, ) - return patch - - def _storage_patch_default(self, xy, radius, edgecolor, facecolor): - """default patch used to draw generator""" - # TODO maybe make a better version of this - patch = StorageDraw( - xy, - radius=radius, + + def _storage_patch_default(self, xy:Tuple[float, float], radius:float, + edgecolor:Union[object,None], + facecolor:Union[object,None]) -> Patch: + """Default patch used to draw Storage systems""" + return StoragePatch( + xy, radius=radius, edgecolor=edgecolor, facecolor=facecolor, - resolution=self._storage_resolution, + linewidth=self.settings.storage.line_width, + n_vertices=self.settings.storage.vertices, ) - return patch - def _v_textpos_from_dir(self, dirx, diry): + def _v_textpos_from_dir(self, diry:float) -> str: if diry > 0: return "bottom" else: return "top" - def _h_textpos_from_dir(self, dirx, diry): + def _h_textpos_from_dir(self, dirx:float) -> str: if dirx == 0: return "center" elif dirx > 0: @@ -295,28 +442,29 @@ def _h_textpos_from_dir(self, dirx, diry): else: return "right" - def create_figure(self): - # lazy loading of graphics library (reduce loading time) - # and mainly because matplolib has weird impact on argparse + def create_figure(self) -> Figure: + """ + Lazy loading of Graphics library (reduces loading time) + Also needed because Matplotlib has a weird impact on argparse. + """ import matplotlib.pyplot as plt - w_inch = self.width / self.dpi h_inch = self.height / self.dpi f = plt.figure(figsize=(w_inch, h_inch), dpi=self.dpi) self.ax = f.subplots() f.canvas.draw() + self.blitting = f.canvas.supports_blit return f - def clear_figure(self, figure): + def clear_figure(self, figure:Figure): self.xlim = [0, 0] self.ylim = [0, 0] figure.clear() self.ax = figure.subplots() - def convert_figure_to_numpy_HWC(self, figure): + def convert_figure_to_numpy_HWC(self, figure:Figure) -> np.ndarray: w, h = figure.get_size_inches() * figure.dpi - w = int(w) - h = int(h) + w, h = int(w), int(h) buf = io.BytesIO() figure.canvas.print_raw(buf) buf.seek(0) @@ -325,415 +473,342 @@ def convert_figure_to_numpy_HWC(self, figure): img_arr = np.reshape(img_arr, (h, w, 4)) return img_arr - def _draw_substation_txt(self, pos_x, pos_y, text): + def _draw_substation_txt(self, pos_x:float, pos_y:float, text:str): self.ax.text( - pos_x, - pos_y, - text, - color=self._sub_txt_color, + pos_x, pos_y, text, + color=self.settings.sub.txt_color, horizontalalignment="center", - verticalalignment="center", + verticalalignment="center" ) - def _draw_substation_circle(self, pos_x, pos_y): + def _draw_substation_circle(self, pos_x:float, pos_y:float, + sub_edgecolor:Union[object,None]): patch = patches.Circle( (pos_x, pos_y), - radius=self._sub_radius, - facecolor=self._sub_face_color, - edgecolor=self._sub_edge_color, + radius=self.settings.sub.radius, + facecolor=self.settings.sub.face_color, + edgecolor=sub_edgecolor, ) self.ax.add_patch(patch) - def draw_substation(self, figure, observation, sub_id, sub_name, pos_x, pos_y): - self.xlim[0] = min(self.xlim[0], pos_x - self._sub_radius) - self.xlim[1] = max(self.xlim[1], pos_x + self._sub_radius) - self.ylim[0] = min(self.ylim[0], pos_y - self._sub_radius) - self.ylim[1] = max(self.ylim[1], pos_y + self._sub_radius) - - self._draw_substation_circle(pos_x, pos_y) - if self._display_sub_name: + def draw_substation(self, figure:Figure, observation:BaseObservation, + sub_id:int, sub_name:str, + pos_x:int, pos_y:int): + self.xlim[0] = min(self.xlim[0], pos_x - self.settings.sub.radius) + self.xlim[1] = max(self.xlim[1], pos_x + self.settings.sub.radius) + self.ylim[0] = min(self.ylim[0], pos_y - self.settings.sub.radius) + self.ylim[1] = max(self.ylim[1], pos_y + self.settings.sub.radius) + + edge_color = self.settings.sub.cmap(0.5) + self._draw_substation_circle(pos_x, pos_y, edge_color) + if self.settings.sub.display_name: self._draw_substation_txt(pos_x, pos_y, str(sub_id)) - def _draw_load_txt(self, pos_x, pos_y, sub_x, sub_y, text): + def _draw_load_txt(self, pos_x:float, pos_y:float, + sub_x:float, sub_y:float, text:str): dir_x, dir_y = pltu.vec_from_points(sub_x, sub_y, pos_x, pos_y) off_x, off_y = pltu.norm_from_points(sub_x, sub_y, pos_x, pos_y) - txt_x = pos_x + off_x * self._gen_radius - txt_y = pos_y + off_y * self._gen_radius - ha = self._h_textpos_from_dir(dir_x, dir_y) - va = self._v_textpos_from_dir(dir_x, dir_y) + txt_x = pos_x + off_x * self.settings.gen.radius + txt_y = pos_y + off_y * self.settings.gen.radius + ha = self._h_textpos_from_dir(dir_x) + va = self._v_textpos_from_dir(dir_y) self.ax.text( - txt_x, - txt_y, - text, - color=self._load_txt_color, - horizontalalignment=ha, + txt_x, txt_y, text, + color=self.settings.load.txt_color, + ha=ha, va=va, fontsize="small", - verticalalignment=va, ) - def _draw_load_name(self, pos_x, pos_y, txt): + def _draw_load_name(self, pos_x:float, pos_y:float, txt:str): self.ax.text( - pos_x, - pos_y, - txt, - color=self._load_txt_color, - va="center", - ha="center", + pos_x, pos_y, txt, + color=self.settings.load.txt_color, + va="center", ha="center", fontsize="x-small", ) - def _draw_load_circle(self, pos_x, pos_y): + def _draw_load_circle(self, pos_x:float, pos_y:float, + load_edgecolor:Union[object,None]): patch = self._load_patch( (pos_x, pos_y), - radius=self._load_radius, - facecolor=self._load_face_color, - edgecolor=self._load_edge_color, + radius=self.settings.load.radius, + facecolor=self.settings.load.face_color, + edgecolor=load_edgecolor, ) self.ax.add_patch(patch) - def _draw_load_line(self, pos_x, pos_y, sub_x, sub_y): + def _draw_load_line(self, pos_x:float, pos_y:float, + sub_x:float, sub_y:float): codes = [Path.MOVETO, Path.LINETO] verts = [(pos_x, pos_y), (sub_x, sub_y)] path = Path(verts, codes) patch = patches.PathPatch( - path, color=self._load_line_color, lw=self._load_line_width - ) - self.ax.add_patch(patch) - - def _draw_load_bus(self, pos_x, pos_y, norm_dir_x, norm_dir_y, bus_id): - center_x = pos_x + norm_dir_x * self._sub_radius - center_y = pos_y + norm_dir_y * self._sub_radius - face_color = self._line_bus_face_colors[bus_id] - patch = patches.Circle( - (center_x, center_y), radius=self._line_bus_radius, facecolor=face_color + path, + color=self.settings.load.line_color, + lw=self.settings.load.line_width ) self.ax.add_patch(patch) - def draw_load( - self, - figure, - observation, - load_id, - load_name, - load_bus, - load_value, - load_unit, - pos_x, - pos_y, - sub_x, - sub_y, - ): - self.xlim[0] = min(self.xlim[0], pos_x - self._load_radius) - self.xlim[1] = max(self.xlim[1], pos_x + self._load_radius) - self.ylim[0] = min(self.ylim[0], pos_y - self._load_radius) - self.ylim[1] = max(self.ylim[1], pos_y + self._load_radius) + def draw_load(self, figure:Figure, observation:BaseObservation, + load_id:int, load_name:str, load_bus:int, + load_value:float, load_unit:str, + pos_x:int, pos_y:int, sub_x:int, sub_y:int): + self.xlim[0] = min(self.xlim[0], pos_x - self.settings.load.radius) + self.xlim[1] = max(self.xlim[1], pos_x + self.settings.load.radius) + self.ylim[0] = min(self.ylim[0], pos_y - self.settings.load.radius) + self.ylim[1] = max(self.ylim[1], pos_y + self.settings.load.radius) self._draw_load_line(pos_x, pos_y, sub_x, sub_y) - self._draw_load_circle(pos_x, pos_y) - load_txt = "" - if self._load_name: - load_txt += '"{}":\n'.format(load_name) - if self._load_id: - load_txt += "id: {}\n".format(load_id) - if load_value is not None: - load_txt += pltu.format_value_unit(load_value, load_unit) - if load_txt: - self._draw_load_txt(pos_x, pos_y, sub_x, sub_y, load_txt) - if self._display_load_name: - self._draw_load_name(pos_x, pos_y, str(load_id)) - load_dir_x, load_dir_y = pltu.norm_from_points(sub_x, sub_y, pos_x, pos_y) - self._draw_load_bus(sub_x, sub_y, load_dir_x, load_dir_y, load_bus) - - def update_load( - self, - figure, - observation, - load_id, - load_name, - load_bus, - load_value, - load_unit, - pos_x, - pos_y, - sub_x, - sub_y, - ): - pass - - def draw_storage( - self, - figure, - observation, - load_id, - load_name, - load_bus, - load_value, - load_unit, - pos_x, - pos_y, - sub_x, - sub_y, - ): - self.xlim[0] = min(self.xlim[0], pos_x - self._load_radius) - self.xlim[1] = max(self.xlim[1], pos_x + self._load_radius) - self.ylim[0] = min(self.ylim[0], pos_y - self._load_radius) - self.ylim[1] = max(self.ylim[1], pos_y + self._load_radius) - self._draw_storage_line( - pos_x, pos_y, sub_x, sub_y - ) # line from the storage to the substation - self._draw_storage_circle(pos_x, pos_y) # storage element + color_attr = "" if self.settings.load.color_attr is None else self.settings.load.color_attr + if self.settings.load.active_cnorm is None: + color_value = 0.5 + elif hasattr(observation, color_attr): + color_value = self.settings.load.active_cnorm(np.abs(getattr(observation, self.settings.load.color_attr)[load_id])) + else: + color_value = (self.settings.load.active_cnorm.vmax - self.settings.load.active_cnorm.vmin) / 2 + edge_color = self.settings.load.cmap(color_value) + self._draw_load_circle(pos_x, pos_y, load_edgecolor=edge_color) load_txt = "" - if self._storage_name: - load_txt += '"{}":\n'.format(load_name) - if self._storage_id: - load_txt += "id: {}\n".format(load_id) + if self.settings.load.name: + load_txt += f'"{load_name}":\n' + if self.settings.load.id: + load_txt += f"id: {load_id}\n" if load_value is not None: load_txt += pltu.format_value_unit(load_value, load_unit) if load_txt: self._draw_load_txt(pos_x, pos_y, sub_x, sub_y, load_txt) - if self._display_load_name: + if self.settings.load.display_name: self._draw_load_name(pos_x, pos_y, str(load_id)) load_dir_x, load_dir_y = pltu.norm_from_points(sub_x, sub_y, pos_x, pos_y) - self._draw_storage_bus(sub_x, sub_y, load_dir_x, load_dir_y, load_bus) + self._draw_busbar(sub_x, sub_y, load_dir_x, load_dir_y, load_bus) - def _draw_storage_circle(self, pos_x, pos_y): - patch = self._storage_patch( + def _draw_gen_txt(self, pos_x:float, pos_y:float, + sub_x:float, sub_y:float, text:str): + dir_x, dir_y = pltu.vec_from_points(sub_x, sub_y, pos_x, pos_y) + off_x, off_y = pltu.norm_from_points(sub_x, sub_y, pos_x, pos_y) + txt_x = pos_x + off_x * self.settings.gen.radius + txt_y = pos_y + off_y * self.settings.gen.radius + ha = self._h_textpos_from_dir(dir_x) + va = self._v_textpos_from_dir(dir_y) + self.ax.text( + txt_x, txt_y, text, + color=self.settings.gen.txt_color, + ha=ha, va=va, wrap=True, + fontsize="small", + ) + + def _draw_gen_circle(self, pos_x:float, pos_y:float, + gen_edgecolor:Union[object,None]): + patch = self._gen_patch( (pos_x, pos_y), - radius=self._storage_radius, - facecolor=self._storage_face_color, - edgecolor=self._storage_edge_color, + radius=self.settings.gen.radius, + edgecolor=gen_edgecolor, + facecolor=self.settings.gen.face_color, ) self.ax.add_patch(patch) - def _draw_storage_line(self, pos_x, pos_y, sub_x, sub_y): + def _draw_gen_line(self, pos_x:float, pos_y:float, + sub_x:float, sub_y:float): codes = [Path.MOVETO, Path.LINETO] verts = [(pos_x, pos_y), (sub_x, sub_y)] path = Path(verts, codes) patch = patches.PathPatch( - path, color=self._storage_line_color, lw=self._storage_line_width - ) - self.ax.add_patch(patch) - - def _draw_storage_bus(self, pos_x, pos_y, norm_dir_x, norm_dir_y, bus_id): - center_x = pos_x + norm_dir_x * self._sub_radius - center_y = pos_y + norm_dir_y * self._sub_radius - face_color = self._line_bus_face_colors[bus_id] - patch = patches.Circle( - (center_x, center_y), radius=self._line_bus_radius, facecolor=face_color + path, + color=self.settings.gen.line_color, + lw=self.settings.load.line_width ) self.ax.add_patch(patch) - def update_storage( - self, - figure, - observation, - storage_name, - storage_id, - storage_bus, - storage_value, - storage_unit, - pos_x, - pos_y, - sub_x, - sub_y, - ): - pass - - def _draw_gen_txt(self, pos_x, pos_y, sub_x, sub_y, text): - dir_x, dir_y = pltu.vec_from_points(sub_x, sub_y, pos_x, pos_y) - off_x, off_y = pltu.norm_from_points(sub_x, sub_y, pos_x, pos_y) - txt_x = pos_x + off_x * self._gen_radius - txt_y = pos_y + off_y * self._gen_radius - ha = self._h_textpos_from_dir(dir_x, dir_y) - va = self._v_textpos_from_dir(dir_x, dir_y) + def _draw_gen_name(self, pos_x:float, pos_y:float, txt:str): self.ax.text( - txt_x, - txt_y, - text, - color=self._gen_txt_color, - wrap=True, - fontsize="small", - horizontalalignment=ha, - verticalalignment=va, + pos_x, pos_y, txt, + color=self.settings.gen.txt_color, + va="center", ha="center", + fontsize="x-small", ) - def _draw_gen_circle(self, pos_x, pos_y, gen_edgecolor): - patch = self._gen_patch( + def draw_gen(self, figure:Figure, observation:BaseObservation, + gen_id:int, gen_name:str, gen_bus:int, + gen_value:float, gen_unit:str, + pos_x:int, pos_y:int, sub_x:int, sub_y:int): + self.xlim[0] = min(self.xlim[0], pos_x - self.settings.gen.radius) + self.xlim[1] = max(self.xlim[1], pos_x + self.settings.gen.radius) + self.ylim[0] = min(self.ylim[0], pos_y - self.settings.gen.radius) + self.ylim[1] = max(self.ylim[1], pos_y + self.settings.gen.radius) + + color_attr = "" if self.settings.gen.color_attr is None else self.settings.gen.color_attr + + if self.settings.gen.color_attr == "prod_p": + vmin = observation.gen_pmin[gen_id] if observation.gen_pmin is not None else None + vmax = observation.gen_pmax[gen_id] if observation.gen_pmax is not None else None + norm = Normalize(vmin=vmin, vmax=vmax) + try: + color_value = norm(observation.prod_p[gen_id]) + except Exception: # Only static info available, don't show generator output + color_value = norm(np.nan) + elif self.settings.gen.color_attr == "gen_type": + norm = Normalize(vmin=0.0, vmax=len(TYPE_GEN)) + color_value = norm(TYPE_GEN[observation.gen_type[gen_id]]) + elif self.settings.gen.active_cnorm is None: + color_value = Normalize(vmin=0.0, vmax=1.0)(0.5) + elif hasattr(observation, color_attr): + color_value = self.settings.gen.active_cnorm(np.abs(getattr(observation, self.settings.gen.color_attr)[gen_id])) + else: + color_value = (self.settings.gen.active_cnorm.vmax - self.settings.gen.active_cnorm.vmin) / 2 + + gen_edge_color = self.settings.gen.cmap(color_value) if np.isfinite(color_value) else "green" + self._draw_gen_line(pos_x, pos_y, sub_x, sub_y) + self._draw_gen_circle(pos_x, pos_y, gen_edge_color) + gen_txt = "" + if self.settings.gen.name: + gen_txt += f'"{gen_name}":\n' + if self.settings.gen.id: + gen_txt += f"id: {gen_id}\n" + if np.isfinite(color_value): + if gen_value is not None and self.settings.gen.display_value: + gen_txt += pltu.format_value_unit(gen_value, gen_unit) + if gen_txt: + self._draw_gen_txt(pos_x, pos_y, sub_x, sub_y, gen_txt) + if self.settings.gen.display_name: + self._draw_gen_name(pos_x, pos_y, str(gen_id)) + gen_dir_x, gen_dir_y = pltu.norm_from_points(sub_x, sub_y, pos_x, pos_y) + self._draw_busbar(sub_x, sub_y, gen_dir_x, gen_dir_y, gen_bus) + + def _draw_storage_txt(self, pos_x:float, pos_y:float, + sub_x:float, sub_y:float, text:str): + self._draw_load_txt(pos_x, pos_y, sub_x, sub_y, text) + + def _draw_storage_name(self, pos_x:float, pos_y:float, txt:str): + self._draw_load_name(pos_x, pos_y, txt) + + def draw_storage(self, figure:Figure, observation:BaseObservation, + storage_id:int, storage_name:str, storage_bus:int, + storage_value:float, storage_unit:str, + pos_x:int, pos_y:int, sub_x:int, sub_y:int): + self.xlim[0] = min(self.xlim[0], pos_x - self.settings.load.radius) + self.xlim[1] = max(self.xlim[1], pos_x + self.settings.load.radius) + self.ylim[0] = min(self.ylim[0], pos_y - self.settings.load.radius) + self.ylim[1] = max(self.ylim[1], pos_y + self.settings.load.radius) + self._draw_storage_line( + pos_x, pos_y, sub_x, sub_y + ) # line from the storage to the substation + + color_attr = "" if self.settings.storage.color_attr is None else self.settings.storage.color_attr + if self.settings.storage.active_cnorm is None: + color_value = 0.5 + elif hasattr(observation, color_attr): + color_value = self.settings.storage.active_cnorm(np.abs(getattr(observation, self.settings.storage.color_attr)[storage_id])) + else: + color_value = (self.settings.storage.active_cnorm.vmax - self.settings.storage.active_cnorm.vmin) / 2 + edge_color = self.settings.storage.cmap(color_value) + self._draw_storage_circle(pos_x, pos_y, stor_edgecolor=edge_color) # storage element + + storage_txt = "" + if self.settings.storage.name: + storage_txt += '"{}":\n'.format(storage_name) + if self.settings.storage.id: + storage_txt += "id: {}\n".format(storage_id) + if storage_value is not None: + storage_txt += pltu.format_value_unit(storage_value, storage_unit) + if storage_txt: + self._draw_storage_txt(pos_x, pos_y, sub_x, sub_y, storage_txt) + if self.settings.storage.display_name: + self._draw_storage_name(pos_x, pos_y, str(storage_id)) + storage_dir_x, storage_dir_y = pltu.norm_from_points(sub_x, sub_y, pos_x, pos_y) + self._draw_busbar(sub_x, sub_y, storage_dir_x, storage_dir_y, storage_bus) + + def _draw_storage_circle(self, pos_x:float, pos_y:float, + stor_edgecolor:Union[object,None]): + patch = self._storage_patch( (pos_x, pos_y), - radius=self._gen_radius, - edgecolor=gen_edgecolor, - facecolor=self._gen_face_color, + radius=self.settings.storage.radius, + facecolor=self.settings.storage.face_color, + edgecolor=stor_edgecolor, ) self.ax.add_patch(patch) - def _draw_gen_line(self, pos_x, pos_y, sub_x, sub_y): + def _draw_storage_line(self, pos_x:float, pos_y:float, sub_x:float, sub_y:float): codes = [Path.MOVETO, Path.LINETO] verts = [(pos_x, pos_y), (sub_x, sub_y)] path = Path(verts, codes) patch = patches.PathPatch( - path, color=self._gen_line_color, lw=self._load_line_width + path, color=self.settings.storage.line_color, + lw=self.settings.storage.line_width ) self.ax.add_patch(patch) - def _draw_gen_name(self, pos_x, pos_y, txt): - self.ax.text( - pos_x, - pos_y, - txt, - color=self._gen_txt_color, - va="center", - ha="center", - fontsize="x-small", - ) - - def _draw_gen_bus(self, pos_x, pos_y, norm_dir_x, norm_dir_y, bus_id): - center_x = pos_x + norm_dir_x * self._sub_radius - center_y = pos_y + norm_dir_y * self._sub_radius - face_color = self._line_bus_face_colors[bus_id] + def _draw_busbar(self, pos_x:float, pos_y:float, + norm_dir_x:float, norm_dir_y:float, bus_id:int): + center_x = pos_x + norm_dir_x * self.settings.sub.radius + center_y = pos_y + norm_dir_y * self.settings.sub.radius + norm = Normalize(vmin=0, vmax=self.observation_space.n_busbar_per_sub) + face_color = self.settings.line.busbar_cmap(norm(bus_id)) patch = patches.Circle( - (center_x, center_y), radius=self._line_bus_radius, facecolor=face_color + (center_x, center_y), facecolor=face_color, + radius=self.settings.line.busbar_radius ) self.ax.add_patch(patch) - def draw_gen( - self, - figure, - observation, - gen_id, - gen_name, - gen_bus, - gen_value, - gen_unit, - pos_x, - pos_y, - sub_x, - sub_y, - ): - self.xlim[0] = min(self.xlim[0], pos_x - self._gen_radius) - self.xlim[1] = max(self.xlim[1], pos_x + self._gen_radius) - self.ylim[0] = min(self.ylim[0], pos_y - self._gen_radius) - self.ylim[1] = max(self.ylim[1], pos_y + self._gen_radius) - hide = False - if isinstance(self._gen_edge_color, str): - # case where the color of the generator is a string (same color for all generators) - gen_color = self._gen_edge_color - else: - my_val = observation.prod_p[gen_id] - n_colors = len(self._gen_edge_color) - 1 - if np.isfinite(my_val): - color_idx = max(0, min(n_colors, int(my_val * n_colors))) - else: - color_idx = 0 - hide = True - gen_color = self._gen_edge_color[color_idx] - - if not hide: - self._draw_gen_line(pos_x, pos_y, sub_x, sub_y) - self._draw_gen_circle(pos_x, pos_y, gen_color) - gen_txt = "" - if self._gen_name: - gen_txt += '"{}":\n'.format(gen_name) - if self._gen_id: - gen_txt += "id: {}\n".format(gen_id) - if gen_value is not None and self._display_gen_value: - gen_txt += pltu.format_value_unit(gen_value, gen_unit) - if gen_txt: - self._draw_gen_txt(pos_x, pos_y, sub_x, sub_y, gen_txt) - if self._display_gen_name: - self._draw_gen_name(pos_x, pos_y, str(gen_id)) - gen_dir_x, gen_dir_y = pltu.norm_from_points(sub_x, sub_y, pos_x, pos_y) - self._draw_gen_bus(sub_x, sub_y, gen_dir_x, gen_dir_y, gen_bus) - - def update_gen( - self, - figure, - observation, - gen_id, - gen_name, - gen_bus, - gen_value, - gen_unit, - pos_x, - pos_y, - sub_x, - sub_y, - ): - pass - - def _draw_powerline_txt(self, pos_or_x, pos_or_y, pos_ex_x, pos_ex_y, text): + def _draw_powerline_txt(self, pos_or_x:float, pos_or_y:float, + pos_ex_x:float, pos_ex_y:float, text:str): pos_x, pos_y = pltu.middle_from_points(pos_or_x, pos_or_y, pos_ex_x, pos_ex_y) off_x, off_y = pltu.orth_norm_from_points( pos_or_x, pos_or_y, pos_ex_x, pos_ex_y ) - txt_x = pos_x + off_x * (self._load_radius / 2) - txt_y = pos_y + off_y * (self._load_radius / 2) - ha = self._h_textpos_from_dir(off_x, off_y) - va = self._v_textpos_from_dir(off_x, off_y) + txt_x = pos_x + off_x * (self.settings.load.radius / 2) + txt_y = pos_y + off_y * (self.settings.load.radius / 2) + ha = self._h_textpos_from_dir(off_x) + va = self._v_textpos_from_dir(off_y) self.ax.text( - txt_x, - txt_y, - text, - color=self._gen_txt_color, + txt_x, txt_y, text, + color=self.settings.gen.txt_color, + ha=ha, va=va, fontsize="small", - horizontalalignment=ha, - verticalalignment=va, ) - def _draw_powerline_line( - self, pos_or_x, pos_or_y, pos_ex_x, pos_ex_y, color, line_style - ): + def _draw_powerline_line(self, pos_or_x:float, pos_or_y:float, + pos_ex_x:float, pos_ex_y:float, + color:Union[object,None], line_style:str): codes = [Path.MOVETO, Path.LINETO] verts = [(pos_or_x, pos_or_y), (pos_ex_x, pos_ex_y)] path = Path(verts, codes) patch = patches.PathPatch( - path, color=color, lw=self._line_color_width, ls=line_style + path, color=color, ls=line_style, + lw=self.settings.line.width, ) self.ax.add_patch(patch) - def _draw_powerline_bus(self, pos_x, pos_y, norm_dir_x, norm_dir_y, bus_id): - center_x = pos_x + norm_dir_x * self._sub_radius - center_y = pos_y + norm_dir_y * self._sub_radius - face_color = self._line_bus_face_colors[bus_id] - patch = patches.Circle( - (center_x, center_y), radius=self._line_bus_radius, facecolor=face_color - ) - self.ax.add_patch(patch) - - def _draw_powerline_arrow( - self, pos_or_x, pos_or_y, pos_ex_x, pos_ex_y, color, watt_value - ): + def _draw_powerline_arrow(self, pos_or_x:float, pos_or_y:float, + pos_ex_x:float, pos_ex_y:float, + color:Union[object,None], watt_value:float): sign = 1.0 if watt_value > 0.0 else -1.0 off = 1.0 if watt_value > 0.0 else 2.0 dx, dy = pltu.norm_from_points(pos_or_x, pos_or_y, pos_ex_x, pos_ex_y) - lx = dx * self._line_arrow_len - ly = dy * self._line_arrow_len - arr_x = pos_or_x + dx * self._sub_radius + off * lx - arr_y = pos_or_y + dy * self._sub_radius + off * ly + lx = dx * self.settings.line.arrow_len + ly = dy * self.settings.line.arrow_len + arr_x = pos_or_x + dx * self.settings.sub.radius+ off * lx + arr_y = pos_or_y + dy * self.settings.sub.radius+ off * ly patch = patches.FancyArrow( - arr_x, - arr_y, - sign * lx, - sign * ly, + arr_x, arr_y, # (x,y) + sign * lx, sign * ly, # (dx, dy) length_includes_head=True, - head_length=self._line_arrow_len, - head_width=self._line_arrow_width, - edgecolor=color, - facecolor=color, + head_length=self.settings.line.arrow_len, + head_width=self.settings.line.arrow_width, + edgecolor=color, facecolor=color, ) self.ax.add_patch(patch) - def assign_line_palette( - self, palette_name="YlOrRd", nb_color=10, line_color_scheme=None - ): + def assign_line_palette(self, color_scheme:Union[Colormap,str,list]="YlOrRd", nb_color:int=-1): """ Assign a new color palette when you want to plot information on the powerline. Parameters ---------- - palette_name: ``str`` + color_scheme: ``Colormap|str|list`` Name of the Maplotlib.plyplot palette to use (name forwarded to `plt.get_cmap(palette_name)`) + nb_color: ``int`` - Number of color to use + Number of color to use. Examples ------- @@ -753,25 +828,13 @@ def assign_line_palette( Some palette are available there `colormaps `_ """ - if line_color_scheme is None: - palette = plt.get_cmap(palette_name) - cols = [] - for i in range(1, nb_color + 1): - cols.append(palette(i / nb_color)) - self._line_color_scheme = cols - else: - self._line_color_scheme = copy.deepcopy(line_color_scheme) - - def restore_line_palette(self): - self._line_color_scheme = self._line_color_scheme_orig - - def assign_gen_palette( - self, - palette_name="YlOrRd", - nb_color=10, - increase_gen_size=None, - gen_line_width=None, - ): + self.settings.line.cmap = self._convert_colors_to_cmap(color_scheme, nb_color=nb_color) + + def restore_line_settings(self): + self.settings.line = copy.deepcopy(self.orig_settings.line) + + def assign_gen_palette(self, color_scheme:Union[Colormap,str,list]="YlOrRd", nb_color:int=-1, + increase_gen_size:Union[float,None]=None, gen_line_width:Union[float,None]=None): """ Assign a new color palette when you want to plot information on the generator. @@ -779,6 +842,7 @@ def assign_gen_palette( ---------- palette_name: ``str`` Name of the Maplotlib.plyplot palette to use (name forwarded to `plt.get_cmap(palette_name)`) + nb_color: ``int`` Number of color to use @@ -806,64 +870,44 @@ def assign_gen_palette( Some palette are available there `colormaps `_ """ - if palette_name is not None and nb_color > 0: - # the user changed the palette - palette = plt.get_cmap(palette_name) - cols = [] - for i in range(1, nb_color + 1): - cols.append(palette(i / nb_color)) - self._gen_edge_color = cols - if increase_gen_size is not None: - # the user changed the generator sizes - self._gen_radius = float(increase_gen_size) * self._gen_radius_orig - if gen_line_width is not None: - # the user changed the generator line width - self._gen_line_width = float(gen_line_width) - - def restore_gen_palette(self): - """restore every properties of the default generator layout""" - self._gen_edge_color = self._gen_edge_color_orig - self._gen_radius = self._gen_radius_orig - self._gen_line_width = self._gen_line_width_orig - - def draw_powerline( - self, - figure, - observation, - line_id, - line_name, - connected, - line_value, - line_unit, - or_bus, - pos_or_x, - pos_or_y, - ex_bus, - pos_ex_x, - pos_ex_y, - ): - rho = observation.rho[line_id] - n_colors = len(self._line_color_scheme) - 1 - hide = False - if np.isfinite(rho): - color_idx = max(0, min(n_colors, int(rho * n_colors))) + self.settings.gen.cmap = self._convert_colors_to_cmap(color_scheme, nb_color=nb_color) + + if increase_gen_size is not None: # The user changed the generator sizes + self.settings.gen.radius = float(increase_gen_size) * self.orig_settings.gen.radius + if gen_line_width is not None: # The user changed the generator line width + self.settings.gen.line_width = float(gen_line_width) + + def restore_gen_settings(self): + """Restore every properties of the default generator layout""" + self.settings.gen = copy.deepcopy(self.orig_settings.gen) + + def restore_settings(self): + """Restore every default setting""" + self.settings = copy.deepcopy(self.orig_settings) + + def draw_powerline(self, figure:Figure, observation:BaseObservation, + line_id:int, line_name:str, connected:bool, + line_value:float, line_unit:str, + or_bus:int, pos_or_x:int, pos_or_y:int, + ex_bus:int, pos_ex_x:int, pos_ex_y:int): + color_attr = "" if self.settings.line.color_attr is None else self.settings.line.color_attr + if self.settings.line.active_cnorm is None: + color_value = 0.0 + elif hasattr(observation, self.settings.line.color_attr): + color_value = self.settings.line.active_cnorm(np.abs(getattr(observation, color_attr)[line_id])) else: - color_idx = 0 - hide = True - - color = "black" - if not hide: - if connected and rho > 0.0: - color = self._line_color_scheme[color_idx] + color_value = (self.settings.line.active_cnorm.vmax - self.settings.line.active_cnorm.vmin) / 2 + if np.isfinite(color_value): + color = self.settings.line.cmap(color_value) line_style = "-" if connected else "--" self._draw_powerline_line( pos_or_x, pos_or_y, pos_ex_x, pos_ex_y, color, line_style ) # Deal with line text configurations txt = "" - if self._line_name: + if self.settings.line.name: txt += '"{}"\n'.format(line_name) - if self._line_id: + if self.settings.line.id: txt += "id: {}\n".format(str(line_id)) if line_value is not None: txt += pltu.format_value_unit(line_value, line_unit) @@ -873,142 +917,109 @@ def draw_powerline( or_dir_x, or_dir_y = pltu.norm_from_points( pos_or_x, pos_or_y, pos_ex_x, pos_ex_y ) - self._draw_powerline_bus(pos_or_x, pos_or_y, or_dir_x, or_dir_y, or_bus) + self._draw_busbar(pos_or_x, pos_or_y, or_dir_x, or_dir_y, or_bus) ex_dir_x, ex_dir_y = pltu.norm_from_points( pos_ex_x, pos_ex_y, pos_or_x, pos_or_y ) - self._draw_powerline_bus(pos_ex_x, pos_ex_y, ex_dir_x, ex_dir_y, ex_bus) + self._draw_busbar(pos_ex_x, pos_ex_y, ex_dir_x, ex_dir_y, ex_bus) watt_value = observation.p_or[line_id] - if rho > 0.0 and np.abs(watt_value) >= 1e-7: + if color_value > 0.0 and np.abs(watt_value) >= 1e-7: self._draw_powerline_arrow( pos_or_x, pos_or_y, pos_ex_x, pos_ex_y, color, watt_value ) - def update_powerline( - self, - figure, - observation, - line_id, - line_name, - connected, - line_value, - line_unit, - or_bus, - pos_or_x, - pos_or_y, - ex_bus, - pos_ex_x, - pos_ex_y, - ): - pass - def _get_gen_legend(self): """super complex function to display the proper shape in the legend""" - if isinstance(self._gen_edge_color, str): - gen_legend_col = self._gen_edge_color - else: - gen_legend_col = self._gen_edge_color[int(len(self._gen_edge_color) / 2)] - my_res = self._gen_resolution + gen_legend_col = "green" # self.settings.gen.cmap(0.5) + settings = self.settings.gen class GenObjectHandler: def legend_artist(self, legend, orig_handle, fontsize, handlebox): xdescent, ydescent = handlebox.xdescent, handlebox.ydescent width, height = handlebox.width, handlebox.height center = 0.5 * width - 0.5 * xdescent, 0.5 * height - 0.5 * ydescent - pp_ = GenDraw( + pp_ = GenPatch( xy=center, radius=min(width, height), - facecolor="w", + facecolor=settings.face_color, edgecolor=gen_legend_col, transform=handlebox.get_transform(), - resolution=my_res, + n_vertices=settings.vertices, ) handlebox.add_artist(pp_) return pp_ gen_legend = self._gen_patch( (0, 0), - facecolor=self._gen_face_color, + facecolor=self.settings.gen.face_color, edgecolor=gen_legend_col, - radius=self._gen_radius, + radius=self.settings.gen.radius, ) return gen_legend, GenObjectHandler() def _get_load_legend(self): """super complex function to display the proper shape in the legend""" - if isinstance(self._load_edge_color, str): - load_legend_col = self._load_edge_color - else: - load_legend_col = self._load_edge_color[int(len(self._load_edge_color) / 2)] - my_res = self._load_resolution + load_legend_col = "orange" # self.settings.load.cmap(0.5) + settings = self.settings.load class LoadObjectHandler: def legend_artist(self, legend, orig_handle, fontsize, handlebox): xdescent, ydescent = handlebox.xdescent, handlebox.ydescent width, height = handlebox.width, handlebox.height center = 0.5 * width - 0.5 * xdescent, 0.5 * height - 0.5 * ydescent - pp_ = LoadDraw( + pp_ = LoadPatch( xy=center, radius=min(width, height), - facecolor="w", + facecolor=settings.face_color, edgecolor=load_legend_col, transform=handlebox.get_transform(), - resolution=my_res, + n_vertices=settings.vertices, ) handlebox.add_artist(pp_) return pp_ load_legend = self._load_patch( (0, 0), - facecolor=self._load_face_color, + facecolor=self.settings.load.face_color, edgecolor=load_legend_col, - radius=self._load_radius, + radius=self.settings.load.radius, ) return load_legend, LoadObjectHandler() def _get_storage_legend(self): """super complex function to display the proper shape in the legend""" - if isinstance(self._storage_edge_color, str): - storage_legend_col = self._storage_edge_color - else: - storage_legend_col = self._storage_edge_color[ - int(len(self._storage_edge_color) / 2) - ] - my_res = self._storage_resolution + storage_legend_col = "purple" # self.settings.storage.cmap(0.5) + settings = self.settings.storage class StorageObjectHandler: def legend_artist(self, legend, orig_handle, fontsize, handlebox): xdescent, ydescent = handlebox.xdescent, handlebox.ydescent width, height = handlebox.width, handlebox.height center = 0.5 * width - 0.5 * xdescent, 0.5 * height - 0.5 * ydescent - pp_ = StorageDraw( + pp_ = StoragePatch( xy=center, radius=min(width, height), - facecolor="w", + facecolor=settings.face_color, edgecolor=storage_legend_col, transform=handlebox.get_transform(), - resolution=my_res, + n_vertices=settings.vertices, ) handlebox.add_artist(pp_) return pp_ storage_legend = self._storage_patch( (0, 0), - facecolor=self._storage_face_color, + facecolor=self.settings.storage.face_color, edgecolor=storage_legend_col, - radius=self._storage_radius, + radius=self.settings.storage.radius, ) return storage_legend, StorageObjectHandler() - def draw_legend(self, figure, observation): + def draw_legend(self, figure:Figure, observation:BaseObservation): title_str = observation.env_name if hasattr(observation, "month"): - title_str = "{:02d}/{:02d} {:02d}:{:02d}".format( - observation.day, - observation.month, - observation.hour_of_day, - observation.minute_of_hour, - ) + title_str = (f"{observation.day:02d}/{observation.month:02d} " + + f"{observation.hour_of_day:02d}:{observation.minute_of_hour:02d}") # generate the right legend for generator gen_legend, gen_handler = self._get_gen_legend() @@ -1017,33 +1028,25 @@ def draw_legend(self, figure, observation): # generate the correct legend for storage storage_legend, storage_handler = self._get_storage_legend() + bus_norm = Normalize(vmin=-1, vmax=self.observation_space.n_busbar_per_sub) legend_help = [ Line2D([0], [0], color="black", lw=1), - Line2D([0], [0], color=self._sub_edge_color, lw=3), + Line2D([0], [0], color=self.settings.sub.cmap(0.5), lw=3), load_legend, gen_legend, - storage_legend, - Line2D([0], [0], marker="o", color=self._line_bus_face_colors[0]), - Line2D([0], [0], marker="o", color=self._line_bus_face_colors[1]), - Line2D([0], [0], marker="o", color=self._line_bus_face_colors[2]), - ] + storage_legend + ] + [Line2D([0], [0], marker="o", color=self.settings.line.busbar_cmap(bus_norm(i))) + for i in range(-1, self.observation_space.n_busbar_per_sub)] self.legend = self.ax.legend( - legend_help, - [ - "powerline", - "substation", - "load", - "generator", - "storage", - "no bus", - "bus 1", - "bus 2", - ], + handles=legend_help, + labels=["powerline", "substation", "load", + "generator", "storage", "no bus"] + + [f"bus {i+1}" for i in range(self.observation_space.n_busbar_per_sub)], title=title_str, handler_map={ - GenDraw: gen_handler, - LoadDraw: load_handler, - StorageDraw: storage_handler, + GenPatch: gen_handler, + LoadPatch: load_handler, + StoragePatch: storage_handler, }, ) # Hide axis @@ -1054,8 +1057,32 @@ def draw_legend(self, figure, observation): # save the figure self.figure = figure + + def plot_obs(self, observation:BaseObservation, *args, **kwargs): + self._set_active_color_norms(observation, **kwargs) + fig = super(PlotMatplot, self).plot_obs(observation, *args, **kwargs) + if self.settings.gen.color_attr == "gen_type": + self.add_legend_gentype() + self._set_active_color_norms(observation) # Return to default + return fig + + def plot_info(self, *args, **kwargs): + fig = super(PlotMatplot, self).plot_info(*args, **kwargs) + if self.settings.gen.color_attr == "gen_type": + self.add_legend_gentype() + self.restore_settings() + return fig + + def plot_layout(self): + """ + This function plot the layout of the grid, as well as the object. You will see the name of each elements and + their id. + """ + return self.plot_info( + observation=None, figure=None, redraw=True, gen_values=np.zeros(self.observation_space.n_gen), + ) - def plot_postprocess(self, figure, observation, update): + def plot_postprocess(self, figure:Figure, observation:BaseObservation, update:bool): if not update: xmin = self.xlim[0] - self.xpad xmax = self.xlim[1] + self.xpad @@ -1066,57 +1093,24 @@ def plot_postprocess(self, figure, observation, update): figure.tight_layout() def _save_plot_charact(self): - _gen_edge_color_orig = self._gen_edge_color - _gen_radius_orig = self._gen_radius - _gen_line_width_orig = self._gen_line_width - _display_gen_value = self._display_gen_value - _display_gen_name = self._display_gen_name - _display_sub_name = self._display_sub_name - _display_load_name = self._display_load_name - - return ( - _gen_edge_color_orig, - _gen_radius_orig, - _gen_line_width_orig, - _display_gen_value, - _display_gen_name, - _display_sub_name, - _display_load_name, - ) + return copy.deepcopy(self.settings) def _restore_plot_charact(self, data): - ( - _gen_edge_color_orig, - _gen_radius_orig, - _gen_line_width_orig, - _display_gen_value, - _display_gen_name, - _display_sub_name, - _display_load_name, - ) = data - self._gen_edge_color = _gen_edge_color_orig - self._gen_radius = _gen_radius_orig - self._gen_line_width = _gen_line_width_orig - self._display_gen_value = _display_gen_value - self._display_gen_name = _display_gen_name - self._display_sub_name = _display_sub_name - self._display_load_name = _display_load_name - - def plot_gen_type(self, increase_gen_size=1.5, gen_line_width=3): + self.settings = data + + def plot_gen_type(self, increase_gen_size:float=1.5, gen_line_width:float=3): # save the sate of the generators config data = self._save_plot_charact() + self.settings.gen.radius = self.settings.gen.radius * increase_gen_size + self.settings.gen.line_width = gen_line_width # do the plot - self._display_gen_value = False - self._display_gen_name = False - self._display_sub_name = False - self._display_load_name = False - self.assign_gen_palette( - nb_color=0, - increase_gen_size=increase_gen_size, - gen_line_width=gen_line_width, - ) - self._gen_edge_color = [COLOR_GEN[i] for i in range(len(TYPE_GEN))] + self.settings.gen.display_value = False + self.settings.gen.display_name = False + self.settings.sub.display_name= False + self.settings.load.display_name = False + + self.settings.gen.cmap = self._convert_colors_to_cmap([COLOR_GEN[i] for i in range(len(TYPE_GEN))]) gen_values = [TYPE_GEN[el] for el in self.observation_space.gen_type] self.figure = self.plot_info(gen_values=gen_values, coloring="gen") self.add_legend_gentype() @@ -1126,23 +1120,18 @@ def plot_gen_type(self, increase_gen_size=1.5, gen_line_width=3): return self.figure - def plot_current_dispatch( - self, - obs, - do_plot_actual_dispatch=True, - increase_gen_size=1.5, - gen_line_width=3, - palette_name="coolwarm", - ): + def plot_current_dispatch(self, obs:BaseObservation, do_plot_actual_dispatch:bool=True, + increase_gen_size:float=1.5, gen_line_width:float=3, + palette_name:str="coolwarm"): # save the sate of the generators config data = self._save_plot_charact() # do the plot - self._display_sub_name = False - self._display_load_name = False + self.settings.sub.display_name = False + self.settings.load.display_name = False self.assign_gen_palette( nb_color=5, - palette_name=palette_name, + color_scheme=palette_name, increase_gen_size=increase_gen_size, gen_line_width=gen_line_width, ) @@ -1159,12 +1148,12 @@ def plot_current_dispatch( return self.figure - def add_legend_gentype(self, loc="lower right"): - """add the legend for each generator type""" + def add_legend_gentype(self, loc:str="lower right"): + """Add the legend for each generator type""" keys = sorted(TYPE_GEN.keys()) ax_ = self.figure.axes[0] legend_help = [ Line2D([0], [0], color=COLOR_GEN[TYPE_GEN[k]], label=k) for k in keys ] _ = ax_.legend(legend_help, keys, title="generator types", loc=loc) - ax_.add_artist(self.legend) + ax_.add_artist(self.legend) \ No newline at end of file