From 96e1ca5bcd82399a856a0095a028e89c652fde31 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Mon, 4 Nov 2024 11:45:34 +0100 Subject: [PATCH 01/60] Add: AllowShedding attribute to GridObjects Signed-off-by: Xavier Weiss --- getting_started/13_Shedding.ipynb | 122 +++++++++++++++++++++++ grid2op/Action/baseAction.py | 10 +- grid2op/Backend/backend.py | 8 +- grid2op/Backend/educPandaPowerBackend.py | 2 + grid2op/Backend/pandaPowerBackend.py | 10 +- grid2op/Converter/BackendConverter.py | 4 +- grid2op/Environment/baseEnv.py | 4 + grid2op/Environment/environment.py | 13 ++- grid2op/Environment/multiMixEnv.py | 8 +- grid2op/MakeEnv/Make.py | 14 +++ grid2op/MakeEnv/MakeFromPath.py | 5 + grid2op/Observation/baseObservation.py | 4 +- grid2op/Space/GridObjects.py | 34 ++++++- grid2op/Space/__init__.py | 4 +- grid2op/tests/test_shedding.py | 56 +++++++++++ 15 files changed, 275 insertions(+), 23 deletions(-) create mode 100644 getting_started/13_Shedding.ipynb create mode 100644 grid2op/tests/test_shedding.py diff --git a/getting_started/13_Shedding.ipynb b/getting_started/13_Shedding.ipynb new file mode 100644 index 00000000..c70d9143 --- /dev/null +++ b/getting_started/13_Shedding.ipynb @@ -0,0 +1,122 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Shedding\n", + "In emergency conditions, it may be possible / necessary for a grid operator to disconnect certain loads, generators, or other components in order to prevent a larger blackout. This notebook explores how this can be achieved in Grid2OP. \n", + "\n", + "By default shedding is disabled in all environments, to enable it provide set the environment parameter ALLOW_SHEDDING to True.\n", + "\n", + "Shed load:\n", + "* **Vector** :: $\\text{n\\_load}$\n", + "* **List** :: $[(\\text{load\\_id}, \\text{status})]$\n", + "* **Dictionary** :: $\\text{load\\_name}: \\text{status}$\n", + "\n", + "Shed generator:\n", + "* **Vector** :: $\\text{n\\_gen}$\n", + "* **List** :: $[(\\text{gen\\_id}, \\text{status})]$\n", + "* **Dictionary** :: $\\text{gen\\_name}: \\text{status}$\n", + "\n", + "Where $\\text{status}$ is a boolean (True/False)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import grid2op\n", + "from grid2op.Parameters import Parameters\n", + "\n", + "from grid2op.PlotGrid import PlotMatplot\n", + "from pathlib import Path\n", + "\n", + "\n", + "p = Parameters()\n", + "p.MAX_SUB_CHANGED = 5\n", + "\n", + "data_path = Path.cwd() / \"grid2op\" / \"data\"\n", + "env = grid2op.make(data_path / \"rte_case5_example\", allow_shedding=True, n_busbar=3, param=p)\n", + "plotter = PlotMatplot(env.observation_space, load_name=True, gen_name=True, dpi=150)\n", + "env.set_id(\"00\")\n", + "obs = env.reset()\n", + "\n", + "print(f\"Loads: {env.n_load}, Generators: {env.n_gen}, Storage: {env.n_storage}\")\n", + "plotter.plot_obs(obs, figure=plt.figure(figsize=(8,5)))\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "load_lookup = {name:i for i,name in enumerate(env.name_load)}\n", + "act = env.action_space({\"set_bus\":[(env.load_pos_topo_vect[load_lookup[\"load_4_2\"]], -1)]})\n", + "print(act)\n", + "env.set_id(\"00\")\n", + "init_obs = env.reset()\n", + "obs, reward, done, info = env.step(act)\n", + "plotter.plot_obs(obs, figure=plt.figure(figsize=(8,5)))\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "load_shed_mask = (init_obs + act).topo_vect[env.load_pos_topo_vect]\n", + "gen_shed_mask = (init_obs + act).topo_vect[env.gen_pos_topo_vect]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandapower as pp\n", + "from pandapower.plotting import simple_plot\n", + "network = env.backend._grid.deepcopy()\n", + "display(network.res_line.loc[:, [\"p_from_mw\", \"p_to_mw\", \"q_from_mvar\", \"q_to_mvar\"]])\n", + "pp.runpp(network,\n", + " check_connectivity=False,\n", + " init=\"dc\",\n", + " lightsim2grid=False,\n", + " max_iteration=10,\n", + " distributed_slack=False,\n", + ")\n", + "display(network.res_line.loc[:, [\"p_from_mw\", \"p_to_mw\", \"q_from_mvar\", \"q_to_mvar\"]])" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv_grid2op", + "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.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 1b54e77e..3e2a2901 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -493,7 +493,7 @@ def __init__(self, _names_chronics_to_backend: Optional[Dict[Literal["loads", "p self._modif_alert = False @classmethod - def process_shunt_satic_data(cls): + def process_shunt_static_data(cls): if not cls.shunts_data_available: # this is really important, otherwise things from grid2op base types will be affected cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) @@ -506,7 +506,7 @@ def process_shunt_satic_data(cls): except ValueError: pass cls.attr_list_set = set(cls.attr_list_vect) - return super().process_shunt_satic_data() + return super().process_shunt_static_data() def copy(self) -> "BaseAction": # sometimes this method is used... @@ -573,8 +573,8 @@ def __copy__(self) -> "BaseAction": return res @classmethod - def process_shunt_satic_data(cls): - return super().process_shunt_satic_data() + def process_shunt_static_data(cls): + return super().process_shunt_static_data() def __deepcopy__(self, memodict={}) -> "BaseAction": res = type(self)() @@ -1098,7 +1098,7 @@ def __eq__(self, other) -> bool: self._change_bus_vect == other._change_bus_vect ): return False - + # shunts are the same if type(self).shunts_data_available: if self.n_shunt != other.n_shunt: diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index b71c8532..84dd0d5f 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -34,7 +34,7 @@ DivergingPowerflow, Grid2OpException, ) -from grid2op.Space import GridObjects, DEFAULT_N_BUSBAR_PER_SUB +from grid2op.Space import GridObjects, DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_SHEDDING # TODO method to get V and theta at each bus, could be in the same shape as check_kirchoff @@ -122,8 +122,8 @@ class Backend(GridObjects, ABC): ERR_INIT_POWERFLOW : str = "Power cannot be computed on the first time step, please check your data." def __init__(self, - detailed_infos_for_cascading_failures: bool=False, - can_be_copied: bool=True, + detailed_infos_for_cascading_failures:bool=False, + can_be_copied:bool=True, allow_shedding:bool=DEFAULT_ALLOW_SHEDDING, **kwargs): """ Initialize an instance of Backend. This does nothing per se. Only the call to :func:`Backend.load_grid` @@ -179,6 +179,8 @@ def __init__(self, #: There is a difference between this and the class attribute. #: You should not worry about the class attribute of the backend in :func:`Backend.apply_action` self.n_busbar_per_sub: int = DEFAULT_N_BUSBAR_PER_SUB + + self.allow_shedding: bool = allow_shedding def can_handle_more_than_2_busbar(self): """ diff --git a/grid2op/Backend/educPandaPowerBackend.py b/grid2op/Backend/educPandaPowerBackend.py index a56d6600..4520b1c4 100644 --- a/grid2op/Backend/educPandaPowerBackend.py +++ b/grid2op/Backend/educPandaPowerBackend.py @@ -67,6 +67,7 @@ class EducPandaPowerBackend(Backend): def __init__(self, detailed_infos_for_cascading_failures : Optional[bool]=False, + allow_shedding:bool=False, can_be_copied : Optional[bool]=True): """ Nothing much to do here except initializing what you would need (a tensorflow session, link to some @@ -83,6 +84,7 @@ def __init__(self, self, detailed_infos_for_cascading_failures=detailed_infos_for_cascading_failures, can_be_copied=can_be_copied, + allow_shedding=allow_shedding, # extra arguments that might be needed for building such a backend # these extra kwargs will be stored (without copy) in the # base class and used when another backend will be created diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index 98711ce4..677fd47c 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -122,6 +122,7 @@ def __init__( max_iter : int=10, can_be_copied: bool=True, with_numba: bool=NUMBA_, + allow_shedding:bool=False, ): from grid2op.MakeEnv.Make import _force_test_dataset if _force_test_dataset(): @@ -136,7 +137,8 @@ def __init__( lightsim2grid=lightsim2grid, dist_slack=dist_slack, max_iter=max_iter, - with_numba=with_numba + with_numba=with_numba, + allow_shedding=allow_shedding, ) self.with_numba : bool = with_numba self.prod_pu_to_kv : Optional[np.ndarray] = None @@ -541,7 +543,7 @@ def load_grid(self, pp.create_bus(self._grid, index=ind, **el) self._init_private_attrs() self._aux_run_pf_init() # run yet another powerflow with the added buses - + # do this at the end self._in_service_line_col_id = int((self._grid.line.columns == "in_service").nonzero()[0][0]) self._in_service_trafo_col_id = int((self._grid.trafo.columns == "in_service").nonzero()[0][0]) @@ -1024,14 +1026,14 @@ def _aux_runpf_pp(self, is_dc: bool): # else: # self._pf_init = "auto" - if (~self._grid.load["in_service"]).any(): + if not self.allow_shedding and (~self._grid.load["in_service"]).any(): # TODO see if there is a better way here -> do not handle this here, but rather in Backend._next_grid_state raise pp.powerflow.LoadflowNotConverged("Disconnected load: for now grid2op cannot handle properly" " disconnected load. If you want to disconnect one, say it" " consumes 0. instead. Please check loads: " f"{(~self._grid.load['in_service'].values).nonzero()[0]}" ) - if (~self._grid.gen["in_service"]).any(): + if not self.allow_shedding and (~self._grid.gen["in_service"]).any(): # TODO see if there is a better way here -> do not handle this here, but rather in Backend._next_grid_state raise pp.powerflow.LoadflowNotConverged("Disconnected gen: for now grid2op cannot handle properly" " disconnected generators. If you want to disconnect one, say it" diff --git a/grid2op/Converter/BackendConverter.py b/grid2op/Converter/BackendConverter.py index 4c023c85..1dda02fe 100644 --- a/grid2op/Converter/BackendConverter.py +++ b/grid2op/Converter/BackendConverter.py @@ -95,6 +95,7 @@ def __init__( use_target_backend_name=False, kwargs_target_backend=None, kwargs_source_backend=None, + allow_shedding:bool=False, ): Backend.__init__( self, @@ -102,6 +103,7 @@ def __init__( use_target_backend_name=use_target_backend_name, kwargs_target_backend=kwargs_target_backend, kwargs_source_backend=kwargs_source_backend, + allow_shedding=allow_shedding, ) difcf = detailed_infos_for_cascading_failures if kwargs_source_backend is None: @@ -165,7 +167,7 @@ def load_grid(self, path=None, filename=None): # register the "n_busbar_per_sub" (set for the backend class) # TODO in case source supports the "more than 2" feature but not target # it's unclear how I can "reload" the grid... - from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB + from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_SHEDDING type(self.source_backend).set_n_busbar_per_sub(DEFAULT_N_BUSBAR_PER_SUB) type(self.target_backend).set_n_busbar_per_sub(DEFAULT_N_BUSBAR_PER_SUB) self.cannot_handle_more_than_2_busbar() diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index 5d8e76a2..448a867e 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -334,6 +334,7 @@ def __init__( highres_sim_counter=None, update_obs_after_reward=False, n_busbar=2, + allow_shedding:bool=False, _is_test: bool = False, # TODO not implemented !! _init_obs: Optional[BaseObservation] =None, _local_dir_cls=None, @@ -360,6 +361,8 @@ def __init__( self._raw_backend_class = _raw_backend_class self._n_busbar = n_busbar # env attribute not class attribute ! + self.allow_shedding = allow_shedding + if other_rewards is None: other_rewards = {} if kwargs_attention_budget is None: @@ -656,6 +659,7 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None): if dict_ is None: dict_ = {} new_obj._n_busbar = self._n_busbar + new_obj.allow_shedding = self.allow_shedding new_obj._init_grid_path = copy.deepcopy(self._init_grid_path) new_obj._init_env_path = copy.deepcopy(self._init_env_path) diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 164e7203..1955f976 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -32,7 +32,7 @@ from grid2op.Environment.baseEnv import BaseEnv from grid2op.Opponent import BaseOpponent, NeverAttackBudget from grid2op.operator_attention import LinearAttentionBudget -from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB +from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_SHEDDING from grid2op.typing_variables import RESET_OPTIONS_TYPING, N_BUSBAR_PER_SUB_TYPING from grid2op.MakeEnv.PathUtils import USE_CLASS_IN_FILE @@ -86,6 +86,7 @@ def __init__( parameters, name="unknown", n_busbar : N_BUSBAR_PER_SUB_TYPING=DEFAULT_N_BUSBAR_PER_SUB, + allow_shedding:bool=DEFAULT_ALLOW_SHEDDING, names_chronics_to_backend=None, actionClass=TopologyAction, observationClass=CompleteObservation, @@ -156,6 +157,7 @@ def __init__( highres_sim_counter=highres_sim_counter, update_obs_after_reward=_update_obs_after_reward, n_busbar=n_busbar, # TODO n_busbar_per_sub different num per substations: read from a config file maybe (if not provided by the user) + allow_shedding=allow_shedding, name=name, _raw_backend_class=_raw_backend_class if _raw_backend_class is not None else type(backend), _init_obs=_init_obs, @@ -278,8 +280,10 @@ def _init_backend( # this is due to the class attribute type(self.backend).set_env_name(self.name) type(self.backend).set_n_busbar_per_sub(self._n_busbar) + if self._compat_glop_version is not None: type(self.backend).glop_version = self._compat_glop_version + self.backend.load_grid( self._init_grid_path ) # the real powergrid of the environment @@ -299,6 +303,9 @@ def _init_backend( f'not be able to use the renderer, plot the grid etc. The error was "{exc_}"' ) + # Shedding + self.backend.allow_shedding = self.allow_shedding + # alarm set up self.load_alarm_data() self.load_alert_data() @@ -2204,7 +2211,8 @@ def init_obj_from_kwargs(cls, _read_from_local_dir, _local_dir_cls, _overload_name_multimix, - n_busbar=DEFAULT_N_BUSBAR_PER_SUB + n_busbar=DEFAULT_N_BUSBAR_PER_SUB, + allow_shedding=DEFAULT_ALLOW_SHEDDING, ): res = cls(init_env_path=init_env_path, init_grid_path=init_grid_path, @@ -2237,6 +2245,7 @@ def init_obj_from_kwargs(cls, observation_bk_class=observation_bk_class, observation_bk_kwargs=observation_bk_kwargs, n_busbar=int(n_busbar), + allow_shedding=bool(allow_shedding), _raw_backend_class=_raw_backend_class, _read_from_local_dir=_read_from_local_dir, _local_dir_cls=_local_dir_cls, diff --git a/grid2op/Environment/multiMixEnv.py b/grid2op/Environment/multiMixEnv.py index be250847..b5ef41f9 100644 --- a/grid2op/Environment/multiMixEnv.py +++ b/grid2op/Environment/multiMixEnv.py @@ -13,7 +13,7 @@ from typing import Any, Dict, Tuple, Union, List, Literal from grid2op.dtypes import dt_int, dt_float -from grid2op.Space import GridObjects, RandomObject, DEFAULT_N_BUSBAR_PER_SUB +from grid2op.Space import GridObjects, RandomObject, DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_SHEDDING from grid2op.Exceptions import EnvError, Grid2OpException from grid2op.Observation import BaseObservation from grid2op.MakeEnv.PathUtils import USE_CLASS_IN_FILE @@ -165,6 +165,7 @@ def __init__( logger=None, experimental_read_from_local_dir=None, n_busbar=DEFAULT_N_BUSBAR_PER_SUB, + allow_shedding=DEFAULT_ALLOW_SHEDDING, _add_to_name="", # internal, for test only, do not use ! _compat_glop_version=None, # internal, for test only, do not use ! _test=False, @@ -207,6 +208,7 @@ def __init__( _add_to_name, _compat_glop_version, n_busbar, + allow_shedding, _test, experimental_read_from_local_dir, multi_env_name, @@ -235,6 +237,7 @@ def __init__( _add_to_name, _compat_glop_version, n_busbar, + allow_shedding, _test, experimental_read_from_local_dir, multi_env_name, @@ -301,6 +304,7 @@ def _aux_create_a_mix(self, _add_to_name, _compat_glop_version, n_busbar, + allow_shedding, _test, experimental_read_from_local_dir, multi_env_name, @@ -335,6 +339,7 @@ def _aux_create_a_mix(self, _add_to_name=_add_to_name, _compat_glop_version=_compat_glop_version, n_busbar=n_busbar, + allow_shedding=allow_shedding, test=_test, logger=this_logger, experimental_read_from_local_dir=experimental_read_from_local_dir, @@ -345,6 +350,7 @@ def _aux_create_a_mix(self, mix = make( mix_path, n_busbar=n_busbar, + allow_shedding=allow_shedding, _add_to_name=_add_to_name, _compat_glop_version=_compat_glop_version, test=_test, diff --git a/grid2op/MakeEnv/Make.py b/grid2op/MakeEnv/Make.py index 11a202e5..1e954d72 100644 --- a/grid2op/MakeEnv/Make.py +++ b/grid2op/MakeEnv/Make.py @@ -248,6 +248,7 @@ def _aux_make_multimix( test=False, experimental_read_from_local_dir=False, n_busbar=2, + allow_shedding=False, _add_to_name="", _compat_glop_version=None, _overload_name_multimix=None, @@ -262,6 +263,7 @@ def _aux_make_multimix( dataset_path, experimental_read_from_local_dir=experimental_read_from_local_dir, n_busbar=n_busbar, + allow_shedding=allow_shedding, _test=test, _add_to_name=_add_to_name, _compat_glop_version=_compat_glop_version, @@ -286,6 +288,7 @@ def make( logger: Optional[logging.Logger]=None, experimental_read_from_local_dir : bool=False, n_busbar=2, + allow_shedding=False, _add_to_name : str="", _compat_glop_version : Optional[str]=None, _overload_name_multimix : Optional[str]=None, # do not use ! @@ -304,6 +307,9 @@ def make( .. versionadded:: 1.10.0 The `n_busbar` parameters + + .. versionadded:: 1.11.0 + The `allow_shedding` parameter Parameters ---------- @@ -328,6 +334,9 @@ def make( n_busbar: ``int`` Number of independant busbars allowed per substations. By default it's 2. + + allow_shedding: ``bool`` + Whether to allow loads and generators to be shed without a game over. By default it's False. kwargs: Other keyword argument to give more control on the environment you are creating. See @@ -383,6 +392,7 @@ def make( raise Grid2OpException(f"n_busbar parameters should be convertible to integer, but we have " f"int(n_busbar) = {n_busbar_int} != {n_busbar}") + accepted_kwargs = ERR_MSG_KWARGS.keys() | {"dataset", "test"} for el in kwargs: if el not in accepted_kwargs: @@ -436,6 +446,7 @@ def make_from_path_fn_(*args, **kwargs): _compat_glop_version=_compat_glop_version_tmp, _overload_name_multimix=_overload_name_multimix, n_busbar=n_busbar, + allow_shedding=allow_shedding, **kwargs ) @@ -482,6 +493,7 @@ def make_from_path_fn_(*args, **kwargs): dataset_path=ds_path, logger=logger, n_busbar=n_busbar, + allow_shedding=allow_shedding, _add_to_name=_add_to_name, _compat_glop_version=_compat_glop_version, experimental_read_from_local_dir=experimental_read_from_local_dir, @@ -497,6 +509,7 @@ def make_from_path_fn_(*args, **kwargs): real_ds_path, logger=logger, n_busbar=n_busbar, + allow_shedding=allow_shedding, experimental_read_from_local_dir=experimental_read_from_local_dir, _overload_name_multimix=_overload_name_multimix, **kwargs @@ -517,6 +530,7 @@ def make_from_path_fn_(*args, **kwargs): dataset_path=real_ds_path, logger=logger, n_busbar=n_busbar, + allow_shedding=allow_shedding, experimental_read_from_local_dir=experimental_read_from_local_dir, _overload_name_multimix=_overload_name_multimix, **kwargs diff --git a/grid2op/MakeEnv/MakeFromPath.py b/grid2op/MakeEnv/MakeFromPath.py index c051bf67..c966e465 100644 --- a/grid2op/MakeEnv/MakeFromPath.py +++ b/grid2op/MakeEnv/MakeFromPath.py @@ -127,6 +127,7 @@ def make_from_dataset_path( logger=None, experimental_read_from_local_dir=False, n_busbar=2, + allow_shedding=False, _add_to_name="", _compat_glop_version=None, _overload_name_multimix=None, @@ -165,6 +166,8 @@ def make_from_dataset_path( n_busbar: ``int`` Number of independant busbars allowed per substations. By default it's 2. + + allow_shedding; ``bool`` action_class: ``type``, optional Type of BaseAction the BaseAgent will be able to perform. @@ -951,6 +954,7 @@ def make_from_dataset_path( kwargs_attention_budget=kwargs_attention_budget, logger=logger, n_busbar=n_busbar, # TODO n_busbar_per_sub different num per substations: read from a config file maybe (if not provided by the user) + allow_shedding=allow_shedding, _compat_glop_version=_compat_glop_version, _read_from_local_dir=None, # first environment to generate the classes and save them _local_dir_cls=None, @@ -1031,6 +1035,7 @@ def make_from_dataset_path( kwargs_attention_budget=kwargs_attention_budget, logger=logger, n_busbar=n_busbar, # TODO n_busbar_per_sub different num per substations: read from a config file maybe (if not provided by the user) + allow_shedding=allow_shedding, _compat_glop_version=_compat_glop_version, _read_from_local_dir=classes_path, _allow_loaded_backend=allow_loaded_backend, diff --git a/grid2op/Observation/baseObservation.py b/grid2op/Observation/baseObservation.py index 10f36207..d03406cb 100644 --- a/grid2op/Observation/baseObservation.py +++ b/grid2op/Observation/baseObservation.py @@ -1054,7 +1054,7 @@ def state_of( return res @classmethod - def process_shunt_satic_data(cls) -> None: + def process_shunt_static_data(cls) -> None: if not cls.shunts_data_available: # this is really important, otherwise things from grid2op base types will be affected cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) @@ -1067,7 +1067,7 @@ def process_shunt_satic_data(cls) -> None: except ValueError: pass cls.attr_list_set = set(cls.attr_list_vect) - return super().process_shunt_satic_data() + return super().process_shunt_static_data() @classmethod def _aux_process_grid2op_compat_old(cls): diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index f6c84dd4..24095ce4 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -33,6 +33,7 @@ # TODO tests of these methods and this class in general DEFAULT_N_BUSBAR_PER_SUB = 2 +DEFAULT_ALLOW_SHEDDING = False class GridObjects: @@ -513,6 +514,7 @@ class GridObjects: sub_info : ClassVar[np.ndarray] = None dim_topo : ClassVar[np.ndarray] = -1 + allow_shedding : ClassVar[bool] = DEFAULT_ALLOW_SHEDDING # to which substation is connected each element load_to_subid : ClassVar[np.ndarray] = None @@ -685,6 +687,7 @@ def _clear_class_attribute(cls) -> None: """ cls.shunts_data_available = False cls.n_busbar_per_sub = DEFAULT_N_BUSBAR_PER_SUB + cls.allow_shedding = DEFAULT_ALLOW_SHEDDING # for redispatching / unit commitment cls._li_attr_disp = [ @@ -2332,6 +2335,9 @@ def assert_grid_correct_cls(cls): # alert data cls._check_validity_alert_data() + # shedding + assert isinstance(cls.allow_shedding, bool) + @classmethod def _check_validity_alarm_data(cls): if cls.dim_alarms == 0: @@ -3013,7 +3019,7 @@ def init_grid(cls, gridobj, force=False, extra_name=None, force_module=None, _lo res_cls._IS_INIT = True res_cls._compute_pos_big_topo_cls() - res_cls.process_shunt_satic_data() + res_cls.process_shunt_static_data() compat_mode = res_cls.process_grid2op_compat() res_cls._check_convert_to_np_array() # convert everything to numpy array if force_module is not None: @@ -3085,6 +3091,12 @@ def process_grid2op_compat(cls): # I need to set it to the default if set elsewhere cls.n_busbar_per_sub = DEFAULT_N_BUSBAR_PER_SUB res = True + + if glop_ver < version.parse("1.11.0.dev0"): + # Shedding did not exist, default value should have + # no effect + cls.allow_shedding = DEFAULT_ALLOW_SHEDDING + res = True if res: cls._reset_cls_dict() # forget the previous class (stored as dict) @@ -4060,6 +4072,10 @@ def _make_cls_dict(cls, res, as_list=True, copy_=True, _topo_vect_only=False): save_to_dict( res, cls, "alertable_line_ids", (lambda li: [int(el) for el in li]) if as_list else None, copy_ ) + + # Shedding + save_to_dict(res, cls, "allow_shedding", str, copy_) + # avoid further computation and save it if not as_list: cls._CLS_DICT = res.copy() @@ -4114,6 +4130,9 @@ def _make_cls_dict_extended(cls, res: CLS_AS_DICT_TYPING, as_list=True, copy_=Tr # n_busbar_per_sub res["n_busbar_per_sub"] = cls.n_busbar_per_sub + + # Shedding + res["allow_shedding"] = cls.allow_shedding # avoid further computation and save it if not as_list and not _topo_vect_only: @@ -4360,7 +4379,7 @@ class res(GridObjects): # backward compatibility: no storage were supported cls.set_no_storage() - cls.process_shunt_satic_data() + cls.process_shunt_static_data() if cls.glop_version != grid2op.__version__: # change name of the environment, this is done in Environment.py for regular environment @@ -4397,6 +4416,12 @@ class res(GridObjects): cls.alertable_line_names = [] cls.alertable_line_ids = [] + # Shedding + if 'allow_shedding' in dict_: + cls.allow_shedding = int(dict_["allow_shedding"]) + else: # Compatibility for older versions + cls.allow_shedding = DEFAULT_ALLOW_SHEDDING + # save the representation of this class as dict tmp = {} cls._make_cls_dict_extended(cls, tmp, as_list=False, copy_=True) @@ -4408,7 +4433,7 @@ class res(GridObjects): return cls() @classmethod - def process_shunt_satic_data(cls): + def process_shunt_static_data(cls): """remove possible shunts data from the classes, if shunts are deactivated""" pass @@ -5043,6 +5068,9 @@ class {cls.__name__}({cls._INIT_GRID_CLS.__name__}): alertable_line_names = {alertable_line_names_str} alertable_line_ids = {alertable_line_ids_str} + # shedding + allow_shedding = {cls.allow_shedding} + """ return res diff --git a/grid2op/Space/__init__.py b/grid2op/Space/__init__.py index 69387627..3cc3d895 100644 --- a/grid2op/Space/__init__.py +++ b/grid2op/Space/__init__.py @@ -1,5 +1,5 @@ -__all__ = ["RandomObject", "SerializableSpace", "GridObjects", "DEFAULT_N_BUSBAR_PER_SUB"] +__all__ = ["RandomObject", "SerializableSpace", "GridObjects", "DEFAULT_N_BUSBAR_PER_SUB", "DEFAULT_ALLOW_SHEDDING"] from grid2op.Space.RandomObject import RandomObject from grid2op.Space.SerializableSpace import SerializableSpace -from grid2op.Space.GridObjects import GridObjects, DEFAULT_N_BUSBAR_PER_SUB +from grid2op.Space.GridObjects import GridObjects, DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_SHEDDING diff --git a/grid2op/tests/test_shedding.py b/grid2op/tests/test_shedding.py new file mode 100644 index 00000000..af98fca1 --- /dev/null +++ b/grid2op/tests/test_shedding.py @@ -0,0 +1,56 @@ +import warnings +import unittest +import grid2op +from grid2op.Parameters import Parameters + +class TestShedding(unittest.TestCase): + + def setUp(self) -> None: + super().setUp() + p = Parameters() + p.ALLOW_SHEDDING = True + p.MAX_SUB_CHANGED = 5 + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make("rte_case5_example", param=p, test=True) + + def tearDown(self) -> None: + self.env.close() + + def test_shedding_parameter_is_true(self): + assert self.env.parameters.ALLOW_SHEDDING is True + + def test_shed_single_load(self): + # Check that a single load can be shed + pass + + def test_shed_single_generator(self): + # Check that a single generator can be shed + pass + + def test_shed_multiple_loads(self): + # Check that multiple loads can be shed at the same time + pass + + def test_shed_multiple_generators(self): + # Check that multiple generators can be shed at the same time + pass + + def test_shed_load_and_generator(self): + # Check that load and generator can be shed at the same time + pass + + def test_shed_energy_storage(self): + # Check that energy storage device can be shed successfully + pass + + def test_shedding_appears_in_observation(self): + # Check that observation contains correct information about shedding + pass + + def test_shedding_persistance(self): + # Check that components remains disconnected if shed + pass + +if __name__ == "__main__": + unittest.main() From 75cf5a7d1e38650f5683f58fea9db05e419ec9ea Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Mon, 4 Nov 2024 11:48:57 +0100 Subject: [PATCH 02/60] Debug: GridObject AllowShedding conversion Signed-off-by: Xavier Weiss --- grid2op/Space/GridObjects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 24095ce4..d9ce4cf1 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -4418,7 +4418,7 @@ class res(GridObjects): # Shedding if 'allow_shedding' in dict_: - cls.allow_shedding = int(dict_["allow_shedding"]) + cls.allow_shedding = bool(dict_["allow_shedding"]) else: # Compatibility for older versions cls.allow_shedding = DEFAULT_ALLOW_SHEDDING From 55e8c411b1763a24699071c09b6ebc7d328fde5e Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Mon, 4 Nov 2024 13:22:23 +0100 Subject: [PATCH 03/60] Add: Exception if shedding is not supported by Backend Signed-off-by: Xavier Weiss --- getting_started/13_Shedding.ipynb | 19 ++++++------------- grid2op/Backend/backend.py | 11 ++++++++++- grid2op/Backend/pandaPowerBackend.py | 4 ++++ grid2op/Environment/environment.py | 2 +- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/getting_started/13_Shedding.ipynb b/getting_started/13_Shedding.ipynb index c70d9143..e5a11cbe 100644 --- a/getting_started/13_Shedding.ipynb +++ b/getting_started/13_Shedding.ipynb @@ -31,24 +31,16 @@ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import grid2op\n", + "from lightsim2grid import LightSimBackend\n", "from grid2op.Parameters import Parameters\n", "\n", "from grid2op.PlotGrid import PlotMatplot\n", "from pathlib import Path\n", "\n", - "\n", - "p = Parameters()\n", - "p.MAX_SUB_CHANGED = 5\n", - "\n", "data_path = Path.cwd() / \"grid2op\" / \"data\"\n", - "env = grid2op.make(data_path / \"rte_case5_example\", allow_shedding=True, n_busbar=3, param=p)\n", + "env = grid2op.make(data_path / \"rte_case5_example\", allow_shedding=True)\n", "plotter = PlotMatplot(env.observation_space, load_name=True, gen_name=True, dpi=150)\n", - "env.set_id(\"00\")\n", - "obs = env.reset()\n", - "\n", - "print(f\"Loads: {env.n_load}, Generators: {env.n_gen}, Storage: {env.n_storage}\")\n", - "plotter.plot_obs(obs, figure=plt.figure(figsize=(8,5)))\n", - "plt.show()" + "print(f\"Loads: {env.n_load}, Generators: {env.n_gen}, Storage: {env.n_storage}, Allow Shedding: {env.allow_shedding}\")" ] }, { @@ -57,6 +49,7 @@ "metadata": {}, "outputs": [], "source": [ + "# Disconnect the load at substation 4\n", "load_lookup = {name:i for i,name in enumerate(env.name_load)}\n", "act = env.action_space({\"set_bus\":[(env.load_pos_topo_vect[load_lookup[\"load_4_2\"]], -1)]})\n", "print(act)\n", @@ -73,8 +66,8 @@ "metadata": {}, "outputs": [], "source": [ - "load_shed_mask = (init_obs + act).topo_vect[env.load_pos_topo_vect]\n", - "gen_shed_mask = (init_obs + act).topo_vect[env.gen_pos_topo_vect]" + "load_shed_mask = (init_obs + act).topo_vect[env.load_pos_topo_vect] == -1\n", + "gen_shed_mask = (init_obs + act).topo_vect[env.gen_pos_topo_vect] == -1" ] }, { diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index 84dd0d5f..53e09add 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -180,7 +180,7 @@ def __init__(self, #: You should not worry about the class attribute of the backend in :func:`Backend.apply_action` self.n_busbar_per_sub: int = DEFAULT_N_BUSBAR_PER_SUB - self.allow_shedding: bool = allow_shedding + self.set_shedding(allow_shedding) def can_handle_more_than_2_busbar(self): """ @@ -242,6 +242,15 @@ def cannot_handle_more_than_2_busbar(self): "'fix' this issue, you need to change the implementation of your backend or " "upgrade it to a newer version.") self.n_busbar_per_sub = DEFAULT_N_BUSBAR_PER_SUB + + def set_shedding(self, allow_shedding:bool=False) -> bool: + """ + Override if the Backend supports shedding. + """ + if allow_shedding: + raise BackendError("Backend does not support shedding") + else: + self.allow_shedding = allow_shedding def make_complete_path(self, path : Union[os.PathLike, str], diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index 677fd47c..9e5cf180 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -1548,3 +1548,7 @@ def sub_from_bus_id(self, bus_id : int) -> int: if bus_id >= self._number_true_line: return bus_id - self._number_true_line return bus_id + + def set_shedding(self, allow_shedding:bool=False) -> bool: + # Supported as of Grid2Op 1.11.0 + self.allow_shedding = allow_shedding \ No newline at end of file diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 1955f976..dfb87473 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -304,7 +304,7 @@ def _init_backend( ) # Shedding - self.backend.allow_shedding = self.allow_shedding + self.backend.set_shedding(self.allow_shedding) # alarm set up self.load_alarm_data() From 51eb97ee0102dec4692fb9c1ddff895c6400e898 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Mon, 4 Nov 2024 13:30:07 +0100 Subject: [PATCH 04/60] Refact: Remove bool return from set_shedding() method Signed-off-by: Xavier Weiss --- grid2op/Backend/backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index 53e09add..7e535af3 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -243,7 +243,7 @@ def cannot_handle_more_than_2_busbar(self): "upgrade it to a newer version.") self.n_busbar_per_sub = DEFAULT_N_BUSBAR_PER_SUB - def set_shedding(self, allow_shedding:bool=False) -> bool: + def set_shedding(self, allow_shedding:bool=False): """ Override if the Backend supports shedding. """ From 623d04bb8fe0c091a24ea22d3041ceaaf02238aa Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Mon, 4 Nov 2024 19:12:57 +0100 Subject: [PATCH 05/60] Test: Add Entry for Allow_Shedding Signed-off-by: Xavier Weiss --- getting_started/13_Shedding.ipynb | 28 ++++------------------------ grid2op/Backend/backend.py | 1 + grid2op/Space/GridObjects.py | 6 +++--- grid2op/tests/test_Observation.py | 1 + 4 files changed, 9 insertions(+), 27 deletions(-) diff --git a/getting_started/13_Shedding.ipynb b/getting_started/13_Shedding.ipynb index e5a11cbe..78605dcf 100644 --- a/getting_started/13_Shedding.ipynb +++ b/getting_started/13_Shedding.ipynb @@ -7,19 +7,7 @@ "### Shedding\n", "In emergency conditions, it may be possible / necessary for a grid operator to disconnect certain loads, generators, or other components in order to prevent a larger blackout. This notebook explores how this can be achieved in Grid2OP. \n", "\n", - "By default shedding is disabled in all environments, to enable it provide set the environment parameter ALLOW_SHEDDING to True.\n", - "\n", - "Shed load:\n", - "* **Vector** :: $\\text{n\\_load}$\n", - "* **List** :: $[(\\text{load\\_id}, \\text{status})]$\n", - "* **Dictionary** :: $\\text{load\\_name}: \\text{status}$\n", - "\n", - "Shed generator:\n", - "* **Vector** :: $\\text{n\\_gen}$\n", - "* **List** :: $[(\\text{gen\\_id}, \\text{status})]$\n", - "* **Dictionary** :: $\\text{gen\\_name}: \\text{status}$\n", - "\n", - "Where $\\text{status}$ is a boolean (True/False)." + "By default shedding is disabled in all environments, to provide the keyword argument allow_shedding when initializing the environment." ] }, { @@ -38,7 +26,7 @@ "from pathlib import Path\n", "\n", "data_path = Path.cwd() / \"grid2op\" / \"data\"\n", - "env = grid2op.make(data_path / \"rte_case5_example\", allow_shedding=True)\n", + "env = grid2op.make(data_path / \"rte_case5_example\", backend=LightSimBackend(), allow_shedding=True)\n", "plotter = PlotMatplot(env.observation_space, load_name=True, gen_name=True, dpi=150)\n", "print(f\"Loads: {env.n_load}, Generators: {env.n_gen}, Storage: {env.n_storage}, Allow Shedding: {env.allow_shedding}\")" ] @@ -51,7 +39,9 @@ "source": [ "# Disconnect the load at substation 4\n", "load_lookup = {name:i for i,name in enumerate(env.name_load)}\n", + "gen_lookup = {name:i for i,name in enumerate(env.name_gen)}\n", "act = env.action_space({\"set_bus\":[(env.load_pos_topo_vect[load_lookup[\"load_4_2\"]], -1)]})\n", + "act = env.action_space({\"set_bus\":[(env.gen_pos_topo_vect[gen_lookup[\"gen_0_0\"]], -1)]})\n", "print(act)\n", "env.set_id(\"00\")\n", "init_obs = env.reset()\n", @@ -60,16 +50,6 @@ "plt.show()" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "load_shed_mask = (init_obs + act).topo_vect[env.load_pos_topo_vect] == -1\n", - "gen_shed_mask = (init_obs + act).topo_vect[env.gen_pos_topo_vect] == -1" - ] - }, { "cell_type": "code", "execution_count": null, diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index 7e535af3..378945d3 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -247,6 +247,7 @@ def set_shedding(self, allow_shedding:bool=False): """ Override if the Backend supports shedding. """ + if allow_shedding: raise BackendError("Backend does not support shedding") else: diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index d9ce4cf1..5d854318 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -3930,6 +3930,9 @@ def _make_cls_dict(cls, res, as_list=True, copy_=True, _topo_vect_only=False): else: res["name_shunt"] = None res["shunt_to_subid"] = None + + # Shedding + save_to_dict(res, cls, "allow_shedding", bool, copy_) if not _topo_vect_only: # all the attributes bellow are not needed for the "first call" @@ -4072,9 +4075,6 @@ def _make_cls_dict(cls, res, as_list=True, copy_=True, _topo_vect_only=False): save_to_dict( res, cls, "alertable_line_ids", (lambda li: [int(el) for el in li]) if as_list else None, copy_ ) - - # Shedding - save_to_dict(res, cls, "allow_shedding", str, copy_) # avoid further computation and save it if not as_list: diff --git a/grid2op/tests/test_Observation.py b/grid2op/tests/test_Observation.py index dff0b205..ed8feb0a 100644 --- a/grid2op/tests/test_Observation.py +++ b/grid2op/tests/test_Observation.py @@ -299,6 +299,7 @@ def setUp(self): "alertable_line_ids": [], "assistant_warning_type": None, "_PATH_GRID_CLASSES": None, + "allow_shedding": False, } self.json_ref = { From ccbd1db95f50bfce0c9bc8cd3902e7814266a94f Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Mon, 4 Nov 2024 20:48:14 +0100 Subject: [PATCH 06/60] Refact: Remove Allow_Shedding from dict repr of GridObject Signed-off-by: Xavier Weiss --- grid2op/Space/GridObjects.py | 5 ----- grid2op/tests/test_Observation.py | 1 - 2 files changed, 6 deletions(-) diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 5d854318..a31f8b68 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -3931,8 +3931,6 @@ def _make_cls_dict(cls, res, as_list=True, copy_=True, _topo_vect_only=False): res["name_shunt"] = None res["shunt_to_subid"] = None - # Shedding - save_to_dict(res, cls, "allow_shedding", bool, copy_) if not _topo_vect_only: # all the attributes bellow are not needed for the "first call" @@ -4131,9 +4129,6 @@ def _make_cls_dict_extended(cls, res: CLS_AS_DICT_TYPING, as_list=True, copy_=Tr # n_busbar_per_sub res["n_busbar_per_sub"] = cls.n_busbar_per_sub - # Shedding - res["allow_shedding"] = cls.allow_shedding - # avoid further computation and save it if not as_list and not _topo_vect_only: cls._CLS_DICT_EXTENDED = res.copy() diff --git a/grid2op/tests/test_Observation.py b/grid2op/tests/test_Observation.py index ed8feb0a..dff0b205 100644 --- a/grid2op/tests/test_Observation.py +++ b/grid2op/tests/test_Observation.py @@ -299,7 +299,6 @@ def setUp(self): "alertable_line_ids": [], "assistant_warning_type": None, "_PATH_GRID_CLASSES": None, - "allow_shedding": False, } self.json_ref = { From 9c0078eabb8d5afc7633ead41bf35153522022ca Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Tue, 5 Nov 2024 08:53:33 +0100 Subject: [PATCH 07/60] Add: Unit Tests Signed-off-by: Xavier Weiss --- getting_started/13_Shedding.ipynb | 10 ++-- grid2op/tests/test_shedding.py | 79 +++++++++++++++++++++++-------- 2 files changed, 65 insertions(+), 24 deletions(-) diff --git a/getting_started/13_Shedding.ipynb b/getting_started/13_Shedding.ipynb index 78605dcf..cba22e40 100644 --- a/getting_started/13_Shedding.ipynb +++ b/getting_started/13_Shedding.ipynb @@ -26,7 +26,9 @@ "from pathlib import Path\n", "\n", "data_path = Path.cwd() / \"grid2op\" / \"data\"\n", - "env = grid2op.make(data_path / \"rte_case5_example\", backend=LightSimBackend(), allow_shedding=True)\n", + "p = Parameters()\n", + "p.MAX_SUB_CHANGED = 5\n", + "env = grid2op.make(data_path / \"rte_case5_example\", param=p, allow_shedding=True)\n", "plotter = PlotMatplot(env.observation_space, load_name=True, gen_name=True, dpi=150)\n", "print(f\"Loads: {env.n_load}, Generators: {env.n_gen}, Storage: {env.n_storage}, Allow Shedding: {env.allow_shedding}\")" ] @@ -40,8 +42,9 @@ "# Disconnect the load at substation 4\n", "load_lookup = {name:i for i,name in enumerate(env.name_load)}\n", "gen_lookup = {name:i for i,name in enumerate(env.name_gen)}\n", - "act = env.action_space({\"set_bus\":[(env.load_pos_topo_vect[load_lookup[\"load_4_2\"]], -1)]})\n", - "act = env.action_space({\"set_bus\":[(env.gen_pos_topo_vect[gen_lookup[\"gen_0_0\"]], -1)]})\n", + "act = env.action_space({\"set_bus\":[(env.load_pos_topo_vect[load_lookup[\"load_4_2\"]], -1),\n", + " (env.load_pos_topo_vect[load_lookup[\"load_3_1\"]], -1)]})\n", + "# act = env.action_space({\"set_bus\":[(env.gen_pos_topo_vect[gen_lookup[\"gen_0_0\"]], -1)]})\n", "print(act)\n", "env.set_id(\"00\")\n", "init_obs = env.reset()\n", @@ -57,7 +60,6 @@ "outputs": [], "source": [ "import pandapower as pp\n", - "from pandapower.plotting import simple_plot\n", "network = env.backend._grid.deepcopy()\n", "display(network.res_line.loc[:, [\"p_from_mw\", \"p_to_mw\", \"q_from_mvar\", \"q_to_mvar\"]])\n", "pp.runpp(network,\n", diff --git a/grid2op/tests/test_shedding.py b/grid2op/tests/test_shedding.py index af98fca1..f79f1252 100644 --- a/grid2op/tests/test_shedding.py +++ b/grid2op/tests/test_shedding.py @@ -8,49 +8,88 @@ class TestShedding(unittest.TestCase): def setUp(self) -> None: super().setUp() p = Parameters() - p.ALLOW_SHEDDING = True p.MAX_SUB_CHANGED = 5 with warnings.catch_warnings(): warnings.filterwarnings("ignore") - self.env = grid2op.make("rte_case5_example", param=p, test=True) + self.env = grid2op.make("rte_case5_example", param=p, + allow_shedding=True, test=True) + self.env.set_id("00") # Reproducibility + self.load_lookup = {name:i for i,name in enumerate(self.env.name_load)} + self.gen_lookup = {name:i for i,name in enumerate(self.env.name_gen)} def tearDown(self) -> None: self.env.close() def test_shedding_parameter_is_true(self): - assert self.env.parameters.ALLOW_SHEDDING is True + assert hasattr(self.env, "allow_shedding") + assert self.env.allow_shedding is True def test_shed_single_load(self): # Check that a single load can be shed - pass + load_idx = self.load_lookup["load_4_2"] + load_pos = self.env.load_pos_topo_vect[load_idx] + act = self.env.action_space({ + "set_bus": [(load_pos, -1)] + }) + obs, _, done, info = self.env.step(act) + assert not done + assert info["is_illegal"] is False + assert obs.topo_vect[load_pos] == -1 def test_shed_single_generator(self): # Check that a single generator can be shed - pass + gen_idx = self.gen_lookup["gen_0_0"] + gen_pos = self.env.gen_pos_topo_vect[gen_idx] + act = self.env.action_space({ + "set_bus": [(gen_pos, -1)] + }) + obs, _, done, info = self.env.step(act) + assert not done + assert info["is_illegal"] is False + assert obs.topo_vect[gen_pos] == -1 def test_shed_multiple_loads(self): # Check that multiple loads can be shed at the same time - pass - - def test_shed_multiple_generators(self): - # Check that multiple generators can be shed at the same time - pass + load_idx1 = self.load_lookup["load_4_2"] + load_idx2 = self.load_lookup["load_3_1"] + load_pos1 = self.env.load_pos_topo_vect[load_idx1] + load_pos2 = self.env.load_pos_topo_vect[load_idx2] + act = self.env.action_space({ + "set_bus": [(load_pos1, -1), (load_pos2, -1)] + }) + obs, _, done, info = self.env.step(act) + assert not done + assert info["is_illegal"] is False + assert obs.topo_vect[load_pos1] == -1 + assert obs.topo_vect[load_pos2] == -1 def test_shed_load_and_generator(self): # Check that load and generator can be shed at the same time - pass - - def test_shed_energy_storage(self): - # Check that energy storage device can be shed successfully - pass - - def test_shedding_appears_in_observation(self): - # Check that observation contains correct information about shedding - pass + # Check that multiple loads can be shed at the same time + load_idx = self.load_lookup["load_4_2"] + gen_idx = self.gen_lookup["gen_0_0"] + load_pos = self.env.load_pos_topo_vect[load_idx] + gen_pos = self.env.gen_pos_topo_vect[gen_idx] + act = self.env.action_space({ + "set_bus": [(load_pos, -1), (gen_pos, -1)] + }) + obs, _, done, info = self.env.step(act) + assert not done + assert info["is_illegal"] is False + assert obs.topo_vect[load_pos] == -1 + assert obs.topo_vect[gen_pos] == -1 def test_shedding_persistance(self): # Check that components remains disconnected if shed - pass + load_idx = self.load_lookup["load_4_2"] + load_pos = self.env.load_pos_topo_vect[load_idx] + act = self.env.action_space({ + "set_bus": [(load_pos, -1)] + }) + _ = self.env.step(act) + obs, _, done, _ = self.env.step(self.env.action_space({})) + assert not done + assert obs.topo_vect[load_pos] == -1 if __name__ == "__main__": unittest.main() From 90c96d8a71109b19e7e14432010b67df24142cbe Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Wed, 6 Nov 2024 15:41:40 +0100 Subject: [PATCH 08/60] Refact: Use DEFAULT constants for n_busbar and allow_shedding Signed-off-by: Xavier Weiss --- grid2op/MakeEnv/Make.py | 9 +++++---- grid2op/MakeEnv/MakeFromPath.py | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/grid2op/MakeEnv/Make.py b/grid2op/MakeEnv/Make.py index 1e954d72..cedefc8f 100644 --- a/grid2op/MakeEnv/Make.py +++ b/grid2op/MakeEnv/Make.py @@ -20,6 +20,7 @@ import grid2op.MakeEnv.PathUtils from grid2op.MakeEnv.PathUtils import _create_path_folder from grid2op.Download.DownloadDataset import _aux_download +from grid2op.Space import DEFAULT_ALLOW_SHEDDING, DEFAULT_N_BUSBAR_PER_SUB _VAR_FORCE_TEST = "_GRID2OP_FORCE_TEST" @@ -247,8 +248,8 @@ def _aux_make_multimix( dataset_path, test=False, experimental_read_from_local_dir=False, - n_busbar=2, - allow_shedding=False, + n_busbar=DEFAULT_N_BUSBAR_PER_SUB, + allow_shedding=DEFAULT_ALLOW_SHEDDING, _add_to_name="", _compat_glop_version=None, _overload_name_multimix=None, @@ -287,8 +288,8 @@ def make( test : bool=False, logger: Optional[logging.Logger]=None, experimental_read_from_local_dir : bool=False, - n_busbar=2, - allow_shedding=False, + n_busbar=DEFAULT_N_BUSBAR_PER_SUB, + allow_shedding=DEFAULT_ALLOW_SHEDDING, _add_to_name : str="", _compat_glop_version : Optional[str]=None, _overload_name_multimix : Optional[str]=None, # do not use ! diff --git a/grid2op/MakeEnv/MakeFromPath.py b/grid2op/MakeEnv/MakeFromPath.py index c966e465..4ca56397 100644 --- a/grid2op/MakeEnv/MakeFromPath.py +++ b/grid2op/MakeEnv/MakeFromPath.py @@ -33,6 +33,7 @@ from grid2op.VoltageControler import ControlVoltageFromFile from grid2op.Opponent import BaseOpponent, BaseActionBudget, NeverAttackBudget from grid2op.operator_attention import LinearAttentionBudget +from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_SHEDDING from grid2op.MakeEnv.get_default_aux import _get_default_aux from grid2op.MakeEnv.PathUtils import _aux_fix_backend_internal_classes @@ -126,8 +127,8 @@ def make_from_dataset_path( dataset_path="/", logger=None, experimental_read_from_local_dir=False, - n_busbar=2, - allow_shedding=False, + n_busbar=DEFAULT_N_BUSBAR_PER_SUB, + allow_shedding=DEFAULT_ALLOW_SHEDDING, _add_to_name="", _compat_glop_version=None, _overload_name_multimix=None, From 50b7c0f6a6532a8909e1dfbef769096113c21adf Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Wed, 6 Nov 2024 16:00:00 +0100 Subject: [PATCH 09/60] Add: Can/Cannot Handle Interface for Load/Gen detachment Signed-off-by: Xavier Weiss --- getting_started/13_Shedding.ipynb | 8 +- grid2op/Backend/backend.py | 93 ++++++++++++++++++++++-- grid2op/Backend/educPandaPowerBackend.py | 4 +- grid2op/Backend/pandaPowerBackend.py | 37 ++++------ grid2op/Converter/BackendConverter.py | 6 +- grid2op/Environment/baseEnv.py | 6 +- grid2op/Environment/environment.py | 12 +-- grid2op/Environment/multiMixEnv.py | 14 ++-- grid2op/MakeEnv/Make.py | 20 ++--- grid2op/MakeEnv/MakeFromPath.py | 10 +-- grid2op/Space/GridObjects.py | 18 ++--- grid2op/Space/__init__.py | 4 +- grid2op/tests/test_shedding.py | 4 +- 13 files changed, 156 insertions(+), 80 deletions(-) diff --git a/getting_started/13_Shedding.ipynb b/getting_started/13_Shedding.ipynb index cba22e40..ea56ff16 100644 --- a/getting_started/13_Shedding.ipynb +++ b/getting_started/13_Shedding.ipynb @@ -7,10 +7,10 @@ "### Shedding\n", "In emergency conditions, it may be possible / necessary for a grid operator to disconnect certain loads, generators, or other components in order to prevent a larger blackout. This notebook explores how this can be achieved in Grid2OP. \n", "\n", - "By default shedding is disabled in all environments, to provide the keyword argument allow_shedding when initializing the environment." + "By default shedding is disabled in all environments, to provide the keyword argument allow_detachment when initializing the environment." ] }, - { + {allow_detachment "cell_type": "code", "execution_count": null, "metadata": {}, @@ -28,9 +28,9 @@ "data_path = Path.cwd() / \"grid2op\" / \"data\"\n", "p = Parameters()\n", "p.MAX_SUB_CHANGED = 5\n", - "env = grid2op.make(data_path / \"rte_case5_example\", param=p, allow_shedding=True)\n", + "env = grid2op.make(data_path / \"rte_case5_example\", param=p, allow_detachment=True)\n", "plotter = PlotMatplot(env.observation_space, load_name=True, gen_name=True, dpi=150)\n", - "print(f\"Loads: {env.n_load}, Generators: {env.n_gen}, Storage: {env.n_storage}, Allow Shedding: {env.allow_shedding}\")" + "print(f\"Loads: {env.n_load}, Generators: {env.n_gen}, Storage: {env.n_storage}, Allow Detachment: {env.allow_detachment}\")" ] }, { diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index 378945d3..f448a7e2 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -34,7 +34,7 @@ DivergingPowerflow, Grid2OpException, ) -from grid2op.Space import GridObjects, DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_SHEDDING +from grid2op.Space import GridObjects, DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_DETACHMENT # TODO method to get V and theta at each bus, could be in the same shape as check_kirchoff @@ -123,7 +123,7 @@ class Backend(GridObjects, ABC): ERR_INIT_POWERFLOW : str = "Power cannot be computed on the first time step, please check your data." def __init__(self, detailed_infos_for_cascading_failures:bool=False, - can_be_copied:bool=True, allow_shedding:bool=DEFAULT_ALLOW_SHEDDING, + can_be_copied:bool=True, allow_detachment:bool=DEFAULT_ALLOW_DETACHMENT, **kwargs): """ Initialize an instance of Backend. This does nothing per se. Only the call to :func:`Backend.load_grid` @@ -180,7 +180,9 @@ def __init__(self, #: You should not worry about the class attribute of the backend in :func:`Backend.apply_action` self.n_busbar_per_sub: int = DEFAULT_N_BUSBAR_PER_SUB - self.set_shedding(allow_shedding) + # .. versionadded: 1.11.0 + self._missing_detachment_support:bool = True + self.allow_detachment:bool = DEFAULT_ALLOW_DETACHMENT def can_handle_more_than_2_busbar(self): """ @@ -243,15 +245,73 @@ def cannot_handle_more_than_2_busbar(self): "upgrade it to a newer version.") self.n_busbar_per_sub = DEFAULT_N_BUSBAR_PER_SUB - def set_shedding(self, allow_shedding:bool=False): + + def can_handle_detachment(self): + """ + .. versionadded:: 1.11.0 + + This function should be called once in :func:`Backend.load_grid` if your backend is able + to handle the detachment of loads and generators. + + If not called, then the `environment` will not be able to detach loads and generators. + + .. seealso:: + :func:`Backend.cannot_handle_detachment` + + .. note:: + From grid2op 1.11.0 it is preferable that your backend calls one of + :func:`Backend.can_handle_detachment` or + :func:`Backend.cannot_handle_detachment`. + + If not, then the environments created with your backend will not be able to + "operate" grid with load and generator detachment. + + .. danger:: + We highly recommend you do not try to override this function. + At least, at time of writing there is no good reason to do so. + """ + self._missing_detachment_support = False + self.allow_detachment = type(self).allow_detachment + + def cannot_handle_detachment(self): + """ + .. versionadded:: 1.11.0 + + This function should be called once in :func:`Backend.load_grid` if your backend is **NOT** able + to handle the detachment of loads and generators. + + If not called, then the `environment` will not be able to detach loads and generators. + + .. seealso:: + :func:`Backend.cannot_handle_detachment` + + .. note:: + From grid2op 1.11.0 it is preferable that your backend calls one of + :func:`Backend.can_handle_detachment` or + :func:`Backend.cannot_handle_detachment`. + + If not, then the environments created with your backend will not be able to + "operate" grid with load and generator detachment. + + .. danger:: + We highly recommend you do not try to override this function. + At least, at time of writing there is no good reason to do so. + """ + self._missing_detachment_support = False + if type(self.allow_detachment != DEFAULT_ALLOW_DETACHMENT): + warnings.warn("You asked in 'make' function to allow shedding. This is" + f"not possible with a backend of type {type(self)}.") + self.allow_detachment = DEFAULT_ALLOW_DETACHMENT + + def set_shedding(self, allow_detachment:bool=False): """ Override if the Backend supports shedding. """ - if allow_shedding: + if allow_detachment: raise BackendError("Backend does not support shedding") else: - self.allow_shedding = allow_shedding + self.allow_detachment = allow_detachment def make_complete_path(self, path : Union[os.PathLike, str], @@ -2093,6 +2153,27 @@ def assert_grid_correct(self, _local_dir_cls=None) -> None: warnings.warn("Your backend is missing the `_missing_two_busbars_support_info` " "attribute. This is known issue in lightims2grid <= 0.7.5. Please " "upgrade your backend. This will raise an error in the future.") + + if hasattr(self, "_missing_detachment_support"): + if self._missing_detachment_support: + warnings.warn("The backend implementation you are using is probably too old to take advantage of the " + "new feature added in grid2op 1.11.0: the possibility " + "to detach loads or generators without leading to an immediate game over. " + "To silence this warning, you can modify the `load_grid` implementation " + "of your backend and either call:\n" + "- self.can_handle_detachment if the current implementation " + " can handle detachments OR\n" + "- self.cannot_handle_detachment if not." + "\nAnd of course, ideally, if the current implementation " + "of your backend cannot handle detachment then change it :-)\n" + "Your backend will behave as if it did not support it.") + self._missing_detachment_support = False + self.allow_detachment = DEFAULT_ALLOW_DETACHMENT + else: + self._missing_detachment_support = False + self.allow_detachment = DEFAULT_ALLOW_DETACHMENT + warnings.warn("Your backend is missing the `_missing_detachment_support` " + "attribute.") orig_type = type(self) if orig_type.my_bk_act_class is None and orig_type._INIT_GRID_CLS is None: diff --git a/grid2op/Backend/educPandaPowerBackend.py b/grid2op/Backend/educPandaPowerBackend.py index 4520b1c4..57e6afad 100644 --- a/grid2op/Backend/educPandaPowerBackend.py +++ b/grid2op/Backend/educPandaPowerBackend.py @@ -67,7 +67,7 @@ class EducPandaPowerBackend(Backend): def __init__(self, detailed_infos_for_cascading_failures : Optional[bool]=False, - allow_shedding:bool=False, + allow_detachment:bool=False, can_be_copied : Optional[bool]=True): """ Nothing much to do here except initializing what you would need (a tensorflow session, link to some @@ -84,7 +84,7 @@ def __init__(self, self, detailed_infos_for_cascading_failures=detailed_infos_for_cascading_failures, can_be_copied=can_be_copied, - allow_shedding=allow_shedding, + allow_detachment=allow_detachment, # extra arguments that might be needed for building such a backend # these extra kwargs will be stored (without copy) in the # base class and used when another backend will be created diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index 9e5cf180..82477f25 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -122,7 +122,6 @@ def __init__( max_iter : int=10, can_be_copied: bool=True, with_numba: bool=NUMBA_, - allow_shedding:bool=False, ): from grid2op.MakeEnv.Make import _force_test_dataset if _force_test_dataset(): @@ -138,7 +137,6 @@ def __init__( dist_slack=dist_slack, max_iter=max_iter, with_numba=with_numba, - allow_shedding=allow_shedding, ) self.with_numba : bool = with_numba self.prod_pu_to_kv : Optional[np.ndarray] = None @@ -1026,20 +1024,20 @@ def _aux_runpf_pp(self, is_dc: bool): # else: # self._pf_init = "auto" - if not self.allow_shedding and (~self._grid.load["in_service"]).any(): - # TODO see if there is a better way here -> do not handle this here, but rather in Backend._next_grid_state - raise pp.powerflow.LoadflowNotConverged("Disconnected load: for now grid2op cannot handle properly" - " disconnected load. If you want to disconnect one, say it" - " consumes 0. instead. Please check loads: " - f"{(~self._grid.load['in_service'].values).nonzero()[0]}" - ) - if not self.allow_shedding and (~self._grid.gen["in_service"]).any(): - # TODO see if there is a better way here -> do not handle this here, but rather in Backend._next_grid_state - raise pp.powerflow.LoadflowNotConverged("Disconnected gen: for now grid2op cannot handle properly" - " disconnected generators. If you want to disconnect one, say it" - " produces 0. instead. Please check generators: " - f"{(~self._grid.gen['in_service'].values).nonzero()[0]}" - ) + # if not self.allow_detachment and (~self._grid.load["in_service"]).any(): + # # TODO see if there is a better way here -> do not handle this here, but rather in Backend._next_grid_state + # raise pp.powerflow.LoadflowNotConverged("Disconnected load: for now grid2op cannot handle properly" + # " disconnected load. If you want to disconnect one, say it" + # " consumes 0. instead. Please check loads: " + # f"{(~self._grid.load['in_service'].values).nonzero()[0]}" + # ) + # if not self.allow_detachment and (~self._grid.gen["in_service"]).any(): + # # TODO see if there is a better way here -> do not handle this here, but rather in Backend._next_grid_state + # raise pp.powerflow.LoadflowNotConverged("Disconnected gen: for now grid2op cannot handle properly" + # " disconnected generators. If you want to disconnect one, say it" + # " produces 0. instead. Please check generators: " + # f"{(~self._grid.gen['in_service'].values).nonzero()[0]}" + # ) try: if is_dc: pp.rundcpp(self._grid, check_connectivity=True, init="flat") @@ -1327,6 +1325,7 @@ def copy(self) -> "PandaPowerBackend": res._in_service_trafo_col_id = self._in_service_trafo_col_id res._missing_two_busbars_support_info = self._missing_two_busbars_support_info + res._missing_detachment_support = self._missing_detachment_support res.div_exception = self.div_exception return res @@ -1547,8 +1546,4 @@ def _storages_info(self): def sub_from_bus_id(self, bus_id : int) -> int: if bus_id >= self._number_true_line: return bus_id - self._number_true_line - return bus_id - - def set_shedding(self, allow_shedding:bool=False) -> bool: - # Supported as of Grid2Op 1.11.0 - self.allow_shedding = allow_shedding \ No newline at end of file + return bus_id \ No newline at end of file diff --git a/grid2op/Converter/BackendConverter.py b/grid2op/Converter/BackendConverter.py index 1dda02fe..f00f9953 100644 --- a/grid2op/Converter/BackendConverter.py +++ b/grid2op/Converter/BackendConverter.py @@ -95,7 +95,7 @@ def __init__( use_target_backend_name=False, kwargs_target_backend=None, kwargs_source_backend=None, - allow_shedding:bool=False, + allow_detachmentnt:bool=False, ): Backend.__init__( self, @@ -103,7 +103,7 @@ def __init__( use_target_backend_name=use_target_backend_name, kwargs_target_backend=kwargs_target_backend, kwargs_source_backend=kwargs_source_backend, - allow_shedding=allow_shedding, + allow_detachment=allow_detachmentnt, ) difcf = detailed_infos_for_cascading_failures if kwargs_source_backend is None: @@ -167,7 +167,7 @@ def load_grid(self, path=None, filename=None): # register the "n_busbar_per_sub" (set for the backend class) # TODO in case source supports the "more than 2" feature but not target # it's unclear how I can "reload" the grid... - from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_SHEDDING + from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_DETACHMENT type(self.source_backend).set_n_busbar_per_sub(DEFAULT_N_BUSBAR_PER_SUB) type(self.target_backend).set_n_busbar_per_sub(DEFAULT_N_BUSBAR_PER_SUB) self.cannot_handle_more_than_2_busbar() diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index 448a867e..ea3de461 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -334,7 +334,7 @@ def __init__( highres_sim_counter=None, update_obs_after_reward=False, n_busbar=2, - allow_shedding:bool=False, + allow_detachmentnt:bool=False, _is_test: bool = False, # TODO not implemented !! _init_obs: Optional[BaseObservation] =None, _local_dir_cls=None, @@ -361,7 +361,7 @@ def __init__( self._raw_backend_class = _raw_backend_class self._n_busbar = n_busbar # env attribute not class attribute ! - self.allow_shedding = allow_shedding + self.allow_detachment = allow_detachmentnt if other_rewards is None: other_rewards = {} @@ -659,7 +659,7 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None): if dict_ is None: dict_ = {} new_obj._n_busbar = self._n_busbar - new_obj.allow_shedding = self.allow_shedding + new_obj.allow_detachmentnt = self.allow_detachment new_obj._init_grid_path = copy.deepcopy(self._init_grid_path) new_obj._init_env_path = copy.deepcopy(self._init_env_path) diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index dfb87473..a39926b5 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -32,7 +32,7 @@ from grid2op.Environment.baseEnv import BaseEnv from grid2op.Opponent import BaseOpponent, NeverAttackBudget from grid2op.operator_attention import LinearAttentionBudget -from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_SHEDDING +from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_DETACHMENT from grid2op.typing_variables import RESET_OPTIONS_TYPING, N_BUSBAR_PER_SUB_TYPING from grid2op.MakeEnv.PathUtils import USE_CLASS_IN_FILE @@ -86,7 +86,7 @@ def __init__( parameters, name="unknown", n_busbar : N_BUSBAR_PER_SUB_TYPING=DEFAULT_N_BUSBAR_PER_SUB, - allow_shedding:bool=DEFAULT_ALLOW_SHEDDING, + allow_detachmentnt:bool=DEFAULT_ALLOW_DETACHMENT, names_chronics_to_backend=None, actionClass=TopologyAction, observationClass=CompleteObservation, @@ -157,7 +157,7 @@ def __init__( highres_sim_counter=highres_sim_counter, update_obs_after_reward=_update_obs_after_reward, n_busbar=n_busbar, # TODO n_busbar_per_sub different num per substations: read from a config file maybe (if not provided by the user) - allow_shedding=allow_shedding, + allow_detachmentnallow_detachmentment, name=name, _raw_backend_class=_raw_backend_class if _raw_backend_class is not None else type(backend), _init_obs=_init_obs, @@ -304,7 +304,7 @@ def _init_backend( ) # Shedding - self.backend.set_shedding(self.allow_shedding) + self.backend.set_shedding(self.allow_detachment) # alarm set up self.load_alarm_data() @@ -2212,7 +2212,7 @@ def init_obj_from_kwargs(cls, _local_dir_cls, _overload_name_multimix, n_busbar=DEFAULT_N_BUSBAR_PER_SUB, - allow_shedding=DEFAULT_ALLOW_SHEDDING, + allow_detachmentnt=DEFAULT_ALLOW_DETACHMENT, ): res = cls(init_env_path=init_env_path, init_grid_path=init_grid_path, @@ -2245,7 +2245,7 @@ def init_obj_from_kwargs(cls, observation_bk_class=observation_bk_class, observation_bk_kwargs=observation_bk_kwargs, n_busbar=int(n_busbar), - allow_shedding=bool(allow_shedding), + allow_detachmentnt=booallow_detachmentment), _raw_backend_class=_raw_backend_class, _read_from_local_dir=_read_from_local_dir, _local_dir_cls=_local_dir_cls, diff --git a/grid2op/Environment/multiMixEnv.py b/grid2op/Environment/multiMixEnv.py index b5ef41f9..d6948673 100644 --- a/grid2op/Environment/multiMixEnv.py +++ b/grid2op/Environment/multiMixEnv.py @@ -13,7 +13,7 @@ from typing import Any, Dict, Tuple, Union, List, Literal from grid2op.dtypes import dt_int, dt_float -from grid2op.Space import GridObjects, RandomObject, DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_SHEDDING +from grid2op.Space import GridObjects, RandomObject, DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_DETACHMENT from grid2op.Exceptions import EnvError, Grid2OpException from grid2op.Observation import BaseObservation from grid2op.MakeEnv.PathUtils import USE_CLASS_IN_FILE @@ -165,7 +165,7 @@ def __init__( logger=None, experimental_read_from_local_dir=None, n_busbar=DEFAULT_N_BUSBAR_PER_SUB, - allow_shedding=DEFAULT_ALLOW_SHEDDING, + allow_detachment=DEFAULT_ALLOW_DETACHMENT, _add_to_name="", # internal, for test only, do not use ! _compat_glop_version=None, # internal, for test only, do not use ! _test=False, @@ -208,7 +208,7 @@ def __init__( _add_to_name, _compat_glop_version, n_busbar, - allow_shedding, + allow_detachment, _test, experimental_read_from_local_dir, multi_env_name, @@ -237,7 +237,7 @@ def __init__( _add_to_name, _compat_glop_version, n_busbar, - allow_shedding, + allow_detachment, _test, experimental_read_from_local_dir, multi_env_name, @@ -304,7 +304,7 @@ def _aux_create_a_mix(self, _add_to_name, _compat_glop_version, n_busbar, - allow_shedding, + allow_detachment, _test, experimental_read_from_local_dir, multi_env_name, @@ -339,7 +339,7 @@ def _aux_create_a_mix(self, _add_to_name=_add_to_name, _compat_glop_version=_compat_glop_version, n_busbar=n_busbar, - allow_shedding=allow_shedding, + allow_detachment=allow_detachment, test=_test, logger=this_logger, experimental_read_from_local_dir=experimental_read_from_local_dir, @@ -350,7 +350,7 @@ def _aux_create_a_mix(self, mix = make( mix_path, n_busbar=n_busbar, - allow_shedding=allow_shedding, + allow_detachment=allow_detachment, _add_to_name=_add_to_name, _compat_glop_version=_compat_glop_version, test=_test, diff --git a/grid2op/MakeEnv/Make.py b/grid2op/MakeEnv/Make.py index cedefc8f..a38cf2ef 100644 --- a/grid2op/MakeEnv/Make.py +++ b/grid2op/MakeEnv/Make.py @@ -20,7 +20,7 @@ import grid2op.MakeEnv.PathUtils from grid2op.MakeEnv.PathUtils import _create_path_folder from grid2op.Download.DownloadDataset import _aux_download -from grid2op.Space import DEFAULT_ALLOW_SHEDDING, DEFAULT_N_BUSBAR_PER_SUB +from grid2op.Space import DEFAULT_ALLOW_DETACHMENT, DEFAULT_N_BUSBAR_PER_SUB _VAR_FORCE_TEST = "_GRID2OP_FORCE_TEST" @@ -249,7 +249,7 @@ def _aux_make_multimix( test=False, experimental_read_from_local_dir=False, n_busbar=DEFAULT_N_BUSBAR_PER_SUB, - allow_shedding=DEFAULT_ALLOW_SHEDDING, + allow_detachmentnt=DEFAULT_ALLOW_DETACHMENT, _add_to_name="", _compat_glop_version=None, _overload_name_multimix=None, @@ -264,7 +264,7 @@ def _aux_make_multimix( dataset_path, experimental_read_from_local_dir=experimental_read_from_local_dir, n_busbar=n_busbar, - allow_shedding=allow_shedding, + allow_detachmentnallow_detachmentment, _test=test, _add_to_name=_add_to_name, _compat_glop_version=_compat_glop_version, @@ -289,7 +289,7 @@ def make( logger: Optional[logging.Logger]=None, experimental_read_from_local_dir : bool=False, n_busbar=DEFAULT_N_BUSBAR_PER_SUB, - allow_shedding=DEFAULT_ALLOW_SHEDDING, + allow_detachment=DEFAULT_ALLOW_DETACHMENT, _add_to_name : str="", _compat_glop_version : Optional[str]=None, _overload_name_multimix : Optional[str]=None, # do not use ! @@ -310,7 +310,7 @@ def make( The `n_busbar` parameters .. versionadded:: 1.11.0 - The `allow_shedding` parameter + The `allow_detachmentnt` parameter Parameters ---------- @@ -336,7 +336,7 @@ def make( n_busbar: ``int`` Number of independant busbars allowed per substations. By default it's 2. - allow_shedding: ``bool`` + allow_detachmentnt: ``bool`` Whether to allow loads and generators to be shed without a game over. By default it's False. kwargs: @@ -447,7 +447,7 @@ def make_from_path_fn_(*args, **kwargs): _compat_glop_version=_compat_glop_version_tmp, _overload_name_multimix=_overload_name_multimix, n_busbar=n_busbar, - allow_shedding=allow_shedding, + allow_detachmentnt=allow_detachment, **kwargs ) @@ -494,7 +494,7 @@ def make_from_path_fn_(*args, **kwargs): dataset_path=ds_path, logger=logger, n_busbar=n_busbar, - allow_shedding=allow_shedding, + allow_detachmentnt=allow_detachment, _add_to_name=_add_to_name, _compat_glop_version=_compat_glop_version, experimental_read_from_local_dir=experimental_read_from_local_dir, @@ -510,7 +510,7 @@ def make_from_path_fn_(*args, **kwargs): real_ds_path, logger=logger, n_busbar=n_busbar, - allow_shedding=allow_shedding, + allow_detachmentnt=allow_detachment, experimental_read_from_local_dir=experimental_read_from_local_dir, _overload_name_multimix=_overload_name_multimix, **kwargs @@ -531,7 +531,7 @@ def make_from_path_fn_(*args, **kwargs): dataset_path=real_ds_path, logger=logger, n_busbar=n_busbar, - allow_shedding=allow_shedding, + allow_detachmentnt=allow_detachment, experimental_read_from_local_dir=experimental_read_from_local_dir, _overload_name_multimix=_overload_name_multimix, **kwargs diff --git a/grid2op/MakeEnv/MakeFromPath.py b/grid2op/MakeEnv/MakeFromPath.py index 4ca56397..feb19c93 100644 --- a/grid2op/MakeEnv/MakeFromPath.py +++ b/grid2op/MakeEnv/MakeFromPath.py @@ -33,7 +33,7 @@ from grid2op.VoltageControler import ControlVoltageFromFile from grid2op.Opponent import BaseOpponent, BaseActionBudget, NeverAttackBudget from grid2op.operator_attention import LinearAttentionBudget -from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_SHEDDING +from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_DETACHMENT from grid2op.MakeEnv.get_default_aux import _get_default_aux from grid2op.MakeEnv.PathUtils import _aux_fix_backend_internal_classes @@ -128,7 +128,7 @@ def make_from_dataset_path( logger=None, experimental_read_from_local_dir=False, n_busbar=DEFAULT_N_BUSBAR_PER_SUB, - allow_shedding=DEFAULT_ALLOW_SHEDDING, + allow_detachment=DEFAULT_ALLOW_DETACHMENT, _add_to_name="", _compat_glop_version=None, _overload_name_multimix=None, @@ -168,7 +168,7 @@ def make_from_dataset_path( n_busbar: ``int`` Number of independant busbars allowed per substations. By default it's 2. - allow_shedding; ``bool`` + allow_detachment; ``bool`` action_class: ``type``, optional Type of BaseAction the BaseAgent will be able to perform. @@ -955,7 +955,7 @@ def make_from_dataset_path( kwargs_attention_budget=kwargs_attention_budget, logger=logger, n_busbar=n_busbar, # TODO n_busbar_per_sub different num per substations: read from a config file maybe (if not provided by the user) - allow_shedding=allow_shedding, + allow_detachment=allow_detachment, _compat_glop_version=_compat_glop_version, _read_from_local_dir=None, # first environment to generate the classes and save them _local_dir_cls=None, @@ -1036,7 +1036,7 @@ def make_from_dataset_path( kwargs_attention_budget=kwargs_attention_budget, logger=logger, n_busbar=n_busbar, # TODO n_busbar_per_sub different num per substations: read from a config file maybe (if not provided by the user) - allow_shedding=allow_shedding, + allow_detachment=allow_detachment, _compat_glop_version=_compat_glop_version, _read_from_local_dir=classes_path, _allow_loaded_backend=allow_loaded_backend, diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index a31f8b68..32608346 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -33,7 +33,7 @@ # TODO tests of these methods and this class in general DEFAULT_N_BUSBAR_PER_SUB = 2 -DEFAULT_ALLOW_SHEDDING = False +DEFAULT_ALLOW_DETACHMENT = False class GridObjects: @@ -514,7 +514,7 @@ class GridObjects: sub_info : ClassVar[np.ndarray] = None dim_topo : ClassVar[np.ndarray] = -1 - allow_shedding : ClassVar[bool] = DEFAULT_ALLOW_SHEDDING + allow_detachment : ClassVar[bool] = DEFAULT_ALLOW_DETACHMENT # to which substation is connected each element load_to_subid : ClassVar[np.ndarray] = None @@ -687,7 +687,7 @@ def _clear_class_attribute(cls) -> None: """ cls.shunts_data_available = False cls.n_busbar_per_sub = DEFAULT_N_BUSBAR_PER_SUB - cls.allow_shedding = DEFAULT_ALLOW_SHEDDING + cls.allow_detachment = DEFAULT_ALLOW_DETACHMENT # for redispatching / unit commitment cls._li_attr_disp = [ @@ -2336,7 +2336,7 @@ def assert_grid_correct_cls(cls): cls._check_validity_alert_data() # shedding - assert isinstance(cls.allow_shedding, bool) + assert isinstance(cls.allow_detachment, bool) @classmethod def _check_validity_alarm_data(cls): @@ -3095,7 +3095,7 @@ def process_grid2op_compat(cls): if glop_ver < version.parse("1.11.0.dev0"): # Shedding did not exist, default value should have # no effect - cls.allow_shedding = DEFAULT_ALLOW_SHEDDING + cls.allow_detachment = DEFAULT_ALLOW_DETACHMENT res = True if res: @@ -4412,10 +4412,10 @@ class res(GridObjects): cls.alertable_line_ids = [] # Shedding - if 'allow_shedding' in dict_: - cls.allow_shedding = bool(dict_["allow_shedding"]) + if 'allow_detachment' in dict_: + cls.allow_detachment = bool(dict_["allow_detachment"]) else: # Compatibility for older versions - cls.allow_shedding = DEFAULT_ALLOW_SHEDDING + cls.allow_detachment = DEFAULT_ALLOW_DETACHMENT # save the representation of this class as dict tmp = {} @@ -5064,7 +5064,7 @@ class {cls.__name__}({cls._INIT_GRID_CLS.__name__}): alertable_line_ids = {alertable_line_ids_str} # shedding - allow_shedding = {cls.allow_shedding} + allow_detachment = {cls.allow_detachment} """ return res diff --git a/grid2op/Space/__init__.py b/grid2op/Space/__init__.py index 3cc3d895..56486e8c 100644 --- a/grid2op/Space/__init__.py +++ b/grid2op/Space/__init__.py @@ -1,5 +1,5 @@ -__all__ = ["RandomObject", "SerializableSpace", "GridObjects", "DEFAULT_N_BUSBAR_PER_SUB", "DEFAULT_ALLOW_SHEDDING"] +__all__ = ["RandomObject", "SerializableSpace", "GridObjects", "DEFAULT_N_BUSBAR_PER_SUB", "DEFAULT_ALLOW_DETACHMENT"] from grid2op.Space.RandomObject import RandomObject from grid2op.Space.SerializableSpace import SerializableSpace -from grid2op.Space.GridObjects import GridObjects, DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_SHEDDING +from grid2op.Space.GridObjects import GridObjects, DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_DETACHMENT diff --git a/grid2op/tests/test_shedding.py b/grid2op/tests/test_shedding.py index f79f1252..7575eeef 100644 --- a/grid2op/tests/test_shedding.py +++ b/grid2op/tests/test_shedding.py @@ -12,7 +12,7 @@ def setUp(self) -> None: with warnings.catch_warnings(): warnings.filterwarnings("ignore") self.env = grid2op.make("rte_case5_example", param=p, - allow_shedding=True, test=True) + allow_detachment=True, test=True) self.env.set_id("00") # Reproducibility self.load_lookup = {name:i for i,name in enumerate(self.env.name_load)} self.gen_lookup = {name:i for i,name in enumerate(self.env.name_gen)} @@ -22,7 +22,7 @@ def tearDown(self) -> None: def test_shedding_parameter_is_true(self): assert hasattr(self.env, "allow_shedding") - assert self.env.allow_shedding is True + assert self.env.allow_detachment is True def test_shed_single_load(self): # Check that a single load can be shed From 54ef7196f3c484dd7e7ced8a5bf94c76c2a26122 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Wed, 6 Nov 2024 16:14:24 +0100 Subject: [PATCH 10/60] Refact: Remove Set_Shedding Signed-off-by: Xavier Weiss --- ...> 13_DetachmentOfLoadsAndGenerators.ipynb} | 2 +- grid2op/Backend/backend.py | 20 +++++++++---------- grid2op/Environment/baseEnv.py | 6 +++--- grid2op/Environment/environment.py | 11 ++++------ grid2op/MakeEnv/Make.py | 12 +++++------ 5 files changed, 24 insertions(+), 27 deletions(-) rename getting_started/{13_Shedding.ipynb => 13_DetachmentOfLoadsAndGenerators.ipynb} (99%) diff --git a/getting_started/13_Shedding.ipynb b/getting_started/13_DetachmentOfLoadsAndGenerators.ipynb similarity index 99% rename from getting_started/13_Shedding.ipynb rename to getting_started/13_DetachmentOfLoadsAndGenerators.ipynb index ea56ff16..c5522a1d 100644 --- a/getting_started/13_Shedding.ipynb +++ b/getting_started/13_DetachmentOfLoadsAndGenerators.ipynb @@ -10,7 +10,7 @@ "By default shedding is disabled in all environments, to provide the keyword argument allow_detachment when initializing the environment." ] }, - {allow_detachment + { "cell_type": "code", "execution_count": null, "metadata": {}, diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index f448a7e2..d843edb8 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -303,16 +303,6 @@ def cannot_handle_detachment(self): f"not possible with a backend of type {type(self)}.") self.allow_detachment = DEFAULT_ALLOW_DETACHMENT - def set_shedding(self, allow_detachment:bool=False): - """ - Override if the Backend supports shedding. - """ - - if allow_detachment: - raise BackendError("Backend does not support shedding") - else: - self.allow_detachment = allow_detachment - def make_complete_path(self, path : Union[os.PathLike, str], filename : Optional[Union[os.PathLike, str]]=None) -> str: @@ -1089,7 +1079,17 @@ def _runpf_with_diverging_exception(self, is_dc : bool) -> Optional[Exception]: """ conv = False exc_me = None + + # if not self.allow_detachment and (~self._grid.load["in_service"]).any(): + # # TODO see if there is a better way here -> do not handle this here, but rather in Backend._next_grid_state + # raise pp.powerflow.LoadflowNotConverged("Disconnected load: for now grid2op cannot handle properly" + # " disconnected load. If you want to disconnect one, say it" + # " consumes 0. instead. Please check loads: " + # f"{(~self._grid.load['in_service'].values).nonzero()[0]}" + # ) try: + # load_p, load_q, load_v = self.loads_info() + # prod_p, prod_q, prod_v = self.generators_info() conv, exc_me = self.runpf(is_dc=is_dc) # run powerflow except Grid2OpException as exc_: exc_me = exc_ diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index ea3de461..954e88ef 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -334,7 +334,7 @@ def __init__( highres_sim_counter=None, update_obs_after_reward=False, n_busbar=2, - allow_detachmentnt:bool=False, + allow_detachment:bool=False, _is_test: bool = False, # TODO not implemented !! _init_obs: Optional[BaseObservation] =None, _local_dir_cls=None, @@ -361,7 +361,7 @@ def __init__( self._raw_backend_class = _raw_backend_class self._n_busbar = n_busbar # env attribute not class attribute ! - self.allow_detachment = allow_detachmentnt + self.allow_detachment = allow_detachment if other_rewards is None: other_rewards = {} @@ -659,7 +659,7 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None): if dict_ is None: dict_ = {} new_obj._n_busbar = self._n_busbar - new_obj.allow_detachmentnt = self.allow_detachment + new_obj.allow_detachment = self.allow_detachment new_obj._init_grid_path = copy.deepcopy(self._init_grid_path) new_obj._init_env_path = copy.deepcopy(self._init_env_path) diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index a39926b5..26358815 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -86,7 +86,7 @@ def __init__( parameters, name="unknown", n_busbar : N_BUSBAR_PER_SUB_TYPING=DEFAULT_N_BUSBAR_PER_SUB, - allow_detachmentnt:bool=DEFAULT_ALLOW_DETACHMENT, + allow_detachment:bool=DEFAULT_ALLOW_DETACHMENT, names_chronics_to_backend=None, actionClass=TopologyAction, observationClass=CompleteObservation, @@ -157,7 +157,7 @@ def __init__( highres_sim_counter=highres_sim_counter, update_obs_after_reward=_update_obs_after_reward, n_busbar=n_busbar, # TODO n_busbar_per_sub different num per substations: read from a config file maybe (if not provided by the user) - allow_detachmentnallow_detachmentment, + allow_detachment=allow_detachment, name=name, _raw_backend_class=_raw_backend_class if _raw_backend_class is not None else type(backend), _init_obs=_init_obs, @@ -303,9 +303,6 @@ def _init_backend( f'not be able to use the renderer, plot the grid etc. The error was "{exc_}"' ) - # Shedding - self.backend.set_shedding(self.allow_detachment) - # alarm set up self.load_alarm_data() self.load_alert_data() @@ -2212,7 +2209,7 @@ def init_obj_from_kwargs(cls, _local_dir_cls, _overload_name_multimix, n_busbar=DEFAULT_N_BUSBAR_PER_SUB, - allow_detachmentnt=DEFAULT_ALLOW_DETACHMENT, + allow_detachment=DEFAULT_ALLOW_DETACHMENT, ): res = cls(init_env_path=init_env_path, init_grid_path=init_grid_path, @@ -2245,7 +2242,7 @@ def init_obj_from_kwargs(cls, observation_bk_class=observation_bk_class, observation_bk_kwargs=observation_bk_kwargs, n_busbar=int(n_busbar), - allow_detachmentnt=booallow_detachmentment), + allow_detachment=allow_detachment, _raw_backend_class=_raw_backend_class, _read_from_local_dir=_read_from_local_dir, _local_dir_cls=_local_dir_cls, diff --git a/grid2op/MakeEnv/Make.py b/grid2op/MakeEnv/Make.py index a38cf2ef..555354b1 100644 --- a/grid2op/MakeEnv/Make.py +++ b/grid2op/MakeEnv/Make.py @@ -249,7 +249,7 @@ def _aux_make_multimix( test=False, experimental_read_from_local_dir=False, n_busbar=DEFAULT_N_BUSBAR_PER_SUB, - allow_detachmentnt=DEFAULT_ALLOW_DETACHMENT, + allow_detachment=DEFAULT_ALLOW_DETACHMENT, _add_to_name="", _compat_glop_version=None, _overload_name_multimix=None, @@ -264,7 +264,7 @@ def _aux_make_multimix( dataset_path, experimental_read_from_local_dir=experimental_read_from_local_dir, n_busbar=n_busbar, - allow_detachmentnallow_detachmentment, + allow_detachment=allow_detachment, _test=test, _add_to_name=_add_to_name, _compat_glop_version=_compat_glop_version, @@ -447,7 +447,7 @@ def make_from_path_fn_(*args, **kwargs): _compat_glop_version=_compat_glop_version_tmp, _overload_name_multimix=_overload_name_multimix, n_busbar=n_busbar, - allow_detachmentnt=allow_detachment, + allow_detachment=allow_detachment, **kwargs ) @@ -494,7 +494,7 @@ def make_from_path_fn_(*args, **kwargs): dataset_path=ds_path, logger=logger, n_busbar=n_busbar, - allow_detachmentnt=allow_detachment, + allow_detachment=allow_detachment, _add_to_name=_add_to_name, _compat_glop_version=_compat_glop_version, experimental_read_from_local_dir=experimental_read_from_local_dir, @@ -510,7 +510,7 @@ def make_from_path_fn_(*args, **kwargs): real_ds_path, logger=logger, n_busbar=n_busbar, - allow_detachmentnt=allow_detachment, + allow_detachment=allow_detachment, experimental_read_from_local_dir=experimental_read_from_local_dir, _overload_name_multimix=_overload_name_multimix, **kwargs @@ -531,7 +531,7 @@ def make_from_path_fn_(*args, **kwargs): dataset_path=real_ds_path, logger=logger, n_busbar=n_busbar, - allow_detachmentnt=allow_detachment, + allow_detachment=allow_detachment, experimental_read_from_local_dir=experimental_read_from_local_dir, _overload_name_multimix=_overload_name_multimix, **kwargs From d9da4bffdc5629d112ecf151a7cd0d75f35652fa Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Wed, 6 Nov 2024 16:26:54 +0100 Subject: [PATCH 11/60] Add: Backend-Agnostic Detachment Check Signed-off-by: Xavier Weiss --- .../13_DetachmentOfLoadsAndGenerators.ipynb | 32 +++++++++++++++++-- grid2op/Backend/backend.py | 23 +++++++------ grid2op/Backend/pandaPowerBackend.py | 1 + 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/getting_started/13_DetachmentOfLoadsAndGenerators.ipynb b/getting_started/13_DetachmentOfLoadsAndGenerators.ipynb index c5522a1d..1d01ce17 100644 --- a/getting_started/13_DetachmentOfLoadsAndGenerators.ipynb +++ b/getting_started/13_DetachmentOfLoadsAndGenerators.ipynb @@ -4,8 +4,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Shedding\n", - "In emergency conditions, it may be possible / necessary for a grid operator to disconnect certain loads, generators, or other components in order to prevent a larger blackout. This notebook explores how this can be achieved in Grid2OP. \n", + "### Detachment of Loads and Generators\n", + "In emergency conditions, it may be possible / necessary for a grid operator to detach certain loads, generators, or other components in order to prevent a larger blackout. This notebook explores how this can be achieved in Grid2OP. \n", "\n", "By default shedding is disabled in all environments, to provide the keyword argument allow_detachment when initializing the environment." ] @@ -71,6 +71,34 @@ ")\n", "display(network.res_line.loc[:, [\"p_from_mw\", \"p_to_mw\", \"q_from_mvar\", \"q_to_mvar\"]])" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.backend.loads_info()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "env.backend.generators_info()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "topo_vect = env.backend.get_topo_vect()\n", + "topo_vect[env.backend.load_pos_topo_vect]" + ] } ], "metadata": { diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index d843edb8..87d76738 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -1080,16 +1080,21 @@ def _runpf_with_diverging_exception(self, is_dc : bool) -> Optional[Exception]: conv = False exc_me = None - # if not self.allow_detachment and (~self._grid.load["in_service"]).any(): - # # TODO see if there is a better way here -> do not handle this here, but rather in Backend._next_grid_state - # raise pp.powerflow.LoadflowNotConverged("Disconnected load: for now grid2op cannot handle properly" - # " disconnected load. If you want to disconnect one, say it" - # " consumes 0. instead. Please check loads: " - # f"{(~self._grid.load['in_service'].values).nonzero()[0]}" - # ) try: - # load_p, load_q, load_v = self.loads_info() - # prod_p, prod_q, prod_v = self.generators_info() + # Check if loads/gens have been detached and if this is allowed, otherwise raise an error + # .. versionadded:: 1.11.0 + topo_vect = self.get_topo_vect() + + load_buses = topo_vect[self.load_pos_topo_vect] + if not self.allow_detachment and (load_buses == -1).any(): + raise Grid2OpException(f"One or more loads were detached before powerflow in Backend {type(self).__name__}" + "but this is not allowed or not supported (Game Over)") + + gen_buses = topo_vect[self.gen_pos_topo_vect] + if not self.allow_detachment and (gen_buses == -1).any(): + raise Grid2OpException(f"One or more generators were detached before powerflow in Backend {type(self).__name__}" + "but this is not allowed or not supported (Game Over)") + conv, exc_me = self.runpf(is_dc=is_dc) # run powerflow except Grid2OpException as exc_: exc_me = exc_ diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index 82477f25..9bf85f27 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -344,6 +344,7 @@ def load_grid(self, """ self.can_handle_more_than_2_busbar() + self.can_handle_detachment() full_path = self.make_complete_path(path, filename) with warnings.catch_warnings(): From e66fd034df0427ac7b8db9b77a628ef017b0d4a3 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Wed, 6 Nov 2024 16:38:02 +0100 Subject: [PATCH 12/60] Add: ClassAttribute integration for AllowDetachment Signed-off-by: Xavier Weiss --- grid2op/Space/GridObjects.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 32608346..bc82dc72 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -2034,17 +2034,9 @@ def assert_grid_correct_cls(cls): # TODO n_busbar_per_sub different num per substations if isinstance(cls.n_busbar_per_sub, (int, dt_int, np.int32, np.int64)): cls.n_busbar_per_sub = dt_int(cls.n_busbar_per_sub) - # np.full(cls.n_sub, - # fill_value=cls.n_busbar_per_sub, - # dtype=dt_int) else: - # cls.n_busbar_per_sub = np.array(cls.n_busbar_per_sub) - # cls.n_busbar_per_sub = cls.n_busbar_per_sub.astype(dt_int) raise EnvError("Grid2op cannot handle a different number of busbar per substations at the moment.") - # if cls.n_busbar_per_sub != int(cls.n_busbar_per_sub): - # raise EnvError(f"`n_busbar_per_sub` should be convertible to an integer, found {cls.n_busbar_per_sub}") - # cls.n_busbar_per_sub = int(cls.n_busbar_per_sub) if (cls.n_busbar_per_sub < 1).any(): raise EnvError(f"`n_busbar_per_sub` should be >= 1 found {cls.n_busbar_per_sub}") @@ -3931,6 +3923,8 @@ def _make_cls_dict(cls, res, as_list=True, copy_=True, _topo_vect_only=False): res["name_shunt"] = None res["shunt_to_subid"] = None + + save_to_dict(res, cls, "allow_detachment", bool, copy_) if not _topo_vect_only: # all the attributes bellow are not needed for the "first call" @@ -4411,7 +4405,7 @@ class res(GridObjects): cls.alertable_line_names = [] cls.alertable_line_ids = [] - # Shedding + # Detachment of Loads / Generators if 'allow_detachment' in dict_: cls.allow_detachment = bool(dict_["allow_detachment"]) else: # Compatibility for older versions @@ -4976,7 +4970,8 @@ class {cls.__name__}({cls._INIT_GRID_CLS.__name__}): sub_info = {sub_info_str} dim_topo = {cls.dim_topo} - + allow_detachment = {cls.allow_detachment} + # to which substation is connected each element load_to_subid = {load_to_subid_str} gen_to_subid = {gen_to_subid_str} From 6aa67c08f625e4755cc46fb2ffb62b5776f67508 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Fri, 8 Nov 2024 10:55:40 +0100 Subject: [PATCH 13/60] Debug: Use _get_topo_vect inside Backend check for Detachment Signed-off-by: Xavier Weiss --- grid2op/Action/baseAction.py | 2 +- grid2op/Backend/backend.py | 20 +++++----- grid2op/Backend/educPandaPowerBackend.py | 2 - grid2op/Backend/pandaPowerBackend.py | 23 +---------- grid2op/Converter/BackendConverter.py | 4 +- grid2op/Environment/baseEnv.py | 11 +++--- grid2op/Environment/environment.py | 6 ++- grid2op/Environment/maskedEnvironment.py | 8 ++-- grid2op/Environment/timedOutEnv.py | 8 ++-- grid2op/MakeEnv/Make.py | 2 +- grid2op/MakeEnv/MakeFromPath.py | 1 + grid2op/Runner/runner.py | 6 ++- grid2op/Space/GridObjects.py | 49 ++++++++++++++---------- grid2op/tests/test_Action.py | 2 + grid2op/tests/test_shedding.py | 2 +- 15 files changed, 73 insertions(+), 73 deletions(-) diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 3e2a2901..4246c166 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -845,7 +845,7 @@ def process_grid2op_compat(cls): # if there are only one busbar, the "set_bus" action can still be used # to disconnect the element, this is why it's not removed cls._aux_process_n_busbar_per_sub() - + cls.attr_list_set = copy.deepcopy(cls.attr_list_set) cls.attr_list_set = set(cls.attr_list_vect) diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index 87d76738..2b21d61b 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -123,7 +123,7 @@ class Backend(GridObjects, ABC): ERR_INIT_POWERFLOW : str = "Power cannot be computed on the first time step, please check your data." def __init__(self, detailed_infos_for_cascading_failures:bool=False, - can_be_copied:bool=True, allow_detachment:bool=DEFAULT_ALLOW_DETACHMENT, + can_be_copied:bool=True, **kwargs): """ Initialize an instance of Backend. This does nothing per se. Only the call to :func:`Backend.load_grid` @@ -182,7 +182,7 @@ def __init__(self, # .. versionadded: 1.11.0 self._missing_detachment_support:bool = True - self.allow_detachment:bool = DEFAULT_ALLOW_DETACHMENT + self._allow_detachment:bool = DEFAULT_ALLOW_DETACHMENT def can_handle_more_than_2_busbar(self): """ @@ -271,7 +271,7 @@ def can_handle_detachment(self): At least, at time of writing there is no good reason to do so. """ self._missing_detachment_support = False - self.allow_detachment = type(self).allow_detachment + self._allow_detachment = type(self)._allow_detachment def cannot_handle_detachment(self): """ @@ -298,10 +298,10 @@ def cannot_handle_detachment(self): At least, at time of writing there is no good reason to do so. """ self._missing_detachment_support = False - if type(self.allow_detachment != DEFAULT_ALLOW_DETACHMENT): + if type(self._allow_detachment != DEFAULT_ALLOW_DETACHMENT): warnings.warn("You asked in 'make' function to allow shedding. This is" f"not possible with a backend of type {type(self)}.") - self.allow_detachment = DEFAULT_ALLOW_DETACHMENT + self._allow_detachment = DEFAULT_ALLOW_DETACHMENT def make_complete_path(self, path : Union[os.PathLike, str], @@ -1083,15 +1083,15 @@ def _runpf_with_diverging_exception(self, is_dc : bool) -> Optional[Exception]: try: # Check if loads/gens have been detached and if this is allowed, otherwise raise an error # .. versionadded:: 1.11.0 - topo_vect = self.get_topo_vect() + topo_vect = self._get_topo_vect() load_buses = topo_vect[self.load_pos_topo_vect] - if not self.allow_detachment and (load_buses == -1).any(): + if not self._allow_detachment and (load_buses == -1).any(): raise Grid2OpException(f"One or more loads were detached before powerflow in Backend {type(self).__name__}" "but this is not allowed or not supported (Game Over)") gen_buses = topo_vect[self.gen_pos_topo_vect] - if not self.allow_detachment and (gen_buses == -1).any(): + if not self._allow_detachment and (gen_buses == -1).any(): raise Grid2OpException(f"One or more generators were detached before powerflow in Backend {type(self).__name__}" "but this is not allowed or not supported (Game Over)") @@ -2173,10 +2173,10 @@ def assert_grid_correct(self, _local_dir_cls=None) -> None: "of your backend cannot handle detachment then change it :-)\n" "Your backend will behave as if it did not support it.") self._missing_detachment_support = False - self.allow_detachment = DEFAULT_ALLOW_DETACHMENT + self._allow_detachment = DEFAULT_ALLOW_DETACHMENT else: self._missing_detachment_support = False - self.allow_detachment = DEFAULT_ALLOW_DETACHMENT + self._allow_detachment = DEFAULT_ALLOW_DETACHMENT warnings.warn("Your backend is missing the `_missing_detachment_support` " "attribute.") diff --git a/grid2op/Backend/educPandaPowerBackend.py b/grid2op/Backend/educPandaPowerBackend.py index 57e6afad..a56d6600 100644 --- a/grid2op/Backend/educPandaPowerBackend.py +++ b/grid2op/Backend/educPandaPowerBackend.py @@ -67,7 +67,6 @@ class EducPandaPowerBackend(Backend): def __init__(self, detailed_infos_for_cascading_failures : Optional[bool]=False, - allow_detachment:bool=False, can_be_copied : Optional[bool]=True): """ Nothing much to do here except initializing what you would need (a tensorflow session, link to some @@ -84,7 +83,6 @@ def __init__(self, self, detailed_infos_for_cascading_failures=detailed_infos_for_cascading_failures, can_be_copied=can_be_copied, - allow_detachment=allow_detachment, # extra arguments that might be needed for building such a backend # these extra kwargs will be stored (without copy) in the # base class and used when another backend will be created diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index 9bf85f27..c3b048fd 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -1017,28 +1017,7 @@ def _aux_runpf_pp(self, is_dc: bool): warnings.filterwarnings("ignore", category=RuntimeWarning) warnings.filterwarnings("ignore", category=DeprecationWarning) self._pf_init = "dc" - # nb_bus = self.get_nb_active_bus() - # if self._nb_bus_before is None: - # self._pf_init = "dc" - # elif nb_bus == self._nb_bus_before: - # self._pf_init = "results" - # else: - # self._pf_init = "auto" - - # if not self.allow_detachment and (~self._grid.load["in_service"]).any(): - # # TODO see if there is a better way here -> do not handle this here, but rather in Backend._next_grid_state - # raise pp.powerflow.LoadflowNotConverged("Disconnected load: for now grid2op cannot handle properly" - # " disconnected load. If you want to disconnect one, say it" - # " consumes 0. instead. Please check loads: " - # f"{(~self._grid.load['in_service'].values).nonzero()[0]}" - # ) - # if not self.allow_detachment and (~self._grid.gen["in_service"]).any(): - # # TODO see if there is a better way here -> do not handle this here, but rather in Backend._next_grid_state - # raise pp.powerflow.LoadflowNotConverged("Disconnected gen: for now grid2op cannot handle properly" - # " disconnected generators. If you want to disconnect one, say it" - # " produces 0. instead. Please check generators: " - # f"{(~self._grid.gen['in_service'].values).nonzero()[0]}" - # ) + try: if is_dc: pp.rundcpp(self._grid, check_connectivity=True, init="flat") diff --git a/grid2op/Converter/BackendConverter.py b/grid2op/Converter/BackendConverter.py index f00f9953..84bed15c 100644 --- a/grid2op/Converter/BackendConverter.py +++ b/grid2op/Converter/BackendConverter.py @@ -95,7 +95,7 @@ def __init__( use_target_backend_name=False, kwargs_target_backend=None, kwargs_source_backend=None, - allow_detachmentnt:bool=False, + allow_detachment:bool=False, ): Backend.__init__( self, @@ -103,7 +103,7 @@ def __init__( use_target_backend_name=use_target_backend_name, kwargs_target_backend=kwargs_target_backend, kwargs_source_backend=kwargs_source_backend, - allow_detachment=allow_detachmentnt, + allow_detachment=allow_detachment, ) difcf = detailed_infos_for_cascading_failures if kwargs_source_backend is None: diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index 954e88ef..fc5ebdb0 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -28,7 +28,8 @@ HighResSimCounter) from grid2op.Backend import Backend from grid2op.dtypes import dt_int, dt_float, dt_bool -from grid2op.Space import GridObjects, RandomObject +from grid2op.Space import GridObjects, RandomObject, DEFAULT_ALLOW_DETACHMENT, DEFAULT_N_BUSBAR_PER_SUB +from grid2op.typing_variables import N_BUSBAR_PER_SUB_TYPING from grid2op.Exceptions import (Grid2OpException, EnvError, InvalidRedispatching, @@ -333,8 +334,8 @@ def __init__( observation_bk_kwargs=None, # type of backend for the observation space highres_sim_counter=None, update_obs_after_reward=False, - n_busbar=2, - allow_detachment:bool=False, + n_busbar:N_BUSBAR_PER_SUB_TYPING=DEFAULT_N_BUSBAR_PER_SUB, + allow_detachment:bool=DEFAULT_ALLOW_DETACHMENT, _is_test: bool = False, # TODO not implemented !! _init_obs: Optional[BaseObservation] =None, _local_dir_cls=None, @@ -361,7 +362,7 @@ def __init__( self._raw_backend_class = _raw_backend_class self._n_busbar = n_busbar # env attribute not class attribute ! - self.allow_detachment = allow_detachment + self._allow_detachment = allow_detachment if other_rewards is None: other_rewards = {} @@ -659,7 +660,7 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None): if dict_ is None: dict_ = {} new_obj._n_busbar = self._n_busbar - new_obj.allow_detachment = self.allow_detachment + new_obj._allow_detachment = self._allow_detachment new_obj._init_grid_path = copy.deepcopy(self._init_grid_path) new_obj._init_env_path = copy.deepcopy(self._init_env_path) diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 26358815..39fc4482 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -85,7 +85,7 @@ def __init__( backend, parameters, name="unknown", - n_busbar : N_BUSBAR_PER_SUB_TYPING=DEFAULT_N_BUSBAR_PER_SUB, + n_busbar:N_BUSBAR_PER_SUB_TYPING=DEFAULT_N_BUSBAR_PER_SUB, allow_detachment:bool=DEFAULT_ALLOW_DETACHMENT, names_chronics_to_backend=None, actionClass=TopologyAction, @@ -1495,6 +1495,7 @@ def get_kwargs(self, """ res = {} res["n_busbar"] = self._n_busbar + res["allow_detachment"] = self._allow_detachment res["init_env_path"] = self._init_env_path res["init_grid_path"] = self._init_grid_path if with_chronics_handler: @@ -2147,6 +2148,7 @@ def get_params_for_runner(self): res["grid_layout"] = self.grid_layout res["name_env"] = self.name res["n_busbar"] = self._n_busbar + res["allow_detachment"] = self._allow_detachment res["opponent_space_type"] = self._opponent_space_type res["opponent_action_class"] = self._opponent_action_class @@ -2242,7 +2244,7 @@ def init_obj_from_kwargs(cls, observation_bk_class=observation_bk_class, observation_bk_kwargs=observation_bk_kwargs, n_busbar=int(n_busbar), - allow_detachment=allow_detachment, + allow_detachment=bool(allow_detachment), _raw_backend_class=_raw_backend_class, _read_from_local_dir=_read_from_local_dir, _local_dir_cls=_local_dir_cls, diff --git a/grid2op/Environment/maskedEnvironment.py b/grid2op/Environment/maskedEnvironment.py index e3c55a7d..ba7c1ee8 100644 --- a/grid2op/Environment/maskedEnvironment.py +++ b/grid2op/Environment/maskedEnvironment.py @@ -5,7 +5,7 @@ # you can obtain one at http://mozilla.org/MPL/2.0/. # 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 warnings import copy import numpy as np import os @@ -14,7 +14,7 @@ from grid2op.Environment.environment import Environment from grid2op.Exceptions import EnvError from grid2op.dtypes import dt_bool, dt_float, dt_int -from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB +from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_DETACHMENT from grid2op.MakeEnv.PathUtils import USE_CLASS_IN_FILE @@ -163,7 +163,8 @@ def init_obj_from_kwargs(cls, _read_from_local_dir, _overload_name_multimix, _local_dir_cls, - n_busbar=DEFAULT_N_BUSBAR_PER_SUB): + n_busbar=DEFAULT_N_BUSBAR_PER_SUB, + allow_detachment=DEFAULT_ALLOW_DETACHMENT): grid2op_env = {"init_env_path": init_env_path, "init_grid_path": init_grid_path, "chronics_handler": chronics_handler, @@ -195,6 +196,7 @@ def init_obj_from_kwargs(cls, "observation_bk_class": observation_bk_class, "observation_bk_kwargs": observation_bk_kwargs, "n_busbar": int(n_busbar), + "allow_detachment": bool(allow_detachment), "_raw_backend_class": _raw_backend_class, "_read_from_local_dir": _read_from_local_dir, "_local_dir_cls": _local_dir_cls, diff --git a/grid2op/Environment/timedOutEnv.py b/grid2op/Environment/timedOutEnv.py index a1952f99..377d55c6 100644 --- a/grid2op/Environment/timedOutEnv.py +++ b/grid2op/Environment/timedOutEnv.py @@ -5,7 +5,7 @@ # you can obtain one at http://mozilla.org/MPL/2.0/. # 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 warnings import time from math import floor from typing import Any, Dict, Tuple, Union, List, Literal @@ -15,7 +15,7 @@ from grid2op.Action import BaseAction from grid2op.Observation import BaseObservation from grid2op.Exceptions import EnvError -from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB +from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_DETACHMENT from grid2op.MakeEnv.PathUtils import USE_CLASS_IN_FILE @@ -232,7 +232,8 @@ def init_obj_from_kwargs(cls, _read_from_local_dir, _local_dir_cls, _overload_name_multimix, - n_busbar=DEFAULT_N_BUSBAR_PER_SUB): + n_busbar=DEFAULT_N_BUSBAR_PER_SUB, + allow_detachment=DEFAULT_ALLOW_DETACHMENT): grid2op_env={"init_env_path": init_env_path, "init_grid_path": init_grid_path, "chronics_handler": chronics_handler, @@ -266,6 +267,7 @@ def init_obj_from_kwargs(cls, "_raw_backend_class": _raw_backend_class, "_read_from_local_dir": _read_from_local_dir, "n_busbar": int(n_busbar), + "allow_detachment": bool(allow_detachment), "_local_dir_cls": _local_dir_cls, "_overload_name_multimix": _overload_name_multimix} if not "time_out_ms" in other_env_kwargs: diff --git a/grid2op/MakeEnv/Make.py b/grid2op/MakeEnv/Make.py index 555354b1..ec332d32 100644 --- a/grid2op/MakeEnv/Make.py +++ b/grid2op/MakeEnv/Make.py @@ -310,7 +310,7 @@ def make( The `n_busbar` parameters .. versionadded:: 1.11.0 - The `allow_detachmentnt` parameter + The `allow_detachment` parameter Parameters ---------- diff --git a/grid2op/MakeEnv/MakeFromPath.py b/grid2op/MakeEnv/MakeFromPath.py index feb19c93..5eaa30d8 100644 --- a/grid2op/MakeEnv/MakeFromPath.py +++ b/grid2op/MakeEnv/MakeFromPath.py @@ -169,6 +169,7 @@ def make_from_dataset_path( Number of independant busbars allowed per substations. By default it's 2. allow_detachment; ``bool`` + Whether to allow loads/generators to be detached without a game over. By default False. action_class: ``type``, optional Type of BaseAction the BaseAgent will be able to perform. diff --git a/grid2op/Runner/runner.py b/grid2op/Runner/runner.py index a16a66b6..9ee0e28c 100644 --- a/grid2op/Runner/runner.py +++ b/grid2op/Runner/runner.py @@ -29,7 +29,7 @@ from grid2op.dtypes import dt_float from grid2op.Opponent import BaseOpponent, NeverAttackBudget from grid2op.operator_attention import LinearAttentionBudget -from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB +from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_DETACHMENT from grid2op.Episode import EpisodeData # on windows if i start using sequential, i need to continue using sequential # if i start using parallel i need to continue using parallel @@ -354,6 +354,7 @@ def __init__( init_grid_path: str, path_chron, # path where chronics of injections are stored n_busbar=DEFAULT_N_BUSBAR_PER_SUB, + allow_detachment=DEFAULT_ALLOW_DETACHMENT, name_env="unknown", parameters_path=None, names_chronics_to_backend=None, @@ -466,6 +467,7 @@ def __init__( # TOOD doc for the attention budget """ self._n_busbar = n_busbar + self._allow_detachment = allow_detachment self.with_forecast = with_forecast self.name_env = name_env self._overload_name_multimix = _overload_name_multimix @@ -765,6 +767,7 @@ def _new_env(self, parameters) -> Tuple[BaseEnv, BaseAgent]: res = self.envClass.init_obj_from_kwargs( other_env_kwargs=self.other_env_kwargs, n_busbar=self._n_busbar, + allow_detachment=self._allow_detachment, init_env_path=self.init_env_path, init_grid_path=self.init_grid_path, chronics_handler=chronics_handler, @@ -1301,6 +1304,7 @@ def _get_params(self): "_overload_name_multimix": self._overload_name_multimix, "other_env_kwargs": self.other_env_kwargs, "n_busbar": self._n_busbar, + "allow_detachment": self._allow_detachment, "mp_context": None, # this is used in multi processing context, avoid to multi process a multi process stuff "_local_dir_cls": self._local_dir_cls, } diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index bc82dc72..dbe3f9f5 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -514,7 +514,7 @@ class GridObjects: sub_info : ClassVar[np.ndarray] = None dim_topo : ClassVar[np.ndarray] = -1 - allow_detachment : ClassVar[bool] = DEFAULT_ALLOW_DETACHMENT + _allow_detachment : ClassVar[bool] = DEFAULT_ALLOW_DETACHMENT # to which substation is connected each element load_to_subid : ClassVar[np.ndarray] = None @@ -687,7 +687,7 @@ def _clear_class_attribute(cls) -> None: """ cls.shunts_data_available = False cls.n_busbar_per_sub = DEFAULT_N_BUSBAR_PER_SUB - cls.allow_detachment = DEFAULT_ALLOW_DETACHMENT + cls._allow_detachment = DEFAULT_ALLOW_DETACHMENT # for redispatching / unit commitment cls._li_attr_disp = [ @@ -2326,9 +2326,8 @@ def assert_grid_correct_cls(cls): # alert data cls._check_validity_alert_data() - - # shedding - assert isinstance(cls.allow_detachment, bool) + + assert isinstance(cls._allow_detachment, bool) @classmethod def _check_validity_alarm_data(cls): @@ -2966,6 +2965,9 @@ def init_grid(cls, gridobj, force=False, extra_name=None, force_module=None, _lo # to be able to load same environment with # different `n_busbar_per_sub` name_res += f"_{gridobj.n_busbar_per_sub}" + + if gridobj._allow_detachment != DEFAULT_ALLOW_DETACHMENT: + name_res += "_allowDetach" if _local_dir_cls is not None and gridobj._PATH_GRID_CLASSES is not None: # new in grid2op 1.10.3: @@ -3085,9 +3087,9 @@ def process_grid2op_compat(cls): res = True if glop_ver < version.parse("1.11.0.dev0"): - # Shedding did not exist, default value should have + # Detachment did not exist, default value should have # no effect - cls.allow_detachment = DEFAULT_ALLOW_DETACHMENT + cls._allow_detachment = DEFAULT_ALLOW_DETACHMENT res = True if res: @@ -3740,6 +3742,8 @@ def _make_cls_dict(cls, res, as_list=True, copy_=True, _topo_vect_only=False): else: res[k] = v return + + save_to_dict(res, cls, "_allow_detachment", str, copy_) if not _topo_vect_only: # all the attributes bellow are not needed for the "first call" @@ -3922,9 +3926,6 @@ def _make_cls_dict(cls, res, as_list=True, copy_=True, _topo_vect_only=False): else: res["name_shunt"] = None res["shunt_to_subid"] = None - - - save_to_dict(res, cls, "allow_detachment", bool, copy_) if not _topo_vect_only: # all the attributes bellow are not needed for the "first call" @@ -4108,7 +4109,7 @@ def _make_cls_dict_extended(cls, res: CLS_AS_DICT_TYPING, as_list=True, copy_=Tr # shunt (not in topo vect but might be usefull) res["shunts_data_available"] = cls.shunts_data_available res["n_shunt"] = cls.n_shunt - + if not _topo_vect_only: # all the attributes bellow are not needed for the "first call" # to this function when the elements are put together in the topo_vect. @@ -4122,7 +4123,9 @@ def _make_cls_dict_extended(cls, res: CLS_AS_DICT_TYPING, as_list=True, copy_=Tr # n_busbar_per_sub res["n_busbar_per_sub"] = cls.n_busbar_per_sub - + + res["_allow_detachment"] = cls._allow_detachment + # avoid further computation and save it if not as_list and not _topo_vect_only: cls._CLS_DICT_EXTENDED = res.copy() @@ -4195,6 +4198,18 @@ class res(GridObjects): cls._PATH_GRID_CLASSES = None else: cls._PATH_GRID_CLASSES = None + + # Detachment of Loads / Generators + if '_allow_detachment' in dict_: + if dict_["_allow_detachment"] == "True": + cls._allow_detachment = True + elif dict_["_allow_detachment"] == "False": + cls._allow_detachment = False + else: + raise ValueError(f"'allow_detachment' (value: {dict_["_allow_detachment"]}'')" + + "could not be converted to Boolean ") + else: # Compatibility for older versions + cls._allow_detachment = DEFAULT_ALLOW_DETACHMENT if 'n_busbar_per_sub' in dict_: cls.n_busbar_per_sub = int(dict_["n_busbar_per_sub"]) @@ -4405,12 +4420,6 @@ class res(GridObjects): cls.alertable_line_names = [] cls.alertable_line_ids = [] - # Detachment of Loads / Generators - if 'allow_detachment' in dict_: - cls.allow_detachment = bool(dict_["allow_detachment"]) - else: # Compatibility for older versions - cls.allow_detachment = DEFAULT_ALLOW_DETACHMENT - # save the representation of this class as dict tmp = {} cls._make_cls_dict_extended(cls, tmp, as_list=False, copy_=True) @@ -4970,7 +4979,7 @@ class {cls.__name__}({cls._INIT_GRID_CLS.__name__}): sub_info = {sub_info_str} dim_topo = {cls.dim_topo} - allow_detachment = {cls.allow_detachment} + _allow_detachment = {cls._allow_detachment} # to which substation is connected each element load_to_subid = {load_to_subid_str} @@ -5059,7 +5068,7 @@ class {cls.__name__}({cls._INIT_GRID_CLS.__name__}): alertable_line_ids = {alertable_line_ids_str} # shedding - allow_detachment = {cls.allow_detachment} + _allow_detachment = {cls._allow_detachment} """ return res diff --git a/grid2op/tests/test_Action.py b/grid2op/tests/test_Action.py index 059686f0..c8866db1 100644 --- a/grid2op/tests/test_Action.py +++ b/grid2op/tests/test_Action.py @@ -31,6 +31,7 @@ def _get_action_grid_class(): GridObjects._clear_class_attribute() GridObjects.env_name = "test_action_env" GridObjects.n_busbar_per_sub = 2 + GridObjects._allow_detachment = False GridObjects.n_gen = 5 GridObjects.name_gen = np.array(["gen_{}".format(i) for i in range(5)]) GridObjects.n_load = 11 @@ -107,6 +108,7 @@ def _get_action_grid_class(): json_ = { "glop_version": grid2op.__version__, "n_busbar_per_sub": "2", + "_allow_detachment": "False", "name_gen": ["gen_0", "gen_1", "gen_2", "gen_3", "gen_4"], "name_load": [ "load_0", diff --git a/grid2op/tests/test_shedding.py b/grid2op/tests/test_shedding.py index 7575eeef..760a8a60 100644 --- a/grid2op/tests/test_shedding.py +++ b/grid2op/tests/test_shedding.py @@ -22,7 +22,7 @@ def tearDown(self) -> None: def test_shedding_parameter_is_true(self): assert hasattr(self.env, "allow_shedding") - assert self.env.allow_detachment is True + assert self.env._allow_detachment is True def test_shed_single_load(self): # Check that a single load can be shed From aebfaa13198ec95ca24e1d3fa437b461fc340172 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Fri, 8 Nov 2024 11:11:33 +0100 Subject: [PATCH 14/60] Test: Update Backend Tests to expect _run_pf_with_diverging to catch detachments Signed-off-by: Xavier Weiss --- grid2op/tests/aaa_test_backend_interface.py | 74 +++++++++------------ grid2op/tests/test_alert_gym_compat.py | 1 + 2 files changed, 31 insertions(+), 44 deletions(-) diff --git a/grid2op/tests/aaa_test_backend_interface.py b/grid2op/tests/aaa_test_backend_interface.py index b45bd379..a3290a4c 100644 --- a/grid2op/tests/aaa_test_backend_interface.py +++ b/grid2op/tests/aaa_test_backend_interface.py @@ -814,7 +814,7 @@ def test_16_isolated_load_stops_computation(self): bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 res = backend.runpf(is_dc=False) - assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of isolated loads in AC." + # assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of isolated loads in AC." assert res[1] is not None, "When your backend diverges, we expect it throws an exception (second return value)" error = res[1] assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" @@ -829,7 +829,7 @@ def test_16_isolated_load_stops_computation(self): bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 res = backend.runpf(is_dc=True) - assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of isolated loads in DC." + # assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of isolated loads in DC." assert res[1] is not None, "When your backend diverges, we expect it throws an exception (second return value)" error = res[1] assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" @@ -862,7 +862,7 @@ def test_17_isolated_gen_stops_computation(self): bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 res = backend.runpf(is_dc=False) - assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of isolated gen." + # assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of isolated gen." assert res[1] is not None, "When your backend diverges, we expect it throws an exception (second return value)" error = res[1] assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" @@ -877,7 +877,7 @@ def test_17_isolated_gen_stops_computation(self): bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 res = backend.runpf(is_dc=True) - assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of isolated gen." + # assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of isolated gen." assert res[1] is not None, "When your backend diverges, we expect it throws an exception (second return value)" error = res[1] assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" @@ -916,7 +916,7 @@ def test_18_isolated_shunt_stops_computation(self): bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 res = backend.runpf(is_dc=False) - assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of isolated shunt." + # assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of isolated shunt." assert res[1] is not None, "When your backend diverges, we expect it throws an exception (second return value)" error = res[1] assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" @@ -931,7 +931,7 @@ def test_18_isolated_shunt_stops_computation(self): bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 res = backend.runpf(is_dc=True) - assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of isolated shunt in DC." + # assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of isolated shunt in DC." assert res[1] is not None, "When your backend stops, we expect it throws an exception (second return value)" error = res[1] assert isinstance(error, Grid2OpException), f"When your backend returns `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" @@ -967,7 +967,7 @@ def test_19_isolated_storage_stops_computation(self): bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 res = backend.runpf(is_dc=False) - assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of isolated storage units in AC." + # assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of isolated storage units in AC." assert res[1] is not None, "When your backend stops, we expect it throws an exception (second return value)" error = res[1] assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" @@ -981,7 +981,7 @@ def test_19_isolated_storage_stops_computation(self): bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 res = backend.runpf(is_dc=True) - assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of isolated storage unit." + # assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of isolated storage unit." assert res[1] is not None, "When your backend stops, we expect it throws an exception (second return value)" error = res[1] assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" @@ -989,7 +989,8 @@ def test_19_isolated_storage_stops_computation(self): warnings.warn("The error returned by your backend when it stopped (due to isolated storage units) should preferably inherit from BackendError") def test_20_disconnected_load_stops_computation(self): - """Tests that a disconnected load unit will be spotted by the `run_pf` method and forwarded to grid2op by returining `False, an_exception` (in AC and DC) + """ + Tests that a disconnected load unit will be caught by the `_runpf_with_diverging_exception` method. This test supposes that : @@ -1003,7 +1004,7 @@ def test_20_disconnected_load_stops_computation(self): .. note:: Currently this stops the computation of the environment and lead to a game over. - This behaviour might change in the future. + Behaviour changed in version 1.11.0 (no longer caught by runpf() itelf) """ self.skip_if_needed() backend = self.aux_make_backend() @@ -1014,13 +1015,9 @@ def test_20_disconnected_load_stops_computation(self): bk_act = type(backend).my_bk_act_class() bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 - res = backend.runpf(is_dc=False) - assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of disconnected load in AC." - assert res[1] is not None, "When your backend stops, we expect it throws an exception (second return value)" - error = res[1] - assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" - if not isinstance(error, BackendError): - warnings.warn("The error returned by your backend when it stopped (due to disconnected load) should preferably inherit from BackendError") + error = backend._runpf_with_diverging_exception(is_dc=False) + assert error is not None + assert isinstance(error, Grid2OpException) backend.reset(self.get_path(), self.get_casefile()) # a load alone on a bus @@ -1029,16 +1026,13 @@ def test_20_disconnected_load_stops_computation(self): bk_act = type(backend).my_bk_act_class() bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 - res = backend.runpf(is_dc=True) - assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of disconnected load in DC." - assert res[1] is not None, "When your backend stops, we expect it throws an exception (second return value)" - error = res[1] - assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" - if not isinstance(error, BackendError): - warnings.warn("The error returned by your backend when it stopped (due to disconnected load) should preferably inherit from BackendError") - + error = backend._runpf_with_diverging_exception(is_dc=True) + assert error is not None + assert isinstance(error, Grid2OpException) + def test_21_disconnected_gen_stops_computation(self): - """Tests that a disconnected generator will be spotted by the `run_pf` method and forwarded to grid2op by returining `False, an_exception` (in AC and DC) + """ + Tests that a disconnected generator will be caught by the `_runpf_with_diverging_exception` method This test supposes that : @@ -1052,7 +1046,7 @@ def test_21_disconnected_gen_stops_computation(self): .. note:: Currently this stops the computation of the environment and lead to a game over. - This behaviour might change in the future. + Behaviour changed in version 1.11.0 (no longer caught by runpf() itelf) """ self.skip_if_needed() backend = self.aux_make_backend() @@ -1063,13 +1057,9 @@ def test_21_disconnected_gen_stops_computation(self): bk_act = type(backend).my_bk_act_class() bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 - res = backend.runpf(is_dc=False) - assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of disconnected gen in AC." - assert res[1] is not None, "When your backend stops, we expect it throws an exception (second return value)" - error = res[1] - assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" - if not isinstance(error, BackendError): - warnings.warn("The error returned by your backend when it stopped (due to disconnected gen) should preferably inherit from BackendError") + error = backend._runpf_with_diverging_exception(is_dc=False) + assert error is not None + assert isinstance(error, Grid2OpException) backend.reset(self.get_path(), self.get_casefile()) # a disconnected generator @@ -1078,14 +1068,10 @@ def test_21_disconnected_gen_stops_computation(self): bk_act = type(backend).my_bk_act_class() bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 - res = backend.runpf(is_dc=True) - assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of disconnected gen in DC." - assert res[1] is not None, "When your backend stops, we expect it throws an exception (second return value)" - error = res[1] - assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" - if not isinstance(error, BackendError): - warnings.warn("The error returned by your backend when it stopped (due to disconnected gen) should preferably inherit from BackendError") - + res = backend._runpf_with_diverging_exception(is_dc=True) + assert error is not None + assert isinstance(error, Grid2OpException) + def test_22_islanded_grid_stops_computation(self): """Tests that when the grid is split in two different "sub_grid" is spotted by the `run_pf` method and forwarded to grid2op by returining `False, an_exception` (in AC and DC) @@ -1117,7 +1103,7 @@ def test_22_islanded_grid_stops_computation(self): bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 res = backend.runpf(is_dc=False) - assert not res[0], f"It is expected that your backend return `(False, _)` in case of non connected grid in AC." + # assert not res[0], f"It is expected that your backend return `(False, _)` in case of non connected grid in AC." error = res[1] assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" if not isinstance(error, BackendError): @@ -1133,7 +1119,7 @@ def test_22_islanded_grid_stops_computation(self): bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 res = backend.runpf(is_dc=True) - assert not res[0], f"It is expected that your backend return `(False, _)` in case of non connected grid in DC." + # assert not res[0], f"It is expected that your backend return `(False, _)` in case of non connected grid in DC." error = res[1] assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" if not isinstance(error, BackendError): diff --git a/grid2op/tests/test_alert_gym_compat.py b/grid2op/tests/test_alert_gym_compat.py index e522deee..04895e37 100644 --- a/grid2op/tests/test_alert_gym_compat.py +++ b/grid2op/tests/test_alert_gym_compat.py @@ -120,6 +120,7 @@ def test_print_alert(self): act.raise_alert = [2] act_gym = env_gym.action_space.to_gym(act) act_str = act_gym.__str__() + assert act_str == ("OrderedDict([('change_bus', array([False, False, False, False, False, False, False, False, False," "\n False, False, False, False, False, False, False, False, False,\n " "False, False, False, False, False, False, False, False, False,\n False, False, False, " From 486cffa74bcb6eafa2f823c336e93a2705f6e4a9 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Fri, 8 Nov 2024 11:46:10 +0100 Subject: [PATCH 15/60] Debug: Try to fix Test_Issue_125 Signed-off-by: Xavier Weiss --- grid2op/Backend/backend.py | 6 ++++-- grid2op/Converter/BackendConverter.py | 5 ++--- grid2op/tests/helper_path_test.py | 4 +++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index 2b21d61b..b38becc1 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -1083,8 +1083,10 @@ def _runpf_with_diverging_exception(self, is_dc : bool) -> Optional[Exception]: try: # Check if loads/gens have been detached and if this is allowed, otherwise raise an error # .. versionadded:: 1.11.0 - topo_vect = self._get_topo_vect() - + if hasattr(self, "_get_topo_vect"): + topo_vect = self._get_topo_vect() + else: + topo_vect = self.get_topo_vect() load_buses = topo_vect[self.load_pos_topo_vect] if not self._allow_detachment and (load_buses == -1).any(): raise Grid2OpException(f"One or more loads were detached before powerflow in Backend {type(self).__name__}" diff --git a/grid2op/Converter/BackendConverter.py b/grid2op/Converter/BackendConverter.py index 84bed15c..38178908 100644 --- a/grid2op/Converter/BackendConverter.py +++ b/grid2op/Converter/BackendConverter.py @@ -95,7 +95,6 @@ def __init__( use_target_backend_name=False, kwargs_target_backend=None, kwargs_source_backend=None, - allow_detachment:bool=False, ): Backend.__init__( self, @@ -103,7 +102,6 @@ def __init__( use_target_backend_name=use_target_backend_name, kwargs_target_backend=kwargs_target_backend, kwargs_source_backend=kwargs_source_backend, - allow_detachment=allow_detachment, ) difcf = detailed_infos_for_cascading_failures if kwargs_source_backend is None: @@ -167,10 +165,11 @@ def load_grid(self, path=None, filename=None): # register the "n_busbar_per_sub" (set for the backend class) # TODO in case source supports the "more than 2" feature but not target # it's unclear how I can "reload" the grid... - from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_DETACHMENT + from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB type(self.source_backend).set_n_busbar_per_sub(DEFAULT_N_BUSBAR_PER_SUB) type(self.target_backend).set_n_busbar_per_sub(DEFAULT_N_BUSBAR_PER_SUB) self.cannot_handle_more_than_2_busbar() + self.cannot_handle_detachment() self.source_backend.load_grid(path, filename) # and now i load the target backend diff --git a/grid2op/tests/helper_path_test.py b/grid2op/tests/helper_path_test.py index cd72b9ef..39d8439a 100644 --- a/grid2op/tests/helper_path_test.py +++ b/grid2op/tests/helper_path_test.py @@ -67,12 +67,14 @@ class MakeBackend(ABC, HelperTests): def make_backend(self, detailed_infos_for_cascading_failures=False) -> Backend: pass - def make_backend_with_glue_code(self, detailed_infos_for_cascading_failures=False, extra_name="", n_busbar=2) -> Backend: + def make_backend_with_glue_code(self, detailed_infos_for_cascading_failures=False, extra_name="", + n_busbar=2, allow_detachment=False) -> Backend: Backend._clear_class_attribute() bk = self.make_backend(detailed_infos_for_cascading_failures=detailed_infos_for_cascading_failures) type(bk)._clear_grid_dependant_class_attributes() type(bk).set_env_name(type(self).__name__ + extra_name) type(bk).set_n_busbar_per_sub(n_busbar) + type(bk)._allow_detachment = allow_detachment return bk def get_path(self) -> str: From 938709a96175fbe2e1c74dda6abbff2d8cffd13a Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Sat, 23 Nov 2024 20:53:29 +0100 Subject: [PATCH 16/60] Add: 'detachment_is_allowed' in GridObj and Environment Signed-off-by: Xavier Weiss --- grid2op/Backend/backend.py | 16 ++++++------- grid2op/Converter/BackendConverter.py | 4 +++- grid2op/Environment/environment.py | 1 + grid2op/Space/GridObjects.py | 34 +++++++++++++++------------ grid2op/tests/test_shedding.py | 2 +- 5 files changed, 32 insertions(+), 25 deletions(-) diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index b38becc1..41364ca1 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -182,7 +182,7 @@ def __init__(self, # .. versionadded: 1.11.0 self._missing_detachment_support:bool = True - self._allow_detachment:bool = DEFAULT_ALLOW_DETACHMENT + self.detachment_is_allowed:bool = DEFAULT_ALLOW_DETACHMENT def can_handle_more_than_2_busbar(self): """ @@ -271,7 +271,7 @@ def can_handle_detachment(self): At least, at time of writing there is no good reason to do so. """ self._missing_detachment_support = False - self._allow_detachment = type(self)._allow_detachment + self.detachment_is_allowed = type(self).detachment_is_allowed def cannot_handle_detachment(self): """ @@ -298,10 +298,10 @@ def cannot_handle_detachment(self): At least, at time of writing there is no good reason to do so. """ self._missing_detachment_support = False - if type(self._allow_detachment != DEFAULT_ALLOW_DETACHMENT): + if type(self).detachment_is_allowed != DEFAULT_ALLOW_DETACHMENT: warnings.warn("You asked in 'make' function to allow shedding. This is" f"not possible with a backend of type {type(self)}.") - self._allow_detachment = DEFAULT_ALLOW_DETACHMENT + self.detachment_is_allowed = DEFAULT_ALLOW_DETACHMENT def make_complete_path(self, path : Union[os.PathLike, str], @@ -1088,12 +1088,12 @@ def _runpf_with_diverging_exception(self, is_dc : bool) -> Optional[Exception]: else: topo_vect = self.get_topo_vect() load_buses = topo_vect[self.load_pos_topo_vect] - if not self._allow_detachment and (load_buses == -1).any(): + if not self.detachment_is_allowed and (load_buses == -1).any(): raise Grid2OpException(f"One or more loads were detached before powerflow in Backend {type(self).__name__}" "but this is not allowed or not supported (Game Over)") gen_buses = topo_vect[self.gen_pos_topo_vect] - if not self._allow_detachment and (gen_buses == -1).any(): + if not self.detachment_is_allowed and (gen_buses == -1).any(): raise Grid2OpException(f"One or more generators were detached before powerflow in Backend {type(self).__name__}" "but this is not allowed or not supported (Game Over)") @@ -2175,10 +2175,10 @@ def assert_grid_correct(self, _local_dir_cls=None) -> None: "of your backend cannot handle detachment then change it :-)\n" "Your backend will behave as if it did not support it.") self._missing_detachment_support = False - self._allow_detachment = DEFAULT_ALLOW_DETACHMENT + self.detachment_is_allowed = DEFAULT_ALLOW_DETACHMENT else: self._missing_detachment_support = False - self._allow_detachment = DEFAULT_ALLOW_DETACHMENT + self.detachment_is_allowed = DEFAULT_ALLOW_DETACHMENT warnings.warn("Your backend is missing the `_missing_detachment_support` " "attribute.") diff --git a/grid2op/Converter/BackendConverter.py b/grid2op/Converter/BackendConverter.py index 38178908..19e62c2a 100644 --- a/grid2op/Converter/BackendConverter.py +++ b/grid2op/Converter/BackendConverter.py @@ -165,9 +165,11 @@ def load_grid(self, path=None, filename=None): # register the "n_busbar_per_sub" (set for the backend class) # TODO in case source supports the "more than 2" feature but not target # it's unclear how I can "reload" the grid... - from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB + from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_DETACHMENT type(self.source_backend).set_n_busbar_per_sub(DEFAULT_N_BUSBAR_PER_SUB) type(self.target_backend).set_n_busbar_per_sub(DEFAULT_N_BUSBAR_PER_SUB) + type(self.source_backend).set_detachment_is_allowed(DEFAULT_ALLOW_DETACHMENT) + type(self.target_backend).set_detachment_is_allowed(DEFAULT_ALLOW_DETACHMENT) self.cannot_handle_more_than_2_busbar() self.cannot_handle_detachment() diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 39fc4482..e29a7238 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -280,6 +280,7 @@ def _init_backend( # this is due to the class attribute type(self.backend).set_env_name(self.name) type(self.backend).set_n_busbar_per_sub(self._n_busbar) + type(self.backend).set_detachment_is_allowed(self._allow_detachment) if self._compat_glop_version is not None: type(self.backend).glop_version = self._compat_glop_version diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index dbe3f9f5..55382a1e 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -514,7 +514,7 @@ class GridObjects: sub_info : ClassVar[np.ndarray] = None dim_topo : ClassVar[np.ndarray] = -1 - _allow_detachment : ClassVar[bool] = DEFAULT_ALLOW_DETACHMENT + detachment_is_allowed : ClassVar[bool] = DEFAULT_ALLOW_DETACHMENT # to which substation is connected each element load_to_subid : ClassVar[np.ndarray] = None @@ -643,6 +643,10 @@ def set_n_busbar_per_sub(cls, n_busbar_per_sub: N_BUSBAR_PER_SUB_TYPING) -> None # TODO n_busbar_per_sub different num per substations cls.n_busbar_per_sub = n_busbar_per_sub + @classmethod + def set_detachment_is_allowed(cls, detachment_is_allowed: bool) -> None: + cls.detachment_is_allowed = detachment_is_allowed + @classmethod def tell_dim_alarm(cls, dim_alarms: int) -> None: if cls.dim_alarms != 0: @@ -687,7 +691,7 @@ def _clear_class_attribute(cls) -> None: """ cls.shunts_data_available = False cls.n_busbar_per_sub = DEFAULT_N_BUSBAR_PER_SUB - cls._allow_detachment = DEFAULT_ALLOW_DETACHMENT + cls.detachment_is_allowed = DEFAULT_ALLOW_DETACHMENT # for redispatching / unit commitment cls._li_attr_disp = [ @@ -2966,7 +2970,7 @@ def init_grid(cls, gridobj, force=False, extra_name=None, force_module=None, _lo # different `n_busbar_per_sub` name_res += f"_{gridobj.n_busbar_per_sub}" - if gridobj._allow_detachment != DEFAULT_ALLOW_DETACHMENT: + if gridobj.detachment_is_allowed != DEFAULT_ALLOW_DETACHMENT: name_res += "_allowDetach" if _local_dir_cls is not None and gridobj._PATH_GRID_CLASSES is not None: @@ -3089,7 +3093,7 @@ def process_grid2op_compat(cls): if glop_ver < version.parse("1.11.0.dev0"): # Detachment did not exist, default value should have # no effect - cls._allow_detachment = DEFAULT_ALLOW_DETACHMENT + cls.detachment_is_allowed = DEFAULT_ALLOW_DETACHMENT res = True if res: @@ -3743,7 +3747,7 @@ def _make_cls_dict(cls, res, as_list=True, copy_=True, _topo_vect_only=False): res[k] = v return - save_to_dict(res, cls, "_allow_detachment", str, copy_) + save_to_dict(res, cls, "detachment_is_allowed", str, copy_) if not _topo_vect_only: # all the attributes bellow are not needed for the "first call" @@ -4124,7 +4128,7 @@ def _make_cls_dict_extended(cls, res: CLS_AS_DICT_TYPING, as_list=True, copy_=Tr # n_busbar_per_sub res["n_busbar_per_sub"] = cls.n_busbar_per_sub - res["_allow_detachment"] = cls._allow_detachment + res["detachment_is_allowed"] = cls.detachment_is_allowed # avoid further computation and save it if not as_list and not _topo_vect_only: @@ -4200,16 +4204,16 @@ class res(GridObjects): cls._PATH_GRID_CLASSES = None # Detachment of Loads / Generators - if '_allow_detachment' in dict_: - if dict_["_allow_detachment"] == "True": - cls._allow_detachment = True - elif dict_["_allow_detachment"] == "False": - cls._allow_detachment = False + if 'detachment_is_allowed' in dict_: + if dict_["detachment_is_allowed"] == "True": + cls.detachment_is_allowed = True + elif dict_["detachment_is_allowed"] == "False": + cls.detachment_is_allowed = False else: - raise ValueError(f"'allow_detachment' (value: {dict_["_allow_detachment"]}'')" + - "could not be converted to Boolean ") + raise ValueError(f"'detachment_is_allowed' (value: {dict_["detachment_is_allowed"]}'')" + + "could not be converted to Boolean ") else: # Compatibility for older versions - cls._allow_detachment = DEFAULT_ALLOW_DETACHMENT + cls.detachment_is_allowed = DEFAULT_ALLOW_DETACHMENT if 'n_busbar_per_sub' in dict_: cls.n_busbar_per_sub = int(dict_["n_busbar_per_sub"]) @@ -4979,7 +4983,7 @@ class {cls.__name__}({cls._INIT_GRID_CLS.__name__}): sub_info = {sub_info_str} dim_topo = {cls.dim_topo} - _allow_detachment = {cls._allow_detachment} + detachment_is_allowed = {cls.detachment_is_allowed} # to which substation is connected each element load_to_subid = {load_to_subid_str} diff --git a/grid2op/tests/test_shedding.py b/grid2op/tests/test_shedding.py index 760a8a60..b59eef0e 100644 --- a/grid2op/tests/test_shedding.py +++ b/grid2op/tests/test_shedding.py @@ -21,8 +21,8 @@ def tearDown(self) -> None: self.env.close() def test_shedding_parameter_is_true(self): - assert hasattr(self.env, "allow_shedding") assert self.env._allow_detachment is True + assert self.env.backend.detachment_is_allowed is True def test_shed_single_load(self): # Check that a single load can be shed From 4112dfeb0e0db2dd3c9f13efadae1e149f6f1c6a Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Sat, 23 Nov 2024 20:56:22 +0100 Subject: [PATCH 17/60] Debug: assert 'detachment_is_allowed' in GridObject Signed-off-by: Xavier Weiss --- grid2op/Space/GridObjects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 55382a1e..145eaba3 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -2331,7 +2331,7 @@ def assert_grid_correct_cls(cls): # alert data cls._check_validity_alert_data() - assert isinstance(cls._allow_detachment, bool) + assert isinstance(cls.detachment_is_allowed, bool) @classmethod def _check_validity_alarm_data(cls): From 7b6dabc7c0e641a453ab482ade39851d0abbfaf3 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Sun, 24 Nov 2024 09:31:13 +0100 Subject: [PATCH 18/60] Test: Update strings for detachment Signed-off-by: Xavier Weiss --- grid2op/tests/test_Action.py | 2 +- grid2op/tests/test_Observation.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/grid2op/tests/test_Action.py b/grid2op/tests/test_Action.py index c8866db1..a852b3ed 100644 --- a/grid2op/tests/test_Action.py +++ b/grid2op/tests/test_Action.py @@ -108,7 +108,7 @@ def _get_action_grid_class(): json_ = { "glop_version": grid2op.__version__, "n_busbar_per_sub": "2", - "_allow_detachment": "False", + "detachment_is_allowed": "False", "name_gen": ["gen_0", "gen_1", "gen_2", "gen_3", "gen_4"], "name_load": [ "load_0", diff --git a/grid2op/tests/test_Observation.py b/grid2op/tests/test_Observation.py index dff0b205..cb621595 100644 --- a/grid2op/tests/test_Observation.py +++ b/grid2op/tests/test_Observation.py @@ -53,6 +53,7 @@ def setUp(self): self.dict_ = { "name_gen": ["gen_1_0", "gen_2_1", "gen_5_2", "gen_7_3", "gen_0_4"], "n_busbar_per_sub": "2", + "detachment_is_allowed": "False", "name_load": [ "load_1_0", "load_2_1", From 0666b398c4e60b04a2974864fa69161807e5c0d5 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Sun, 24 Nov 2024 09:36:33 +0100 Subject: [PATCH 19/60] Debug: Fix nested f-string in GridObjects Signed-off-by: Xavier Weiss --- grid2op/Space/GridObjects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 145eaba3..4737c802 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -4210,7 +4210,7 @@ class res(GridObjects): elif dict_["detachment_is_allowed"] == "False": cls.detachment_is_allowed = False else: - raise ValueError(f"'detachment_is_allowed' (value: {dict_["detachment_is_allowed"]}'')" + + raise ValueError(f"'detachment_is_allowed' (value: {dict_['detachment_is_allowed']}'')" + "could not be converted to Boolean ") else: # Compatibility for older versions cls.detachment_is_allowed = DEFAULT_ALLOW_DETACHMENT From 7cdf6c2a0d4a5902222bec3ed75a8804599c372e Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Sun, 24 Nov 2024 10:56:56 +0100 Subject: [PATCH 20/60] Debug: Add post-powerflow check for load / gen disconnect Signed-off-by: Xavier Weiss --- grid2op/Backend/backend.py | 12 ++++++++++++ grid2op/Space/GridObjects.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index 41364ca1..78c5aabb 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -1087,12 +1087,15 @@ def _runpf_with_diverging_exception(self, is_dc : bool) -> Optional[Exception]: topo_vect = self._get_topo_vect() else: topo_vect = self.get_topo_vect() + load_buses = topo_vect[self.load_pos_topo_vect] + if not self.detachment_is_allowed and (load_buses == -1).any(): raise Grid2OpException(f"One or more loads were detached before powerflow in Backend {type(self).__name__}" "but this is not allowed or not supported (Game Over)") gen_buses = topo_vect[self.gen_pos_topo_vect] + if not self.detachment_is_allowed and (gen_buses == -1).any(): raise Grid2OpException(f"One or more generators were detached before powerflow in Backend {type(self).__name__}" "but this is not allowed or not supported (Game Over)") @@ -1106,6 +1109,15 @@ def _runpf_with_diverging_exception(self, is_dc : bool) -> Optional[Exception]: "GAME OVER: Powerflow has diverged during computation " "or a load has been disconnected or a generator has been disconnected." ) + + # Post-Powerflow Check + if not self.detachment_is_allowed: + resulting_act = self.get_action_to_set() + load_buses_act_set = resulting_act._set_topo_vect[self.load_pos_topo_vect] + gen_buses_act_set = resulting_act._set_topo_vect[self.gen_pos_topo_vect] + if (load_buses_act_set == -1).any() or (gen_buses_act_set == -1).any(): + exc_me = Grid2OpException(f"One or more generators or loads were detached in Backend {type(self).__name__}" + " as a result of a Grid2Op action, but this is not allowed or not supported (Game Over)") return exc_me def next_grid_state(self, diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 4737c802..37b36685 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -5072,7 +5072,7 @@ class {cls.__name__}({cls._INIT_GRID_CLS.__name__}): alertable_line_ids = {alertable_line_ids_str} # shedding - _allow_detachment = {cls._allow_detachment} + detachment_is_allowed = {cls.detachment_is_allowed} """ return res From bb231937b0c7689490c34f45492733660d8c7703 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Sun, 24 Nov 2024 11:04:42 +0100 Subject: [PATCH 21/60] Debug: Add convergence check to post-powerflow check Signed-off-by: Xavier Weiss --- grid2op/Backend/backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index 78c5aabb..73735ae7 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -1111,7 +1111,7 @@ def _runpf_with_diverging_exception(self, is_dc : bool) -> Optional[Exception]: ) # Post-Powerflow Check - if not self.detachment_is_allowed: + if not self.detachment_is_allowed and conv: resulting_act = self.get_action_to_set() load_buses_act_set = resulting_act._set_topo_vect[self.load_pos_topo_vect] gen_buses_act_set = resulting_act._set_topo_vect[self.gen_pos_topo_vect] From e921057fe9473d90d84f704985ec6b3d0599f9ca Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Mon, 25 Nov 2024 10:04:46 +0100 Subject: [PATCH 22/60] fix some minor things, before handling actions [skip ci] Signed-off-by: DONNOT Benjamin --- docs/conf.py | 2 +- grid2op/Backend/backend.py | 16 ++++++---------- grid2op/Space/GridObjects.py | 16 ++++++++++++---- grid2op/__init__.py | 2 +- grid2op/gym_compat/box_gym_obsspace.py | 3 ++- 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index fc753b64..5205f702 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ author = 'Benjamin Donnot' # The full version, including alpha/beta/rc tags -release = '1.11.0.dev1' +release = '1.11.0.dev2' version = '1.11' diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index 1aae3966..4144cb33 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -1081,26 +1081,22 @@ def _runpf_with_diverging_exception(self, is_dc : bool) -> Optional[Exception]: exc_me = None try: + conv, exc_me = self.runpf(is_dc=is_dc) # run powerflow + # Check if loads/gens have been detached and if this is allowed, otherwise raise an error # .. versionadded:: 1.11.0 - if hasattr(self, "_get_topo_vect"): - topo_vect = self._get_topo_vect() - else: - topo_vect = self.get_topo_vect() - - load_buses = topo_vect[self.load_pos_topo_vect] - + cls = type(self) + topo_vect = self.get_topo_vect() + load_buses = topo_vect[cls.load_pos_topo_vect] if not self.detachment_is_allowed and (load_buses == -1).any(): raise Grid2OpException(f"One or more loads were detached before powerflow in Backend {type(self).__name__}" "but this is not allowed or not supported (Game Over)") - gen_buses = topo_vect[self.gen_pos_topo_vect] - + gen_buses = topo_vect[cls.gen_pos_topo_vect] if not self.detachment_is_allowed and (gen_buses == -1).any(): raise Grid2OpException(f"One or more generators were detached before powerflow in Backend {type(self).__name__}" "but this is not allowed or not supported (Game Over)") - conv, exc_me = self.runpf(is_dc=is_dc) # run powerflow except Grid2OpException as exc_: exc_me = exc_ diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index ed8517f5..90d820cf 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -2040,7 +2040,16 @@ def assert_grid_correct_cls(cls): if isinstance(cls.n_busbar_per_sub, (int, dt_int, np.int32, np.int64)): cls.n_busbar_per_sub = dt_int(cls.n_busbar_per_sub) else: - raise EnvError("Grid2op cannot handle a different number of busbar per substations at the moment.") + raise EnvError("Grid2op cannot handle a different number " + "of busbar per substations with provided input " + "(make sure `n_busbar_per_sub` is an int)") + + if isinstance(cls.detachment_is_allowed, (bool, dt_bool)): + cls.detachment_is_allowed = dt_bool(cls.detachment_is_allowed) + else: + raise EnvError("Grid2op cannot handle disconnection of loads / generators " + "at the moment (make sure `detachment_is_allowed` " + "is a bool)") if (cls.n_busbar_per_sub < 1).any(): raise EnvError(f"`n_busbar_per_sub` should be >= 1 found {cls.n_busbar_per_sub}") @@ -3091,7 +3100,7 @@ def process_grid2op_compat(cls): cls.n_busbar_per_sub = DEFAULT_N_BUSBAR_PER_SUB res = True - if glop_ver < version.parse("1.11.0.dev0"): + if glop_ver < version.parse("1.11.0.dev2"): # Detachment did not exist, default value should have # no effect cls.detachment_is_allowed = DEFAULT_ALLOW_DETACHMENT @@ -4211,7 +4220,7 @@ class res(GridObjects): elif dict_["detachment_is_allowed"] == "False": cls.detachment_is_allowed = False else: - raise ValueError(f"'detachment_is_allowed' (value: {dict_['detachment_is_allowed']}'')" + + raise ValueError(f"'detachment_is_allowed' (value: {dict_['detachment_is_allowed']}'')" "could not be converted to Boolean ") else: # Compatibility for older versions cls.detachment_is_allowed = DEFAULT_ALLOW_DETACHMENT @@ -4984,7 +4993,6 @@ class {cls.__name__}({cls._INIT_GRID_CLS.__name__}): sub_info = {sub_info_str} dim_topo = {cls.dim_topo} - detachment_is_allowed = {cls.detachment_is_allowed} # to which substation is connected each element load_to_subid = {load_to_subid_str} diff --git a/grid2op/__init__.py b/grid2op/__init__.py index 35522b93..ec46d6b8 100644 --- a/grid2op/__init__.py +++ b/grid2op/__init__.py @@ -11,7 +11,7 @@ Grid2Op """ -__version__ = '1.11.0.dev1' +__version__ = '1.11.0.dev2' __all__ = [ "Action", diff --git a/grid2op/gym_compat/box_gym_obsspace.py b/grid2op/gym_compat/box_gym_obsspace.py index 298488cb..795cf7ad 100644 --- a/grid2op/gym_compat/box_gym_obsspace.py +++ b/grid2op/gym_compat/box_gym_obsspace.py @@ -849,7 +849,8 @@ def get_indexes(self, key: str) -> Tuple[int, int]: key = "redispatch" # "redispatch", "curtail", "set_storage" start_, end_ = gym_env.action_space.get_indexes(key) act[start_:end_] = np.random.uniform(high=1, low=-1, size=env.gen_redispatchable.sum()) - # act only modifies the redispatch with the input given (here a uniform redispatching between -1 and 1) + # act only modifies the redispatch with the input given + # (here a uniform redispatching between -1 and 1) """ error_msg =(f"Impossible to use the grid2op action property \"{key}\"" From d9b3e11b5ca40060a788a5c16f97645d26fad806 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Mon, 25 Nov 2024 11:12:42 +0100 Subject: [PATCH 23/60] some addition of the feature on the action, not finished yet Signed-off-by: DONNOT Benjamin --- grid2op/Action/baseAction.py | 450 +++++++++++++++++++++++++-------- grid2op/MakeEnv/Make.py | 2 +- grid2op/Space/GridObjects.py | 2 - grid2op/tests/test_shedding.py | 54 +++- grid2op/typing_variables.py | 3 +- 5 files changed, 398 insertions(+), 113 deletions(-) diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 2d321638..776db465 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -370,6 +370,7 @@ class BaseAction(GridObjects): "curtail", "raise_alarm", "raise_alert", + "detach_load", # new in 1.11.0 } attr_list_vect = [ @@ -388,6 +389,7 @@ class BaseAction(GridObjects): "_curtail", "_raise_alarm", "_raise_alert", + "_detach_load", # new in 1.11.0 ] attr_nan_list_set = set() @@ -416,6 +418,7 @@ def __init__(self, _names_chronics_to_backend: Optional[Dict[Literal["loads", "p """ GridObjects.__init__(self) + cls = type(self) if _names_chronics_to_backend is not None: # should only be the case for the "init state" action self._names_chronics_to_backend = _names_chronics_to_backend @@ -423,48 +426,48 @@ def __init__(self, _names_chronics_to_backend: Optional[Dict[Literal["loads", "p self._names_chronics_to_backend = None # False(line is disconnected) / True(line is connected) - self._set_line_status = np.full(shape=self.n_line, fill_value=0, dtype=dt_int) + self._set_line_status = np.full(shape=cls.n_line, fill_value=0, dtype=dt_int) self._switch_line_status = np.full( - shape=self.n_line, fill_value=False, dtype=dt_bool + shape=cls.n_line, fill_value=False, dtype=dt_bool ) # injection change self._dict_inj = {} # topology changed - self._set_topo_vect = np.full(shape=self.dim_topo, fill_value=0, dtype=dt_int) + self._set_topo_vect = np.full(shape=cls.dim_topo, fill_value=0, dtype=dt_int) self._change_bus_vect = np.full( - shape=self.dim_topo, fill_value=False, dtype=dt_bool + shape=cls.dim_topo, fill_value=False, dtype=dt_bool ) # add the hazards and maintenance usefull for saving. - self._hazards = np.full(shape=self.n_line, fill_value=False, dtype=dt_bool) - self._maintenance = np.full(shape=self.n_line, fill_value=False, dtype=dt_bool) + self._hazards = np.full(shape=cls.n_line, fill_value=False, dtype=dt_bool) + self._maintenance = np.full(shape=cls.n_line, fill_value=False, dtype=dt_bool) # redispatching vector - self._redispatch = np.full(shape=self.n_gen, fill_value=0.0, dtype=dt_float) + self._redispatch = np.full(shape=cls.n_gen, fill_value=0.0, dtype=dt_float) # storage unit vector self._storage_power = np.full( - shape=self.n_storage, fill_value=0.0, dtype=dt_float + shape=cls.n_storage, fill_value=0.0, dtype=dt_float ) # curtailment of renewable energy - self._curtail = np.full(shape=self.n_gen, fill_value=-1.0, dtype=dt_float) + self._curtail = np.full(shape=cls.n_gen, fill_value=-1.0, dtype=dt_float) self._vectorized = None self._lines_impacted = None self._subs_impacted = None # shunts - if type(self).shunts_data_available: + if cls.shunts_data_available: self.shunt_p = np.full( - shape=self.n_shunt, fill_value=np.NaN, dtype=dt_float + shape=cls.n_shunt, fill_value=np.NaN, dtype=dt_float ) self.shunt_q = np.full( - shape=self.n_shunt, fill_value=np.NaN, dtype=dt_float + shape=cls.n_shunt, fill_value=np.NaN, dtype=dt_float ) - self.shunt_bus = np.full(shape=self.n_shunt, fill_value=0, dtype=dt_int) + self.shunt_bus = np.full(shape=cls.n_shunt, fill_value=0, dtype=dt_int) else: self.shunt_p = None self.shunt_q = None @@ -473,13 +476,18 @@ def __init__(self, _names_chronics_to_backend: Optional[Dict[Literal["loads", "p self._single_act = True self._raise_alarm = np.full( - shape=self.dim_alarms, dtype=dt_bool, fill_value=False - ) # TODO + shape=cls.dim_alarms, dtype=dt_bool, fill_value=False + ) self._raise_alert = np.full( - shape=self.dim_alerts, dtype=dt_bool, fill_value=False - ) # TODO + shape=cls.dim_alerts, dtype=dt_bool, fill_value=False + ) + if cls.detachment_is_allowed: + self._detach_load = np.full(cls.n_load, dtype=dt_bool, fill_value=False) + else: + self._detach_load = None + # change the stuff self._modif_inj = False self._modif_set_bus = False @@ -491,6 +499,7 @@ def __init__(self, _names_chronics_to_backend: Optional[Dict[Literal["loads", "p self._modif_curtailment = False self._modif_alarm = False self._modif_alert = False + self._modif_detach_load = False @classmethod def process_shunt_static_data(cls): @@ -530,6 +539,7 @@ def _aux_copy(self, other): "_modif_curtailment", "_modif_alarm", "_modif_alert", + "_modif_detach_load", "_single_act", ] @@ -545,6 +555,7 @@ def _aux_copy(self, other): "_curtail", "_raise_alarm", "_raise_alert", + "_detach_load", ] if type(self).shunts_data_available: @@ -743,6 +754,13 @@ def as_serializable_dict(self) -> dict: ] if not res["shunt"]: del res["shunt"] + + if cls.detachment_is_allowed: + res["loads_detached"] = {} + if self._detach_load.any(): + res["loads_detached"] = [int(el) for el in self._detach_load.nonzero()[0]] + if not res["loads_detached"]: + del res["loads_detached"] return res @classmethod @@ -837,6 +855,16 @@ def process_grid2op_compat(cls): if glop_ver < version.parse("1.9.1"): # this feature did not exist before. cls.dim_alerts = 0 + + if glop_ver < version.parse("1.11.0.dev2"): + # this feature did not exist before. + cls.authorized_keys = copy.deepcopy(cls.authorized_keys) + cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) + + if "detach_load" in cls.authorized_keys: + cls.authorized_keys.remove("detach_load") + if "_detach_load" in cls.attr_list_vect: + cls.attr_list_vect.remove("_detach_load") if (cls.n_busbar_per_sub >= 3) or (cls.n_busbar_per_sub == 1): # only relevant for grid2op >= 1.10.0 @@ -860,6 +888,7 @@ def _reset_modified_flags(self): self._modif_curtailment = False self._modif_alarm = False self._modif_alert = False + self._modif_detach_load = False def can_affect_something(self) -> bool: """ @@ -880,6 +909,7 @@ def can_affect_something(self) -> bool: or self._modif_curtailment or self._modif_alarm or self._modif_alert + or self._modif_detach_load ) def _get_array_from_attr_name(self, attr_name): @@ -913,6 +943,7 @@ def _post_process_from_vect(self): self._modif_curtailment = (np.abs(self._curtail + 1.0) >= 1e-7).any() self._modif_alarm = self._raise_alarm.any() self._modif_alert = self._raise_alert.any() + self._modif_change_bus = (self._detach_load).any() def _assign_attr_from_name(self, attr_nm, vect): if hasattr(self, attr_nm): @@ -986,8 +1017,63 @@ def get_change_line_status_vect(self) -> np.ndarray: """ return np.full(shape=self.n_line, fill_value=False, dtype=dt_bool) - - def __eq__(self, other) -> bool: + + def _aux_eq_detachment(self, other: "BaseAction") -> bool: + cls = type(self) + cls_oth = type(other) + if cls.detachment_is_allowed: + if cls_oth.detachment_is_allowed: + # easy case, both detachement allowed + if ((self._modif_detach_load != other._modif_detach_load) or + (self._detach_load != other._detach_load).any() + ): + return False + else: + # self supports detachment but not other + # they are equal if an only if self did not + # modify any loads with detachment + if self._modif_detach_load: + return False + if self._detach_load.any(): + return False + else: + # detachment is not allowed on self + # check if it's allowed on other + if cls_oth.detachment_is_allowed: + # hard case, I don't support detachment, but + # oth does. + # they can be equal if oth does not modify this + # attribute + if other._modif_detach_load: + return False + if other._detach_load.any(): + return False + else: + # easy case, None supports detachment + return True + return True + + def _aux_eq_shunts(self, other: "BaseAction") -> bool: + if type(self).shunts_data_available: + if self.n_shunt != other.n_shunt: + return False + is_ok_me = np.isfinite(self.shunt_p) + is_ok_ot = np.isfinite(other.shunt_p) + if (is_ok_me != is_ok_ot).any(): + return False + if not (self.shunt_p[is_ok_me] == other.shunt_p[is_ok_ot]).all(): + return False + is_ok_me = np.isfinite(self.shunt_q) + is_ok_ot = np.isfinite(other.shunt_q) + if (is_ok_me != is_ok_ot).any(): + return False + if not (self.shunt_q[is_ok_me] == other.shunt_q[is_ok_ot]).all(): + return False + if not (self.shunt_bus == other.shunt_bus).all(): + return False + return True + + def __eq__(self, other: "BaseAction") -> bool: """ Test the equality of two actions. @@ -1021,9 +1107,9 @@ def __eq__(self, other) -> bool: """ if other is None: return False - + cls = type(self) # check that the underlying grid is the same in both instances - same_grid = type(self).same_grid_class(type(other)) + same_grid = cls.same_grid_class(type(other)) if not same_grid: return False @@ -1099,25 +1185,14 @@ def __eq__(self, other) -> bool: ): return False + # handle detachment + if not self._aux_eq_detachment(other): + return False + # shunts are the same - if type(self).shunts_data_available: - if self.n_shunt != other.n_shunt: - return False - is_ok_me = np.isfinite(self.shunt_p) - is_ok_ot = np.isfinite(other.shunt_p) - if (is_ok_me != is_ok_ot).any(): - return False - if not (self.shunt_p[is_ok_me] == other.shunt_p[is_ok_ot]).all(): - return False - is_ok_me = np.isfinite(self.shunt_q) - is_ok_ot = np.isfinite(other.shunt_q) - if (is_ok_me != is_ok_ot).any(): - return False - if not (self.shunt_q[is_ok_me] == other.shunt_q[is_ok_ot]).all(): - return False - if not (self.shunt_bus == other.shunt_bus).all(): - return False - + if not self._aux_eq_shunts(other): + return False + return True def _dont_affect_topology(self) -> bool: @@ -1126,6 +1201,7 @@ def _dont_affect_topology(self) -> bool: and (not self._modif_change_bus) and (not self._modif_set_status) and (not self._modif_change_status) + and (not self._modif_detach_load) ) def get_topological_impact(self, powerline_status=None) -> Tuple[np.ndarray, np.ndarray]: @@ -1597,6 +1673,7 @@ def _aux_iadd_modif_flags(self, other): self._modif_curtailment = self._modif_curtailment or other._modif_curtailment self._modif_alarm = self._modif_alarm or other._modif_alarm self._modif_alert = self._modif_alert or other._modif_alert + self._modif_detach_load = self._modif_detach_load or other._modif_detach_load def _aux_iadd_shunt(self, other): if not type(other).shunts_data_available: @@ -1954,10 +2031,10 @@ def _digest_injection(self, dict_): def _digest_setbus(self, dict_): if "set_bus" in dict_: - self._modif_set_bus = True if dict_["set_bus"] is None: # no real action has been made return + self._modif_set_bus = True if isinstance(dict_["set_bus"], dict): ddict_ = dict_["set_bus"] @@ -1997,10 +2074,10 @@ def _digest_setbus(self, dict_): def _digest_change_bus(self, dict_): if "change_bus" in dict_: - self._modif_change_bus = True if dict_["change_bus"] is None: # no real action has been made return + self._modif_change_bus = True if isinstance(dict_["change_bus"], dict): ddict_ = dict_["change_bus"] @@ -2115,6 +2192,16 @@ def _digest_change_status(self, dict_): if dict_["change_line_status"] is not None: self.line_change_status = dict_["change_line_status"] + def _digest_detach_load(self, dict_): + if "detach_load" in dict_: + # the action will switch the status of the powerline + # for each element equal to 1 in this dict_["change_line_status"] + # if the status is "disconnected" it will be transformed into "connected" + # and if the status is "connected" it will be switched to "disconnected" + # Lines with "0" in this vector are not impacted. + if dict_["detach_load"] is not None: + self.detach_load = dict_["detach_load"] + def _digest_redispatching(self, dict_): if "redispatch" in dict_: self.redispatch = dict_["redispatch"] @@ -2417,7 +2504,8 @@ def update(self, self._digest_change_status(dict_) self._digest_alarm(dict_) self._digest_alert(dict_) - + if cls.detachment_is_allowed: + self._digest_detach_load(dict_) return self def is_ambiguous(self) -> Tuple[bool, AmbiguousAction]: @@ -2443,13 +2531,14 @@ def is_ambiguous(self) -> Tuple[bool, AmbiguousAction]: return res, info def _check_for_correct_modif_flags(self): + cls = type(self) if self._dict_inj: if not self._modif_inj: raise AmbiguousAction( "A action on the injection is performed while the appropriate flag is not " "set. Please use the official grid2op action API to modify the injections." ) - if "injection" not in self.authorized_keys: + if "injection" not in cls.authorized_keys: raise IllegalAction("You illegally act on the injection") if self._change_bus_vect.any(): if not self._modif_change_bus: @@ -2458,7 +2547,7 @@ def _check_for_correct_modif_flags(self): "set. Please use the official grid2op action API to modify the bus using " "'change'." ) - if "change_bus" not in self.authorized_keys: + if "change_bus" not in cls.authorized_keys: raise IllegalAction("You illegally act on the bus (using change)") if (self._set_topo_vect != 0).any(): if not self._modif_set_bus: @@ -2467,7 +2556,7 @@ def _check_for_correct_modif_flags(self): "set. Please use the official grid2op action API to modify the bus using " "'set'." ) - if "set_bus" not in self.authorized_keys: + if "set_bus" not in cls.authorized_keys: raise IllegalAction("You illegally act on the bus (using set)") if (self._set_line_status != 0).any(): @@ -2478,7 +2567,7 @@ def _check_for_correct_modif_flags(self): "powerline using " "'set'." ) - if "set_line_status" not in self.authorized_keys: + if "set_line_status" not in cls.authorized_keys: raise IllegalAction( "You illegally act on the powerline status (using set)" ) @@ -2491,7 +2580,7 @@ def _check_for_correct_modif_flags(self): "set. Please use the official grid2op action API to modify the status of " "powerlines using 'change'." ) - if "change_line_status" not in self.authorized_keys: + if "change_line_status" not in cls.authorized_keys: raise IllegalAction( "You illegally act on the powerline status (using change)" ) @@ -2504,7 +2593,7 @@ def _check_for_correct_modif_flags(self): "set. Please use the official grid2op action API to perform redispatching " "action." ) - if "redispatch" not in self.authorized_keys: + if "redispatch" not in cls.authorized_keys: raise IllegalAction("You illegally act on the redispatching") if (np.abs(self._storage_power) >= 1e-7).any(): @@ -2515,7 +2604,7 @@ def _check_for_correct_modif_flags(self): "set. Please use the official grid2op action API to perform " "action on storage unit." ) - if "set_storage" not in self.authorized_keys: + if "set_storage" not in cls.authorized_keys: raise IllegalAction("You illegally act on the storage unit") if (np.abs(self._curtail + 1.0) >= 1e-7).any(): @@ -2524,7 +2613,7 @@ def _check_for_correct_modif_flags(self): "A curtailment is performed while the action is not supposed to have done so. " "Please use the official grid2op action API to perform curtailment action." ) - if "curtail" not in self.authorized_keys: + if "curtail" not in cls.authorized_keys: raise IllegalAction("You illegally act on the curtailment") if (self._raise_alarm).any(): @@ -2533,7 +2622,7 @@ def _check_for_correct_modif_flags(self): "Incorrect way to raise some alarm, the appropriate flag is not " "modified properly." ) - if "raise_alarm" not in self.authorized_keys: + if "raise_alarm" not in cls.authorized_keys: raise IllegalAction("You illegally send an alarm.") if (self._raise_alert).any(): @@ -2542,9 +2631,19 @@ def _check_for_correct_modif_flags(self): "Incorrect way to raise some alert, the appropriate flag is not " "modified properly." ) - if "raise_alert" not in self.authorized_keys: + if "raise_alert" not in cls.authorized_keys: raise IllegalAction("You illegally send an alert.") - + + if cls.detachment_is_allowed: + if (self._detach_load).any(): + if not self._modif_detach_load: + raise AmbiguousAction( + "Incorrect way to detach some loads, the appropriate flag is not " + "modified properly." + ) + if "detach_load" not in self.authorized_keys: + raise IllegalAction("You illegally detached a load.") + def _check_for_ambiguity(self): """ This method checks if an action is ambiguous or not. If the instance is ambiguous, an @@ -4909,14 +5008,19 @@ def change_bus(self) -> np.ndarray: @change_bus.setter def change_bus(self, values): + cls = type(self) + if "change_bus" not in cls.authorized_keys: + raise IllegalAction( + 'Impossible to modify an element with "change_bus" with this action type.' + ) orig_ = self.change_bus try: self._aux_affect_object_bool( values, "", - self.dim_topo, + cls.dim_topo, None, - np.arange(self.dim_topo), + np.arange(cls.dim_topo), self._change_bus_vect, ) self._modif_change_bus = True @@ -4924,16 +5028,16 @@ def change_bus(self, values): self._aux_affect_object_bool( orig_, "", - self.dim_topo, + cls.dim_topo, None, - np.arange(self.dim_topo), + np.arange(cls.dim_topo), self._change_bus_vect, ) raise IllegalAction( f"Impossible to modify the bus with your input. " f"Please consult the documentation. " - f'The error was:\n"{exc_}"' - ) + f'The error was:\n"{exc_}"' + ) from exc_ @property def load_change_bus(self) -> np.ndarray: @@ -4948,7 +5052,8 @@ def load_change_bus(self) -> np.ndarray: @load_change_bus.setter def load_change_bus(self, values): - if "change_bus" not in self.authorized_keys: + cls = type(self) + if "change_bus" not in cls.authorized_keys: raise IllegalAction( 'Impossible to modify the load bus (with "change") with this action type.' ) @@ -4957,19 +5062,19 @@ def load_change_bus(self, values): self._aux_affect_object_bool( values, "load", - self.n_load, - self.name_load, - self.load_pos_topo_vect, + cls.n_load, + cls.name_load, + cls.load_pos_topo_vect, self._change_bus_vect, _nm_ch_bk_key="loads", ) self._modif_change_bus = True except Exception as exc_: - self._change_bus_vect[self.load_pos_topo_vect] = orig_ + self._change_bus_vect[cls.load_pos_topo_vect] = orig_ raise IllegalAction( f"Impossible to modify the load bus with your input. Please consult the documentation. " f'The error was "{exc_}"' - ) + ) from exc_ @property def gen_change_bus(self) -> np.ndarray: @@ -4983,7 +5088,7 @@ def gen_change_bus(self) -> np.ndarray: each generator units with the convention : * ``False`` this generator is not affected by any "change" action - * ``True`` this generator bus is not affected by any "change" action. If it was + * ``True`` this generator bus is affected by a "change" action. If it was on bus 1, it will be moved to bus 2, if it was on bus 2 it will be moved to bus 1 ( and if it was disconnected it will stay disconnected) @@ -5071,7 +5176,8 @@ def gen_change_bus(self) -> np.ndarray: @gen_change_bus.setter def gen_change_bus(self, values): - if "change_bus" not in self.authorized_keys: + cls = type(self) + if "change_bus" not in cls.authorized_keys: raise IllegalAction( 'Impossible to modify the gen bus (with "change") with this action type.' ) @@ -5080,19 +5186,19 @@ def gen_change_bus(self, values): self._aux_affect_object_bool( values, "gen", - self.n_gen, - self.name_gen, - self.gen_pos_topo_vect, + cls.n_gen, + cls.name_gen, + cls.gen_pos_topo_vect, self._change_bus_vect, _nm_ch_bk_key="prods", ) self._modif_change_bus = True except Exception as exc_: - self._change_bus_vect[self.gen_pos_topo_vect] = orig_ + self._change_bus_vect[cls.gen_pos_topo_vect] = orig_ raise IllegalAction( f"Impossible to modify the gen bus with your input. Please consult the documentation. " f'The error was:\n"{exc_}"' - ) + ) from exc_ @property def storage_change_bus(self) -> np.ndarray: @@ -5107,11 +5213,12 @@ def storage_change_bus(self) -> np.ndarray: @storage_change_bus.setter def storage_change_bus(self, values): - if "change_bus" not in self.authorized_keys: + cls = type(self) + if "change_bus" not in cls.authorized_keys: raise IllegalAction( 'Impossible to modify the storage bus (with "change") with this action type.' ) - if "set_storage" not in self.authorized_keys: + if "set_storage" not in cls.authorized_keys: raise IllegalAction( "Impossible to modify the storage units with this action type." ) @@ -5120,19 +5227,19 @@ def storage_change_bus(self, values): self._aux_affect_object_bool( values, "storage", - self.n_storage, - self.name_storage, - self.storage_pos_topo_vect, + cls.n_storage, + cls.name_storage, + cls.storage_pos_topo_vect, self._change_bus_vect, ) self._modif_change_bus = True except Exception as exc_: - self._change_bus_vect[self.storage_pos_topo_vect] = orig_ + self._change_bus_vect[cls.storage_pos_topo_vect] = orig_ raise IllegalAction( f"Impossible to modify the storage bus with your input. " f"Please consult the documentation. " f'The error was:\n"{exc_}"' - ) + ) from exc_ @property def line_or_change_bus(self) -> np.ndarray: @@ -5147,7 +5254,8 @@ def line_or_change_bus(self) -> np.ndarray: @line_or_change_bus.setter def line_or_change_bus(self, values): - if "change_bus" not in self.authorized_keys: + cls = type(self) + if "change_bus" not in cls.authorized_keys: raise IllegalAction( 'Impossible to modify the line (origin) bus (with "change") with this action type.' ) @@ -5156,20 +5264,20 @@ def line_or_change_bus(self, values): self._aux_affect_object_bool( values, self._line_or_str, - self.n_line, - self.name_line, - self.line_or_pos_topo_vect, + cls.n_line, + cls.name_line, + cls.line_or_pos_topo_vect, self._change_bus_vect, _nm_ch_bk_key="lines", ) self._modif_change_bus = True except Exception as exc_: - self._change_bus_vect[self.line_or_pos_topo_vect] = orig_ + self._change_bus_vect[cls.line_or_pos_topo_vect] = orig_ raise IllegalAction( f"Impossible to modify the line origin bus with your input. " f"Please consult the documentation. " f'The error was:\n"{exc_}"' - ) + ) from exc_ @property def line_ex_change_bus(self) -> np.ndarray: @@ -5184,7 +5292,8 @@ def line_ex_change_bus(self) -> np.ndarray: @line_ex_change_bus.setter def line_ex_change_bus(self, values): - if "change_bus" not in self.authorized_keys: + cls = type(self) + if "change_bus" not in cls.authorized_keys: raise IllegalAction( 'Impossible to modify the line (ex) bus (with "change") with this action type.' ) @@ -5193,20 +5302,20 @@ def line_ex_change_bus(self, values): self._aux_affect_object_bool( values, self._line_ex_str, - self.n_line, - self.name_line, - self.line_ex_pos_topo_vect, + cls.n_line, + cls.name_line, + cls.line_ex_pos_topo_vect, self._change_bus_vect, _nm_ch_bk_key="lines", ) self._modif_change_bus = True except Exception as exc_: - self._change_bus_vect[self.line_ex_pos_topo_vect] = orig_ + self._change_bus_vect[cls.line_ex_pos_topo_vect] = orig_ raise IllegalAction( f"Impossible to modify the line extrmity bus with your input. " f"Please consult the documentation. " f'The error was:\n"{exc_}"' - ) + ) from exc_ @property def line_change_status(self) -> np.ndarray: @@ -5226,7 +5335,8 @@ def line_change_status(self) -> np.ndarray: @line_change_status.setter def line_change_status(self, values): - if "change_line_status" not in self.authorized_keys: + cls = type(self) + if "change_line_status" not in cls.authorized_keys: raise IllegalAction( 'Impossible to modify the status of powerlines (with "change") with this action type.' ) @@ -5235,9 +5345,9 @@ def line_change_status(self, values): self._aux_affect_object_bool( values, "line status", - self.n_line, - self.name_line, - np.arange(self.n_line), + cls.n_line, + cls.name_line, + np.arange(cls.n_line), self._switch_line_status, _nm_ch_bk_key="lines", ) @@ -5248,7 +5358,7 @@ def line_change_status(self, values): f"Impossible to modify the line status with your input. " f"Please consult the documentation. " f'The error was:\n"{exc_}"' - ) + ) from exc_ @property def raise_alarm(self) -> np.ndarray: @@ -5291,16 +5401,17 @@ def raise_alarm(self, values): .. warning:: /!\\\\ Only valid with "l2rpn_icaps_2021" environment /!\\\\ """ - if "raise_alarm" not in self.authorized_keys: + cls = type(self) + if "raise_alarm" not in cls.authorized_keys: raise IllegalAction("Impossible to send alarms with this action type.") orig_ = copy.deepcopy(self._raise_alarm) try: self._aux_affect_object_bool( values, "raise alarm", - self.dim_alarms, - self.alarms_area_names, - np.arange(self.dim_alarms), + cls.dim_alarms, + cls.alarms_area_names, + np.arange(cls.dim_alarms), self._raise_alarm, ) self._modif_alarm = True @@ -5310,7 +5421,7 @@ def raise_alarm(self, values): f"Impossible to modify the alarm with your input. " f"Please consult the documentation. " f'The error was:\n"{exc_}"' - ) + ) from exc_ @property def raise_alert(self) -> np.ndarray: @@ -5339,16 +5450,17 @@ def raise_alert(self) -> np.ndarray: @raise_alert.setter def raise_alert(self, values): - if "raise_alert" not in self.authorized_keys: + cls = type(self) + if "raise_alert" not in cls.authorized_keys: raise IllegalAction("Impossible to send alerts with this action type.") orig_ = copy.deepcopy(self._raise_alert) try: self._aux_affect_object_bool( values, "raise alert", - self.dim_alerts, - self.alertable_line_names, - np.arange(self.dim_alerts), + cls.dim_alerts, + cls.alertable_line_names, + np.arange(cls.dim_alerts), self._raise_alert, ) self._modif_alert = True @@ -5358,7 +5470,135 @@ def raise_alert(self, values): f"Impossible to modify the alert with your input. " f"Please consult the documentation. " f'The error was:\n"{exc_}"' + ) from exc_ + + @property + def detach_load(self) -> np.ndarray: + """ + + ..versionadded:: 1.11.0 + + Allows to retrieve (and affect) the status (connected / disconnected) of loads. + + .. note:: + It is only available after grid2op version 1.11.0 and if the backend + allows it. + + Returns + ------- + res: + A vector of bool, of size `act.n_load` indicating whether this load + is detached or not. + + * ``False`` this load is not affected by any "detach" action + * ``True`` this load will be deactivated. + + Examples + -------- + + To retrieve the impact of the action on the storage unit, you can do: + + .. code-block:: python + + detach_load = act.detach_load + + To modify these buses you can do: + + .. code-block:: python + + # create an environment where i can modify everything + import numpy as np + import grid2op + from grid2op.Action import CompleteAction + env = grid2op.make("educ_case14_storage", + test=True, + action_class=CompleteAction, + allow_detachment=True) + + # create an action + act = env.action_space() + + # method 1 : provide the full vector + act.detach_load = np.ones(act.n_load, dtype=bool) + + # method 2: provide the index of the unit you want to modify + act.detach_load = 1 + + # method 3: provide a list of the units you want to modify + act.detach_load = [1, 2] + + # method 4: change the storage unit by their name with a set + act.detach_load = {"load_1_0"} + + .. note:: The "rule of thumb" to modify an object using "change" method it to provide always + the ID of an object. The ID should be an integer (or a name in some cases). It does not + make any sense to provide a "value" associated to an ID: either you change it, or not. + + Notes + ----- + It is a "property", you don't have to use parenthesis to access it: + + .. code-block:: python + + # valid code + gen_buses = act.gen_change_bus + + # invalid code, it will crash, do not run + gen_buses = act.gen_change_bus() + # end do not run + + And neither should you uses parenthesis to modify it: + + .. code-block:: python + + # valid code + act.gen_change_bus = [1, 2, 3] + + # invalid code, it will crash, do not run + act.gen_change_bus([1, 2, 3]) + # end do not run + + Property cannot be set "directly", you have to use the `act.XXX = ..` syntax. For example: + + .. code-block:: python + + # valid code + act.gen_change_bus = [1, 3, 4] + + # invalid code, it will raise an error, and even if it did not it would have not effect + # do not run + act.gen_change_bus[1] = True + # end do not run + + .. note:: Be careful not to mix "change" and "set". For "change" you only need to provide the ID of the elements + you want to change, for "set" you need to provide the ID **AND** where you want to set them. + + """ + res = copy.deepcopy(self._detach_load) + res.flags.writeable = False + return res + + @detach_load.setter + def detach_load(self, values): + cls = type(self) + if "detach_load" not in cls.authorized_keys: + raise IllegalAction("Impossible to send alerts with this action type.") + orig_ = self.detach_load + try: + self._aux_affect_object_bool( + values, + "detach loads", + cls.n_load, + cls.name_load, + np.arange(cls.n_load), + self._detach_load, ) + self._modif_alert = True + except Exception as exc_: + self._detach_load[:] = orig_ + raise IllegalAction( + f"Impossible to detach a load with your input." + ) from exc_ def _aux_affect_object_float( self, diff --git a/grid2op/MakeEnv/Make.py b/grid2op/MakeEnv/Make.py index 63fe19f9..7ddf9c09 100644 --- a/grid2op/MakeEnv/Make.py +++ b/grid2op/MakeEnv/Make.py @@ -339,7 +339,7 @@ def make( n_busbar: ``int`` Number of independant busbars allowed per substations. By default it's 2. - allow_detachmentnt: ``bool`` + allow_detachment: ``bool`` Whether to allow loads and generators to be shed without a game over. By default it's False. kwargs: diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 90d820cf..26b136ac 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -2340,8 +2340,6 @@ def assert_grid_correct_cls(cls): # alert data cls._check_validity_alert_data() - - assert isinstance(cls.detachment_is_allowed, bool) @classmethod def _check_validity_alarm_data(cls): diff --git a/grid2op/tests/test_shedding.py b/grid2op/tests/test_shedding.py index b59eef0e..9541b9a5 100644 --- a/grid2op/tests/test_shedding.py +++ b/grid2op/tests/test_shedding.py @@ -1,8 +1,19 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# 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 warnings import unittest +import numpy as np import grid2op +from grid2op.Action import CompleteAction from grid2op.Parameters import Parameters + class TestShedding(unittest.TestCase): def setUp(self) -> None: @@ -11,9 +22,13 @@ def setUp(self) -> None: p.MAX_SUB_CHANGED = 5 with warnings.catch_warnings(): warnings.filterwarnings("ignore") - self.env = grid2op.make("rte_case5_example", param=p, - allow_detachment=True, test=True) - self.env.set_id("00") # Reproducibility + self.env = grid2op.make("rte_case5_example", + param=p, + action_class=CompleteAction, + allow_detachment=True, + test=True, + _add_to_name=type(self).__name__) + obs = self.env.reset(seed=0, options={"time serie id": "00"}) # Reproducibility self.load_lookup = {name:i for i,name in enumerate(self.env.name_load)} self.gen_lookup = {name:i for i,name in enumerate(self.env.name_gen)} @@ -22,7 +37,9 @@ def tearDown(self) -> None: def test_shedding_parameter_is_true(self): assert self.env._allow_detachment is True - assert self.env.backend.detachment_is_allowed is True + assert type(self.env).detachment_is_allowed + assert type(self.env.backend).detachment_is_allowed + assert self.env.backend.detachment_is_allowed def test_shed_single_load(self): # Check that a single load can be shed @@ -91,5 +108,34 @@ def test_shedding_persistance(self): assert not done assert obs.topo_vect[load_pos] == -1 + def test_action_property(self): + act = self.env.action_space() + assert "detach_load" in type(act).authorized_keys + act.detach_load = np.ones(act.n_load, dtype=bool) + assert act._detach_load.all() + + act2 = self.env.action_space() + act2.detach_load = 1 + assert act2._detach_load[1] + + act3 = self.env.action_space() + act3.detach_load = [0, 2] + assert act3._detach_load[0] + assert act3._detach_load[2] + + for k, v in self.load_lookup.items(): + act4 = self.env.action_space() + act4.detach_load = {k} + assert act4._detach_load[v] + +# TODO Shedding: test when backend does not support it is not set +# TODO shedding: test when user deactivates it it is not set + +# TODO Shedding: Runner +# TODO Shedding: environment copied +# TODO Shedding: MultiMix environment +# TODO Shedding: TimedOutEnvironment +# TODO Shedding: MaskedEnvironment + if __name__ == "__main__": unittest.main() diff --git a/grid2op/typing_variables.py b/grid2op/typing_variables.py index ea19ec21..5e1a25b8 100644 --- a/grid2op/typing_variables.py +++ b/grid2op/typing_variables.py @@ -38,7 +38,8 @@ "injection", "hazards", "maintenance", - "shunt"], + "shunt", + "detach_load"], Any] # TODO improve that (especially the Any part) From 6e082120d4009661e78bd61e2d3a8216afd6424f Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Mon, 25 Nov 2024 11:56:51 +0100 Subject: [PATCH 24/60] forbid args in env creation, fix bug on CI, refacto the 'make' call Signed-off-by: DONNOT Benjamin --- CHANGELOG.rst | 2 + grid2op/Environment/_forecast_env.py | 4 +- grid2op/Environment/_obsEnv.py | 14 +-- grid2op/Environment/baseEnv.py | 4 +- grid2op/Environment/environment.py | 3 +- grid2op/MakeEnv/MakeFromPath.py | 110 +++++++++--------------- grid2op/Observation/observationSpace.py | 1 + grid2op/tests/automatic_classes.py | 4 +- 8 files changed, 62 insertions(+), 80 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e4a63b0f..e8c9cc6f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -107,6 +107,8 @@ Native multi agents support: name is added. This behaviour can be turned off by passing `_add_cls_nm_bk=False` when calling `grid2op.make(...)`. If you develop a new Backend, you can also customize the added name by overloading the `get_class_added_name` class method. +- [BREAKING] it is now forbidden to create environment with arguments. + Only key-word arguments are allowed. - [FIXED] issue https://github.com/Grid2op/grid2op/issues/657 - [FIXED] missing an import on the `MaskedEnvironment` class - [FIXED] a bug when trying to set the load_p, load_q, gen_p, gen_v by names. diff --git a/grid2op/Environment/_forecast_env.py b/grid2op/Environment/_forecast_env.py index ab4d7056..0f468e0b 100644 --- a/grid2op/Environment/_forecast_env.py +++ b/grid2op/Environment/_forecast_env.py @@ -19,10 +19,10 @@ class _ForecastEnv(Environment): It is used by obs.get_forecast_env. """ - def __init__(self, *args, **kwargs): + def __init__(self,**kwargs): if "_update_obs_after_reward" not in kwargs: kwargs["_update_obs_after_reward"] = False - super().__init__(*args, **kwargs) + super().__init__(**kwargs) self._do_not_erase_local_dir_cls = True def step(self, action: BaseAction) -> Tuple[BaseObservation, float, bool, STEP_INFO_TYPING]: diff --git a/grid2op/Environment/_obsEnv.py b/grid2op/Environment/_obsEnv.py index 4048cedb..c8b8ac79 100644 --- a/grid2op/Environment/_obsEnv.py +++ b/grid2op/Environment/_obsEnv.py @@ -18,6 +18,7 @@ from grid2op.Chronics import ChangeNothing from grid2op.Chronics._obs_fake_chronics_handler import _ObsCH from grid2op.Rules import RulesChecker +from grid2op.Space import DEFAULT_ALLOW_DETACHMENT from grid2op.operator_attention import LinearAttentionBudget from grid2op.Environment.baseEnv import BaseEnv @@ -41,6 +42,7 @@ class _ObsEnv(BaseEnv): def __init__( self, + *, # since 1.11.0 I force kwargs init_env_path, init_grid_path, backend_instanciated, @@ -63,16 +65,17 @@ def __init__( logger=None, highres_sim_counter=None, _complete_action_cls=None, + allow_detachment:bool=DEFAULT_ALLOW_DETACHMENT, _ptr_orig_obs_space=None, _local_dir_cls=None, # only set at the first call to `make(...)` after should be false _read_from_local_dir=None, ): BaseEnv.__init__( self, - init_env_path, - init_grid_path, - copy.deepcopy(parameters), - thermal_limit_a, + init_env_path=init_env_path, + init_grid_path=init_grid_path, + parameters=copy.deepcopy(parameters), + thermal_limit_a=thermal_limit_a, other_rewards=other_rewards, epsilon_poly=epsilon_poly, tol_poly=tol_poly, @@ -84,7 +87,8 @@ def __init__( highres_sim_counter=highres_sim_counter, update_obs_after_reward=False, _local_dir_cls=_local_dir_cls, - _read_from_local_dir=_read_from_local_dir + _read_from_local_dir=_read_from_local_dir, + allow_detachment=allow_detachment ) self._do_not_erase_local_dir_cls = True self.__unusable = False # unsuable if backend cannot be copied diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index b609485e..5d405b21 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -50,6 +50,7 @@ from grid2op.Chronics import ChronicsHandler from grid2op.Rules import AlwaysLegal, BaseRules, AlwaysLegal from grid2op.typing_variables import STEP_INFO_TYPING, RESET_OPTIONS_TYPING +from grid2op.VoltageControler import ControlVoltageFromFile # TODO put in a separate class the redispatching function @@ -315,10 +316,11 @@ def foo(manager): def __init__( self, + *, # since 1.11.0 I force kwargs init_env_path: os.PathLike, init_grid_path: os.PathLike, parameters: Parameters, - voltagecontrolerClass: type, + voltagecontrolerClass: type=ControlVoltageFromFile, name="unknown", thermal_limit_a: Optional[np.ndarray] = None, epsilon_poly: float = 1e-4, # precision of the redispatching algorithm diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 68e98de0..431ea224 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -79,6 +79,7 @@ class Environment(BaseEnv): def __init__( self, + *, # since 1.11.0 I force kwargs init_env_path: str, init_grid_path: str, chronics_handler, @@ -427,7 +428,7 @@ def _init_backend( kwargs_observation=self._kwargs_observation, observation_bk_class=self._observation_bk_class, observation_bk_kwargs=self._observation_bk_kwargs, - _local_dir_cls=self._local_dir_cls + _local_dir_cls=self._local_dir_cls, ) # test to make sure the backend is consistent with the chronics generator diff --git a/grid2op/MakeEnv/MakeFromPath.py b/grid2op/MakeEnv/MakeFromPath.py index e4a96e61..f7febf50 100644 --- a/grid2op/MakeEnv/MakeFromPath.py +++ b/grid2op/MakeEnv/MakeFromPath.py @@ -915,7 +915,44 @@ def make_from_dataset_path( # for the other mix I need to read the data from files and NOT # create the classes use_class_in_files = False + _add_to_name = '' # already defined in the first mix + name_env = _overload_name_multimix.name_env + + default_kwargs = dict( + init_env_path=os.path.abspath(dataset_path), + init_grid_path=grid_path_abs, + backend=backend, + parameters=param, + name=name_env + _add_to_name, + names_chronics_to_backend=names_chronics_to_backend, + actionClass=action_class, + observationClass=observation_class, + rewardClass=reward_class, + legalActClass=gamerules_class, + voltagecontrolerClass=volagecontroler_class, + other_rewards=other_rewards, + opponent_space_type=opponent_space_type, + opponent_action_class=opponent_action_class, + opponent_class=opponent_class, + opponent_init_budget=opponent_init_budget, + opponent_attack_duration=opponent_attack_duration, + opponent_attack_cooldown=opponent_attack_cooldown, + opponent_budget_per_ts=opponent_budget_per_ts, + opponent_budget_class=opponent_budget_class, + kwargs_opponent=kwargs_opponent, + has_attention_budget=has_attention_budget, + attention_budget_cls=attention_budget_class, + kwargs_attention_budget=kwargs_attention_budget, + logger=logger, + n_busbar=n_busbar, # TODO n_busbar_per_sub different num per substations: read from a config file maybe (if not provided by the user) + _compat_glop_version=_compat_glop_version, + _overload_name_multimix=_overload_name_multimix, + kwargs_observation=kwargs_observation, + observation_bk_class=observation_backend_class, + observation_bk_kwargs=observation_backend_kwargs, + allow_detachment=allow_detachment, + ) if use_class_in_files: # new behaviour if _overload_name_multimix is None: @@ -964,48 +1001,14 @@ def make_from_dataset_path( if not os.path.exists(this_local_dir_name): raise EnvError(f"Path {this_local_dir_name} has not been created by the tempfile package") - init_env = Environment(init_grid_path=grid_path_abs, + init_env = Environment(**default_kwargs, chronics_handler=data_feeding_fake, - backend=backend, - parameters=param, - name=name_env + _add_to_name, - names_chronics_to_backend=names_chronics_to_backend, - actionClass=action_class, - observationClass=observation_class, - rewardClass=reward_class, - legalActClass=gamerules_class, - voltagecontrolerClass=volagecontroler_class, - other_rewards=other_rewards, - opponent_space_type=opponent_space_type, - opponent_action_class=opponent_action_class, - opponent_class=opponent_class, - opponent_init_budget=opponent_init_budget, - opponent_attack_duration=opponent_attack_duration, - opponent_attack_cooldown=opponent_attack_cooldown, - opponent_budget_per_ts=opponent_budget_per_ts, - opponent_budget_class=opponent_budget_class, - kwargs_opponent=kwargs_opponent, - has_attention_budget=has_attention_budget, - attention_budget_cls=attention_budget_class, - kwargs_attention_budget=kwargs_attention_budget, - logger=logger, - n_busbar=n_busbar, # TODO n_busbar_per_sub different num per substations: read from a config file maybe (if not provided by the user) - _compat_glop_version=_compat_glop_version, _read_from_local_dir=None, # first environment to generate the classes and save them _local_dir_cls=None, - _overload_name_multimix=_overload_name_multimix, - kwargs_observation=kwargs_observation, - observation_bk_class=observation_backend_class, - observation_bk_kwargs=observation_backend_kwargs, - allow_detachment=allow_detachment, ) if not os.path.exists(this_local_dir.name): raise EnvError(f"Path {this_local_dir.name} has not been created by the tempfile package") init_env.generate_classes(local_dir_id=this_local_dir.name) - - if not os.path.exists(this_local_dir_name): - raise EnvError(f"Path {this_local_dir_name} has not been created by the tempfile package") - init_env.generate_classes(local_dir_id=this_local_dir_name) # fix `my_bk_act_class` and `_complete_action_class` _aux_fix_backend_internal_classes(type(backend), this_local_dir) init_env.backend = None # to avoid to close the backend when init_env is deleted @@ -1044,8 +1047,6 @@ def make_from_dataset_path( # new in 1.11.0 if _overload_name_multimix is not None: # case of multimix - _add_to_name = '' # already defined in the first mix - name_env = _overload_name_multimix.name_env if _overload_name_multimix.mix_id >= 1 and _overload_name_multimix.local_dir_tmpfolder is not None: # this is not the first mix # for the other mix I need to read the data from files and NOT @@ -1056,42 +1057,11 @@ def make_from_dataset_path( # Finally instantiate env from config & overrides # including (if activated the new grid2op behaviour) env = Environment( - init_env_path=os.path.abspath(dataset_path), - init_grid_path=grid_path_abs, - chronics_handler=data_feeding, - backend=backend, - parameters=param, - name=name_env + _add_to_name, - names_chronics_to_backend=names_chronics_to_backend, - actionClass=action_class, - observationClass=observation_class, - rewardClass=reward_class, - legalActClass=gamerules_class, - voltagecontrolerClass=volagecontroler_class, - other_rewards=other_rewards, - opponent_space_type=opponent_space_type, - opponent_action_class=opponent_action_class, - opponent_class=opponent_class, - opponent_init_budget=opponent_init_budget, - opponent_attack_duration=opponent_attack_duration, - opponent_attack_cooldown=opponent_attack_cooldown, - opponent_budget_per_ts=opponent_budget_per_ts, - opponent_budget_class=opponent_budget_class, - kwargs_opponent=kwargs_opponent, - has_attention_budget=has_attention_budget, - attention_budget_cls=attention_budget_class, - kwargs_attention_budget=kwargs_attention_budget, - logger=logger, - n_busbar=n_busbar, # TODO n_busbar_per_sub different num per substations: read from a config file maybe (if not provided by the user) - allow_detachment=allow_detachment, - _compat_glop_version=_compat_glop_version, + **default_kwargs, + chronics_handler=data_feeding, _read_from_local_dir=classes_path, _allow_loaded_backend=allow_loaded_backend, _local_dir_cls=this_local_dir, - _overload_name_multimix=_overload_name_multimix, - kwargs_observation=kwargs_observation, - observation_bk_class=observation_backend_class, - observation_bk_kwargs=observation_backend_kwargs ) if do_not_erase_cls is not None: env._do_not_erase_local_dir_cls = do_not_erase_cls diff --git a/grid2op/Observation/observationSpace.py b/grid2op/Observation/observationSpace.py index 5b4a00d9..1bc0290a 100644 --- a/grid2op/Observation/observationSpace.py +++ b/grid2op/Observation/observationSpace.py @@ -202,6 +202,7 @@ def _create_obs_env(self, env, observationClass): _ptr_orig_obs_space=self, _local_dir_cls=env._local_dir_cls, _read_from_local_dir=env._read_from_local_dir, + allow_detachment=type(env.backend).detachment_is_allowed ) for k, v in self.obs_env.other_rewards.items(): v.initialize(self.obs_env) diff --git a/grid2op/tests/automatic_classes.py b/grid2op/tests/automatic_classes.py index c50b91c5..428e5c34 100644 --- a/grid2op/tests/automatic_classes.py +++ b/grid2op/tests/automatic_classes.py @@ -154,7 +154,8 @@ def test_all_classes_from_file(self, f"ObservationSpace_{classes_name}", f"PandaPowerBackend_{classes_name}", name_action_cls, - f"VoltageOnlyAction_{classes_name}" + f"VoltageOnlyAction_{classes_name}", + f"_ForecastEnv_{classes_name}", ] names_attr = ["action_space", "_backend_action_class", @@ -167,6 +168,7 @@ def test_all_classes_from_file(self, "backend", "_actionClass", None, # VoltageOnlyAction not in env + None, # _ForecastEnv_ not in env ] # NB: these imports needs to be consistent with what is done in # base_env.generate_classes() and gridobj.init_grid(...) From 4cf73c66e1ee6fd9bbd41b211f4ddcbdf13eb458 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Mon, 25 Nov 2024 14:33:25 +0100 Subject: [PATCH 25/60] fixes to make tests pass Signed-off-by: DONNOT Benjamin --- grid2op/Backend/backend.py | 28 +++++++++++++--------------- grid2op/Space/GridObjects.py | 2 +- grid2op/tests/automatic_classes.py | 6 ++++++ 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index 1aae3966..cd8f361e 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -1081,12 +1081,16 @@ def _runpf_with_diverging_exception(self, is_dc : bool) -> Optional[Exception]: exc_me = None try: + conv, exc_me = self.runpf(is_dc=is_dc) # run powerflow + + if not conv: + if exc_me is not None: + raise exc_me + raise BackendError("Divergence of the powerflow without further information.") + # Check if loads/gens have been detached and if this is allowed, otherwise raise an error # .. versionadded:: 1.11.0 - if hasattr(self, "_get_topo_vect"): - topo_vect = self._get_topo_vect() - else: - topo_vect = self.get_topo_vect() + topo_vect = self.get_topo_vect() load_buses = topo_vect[self.load_pos_topo_vect] @@ -1099,8 +1103,6 @@ def _runpf_with_diverging_exception(self, is_dc : bool) -> Optional[Exception]: if not self.detachment_is_allowed and (gen_buses == -1).any(): raise Grid2OpException(f"One or more generators were detached before powerflow in Backend {type(self).__name__}" "but this is not allowed or not supported (Game Over)") - - conv, exc_me = self.runpf(is_dc=is_dc) # run powerflow except Grid2OpException as exc_: exc_me = exc_ @@ -1109,15 +1111,6 @@ def _runpf_with_diverging_exception(self, is_dc : bool) -> Optional[Exception]: "GAME OVER: Powerflow has diverged during computation " "or a load has been disconnected or a generator has been disconnected." ) - - # Post-Powerflow Check - if not self.detachment_is_allowed and conv: - resulting_act = self.get_action_to_set() - load_buses_act_set = resulting_act._set_topo_vect[self.load_pos_topo_vect] - gen_buses_act_set = resulting_act._set_topo_vect[self.gen_pos_topo_vect] - if (load_buses_act_set == -1).any() or (gen_buses_act_set == -1).any(): - exc_me = Grid2OpException(f"One or more generators or loads were detached in Backend {type(self).__name__}" - " as a result of a Grid2Op action, but this is not allowed or not supported (Game Over)") return exc_me def next_grid_state(self, @@ -2047,6 +2040,11 @@ def get_action_to_set(self) -> "grid2op.Action.CompleteAction": ) prod_p, _, prod_v = self.generators_info() load_p, load_q, _ = self.loads_info() + if type(self)._complete_action_class is None: + # some bug in multiprocessing, this was not set + # sub processes + from grid2op.Action.completeAction import CompleteAction + type(self)._complete_action_class = CompleteAction.init_grid(type(self)) set_me = self._complete_action_class() dict_ = { "set_line_status": line_status, diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index ed8517f5..8739e547 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -4568,7 +4568,7 @@ def init_grid_from_dict_for_pickle(name_res, orig_cls, cls_attr): res_cls._compute_pos_big_topo_cls() if res_cls.glop_version != grid2op.__version__: res_cls.process_grid2op_compat() - res_cls.process_shunt_satic_data() + res_cls.process_shunt_static_data() # add the class in the "globals" for reuse later globals()[name_res] = res_cls diff --git a/grid2op/tests/automatic_classes.py b/grid2op/tests/automatic_classes.py index 428e5c34..f0eb5d05 100644 --- a/grid2op/tests/automatic_classes.py +++ b/grid2op/tests/automatic_classes.py @@ -613,6 +613,12 @@ def test_asynch_fork(self): obs = async_vect_env.reset() def test_asynch_spawn(self): + # test I can reset everything on the same process + env1 = GymEnv(self.env) + env2 = GymEnv(self.env) + obs1, info1 = env1.reset() + obs2, info2 = env2.reset() + # now do the same in the same process async_vect_env = AsyncVectorEnv((lambda: GymEnv(self.env), lambda: GymEnv(self.env)), context="spawn") obs = async_vect_env.reset() From dbba375f26a4b30300cdade5928fc63f465ba985 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Mon, 25 Nov 2024 16:00:12 +0100 Subject: [PATCH 26/60] fix issue 125 Signed-off-by: DONNOT Benjamin --- grid2op/Backend/backend.py | 10 ++-- grid2op/Backend/pandaPowerBackend.py | 50 +++++++++++-------- grid2op/tests/helper_path_test.py | 7 ++- .../test_PandaPowerBackendDefaultFunc.py | 7 ++- 4 files changed, 44 insertions(+), 30 deletions(-) diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index cd8f361e..5007fa2a 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -724,10 +724,10 @@ def get_line_status(self) -> np.ndarray: :return: an array with the line status of each powerline :rtype: np.array, dtype:bool """ + cls = type(self) topo_vect = self.get_topo_vect() - return (topo_vect[self.line_or_pos_topo_vect] >= 0) & ( - topo_vect[self.line_ex_pos_topo_vect] >= 0 - ) + return ((topo_vect[cls.line_or_pos_topo_vect] >= 0) & + (topo_vect[cls.line_ex_pos_topo_vect] >= 0)) def get_line_flow(self) -> np.ndarray: """ @@ -1090,10 +1090,8 @@ def _runpf_with_diverging_exception(self, is_dc : bool) -> Optional[Exception]: # Check if loads/gens have been detached and if this is allowed, otherwise raise an error # .. versionadded:: 1.11.0 - topo_vect = self.get_topo_vect() - + topo_vect = self.get_topo_vect() load_buses = topo_vect[self.load_pos_topo_vect] - if not self.detachment_is_allowed and (load_buses == -1).any(): raise Grid2OpException(f"One or more loads were detached before powerflow in Backend {type(self).__name__}" "but this is not allowed or not supported (Game Over)") diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index bc02668d..33ce0806 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -327,7 +327,8 @@ def reset(self, warnings.simplefilter("ignore", FutureWarning) self._grid = copy.deepcopy(self.__pp_backend_initial_grid) self._reset_all_nan() - self._topo_vect[:] = self._get_topo_vect() + self._get_topo_vect() + self.line_status[:] = self._get_line_status() self.comp_time = 0.0 def load_grid(self, @@ -717,6 +718,8 @@ def _init_private_attrs(self) -> None: ) self._compute_pos_big_topo() + + self._topo_vect = np.full(self.dim_topo, fill_value=-1, dtype=dt_int) # utilities for imeplementing apply_action self._corresp_name_fun = {} @@ -805,7 +808,7 @@ def _init_private_attrs(self) -> None: self.gen_theta = np.full(self.n_gen, fill_value=np.NaN, dtype=dt_float) self.storage_theta = np.full(self.n_storage, fill_value=np.NaN, dtype=dt_float) - self._topo_vect = self._get_topo_vect() + self._get_topo_vect() self.tol = 1e-5 # this is NOT the pandapower tolerance !!!! this is used to check if a storage unit # produce / absorbs anything @@ -824,7 +827,7 @@ def storage_deact_for_backward_comaptibility(self) -> None: self.storage_p = np.full(cls.n_storage, dtype=dt_float, fill_value=np.NaN) self.storage_q = np.full(cls.n_storage, dtype=dt_float, fill_value=np.NaN) self.storage_v = np.full(cls.n_storage, dtype=dt_float, fill_value=np.NaN) - self._topo_vect = self._get_topo_vect() + self._get_topo_vect() def _convert_id_topo(self, id_big_topo): """ @@ -1083,6 +1086,7 @@ def runpf(self, is_dc : bool=False) -> Tuple[bool, Union[Exception, None]]: in case of "do nothing" action applied. """ try: + self._get_topo_vect() # do that before any possible divergence self._aux_runpf_pp(is_dc) cls = type(self) # if a connected bus has a no voltage, it's a divergence (grid was not connected) @@ -1128,7 +1132,7 @@ def runpf(self, is_dc : bool=False) -> Tuple[bool, Union[Exception, None]]: ): self.load_v[l_id] = self.prod_v[g_id] break - + self.line_status[:] = self._get_line_status() # I retrieve the data once for the flows, so has to not re read multiple dataFrame self.p_or[:] = self._aux_get_line_info("p_from_mw", "p_hv_mw") @@ -1183,11 +1187,10 @@ def runpf(self, is_dc : bool=False) -> Tuple[bool, Union[Exception, None]]: self.storage_q[deact_storage] = 0.0 self.storage_v[deact_storage] = 0.0 self._grid.storage["in_service"].values[deact_storage] = False - - self._topo_vect[:] = self._get_topo_vect() if not self._grid.converged: raise pp.powerflow.LoadflowNotConverged("Divergence without specific reason (self._grid.converged is False)") self.div_exception = None + self._get_topo_vect() # do that after (maybe useless) return True, None except pp.powerflow.LoadflowNotConverged as exc_: @@ -1222,6 +1225,10 @@ def _reset_all_nan(self) -> None: self.load_theta[:] = np.NaN self.gen_theta[:] = np.NaN self.storage_theta[:] = np.NaN + self._topo_vect.flags.writeable = True + self._topo_vect[:] = -1 + self._topo_vect.flags.writeable = False + self.line_status[:] = False def copy(self) -> "PandaPowerBackend": """ @@ -1383,8 +1390,10 @@ def _disconnect_line(self, id_): self._grid.line.iloc[id_, self._in_service_line_col_id] = False else: self._grid.trafo.iloc[id_ - self._number_true_line, self._in_service_trafo_col_id] = False + self._topo_vect.flags.writeable = True self._topo_vect[self.line_or_pos_topo_vect[id_]] = -1 self._topo_vect[self.line_ex_pos_topo_vect[id_]] = -1 + self._topo_vect.flags.writeable = False self.line_status[id_] = False def _reconnect_line(self, id_): @@ -1399,29 +1408,30 @@ def get_topo_vect(self) -> np.ndarray: def _get_topo_vect(self): cls = type(self) - res = np.full(cls.dim_topo, fill_value=np.iinfo(dt_int).max, dtype=dt_int) - + # lines / trafo line_status = self.get_line_status() + self._topo_vect.flags.writeable = True glob_bus_or = np.concatenate((self._grid.line["from_bus"].values, self._grid.trafo["hv_bus"].values)) - res[cls.line_or_pos_topo_vect] = cls.global_bus_to_local(glob_bus_or, cls.line_or_to_subid) - res[cls.line_or_pos_topo_vect[~line_status]] = -1 + self._topo_vect[cls.line_or_pos_topo_vect] = cls.global_bus_to_local(glob_bus_or, cls.line_or_to_subid) + self._topo_vect[cls.line_or_pos_topo_vect[~line_status]] = -1 glob_bus_ex = np.concatenate((self._grid.line["to_bus"].values, self._grid.trafo["lv_bus"].values)) - res[cls.line_ex_pos_topo_vect] = cls.global_bus_to_local(glob_bus_ex, cls.line_ex_to_subid) - res[cls.line_ex_pos_topo_vect[~line_status]] = -1 + self._topo_vect[cls.line_ex_pos_topo_vect] = cls.global_bus_to_local(glob_bus_ex, cls.line_ex_to_subid) + self._topo_vect[cls.line_ex_pos_topo_vect[~line_status]] = -1 # load, gen load_status = self._grid.load["in_service"].values - res[cls.load_pos_topo_vect] = cls.global_bus_to_local(self._grid.load["bus"].values, cls.load_to_subid) - res[cls.load_pos_topo_vect[~load_status]] = -1 + self._topo_vect[cls.load_pos_topo_vect] = cls.global_bus_to_local(self._grid.load["bus"].values, cls.load_to_subid) + self._topo_vect[cls.load_pos_topo_vect[~load_status]] = -1 gen_status = self._grid.gen["in_service"].values - res[cls.gen_pos_topo_vect] = cls.global_bus_to_local(self._grid.gen["bus"].values, cls.gen_to_subid) - res[cls.gen_pos_topo_vect[~gen_status]] = -1 + self._topo_vect[cls.gen_pos_topo_vect] = cls.global_bus_to_local(self._grid.gen["bus"].values, cls.gen_to_subid) + self._topo_vect[cls.gen_pos_topo_vect[~gen_status]] = -1 # storage if cls.n_storage: - storage_status = self._grid.storage["in_service"].values - res[cls.storage_pos_topo_vect] = cls.global_bus_to_local(self._grid.storage["bus"].values, cls.storage_to_subid) - res[cls.storage_pos_topo_vect[~storage_status]] = -1 - return res + storage_status = 1 * self._grid.storage["in_service"].values + self._topo_vect[cls.storage_pos_topo_vect] = cls.global_bus_to_local(self._grid.storage["bus"].values, cls.storage_to_subid) + self._topo_vect[cls.storage_pos_topo_vect[~storage_status]] = -1 + self._topo_vect.flags.writeable = False + return self._topo_vect def _gens_info(self): prod_p = self.cst_1 * self._grid.res_gen["p_mw"].values.astype(dt_float) diff --git a/grid2op/tests/helper_path_test.py b/grid2op/tests/helper_path_test.py index 39d8439a..e057aedd 100644 --- a/grid2op/tests/helper_path_test.py +++ b/grid2op/tests/helper_path_test.py @@ -67,8 +67,11 @@ class MakeBackend(ABC, HelperTests): def make_backend(self, detailed_infos_for_cascading_failures=False) -> Backend: pass - def make_backend_with_glue_code(self, detailed_infos_for_cascading_failures=False, extra_name="", - n_busbar=2, allow_detachment=False) -> Backend: + def make_backend_with_glue_code(self, + detailed_infos_for_cascading_failures=False, + extra_name="", + n_busbar=2, + allow_detachment=False) -> Backend: Backend._clear_class_attribute() bk = self.make_backend(detailed_infos_for_cascading_failures=detailed_infos_for_cascading_failures) type(bk)._clear_grid_dependant_class_attributes() diff --git a/grid2op/tests/test_PandaPowerBackendDefaultFunc.py b/grid2op/tests/test_PandaPowerBackendDefaultFunc.py index 33a29011..3770e336 100644 --- a/grid2op/tests/test_PandaPowerBackendDefaultFunc.py +++ b/grid2op/tests/test_PandaPowerBackendDefaultFunc.py @@ -64,7 +64,8 @@ def get_topo_vect(self): """ otherwise there are some infinite recursions """ - res = np.full(self.dim_topo, fill_value=-1, dtype=dt_int) + self._topo_vect.flags.writeable = True + res = self._topo_vect line_status = np.concatenate( ( @@ -112,13 +113,14 @@ def get_topo_vect(self): for bus_id in self._grid.gen["bus"].values: res[self.gen_pos_topo_vect[i]] = 1 if bus_id == self.gen_to_subid[i] else 2 i += 1 - + res[self.gen_pos_topo_vect[~self._grid.gen["in_service"]]] = -1 i = 0 for bus_id in self._grid.load["bus"].values: res[self.load_pos_topo_vect[i]] = ( 1 if bus_id == self.load_to_subid[i] else 2 ) i += 1 + res[self.load_pos_topo_vect[~self._grid.load["in_service"]]] = -1 # do not forget storage units ! i = 0 @@ -127,6 +129,7 @@ def get_topo_vect(self): 1 if bus_id == self.storage_to_subid[i] else 2 ) i += 1 + self._topo_vect.flags.writeable = False return res From 09d1a650ea46d293ec4ef9a44895f9e248bed261 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Mon, 25 Nov 2024 17:47:56 +0100 Subject: [PATCH 27/60] still working on making tests pass with new more concise implementation Signed-off-by: DONNOT Benjamin --- CHANGELOG.rst | 4 +++- grid2op/Backend/pandaPowerBackend.py | 11 ++++------- grid2op/tests/aaa_test_backend_interface.py | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e8c9cc6f..28e7c3ce 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -140,7 +140,9 @@ Native multi agents support: does not have shunt information but there are not shunts on the grid. - [IMPROVED] consistency of `MultiMixEnv` in case of automatic_classes (only one class is generated for all mixes) - +- [IMRPOVED] handling of disconnected elements in the backend no more + raise error. The base `Backend` class does that. + [1.10.4] - 2024-10-15 ------------------------- - [FIXED] new pypi link (no change in code) diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index 33ce0806..ee6fbffe 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -909,11 +909,7 @@ def apply_action(self, backendAction: Union["grid2op.Action._backendAction._Back self._grid.storage.loc[stor_bus.changed & deactivated, "in_service"] = False self._grid.storage.loc[stor_bus.changed & ~deactivated, "in_service"] = True self._grid.storage["bus"] = new_bus_num - self._topo_vect[cls.storage_pos_topo_vect[stor_bus.changed]] = new_bus_id - self._topo_vect[ - cls.storage_pos_topo_vect[deact_and_changed] - ] = -1 - + if type(backendAction).shunts_data_available: shunt_p, shunt_q, shunt_bus = shunts__ @@ -939,6 +935,8 @@ def apply_action(self, backendAction: Union["grid2op.Action._backendAction._Back if type_obj is not None: # storage unit are handled elsewhere self._type_to_bus_set[type_obj](new_bus, id_el_backend, id_topo) + + self._topo_vect.flags.writeable = False def _apply_load_bus(self, new_bus, id_el_backend, id_topo): new_bus_backend = type(self).local_bus_to_global_int( @@ -1190,7 +1188,6 @@ def runpf(self, is_dc : bool=False) -> Tuple[bool, Union[Exception, None]]: if not self._grid.converged: raise pp.powerflow.LoadflowNotConverged("Divergence without specific reason (self._grid.converged is False)") self.div_exception = None - self._get_topo_vect() # do that after (maybe useless) return True, None except pp.powerflow.LoadflowNotConverged as exc_: @@ -1427,7 +1424,7 @@ def _get_topo_vect(self): self._topo_vect[cls.gen_pos_topo_vect[~gen_status]] = -1 # storage if cls.n_storage: - storage_status = 1 * self._grid.storage["in_service"].values + storage_status = self._grid.storage["in_service"].values self._topo_vect[cls.storage_pos_topo_vect] = cls.global_bus_to_local(self._grid.storage["bus"].values, cls.storage_to_subid) self._topo_vect[cls.storage_pos_topo_vect[~storage_status]] = -1 self._topo_vect.flags.writeable = False diff --git a/grid2op/tests/aaa_test_backend_interface.py b/grid2op/tests/aaa_test_backend_interface.py index 1f8e8732..7b6872cc 100644 --- a/grid2op/tests/aaa_test_backend_interface.py +++ b/grid2op/tests/aaa_test_backend_interface.py @@ -1471,7 +1471,7 @@ def _aux_check_el_generic(self, backend, busbar_id, key_: val # move the line }}) bk_act = type(backend).my_bk_act_class() - bk_act += action + bk_act += action # "compile" all the user action into one single action sent to the backend backend.apply_action(bk_act) # apply the action res = backend.runpf(is_dc=False) assert res[0], f"Your backend diverged in AC after setting a {el_nm} on busbar {busbar_id}, error was {res[1]}" From d6bf9a8c291f80050c28a695f552ed468eb23226 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Tue, 26 Nov 2024 10:26:03 +0100 Subject: [PATCH 28/60] should fix all issues, need to make the AAA test suite better now Signed-off-by: DONNOT Benjamin --- grid2op/Backend/backend.py | 6 ++-- grid2op/Backend/pandaPowerBackend.py | 37 +++++++++++++++++---- grid2op/tests/aaa_test_backend_interface.py | 26 ++++++++++++--- grid2op/tests/helper_path_test.py | 2 +- 4 files changed, 56 insertions(+), 15 deletions(-) diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index 5007fa2a..9ba913fc 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -180,9 +180,9 @@ def __init__(self, #: You should not worry about the class attribute of the backend in :func:`Backend.apply_action` self.n_busbar_per_sub: int = DEFAULT_N_BUSBAR_PER_SUB - # .. versionadded: 1.11.0 - self._missing_detachment_support:bool = True - self.detachment_is_allowed:bool = DEFAULT_ALLOW_DETACHMENT + #: .. versionadded: 1.11.0 + self._missing_detachment_support : bool = True + self.detachment_is_allowed : bool = DEFAULT_ALLOW_DETACHMENT def can_handle_more_than_2_busbar(self): """ diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index ee6fbffe..1adda34d 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -327,8 +327,8 @@ def reset(self, warnings.simplefilter("ignore", FutureWarning) self._grid = copy.deepcopy(self.__pp_backend_initial_grid) self._reset_all_nan() + self._get_line_status() self._get_topo_vect() - self.line_status[:] = self._get_line_status() self.comp_time = 0.0 def load_grid(self, @@ -771,7 +771,6 @@ def _init_private_attrs(self) -> None: self.q_ex = np.full(self.n_line, dtype=dt_float, fill_value=np.NaN) self.v_ex = np.full(self.n_line, dtype=dt_float, fill_value=np.NaN) self.a_ex = np.full(self.n_line, dtype=dt_float, fill_value=np.NaN) - self.line_status = np.full(self.n_line, dtype=dt_bool, fill_value=np.NaN) self.load_p = np.full(self.n_load, dtype=dt_float, fill_value=np.NaN) self.load_q = np.full(self.n_load, dtype=dt_float, fill_value=np.NaN) self.load_v = np.full(self.n_load, dtype=dt_float, fill_value=np.NaN) @@ -782,6 +781,9 @@ def _init_private_attrs(self) -> None: self.storage_q = np.full(self.n_storage, dtype=dt_float, fill_value=np.NaN) self.storage_v = np.full(self.n_storage, dtype=dt_float, fill_value=np.NaN) self._nb_bus_before = None + + self.line_status = np.full(self.n_line, dtype=dt_bool, fill_value=np.NaN) + self.line_status.flags.writeable = False # store the topoid -> objid self._init_topoid_objid() @@ -1084,8 +1086,14 @@ def runpf(self, is_dc : bool=False) -> Tuple[bool, Union[Exception, None]]: in case of "do nothing" action applied. """ try: - self._get_topo_vect() # do that before any possible divergence + # as pandapower does not modify the topology or the status of + # powerline, then we can compute the topology (and the line status) + # at the beginning + # This is also interesting in case of divergence :-) + self._get_line_status() + self._get_topo_vect() self._aux_runpf_pp(is_dc) + cls = type(self) # if a connected bus has a no voltage, it's a divergence (grid was not connected) if self._grid.res_bus.loc[self._grid.bus["in_service"]]["va_degree"].isnull().any(): @@ -1131,7 +1139,6 @@ def runpf(self, is_dc : bool=False) -> Tuple[bool, Union[Exception, None]]: self.load_v[l_id] = self.prod_v[g_id] break - self.line_status[:] = self._get_line_status() # I retrieve the data once for the flows, so has to not re read multiple dataFrame self.p_or[:] = self._aux_get_line_info("p_from_mw", "p_hv_mw") self.q_or[:] = self._aux_get_line_info("q_from_mvar", "q_hv_mvar") @@ -1225,8 +1232,9 @@ def _reset_all_nan(self) -> None: self._topo_vect.flags.writeable = True self._topo_vect[:] = -1 self._topo_vect.flags.writeable = False + self.line_status.flags.writeable = True self.line_status[:] = False - + self.line_status.flags.writeable = False def copy(self) -> "PandaPowerBackend": """ INTERNAL @@ -1372,12 +1380,15 @@ def get_line_status(self) -> np.ndarray: return self.line_status def _get_line_status(self): - return np.concatenate( + self.line_status.flags.writeable = True + self.line_status[:] = np.concatenate( ( self._grid.line["in_service"].values, self._grid.trafo["in_service"].values, ) ).astype(dt_bool) + self.line_status.flags.writeable = False + return self.line_status def get_line_flow(self) -> np.ndarray: return self.a_or @@ -1391,19 +1402,33 @@ def _disconnect_line(self, id_): self._topo_vect[self.line_or_pos_topo_vect[id_]] = -1 self._topo_vect[self.line_ex_pos_topo_vect[id_]] = -1 self._topo_vect.flags.writeable = False + self.line_status.flags.writeable = True self.line_status[id_] = False + self.line_status.flags.writeable = False def _reconnect_line(self, id_): if id_ < self._number_true_line: self._grid.line.iloc[id_, self._in_service_line_col_id] = True else: self._grid.trafo.iloc[id_ - self._number_true_line, self._in_service_trafo_col_id] = True + self.line_status.flags.writeable = True self.line_status[id_] = True + self.line_status.flags.writeable = False def get_topo_vect(self) -> np.ndarray: return self._topo_vect def _get_topo_vect(self): + """ + .. danger:: + you should have called `self._get_line_status` before otherwise it might + not behave correctly ! + + Returns + ------- + _type_ + _description_ + """ cls = type(self) # lines / trafo diff --git a/grid2op/tests/aaa_test_backend_interface.py b/grid2op/tests/aaa_test_backend_interface.py index 7b6872cc..40409eb5 100644 --- a/grid2op/tests/aaa_test_backend_interface.py +++ b/grid2op/tests/aaa_test_backend_interface.py @@ -14,6 +14,7 @@ from grid2op.dtypes import dt_int from grid2op.tests.helper_path_test import HelperTests, MakeBackend, PATH_DATA from grid2op.Exceptions import BackendError, Grid2OpException +from grid2op.Space import DEFAULT_ALLOW_DETACHMENT, DEFAULT_N_BUSBAR_PER_SUB class AAATestBackendAPI(MakeBackend): @@ -39,21 +40,36 @@ def aux_get_env_name(self): """do not run nor modify ! (used for this test class only)""" return "BasicTest_load_grid_" + type(self).__name__ - def aux_make_backend(self, n_busbar=2) -> Backend: + def aux_make_backend(self, + n_busbar=DEFAULT_N_BUSBAR_PER_SUB, + allow_detachment=DEFAULT_ALLOW_DETACHMENT, + extra_name=None) -> Backend: """do not run nor modify ! (used for this test class only)""" - backend = self.make_backend_with_glue_code(n_busbar=n_busbar) + + if extra_name is None: + extra_name = self.aux_get_env_name() + backend = self.make_backend_with_glue_code(n_busbar=n_busbar, + allow_detachment=allow_detachment, + extra_name=extra_name) backend.load_grid(self.get_path(), self.get_casefile()) backend.load_redispacthing_data("tmp") # pretend there is no generator backend.load_storage_data(self.get_path()) - env_name = self.aux_get_env_name() - backend.env_name = env_name - backend.assert_grid_correct() + backend.assert_grid_correct() return backend def test_00create_backend(self): """Tests the backend can be created (not integrated in a grid2op environment yet)""" self.skip_if_needed() backend = self.make_backend_with_glue_code() + if not backend._missing_two_busbars_support_info: + warnings.warn("You should call either `self.can_handle_more_than_2_busbar()` " + "or `self.cannot_handle_more_than_2_busbar()` in the `load_grid` " + "method of your backend. Please refer to documentation for more information.") + + if not backend._missing_detachment_support: + warnings.warn("You should call either `self.can_handle_detachment()` " + "or `self.cannot_handle_detachment()` in the `load_grid` " + "method of your backend. Please refer to documentation for more information.") def test_01load_grid(self): """Tests the grid can be loaded (supposes that your backend can read the grid.json in educ_case14_storage)* diff --git a/grid2op/tests/helper_path_test.py b/grid2op/tests/helper_path_test.py index e057aedd..35083efa 100644 --- a/grid2op/tests/helper_path_test.py +++ b/grid2op/tests/helper_path_test.py @@ -77,7 +77,7 @@ def make_backend_with_glue_code(self, type(bk)._clear_grid_dependant_class_attributes() type(bk).set_env_name(type(self).__name__ + extra_name) type(bk).set_n_busbar_per_sub(n_busbar) - type(bk)._allow_detachment = allow_detachment + type(bk).set_detachment_is_allowed(allow_detachment) return bk def get_path(self) -> str: From 8b70407ce3e282ad365690895f6bddc0b02b299c Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Tue, 26 Nov 2024 17:44:48 +0100 Subject: [PATCH 29/60] fixing broken tests (hopefully) and improve AAA backend test suite for new feature Signed-off-by: DONNOT Benjamin --- grid2op/Backend/backend.py | 46 ++-- grid2op/Backend/pandaPowerBackend.py | 17 +- grid2op/Environment/environment.py | 7 +- grid2op/tests/aaa_test_backend_interface.py | 285 +++++++++++++------- grid2op/tests/test_attached_envs_compat.py | 25 +- grid2op/tests/test_n_busbar_per_sub.py | 10 +- 6 files changed, 252 insertions(+), 138 deletions(-) diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index 9ba913fc..fd71dde5 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -121,6 +121,9 @@ class Backend(GridObjects, ABC): _complete_action_class : "Optional[grid2op.Action.CompleteAction]"= None ERR_INIT_POWERFLOW : str = "Power cannot be computed on the first time step, please check your data." + ERR_DETACHMENT : str = ("One or more {} were isolated from the grid " + "but this is not allowed or not supported (Game Over) (detachment_is_allowed is False), " + "check {} {}") def __init__(self, detailed_infos_for_cascading_failures:bool=False, can_be_copied:bool=True, @@ -181,7 +184,7 @@ def __init__(self, self.n_busbar_per_sub: int = DEFAULT_N_BUSBAR_PER_SUB #: .. versionadded: 1.11.0 - self._missing_detachment_support : bool = True + self._missing_detachment_support_info : bool = True self.detachment_is_allowed : bool = DEFAULT_ALLOW_DETACHMENT def can_handle_more_than_2_busbar(self): @@ -270,7 +273,7 @@ def can_handle_detachment(self): We highly recommend you do not try to override this function. At least, at time of writing there is no good reason to do so. """ - self._missing_detachment_support = False + self._missing_detachment_support_info = False self.detachment_is_allowed = type(self).detachment_is_allowed def cannot_handle_detachment(self): @@ -297,7 +300,7 @@ def cannot_handle_detachment(self): We highly recommend you do not try to override this function. At least, at time of writing there is no good reason to do so. """ - self._missing_detachment_support = False + self._missing_detachment_support_info = False if type(self).detachment_is_allowed != DEFAULT_ALLOW_DETACHMENT: warnings.warn("You asked in 'make' function to allow shedding. This is" f"not possible with a backend of type {type(self)}.") @@ -1079,7 +1082,7 @@ def _runpf_with_diverging_exception(self, is_dc : bool) -> Optional[Exception]: """ conv = False exc_me = None - + cls = type(self) try: conv, exc_me = self.runpf(is_dc=is_dc) # run powerflow @@ -1091,21 +1094,28 @@ def _runpf_with_diverging_exception(self, is_dc : bool) -> Optional[Exception]: # Check if loads/gens have been detached and if this is allowed, otherwise raise an error # .. versionadded:: 1.11.0 topo_vect = self.get_topo_vect() - load_buses = topo_vect[self.load_pos_topo_vect] - if not self.detachment_is_allowed and (load_buses == -1).any(): - raise Grid2OpException(f"One or more loads were detached before powerflow in Backend {type(self).__name__}" - "but this is not allowed or not supported (Game Over)") + load_buses = topo_vect[cls.load_pos_topo_vect] + if not cls.detachment_is_allowed and (load_buses == -1).any(): + raise BackendError(cls.ERR_DETACHMENT.format("loads", "loads", (load_buses == -1).nonzero()[0])) - gen_buses = topo_vect[self.gen_pos_topo_vect] + gen_buses = topo_vect[cls.gen_pos_topo_vect] + if not cls.detachment_is_allowed and (gen_buses == -1).any(): + raise BackendError(cls.ERR_DETACHMENT.format("gens", "gens", (gen_buses == -1).nonzero()[0])) - if not self.detachment_is_allowed and (gen_buses == -1).any(): - raise Grid2OpException(f"One or more generators were detached before powerflow in Backend {type(self).__name__}" - "but this is not allowed or not supported (Game Over)") + if cls.n_storage > 0: + storage_buses = topo_vect[cls.storage_pos_topo_vect] + storage_p, *_ = self.storages_info() + sto_maybe_error = (storage_buses == -1) & (np.abs(storage_p) >= 1e-6) + if not cls.detachment_is_allowed and sto_maybe_error.any(): + raise BackendError((cls.ERR_DETACHMENT.format("storages", "storages", sto_maybe_error.nonzero()[0]) + + " NB storage units are allowed to be disconnected even if " + "`detachment_is_allowed` is False but only if the don't produce active power.")) + except Grid2OpException as exc_: exc_me = exc_ if not conv and exc_me is None: - exc_me = DivergingPowerflow( + exc_me = BackendError( "GAME OVER: Powerflow has diverged during computation " "or a load has been disconnected or a generator has been disconnected." ) @@ -2181,8 +2191,8 @@ def assert_grid_correct(self, _local_dir_cls=None) -> None: "attribute. This is known issue in lightims2grid <= 0.7.5. Please " "upgrade your backend. This will raise an error in the future.") - if hasattr(self, "_missing_detachment_support"): - if self._missing_detachment_support: + if hasattr(self, "_missing_detachment_support_info"): + if self._missing_detachment_support_info: warnings.warn("The backend implementation you are using is probably too old to take advantage of the " "new feature added in grid2op 1.11.0: the possibility " "to detach loads or generators without leading to an immediate game over. " @@ -2194,12 +2204,12 @@ def assert_grid_correct(self, _local_dir_cls=None) -> None: "\nAnd of course, ideally, if the current implementation " "of your backend cannot handle detachment then change it :-)\n" "Your backend will behave as if it did not support it.") - self._missing_detachment_support = False + self._missing_detachment_support_info = False self.detachment_is_allowed = DEFAULT_ALLOW_DETACHMENT else: - self._missing_detachment_support = False + self._missing_detachment_support_info = False self.detachment_is_allowed = DEFAULT_ALLOW_DETACHMENT - warnings.warn("Your backend is missing the `_missing_detachment_support` " + warnings.warn("Your backend is missing the `_missing_detachment_support_info` " "attribute.") orig_type = type(self) diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index 1adda34d..533017b1 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -829,6 +829,9 @@ def storage_deact_for_backward_comaptibility(self) -> None: self.storage_p = np.full(cls.n_storage, dtype=dt_float, fill_value=np.NaN) self.storage_q = np.full(cls.n_storage, dtype=dt_float, fill_value=np.NaN) self.storage_v = np.full(cls.n_storage, dtype=dt_float, fill_value=np.NaN) + self._topo_vect.flags.writeable = True + self._topo_vect.resize(cls.dim_topo) + self._topo_vect.flags.writeable = False self._get_topo_vect() def _convert_id_topo(self, id_big_topo): @@ -899,7 +902,6 @@ def apply_action(self, backendAction: Union["grid2op.Action._backendAction._Back tmp_stor_p = self._grid.storage["p_mw"] if (storage.changed).any(): tmp_stor_p.iloc[storage.changed] = storage.values[storage.changed] - # topology of the storage stor_bus = backendAction.get_storages_bus() new_bus_num = dt_int(1) * self._grid.storage["bus"].values @@ -1183,11 +1185,8 @@ def runpf(self, is_dc : bool=False) -> Tuple[bool, Union[Exception, None]]: self.storage_v[:], self.storage_theta[:], ) = self._storages_info() + deact_storage = ~np.isfinite(self.storage_v) - if (np.abs(self.storage_p[deact_storage]) > self.tol).any(): - raise pp.powerflow.LoadflowNotConverged( - "Isolated storage set to absorb / produce something" - ) self.storage_p[deact_storage] = 0.0 self.storage_q[deact_storage] = 0.0 self.storage_v[deact_storage] = 0.0 @@ -1336,7 +1335,7 @@ def copy(self) -> "PandaPowerBackend": res._in_service_trafo_col_id = self._in_service_trafo_col_id res._missing_two_busbars_support_info = self._missing_two_busbars_support_info - res._missing_detachment_support = self._missing_detachment_support + res._missing_detachment_support_info = self._missing_detachment_support_info res.div_exception = self.div_exception return res @@ -1552,8 +1551,10 @@ def _storages_info(self): if self.n_storage: # this is because we support "backward comaptibility" feature. So the storage can be # deactivated from the Environment... - p_storage = self._grid.res_storage["p_mw"].values.astype(dt_float) - q_storage = self._grid.res_storage["q_mvar"].values.astype(dt_float) + # p_storage = self._grid.res_storage["p_mw"].values.astype(dt_float) + # q_storage = self._grid.res_storage["q_mvar"].values.astype(dt_float) + p_storage = self._grid.storage["p_mw"].values.astype(dt_float) + q_storage = self._grid.storage["q_mvar"].values.astype(dt_float) v_storage = ( self._grid.res_bus.loc[self._grid.storage["bus"].values][ "vm_pu" diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 431ea224..299e0104 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -265,7 +265,7 @@ def _init_backend( need_process_backend = False if not self.backend.is_loaded: if hasattr(self.backend, "init_pp_backend") and self.backend.init_pp_backend is not None: - # hack for lightsim2grid ... + # hack for legacy lightsim2grid ... if type(self.backend.init_pp_backend)._INIT_GRID_CLS is not None: type(self.backend.init_pp_backend)._INIT_GRID_CLS._clear_grid_dependant_class_attributes() type(self.backend.init_pp_backend)._clear_grid_dependant_class_attributes() @@ -282,7 +282,6 @@ def _init_backend( type(self.backend).set_env_name(self.name) type(self.backend).set_n_busbar_per_sub(self._n_busbar) type(self.backend).set_detachment_is_allowed(self._allow_detachment) - if self._compat_glop_version is not None: type(self.backend).glop_version = self._compat_glop_version @@ -296,8 +295,8 @@ def _init_backend( except BackendError as exc_: self.backend.redispatching_unit_commitment_availble = False warnings.warn(f"Impossible to load redispatching data. This is not an error but you will not be able " - f"to use all grid2op functionalities. " - f"The error was: \"{exc_}\"") + f"to use all grid2op functionalities. " + f"The error was: \"{exc_}\"") exc_ = self.backend.load_grid_layout(self.get_path_env()) if exc_ is not None: warnings.warn( diff --git a/grid2op/tests/aaa_test_backend_interface.py b/grid2op/tests/aaa_test_backend_interface.py index 40409eb5..71cabe00 100644 --- a/grid2op/tests/aaa_test_backend_interface.py +++ b/grid2op/tests/aaa_test_backend_interface.py @@ -66,7 +66,7 @@ def test_00create_backend(self): "or `self.cannot_handle_more_than_2_busbar()` in the `load_grid` " "method of your backend. Please refer to documentation for more information.") - if not backend._missing_detachment_support: + if not backend._missing_detachment_support_info: warnings.warn("You should call either `self.can_handle_detachment()` " "or `self.cannot_handle_detachment()` in the `load_grid` " "method of your backend. Please refer to documentation for more information.") @@ -803,8 +803,40 @@ def test_15_reset(self): assert np.allclose(q2_or, q_or), f"The q_or flow differ between its original value and after a reset. Check backend.reset()" assert np.allclose(v2_or, v_or), f"The v_or differ between its original value and after a reset. Check backend.reset()" assert np.allclose(a2_or, a_or), f"The a_or flow differ between its original value and after a reset. Check backend.reset()" - - def test_16_isolated_load_stops_computation(self): + + def _aux_aux_test_detachment_should_fail(self, maybe_exc): + assert maybe_exc is not None, "When your backend diverges, we expect it throws an exception (second return value)" + assert isinstance(maybe_exc, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(maybe_exc)}" + if not isinstance(maybe_exc, BackendError): + warnings.warn("The error returned by your backend when it stopped (due to isolated element) should preferably inherit from BackendError") + + def _aux_test_detachment(self, backend : Backend, is_dc=True, detachment_should_pass = False): + """auxilliary method to handle the "legacy" code, when the backend was expected to + handle the error """ + str_ = "DC" if is_dc else "AC" + if backend._missing_detachment_support_info: + # legacy behaviour, should behave as if it diverges + # for new (>= 1.11.0) behaviour, it is catched in the method `_runpf_with_diverging_exception` + res = backend.runpf(is_dc=is_dc) + assert not res[0], f"It is expected (at time of writing) that your backend returns `False` in case of isolated loads in {str_}." + maybe_exc = res[1] + detachment_allowed = False + else: + # new (1.11.0) test here + maybe_exc = backend._runpf_with_diverging_exception(is_dc=is_dc) + detachment_allowed = type(backend).detachment_is_allowed + if not detachment_allowed: + # should raise in all cases as the backend prevent detachment + self._aux_aux_test_detachment_should_fail(maybe_exc) + elif not detachment_should_pass: + # it expected that even if the backend supports detachment, + # this test should fail (kwargs detachment_should_pass set to False) + self._aux_aux_test_detachment_should_fail(maybe_exc) + else: + # detachment should not make things diverge + assert maybe_exc is None, f"Your backend supports detachment of loads or generator, yet it diverges when some loads / generators are disconnected." + + def test_16_isolated_load_stops_computation(self, allow_detachment=DEFAULT_ALLOW_DETACHMENT): """Tests that an isolated load will be spotted by the `run_pf` method and forwarded to grid2op by returining `False, an_exception` (in AC and DC) This test supposes that : @@ -818,9 +850,12 @@ def test_16_isolated_load_stops_computation(self): Currently this stops the computation of the environment and lead to a game over. This behaviour might change in the future. + + .. note:: + This test is also used in `attr:AAATestBackendAPI.test_33_allow_detachment` """ self.skip_if_needed() - backend = self.aux_make_backend() + backend = self.aux_make_backend(allow_detachment=allow_detachment) cls = type(backend) # a load alone on a bus @@ -830,13 +865,8 @@ def test_16_isolated_load_stops_computation(self): bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 res = backend.runpf(is_dc=False) - # assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of isolated loads in AC." - assert res[1] is not None, "When your backend diverges, we expect it throws an exception (second return value)" - error = res[1] - assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" - if not isinstance(error, BackendError): - warnings.warn("The error returned by your backend when it stopped (due to isolated shunt) should preferably inherit from BackendError") - + self._aux_test_detachment(backend, is_dc=False) + backend.reset(self.get_path(), self.get_casefile()) # a load alone on a bus action = type(backend)._complete_action_class() @@ -844,15 +874,9 @@ def test_16_isolated_load_stops_computation(self): bk_act = type(backend).my_bk_act_class() bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 - res = backend.runpf(is_dc=True) - # assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of isolated loads in DC." - assert res[1] is not None, "When your backend diverges, we expect it throws an exception (second return value)" - error = res[1] - assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" - if not isinstance(error, BackendError): - warnings.warn("The error returned by your backend when it stopped (due to isolated shunt) should preferably inherit from BackendError") - - def test_17_isolated_gen_stops_computation(self): + self._aux_test_detachment(backend, is_dc=True) + + def test_17_isolated_gen_stops_computation(self, allow_detachment=DEFAULT_ALLOW_DETACHMENT): """Tests that an isolated generator will be spotted by the `run_pf` method and forwarded to grid2op by returining `False, an_exception` (in AC and DC) This test supposes that : @@ -866,9 +890,12 @@ def test_17_isolated_gen_stops_computation(self): Currently this stops the computation of the environment and lead to a game over. This behaviour might change in the future. + + .. note:: + This test is also used in `attr:AAATestBackendAPI.test_33_allow_detachment` """ self.skip_if_needed() - backend = self.aux_make_backend() + backend = self.aux_make_backend(allow_detachment=allow_detachment) cls = type(backend) # disconnect a gen @@ -877,14 +904,8 @@ def test_17_isolated_gen_stops_computation(self): bk_act = type(backend).my_bk_act_class() bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 - res = backend.runpf(is_dc=False) - # assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of isolated gen." - assert res[1] is not None, "When your backend diverges, we expect it throws an exception (second return value)" - error = res[1] - assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" - if not isinstance(error, BackendError): - warnings.warn("The error returned by your backend when it stopped (due to isolated shunt) should preferably inherit from BackendError") - + self._aux_test_detachment(backend, is_dc=False) + backend.reset(self.get_path(), self.get_casefile()) # disconnect a gen action = type(backend)._complete_action_class() @@ -892,15 +913,9 @@ def test_17_isolated_gen_stops_computation(self): bk_act = type(backend).my_bk_act_class() bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 - res = backend.runpf(is_dc=True) - # assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of isolated gen." - assert res[1] is not None, "When your backend diverges, we expect it throws an exception (second return value)" - error = res[1] - assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" - if not isinstance(error, BackendError): - warnings.warn("The error returned by your backend when it stopped (due to isolated shunt) should preferably inherit from BackendError") - - def test_18_isolated_shunt_stops_computation(self): + self._aux_test_detachment(backend, is_dc=True) + + def test_18_isolated_shunt_stops_computation(self, allow_detachment=DEFAULT_ALLOW_DETACHMENT): """Tests test that an isolated shunt will be spotted by the `run_pf` method and forwarded to grid2op by returining `False, an_exception` (in AC and DC) This test supposes that : @@ -916,9 +931,12 @@ def test_18_isolated_shunt_stops_computation(self): Currently this stops the computation of the environment and lead to a game over. This behaviour might change in the future. + + .. note:: + This test is also used in `attr:AAATestBackendAPI.test_33_allow_detachment` """ self.skip_if_needed() - backend = self.aux_make_backend() + backend = self.aux_make_backend(allow_detachment=allow_detachment) cls = type(backend) if not cls.shunts_data_available: self.skipTest("Your backend does not support shunts") @@ -931,14 +949,8 @@ def test_18_isolated_shunt_stops_computation(self): bk_act = type(backend).my_bk_act_class() bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 - res = backend.runpf(is_dc=False) - # assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of isolated shunt." - assert res[1] is not None, "When your backend diverges, we expect it throws an exception (second return value)" - error = res[1] - assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" - if not isinstance(error, BackendError): - warnings.warn("The error returned by your backend when it stopped (due to isolated shunt) should preferably inherit from BackendError") - + self._aux_test_detachment(backend, is_dc=False) + backend.reset(self.get_path(), self.get_casefile()) # make a shunt alone on a bus action = type(backend)._complete_action_class() @@ -946,15 +958,9 @@ def test_18_isolated_shunt_stops_computation(self): bk_act = type(backend).my_bk_act_class() bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 - res = backend.runpf(is_dc=True) - # assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of isolated shunt in DC." - assert res[1] is not None, "When your backend stops, we expect it throws an exception (second return value)" - error = res[1] - assert isinstance(error, Grid2OpException), f"When your backend returns `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" - if not isinstance(error, BackendError): - warnings.warn("The error returned by your backend when it stopped (due to isolated shunt) should preferably inherit from BackendError") - - def test_19_isolated_storage_stops_computation(self): + self._aux_test_detachment(backend, is_dc=True) + + def test_19_isolated_storage_stops_computation(self, allow_detachment=DEFAULT_ALLOW_DETACHMENT): """Teststest that an isolated storage unit will be spotted by the `run_pf` method and forwarded to grid2op by returining `False, an_exception` (in AC and DC) This test supposes that : @@ -969,10 +975,11 @@ def test_19_isolated_storage_stops_computation(self): .. note:: Currently this stops the computation of the environment and lead to a game over. - This behaviour might change in the future. + .. note:: + This test is also used in `attr:AAATestBackendAPI.test_33_allow_detachment` """ self.skip_if_needed() - backend = self.aux_make_backend() + backend = self.aux_make_backend(allow_detachment=allow_detachment) cls = type(backend) if cls.n_storage == 0: self.skipTest("Your backend does not support storage units") @@ -982,29 +989,17 @@ def test_19_isolated_storage_stops_computation(self): bk_act = type(backend).my_bk_act_class() bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 - res = backend.runpf(is_dc=False) - # assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of isolated storage units in AC." - assert res[1] is not None, "When your backend stops, we expect it throws an exception (second return value)" - error = res[1] - assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" - if not isinstance(error, BackendError): - warnings.warn("The error returned by your backend when it stopped (due to isolated storage units) should preferably inherit from BackendError") - + self._aux_test_detachment(backend, is_dc=False) + backend.reset(self.get_path(), self.get_casefile()) action = type(backend)._complete_action_class() action.update({"set_bus": {"storages_id": [(0, 2)]}}) bk_act = type(backend).my_bk_act_class() bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 - res = backend.runpf(is_dc=True) - # assert not res[0], "It is expected (at time of writing) that your backend returns `False` in case of isolated storage unit." - assert res[1] is not None, "When your backend stops, we expect it throws an exception (second return value)" - error = res[1] - assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" - if not isinstance(error, BackendError): - warnings.warn("The error returned by your backend when it stopped (due to isolated storage units) should preferably inherit from BackendError") - - def test_20_disconnected_load_stops_computation(self): + self._aux_test_detachment(backend, is_dc=True) + + def test_20_disconnected_load_stops_computation(self, allow_detachment=DEFAULT_ALLOW_DETACHMENT): """ Tests that a disconnected load unit will be caught by the `_runpf_with_diverging_exception` method. @@ -1020,10 +1015,11 @@ def test_20_disconnected_load_stops_computation(self): .. note:: Currently this stops the computation of the environment and lead to a game over. - Behaviour changed in version 1.11.0 (no longer caught by runpf() itelf) + .. note:: + This test is also used in `attr:AAATestBackendAPI.test_33_allow_detachment` """ self.skip_if_needed() - backend = self.aux_make_backend() + backend = self.aux_make_backend(allow_detachment=allow_detachment) # a load alone on a bus action = type(backend)._complete_action_class() @@ -1031,9 +1027,7 @@ def test_20_disconnected_load_stops_computation(self): bk_act = type(backend).my_bk_act_class() bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 - error = backend._runpf_with_diverging_exception(is_dc=False) - assert error is not None - assert isinstance(error, Grid2OpException) + self._aux_test_detachment(backend, is_dc=False, detachment_should_pass=True) backend.reset(self.get_path(), self.get_casefile()) # a load alone on a bus @@ -1042,11 +1036,9 @@ def test_20_disconnected_load_stops_computation(self): bk_act = type(backend).my_bk_act_class() bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 - error = backend._runpf_with_diverging_exception(is_dc=True) - assert error is not None - assert isinstance(error, Grid2OpException) + self._aux_test_detachment(backend, is_dc=True, detachment_should_pass=True) - def test_21_disconnected_gen_stops_computation(self): + def test_21_disconnected_gen_stops_computation(self, allow_detachment=DEFAULT_ALLOW_DETACHMENT): """ Tests that a disconnected generator will be caught by the `_runpf_with_diverging_exception` method @@ -1062,10 +1054,11 @@ def test_21_disconnected_gen_stops_computation(self): .. note:: Currently this stops the computation of the environment and lead to a game over. - Behaviour changed in version 1.11.0 (no longer caught by runpf() itelf) + .. note:: + This test is also used in `attr:AAATestBackendAPI.test_33_allow_detachment` """ self.skip_if_needed() - backend = self.aux_make_backend() + backend = self.aux_make_backend(allow_detachment=allow_detachment) # a disconnected generator action = type(backend)._complete_action_class() @@ -1073,9 +1066,7 @@ def test_21_disconnected_gen_stops_computation(self): bk_act = type(backend).my_bk_act_class() bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 - error = backend._runpf_with_diverging_exception(is_dc=False) - assert error is not None - assert isinstance(error, Grid2OpException) + self._aux_test_detachment(backend, is_dc=False, detachment_should_pass=True) backend.reset(self.get_path(), self.get_casefile()) # a disconnected generator @@ -1084,9 +1075,7 @@ def test_21_disconnected_gen_stops_computation(self): bk_act = type(backend).my_bk_act_class() bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 - res = backend._runpf_with_diverging_exception(is_dc=True) - assert error is not None - assert isinstance(error, Grid2OpException) + self._aux_test_detachment(backend, is_dc=True, detachment_should_pass=True) def test_22_islanded_grid_stops_computation(self): """Tests that when the grid is split in two different "sub_grid" is spotted by the `run_pf` method and forwarded to grid2op by returining `False, an_exception` (in AC and DC) @@ -1119,7 +1108,7 @@ def test_22_islanded_grid_stops_computation(self): bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 res = backend.runpf(is_dc=False) - # assert not res[0], f"It is expected that your backend return `(False, _)` in case of non connected grid in AC." + assert not res[0], f"It is expected that your backend return `(False, _)` in case of non connected grid in AC." error = res[1] assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" if not isinstance(error, BackendError): @@ -1135,7 +1124,7 @@ def test_22_islanded_grid_stops_computation(self): bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 res = backend.runpf(is_dc=True) - # assert not res[0], f"It is expected that your backend return `(False, _)` in case of non connected grid in DC." + assert not res[0], f"It is expected that your backend return `(False, _)` in case of non connected grid in DC." error = res[1] assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" if not isinstance(error, BackendError): @@ -1278,7 +1267,7 @@ def test_25_disco_storage_v_null(self): res = backend.runpf(is_dc=True) assert res[0], f"Your backend diverged in DC after a storage disconnection, error was {res[1]}" p_, q_, v_ = backend.storages_info() - assert np.allclose(v_[storage_id], 0.), f"v should be 0. for disconnected storage, but is currently {v_[storage_id]} (AC)" + assert np.allclose(v_[storage_id], 0.), f"v should be 0. for disconnected storage, but is currently {v_[storage_id]} (DC)" def test_26_copy(self): """Tests that the backend can be copied (and that the copied backend and the @@ -1701,4 +1690,110 @@ def test_30_n_busbar_per_sub_ok(self): el_nm, el_key, el_pos_topo_vect) else: warnings.warn(f"{type(self).__name__} test_30_n_busbar_per_sub_ok: This test is not performed in depth as your backend does not support storage units (or there are none on the grid)") + + def _aux_disco_sto_then_add_sto_p(self, backend: Backend): + action = type(backend)._complete_action_class() + action.update({"set_bus": {"storages_id": [(0, -1)]}}) + bk_act = type(backend).my_bk_act_class() + bk_act += action + backend.apply_action(bk_act) + action = type(backend)._complete_action_class() + action.update({"set_storage": [(0, 0.1)]}) + bk_act = type(backend).my_bk_act_class() + bk_act += action + backend.apply_action(bk_act) + + def test_31_disconnected_storage_with_p_stops_computation(self, allow_detachment=DEFAULT_ALLOW_DETACHMENT): + """ + Tests that a disconnected storage unit that is asked to produce active power + raise an error if the backend does not support `allow_detachment` + + This test supposes that : + + - backend.load_grid(...) is implemented + - backend.runpf() (AC and DC mode) is implemented + - backend.apply_action() for topology modification + - backend.reset() is implemented + + NB: this test is skipped if your backend does not (yet :-) ) supports storage units + + .. note:: + Currently this stops the computation of the environment and lead to a game over. + + .. note:: + This test is also used in `attr:AAATestBackendAPI.test_31_allow_detachment` + + """ + self.skip_if_needed() + backend = self.aux_make_backend(allow_detachment=allow_detachment) + if type(backend).n_storage == 0: + self.skipTest("Your backend does not support storage unit") + + # a disconnected generator + self._aux_disco_sto_then_add_sto_p(backend) + self._aux_test_detachment(backend, is_dc=False, detachment_should_pass=True) + + backend.reset(self.get_path(), self.get_casefile()) + # a disconnected generator + self._aux_disco_sto_then_add_sto_p(backend) + self._aux_test_detachment(backend, is_dc=True, detachment_should_pass=True) + + def test_32_xxx_handle_detachment_called(self): + """Tests that at least one of the function: + + - :func:`grid2op.Backend.Backend.can_handle_detachment` + - :func:`grid2op.Backend.Backend.cannot_handle_detachment` + + has been implemented in the :func:`grid2op.Backend.Backend.load_grid` + implementation. + + This test supposes that : + + - backend.load_grid(...) is implemented + + .. versionadded:: 1.11.0 + + """ + self.skip_if_needed() + backend = self.aux_make_backend() + assert not backend._missing_detachment_support_info + + def test_33_allow_detachment(self): + """Tests that your backend model disconnected load / generator (is the proper flag is present.) + + Concretely it will run the tests + + - :attr:`TestBackendAPI.test_16_isolated_load_stops_computation` + - :attr:`TestBackendAPI.test_17_isolated_gen_stops_computation` + - :attr:`TestBackendAPI.test_18_isolated_shunt_stops_computation` + - :attr:`TestBackendAPI.test_19_isolated_storage_stops_computation` + - :attr:`TestBackendAPI.test_20_disconnected_load_stops_computation` + - :attr:`TestBackendAPI.test_21_disconnected_gen_stops_computation` + + When your backend is initialized with "allow_detachment". + + NB: of course these tests have been modified such that things that should pass + will pass and things that should fail will fail. + + .. versionadded:: 1.11.0 + + """ + self.skip_if_needed() + backend = self.aux_make_backend(allow_detachment=True) + if backend._missing_detachment_support_info: + self.skipTest("Cannot perform this test as you have not specified whether " + "the backend class supports the 'detachement' of loads and " + "generators. Falling back to default grid2op behaviour, which " + "is to fail if a load or a generator is disconnected.") + if not type(backend).detachment_is_allowed: + self.skipTest("Cannot perform this test as your backend does not appear " + "to support the `detachment` information: a disconnect load " + "or generator is necessarily causing a game over.") + self.test_16_isolated_load_stops_computation(allow_detachment=True) + self.test_17_isolated_gen_stops_computation(allow_detachment=True) + self.test_18_isolated_shunt_stops_computation(allow_detachment=True) + self.test_19_isolated_storage_stops_computation(allow_detachment=True) + self.test_20_disconnected_load_stops_computation(allow_detachment=True) + self.test_21_disconnected_gen_stops_computation(allow_detachment=True) + self.test_31_disconnected_storage_with_p_stops_computation(allow_detachment=True) \ No newline at end of file diff --git a/grid2op/tests/test_attached_envs_compat.py b/grid2op/tests/test_attached_envs_compat.py index 9b790497..6418b56e 100644 --- a/grid2op/tests/test_attached_envs_compat.py +++ b/grid2op/tests/test_attached_envs_compat.py @@ -12,6 +12,7 @@ import grid2op import numpy as np +from grid2op.Backend import Backend, PandaPowerBackend from grid2op.Space import GridObjects from grid2op.Action import PowerlineSetAction, DontAct, PlayableAction from grid2op.Observation import CompleteObservation @@ -46,11 +47,11 @@ def test_opponent(self): def test_action_space(self): assert issubclass(self.env.action_space.subtype, PlayableAction) - assert self.env.action_space.n == 494 + assert self.env.action_space.n == 494, f"{self.env.action_space.n}" def test_observation_space(self): assert issubclass(self.env.observation_space.subtype, CompleteObservation) - assert self.env.observation_space.n == 1266 + assert self.env.observation_space.n == 1266, f"{self.env.observation_space.n}" def test_random_action(self): """test i can perform some step (random)""" @@ -91,14 +92,14 @@ def test_opponent(self): def test_action_space(self): assert issubclass(self.env.action_space.subtype, PlayableAction) - assert self.env.action_space.n == 1500 + assert self.env.action_space.n == 1500, f"{self.env.action_space.n}" def test_observation_space(self): assert issubclass(self.env.observation_space.subtype, CompleteObservation) assert ( "curtailment" not in self.env.observation_space.subtype.attr_list_vect ), "curtailment should not be there" - assert self.env.observation_space.n == 3868 + assert self.env.observation_space.n == 3868, f"{self.env.observation_space.n}" def test_random_action(self): """test i can perform some step (random)""" @@ -139,11 +140,11 @@ def test_opponent(self): def test_action_space(self): assert issubclass(self.env.action_space.subtype, PlayableAction) - assert self.env.action_space.n == 160 + assert self.env.action_space.n == 160, f"{self.env.action_space.n}" def test_observation_space(self): assert issubclass(self.env.observation_space.subtype, CompleteObservation) - assert self.env.observation_space.n == 420 + assert self.env.observation_space.n == 420, f"{self.env.observation_space.n}" def test_random_action(self): """test i can perform some step (random)""" @@ -184,11 +185,11 @@ def test_opponent(self): def test_action_space(self): assert issubclass(self.env.action_space.subtype, PlayableAction) - assert self.env.action_space.n == 26 + assert self.env.action_space.n == 26, f"{self.env.action_space.n}" def test_observation_space(self): assert issubclass(self.env.observation_space.subtype, CompleteObservation) - assert self.env.observation_space.n == 420 + assert self.env.observation_space.n == 420, f"{self.env.observation_space.n}" def test_random_action(self): """test i can perform some step (random)""" @@ -225,15 +226,17 @@ def test_elements(self): def test_opponent(self): assert issubclass(self.env._opponent_action_class, DontAct) - assert self.env._opponent_action_space.n == 0 + assert self.env._opponent_action_space.n == 0, f"{self.env._opponent_action_space.n}" def test_action_space(self): assert issubclass(self.env.action_space.subtype, PlayableAction) - assert self.env.action_space.n == 26 + assert self.env.action_space.n == 26, f"{self.env.action_space.n}" def test_observation_space(self): assert issubclass(self.env.observation_space.subtype, CompleteObservation) - assert self.env.observation_space.n == 420 + import pdb + pdb.set_trace() + assert self.env.observation_space.n == 420, f"{self.env.observation_space.n}" def test_same_env_as_no_storage(self): res = 0 diff --git a/grid2op/tests/test_n_busbar_per_sub.py b/grid2op/tests/test_n_busbar_per_sub.py index f1e59b0c..778e83c0 100644 --- a/grid2op/tests/test_n_busbar_per_sub.py +++ b/grid2op/tests/test_n_busbar_per_sub.py @@ -1254,7 +1254,10 @@ def test_move_line_or(self): assert self.env.backend._grid.bus.loc[global_bus]["in_service"] else: assert not self.env.backend._grid.line.iloc[line_id]["in_service"] - self.env.backend.line_status[:] = self.env.backend._get_line_status() # otherwise it's not updated + tmp = self.env.backend._get_line_status() # otherwise it's not updated + self.env.backend.line_status.flags.writeable = True + self.env.backend.line_status[:] = tmp + self.env.backend.line_status.flags.writeable = False topo_vect = self.env.backend._get_topo_vect() assert topo_vect[cls.line_or_pos_topo_vect[line_id]] == new_bus, f"{topo_vect[cls.line_or_pos_topo_vect[line_id]]} vs {new_bus}" @@ -1272,7 +1275,10 @@ def test_move_line_ex(self): assert self.env.backend._grid.bus.loc[global_bus]["in_service"] else: assert not self.env.backend._grid.line.iloc[line_id]["in_service"] - self.env.backend.line_status[:] = self.env.backend._get_line_status() # otherwise it's not updated + tmp = self.env.backend._get_line_status() # otherwise it's not updated + self.env.backend.line_status.flags.writeable = True + self.env.backend.line_status[:] = tmp + self.env.backend.line_status.flags.writeable = False topo_vect = self.env.backend._get_topo_vect() assert topo_vect[cls.line_ex_pos_topo_vect[line_id]] == new_bus, f"{topo_vect[cls.line_ex_pos_topo_vect[line_id]]} vs {new_bus}" From 2e813009f285825d273ecfb9c69d026ba049ff5b Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Tue, 26 Nov 2024 18:14:48 +0100 Subject: [PATCH 30/60] remove a pdb... Signed-off-by: DONNOT Benjamin --- grid2op/tests/test_attached_envs_compat.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/grid2op/tests/test_attached_envs_compat.py b/grid2op/tests/test_attached_envs_compat.py index 6418b56e..7a40d86e 100644 --- a/grid2op/tests/test_attached_envs_compat.py +++ b/grid2op/tests/test_attached_envs_compat.py @@ -234,8 +234,6 @@ def test_action_space(self): def test_observation_space(self): assert issubclass(self.env.observation_space.subtype, CompleteObservation) - import pdb - pdb.set_trace() assert self.env.observation_space.n == 420, f"{self.env.observation_space.n}" def test_same_env_as_no_storage(self): From 3e25e952c3cd5bb11dfe6ab733973239fbc84ad7 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Wed, 27 Nov 2024 10:58:48 +0100 Subject: [PATCH 31/60] Debug: Fix check in PandaPower Backend that prevents large-scale load/gen detachment Signed-off-by: Xavier Weiss --- grid2op/Backend/pandaPowerBackend.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index c3b048fd..4b41084d 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -1085,8 +1085,9 @@ def runpf(self, is_dc : bool=False) -> Tuple[bool, Union[Exception, None]]: self.load_theta[:], ) = self._loads_info() + load_in_service = self._grid.res_load["in_service"] if not is_dc: - if not np.isfinite(self.load_v).all(): + if not np.isfinite(self.load_v[load_in_service]).all(): # TODO see if there is a better way here # some loads are disconnected: it's a game over case! raise pp.powerflow.LoadflowNotConverged(f"Isolated load: check loads {np.isfinite(self.load_v).nonzero()[0]}") From d2d71ba33f261e3692ef17e8386cfec8c3c79ec4 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Wed, 27 Nov 2024 11:46:46 +0100 Subject: [PATCH 32/60] Debug: Use load dataframe instead of res_load for in_service check Signed-off-by: Xavier Weiss --- grid2op/Backend/pandaPowerBackend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index 4b41084d..d0db0e38 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -1085,7 +1085,7 @@ def runpf(self, is_dc : bool=False) -> Tuple[bool, Union[Exception, None]]: self.load_theta[:], ) = self._loads_info() - load_in_service = self._grid.res_load["in_service"] + load_in_service = self._grid.load["in_service"] if not is_dc: if not np.isfinite(self.load_v[load_in_service]).all(): # TODO see if there is a better way here From 809dfe4df2f067f429aa3c0db97ef5c6051ea0bf Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Wed, 27 Nov 2024 13:30:58 +0100 Subject: [PATCH 33/60] Debug: Temporarily Rollback HotFix to PandaPowerBackend Signed-off-by: Xavier Weiss --- grid2op/Backend/pandaPowerBackend.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index d0db0e38..c3b048fd 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -1085,9 +1085,8 @@ def runpf(self, is_dc : bool=False) -> Tuple[bool, Union[Exception, None]]: self.load_theta[:], ) = self._loads_info() - load_in_service = self._grid.load["in_service"] if not is_dc: - if not np.isfinite(self.load_v[load_in_service]).all(): + if not np.isfinite(self.load_v).all(): # TODO see if there is a better way here # some loads are disconnected: it's a game over case! raise pp.powerflow.LoadflowNotConverged(f"Isolated load: check loads {np.isfinite(self.load_v).nonzero()[0]}") From a97643c98caddac65ec336d4d8833f36327d328c Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 28 Nov 2024 11:25:45 +0100 Subject: [PATCH 34/60] add possibility to detach generators and storage units in the action Signed-off-by: DONNOT Benjamin --- grid2op/Action/baseAction.py | 263 +++++++++++++++++++++++++++------ grid2op/tests/test_shedding.py | 5 +- 2 files changed, 225 insertions(+), 43 deletions(-) diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 776db465..fc122c13 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -371,6 +371,8 @@ class BaseAction(GridObjects): "raise_alarm", "raise_alert", "detach_load", # new in 1.11.0 + "detach_gen", # new in 1.11.0 + "detach_storage", # new in 1.11.0 } attr_list_vect = [ @@ -390,6 +392,8 @@ class BaseAction(GridObjects): "_raise_alarm", "_raise_alert", "_detach_load", # new in 1.11.0 + "_detach_gen", # new in 1.11.0 + "_detach_storage", # new in 1.11.0 ] attr_nan_list_set = set() @@ -485,8 +489,12 @@ def __init__(self, _names_chronics_to_backend: Optional[Dict[Literal["loads", "p if cls.detachment_is_allowed: self._detach_load = np.full(cls.n_load, dtype=dt_bool, fill_value=False) + self._detach_gen = np.full(cls.n_gen, dtype=dt_bool, fill_value=False) + self._detach_storage = np.full(cls.n_storage, dtype=dt_bool, fill_value=False) else: self._detach_load = None + self._detach_gen = None + self._detach_storage = None # change the stuff self._modif_inj = False @@ -500,6 +508,8 @@ def __init__(self, _names_chronics_to_backend: Optional[Dict[Literal["loads", "p self._modif_alarm = False self._modif_alert = False self._modif_detach_load = False + self._modif_detach_gen = False + self._modif_detach_storage = False @classmethod def process_shunt_static_data(cls): @@ -540,6 +550,8 @@ def _aux_copy(self, other): "_modif_alarm", "_modif_alert", "_modif_detach_load", + "_modif_detach_gen", + "_modif_detach_storage", "_single_act", ] @@ -556,6 +568,8 @@ def _aux_copy(self, other): "_raise_alarm", "_raise_alert", "_detach_load", + "_detach_gen", + "_detach_storage", ] if type(self).shunts_data_available: @@ -756,11 +770,15 @@ def as_serializable_dict(self) -> dict: del res["shunt"] if cls.detachment_is_allowed: - res["loads_detached"] = {} - if self._detach_load.any(): - res["loads_detached"] = [int(el) for el in self._detach_load.nonzero()[0]] - if not res["loads_detached"]: - del res["loads_detached"] + for el in ["load", "gen", "storage"]: + attr_key = f"{el}s_detached" + attr_vect = f"_detach_{el}" + res[attr_key] = {} + vect_ = getattr(self, attr_vect) + if vect_.any(): + res[attr_key] = [int(el) for el in vect_.nonzero()[0]] + if not res[attr_key]: + del res[attr_key] return res @classmethod @@ -861,10 +879,13 @@ def process_grid2op_compat(cls): cls.authorized_keys = copy.deepcopy(cls.authorized_keys) cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) - if "detach_load" in cls.authorized_keys: - cls.authorized_keys.remove("detach_load") - if "_detach_load" in cls.attr_list_vect: - cls.attr_list_vect.remove("_detach_load") + for el in ["load", "gen", "storage"]: + attr_key = f"detach_{el}" + attr_vect = f"_{attr_key}" + if attr_key in cls.authorized_keys: + cls.authorized_keys.remove(attr_key) + if attr_vect in cls.attr_list_vect: + cls.attr_list_vect.remove(attr_vect) if (cls.n_busbar_per_sub >= 3) or (cls.n_busbar_per_sub == 1): # only relevant for grid2op >= 1.10.0 @@ -889,6 +910,8 @@ def _reset_modified_flags(self): self._modif_alarm = False self._modif_alert = False self._modif_detach_load = False + self._modif_detach_gen = False + self._modif_detach_storage = False def can_affect_something(self) -> bool: """ @@ -910,6 +933,8 @@ def can_affect_something(self) -> bool: or self._modif_alarm or self._modif_alert or self._modif_detach_load + or self._modif_detach_gen + or self._modif_detach_storage ) def _get_array_from_attr_name(self, attr_name): @@ -943,7 +968,9 @@ def _post_process_from_vect(self): self._modif_curtailment = (np.abs(self._curtail + 1.0) >= 1e-7).any() self._modif_alarm = self._raise_alarm.any() self._modif_alert = self._raise_alert.any() - self._modif_change_bus = (self._detach_load).any() + self._modif_detach_load = (self._detach_load).any() + self._modif_detach_gen = (self._detach_gen).any() + self._modif_detach_storage = (self._detach_storage).any() def _assign_attr_from_name(self, attr_nm, vect): if hasattr(self, attr_nm): @@ -1017,6 +1044,38 @@ def get_change_line_status_vect(self) -> np.ndarray: """ return np.full(shape=self.n_line, fill_value=False, dtype=dt_bool) + + def _aux_eq_detachment_aux_both_ok(self, other, el_nm: Literal["load", "gen", "storage"]) -> bool: + attr_chgt = f"_modif_detach_{el_nm}" + attr_vect = f"_detach_{el_nm}" + # what I want to do: + # if ((self._modif_detach_load != other._modif_detach_load) or + # (self._detach_load != other._detach_load).any() + # ): + # return False + # but for all attribute related to "detach" feature + + if ((getattr(self, attr_chgt) != getattr(other, attr_chgt)) or + (getattr(self, attr_vect) != getattr(other, attr_vect)).any() + ): + return False + return True + + def _aux_eq_detachment_aux_one_not_ok(self, obj_detach_unsupported, el_nm: Literal["load", "gen", "storage"]) -> bool: + # self supports detachment but not other + # they are equal if an only if self did not + # modify any loads with detachment + # if self._modif_detach_load: + # return False + # if self._detach_load.any(): + # return False + attr_chgt = f"_modif_detach_{el_nm}" + attr_vect = f"_detach_{el_nm}" + if getattr(obj_detach_unsupported, attr_chgt): + return False + if getattr(obj_detach_unsupported, attr_vect): + return False + return True def _aux_eq_detachment(self, other: "BaseAction") -> bool: cls = type(self) @@ -1024,18 +1083,16 @@ def _aux_eq_detachment(self, other: "BaseAction") -> bool: if cls.detachment_is_allowed: if cls_oth.detachment_is_allowed: # easy case, both detachement allowed - if ((self._modif_detach_load != other._modif_detach_load) or - (self._detach_load != other._detach_load).any() - ): - return False + for el in ["load", "gen", "storage"]: + if not self._aux_eq_detachment_aux_both_ok(other, el): + return False else: # self supports detachment but not other # they are equal if an only if self did not # modify any loads with detachment - if self._modif_detach_load: - return False - if self._detach_load.any(): - return False + for el in ["load", "gen", "storage"]: + if not self._aux_eq_detachment_aux_one_not_ok(self, el): + return False else: # detachment is not allowed on self # check if it's allowed on other @@ -1044,12 +1101,11 @@ def _aux_eq_detachment(self, other: "BaseAction") -> bool: # oth does. # they can be equal if oth does not modify this # attribute - if other._modif_detach_load: - return False - if other._detach_load.any(): - return False + for el in ["load", "gen", "storage"]: + if not self._aux_eq_detachment_aux_one_not_ok(other, el): + return False else: - # easy case, None supports detachment + # if None support detachment, they are both equal concerning the detachment return True return True @@ -1202,6 +1258,8 @@ def _dont_affect_topology(self) -> bool: and (not self._modif_set_status) and (not self._modif_change_status) and (not self._modif_detach_load) + and (not self._modif_detach_gen) + and (not self._modif_detach_storage) ) def get_topological_impact(self, powerline_status=None) -> Tuple[np.ndarray, np.ndarray]: @@ -1674,6 +1732,8 @@ def _aux_iadd_modif_flags(self, other): self._modif_alarm = self._modif_alarm or other._modif_alarm self._modif_alert = self._modif_alert or other._modif_alert self._modif_detach_load = self._modif_detach_load or other._modif_detach_load + self._modif_detach_gen = self._modif_detach_gen or other._modif_detach_gen + self._modif_detach_storage = self._modif_detach_storage or other._modif_detach_storage def _aux_iadd_shunt(self, other): if not type(other).shunts_data_available: @@ -2192,15 +2252,12 @@ def _digest_change_status(self, dict_): if dict_["change_line_status"] is not None: self.line_change_status = dict_["change_line_status"] - def _digest_detach_load(self, dict_): - if "detach_load" in dict_: - # the action will switch the status of the powerline - # for each element equal to 1 in this dict_["change_line_status"] - # if the status is "disconnected" it will be transformed into "connected" - # and if the status is "connected" it will be switched to "disconnected" - # Lines with "0" in this vector are not impacted. - if dict_["detach_load"] is not None: - self.detach_load = dict_["detach_load"] + def _digest_detach_eltype(self, el : Literal["load", "gen", "storage"], dict_): + attr_key = f'detach_{el}' + if attr_key in dict_: + if dict_[attr_key] is not None: + setattr(self, attr_key, dict_[attr_key]) + # eg self.detach_load = dict_["detach_load"] def _digest_redispatching(self, dict_): if "redispatch" in dict_: @@ -2352,6 +2409,9 @@ def update(self, - "curtail" : TODO - "raise_alarm" : TODO - "raise_alert": TODO + - "detach_load": TODO + - "detach_gen": TODO + - "detach_storage": TODO **NB**: CHANGES: you can reconnect a powerline without specifying on each bus you reconnect it at both its ends. In that case the last known bus id for each its end is used. @@ -2505,7 +2565,8 @@ def update(self, self._digest_alarm(dict_) self._digest_alert(dict_) if cls.detachment_is_allowed: - self._digest_detach_load(dict_) + for el in ["load", "gen", "storage"]: + self._digest_detach_eltype(el, dict_) return self def is_ambiguous(self) -> Tuple[bool, AmbiguousAction]: @@ -2635,14 +2696,18 @@ def _check_for_correct_modif_flags(self): raise IllegalAction("You illegally send an alert.") if cls.detachment_is_allowed: - if (self._detach_load).any(): - if not self._modif_detach_load: - raise AmbiguousAction( - "Incorrect way to detach some loads, the appropriate flag is not " - "modified properly." - ) - if "detach_load" not in self.authorized_keys: - raise IllegalAction("You illegally detached a load.") + for el in ["load", "gen", "storage"]: + attr_auth = f"detach_{el}" + attr_modif = f"_modif_detach_{el}" + attr_vect = f"_detach_{el}" + if (getattr(self, attr_vect)).any(): + if not getattr(self, attr_modif): + raise AmbiguousAction( + f"Incorrect way to detach some {el}, the appropriate flag is not " + f"modified properly." + ) + if attr_auth not in self.authorized_keys: + raise IllegalAction(f"You illegally detached a {el}.") def _check_for_ambiguity(self): """ @@ -5582,7 +5647,7 @@ def detach_load(self) -> np.ndarray: def detach_load(self, values): cls = type(self) if "detach_load" not in cls.authorized_keys: - raise IllegalAction("Impossible to send alerts with this action type.") + raise IllegalAction("Impossible detach loads with this action type.") orig_ = self.detach_load try: self._aux_affect_object_bool( @@ -5599,6 +5664,120 @@ def detach_load(self, values): raise IllegalAction( f"Impossible to detach a load with your input." ) from exc_ + + @property + def detach_gen(self) -> np.ndarray: + """ + + ..versionadded:: 1.11.0 + + Allows to retrieve (and affect) the status (connected / disconnected) of generators. + + .. note:: + It is only available after grid2op version 1.11.0 and if the backend + allows it. + + Returns + ------- + res: + A vector of bool, of size `act.n_gen` indicating whether this generator + is detached or not. + + * ``False`` this generator is not affected by any "detach" action + * ``True`` this generator will be deactivated. + + Examples + -------- + + See examples in the :attr:`BaseAction.detach_load` for more information + + Notes + ----- + See notes in the :attr:`BaseAction.detach_load` for more information + + """ + res = copy.deepcopy(self._detach_gen) + res.flags.writeable = False + return res + + @detach_gen.setter + def detach_gen(self, values): + cls = type(self) + if "detach_gen" not in cls.authorized_keys: + raise IllegalAction("Impossible to detach generator with this action type.") + orig_ = self.detach_gen + try: + self._aux_affect_object_bool( + values, + "detach gens", + cls.n_gen, + cls.name_gen, + np.arange(cls.n_gen), + self._detach_gen, + ) + self._modif_alert = True + except Exception as exc_: + self._detach_gen[:] = orig_ + raise IllegalAction( + f"Impossible to detach a generator with your input." + ) from exc_ + + @property + def detach_storage(self) -> np.ndarray: + """ + + ..versionadded:: 1.11.0 + + Allows to retrieve (and affect) the status (connected / disconnected) of storage units. + + .. note:: + It is only available after grid2op version 1.11.0 and if the backend + allows it. + + Returns + ------- + res: + A vector of bool, of size `act.n_storage` indicating whether this generator + is detached or not. + + * ``False`` this storage unit is not affected by any "detach" action + * ``True`` this storage unit will be deactivated. + + Examples + -------- + + See examples in the :attr:`BaseAction.detach_load` for more information + + Notes + ----- + See notes in the :attr:`BaseAction.detach_load` for more information + + """ + res = copy.deepcopy(self._detach_storage) + res.flags.writeable = False + return res + + @detach_storage.setter + def detach_storage(self, values): + cls = type(self) + if "detach_storage" not in cls.authorized_keys: + raise IllegalAction("Impossible to detach a storage unit with this action type.") + orig_ = self.detach_storage + try: + self._aux_affect_object_bool( + values, + "detach storage units", + cls.n_storage, + cls.name_storage, + np.arange(cls.n_storage), + self._detach_storage, + ) + self._modif_alert = True + except Exception as exc_: + self._detach_storage[:] = orig_ + raise IllegalAction( + f"Impossible to detach a storage unit with your input." + ) from exc_ def _aux_affect_object_float( self, diff --git a/grid2op/tests/test_shedding.py b/grid2op/tests/test_shedding.py index 9541b9a5..94f99f71 100644 --- a/grid2op/tests/test_shedding.py +++ b/grid2op/tests/test_shedding.py @@ -127,7 +127,10 @@ def test_action_property(self): act4 = self.env.action_space() act4.detach_load = {k} assert act4._detach_load[v] - + # TODO shedding: test act.to_dict + # TODO shedding: test act.from_dict + # TODO shedding test.act.to_json + # TODO Shedding: test when backend does not support it is not set # TODO shedding: test when user deactivates it it is not set From edcc81c0f1e62f466d80544d53fbe33f4f175a6d Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Mon, 2 Dec 2024 10:30:57 +0100 Subject: [PATCH 35/60] Refact: Improve Code Smell Signed-off-by: Xavier Weiss --- .../13_DetachmentOfLoadsAndGenerators.ipynb | 72 ++++--------------- grid2op/Agent/topologyGreedy.py | 4 +- 2 files changed, 17 insertions(+), 59 deletions(-) diff --git a/getting_started/13_DetachmentOfLoadsAndGenerators.ipynb b/getting_started/13_DetachmentOfLoadsAndGenerators.ipynb index 1d01ce17..81e6a5d8 100644 --- a/getting_started/13_DetachmentOfLoadsAndGenerators.ipynb +++ b/getting_started/13_DetachmentOfLoadsAndGenerators.ipynb @@ -7,7 +7,7 @@ "### Detachment of Loads and Generators\n", "In emergency conditions, it may be possible / necessary for a grid operator to detach certain loads, generators, or other components in order to prevent a larger blackout. This notebook explores how this can be achieved in Grid2OP. \n", "\n", - "By default shedding is disabled in all environments, to provide the keyword argument allow_detachment when initializing the environment." + "By default detachment is disabled in all environments, to provide the keyword argument allow_detachment when initializing the environment. The backend must be able to support this feature." ] }, { @@ -16,21 +16,28 @@ "metadata": {}, "outputs": [], "source": [ - "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import grid2op\n", - "from lightsim2grid import LightSimBackend\n", "from grid2op.Parameters import Parameters\n", - "\n", "from grid2op.PlotGrid import PlotMatplot\n", "from pathlib import Path\n", "\n", + "# Setup Environment\n", "data_path = Path.cwd() / \"grid2op\" / \"data\"\n", "p = Parameters()\n", "p.MAX_SUB_CHANGED = 5\n", "env = grid2op.make(data_path / \"rte_case5_example\", param=p, allow_detachment=True)\n", + "\n", + "# Setup Plotter Utility\n", "plotter = PlotMatplot(env.observation_space, load_name=True, gen_name=True, dpi=150)\n", - "print(f\"Loads: {env.n_load}, Generators: {env.n_gen}, Storage: {env.n_storage}, Allow Detachment: {env.allow_detachment}\")" + "print(f\"Loads: {env.n_load}, Generators: {env.n_gen}, Storage: {env.n_storage}, Allow Detachment: {env._allow_detachment}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Detach the loads at substation 3 and 4. Normally this would cause a game-over, but if allow_detachment is True, the powerflow will be run. Game over in these cases can only occur if the powerflow does not converge." ] }, { @@ -39,12 +46,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Disconnect the load at substation 4\n", "load_lookup = {name:i for i,name in enumerate(env.name_load)}\n", "gen_lookup = {name:i for i,name in enumerate(env.name_gen)}\n", - "act = env.action_space({\"set_bus\":[(env.load_pos_topo_vect[load_lookup[\"load_4_2\"]], -1),\n", - " (env.load_pos_topo_vect[load_lookup[\"load_3_1\"]], -1)]})\n", - "# act = env.action_space({\"set_bus\":[(env.gen_pos_topo_vect[gen_lookup[\"gen_0_0\"]], -1)]})\n", + "act = env.action_space({\"set_bus\":[(env.load_pos_topo_vect[load_lookup[\"load_3_1\"]], -1),\n", + " (env.load_pos_topo_vect[load_lookup[\"load_4_2\"]], -1)]})\n", "print(act)\n", "env.set_id(\"00\")\n", "init_obs = env.reset()\n", @@ -52,58 +57,11 @@ "plotter.plot_obs(obs, figure=plt.figure(figsize=(8,5)))\n", "plt.show()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import pandapower as pp\n", - "network = env.backend._grid.deepcopy()\n", - "display(network.res_line.loc[:, [\"p_from_mw\", \"p_to_mw\", \"q_from_mvar\", \"q_to_mvar\"]])\n", - "pp.runpp(network,\n", - " check_connectivity=False,\n", - " init=\"dc\",\n", - " lightsim2grid=False,\n", - " max_iteration=10,\n", - " distributed_slack=False,\n", - ")\n", - "display(network.res_line.loc[:, [\"p_from_mw\", \"p_to_mw\", \"q_from_mvar\", \"q_to_mvar\"]])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "env.backend.loads_info()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "env.backend.generators_info()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "topo_vect = env.backend.get_topo_vect()\n", - "topo_vect[env.backend.load_pos_topo_vect]" - ] } ], "metadata": { "kernelspec": { - "display_name": "venv_grid2op", + "display_name": "venv_test", "language": "python", "name": "python3" }, diff --git a/grid2op/Agent/topologyGreedy.py b/grid2op/Agent/topologyGreedy.py index a6f84239..a37492ba 100644 --- a/grid2op/Agent/topologyGreedy.py +++ b/grid2op/Agent/topologyGreedy.py @@ -6,7 +6,7 @@ # SPDX-License-Identifier: MPL-2.0 # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. -from typing import List +from typing import List, Optional from grid2op.Observation import BaseObservation from grid2op.Action import BaseAction, ActionSpace from grid2op.Agent.greedyAgent import GreedyAgent @@ -27,7 +27,7 @@ class TopologyGreedy(GreedyAgent): def __init__(self, action_space: ActionSpace, simulated_time_step : int =1): GreedyAgent.__init__(self, action_space, simulated_time_step=simulated_time_step) - self.tested_action : List[BaseAction]= None + self.tested_action : Optional[list[BaseAction]] = None def _get_tested_action(self, observation: BaseObservation) -> List[BaseAction]: if self.tested_action is None: From 5ee9aee5e5a9e136ce57fdcdbe271b8feeba0251 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Mon, 2 Dec 2024 11:12:53 +0100 Subject: [PATCH 36/60] Add: Bring back fix to PandaPowerBackend for isolated loads Signed-off-by: Xavier Weiss --- grid2op/Backend/pandaPowerBackend.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index 533017b1..9fabd2d7 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -1116,8 +1116,9 @@ def runpf(self, is_dc : bool=False) -> Tuple[bool, Union[Exception, None]]: self.load_theta[:], ) = self._loads_info() + load_in_service = self._grid.load["in_service"] if not is_dc: - if not np.isfinite(self.load_v).all(): + if not np.isfinite(self.load_v[load_in_service]).all(): # TODO see if there is a better way here # some loads are disconnected: it's a game over case! raise pp.powerflow.LoadflowNotConverged(f"Isolated load: check loads {np.isfinite(self.load_v).nonzero()[0]}") From 9adc3a4d8e40cdce0cd95fd392f0e1972f8ce5e1 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Mon, 2 Dec 2024 15:57:01 +0100 Subject: [PATCH 37/60] Refact: Simplify _check_kirchhoff Signed-off-by: Xavier Weiss --- grid2op/Action/baseAction.py | 46 +++-- grid2op/Backend/backend.py | 366 ++++++++++++++++++++--------------- 2 files changed, 244 insertions(+), 168 deletions(-) diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 2d321638..5cc700f3 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -2171,6 +2171,34 @@ def _reset_vect(self): self._vectorized = None self._subs_impacted = None self._lines_impacted = None + + @staticmethod + def _check_keys_exist(action_cls:GridObjects, act_dict): + """ + Checks whether an action has the same keys in its + action space as are present in the provided dictionary. + + Args: + action_cls (GridObjects): A Grid2Op action + act_dict (str:Any): Dictionary representation of an action + """ + for kk in act_dict.keys(): + if kk not in action_cls.authorized_keys: + if kk == "shunt" and not action_cls.shunts_data_available: + # no warnings are raised in this case because if a warning + # were raised it could crash some environment + # with shunt in "init_state.json" with a backend that does not + # handle shunt + continue + if kk == "set_storage" and action_cls.n_storage == 0: + # no warnings are raised in this case because if a warning + # were raised it could crash some environment + # with storage in "init_state.json" but if the backend did not + # handle storage units + continue + warnings.warn( + f"The key '{kk}' used to update an action will be ignored. Valid keys are {action_cls.authorized_keys}" + ) def update(self, dict_: DICT_ACT_TYPING @@ -2381,23 +2409,7 @@ def update(self, cls = type(self) if dict_ is not None: - for kk in dict_.keys(): - if kk not in cls.authorized_keys: - if kk == "shunt" and not cls.shunts_data_available: - # no warnings are raised in this case because if a warning - # were raised it could crash some environment - # with shunt in "init_state.json" with a backend that does not - # handle shunt - continue - if kk == "set_storage" and cls.n_storage == 0: - # no warnings are raised in this case because if a warning - # were raised it could crash some environment - # with storage in "init_state.json" but if the backend did not - # handle storage units - continue - warn = 'The key "{}" used to update an action will be ignored. Valid keys are {}' - warn = warn.format(kk, cls.authorized_keys) - warnings.warn(warn) + BaseAction._check_keys_exist(cls, dict_) if cls.shunts_data_available: # do not digest shunt when backend does not support it diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index fd71dde5..ed019e64 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -15,7 +15,9 @@ from abc import ABC, abstractmethod import numpy as np import pandas as pd +from dataclasses import dataclass from typing import Tuple, Optional, Any, Dict, Union + try: from typing import Self except ImportError: @@ -37,6 +39,34 @@ from grid2op.Space import GridObjects, DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_DETACHMENT +@dataclass +class KirchhoffInfo: + p_or:np.ndarray + q_or:np.ndarray + v_or:np.ndarray + p_ex:np.ndarray + q_ex:np.ndarray + v_ex:np.ndarray + p_gen:np.ndarray + q_gen:np.ndarray + v_gen:np.ndarray + p_load:np.ndarray + q_load:np.ndarray + v_load:np.ndarray + p_subs:np.ndarray + q_subs:np.ndarray + p_bus:np.ndarray + q_bus:np.ndarray + v_bus:np.ndarray + topo_vect:np.ndarray + p_storage:Optional[np.ndarray] = None + q_storage:Optional[np.ndarray] = None + v_storage:Optional[np.ndarray] = None + p_s:Optional[np.ndarray] = None + q_s:Optional[np.ndarray] = None + v_s:Optional[np.ndarray] = None + bus_s:Optional[np.ndarray] = None + # TODO method to get V and theta at each bus, could be in the same shape as check_kirchoff @@ -1254,264 +1284,298 @@ def check_kirchoff(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray warnings.warn(message="please use backend.check_kirchhoff() instead", category=DeprecationWarning) return self.check_kirchhoff() - def check_kirchhoff(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - """ - INTERNAL - - .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ - - .. versionadded:: 1.11.0 - Fix the typo of the :attr:`Backend.check_kirchoff` function - - Check that the powergrid respects kirchhoff's law. - This function can be called at any moment (after a powerflow has been run) - to make sure a powergrid is in a consistent state, or to perform - some tests for example. - - In order to function properly, this method requires that :func:`Backend.shunt_info` and - :func:`Backend.sub_from_bus_id` are properly defined. Otherwise the results might be wrong, especially - for reactive values (q_subs and q_bus bellow) - - Returns - ------- - p_subs ``numpy.ndarray`` - sum of injected active power at each substations (MW) - q_subs ``numpy.ndarray`` - sum of injected reactive power at each substations (MVAr) - p_bus ``numpy.ndarray`` - sum of injected active power at each buses. It is given in form of a matrix, with number of substations as - row, and number of columns equal to the maximum number of buses for a substation (MW) - q_bus ``numpy.ndarray`` - sum of injected reactive power at each buses. It is given in form of a matrix, with number of substations as - row, and number of columns equal to the maximum number of buses for a substation (MVAr) - diff_v_bus: ``numpy.ndarray`` (2d array) - difference between maximum voltage and minimum voltage (computed for each elements) - at each bus. It is an array of two dimension: - - - first dimension represents the the substation (between 1 and self.n_sub) - - second element represents the busbar in the substation (0 or 1 usually) - - """ - - p_or, q_or, v_or, *_ = self.lines_or_info() - p_ex, q_ex, v_ex, *_ = self.lines_ex_info() - p_gen, q_gen, v_gen = self.generators_info() - p_load, q_load, v_load = self.loads_info() - cls = type(self) - if cls.n_storage > 0: - p_storage, q_storage, v_storage = self.storages_info() - - # fist check the "substation law" : nothing is created at any substation - p_subs = np.zeros(cls.n_sub, dtype=dt_float) - q_subs = np.zeros(cls.n_sub, dtype=dt_float) - - # check for each bus - p_bus = np.zeros((cls.n_sub, cls.n_busbar_per_sub), dtype=dt_float) - q_bus = np.zeros((cls.n_sub, cls.n_busbar_per_sub), dtype=dt_float) - v_bus = ( - np.zeros((cls.n_sub, cls.n_busbar_per_sub, 2), dtype=dt_float) - 1.0 - ) # sub, busbar, [min,max] - topo_vect = self.get_topo_vect() - - # bellow i'm "forced" to do a loop otherwise, numpy do not compute the "+=" the way I want it to. - # for example, if two powerlines are such that line_or_to_subid is equal (eg both connected to substation 0) - # then numpy do not guarantee that `p_subs[self.line_or_to_subid] += p_or` will add the two "corresponding p_or" - # TODO this can be vectorized with matrix product, see example in obs.flow_bus_matrix (BaseObervation.py) + @staticmethod + def _check_kirchhoff_lines(cls, info:KirchhoffInfo): for i in range(cls.n_line): sub_or_id = cls.line_or_to_subid[i] sub_ex_id = cls.line_ex_to_subid[i] - if (topo_vect[cls.line_or_pos_topo_vect[i]] == -1 or - topo_vect[cls.line_ex_pos_topo_vect[i]] == -1): + if (info.topo_vect[cls.line_or_pos_topo_vect[i]] == -1 or + info.topo_vect[cls.line_ex_pos_topo_vect[i]] == -1): # line is disconnected continue - loc_bus_or = topo_vect[cls.line_or_pos_topo_vect[i]] - 1 - loc_bus_ex = topo_vect[cls.line_ex_pos_topo_vect[i]] - 1 + loc_bus_or = info.topo_vect[cls.line_or_pos_topo_vect[i]] - 1 + loc_bus_ex = info.topo_vect[cls.line_ex_pos_topo_vect[i]] - 1 # for substations - p_subs[sub_or_id] += p_or[i] - p_subs[sub_ex_id] += p_ex[i] + info.p_subs[sub_or_id] += info.p_or[i] + info.p_subs[sub_ex_id] += info.p_ex[i] - q_subs[sub_or_id] += q_or[i] - q_subs[sub_ex_id] += q_ex[i] + info.q_subs[sub_or_id] += info.q_or[i] + info.q_subs[sub_ex_id] += info.q_ex[i] # for bus - p_bus[sub_or_id, loc_bus_or] += p_or[i] - q_bus[sub_or_id, loc_bus_or] += q_or[i] + info.p_bus[sub_or_id, loc_bus_or] += info.p_or[i] + info.q_bus[sub_or_id, loc_bus_or] += info.q_or[i] - p_bus[ sub_ex_id, loc_bus_ex] += p_ex[i] - q_bus[sub_ex_id, loc_bus_ex] += q_ex[i] + info.p_bus[ sub_ex_id, loc_bus_ex] += info.p_ex[i] + info.q_bus[sub_ex_id, loc_bus_ex] += info.q_ex[i] # fill the min / max voltage per bus (initialization) - if (v_bus[sub_or_id, loc_bus_or,][0] == -1): - v_bus[sub_or_id, loc_bus_or,][0] = v_or[i] - if (v_bus[sub_ex_id, loc_bus_ex,][0] == -1): - v_bus[sub_ex_id, loc_bus_ex,][0] = v_ex[i] - if (v_bus[sub_or_id, loc_bus_or,][1]== -1): - v_bus[sub_or_id, loc_bus_or,][1] = v_or[i] - if (v_bus[sub_ex_id, loc_bus_ex,][1]== -1): - v_bus[sub_ex_id, loc_bus_ex,][1] = v_ex[i] + if (info.v_bus[sub_or_id, loc_bus_or,][0] == -1): + info.v_bus[sub_or_id, loc_bus_or,][0] = info.v_or[i] + if (info.v_bus[sub_ex_id, loc_bus_ex,][0] == -1): + info.v_bus[sub_ex_id, loc_bus_ex,][0] = info.v_ex[i] + if (info.v_bus[sub_or_id, loc_bus_or,][1]== -1): + info.v_bus[sub_or_id, loc_bus_or,][1] = info.v_or[i] + if (info.v_bus[sub_ex_id, loc_bus_ex,][1]== -1): + info.v_bus[sub_ex_id, loc_bus_ex,][1] = info.v_ex[i] # now compute the correct stuff - if v_or[i] > 0.0: + if info.v_or[i] > 0.0: # line is connected - v_bus[sub_or_id, loc_bus_or,][0] = min(v_bus[sub_or_id, loc_bus_or,][0],v_or[i],) - v_bus[sub_or_id, loc_bus_or,][1] = max(v_bus[sub_or_id, loc_bus_or,][1],v_or[i],) + info.v_bus[sub_or_id, loc_bus_or,][0] = min(info.v_bus[sub_or_id, loc_bus_or,][0],info.v_or[i],) + info.v_bus[sub_or_id, loc_bus_or,][1] = max(info.v_bus[sub_or_id, loc_bus_or,][1],info.v_or[i],) - if v_ex[i] > 0: + if info.v_ex[i] > 0: # line is connected - v_bus[sub_ex_id, loc_bus_ex,][0] = min(v_bus[sub_ex_id, loc_bus_ex,][0],v_ex[i],) - v_bus[sub_ex_id, loc_bus_ex,][1] = max(v_bus[sub_ex_id, loc_bus_ex,][1],v_ex[i],) - + info.v_bus[sub_ex_id, loc_bus_ex,][0] = min(info.v_bus[sub_ex_id, loc_bus_ex,][0],info.v_ex[i],) + info.v_bus[sub_ex_id, loc_bus_ex,][1] = max(info.v_bus[sub_ex_id, loc_bus_ex,][1],info.v_ex[i],) + return info + + @staticmethod + def _check_kirchhoff_gens(cls, info:KirchhoffInfo): for i in range(cls.n_gen): gptv = cls.gen_pos_topo_vect[i] - if topo_vect[gptv] == -1: + if info.topo_vect[gptv] == -1: # gen is disconnected continue # for substations - p_subs[cls.gen_to_subid[i]] -= p_gen[i] - q_subs[cls.gen_to_subid[i]] -= q_gen[i] + info.p_subs[cls.gen_to_subid[i]] -= info.p_gen[i] + info.q_subs[cls.gen_to_subid[i]] -= info.q_gen[i] - loc_bus = topo_vect[gptv] - 1 + loc_bus = info.topo_vect[gptv] - 1 # for bus - p_bus[ + info.p_bus[ cls.gen_to_subid[i], loc_bus - ] -= p_gen[i] - q_bus[ + ] -= info.p_gen[i] + info.q_bus[ cls.gen_to_subid[i], loc_bus - ] -= q_gen[i] + ] -= info.q_gen[i] # compute max and min values - if v_gen[i]: + if info.v_gen[i]: # but only if gen is connected - v_bus[cls.gen_to_subid[i], loc_bus][ + info.v_bus[cls.gen_to_subid[i], loc_bus][ 0 ] = min( - v_bus[ + info.v_bus[ cls.gen_to_subid[i], loc_bus ][0], - v_gen[i], + info.v_gen[i], ) - v_bus[cls.gen_to_subid[i], loc_bus][ + info.v_bus[cls.gen_to_subid[i], loc_bus][ 1 ] = max( - v_bus[ + info.v_bus[ cls.gen_to_subid[i], loc_bus ][1], - v_gen[i], + info.v_gen[i], ) - + return info + + def _check_kirchhoff_loads(cls, info:KirchhoffInfo): for i in range(cls.n_load): gptv = cls.load_pos_topo_vect[i] - if topo_vect[gptv] == -1: + if info.topo_vect[gptv] == -1: # load is disconnected continue - loc_bus = topo_vect[gptv] - 1 + loc_bus = info.topo_vect[gptv] - 1 # for substations - p_subs[cls.load_to_subid[i]] += p_load[i] - q_subs[cls.load_to_subid[i]] += q_load[i] + info.p_subs[cls.load_to_subid[i]] += info.p_load[i] + info.q_subs[cls.load_to_subid[i]] += info.q_load[i] # for buses - p_bus[ + info.p_bus[ cls.load_to_subid[i], loc_bus - ] += p_load[i] - q_bus[ + ] += info.p_load[i] + info.q_bus[ cls.load_to_subid[i], loc_bus - ] += q_load[i] + ] += info.q_load[i] # compute max and min values - if v_load[i]: + if info.v_load[i]: # but only if load is connected - v_bus[cls.load_to_subid[i], loc_bus][ + info.v_bus[cls.load_to_subid[i], loc_bus][ 0 ] = min( - v_bus[ + info.v_bus[ cls.load_to_subid[i], loc_bus ][0], - v_load[i], + info.v_load[i], ) - v_bus[cls.load_to_subid[i], loc_bus][ + info.v_bus[cls.load_to_subid[i], loc_bus][ 1 ] = max( - v_bus[ + info.v_bus[ cls.load_to_subid[i], loc_bus ][1], - v_load[i], + info.v_load[i], ) - + return info + + def _check_kirchhoff_storage(cls, info:KirchhoffInfo): for i in range(cls.n_storage): gptv = cls.storage_pos_topo_vect[i] - if topo_vect[gptv] == -1: + if info.topo_vect[gptv] == -1: # storage is disconnected continue - loc_bus = topo_vect[gptv] - 1 + loc_bus = info.topo_vect[gptv] - 1 - p_subs[cls.storage_to_subid[i]] += p_storage[i] - q_subs[cls.storage_to_subid[i]] += q_storage[i] - p_bus[ + info.p_subs[cls.storage_to_subid[i]] += info.p_storage[i] + info.q_subs[cls.storage_to_subid[i]] += info.q_storage[i] + info.p_bus[ cls.storage_to_subid[i], loc_bus - ] += p_storage[i] - q_bus[ + ] += info.p_storage[i] + info.q_bus[ cls.storage_to_subid[i], loc_bus - ] += q_storage[i] + ] += info.q_storage[i] # compute max and min values - if v_storage[i] > 0: + if info.v_storage[i] > 0: # the storage unit is connected - v_bus[ + info.v_bus[ cls.storage_to_subid[i], loc_bus, ][0] = min( - v_bus[ + info.v_bus[ cls.storage_to_subid[i], loc_bus, ][0], - v_storage[i], + info.v_storage[i], ) - v_bus[ - self.storage_to_subid[i], + info.v_bus[ + cls.storage_to_subid[i], loc_bus, ][1] = max( - v_bus[ + info.v_bus[ cls.storage_to_subid[i], loc_bus, ][1], - v_storage[i], + info.v_storage[i], ) - + return info + + def _check_kirchhoff_shunt(cls, info:KirchhoffInfo): if cls.shunts_data_available: - p_s, q_s, v_s, bus_s = self.shunt_info() for i in range(cls.n_shunt): - if bus_s[i] == -1: + if info.bus_s[i] == -1: # shunt is disconnected continue # for substations - p_subs[cls.shunt_to_subid[i]] += p_s[i] - q_subs[cls.shunt_to_subid[i]] += q_s[i] + info.p_subs[cls.shunt_to_subid[i]] += info.p_s[i] + info.q_subs[cls.shunt_to_subid[i]] += info.q_s[i] # for buses - p_bus[cls.shunt_to_subid[i], bus_s[i] - 1] += p_s[i] - q_bus[cls.shunt_to_subid[i], bus_s[i] - 1] += q_s[i] + info.p_bus[cls.shunt_to_subid[i], info.bus_s[i] - 1] += info.p_s[i] + info.q_bus[cls.shunt_to_subid[i], info.bus_s[i] - 1] += info.q_s[i] # compute max and min values - v_bus[cls.shunt_to_subid[i], bus_s[i] - 1][0] = min( - v_bus[cls.shunt_to_subid[i], bus_s[i] - 1][0], v_s[i] + info.v_bus[cls.shunt_to_subid[i], info.bus_s[i] - 1][0] = min( + info.v_bus[cls.shunt_to_subid[i], info.bus_s[i] - 1][0], info.v_s[i] ) - v_bus[cls.shunt_to_subid[i], bus_s[i] - 1][1] = max( - v_bus[cls.shunt_to_subid[i], bus_s[i] - 1][1], v_s[i] + info.v_bus[cls.shunt_to_subid[i], info.bus_s[i] - 1][1] = max( + info.v_bus[cls.shunt_to_subid[i], info.bus_s[i] - 1][1], info.v_s[i] ) else: warnings.warn( "Backend.check_kirchhoff Impossible to get shunt information. Reactive information might be " "incorrect." ) + return info + + def check_kirchhoff(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + """ + INTERNAL + + .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ + + .. versionadded:: 1.11.0 + Fix the typo of the :attr:`Backend.check_kirchoff` function + + Check that the powergrid respects kirchhoff's law. + This function can be called at any moment (after a powerflow has been run) + to make sure a powergrid is in a consistent state, or to perform + some tests for example. + + In order to function properly, this method requires that :func:`Backend.shunt_info` and + :func:`Backend.sub_from_bus_id` are properly defined. Otherwise the results might be wrong, especially + for reactive values (q_subs and q_bus bellow) + + Returns + ------- + p_subs ``numpy.ndarray`` + sum of injected active power at each substations (MW) + q_subs ``numpy.ndarray`` + sum of injected reactive power at each substations (MVAr) + p_bus ``numpy.ndarray`` + sum of injected active power at each buses. It is given in form of a matrix, with number of substations as + row, and number of columns equal to the maximum number of buses for a substation (MW) + q_bus ``numpy.ndarray`` + sum of injected reactive power at each buses. It is given in form of a matrix, with number of substations as + row, and number of columns equal to the maximum number of buses for a substation (MVAr) + diff_v_bus: ``numpy.ndarray`` (2d array) + difference between maximum voltage and minimum voltage (computed for each elements) + at each bus. It is an array of two dimension: + + - first dimension represents the the substation (between 1 and self.n_sub) + - second element represents the busbar in the substation (0 or 1 usually) + + """ + cls = type(self) + p_or, q_or, v_or, *_ = self.lines_or_info() + p_ex, q_ex, v_ex, *_ = self.lines_ex_info() + p_gen, q_gen, v_gen = self.generators_info() + p_load, q_load, v_load = self.loads_info() + + if cls.n_storage > 0: + p_storage, q_storage, v_storage = self.storages_info() + else: + p_storage, q_storage, v_storage = (None, None, None) + + if cls.shunts_data_available: + p_s, q_s, v_s, bus_s = self.shunt_info() + else: + p_s, q_s, v_s, bus_s = (None, None, None, None) + + # fist check the "substation law" : nothing is created at any substation + p_subs = np.zeros(cls.n_sub, dtype=dt_float) + q_subs = np.zeros(cls.n_sub, dtype=dt_float) + + # check for each bus + p_bus = np.zeros((cls.n_sub, cls.n_busbar_per_sub), dtype=dt_float) + q_bus = np.zeros((cls.n_sub, cls.n_busbar_per_sub), dtype=dt_float) + v_bus = ( + np.zeros((cls.n_sub, cls.n_busbar_per_sub, 2), dtype=dt_float) - 1.0 + ) # sub, busbar, [min,max] + topo_vect = self.get_topo_vect() + info = KirchhoffInfo(p_or, q_or, v_or, + p_ex, q_ex, v_ex, + p_gen, q_gen, v_gen, + p_load, q_load, v_load, + p_subs, q_subs, + p_bus, q_bus, v_bus, + topo_vect, + p_storage, q_storage, v_storage, + p_s, q_s, v_s, bus_s) + + # bellow i'm "forced" to do a loop otherwise, numpy do not compute the "+=" the way I want it to. + # for example, if two powerlines are such that line_or_to_subid is equal (eg both connected to substation 0) + # then numpy do not guarantee that `p_subs[self.line_or_to_subid] += p_or` will add the two "corresponding p_or" + # TODO this can be vectorized with matrix product, see example in obs.flow_bus_matrix (BaseObervation.py) + info = Backend._check_kirchhoff_lines(info) + info = Backend._check_kirchhoff_gens(info) + info = Backend._check_kirchhoff_loads(info) + info = Backend._check_kirchhoff_storage(info) + if cls.shunts_data_available: + info = Backend._check_kirchhoff_shunt(info) diff_v_bus = np.zeros((cls.n_sub, cls.n_busbar_per_sub), dtype=dt_float) - diff_v_bus[:, :] = v_bus[:, :, 1] - v_bus[:, :, 0] - return p_subs, q_subs, p_bus, q_bus, diff_v_bus + diff_v_bus[:, :] = info.v_bus[:, :, 1] - info.v_bus[:, :, 0] + return info.p_subs, info.q_subs, info.p_bus, info.q_bus, diff_v_bus def _fill_names_obj(self): """fill the name vectors (**eg** name_line) if not done already in the backend. From 42535de5bcd4cf1112cfabd2abd6874c22cd875e Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Mon, 2 Dec 2024 16:27:25 +0100 Subject: [PATCH 38/60] Debug: Fix missing param in kirchhoff static methods Signed-off-by: Xavier Weiss --- grid2op/Backend/backend.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index ed019e64..ba8eb402 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -1567,10 +1567,10 @@ def check_kirchhoff(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarra # for example, if two powerlines are such that line_or_to_subid is equal (eg both connected to substation 0) # then numpy do not guarantee that `p_subs[self.line_or_to_subid] += p_or` will add the two "corresponding p_or" # TODO this can be vectorized with matrix product, see example in obs.flow_bus_matrix (BaseObervation.py) - info = Backend._check_kirchhoff_lines(info) - info = Backend._check_kirchhoff_gens(info) - info = Backend._check_kirchhoff_loads(info) - info = Backend._check_kirchhoff_storage(info) + info = Backend._check_kirchhoff_lines(cls, info) + info = Backend._check_kirchhoff_gens(cls, info) + info = Backend._check_kirchhoff_loads(cls, info) + info = Backend._check_kirchhoff_storage(cls, info) if cls.shunts_data_available: info = Backend._check_kirchhoff_shunt(info) diff_v_bus = np.zeros((cls.n_sub, cls.n_busbar_per_sub), dtype=dt_float) From 9a6b696a8b1e7996d41c2cf2a2dc36e9592694c9 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Mon, 2 Dec 2024 16:49:13 +0100 Subject: [PATCH 39/60] Refact: Emulate obs._check_kirchhoff in Backend._check_kirchhoff Signed-off-by: Xavier Weiss --- grid2op/Backend/backend.py | 338 ++++++++++--------------------------- 1 file changed, 89 insertions(+), 249 deletions(-) diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index ba8eb402..48fccb93 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -15,7 +15,6 @@ from abc import ABC, abstractmethod import numpy as np import pandas as pd -from dataclasses import dataclass from typing import Tuple, Optional, Any, Dict, Union try: @@ -39,37 +38,7 @@ from grid2op.Space import GridObjects, DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_DETACHMENT -@dataclass -class KirchhoffInfo: - p_or:np.ndarray - q_or:np.ndarray - v_or:np.ndarray - p_ex:np.ndarray - q_ex:np.ndarray - v_ex:np.ndarray - p_gen:np.ndarray - q_gen:np.ndarray - v_gen:np.ndarray - p_load:np.ndarray - q_load:np.ndarray - v_load:np.ndarray - p_subs:np.ndarray - q_subs:np.ndarray - p_bus:np.ndarray - q_bus:np.ndarray - v_bus:np.ndarray - topo_vect:np.ndarray - p_storage:Optional[np.ndarray] = None - q_storage:Optional[np.ndarray] = None - v_storage:Optional[np.ndarray] = None - p_s:Optional[np.ndarray] = None - q_s:Optional[np.ndarray] = None - v_s:Optional[np.ndarray] = None - bus_s:Optional[np.ndarray] = None - # TODO method to get V and theta at each bus, could be in the same shape as check_kirchoff - - class Backend(GridObjects, ABC): """ INTERNAL @@ -1284,210 +1253,53 @@ def check_kirchoff(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray warnings.warn(message="please use backend.check_kirchhoff() instead", category=DeprecationWarning) return self.check_kirchhoff() - @staticmethod - def _check_kirchhoff_lines(cls, info:KirchhoffInfo): - for i in range(cls.n_line): - sub_or_id = cls.line_or_to_subid[i] - sub_ex_id = cls.line_ex_to_subid[i] - if (info.topo_vect[cls.line_or_pos_topo_vect[i]] == -1 or - info.topo_vect[cls.line_ex_pos_topo_vect[i]] == -1): - # line is disconnected + def _aux_check_kirchhoff(self, + n_elt:int, # cst eg. cls.n_gen + el_to_subid:np.ndarray, # cst eg. cls.gen_to_subid + el_bus:np.ndarray, # cst eg. gen_bus + el_p:np.ndarray, # cst, eg. gen_p + el_q:np.ndarray, # cst, eg. gen_q + el_v:np.ndarray, # cst, eg. gen_v + p_subs:np.ndarray, q_subs:np.ndarray, + p_bus:np.ndarray, q_bus:np.ndarray, + v_bus:np.ndarray, + # whether the object is load convention (True) or gen convention (False) + load_conv:np.ndarray=True): + for i in range(n_elt): + psubid = el_to_subid[i] + if el_bus[i] == -1: + # el is disconnected continue - loc_bus_or = info.topo_vect[cls.line_or_pos_topo_vect[i]] - 1 - loc_bus_ex = info.topo_vect[cls.line_ex_pos_topo_vect[i]] - 1 # for substations - info.p_subs[sub_or_id] += info.p_or[i] - info.p_subs[sub_ex_id] += info.p_ex[i] - - info.q_subs[sub_or_id] += info.q_or[i] - info.q_subs[sub_ex_id] += info.q_ex[i] - - # for bus - info.p_bus[sub_or_id, loc_bus_or] += info.p_or[i] - info.q_bus[sub_or_id, loc_bus_or] += info.q_or[i] - - info.p_bus[ sub_ex_id, loc_bus_ex] += info.p_ex[i] - info.q_bus[sub_ex_id, loc_bus_ex] += info.q_ex[i] - - # fill the min / max voltage per bus (initialization) - if (info.v_bus[sub_or_id, loc_bus_or,][0] == -1): - info.v_bus[sub_or_id, loc_bus_or,][0] = info.v_or[i] - if (info.v_bus[sub_ex_id, loc_bus_ex,][0] == -1): - info.v_bus[sub_ex_id, loc_bus_ex,][0] = info.v_ex[i] - if (info.v_bus[sub_or_id, loc_bus_or,][1]== -1): - info.v_bus[sub_or_id, loc_bus_or,][1] = info.v_or[i] - if (info.v_bus[sub_ex_id, loc_bus_ex,][1]== -1): - info.v_bus[sub_ex_id, loc_bus_ex,][1] = info.v_ex[i] - - # now compute the correct stuff - if info.v_or[i] > 0.0: - # line is connected - info.v_bus[sub_or_id, loc_bus_or,][0] = min(info.v_bus[sub_or_id, loc_bus_or,][0],info.v_or[i],) - info.v_bus[sub_or_id, loc_bus_or,][1] = max(info.v_bus[sub_or_id, loc_bus_or,][1],info.v_or[i],) - - if info.v_ex[i] > 0: - # line is connected - info.v_bus[sub_ex_id, loc_bus_ex,][0] = min(info.v_bus[sub_ex_id, loc_bus_ex,][0],info.v_ex[i],) - info.v_bus[sub_ex_id, loc_bus_ex,][1] = max(info.v_bus[sub_ex_id, loc_bus_ex,][1],info.v_ex[i],) - return info - - @staticmethod - def _check_kirchhoff_gens(cls, info:KirchhoffInfo): - for i in range(cls.n_gen): - gptv = cls.gen_pos_topo_vect[i] - - if info.topo_vect[gptv] == -1: - # gen is disconnected - continue - - # for substations - info.p_subs[cls.gen_to_subid[i]] -= info.p_gen[i] - info.q_subs[cls.gen_to_subid[i]] -= info.q_gen[i] + if load_conv: + p_subs[psubid] += el_p[i] + q_subs[psubid] += el_q[i] + else: + p_subs[psubid] -= el_p[i] + q_subs[psubid] -= el_q[i] - loc_bus = info.topo_vect[gptv] - 1 # for bus - info.p_bus[ - cls.gen_to_subid[i], loc_bus - ] -= info.p_gen[i] - info.q_bus[ - cls.gen_to_subid[i], loc_bus - ] -= info.q_gen[i] + loc_bus = el_bus[i] - 1 + if load_conv: + p_bus[psubid, loc_bus] += el_p[i] + q_bus[psubid, loc_bus] += el_q[i] + else: + p_bus[psubid, loc_bus] -= el_p[i] + q_bus[psubid, loc_bus] -= el_q[i] # compute max and min values - if info.v_gen[i]: + if el_v is not None and el_v[i]: # but only if gen is connected - info.v_bus[cls.gen_to_subid[i], loc_bus][ - 0 - ] = min( - info.v_bus[ - cls.gen_to_subid[i], loc_bus - ][0], - info.v_gen[i], - ) - info.v_bus[cls.gen_to_subid[i], loc_bus][ - 1 - ] = max( - info.v_bus[ - cls.gen_to_subid[i], loc_bus - ][1], - info.v_gen[i], - ) - return info - - def _check_kirchhoff_loads(cls, info:KirchhoffInfo): - for i in range(cls.n_load): - gptv = cls.load_pos_topo_vect[i] - - if info.topo_vect[gptv] == -1: - # load is disconnected - continue - loc_bus = info.topo_vect[gptv] - 1 - - # for substations - info.p_subs[cls.load_to_subid[i]] += info.p_load[i] - info.q_subs[cls.load_to_subid[i]] += info.q_load[i] - - # for buses - info.p_bus[ - cls.load_to_subid[i], loc_bus - ] += info.p_load[i] - info.q_bus[ - cls.load_to_subid[i], loc_bus - ] += info.q_load[i] - - # compute max and min values - if info.v_load[i]: - # but only if load is connected - info.v_bus[cls.load_to_subid[i], loc_bus][ - 0 - ] = min( - info.v_bus[ - cls.load_to_subid[i], loc_bus - ][0], - info.v_load[i], - ) - info.v_bus[cls.load_to_subid[i], loc_bus][ - 1 - ] = max( - info.v_bus[ - cls.load_to_subid[i], loc_bus - ][1], - info.v_load[i], + v_bus[psubid, loc_bus][0] = min( + v_bus[psubid, loc_bus][0], + el_v[i], ) - return info - - def _check_kirchhoff_storage(cls, info:KirchhoffInfo): - for i in range(cls.n_storage): - gptv = cls.storage_pos_topo_vect[i] - if info.topo_vect[gptv] == -1: - # storage is disconnected - continue - loc_bus = info.topo_vect[gptv] - 1 - - info.p_subs[cls.storage_to_subid[i]] += info.p_storage[i] - info.q_subs[cls.storage_to_subid[i]] += info.q_storage[i] - info.p_bus[ - cls.storage_to_subid[i], loc_bus - ] += info.p_storage[i] - info.q_bus[ - cls.storage_to_subid[i], loc_bus - ] += info.q_storage[i] - - # compute max and min values - if info.v_storage[i] > 0: - # the storage unit is connected - info.v_bus[ - cls.storage_to_subid[i], - loc_bus, - ][0] = min( - info.v_bus[ - cls.storage_to_subid[i], - loc_bus, - ][0], - info.v_storage[i], - ) - info.v_bus[ - cls.storage_to_subid[i], - loc_bus, - ][1] = max( - info.v_bus[ - cls.storage_to_subid[i], - loc_bus, - ][1], - info.v_storage[i], + v_bus[psubid, loc_bus][1] = max( + v_bus[psubid, loc_bus][1], + el_v[i], ) - return info - - def _check_kirchhoff_shunt(cls, info:KirchhoffInfo): - if cls.shunts_data_available: - for i in range(cls.n_shunt): - if info.bus_s[i] == -1: - # shunt is disconnected - continue - - # for substations - info.p_subs[cls.shunt_to_subid[i]] += info.p_s[i] - info.q_subs[cls.shunt_to_subid[i]] += info.q_s[i] - - # for buses - info.p_bus[cls.shunt_to_subid[i], info.bus_s[i] - 1] += info.p_s[i] - info.q_bus[cls.shunt_to_subid[i], info.bus_s[i] - 1] += info.q_s[i] - # compute max and min values - info.v_bus[cls.shunt_to_subid[i], info.bus_s[i] - 1][0] = min( - info.v_bus[cls.shunt_to_subid[i], info.bus_s[i] - 1][0], info.v_s[i] - ) - info.v_bus[cls.shunt_to_subid[i], info.bus_s[i] - 1][1] = max( - info.v_bus[cls.shunt_to_subid[i], info.bus_s[i] - 1][1], info.v_s[i] - ) - else: - warnings.warn( - "Backend.check_kirchhoff Impossible to get shunt information. Reactive information might be " - "incorrect." - ) - return info - def check_kirchhoff(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: """ INTERNAL @@ -1534,13 +1346,9 @@ def check_kirchhoff(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarra if cls.n_storage > 0: p_storage, q_storage, v_storage = self.storages_info() - else: - p_storage, q_storage, v_storage = (None, None, None) if cls.shunts_data_available: p_s, q_s, v_s, bus_s = self.shunt_info() - else: - p_s, q_s, v_s, bus_s = (None, None, None, None) # fist check the "substation law" : nothing is created at any substation p_subs = np.zeros(cls.n_sub, dtype=dt_float) @@ -1553,29 +1361,61 @@ def check_kirchhoff(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarra np.zeros((cls.n_sub, cls.n_busbar_per_sub, 2), dtype=dt_float) - 1.0 ) # sub, busbar, [min,max] topo_vect = self.get_topo_vect() - info = KirchhoffInfo(p_or, q_or, v_or, - p_ex, q_ex, v_ex, - p_gen, q_gen, v_gen, - p_load, q_load, v_load, - p_subs, q_subs, - p_bus, q_bus, v_bus, - topo_vect, - p_storage, q_storage, v_storage, - p_s, q_s, v_s, bus_s) - - # bellow i'm "forced" to do a loop otherwise, numpy do not compute the "+=" the way I want it to. - # for example, if two powerlines are such that line_or_to_subid is equal (eg both connected to substation 0) - # then numpy do not guarantee that `p_subs[self.line_or_to_subid] += p_or` will add the two "corresponding p_or" - # TODO this can be vectorized with matrix product, see example in obs.flow_bus_matrix (BaseObervation.py) - info = Backend._check_kirchhoff_lines(cls, info) - info = Backend._check_kirchhoff_gens(cls, info) - info = Backend._check_kirchhoff_loads(cls, info) - info = Backend._check_kirchhoff_storage(cls, info) + + self._aux_check_kirchhoff( + cls.n_line, + cls.line_or_to_subid, + topo_vect[cls.line_or_pos_topo_vect], + p_or, q_or, v_or, + p_subs, q_subs, + p_bus, q_bus, v_bus) + self._aux_check_kirchhoff( + cls.n_line, + cls.line_ex_to_subid, + topo_vect[cls.line_ex_pos_topo_vect], + p_ex, q_ex, v_ex, + p_subs, q_subs, + p_bus, q_bus, v_bus) + self._aux_check_kirchhoff( + cls.n_load, + cls.load_to_subid, + topo_vect[cls.load_pos_topo_vect], + p_load, q_load, v_load, + p_subs, q_subs, + p_bus, q_bus, v_bus) + self._aux_check_kirchhoff( + cls.n_gen, + cls.gen_to_subid, + topo_vect[cls.gen_pos_topo_vect], + p_gen, q_gen, v_gen, + p_subs, q_subs, + p_bus, q_bus, v_bus, + load_conv=False) + if cls.n_storage: + self._aux_check_kirchhoff( + cls.n_storage, + cls.storage_to_subid, + topo_vect[cls.storage_pos_topo_vect], + p_storage, q_storage, v_storage, + p_subs, q_subs, + p_bus, q_bus, v_bus) + if cls.shunts_data_available: - info = Backend._check_kirchhoff_shunt(info) + self._aux_check_kirchhoff( + cls.n_shunt, + cls.shunt_to_subid, + bus_s, + p_s, q_s, v_s, + p_subs, q_subs, + p_bus, q_bus, v_bus) + else: + warnings.warn( + "Observation.check_kirchhoff Impossible to get shunt information. Reactive information might be " + "incorrect." + ) diff_v_bus = np.zeros((cls.n_sub, cls.n_busbar_per_sub), dtype=dt_float) - diff_v_bus[:, :] = info.v_bus[:, :, 1] - info.v_bus[:, :, 0] - return info.p_subs, info.q_subs, info.p_bus, info.q_bus, diff_v_bus + diff_v_bus[:, :] = v_bus[:, :, 1] - v_bus[:, :, 0] + return p_subs, q_subs, p_bus, q_bus, diff_v_bus def _fill_names_obj(self): """fill the name vectors (**eg** name_line) if not done already in the backend. From 5851026ce1ab188fb9a71c0beb149d74bb8170a8 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Wed, 4 Dec 2024 09:28:12 +0100 Subject: [PATCH 40/60] refactoring check_kirchhoff methods of Observation and Backend, should fix some issues Signed-off-by: DONNOT Benjamin --- grid2op/Backend/backend.py | 142 ++++------------- grid2op/Observation/baseObservation.py | 145 +++-------------- grid2op/Space/GridObjects.py | 213 ++++++++++++++++++++++++- grid2op/Space/__init__.py | 2 + grid2op/Space/space_utils.py | 35 ++++ 5 files changed, 309 insertions(+), 228 deletions(-) diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index 48fccb93..2dc61d7b 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -35,7 +35,7 @@ DivergingPowerflow, Grid2OpException, ) -from grid2op.Space import GridObjects, DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_DETACHMENT +from grid2op.Space import GridObjects, ElTypeInfo, DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_DETACHMENT # TODO method to get V and theta at each bus, could be in the same shape as check_kirchoff @@ -1252,53 +1252,6 @@ def check_kirchoff(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray """ warnings.warn(message="please use backend.check_kirchhoff() instead", category=DeprecationWarning) return self.check_kirchhoff() - - def _aux_check_kirchhoff(self, - n_elt:int, # cst eg. cls.n_gen - el_to_subid:np.ndarray, # cst eg. cls.gen_to_subid - el_bus:np.ndarray, # cst eg. gen_bus - el_p:np.ndarray, # cst, eg. gen_p - el_q:np.ndarray, # cst, eg. gen_q - el_v:np.ndarray, # cst, eg. gen_v - p_subs:np.ndarray, q_subs:np.ndarray, - p_bus:np.ndarray, q_bus:np.ndarray, - v_bus:np.ndarray, - # whether the object is load convention (True) or gen convention (False) - load_conv:np.ndarray=True): - for i in range(n_elt): - psubid = el_to_subid[i] - if el_bus[i] == -1: - # el is disconnected - continue - - # for substations - if load_conv: - p_subs[psubid] += el_p[i] - q_subs[psubid] += el_q[i] - else: - p_subs[psubid] -= el_p[i] - q_subs[psubid] -= el_q[i] - - # for bus - loc_bus = el_bus[i] - 1 - if load_conv: - p_bus[psubid, loc_bus] += el_p[i] - q_bus[psubid, loc_bus] += el_q[i] - else: - p_bus[psubid, loc_bus] -= el_p[i] - q_bus[psubid, loc_bus] -= el_q[i] - - # compute max and min values - if el_v is not None and el_v[i]: - # but only if gen is connected - v_bus[psubid, loc_bus][0] = min( - v_bus[psubid, loc_bus][0], - el_v[i], - ) - v_bus[psubid, loc_bus][1] = max( - v_bus[psubid, loc_bus][1], - el_v[i], - ) def check_kirchhoff(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: """ @@ -1343,78 +1296,53 @@ def check_kirchhoff(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarra p_ex, q_ex, v_ex, *_ = self.lines_ex_info() p_gen, q_gen, v_gen = self.generators_info() p_load, q_load, v_load = self.loads_info() - - if cls.n_storage > 0: - p_storage, q_storage, v_storage = self.storages_info() - - if cls.shunts_data_available: - p_s, q_s, v_s, bus_s = self.shunt_info() - - # fist check the "substation law" : nothing is created at any substation - p_subs = np.zeros(cls.n_sub, dtype=dt_float) - q_subs = np.zeros(cls.n_sub, dtype=dt_float) - - # check for each bus - p_bus = np.zeros((cls.n_sub, cls.n_busbar_per_sub), dtype=dt_float) - q_bus = np.zeros((cls.n_sub, cls.n_busbar_per_sub), dtype=dt_float) - v_bus = ( - np.zeros((cls.n_sub, cls.n_busbar_per_sub, 2), dtype=dt_float) - 1.0 - ) # sub, busbar, [min,max] topo_vect = self.get_topo_vect() - - self._aux_check_kirchhoff( - cls.n_line, - cls.line_or_to_subid, + lineor_info = ElTypeInfo( topo_vect[cls.line_or_pos_topo_vect], - p_or, q_or, v_or, - p_subs, q_subs, - p_bus, q_bus, v_bus) - self._aux_check_kirchhoff( - cls.n_line, - cls.line_ex_to_subid, + p_or, + q_or, + v_or, + ) + lineex_info = ElTypeInfo( topo_vect[cls.line_ex_pos_topo_vect], - p_ex, q_ex, v_ex, - p_subs, q_subs, - p_bus, q_bus, v_bus) - self._aux_check_kirchhoff( - cls.n_load, - cls.load_to_subid, + p_ex, + q_ex, + v_ex, + ) + load_info = ElTypeInfo( topo_vect[cls.load_pos_topo_vect], - p_load, q_load, v_load, - p_subs, q_subs, - p_bus, q_bus, v_bus) - self._aux_check_kirchhoff( - cls.n_gen, - cls.gen_to_subid, + p_load, + q_load, + v_load, + ) + gen_info = ElTypeInfo( topo_vect[cls.gen_pos_topo_vect], p_gen, q_gen, v_gen, - p_subs, q_subs, - p_bus, q_bus, v_bus, - load_conv=False) - if cls.n_storage: - self._aux_check_kirchhoff( - cls.n_storage, - cls.storage_to_subid, + ) + if cls.n_storage > 0: + p_storage, q_storage, v_storage = self.storages_info() + storage_info = ElTypeInfo( topo_vect[cls.storage_pos_topo_vect], p_storage, q_storage, v_storage, - p_subs, q_subs, - p_bus, q_bus, v_bus) + ) + else: + storage_info = None if cls.shunts_data_available: - self._aux_check_kirchhoff( - cls.n_shunt, - cls.shunt_to_subid, + p_s, q_s, v_s, bus_s = self.shunt_info() + shunt_info = ElTypeInfo( bus_s, p_s, q_s, v_s, - p_subs, q_subs, - p_bus, q_bus, v_bus) - else: - warnings.warn( - "Observation.check_kirchhoff Impossible to get shunt information. Reactive information might be " - "incorrect." ) - diff_v_bus = np.zeros((cls.n_sub, cls.n_busbar_per_sub), dtype=dt_float) - diff_v_bus[:, :] = v_bus[:, :, 1] - v_bus[:, :, 0] + else: + shunt_info = None + + p_subs, q_subs, p_bus, q_bus, diff_v_bus = cls._aux_check_kirchhoff(lineor_info, + lineex_info, + load_info, + gen_info, + storage_info, + shunt_info) return p_subs, q_subs, p_bus, q_bus, diff_v_bus def _fill_names_obj(self): diff --git a/grid2op/Observation/baseObservation.py b/grid2op/Observation/baseObservation.py index 72370ec5..28b1f257 100644 --- a/grid2op/Observation/baseObservation.py +++ b/grid2op/Observation/baseObservation.py @@ -30,7 +30,7 @@ NoForecastAvailable, BaseObservationError, ) -from grid2op.Space import GridObjects +from grid2op.Space import GridObjects, ElTypeInfo # TODO have a method that could do "forecast" by giving the _injection by the agent, # TODO if he wants to make custom forecasts @@ -4852,58 +4852,6 @@ def get_back_to_ref_state( if self._is_done: raise Grid2OpException("Cannot use this function in a 'done' state.") return self.action_helper.get_back_to_ref_state(self, storage_setpoint, precision) - - def _aux_kcl(self, - n_el, # cst eg. cls.n_gen - el_to_subid, # cst eg. cls.gen_to_subid - el_bus, # cst eg. gen_bus - el_p, # cst, eg. gen_p - el_q, # cst, eg. gen_q - el_v, # cst, eg. gen_v - p_subs, q_subs, - p_bus, q_bus, - v_bus, - load_conv=True # whether the object is load convention (True) or gen convention (False) - ): - - # bellow i'm "forced" to do a loop otherwise, numpy do not compute the "+=" the way I want it to. - # for example, if two powerlines are such that line_or_to_subid is equal (eg both connected to substation 0) - # then numpy do not guarantee that `p_subs[self.line_or_to_subid] += p_or` will add the two "corresponding p_or" - # TODO this can be vectorized with matrix product, see example in obs.flow_bus_matrix (BaseObervation.py) - for i in range(n_el): - psubid = el_to_subid[i] - if el_bus[i] == -1: - # el is disconnected - continue - - # for substations - if load_conv: - p_subs[psubid] += el_p[i] - q_subs[psubid] += el_q[i] - else: - p_subs[psubid] -= el_p[i] - q_subs[psubid] -= el_q[i] - - # for bus - loc_bus = el_bus[i] - 1 - if load_conv: - p_bus[psubid, loc_bus] += el_p[i] - q_bus[psubid, loc_bus] += el_q[i] - else: - p_bus[psubid, loc_bus] -= el_p[i] - q_bus[psubid, loc_bus] -= el_q[i] - - # compute max and min values - if el_v is not None and el_v[i]: - # but only if gen is connected - v_bus[psubid, loc_bus][0] = min( - v_bus[psubid, loc_bus][0], - el_v[i], - ) - v_bus[psubid, loc_bus][1] = max( - v_bus[psubid, loc_bus][1], - el_v[i], - ) def check_kirchhoff(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: """ @@ -4932,98 +4880,55 @@ def check_kirchhoff(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarra """ cls = type(self) - - # fist check the "substation law" : nothing is created at any substation - p_subs = np.zeros(cls.n_sub, dtype=dt_float) - q_subs = np.zeros(cls.n_sub, dtype=dt_float) - - # check for each bus - p_bus = np.zeros((cls.n_sub, cls.n_busbar_per_sub), dtype=dt_float) - q_bus = np.zeros((cls.n_sub, cls.n_busbar_per_sub), dtype=dt_float) - v_bus = ( - np.zeros((cls.n_sub, cls.n_busbar_per_sub, 2), dtype=dt_float) - 1.0 - ) # sub, busbar, [min,max] - some_kind_of_inf = 1_000_000_000. - v_bus[:,:,0] = some_kind_of_inf - v_bus[:,:,1] = -1 * some_kind_of_inf - - self._aux_kcl( - cls.n_line, # cst eg. cls.n_gen - cls.line_or_to_subid, # cst eg. cls.gen_to_subid - self.line_or_bus, + lineor_info = ElTypeInfo( + self.line_or_bus, # cst eg. self.gen_bus self.p_or, # cst, eg. gen_p self.q_or, # cst, eg. gen_q self.v_or, # cst, eg. gen_v - p_subs, q_subs, - p_bus, q_bus, - v_bus, ) - self._aux_kcl( - cls.n_line, # cst eg. cls.n_gen - cls.line_ex_to_subid, # cst eg. cls.gen_to_subid - self.line_ex_bus, + lineex_info = ElTypeInfo( + self.line_ex_bus, # cst eg. self.gen_bus self.p_ex, # cst, eg. gen_p self.q_ex, # cst, eg. gen_q self.v_ex, # cst, eg. gen_v - p_subs, q_subs, - p_bus, q_bus, - v_bus, ) - self._aux_kcl( - cls.n_load, # cst eg. cls.n_gen - cls.load_to_subid, # cst eg. cls.gen_to_subid - self.load_bus, + load_info = ElTypeInfo( + self.load_bus, # cst eg. self.gen_bus self.load_p, # cst, eg. gen_p self.load_q, # cst, eg. gen_q self.load_v, # cst, eg. gen_v - p_subs, q_subs, - p_bus, q_bus, - v_bus, ) - self._aux_kcl( - cls.n_gen, # cst eg. cls.n_gen - cls.gen_to_subid, # cst eg. cls.gen_to_subid + gen_info = ElTypeInfo( self.gen_bus, # cst eg. self.gen_bus self.gen_p, # cst, eg. gen_p self.gen_q, # cst, eg. gen_q self.gen_v, # cst, eg. gen_v - p_subs, q_subs, - p_bus, q_bus, - v_bus, - load_conv=False ) - if cls.n_storage: - self._aux_kcl( - cls.n_storage, # cst eg. cls.n_gen - cls.storage_to_subid, # cst eg. cls.gen_to_subid - self.storage_bus, + if cls.n_storage > 0: + storage_info = ElTypeInfo( + self.storage_bus, # cst eg. self.gen_bus self.storage_power, # cst, eg. gen_p np.zeros(cls.n_storage), # cst, eg. gen_q None, # cst, eg. gen_v - p_subs, q_subs, - p_bus, q_bus, - v_bus, - ) + ) + else: + storage_info = None if cls.shunts_data_available: - self._aux_kcl( - cls.n_shunt, # cst eg. cls.n_gen - cls.shunt_to_subid, # cst eg. cls.gen_to_subid - self._shunt_bus, + shunt_info = ElTypeInfo( + self._shunt_bus, # cst eg. self.gen_bus self._shunt_p, # cst, eg. gen_p self._shunt_q, # cst, eg. gen_q self._shunt_v, # cst, eg. gen_v - p_subs, q_subs, - p_bus, q_bus, - v_bus, - ) - else: - warnings.warn( - "Observation.check_kirchhoff Impossible to get shunt information. Reactive information might be " - "incorrect." ) - diff_v_bus = np.zeros((cls.n_sub, cls.n_busbar_per_sub), dtype=dt_float) - diff_v_bus[:, :] = v_bus[:, :, 1] - v_bus[:, :, 0] - diff_v_bus[np.abs(diff_v_bus - -2. * some_kind_of_inf) <= 1e-5 ] = 0. # disconnected bus + else: + shunt_info = None + + p_subs, q_subs, p_bus, q_bus, diff_v_bus = cls._aux_check_kirchhoff(lineor_info, + lineex_info, + load_info, + gen_info, + storage_info, + shunt_info) return p_subs, q_subs, p_bus, q_bus, diff_v_bus \ No newline at end of file diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 8739e547..e29048c6 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -29,7 +29,7 @@ from grid2op.dtypes import dt_int, dt_float, dt_bool from grid2op.typing_variables import CLS_AS_DICT_TYPING, N_BUSBAR_PER_SUB_TYPING from grid2op.Exceptions import * -from grid2op.Space.space_utils import extract_from_dict, save_to_dict +from grid2op.Space.space_utils import extract_from_dict, save_to_dict, ElTypeInfo # TODO tests of these methods and this class in general DEFAULT_N_BUSBAR_PER_SUB = 2 @@ -5261,3 +5261,214 @@ def get_storage_info(cls, *, storage_id : Optional[int]=None, storage_name : Opt obj_name=storage_name) sub_id = cls.storage_to_subid[storage_id] return storage_id, storage_name, sub_id + + + @classmethod + def _aux_kcl_eltype(cls, + n_el : int, # cst eg. cls.n_gen + el_to_subid : np.ndarray, # cst eg. cls.gen_to_subid + el_bus : np.ndarray, # cst eg. gen_bus + el_p : np.ndarray, # cst, eg. gen_p + el_q : np.ndarray, # cst, eg. gen_q + el_v : np.ndarray, # cst, eg. gen_v + p_subs : np.ndarray, q_subs : np.ndarray, + p_bus : np.ndarray, q_bus : np.ndarray, + v_bus : np.ndarray, + load_conv: bool=True # whether the object is load convention (True) or gen convention (False) + ): + """This function is used as an auxilliary function in the function :func:`grid2op.Observation.BaseObservation.check_kirchhoff` + and :func:`grid2op.Backend.Backend.check_kirchhoff` + + Parameters + ---------- + n_el : int + number of this element type (*eg* cls.n_gen) + el_to_subid : np.ndarray + for each element of this element type, on which substation this element is connected (*eg* cls.gen_to_subid) + el_bus : np.ndarray + for each element of this element type, on which busbar this element is connected (*eg* obs.gen_bus) + el_p : np.ndarray + for each element of this element type, it gives the active power value consumed / produced by this element (*eg* obs.gen_p) + el_q : np.ndarray + for each element of this element type, it gives the reactive power value consumed / produced by this element (*eg* obs.gen_q) + el_v : np.ndarray + for each element of this element type, it gives the voltage magnitude of the bus to which this element (*eg* obs.gen_v) + is connected + p_subs : np.ndarray + results, modified in place: p absorbed at each substation + q_subs : np.ndarray + results, modified in place: q absorbed at each substation + p_bus : np.ndarray + results, modified in place: p absorbed at each bus (shape nb_sub, max_nb_busbar_per_sub) + q_bus : np.ndarray + results, modified in place: q absorbed at each bus (shape nb_sub, max_nb_busbar_per_sub) + v_bus : np.ndarray + results, modified in place: min voltage and max voltage found per bus (shape nb_sub, max_nb_busbar_per_sub, 2) + load_conv : _type_, optional + Whetherthis object type use the "load convention" or "generator convention" for p and q, by default True + """ + # bellow i'm "forced" to do a loop otherwise, numpy do not compute the "+=" the way I want it to. + # for example, if two powerlines are such that line_or_to_subid is equal (eg both connected to substation 0) + # then numpy do not guarantee that `p_subs[self.line_or_to_subid] += p_or` will add the two "corresponding p_or" + # TODO this can be vectorized with matrix product, see example in obs.flow_bus_matrix (BaseObervation.py) + for i in range(n_el): + psubid = el_to_subid[i] + if el_bus[i] == -1: + # el is disconnected + continue + + # for substations + if load_conv: + p_subs[psubid] += el_p[i] + q_subs[psubid] += el_q[i] + else: + p_subs[psubid] -= el_p[i] + q_subs[psubid] -= el_q[i] + + # for bus + loc_bus = el_bus[i] - 1 + if load_conv: + p_bus[psubid, loc_bus] += el_p[i] + q_bus[psubid, loc_bus] += el_q[i] + else: + p_bus[psubid, loc_bus] -= el_p[i] + q_bus[psubid, loc_bus] -= el_q[i] + + # compute max and min values + if el_v is not None and el_v[i]: + # but only if gen is connected + v_bus[psubid, loc_bus][0] = min( + v_bus[psubid, loc_bus][0], + el_v[i], + ) + v_bus[psubid, loc_bus][1] = max( + v_bus[psubid, loc_bus][1], + el_v[i], + ) + + @classmethod + def _aux_check_kirchhoff(cls, + lineor_info : ElTypeInfo, + lineex_info: ElTypeInfo, + load_info: ElTypeInfo, + gen_info: ElTypeInfo, + storage_info: Optional[ElTypeInfo] = None, + shunt_info : Optional[ElTypeInfo] = None, + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + """ + Analogous to "backend.check_kirchhoff" but from the observation + + .. versionadded:: 1.11.0 + + Returns + ------- + p_subs ``numpy.ndarray`` + sum of injected active power at each substations (MW) + q_subs ``numpy.ndarray`` + sum of injected reactive power at each substations (MVAr) + p_bus ``numpy.ndarray`` + sum of injected active power at each buses. It is given in form of a matrix, with number of substations as + row, and number of columns equal to the maximum number of buses for a substation (MW) + q_bus ``numpy.ndarray`` + sum of injected reactive power at each buses. It is given in form of a matrix, with number of substations as + row, and number of columns equal to the maximum number of buses for a substation (MVAr) + diff_v_bus: ``numpy.ndarray`` (2d array) + difference between maximum voltage and minimum voltage (computed for each elements) + at each bus. It is an array of two dimension: + + - first dimension represents the the substation (between 1 and self.n_sub) + - second element represents the busbar in the substation (0 or 1 usually) + + """ + # fist check the "substation law" : nothing is created at any substation + p_subs = np.zeros(cls.n_sub, dtype=dt_float) + q_subs = np.zeros(cls.n_sub, dtype=dt_float) + + # check for each bus + p_bus = np.zeros((cls.n_sub, cls.n_busbar_per_sub), dtype=dt_float) + q_bus = np.zeros((cls.n_sub, cls.n_busbar_per_sub), dtype=dt_float) + v_bus = ( + np.zeros((cls.n_sub, cls.n_busbar_per_sub, 2), dtype=dt_float) - 1.0 + ) # sub, busbar, [min,max] + some_kind_of_inf = 1_000_000_000. + v_bus[:,:,0] = some_kind_of_inf + v_bus[:,:,1] = -1 * some_kind_of_inf + cls._aux_kcl_eltype( + cls.n_line, # cst eg. cls.n_gen + cls.line_or_to_subid, # cst eg. cls.gen_to_subid + lineor_info._bus, # cst eg. self.gen_bus + lineor_info._p, # cst, eg. gen_p + lineor_info._q, # cst, eg. gen_q + lineor_info._v, # cst, eg. gen_v + p_subs, q_subs, + p_bus, q_bus, + v_bus, + ) + cls._aux_kcl_eltype( + cls.n_line, # cst eg. cls.n_gen + cls.line_ex_to_subid, # cst eg. cls.gen_to_subid + lineex_info._bus, # cst eg. self.gen_bus + lineex_info._p, # cst, eg. gen_p + lineex_info._q, # cst, eg. gen_q + lineex_info._v, # cst, eg. gen_v + p_subs, q_subs, + p_bus, q_bus, + v_bus, + ) + cls._aux_kcl_eltype( + cls.n_load, # cst eg. cls.n_gen + cls.load_to_subid, # cst eg. cls.gen_to_subid + load_info._bus, # cst eg. self.gen_bus + load_info._p, # cst, eg. gen_p + load_info._q, # cst, eg. gen_q + load_info._v, # cst, eg. gen_v + p_subs, q_subs, + p_bus, q_bus, + v_bus, + ) + cls._aux_kcl_eltype( + cls.n_gen, # cst eg. cls.n_gen + cls.gen_to_subid, # cst eg. cls.gen_to_subid + gen_info._bus, # cst eg. self.gen_bus + gen_info._p, # cst, eg. gen_p + gen_info._q, # cst, eg. gen_q + gen_info._v, # cst, eg. gen_v + p_subs, q_subs, + p_bus, q_bus, + v_bus, + load_conv=False + ) + if storage_info is not None: + cls._aux_kcl_eltype( + cls.n_storage, # cst eg. cls.n_gen + cls.storage_to_subid, # cst eg. cls.gen_to_subid + storage_info._bus, # cst eg. self.gen_bus + storage_info._p, # cst, eg. gen_p + storage_info._q, # cst, eg. gen_q + storage_info._v, # cst, eg. gen_v + p_subs, q_subs, + p_bus, q_bus, + v_bus, + ) + + if shunt_info is not None: + GridObjects._aux_kcl_eltype( + cls.n_shunt, # cst eg. cls.n_gen + cls.shunt_to_subid, # cst eg. cls.gen_to_subid + shunt_info._bus, # cst eg. self.gen_bus + shunt_info._p, # cst, eg. gen_p + shunt_info._q, # cst, eg. gen_q + shunt_info._v, # cst, eg. gen_v + p_subs, q_subs, + p_bus, q_bus, + v_bus, + ) + else: + warnings.warn( + f"{cls.__name__}.check_kirchhoff Impossible to get shunt information. Reactive information might be " + "incorrect." + ) + diff_v_bus = np.zeros((cls.n_sub, cls.n_busbar_per_sub), dtype=dt_float) + diff_v_bus[:, :] = v_bus[:, :, 1] - v_bus[:, :, 0] + diff_v_bus[np.abs(diff_v_bus - -2. * some_kind_of_inf) <= 1e-5 ] = 0. # disconnected bus + return p_subs, q_subs, p_bus, q_bus, diff_v_bus diff --git a/grid2op/Space/__init__.py b/grid2op/Space/__init__.py index 14b2c8b3..7b3683b9 100644 --- a/grid2op/Space/__init__.py +++ b/grid2op/Space/__init__.py @@ -1,6 +1,7 @@ __all__ = ["RandomObject", "SerializableSpace", "GridObjects", + "ElTypeInfo", "DEFAULT_N_BUSBAR_PER_SUB", "GRID2OP_CLASSES_ENV_FOLDER", "DEFAULT_ALLOW_DETACHMENT"] @@ -8,6 +9,7 @@ from grid2op.Space.RandomObject import RandomObject from grid2op.Space.SerializableSpace import SerializableSpace from grid2op.Space.GridObjects import (GridObjects, + ElTypeInfo, DEFAULT_N_BUSBAR_PER_SUB, GRID2OP_CLASSES_ENV_FOLDER, DEFAULT_ALLOW_DETACHMENT) diff --git a/grid2op/Space/space_utils.py b/grid2op/Space/space_utils.py index 0ab3dc95..285bc955 100644 --- a/grid2op/Space/space_utils.py +++ b/grid2op/Space/space_utils.py @@ -7,6 +7,9 @@ # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. import copy +from typing import Optional +import numpy as np + from grid2op.Exceptions import Grid2OpException # i already issued the warning for the "some substations have no controllable elements" @@ -74,3 +77,35 @@ def save_to_dict(res_dict, me, key, converter, copy_=True): ) raise Grid2OpException(msg_err_.format(key)) res_dict[key] = res + + +class ElTypeInfo: + """ + For each element type (*eg* generator, or load) this is a container for basic element + information, it is used in the `check_kirchhoff` functions of observation :func:`grid2op.Observation.BaseObservation.check_kirchhoff` + and backend :func:`grid2op.Backend.Backend.check_kirchhoff` + + The element it contains are: + + - el_bus : np.ndarray + for each element of this element type, on which busbar this element is connected (*eg* obs.gen_bus) + - el_p : np.ndarray + for each element of this element type, it gives the active power value consumed / produced by this element (*eg* obs.gen_p) + - el_q : np.ndarray + for each element of this element type, it gives the reactive power value consumed / produced by this element (*eg* obs.gen_q) + - el_v : np.ndarray + for each element of this element type, it gives the voltage magnitude of the bus to which this element (*eg* obs.gen_v) + is connected + + """ + def __init__(self, + el_bus : np.ndarray, + el_p : np.ndarray, + el_q : np.ndarray, + el_v : Optional[np.ndarray] = None, + # load_conv: bool = True + ): + self._bus = el_bus + self._p = el_p + self._q = el_q + self._v = el_v From 3529f1088ff8121bf72e1927b89705d34297508a Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Wed, 4 Dec 2024 11:19:11 +0100 Subject: [PATCH 41/60] fix a bug in loading back actions from / to json and to / from vect + improve test coverage of shedding with load Signed-off-by: DONNOT Benjamin --- CHANGELOG.rst | 4 + grid2op/Action/baseAction.py | 142 ++++++++++++++++++++++++--------- grid2op/Space/GridObjects.py | 31 ++++--- grid2op/tests/test_shedding.py | 66 ++++++++++++++- 4 files changed, 188 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 81dae607..eb0780f8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -109,6 +109,9 @@ Native multi agents support: customize the added name by overloading the `get_class_added_name` class method. - [BREAKING] it is now forbidden to create environment with arguments. Only key-word arguments are allowed. +- [BREAKING] the way actions is serialized has been changed with respect to the `from_vect` / + `to_vect` method. This might introduce some issues when loading previously saved actions + with this methods. - [FIXED] issue https://github.com/Grid2op/grid2op/issues/657 - [FIXED] missing an import on the `MaskedEnvironment` class - [FIXED] a bug when trying to set the load_p, load_q, gen_p, gen_v by names. @@ -125,6 +128,7 @@ Native multi agents support: environment data - [FIXED] an issue preventing to set the thermal limit in the options if the last simulated action lead to a game over +- [FIXED] some bugs in `act.from_json(...)` due to the handling of the injection modifications. - [ADDED] possibility to set the "thermal limits" when calling `env.reset(..., options={"thermal limit": xxx})` - [ADDED] possibility to retrieve some structural information about elements with with `gridobj.get_line_info(...)`, `gridobj.get_load_info(...)`, `gridobj.get_gen_info(...)` diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 61082cc5..299ffd35 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -395,7 +395,10 @@ class BaseAction(GridObjects): "_detach_gen", # new in 1.11.0 "_detach_storage", # new in 1.11.0 ] - attr_nan_list_set = set() + attr_nan_list_set = set(["prod_p", + "prod_v", + "load_p", + "load_q"]) attr_list_set = set(attr_list_vect) shunt_added = False @@ -436,7 +439,7 @@ def __init__(self, _names_chronics_to_backend: Optional[Dict[Literal["loads", "p ) # injection change - self._dict_inj = {} + self._dict_inj : Dict[Literal["prod_p", "prod_v", "load_p", "load_q"], np.ndarray] = {} # topology changed self._set_topo_vect = np.full(shape=cls.dim_topo, fill_value=0, dtype=dt_int) @@ -772,12 +775,13 @@ def as_serializable_dict(self) -> dict: if cls.detachment_is_allowed: for el in ["load", "gen", "storage"]: - attr_key = f"{el}s_detached" + attr_key = f"detach_{el}" attr_vect = f"_detach_{el}" + xxx_name = getattr(cls, f"name_{el}") res[attr_key] = {} vect_ = getattr(self, attr_vect) if vect_.any(): - res[attr_key] = [int(el) for el in vect_.nonzero()[0]] + res[attr_key] = [str(xxx_name[el]) for el in vect_.nonzero()[0]] if not res[attr_key]: del res[attr_key] return res @@ -941,23 +945,60 @@ def can_affect_something(self) -> bool: def _get_array_from_attr_name(self, attr_name): if hasattr(self, attr_name): res = super()._get_array_from_attr_name(attr_name) - else: - if attr_name in self._dict_inj: - res = self._dict_inj[attr_name] - else: - if attr_name == "prod_p" or attr_name == "prod_v": - res = np.full(self.n_gen, fill_value=0.0, dtype=dt_float) - elif attr_name == "load_p" or attr_name == "load_q": - res = np.full(self.n_load, fill_value=0.0, dtype=dt_float) - else: - raise Grid2OpException( - 'Impossible to find the attribute "{}" ' - 'into the BaseAction of type "{}"'.format(attr_name, type(self)) - ) - return res + return res + + if attr_name in self._dict_inj: + res = self._dict_inj[attr_name] + return res + + cls = type(self) + if attr_name == "prod_p" or attr_name == "prod_v": + res = np.full(cls.n_gen, fill_value=np.nan, dtype=dt_float) + return res + if attr_name == "load_p" or attr_name == "load_q": + res = np.full(cls.n_load, fill_value=np.nan, dtype=dt_float) + return res + raise Grid2OpException( + 'Impossible to find the attribute "{}" ' + 'into the BaseAction of type "{}"'.format(attr_name, cls) + ) + def _set_array_from_attr_name(self, allowed_keys, key: str, array_) -> None: + """used for `from_json` please see `_assign_attr_from_name` for from_vect""" + if key in self._dict_inj: + if np.isfinite(array_).any(): + # because it is used elsewhere, to / from json stores also injection even if it's all nan + # so i need to check in this case or if it's a real injection modification + self._dict_inj[key][:] = array_ + self._post_process_from_vect() # set the correct flags + return + + if key == "prod_p" or key == "prod_v" or key == "load_p" or key == "load_q": + if np.isfinite(array_).any(): + # because it is used elsewhere, to / from json stores also injection even if it's all nan + # so i need to check in this case or if it's a real injection modification + self._dict_inj[key] = np.asarray(array_).astype(dt_float) + self._post_process_from_vect() # set the correct flags + return + + if key in allowed_keys: + # regular stuff + super()._set_array_from_attr_name(allowed_keys, key, array_) + self._post_process_from_vect() # set the correct flags + return + + raise Grid2OpException( + 'Impossible to find the attribute "{}" ' + 'into the BaseAction of type "{}"'.format(key, type(self)) + ) + def _post_process_from_vect(self): - self._modif_inj = self._dict_inj != {} + modif_inj = False + if self._dict_inj != {}: + for k, v in self._dict_inj: + if np.isfinite(v).any(): + modif_inj = True + self._modif_inj = modif_inj self._modif_set_bus = (self._set_topo_vect != 0).any() self._modif_change_bus = (self._change_bus_vect).any() self._modif_set_status = (self._set_line_status != 0).any() @@ -974,6 +1015,7 @@ def _post_process_from_vect(self): self._modif_detach_storage = (self._detach_storage).any() def _assign_attr_from_name(self, attr_nm, vect): + """used for from_vect, for from_json please see `_set_array_from_attr_name`""" if hasattr(self, attr_nm): if attr_nm not in type(self).attr_list_set: raise AmbiguousAction( @@ -982,8 +1024,14 @@ def _assign_attr_from_name(self, attr_nm, vect): super()._assign_attr_from_name(attr_nm, vect) self._post_process_from_vect() else: + if attr_nm != "load_p" and attr_nm != "load_q" and attr_nm != "prod_p" and attr_nm != "prod_v": + raise AmbiguousAction(f"Unknown action attribute with name {attr_nm}") if np.isfinite(vect).any() and (np.abs(vect) >= 1e-7).any(): - self._dict_inj[attr_nm] = vect + if attr_nm in self._dict_inj: + self._dict_inj[attr_nm][:] = vect + else: + self._dict_inj[attr_nm] = vect.astype(dt_float) + self._post_process_from_vect() def check_space_legit(self): """ @@ -2907,22 +2955,6 @@ def _check_for_ambiguity(self): "request on github if you need this feature." ) - if False: - # TODO find an elegant way to disable that - # now it's possible. - for q_id, status in enumerate(self._set_line_status): - if status == 1: - # i reconnect a powerline, i need to check that it's connected on both ends - if ( - self._set_topo_vect[self.line_or_pos_topo_vect[q_id]] == 0 - or self._set_topo_vect[self.line_ex_pos_topo_vect[q_id]] == 0 - ): - - raise InvalidLineStatus( - "You ask to reconnect powerline {} yet didn't tell on" - " which bus.".format(q_id) - ) - if self._modif_set_bus: disco_or = self._set_topo_vect[cls.line_or_pos_topo_vect] == -1 if (self._set_topo_vect[cls.line_ex_pos_topo_vect][disco_or] > 0).any(): @@ -3060,6 +3092,38 @@ def _check_for_ambiguity(self): "as doing so. Expect wrong behaviour." ) + self._is_detachment_ambiguous() + + def _is_detachment_ambiguous(self): + """check if any of the detachment action is ambiguous""" + cls = type(self) + if (not self._modif_detach_gen) and self._detach_gen.any(): + raise AmbiguousAction("Invalid flag for gen detachment, please use standard grid2op API for action.") + if (not self._modif_detach_load) and self._detach_load.any(): + raise AmbiguousAction("Invalid flag for load detachment, please use standard grid2op API for action.") + if (not self._modif_detach_storage) and self._detach_storage.any(): + raise AmbiguousAction("Invalid flag for load detachment, please use standard grid2op API for action.") + if self._modif_detach_load and "detach_load" not in cls.authorized_keys: + raise IllegalAction("It's forbidden to do a load detachment with this action type") + if self._modif_detach_gen and "detach_gen" not in cls.authorized_keys: + raise IllegalAction("It's forbidden to do a generator detachment with this action type") + if self._modif_detach_storage and "detach_storage" not in cls.authorized_keys: + raise IllegalAction("It's forbidden to do a storage detachment with this action type") + for el_nm in ["load", "gen", "storage"]: + _modif_detach_xxx = getattr(self, f"_modif_detach_{el_nm}") + xxx_pos_topo_vect = getattr(cls, f"{el_nm}_pos_topo_vect") + _detach_xxx = getattr(self, f"_detach_{el_nm}") + name_xxx = getattr(cls, f"name_{el_nm}") + if _modif_detach_xxx: + issue_xxx = self._change_bus_vect[xxx_pos_topo_vect] & _detach_xxx + if (issue_xxx).any(): + raise AmbiguousAction(f"Trying to both change a {el_nm} of busbar (change_bus) AND detach it from the grid. " + f"Check {el_nm}: {name_xxx[issue_xxx]}") + issue_xxx = self._set_topo_vect[xxx_pos_topo_vect] & _detach_xxx + if (issue_xxx).any(): + raise AmbiguousAction(f"Trying to both set a {el_nm} of busbar (set_bus) AND detach it from the grid. " + f"Check {el_nm}: {name_xxx[issue_xxx]}") + def _is_storage_ambiguous(self): """check if storage actions are ambiguous""" cls = type(self) @@ -5663,7 +5727,7 @@ def detach_load(self, values): np.arange(cls.n_load), self._detach_load, ) - self._modif_alert = True + self._modif_detach_load = True except Exception as exc_: self._detach_load[:] = orig_ raise IllegalAction( @@ -5720,7 +5784,7 @@ def detach_gen(self, values): np.arange(cls.n_gen), self._detach_gen, ) - self._modif_alert = True + self._modif_detach_gen = True except Exception as exc_: self._detach_gen[:] = orig_ raise IllegalAction( @@ -5777,7 +5841,7 @@ def detach_storage(self, values): np.arange(cls.n_storage), self._detach_storage, ) - self._modif_alert = True + self._modif_detach_storage = True except Exception as exc_: self._detach_storage[:] = orig_ raise IllegalAction( diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index ed3460da..118d3f9c 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -901,6 +901,20 @@ def _get_array_from_attr_name(self, attr_name: str) -> Union[np.ndarray, int, st """ return np.array(getattr(self, attr_name)).flatten() + def _set_array_from_attr_name(self, allowed_keys, key: str, array_) -> None: + """used for `from_json` please see `_assign_attr_from_name` for `from_vect`""" + if key not in allowed_keys: + raise AmbiguousAction(f'Impossible to recognize the key "{key}"') + my_attr = getattr(self, key) + if isinstance(my_attr, np.ndarray): + # the regular instance is an array, so i just need to assign the right values to it + my_attr[:] = array_ + else: + # normal values is a scalar. So i need to convert the array received as a scalar, and + # convert it to the proper type + type_ = type(my_attr) + setattr(self, key, type_(array_[0])) + def to_vect(self) -> np.ndarray: """ Convert this instance of GridObjects to a numpy ndarray. @@ -996,19 +1010,10 @@ def from_json(self, dict_: Dict[str, Any]) -> None: """ # TODO optimization for action or observation, to reduce json size, for example using the see `to_json` - all_keys = type(self).attr_list_vect + type(self).attr_list_json + cls = type(self) + all_keys = cls.attr_list_vect + cls.attr_list_json for key, array_ in dict_.items(): - if key not in all_keys: - raise AmbiguousAction(f'Impossible to recognize the key "{key}"') - my_attr = getattr(self, key) - if isinstance(my_attr, np.ndarray): - # the regular instance is an array, so i just need to assign the right values to it - my_attr[:] = array_ - else: - # normal values is a scalar. So i need to convert the array received as a scalar, and - # convert it to the proper type - type_ = type(my_attr) - setattr(self, key, type_(array_[0])) + self._set_array_from_attr_name(all_keys, key, array_) @classmethod def _convert_to_json(cls, dict_: Dict[str, Any]) -> None: @@ -1129,6 +1134,8 @@ def _assign_attr_from_name(self, attr_nm, vect): If this function is overloaded, then the _get_array_from_attr_name must be too. + Used for `from_vect`, please see `_set_array_from_attr_name` for `from_json` + Parameters ---------- attr_nm diff --git a/grid2op/tests/test_shedding.py b/grid2op/tests/test_shedding.py index 94f99f71..3b06b60c 100644 --- a/grid2op/tests/test_shedding.py +++ b/grid2op/tests/test_shedding.py @@ -6,10 +6,14 @@ # 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 json import warnings import unittest import numpy as np +import tempfile + import grid2op +from grid2op.Exceptions import AmbiguousAction from grid2op.Action import CompleteAction from grid2op.Parameters import Parameters @@ -108,28 +112,82 @@ def test_shedding_persistance(self): assert not done assert obs.topo_vect[load_pos] == -1 - def test_action_property(self): + def test_action_property_load(self): act = self.env.action_space() assert "detach_load" in type(act).authorized_keys act.detach_load = np.ones(act.n_load, dtype=bool) assert act._detach_load.all() + assert act._modif_detach_load act2 = self.env.action_space() act2.detach_load = 1 assert act2._detach_load[1] + assert act2._modif_detach_load act3 = self.env.action_space() act3.detach_load = [0, 2] assert act3._detach_load[0] assert act3._detach_load[2] + assert act3._modif_detach_load for k, v in self.load_lookup.items(): act4 = self.env.action_space() act4.detach_load = {k} assert act4._detach_load[v] - # TODO shedding: test act.to_dict - # TODO shedding: test act.from_dict - # TODO shedding test.act.to_json + assert act4._modif_detach_load + + # change and disconnect + act = self.env.action_space() + act.load_change_bus = [0] + act.detach_load = [0] + is_amb, exc_ = act.is_ambiguous() + assert is_amb + assert isinstance(exc_, AmbiguousAction) + + # set_bus and disconnect + act = self.env.action_space() + act.load_set_bus = [(0, 1)] + act.detach_load = [0] + is_amb, exc_ = act.is_ambiguous() + assert is_amb + assert isinstance(exc_, AmbiguousAction) + + # flag not set + act = self.env.action_space() + act._detach_load[0] = True + is_amb, exc_ = act.is_ambiguous() + assert is_amb + assert isinstance(exc_, AmbiguousAction) + + # test to / from dict + act = self.env.action_space() + act.detach_load = [0] + dict_ = act.as_serializable_dict() # you can save this dict with the json library + act2 = self.env.action_space(dict_) + act == act2 + + # test to / from json + act = self.env.action_space() + act.detach_load = [0] + dict_ = act.to_json() + with tempfile.NamedTemporaryFile() as f_tmp: + with open(f_tmp.name, "w", encoding="utf-8") as f: + json.dump(obj=dict_, fp=f) + + with open(f_tmp.name, "r", encoding="utf-8") as f: + dict_reload = json.load(fp=f) + + act_reload = self.env.action_space() + act_reload.from_json(dict_reload) + assert act == act_reload + + # test to / from vect + act = self.env.action_space() + act.detach_load = [0] + vect_ = act.to_vect() + act_reload = self.env.action_space() + act_reload.from_vect(vect_) + assert act == act_reload # TODO Shedding: test when backend does not support it is not set # TODO shedding: test when user deactivates it it is not set From 38cd8c397d6dbf2bcf46487858abdc156550b708 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Wed, 4 Dec 2024 15:12:00 +0100 Subject: [PATCH 42/60] fix issue with copying action, add action topological_impact caching Signed-off-by: DONNOT Benjamin --- CHANGELOG.rst | 5 +- grid2op/Action/baseAction.py | 188 +++++++++++++++++++++++------- grid2op/Environment/baseEnv.py | 28 ++++- grid2op/Opponent/opponentSpace.py | 5 + grid2op/Space/GridObjects.py | 9 +- grid2op/tests/test_Action.py | 33 +++--- grid2op/tests/test_shedding.py | 161 +++++++++++++++---------- 7 files changed, 303 insertions(+), 126 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index eb0780f8..f99b908f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -162,7 +162,10 @@ Native multi agents support: possible to use the same things as for the other types of element) - [IMPROVED] grid2op does not require the `chronics` folder when using the `FromHandlers` class - +- [IMPROVED] the function `action.get_topological_impact(...)` has now a "caching" mechanism + that allows not to recompute it over and over again (this is internal API please do not change + it... unless you know what you are doing) + [1.10.4] - 2024-10-15 ------------------------- - [FIXED] new pypi link (no change in code) diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 299ffd35..5e1276aa 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -395,10 +395,12 @@ class BaseAction(GridObjects): "_detach_gen", # new in 1.11.0 "_detach_storage", # new in 1.11.0 ] + # new in 1.11.0 (was not set to nan before in serialization) attr_nan_list_set = set(["prod_p", "prod_v", "load_p", "load_q"]) + # attr_nan_list_set = set() attr_list_set = set(attr_list_vect) shunt_added = False @@ -527,9 +529,29 @@ def process_shunt_static_data(cls): cls.attr_list_vect.remove(el) except ValueError: pass - cls.attr_list_set = set(cls.attr_list_vect) + cls._update_value_set() return super().process_shunt_static_data() + @classmethod + def process_detachment_compat(cls): + if not cls.detachment_is_allowed: + # this is really important, otherwise things from grid2op base types will be affected + cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) + cls.attr_list_set = copy.deepcopy(cls.attr_list_set) + # remove the detachment from the list to vector + for el in ["_detach_load", "_detach_gen", "_detach_storage"]: + if el in cls.attr_list_vect: + try: + cls.attr_list_vect.remove(el) + except ValueError: + pass + # remove the detachment from the allowed action + for el in ["detach_load", "detach_gen", "detach_storage"]: + if el in cls.authorized_keys: + cls.authorized_keys.remove(el) + cls._update_value_set() + return super().process_detachment_compat() + def copy(self) -> "BaseAction": # sometimes this method is used... return self.__deepcopy__() @@ -570,14 +592,15 @@ def _aux_copy(self, other): "_curtail", "_raise_alarm", "_raise_alert", - "_detach_load", - "_detach_gen", - "_detach_storage", ] - if type(self).shunts_data_available: + cls = type(self) + if cls.shunts_data_available: attr_vect += ["shunt_p", "shunt_q", "shunt_bus"] + if cls.detachment_is_allowed: + attr_vect += ["_detach_load", "_detach_gen", "_detach_storage"] + for attr_nm in attr_simple: setattr(other, attr_nm, getattr(self, attr_nm)) @@ -599,10 +622,6 @@ def __copy__(self) -> "BaseAction": res._subs_impacted = self._subs_impacted return res - - @classmethod - def process_shunt_static_data(cls): - return super().process_shunt_static_data() def __deepcopy__(self, memodict={}) -> "BaseAction": res = type(self)() @@ -1311,11 +1330,16 @@ def _dont_affect_topology(self) -> bool: and (not self._modif_detach_storage) ) - def get_topological_impact(self, powerline_status=None) -> Tuple[np.ndarray, np.ndarray]: + def get_topological_impact(self, + powerline_status : Optional[np.ndarray]=None, + _store_in_cache : bool =False, + _read_from_cache : bool =True) -> Tuple[np.ndarray, np.ndarray]: """ Gives information about the element being impacted by this action. + **NB** The impacted elements can be used by :class:`grid2op.BaseRules` to determine whether or not an action is legal or not. + **NB** The impacted are the elements that can potentially be impacted by the action. This does not mean they will be impacted. For examples: @@ -1335,6 +1359,41 @@ def get_topological_impact(self, powerline_status=None) -> Tuple[np.ndarray, np. Any such "change" that would be illegal is declared as "illegal" regardless of the real impact of this action on the powergrid. + Parameters + ----------- + powerline_status: Optional[np.ndarray] + The impact of a powerline can change depending on the status (connected or disconnected) of + the powerlines of the grid (see section :ref:`action_powerline_status` of the documentation). + This argument gives this information to this function. It should be read from the current observation. + + _store_in_cache: ``bool`` + Whether to store the result of this processing in a cache. This is for example used by the + :class:`grid2op.Environment.Environment` especially in the :func:`grid2op.Environment.BaseEnv.step` + to avoid to compute this result over and over again. + + By default its ``False`` and we don't recommend to set it to ``True``. Indeed, if set to ``True`` + then the argument `powerline_status` might be ignored in future calls where `_read_from_cache` is + ``True`` + + .. newinversion:: 1.11.0 + + .. warning:: + Use with extra care, it's private API. + + _read_from_cache: ``bool`` + Whether to read from the cache. + + If the cache has been set by a previous calls to this same function + by explicitly setting `_store_in_cache = True` it will skip all the computation and returns the + values stored in the cache, *de facto* ignoring the argument `powerline_status`. + + If the cache has not been set up then this has no effect (which is the default behaviour). + + By default it's ``True``, but by default no cache is not set up. This means that by default + the argument `powerline_status` is in fact used. + + .. newinversion:: 1.11.0 + Returns ------- lines_impacted: :class:`numpy.ndarray`, dtype:dt_bool @@ -1358,35 +1417,49 @@ def get_topological_impact(self, powerline_status=None) -> Tuple[np.ndarray, np. env_name = "l2rpn_case14_sandbox" # or any other name env = grid2op.make(env_name) + obs = env.reset() # get an action action = env.action_space.sample() # inspect its impact - lines_impacted, subs_impacted = action.get_topological_impact() + lines_impacted, subs_impacted = action.get_topological_impact(obs.line_status) for line_id in np.where(lines_impacted)[0]: print(f"The line {env.name_line[line_id]} with id {line_id} is impacted by this action") print(action) + """ + if (_read_from_cache and + self._lines_impacted is not None and + self._subs_impacted is not None): + # cache is set and I ask to read it + # no need to recompute this + return True & self._lines_impacted, True & self._subs_impacted + + cls = type(self) if self._dont_affect_topology(): # action is not impacting the topology # so it does not modified anything concerning the topology - self._lines_impacted = np.full( - shape=self.n_line, fill_value=False, dtype=dt_bool + _lines_impacted = np.full( + shape=cls.n_line, fill_value=False, dtype=dt_bool ) - self._subs_impacted = np.full( - shape=self.sub_info.shape, fill_value=False, dtype=dt_bool + _subs_impacted = np.full( + shape=cls.n_sub, fill_value=False, dtype=dt_bool ) - return self._lines_impacted, self._subs_impacted + if _store_in_cache: + # store the result in cache is asked too + self._lines_impacted = _lines_impacted + self._subs_impacted = _subs_impacted + return _lines_impacted, _subs_impacted if powerline_status is None: - isnotconnected = np.full(self.n_line, fill_value=True, dtype=dt_bool) + isnotconnected = np.full(cls.n_line, fill_value=True, dtype=dt_bool) else: isnotconnected = ~powerline_status - self._lines_impacted = self._switch_line_status | (self._set_line_status != 0) - self._subs_impacted = np.full( - shape=self.sub_info.shape, fill_value=False, dtype=dt_bool + _lines_impacted = self._switch_line_status | (self._set_line_status != 0) + _subs_impacted = np.full( + shape=cls.n_sub, fill_value=False, dtype=dt_bool ) # compute the changes of the topo vector @@ -1394,10 +1467,10 @@ def get_topological_impact(self, powerline_status=None) -> Tuple[np.ndarray, np. # remove the change due to powerline only effective_change[ - self.line_or_pos_topo_vect[self._lines_impacted & isnotconnected] + self.line_or_pos_topo_vect[_lines_impacted & isnotconnected] ] = False effective_change[ - self.line_ex_pos_topo_vect[self._lines_impacted & isnotconnected] + self.line_ex_pos_topo_vect[_lines_impacted & isnotconnected] ] = False # i can change also the status of a powerline by acting on its extremity @@ -1406,36 +1479,50 @@ def get_topological_impact(self, powerline_status=None) -> Tuple[np.ndarray, np. # if we don't know the state of the grid, we don't consider # these "improvments": we consider a powerline is never # affected if its bus is modified at any of its ends. - connect_set_or = (self._set_topo_vect[self.line_or_pos_topo_vect] > 0) & ( + connect_set_or = (self._set_topo_vect[cls.line_or_pos_topo_vect] > 0) & ( isnotconnected ) - self._lines_impacted |= connect_set_or - effective_change[self.line_or_pos_topo_vect[connect_set_or]] = False - effective_change[self.line_ex_pos_topo_vect[connect_set_or]] = False - connect_set_ex = (self._set_topo_vect[self.line_ex_pos_topo_vect] > 0) & ( + _lines_impacted |= connect_set_or + effective_change[cls.line_or_pos_topo_vect[connect_set_or]] = False + effective_change[cls.line_ex_pos_topo_vect[connect_set_or]] = False + connect_set_ex = (self._set_topo_vect[cls.line_ex_pos_topo_vect] > 0) & ( isnotconnected ) - self._lines_impacted |= connect_set_ex - effective_change[self.line_or_pos_topo_vect[connect_set_ex]] = False - effective_change[self.line_ex_pos_topo_vect[connect_set_ex]] = False + _lines_impacted |= connect_set_ex + effective_change[cls.line_or_pos_topo_vect[connect_set_ex]] = False + effective_change[cls.line_ex_pos_topo_vect[connect_set_ex]] = False # second sub case i disconnected the powerline by setting origin or extremity to negative stuff - disco_set_or = (self._set_topo_vect[self.line_or_pos_topo_vect] < 0) & ( + disco_set_or = (self._set_topo_vect[cls.line_or_pos_topo_vect] < 0) & ( powerline_status ) - self._lines_impacted |= disco_set_or - effective_change[self.line_or_pos_topo_vect[disco_set_or]] = False - effective_change[self.line_ex_pos_topo_vect[disco_set_or]] = False - disco_set_ex = (self._set_topo_vect[self.line_ex_pos_topo_vect] < 0) & ( + _lines_impacted |= disco_set_or + effective_change[cls.line_or_pos_topo_vect[disco_set_or]] = False + effective_change[cls.line_ex_pos_topo_vect[disco_set_or]] = False + disco_set_ex = (self._set_topo_vect[cls.line_ex_pos_topo_vect] < 0) & ( powerline_status ) - self._lines_impacted |= disco_set_ex - effective_change[self.line_or_pos_topo_vect[disco_set_ex]] = False - effective_change[self.line_ex_pos_topo_vect[disco_set_ex]] = False + _lines_impacted |= disco_set_ex + effective_change[cls.line_or_pos_topo_vect[disco_set_ex]] = False + effective_change[cls.line_ex_pos_topo_vect[disco_set_ex]] = False + + _subs_impacted[cls._topo_vect_to_sub[effective_change]] = True + + if _store_in_cache: + # store the results in cache if asked too + self._lines_impacted = _lines_impacted + self._subs_impacted = _subs_impacted + return _lines_impacted, _subs_impacted + + def reset_cache_topological_impact(self) -> None: + """INTERNAL + + .. versionadded:: 1.11.0 + + """ + self._lines_impacted = None + self._subs_impacted = None - self._subs_impacted[self._topo_vect_to_sub[effective_change]] = True - return self._lines_impacted, self._subs_impacted - def remove_line_status_from_topo(self, obs: "grid2op.Observation.BaseObservation" = None, check_cooldown: bool = True): @@ -3097,6 +3184,23 @@ def _check_for_ambiguity(self): def _is_detachment_ambiguous(self): """check if any of the detachment action is ambiguous""" cls = type(self) + if not cls.detachment_is_allowed: + # detachment is not allowed + if self._modif_detach_gen: + raise IllegalAction("Generators cannot be detached with this environment") + if self._modif_detach_load: + raise IllegalAction("Loads cannot be detached with this environment") + if self._modif_detach_storage: + raise IllegalAction("Storage units cannot be detached with this environment") + if self._detach_gen is not None: + raise IllegalAction("Generators cannot be detached with this environment") + if self._detach_load is not None: + raise IllegalAction("Loads cannot be detached with this environment") + if self._detach_storage is not None: + raise IllegalAction("Storage units cannot be detached with this environment") + return + + # here detachment is allowed, I check consistency between everything if (not self._modif_detach_gen) and self._detach_gen.any(): raise AmbiguousAction("Invalid flag for gen detachment, please use standard grid2op API for action.") if (not self._modif_detach_load) and self._detach_load.any(): @@ -3119,7 +3223,7 @@ def _is_detachment_ambiguous(self): if (issue_xxx).any(): raise AmbiguousAction(f"Trying to both change a {el_nm} of busbar (change_bus) AND detach it from the grid. " f"Check {el_nm}: {name_xxx[issue_xxx]}") - issue_xxx = self._set_topo_vect[xxx_pos_topo_vect] & _detach_xxx + issue_xxx = (self._set_topo_vect[xxx_pos_topo_vect] >= 1) & _detach_xxx if (issue_xxx).any(): raise AmbiguousAction(f"Trying to both set a {el_nm} of busbar (set_bus) AND detach it from the grid. " f"Check {el_nm}: {name_xxx[issue_xxx]}") diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index 5d405b21..3f666c59 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -2953,7 +2953,7 @@ def _aux_handle_attack(self, action: BaseAction): self._backend_action += attack return lines_attacked, subs_attacked, attack_duration - def _aux_apply_redisp(self, action, new_p, new_p_th, gen_curtailed, except_): + def _aux_apply_redisp(self, action, new_p, new_p_th, gen_curtailed, except_, powerline_status): is_illegal_redisp = False is_done = False is_illegal_reco = False @@ -2969,7 +2969,9 @@ def _aux_apply_redisp(self, action, new_p, new_p_th, gen_curtailed, except_): if except_tmp is not None: orig_action = action + action.reset_cache_topological_impact() action = self._action_space({}) + _ = action.get_topological_impact(powerline_status, _store_in_cache=True, _read_from_cache=False) if type(self).dim_alerts: action.raise_alert = orig_action.raise_alert is_illegal_redisp = True @@ -3001,7 +3003,9 @@ def _aux_apply_redisp(self, action, new_p, new_p_th, gen_curtailed, except_): if not valid_disp or except_tmp is not None: # game over case (divergence of the scipy routine to compute redispatching) + action.reset_cache_topological_impact() res_action = self._action_space({}) + _ = res_action.get_topological_impact(powerline_status, _store_in_cache=True, _read_from_cache=False) if type(self).dim_alerts: res_action.raise_alert = action.raise_alert is_illegal_redisp = True @@ -3023,7 +3027,9 @@ def _aux_apply_redisp(self, action, new_p, new_p_th, gen_curtailed, except_): except_tmp = self._handle_updown_times(gen_up_before, self._actual_dispatch) if except_tmp is not None: is_illegal_reco = True + action.reset_cache_topological_impact() res_action = self._action_space({}) + _ = res_action.get_topological_impact(powerline_status, _store_in_cache=True, _read_from_cache=False) if type(self).dim_alerts: res_action.raise_alert = action.raise_alert except_.append(except_tmp) @@ -3115,7 +3121,7 @@ def _aux_register_env_converged(self, disc_lines, action, init_line_status, new_ self._timestep_overflow[~overflow_lines] = 0 # build the topological action "cooldown" - aff_lines, aff_subs = action.get_topological_impact(init_line_status) + aff_lines, aff_subs = action.get_topological_impact(_read_from_cache=True) if self._max_timestep_line_status_deactivated > 0: # i update the cooldown only when this does not impact the line disconnected for the # opponent or by maintenance for example @@ -3342,11 +3348,22 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, else: action.raise_alert = init_alert except_.append(except_tmp) - + + # speed optimization: during all the "env.step" the "topological impact" + # of an action is called multiple times, I cache the results + # at first iteration, self.current_obs is None so I cannot use self.current_obs.line_status + powerline_status = self.get_current_line_status() + # explicitly store in cache the topological impact (not to recompute it again and again) + # and this regardless of the + _ = action.get_topological_impact(powerline_status, _store_in_cache=True, _read_from_cache=False) + is_legal, reason = self._game_rules(action=action, env=self) if not is_legal: # action is replace by do nothing + action.reset_cache_topological_impact() action = self._action_space({}) + _ = action.get_topological_impact(powerline_status, _store_in_cache=True, _read_from_cache=False) + init_disp = 1.0 * action._redispatch # dispatching action action_storage_power = ( 1.0 * action._storage_power @@ -3390,7 +3407,7 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, # and it is also in this function that the limiting of the curtailment / storage actions # is perform to make the state "feasible" res_disp = self._aux_apply_redisp( - action, new_p, new_p_th, gen_curtailed, except_ + action, new_p, new_p_th, gen_curtailed, except_, powerline_status ) action, is_illegal_redisp, is_illegal_reco, is_done = res_disp @@ -3488,7 +3505,8 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, if self._update_obs_after_reward and self.current_obs is not None: # transfer some information computed in the reward into the obs (if any) self.current_obs.update_after_reward(self) - + + action.reset_cache_topological_impact() # TODO documentation on all the possible way to be illegal now if self.done: self.__is_init = False diff --git a/grid2op/Opponent/opponentSpace.py b/grid2op/Opponent/opponentSpace.py index bca588d4..042e32f5 100644 --- a/grid2op/Opponent/opponentSpace.py +++ b/grid2op/Opponent/opponentSpace.py @@ -241,6 +241,11 @@ def attack(self, observation, agent_action, env_action): attack_duration = self.current_attack_duration if attack is None: attack_duration = 0 + else: + if observation is not None: + # cache the get_topological_impact to avoid useless computation later + # this is a speed optimization + _ = attack.get_topological_impact(observation.line_status, _store_in_cache=True, _read_from_cache=False) return attack, attack_duration def close(self): diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 118d3f9c..f310cb6c 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -3034,6 +3034,7 @@ def init_grid(cls, gridobj, force=False, extra_name=None, force_module=None, _lo res_cls._compute_pos_big_topo_cls() res_cls.process_shunt_static_data() compat_mode = res_cls.process_grid2op_compat() + res_cls.process_detachment_compat() res_cls._check_convert_to_np_array() # convert everything to numpy array if force_module is not None: res_cls.__module__ = force_module # hack because otherwise it says "abc" which is not the case @@ -4410,7 +4411,9 @@ class res(GridObjects): # cls.set_env_name(f"{cls.env_name}_{cls.glop_version}") # and now post process the class attributes for that cls.process_grid2op_compat() - + + cls.process_detachment_compat() + if "assistant_warning_type" in dict_: cls.assistant_warning_type = dict_["assistant_warning_type"] else: @@ -4454,6 +4457,10 @@ def process_shunt_static_data(cls): """remove possible shunts data from the classes, if shunts are deactivated""" pass + @classmethod + def process_detachment_compat(cls): + pass + @classmethod def set_no_storage(cls): """ diff --git a/grid2op/tests/test_Action.py b/grid2op/tests/test_Action.py index a852b3ed..cb68325a 100644 --- a/grid2op/tests/test_Action.py +++ b/grid2op/tests/test_Action.py @@ -31,7 +31,7 @@ def _get_action_grid_class(): GridObjects._clear_class_attribute() GridObjects.env_name = "test_action_env" GridObjects.n_busbar_per_sub = 2 - GridObjects._allow_detachment = False + GridObjects.detachment_is_allowed = False GridObjects.n_gen = 5 GridObjects.name_gen = np.array(["gen_{}".format(i) for i in range(5)]) GridObjects.n_load = 11 @@ -870,20 +870,23 @@ def test_to_vect(self): "set_bus": {"substations_id": [(id_2, arr2)]}, } ) - - res = action.to_vect() - tmp = np.zeros(self.size_act) - if "curtail" in action.authorized_keys: + act_cls = type(action) + + act_serialized = action.to_vect() + th_res = np.zeros(self.size_act) + if "curtail" in act_cls.authorized_keys: # for curtailment, at the end, and by default its -1 - tmp[-action.n_gen :] = -1 - + th_res[-action.n_gen :] = -1 + # set to nan the first elements + # corresponding to prod_p, prod_v, load_p and load_q + th_res[:(2 * (act_cls.n_gen + act_cls.n_load))] = np.nan # compute the "set_bus" vect - id_set = np.nonzero(np.array(type(action).attr_list_vect) == "_set_topo_vect")[0][0] + id_set = np.nonzero(np.array(act_cls.attr_list_vect) == "_set_topo_vect")[0][0] size_before = 0 - for el in type(action).attr_list_vect[:id_set]: + for el in act_cls.attr_list_vect[:id_set]: arr_ = action._get_array_from_attr_name(el) size_before += arr_.shape[0] - tmp[size_before : (size_before + action.dim_topo)] = np.array( + th_res[size_before : (size_before + action.dim_topo)] = np.array( [ 0, 0, @@ -945,14 +948,14 @@ def test_to_vect(self): 0, ] ) - id_change = np.nonzero(np.array(type(action).attr_list_vect) == "_change_bus_vect")[0][ + id_change = np.nonzero(np.array(act_cls.attr_list_vect) == "_change_bus_vect")[0][ 0 ] size_before = 0 - for el in type(action).attr_list_vect[:id_change]: + for el in act_cls.attr_list_vect[:id_change]: arr_ = action._get_array_from_attr_name(el) size_before += arr_.shape[0] - tmp[size_before : (size_before + action.dim_topo)] = 1.0 * np.array( + th_res[size_before : (size_before + act_cls.dim_topo)] = 1.0 * np.array( [ False, False, @@ -1014,8 +1017,8 @@ def test_to_vect(self): False, ] ) - assert np.all(res[np.isfinite(tmp)] == tmp[np.isfinite(tmp)]) - assert np.all(np.isfinite(res) == np.isfinite(tmp)) + assert np.all(act_serialized[np.isfinite(th_res)] == th_res[np.isfinite(th_res)]) + assert np.all(np.isfinite(act_serialized) == np.isfinite(th_res)) def test__eq__(self): self._skipMissingKey("set_bus") diff --git a/grid2op/tests/test_shedding.py b/grid2op/tests/test_shedding.py index 3b06b60c..c9d7052f 100644 --- a/grid2op/tests/test_shedding.py +++ b/grid2op/tests/test_shedding.py @@ -112,83 +112,120 @@ def test_shedding_persistance(self): assert not done assert obs.topo_vect[load_pos] == -1 - def test_action_property_load(self): - act = self.env.action_space() - assert "detach_load" in type(act).authorized_keys - act.detach_load = np.ones(act.n_load, dtype=bool) - assert act._detach_load.all() - assert act._modif_detach_load +class TestSheddingActions(unittest.TestCase): + def setUp(self) -> None: + super().setUp() + p = Parameters() + p.MAX_SUB_CHANGED = 999999 + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make("educ_case14_storage", + param=p, + action_class=CompleteAction, + allow_detachment=True, + test=True, + _add_to_name=type(self).__name__) + obs = self.env.reset(seed=0, options={"time serie id": 0}) # Reproducibility + + def tearDown(self) -> None: + self.env.close() + + def aux_test_action_property_xxx(self, el_type): + detach_xxx = f"detach_{el_type}" + _detach_xxx = f"_detach_{el_type}" + _modif_detach_xxx = f"_modif_detach_{el_type}" + n_xxx = getattr(type(self.env), f"n_{el_type}") + name_xxx = getattr(type(self.env), f"name_{el_type}") + xxx_change_bus = f"{el_type}_change_bus" + xxx_set_bus = f"{el_type}_set_bus" + + act1 = self.env.action_space() + assert detach_xxx in type(act1).authorized_keys + setattr(act1, detach_xxx, np.ones(n_xxx, dtype=bool)) + assert getattr(act1, _detach_xxx).all() + assert getattr(act1, _modif_detach_xxx) act2 = self.env.action_space() - act2.detach_load = 1 - assert act2._detach_load[1] - assert act2._modif_detach_load + setattr(act2, detach_xxx, 1) + assert getattr(act2, _detach_xxx)[1] + assert getattr(act2, _modif_detach_xxx) act3 = self.env.action_space() - act3.detach_load = [0, 2] - assert act3._detach_load[0] - assert act3._detach_load[2] - assert act3._modif_detach_load + setattr(act3, detach_xxx, [0, 1]) + assert getattr(act3, _detach_xxx)[0] + assert getattr(act3, _detach_xxx)[1] + assert getattr(act3, _modif_detach_xxx) - for k, v in self.load_lookup.items(): + for el_id, el_nm in enumerate(name_xxx): act4 = self.env.action_space() - act4.detach_load = {k} - assert act4._detach_load[v] - assert act4._modif_detach_load + setattr(act4, detach_xxx, {el_nm}) + assert getattr(act4, _detach_xxx)[el_id] + assert getattr(act4, _modif_detach_xxx) # change and disconnect - act = self.env.action_space() - act.load_change_bus = [0] - act.detach_load = [0] - is_amb, exc_ = act.is_ambiguous() - assert is_amb - assert isinstance(exc_, AmbiguousAction) + act5 = self.env.action_space() + setattr(act5, xxx_change_bus, [0]) + setattr(act5, detach_xxx, [0]) + is_amb, exc_ = act5.is_ambiguous() + assert is_amb, f"error for {el_type}" + assert isinstance(exc_, AmbiguousAction), f"error for {el_type}" # set_bus and disconnect - act = self.env.action_space() - act.load_set_bus = [(0, 1)] - act.detach_load = [0] - is_amb, exc_ = act.is_ambiguous() - assert is_amb - assert isinstance(exc_, AmbiguousAction) + act6 = self.env.action_space() + setattr(act6, xxx_set_bus, [(0, 1)]) + setattr(act6, detach_xxx, [0]) + is_amb, exc_ = act6.is_ambiguous() + assert is_amb, f"error for {el_type}" + assert isinstance(exc_, AmbiguousAction), f"error for {el_type}" # flag not set - act = self.env.action_space() - act._detach_load[0] = True - is_amb, exc_ = act.is_ambiguous() - assert is_amb - assert isinstance(exc_, AmbiguousAction) - - # test to / from dict - act = self.env.action_space() - act.detach_load = [0] - dict_ = act.as_serializable_dict() # you can save this dict with the json library - act2 = self.env.action_space(dict_) - act == act2 + act7 = self.env.action_space() + getattr(act7, _detach_xxx)[0] = True + is_amb, exc_ = act7.is_ambiguous() + assert is_amb, f"error for {el_type}" + assert isinstance(exc_, AmbiguousAction), f"error for {el_type}" - # test to / from json - act = self.env.action_space() - act.detach_load = [0] - dict_ = act.to_json() - with tempfile.NamedTemporaryFile() as f_tmp: - with open(f_tmp.name, "w", encoding="utf-8") as f: - json.dump(obj=dict_, fp=f) - - with open(f_tmp.name, "r", encoding="utf-8") as f: - dict_reload = json.load(fp=f) + for el_id in range(n_xxx): + # test to / from dict + act8 = self.env.action_space() + setattr(act8, detach_xxx, [el_id]) + dict_ = act8.as_serializable_dict() # you can save this dict with the json library + act8_reloaded = self.env.action_space(dict_) + assert act8 == act8_reloaded, f"error for {el_type} for id {el_id}" + + # test to / from json + act9 = self.env.action_space() + setattr(act9, detach_xxx, [el_id]) + dict_ = act9.to_json() + with tempfile.NamedTemporaryFile() as f_tmp: + with open(f_tmp.name, "w", encoding="utf-8") as f: + json.dump(obj=dict_, fp=f) + + with open(f_tmp.name, "r", encoding="utf-8") as f: + dict_reload = json.load(fp=f) + act9_reloaded = self.env.action_space() + act9_reloaded.from_json(dict_reload) + assert act9 == act9_reloaded, f"error for {el_type} for id {el_id}" + + # test to / from vect + act10 = self.env.action_space() + setattr(act10, detach_xxx, [el_id]) + vect_ = act10.to_vect() + act10_reloaded = self.env.action_space() + act10_reloaded.from_vect(vect_) + assert act10 == act10_reloaded, f"error for {el_type} for id {el_id}" + + def test_action_property_load(self): + self.aux_test_action_property_xxx("load") - act_reload = self.env.action_space() - act_reload.from_json(dict_reload) - assert act == act_reload + def test_action_property_gen(self): + self.aux_test_action_property_xxx("gen") - # test to / from vect - act = self.env.action_space() - act.detach_load = [0] - vect_ = act.to_vect() - act_reload = self.env.action_space() - act_reload.from_vect(vect_) - assert act == act_reload - + def test_action_property_storage(self): + self.aux_test_action_property_xxx("storage") + +# TODO Shedding: test the affected_lines, affected_subs of the action + # TODO Shedding: test when backend does not support it is not set # TODO shedding: test when user deactivates it it is not set From 62a1471d14c61780c387800cd0e803ee1ef0df0d Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 5 Dec 2024 09:18:46 +0100 Subject: [PATCH 43/60] fix a bug when deserializing actions Signed-off-by: DONNOT Benjamin --- grid2op/Action/baseAction.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 5e1276aa..9a16974f 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -1014,7 +1014,7 @@ def _set_array_from_attr_name(self, allowed_keys, key: str, array_) -> None: def _post_process_from_vect(self): modif_inj = False if self._dict_inj != {}: - for k, v in self._dict_inj: + for k, v in self._dict_inj.items(): if np.isfinite(v).any(): modif_inj = True self._modif_inj = modif_inj @@ -1029,9 +1029,10 @@ def _post_process_from_vect(self): self._modif_curtailment = (np.abs(self._curtail + 1.0) >= 1e-7).any() self._modif_alarm = self._raise_alarm.any() self._modif_alert = self._raise_alert.any() - self._modif_detach_load = (self._detach_load).any() - self._modif_detach_gen = (self._detach_gen).any() - self._modif_detach_storage = (self._detach_storage).any() + if type(self).detachment_is_allowed: + self._modif_detach_load = (self._detach_load).any() + self._modif_detach_gen = (self._detach_gen).any() + self._modif_detach_storage = (self._detach_storage).any() def _assign_attr_from_name(self, attr_nm, vect): """used for from_vect, for from_json please see `_set_array_from_attr_name`""" From 777d77858df549f7a90089c6c5397c353fc74184 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 5 Dec 2024 09:40:39 +0100 Subject: [PATCH 44/60] fix some other bugs in the tests Signed-off-by: DONNOT Benjamin --- grid2op/tests/test_Action.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/grid2op/tests/test_Action.py b/grid2op/tests/test_Action.py index cb68325a..9a98f4e1 100644 --- a/grid2op/tests/test_Action.py +++ b/grid2op/tests/test_Action.py @@ -879,7 +879,8 @@ def test_to_vect(self): th_res[-action.n_gen :] = -1 # set to nan the first elements # corresponding to prod_p, prod_v, load_p and load_q - th_res[:(2 * (act_cls.n_gen + act_cls.n_load))] = np.nan + if "injection" in act_cls.authorized_keys: + th_res[:(2 * (act_cls.n_gen + act_cls.n_load))] = np.nan # compute the "set_bus" vect id_set = np.nonzero(np.array(act_cls.attr_list_vect) == "_set_topo_vect")[0][0] size_before = 0 From 24c6cf6ad3b0026d799375690444fdea0081dc01 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 5 Dec 2024 15:58:59 +0100 Subject: [PATCH 45/60] try to fix ci bug that I cannot reproduce Signed-off-by: DONNOT Benjamin --- grid2op/Space/GridObjects.py | 21 +++++++++++---------- grid2op/tests/test_shedding.py | 6 +++++- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index f310cb6c..96e8a749 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -690,9 +690,6 @@ def _clear_class_attribute(cls) -> None: This clear the class as if it was defined in grid2op directly. """ - cls.shunts_data_available = False - cls.n_busbar_per_sub = DEFAULT_N_BUSBAR_PER_SUB - cls.detachment_is_allowed = DEFAULT_ALLOW_DETACHMENT # for redispatching / unit commitment cls._li_attr_disp = [ @@ -724,6 +721,13 @@ def _clear_class_attribute(cls) -> None: float, bool, ] + + cls.SUB_COL = 0 + cls.LOA_COL = 1 + cls.GEN_COL = 2 + cls.LOR_COL = 3 + cls.LEX_COL = 4 + cls.STORAGE_COL = 5 cls._clear_grid_dependant_class_attributes() @@ -734,15 +738,12 @@ def _clear_grid_dependant_class_attributes(cls) -> None: cls._INIT_GRID_CLS = None # do not modify that, this is handled by grid2op automatically cls._PATH_GRID_CLASSES = None # especially do not modify that + cls.shunts_data_available = False + cls.n_busbar_per_sub = DEFAULT_N_BUSBAR_PER_SUB + cls.detachment_is_allowed = DEFAULT_ALLOW_DETACHMENT + cls.glop_version = grid2op.__version__ - cls.SUB_COL = 0 - cls.LOA_COL = 1 - cls.GEN_COL = 2 - cls.LOR_COL = 3 - cls.LEX_COL = 4 - cls.STORAGE_COL = 5 - cls.attr_list_vect = None cls.attr_list_set = {} cls.attr_list_json = [] diff --git a/grid2op/tests/test_shedding.py b/grid2op/tests/test_shedding.py index c9d7052f..67178ae8 100644 --- a/grid2op/tests/test_shedding.py +++ b/grid2op/tests/test_shedding.py @@ -112,6 +112,7 @@ def test_shedding_persistance(self): assert not done assert obs.topo_vect[load_pos] == -1 + class TestSheddingActions(unittest.TestCase): def setUp(self) -> None: super().setUp() @@ -126,7 +127,10 @@ def setUp(self) -> None: test=True, _add_to_name=type(self).__name__) obs = self.env.reset(seed=0, options={"time serie id": 0}) # Reproducibility - + assert type(self.env).detachment_is_allowed + assert type(obs).detachment_is_allowed + assert type(self.env.action_space()).detachment_is_allowed + def tearDown(self) -> None: self.env.close() From 17afbf40634463ec6d6bb45a2670bc1ff1683624 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 5 Dec 2024 17:52:43 +0100 Subject: [PATCH 46/60] fix some issue when resetting the class attributes, hopefully Signed-off-by: DONNOT Benjamin --- grid2op/Environment/environment.py | 5 ++++- grid2op/Space/GridObjects.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index da6038b5..1483236b 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -268,8 +268,10 @@ def _init_backend( # hack for legacy lightsim2grid ... if type(self.backend.init_pp_backend)._INIT_GRID_CLS is not None: type(self.backend.init_pp_backend)._INIT_GRID_CLS._clear_grid_dependant_class_attributes() + type(self.backend.init_pp_backend)._INIT_GRID_CLS.shunts_data_available = self.backend.shunts_data_available type(self.backend.init_pp_backend)._clear_grid_dependant_class_attributes() - + type(self.backend.init_pp_backend).shunts_data_available = self.backend.shunts_data_available + # usual case: the backend is not loaded # NB it is loaded when the backend comes from an observation for # example @@ -281,6 +283,7 @@ def _init_backend( # this is due to the class attribute type(self.backend).set_env_name(self.name) type(self.backend).set_n_busbar_per_sub(self._n_busbar) + type(self.backend).shunts_data_available = self.backend.shunts_data_available type(self.backend).set_detachment_is_allowed(self._allow_detachment) if self._compat_glop_version is not None: type(self.backend).glop_version = self._compat_glop_version diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 96e8a749..e19d0aa5 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -691,6 +691,10 @@ def _clear_class_attribute(cls) -> None: This clear the class as if it was defined in grid2op directly. """ + #: this has to be here and not in _clear_grid_dependant_class_attributes + # otherwise it breaks some lightsim2grid versions + cls.shunts_data_available = False + # for redispatching / unit commitment cls._li_attr_disp = [ "gen_type", @@ -738,7 +742,6 @@ def _clear_grid_dependant_class_attributes(cls) -> None: cls._INIT_GRID_CLS = None # do not modify that, this is handled by grid2op automatically cls._PATH_GRID_CLASSES = None # especially do not modify that - cls.shunts_data_available = False cls.n_busbar_per_sub = DEFAULT_N_BUSBAR_PER_SUB cls.detachment_is_allowed = DEFAULT_ALLOW_DETACHMENT From 1c0220f4751e9d7b79ef7652e795664547637a69 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 6 Dec 2024 16:05:48 +0100 Subject: [PATCH 47/60] trying to fix bug on CI Signed-off-by: DONNOT Benjamin --- grid2op/Action/actionSpace.py | 4 ++-- grid2op/Action/baseAction.py | 29 +++++++++++++++++++-------- grid2op/Space/GridObjects.py | 15 +++++++++----- grid2op/tests/test_score_wcci_2022.py | 2 +- grid2op/tests/test_shedding.py | 4 +++- 5 files changed, 37 insertions(+), 17 deletions(-) diff --git a/grid2op/Action/actionSpace.py b/grid2op/Action/actionSpace.py index 2b55406e..5c269491 100644 --- a/grid2op/Action/actionSpace.py +++ b/grid2op/Action/actionSpace.py @@ -70,8 +70,8 @@ def __init__( Class specifying the rules of the game used to check the legality of the actions. """ - actionClass._add_shunt_data() - actionClass._update_value_set() + # actionClass._add_shunt_data() + # actionClass._update_value_set() SerializableActionSpace.__init__(self, gridobj, actionClass=actionClass, _local_dir_cls=_local_dir_cls) self.legal_action = legal_action diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 9a16974f..dc879b94 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -397,9 +397,9 @@ class BaseAction(GridObjects): ] # new in 1.11.0 (was not set to nan before in serialization) attr_nan_list_set = set(["prod_p", - "prod_v", - "load_p", - "load_q"]) + "prod_v", + "load_p", + "load_q"]) # attr_nan_list_set = set() attr_list_set = set(attr_list_vect) @@ -533,11 +533,11 @@ def process_shunt_static_data(cls): return super().process_shunt_static_data() @classmethod - def process_detachment_compat(cls): + def process_detachment(cls): if not cls.detachment_is_allowed: # this is really important, otherwise things from grid2op base types will be affected cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) - cls.attr_list_set = copy.deepcopy(cls.attr_list_set) + cls.authorized_keys = copy.deepcopy(cls.authorized_keys) # remove the detachment from the list to vector for el in ["_detach_load", "_detach_gen", "_detach_storage"]: if el in cls.attr_list_vect: @@ -550,8 +550,21 @@ def process_detachment_compat(cls): if el in cls.authorized_keys: cls.authorized_keys.remove(el) cls._update_value_set() - return super().process_detachment_compat() - + # else: + # # I support detachment, I need to make sure this is registered + # cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) + # cls.attr_list_set = copy.deepcopy(cls.attr_list_set) + # # add the detachment from the list to vector + # for el in ["_detach_load", "_detach_gen", "_detach_storage"]: + # if el not in cls.attr_list_vect: + # cls.attr_list_vect.append(el) + # # add the detachment from the allowed action + # for el in ["detach_load", "detach_gen", "detach_storage"]: + # if el not in cls.authorized_keys: + # cls.authorized_keys.add(el) + # cls._update_value_set() + return super().process_detachment() + def copy(self) -> "BaseAction": # sometimes this method is used... return self.__deepcopy__() @@ -898,7 +911,7 @@ def process_grid2op_compat(cls): # this feature did not exist before. cls.dim_alerts = 0 - if glop_ver < version.parse("1.11.0.dev2"): + if glop_ver < cls.MIN_VERSION_DETACH: # this feature did not exist before. cls.authorized_keys = copy.deepcopy(cls.authorized_keys) cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index e19d0aa5..587cc08f 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -479,6 +479,7 @@ class GridObjects: """ BEFORE_COMPAT_VERSION : ClassVar[str] = "neurips_2020_compat" + MIN_VERSION_DETACH : ClassVar[str] = version.parse("1.11.0.dev2") glop_version : ClassVar[str] = grid2op.__version__ _INIT_GRID_CLS = None # do not modify that, this is handled by grid2op automatically @@ -2965,6 +2966,11 @@ def init_grid(cls, gridobj, force=False, extra_name=None, force_module=None, _lo """ # nothing to do now that the value are class member + + from grid2op.Action import BaseAction + if issubclass(cls, BaseAction): + print(f"\t-{BaseAction.authorized_keys = }\n") + name_res = "{}_{}".format(cls.__name__, gridobj.env_name) if gridobj.glop_version != grid2op.__version__: name_res += f"_{gridobj.glop_version}" @@ -3032,13 +3038,12 @@ def init_grid(cls, gridobj, force=False, extra_name=None, force_module=None, _lo else: # i am the original class from grid2op res_cls._INIT_GRID_CLS = cls - res_cls._IS_INIT = True res_cls._compute_pos_big_topo_cls() res_cls.process_shunt_static_data() compat_mode = res_cls.process_grid2op_compat() - res_cls.process_detachment_compat() + res_cls.process_detachment() res_cls._check_convert_to_np_array() # convert everything to numpy array if force_module is not None: res_cls.__module__ = force_module # hack because otherwise it says "abc" which is not the case @@ -3110,7 +3115,7 @@ def process_grid2op_compat(cls): cls.n_busbar_per_sub = DEFAULT_N_BUSBAR_PER_SUB res = True - if glop_ver < version.parse("1.11.0.dev2"): + if glop_ver < cls.MIN_VERSION_DETACH: # Detachment did not exist, default value should have # no effect cls.detachment_is_allowed = DEFAULT_ALLOW_DETACHMENT @@ -4416,7 +4421,7 @@ class res(GridObjects): # and now post process the class attributes for that cls.process_grid2op_compat() - cls.process_detachment_compat() + cls.process_detachment() if "assistant_warning_type" in dict_: cls.assistant_warning_type = dict_["assistant_warning_type"] @@ -4462,7 +4467,7 @@ def process_shunt_static_data(cls): pass @classmethod - def process_detachment_compat(cls): + def process_detachment(cls): pass @classmethod diff --git a/grid2op/tests/test_score_wcci_2022.py b/grid2op/tests/test_score_wcci_2022.py index 24bd9fc2..6d43e2fe 100644 --- a/grid2op/tests/test_score_wcci_2022.py +++ b/grid2op/tests/test_score_wcci_2022.py @@ -11,6 +11,7 @@ import numpy as np import grid2op +from grid2op.Action import BaseAction from grid2op.Agent import (BaseAgent, DoNothingAgent) from grid2op.Reward import L2RPNWCCI2022ScoreFun from grid2op.utils import ScoreL2RPN2022 @@ -32,7 +33,6 @@ def setUp(self) -> None: self.scen_id = 0 self.nb_scenario = 2 self.max_iter = 13 - with warnings.catch_warnings(): warnings.filterwarnings("ignore") self.env = grid2op.make("educ_case14_storage", diff --git a/grid2op/tests/test_shedding.py b/grid2op/tests/test_shedding.py index 67178ae8..f07a08fb 100644 --- a/grid2op/tests/test_shedding.py +++ b/grid2op/tests/test_shedding.py @@ -13,6 +13,7 @@ import tempfile import grid2op +from grid2op.Action.baseAction import BaseAction from grid2op.Exceptions import AmbiguousAction from grid2op.Action import CompleteAction from grid2op.Parameters import Parameters @@ -133,6 +134,7 @@ def setUp(self) -> None: def tearDown(self) -> None: self.env.close() + super().tearDown() def aux_test_action_property_xxx(self, el_type): detach_xxx = f"detach_{el_type}" @@ -144,7 +146,7 @@ def aux_test_action_property_xxx(self, el_type): xxx_set_bus = f"{el_type}_set_bus" act1 = self.env.action_space() - assert detach_xxx in type(act1).authorized_keys + assert detach_xxx in type(act1).authorized_keys, f"{detach_xxx} not in {type(act1).authorized_keys}" setattr(act1, detach_xxx, np.ones(n_xxx, dtype=bool)) assert getattr(act1, _detach_xxx).all() assert getattr(act1, _modif_detach_xxx) From eb4991036c46efebfb6286f2d19f99d8b7a06ce6 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 6 Dec 2024 16:18:53 +0100 Subject: [PATCH 48/60] trying to fix bug on CI Signed-off-by: DONNOT Benjamin --- grid2op/Space/GridObjects.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 587cc08f..d6a9b147 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -2965,12 +2965,7 @@ def init_grid(cls, gridobj, force=False, extra_name=None, force_module=None, _lo it does not initialize it. Setting "force=True" will bypass this check and update it accordingly. """ - # nothing to do now that the value are class member - - from grid2op.Action import BaseAction - if issubclass(cls, BaseAction): - print(f"\t-{BaseAction.authorized_keys = }\n") - + # nothing to do now that the value are class member name_res = "{}_{}".format(cls.__name__, gridobj.env_name) if gridobj.glop_version != grid2op.__version__: name_res += f"_{gridobj.glop_version}" From 8dcf4aa4c2ca759d4b21d8adf90bb72b16b13998 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 12 Dec 2024 14:35:05 +0100 Subject: [PATCH 49/60] adding support for detachment in backend action Signed-off-by: DONNOT Benjamin --- grid2op/Action/_backendAction.py | 19 +++++++--- grid2op/Action/baseAction.py | 12 +++++++ grid2op/Backend/backend.py | 4 +-- grid2op/tests/test_shedding.py | 60 +++++++++++++++++++++++++++++++- 4 files changed, 87 insertions(+), 8 deletions(-) diff --git a/grid2op/Action/_backendAction.py b/grid2op/Action/_backendAction.py index 1e8e869c..c873a0f2 100644 --- a/grid2op/Action/_backendAction.py +++ b/grid2op/Action/_backendAction.py @@ -760,13 +760,15 @@ def __iadd__(self, other : BaseAction) -> Self: """ - set_status = other._set_line_status + set_status = 1 * other._set_line_status switch_status = other._switch_line_status - set_topo_vect = other._set_topo_vect + set_topo_vect = 1 * other._set_topo_vect switcth_topo_vect = other._change_bus_vect redispatching = other._redispatch storage_power = other._storage_power - + modif_set_bus = other._modif_set_bus + cls = type(self) + # I deal with injections # Ia set the injection if other._modif_inj: @@ -781,8 +783,15 @@ def __iadd__(self, other : BaseAction) -> Self: self.storage_power.set_val(storage_power) # II shunts - if type(self).shunts_data_available: + if cls.shunts_data_available: self._aux_iadd_shunt(other) + + # III detachment (before all else) + if cls.detachment_is_allowed and other.has_element_detached(): + set_topo_vect[cls.load_pos_topo_vect[other._detach_load]] = -1 + set_topo_vect[cls.gen_pos_topo_vect[other._detach_gen]] = -1 + set_topo_vect[cls.storage_pos_topo_vect[other._detach_storage]] = -1 + modif_set_bus = True # III line status # this need to be done BEFORE the topology, as a connected powerline will be connected to their old bus. @@ -813,7 +822,7 @@ def __iadd__(self, other : BaseAction) -> Self: # IV topo if other._modif_change_bus: self.current_topo.change_val(switcth_topo_vect) - if other._modif_set_bus: + if modif_set_bus: self.current_topo.set_val(set_topo_vect) # V Force disconnected status diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index dc879b94..52e6bbdb 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -1521,6 +1521,12 @@ def get_topological_impact(self, effective_change[cls.line_ex_pos_topo_vect[disco_set_ex]] = False _subs_impacted[cls._topo_vect_to_sub[effective_change]] = True + + if cls.detachment_is_allowed: + # added for detachment: it can also affect substations + _subs_impacted[cls.load_to_subid[self._detach_load]] = True + _subs_impacted[cls.gen_to_subid[self._detach_gen]] = True + _subs_impacted[cls.storage_to_subid[self._detach_storage]] = True if _store_in_cache: # store the results in cache if asked too @@ -7248,3 +7254,9 @@ def remove_change(self) -> "BaseAction": self._switch_line_status[:] = False self._modif_change_status = False return self + + def has_element_detached(self): + """Return whether or not this action impact some elements with `detach`, for example + `detach_load`, `detach_gen` or `detach_storage` + """ + return self._modif_detach_gen or self._modif_detach_load or self._modif_detach_storage diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index 2dc61d7b..501b4027 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -116,8 +116,8 @@ class Backend(GridObjects, ABC): IS_BK_CONVERTER : bool = False # action to set me - my_bk_act_class : "Optional[grid2op.Action._backendAction._BackendAction]"= None - _complete_action_class : "Optional[grid2op.Action.CompleteAction]"= None + my_bk_act_class : "Optional[grid2op.Action._backendAction._BackendAction]" = None + _complete_action_class : "Optional[grid2op.Action.CompleteAction]" = None ERR_INIT_POWERFLOW : str = "Power cannot be computed on the first time step, please check your data." ERR_DETACHMENT : str = ("One or more {} were isolated from the grid " diff --git a/grid2op/tests/test_shedding.py b/grid2op/tests/test_shedding.py index f07a08fb..19af2597 100644 --- a/grid2op/tests/test_shedding.py +++ b/grid2op/tests/test_shedding.py @@ -17,6 +17,7 @@ from grid2op.Exceptions import AmbiguousAction from grid2op.Action import CompleteAction from grid2op.Parameters import Parameters +from grid2op.Action._backendAction import _BackendAction class TestShedding(unittest.TestCase): @@ -144,29 +145,42 @@ def aux_test_action_property_xxx(self, el_type): name_xxx = getattr(type(self.env), f"name_{el_type}") xxx_change_bus = f"{el_type}_change_bus" xxx_set_bus = f"{el_type}_set_bus" + xxx_to_subid = getattr(type(self.env),f"{el_type}_to_subid") act1 = self.env.action_space() assert detach_xxx in type(act1).authorized_keys, f"{detach_xxx} not in {type(act1).authorized_keys}" setattr(act1, detach_xxx, np.ones(n_xxx, dtype=bool)) assert getattr(act1, _detach_xxx).all() assert getattr(act1, _modif_detach_xxx) + lines_imp, subs_imp = act1.get_topological_impact(_read_from_cache=False) + assert subs_imp[xxx_to_subid].all() + assert (~lines_imp).all() act2 = self.env.action_space() setattr(act2, detach_xxx, 1) assert getattr(act2, _detach_xxx)[1] assert getattr(act2, _modif_detach_xxx) + lines_imp, subs_imp = act2.get_topological_impact(_read_from_cache=False) + assert subs_imp[xxx_to_subid[1]].all() + assert (~lines_imp).all() act3 = self.env.action_space() setattr(act3, detach_xxx, [0, 1]) assert getattr(act3, _detach_xxx)[0] assert getattr(act3, _detach_xxx)[1] assert getattr(act3, _modif_detach_xxx) + lines_imp, subs_imp = act3.get_topological_impact(_read_from_cache=False) + assert subs_imp[xxx_to_subid[[0, 1]]].all() + assert (~lines_imp).all() for el_id, el_nm in enumerate(name_xxx): act4 = self.env.action_space() setattr(act4, detach_xxx, {el_nm}) assert getattr(act4, _detach_xxx)[el_id] assert getattr(act4, _modif_detach_xxx) + lines_imp, subs_imp = act4.get_topological_impact(_read_from_cache=False) + assert subs_imp[xxx_to_subid[el_id]].all() + assert (~lines_imp).all() # change and disconnect act5 = self.env.action_space() @@ -230,7 +244,51 @@ def test_action_property_gen(self): def test_action_property_storage(self): self.aux_test_action_property_xxx("storage") -# TODO Shedding: test the affected_lines, affected_subs of the action + def test_backend_action(self): + for load_id in range(self.env.n_load): + bk_act :_BackendAction = self.env.backend.my_bk_act_class() + act = self.env.action_space() + act.detach_load = load_id + assert act._detach_load[load_id] + bk_act += act + ( + active_bus, + (prod_p, prod_v, load_p, load_q, storage), + topo__, + shunts__, + ) = bk_act() + assert topo__.changed[self.env.load_pos_topo_vect[load_id]], f"error for load {load_id}" + assert topo__.values[self.env.load_pos_topo_vect[load_id]] == -1, f"error for load {load_id}" + + for gen_id in range(self.env.n_gen): + bk_act :_BackendAction = self.env.backend.my_bk_act_class() + act = self.env.action_space() + act.detach_gen = gen_id + assert act._detach_gen[gen_id] + bk_act += act + ( + active_bus, + (prod_p, prod_v, load_p, load_q, storage), + topo__, + shunts__, + ) = bk_act() + assert topo__.changed[self.env.gen_pos_topo_vect[gen_id]], f"error for gen {gen_id}" + assert topo__.values[self.env.gen_pos_topo_vect[gen_id]] == -1, f"error for gen {gen_id}" + + for sto_id in range(self.env.n_storage): + bk_act :_BackendAction = self.env.backend.my_bk_act_class() + act = self.env.action_space() + act.detach_storage = sto_id + assert act._detach_storage[sto_id] + bk_act += act + ( + active_bus, + (prod_p, prod_v, load_p, load_q, storage), + topo__, + shunts__, + ) = bk_act() + assert topo__.changed[self.env.storage_pos_topo_vect[sto_id]], f"error for storage {sto_id}" + assert topo__.values[self.env.storage_pos_topo_vect[sto_id]] == -1, f"error for storage {sto_id}" # TODO Shedding: test when backend does not support it is not set # TODO shedding: test when user deactivates it it is not set From d3415f97de87bb5f026880f9012e08f80829699e Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 13 Dec 2024 12:04:08 +0100 Subject: [PATCH 50/60] adding first working -for basic functionality- for shedding, end to end Signed-off-by: DONNOT Benjamin --- CHANGELOG.rst | 4 + grid2op/Action/_backendAction.py | 54 +++++- grid2op/Environment/baseEnv.py | 221 ++++++++++++++++++++----- grid2op/Exceptions/__init__.py | 12 +- grid2op/Exceptions/envExceptions.py | 46 +++++ grid2op/Observation/baseObservation.py | 156 ++++++++++++++++- grid2op/Parameters.py | 66 +++++++- grid2op/simulator/simulator.py | 1 + grid2op/tests/test_shedding.py | 153 +++++++++++++++++ 9 files changed, 652 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f99b908f..615251b4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -137,6 +137,10 @@ Native multi agents support: - [ADDED] a method to check the KCL (`obs.check_kirchhoff`) directly from the observation (previously it was only possible to do it from the backend). This should be used for testing purpose only +- [ADDED] parameters to disable the "redispatching routine" of the environment + (see `params.ENV_DOES_REDISPATCHING`) +- [ADDED] parameters to stop the episode when one of the constraints of one of the + generators is not met (see `params.STOP_EP_IF_SLACK_BREAK_CONSTRAINTS`) - [IMPROVED] possibility to set the injections values with names to be consistent with other way to set the actions (*eg* set_bus) - [IMPROVED] error messages when creating an action which changes the injections diff --git a/grid2op/Action/_backendAction.py b/grid2op/Action/_backendAction.py index c873a0f2..0434335c 100644 --- a/grid2op/Action/_backendAction.py +++ b/grid2op/Action/_backendAction.py @@ -767,8 +767,43 @@ def __iadd__(self, other : BaseAction) -> Self: redispatching = other._redispatch storage_power = other._storage_power modif_set_bus = other._modif_set_bus + modif_inj = other._modif_inj cls = type(self) + # III detachment (before all else) + if cls.detachment_is_allowed and other.has_element_detached(): + if other._modif_detach_load: + set_topo_vect[cls.load_pos_topo_vect[other._detach_load]] = -1 + modif_set_bus = True + if other._modif_detach_gen: + set_topo_vect[cls.gen_pos_topo_vect[other._detach_gen]] = -1 + modif_set_bus = True + if other._modif_detach_storage: + set_topo_vect[cls.storage_pos_topo_vect[other._detach_storage]] = -1 + modif_set_bus = True + if modif_inj: + for key, vect_ in other._dict_inj.items(): + if key == "load_p" or key == "load_q": + vect_[other._detach_load] = 0. + elif key == "prod_p": + vect_[other._detach_gen] = 0. + elif key == "prod_v": + vect_[other._detach_gen] = np.nan + else: + raise NotImplementedError(f"Unknown key {key} for injection found.") + else: + # TODO when injection is not modified by the action (eg change nothing) + if other._modif_detach_load: + modif_inj = True + other._dict_inj["load_p"] = np.full(cls.n_load, fill_value=np.nan, dtype=dt_float) + other._dict_inj["load_q"] = np.full(cls.n_load, fill_value=np.nan, dtype=dt_float) + other._dict_inj["load_p"][other._detach_load] = 0. + other._dict_inj["load_q"][other._detach_load] = 0. + if other._modif_detach_gen: + modif_inj = True + other._dict_inj["prod_p"] = np.full(cls.n_gen, fill_value=np.nan, dtype=dt_float) + other._dict_inj["prod_p"][other._detach_gen] = 0. + # I deal with injections # Ia set the injection if other._modif_inj: @@ -785,13 +820,6 @@ def __iadd__(self, other : BaseAction) -> Self: # II shunts if cls.shunts_data_available: self._aux_iadd_shunt(other) - - # III detachment (before all else) - if cls.detachment_is_allowed and other.has_element_detached(): - set_topo_vect[cls.load_pos_topo_vect[other._detach_load]] = -1 - set_topo_vect[cls.gen_pos_topo_vect[other._detach_gen]] = -1 - set_topo_vect[cls.storage_pos_topo_vect[other._detach_storage]] = -1 - modif_set_bus = True # III line status # this need to be done BEFORE the topology, as a connected powerline will be connected to their old bus. @@ -1453,3 +1481,15 @@ def update_state(self, powerline_disconnected) -> None: ) self.last_topo_registered.update_connected(self.current_topo) self.current_topo.reset() + + def get_load_detached(self): + cls = type(self) + return self.current_topo.values[cls.load_pos_topo_vect] == -1 + + def get_gen_detached(self): + cls = type(self) + return self.current_topo.values[cls.gen_pos_topo_vect] == -1 + + def get_sto_detached(self): + cls = type(self) + return self.current_topo.values[cls.storage_pos_topo_vect] == -1 diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index 3f666c59..9579ba85 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -40,7 +40,11 @@ GeneratorTurnedOffTooSoon, GeneratorTurnedOnTooSoon, AmbiguousActionRaiseAlert, - ImpossibleTopology) + ImpossibleTopology, + SomeGeneratorAbovePmax, + SomeGeneratorBelowPmin, + SomeGeneratorAboveRampmax, + SomeGeneratorBelowRampmin) from grid2op.Parameters import Parameters from grid2op.Reward import BaseReward, RewardHelper from grid2op.Opponent import OpponentSpace, NeverAttackBudget, BaseOpponent @@ -646,6 +650,21 @@ def __init__( # general things that can be used by the reward self._reward_to_obs = {} + + # detachement (1.11.0) + self._loads_detached = None + self._gens_detached = None + self._storages_detached = None + self._prev_load_p = None + self._load_p_detached = None + self._prev_load_q = None + self._load_q_detached = None + self._prev_gen_p = None + self._gen_p_detached = None + self._storage_p_detached = None + + # slack (1.11.0) + self._slack_gen_p = None @property def highres_sim_counter(self): @@ -951,6 +970,21 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None): # breaks for some version of lightsim2grid... (a powerflow need to be run to retrieve the observation) new_obj.current_obs = new_obj.get_obs() + # detachment (1.11.0) + new_obj._loads_detached = copy.deepcopy(self._loads_detached) + new_obj._gens_detached = copy.deepcopy(self._gens_detached) + new_obj._storages_detached = copy.deepcopy(self._storages_detached) + new_obj._prev_load_p = 1. * self.prev_load_p + new_obj._load_p_detached = 1. * self._load_p_detached + new_obj._prev_load_q = 1. * self._prev_load_q + new_obj._load_q_detached = 1. * self._load_q_detached + new_obj._prev_gen_p = 1. * self._prev_gen_p + new_obj._gen_p_detached = 1. * self._gen_p_detached + new_obj._storage_p_detached = 1. * self._storage_p_detached + + # slack (1.11.0) + new_obj._slack_gen_p = 1. * self._slack_gen_p + def get_path_env(self): """ Get the path that allows to create this environment. @@ -1340,41 +1374,41 @@ def _has_been_initialized(self): self._backend_action = self._backend_action_class() # initialize maintenance / hazards - self._time_next_maintenance = np.full(self.n_line, -1, dtype=dt_int) - self._duration_next_maintenance = np.zeros(shape=(self.n_line,), dtype=dt_int) + self._time_next_maintenance = np.full(bk_type.n_line, -1, dtype=dt_int) + self._duration_next_maintenance = np.zeros(shape=(bk_type.n_line,), dtype=dt_int) self._times_before_line_status_actionable = np.full( - shape=(self.n_line,), fill_value=0, dtype=dt_int + shape=(bk_type.n_line,), fill_value=0, dtype=dt_int ) # create the vector to the proper shape - self._target_dispatch = np.zeros(self.n_gen, dtype=dt_float) - self._already_modified_gen = np.zeros(self.n_gen, dtype=dt_bool) - self._actual_dispatch = np.zeros(self.n_gen, dtype=dt_float) - self._gen_uptime = np.zeros(self.n_gen, dtype=dt_int) - self._gen_downtime = np.zeros(self.n_gen, dtype=dt_int) - self._gen_activeprod_t = np.zeros(self.n_gen, dtype=dt_float) - self._gen_activeprod_t_redisp = np.zeros(self.n_gen, dtype=dt_float) + self._target_dispatch = np.zeros(bk_type.n_gen, dtype=dt_float) + self._already_modified_gen = np.zeros(bk_type.n_gen, dtype=dt_bool) + self._actual_dispatch = np.zeros(bk_type.n_gen, dtype=dt_float) + self._gen_uptime = np.zeros(bk_type.n_gen, dtype=dt_int) + self._gen_downtime = np.zeros(bk_type.n_gen, dtype=dt_int) + self._gen_activeprod_t = np.zeros(bk_type.n_gen, dtype=dt_float) + self._gen_activeprod_t_redisp = np.zeros(bk_type.n_gen, dtype=dt_float) self._max_timestep_line_status_deactivated = ( self._parameters.NB_TIMESTEP_COOLDOWN_LINE ) self._times_before_line_status_actionable = np.zeros( - shape=(self.n_line,), dtype=dt_int + shape=(bk_type.n_line,), dtype=dt_int ) self._times_before_topology_actionable = np.zeros( - shape=(self.n_sub,), dtype=dt_int + shape=(bk_type.n_sub,), dtype=dt_int ) self._nb_timestep_overflow_allowed = np.full( - shape=(self.n_line,), + shape=(bk_type.n_line,), fill_value=self._parameters.NB_TIMESTEP_OVERFLOW_ALLOWED, dtype=dt_int, ) self._hard_overflow_threshold = np.full( - shape=(self.n_line,), + shape=(bk_type.n_line,), fill_value=self._parameters.HARD_OVERFLOW_THRESHOLD, dtype=dt_float, ) - self._timestep_overflow = np.zeros(shape=(self.n_line,), dtype=dt_int) + self._timestep_overflow = np.zeros(shape=(bk_type.n_line,), dtype=dt_int) # update the parameters self.__new_param = self._parameters # small hack to have it working as expected @@ -1383,28 +1417,43 @@ def _has_been_initialized(self): self._reset_redispatching() # storage - self._storage_current_charge = np.zeros(self.n_storage, dtype=dt_float) - self._storage_previous_charge = np.zeros(self.n_storage, dtype=dt_float) - self._action_storage = np.zeros(self.n_storage, dtype=dt_float) - self._storage_power = np.zeros(self.n_storage, dtype=dt_float) - self._storage_power_prev = np.zeros(self.n_storage, dtype=dt_float) + self._storage_current_charge = np.zeros(bk_type.n_storage, dtype=dt_float) + self._storage_previous_charge = np.zeros(bk_type.n_storage, dtype=dt_float) + self._action_storage = np.zeros(bk_type.n_storage, dtype=dt_float) + self._storage_power = np.zeros(bk_type.n_storage, dtype=dt_float) + self._storage_power_prev = np.zeros(bk_type.n_storage, dtype=dt_float) self._amount_storage = 0.0 self._amount_storage_prev = 0.0 # curtailment self._limit_curtailment = np.ones( - self.n_gen, dtype=dt_float + bk_type.n_gen, dtype=dt_float ) # in ratio of pmax self._limit_curtailment_prev = np.ones( - self.n_gen, dtype=dt_float + bk_type.n_gen, dtype=dt_float ) # in ratio of pmax - self._gen_before_curtailment = np.zeros(self.n_gen, dtype=dt_float) # in MW + self._gen_before_curtailment = np.zeros(bk_type.n_gen, dtype=dt_float) # in MW self._sum_curtailment_mw = dt_float(0.0) self._sum_curtailment_mw_prev = dt_float(0.0) self._reset_curtailment() # register this is properly initialized self.__is_init = True + + # detachment (1.11.0) + self._loads_detached = np.zeros(bk_type.n_load, dtype=dt_bool) + self._gens_detached = np.zeros(bk_type.n_gen, dtype=dt_bool) + self._storages_detached = np.zeros(bk_type.n_storage, dtype=dt_bool) + self._prev_load_p = np.zeros(bk_type.n_load, dtype=dt_float) + self._load_p_detached = np.zeros(bk_type.n_load, dtype=dt_float) + self._prev_load_q = np.zeros(bk_type.n_load, dtype=dt_float) + self._load_q_detached = np.zeros(bk_type.n_load, dtype=dt_float) + self._prev_gen_p = np.zeros(bk_type.n_gen, dtype=dt_float) + self._gen_p_detached = np.zeros(bk_type.n_gen, dtype=dt_float) + self._storage_p_detached = np.zeros(bk_type.n_storage, dtype=dt_float) + + # slack (1.11.0) + self._slack_gen_p = np.zeros(bk_type.n_gen, dtype=dt_float) def _update_parameters(self): """update value for the new parameters""" @@ -1491,9 +1540,24 @@ def reset(self, self._reset_storage() self._reset_curtailment() self._reset_alert() + self._reset_slack_and_detachment() self._reward_to_obs = {} self._has_just_been_seeded = False + def _reset_slack_and_detachment(self): + self._loads_detached[:] = False + self._gens_detached[:] = False + self._storages_detached[:] = False + self._prev_load_p[:] = 0. + self._load_p_detached[:] = 0. + self._prev_load_q[:] = 0. + self._load_q_detached[:] = 0. + self._prev_gen_p[:] = 0. + self._gen_p_detached[:] = 0. + self._storage_p_detached[:] = 0. + + self._slack_gen_p[:] = 0. + def _reset_alert(self): self._last_alert[:] = False self._is_already_attacked[:] = False @@ -1609,7 +1673,7 @@ def seed(self, seed=None, _seed_me=True): raise Grid2OpException( "Impossible to seed with the seed provided. Make sure it can be converted to a" "numpy 32 bits integer." - ) + ) from exc_ # example from gym # self.np_random, seed = seeding.np_random(seed) # inspiration from @ https://github.com/openai/gym/tree/master/gym/utils @@ -1887,6 +1951,23 @@ def _reset_redispatching(self): self._gen_activeprod_t[:] = 0.0 self._gen_activeprod_t_redisp[:] = 0.0 + def _feed_data_for_detachment(self, new_p_th): + """feed the attribute for the detachment""" + + self._prev_gen_p[:] = new_p_th + self._aux_retrieve_modif_act(self._prev_load_p, self._env_modification, "load_p") + self._aux_retrieve_modif_act(self._prev_load_q, self._env_modification, "load_q") + + def _aux_retrieve_modif_act(self, + input_ : np.ndarray, + act: BaseAction, + key: Literal["prod_p", "prod_v", "load_p", "load_q"]): + """It does modify directly its imput !""" + if key in act._dict_inj: + tmp = act._dict_inj[key] + indx_ok = np.isfinite(tmp) + input_[indx_ok] = tmp[indx_ok] + def _get_new_prod_setpoint(self, action): """ NB this is overidden in _ObsEnv where the data are read from the action to set this environment @@ -1894,18 +1975,11 @@ def _get_new_prod_setpoint(self, action): """ # get the modification of generator active setpoint from the action new_p = 1.0 * self._gen_activeprod_t - if "prod_p" in action._dict_inj: - tmp = action._dict_inj["prod_p"] - indx_ok = np.isfinite(tmp) - new_p[indx_ok] = tmp[indx_ok] + self._aux_retrieve_modif_act(new_p, action, "prod_p") # modification of the environment always override the modification of the agents (if any) # TODO have a flag there if this is the case. - if "prod_p" in self._env_modification._dict_inj: - # modification of the production setpoint value - tmp = self._env_modification._dict_inj["prod_p"] - indx_ok = np.isfinite(tmp) - new_p[indx_ok] = tmp[indx_ok] + self._aux_retrieve_modif_act(new_p, self._env_modification, "prod_p") return new_p def _get_already_modified_gen(self, action): @@ -3094,11 +3168,10 @@ def _update_alert_properties(self, action, lines_attacked, subs_attacked): # TODO after alert budget will be implemented ! # self._is_alert_illegal - def _aux_register_env_converged(self, disc_lines, action, init_line_status, new_p): + def _aux_register_env_converged(self, disc_lines, action, init_line_status, new_p) -> bool: beg_res = time.perf_counter() - self.backend.update_thermal_limit( - self - ) # update the thermal limit, for DLR for example + # update the thermal limit, for DLR for example + self.backend.update_thermal_limit(self) overflow_lines = self.backend.get_line_overflow() # save the current topology as "last" topology (for connected powerlines) # and update the state of the disconnected powerline due to cascading failure @@ -3144,21 +3217,55 @@ def _aux_register_env_converged(self, disc_lines, action, init_line_status, new_ ] = self._max_timestep_topology_deactivated # extract production active value at this time step (should be independent of action class) - self._gen_activeprod_t[:], *_ = self.backend.generators_info() + tmp_gen_p, *_ = self.backend.generators_info() + if not self._parameters.STOP_EP_IF_SLACK_BREAK_CONSTRAINTS: + # default behaviour, no check performed + self._gen_activeprod_t[:] = tmp_gen_p + else: + # I need to check whether all generators meet the constraints + cls = type(self) + if tmp_gen_p[cls.gen_redispatchable] > cls.gen_pmax[cls.gen_redispatchable] + self._tol_poly: + gen_ko = (tmp_gen_p[cls.gen_redispatchable] > cls.gen_pmax[cls.gen_redispatchable]).nonzero()[0] + gen_ko_nms = cls.name_gen[cls.gen_redispatchable][gen_ko] + return SomeGeneratorAbovePmax(f"Especially generators id {gen_ko_nms}") + if tmp_gen_p[cls.gen_redispatchable] < cls.gen_pmin[cls.gen_redispatchable] - self._tol_pol: + gen_ko = (tmp_gen_p[cls.gen_redispatchable] < cls.gen_pmin[cls.gen_redispatchable]).nonzero()[0] + gen_ko_nms = cls.name_gen[cls.gen_redispatchable][gen_ko] + return SomeGeneratorBelowPmin(f"Especially generators {gen_ko_nms}") + diff_ = tmp_gen_p - self._gen_activeprod_t + + if diff_[cls.gen_redispatchable] > cls.gen_max_ramp_up[cls.gen_redispatchable] + self._tol_poly: + gen_ko = (diff_[cls.gen_redispatchable] > cls.gen_max_ramp_up[cls.gen_redispatchable]).nonzero()[0] + gen_ko_nms = cls.name_gen[cls.gen_redispatchable][gen_ko] + return SomeGeneratorAboveRampmax(f"Especially generators {gen_ko}") + if diff_[cls.gen_redispatchable] < -cls.gen_max_ramp_down[cls.gen_redispatchable] - self._tol_poly: + gen_ko = (diff_[cls.gen_redispatchable] < -cls.gen_max_ramp_down[cls.gen_redispatchable]).nonzero()[0] + gen_ko_nms = cls.name_gen[cls.gen_redispatchable][gen_ko] + return SomeGeneratorBelowRampmin(f"Especially generators {gen_ko_nms}") + + self._gen_activeprod_t[:] = tmp_gen_p + # problem with the gen_activeprod_t above, is that the slack bus absorbs alone all the losses # of the system. So basically, when it's too high (higher than the ramp) it can # mess up the rest of the environment self._gen_activeprod_t_redisp[:] = new_p + self._actual_dispatch # set the line status - self._line_status[:] = copy.deepcopy(self.backend.get_line_status()) + self._line_status[:] = self.backend.get_line_status() + # for detachment remember previous loads and generation + self._prev_load_p[:], self._prev_load_q[:], *_ = self.backend.loads_info() + self._slack_gen_p[:] = self._gen_activeprod_t - self._gen_activeprod_t_redisp + self._slack_gen_p[self._gens_detached] = 0. + self._prev_gen_p[:] = self._gen_activeprod_t + # finally, build the observation (it's a different one at each step, we cannot reuse the same one) # THIS SHOULD BE DONE AFTER EVERYTHING IS INITIALIZED ! self.current_obs = self.get_obs(_do_copy=False) # TODO storage: get back the result of the storage ! with the illegal action when a storage unit # TODO is non zero and disconnected, this should be ok. self._time_extract_obs += time.perf_counter() - beg_res + return None def _backend_next_grid_state(self): """overlaoded in MaskedEnv""" @@ -3169,6 +3276,25 @@ def _aux_run_pf_after_state_properly_set( ): has_error = True detailed_info = None + cls = type(self) + if cls.detachment_is_allowed: + self._loads_detached[:] = self._backend_action.get_load_detached() + self._gens_detached[:] = self._backend_action.get_gen_detached() + self._storages_detached[:] = self._backend_action.get_sto_detached() + + self._load_p_detached[:] = self._prev_load_p + self._load_p_detached[~self._loads_detached] = 0. + + self._load_q_detached[:] = self._prev_load_q + self._load_q_detached[~self._loads_detached] = 0. + + self._gen_p_detached[:] = self._prev_gen_p + self._gen_p_detached[~self._gens_detached] = 0. + + self._storage_p_detached[:] = 0. + self._storage_p_detached[:] = self._backend_action.storage_power.values + self._storage_p_detached[~self._storages_detached] = 0. + try: # compute the next _grid state beg_pf = time.perf_counter() @@ -3177,10 +3303,14 @@ def _aux_run_pf_after_state_properly_set( self._time_powerflow += time.perf_counter() - beg_pf if conv_ is None: # everything went well, so i register what is needed - self._aux_register_env_converged( + maybe_error = self._aux_register_env_converged( disc_lines, action, init_line_status, new_p ) - has_error = False + if maybe_error is None: + has_error = False + else: + has_error = True + except_.append(maybe_error) else: except_.append(conv_) except Grid2OpException as exc_: @@ -3389,7 +3519,8 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, ) new_p = self._get_new_prod_setpoint(action) new_p_th = 1.0 * new_p - + self._feed_data_for_detachment(new_p_th) + # storage unit if cls.n_storage > 0: # limiting the storage units is done in `_aux_apply_redisp` @@ -3398,11 +3529,11 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, # curtailment (does not attempt to "limit" the curtailment to make sure # it is feasible) - self._gen_before_curtailment[self.gen_renewable] = new_p[self.gen_renewable] + self._gen_before_curtailment[cls.gen_renewable] = new_p[cls.gen_renewable] gen_curtailed = self._aux_handle_curtailment_without_limit(action, new_p) beg__redisp = time.perf_counter() - if cls.redispatching_unit_commitment_availble or cls.n_storage > 0.0: + if (cls.redispatching_unit_commitment_availble or cls.n_storage > 0.0) and self._parameters.ENV_DOES_REDISPATCHING: # this computes the "optimal" redispatching # and it is also in this function that the limiting of the curtailment / storage actions # is perform to make the state "feasible" diff --git a/grid2op/Exceptions/__init__.py b/grid2op/Exceptions/__init__.py index f75a3bba..0e68b547 100644 --- a/grid2op/Exceptions/__init__.py +++ b/grid2op/Exceptions/__init__.py @@ -59,7 +59,11 @@ "NotEnoughAttentionBudget", "AgentError", "SimulatorError", - "HandlerError" + "HandlerError", + "SomeGeneratorAbovePmax", + "SomeGeneratorBelowPmin", + "SomeGeneratorAboveRampmax", + "SomeGeneratorBelowRampmin" ] from grid2op.Exceptions.grid2OpException import Grid2OpException @@ -76,7 +80,11 @@ IncorrectPositionOfLines, IncorrectPositionOfStorages, UnknownEnv, - MultiEnvException) + MultiEnvException, + SomeGeneratorAbovePmax, + SomeGeneratorBelowPmin, + SomeGeneratorAboveRampmax, + SomeGeneratorBelowRampmin) from grid2op.Exceptions.illegalActionExceptions import (IllegalAction, OnProduction, diff --git a/grid2op/Exceptions/envExceptions.py b/grid2op/Exceptions/envExceptions.py index 33d3f9b3..eb620e9d 100644 --- a/grid2op/Exceptions/envExceptions.py +++ b/grid2op/Exceptions/envExceptions.py @@ -111,6 +111,52 @@ class IncorrectPositionOfStorages(EnvError): pass +class SomeGeneratorAbovePmax(EnvError): + """This is a more precise exception saying that, at the end of the simulation, some generator would + have their production above pmax, which is not possible in practice. + + .. versionadded:: 1.11.0 + + This can only be triggered if :attr:`grid2op.Parameters.Parameters.STOP_EP_IF_SLACK_BREAK_CONSTRAINTS` + is ``True`` (which is not the default). + """ + pass + + +class SomeGeneratorBelowPmin(EnvError): + """This is a more precise exception saying that, at the end of the simulation, some generator would + have their production below pmin, which is not possible in practice. + + .. versionadded:: 1.11.0 + + This can only be triggered if :attr:`grid2op.Parameters.Parameters.STOP_EP_IF_SLACK_BREAK_CONSTRAINTS` + is ``True`` (which is not the default). + """ + pass + +class SomeGeneratorAboveRampmax(EnvError): + """This is a more precise exception saying that, at the end of the simulation, some generator would + have their production vary too much, which is not possible in practice. + + .. versionadded:: 1.11.0 + + This can only be triggered if :attr:`grid2op.Parameters.Parameters.STOP_EP_IF_SLACK_BREAK_CONSTRAINTS` + is ``True`` (which is not the default). + """ + pass + + +class SomeGeneratorBelowRampmin(EnvError): + """This is a more precise exception saying that, at the end of the simulation, some generator would + have their production vary too much, which is not possible in practice. + + .. versionadded:: 1.11.0 + + This can only be triggered if :attr:`grid2op.Parameters.Parameters.STOP_EP_IF_SLACK_BREAK_CONSTRAINTS` + is ``True`` (which is not the default). + """ + pass + # Unknown environment at creation class UnknownEnv(Grid2OpException): """ diff --git a/grid2op/Observation/baseObservation.py b/grid2op/Observation/baseObservation.py index 28b1f257..6b7a41c1 100644 --- a/grid2op/Observation/baseObservation.py +++ b/grid2op/Observation/baseObservation.py @@ -487,6 +487,16 @@ class BaseObservation(GridObjects): # gen up / down "gen_margin_up", "gen_margin_down", + # slack (>= 1.11.0) + "gen_p_slack", + # detachment (>= 1.11.0) + "load_detached", + "gen_detached", + "storage_detached", + "load_p_detached", + "load_q_detached", + "gen_p_detached", + "storage_p_detached", ] attr_list_vect = None @@ -620,6 +630,18 @@ def __init__(self, self.max_step = dt_int(np.iinfo(dt_int).max) self.delta_time = dt_float(5.0) + # slack (1.11.0) + self.gen_p_slack = np.empty(shape=cls.n_gen, dtype=dt_float) + + # detachment (>= 1.11.0) + self.load_detached = np.empty(shape=cls.n_load, dtype=dt_bool) + self.gen_detached = np.empty(shape=cls.n_gen, dtype=dt_bool) + self.storage_detached = np.empty(shape=cls.n_storage, dtype=dt_bool) + self.load_p_detached = np.empty(shape=cls.n_load, dtype=dt_float) + self.load_q_detached = np.empty(shape=cls.n_load, dtype=dt_float) + self.gen_p_detached = np.empty(shape=cls.n_gen, dtype=dt_float) + self.storage_p_detached = np.empty(shape=cls.n_storage, dtype=dt_float) + def _aux_copy(self, other : Self) -> None: attr_simple = [ "max_step", @@ -689,6 +711,16 @@ def _aux_copy(self, other : Self) -> None: "gen_margin_up", "gen_margin_down", "curtailment_limit_effective", + # slack (>= 1.11.0) + "gen_p_slack", + # detachment (>= 1.11.0) + "load_detached", + "gen_detached", + "storage_detached", + "load_p_detached", + "load_q_detached", + "gen_p_detached", + "storage_p_detached", ] if type(self).shunts_data_available: @@ -828,6 +860,9 @@ def state_of( - "theta" (optional) the voltage angle (in degree) of the bus to which the load is connected - "bus" on which bus the load is connected in the substation - "sub_id" the id of the substation to which the load is connected + - "detached" (>= 1.11.0) whether this load is detached from the grid + - "p_detached" (>= 1.11.0) amount of MW detached from the grid cause by the detachment of this load + - "q_detached" (>= 1.11.0) amount of MVAr detached from the grid cause by the detachment of this load - if a generator is inspected, then the keys are: @@ -840,6 +875,18 @@ def state_of( - "actual_dispatch" the actual dispatch implemented for this generator - "target_dispatch" the target dispatch (cumulation of all previously asked dispatch by the agent) for this generator + - "curtailment": the curtailment applied on this generator (0. for non renewable generator) + - "curtailment_limit": the curtailment limit as given by the agent + - "curtailment_limit_effective": the effective curtailment limit + - "p_before_curtail": the active production (in MW) before any curtailment is applied + this should be 0. for non renewable generator + - "margin_up": by how much this generateur can see its production increase between this step + and the next (in MW). It's 0. for renewable generators. + - "margin_down": by how much this generateur can see its production decrease between this step + and the next (in MW). It's 0. for renewable generators. + - "p_slack" (>= 1.11.0) slack participation (in MW) for this generator + - "detached" (>= 1.11.0) whether this generator is detached from the grid + - "p_detached" (>= 1.11.0) amount of MW detached from the grid cause by the detachment of this generator - if a powerline is inspected then the keys are "origin" and "extremity" each being dictionary with keys: @@ -867,6 +914,9 @@ def state_of( - "storage_theta": (optional) the voltage angle of the bus at which the storage unit is connected - "bus": the bus (1 or 2) to which the storage unit is connected - "sub_id" : the id of the substation to which the sotrage unit is connected + - "detached" (>= 1.11.0) whether this storage is detached from the grid + - "p_detached" (>= 1.11.0) amount of MW detached from the grid cause by the detachment of this storage + - if a substation is inspected, it returns the topology to this substation in a dictionary with keys: @@ -926,6 +976,9 @@ def state_of( "v": self.load_v[load_id], "bus": self.topo_vect[self.load_pos_topo_vect[load_id]], "sub_id": cls.load_to_subid[load_id], + "detached": self.load_detached[load_id], + "p_detached": self.load_p_detached[load_id], + "q_detached": self.load_q_detached[load_id], } if self.support_theta: res["theta"] = self.load_theta[load_id] @@ -959,6 +1012,9 @@ def state_of( "p_before_curtail": self.gen_p_before_curtail[gen_id], "margin_up": self.gen_margin_up[gen_id], "margin_down": self.gen_margin_down[gen_id], + "gen_p_slack": self.gen_p_slack[gen_id], + "detached": self.gen_detached[gen_id], + "p_detached": self.gen_p_detached[gen_id], } if self.support_theta: res["theta"] = self.gen_theta[gen_id] @@ -1026,6 +1082,8 @@ def state_of( res["storage_power_target"] = self.storage_power_target[storage_id] res["bus"] = self.topo_vect[cls.storage_pos_topo_vect[storage_id]] res["sub_id"] = cls.storage_to_subid[storage_id] + res["detached"] = self.storage_detached[storage_id], + res["p_detached"] = self.storage_p_detached[storage_id], if self.support_theta: res["theta"] = self.storage_theta[storage_id] else: @@ -1171,6 +1229,28 @@ def _aux_process_grid2op_compat_191(cls): except ValueError as exc_: # this attribute was not there in the first place pass + + @classmethod + def _aux_process_grid2op_compat_1_11_0(cls): + cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) + + for el in [ + # slack (>= 1.11.0) + "gen_p_slack", + # detachment (>= 1.11.0) + "load_detached", + "gen_detached", + "storage_detached", + "load_p_detached", + "load_q_detached", + "gen_p_detached", + "storage_p_detached", + ]: + try: + cls.attr_list_vect.remove(el) + except ValueError as exc_: + # this attribute was not there in the first place + pass @classmethod def process_grid2op_compat(cls) -> None: @@ -1201,6 +1281,10 @@ def process_grid2op_compat(cls) -> None: # alert attributes have been added in 1.9.1 cls._aux_process_grid2op_compat_191() + if glop_ver < version.parse("1.11.0"): + # alert attributes have been added in 1.9.1 + cls._aux_process_grid2op_compat_1_11_0() + cls.attr_list_set = copy.deepcopy(cls.attr_list_set) cls.attr_list_set = set(cls.attr_list_vect) @@ -1326,6 +1410,18 @@ def reset(self) -> None: self.curtailment_limit_effective[:] = 0. self.curtailment[:] = 0. + # slack (>= 1.11.0) + self.gen_p_slack[:] = 0. + + # detachment (>= 1.11.0) + self.load_detached[:] = True + self.gen_detached[:] = True + self.storage_detached[:] = True + self.load_p_detached[:] = 0. + self.load_q_detached[:] = 0. + self.gen_p_detached[:] = 0. + self.storage_p_detached[:] = 0. + def set_game_over(self, env: Optional["grid2op.Environment.Environment"]=None) -> None: """ @@ -1463,6 +1559,18 @@ def set_game_over(self, # was_alert_used_after_attack not updated here in this case # attack_under_alert not updated here in this case + # slack (>= 1.11.0) + self.gen_p_slack[:] = 0. + + # detachment (>= 1.11.0) + self.load_detached[:] = True + self.gen_detached[:] = True + self.storage_detached[:] = True + self.load_p_detached[:] = 0. + self.load_q_detached[:] = 0. + self.gen_p_detached[:] = 0. + self.storage_p_detached[:] = 0. + def __compare_stats(self, other: Self, name: str) -> bool: attr_me = getattr(self, name) attr_other = getattr(other, name) @@ -2706,13 +2814,24 @@ def _aux_add_buses(self, graph, cls, first_id): return bus_ids def _aux_add_loads(self, graph, cls, first_id): + nodes_prop = [ + ("detached", self.load_detached), + ("p_detached", self.load_p_detached), + ("q_detached", self.load_q_detached), + ] edges_prop=[ ("p", self.load_p), ("q", self.load_q), - ("v", self.load_v) + ("v", self.load_v), ] if self.support_theta: edges_prop.append(("theta", self.load_theta)) + + # slack (>= 1.11.0) + self.gen_p_slack[:] = 0. + + if "load_detached" in self.attr_list_set: + edges_prop.append(("is_detached", self.load_detached)) load_ids = self._aux_add_el_to_comp_graph(graph, first_id, cls.name_load, @@ -2720,7 +2839,7 @@ def _aux_add_loads(self, graph, cls, first_id): cls.n_load, self.load_bus, cls.load_to_subid, - nodes_prop=None, + nodes_prop=nodes_prop, edges_prop=edges_prop) return load_ids @@ -2732,7 +2851,10 @@ def _aux_add_gens(self, graph, cls, first_id): ("curtailment", self.curtailment), ("curtailment_limit", self.curtailment_limit), ("gen_margin_up", self.gen_margin_up), - ("gen_margin_down", self.gen_margin_down) + ("gen_margin_down", self.gen_margin_down), + ("p_slack", self.gen_p_slack), + ("detached", self.gen_detached), + ("p_detached", self.gen_p_detached), ] # todo class attributes gen_max_ramp_up etc. edges_prop=[ ("p", - self.gen_p), @@ -2754,7 +2876,10 @@ def _aux_add_gens(self, graph, cls, first_id): def _aux_add_storages(self, graph, cls, first_id): nodes_prop = [("storage_charge", self.storage_charge), - ("storage_power_target", self.storage_power_target)] + ("storage_power_target", self.storage_power_target), + ("detached", self.storage_detached), + ("p_detached", self.storage_p_detached),] + # TODO class attr in nodes_prop: storageEmax etc. edges_prop=[("p", self.storage_power)] if self.support_theta: @@ -3766,6 +3891,8 @@ def to_dict(self): # current_step / max step self._dictionnarized["current_step"] = self.current_step self._dictionnarized["max_step"] = self.max_step + + # TODO shedding: add relevant attributes return self._dictionnarized @@ -3965,7 +4092,7 @@ def add_act(self, act : "grid2op.Action.BaseAction", issue_warn=True) -> Self: cls = type(self) cls_act = type(act) - act = copy.deepcopy(act) + act : BaseAction = copy.deepcopy(act) res = cls() res.set_game_over(env=None) @@ -3977,8 +4104,13 @@ def add_act(self, act : "grid2op.Action.BaseAction", issue_warn=True) -> Self: raise RuntimeError( f"Impossible to add an ambiguous action to an observation. Your action was " f'ambiguous because: "{except_tmp}"' - ) + ) from except_tmp + if act._modif_detach_gen or act._modif_detach_load or act._modif_detach_storage: + raise NotImplementedError("This function is not yet implemented when some elements " + "are detached from the grid. Please write a feature request " + "if you are interested in this feature.") + # if a powerline has been reconnected without specific bus, i issue a warning if "set_line_status" in cls_act.authorized_keys: self._aux_add_act_set_line_status(cls, cls_act, act, res, issue_warn) @@ -4262,6 +4394,18 @@ def _update_obs_complete(self, env: "grid2op.Environment.BaseEnv", with_forecast self.curtailment_limit_effective[:] = 1.0 self.delta_time = dt_float(1.0 * env.delta_time_seconds / 60.0) + + # slack (1.11.0) + self.gen_p_slack[:] = env._slack_gen_p + + # detachment (>= 1.11.0) + self.load_detached[:] = env._loads_detached + self.gen_detached[:] = env._gens_detached + self.storage_detached[:] = env._storages_detached + self.load_p_detached[:] = env._load_p_detached + self.load_q_detached[:] = env._load_q_detached + self.gen_p_detached[:] = env._gen_p_detached + self.storage_p_detached[:] = env._storage_p_detached # handles forecasts here self._update_forecast(env, with_forecast) diff --git a/grid2op/Parameters.py b/grid2op/Parameters.py index c5ec67b2..eef392f9 100644 --- a/grid2op/Parameters.py +++ b/grid2op/Parameters.py @@ -161,6 +161,42 @@ class Parameters: `env.reset(options={"init state": ...})` (see doc of :func:`grid2op.Environment.Environment.reset` for more information) + ENV_DOES_REDISPATCHING: ``bool`` + Whether to let the environment do the redispatching instead of relying on + the backend internal implementation (usually done with the "slack buses"). + + It is ``True`` by default and for all grid2op environment before version 1.11.0 + + .. versionadded: 1.11.0 + + .. note:: + Disabling this feature might speed up the `env.step` computation, but the result + will be highly dependant on the backend (for example results might differ + between using lightsim2grid or pypowsybl2grid) and more importantly between the + parameters of the backend + + Setting this parameter to `False` is not advised if your agent really depends on redispatching, curtailment + or actions on storage units OR if your backend does not have a distributed slack (at least). (note + that we do not recommend to use it even if your backend has a distributed slack). + + Furthermore, if you consider doing a lot of "detachment" / "shedding" / "curtailment" + on the loads or generator, it's best to avoid setting this flag to `False`. + + If you don't know what a "distributed slack" is you probably should not set this to `False`. + + And to be somewhat realistic, you might also consider setting the flag + :attr:`Parameters.STOP_EP_IF_SLACK_BREAK_CONSTRAINTS` to avoid simulating unrealistic + episode. + + STOP_EP_IF_SLACK_BREAK_CONSTRAINTS: ``bool`` + Whether to stop the episode when a constraint on the slack generator(s) are violated. + + In grid2op < 1.11.0 these were not checked at all. But from grid2op 1.11.0 you have the option + to check whether some slack generators would absorb / produce too much between two consecutive + steps which would be unrealistic in practice. + + It defaults to ``False``. + """ def __init__(self, parameters_path=None): @@ -242,6 +278,10 @@ def __init__(self, parameters_path=None): warnings.warn(warn_msg.format(parameters_path)) self.IGNORE_INITIAL_STATE_TIME_SERIE = False + + # Added in 1.11.0 with detachement + self.ENV_DOES_REDISPATCHING = True + self.STOP_EP_IF_SLACK_BREAK_CONSTRAINTS = False @staticmethod def _isok_txt(arg): @@ -387,7 +427,12 @@ def init_from_dict(self, dict_): self.IGNORE_INITIAL_STATE_TIME_SERIE = Parameters._isok_txt( dict_["IGNORE_INITIAL_STATE_TIME_SERIE"] ) - + + if "ENV_DOES_REDISPATCHING" in dict_: + self.ENV_DOES_REDISPATCHING = Parameters._isok_txt(dict_["ENV_DOES_REDISPATCHING"]) + if "STOP_EP_IF_SLACK_BREAK_CONSTRAINTS" in dict_: + self.STOP_EP_IF_SLACK_BREAK_CONSTRAINTS = Parameters._isok_txt(dict_["BACKEND_DOES_REDISPATCHING"]) + authorized_keys = set(self.__dict__.keys()) authorized_keys = authorized_keys | { "NB_TIMESTEP_POWERFLOW_ALLOWED", @@ -437,6 +482,8 @@ def to_dict(self): res["MAX_SIMULATE_PER_STEP"] = int(self.MAX_SIMULATE_PER_STEP) res["MAX_SIMULATE_PER_EPISODE"] = int(self.MAX_SIMULATE_PER_EPISODE) res["IGNORE_INITIAL_STATE_TIME_SERIE"] = int(self.IGNORE_INITIAL_STATE_TIME_SERIE) + res["ENV_DOES_REDISPATCHING"] = bool(self.ENV_DOES_REDISPATCHING) + res["STOP_EP_IF_SLACK_BREAK_CONSTRAINTS"] = bool(self.STOP_EP_IF_SLACK_BREAK_CONSTRAINTS) return res def init_from_json(self, json_path): @@ -741,3 +788,20 @@ def check_valid(self): raise RuntimeError( f'Impossible to convert IGNORE_INITIAL_STATE_TIME_SERIE to bool with error \n:"{exc_}"' ) from exc_ + + try: + if not isinstance(self.ENV_DOES_REDISPATCHING, (bool, dt_bool)): + raise RuntimeError("ENV_DOES_REDISPATCHING should be a boolean") + self.ENV_DOES_REDISPATCHING = dt_bool(self.ENV_DOES_REDISPATCHING) + except Exception as exc_: + raise RuntimeError( + f'Impossible to convert ENV_DOES_REDISPATCHING to bool with error \n:"{exc_}"' + ) from exc_ + try: + if not isinstance(self.STOP_EP_IF_SLACK_BREAK_CONSTRAINTS, (bool, dt_bool)): + raise RuntimeError("STOP_EP_IF_SLACK_BREAK_CONSTRAINTS should be a boolean") + self.STOP_EP_IF_SLACK_BREAK_CONSTRAINTS = dt_bool(self.STOP_EP_IF_SLACK_BREAK_CONSTRAINTS) + except Exception as exc_: + raise RuntimeError( + f'Impossible to convert STOP_EP_IF_SLACK_BREAK_CONSTRAINTS to bool with error \n:"{exc_}"' + ) from exc_ diff --git a/grid2op/simulator/simulator.py b/grid2op/simulator/simulator.py index 553d82f8..d0e8e5e6 100644 --- a/grid2op/simulator/simulator.py +++ b/grid2op/simulator/simulator.py @@ -570,6 +570,7 @@ def predict( res = self.copy() else: res = self + this_act = act.copy() if new_gen_p is None: diff --git a/grid2op/tests/test_shedding.py b/grid2op/tests/test_shedding.py index 19af2597..e38ae0a1 100644 --- a/grid2op/tests/test_shedding.py +++ b/grid2op/tests/test_shedding.py @@ -13,6 +13,7 @@ import tempfile import grid2op +from grid2op.dtypes import dt_float from grid2op.Action.baseAction import BaseAction from grid2op.Exceptions import AmbiguousAction from grid2op.Action import CompleteAction @@ -259,6 +260,8 @@ def test_backend_action(self): ) = bk_act() assert topo__.changed[self.env.load_pos_topo_vect[load_id]], f"error for load {load_id}" assert topo__.values[self.env.load_pos_topo_vect[load_id]] == -1, f"error for load {load_id}" + assert bk_act.get_load_detached()[load_id], f"error for load {load_id}" + assert bk_act.get_load_detached().sum() == 1, f"error for load {load_id}" for gen_id in range(self.env.n_gen): bk_act :_BackendAction = self.env.backend.my_bk_act_class() @@ -274,6 +277,8 @@ def test_backend_action(self): ) = bk_act() assert topo__.changed[self.env.gen_pos_topo_vect[gen_id]], f"error for gen {gen_id}" assert topo__.values[self.env.gen_pos_topo_vect[gen_id]] == -1, f"error for gen {gen_id}" + assert bk_act.get_gen_detached()[gen_id], f"error for gen {gen_id}" + assert bk_act.get_gen_detached().sum() == 1, f"error for gen {gen_id}" for sto_id in range(self.env.n_storage): bk_act :_BackendAction = self.env.backend.my_bk_act_class() @@ -289,11 +294,159 @@ def test_backend_action(self): ) = bk_act() assert topo__.changed[self.env.storage_pos_topo_vect[sto_id]], f"error for storage {sto_id}" assert topo__.values[self.env.storage_pos_topo_vect[sto_id]] == -1, f"error for storage {sto_id}" + assert bk_act.get_sto_detached()[sto_id], f"error for storage {sto_id}" + assert bk_act.get_sto_detached().sum() == 1, f"error for storage {sto_id}" + + +class TestSheddingEnv(unittest.TestCase): + def get_parameters(self): + params = Parameters() + params.MAX_SUB_CHANGED = 999999 + return params + + def setUp(self): + params = self.get_parameters() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make("educ_case14_storage", + param=params, + action_class=CompleteAction, + allow_detachment=True, + test=True, + _add_to_name=type(self).__name__) + obs = self.env.reset(seed=0, options={"time serie id": 0}) # Reproducibility + assert type(self.env).detachment_is_allowed + assert type(obs).detachment_is_allowed + assert type(self.env.action_space()).detachment_is_allowed + + def tearDown(self): + self.env.close() + return super().tearDown() + + def test_no_shedding(self): + obs, reward, done, info = self.env.step(self.env.action_space()) + assert (np.abs(self.env._prev_load_p - obs.load_p) <= 1e-7).all() + assert (np.abs(self.env._prev_load_q - obs.load_q) <= 1e-7).all() + assert (np.abs(self.env._prev_gen_p - obs.gen_p) <= 1e-7).all() + # for env + assert (~self.env._loads_detached).all() + assert (~self.env._gens_detached).all() + assert (~self.env._storages_detached).all() + assert (np.abs(self.env._load_p_detached) <= 1e-7).all() + assert (np.abs(self.env._load_q_detached) <= 1e-7).all() + assert (np.abs(self.env._gen_p_detached) <= 1e-7).all() + assert (np.abs(self.env._storage_p_detached) <= 1e-7).all() + # for obs + assert (~obs.load_detached).all() + assert (~obs.gen_detached).all() + assert (~obs.storage_detached).all() + assert (np.abs(obs.load_p_detached) <= 1e-7).all() + assert (np.abs(obs.load_q_detached) <= 1e-7).all() + assert (np.abs(obs.gen_p_detached) <= 1e-7).all() + assert (np.abs(obs.storage_p_detached) <= 1e-7).all() + + # slack ok + assert np.abs(self.env._slack_gen_p.sum() / self.env._gen_activeprod_t.sum()) <= 0.02 # less than 2% losses + + def test_shedding_load_step(self): + # NB warning this test does not pass if STOP_EP_IF_SLACK_BREAK_CONSTRAINTS (slack breaks its rampdown !) + obs, reward, done, info = self.env.step(self.env.action_space({"detach_load": 0})) + # env converged + assert not done + # load properly disconnected + assert obs.topo_vect[obs.load_pos_topo_vect[0]] == -1 + # 0 in the observation for this load + assert np.abs(obs.load_p[0]) <= 1e-7 + assert np.abs(obs.load_q[0]) <= 1e-7 + + # all other loads ok + assert (np.abs(self.env._prev_load_p[1:] - obs.load_p[1:]) <= 1e-7).all() + assert (np.abs(self.env._prev_load_q[1:] - obs.load_q[1:]) <= 1e-7).all() + assert (~self.env._loads_detached[1:]).all() + assert (np.abs(self.env._load_p_detached[1:]) <= 1e-7).all() + assert (np.abs(self.env._load_q_detached[1:]) <= 1e-7).all() + assert (~obs.load_detached[1:]).all() + assert (np.abs(obs.load_p_detached[1:]) <= 1e-7).all() + assert (np.abs(obs.load_q_detached[1:]) <= 1e-7).all() + + # load properly written as detached + normal_load_p = dt_float(21.9) + normal_load_q = dt_float(15.3) + assert np.abs(self.env._load_p_detached[0] - normal_load_p) <= 1e-7 + assert np.abs(self.env._load_q_detached[0] - normal_load_q) <= 1e-7 + assert np.abs(obs.load_p_detached[0] - normal_load_p) <= 1e-7 + assert np.abs(obs.load_q_detached[0] - normal_load_q) <= 1e-7 + + # rest is ok + assert (np.abs(self.env._prev_gen_p - obs.gen_p) <= 1e-7).all() + assert (~self.env._gens_detached).all() + assert (~self.env._storages_detached).all() + assert (np.abs(self.env._gen_p_detached) <= 1e-7).all() + assert (np.abs(self.env._storage_p_detached) <= 1e-7).all() + + assert (~obs.gen_detached).all() + assert (~obs.storage_detached).all() + assert (np.abs(obs.gen_p_detached) <= 1e-7).all() + assert (np.abs(obs.storage_p_detached) <= 1e-7).all() + + # slack completely "messed up" + assert self.env._slack_gen_p.sum() <= -normal_load_p + assert obs.gen_p_slack.sum() <= -normal_load_p + + # another step + obs, reward, done, info = self.env.step(self.env.action_space()) + # env converged + assert not done + # load properly disconnected + assert obs.topo_vect[obs.load_pos_topo_vect[0]] == -1 + # load properly written as detached + normal_load_p = dt_float(22.0) + normal_load_q = dt_float(15.2) + assert np.abs(self.env._load_p_detached[0] - normal_load_p) <= 1e-7 + assert np.abs(self.env._load_q_detached[0] - normal_load_q) <= 1e-7 + assert self.env._slack_gen_p.sum() <= -normal_load_p + + # another step + obs, reward, done, info = self.env.step(self.env.action_space()) + # env converged + assert not done + # load properly disconnected + assert obs.topo_vect[obs.load_pos_topo_vect[0]] == -1 + # load properly written as detached + normal_load_p = dt_float(21.6) + normal_load_q = dt_float(15.1) + assert np.abs(self.env._load_p_detached[0] - normal_load_p) <= 1e-7 + assert np.abs(self.env._load_q_detached[0] - normal_load_q) <= 1e-7 + assert self.env._slack_gen_p.sum() <= -normal_load_p + + # now attached it again + obs, reward, done, info = self.env.step(self.env.action_space({"set_bus": {"loads_id": [(0, 1)]}})) + # env converged + assert not done + # load properly disconnected + assert obs.topo_vect[obs.load_pos_topo_vect[0]] == 1 + # load properly written as detached + assert np.abs(self.env._load_p_detached[0] - 0.) <= 1e-7 + assert np.abs(self.env._load_q_detached[0] - 0.) <= 1e-7 + # slack ok + assert np.abs(self.env._slack_gen_p.sum() / self.env._gen_activeprod_t.sum()) <= 0.02 # less than 2% losses + + + + + +# TODO with the env parameters STOP_EP_IF_SLACK_BREAK_CONSTRAINTS and ENV_DOES_REDISPATCHING +# TODO when something is "re attached" on the grid +# TODO check gen detached does not participate in redisp + +# TODO shedding in simulate +# TODO shedding in Simulator ! # TODO Shedding: test when backend does not support it is not set # TODO shedding: test when user deactivates it it is not set # TODO Shedding: Runner + # TODO Shedding: environment copied # TODO Shedding: MultiMix environment # TODO Shedding: TimedOutEnvironment From c29b619e15d3cad494e37c0a60f1b4be47abc777 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 13 Dec 2024 12:16:36 +0100 Subject: [PATCH 51/60] fix some last issue before merging on dev_turnoff_gen_load Signed-off-by: DONNOT Benjamin --- grid2op/Backend/backend.py | 2 ++ grid2op/Backend/pandaPowerBackend.py | 8 +------- grid2op/tests/aaa_test_backend_interface.py | 14 ++++++-------- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index 2dc61d7b..62c8cc47 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -1226,6 +1226,8 @@ def storages_info(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: raise BackendError( "storages_info method is not implemented yet there is batteries on the grid." ) + empty_ = np.array([]) + return empty_, empty_, empty_ def storage_deact_for_backward_comaptibility(self) -> None: """ diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index 9fabd2d7..b073c16c 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -1116,13 +1116,7 @@ def runpf(self, is_dc : bool=False) -> Tuple[bool, Union[Exception, None]]: self.load_theta[:], ) = self._loads_info() - load_in_service = self._grid.load["in_service"] - if not is_dc: - if not np.isfinite(self.load_v[load_in_service]).all(): - # TODO see if there is a better way here - # some loads are disconnected: it's a game over case! - raise pp.powerflow.LoadflowNotConverged(f"Isolated load: check loads {np.isfinite(self.load_v).nonzero()[0]}") - else: + if is_dc: # fix voltages magnitude that are always "nan" for dc case # self._grid.res_bus["vm_pu"] is always nan when computed in DC self.load_v[:] = self.load_pu_to_kv # TODO diff --git a/grid2op/tests/aaa_test_backend_interface.py b/grid2op/tests/aaa_test_backend_interface.py index 71cabe00..29ccd59a 100644 --- a/grid2op/tests/aaa_test_backend_interface.py +++ b/grid2op/tests/aaa_test_backend_interface.py @@ -1001,7 +1001,9 @@ def test_19_isolated_storage_stops_computation(self, allow_detachment=DEFAULT_AL def test_20_disconnected_load_stops_computation(self, allow_detachment=DEFAULT_ALLOW_DETACHMENT): """ - Tests that a disconnected load unit will be caught by the `_runpf_with_diverging_exception` method. + Tests that a disconnected load unit will be caught by the `_runpf_with_diverging_exception` method + if loads are not allowed to be "detached" from the grid (or if your backend does not support + the "detachment" feature.) This test supposes that : @@ -1010,8 +1012,6 @@ def test_20_disconnected_load_stops_computation(self, allow_detachment=DEFAULT_A - backend.apply_action() for topology modification - backend.reset() is implemented - NB: this test is skipped if your backend does not (yet :-) ) supports storage units - .. note:: Currently this stops the computation of the environment and lead to a game over. @@ -1041,6 +1041,8 @@ def test_20_disconnected_load_stops_computation(self, allow_detachment=DEFAULT_A def test_21_disconnected_gen_stops_computation(self, allow_detachment=DEFAULT_ALLOW_DETACHMENT): """ Tests that a disconnected generator will be caught by the `_runpf_with_diverging_exception` method + if generators are not allowed to be "detached" from the grid (or if your backend does not support + the "detachment" feature.) This test supposes that : @@ -1049,8 +1051,6 @@ def test_21_disconnected_gen_stops_computation(self, allow_detachment=DEFAULT_AL - backend.apply_action() for topology modification - backend.reset() is implemented - NB: this test is skipped if your backend does not (yet :-) ) supports storage units - .. note:: Currently this stops the computation of the environment and lead to a game over. @@ -1091,8 +1091,6 @@ def test_22_islanded_grid_stops_computation(self): - backend.apply_action() for topology modification - backend.reset() is implemented - NB: this test is skipped if your backend does not (yet :-) ) supports storage units - .. note:: Currently this stops the computation of the environment and lead to a game over. @@ -1721,7 +1719,7 @@ def test_31_disconnected_storage_with_p_stops_computation(self, allow_detachment Currently this stops the computation of the environment and lead to a game over. .. note:: - This test is also used in `attr:AAATestBackendAPI.test_31_allow_detachment` + This test is also used in `attr:AAATestBackendAPI.test_33_allow_detachment` """ self.skip_if_needed() From 7241e138108190f949a9a1854d72cc5a2329f23f Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 13 Dec 2024 12:29:40 +0100 Subject: [PATCH 52/60] fix another typo Signed-off-by: DONNOT Benjamin --- grid2op/Parameters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grid2op/Parameters.py b/grid2op/Parameters.py index eef392f9..6d5adcf8 100644 --- a/grid2op/Parameters.py +++ b/grid2op/Parameters.py @@ -431,7 +431,7 @@ def init_from_dict(self, dict_): if "ENV_DOES_REDISPATCHING" in dict_: self.ENV_DOES_REDISPATCHING = Parameters._isok_txt(dict_["ENV_DOES_REDISPATCHING"]) if "STOP_EP_IF_SLACK_BREAK_CONSTRAINTS" in dict_: - self.STOP_EP_IF_SLACK_BREAK_CONSTRAINTS = Parameters._isok_txt(dict_["BACKEND_DOES_REDISPATCHING"]) + self.STOP_EP_IF_SLACK_BREAK_CONSTRAINTS = Parameters._isok_txt(dict_["STOP_EP_IF_SLACK_BREAK_CONSTRAINTS"]) authorized_keys = set(self.__dict__.keys()) authorized_keys = authorized_keys | { From 93ec180c64f099749b5605d6e171e118210701f4 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 13 Dec 2024 15:10:20 +0100 Subject: [PATCH 53/60] fixing broken tests for the Observation Signed-off-by: DONNOT Benjamin --- grid2op/Observation/baseObservation.py | 19 ++++++++- grid2op/Observation/completeObservation.py | 10 +++++ grid2op/tests/test_Observation.py | 45 ++++++++++++++++++---- 3 files changed, 65 insertions(+), 9 deletions(-) diff --git a/grid2op/Observation/baseObservation.py b/grid2op/Observation/baseObservation.py index 6b7a41c1..23069429 100644 --- a/grid2op/Observation/baseObservation.py +++ b/grid2op/Observation/baseObservation.py @@ -417,6 +417,23 @@ class BaseObservation(GridObjects): - obs.attack_under_alert[i] = +1 => attackable line i has been attacked and (before the attack) an alert was sent (so your agent expects to "game over" within the next `env.parameters.ALERT_TIME_WINDOW` steps) + + + gen_p_slack: :class:`numpy.ndarray`, dtype:float + + load_detached: :class:`numpy.ndarray`, dtype:bool + + gen_detached: :class:`numpy.ndarray`, dtype:bool + + storage_detached: :class:`numpy.ndarray`, dtype:bool + + load_p_detached: :class:`numpy.ndarray`, dtype:float + + load_q_detached: :class:`numpy.ndarray`, dtype:float + + gen_p_detached: :class:`numpy.ndarray`, dtype:float + + storage_p_detached: :class:`numpy.ndarray`, dtype:float _shunt_p: :class:`numpy.ndarray`, dtype:float Shunt active value (only available if shunts are available) (in MW) @@ -1281,7 +1298,7 @@ def process_grid2op_compat(cls) -> None: # alert attributes have been added in 1.9.1 cls._aux_process_grid2op_compat_191() - if glop_ver < version.parse("1.11.0"): + if glop_ver < cls.MIN_VERSION_DETACH: # alert attributes have been added in 1.9.1 cls._aux_process_grid2op_compat_1_11_0() diff --git a/grid2op/Observation/completeObservation.py b/grid2op/Observation/completeObservation.py index 201e94f0..7dcab022 100644 --- a/grid2op/Observation/completeObservation.py +++ b/grid2op/Observation/completeObservation.py @@ -195,6 +195,16 @@ class CompleteObservation(BaseObservation): "total_number_of_alert", "time_since_last_attack", "was_alert_used_after_attack", + # slack (>= 1.11.0) + "gen_p_slack", + # detachment (>= 1.11.0) + "load_detached", + "gen_detached", + "storage_detached", + "load_p_detached", + "load_q_detached", + "gen_p_detached", + "storage_p_detached", ] attr_list_json = [ "_thermal_limit", diff --git a/grid2op/tests/test_Observation.py b/grid2op/tests/test_Observation.py index ea961ef2..bda6af69 100644 --- a/grid2op/tests/test_Observation.py +++ b/grid2op/tests/test_Observation.py @@ -18,7 +18,7 @@ import grid2op from grid2op.dtypes import dt_int, dt_float, dt_bool from grid2op.Exceptions import * -from grid2op.Observation import ObservationSpace +from grid2op.Observation import ObservationSpace, CompleteObservation from grid2op.Reward import ( L2RPNReward, CloseToOverflowReward, @@ -848,6 +848,14 @@ def setUp(self): "time_since_last_attack": [], "was_alert_used_after_attack": [], "attack_under_alert": [], + "gen_p_slack": [0.0, 0.0, 0.0, 0.0, 2.2990264892578125], + "load_detached": [False, False, False, False, False, False, False, False, False, False, False], + "gen_detached": [False, False, False, False, False], + "storage_detached": [], + "load_p_detached": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + "load_q_detached": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + "gen_p_detached": [0.0, 0.0, 0.0, 0.0, 0.0], + "storage_p_detached": [], } self.dtypes = np.array( [ @@ -916,6 +924,16 @@ def setUp(self): dt_int, dt_int, dt_int, + # slack (>= 1.11.0) + dt_float, + # detachment (>= 1.11.0) + dt_bool, + dt_bool, + dt_bool, + dt_float, + dt_float, + dt_float, + dt_float, ], dtype=object, ) @@ -983,10 +1001,20 @@ def setUp(self): 0, 0, 0, - 0 + 0, + # slack (>= 1.11.0) + 5, + # detachment (>= 1.11.0) + 11, + 5, + 0, + 11, + 11, + 5, + 0, ] ) - self.size_obs = 429 + 4 + 4 + 2 + 1 + 10 + 5 + 0 + self.size_obs = 429 + 4 + 4 + 2 + 1 + 10 + 5 + 0 + 5 + 11 + 5 + 0 + 11 + 11 + 5 def tearDown(self): self.env.close() @@ -2304,10 +2332,9 @@ def test_json_loadable(self): def test_to_from_json(self): """test the to_json, and from_json and make sure these are all json serializable""" - obs = self.env.observation_space(self.env) - obs2 = self.env.observation_space(self.env) - dict_ = obs.to_json() - + obs : CompleteObservation = self.env.observation_space(self.env) + obs2 : CompleteObservation = self.env.observation_space(self.env) + dict_ = obs.to_json() # test that the right dictionary is returned for k in dict_: assert ( @@ -2328,7 +2355,9 @@ def test_to_from_json(self): # test i can initialize an observation from it obs2.reset() obs2.from_json(dict_realoaded) - assert obs == obs2 + if obs != obs2: + diff_, attr_diff = obs2.where_different(obs) + raise AssertionError(f"Following attributes are different: {attr_diff}") class TestUpdateEnvironement(unittest.TestCase): From d67e08d7634b3c3069f843f54bfe124e1b0fbf5b Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 13 Dec 2024 16:14:54 +0100 Subject: [PATCH 54/60] fix some broken tests again Signed-off-by: DONNOT Benjamin --- grid2op/Environment/baseEnv.py | 12 +- grid2op/Observation/baseObservation.py | 171 +++++++++++++-------- grid2op/Observation/completeObservation.py | 2 +- grid2op/tests/test_Observation.py | 48 +++--- grid2op/tests/test_attached_envs.py | 53 ++++++- grid2op/tests/test_shedding.py | 12 +- 6 files changed, 194 insertions(+), 104 deletions(-) diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index e3b31adf..b0466d1a 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -665,7 +665,7 @@ def __init__( self._storage_p_detached = None # slack (1.11.0) - self._slack_gen_p = None + self._delta_gen_p = None @property def highres_sim_counter(self): @@ -984,7 +984,7 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None): new_obj._storage_p_detached = 1. * self._storage_p_detached # slack (1.11.0) - new_obj._slack_gen_p = 1. * self._slack_gen_p + new_obj._delta_gen_p = 1. * self._delta_gen_p def get_path_env(self): """ @@ -1454,7 +1454,7 @@ def _has_been_initialized(self): self._storage_p_detached = np.zeros(bk_type.n_storage, dtype=dt_float) # slack (1.11.0) - self._slack_gen_p = np.zeros(bk_type.n_gen, dtype=dt_float) + self._delta_gen_p = np.zeros(bk_type.n_gen, dtype=dt_float) def _update_parameters(self): """update value for the new parameters""" @@ -1557,7 +1557,7 @@ def _reset_slack_and_detachment(self): self._gen_p_detached[:] = 0. self._storage_p_detached[:] = 0. - self._slack_gen_p[:] = 0. + self._delta_gen_p[:] = 0. def _reset_alert(self): self._last_alert[:] = False @@ -3256,8 +3256,8 @@ def _aux_register_env_converged(self, disc_lines, action, init_line_status, new_ # for detachment remember previous loads and generation self._prev_load_p[:], self._prev_load_q[:], *_ = self.backend.loads_info() - self._slack_gen_p[:] = self._gen_activeprod_t - self._gen_activeprod_t_redisp - self._slack_gen_p[self._gens_detached] = 0. + self._delta_gen_p[:] = self._gen_activeprod_t - self._gen_activeprod_t_redisp + self._delta_gen_p[self._gens_detached] = 0. self._prev_gen_p[:] = self._gen_activeprod_t # finally, build the observation (it's a different one at each step, we cannot reuse the same one) diff --git a/grid2op/Observation/baseObservation.py b/grid2op/Observation/baseObservation.py index 23069429..df727e19 100644 --- a/grid2op/Observation/baseObservation.py +++ b/grid2op/Observation/baseObservation.py @@ -418,8 +418,7 @@ class BaseObservation(GridObjects): the attack) an alert was sent (so your agent expects to "game over" within the next `env.parameters.ALERT_TIME_WINDOW` steps) - - gen_p_slack: :class:`numpy.ndarray`, dtype:float + gen_p_delta: :class:`numpy.ndarray`, dtype:float load_detached: :class:`numpy.ndarray`, dtype:bool @@ -505,7 +504,7 @@ class BaseObservation(GridObjects): "gen_margin_up", "gen_margin_down", # slack (>= 1.11.0) - "gen_p_slack", + "gen_p_delta", # detachment (>= 1.11.0) "load_detached", "gen_detached", @@ -648,16 +647,16 @@ def __init__(self, self.delta_time = dt_float(5.0) # slack (1.11.0) - self.gen_p_slack = np.empty(shape=cls.n_gen, dtype=dt_float) + self.gen_p_delta = np.empty(shape=cls.n_gen, dtype=dt_float) # detachment (>= 1.11.0) - self.load_detached = np.empty(shape=cls.n_load, dtype=dt_bool) - self.gen_detached = np.empty(shape=cls.n_gen, dtype=dt_bool) - self.storage_detached = np.empty(shape=cls.n_storage, dtype=dt_bool) - self.load_p_detached = np.empty(shape=cls.n_load, dtype=dt_float) - self.load_q_detached = np.empty(shape=cls.n_load, dtype=dt_float) - self.gen_p_detached = np.empty(shape=cls.n_gen, dtype=dt_float) - self.storage_p_detached = np.empty(shape=cls.n_storage, dtype=dt_float) + self.load_detached = np.ones(shape=cls.n_load, dtype=dt_bool) + self.gen_detached = np.ones(shape=cls.n_gen, dtype=dt_bool) + self.storage_detached = np.ones(shape=cls.n_storage, dtype=dt_bool) + self.load_p_detached = np.zeros(shape=cls.n_load, dtype=dt_float) + self.load_q_detached = np.zeros(shape=cls.n_load, dtype=dt_float) + self.gen_p_detached = np.zeros(shape=cls.n_gen, dtype=dt_float) + self.storage_p_detached = np.zeros(shape=cls.n_storage, dtype=dt_float) def _aux_copy(self, other : Self) -> None: attr_simple = [ @@ -729,7 +728,7 @@ def _aux_copy(self, other : Self) -> None: "gen_margin_down", "curtailment_limit_effective", # slack (>= 1.11.0) - "gen_p_slack", + "gen_p_delta", # detachment (>= 1.11.0) "load_detached", "gen_detached", @@ -877,9 +876,12 @@ def state_of( - "theta" (optional) the voltage angle (in degree) of the bus to which the load is connected - "bus" on which bus the load is connected in the substation - "sub_id" the id of the substation to which the load is connected - - "detached" (>= 1.11.0) whether this load is detached from the grid - - "p_detached" (>= 1.11.0) amount of MW detached from the grid cause by the detachment of this load - - "q_detached" (>= 1.11.0) amount of MVAr detached from the grid cause by the detachment of this load + - "detached" (>= 1.11.0) whether this load is detached from the grid + (if detachement is allowed in the environment) + - "p_detached" (>= 1.11.0) amount of MW detached from the grid cause by + the detachment of this load (if detachement is allowed in the environment) + - "q_detached" (>= 1.11.0) amount of MVAr detached from the grid cause by + the detachment of this load (if detachement is allowed in the environment) - if a generator is inspected, then the keys are: @@ -901,9 +903,12 @@ def state_of( and the next (in MW). It's 0. for renewable generators. - "margin_down": by how much this generateur can see its production decrease between this step and the next (in MW). It's 0. for renewable generators. - - "p_slack" (>= 1.11.0) slack participation (in MW) for this generator - - "detached" (>= 1.11.0) whether this generator is detached from the grid - - "p_detached" (>= 1.11.0) amount of MW detached from the grid cause by the detachment of this generator + - "p_delta" (>= 1.11.0) difference (in MW) between what the environment ask this generator to produce + and what it actually produces (difference is not caused by grid2op but by the Backend) + - "detached" (>= 1.11.0) whether this generator is detached from the + grid (if detachement is allowed in the environment) + - "p_detached" (>= 1.11.0) amount of MW detached from the grid cause by the + detachment of this generator (if detachement is allowed in the environment) - if a powerline is inspected then the keys are "origin" and "extremity" each being dictionary with keys: @@ -932,7 +937,9 @@ def state_of( - "bus": the bus (1 or 2) to which the storage unit is connected - "sub_id" : the id of the substation to which the sotrage unit is connected - "detached" (>= 1.11.0) whether this storage is detached from the grid + (if detachement is allowed in the environment) - "p_detached" (>= 1.11.0) amount of MW detached from the grid cause by the detachment of this storage + (if detachement is allowed in the environment) - if a substation is inspected, it returns the topology to this substation in a dictionary with keys: @@ -993,12 +1000,13 @@ def state_of( "v": self.load_v[load_id], "bus": self.topo_vect[self.load_pos_topo_vect[load_id]], "sub_id": cls.load_to_subid[load_id], - "detached": self.load_detached[load_id], - "p_detached": self.load_p_detached[load_id], - "q_detached": self.load_q_detached[load_id], } if self.support_theta: res["theta"] = self.load_theta[load_id] + if cls.detachment_is_allowed: + res["detached"] = self.load_detached[load_id] + res["p_detached"] = self.load_p_detached[load_id] + res["q_detached"] = self.load_q_detached[load_id] elif gen_id is not None: if ( line_id is not None @@ -1029,12 +1037,13 @@ def state_of( "p_before_curtail": self.gen_p_before_curtail[gen_id], "margin_up": self.gen_margin_up[gen_id], "margin_down": self.gen_margin_down[gen_id], - "gen_p_slack": self.gen_p_slack[gen_id], - "detached": self.gen_detached[gen_id], - "p_detached": self.gen_p_detached[gen_id], + "gen_p_delta": self.gen_p_delta[gen_id], } if self.support_theta: res["theta"] = self.gen_theta[gen_id] + if cls.detachment_is_allowed: + res["detached"] = self.gen_detached[gen_id] + res["p_detached"] = self.gen_p_detached[gen_id] elif line_id is not None: if substation_id is not None or storage_id is not None: raise Grid2OpException(ERROR_ONLY_SINGLE_EL) @@ -1099,10 +1108,11 @@ def state_of( res["storage_power_target"] = self.storage_power_target[storage_id] res["bus"] = self.topo_vect[cls.storage_pos_topo_vect[storage_id]] res["sub_id"] = cls.storage_to_subid[storage_id] - res["detached"] = self.storage_detached[storage_id], - res["p_detached"] = self.storage_p_detached[storage_id], if self.support_theta: res["theta"] = self.storage_theta[storage_id] + if cls.detachment_is_allowed: + res["detached"] = self.storage_detached[storage_id] + res["p_detached"] = self.storage_p_detached[storage_id] else: if substation_id >= len(cls.sub_info): raise Grid2OpException( @@ -1253,7 +1263,7 @@ def _aux_process_grid2op_compat_1_11_0(cls): for el in [ # slack (>= 1.11.0) - "gen_p_slack", + "gen_p_delta", # detachment (>= 1.11.0) "load_detached", "gen_detached", @@ -1428,16 +1438,17 @@ def reset(self) -> None: self.curtailment[:] = 0. # slack (>= 1.11.0) - self.gen_p_slack[:] = 0. + self.gen_p_delta[:] = 0. # detachment (>= 1.11.0) - self.load_detached[:] = True - self.gen_detached[:] = True - self.storage_detached[:] = True - self.load_p_detached[:] = 0. - self.load_q_detached[:] = 0. - self.gen_p_detached[:] = 0. - self.storage_p_detached[:] = 0. + if type(self).detachment_is_allowed: + self.load_detached[:] = True + self.gen_detached[:] = True + self.storage_detached[:] = True + self.load_p_detached[:] = 0. + self.load_q_detached[:] = 0. + self.gen_p_detached[:] = 0. + self.storage_p_detached[:] = 0. def set_game_over(self, env: Optional["grid2op.Environment.Environment"]=None) -> None: @@ -1577,16 +1588,17 @@ def set_game_over(self, # attack_under_alert not updated here in this case # slack (>= 1.11.0) - self.gen_p_slack[:] = 0. + self.gen_p_delta[:] = 0. # detachment (>= 1.11.0) - self.load_detached[:] = True - self.gen_detached[:] = True - self.storage_detached[:] = True - self.load_p_detached[:] = 0. - self.load_q_detached[:] = 0. - self.gen_p_detached[:] = 0. - self.storage_p_detached[:] = 0. + if type(self).detachment_is_allowed: + self.load_detached[:] = True + self.gen_detached[:] = True + self.storage_detached[:] = True + self.load_p_detached[:] = 0. + self.load_q_detached[:] = 0. + self.gen_p_detached[:] = 0. + self.storage_p_detached[:] = 0. def __compare_stats(self, other: Self, name: str) -> bool: attr_me = getattr(self, name) @@ -2831,11 +2843,15 @@ def _aux_add_buses(self, graph, cls, first_id): return bus_ids def _aux_add_loads(self, graph, cls, first_id): - nodes_prop = [ - ("detached", self.load_detached), - ("p_detached", self.load_p_detached), - ("q_detached", self.load_q_detached), - ] + if type(self).detachment_is_allowed: + nodes_prop = [ + ("detached", self.load_detached), + ("p_detached", self.load_p_detached), + ("q_detached", self.load_q_detached), + ] + else: + nodes_prop = None + edges_prop=[ ("p", self.load_p), ("q", self.load_q), @@ -2845,7 +2861,7 @@ def _aux_add_loads(self, graph, cls, first_id): edges_prop.append(("theta", self.load_theta)) # slack (>= 1.11.0) - self.gen_p_slack[:] = 0. + self.gen_p_delta[:] = 0. if "load_detached" in self.attr_list_set: edges_prop.append(("is_detached", self.load_detached)) @@ -2869,10 +2885,8 @@ def _aux_add_gens(self, graph, cls, first_id): ("curtailment_limit", self.curtailment_limit), ("gen_margin_up", self.gen_margin_up), ("gen_margin_down", self.gen_margin_down), - ("p_slack", self.gen_p_slack), - ("detached", self.gen_detached), - ("p_detached", self.gen_p_detached), - ] # todo class attributes gen_max_ramp_up etc. + ("p_delta", self.gen_p_delta)] + # todo class attributes gen_max_ramp_up etc. edges_prop=[ ("p", - self.gen_p), ("q", - self.gen_q), @@ -2880,6 +2894,11 @@ def _aux_add_gens(self, graph, cls, first_id): ] if self.support_theta: edges_prop.append(("theta", self.gen_theta)) + + if type(self).detachment_is_allowed: + nodes_prop.append(("detached", self.gen_detached)) + nodes_prop.append(("p_detached", self.gen_p_detached)) + gen_ids = self._aux_add_el_to_comp_graph(graph, first_id, cls.name_gen, @@ -2894,13 +2913,16 @@ def _aux_add_gens(self, graph, cls, first_id): def _aux_add_storages(self, graph, cls, first_id): nodes_prop = [("storage_charge", self.storage_charge), ("storage_power_target", self.storage_power_target), - ("detached", self.storage_detached), - ("p_detached", self.storage_p_detached),] + ] # TODO class attr in nodes_prop: storageEmax etc. edges_prop=[("p", self.storage_power)] if self.support_theta: edges_prop.append(("theta", self.storage_theta)) + + if type(self).detachment_is_allowed: + nodes_prop.append(("detached", self.storage_detached)) + nodes_prop.append(("p_detached", self.storage_p_detached)) sto_ids = self._aux_add_el_to_comp_graph(graph, first_id, cls.name_storage, @@ -4413,16 +4435,17 @@ def _update_obs_complete(self, env: "grid2op.Environment.BaseEnv", with_forecast self.delta_time = dt_float(1.0 * env.delta_time_seconds / 60.0) # slack (1.11.0) - self.gen_p_slack[:] = env._slack_gen_p + self.gen_p_delta[:] = env._delta_gen_p # detachment (>= 1.11.0) - self.load_detached[:] = env._loads_detached - self.gen_detached[:] = env._gens_detached - self.storage_detached[:] = env._storages_detached - self.load_p_detached[:] = env._load_p_detached - self.load_q_detached[:] = env._load_q_detached - self.gen_p_detached[:] = env._gen_p_detached - self.storage_p_detached[:] = env._storage_p_detached + if type(self).detachment_is_allowed: + self.load_detached[:] = env._loads_detached + self.gen_detached[:] = env._gens_detached + self.storage_detached[:] = env._storages_detached + self.load_p_detached[:] = env._load_p_detached + self.load_q_detached[:] = env._load_q_detached + self.gen_p_detached[:] = env._gen_p_detached + self.storage_p_detached[:] = env._storage_p_detached # handles forecasts here self._update_forecast(env, with_forecast) @@ -5092,4 +5115,24 @@ def check_kirchhoff(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarra storage_info, shunt_info) return p_subs, q_subs, p_bus, q_bus, diff_v_bus - \ No newline at end of file + + @classmethod + def process_detachment(cls): + if not cls.detachment_is_allowed: + # this is really important, otherwise things from grid2op base types will be affected + cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) + # remove the detachment from the list to vector + for el in ["load_detached", + "gen_detached", + "storage_detached", + "load_p_detached", + "load_q_detached", + "gen_p_detached", + "storage_p_detached",]: + if el in cls.attr_list_vect: + try: + cls.attr_list_vect.remove(el) + except ValueError: + pass + cls._update_value_set() + return super().process_detachment() diff --git a/grid2op/Observation/completeObservation.py b/grid2op/Observation/completeObservation.py index 7dcab022..31da50cb 100644 --- a/grid2op/Observation/completeObservation.py +++ b/grid2op/Observation/completeObservation.py @@ -196,7 +196,7 @@ class CompleteObservation(BaseObservation): "time_since_last_attack", "was_alert_used_after_attack", # slack (>= 1.11.0) - "gen_p_slack", + "gen_p_delta", # detachment (>= 1.11.0) "load_detached", "gen_detached", diff --git a/grid2op/tests/test_Observation.py b/grid2op/tests/test_Observation.py index bda6af69..80f66fdf 100644 --- a/grid2op/tests/test_Observation.py +++ b/grid2op/tests/test_Observation.py @@ -848,14 +848,14 @@ def setUp(self): "time_since_last_attack": [], "was_alert_used_after_attack": [], "attack_under_alert": [], - "gen_p_slack": [0.0, 0.0, 0.0, 0.0, 2.2990264892578125], - "load_detached": [False, False, False, False, False, False, False, False, False, False, False], - "gen_detached": [False, False, False, False, False], - "storage_detached": [], - "load_p_detached": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - "load_q_detached": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - "gen_p_detached": [0.0, 0.0, 0.0, 0.0, 0.0], - "storage_p_detached": [], + "gen_p_delta": [0.0, 0.0, 0.0, 0.0, 2.2990264892578125], + # "load_detached": [False, False, False, False, False, False, False, False, False, False, False], + # "gen_detached": [False, False, False, False, False], + # "storage_detached": [], + # "load_p_detached": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + # "load_q_detached": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + # "gen_p_detached": [0.0, 0.0, 0.0, 0.0, 0.0], + # "storage_p_detached": [], } self.dtypes = np.array( [ @@ -927,13 +927,13 @@ def setUp(self): # slack (>= 1.11.0) dt_float, # detachment (>= 1.11.0) - dt_bool, - dt_bool, - dt_bool, - dt_float, - dt_float, - dt_float, - dt_float, + # dt_bool, + # dt_bool, + # dt_bool, + # dt_float, + # dt_float, + # dt_float, + # dt_float, ], dtype=object, ) @@ -1004,17 +1004,17 @@ def setUp(self): 0, # slack (>= 1.11.0) 5, - # detachment (>= 1.11.0) - 11, - 5, - 0, - 11, - 11, - 5, - 0, + # # detachment (>= 1.11.0) + # 11, + # 5, + # 0, + # 11, + # 11, + # 5, + # 0, ] ) - self.size_obs = 429 + 4 + 4 + 2 + 1 + 10 + 5 + 0 + 5 + 11 + 5 + 0 + 11 + 11 + 5 + self.size_obs = 429 + 4 + 4 + 2 + 1 + 10 + 5 + 0 + 5 # + 11 + 5 + 0 + 11 + 11 + 5 def tearDown(self): self.env.close() diff --git a/grid2op/tests/test_attached_envs.py b/grid2op/tests/test_attached_envs.py index 0451dfb5..7a09e9c5 100644 --- a/grid2op/tests/test_attached_envs.py +++ b/grid2op/tests/test_attached_envs.py @@ -182,7 +182,52 @@ def test_action_space(self): def test_observation_space(self): assert issubclass(self.env.observation_space.subtype, CompleteObservation) - size_th = 467 + # size_th = 467 + size_th = 473 # gen_delta + assert self.env.observation_space.n == size_th, ( + f"obs space size is {self.env.observation_space.n}," f"should be {size_th}" + ) + + def test_random_action(self): + """test i can perform some step (random)""" + i = 0 + for i in range(10): + act = self.env.action_space.sample() + obs, reward, done, info = self.env.step(act) + if done: + break + assert i >= 1, ( + "could not perform the random action test because it games over first time step. " + "Please fix the test and try again" + ) + + +class TestL2RPN_CASE14_SANDBOX_DETACH(unittest.TestCase): + def setUp(self) -> None: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make("l2rpn_case14_sandbox", test=True, _add_to_name=type(self).__name__, allow_detachment=True) + self.env.seed(42) + _ = self.env.reset() + + def test_elements(self): + assert type(self.env).n_sub == 14 + assert type(self.env).n_line == 20 + assert type(self.env).n_load == 11 + assert type(self.env).n_gen == 6 + assert type(self.env).n_storage == 0 + + def test_opponent(self): + assert issubclass(self.env._opponent_action_class, DontAct) + assert self.env._opponent_action_space.n == 0 + + def test_action_space(self): + assert issubclass(self.env.action_space.subtype, PlayableAction) + assert self.env.action_space.n == 166, f"{self.env.action_space.n} instead of 166" + + def test_observation_space(self): + assert issubclass(self.env.observation_space.subtype, CompleteObservation) + size_th = 518 assert self.env.observation_space.n == size_th, ( f"obs space size is {self.env.observation_space.n}," f"should be {size_th}" ) @@ -226,7 +271,8 @@ def test_action_space(self): def test_observation_space(self): assert issubclass(self.env.observation_space.subtype, CompleteObservation) - size_th = 467 + # size_th = 467 + size_th = 473 # gen_p_delta assert self.env.observation_space.n == size_th, ( f"obs space size is {self.env.observation_space.n}," f"should be {size_th}" ) @@ -270,7 +316,8 @@ def test_action_space(self): def test_observation_space(self): assert issubclass(self.env.observation_space.subtype, CompleteObservation) - size_th = 475 + # size_th = 475 + size_th = 481 # gen_p_delta assert self.env.observation_space.n == size_th, ( f"obs space size is {self.env.observation_space.n}," f"should be {size_th}" ) diff --git a/grid2op/tests/test_shedding.py b/grid2op/tests/test_shedding.py index e38ae0a1..deddcc1e 100644 --- a/grid2op/tests/test_shedding.py +++ b/grid2op/tests/test_shedding.py @@ -346,7 +346,7 @@ def test_no_shedding(self): assert (np.abs(obs.storage_p_detached) <= 1e-7).all() # slack ok - assert np.abs(self.env._slack_gen_p.sum() / self.env._gen_activeprod_t.sum()) <= 0.02 # less than 2% losses + assert np.abs(self.env._delta_gen_p.sum() / self.env._gen_activeprod_t.sum()) <= 0.02 # less than 2% losses def test_shedding_load_step(self): # NB warning this test does not pass if STOP_EP_IF_SLACK_BREAK_CONSTRAINTS (slack breaks its rampdown !) @@ -390,8 +390,8 @@ def test_shedding_load_step(self): assert (np.abs(obs.storage_p_detached) <= 1e-7).all() # slack completely "messed up" - assert self.env._slack_gen_p.sum() <= -normal_load_p - assert obs.gen_p_slack.sum() <= -normal_load_p + assert self.env._delta_gen_p.sum() <= -normal_load_p + assert obs.gen_p_delta.sum() <= -normal_load_p # another step obs, reward, done, info = self.env.step(self.env.action_space()) @@ -404,7 +404,7 @@ def test_shedding_load_step(self): normal_load_q = dt_float(15.2) assert np.abs(self.env._load_p_detached[0] - normal_load_p) <= 1e-7 assert np.abs(self.env._load_q_detached[0] - normal_load_q) <= 1e-7 - assert self.env._slack_gen_p.sum() <= -normal_load_p + assert self.env._delta_gen_p.sum() <= -normal_load_p # another step obs, reward, done, info = self.env.step(self.env.action_space()) @@ -417,7 +417,7 @@ def test_shedding_load_step(self): normal_load_q = dt_float(15.1) assert np.abs(self.env._load_p_detached[0] - normal_load_p) <= 1e-7 assert np.abs(self.env._load_q_detached[0] - normal_load_q) <= 1e-7 - assert self.env._slack_gen_p.sum() <= -normal_load_p + assert self.env._delta_gen_p.sum() <= -normal_load_p # now attached it again obs, reward, done, info = self.env.step(self.env.action_space({"set_bus": {"loads_id": [(0, 1)]}})) @@ -429,7 +429,7 @@ def test_shedding_load_step(self): assert np.abs(self.env._load_p_detached[0] - 0.) <= 1e-7 assert np.abs(self.env._load_q_detached[0] - 0.) <= 1e-7 # slack ok - assert np.abs(self.env._slack_gen_p.sum() / self.env._gen_activeprod_t.sum()) <= 0.02 # less than 2% losses + assert np.abs(self.env._delta_gen_p.sum() / self.env._gen_activeprod_t.sum()) <= 0.02 # less than 2% losses From 953d2734b52f7ef72c5812d300c1f366bb8b3f52 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 13 Dec 2024 17:00:36 +0100 Subject: [PATCH 55/60] fix some other broken tests Signed-off-by: DONNOT Benjamin --- grid2op/gym_compat/gym_act_space.py | 11 ++-- grid2op/gym_compat/gym_space_converter.py | 5 +- grid2op/tests/_aux_test_gym_compat.py | 3 +- grid2op/tests/test_alert_gym_compat.py | 75 +++++++++++------------ grid2op/tests/test_issue_418.py | 4 +- 5 files changed, 50 insertions(+), 48 deletions(-) diff --git a/grid2op/gym_compat/gym_act_space.py b/grid2op/gym_compat/gym_act_space.py index c0dd4643..ee1033a1 100644 --- a/grid2op/gym_compat/gym_act_space.py +++ b/grid2op/gym_compat/gym_act_space.py @@ -108,9 +108,12 @@ class __AuxGymActionSpace: "_curtail": "curtail", "_raise_alarm": "raise_alarm", "_raise_alert": "raise_alert", - "shunt_p": "_shunt_p", - "shunt_q": "_shunt_q", - "shunt_bus": "_shunt_bus", + "shunt_p": "shunt_p", + "_shunt_q": "shunt_q", + "_shunt_bus": "shunt_bus", + "_detach_load": "detach_load", # new in 1.11.0 + "_detach_gen": "detach_gen", # new in 1.11.0 + "_detach_storage": "detach_storage", # new in 1.11.0 } keys_human_2_grid2op = {v: k for k, v in keys_grid2op_2_human.items()} @@ -213,7 +216,7 @@ def reencode_space(self, key, fun): raise RuntimeError( "Impossible to reencode a space that is a converter space." ) - + my_dict = self.get_dict_encoding() if fun is not None and not isinstance(fun, type(self)._BaseGymAttrConverterType): raise RuntimeError( diff --git a/grid2op/gym_compat/gym_space_converter.py b/grid2op/gym_compat/gym_space_converter.py index 5aa7d509..d10c75e6 100644 --- a/grid2op/gym_compat/gym_space_converter.py +++ b/grid2op/gym_compat/gym_space_converter.py @@ -213,7 +213,10 @@ def get_dict_encoding(self): ------- """ - return copy.deepcopy(self._keys_encoding) + res = {} + for k, v in self._keys_encoding.items(): + res[k] = v # TODO shedding, why I can't deep copy this anymore ? + return res def reencode_space(self, key, func): """ diff --git a/grid2op/tests/_aux_test_gym_compat.py b/grid2op/tests/_aux_test_gym_compat.py index 6f574b37..59435553 100644 --- a/grid2op/tests/_aux_test_gym_compat.py +++ b/grid2op/tests/_aux_test_gym_compat.py @@ -153,7 +153,7 @@ def test_convert_togym(self): for el in env_gym.observation_space.spaces ] ) - size_th = 536 # as of grid2Op 1.7.1 (where all obs attributes are there) + size_th = 542 # as of grid2Op 1.11.0 (with obs.gen_p_delta) assert ( dim_obs_space == size_th ), f"Size should be {size_th} but is {dim_obs_space}" @@ -1683,7 +1683,6 @@ def test_supported_keys(self): # check that all types ok_ = func_check[attr_nm](grid2op_act) if not ok_: - pdb.set_trace() raise RuntimeError( f"Some property of the actions are not modified for attr {attr_nm}" ) diff --git a/grid2op/tests/test_alert_gym_compat.py b/grid2op/tests/test_alert_gym_compat.py index 04895e37..59a1e5fc 100644 --- a/grid2op/tests/test_alert_gym_compat.py +++ b/grid2op/tests/test_alert_gym_compat.py @@ -77,50 +77,48 @@ def test_print_alert(self): "1.4 0. 0. 0. 0. 0. 2.8 0. 0. 2.8\n 0. 0. 4.3 0. 0. 2.8 8.5 9.9], " "(22,), float32), 'set_bus': Box(-1, 2, (177,), int32), 'set_line_status': Box(-1, 1, (59,), int32))") str_ = env_gym.observation_space.__str__() + assert str_ == ("Dict('_shunt_bus': Box(-2147483648, 2147483647, (6,), int32), '_shunt_p': Box(-inf, inf, (6,), float32), " - "'_shunt_q': Box(-inf, inf, (6,), float32), '_shunt_v': Box(-inf, inf, (6,), float32), " - "'a_ex': Box(0.0, inf, (59,), float32), 'a_or': Box(0.0, inf, (59,), float32), 'active_alert': MultiBinary(10), " - "'actual_dispatch': Box([ -50. -67.2 -50. -250. -50. -33.6 -37.3 -37.3 -33.6 -74.7\n -100. -37.3 -37.3 " - "-100. -74.7 -74.7 -150. -67.2 -74.7 -400.\n -300. -350. ], [ 50. 67.2 50. 250. 50. 33.6 37.3 37.3 " - "33.6 74.7 100. 37.3\n 37.3 100. 74.7 74.7 150. 67.2 74.7 400. 300. 350. ], (22,), float32), " - "'alert_duration': Box(0, 2147483647, (10,), int32), 'attack_under_alert': Box(-1, 1, (10,), int32), " - "'attention_budget': Box(0.0, inf, (1,), float32), 'current_step': Box(-2147483648, 2147483647, (1,), int32), " - "'curtailment': Box(0.0, 1.0, (22,), float32), 'curtailment_limit': Box(0.0, 1.0, (22,), float32), " - "'curtailment_limit_effective': Box(0.0, 1.0, (22,), float32), 'day': Discrete(32), 'day_of_week': Discrete(8), " - "'delta_time': Box(0.0, inf, (1,), float32), 'duration_next_maintenance': Box(-1, 2147483647, (59,), int32), " - "'gen_margin_down': Box(0.0, [ 1.4 0. 1.4 10.4 1.4 0. 0. 0. 0. 0. 2.8 0. 0. 2.8\n 0. 0. " - "4.3 0. 0. 2.8 8.5 9.9], (22,), float32), 'gen_margin_up': Box(0.0, [ 1.4 0. 1.4 10.4 1.4 0. " - "0. 0. 0. 0. 2.8 0. 0. 2.8\n 0. 0. 4.3 0. 0. 2.8 8.5 9.9], (22,), float32), " - "'gen_p': Box(-734.88995, [ 784.88995 802.08997 784.88995 984.88995 784.88995 768.4899\n " - "772.18994 772.18994 768.4899 809.58997 834.88995 772.18994\n 772.18994 834.88995 " - "809.58997 809.58997 884.88995 802.08997\n 809.58997 1134.8899 1034.8899 1084.8899 ], " - "(22,), float32), 'gen_p_before_curtail': Box(-734.88995, [ 784.88995 802.08997 784.88995 " - "984.88995 784.88995 768.4899\n 772.18994 772.18994 768.4899 809.58997 834.88995 " - "772.18994\n 772.18994 834.88995 809.58997 809.58997 884.88995 802.08997\n 809.58997 " - "1134.8899 1034.8899 1084.8899 ], (22,), float32), 'gen_q': Box(-inf, inf, (22,), float32), " - "'gen_theta': Box(-180.0, 180.0, (22,), float32), 'gen_v': Box(0.0, inf, (22,), float32), " - "'hour_of_day': Discrete(24), 'is_alarm_illegal': Discrete(2), 'line_status': MultiBinary(59), " - "'load_p': Box(-inf, inf, (37,), float32), 'load_q': Box(-inf, inf, (37,), float32), " - "'load_theta': Box(-180.0, 180.0, (37,), float32), 'load_v': Box(0.0, inf, (37,), float32), " - "'max_step': Box(-2147483648, 2147483647, (1,), int32), 'minute_of_hour': Discrete(60), " - "'month': Discrete(13), 'p_ex': Box(-inf, inf, (59,), float32), 'p_or': Box(-inf, inf, (59,), float32), " - "'q_ex': Box(-inf, inf, (59,), float32), 'q_or': Box(-inf, inf, (59,), float32), 'rho': Box(0.0, inf, (59,), float32), " - "'target_dispatch': Box([ -50. -67.2 -50. -250. -50. -33.6 -37.3 -37.3 -33.6 -74.7\n -100. " - "-37.3 -37.3 -100. -74.7 -74.7 -150. -67.2 -74.7 -400.\n -300. -350. ], [ 50. 67.2 50. 250. 50. " - "33.6 37.3 37.3 33.6 74.7 100. 37.3\n 37.3 100. 74.7 74.7 150. 67.2 74.7 400. 300. 350. ], " - "(22,), float32), 'thermal_limit': Box(0.0, inf, (59,), float32), 'theta_ex': Box(-180.0, 180.0, (59,), " - "float32), 'theta_or': Box(-180.0, 180.0, (59,), float32), 'time_before_cooldown_line': Box(0, 96, (59,), int32), " - "'time_before_cooldown_sub': Box(0, 3, (36,), int32), 'time_next_maintenance': Box(-1, 2147483647, (59,), int32), " - "'time_since_last_alarm': Box(-1, 2147483647, (1,), int32), 'time_since_last_alert': Box(-1, 2147483647, (10,), int32), " - "'time_since_last_attack': Box(-1, 2147483647, (10,), int32), 'timestep_overflow': Box(-2147483648, 2147483647, (59,), int32), " - "'topo_vect': Box(-1, 2, (177,), int32), 'total_number_of_alert': Box(0, 2147483647, (1,), int32), 'v_ex': Box(0.0, inf, " - "(59,), float32), 'v_or': Box(0.0, inf, (59,), float32), 'was_alarm_used_after_game_over': Discrete(2), " + "'_shunt_q': Box(-inf, inf, (6,), float32), '_shunt_v': Box(-inf, inf, (6,), float32), 'a_ex': Box(0.0, inf, " + "(59,), float32), 'a_or': Box(0.0, inf, (59,), float32), 'active_alert': MultiBinary(10), 'actual_dispatch': " + "Box([ -50. -67.2 -50. -250. -50. -33.6 -37.3 -37.3 -33.6 -74.7\n -100. -37.3 -37.3 -100. -74.7 " + "-74.7 -150. -67.2 -74.7 -400.\n -300. -350. ], [ 50. 67.2 50. 250. 50. 33.6 37.3 37.3 33.6 74.7 100. " + "37.3\n 37.3 100. 74.7 74.7 150. 67.2 74.7 400. 300. 350. ], (22,), float32), 'alert_duration': " + "Box(0, 2147483647, (10,), int32), 'attack_under_alert': Box(-1, 1, (10,), int32), 'attention_budget': " + "Box(0.0, inf, (1,), float32), 'current_step': Box(-2147483648, 2147483647, (1,), int32), 'curtailment': " + "Box(0.0, 1.0, (22,), float32), 'curtailment_limit': Box(0.0, 1.0, (22,), float32), 'curtailment_limit_effective': " + "Box(0.0, 1.0, (22,), float32), 'day': Discrete(32), 'day_of_week': Discrete(8), 'delta_time': Box(0.0, inf, (1,), " + "float32), 'duration_next_maintenance': Box(-1, 2147483647, (59,), int32), 'gen_margin_down': Box(0.0, " + "[ 1.4 0. 1.4 10.4 1.4 0. 0. 0. 0. 0. 2.8 0. 0. 2.8\n 0. 0. 4.3 0. 0. 2.8 8.5 " + "9.9], (22,), float32), 'gen_margin_up': Box(0.0, [ 1.4 0. 1.4 10.4 1.4 0. 0. 0. 0. 0. 2.8 0. " + "0. 2.8\n 0. 0. 4.3 0. 0. 2.8 8.5 9.9], (22,), float32), 'gen_p': Box(-734.88995, [ 784.88995 " + "802.08997 784.88995 984.88995 784.88995 768.4899\n 772.18994 772.18994 768.4899 809.58997 834.88995 " + "772.18994\n 772.18994 834.88995 809.58997 809.58997 884.88995 802.08997\n 809.58997 1134.8899 1034.8899 " + "1084.8899 ], (22,), float32), 'gen_p_before_curtail': Box(-734.88995, [ 784.88995 802.08997 784.88995 984.88995 " + "784.88995 768.4899\n 772.18994 772.18994 768.4899 809.58997 834.88995 772.18994\n 772.18994 834.88995 " + "809.58997 809.58997 884.88995 802.08997\n 809.58997 1134.8899 1034.8899 1084.8899 ], (22,), float32), " + "'gen_p_delta': Box(-inf, inf, (22,), float32), 'gen_q': Box(-inf, inf, (22,), float32), 'gen_theta': Box(-180.0, " + "180.0, (22,), float32), 'gen_v': Box(0.0, inf, (22,), float32), 'hour_of_day': Discrete(24), 'is_alarm_illegal': " + "Discrete(2), 'line_status': MultiBinary(59), 'load_p': Box(-inf, inf, (37,), float32), 'load_q': Box(-inf, inf, " + "(37,), float32), 'load_theta': Box(-180.0, 180.0, (37,), float32), 'load_v': Box(0.0, inf, (37,), float32), 'max_step': " + "Box(-2147483648, 2147483647, (1,), int32), 'minute_of_hour': Discrete(60), 'month': Discrete(13), 'p_ex': " + "Box(-inf, inf, (59,), float32), 'p_or': Box(-inf, inf, (59,), float32), 'q_ex': Box(-inf, inf, (59,), float32), " + "'q_or': Box(-inf, inf, (59,), float32), 'rho': Box(0.0, inf, (59,), float32), 'target_dispatch': Box([ -50. " + "-67.2 -50. -250. -50. -33.6 -37.3 -37.3 -33.6 -74.7\n -100. -37.3 -37.3 -100. -74.7 -74.7 " + "-150. -67.2 -74.7 -400.\n -300. -350. ], [ 50. 67.2 50. 250. 50. 33.6 37.3 37.3 33.6 74.7 100. " + "37.3\n 37.3 100. 74.7 74.7 150. 67.2 74.7 400. 300. 350. ], (22,), float32), 'thermal_limit': Box(0.0, inf, " + "(59,), float32), 'theta_ex': Box(-180.0, 180.0, (59,), float32), 'theta_or': Box(-180.0, 180.0, (59,), float32), " + "'time_before_cooldown_line': Box(0, 96, (59,), int32), 'time_before_cooldown_sub': Box(0, 3, (36,), int32), " + "'time_next_maintenance': Box(-1, 2147483647, (59,), int32), 'time_since_last_alarm': Box(-1, 2147483647, " + "(1,), int32), 'time_since_last_alert': Box(-1, 2147483647, (10,), int32), 'time_since_last_attack': " + "Box(-1, 2147483647, (10,), int32), 'timestep_overflow': Box(-2147483648, 2147483647, (59,), int32), " + "'topo_vect': Box(-1, 2, (177,), int32), 'total_number_of_alert': Box(0, 2147483647, (1,), int32), 'v_ex': " + "Box(0.0, inf, (59,), float32), 'v_or': Box(0.0, inf, (59,), float32), 'was_alarm_used_after_game_over': Discrete(2), " "'was_alert_used_after_attack': Box(-1, 1, (10,), int32), 'year': Discrete(2100))") act = self.env.action_space() act.raise_alert = [2] act_gym = env_gym.action_space.to_gym(act) act_str = act_gym.__str__() - assert act_str == ("OrderedDict([('change_bus', array([False, False, False, False, False, False, False, False, False," "\n False, False, False, False, False, False, False, False, False,\n " "False, False, False, False, False, False, False, False, False,\n False, False, False, " @@ -169,6 +167,7 @@ def test_convert_alert_to_gym(self): ] ) size_th = 1718 # as of grid2Op 1.9.1 (where alerts are added) + size_th = 1740 # as of grid2Op 1.11.0 (where gen_p_delta) assert ( dim_obs_space == size_th ), f"Size should be {size_th} but is {dim_obs_space}" diff --git a/grid2op/tests/test_issue_418.py b/grid2op/tests/test_issue_418.py index 0621a2ca..e72d40fa 100644 --- a/grid2op/tests/test_issue_418.py +++ b/grid2op/tests/test_issue_418.py @@ -37,10 +37,8 @@ def test_seed(self): obs = gymenv.reset(seed=42) curt = np.array([1,1.,0.18852758,0.5537014,0.43770432,1]) curt = np.array([-1,-1.,0.18852758,0.5537014,0.43770432,-1]) - year = 571 + year = 1476 # gen_p_delta being sampled before day = 9 - # year = 1887 - # day = 9 # test that the seeding worked also in action space and observation space sampled_act = gymenv.action_space.sample() From 0a40dafc8d73790401a4c7fd47a00862d2ae684c Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 13 Dec 2024 17:23:13 +0100 Subject: [PATCH 56/60] yet some other fixes Signed-off-by: DONNOT Benjamin --- grid2op/Space/GridObjects.py | 8 ++++++-- grid2op/gym_compat/gym_act_space.py | 6 +++--- grid2op/tests/automatic_classes.py | 3 ++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index d6a9b147..a5e7fc82 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -23,7 +23,7 @@ import numpy as np import sys from packaging import version -from typing import Dict, Union, Literal, Any, List, Optional, ClassVar, Tuple +from typing import Dict, Type, Union, Literal, Any, List, Optional, ClassVar, Tuple import grid2op from grid2op.dtypes import dt_int, dt_float, dt_bool @@ -4553,11 +4553,14 @@ def _build_cls_from_import(name_cls, path_env): try: module = importlib.import_module(GRID2OP_CLASSES_ENV_FOLDER) if hasattr(module, name_cls): - my_class = getattr(module, name_cls) + my_class : Type["GridObjects"] = getattr(module, name_cls) except (ModuleNotFoundError, ImportError) as exc_: # normal behaviour i don't do anything there # TODO explain why pass + my_class.process_grid2op_compat() + my_class.process_detachment() + my_class.process_shunt_static_data() return my_class @staticmethod @@ -4594,6 +4597,7 @@ def init_grid_from_dict_for_pickle(name_res, orig_cls, cls_attr): if res_cls.glop_version != grid2op.__version__: res_cls.process_grid2op_compat() res_cls.process_shunt_static_data() + res_cls.process_detachment() # add the class in the "globals" for reuse later globals()[name_res] = res_cls diff --git a/grid2op/gym_compat/gym_act_space.py b/grid2op/gym_compat/gym_act_space.py index ee1033a1..02c7887c 100644 --- a/grid2op/gym_compat/gym_act_space.py +++ b/grid2op/gym_compat/gym_act_space.py @@ -108,9 +108,9 @@ class __AuxGymActionSpace: "_curtail": "curtail", "_raise_alarm": "raise_alarm", "_raise_alert": "raise_alert", - "shunt_p": "shunt_p", - "_shunt_q": "shunt_q", - "_shunt_bus": "shunt_bus", + "shunt_p": "_shunt_p", + "shunt_q": "_shunt_q", + "shunt_bus": "_shunt_bus", "_detach_load": "detach_load", # new in 1.11.0 "_detach_gen": "detach_gen", # new in 1.11.0 "_detach_storage": "detach_storage", # new in 1.11.0 diff --git a/grid2op/tests/automatic_classes.py b/grid2op/tests/automatic_classes.py index f0eb5d05..0fca0185 100644 --- a/grid2op/tests/automatic_classes.py +++ b/grid2op/tests/automatic_classes.py @@ -533,7 +533,8 @@ def setUp(self) -> None: warnings.filterwarnings("ignore") self.env = grid2op.make("l2rpn_case14_sandbox", test=True, - class_in_file=True) + class_in_file=True, + ) self.line_id = 3 th_lim = self.env.get_thermal_limit() * 2. # avoid all problem in general th_lim[self.line_id] /= 10. # make sure to get trouble in line 3 From 7d8af657310ec026d0b2e11452cfcfab2d5e0a03 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 13 Dec 2024 22:03:02 +0100 Subject: [PATCH 57/60] upgrade to version 1.11.0.dev3 Signed-off-by: DONNOT Benjamin --- docs/conf.py | 2 +- grid2op/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 5205f702..191c276a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ author = 'Benjamin Donnot' # The full version, including alpha/beta/rc tags -release = '1.11.0.dev2' +release = '1.11.0.dev3' version = '1.11' diff --git a/grid2op/__init__.py b/grid2op/__init__.py index 95050dfb..e387ae1c 100644 --- a/grid2op/__init__.py +++ b/grid2op/__init__.py @@ -11,7 +11,7 @@ Grid2Op a testbed platform to model sequential decision making in power systems. """ -__version__ = '1.11.0.dev2' +__version__ = '1.11.0.dev3' __all__ = [ "Action", From cb7a9f436d03da9dca5b0b4ab9af345124e08b28 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Mon, 16 Dec 2024 09:46:28 +0100 Subject: [PATCH 58/60] fixing some issues spotted by sonarcube Signed-off-by: DONNOT Benjamin --- grid2op/Action/_backendAction.py | 124 ++++++++++++++++-------------- grid2op/Action/actionSpace.py | 2 - grid2op/Action/baseAction.py | 124 +++++++++++++++--------------- grid2op/Environment/baseEnv.py | 7 +- grid2op/Opponent/opponentSpace.py | 8 +- grid2op/Space/GridObjects.py | 3 + 6 files changed, 140 insertions(+), 128 deletions(-) diff --git a/grid2op/Action/_backendAction.py b/grid2op/Action/_backendAction.py index 0434335c..ec23c13b 100644 --- a/grid2op/Action/_backendAction.py +++ b/grid2op/Action/_backendAction.py @@ -733,7 +733,66 @@ def _aux_iadd_reconcile_disco_reco(self): self.line_ex_pos_topo_vect, self.last_topo_registered, ) - + + def _aux_iadd_detach(self, other, set_topo_vect : np.ndarray, modif_inj: bool): + cls = type(self) + if other._modif_detach_load: + set_topo_vect[cls.load_pos_topo_vect[other._detach_load]] = -1 + modif_set_bus = True + if other._modif_detach_gen: + set_topo_vect[cls.gen_pos_topo_vect[other._detach_gen]] = -1 + modif_set_bus = True + if other._modif_detach_storage: + set_topo_vect[cls.storage_pos_topo_vect[other._detach_storage]] = -1 + modif_set_bus = True + if modif_inj: + for key, vect_ in other._dict_inj.items(): + if key == "load_p" or key == "load_q": + vect_[other._detach_load] = 0. + elif key == "prod_p": + vect_[other._detach_gen] = 0. + elif key == "prod_v": + vect_[other._detach_gen] = np.nan + else: + raise RuntimeError(f"Unknown key {key} for injection found.") + else: + # TODO when injection is not modified by the action (eg change nothing) + if other._modif_detach_load: + modif_inj = True + other._dict_inj["load_p"] = np.full(cls.n_load, fill_value=np.nan, dtype=dt_float) + other._dict_inj["load_q"] = np.full(cls.n_load, fill_value=np.nan, dtype=dt_float) + other._dict_inj["load_p"][other._detach_load] = 0. + other._dict_inj["load_q"][other._detach_load] = 0. + if other._modif_detach_gen: + modif_inj = True + other._dict_inj["prod_p"] = np.full(cls.n_gen, fill_value=np.nan, dtype=dt_float) + other._dict_inj["prod_p"][other._detach_gen] = 0. + return modif_set_bus, modif_inj + + def _aux_iadd_line_status(self, other: BaseAction, switch_status: np.ndarray, set_status: np.ndarray): + if other._modif_change_status: + self.current_topo.change_status( + switch_status, + self.line_or_pos_topo_vect, + self.line_ex_pos_topo_vect, + self.last_topo_registered, + ) + if other._modif_set_status: + self.current_topo.set_status( + set_status, + self.line_or_pos_topo_vect, + self.line_ex_pos_topo_vect, + self.last_topo_registered, + ) + + # if other._modif_change_status or other._modif_set_status: + ( + self._status_or_before[:], + self._status_ex_before[:], + ) = self.current_topo.get_line_status( + self.line_or_pos_topo_vect, self.line_ex_pos_topo_vect + ) + def __iadd__(self, other : BaseAction) -> Self: """ .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ @@ -770,43 +829,13 @@ def __iadd__(self, other : BaseAction) -> Self: modif_inj = other._modif_inj cls = type(self) - # III detachment (before all else) + # I detachment (before all else) if cls.detachment_is_allowed and other.has_element_detached(): - if other._modif_detach_load: - set_topo_vect[cls.load_pos_topo_vect[other._detach_load]] = -1 - modif_set_bus = True - if other._modif_detach_gen: - set_topo_vect[cls.gen_pos_topo_vect[other._detach_gen]] = -1 - modif_set_bus = True - if other._modif_detach_storage: - set_topo_vect[cls.storage_pos_topo_vect[other._detach_storage]] = -1 - modif_set_bus = True - if modif_inj: - for key, vect_ in other._dict_inj.items(): - if key == "load_p" or key == "load_q": - vect_[other._detach_load] = 0. - elif key == "prod_p": - vect_[other._detach_gen] = 0. - elif key == "prod_v": - vect_[other._detach_gen] = np.nan - else: - raise NotImplementedError(f"Unknown key {key} for injection found.") - else: - # TODO when injection is not modified by the action (eg change nothing) - if other._modif_detach_load: - modif_inj = True - other._dict_inj["load_p"] = np.full(cls.n_load, fill_value=np.nan, dtype=dt_float) - other._dict_inj["load_q"] = np.full(cls.n_load, fill_value=np.nan, dtype=dt_float) - other._dict_inj["load_p"][other._detach_load] = 0. - other._dict_inj["load_q"][other._detach_load] = 0. - if other._modif_detach_gen: - modif_inj = True - other._dict_inj["prod_p"] = np.full(cls.n_gen, fill_value=np.nan, dtype=dt_float) - other._dict_inj["prod_p"][other._detach_gen] = 0. + modif_set_bus, modif_inj = self._aux_iadd_detach(other, set_topo_vect, modif_inj) # I deal with injections # Ia set the injection - if other._modif_inj: + if modif_inj: self._aux_iadd_inj(other._dict_inj) # Ib change the injection aka redispatching @@ -824,29 +853,8 @@ def __iadd__(self, other : BaseAction) -> Self: # III line status # this need to be done BEFORE the topology, as a connected powerline will be connected to their old bus. # regardless if the status is changed in the action or not. - if other._modif_change_status: - self.current_topo.change_status( - switch_status, - self.line_or_pos_topo_vect, - self.line_ex_pos_topo_vect, - self.last_topo_registered, - ) - if other._modif_set_status: - self.current_topo.set_status( - set_status, - self.line_or_pos_topo_vect, - self.line_ex_pos_topo_vect, - self.last_topo_registered, - ) - - # if other._modif_change_status or other._modif_set_status: - ( - self._status_or_before[:], - self._status_ex_before[:], - ) = self.current_topo.get_line_status( - self.line_or_pos_topo_vect, self.line_ex_pos_topo_vect - ) - + self._aux_iadd_line_status(other, switch_status, set_status) + # IV topo if other._modif_change_bus: self.current_topo.change_val(switcth_topo_vect) @@ -860,7 +868,7 @@ def __iadd__(self, other : BaseAction) -> Self: ) # At least one disconnected extremity - if other._modif_change_bus or other._modif_set_bus: + if other._modif_change_bus or modif_set_bus: self._aux_iadd_reconcile_disco_reco() return self diff --git a/grid2op/Action/actionSpace.py b/grid2op/Action/actionSpace.py index 5c269491..5ebec567 100644 --- a/grid2op/Action/actionSpace.py +++ b/grid2op/Action/actionSpace.py @@ -70,8 +70,6 @@ def __init__( Class specifying the rules of the game used to check the legality of the actions. """ - # actionClass._add_shunt_data() - # actionClass._update_value_set() SerializableActionSpace.__init__(self, gridobj, actionClass=actionClass, _local_dir_cls=_local_dir_cls) self.legal_action = legal_action diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 52e6bbdb..7e68cce8 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -400,7 +400,6 @@ class BaseAction(GridObjects): "prod_v", "load_p", "load_q"]) - # attr_nan_list_set = set() attr_list_set = set(attr_list_vect) shunt_added = False @@ -410,6 +409,7 @@ class BaseAction(GridObjects): ERR_ACTION_CUT = 'The action added to me will be cut, because i don\'t support modification of "{}"' ERR_NO_STOR_SET_BUS = 'Impossible to modify the storage bus (with "set") with this action type.' + OBJ_SUPPORT_DETACH = ["load", "gen", "storage"] def __init__(self, _names_chronics_to_backend: Optional[Dict[Literal["loads", "prods", "lines"], Dict[str, str]]]=None): """ @@ -806,7 +806,7 @@ def as_serializable_dict(self) -> dict: del res["shunt"] if cls.detachment_is_allowed: - for el in ["load", "gen", "storage"]: + for el in cls.OBJ_SUPPORT_DETACH: attr_key = f"detach_{el}" attr_vect = f"_detach_{el}" xxx_name = getattr(cls, f"name_{el}") @@ -916,7 +916,7 @@ def process_grid2op_compat(cls): cls.authorized_keys = copy.deepcopy(cls.authorized_keys) cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) - for el in ["load", "gen", "storage"]: + for el in cls.OBJ_SUPPORT_DETACH: attr_key = f"detach_{el}" attr_vect = f"_{attr_key}" if attr_key in cls.authorized_keys: @@ -1165,14 +1165,14 @@ def _aux_eq_detachment(self, other: "BaseAction") -> bool: if cls.detachment_is_allowed: if cls_oth.detachment_is_allowed: # easy case, both detachement allowed - for el in ["load", "gen", "storage"]: + for el in cls.OBJ_SUPPORT_DETACH: if not self._aux_eq_detachment_aux_both_ok(other, el): return False else: # self supports detachment but not other # they are equal if an only if self did not # modify any loads with detachment - for el in ["load", "gen", "storage"]: + for el in cls.OBJ_SUPPORT_DETACH: if not self._aux_eq_detachment_aux_one_not_ok(self, el): return False else: @@ -1183,7 +1183,7 @@ def _aux_eq_detachment(self, other: "BaseAction") -> bool: # oth does. # they can be equal if oth does not modify this # attribute - for el in ["load", "gen", "storage"]: + for el in cls.OBJ_SUPPORT_DETACH: if not self._aux_eq_detachment_aux_one_not_ok(other, el): return False else: @@ -1210,6 +1210,45 @@ def _aux_eq_shunts(self, other: "BaseAction") -> bool: if not (self.shunt_bus == other.shunt_bus).all(): return False return True + + def _aux_eq_compare_vect(self, other, modif_flag_nm, vect_nm): + """Implement something similar to : + + ((self._modif_set_status != other._modif_set_status) or + not np.all(self._set_line_status == other._set_line_status) + ) + But for different flag (*eg* `_modif_set_status`) and vector (*eg* `_set_line_status`) + """ + return ((getattr(self, modif_flag_nm) != getattr(other, modif_flag_nm)) or + not np.array_equal(getattr(self, vect_nm), getattr(other, vect_nm)) + ) + + def _aux_eq_inj(self, other: "BaseAction"): + # mismatch in flags + same_action = self._modif_inj == other._modif_inj + if not same_action: + return False + + # all injections are the same + for el in other._dict_inj.keys(): + if el not in self._dict_inj: + # other modify "el" but not "self" + return False + # all injections are the same + for el in self._dict_inj.keys(): + if el not in other._dict_inj: + # "self" modify "el" but not "other" + return False + for el in self._dict_inj.keys(): + me_inj = self._dict_inj[el] + other_inj = other._dict_inj[el] + tmp_me = np.isfinite(me_inj) + tmp_other = np.isfinite(other_inj) + if not np.all(tmp_me == tmp_other) or not np.all( + me_inj[tmp_me] == other_inj[tmp_other] + ): + return False + return True def __eq__(self, other: "BaseAction") -> bool: """ @@ -1252,37 +1291,17 @@ def __eq__(self, other: "BaseAction") -> bool: return False # _grid is the same, now I test the the injections modifications are the same - same_action = self._modif_inj == other._modif_inj - same_action = same_action and self._dict_inj.keys() == other._dict_inj.keys() - if not same_action: + if not self._aux_eq_inj(other): return False - - # all injections are the same - for el in self._dict_inj.keys(): - me_inj = self._dict_inj[el] - other_inj = other._dict_inj[el] - tmp_me = np.isfinite(me_inj) - tmp_other = np.isfinite(other_inj) - if not np.all(tmp_me == tmp_other) or not np.all( - me_inj[tmp_me] == other_inj[tmp_other] - ): - return False - + # same line status - if (self._modif_set_status != other._modif_set_status) or not np.all( - self._set_line_status == other._set_line_status - ): + if self._aux_eq_compare_vect(other, "_modif_set_status", "_set_line_status"): return False - - if (self._modif_change_status != other._modif_change_status) or not np.all( - self._switch_line_status == other._switch_line_status - ): + if self._aux_eq_compare_vect(other, "_modif_change_status", "_switch_line_status"): return False # redispatching is same - if (self._modif_redispatch != other._modif_redispatch) or not np.all( - self._redispatch == other._redispatch - ): + if self._aux_eq_compare_vect(other, "_modif_redispatch", "_redispatch"): return False # storage is same @@ -1296,31 +1315,21 @@ def __eq__(self, other: "BaseAction") -> bool: return False # curtailment - if (self._modif_curtailment != other._modif_curtailment) or not np.array_equal( - self._curtail, other._curtail - ): + if self._aux_eq_compare_vect(other, "_modif_curtailment", "_curtail"): return False # alarm - if (self._modif_alarm != other._modif_alarm) or not np.array_equal( - self._raise_alarm, other._raise_alarm - ): + if self._aux_eq_compare_vect(other, "_modif_alarm", "_raise_alarm"): return False - # alarm - if (self._modif_alert != other._modif_alert) or not np.array_equal( - self._raise_alert, other._raise_alert - ): + # alert + if self._aux_eq_compare_vect(other, "_modif_alert", "_raise_alert"): return False # same topology changes - if (self._modif_set_bus != other._modif_set_bus) or not np.all( - self._set_topo_vect == other._set_topo_vect - ): + if self._aux_eq_compare_vect(other, "_modif_set_bus", "_set_topo_vect"): return False - if (self._modif_change_bus != other._modif_change_bus) or not np.all( - self._change_bus_vect == other._change_bus_vect - ): + if self._aux_eq_compare_vect(other, "_modif_change_bus", "_change_bus_vect"): return False # handle detachment @@ -2401,8 +2410,7 @@ def _digest_change_status(self, dict_): def _digest_detach_eltype(self, el : Literal["load", "gen", "storage"], dict_): attr_key = f'detach_{el}' - if attr_key in dict_: - if dict_[attr_key] is not None: + if attr_key in dict_ and dict_[attr_key] is not None: setattr(self, attr_key, dict_[attr_key]) # eg self.detach_load = dict_["detach_load"] @@ -2725,7 +2733,7 @@ def update(self, self._digest_alarm(dict_) self._digest_alert(dict_) if cls.detachment_is_allowed: - for el in ["load", "gen", "storage"]: + for el in cls.OBJ_SUPPORT_DETACH: self._digest_detach_eltype(el, dict_) return self @@ -2856,7 +2864,7 @@ def _check_for_correct_modif_flags(self): raise IllegalAction("You illegally send an alert.") if cls.detachment_is_allowed: - for el in ["load", "gen", "storage"]: + for el in cls.OBJ_SUPPORT_DETACH: attr_auth = f"detach_{el}" attr_modif = f"_modif_detach_{el}" attr_vect = f"_detach_{el}" @@ -3233,7 +3241,7 @@ def _is_detachment_ambiguous(self): raise IllegalAction("It's forbidden to do a generator detachment with this action type") if self._modif_detach_storage and "detach_storage" not in cls.authorized_keys: raise IllegalAction("It's forbidden to do a storage detachment with this action type") - for el_nm in ["load", "gen", "storage"]: + for el_nm in cls.OBJ_SUPPORT_DETACH: _modif_detach_xxx = getattr(self, f"_modif_detach_{el_nm}") xxx_pos_topo_vect = getattr(cls, f"{el_nm}_pos_topo_vect") _detach_xxx = getattr(self, f"_detach_{el_nm}") @@ -5854,9 +5862,7 @@ def detach_load(self, values): self._modif_detach_load = True except Exception as exc_: self._detach_load[:] = orig_ - raise IllegalAction( - f"Impossible to detach a load with your input." - ) from exc_ + raise IllegalAction("Impossible to detach a load with your input.") from exc_ @property def detach_gen(self) -> np.ndarray: @@ -5911,9 +5917,7 @@ def detach_gen(self, values): self._modif_detach_gen = True except Exception as exc_: self._detach_gen[:] = orig_ - raise IllegalAction( - f"Impossible to detach a generator with your input." - ) from exc_ + raise IllegalAction("Impossible to detach a generator with your input.") from exc_ @property def detach_storage(self) -> np.ndarray: @@ -5968,9 +5972,7 @@ def detach_storage(self, values): self._modif_detach_storage = True except Exception as exc_: self._detach_storage[:] = orig_ - raise IllegalAction( - f"Impossible to detach a storage unit with your input." - ) from exc_ + raise IllegalAction("Impossible to detach a storage unit with your input.") from exc_ def _aux_affect_object_float( self, diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index b0466d1a..e3c32323 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -3169,7 +3169,7 @@ def _update_alert_properties(self, action, lines_attacked, subs_attacked): # TODO after alert budget will be implemented ! # self._is_alert_illegal - def _aux_register_env_converged(self, disc_lines, action, init_line_status, new_p) -> bool: + def _aux_register_env_converged(self, disc_lines, action, init_line_status, new_p) -> Optional[Grid2OpException]: beg_res = time.perf_counter() # update the thermal limit, for DLR for example self.backend.update_thermal_limit(self) @@ -3238,7 +3238,7 @@ def _aux_register_env_converged(self, disc_lines, action, init_line_status, new_ if diff_[cls.gen_redispatchable] > cls.gen_max_ramp_up[cls.gen_redispatchable] + self._tol_poly: gen_ko = (diff_[cls.gen_redispatchable] > cls.gen_max_ramp_up[cls.gen_redispatchable]).nonzero()[0] gen_ko_nms = cls.name_gen[cls.gen_redispatchable][gen_ko] - return SomeGeneratorAboveRampmax(f"Especially generators {gen_ko}") + return SomeGeneratorAboveRampmax(f"Especially generators {gen_ko_nms}") if diff_[cls.gen_redispatchable] < -cls.gen_max_ramp_down[cls.gen_redispatchable] - self._tol_poly: gen_ko = (diff_[cls.gen_redispatchable] < -cls.gen_max_ramp_down[cls.gen_redispatchable]).nonzero()[0] gen_ko_nms = cls.name_gen[cls.gen_redispatchable][gen_ko] @@ -3293,7 +3293,8 @@ def _aux_run_pf_after_state_properly_set( self._gen_p_detached[~self._gens_detached] = 0. self._storage_p_detached[:] = 0. - self._storage_p_detached[:] = self._backend_action.storage_power.values + mask_chgt = self._backend_action.storage_power.changed + self._storage_p_detached[mask_chgt] = self._backend_action.storage_power.values[mask_chgt] self._storage_p_detached[~self._storages_detached] = 0. try: diff --git a/grid2op/Opponent/opponentSpace.py b/grid2op/Opponent/opponentSpace.py index 042e32f5..9a1583be 100644 --- a/grid2op/Opponent/opponentSpace.py +++ b/grid2op/Opponent/opponentSpace.py @@ -242,10 +242,10 @@ def attack(self, observation, agent_action, env_action): if attack is None: attack_duration = 0 else: - if observation is not None: - # cache the get_topological_impact to avoid useless computation later - # this is a speed optimization - _ = attack.get_topological_impact(observation.line_status, _store_in_cache=True, _read_from_cache=False) + # NB : here observation is not None (check done first line of this function) + # cache the get_topological_impact to avoid useless computation later + # this is a speed optimization + _ = attack.get_topological_impact(observation.line_status, _store_in_cache=True, _read_from_cache=False) return attack, attack_duration def close(self): diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index a5e7fc82..2c40db3f 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -4463,6 +4463,9 @@ def process_shunt_static_data(cls): @classmethod def process_detachment(cls): + """process the status of detachment, that can be turned on or off, is overloaded for :class:`grid2op.Action.BaseAction` + or :class:`grid2op.Observation.BaseObservation` + """ pass @classmethod From 55f08cf272317cfb105de948ea860a7d4a47127a Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Mon, 16 Dec 2024 10:18:08 +0100 Subject: [PATCH 59/60] two minor fixes Signed-off-by: DONNOT Benjamin --- CHANGELOG.rst | 3 +++ grid2op/Environment/environment.py | 6 +++++- grid2op/tests/aaa_test_backend_interface.py | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f4130a5a..5483d8c2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -130,6 +130,9 @@ Native multi agents support: if the last simulated action lead to a game over - [FIXED] some bugs in `act.from_json(...)` due to the handling of the injection modifications. - [FIXED] logos now have the correct URL +- [FIXED] deprecated call to `tostring_rgb` (replaced `tostring_argb`) in the env.render function. +- [FIXED] warnings not properly issued in the AAA test when backend failed to call + `can_support_XXX` functions (*eg* `can_handle_more_than_2_busbar()` or `can_handle_detachment()`) - [ADDED] possibility to set the "thermal limits" when calling `env.reset(..., options={"thermal limit": xxx})` - [ADDED] possibility to retrieve some structural information about elements with with `gridobj.get_line_info(...)`, `gridobj.get_load_info(...)`, `gridobj.get_gen_info(...)` diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 63fe71cb..cabee0d2 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -1469,7 +1469,11 @@ def render(self, mode="rgb_array"): self.viewer_fig = fig # Return the rgb array - rgb_array = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8).reshape(self._viewer.height, self._viewer.width, 3) + try: + tmp = fig.canvas.tostring_argb() + except AttributeError: + tmp = fig.canvas.tostring_rgb() + rgb_array = np.frombuffer(tmp, dtype=np.uint8).reshape(self._viewer.height, self._viewer.width, 3) return rgb_array def _custom_deepcopy_for_copy(self, new_obj): diff --git a/grid2op/tests/aaa_test_backend_interface.py b/grid2op/tests/aaa_test_backend_interface.py index 29ccd59a..dc9fff03 100644 --- a/grid2op/tests/aaa_test_backend_interface.py +++ b/grid2op/tests/aaa_test_backend_interface.py @@ -61,12 +61,12 @@ def test_00create_backend(self): """Tests the backend can be created (not integrated in a grid2op environment yet)""" self.skip_if_needed() backend = self.make_backend_with_glue_code() - if not backend._missing_two_busbars_support_info: + if backend._missing_two_busbars_support_info: warnings.warn("You should call either `self.can_handle_more_than_2_busbar()` " "or `self.cannot_handle_more_than_2_busbar()` in the `load_grid` " "method of your backend. Please refer to documentation for more information.") - if not backend._missing_detachment_support_info: + if backend._missing_detachment_support_info: warnings.warn("You should call either `self.can_handle_detachment()` " "or `self.cannot_handle_detachment()` in the `load_grid` " "method of your backend. Please refer to documentation for more information.") From c6299c9190b88d1032e6946c5bc44f5ea2f3eead Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Mon, 16 Dec 2024 11:08:44 +0100 Subject: [PATCH 60/60] some fixes due to matplotlib deprecation function Signed-off-by: DONNOT Benjamin --- CHANGELOG.rst | 2 +- grid2op/Environment/environment.py | 5 ++++- grid2op/tests/aaa_test_backend_interface.py | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5483d8c2..e7a489d9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -132,7 +132,7 @@ Native multi agents support: - [FIXED] logos now have the correct URL - [FIXED] deprecated call to `tostring_rgb` (replaced `tostring_argb`) in the env.render function. - [FIXED] warnings not properly issued in the AAA test when backend failed to call - `can_support_XXX` functions (*eg* `can_handle_more_than_2_busbar()` or `can_handle_detachment()`) + `can_handle_XXX` functions (*eg* `can_handle_more_than_2_busbar()` or `can_handle_detachment()`) - [ADDED] possibility to set the "thermal limits" when calling `env.reset(..., options={"thermal limit": xxx})` - [ADDED] possibility to retrieve some structural information about elements with with `gridobj.get_line_info(...)`, `gridobj.get_load_info(...)`, `gridobj.get_gen_info(...)` diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index cabee0d2..6f13d926 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -1470,10 +1470,13 @@ def render(self, mode="rgb_array"): # Return the rgb array try: + import matplotlib.colors tmp = fig.canvas.tostring_argb() + argb_array = np.frombuffer(tmp, dtype=np.uint8).reshape(self._viewer.height, self._viewer.width, 4) + rgb_array = argb_array[:,:,1:] except AttributeError: tmp = fig.canvas.tostring_rgb() - rgb_array = np.frombuffer(tmp, dtype=np.uint8).reshape(self._viewer.height, self._viewer.width, 3) + rgb_array = np.frombuffer(tmp, dtype=np.uint8).reshape(self._viewer.height, self._viewer.width, 3) return rgb_array def _custom_deepcopy_for_copy(self, new_obj): diff --git a/grid2op/tests/aaa_test_backend_interface.py b/grid2op/tests/aaa_test_backend_interface.py index dc9fff03..0d0d8ccf 100644 --- a/grid2op/tests/aaa_test_backend_interface.py +++ b/grid2op/tests/aaa_test_backend_interface.py @@ -818,13 +818,14 @@ def _aux_test_detachment(self, backend : Backend, is_dc=True, detachment_should_ # legacy behaviour, should behave as if it diverges # for new (>= 1.11.0) behaviour, it is catched in the method `_runpf_with_diverging_exception` res = backend.runpf(is_dc=is_dc) - assert not res[0], f"It is expected (at time of writing) that your backend returns `False` in case of isolated loads in {str_}." + assert not res[0], f"It is expected (at time of writing) that your backend returns `False` in case of isolated elements (eg load, gen or storage unit) in {str_}." maybe_exc = res[1] detachment_allowed = False else: # new (1.11.0) test here maybe_exc = backend._runpf_with_diverging_exception(is_dc=is_dc) detachment_allowed = type(backend).detachment_is_allowed + if not detachment_allowed: # should raise in all cases as the backend prevent detachment self._aux_aux_test_detachment_should_fail(maybe_exc)