From 42b3c8e2102b0254974df6d3d87b2ffb439d58ff Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 26 Jan 2024 11:28:52 +0100 Subject: [PATCH 01/24] some little upgrade, especially for wcci_2020 action --- .github/workflows/main.yml | 2 +- .gitignore | 4 ++ CHANGELOG.rst | 5 +++ docs/conf.py | 2 +- grid2op/__init__.py | 2 +- grid2op/l2rpn_utils/wcci_2020.py | 74 ++++++++++++++++---------------- 6 files changed, 48 insertions(+), 41 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 310f61316..1e311d426 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -156,7 +156,7 @@ jobs: - name: Upload source archive uses: actions/upload-artifact@v2 - if: matrix.config.name == 'darwin' && matrix.python.name == 'cp39' + if: matrix.config.name == 'darwin' && matrix.python.name == 'cp310' with: name: grid2op-sources path: dist/*.tar.gz diff --git a/.gitignore b/.gitignore index 84e7e7bd5..e950fdba4 100644 --- a/.gitignore +++ b/.gitignore @@ -399,6 +399,10 @@ pp_bug_gen_alone.py test_dunder.py grid2op/tests/test_fail_ci.txt saved_multiepisode_agent_36bus_DN_4/ +grid2op/tests/requirements.txt +grid2op/tests/venv_test_311/ +issue_577/ +junk.py # profiling files **.prof diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 821c3365b..d3a9bec78 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -31,6 +31,11 @@ Change Log - [???] "asynch" multienv - [???] properly model interconnecting powerlines +[1.9.9] - 2024-xx-yy +---------------------- +- [FIXED] github CI did not upload the source files +- [FIXED] l2rpn_utils module did not stored correctly the order + of actions and observation for wcci_2020 [1.9.8] - 2024-01-26 ---------------------- diff --git a/docs/conf.py b/docs/conf.py index 133a9a84b..e7b495411 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.9.8' +release = '1.9.9.dev0' version = '1.9' diff --git a/grid2op/__init__.py b/grid2op/__init__.py index 2979858ec..c3c4bb23a 100644 --- a/grid2op/__init__.py +++ b/grid2op/__init__.py @@ -11,7 +11,7 @@ Grid2Op """ -__version__ = '1.9.8' +__version__ = '1.9.9.dev0' __all__ = [ "Action", diff --git a/grid2op/l2rpn_utils/wcci_2020.py b/grid2op/l2rpn_utils/wcci_2020.py index 9293326f6..3636fbfa0 100644 --- a/grid2op/l2rpn_utils/wcci_2020.py +++ b/grid2op/l2rpn_utils/wcci_2020.py @@ -18,50 +18,48 @@ class ActionWCCI2020(PlayableAction): "redispatch", } - attr_list_vect = [ - "_set_line_status", - "_switch_line_status", - "_set_topo_vect", - "_change_bus_vect", - '_redispatch' - ] + attr_list_vect = ['_set_line_status', + '_set_topo_vect', + '_change_bus_vect', + '_switch_line_status', + '_redispatch'] attr_list_set = set(attr_list_vect) pass class ObservationWCCI2020(CompleteObservation): attr_list_vect = [ - 'year', - 'month', - 'day', - 'hour_of_day', - 'minute_of_hour', - 'day_of_week', - "gen_p", - "gen_q", - "gen_v", - 'load_p', - 'load_q', - 'load_v', - 'p_or', - 'q_or', - 'v_or', - 'a_or', - 'p_ex', - 'q_ex', - 'v_ex', - 'a_ex', - 'rho', - 'line_status', - 'timestep_overflow', - 'topo_vect', - 'time_before_cooldown_line', - 'time_before_cooldown_sub', - 'time_next_maintenance', - 'duration_next_maintenance', - 'target_dispatch', - 'actual_dispatch' - ] + "year", + "month", + "day", + "hour_of_day", + "minute_of_hour", + "day_of_week", + "gen_p", + "gen_q", + "gen_v", + "load_p", + "load_q", + "load_v", + "p_or", + "q_or", + "v_or", + "a_or", + "p_ex", + "q_ex", + "v_ex", + "a_ex", + "rho", + "line_status", + "timestep_overflow", + "topo_vect", + "time_before_cooldown_line", + "time_before_cooldown_sub", + "time_next_maintenance", + "duration_next_maintenance", + "target_dispatch", + "actual_dispatch" + ] attr_list_json = [ "storage_charge", "storage_power_target", From 73236969ce976b424b43578db79a467a66833fba Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 26 Jan 2024 14:46:09 +0100 Subject: [PATCH 02/24] start of implementation [skip ci] --- CHANGELOG.rst | 4 + grid2op/Action/baseAction.py | 8 +- grid2op/Backend/backend.py | 68 +++++++++- grid2op/Backend/pandaPowerBackend.py | 4 +- grid2op/Environment/baseEnv.py | 5 +- grid2op/Environment/environment.py | 80 ++++++------ grid2op/Environment/maskedEnvironment.py | 7 +- grid2op/Environment/multiMixEnv.py | 5 +- grid2op/Environment/timedOutEnv.py | 7 +- grid2op/MakeEnv/Make.py | 10 ++ grid2op/MakeEnv/MakeFromPath.py | 5 + grid2op/Observation/baseObservation.py | 14 +- grid2op/Runner/runner.py | 3 + grid2op/Space/GridObjects.py | 43 +++++- grid2op/Space/__init__.py | 4 +- grid2op/tests/test_n_busbar_per_sub.py | 158 +++++++++++++++++++++++ 16 files changed, 365 insertions(+), 60 deletions(-) create mode 100644 grid2op/tests/test_n_busbar_per_sub.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d3a9bec78..b74c6b1f5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -36,6 +36,10 @@ Change Log - [FIXED] github CI did not upload the source files - [FIXED] l2rpn_utils module did not stored correctly the order of actions and observation for wcci_2020 +- [IMPROVED] handling of "compatibility" grid2op version + (by calling the relevant things done in the base class + in `BaseAction` and `BaseObservation`) and by using the `from packaging import version` + to check version (instead of comparing strings) [1.9.8] - 2024-01-26 ---------------------- diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 6f92ca139..10678eba5 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -10,6 +10,7 @@ import numpy as np import warnings from typing import Tuple +from packaging import version from grid2op.dtypes import dt_int, dt_bool, dt_float from grid2op.Exceptions import * @@ -774,6 +775,9 @@ def alert_raised(self) -> np.ndarray: @classmethod def process_grid2op_compat(cls): + GridObjects.process_grid2op_compat(cls) + glop_ver = cls._get_grid2op_version_as_version_obj() + if cls.glop_version == cls.BEFORE_COMPAT_VERSION: # oldest version: no storage and no curtailment available @@ -797,11 +801,11 @@ def process_grid2op_compat(cls): cls.attr_list_vect.remove("_curtail") cls.attr_list_set = set(cls.attr_list_vect) - if cls.glop_version < "1.6.0": + if glop_ver < version.parse("1.6.0"): # this feature did not exist before. cls.dim_alarms = 0 - if cls.glop_version < "1.9.1": + if glop_ver < version.parse("1.9.1"): # this feature did not exist before. cls.dim_alerts = 0 diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index a06fc00b0..a55006797 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -33,7 +33,7 @@ DivergingPowerflow, Grid2OpException, ) -from grid2op.Space import GridObjects +from grid2op.Space import GridObjects, DEFAULT_N_BUSBAR_PER_SUB # TODO method to get V and theta at each bus, could be in the same shape as check_kirchoff @@ -171,6 +171,56 @@ def __init__(self, for k, v in kwargs.items(): self._my_kwargs[k] = v + #: .. versionadded:: 1.9.9 + #: A flag to indicate whether the :func:`Backend.cannot_handle_more_than_2_busbar` + #: or the :func:`Backend.cannot_handle_more_than_2_busbar` + #: has been called when :func:`Backend.load_grid` was called. + #: Starting from grid2op 1.9.9 this is a requirement (to + #: ensure backward compatibility) + self._missing_two_busbars_support_info = True + + def can_handle_more_than_2_busbar(self): + """ + .. versionadded:: 1.9.9 + + This function should be called once in `load_grid` if your backend is able + to handle more than 2 busbars per substation. + + If not called, then the `environment` will not be able to use more than 2 busbars per substations. + + .. seealso:: + :func:`Backend.cannot_handle_more_than_2_busbar` + + .. danger:: + We highly recommend you do not try to override this function. + + At time of writing I can't find any good reason to do so. + """ + self._missing_two_busbars_support_info = False + self.n_busbar_per_sub = type(self).n_busbar_per_sub + + def cannot_handle_more_than_2_busbar(self): + """ + .. versionadded:: 1.9.9 + + This function should be called once in `load_grid` if your backend is able + to handle more than 2 busbars per substation. + + If not called, then the `environment` will not be able to use more than 2 busbars per substations. + + .. seealso:: + :func:`Backend.cnot_handle_more_than_2_busbar` + + .. danger:: + We highly recommend you do not try to override this function. + + At time of writing I can't find any good reason to do so. + """ + self._missing_two_busbars_support_info = False + if type(self).n_busbar_per_sub != DEFAULT_N_BUSBAR_PER_SUB: + warnings.warn("You asked in `make` function to pass ") + self.n_busbar_per_sub = DEFAULT_N_BUSBAR_PER_SUB + def make_complete_path(self, path : Union[os.PathLike, str], filename : Optional[Union[os.PathLike, str]]=None) -> str: @@ -1859,6 +1909,22 @@ def assert_grid_correct(self) -> None: from grid2op.Action import CompleteAction from grid2op.Action._backendAction import _BackendAction + if self._missing_two_busbars_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.9.9: the possibility " + "to have more than 2 busbars per substations (or not). " + "To silence this warning, you can modify the `load_grid` implementation " + "of your backend and either call:\n" + "- self.can_handle_more_than_2_busbar if the current implementation " + " can handle more than 2 busbsars OR\n" + "- self.cannot_handle_more_than_2_busbar if not." + "\nAnd of course, ideally, if the current implementation " + "of your backend cannot " + "handle more than 2 busbars per substation, then change it :-)\n" + "Your backend will behave as if it did not support it.") + self._missing_two_busbars_support_info = False + self.n_busbar_per_sub = DEFAULT_N_BUSBAR_PER_SUB + orig_type = type(self) if orig_type.my_bk_act_class is None: # class is already initialized diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index e4b9c0ccf..824bee029 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -337,6 +337,7 @@ def load_grid(self, are set as "out of service" unless a topological action acts on these specific substations. """ + self.cannot_handle_more_than_2_busbar() full_path = self.make_complete_path(path, filename) with warnings.catch_warnings(): @@ -1306,7 +1307,8 @@ def copy(self) -> "PandaPowerBackend": res._in_service_line_col_id = self._in_service_line_col_id res._in_service_trafo_col_id = self._in_service_trafo_col_id - + + res._missing_two_busbars_support_info = self._missing_two_busbars_support_info return res def close(self) -> None: diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index 613e3e409..10f2d24e2 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -335,11 +335,13 @@ 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, _is_test: bool = False, # TODO not implemented !! _init_obs: Optional[BaseObservation] =None ): GridObjects.__init__(self) RandomObject.__init__(self) + self._n_busbar = n_busbar # env attribute not class attribute ! if other_rewards is None: other_rewards = {} if kwargs_attention_budget is None: @@ -630,7 +632,8 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None): RandomObject._custom_deepcopy_for_copy(self, new_obj) if dict_ is None: dict_ = {} - + new_obj._n_busbar = self._n_busbar + 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 c88b4f32b..5213c695f 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -32,6 +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 class Environment(BaseEnv): @@ -82,6 +83,7 @@ def __init__( backend, parameters, name="unknown", + n_busbar=DEFAULT_N_BUSBAR_PER_SUB, names_chronics_to_backend=None, actionClass=TopologyAction, observationClass=CompleteObservation, @@ -148,6 +150,7 @@ def __init__( observation_bk_kwargs=observation_bk_kwargs, highres_sim_counter=highres_sim_counter, update_obs_after_reward=_update_obs_after_reward, + n_busbar=n_busbar, _init_obs=_init_obs, _is_test=_is_test, # is this created with "test=True" # TODO not implemented !! ) @@ -244,7 +247,8 @@ def _init_backend( self.backend._PATH_ENV = self.get_path_env() # all the above should be done in this exact order, otherwise some weird behaviour might occur # this is due to the class attribute - self.backend.set_env_name(self.name) + type(self.backend).set_env_name(self.name) + type(self.backend).set_n_busbar_per_sub(self._n_busbar) self.backend.load_grid( self._init_grid_path ) # the real powergrid of the environment @@ -1136,6 +1140,7 @@ def get_kwargs(self, with_backend=True, with_chronics_handler=True): """ res = {} + res["n_busbar"] = self._n_busbar res["init_env_path"] = self._init_env_path res["init_grid_path"] = self._init_grid_path if with_chronics_handler: @@ -1774,6 +1779,7 @@ def get_params_for_runner(self): res["other_rewards"] = {k: v.rewardClass for k, v in self.other_rewards.items()} res["grid_layout"] = self.grid_layout res["name_env"] = self.name + res["n_busbar"] = self._n_busbar res["opponent_space_type"] = self._opponent_space_type res["opponent_action_class"] = self._opponent_action_class @@ -1798,6 +1804,7 @@ def get_params_for_runner(self): @classmethod def init_obj_from_kwargs(cls, + *, other_env_kwargs, init_env_path, init_grid_path, @@ -1830,39 +1837,41 @@ def init_obj_from_kwargs(cls, observation_bk_class, observation_bk_kwargs, _raw_backend_class, - _read_from_local_dir): - res = Environment(init_env_path=init_env_path, - init_grid_path=init_grid_path, - chronics_handler=chronics_handler, - backend=backend, - parameters=parameters, - name=name, - names_chronics_to_backend=names_chronics_to_backend, - actionClass=actionClass, - observationClass=observationClass, - rewardClass=rewardClass, - legalActClass=legalActClass, - voltagecontrolerClass=voltagecontrolerClass, - 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_budget_per_ts=opponent_budget_per_ts, - opponent_budget_class=opponent_budget_class, - opponent_attack_duration=opponent_attack_duration, - opponent_attack_cooldown=opponent_attack_cooldown, - kwargs_opponent=kwargs_opponent, - with_forecast=with_forecast, - attention_budget_cls=attention_budget_cls, - kwargs_attention_budget=kwargs_attention_budget, - has_attention_budget=has_attention_budget, - logger=logger, - kwargs_observation=kwargs_observation, - observation_bk_class=observation_bk_class, - observation_bk_kwargs=observation_bk_kwargs, - _raw_backend_class=_raw_backend_class, - _read_from_local_dir=_read_from_local_dir) + _read_from_local_dir, + n_busbar=DEFAULT_N_BUSBAR_PER_SUB): + res = cls(init_env_path=init_env_path, + init_grid_path=init_grid_path, + chronics_handler=chronics_handler, + backend=backend, + parameters=parameters, + name=name, + names_chronics_to_backend=names_chronics_to_backend, + actionClass=actionClass, + observationClass=observationClass, + rewardClass=rewardClass, + legalActClass=legalActClass, + voltagecontrolerClass=voltagecontrolerClass, + 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_budget_per_ts=opponent_budget_per_ts, + opponent_budget_class=opponent_budget_class, + opponent_attack_duration=opponent_attack_duration, + opponent_attack_cooldown=opponent_attack_cooldown, + kwargs_opponent=kwargs_opponent, + with_forecast=with_forecast, + attention_budget_cls=attention_budget_cls, + kwargs_attention_budget=kwargs_attention_budget, + has_attention_budget=has_attention_budget, + logger=logger, + kwargs_observation=kwargs_observation, + observation_bk_class=observation_bk_class, + observation_bk_kwargs=observation_bk_kwargs, + n_busbar=int(n_busbar), + _raw_backend_class=_raw_backend_class, + _read_from_local_dir=_read_from_local_dir) return res def generate_data(self, nb_year=1, nb_core=1, seed=None, **kwargs): @@ -1872,8 +1881,7 @@ def generate_data(self, nb_year=1, nb_core=1, seed=None, **kwargs): I also requires the lightsim2grid simulator. - This is only available for some environment (only the environment used for wcci 2022 competition at - time of writing). + This is only available for some environment (only the environment after 2022). Generating data takes some time (around 1 - 2 minutes to generate a weekly scenario) and this why we recommend to do it "offline" and then use the generated data for training or evaluation. diff --git a/grid2op/Environment/maskedEnvironment.py b/grid2op/Environment/maskedEnvironment.py index b97bf986c..bd7caaffa 100644 --- a/grid2op/Environment/maskedEnvironment.py +++ b/grid2op/Environment/maskedEnvironment.py @@ -10,10 +10,9 @@ import numpy as np from typing import Tuple, Union, List from grid2op.Environment.environment import Environment -from grid2op.Action import BaseAction -from grid2op.Observation import BaseObservation from grid2op.Exceptions import EnvError from grid2op.dtypes import dt_bool, dt_float, dt_int +from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB class MaskedEnvironment(Environment): # TODO heritage ou alors on met un truc de base @@ -122,7 +121,8 @@ def init_obj_from_kwargs(cls, observation_bk_class, observation_bk_kwargs, _raw_backend_class, - _read_from_local_dir): + _read_from_local_dir, + n_busbar=DEFAULT_N_BUSBAR_PER_SUB): res = MaskedEnvironment(grid2op_env={"init_env_path": init_env_path, "init_grid_path": init_grid_path, "chronics_handler": chronics_handler, @@ -153,6 +153,7 @@ def init_obj_from_kwargs(cls, "kwargs_observation": kwargs_observation, "observation_bk_class": observation_bk_class, "observation_bk_kwargs": observation_bk_kwargs, + "n_busbar": int(n_busbar), "_raw_backend_class": _raw_backend_class, "_read_from_local_dir": _read_from_local_dir}, **other_env_kwargs) diff --git a/grid2op/Environment/multiMixEnv.py b/grid2op/Environment/multiMixEnv.py index d20e73b75..5e86de132 100644 --- a/grid2op/Environment/multiMixEnv.py +++ b/grid2op/Environment/multiMixEnv.py @@ -13,7 +13,7 @@ from typing import Any, Dict, Tuple, Union, List from grid2op.dtypes import dt_int, dt_float -from grid2op.Space import GridObjects, RandomObject +from grid2op.Space import GridObjects, RandomObject, DEFAULT_N_BUSBAR_PER_SUB from grid2op.Exceptions import EnvError, Grid2OpException from grid2op.Observation import BaseObservation @@ -161,6 +161,7 @@ def __init__( envs_dir, logger=None, experimental_read_from_local_dir=False, + n_busbar=DEFAULT_N_BUSBAR_PER_SUB, _add_to_name="", # internal, for test only, do not use ! _compat_glop_version=None, # internal, for test only, do not use ! _test=False, @@ -217,6 +218,7 @@ def __init__( backend=bk, _add_to_name=_add_to_name, _compat_glop_version=_compat_glop_version, + n_busbar=n_busbar, test=_test, logger=this_logger, experimental_read_from_local_dir=experimental_read_from_local_dir, @@ -225,6 +227,7 @@ def __init__( else: env = make( env_path, + n_busbar=n_busbar, _add_to_name=_add_to_name, _compat_glop_version=_compat_glop_version, test=_test, diff --git a/grid2op/Environment/timedOutEnv.py b/grid2op/Environment/timedOutEnv.py index 84fafef58..bbf3593f3 100644 --- a/grid2op/Environment/timedOutEnv.py +++ b/grid2op/Environment/timedOutEnv.py @@ -13,6 +13,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 class TimedOutEnvironment(Environment): # TODO heritage ou alors on met un truc de base @@ -212,7 +213,8 @@ def init_obj_from_kwargs(cls, observation_bk_class, observation_bk_kwargs, _raw_backend_class, - _read_from_local_dir): + _read_from_local_dir, + n_busbar=DEFAULT_N_BUSBAR_PER_SUB): res = TimedOutEnvironment(grid2op_env={"init_env_path": init_env_path, "init_grid_path": init_grid_path, "chronics_handler": chronics_handler, @@ -244,7 +246,8 @@ def init_obj_from_kwargs(cls, "observation_bk_class": observation_bk_class, "observation_bk_kwargs": observation_bk_kwargs, "_raw_backend_class": _raw_backend_class, - "_read_from_local_dir": _read_from_local_dir}, + "_read_from_local_dir": _read_from_local_dir, + "n_busbar": int(n_busbar)}, **other_env_kwargs) return res diff --git a/grid2op/MakeEnv/Make.py b/grid2op/MakeEnv/Make.py index 8dbb24104..b4caf28b3 100644 --- a/grid2op/MakeEnv/Make.py +++ b/grid2op/MakeEnv/Make.py @@ -247,6 +247,7 @@ def _aux_make_multimix( dataset_path, test=False, experimental_read_from_local_dir=False, + n_busbar=2, _add_to_name="", _compat_glop_version=None, logger=None, @@ -258,6 +259,7 @@ def _aux_make_multimix( return MultiMixEnvironment( dataset_path, experimental_read_from_local_dir=experimental_read_from_local_dir, + n_busbar=n_busbar, _test=test, _add_to_name=_add_to_name, _compat_glop_version=_compat_glop_version, @@ -272,6 +274,7 @@ def make( test : bool=False, logger: Optional[logging.Logger]=None, experimental_read_from_local_dir : bool=False, + n_busbar=2, _add_to_name : str="", _compat_glop_version : Optional[str]=None, **kwargs @@ -308,6 +311,9 @@ def make( processing, you can set this flag to ``True``. See the doc of :func:`grid2op.Environment.BaseEnv.generate_classes` for more information. + n_busbar: ``int`` + Number of independant busbars allowed per substations. By default it's 2. + kwargs: Other keyword argument to give more control on the environment you are creating. See the Parameters information of the :func:`make_from_dataset_path`. @@ -402,6 +408,7 @@ def make_from_path_fn_(*args, **kwargs): dataset_path=dataset, _add_to_name=_add_to_name_tmp, _compat_glop_version=_compat_glop_version_tmp, + n_busbar=n_busbar, **kwargs ) @@ -441,6 +448,7 @@ def make_from_path_fn_(*args, **kwargs): return make_from_path_fn( dataset_path=ds_path, logger=logger, + n_busbar=n_busbar, _add_to_name=_add_to_name, _compat_glop_version=_compat_glop_version, experimental_read_from_local_dir=experimental_read_from_local_dir, @@ -454,6 +462,7 @@ def make_from_path_fn_(*args, **kwargs): return make_from_path_fn( real_ds_path, logger=logger, + n_busbar=n_busbar, experimental_read_from_local_dir=experimental_read_from_local_dir, **kwargs ) @@ -472,6 +481,7 @@ def make_from_path_fn_(*args, **kwargs): return make_from_path_fn( dataset_path=real_ds_path, logger=logger, + n_busbar=n_busbar, experimental_read_from_local_dir=experimental_read_from_local_dir, **kwargs ) diff --git a/grid2op/MakeEnv/MakeFromPath.py b/grid2op/MakeEnv/MakeFromPath.py index 98054513f..708da74ba 100644 --- a/grid2op/MakeEnv/MakeFromPath.py +++ b/grid2op/MakeEnv/MakeFromPath.py @@ -115,6 +115,7 @@ def make_from_dataset_path( dataset_path="/", logger=None, experimental_read_from_local_dir=False, + n_busbar=2, _add_to_name="", _compat_glop_version=None, **kwargs, @@ -150,6 +151,9 @@ def make_from_dataset_path( backend: ``grid2op.Backend.Backend``, optional The backend to use for the computation. If provided, it must be an instance of :class:`grid2op.Backend.Backend`. + n_busbar: ``int`` + Number of independant busbars allowed per substations. By default it's 2. + action_class: ``type``, optional Type of BaseAction the BaseAgent will be able to perform. If provided, it must be a subclass of :class:`grid2op.BaseAction.BaseAction` @@ -885,6 +889,7 @@ def make_from_dataset_path( attention_budget_cls=attention_budget_class, kwargs_attention_budget=kwargs_attention_budget, logger=logger, + n_busbar=n_busbar, _compat_glop_version=_compat_glop_version, _read_from_local_dir=experimental_read_from_local_dir, kwargs_observation=kwargs_observation, diff --git a/grid2op/Observation/baseObservation.py b/grid2op/Observation/baseObservation.py index 6b401502b..c7c6484a5 100644 --- a/grid2op/Observation/baseObservation.py +++ b/grid2op/Observation/baseObservation.py @@ -14,6 +14,7 @@ import numpy as np from scipy.sparse import csr_matrix from typing import Optional +from packaging import version from grid2op.dtypes import dt_int, dt_float, dt_bool from grid2op.Exceptions import ( @@ -1027,6 +1028,9 @@ def process_shunt_satic_data(cls): @classmethod def process_grid2op_compat(cls): + GridObjects.process_grid2op_compat(cls) + glop_ver = cls._get_grid2op_version_as_version_obj() + if cls.glop_version == cls.BEFORE_COMPAT_VERSION: # oldest version: no storage and no curtailment available @@ -1053,7 +1057,7 @@ def process_grid2op_compat(cls): cls.attr_list_set = set(cls.attr_list_vect) - if cls.glop_version < "1.6.0" or cls.glop_version == cls.BEFORE_COMPAT_VERSION: + if glop_ver < version.parse("1.6.0"): # this feature did not exist before and was introduced in grid2op 1.6.0 cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) cls.attr_list_set = copy.deepcopy(cls.attr_list_set) @@ -1080,7 +1084,7 @@ def process_grid2op_compat(cls): pass cls.attr_list_set = set(cls.attr_list_vect) - if cls.glop_version < "1.6.4" or cls.glop_version == cls.BEFORE_COMPAT_VERSION: + if glop_ver < version.parse("1.6.4"): # "current_step", "max_step" were added in grid2Op 1.6.4 cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) cls.attr_list_set = copy.deepcopy(cls.attr_list_set) @@ -1093,7 +1097,7 @@ def process_grid2op_compat(cls): pass cls.attr_list_set = set(cls.attr_list_vect) - if cls.glop_version < "1.6.5" or cls.glop_version == cls.BEFORE_COMPAT_VERSION: + if glop_ver < version.parse("1.6.5"): # "current_step", "max_step" were added in grid2Op 1.6.5 cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) cls.attr_list_set = copy.deepcopy(cls.attr_list_set) @@ -1106,7 +1110,7 @@ def process_grid2op_compat(cls): pass cls.attr_list_set = set(cls.attr_list_vect) - if cls.glop_version < "1.6.6" or cls.glop_version == cls.BEFORE_COMPAT_VERSION: + if glop_ver < version.parse("1.6.6"): # "gen_margin_up", "gen_margin_down" were added in grid2Op 1.6.6 cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) cls.attr_list_set = copy.deepcopy(cls.attr_list_set) @@ -1123,7 +1127,7 @@ def process_grid2op_compat(cls): pass cls.attr_list_set = set(cls.attr_list_vect) - if cls.glop_version < "1.9.1" or cls.glop_version == cls.BEFORE_COMPAT_VERSION: + if glop_ver < version.parse("1.9.1"): # alert attributes have been added in 1.9.1 cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) cls.attr_list_set = copy.deepcopy(cls.attr_list_set) diff --git a/grid2op/Runner/runner.py b/grid2op/Runner/runner.py index 59747a116..89037f026 100644 --- a/grid2op/Runner/runner.py +++ b/grid2op/Runner/runner.py @@ -243,6 +243,7 @@ def __init__( init_env_path: str, init_grid_path: str, path_chron, # path where chronics of injections are stored + n_busbar=2, name_env="unknown", parameters_path=None, names_chronics_to_backend=None, @@ -346,6 +347,7 @@ def __init__( # TODO documentation on the opponent # TOOD doc for the attention budget """ + self._n_busbar = n_busbar self.with_forecast = with_forecast self.name_env = name_env if not isinstance(envClass, type): @@ -614,6 +616,7 @@ def _new_env(self, chronics_handler, parameters) -> Tuple[BaseEnv, BaseAgent]: with warnings.catch_warnings(): warnings.filterwarnings("ignore") res = self.envClass.init_obj_from_kwargs( + n_busbar=self._n_busbar, other_env_kwargs=self.other_env_kwargs, init_env_path=self.init_env_path, init_grid_path=self.init_grid_path, diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index f14eb3a46..96027d0cc 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -20,6 +20,7 @@ import warnings import copy import numpy as np +from packaging import version import grid2op from grid2op.dtypes import dt_int, dt_float, dt_bool @@ -27,6 +28,7 @@ from grid2op.Space.space_utils import extract_from_dict, save_to_dict # TODO tests of these methods and this class in general +DEFAULT_N_BUSBAR_PER_SUB = 2 class GridObjects: @@ -487,6 +489,7 @@ class GridObjects: name_sub = None name_storage = None + n_busbar_per_sub = DEFAULT_N_BUSBAR_PER_SUB n_gen = -1 n_load = -1 n_line = -1 @@ -618,6 +621,10 @@ def __init__(self): """nothing to do when an object of this class is created, the information is held by the class attributes""" pass + @classmethod + def set_n_busbar_per_sub(cls, n_busbar_per_sub): + cls.n_busbar_per_sub = n_busbar_per_sub + @classmethod def tell_dim_alarm(cls, dim_alarms): if cls.dim_alarms != 0: @@ -651,6 +658,7 @@ def tell_dim_alert(cls, dim_alerts): @classmethod def _clear_class_attribute(cls): cls.shunts_data_available = False + cls.n_busbar_per_sub = DEFAULT_N_BUSBAR_PER_SUB # for redispatching / unit commitment cls._li_attr_disp = [ @@ -2714,6 +2722,11 @@ def init_grid(cls, gridobj, force=False, extra_name=None, force_module=None): # with shunt and without shunt, then # there might be issues name_res += "_noshunt" + + if gridobj.n_busbar_per_sub != DEFAULT_N_BUSBAR_PER_SUB: + # to be able to load same environment with + # different `n_busbar_per_sub` + name_res += f"_{gridobj.n_busbar_per_sub}" if name_res in globals(): if not force: @@ -2749,23 +2762,38 @@ def init_grid(cls, gridobj, force=False, extra_name=None, force_module=None): del res_cls return globals()[name_res] + @classmethod + def _get_grid2op_version_as_version_obj(cls): + if cls.glop_version == cls.BEFORE_COMPAT_VERSION: + glop_ver = version.parse("0.0.0") + else: + glop_ver = version.parse(cls.glop_version) + return glop_ver + @classmethod def process_grid2op_compat(cls): - """ - This function can be overloaded. + """This is called when the class is initialized, with `init_grid` to broadcast grid2op compatibility feature. + + This function can be overloaded, but in this case it's best to call this original method too. - This is called when the class is initialized, with `init_grid` to broadcast grid2op compatibility feature. """ - if cls.glop_version < "1.6.0": + glop_ver = cls._get_grid2op_version_as_version_obj() + + if glop_ver < version.parse("1.6.0"): # this feature did not exist before. cls.dim_alarms = 0 cls.assistant_warning_type = None - if cls.glop_version < "1.9.1": + if glop_ver < version.parse("1.9.1"): # this feature did not exists before cls.dim_alerts = 0 cls.alertable_line_names = [] cls.alertable_line_ids = [] + + if glop_ver < version.parse("1.9.9.dev0"): + # this feature did not exists before + # I need to set it to the default if set elsewhere + cls.n_busbar_per_sub = DEFAULT_N_BUSBAR_PER_SUB @classmethod def get_obj_connect_to(cls, _sentinel=None, substation_id=None): @@ -3510,6 +3538,8 @@ def _make_cls_dict_extended(cls, res, as_list=True, copy_=True): res[ "redispatching_unit_commitment_availble" ] = cls.redispatching_unit_commitment_availble + # n_busbar_per_sub + res["n_busbar_per_sub"] = cls.n_busbar_per_sub @classmethod def cls_to_dict(cls): @@ -4250,7 +4280,7 @@ def format_el(values): tmp_tmp_ = ",".join([f"{el}" for el in cls.alertable_line_ids]) tmp_ = f"[{tmp_tmp_}]" alertable_line_ids_str = '[]' if cls.dim_alerts == 0 else tmp_ - res = f"""# Copyright (c) 2019-2023, RTE (https://www.rte-france.com) + res = f"""# Copyright (c) 2019-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, @@ -4293,6 +4323,7 @@ class {cls.__name__}({cls._INIT_GRID_CLS.__name__}): name_sub = np.array([{name_sub_str}]) name_storage = np.array([{name_storage_str}]) + n_busbar_per_sub = {cls.n_busbar_per_sub} n_gen = {cls.n_gen} n_load = {cls.n_load} n_line = {cls.n_line} diff --git a/grid2op/Space/__init__.py b/grid2op/Space/__init__.py index 635b30e44..69387627d 100644 --- a/grid2op/Space/__init__.py +++ b/grid2op/Space/__init__.py @@ -1,5 +1,5 @@ -__all__ = ["RandomObject", "SerializableSpace", "GridObjects"] +__all__ = ["RandomObject", "SerializableSpace", "GridObjects", "DEFAULT_N_BUSBAR_PER_SUB"] from grid2op.Space.RandomObject import RandomObject from grid2op.Space.SerializableSpace import SerializableSpace -from grid2op.Space.GridObjects import GridObjects +from grid2op.Space.GridObjects import GridObjects, DEFAULT_N_BUSBAR_PER_SUB diff --git a/grid2op/tests/test_n_busbar_per_sub.py b/grid2op/tests/test_n_busbar_per_sub.py new file mode 100644 index 000000000..fcd1eac39 --- /dev/null +++ b/grid2op/tests/test_n_busbar_per_sub.py @@ -0,0 +1,158 @@ +# 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. + +from os import PathLike +from typing import Optional, Union +import warnings +import unittest +from grid2op.tests.helper_path_test import * + +import grid2op +from grid2op.Environment import MaskedEnvironment, TimedOutEnvironment +from grid2op.Backend import PandaPowerBackend +import pdb + +class _AuxFakeBackendSupport(PandaPowerBackend): + def cannot_handle_more_than_2_busbar(self): + """dont do it at home !""" + return self.can_handle_more_than_2_busbar() + + +class _AuxFakeBackendNoSupport(PandaPowerBackend): + def can_handle_more_than_2_busbar(self): + """dont do it at home !""" + return self.cannot_handle_more_than_2_busbar() + + +class _AuxFakeBackendNoCalled(PandaPowerBackend): + def can_handle_more_than_2_busbar(self): + """dont do it at home !""" + pass + def cannot_handle_more_than_2_busbar(self): + """dont do it at home !""" + pass + + +class TestRightNumber(unittest.TestCase): + """This test that, when changing n_busbar in make it is + back propagated where it needs""" + def _aux_fun_test(self, env, n_busbar): + assert type(env).n_busbar_per_sub == n_busbar, f"type(env).n_busbar_per_sub = {type(env).n_busbar_per_sub} != {n_busbar}" + assert type(env.backend).n_busbar_per_sub == n_busbar, f"env.backend).n_busbar_per_sub = {type(env.backend).n_busbar_per_sub} != {n_busbar}" + assert type(env.action_space).n_busbar_per_sub == n_busbar, f"type(env.action_space).n_busbar_per_sub = {type(env.action_space).n_busbar_per_sub} != {n_busbar}" + assert type(env.observation_space).n_busbar_per_sub == n_busbar, f"type(env.observation_space).n_busbar_per_sub = {type(env.observation_space).n_busbar_per_sub} != {n_busbar}" + obs = env.reset(seed=0, options={"time serie id": 0}) + assert type(obs).n_busbar_per_sub == n_busbar, f"type(obs).n_busbar_per_sub = {type(obs).n_busbar_per_sub} != {n_busbar}" + act = env.action_space() + assert type(act).n_busbar_per_sub == n_busbar, f"type(act).n_busbar_per_sub = {type(act).n_busbar_per_sub} != {n_busbar}" + + def test_regular_env(self): + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendSupport(), test=True, _add_to_name=type(self).__name__+"_2") + self._aux_fun_test(env, 2) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendSupport(), test=True, n_busbar=3, _add_to_name=type(self).__name__+"_3") + self._aux_fun_test(env, 3) + + def test_multimix_env(self): + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make("l2rpn_neurips_2020_track2", backend=_AuxFakeBackendSupport(), test=True, _add_to_name=type(self).__name__+"_2") + self._aux_fun_test(env, 2) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make("l2rpn_neurips_2020_track2", backend=_AuxFakeBackendSupport(), test=True, n_busbar=3, _add_to_name=type(self).__name__+"_3") + self._aux_fun_test(env, 3) + + def test_masked_env(self): + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = MaskedEnvironment(grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendSupport(), test=True, _add_to_name=type(self).__name__+"_mask_2"), + lines_of_interest=np.ones(shape=20, dtype=bool)) + self._aux_fun_test(env, 2) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = MaskedEnvironment(grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendSupport(), test=True, n_busbar=3, _add_to_name=type(self).__name__+"_mask_3"), + lines_of_interest=np.ones(shape=20, dtype=bool)) + self._aux_fun_test(env, 3) + + def test_to_env(self): + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = TimedOutEnvironment(grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendSupport(), test=True, _add_to_name=type(self).__name__+"_to_2"), + time_out_ms=3000) + self._aux_fun_test(env, 2) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = TimedOutEnvironment(grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendSupport(), test=True, n_busbar=3, _add_to_name=type(self).__name__+"_to_3"), + time_out_ms=3000) + self._aux_fun_test(env, 3) + + def test_xxxhandle_more_than_2_busbar_not_called(self): + """when using a backend that did not called the `can_handle_more_than_2_busbar_not_called` + nor the `cannot_handle_more_than_2_busbar_not_called` then it's equivalent + to not support this new feature.""" + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendNoCalled(), test=True, _add_to_name=type(self).__name__+"_nocall_2") + self._aux_fun_test(env, 2) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendNoCalled(), test=True, n_busbar=3, _add_to_name=type(self).__name__+"_nocall_3") + self._aux_fun_test(env, 2) + + def test_cannot_handle_more_than_2_busbar_not_called(self): + """when using a backend that called `cannot_handle_more_than_2_busbar_not_called` then it's equivalent + to not support this new feature.""" + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendNoSupport(), test=True, _add_to_name=type(self).__name__+"_dontcalled_2") + self._aux_fun_test(env, 2) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendNoSupport(), test=True, n_busbar=3, _add_to_name=type(self).__name__+"_dontcalled_3") + self._aux_fun_test(env, 2) + + def test_env_copy(self): + """test env copy does work correctly""" + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendSupport(), test=True, _add_to_name=type(self).__name__+"_copy_2") + self._aux_fun_test(env, 2) + env_cpy = env.copy() + self._aux_fun_test(env_cpy, 2) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendSupport(), test=True, n_busbar=3, _add_to_name=type(self).__name__+"_copy_3") + self._aux_fun_test(env, 3) + env_cpy = env.copy() + self._aux_fun_test(env_cpy, 3) + + def test_two_env_same_name(self): + """test i can load 2 env with the same name but different n_busbar""" + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env_2 = grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendSupport(), test=True, _add_to_name=type(self).__name__+"_same_name") + self._aux_fun_test(env_2, 2) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env_3 = grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendSupport(), test=True, n_busbar=3, _add_to_name=type(self).__name__+"_same_name") + self._aux_fun_test(env_3, 3) # check env_3 has indeed 3 buses + self._aux_fun_test(env_2, 2) # check env_2 is not modified + + \ No newline at end of file From 0aa53f1fac4d2ba8501b6d54f7c057034a3c4180 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 26 Jan 2024 14:54:59 +0100 Subject: [PATCH 03/24] fixing 2 bugs spotted by sonar cloud --- CHANGELOG.rst | 3 ++- grid2op/Observation/observationSpace.py | 5 +++-- grid2op/utils/l2rpn_idf_2023_scores.py | 6 +++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d3a9bec78..9ad13e497 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -34,8 +34,9 @@ Change Log [1.9.9] - 2024-xx-yy ---------------------- - [FIXED] github CI did not upload the source files -- [FIXED] l2rpn_utils module did not stored correctly the order +- [FIXED] `l2rpn_utils` module did not stored correctly the order of actions and observation for wcci_2020 +- [FIXED] 2 bugs detected by static code analysis (thanks sonar cloud) [1.9.8] - 2024-01-26 ---------------------- diff --git a/grid2op/Observation/observationSpace.py b/grid2op/Observation/observationSpace.py index add75c631..af454bde2 100644 --- a/grid2op/Observation/observationSpace.py +++ b/grid2op/Observation/observationSpace.py @@ -93,7 +93,7 @@ def __init__( self.logger.disabled = True else: self.logger: logging.Logger = logger.getChild("grid2op_ObsSpace") - + self._init_observationClass = observationClass SerializableObservationSpace.__init__( self, gridobj, observationClass=observationClass ) @@ -283,7 +283,7 @@ def reactivate_forecast(self, env): if self.obs_env is not None : self.obs_env.close() self.obs_env = None - self._create_obs_env(env) + self._create_obs_env(env, self._init_observationClass) self.set_real_env_kwargs(env) self.with_forecast = True @@ -463,6 +463,7 @@ def _custom_deepcopy_for_copy(self, new_obj): super()._custom_deepcopy_for_copy(new_obj) # now fill my class + new_obj._init_observationClass = self._init_observationClass new_obj.with_forecast = self.with_forecast new_obj._simulate_parameters = copy.deepcopy(self._simulate_parameters) new_obj._reward_func = copy.deepcopy(self._reward_func) diff --git a/grid2op/utils/l2rpn_idf_2023_scores.py b/grid2op/utils/l2rpn_idf_2023_scores.py index 307cf3881..6655de254 100644 --- a/grid2op/utils/l2rpn_idf_2023_scores.py +++ b/grid2op/utils/l2rpn_idf_2023_scores.py @@ -114,13 +114,13 @@ def __init__( score_names=score_names, add_nb_highres_sim=add_nb_highres_sim, ) - weights=np.array([weight_op_score,weight_assistant_score,weight_nres_score]) + weights=np.array([weight_op_score, weight_assistant_score, weight_nres_score]) total_weights = weights.sum() - if total_weights != 1.0: + if abs(total_weights - 1.0) >= 1e-8: raise Grid2OpException( 'The weights of each component of the score shall sum to 1' ) - if np.any(weights <0): + if np.any(weights < 0.): raise Grid2OpException( 'All weights should be positive' ) From 2ac4a03d8f5e568980b750f5dbf0f3e97f5136a6 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Mon, 29 Jan 2024 15:51:25 +0100 Subject: [PATCH 04/24] feature implemented for actions too --- CHANGELOG.rst | 3 + grid2op/Action/_backendAction.py | 104 ++--- grid2op/Action/baseAction.py | 471 +++++++++++++++-------- grid2op/Backend/educPandaPowerBackend.py | 3 +- grid2op/Backend/pandaPowerBackend.py | 53 +-- grid2op/Observation/baseObservation.py | 2 +- grid2op/Space/GridObjects.py | 17 +- grid2op/tests/test_n_busbar_per_sub.py | 172 ++++++++- 8 files changed, 586 insertions(+), 239 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5ccc840e0..5d3d6f758 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -37,6 +37,9 @@ Change Log - [FIXED] `l2rpn_utils` module did not stored correctly the order of actions and observation for wcci_2020 - [FIXED] 2 bugs detected by static code analysis (thanks sonar cloud) +- [FIXED] a bug in `act.get_gen_modif` (vector of wrong size was used, could lead + to some crashes if n_gen >= n_load) +- [FIXED] a bug in `act.as_dict` when shunts were modified - [IMPROVED] handling of "compatibility" grid2op version (by calling the relevant things done in the base class in `BaseAction` and `BaseObservation`) and by using the `from packaging import version` diff --git a/grid2op/Action/_backendAction.py b/grid2op/Action/_backendAction.py index b5e19022c..0e60d9c05 100644 --- a/grid2op/Action/_backendAction.py +++ b/grid2op/Action/_backendAction.py @@ -8,7 +8,13 @@ import copy import numpy as np +from typing import Tuple, Union +try: + from typing import Self +except ImportError: + from typing_extensions import Self +from grid2op.Action.baseAction import BaseAction from grid2op.dtypes import dt_int, dt_bool, dt_float from grid2op.Space import GridObjects @@ -213,41 +219,41 @@ class _BackendAction(GridObjects): def __init__(self): GridObjects.__init__(self) + cls = type(self) # last connected registered - self.last_topo_registered = ValueStore(self.dim_topo, dtype=dt_int) + self.last_topo_registered = ValueStore(cls.dim_topo, dtype=dt_int) # topo at time t - self.current_topo = ValueStore(self.dim_topo, dtype=dt_int) + self.current_topo = ValueStore(cls.dim_topo, dtype=dt_int) # by default everything is on busbar 1 self.last_topo_registered.values[:] = 1 self.current_topo.values[:] = 1 # injection at time t - self.prod_p = ValueStore(self.n_gen, dtype=dt_float) - self.prod_v = ValueStore(self.n_gen, dtype=dt_float) - self.load_p = ValueStore(self.n_load, dtype=dt_float) - self.load_q = ValueStore(self.n_load, dtype=dt_float) - self.storage_power = ValueStore(self.n_storage, dtype=dt_float) + self.prod_p = ValueStore(cls.n_gen, dtype=dt_float) + self.prod_v = ValueStore(cls.n_gen, dtype=dt_float) + self.load_p = ValueStore(cls.n_load, dtype=dt_float) + self.load_q = ValueStore(cls.n_load, dtype=dt_float) + self.storage_power = ValueStore(cls.n_storage, dtype=dt_float) - self.activated_bus = np.full((self.n_sub, 2), dtype=dt_bool, fill_value=False) + self.activated_bus = np.full((cls.n_sub, cls.n_busbar_per_sub), dtype=dt_bool, fill_value=False) self.big_topo_to_subid = np.repeat( - list(range(self.n_sub)), repeats=self.sub_info + list(range(cls.n_sub)), repeats=cls.sub_info ) # shunts - cls = type(self) if cls.shunts_data_available: - self.shunt_p = ValueStore(self.n_shunt, dtype=dt_float) - self.shunt_q = ValueStore(self.n_shunt, dtype=dt_float) - self.shunt_bus = ValueStore(self.n_shunt, dtype=dt_int) - self.current_shunt_bus = ValueStore(self.n_shunt, dtype=dt_int) + self.shunt_p = ValueStore(cls.n_shunt, dtype=dt_float) + self.shunt_q = ValueStore(cls.n_shunt, dtype=dt_float) + self.shunt_bus = ValueStore(cls.n_shunt, dtype=dt_int) + self.current_shunt_bus = ValueStore(cls.n_shunt, dtype=dt_int) self.current_shunt_bus.values[:] = 1 - self._status_or_before = np.ones(self.n_line, dtype=dt_int) - self._status_ex_before = np.ones(self.n_line, dtype=dt_int) - self._status_or = np.ones(self.n_line, dtype=dt_int) - self._status_ex = np.ones(self.n_line, dtype=dt_int) + self._status_or_before = np.ones(cls.n_line, dtype=dt_int) + self._status_ex_before = np.ones(cls.n_line, dtype=dt_int) + self._status_or = np.ones(cls.n_line, dtype=dt_int) + self._status_ex = np.ones(cls.n_line, dtype=dt_int) self._loads_bus = None self._gens_bus = None @@ -255,7 +261,7 @@ def __init__(self): self._lines_ex_bus = None self._storage_bus = None - def __deepcopy__(self, memodict={}): + def __deepcopy__(self, memodict={}) -> Self: res = type(self)() # last connected registered res.last_topo_registered.copy(self.last_topo_registered) @@ -287,11 +293,11 @@ def __deepcopy__(self, memodict={}): return res - def __copy__(self): + def __copy__(self) -> Self: res = self.__deepcopy__() # nothing less to do return res - def reorder(self, no_load, no_gen, no_topo, no_storage, no_shunt): + def reorder(self, no_load, no_gen, no_topo, no_storage, no_shunt) -> None: """ .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ @@ -316,7 +322,7 @@ def reorder(self, no_load, no_gen, no_topo, no_storage, no_shunt): self.shunt_bus.reorder(no_shunt) self.current_shunt_bus.reorder(no_shunt) - def reset(self): + def reset(self) -> None: # last topo self.last_topo_registered.reset() @@ -341,7 +347,7 @@ def reset(self): self.shunt_bus.reset() self.current_shunt_bus.reset() - def all_changed(self): + def all_changed(self) -> None: # last topo self.last_topo_registered.all_changed() @@ -365,7 +371,7 @@ def all_changed(self): def set_redispatch(self, new_redispatching): self.prod_p.change_val(new_redispatching) - def __iadd__(self, other): + def __iadd__(self, other : BaseAction) -> Self: """ .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ @@ -373,7 +379,7 @@ def __iadd__(self, other): Parameters ---------- - other: :class:`grid2op.Action.BaseAction.BaseAction` + other: :class:`grid2op.Action.BaseAction` Returns ------- @@ -492,23 +498,27 @@ def __iadd__(self, other): return self - def _assign_0_to_disco_el(self): + def _assign_0_to_disco_el(self) -> None: """do not consider disconnected elements are modified for there active / reactive / voltage values""" - gen_changed = self.current_topo.changed[type(self).gen_pos_topo_vect] - gen_bus = self.current_topo.values[type(self).gen_pos_topo_vect] + cls = type(self) + gen_changed = self.current_topo.changed[cls.gen_pos_topo_vect] + gen_bus = self.current_topo.values[cls.gen_pos_topo_vect] self.prod_p.force_unchanged(gen_changed, gen_bus) self.prod_v.force_unchanged(gen_changed, gen_bus) - load_changed = self.current_topo.changed[type(self).load_pos_topo_vect] - load_bus = self.current_topo.values[type(self).load_pos_topo_vect] + load_changed = self.current_topo.changed[cls.load_pos_topo_vect] + load_bus = self.current_topo.values[cls.load_pos_topo_vect] self.load_p.force_unchanged(load_changed, load_bus) self.load_q.force_unchanged(load_changed, load_bus) - sto_changed = self.current_topo.changed[type(self).storage_pos_topo_vect] - sto_bus = self.current_topo.values[type(self).storage_pos_topo_vect] + sto_changed = self.current_topo.changed[cls.storage_pos_topo_vect] + sto_bus = self.current_topo.values[cls.storage_pos_topo_vect] self.storage_power.force_unchanged(sto_changed, sto_bus) - def __call__(self): + def __call__(self) -> Tuple[np.ndarray, + Tuple[ValueStore, ValueStore, ValueStore, ValueStore, ValueStore], + ValueStore, + Union[Tuple[ValueStore, ValueStore, ValueStore], None]]: self._assign_0_to_disco_el() injections = ( self.prod_p, @@ -524,32 +534,32 @@ def __call__(self): self._get_active_bus() return self.activated_bus, injections, topo, shunts - def get_loads_bus(self): + def get_loads_bus(self) -> ValueStore: if self._loads_bus is None: self._loads_bus = ValueStore(self.n_load, dtype=dt_int) self._loads_bus.copy_from_index(self.current_topo, self.load_pos_topo_vect) return self._loads_bus - def _aux_to_global(self, value_store, to_subid): + def _aux_to_global(self, value_store, to_subid) -> ValueStore: value_store = copy.deepcopy(value_store) value_store.values = type(self).local_bus_to_global(value_store.values, to_subid) return value_store - def get_loads_bus_global(self): + def get_loads_bus_global(self) -> ValueStore: tmp_ = self.get_loads_bus() return self._aux_to_global(tmp_, self.load_to_subid) - def get_gens_bus(self): + def get_gens_bus(self) -> ValueStore: if self._gens_bus is None: self._gens_bus = ValueStore(self.n_gen, dtype=dt_int) self._gens_bus.copy_from_index(self.current_topo, self.gen_pos_topo_vect) return self._gens_bus - def get_gens_bus_global(self): + def get_gens_bus_global(self) -> ValueStore: tmp_ = copy.deepcopy(self.get_gens_bus()) return self._aux_to_global(tmp_, self.gen_to_subid) - def get_lines_or_bus(self): + def get_lines_or_bus(self) -> ValueStore: if self._lines_or_bus is None: self._lines_or_bus = ValueStore(self.n_line, dtype=dt_int) self._lines_or_bus.copy_from_index( @@ -557,11 +567,11 @@ def get_lines_or_bus(self): ) return self._lines_or_bus - def get_lines_or_bus_global(self): + def get_lines_or_bus_global(self) -> ValueStore: tmp_ = self.get_lines_or_bus() return self._aux_to_global(tmp_, self.line_or_to_subid) - def get_lines_ex_bus(self): + def get_lines_ex_bus(self) -> ValueStore: if self._lines_ex_bus is None: self._lines_ex_bus = ValueStore(self.n_line, dtype=dt_int) self._lines_ex_bus.copy_from_index( @@ -569,23 +579,23 @@ def get_lines_ex_bus(self): ) return self._lines_ex_bus - def get_lines_ex_bus_global(self): + def get_lines_ex_bus_global(self) -> ValueStore: tmp_ = self.get_lines_ex_bus() return self._aux_to_global(tmp_, self.line_ex_to_subid) - def get_storages_bus(self): + def get_storages_bus(self) -> ValueStore: if self._storage_bus is None: self._storage_bus = ValueStore(self.n_storage, dtype=dt_int) self._storage_bus.copy_from_index(self.current_topo, self.storage_pos_topo_vect) return self._storage_bus - def get_storages_bus_global(self): + def get_storages_bus_global(self) -> ValueStore: tmp_ = self.get_storages_bus() return self._aux_to_global(tmp_, self.storage_to_subid) - def _get_active_bus(self): + def _get_active_bus(self) -> None: self.activated_bus[:, :] = False - tmp = self.current_topo.values - 1 # TODO global to local ! + tmp = self.current_topo.values - 1 is_el_conn = tmp >= 0 self.activated_bus[self.big_topo_to_subid[is_el_conn], tmp[is_el_conn]] = True if type(self).shunts_data_available: @@ -593,7 +603,7 @@ def _get_active_bus(self): tmp = self.current_shunt_bus.values - 1 self.activated_bus[type(self).shunt_to_subid[is_el_conn], tmp[is_el_conn]] = True - def update_state(self, powerline_disconnected): + def update_state(self, powerline_disconnected) -> None: """ .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 10678eba5..15c6c5bbe 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -9,7 +9,7 @@ import copy import numpy as np import warnings -from typing import Tuple +from typing import Tuple, Dict, Literal, Any from packaging import version from grid2op.dtypes import dt_int, dt_bool, dt_float @@ -775,7 +775,7 @@ def alert_raised(self) -> np.ndarray: @classmethod def process_grid2op_compat(cls): - GridObjects.process_grid2op_compat(cls) + super().process_grid2op_compat() glop_ver = cls._get_grid2op_version_as_version_obj() if cls.glop_version == cls.BEFORE_COMPAT_VERSION: @@ -784,7 +784,6 @@ def process_grid2op_compat(cls): # this is really important, otherwise things from grid2op base types will be affected cls.authorized_keys = copy.deepcopy(cls.authorized_keys) cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) - cls.attr_list_set = copy.deepcopy(cls.attr_list_set) # deactivate storage cls.set_no_storage() @@ -799,7 +798,6 @@ def process_grid2op_compat(cls): cls.authorized_keys.remove("curtail") if "_curtail" in cls.attr_list_vect: cls.attr_list_vect.remove("_curtail") - cls.attr_list_set = set(cls.attr_list_vect) if glop_ver < version.parse("1.6.0"): # this feature did not exist before. @@ -809,6 +807,23 @@ def process_grid2op_compat(cls): # this feature did not exist before. cls.dim_alerts = 0 + if (cls.n_busbar_per_sub >= 3) or (cls.n_busbar_per_sub == 1): + # only relevant for grid2op >= 1.9.9 + # remove "change_bus" if it's there more than 3 buses (no sense: where to change it ???) + # or if there are only one busbar (cannot change anything) + # 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.authorized_keys = copy.deepcopy(cls.authorized_keys) + cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) + + if "change_bus" in cls.authorized_keys: + cls.authorized_keys.remove("change_bus") + if "_change_bus_vect" in cls.attr_list_vect: + cls.attr_list_vect.remove("_change_bus_vect") + + cls.attr_list_set = copy.deepcopy(cls.attr_list_set) + cls.attr_list_set = set(cls.attr_list_vect) + def _reset_modified_flags(self): self._modif_inj = False self._modif_set_bus = False @@ -2107,13 +2122,20 @@ def update(self, dict_): - 0 -> don't change anything - +1 -> set to bus 1, - - +2 -> set to bus 2, etc. + - +2 -> set to bus 2 + - +3 -> set to bus 3 (grid2op >= 1.9.9) + - etc. - -1: You can use this method to disconnect an object by setting the value to -1. + + .. versionchanged:: 1.9.9 + This feature is deactivated if `act.n_busbar_per_sub == 1` - "change_bus": (numpy bool vector or dictionary) will change the bus to which the object is connected. True will change it (eg switch it from bus 1 to bus 2 or from bus 2 to bus 1). NB this is only active if the system has only 2 buses per substation. + .. versionchanged:: 1.9.9 + This feature is deactivated if `act.n_busbar_per_sub >= 3` or `act.n_busbar_per_sub == 1` - "redispatch": the best use of this is to specify either the numpy array of the redispatch vector you want to apply (that should have the size of the number of generators on the grid) or to specify a list of @@ -2447,7 +2469,8 @@ def _check_for_ambiguity(self): """ # check that the correct flags are properly computed self._check_for_correct_modif_flags() - + cls = type(self) + if ( self._modif_change_status and self._modif_set_status @@ -2462,58 +2485,58 @@ def _check_for_ambiguity(self): # check size if self._modif_inj: if "load_p" in self._dict_inj: - if len(self._dict_inj["load_p"]) != self.n_load: + if len(self._dict_inj["load_p"]) != cls.n_load: raise InvalidNumberOfLoads( "This action acts on {} loads while there are {} " "in the _grid".format( - len(self._dict_inj["load_p"]), self.n_load + len(self._dict_inj["load_p"]), cls.n_load ) ) if "load_q" in self._dict_inj: - if len(self._dict_inj["load_q"]) != self.n_load: + if len(self._dict_inj["load_q"]) != cls.n_load: raise InvalidNumberOfLoads( "This action acts on {} loads while there are {} in " - "the _grid".format(len(self._dict_inj["load_q"]), self.n_load) + "the _grid".format(len(self._dict_inj["load_q"]), cls.n_load) ) if "prod_p" in self._dict_inj: - if len(self._dict_inj["prod_p"]) != self.n_gen: + if len(self._dict_inj["prod_p"]) != cls.n_gen: raise InvalidNumberOfGenerators( "This action acts on {} generators while there are {} in " - "the _grid".format(len(self._dict_inj["prod_p"]), self.n_gen) + "the _grid".format(len(self._dict_inj["prod_p"]), cls.n_gen) ) if "prod_v" in self._dict_inj: - if len(self._dict_inj["prod_v"]) != self.n_gen: + if len(self._dict_inj["prod_v"]) != cls.n_gen: raise InvalidNumberOfGenerators( "This action acts on {} generators while there are {} in " - "the _grid".format(len(self._dict_inj["prod_v"]), self.n_gen) + "the _grid".format(len(self._dict_inj["prod_v"]), cls.n_gen) ) - if len(self._switch_line_status) != self.n_line: + if len(self._switch_line_status) != cls.n_line: raise InvalidNumberOfLines( "This action acts on {} lines while there are {} in " - "the _grid".format(len(self._switch_line_status), self.n_line) + "the _grid".format(len(self._switch_line_status), cls.n_line) ) - if len(self._set_topo_vect) != self.dim_topo: + if len(self._set_topo_vect) != cls.dim_topo: raise InvalidNumberOfObjectEnds( "This action acts on {} ends of object while there are {} " - "in the _grid".format(len(self._set_topo_vect), self.dim_topo) + "in the _grid".format(len(self._set_topo_vect), cls.dim_topo) ) - if len(self._change_bus_vect) != self.dim_topo: + if len(self._change_bus_vect) != cls.dim_topo: raise InvalidNumberOfObjectEnds( "This action acts on {} ends of object while there are {} " - "in the _grid".format(len(self._change_bus_vect), self.dim_topo) + "in the _grid".format(len(self._change_bus_vect), cls.dim_topo) ) - if len(self._redispatch) != self.n_gen: + if len(self._redispatch) != cls.n_gen: raise InvalidNumberOfGenerators( "This action acts on {} generators (redispatching= while " - "there are {} in the grid".format(len(self._redispatch), self.n_gen) + "there are {} in the grid".format(len(self._redispatch), cls.n_gen) ) # redispatching specific check if self._modif_redispatch: - if "redispatch" not in self.authorized_keys: + if "redispatch" not in cls.authorized_keys: raise AmbiguousAction( 'Action of type "redispatch" are not supported by this action type' ) @@ -2523,17 +2546,17 @@ def _check_for_ambiguity(self): "environment. Please set up the proper costs for generator" ) - if (self._redispatch[~self.gen_redispatchable] != 0.0).any(): + if (self._redispatch[~cls.gen_redispatchable] != 0.0).any(): raise InvalidRedispatching( "Trying to apply a redispatching action on a non redispatchable generator" ) if self._single_act: - if (self._redispatch > self.gen_max_ramp_up).any(): + if (self._redispatch > cls.gen_max_ramp_up).any(): raise InvalidRedispatching( "Some redispatching amount are above the maximum ramp up" ) - if (-self._redispatch > self.gen_max_ramp_down).any(): + if (-self._redispatch > cls.gen_max_ramp_down).any(): raise InvalidRedispatching( "Some redispatching amount are bellow the maximum ramp down" ) @@ -2542,12 +2565,12 @@ def _check_for_ambiguity(self): new_p = self._dict_inj["prod_p"] tmp_p = new_p + self._redispatch indx_ok = np.isfinite(new_p) - if (tmp_p[indx_ok] > self.gen_pmax[indx_ok]).any(): + if (tmp_p[indx_ok] > cls.gen_pmax[indx_ok]).any(): raise InvalidRedispatching( "Some redispatching amount, cumulated with the production setpoint, " "are above pmax for some generator." ) - if (tmp_p[indx_ok] < self.gen_pmin[indx_ok]).any(): + if (tmp_p[indx_ok] < cls.gen_pmin[indx_ok]).any(): raise InvalidRedispatching( "Some redispatching amount, cumulated with the production setpoint, " "are below pmin for some generator." @@ -2576,7 +2599,7 @@ def _check_for_ambiguity(self): "1 (assign this object to bus one) or 2 (assign this object to bus" "2). A negative number has been found." ) - if self._modif_set_bus and (self._set_topo_vect > 2).any(): + if self._modif_set_bus and (self._set_topo_vect > cls.n_busbar_per_sub).any(): raise InvalidBusStatus( "Invalid set_bus. Buses should be either -1 (disconnect), 0 (change nothing)," "1 (assign this object to bus one) or 2 (assign this object to bus" @@ -2602,14 +2625,14 @@ def _check_for_ambiguity(self): ) if self._modif_set_bus: - disco_or = self._set_topo_vect[self.line_or_pos_topo_vect] == -1 - if (self._set_topo_vect[self.line_ex_pos_topo_vect][disco_or] > 0).any(): + 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(): raise InvalidLineStatus( "A powerline is connected (set to a bus at extremity end) and " "disconnected (set to bus -1 at origin end)" ) - disco_ex = self._set_topo_vect[self.line_ex_pos_topo_vect] == -1 - if (self._set_topo_vect[self.line_or_pos_topo_vect][disco_ex] > 0).any(): + disco_ex = self._set_topo_vect[cls.line_ex_pos_topo_vect] == -1 + if (self._set_topo_vect[cls.line_or_pos_topo_vect][disco_ex] > 0).any(): raise InvalidLineStatus( "A powerline is connected (set to a bus at origin end) and " "disconnected (set to bus -1 at extremity end)" @@ -2624,40 +2647,40 @@ def _check_for_ambiguity(self): id_reco = np.where(idx2)[0] if self._modif_set_bus: - if "set_bus" not in self.authorized_keys: + if "set_bus" not in cls.authorized_keys: raise AmbiguousAction( 'Action of type "set_bus" are not supported by this action type' ) if ( - self._set_topo_vect[self.line_or_pos_topo_vect[id_disc]] > 0 - ).any() or (self._set_topo_vect[self.line_ex_pos_topo_vect[id_disc]] > 0).any(): + self._set_topo_vect[cls.line_or_pos_topo_vect[id_disc]] > 0 + ).any() or (self._set_topo_vect[cls.line_ex_pos_topo_vect[id_disc]] > 0).any(): raise InvalidLineStatus( "You ask to disconnect a powerline but also to connect it " "to a certain bus." ) if ( - self._set_topo_vect[self.line_or_pos_topo_vect[id_reco]] == -1 - ).any() or (self._set_topo_vect[self.line_ex_pos_topo_vect[id_reco]] == -1).any(): + self._set_topo_vect[cls.line_or_pos_topo_vect[id_reco]] == -1 + ).any() or (self._set_topo_vect[cls.line_ex_pos_topo_vect[id_reco]] == -1).any(): raise InvalidLineStatus( "You ask to reconnect a powerline but also to disconnect it " "from a certain bus." ) if self._modif_change_bus: - if "change_bus" not in self.authorized_keys: + if "change_bus" not in cls.authorized_keys: raise AmbiguousAction( 'Action of type "change_bus" are not supported by this action type' ) if ( - self._change_bus_vect[self.line_or_pos_topo_vect[id_disc]] > 0 - ).any() or (self._change_bus_vect[self.line_ex_pos_topo_vect[id_disc]] > 0).any(): + self._change_bus_vect[cls.line_or_pos_topo_vect[id_disc]] > 0 + ).any() or (self._change_bus_vect[cls.line_ex_pos_topo_vect[id_disc]] > 0).any(): raise InvalidLineStatus( "You ask to disconnect a powerline but also to change its bus." ) if ( self._change_bus_vect[ - self.line_or_pos_topo_vect[self._set_line_status == 1] + cls.line_or_pos_topo_vect[self._set_line_status == 1] ] ).any(): raise InvalidLineStatus( @@ -2666,7 +2689,7 @@ def _check_for_ambiguity(self): ) if ( self._change_bus_vect[ - self.line_ex_pos_topo_vect[self._set_line_status == 1] + cls.line_ex_pos_topo_vect[self._set_line_status == 1] ] ).any(): raise InvalidLineStatus( @@ -2674,20 +2697,20 @@ def _check_for_ambiguity(self): "which it is connected. This is ambiguous. You must *set* this bus instead." ) - if type(self).shunts_data_available: - if self.shunt_p.shape[0] != self.n_shunt: + if cls.shunts_data_available: + if self.shunt_p.shape[0] != cls.n_shunt: raise IncorrectNumberOfElements( "Incorrect number of shunt (for shunt_p) in your action." ) - if self.shunt_q.shape[0] != self.n_shunt: + if self.shunt_q.shape[0] != cls.n_shunt: raise IncorrectNumberOfElements( "Incorrect number of shunt (for shunt_q) in your action." ) - if self.shunt_bus.shape[0] != self.n_shunt: + if self.shunt_bus.shape[0] != cls.n_shunt: raise IncorrectNumberOfElements( "Incorrect number of shunt (for shunt_bus) in your action." ) - if self.n_shunt > 0: + if cls.n_shunt > 0: if np.max(self.shunt_bus) > 2: raise AmbiguousAction( "Some shunt is connected to a bus greater than 2" @@ -2713,10 +2736,10 @@ def _check_for_ambiguity(self): ) if self._modif_alarm: - if self._raise_alarm.shape[0] != self.dim_alarms: + if self._raise_alarm.shape[0] != cls.dim_alarms: raise AmbiguousAction( f"Wrong number of alarm raised: {self._raise_alarm.shape[0]} raised, expecting " - f"{self.dim_alarms}" + f"{cls.dim_alarms}" ) else: if self._raise_alarm.any(): @@ -2726,10 +2749,10 @@ def _check_for_ambiguity(self): ) if self._modif_alert: - if self._raise_alert.shape[0] != self.dim_alerts: + if self._raise_alert.shape[0] != cls.dim_alerts: raise AmbiguousActionRaiseAlert( f"Wrong number of alert raised: {self._raise_alert.shape[0]} raised, expecting " - f"{self.dim_alerts}" + f"{cls.dim_alerts}" ) else: if self._raise_alert.any(): @@ -2740,55 +2763,57 @@ def _check_for_ambiguity(self): def _is_storage_ambiguous(self): """check if storage actions are ambiguous""" + cls = type(self) if self._modif_storage: - if "set_storage" not in self.authorized_keys: + if "set_storage" not in cls.authorized_keys: raise AmbiguousAction( 'Action of type "set_storage" are not supported by this action type' ) - if self.n_storage == 0: + if cls.n_storage == 0: raise InvalidStorage( "Attempt to modify a storage unit while there is none on the grid" ) - if self._storage_power.shape[0] != self.n_storage: + if self._storage_power.shape[0] != cls.n_storage: raise InvalidStorage( "self._storage_power.shape[0] != self.n_storage: wrong number of storage " "units affected" ) - if (self._storage_power < -self.storage_max_p_prod).any(): - where_bug = np.where(self._storage_power < -self.storage_max_p_prod)[0] + if (self._storage_power < -cls.storage_max_p_prod).any(): + where_bug = np.where(self._storage_power < -cls.storage_max_p_prod)[0] raise InvalidStorage( f"you asked a storage unit to absorb more than what it can: " f"self._storage_power[{where_bug}] < -self.storage_max_p_prod[{where_bug}]." ) - if (self._storage_power > self.storage_max_p_absorb).any(): - where_bug = np.where(self._storage_power > self.storage_max_p_absorb)[0] + if (self._storage_power > cls.storage_max_p_absorb).any(): + where_bug = np.where(self._storage_power > cls.storage_max_p_absorb)[0] raise InvalidStorage( f"you asked a storage unit to produce more than what it can: " f"self._storage_power[{where_bug}] > self.storage_max_p_absorb[{where_bug}]." ) - if "_storage_power" not in self.attr_list_set: - if (self._set_topo_vect[self.storage_pos_topo_vect] > 0).any(): + if "_storage_power" not in cls.attr_list_set: + if (self._set_topo_vect[cls.storage_pos_topo_vect] > 0).any(): raise InvalidStorage("Attempt to modify bus (set) of a storage unit") - if (self._change_bus_vect[self.storage_pos_topo_vect]).any(): + if (self._change_bus_vect[cls.storage_pos_topo_vect]).any(): raise InvalidStorage("Attempt to modify bus (change) of a storage unit") def _is_curtailment_ambiguous(self): """check if curtailment action is ambiguous""" + cls = type(self) if self._modif_curtailment: - if "curtail" not in self.authorized_keys: + if "curtail" not in cls.authorized_keys: raise AmbiguousAction( 'Action of type "curtail" are not supported by this action type' ) - if not self.redispatching_unit_commitment_availble: + if not cls.redispatching_unit_commitment_availble: raise UnitCommitorRedispachingNotAvailable( "Impossible to use a redispatching action in this " "environment. Please set up the proper costs for generator. " "This also means curtailment feature is not available." ) - if self._curtail.shape[0] != self.n_gen: + if self._curtail.shape[0] != cls.n_gen: raise InvalidCurtailment( "self._curtail.shape[0] != self.n_gen: wrong number of generator " "units affected" @@ -2808,7 +2833,7 @@ def _is_curtailment_ambiguous(self): f"self._curtail[{where_bug}] > 1. " f"Curtailment should be a real number between 0.0 and 1.0" ) - if (self._curtail[~self.gen_renewable] != -1.0).any(): + if (self._curtail[~cls.gen_renewable] != -1.0).any(): raise InvalidCurtailment( "Trying to apply a curtailment on a non renewable generator" ) @@ -2820,41 +2845,49 @@ def _ignore_topo_action_if_disconnection(self, sel_): self._set_topo_vect[np.array(self.line_ex_pos_topo_vect[sel_])] = 0 self._change_bus_vect[np.array(self.line_ex_pos_topo_vect[sel_])] = False - def _obj_caract_from_topo_id(self, id_): + def _obj_caract_from_topo_id(self, id_, with_name=False): obj_id = None objt_type = None array_subid = None - for l_id, id_in_topo in enumerate(self.load_pos_topo_vect): + cls = type(self) + for l_id, id_in_topo in enumerate(cls.load_pos_topo_vect): if id_in_topo == id_: obj_id = l_id objt_type = "load" - array_subid = self.load_to_subid + array_subid = cls.load_to_subid + obj_name = cls.name_load[l_id] if obj_id is None: - for l_id, id_in_topo in enumerate(self.gen_pos_topo_vect): + for l_id, id_in_topo in enumerate(cls.gen_pos_topo_vect): if id_in_topo == id_: obj_id = l_id objt_type = "generator" - array_subid = self.gen_to_subid + array_subid = cls.gen_to_subid + obj_name = cls.name_gen[l_id] if obj_id is None: - for l_id, id_in_topo in enumerate(self.line_or_pos_topo_vect): + for l_id, id_in_topo in enumerate(cls.line_or_pos_topo_vect): if id_in_topo == id_: obj_id = l_id objt_type = self._line_or_str - array_subid = self.line_or_to_subid + array_subid = cls.line_or_to_subid + obj_name = cls.name_line[l_id] if obj_id is None: - for l_id, id_in_topo in enumerate(self.line_ex_pos_topo_vect): + for l_id, id_in_topo in enumerate(cls.line_ex_pos_topo_vect): if id_in_topo == id_: obj_id = l_id objt_type = self._line_ex_str - array_subid = self.line_ex_to_subid + array_subid = cls.line_ex_to_subid + obj_name = cls.name_line[l_id] if obj_id is None: - for l_id, id_in_topo in enumerate(self.storage_pos_topo_vect): + for l_id, id_in_topo in enumerate(cls.storage_pos_topo_vect): if id_in_topo == id_: obj_id = l_id objt_type = "storage" - array_subid = self.storage_to_subid + array_subid = cls.storage_to_subid + obj_name = cls.name_storage[l_id] substation_id = array_subid[obj_id] - return obj_id, objt_type, substation_id + if not with_name: + return obj_id, objt_type, substation_id + return obj_id, objt_type, substation_id, obj_name def __str__(self) -> str: """ @@ -3205,7 +3238,11 @@ def impact_on_objects(self) -> dict: "curtailment": curtailment, } - def as_dict(self) -> dict: + def as_dict(self) -> Dict[Literal["load_p", "load_q", "prod_p", "prod_v", + "change_line_status", "set_line_status", + "change_bus_vect", "set_bus_vect", + "redispatch", "storage_power", "curtailment"], + Any]: """ Represent an action "as a" dictionary. This dictionary is useful to further inspect on which elements the actions had an impact. It is not recommended to use it as a way to serialize actions. The "do nothing" @@ -3260,7 +3297,8 @@ def as_dict(self) -> dict: dispatchable one) the amount of power redispatched in this action. * `storage_power`: the setpoint for production / consumption for all storage units * `curtailment`: the curtailment performed on all generator - + * `shunt` : + Returns ------- res: ``dict`` @@ -3303,13 +3341,13 @@ def as_dict(self) -> dict: all_subs = set() for id_, k in enumerate(self._change_bus_vect): if k: - obj_id, objt_type, substation_id = self._obj_caract_from_topo_id( - id_ + obj_id, objt_type, substation_id, nm_ = self._obj_caract_from_topo_id( + id_, with_name=True ) sub_id = "{}".format(substation_id) if not sub_id in res["change_bus_vect"]: res["change_bus_vect"][sub_id] = {} - res["change_bus_vect"][sub_id]["{}_{}".format(objt_type, obj_id)] = { + res["change_bus_vect"][sub_id][nm_] = { "type": objt_type, "id": obj_id, } @@ -3325,13 +3363,13 @@ def as_dict(self) -> dict: all_subs = set() for id_, k in enumerate(self._set_topo_vect): if k != 0: - obj_id, objt_type, substation_id = self._obj_caract_from_topo_id( - id_ + obj_id, objt_type, substation_id, nm_ = self._obj_caract_from_topo_id( + id_, with_name=True ) sub_id = "{}".format(substation_id) if not sub_id in res["set_bus_vect"]: res["set_bus_vect"][sub_id] = {} - res["set_bus_vect"][sub_id]["{}_{}".format(objt_type, obj_id)] = { + res["set_bus_vect"][sub_id][nm_] = { "type": objt_type, "id": obj_id, "new_bus": k, @@ -3357,7 +3395,17 @@ def as_dict(self) -> dict: if self._modif_curtailment: res["curtailment"] = 1.0 * self._curtail - + + if type(self).shunts_data_available: + tmp = {} + if np.any(np.isfinite(self.shunt_p)): + tmp["shunt_p"] = 1.0 * self.shunt_p + if np.any(np.isfinite(self.shunt_q)): + tmp["shunt_q"] = 1.0 * self.shunt_q + if np.any(self.shunt_bus != 0): + tmp["shunt_bus"] = 1.0 * self.shunt_bus + if tmp: + res["shunt"] = tmp return res def get_types(self) -> Tuple[bool, bool, bool, bool, bool, bool, bool]: @@ -3496,9 +3544,10 @@ def _aux_effect_on_storage(self, storage_id): return res def _aux_effect_on_substation(self, substation_id): - if substation_id >= self.n_sub: + cls = type(self) + if substation_id >= cls.n_sub: raise Grid2OpException( - f"There are only {self.n_sub} substations on the grid. " + f"There are only {cls.n_sub} substations on the grid. " f"Cannot check impact on " f"`substation_id={substation_id}`" ) @@ -3506,8 +3555,8 @@ def _aux_effect_on_substation(self, substation_id): raise Grid2OpException(f"`substation_id` should be positive.") res = {} - beg_ = int(self.sub_info[:substation_id].sum()) - end_ = int(beg_ + self.sub_info[substation_id]) + beg_ = int(cls.sub_info[:substation_id].sum()) + end_ = int(beg_ + cls.sub_info[substation_id]) res["change_bus"] = self._change_bus_vect[beg_:end_] res["set_bus"] = self._set_topo_vect[beg_:end_] return res @@ -3678,10 +3727,11 @@ def get_storage_modif(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: New bus of the storage units, affected with "change_bus" command """ + cls = type(self) storage_power = 1.0 * self._storage_power - storage_set_bus = 1 * self._set_topo_vect[self.storage_pos_topo_vect] + storage_set_bus = 1 * self._set_topo_vect[cls.storage_pos_topo_vect] storage_change_bus = copy.deepcopy( - self._change_bus_vect[self.storage_pos_topo_vect] + self._change_bus_vect[cls.storage_pos_topo_vect] ) return storage_power, storage_set_bus, storage_change_bus @@ -3700,14 +3750,15 @@ def get_load_modif(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray load_change_bus: ``np.ndarray`` New bus of the loads, affected with "change_bus" command """ - load_p = np.full(self.n_load, fill_value=np.NaN, dtype=dt_float) + cls = type(self) + load_p = np.full(cls.n_load, fill_value=np.NaN, dtype=dt_float) if "load_p" in self._dict_inj: load_p[:] = self._dict_inj["load_p"] load_q = 1.0 * load_p if "load_q" in self._dict_inj: load_q[:] = self._dict_inj["load_q"] - load_set_bus = 1 * self._set_topo_vect[self.load_pos_topo_vect] - load_change_bus = copy.deepcopy(self._change_bus_vect[self.load_pos_topo_vect]) + load_set_bus = 1 * self._set_topo_vect[cls.load_pos_topo_vect] + load_change_bus = copy.deepcopy(self._change_bus_vect[cls.load_pos_topo_vect]) return load_p, load_q, load_set_bus, load_change_bus def get_gen_modif(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: @@ -3728,14 +3779,15 @@ def get_gen_modif(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray] New bus of the generators, affected with "change_bus" command """ - gen_p = np.full(self.n_load, fill_value=np.NaN, dtype=dt_float) + cls = type(self) + gen_p = np.full(cls.n_gen, fill_value=np.NaN, dtype=dt_float) if "prod_p" in self._dict_inj: gen_p[:] = self._dict_inj["prod_p"] gen_v = 1.0 * gen_p if "prod_v" in self._dict_inj: gen_v[:] = self._dict_inj["prod_v"] - gen_set_bus = 1 * self._set_topo_vect[self.gen_pos_topo_vect] - gen_change_bus = copy.deepcopy(self._change_bus_vect[self.gen_pos_topo_vect]) + gen_set_bus = 1 * self._set_topo_vect[cls.gen_pos_topo_vect] + gen_change_bus = copy.deepcopy(self._change_bus_vect[cls.gen_pos_topo_vect]) return gen_p, gen_v, gen_set_bus, gen_change_bus # TODO do the get_line_modif, get_line_or_modif and get_line_ex_modif @@ -3925,9 +3977,35 @@ def _aux_affect_object_int( @property def load_set_bus(self) -> np.ndarray: """ - Allows to retrieve (and affect) the busbars at which each storage unit is **set**. + Allows to retrieve (and affect) the busbars at which the action **set** the loads. + + .. versionchanged:: 1.9.9 + From grid2op version 1.9.9 it is possible (under some cirumstances, depending on how + the environment is created) to set the busbar to a number >= 3, depending on the value + of `type(act).n_busbar_per_sub`. + + Returns + ------- + res: + A vector of integer, of size `act.n_gen` indicating what type of action is performed for + each load units with the convention : + + * 0 the action do not action on this load + * -1 the action disconnect the load + * 1 the action set the load to busbar 1 + * 2 the action set the load to busbar 2 + * 3 the action set the load to busbar 3 (grid2op >= 1.9.9) + * etc. (grid2op >= 1.9.9) + + Examples + -------- + + Please refer to the documentation of :attr:`BaseAction.gen_set_bus` for more information. + + .. 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. - It behaves similarly as :attr:`BaseAction.gen_set_bus`. See the help there for more information. """ res = self.set_bus[self.load_pos_topo_vect] res.flags.writeable = False @@ -3935,7 +4013,8 @@ def load_set_bus(self) -> np.ndarray: @load_set_bus.setter def load_set_bus(self, values): - if "set_bus" not in self.authorized_keys: + cls = type(self) + if "set_bus" not in cls.authorized_keys: raise IllegalAction( 'Impossible to modify the load bus (with "set") with this action type.' ) @@ -3944,20 +4023,22 @@ def load_set_bus(self, values): self._aux_affect_object_int( 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._set_topo_vect, + max_val=cls.n_busbar_per_sub ) self._modif_set_bus = True except Exception as exc_: self._aux_affect_object_int( orig_, "load", - self.n_load, - self.name_load, - self.load_pos_topo_vect, + cls.n_load, + cls.name_load, + cls.load_pos_topo_vect, self._set_topo_vect, + max_val=cls.n_busbar_per_sub ) raise IllegalAction( f"Impossible to modify the load bus with your input. Please consult the documentation. " @@ -3969,21 +4050,28 @@ def gen_set_bus(self) -> np.ndarray: """ Allows to retrieve (and affect) the busbars at which the action **set** the generator units. + .. versionchanged:: 1.9.9 + From grid2op version 1.9.9 it is possible (under some cirumstances, depending on how + the environment is created) to set the busbar to a number >= 3, depending on the value + of `type(act).n_busbar_per_sub`. + Returns ------- res: A vector of integer, of size `act.n_gen` indicating what type of action is performed for each generator units with the convention : - * 0 the action do not action on this storage unit - * -1 the action disconnect the storage unit - * 1 the action set the storage unit to busbar 1 - * 2 the action set the storage unit to busbar 2 + * 0 the action do not action on this generator + * -1 the action disconnect the generator + * 1 the action set the generator to busbar 1 + * 2 the action set the generator to busbar 2 + * 3 the action set the generator to busbar 3 (grid2op >= 1.9.9) + * etc. (grid2op >= 1.9.9) Examples -------- - To retrieve the impact of the action on the storage unit, you can do: + To retrieve the impact of the action on the generator, you can do: .. code-block:: python @@ -4054,7 +4142,8 @@ def gen_set_bus(self) -> np.ndarray: act.gen_set_bus[1] = 2 # 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 + .. 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. """ @@ -4064,7 +4153,8 @@ def gen_set_bus(self) -> np.ndarray: @gen_set_bus.setter def gen_set_bus(self, values): - if "set_bus" not in self.authorized_keys: + cls = type(self) + if "set_bus" not in cls.authorized_keys: raise IllegalAction( 'Impossible to modify the gen bus (with "set") with this action type.' ) @@ -4073,20 +4163,22 @@ def gen_set_bus(self, values): self._aux_affect_object_int( 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._set_topo_vect, + max_val=cls.n_busbar_per_sub ) self._modif_set_bus = True except Exception as exc_: self._aux_affect_object_int( orig_, "gen", - self.n_gen, - self.name_gen, - self.gen_pos_topo_vect, + cls.n_gen, + cls.name_gen, + cls.gen_pos_topo_vect, self._set_topo_vect, + max_val=cls.n_busbar_per_sub ) raise IllegalAction( f"Impossible to modify the gen bus with your input. Please consult the documentation. " @@ -4096,9 +4188,35 @@ def gen_set_bus(self, values): @property def storage_set_bus(self) -> np.ndarray: """ - Allows to retrieve (and affect) the busbars at which each storage unit is **set**. + Allows to retrieve (and affect) the busbars at which the action **set** the storage units. + + .. versionchanged:: 1.9.9 + From grid2op version 1.9.9 it is possible (under some cirumstances, depending on how + the environment is created) to set the busbar to a number >= 3, depending on the value + of `type(act).n_busbar_per_sub`. + + Returns + ------- + res: + A vector of integer, of size `act.n_gen` indicating what type of action is performed for + each storage unit with the convention : + + * 0 the action do not action on this storage unit + * -1 the action disconnect the storage unit + * 1 the action set the storage unit to busbar 1 + * 2 the action set the storage unit to busbar 2 + * 3 the action set the storage unit to busbar 3 (grid2op >= 1.9.9) + * etc. (grid2op >= 1.9.9) + + Examples + -------- + + Please refer to the documentation of :attr:`BaseAction.gen_set_bus` for more information. + + .. 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. - It behaves similarly as :attr:`BaseAction.gen_set_bus`. See the help there for more information. """ if "set_storage" not in self.authorized_keys: raise IllegalAction(type(self).ERR_NO_STOR_SET_BUS) @@ -4108,29 +4226,32 @@ def storage_set_bus(self) -> np.ndarray: @storage_set_bus.setter def storage_set_bus(self, values): - if "set_bus" not in self.authorized_keys: - raise IllegalAction(type(self).ERR_NO_STOR_SET_BUS) - if "set_storage" not in self.authorized_keys: - raise IllegalAction(type(self).ERR_NO_STOR_SET_BUS) + cls = type(self) + if "set_bus" not in cls.authorized_keys: + raise IllegalAction(cls.ERR_NO_STOR_SET_BUS) + if "set_storage" not in cls.authorized_keys: + raise IllegalAction(cls.ERR_NO_STOR_SET_BUS) orig_ = self.storage_set_bus try: self._aux_affect_object_int( 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._set_topo_vect, + max_val=cls.n_busbar_per_sub ) self._modif_set_bus = True except Exception as exc_: self._aux_affect_object_int( orig_, "storage", - self.n_storage, - self.name_storage, - self.storage_pos_topo_vect, + cls.n_storage, + cls.name_storage, + cls.storage_pos_topo_vect, self._set_topo_vect, + max_val=cls.n_busbar_per_sub ) raise IllegalAction( f"Impossible to modify the storage bus with your input. " @@ -4141,9 +4262,35 @@ def storage_set_bus(self, values): @property def line_or_set_bus(self) -> np.ndarray: """ - Allows to retrieve (and affect) the busbars at which the origin side of each powerline is **set**. + Allows to retrieve (and affect) the busbars at which the action **set** the lines (origin side). + + .. versionchanged:: 1.9.9 + From grid2op version 1.9.9 it is possible (under some cirumstances, depending on how + the environment is created) to set the busbar to a number >= 3, depending on the value + of `type(act).n_busbar_per_sub`. + + Returns + ------- + res: + A vector of integer, of size `act.n_gen` indicating what type of action is performed for + each lines (origin side) with the convention : + + * 0 the action do not action on this line (origin side) + * -1 the action disconnect the line (origin side) + * 1 the action set the line (origin side) to busbar 1 + * 2 the action set the line (origin side) to busbar 2 + * 3 the action set the line (origin side) to busbar 3 (grid2op >= 1.9.9) + * etc. + + Examples + -------- + + Please refer to the documentation of :attr:`BaseAction.gen_set_bus` for more information. + + .. 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. - It behaves similarly as :attr:`BaseAction.gen_set_bus`. See the help there for more information. """ res = self.set_bus[self.line_or_pos_topo_vect] res.flags.writeable = False @@ -4151,7 +4298,8 @@ def line_or_set_bus(self) -> np.ndarray: @line_or_set_bus.setter def line_or_set_bus(self, values): - if "set_bus" not in self.authorized_keys: + cls = type(self) + if "set_bus" not in cls.authorized_keys: raise IllegalAction( 'Impossible to modify the line (origin) bus (with "set") with this action type.' ) @@ -4164,16 +4312,18 @@ def line_or_set_bus(self, values): self.name_line, self.line_or_pos_topo_vect, self._set_topo_vect, + max_val=cls.n_busbar_per_sub ) self._modif_set_bus = True except Exception as exc_: self._aux_affect_object_int( orig_, 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._set_topo_vect, + max_val=cls.n_busbar_per_sub ) raise IllegalAction( f"Impossible to modify the line origin bus with your input. " @@ -4194,7 +4344,8 @@ def line_ex_set_bus(self) -> np.ndarray: @line_ex_set_bus.setter def line_ex_set_bus(self, values): - if "set_bus" not in self.authorized_keys: + cls = type(self) + if "set_bus" not in cls.authorized_keys: raise IllegalAction( 'Impossible to modify the line (ex) bus (with "set") with this action type.' ) @@ -4203,20 +4354,22 @@ def line_ex_set_bus(self, values): self._aux_affect_object_int( 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._set_topo_vect, + max_val=cls.n_busbar_per_sub ) self._modif_set_bus = True except Exception as exc_: self._aux_affect_object_int( orig_, 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._set_topo_vect, + max_val=cls.n_busbar_per_sub ) raise IllegalAction( f"Impossible to modify the line extrmity bus with your input. " @@ -4271,7 +4424,8 @@ def set_bus(self) -> np.ndarray: @set_bus.setter def set_bus(self, values): - if "set_bus" not in self.authorized_keys: + cls = type(self) + if "set_bus" not in cls.authorized_keys: raise IllegalAction( 'Impossible to modify the bus (with "set") with this action type.' ) @@ -4280,20 +4434,22 @@ def set_bus(self, values): self._aux_affect_object_int( values, "", - self.dim_topo, + cls.dim_topo, None, - np.arange(self.dim_topo), + np.arange(cls.dim_topo), self._set_topo_vect, + max_val=cls.n_busbar_per_sub ) self._modif_set_bus = True except Exception as exc_: self._aux_affect_object_int( orig_, "", - self.dim_topo, + cls.dim_topo, None, - np.arange(self.dim_topo), + np.arange(cls.dim_topo), self._set_topo_vect, + max_val=cls.n_busbar_per_sub ) raise IllegalAction( f"Impossible to modify the bus with your input. " @@ -5475,7 +5631,7 @@ def _aux_aux_convert_and_check_np_array(self, array_): f"Impossible to set element to bus {np.min(array_)}. Buses must be " f"-1, 0, 1 or 2." ) - if (array_ > 2).any(): + if (array_ > type(self).n_busbar_per_sub).any(): raise IllegalAction( f"Impossible to set element to bus {np.max(array_)}. Buses must be " f"-1, 0, 1 or 2." @@ -5483,21 +5639,22 @@ def _aux_aux_convert_and_check_np_array(self, array_): return array_ def _aux_set_bus_sub(self, values): + cls = type(self) if isinstance(values, (bool, dt_bool)): raise IllegalAction( "Impossible to modify bus by substation with a single bool." ) - elif isinstance(values, (int, dt_int, np.int64)): + elif isinstance(values, (int, dt_int, np.int64, np.int32)): raise IllegalAction( "Impossible to modify bus by substation with a single integer." ) - elif isinstance(values, (float, dt_float, np.float64)): + elif isinstance(values, (float, dt_float, np.float64, np.float32)): raise IllegalAction( "Impossible to modify bus by substation with a single float." ) elif isinstance(values, np.ndarray): # full topo vect - if values.shape[0] != self.dim_topo: + if values.shape[0] != cls.dim_topo: raise IllegalAction( "Impossible to modify bus when providing a full topology vector " "that has not the right " @@ -5513,11 +5670,11 @@ def _aux_set_bus_sub(self, values): # should be a tuple (sub_id, new_topo) sub_id, topo_repr, nb_el = self._check_for_right_vectors_sub(values) topo_repr = self._aux_aux_convert_and_check_np_array(topo_repr) - start_ = self.sub_info[:sub_id].sum() + start_ = cls.sub_info[:sub_id].sum() end_ = start_ + nb_el self._set_topo_vect[start_:end_] = topo_repr elif isinstance(values, list): - if len(values) == self.dim_topo: + if len(values) == cls.dim_topo: # if list is the size of the full topo vect, it's a list representing it values = self._aux_aux_convert_and_check_np_array(values) self._aux_set_bus_sub(values) diff --git a/grid2op/Backend/educPandaPowerBackend.py b/grid2op/Backend/educPandaPowerBackend.py index 6caf2f039..ec045736d 100644 --- a/grid2op/Backend/educPandaPowerBackend.py +++ b/grid2op/Backend/educPandaPowerBackend.py @@ -131,7 +131,8 @@ def load_grid(self, example. (But of course you can still use switches if you really want to) """ - + self.cannot_handle_more_than_2_busbar() + # first, handles different kind of path: full_path = self.make_complete_path(path, filename) diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index 824bee029..4c4869437 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -18,10 +18,11 @@ import pandapower as pp import scipy +import grid2op from grid2op.dtypes import dt_int, dt_float, dt_bool -from grid2op.Backend.backend import Backend from grid2op.Action import BaseAction from grid2op.Exceptions import BackendError +from grid2op.Backend.backend import Backend try: import numba @@ -337,7 +338,7 @@ def load_grid(self, are set as "out of service" unless a topological action acts on these specific substations. """ - self.cannot_handle_more_than_2_busbar() + self.can_handle_more_than_2_busbar() full_path = self.make_complete_path(path, filename) with warnings.catch_warnings(): @@ -557,12 +558,11 @@ def load_grid(self, # "hack" to handle topological changes, for now only 2 buses per substation add_topo = copy.deepcopy(self._grid.bus) - add_topo.index += add_topo.shape[0] - add_topo["in_service"] = False - # self._grid.bus = pd.concat((self._grid.bus, add_topo)) - for ind, el in add_topo.iterrows(): - pp.create_bus(self._grid, index=ind, **el) - + for busbar_supp in range(self.n_busbar_per_sub - 1): # self.n_busbar_per_sub and not type(self) here otherwise it erases can_handle_more_than_2_busbar / cannot_handle_more_than_2_busbar + add_topo.index += add_topo.shape[0] + add_topo["in_service"] = False + for ind, el in add_topo.iterrows(): + pp.create_bus(self._grid, index=ind, **el) self._init_private_attrs() def _init_private_attrs(self) -> None: @@ -814,6 +814,8 @@ def apply_action(self, backendAction: Union["grid2op.Action._backendAction._Back """ if backendAction is None: return + from grid2op.Action._backendAction import _BackendAction + backendAction : _BackendAction = backendAction cls = type(self) @@ -825,11 +827,9 @@ def apply_action(self, backendAction: Union["grid2op.Action._backendAction._Back ) = backendAction() # handle bus status - bus_is = self._grid.bus["in_service"] - for i, (bus1_status, bus2_status) in enumerate(active_bus): - bus_is[i] = bus1_status # no iloc for bus, don't ask me why please :-/ - bus_is[i + self.__nb_bus_before] = bus2_status - + self._grid.bus["in_service"] = active_bus.T.reshape(-1) + + # handle generators tmp_prod_p = self._get_vector_inj["prod_p"](self._grid) if (prod_p.changed).any(): tmp_prod_p.iloc[prod_p.changed] = prod_p.values[prod_p.changed] @@ -852,7 +852,7 @@ def apply_action(self, backendAction: Union["grid2op.Action._backendAction._Back if (load_q.changed).any(): tmp_load_q.iloc[load_q.changed] = load_q.values[load_q.changed] - if self.n_storage > 0: + if cls.n_storage > 0: # active setpoint tmp_stor_p = self._grid.storage["p_mw"] if (storage.changed).any(): @@ -862,18 +862,19 @@ def apply_action(self, backendAction: Union["grid2op.Action._backendAction._Back stor_bus = backendAction.get_storages_bus() new_bus_id = stor_bus.values[stor_bus.changed] # id of the busbar 1 or 2 if activated = new_bus_id > 0 # mask of storage that have been activated - new_bus_num = ( - self.storage_to_subid[stor_bus.changed] + (new_bus_id - 1) * self.n_sub - ) # bus number - new_bus_num[~activated] = self.storage_to_subid[stor_bus.changed][ - ~activated - ] - self._grid.storage["in_service"].values[stor_bus.changed] = activated - self._grid.storage["bus"].values[stor_bus.changed] = new_bus_num - self._topo_vect[self.storage_pos_topo_vect[stor_bus.changed]] = new_bus_num - self._topo_vect[ - self.storage_pos_topo_vect[stor_bus.changed][~activated] - ] = -1 + # new_bus_num = ( + # cls.storage_to_subid[stor_bus.changed] + (new_bus_id - 1) * cls.n_sub + # ) # bus number + # new_bus_num[~activated] = cls.storage_to_subid[stor_bus.changed][ + # ~activated + # ] + # self._grid.storage["in_service"].values[stor_bus.changed] = activated + # self._grid.storage["bus"].values[stor_bus.changed] = new_bus_num + # self._topo_vect[cls.storage_pos_topo_vect[stor_bus.changed]] = new_bus_num + # self._topo_vect[ + # cls.storage_pos_topo_vect[stor_bus.changed][~activated] + # ] = -1 + new_bus_num = cls.local_bus_to_global(cls.storage_pos_topo_vect[stor_bus.changed], cls.storage_to_subid[stor_bus.changed]) if type(backendAction).shunts_data_available: shunt_p, shunt_q, shunt_bus = shunts__ diff --git a/grid2op/Observation/baseObservation.py b/grid2op/Observation/baseObservation.py index c7c6484a5..c988117bf 100644 --- a/grid2op/Observation/baseObservation.py +++ b/grid2op/Observation/baseObservation.py @@ -1028,7 +1028,7 @@ def process_shunt_satic_data(cls): @classmethod def process_grid2op_compat(cls): - GridObjects.process_grid2op_compat(cls) + super().process_grid2op_compat() glop_ver = cls._get_grid2op_version_as_version_obj() if cls.glop_version == cls.BEFORE_COMPAT_VERSION: diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index a991bfd1b..234391dee 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -189,6 +189,12 @@ class GridObjects: Attributes ---------- + n_busbar_per_sub: :class:`int` + number of independant busbars for all substations [*class attribute*]. It's 2 by default + or if the implementation of the backend does not support this feature. + + .. versionadded:: 1.9.9 + n_line: :class:`int` number of powerlines in the powergrid [*class attribute*] @@ -2756,8 +2762,7 @@ def init_grid(cls, gridobj, force=False, extra_name=None, force_module=None): res_cls._compute_pos_big_topo_cls() res_cls.process_shunt_satic_data() - if res_cls.glop_version != grid2op.__version__: - res_cls.process_grid2op_compat() + res_cls.process_grid2op_compat() if force_module is not None: res_cls.__module__ = force_module # hack because otherwise it says "abc" which is not the case @@ -3204,7 +3209,7 @@ def get_storages_id(self, sub_id): if not res: # res is empty here raise BackendError( - "GridObjects.bd: impossible to find a storage unit connected at substation {}".format( + "GridObjects.get_storages_id: impossible to find a storage unit connected at substation {}".format( sub_id ) ) @@ -3216,6 +3221,8 @@ def _make_cls_dict(cls, res, as_list=True, copy_=True): """NB: `cls` can be here a class or an object of a class...""" save_to_dict(res, cls, "glop_version", str, copy_) res["_PATH_ENV"] = cls._PATH_ENV # i do that manually for more control + res["n_busbar_per_sub"] = f"{cls.n_busbar_per_sub}" + save_to_dict( res, cls, @@ -3598,7 +3605,7 @@ class res(GridObjects): cls = res if "glop_version" in dict_: - cls.glop_version = dict_["glop_version"] + cls.glop_version = str(dict_["glop_version"]) else: cls.glop_version = cls.BEFORE_COMPAT_VERSION @@ -3606,6 +3613,8 @@ class res(GridObjects): cls._PATH_ENV = str(dict_["_PATH_ENV"]) else: cls._PATH_ENV = None + + cls.n_busbar_per_sub = int(dict_["n_busbar_per_sub"]) cls.name_gen = extract_from_dict( dict_, "name_gen", lambda x: np.array(x).astype(str) diff --git a/grid2op/tests/test_n_busbar_per_sub.py b/grid2op/tests/test_n_busbar_per_sub.py index 2199ce387..cb6320e75 100644 --- a/grid2op/tests/test_n_busbar_per_sub.py +++ b/grid2op/tests/test_n_busbar_per_sub.py @@ -18,9 +18,9 @@ from grid2op.Runner import Runner from grid2op.Backend import PandaPowerBackend from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB -from grid2op.Action import ActionSpace, BaseAction +from grid2op.Action import ActionSpace, BaseAction, CompleteAction from grid2op.Observation import BaseObservation -from grid2op.Exceptions import Grid2OpException, EnvError +from grid2op.Exceptions import Grid2OpException, EnvError, IllegalAction import pdb @@ -345,7 +345,6 @@ def test_global_bus_to_local(self): vect[gen_on_3] = 3 assert (res == vect).all() - def test_local_bus_to_global_int(self): cls_env = type(self.env) # easy case: everything on bus 1 @@ -407,6 +406,173 @@ def test_local_bus_to_global(self): assert res[gen_on_2] == cls_env.gen_to_subid[gen_on_2] + cls_env.n_sub assert res[gen_on_3] == cls_env.gen_to_subid[gen_on_3] + 2 * cls_env.n_sub + +class TestAction_3busbars(unittest.TestCase): + def setUp(self) -> None: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make("educ_case14_storage", + backend=_AuxFakeBackendSupport(), + action_class=CompleteAction, + test=True, + n_busbar=3, + _add_to_name=type(self).__name__) + return super().setUp() + + def tearDown(self) -> None: + self.env.close() + return super().tearDown() + + def _aux_test_act_consistent_as_dict(self, act_as_dict, name_xxx, el_id, bus_val): + if name_xxx is not None: + # regular element in the topo_vect + assert "set_bus_vect" in act_as_dict + tmp = act_as_dict["set_bus_vect"] + assert len(tmp['modif_subs_id']) == 1 + sub_id = tmp['modif_subs_id'][0] + assert name_xxx[el_id] in tmp[sub_id] + assert tmp[sub_id][name_xxx[el_id]]["new_bus"] == bus_val + else: + # el not in topo vect (eg shunt) + assert "shunt" in act_as_dict + tmp = act_as_dict["shunt"]["shunt_bus"] + assert tmp[el_id] == bus_val + + def _aux_test_act_consistent_as_serializable_dict(self, act_as_dict, el_nms, el_id, bus_val): + if el_nms is not None: + # regular element + assert "set_bus" in act_as_dict + assert el_nms in act_as_dict["set_bus"] + tmp = act_as_dict["set_bus"][el_nms] + assert tmp == [(el_id, bus_val)] + else: + # shunts of other things not in the topo vect + assert "shunt" in act_as_dict + tmp = act_as_dict["shunt"]["shunt_bus"] + assert tmp == [(el_id, bus_val)] + + def _aux_test_action(self, act : BaseAction, name_xxx, el_id, bus_val, el_nms): + assert act.can_affect_something() + assert not act.is_ambiguous()[0] + tmp = f"{act}" # test the print does not crash + tmp = act.as_dict() # test I can convert to dict + self._aux_test_act_consistent_as_dict(tmp, name_xxx, el_id, bus_val) + tmp = act.as_serializable_dict() # test I can convert to another type of dict + self._aux_test_act_consistent_as_serializable_dict(tmp, el_nms, el_id, bus_val) + + def _aux_test_set_bus_onebus(self, nm_prop, el_id, bus_val, name_xxx, el_nms): + act = self.env.action_space() + setattr(act, nm_prop, [(el_id, bus_val)]) + self._aux_test_action(act, name_xxx, el_id, bus_val, el_nms) + + def test_set_load_bus(self): + self._aux_test_set_bus_onebus("load_set_bus", 0, -1, type(self.env).name_load, 'loads_id') + for bus in range(type(self.env).n_busbar_per_sub): + self._aux_test_set_bus_onebus("load_set_bus", 0, bus + 1, type(self.env).name_load, 'loads_id') + act = self.env.action_space() + with self.assertRaises(IllegalAction): + act.load_set_bus = [(0, type(self.env).n_busbar_per_sub + 1)] + + def test_set_gen_bus(self): + self._aux_test_set_bus_onebus("gen_set_bus", 0, -1, type(self.env).name_gen, 'generators_id') + for bus in range(type(self.env).n_busbar_per_sub): + self._aux_test_set_bus_onebus("gen_set_bus", 0, bus + 1, type(self.env).name_gen, 'generators_id') + act = self.env.action_space() + with self.assertRaises(IllegalAction): + act.gen_set_bus = [(0, type(self.env).n_busbar_per_sub + 1)] + + def test_set_storage_bus(self): + self._aux_test_set_bus_onebus("storage_set_bus", 0, -1, type(self.env).name_storage, 'storages_id') + for bus in range(type(self.env).n_busbar_per_sub): + self._aux_test_set_bus_onebus("storage_set_bus", 0, bus + 1, type(self.env).name_storage, 'storages_id') + act = self.env.action_space() + with self.assertRaises(IllegalAction): + act.storage_set_bus = [(0, type(self.env).n_busbar_per_sub + 1)] + + def test_set_lineor_bus(self): + self._aux_test_set_bus_onebus("line_or_set_bus", 0, -1, type(self.env).name_line, 'lines_or_id') + for bus in range(type(self.env).n_busbar_per_sub): + self._aux_test_set_bus_onebus("line_or_set_bus", 0, bus + 1, type(self.env).name_line, 'lines_or_id') + act = self.env.action_space() + with self.assertRaises(IllegalAction): + act.line_or_set_bus = [(0, type(self.env).n_busbar_per_sub + 1)] + + def test_set_lineex_bus(self): + self._aux_test_set_bus_onebus("line_ex_set_bus", 0, -1, type(self.env).name_line, 'lines_ex_id') + for bus in range(type(self.env).n_busbar_per_sub): + self._aux_test_set_bus_onebus("line_ex_set_bus", 0, bus + 1, type(self.env).name_line, 'lines_ex_id') + act = self.env.action_space() + with self.assertRaises(IllegalAction): + act.line_ex_set_bus = [(0, type(self.env).n_busbar_per_sub + 1)] + + def _aux_test_set_bus_onebus_sub_setbus(self, nm_prop, sub_id, el_id_sub, bus_val, name_xxx, el_nms): + # for now works only with lines_ex (in other words, the name_xxx and name_xxx should be + # provided by the user and it's probably not a good idea to use something + # else than type(self.env).name_line and lines_ex_id + act = self.env.action_space() + buses_val = np.zeros(type(self.env).sub_info[sub_id], dtype=int) + buses_val[el_id_sub] = bus_val + setattr(act, nm_prop, [(sub_id, buses_val)]) + el_id_in_topo_vect = np.where(act._set_topo_vect == bus_val)[0][0] + el_type = np.where(type(self.env).grid_objects_types[el_id_in_topo_vect][1:] != -1)[0][0] + el_id = type(self.env).grid_objects_types[el_id_in_topo_vect][el_type + 1] + self._aux_test_action(act, name_xxx, el_id, bus_val, el_nms) + + def test_sub_set_bus(self): + self._aux_test_set_bus_onebus_sub_setbus("sub_set_bus", 1, 0, -1, type(self.env).name_line, 'lines_ex_id') + for bus in range(type(self.env).n_busbar_per_sub): + self._aux_test_set_bus_onebus_sub_setbus("sub_set_bus", 1, 0, bus + 1, type(self.env).name_line, 'lines_ex_id') + act = self.env.action_space() + with self.assertRaises(IllegalAction): + act.line_ex_set_bus = [(0, type(self.env).n_busbar_per_sub + 1)] + + def test_change_deactivated(self): + assert "set_bus" in type(self.env.action_space()).authorized_keys + assert self.env.action_space.supports_type("set_bus") + + assert "change_bus" not in type(self.env.action_space()).authorized_keys + assert not self.env.action_space.supports_type("change_bus") + + def test_shunt(self): + el_id = 0 + bus_val = -1 + name_xxx = None + el_nms = None + + act = self.env.action_space({"shunt": {"set_bus": [(0, -1)]}}) + # self._aux_test_action(act, type(self.env).name_shunt, el_id, bus_val, None) # does not work for a lot of reasons + assert not act.is_ambiguous()[0] + tmp = f"{act}" # test the print does not crash + tmp = act.as_dict() # test I can convert to dict + self._aux_test_act_consistent_as_dict(tmp, name_xxx, el_id, bus_val) + tmp = act.as_serializable_dict() # test I can convert to another type of dict + self._aux_test_act_consistent_as_serializable_dict(tmp, el_nms, el_id, bus_val) + + +class TestAction_1busbar(TestAction_3busbars): + def setUp(self) -> None: + super().setUp() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make("educ_case14_storage", + backend=_AuxFakeBackendSupport(), + action_class=CompleteAction, + test=True, + n_busbar=1, + _add_to_name=type(self).__name__) + + +class TestActionSpace(unittest.TestCase): + pass + + +class TestBackendAction(unittest.TestCase): + pass + +class TestPandapowerBackend(unittest.TestCase): + pass + + if __name__ == "__main__": unittest.main() \ No newline at end of file From cffcc8485dbf68dc51d890f15443f245347472e7 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Mon, 29 Jan 2024 16:39:09 +0100 Subject: [PATCH 05/24] fixing some broken tests --- grid2op/Backend/pandaPowerBackend.py | 3 ++- grid2op/Space/GridObjects.py | 2 +- grid2op/tests/test_Action.py | 2 ++ grid2op/tests/test_Observation.py | 3 ++- grid2op/tests/test_n_busbar_per_sub.py | 17 ++++++----------- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index 4c4869437..de3b581dd 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -827,7 +827,7 @@ def apply_action(self, backendAction: Union["grid2op.Action._backendAction._Back ) = backendAction() # handle bus status - self._grid.bus["in_service"] = active_bus.T.reshape(-1) + self._grid.bus["in_service"] = pd.Series(data=active_bus.T.reshape(-1), index=np.arange(cls.n_sub * cls.n_busbar_per_sub)) # handle generators tmp_prod_p = self._get_vector_inj["prod_p"](self._grid) @@ -875,6 +875,7 @@ def apply_action(self, backendAction: Union["grid2op.Action._backendAction._Back # cls.storage_pos_topo_vect[stor_bus.changed][~activated] # ] = -1 new_bus_num = cls.local_bus_to_global(cls.storage_pos_topo_vect[stor_bus.changed], cls.storage_to_subid[stor_bus.changed]) + # TODO n_busbar_per_sub if type(backendAction).shunts_data_available: shunt_p, shunt_q, shunt_bus = shunts__ diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 234391dee..923bf1b12 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -3221,7 +3221,7 @@ def _make_cls_dict(cls, res, as_list=True, copy_=True): """NB: `cls` can be here a class or an object of a class...""" save_to_dict(res, cls, "glop_version", str, copy_) res["_PATH_ENV"] = cls._PATH_ENV # i do that manually for more control - res["n_busbar_per_sub"] = f"{cls.n_busbar_per_sub}" + save_to_dict(res, cls, "n_busbar_per_sub", str, copy_) save_to_dict( res, diff --git a/grid2op/tests/test_Action.py b/grid2op/tests/test_Action.py index 5de72f7b9..918cc47a0 100644 --- a/grid2op/tests/test_Action.py +++ b/grid2op/tests/test_Action.py @@ -29,6 +29,7 @@ def _get_action_grid_class(): GridObjects.env_name = "test_action_env" + GridObjects.n_busbar_per_sub = 2 GridObjects.n_gen = 5 GridObjects.name_gen = np.array(["gen_{}".format(i) for i in range(5)]) GridObjects.n_load = 11 @@ -104,6 +105,7 @@ def _get_action_grid_class(): json_ = { "glop_version": grid2op.__version__, + "n_busbar_per_sub": "2", "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 7019c87fc..1742ae4e3 100644 --- a/grid2op/tests/test_Observation.py +++ b/grid2op/tests/test_Observation.py @@ -52,6 +52,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", "name_load": [ "load_1_0", "load_2_1", @@ -1785,7 +1786,7 @@ def aux_test_conn_mat2(self, as_csr=False): obs, reward, done, info = self.env.step( self.env.action_space({"set_bus": {"lines_or_id": [(13, 2), (14, 2)]}}) ) - assert not done + assert not done, f"failed with error {info['exception']}" assert obs.bus_connectivity_matrix(as_csr).shape == (15, 15) assert ( obs.bus_connectivity_matrix(as_csr)[14, 11] == 1.0 diff --git a/grid2op/tests/test_n_busbar_per_sub.py b/grid2op/tests/test_n_busbar_per_sub.py index cb6320e75..1dea3dcce 100644 --- a/grid2op/tests/test_n_busbar_per_sub.py +++ b/grid2op/tests/test_n_busbar_per_sub.py @@ -408,6 +408,9 @@ def test_local_bus_to_global(self): class TestAction_3busbars(unittest.TestCase): + def get_nb_bus(self): + return 3 + def setUp(self) -> None: with warnings.catch_warnings(): warnings.filterwarnings("ignore") @@ -415,7 +418,7 @@ def setUp(self) -> None: backend=_AuxFakeBackendSupport(), action_class=CompleteAction, test=True, - n_busbar=3, + n_busbar=self.get_nb_bus(), _add_to_name=type(self).__name__) return super().setUp() @@ -550,16 +553,8 @@ def test_shunt(self): class TestAction_1busbar(TestAction_3busbars): - def setUp(self) -> None: - super().setUp() - with warnings.catch_warnings(): - warnings.filterwarnings("ignore") - self.env = grid2op.make("educ_case14_storage", - backend=_AuxFakeBackendSupport(), - action_class=CompleteAction, - test=True, - n_busbar=1, - _add_to_name=type(self).__name__) + def get_nb_bus(self): + return 1 class TestActionSpace(unittest.TestCase): From 74248cd027e523593507b6b114d255be45a16d16 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 1 Feb 2024 10:39:31 +0100 Subject: [PATCH 06/24] trying to make the tests pass --- CHANGELOG.rst | 1 + grid2op/Action/actionSpace.py | 28 ++++++++++++++++++++++++-- grid2op/Action/baseAction.py | 7 ++++--- grid2op/Backend/pandaPowerBackend.py | 26 ++++++++++++------------ grid2op/tests/test_n_busbar_per_sub.py | 23 ++++++++++++++------- 5 files changed, 60 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5d3d6f758..a772df12d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -45,6 +45,7 @@ Change Log in `BaseAction` and `BaseObservation`) and by using the `from packaging import version` to check version (instead of comparing strings) - [IMPROVED] slightly the code of `check_kirchoff` to make it slightly clearer +- [IMRPOVED] typing and doc for some of the main classes of the `Action` module [1.9.8] - 2024-01-26 ---------------------- diff --git a/grid2op/Action/actionSpace.py b/grid2op/Action/actionSpace.py index 975b5e9d0..137c9e93a 100644 --- a/grid2op/Action/actionSpace.py +++ b/grid2op/Action/actionSpace.py @@ -8,6 +8,12 @@ import warnings import copy +from typing import Dict, List, Any +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + from grid2op.Action.baseAction import BaseAction from grid2op.Action.serializableActionSpace import SerializableActionSpace @@ -72,7 +78,23 @@ def __init__( self.legal_action = legal_action def __call__( - self, dict_: dict = None, check_legal: bool = False, env: "BaseEnv" = None + self, + dict_: Dict[Literal["injection", + "hazards", + "maintenance", + "set_line_status", + "change_line_status", + "set_bus", + "change_bus", + "redispatch", + "set_storage", + "curtail", + "raise_alarm", + "raise_alert"], Any] = None, + check_legal: bool = False, + env: "grid2op.Environment.BaseEnv" = None, + *, + injection=None, # TODO n_busbar_per_sub ) -> BaseAction: """ This utility allows you to build a valid action, with the proper sizes if you provide it with a valid @@ -116,10 +138,12 @@ def __call__( see :func:`Action.udpate`. """ - + # build the action res = self.actionClass() + # update the action res.update(dict_) + if check_legal: is_legal, reason = self._is_legal(res, env) if not is_legal: diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 15c6c5bbe..f7f24451e 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -2126,9 +2126,6 @@ def update(self, dict_): - +3 -> set to bus 3 (grid2op >= 1.9.9) - etc. - -1: You can use this method to disconnect an object by setting the value to -1. - - .. versionchanged:: 1.9.9 - This feature is deactivated if `act.n_busbar_per_sub == 1` - "change_bus": (numpy bool vector or dictionary) will change the bus to which the object is connected. True will @@ -2156,6 +2153,10 @@ def update(self, dict_): - If "change_bus" is True, then objects will be moved from one bus to another. If the object were on bus 1 then it will be moved on bus 2, and if it were on bus 2, it will be moved on bus 1. If the object is disconnected then the action is ambiguous, and calling it will throw an AmbiguousAction exception. + + - "curtail" : TODO + - "raise_alarm" : TODO + - "raise_alert": 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. diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index de3b581dd..1154c81ac 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -862,19 +862,19 @@ def apply_action(self, backendAction: Union["grid2op.Action._backendAction._Back stor_bus = backendAction.get_storages_bus() new_bus_id = stor_bus.values[stor_bus.changed] # id of the busbar 1 or 2 if activated = new_bus_id > 0 # mask of storage that have been activated - # new_bus_num = ( - # cls.storage_to_subid[stor_bus.changed] + (new_bus_id - 1) * cls.n_sub - # ) # bus number - # new_bus_num[~activated] = cls.storage_to_subid[stor_bus.changed][ - # ~activated - # ] - # self._grid.storage["in_service"].values[stor_bus.changed] = activated - # self._grid.storage["bus"].values[stor_bus.changed] = new_bus_num - # self._topo_vect[cls.storage_pos_topo_vect[stor_bus.changed]] = new_bus_num - # self._topo_vect[ - # cls.storage_pos_topo_vect[stor_bus.changed][~activated] - # ] = -1 - new_bus_num = cls.local_bus_to_global(cls.storage_pos_topo_vect[stor_bus.changed], cls.storage_to_subid[stor_bus.changed]) + new_bus_num = ( + cls.storage_to_subid[stor_bus.changed] + (new_bus_id - 1) * cls.n_sub + ) # bus number + new_bus_num[~activated] = cls.storage_to_subid[stor_bus.changed][ + ~activated + ] + self._grid.storage["in_service"].values[stor_bus.changed] = activated + self._grid.storage["bus"].values[stor_bus.changed] = new_bus_num + self._topo_vect[cls.storage_pos_topo_vect[stor_bus.changed]] = new_bus_num + self._topo_vect[ + cls.storage_pos_topo_vect[stor_bus.changed][~activated] + ] = -1 + # new_bus_num = cls.local_bus_to_global(cls.storage_pos_topo_vect[stor_bus.changed], cls.storage_to_subid[stor_bus.changed]) # TODO n_busbar_per_sub if type(backendAction).shunts_data_available: diff --git a/grid2op/tests/test_n_busbar_per_sub.py b/grid2op/tests/test_n_busbar_per_sub.py index 1dea3dcce..a3d79a708 100644 --- a/grid2op/tests/test_n_busbar_per_sub.py +++ b/grid2op/tests/test_n_busbar_per_sub.py @@ -536,13 +536,9 @@ def test_change_deactivated(self): assert "change_bus" not in type(self.env.action_space()).authorized_keys assert not self.env.action_space.supports_type("change_bus") - def test_shunt(self): - el_id = 0 - bus_val = -1 + def _aux_test_action_shunt(self, act, el_id, bus_val): name_xxx = None el_nms = None - - act = self.env.action_space({"shunt": {"set_bus": [(0, -1)]}}) # self._aux_test_action(act, type(self.env).name_shunt, el_id, bus_val, None) # does not work for a lot of reasons assert not act.is_ambiguous()[0] tmp = f"{act}" # test the print does not crash @@ -550,8 +546,21 @@ def test_shunt(self): self._aux_test_act_consistent_as_dict(tmp, name_xxx, el_id, bus_val) tmp = act.as_serializable_dict() # test I can convert to another type of dict self._aux_test_act_consistent_as_serializable_dict(tmp, el_nms, el_id, bus_val) - - + + def test_shunt(self): + el_id = 0 + bus_val = -1 + act = self.env.action_space({"shunt": {"set_bus": [(0, bus_val)]}}) + self._aux_test_action_shunt(act, el_id, bus_val) + + for bus_val in range(type(self.env).n_busbar_per_sub): + act = self.env.action_space({"shunt": {"set_bus": [(0, bus_val)]}}) + self._aux_test_action_shunt(act, el_id, bus_val) + + act = self.env.action_space() + with self.assertRaises(IllegalAction): + act = self.env.action_space({"shunt": {"set_bus": [(0, type(self.env).n_busbar_per_sub + 1)]}}) + class TestAction_1busbar(TestAction_3busbars): def get_nb_bus(self): return 1 From b2b3854ed959f3eeb4c55a40bf2e02d5358c5ba7 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 1 Feb 2024 14:44:55 +0100 Subject: [PATCH 07/24] fixing some tests --- grid2op/Action/baseAction.py | 16 ++- grid2op/Backend/backend.py | 17 ++- grid2op/Backend/pandaPowerBackend.py | 148 +++++++++++++------------ grid2op/Observation/baseObservation.py | 13 +-- grid2op/Space/GridObjects.py | 8 +- grid2op/tests/test_n_busbar_per_sub.py | 13 ++- 6 files changed, 119 insertions(+), 96 deletions(-) diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index f7f24451e..2623973fd 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -1830,6 +1830,7 @@ def _digest_shunt(self, dict_): vect_self[:] = tmp elif isinstance(tmp, list): # expected a list: (id shunt, new bus) + cls = type(self) for (sh_id, new_bus) in tmp: if sh_id < 0: raise AmbiguousAction( @@ -1837,11 +1838,22 @@ def _digest_shunt(self, dict_): sh_id ) ) - if sh_id >= self.n_shunt: + if sh_id >= cls.n_shunt: raise AmbiguousAction( "Invalid shunt id {}. Shunt id should be less than the number " - "of shunt {}".format(sh_id, self.n_shunt) + "of shunt {}".format(sh_id, cls.n_shunt) ) + if new_bus <= -2: + raise IllegalAction( + f"Cannot ask for a shunt id <= 2, found {new_bus} for shunt id {sh_id}" + ) + elif new_bus > cls.n_busbar_per_sub: + raise IllegalAction( + f"Cannot ask for a shunt id > {cls.n_busbar_per_sub} " + f"the maximum number of busbar per substations" + f", found {new_bus} for shunt id {sh_id}" + ) + vect_self[sh_id] = new_bus elif tmp is None: pass diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index 4cbf957c1..6adfc6a96 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -177,7 +177,12 @@ def __init__(self, #: has been called when :func:`Backend.load_grid` was called. #: Starting from grid2op 1.9.9 this is a requirement (to #: ensure backward compatibility) - self._missing_two_busbars_support_info = True + self._missing_two_busbars_support_info: bool = True + + #: .. versionadded:: 1.9.9 + #: 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 def can_handle_more_than_2_busbar(self): """ @@ -999,11 +1004,11 @@ def _runpf_with_diverging_exception(self, is_dc : bool) -> Optional[Exception]: conv, exc_me = self.runpf(is_dc=is_dc) # run powerflow except Grid2OpException as exc_: exc_me = exc_ - except Exception as exc_: - exc_me = DivergingPowerflow( - f" An unexpected error occurred during the computation of the powerflow." - f"The error is: \n {exc_} \n. This is game over" - ) + # except Exception as exc_: + # exc_me = DivergingPowerflow( + # f" An unexpected error occurred during the computation of the powerflow." + # f"The error is: \n {exc_} \n. This is game over" + # ) if not conv and exc_me is None: exc_me = DivergingPowerflow( diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index 1154c81ac..142f47ce1 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -558,6 +558,7 @@ def load_grid(self, # "hack" to handle topological changes, for now only 2 buses per substation add_topo = copy.deepcopy(self._grid.bus) + # TODO n_busbar: what if non contiguous indexing ??? for busbar_supp in range(self.n_busbar_per_sub - 1): # self.n_busbar_per_sub and not type(self) here otherwise it erases can_handle_more_than_2_busbar / cannot_handle_more_than_2_busbar add_topo.index += add_topo.shape[0] add_topo["in_service"] = False @@ -827,7 +828,10 @@ def apply_action(self, backendAction: Union["grid2op.Action._backendAction._Back ) = backendAction() # handle bus status - self._grid.bus["in_service"] = pd.Series(data=active_bus.T.reshape(-1), index=np.arange(cls.n_sub * cls.n_busbar_per_sub)) + self._grid.bus["in_service"] = pd.Series(data=active_bus.T.reshape(-1), + index=np.arange(cls.n_sub * cls.n_busbar_per_sub), + dtype=bool) + # TODO n_busbar what if index is not continuous # handle generators tmp_prod_p = self._get_vector_inj["prod_p"](self._grid) @@ -902,7 +906,7 @@ 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) - + def _apply_load_bus(self, new_bus, id_el_backend, id_topo): new_bus_backend = type(self).local_bus_to_global_int( new_bus, self._init_bus_load[id_el_backend] @@ -993,6 +997,70 @@ def _aux_get_line_info(self, colname1, colname2): ) return res + def _aux_runpf_pp(self, is_dc: bool): + with warnings.catch_warnings(): + # remove the warning if _grid non connex. And it that case load flow as not converged + warnings.filterwarnings( + "ignore", category=scipy.sparse.linalg.MatrixRankWarning + ) + warnings.filterwarnings("ignore", category=RuntimeWarning) + warnings.filterwarnings("ignore", category=DeprecationWarning) + 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 (~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"{np.where(~self._grid.load['in_service'])[0]}" + ) + if (~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"{np.where(~self._grid.gen['in_service'])[0]}" + ) + try: + if is_dc: + pp.rundcpp(self._grid, check_connectivity=True, init="flat") + # if I put check_connectivity=False then the test AAATestBackendAPI.test_22_islanded_grid_make_divergence + # does not pass + + # if dc i start normally next time i call an ac powerflow + self._nb_bus_before = None + else: + pp.runpp( + self._grid, + check_connectivity=False, + init=self._pf_init, + numba=self.with_numba, + lightsim2grid=self._lightsim2grid, + max_iteration=self._max_iter, + distributed_slack=self._dist_slack, + ) + except IndexError as exc_: + raise pp.powerflow.LoadflowNotConverged(f"Surprising behaviour of pandapower when a bus is not connected to " + f"anything but present on the bus (with check_connectivity=False). " + f"Error was {exc_}" + ) + + # stores the computation time + if "_ppc" in self._grid: + if "et" in self._grid["_ppc"]: + self.comp_time += self._grid["_ppc"]["et"] + if self._grid.res_gen.isnull().values.any(): + # TODO see if there is a better way here -> do not handle this here, but rather in Backend._next_grid_state + # sometimes pandapower does not detect divergence and put Nan. + raise pp.powerflow.LoadflowNotConverged("Divergence due to Nan values in res_gen table (most likely due to " + "a non connected grid).") + def runpf(self, is_dc : bool=False) -> Tuple[bool, Union[Exception, None]]: """ INTERNAL @@ -1004,70 +1072,10 @@ def runpf(self, is_dc : bool=False) -> Tuple[bool, Union[Exception, None]]: buses has not changed between two calls, the previous results are re used. This speeds up the computation in case of "do nothing" action applied. """ - nb_bus = self.get_nb_active_bus() try: - with warnings.catch_warnings(): - # remove the warning if _grid non connex. And it that case load flow as not converged - warnings.filterwarnings( - "ignore", category=scipy.sparse.linalg.MatrixRankWarning - ) - warnings.filterwarnings("ignore", category=RuntimeWarning) - warnings.filterwarnings("ignore", category=DeprecationWarning) - 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 (~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"{np.where(~self._grid.load['in_service'])[0]}" - ) - if (~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"{np.where(~self._grid.gen['in_service'])[0]}" - ) - try: - if is_dc: - pp.rundcpp(self._grid, check_connectivity=True, init="flat") - # if I put check_connectivity=False then the test AAATestBackendAPI.test_22_islanded_grid_make_divergence - # does not pass - - # if dc i start normally next time i call an ac powerflow - self._nb_bus_before = None - else: - pp.runpp( - self._grid, - check_connectivity=False, - init=self._pf_init, - numba=self.with_numba, - lightsim2grid=self._lightsim2grid, - max_iteration=self._max_iter, - distributed_slack=self._dist_slack, - ) - except IndexError as exc_: - raise pp.powerflow.LoadflowNotConverged(f"Surprising behaviour of pandapower when a bus is not connected to " - f"anything but present on the bus (with check_connectivity=False). " - f"Error was {exc_}" - ) - - # stores the computation time - if "_ppc" in self._grid: - if "et" in self._grid["_ppc"]: - self.comp_time += self._grid["_ppc"]["et"] - if self._grid.res_gen.isnull().values.any(): - # TODO see if there is a better way here -> do not handle this here, but rather in Backend._next_grid_state - # sometimes pandapower does not detect divergence and put Nan. - raise pp.powerflow.LoadflowNotConverged("Divergence due to Nan values in res_gen table (most likely due to " - "a non connected grid).") - + 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(): raise pp.powerflow.LoadflowNotConverged("Isolated bus") @@ -1097,15 +1105,15 @@ def runpf(self, is_dc : bool=False) -> Tuple[bool, Union[Exception, None]]: # need to assign the correct value when a generator is present at the same bus # TODO optimize this ugly loop # see https://github.com/e2nIEE/pandapower/issues/1996 for a fix - for l_id in range(self.n_load): - if self.load_to_subid[l_id] in self.gen_to_subid: + for l_id in range(cls.n_load): + if cls.load_to_subid[l_id] in cls.gen_to_subid: ind_gens = np.where( - self.gen_to_subid == self.load_to_subid[l_id] + cls.gen_to_subid == cls.load_to_subid[l_id] )[0] for g_id in ind_gens: if ( - self._topo_vect[self.load_pos_topo_vect[l_id]] - == self._topo_vect[self.gen_pos_topo_vect[g_id]] + self._topo_vect[cls.load_pos_topo_vect[l_id]] + == self._topo_vect[cls.gen_pos_topo_vect[g_id]] ): self.load_v[l_id] = self.prod_v[g_id] break diff --git a/grid2op/Observation/baseObservation.py b/grid2op/Observation/baseObservation.py index c988117bf..aba399e7d 100644 --- a/grid2op/Observation/baseObservation.py +++ b/grid2op/Observation/baseObservation.py @@ -1060,7 +1060,6 @@ def process_grid2op_compat(cls): if glop_ver < version.parse("1.6.0"): # this feature did not exist before and was introduced in grid2op 1.6.0 cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) - cls.attr_list_set = copy.deepcopy(cls.attr_list_set) cls.dim_alarms = 0 for el in [ "is_alarm_illegal", @@ -1082,12 +1081,10 @@ def process_grid2op_compat(cls): except ValueError as exc_: # this attribute was not there in the first place pass - cls.attr_list_set = set(cls.attr_list_vect) if glop_ver < version.parse("1.6.4"): # "current_step", "max_step" were added in grid2Op 1.6.4 cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) - cls.attr_list_set = copy.deepcopy(cls.attr_list_set) for el in ["max_step", "current_step"]: try: @@ -1095,12 +1092,10 @@ def process_grid2op_compat(cls): except ValueError as exc_: # this attribute was not there in the first place pass - cls.attr_list_set = set(cls.attr_list_vect) if glop_ver < version.parse("1.6.5"): # "current_step", "max_step" were added in grid2Op 1.6.5 cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) - cls.attr_list_set = copy.deepcopy(cls.attr_list_set) for el in ["delta_time"]: try: @@ -1108,12 +1103,10 @@ def process_grid2op_compat(cls): except ValueError as exc_: # this attribute was not there in the first place pass - cls.attr_list_set = set(cls.attr_list_vect) if glop_ver < version.parse("1.6.6"): # "gen_margin_up", "gen_margin_down" were added in grid2Op 1.6.6 cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) - cls.attr_list_set = copy.deepcopy(cls.attr_list_set) for el in [ "gen_margin_up", @@ -1125,12 +1118,10 @@ def process_grid2op_compat(cls): except ValueError as exc_: # this attribute was not there in the first place pass - cls.attr_list_set = set(cls.attr_list_vect) if glop_ver < version.parse("1.9.1"): # alert attributes have been added in 1.9.1 cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) - cls.attr_list_set = copy.deepcopy(cls.attr_list_set) for el in [ "active_alert", @@ -1146,7 +1137,9 @@ def process_grid2op_compat(cls): except ValueError as exc_: # this attribute was not there in the first place pass - cls.attr_list_set = set(cls.attr_list_vect) + + cls.attr_list_set = copy.deepcopy(cls.attr_list_set) + cls.attr_list_set = set(cls.attr_list_vect) def shape(self): return type(self).shapes() diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 923bf1b12..1ca89f75a 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -3613,8 +3613,12 @@ class res(GridObjects): cls._PATH_ENV = str(dict_["_PATH_ENV"]) else: cls._PATH_ENV = None - - cls.n_busbar_per_sub = int(dict_["n_busbar_per_sub"]) + + if 'n_busbar_per_sub' in dict_: + cls.n_busbar_per_sub = int(dict_["n_busbar_per_sub"]) + else: + # compat version: was not set + cls.n_busbar_per_sub = DEFAULT_N_BUSBAR_PER_SUB cls.name_gen = extract_from_dict( dict_, "name_gen", lambda x: np.array(x).astype(str) diff --git a/grid2op/tests/test_n_busbar_per_sub.py b/grid2op/tests/test_n_busbar_per_sub.py index a3d79a708..ff3ce9c6f 100644 --- a/grid2op/tests/test_n_busbar_per_sub.py +++ b/grid2op/tests/test_n_busbar_per_sub.py @@ -419,7 +419,7 @@ def setUp(self) -> None: action_class=CompleteAction, test=True, n_busbar=self.get_nb_bus(), - _add_to_name=type(self).__name__) + _add_to_name=type(self).__name__ + f'_{self.get_nb_bus()}') return super().setUp() def tearDown(self) -> None: @@ -536,7 +536,7 @@ def test_change_deactivated(self): assert "change_bus" not in type(self.env.action_space()).authorized_keys assert not self.env.action_space.supports_type("change_bus") - def _aux_test_action_shunt(self, act, el_id, bus_val): + def _aux_test_action_shunt(self, act : BaseAction, el_id, bus_val): name_xxx = None el_nms = None # self._aux_test_action(act, type(self.env).name_shunt, el_id, bus_val, None) # does not work for a lot of reasons @@ -550,16 +550,16 @@ def _aux_test_action_shunt(self, act, el_id, bus_val): def test_shunt(self): el_id = 0 bus_val = -1 - act = self.env.action_space({"shunt": {"set_bus": [(0, bus_val)]}}) + act = self.env.action_space({"shunt": {"set_bus": [(el_id, bus_val)]}}) self._aux_test_action_shunt(act, el_id, bus_val) for bus_val in range(type(self.env).n_busbar_per_sub): - act = self.env.action_space({"shunt": {"set_bus": [(0, bus_val)]}}) - self._aux_test_action_shunt(act, el_id, bus_val) + act = self.env.action_space({"shunt": {"set_bus": [(el_id, bus_val + 1)]}}) + self._aux_test_action_shunt(act, el_id, bus_val + 1) act = self.env.action_space() with self.assertRaises(IllegalAction): - act = self.env.action_space({"shunt": {"set_bus": [(0, type(self.env).n_busbar_per_sub + 1)]}}) + act = self.env.action_space({"shunt": {"set_bus": [(el_id, type(self.env).n_busbar_per_sub + 1)]}}) class TestAction_1busbar(TestAction_3busbars): def get_nb_bus(self): @@ -573,6 +573,7 @@ class TestActionSpace(unittest.TestCase): class TestBackendAction(unittest.TestCase): pass + class TestPandapowerBackend(unittest.TestCase): pass From 28606f72219cf1ee016810a59d1af54f58396f9e Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 2 Feb 2024 12:27:30 +0100 Subject: [PATCH 08/24] action_space should be compatible with n_busbar_per_sub != 2 --- CHANGELOG.rst | 3 + grid2op/Action/baseAction.py | 2 +- grid2op/Action/serializableActionSpace.py | 486 ++++++++++++++-------- grid2op/tests/test_n_busbar_per_sub.py | 347 ++++++++++++++- 4 files changed, 666 insertions(+), 172 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a772df12d..c5238cbf7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -33,6 +33,9 @@ Change Log [1.9.9] - 2024-xx-yy ---------------------- +- [BREAKING] the order of the actions in `env.action_space.get_all_unitary_line_set` and + `env.action_space.get_all_unitary_topologies_set` might have changed (this is caused + by a rewriting of these functions in case there is not 2 busbars per substation) - [FIXED] github CI did not upload the source files - [FIXED] `l2rpn_utils` module did not stored correctly the order of actions and observation for wcci_2020 diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 2623973fd..20347fbe3 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -2724,7 +2724,7 @@ def _check_for_ambiguity(self): "Incorrect number of shunt (for shunt_bus) in your action." ) if cls.n_shunt > 0: - if np.max(self.shunt_bus) > 2: + if np.max(self.shunt_bus) > cls.n_busbar_per_sub: raise AmbiguousAction( "Some shunt is connected to a bus greater than 2" ) diff --git a/grid2op/Action/serializableActionSpace.py b/grid2op/Action/serializableActionSpace.py index d7cee94cf..55a164139 100644 --- a/grid2op/Action/serializableActionSpace.py +++ b/grid2op/Action/serializableActionSpace.py @@ -10,6 +10,10 @@ import numpy as np import itertools from typing import Dict, List +try: + from typing import Literal, Self +except ImportError: + from typing_extensions import Literal, Self from grid2op.dtypes import dt_int, dt_float, dt_bool from grid2op.Exceptions import AmbiguousAction, Grid2OpException @@ -112,11 +116,11 @@ def _get_possible_action_types(self): rnd_types.append(cls.CHANGE_BUS_ID) if "redispatch" in self.actionClass.authorized_keys: rnd_types.append(cls.REDISPATCHING_ID) - if self.n_storage > 0 and "storage_power" in self.actionClass.authorized_keys: + if cls.n_storage > 0 and "storage_power" in self.actionClass.authorized_keys: rnd_types.append(cls.STORAGE_POWER_ID) - if self.dim_alarms > 0 and "raise_alarm" in self.actionClass.authorized_keys: + if cls.dim_alarms > 0 and "raise_alarm" in self.actionClass.authorized_keys: rnd_types.append(cls.RAISE_ALARM_ID) - if self.dim_alerts > 0 and "raise_alert" in self.actionClass.authorized_keys: + if cls.dim_alerts > 0 and "raise_alert" in self.actionClass.authorized_keys: rnd_types.append(cls.RAISE_ALERT_ID) return rnd_types @@ -170,13 +174,13 @@ def supports_type(self, action_type): f"The action type provided should be in {name_action_types}. " f"You provided {action_type} which is not supported." ) - + cls = type(self) if action_type == "storage_power": - return (self.n_storage > 0) and ( + return (cls.n_storage > 0) and ( "set_storage" in self.actionClass.authorized_keys ) elif action_type == "set_storage": - return (self.n_storage > 0) and ( + return (cls.n_storage > 0) and ( "set_storage" in self.actionClass.authorized_keys ) elif action_type == "curtail_mw": @@ -262,7 +266,7 @@ def _sample_raise_alert(self, rnd_update=None): rnd_update["raise_alert"] = rnd_alerted_lines return rnd_update - def sample(self): + def sample(self) -> BaseAction: """ A utility used to sample a new random :class:`BaseAction`. @@ -303,7 +307,7 @@ def sample(self): env = grid2op.make("l2rpn_case14_sandbox") # and now you can sample from the action space - random_action = env.action_space() + random_action = env.action_space() # this action is not random at all, it starts by "do nothing" for i in range(5): # my resulting action will be a complex action # that will be the results of applying 5 random actions @@ -322,22 +326,22 @@ def sample(self): # this sampling rnd_type = self.space_prng.choice(rnd_types) - - if rnd_type == self.SET_STATUS_ID: + cls = type(self) + if rnd_type == cls.SET_STATUS_ID: rnd_update = self._sample_set_line_status() - elif rnd_type == self.CHANGE_STATUS_ID: + elif rnd_type == cls.CHANGE_STATUS_ID: rnd_update = self._sample_change_line_status() - elif rnd_type == self.SET_BUS_ID: + elif rnd_type == cls.SET_BUS_ID: rnd_update = self._sample_set_bus() - elif rnd_type == self.CHANGE_BUS_ID: + elif rnd_type == cls.CHANGE_BUS_ID: rnd_update = self._sample_change_bus() - elif rnd_type == self.REDISPATCHING_ID: + elif rnd_type == cls.REDISPATCHING_ID: rnd_update = self._sample_redispatch() - elif rnd_type == self.STORAGE_POWER_ID: + elif rnd_type == cls.STORAGE_POWER_ID: rnd_update = self._sample_storage_power() - elif rnd_type == self.RAISE_ALARM_ID: + elif rnd_type == cls.RAISE_ALARM_ID: rnd_update = self._sample_raise_alarm() - elif rnd_type == self.RAISE_ALERT_ID: + elif rnd_type == cls.RAISE_ALERT_ID: rnd_update = self._sample_raise_alert() else: raise Grid2OpException( @@ -347,7 +351,10 @@ def sample(self): rnd_act.update(rnd_update) return rnd_act - def disconnect_powerline(self, line_id=None, line_name=None, previous_action=None): + def disconnect_powerline(self, + line_id: int=None, + line_name: str=None, + previous_action: BaseAction=None) -> BaseAction: """ Utilities to disconnect a powerline more easily. @@ -396,6 +403,7 @@ def disconnect_powerline(self, line_id=None, line_name=None, previous_action=Non # after the last call! """ + cls = type(self) if line_id is None and line_name is None: raise AmbiguousAction( 'You need to provide either the "line_id" or the "line_name" of the powerline ' @@ -408,11 +416,11 @@ def disconnect_powerline(self, line_id=None, line_name=None, previous_action=Non ) if line_id is None: - line_id = np.where(self.name_line == line_name)[0] + line_id = np.where(cls.name_line == line_name)[0] if not len(line_id): raise AmbiguousAction( 'Line with name "{}" is not on the grid. The powerlines names are:\n{}' - "".format(line_name, self.name_line) + "".format(line_name, cls.name_line) ) if previous_action is None: res = self.actionClass() @@ -422,17 +430,22 @@ def disconnect_powerline(self, line_id=None, line_name=None, previous_action=Non type(self).ERR_MSG_WRONG_TYPE.format(type(previous_action), self.actionClass) ) res = previous_action - if line_id > self.n_line: + if line_id > cls.n_line: raise AmbiguousAction( "You asked to disconnect powerline of id {} but this id does not exist. The " - "grid counts only {} powerline".format(line_id, self.n_line) + "grid counts only {} powerline".format(line_id, cls.n_line) ) res.update({"set_line_status": [(line_id, -1)]}) return res def reconnect_powerline( - self, bus_or, bus_ex, line_id=None, line_name=None, previous_action=None - ): + self, + bus_or: int, + bus_ex: int, + line_id: int=None, + line_name: str=None, + previous_action: BaseAction=None + ) -> BaseAction: """ Utilities to reconnect a powerline more easily. @@ -503,19 +516,19 @@ def reconnect_powerline( 'You need to provide only of the "line_id" or the "line_name" of the powerline ' "you want to reconnect" ) - + cls = type(self) if line_id is None: - line_id = np.where(self.name_line == line_name)[0] + line_id = np.where(cls.name_line == line_name)[0] if previous_action is None: res = self.actionClass() else: if not isinstance(previous_action, self.actionClass): raise AmbiguousAction( - type(self).ERR_MSG_WRONG_TYPE.format(type(previous_action), self.actionClass) + cls.ERR_MSG_WRONG_TYPE.format(type(previous_action), self.actionClass) ) res = previous_action - if line_id > self.n_line: + if line_id > cls.n_line: raise AmbiguousAction( "You asked to disconnect powerline of id {} but this id does not exist. The " "grid counts only {} powerline".format(line_id, self.n_line) @@ -533,12 +546,12 @@ def reconnect_powerline( def change_bus( self, - name_element, - extremity=None, - substation=None, - type_element=None, - previous_action=None, - ): + name_element : str, + extremity : Literal["or", "ex"] =None, + substation: int=None, + type_element :str=None, + previous_action: BaseAction=None, + ) -> BaseAction: """ Utilities to change the bus of a single element if you give its name. **NB** Changing a bus has the effect to assign the object to bus 1 if it was before that connected to bus 2, and to assign it to bus 2 if it was @@ -557,7 +570,7 @@ def change_bus( Its substation ID, if you know it will increase the performance. Otherwise, the method will search for it. type_element: ``str``, optional Type of the element to look for. It is here to speed up the computation. One of "line", "gen" or "load" - previous_action: :class:`Action`, optional + previous_action: :class:`BaseAction`, optional The (optional) action to update. It should be of the same type as :attr:`ActionSpace.actionClass` Notes @@ -622,15 +635,16 @@ def change_bus( res.update({"change_bus": {"substations_id": [(my_sub_id, arr_)]}}) return res - def _extract_database_powerline(self, extremity): + @classmethod + def _extract_database_powerline(cls, extremity: Literal["or", "ex"]): if extremity[:2] == "or": - to_subid = self.line_or_to_subid - to_sub_pos = self.line_or_to_sub_pos - to_name = self.name_line + to_subid = cls.line_or_to_subid + to_sub_pos = cls.line_or_to_sub_pos + to_name = cls.name_line elif extremity[:2] == "ex": - to_subid = self.line_ex_to_subid - to_sub_pos = self.line_ex_to_sub_pos - to_name = self.name_line + to_subid = cls.line_ex_to_subid + to_sub_pos = cls.line_ex_to_sub_pos + to_name = cls.name_line elif extremity is None: raise Grid2OpException( "It is mandatory to know on which ends you want to change the bus of the powerline" @@ -653,18 +667,18 @@ def _extract_dict_action( to_subid = None to_sub_pos = None to_name = None - + cls = type(self) if type_element is None: # i have to look through all the objects to find it - if name_element in self.name_load: - to_subid = self.load_to_subid - to_sub_pos = self.load_to_sub_pos - to_name = self.name_load - elif name_element in self.name_gen: - to_subid = self.gen_to_subid - to_sub_pos = self.gen_to_sub_pos - to_name = self.name_gen - elif name_element in self.name_line: + if name_element in cls.name_load: + to_subid = cls.load_to_subid + to_sub_pos = cls.load_to_sub_pos + to_name = cls.name_load + elif name_element in cls.name_gen: + to_subid = cls.gen_to_subid + to_sub_pos = cls.gen_to_sub_pos + to_name = cls.name_gen + elif name_element in cls.name_line: to_subid, to_sub_pos, to_name = self._extract_database_powerline( extremity ) @@ -675,13 +689,13 @@ def _extract_dict_action( elif type_element == "line": to_subid, to_sub_pos, to_name = self._extract_database_powerline(extremity) elif type_element[:3] == "gen" or type_element[:4] == "prod": - to_subid = self.gen_to_subid - to_sub_pos = self.gen_to_sub_pos - to_name = self.name_gen + to_subid = cls.gen_to_subid + to_sub_pos = cls.gen_to_sub_pos + to_name = cls.name_gen elif type_element == "load": - to_subid = self.load_to_subid - to_sub_pos = self.load_to_sub_pos - to_name = self.name_load + to_subid = cls.load_to_subid + to_sub_pos = cls.load_to_sub_pos + to_name = cls.name_load else: raise AmbiguousAction( 'unknown type_element specifier "{}". type_element should be "line" or "load" ' @@ -704,13 +718,13 @@ def _extract_dict_action( def set_bus( self, - name_element, - new_bus, - extremity=None, - substation=None, - type_element=None, - previous_action=None, - ): + name_element :str, + new_bus :int, + extremity: Literal["or", "ex"]=None, + substation: int=None, + type_element: int=None, + previous_action: BaseAction=None, + ) -> BaseAction: """ Utilities to set the bus of a single element if you give its name. **NB** Setting a bus has the effect to assign the object to this bus. If it was before that connected to bus 1, and you assign it to bus 1 (*new_bus* @@ -737,7 +751,7 @@ def set_bus( type_element: ``str``, optional Type of the element to look for. It is here to speed up the computation. One of "line", "gen" or "load" - previous_action: :class:`Action`, optional + previous_action: :class:`BaseAction`, optional The (optional) action to update. It should be of the same type as :attr:`ActionSpace.actionClass` Returns @@ -791,7 +805,7 @@ def set_bus( res.update({"set_bus": {"substations_id": [(my_sub_id, dict_["set_bus"])]}}) return res - def get_set_line_status_vect(self): + def get_set_line_status_vect(self) -> np.ndarray: """ Computes and returns a vector that can be used in the "set_status" keyword if building an :class:`BaseAction` @@ -803,7 +817,7 @@ def get_set_line_status_vect(self): """ return self._template_act.get_set_line_status_vect() - def get_change_line_status_vect(self): + def get_change_line_status_vect(self) -> np.ndarray: """ Computes and return a vector that can be used in the "change_line_status" keyword if building an :class:`BaseAction` @@ -816,11 +830,12 @@ def get_change_line_status_vect(self): return self._template_act.get_change_line_status_vect() @staticmethod - def get_all_unitary_line_set(action_space): + def get_all_unitary_line_set(action_space: Self) -> List[BaseAction]: """ Return all unitary actions that "set" powerline status. - For each powerline, there are 5 such actions: + For each powerline, if there are 2 busbars per substation, + there are 5 such actions: - disconnect it - connected it origin at bus 1 and extremity at bus 1 @@ -828,9 +843,18 @@ def get_all_unitary_line_set(action_space): - connected it origin at bus 2 and extremity at bus 1 - connected it origin at bus 2 and extremity at bus 2 + This number increases quite rapidly if there are more busbars + allowed per substation of course. For example if you allow + for 3 busbars per substations, it goes from (1 + 2*2) [=5] + to (1 + 3 * 3) [=10] and if you allow for 4 busbars per substations + you end up with (1 + 4 * 4) [=17] possible actions per powerline. + + .. seealso:: + :func:`SerializableActionSpace.get_all_unitary_line_set_simple` + Parameters ---------- - action_space: :class:`grid2op.BaseAction.ActionSpace` + action_space: :class:`ActionSpace` The action space used. Returns @@ -840,24 +864,23 @@ def get_all_unitary_line_set(action_space): """ res = [] - + cls = type(action_space) # powerline switch: disconnection - for i in range(action_space.n_line): - res.append(action_space.disconnect_powerline(line_id=i)) - - # powerline switch: reconnection - for bus_or in [1, 2]: - for bus_ex in [1, 2]: - for i in range(action_space.n_line): - act = action_space.reconnect_powerline( - line_id=i, bus_ex=bus_ex, bus_or=bus_or - ) - res.append(act) + for i in range(cls.n_line): + res.append(action_space.disconnect_powerline(line_id=i)) + + all_busbars = list(range(1, cls.n_busbar_per_sub + 1)) + for bus1, bus2 in itertools.product(all_busbars, all_busbars): + for i in range(cls.n_line): + act = action_space.reconnect_powerline( + line_id=i, bus_ex=bus1, bus_or=bus2 + ) + res.append(act) return res @staticmethod - def get_all_unitary_line_set_simple(action_space): + def get_all_unitary_line_set_simple(action_space: Self) -> List[BaseAction]: """ Return all unitary actions that "set" powerline status but in a more simple way than :func:`SerializableActionSpace.get_all_unitary_line_set` @@ -869,12 +892,19 @@ def get_all_unitary_line_set_simple(action_space): side used to be connected) It has the main advantages to "only" add 2 actions per powerline - instead of 5. + instead of 5 (if the number of busbars per substation is 2). + + Using this method, powerlines will always be reconnected to their + previous busbars (the last known one) and you will always get + exactly 2 actions per powerlines. + + .. seealso:: + :func:`SerializableActionSpace.get_all_unitary_line_set` Parameters ---------- - action_space: :class:`grid2op.BaseAction.ActionSpace` + action_space: :class:`ActionSpace` The action space used. Returns @@ -884,32 +914,33 @@ def get_all_unitary_line_set_simple(action_space): """ res = [] - + cls = type(action_space) # powerline set: disconnection - for i in range(action_space.n_line): + for i in range(cls.n_line): res.append(action_space({"set_line_status": [(i,-1)]})) # powerline set: reconnection - for i in range(action_space.n_line): + for i in range(cls.n_line): res.append(action_space({"set_line_status": [(i, +1)]})) return res @staticmethod - def get_all_unitary_alarm(action_space): + def get_all_unitary_alarm(action_space: Self) -> List[BaseAction]: """ .. warning:: /!\\\\ Only valid with "l2rpn_icaps_2021" environment /!\\\\ """ + cls = type(action_space) res = [] - for i in range(action_space.dim_alarms): - status = np.full(action_space.dim_alarms, fill_value=False, dtype=dt_bool) + for i in range(cls.dim_alarms): + status = np.full(cls.dim_alarms, fill_value=False, dtype=dt_bool) status[i] = True res.append(action_space({"raise_alarm": status})) return res @staticmethod - def get_all_unitary_alert(action_space): + def get_all_unitary_alert(action_space: Self) -> List[BaseAction]: """ Return all unitary actions that raise an alert on powerlines. @@ -918,15 +949,16 @@ def get_all_unitary_alert(action_space): If you got 22 attackable lines, then you got 2**22 actions... probably a TERRIBLE IDEA ! """ + cls = type(action_space) res = [] possible_values = [False, True] - if action_space.dim_alerts: - for status in itertools.product(possible_values, repeat=type(action_space).dim_alerts): + if cls.dim_alerts: + for status in itertools.product(possible_values, repeat=cls.dim_alerts): res.append(action_space({"raise_alert": np.array(status, dtype=dt_bool)})) return res @staticmethod - def get_all_unitary_line_change(action_space): + def get_all_unitary_line_change(action_space: Self) -> List[BaseAction]: """ Return all unitary actions that "change" powerline status. @@ -934,7 +966,7 @@ def get_all_unitary_line_change(action_space): Parameters ---------- - action_space: :class:`grid2op.BaseAction.ActionSpace` + action_space: :class:`ActionSpace` The action space used. Returns @@ -943,15 +975,16 @@ def get_all_unitary_line_change(action_space): The list of all "change" action acting on powerline status """ + cls = type(action_space) res = [] - for i in range(action_space.n_line): + for i in range(cls.n_line): status = action_space.get_change_line_status_vect() status[i] = True res.append(action_space({"change_line_status": status})) return res @staticmethod - def get_all_unitary_topologies_change(action_space, sub_id=None): + def get_all_unitary_topologies_change(action_space: Self, sub_id : int=None) -> List[BaseAction]: """ This methods allows to compute and return all the unitary topological changes that can be performed on a powergrid. @@ -960,7 +993,7 @@ def get_all_unitary_topologies_change(action_space, sub_id=None): Parameters ---------- - action_space: :class:`grid2op.BaseAction.ActionSpace` + action_space: :class:`ActionSpace` The action space used. sub_id: ``int``, optional @@ -991,9 +1024,14 @@ def get_all_unitary_topologies_change(action_space, sub_id=None): all_change_actions_sub4 = env.action_space.get_all_unitary_topologies_change(env.action_space, sub_id=4) """ + cls = type(action_space) + if cls.n_busbar_per_sub == 1 or cls.n_busbar_per_sub >= 3: + raise Grid2OpException("Impossible to use `change_bus` action type " + "if your grid does not have exactly 2 busbars " + "per substation") res = [] S = [0, 1] - for sub_id_, num_el in enumerate(action_space.sub_info): + for sub_id_, num_el in enumerate(cls.sub_info): if sub_id is not None: if sub_id_ != sub_id: continue @@ -1020,8 +1058,79 @@ def get_all_unitary_topologies_change(action_space, sub_id=None): # a substation, changing A,B or changing C,D always has the same effect. return res + @classmethod + def _is_ok_symmetry(cls, n_busbar_per_sub: int, tup: np.ndarray, bus_start: int=2, id_start: int=1) -> bool: + # id_start: at which index to start in the `tup` vector + # bus_start: which maximum bus id should be present there + # tup: the topology vector + if id_start >= len(tup): + # i reached the end of the tuple + return True + if bus_start >= n_busbar_per_sub: + # all previous buses are filled + return True + + this_bus = tup[id_start] + if this_bus < bus_start: + # this bus id is already assigned + # go to next id, + return cls._is_ok_symmetry(n_busbar_per_sub, tup, bus_start, id_start + 1) + else: + if this_bus == bus_start: + # This is a new bus and it has the correct id + # so I go to next + return cls._is_ok_symmetry(n_busbar_per_sub, tup, bus_start + 1, id_start + 1) + else: + # by symmetry the "current" bus should be relabeled `bus_start` + # which is alreay added somewhere else. The current topologie + # is not valid. + return False + return True + + @classmethod + def _is_ok_line(cls, n_busbar_per_sub: int, tup: np.ndarray, lines_id: np.ndarray) -> bool: + """check there are at least a line connected to each buses""" + # now, this is the "smart" thing: + # as the bus should be labelled "in order" (no way we can add + # bus 3 if bus 2 is not already set in `tup` because of the + # `_is_ok_symmetry` function), I know for a fact that there is + # `tup.max()` active buses in this topology. + # So to make sure that every buses has at least a line connected to it + # then I just check the number of unique buses (tup.max()) + # and compare it to the number of buses where there are + # at least a line len(buses_with_lines) + nb = 0 + only_line = tup[lines_id] + for el in range(1, n_busbar_per_sub +1): + nb += (only_line == el).any() + return nb == tup.max() + # buses_with_lines = np.unique(tup[lines_id]) # slower than python code above + # return buses_with_lines.size == tup.max() + + @classmethod + def _is_ok_2(cls, n_busbar_per_sub : int, tup) -> bool: + """check there are at least 2 elements per busbars""" + # now, this is the "smart" thing: + # as the bus should be labelled "in order" (no way we can add + # bus 3 if bus 2 is not already set in `tup` because of the + # `_is_ok_symmetry` function), I know for a fact that there is + # `tup.max()` active buses in this topology. + # So to make sure that every buses has at least a line connected to it + # then I just check the number of unique buses (tup.max()) + # and compare it to the number of buses where there are + # at least a line len(buses_with_lines) + for el in range(1, tup.max() + 1): + if (tup == el).sum() < 2: + return False + return True + # un_, count = np.unique(tup, return_counts=True) # slower than python code above + # return (count >= 2).all() + @staticmethod - def get_all_unitary_topologies_set(action_space, sub_id=None): + def get_all_unitary_topologies_set(action_space: Self, + sub_id: int=None, + add_alone_line=True, + _count_only=False) -> List[BaseAction]: """ This methods allows to compute and return all the unitary topological changes that can be performed on a powergrid. @@ -1029,14 +1138,60 @@ def get_all_unitary_topologies_set(action_space, sub_id=None): The changes will be performed using the "set_bus" method. The "do nothing" action will be counted once per substation in the grid. + It returns all the "valid" topologies available at any substation (if `sub_id` is ``None`` -default) + or at the requested substation. + + To be valid a topology must satisfy: + + - there are at least one side of the powerline connected to each busbar (there cannot be a load alone + on a bus or a generator alone on a bus for example) + - if `add_alone_line=False` (not the default) then there must be at least two elements in a + substation + + .. info:: + We try to make the result of this function as small as possible. This means that if at any + substation the number of "valid" topology is only 1, it is ignored and will not be added + in the result. + + This imply that when `env.n_busbar_per_sub=1` then this function returns the empty list. + + .. info:: + If `add_alone_line` is True (again NOT the default) then if any substation counts less than + 3 elements or less then no action will be added for this substation. + + If there are 4 or 5 elements at a substation (and add_alone_line=False), then only topologies + using 2 busbar will be used. + + .. warning:: + This generates only topologies were all elements are connected. It does not generate + topologies with disconnected lines. + + .. warning:: + As far as we know, there are no bugs in this implementation. However we did not spend + lots of time finding a "closed form" formula to count exactly the number of possible topologies. + This means that we might have missed some topologies or counted the same "results" multiple + times if there has been an error in the symmetries. + + If you are interested in this topic, let us know with a discussion, for example here + https://github.com/rte-france/Grid2Op/discussions + Parameters ---------- - action_space: :class:`grid2op.BaseAction.ActionHelper` + action_space: :class:`ActionSpace` The action space used. sub_id: ``int``, optional The substation ID. If ``None`` it is done for all substations. + add_alone_line: ``bool``, optional + If ``True`` (default) then topologiees where 1 line side is "alone" on a bus + are valid and put in the output (more topologies are considered). If not + then only topologies with at least one line AND 2 elements per buses + are returned. + + _count_only: ``bool``, optional + Does not return the list but rather only the number of elements there would be + Notes ----- This might take a long time on large grid (possibly 10-15 mins for the IEEE 118 for example) @@ -1062,80 +1217,71 @@ def get_all_unitary_topologies_set(action_space, sub_id=None): all_change_actions_sub4 = env.action_space.get_all_unitary_topologies_set(env.action_space, sub_id=4) """ + cls = type(action_space) + if cls.n_busbar_per_sub == 1: + return [] + res = [] - S = [0, 1] - for sub_id_, num_el in enumerate(action_space.sub_info): - tmp = [] + S = list(range(1, cls.n_busbar_per_sub + 1)) + for sub_id_, num_el in enumerate(cls.sub_info): + if not _count_only: + tmp = [] + else: + tmp = 0 + if sub_id is not None: if sub_id_ != sub_id: continue - new_topo = np.full(shape=num_el, fill_value=1, dtype=dt_int) - # perform the action "set everything on bus 1" - action = action_space( - {"set_bus": {"substations_id": [(sub_id_, new_topo)]}} - ) - tmp.append(action) - - powerlines_or_id = action_space.line_or_to_sub_pos[ - action_space.line_or_to_subid == sub_id_ + powerlines_or_id = cls.line_or_to_sub_pos[ + cls.line_or_to_subid == sub_id_ ] - powerlines_ex_id = action_space.line_ex_to_sub_pos[ - action_space.line_ex_to_subid == sub_id_ + powerlines_ex_id = cls.line_ex_to_sub_pos[ + cls.line_ex_to_subid == sub_id_ ] powerlines_id = np.concatenate((powerlines_or_id, powerlines_ex_id)) # computes all the topologies at 2 buses for this substation for tup in itertools.product(S, repeat=num_el - 1): - indx = np.full(shape=num_el, fill_value=False, dtype=dt_bool) - tup = np.array((0, *tup)).astype( - dt_bool - ) # add a zero to first element -> break symmetry - indx[tup] = True - if indx.sum() >= 2 and (~indx).sum() >= 2: - # i need 2 elements on each bus at least (almost all the times, except when a powerline - # is alone on its bus) - new_topo = np.full(shape=num_el, fill_value=1, dtype=dt_int) - new_topo[~indx] = 2 - - if ( - indx[powerlines_id].sum() == 0 - or (~indx[powerlines_id]).sum() == 0 - ): - # if there is a "node" without a powerline, the topology is not valid - continue - + tup = np.array((1, *tup)) # force first el on bus 1 to break symmetry + + if not action_space._is_ok_symmetry(cls.n_busbar_per_sub, tup): + # already added (by symmetry) + continue + if not action_space._is_ok_line(cls.n_busbar_per_sub, tup, powerlines_id): + # check there is at least one line per busbars + continue + if not add_alone_line and not action_space._is_ok_2(cls.n_busbar_per_sub, tup): + # check there are at least 2 elements per buses + continue + + if not _count_only: action = action_space( - {"set_bus": {"substations_id": [(sub_id_, new_topo)]}} + {"set_bus": {"substations_id": [(sub_id_, tup)]}} ) tmp.append(action) else: - # i need to take into account the case where 1 powerline is alone on a bus too - if ( - (indx[powerlines_id]).sum() >= 1 - and (~indx[powerlines_id]).sum() >= 1 - ): - new_topo = np.full(shape=num_el, fill_value=1, dtype=dt_int) - new_topo[~indx] = 2 - action = action_space( - {"set_bus": {"substations_id": [(sub_id_, new_topo)]}} - ) - tmp.append(action) + tmp += 1 - if len(tmp) >= 2: + if not _count_only and len(tmp) >= 2: # if i have only one single topology on this substation, it doesn't make any action - # i cannot change the topology is there is only one. + # i cannot change the topology if there is only one. res += tmp - + elif _count_only: + if tmp >= 2: + res.append(tmp) + else: + # no real way to change if there is only one valid topology + res.append(0) return res @staticmethod def get_all_unitary_redispatch( action_space, num_down=5, num_up=5, max_ratio_value=1.0 - ): + ) -> List[BaseAction]: """ Redispatching action are continuous action. This method is an helper to convert the continuous - action into discrete action (by rounding). + action into "discrete actions" (by rounding). The number of actions is equal to num_down + num_up (by default 10) per dispatchable generator. @@ -1146,10 +1292,14 @@ def get_all_unitary_redispatch( a distinct action (then counting `num_down` different action, because 0.0 is removed) - it will do the same for [0, gen_maw_ramp_up] + .. note:: + With this "helper" only one generator is affected by one action. For example + there are no action acting on both generator 1 and generator 2 at the same + time. Parameters ---------- - action_space: :class:`grid2op.BaseAction.ActionHelper` + action_space: :class:`ActionSpace` The action space used. num_down: ``int`` @@ -1204,7 +1354,7 @@ def get_all_unitary_redispatch( return res @staticmethod - def get_all_unitary_curtail(action_space, num_bin=10, min_value=0.5): + def get_all_unitary_curtail(action_space : Self, num_bin: int=10, min_value: float=0.5) -> List[BaseAction]: """ Curtailment action are continuous action. This method is an helper to convert the continuous action into discrete action (by rounding). @@ -1218,17 +1368,21 @@ def get_all_unitary_curtail(action_space, num_bin=10, min_value=0.5): - it will divide the interval [0, 1] into `num_bin`, each will make a distinct action (then counting `num_bin` different action, because 0.0 is removed) + .. note:: + With this "helper" only one generator is affected by one action. For example + there are no action acting on both generator 1 and generator 2 at the same + time. Parameters ---------- - action_space: :class:`grid2op.BaseAction.ActionHelper` + action_space: :class:`ActionSpace` The action space used. num_bin: ``int`` Number of actions for each renewable generator min_value: ``float`` - Between 0. and 1.: minimum value allow for the curtailment. FOr example if you set this + Between 0. and 1.: minimum value allow for the curtailment. For example if you set this value to be 0.2 then no curtailment will be done to limit the generator below 20% of its maximum capacity Returns @@ -1255,7 +1409,7 @@ def get_all_unitary_curtail(action_space, num_bin=10, min_value=0.5): return res @staticmethod - def get_all_unitary_storage(action_space, num_down=5, num_up=5): + def get_all_unitary_storage(action_space: Self, num_down: int =5, num_up: int=5) -> List[BaseAction]: """ Storage action are continuous action. This method is an helper to convert the continuous action into discrete action (by rounding). @@ -1269,10 +1423,15 @@ def get_all_unitary_storage(action_space, num_down=5, num_up=5): a distinct action (then counting `num_down` different action, because 0.0 is removed) - it will do the same for [0, storage_max_p_absorb] + .. note:: + With this "helper" only one storage unit is affected by one action. For example + there are no action acting on both storage unit 1 and storage unit 2 at the same + time. + Parameters ---------- - action_space: :class:`grid2op.BaseAction.ActionHelper` + action_space: :class:`ActionSpace` The action space used. Returns @@ -1509,8 +1668,8 @@ def _aux_get_back_to_ref_state_storage( def get_back_to_ref_state( self, obs: "grid2op.Observation.BaseObservation", - storage_setpoint=0.5, - precision=5, + storage_setpoint: float=0.5, + precision: int=5, ) -> Dict[str, List[BaseAction]]: """ This function returns the list of unary actions that you can perform in order to get back to the "fully meshed" / "initial" topology. @@ -1525,8 +1684,8 @@ def get_back_to_ref_state( - an action that acts on a single powerline - an action on a single substation - - a redispatching action - - a storage action + - a redispatching action (acting possibly on all generators) + - a storage action (acting possibly on all generators) The list might be relatively long, in the case where lots of actions are needed. Depending on the rules of the game (for example limiting the action on one single substation), in order to get back to this topology, multiple consecutive actions will need to be implemented. @@ -1536,7 +1695,7 @@ def get_back_to_ref_state( - "powerline" for the list of actions needed to set back the powerlines in a proper state (connected). They can be of type "change_line" or "set_line". - "substation" for the list of actions needed to set back each substation in its initial state (everything connected to bus 1). They can be implemented as "set_bus" or "change_bus" - - "redispatching": for the redispatching action (there can be multiple redispatching actions needed because of the ramps of the generator) + - "redispatching": for the redispatching actions (there can be multiple redispatching actions needed because of the ramps of the generator) - "storage": for action on storage units (you might need to perform multiple storage actions because of the maximum power these units can absorb / produce ) - "curtailment": for curtailment action (usually at most one such action is needed) @@ -1574,7 +1733,6 @@ def get_back_to_ref_state( "You need to provide a grid2op Observation for this function to work correctly." ) res = {} - # powerline actions self._aux_get_back_to_ref_state_line(res, obs) # substations diff --git a/grid2op/tests/test_n_busbar_per_sub.py b/grid2op/tests/test_n_busbar_per_sub.py index ff3ce9c6f..ce252a2e0 100644 --- a/grid2op/tests/test_n_busbar_per_sub.py +++ b/grid2op/tests/test_n_busbar_per_sub.py @@ -6,8 +6,6 @@ # 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 os import PathLike -from typing import Optional, Union import warnings import unittest from grid2op.tests.helper_path_test import * @@ -24,6 +22,9 @@ import pdb +HAS_TIME_AND_MEMORY = False # test on a big computer only with lots of RAM, and lots of time available... + + class _AuxFakeBackendSupport(PandaPowerBackend): def cannot_handle_more_than_2_busbar(self): """dont do it at home !""" @@ -47,7 +48,15 @@ def cannot_handle_more_than_2_busbar(self): class TestRightNumber(unittest.TestCase): """This test that, when changing n_busbar in make it is - back propagated where it needs""" + back propagated where it needs in the class attribute (this includes + testing that the observation_space, action_space, runner, environment etc. + are all 'informed' about this feature) + + This class also tests than when the implementation of the backend does not + use the new `can_handle_more_than_2_busbar` or `cannot_handle_more_than_2_busbar` + then the legacy behaviour is used (only 2 busbar per substation even if the + user asked for a different number) + """ def _aux_fun_test(self, env, n_busbar): assert type(env).n_busbar_per_sub == n_busbar, f"type(env).n_busbar_per_sub = {type(env).n_busbar_per_sub} != {n_busbar}" assert type(env.backend).n_busbar_per_sub == n_busbar, f"env.backend).n_busbar_per_sub = {type(env.backend).n_busbar_per_sub} != {n_busbar}" @@ -60,9 +69,9 @@ def _aux_fun_test(self, env, n_busbar): def test_fail_if_not_int(self): with self.assertRaises(Grid2OpException): - env = grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendSupport(), test=True, n_busbar="froiy", _add_to_name=type(self).__name__+"_wrong") + env = grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendSupport(), test=True, n_busbar="froiy", _add_to_name=type(self).__name__+"_wrong_str") with self.assertRaises(Grid2OpException): - env = grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendSupport(), test=True, n_busbar=3.5, _add_to_name=type(self).__name__+"_wrong") + env = grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendSupport(), test=True, n_busbar=3.5, _add_to_name=type(self).__name__+"_wrong_float") def test_regular_env(self): with warnings.catch_warnings(): @@ -75,6 +84,11 @@ def test_regular_env(self): env = grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendSupport(), test=True, n_busbar=3, _add_to_name=type(self).__name__+"_3") self._aux_fun_test(env, 3) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendSupport(), test=True, n_busbar=1, _add_to_name=type(self).__name__+"_3") + self._aux_fun_test(env, 1) + def test_multimix_env(self): with warnings.catch_warnings(): warnings.filterwarnings("ignore") @@ -86,6 +100,11 @@ def test_multimix_env(self): env = grid2op.make("l2rpn_neurips_2020_track2", backend=_AuxFakeBackendSupport(), test=True, n_busbar=3, _add_to_name=type(self).__name__+"_3") self._aux_fun_test(env, 3) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make("l2rpn_neurips_2020_track2", backend=_AuxFakeBackendSupport(), test=True, n_busbar=1, _add_to_name=type(self).__name__+"_3") + self._aux_fun_test(env, 1) + def test_masked_env(self): with warnings.catch_warnings(): warnings.filterwarnings("ignore") @@ -99,6 +118,12 @@ def test_masked_env(self): lines_of_interest=np.ones(shape=20, dtype=bool)) self._aux_fun_test(env, 3) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = MaskedEnvironment(grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendSupport(), test=True, n_busbar=1, _add_to_name=type(self).__name__+"_mask_1"), + lines_of_interest=np.ones(shape=20, dtype=bool)) + self._aux_fun_test(env, 1) + def test_to_env(self): with warnings.catch_warnings(): warnings.filterwarnings("ignore") @@ -111,6 +136,12 @@ def test_to_env(self): env = TimedOutEnvironment(grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendSupport(), test=True, n_busbar=3, _add_to_name=type(self).__name__+"_to_3"), time_out_ms=3000) self._aux_fun_test(env, 3) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = TimedOutEnvironment(grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendSupport(), test=True, n_busbar=1, _add_to_name=type(self).__name__+"_to_1"), + time_out_ms=3000) + self._aux_fun_test(env, 1) def test_xxxhandle_more_than_2_busbar_not_called(self): """when using a backend that did not called the `can_handle_more_than_2_busbar_not_called` @@ -125,6 +156,11 @@ def test_xxxhandle_more_than_2_busbar_not_called(self): warnings.filterwarnings("ignore") env = grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendNoCalled(), test=True, n_busbar=3, _add_to_name=type(self).__name__+"_nocall_3") self._aux_fun_test(env, DEFAULT_N_BUSBAR_PER_SUB) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendNoCalled(), test=True, n_busbar=1, _add_to_name=type(self).__name__+"_nocall_1") + self._aux_fun_test(env, DEFAULT_N_BUSBAR_PER_SUB) def test_cannot_handle_more_than_2_busbar_not_called(self): """when using a backend that called `cannot_handle_more_than_2_busbar_not_called` then it's equivalent @@ -137,7 +173,12 @@ def test_cannot_handle_more_than_2_busbar_not_called(self): with warnings.catch_warnings(): warnings.filterwarnings("ignore") env = grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendNoSupport(), test=True, n_busbar=3, _add_to_name=type(self).__name__+"_dontcalled_3") - self._aux_fun_test(env, 2) + self._aux_fun_test(env, DEFAULT_N_BUSBAR_PER_SUB) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendNoSupport(), test=True, n_busbar=1, _add_to_name=type(self).__name__+"_dontcalled_1") + self._aux_fun_test(env, DEFAULT_N_BUSBAR_PER_SUB) def test_env_copy(self): """test env copy does work correctly""" @@ -155,6 +196,13 @@ def test_env_copy(self): env_cpy = env.copy() self._aux_fun_test(env_cpy, 3) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendSupport(), test=True, n_busbar=1, _add_to_name=type(self).__name__+"_copy_1") + self._aux_fun_test(env, 1) + env_cpy = env.copy() + self._aux_fun_test(env_cpy, 1) + def test_two_env_same_name(self): """test i can load 2 env with the same name but different n_busbar""" with warnings.catch_warnings(): @@ -167,6 +215,13 @@ def test_two_env_same_name(self): env_3 = grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendSupport(), test=True, n_busbar=3, _add_to_name=type(self).__name__+"_same_name") self._aux_fun_test(env_3, 3) # check env_3 has indeed 3 buses self._aux_fun_test(env_2, DEFAULT_N_BUSBAR_PER_SUB) # check env_2 is not modified + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env_1 = grid2op.make("l2rpn_case14_sandbox", backend=_AuxFakeBackendSupport(), test=True, n_busbar=1, _add_to_name=type(self).__name__+"_same_name") + self._aux_fun_test(env_1, 1) # check env_1 has indeed 3 buses + self._aux_fun_test(env_3, 3) # check env_3 is not modified + self._aux_fun_test(env_2, DEFAULT_N_BUSBAR_PER_SUB) # check env_2 is not modified class _TestAgentRightNBus(BaseAgent): @@ -261,6 +316,7 @@ def test_two_process(self): class TestGridObjt(unittest.TestCase): + """Test that the GridObj class is fully compatible with this feature""" def setUp(self) -> None: with warnings.catch_warnings(): warnings.filterwarnings("ignore") @@ -276,6 +332,7 @@ def tearDown(self) -> None: return super().tearDown() def test_global_bus_to_local_int(self): + """test the function :func:`grid2op.Space.GridObjects.global_bus_to_local_int` """ cls_env = type(self.env) # easy case: everything on bus 1 res = cls_env.global_bus_to_local_int(cls_env.gen_to_subid[0], cls_env.gen_to_subid[0]) @@ -301,6 +358,7 @@ def test_global_bus_to_local_int(self): res = cls_env.global_bus_to_local_int(cls_env.gen_to_subid[gen_on_4] + 3 * cls_env.n_sub, cls_env.gen_to_subid[gen_on_4]) def test_global_bus_to_local(self): + """test the function :func:`grid2op.Space.GridObjects.global_bus_to_local` """ cls_env = type(self.env) # easy case: everything on bus 1 res = cls_env.global_bus_to_local(cls_env.gen_to_subid, cls_env.gen_to_subid) @@ -346,6 +404,7 @@ def test_global_bus_to_local(self): assert (res == vect).all() def test_local_bus_to_global_int(self): + """test the function :func:`grid2op.Space.GridObjects.local_bus_to_global_int` """ cls_env = type(self.env) # easy case: everything on bus 1 res = cls_env.local_bus_to_global_int(1, cls_env.gen_to_subid[0]) @@ -367,6 +426,7 @@ def test_local_bus_to_global_int(self): assert res == cls_env.gen_to_subid[gen_on_3] + 2 * cls_env.n_sub def test_local_bus_to_global(self): + """test the function :func:`grid2op.Space.GridObjects.local_bus_to_global` """ cls_env = type(self.env) # easy case: everything on bus 1 res = cls_env.local_bus_to_global(np.ones(cls_env.n_gen, dtype=int), cls_env.gen_to_subid) @@ -408,6 +468,9 @@ def test_local_bus_to_global(self): class TestAction_3busbars(unittest.TestCase): + """This class test the Agent can perform actions (and that actions are properly working) + even if there are 3 busbars per substation + """ def get_nb_bus(self): return 3 @@ -561,15 +624,279 @@ def test_shunt(self): with self.assertRaises(IllegalAction): act = self.env.action_space({"shunt": {"set_bus": [(el_id, type(self.env).n_busbar_per_sub + 1)]}}) + class TestAction_1busbar(TestAction_3busbars): + """This class test the Agent can perform actions (and that actions are properly working) + even if there is only 1 busbar per substation + """ def get_nb_bus(self): return 1 class TestActionSpace(unittest.TestCase): - pass + """This function test the action space, basically the counting + of unique possible topologies per substation + """ + def get_nb_bus(self): + return 3 + + def get_env_nm(self): + return "educ_case14_storage" + + def setUp(self) -> None: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make(self.get_env_nm(), + backend=_AuxFakeBackendSupport(), + action_class=CompleteAction, + test=True, + n_busbar=self.get_nb_bus(), + _add_to_name=type(self).__name__ + f'_{self.get_nb_bus()}') + return super().setUp() + + def tearDown(self) -> None: + self.env.close() + return super().tearDown() + + def test_legacy_all_unitary_topologies_set_behaviour(self): + """make sure nothing broke for 2 busbars per substation even if the implementation changes""" + class SubMe(TestActionSpace): + def get_nb_bus(self): + return 2 + + tmp = SubMe() + tmp.setUp() + res = tmp.env.action_space.get_all_unitary_topologies_set(tmp.env.action_space, _count_only=True) + res_noalone = tmp.env.action_space.get_all_unitary_topologies_set(tmp.env.action_space, + add_alone_line=False, + _count_only=True) + tmp.tearDown() + assert res == [3, 29, 5, 31, 15, 113, 4, 0, 15, 3, 3, 3, 7, 3], f"found: {res}" + assert res_noalone == [0, 25, 3, 26, 11, 109, 0, 0, 11, 0, 0, 0, 4, 0], f"found: {res_noalone}" + + class SubMe2(TestActionSpace): + def get_nb_bus(self): + return 2 + def get_env_nm(self): + return "l2rpn_idf_2023" + tmp2 = SubMe2() + tmp2.setUp() + res = tmp2.env.action_space.get_all_unitary_topologies_set(tmp2.env.action_space, _count_only=True) + res_noalone = tmp2.env.action_space.get_all_unitary_topologies_set(tmp2.env.action_space, + add_alone_line=False, + _count_only=True) + tmp2.tearDown() + assert res == [3, 3, 7, 9, 16, 3, 3, 13, 2, 0, 57, 253, 3, 3, 241, 3, 63, 5, 29, 3, + 3, 3, 29, 7, 7, 3, 57, 3, 3, 8, 7, 31, 3, 29, 3, 3, 32, 4, 3, 29, 3, + 113, 3, 3, 13, 13, 7, 3, 65505, 3, 7, 3, 3, 125, 13, 497, 3, 3, 505, + 13, 15, 57, 2, 4, 15, 61, 3, 8, 63, 121, 4, 3, 0, 3, 31, 5, 1009, 3, + 3, 1017, 2, 7, 13, 3, 61, 3, 0, 3, 63, 25, 3, 253, 3, 31, 3, 61, 3, + 3, 3, 2033, 3, 3, 15, 13, 61, 7, 5, 3, 3, 15, 0, 0, 9, 3, 3, 0, 0, 3], f"found: {res}" + assert res_noalone == [0, 0, 4, 7, 11, 0, 0, 10, 0, 0, 53, 246, 0, 0, 236, 0, 57, 3, + 25, 0, 0, 0, 25, 4, 4, 0, 53, 0, 0, 4, 4, 26, 0, 25, 0, 0, 26, + 0, 0, 25, 0, 109, 0, 0, 10, 10, 4, 0, 65493, 0, 4, 0, 0, 119, + 10, 491, 0, 0, 498, 10, 11, 53, 0, 0, 11, 56, 0, 4, 57, 116, + 0, 0, 0, 0, 26, 3, 1002, 0, 0, 1009, 0, 4, 10, 0, 56, 0, 0, + 0, 57, 22, 0, 246, 0, 26, 0, 56, 0, 0, 0, 2025, 0, 0, 11, 10, + 56, 4, 3, 0, 0, 11, 0, 0, 7, 0, 0, 0, 0, 0], f"found: {res_noalone}" + + def test_is_ok_symmetry(self): + """test the :func:`grid2op.Action.SerializableActionSpace._is_ok_symmetry`""" + ok = np.array([1, 1, 1, 1]) + assert type(self.env.action_space)._is_ok_symmetry(2, ok), f"should not break for {ok}" + ok = np.array([1, 2, 1, 1]) + assert type(self.env.action_space)._is_ok_symmetry(2, ok), f"should not break for {ok}" + ok = np.array([1, 2, 3, 1]) + assert type(self.env.action_space)._is_ok_symmetry(3, ok), f"should not break for {ok}" + ok = np.array([1, 1, 2, 3]) + assert type(self.env.action_space)._is_ok_symmetry(3, ok), f"should not break for {ok}" + ok = np.array([1, 1, 2, 2]) + assert type(self.env.action_space)._is_ok_symmetry(4, ok), f"should not break for {ok}" + + ko = np.array([1, 3, 2, 1]) # relabel 3 -> 2, so this topology is not valid + assert not type(self.env.action_space)._is_ok_symmetry(3, ko), f"should break for {ko}" + ko = np.array([1, 1, 3, 2]) # relabel 3 -> 2, so this topology is not valid + assert not type(self.env.action_space)._is_ok_symmetry(3, ko), f"should break for {ko}" + + ko = np.array([1, 3, 2, 1]) # relabel 3 -> 2, so this topology is not valid + assert not type(self.env.action_space)._is_ok_symmetry(4, ko), f"should break for {ko}" + ko = np.array([1, 1, 3, 2]) # relabel 3 -> 2, so this topology is not valid + assert not type(self.env.action_space)._is_ok_symmetry(4, ko), f"should break for {ko}" + + def test_is_ok_line(self): + """test the :func:`grid2op.Action.SerializableActionSpace._is_ok_line`""" + lines_id = np.array([1, 3]) + n_busbar_per_sub = 2 + ok = np.array([1, 1, 1, 1]) + assert type(self.env.action_space)._is_ok_line(n_busbar_per_sub, ok, lines_id), f"should not break for {ok}" + ok = np.array([1, 2, 2, 1]) + assert type(self.env.action_space)._is_ok_line(n_busbar_per_sub, ok, lines_id), f"should not break for {ok}" + ko = np.array([1, 2, 1, 2]) # no lines on bus 1 + assert not type(self.env.action_space)._is_ok_line(n_busbar_per_sub, ko, lines_id), f"should break for {ko}" + + n_busbar_per_sub = 3 # should have no impact + ok = np.array([1, 1, 1, 1]) + assert type(self.env.action_space)._is_ok_line(n_busbar_per_sub, ok, lines_id), f"should not break for {ok}" + ok = np.array([1, 2, 2, 1]) + assert type(self.env.action_space)._is_ok_line(n_busbar_per_sub, ok, lines_id), f"should not break for {ok}" + ko = np.array([1, 2, 1, 2]) # no lines on bus 1 + assert not type(self.env.action_space)._is_ok_line(n_busbar_per_sub, ko, lines_id), f"should break for {ko}" + + def test_2_obj_per_bus(self): + """test the :func:`grid2op.Action.SerializableActionSpace._is_ok_2`""" + n_busbar_per_sub = 2 + ok = np.array([1, 1, 1, 1]) + assert type(self.env.action_space)._is_ok_2(n_busbar_per_sub, ok), f"should not break for {ok}" + ok = np.array([1, 2, 2, 1]) + assert type(self.env.action_space)._is_ok_2(n_busbar_per_sub, ok), f"should not break for {ok}" + ok = np.array([1, 2, 1, 2]) + assert type(self.env.action_space)._is_ok_2(n_busbar_per_sub, ok), f"should not break for {ok}" + + ko = np.array([1, 2, 2, 2]) # only 1 element on bus 1 + assert not type(self.env.action_space)._is_ok_2(n_busbar_per_sub, ko), f"should break for {ko}" + ko = np.array([1, 2, 1, 1]) # only 1 element on bus 2 + assert not type(self.env.action_space)._is_ok_2(n_busbar_per_sub, ko), f"should break for {ko}" + ko = np.array([1, 1, 2, 2, 3]) # only 1 element on bus 3 + assert not type(self.env.action_space)._is_ok_2(n_busbar_per_sub, ko), f"should break for {ko}" + + n_busbar_per_sub = 3 + ok = np.array([1, 1, 1, 1]) + assert type(self.env.action_space)._is_ok_2(n_busbar_per_sub, ok), f"should not break for {ok}" + ok = np.array([1, 2, 2, 1]) + assert type(self.env.action_space)._is_ok_2(n_busbar_per_sub, ok), f"should not break for {ok}" + ok = np.array([1, 2, 1, 2]) + assert type(self.env.action_space)._is_ok_2(n_busbar_per_sub, ok), f"should not break for {ok}" + + ko = np.array([1, 2, 2, 2]) # only 1 element on bus 1 + assert not type(self.env.action_space)._is_ok_2(n_busbar_per_sub, ko), f"should break for {ko}" + ko = np.array([1, 2, 1, 1]) # only 1 element on bus 2 + assert not type(self.env.action_space)._is_ok_2(n_busbar_per_sub, ko), f"should break for {ko}" + ko = np.array([1, 1, 2, 2, 3]) # only 1 element on bus 3 + assert not type(self.env.action_space)._is_ok_2(n_busbar_per_sub, ko), f"should break for {ko}" + def test_1_busbar(self): + """test :func:`grid2op.Action.SerializableActionSpace.get_all_unitary_topologies_set` + when there are only 1 busbar per substation""" + class SubMe(TestActionSpace): + def get_nb_bus(self): + return 1 + + tmp = SubMe() + tmp.setUp() + res = [len(tmp.env.action_space.get_all_unitary_topologies_set(tmp.env.action_space, + sub_id)) + for sub_id in range(type(tmp.env).n_sub)] + res_noalone = [len(tmp.env.action_space.get_all_unitary_topologies_set(tmp.env.action_space, + sub_id, + add_alone_line=False)) + for sub_id in range(type(tmp.env).n_sub)] + tmp.tearDown() + assert res == [0] * 14, f"found: {res}" + assert res_noalone == [0] * 14, f"found: {res_noalone}" + + class SubMe2(TestActionSpace): + def get_nb_bus(self): + return 1 + def get_env_nm(self): + return "l2rpn_idf_2023" + + tmp2 = SubMe2() + tmp2.setUp() + res = [len(tmp2.env.action_space.get_all_unitary_topologies_set(tmp2.env.action_space, + sub_id)) + for sub_id in range(type(tmp2.env).n_sub)] + res_noalone = [len(tmp2.env.action_space.get_all_unitary_topologies_set(tmp2.env.action_space, + sub_id, + add_alone_line=False)) + for sub_id in range(type(tmp2.env).n_sub)] + tmp2.tearDown() + assert res == [0] * 118, f"found: {res}" + assert res_noalone == [0] * 118, f"found: {res_noalone}" + def test_3_busbars(self): + """test :func:`grid2op.Action.SerializableActionSpace.get_all_unitary_topologies_set` + when there are 3 busbars per substation""" + res = self.env.action_space.get_all_unitary_topologies_set(self.env.action_space, + _count_only=True) + res_noalone = self.env.action_space.get_all_unitary_topologies_set(self.env.action_space, + add_alone_line=False, + _count_only=True) + assert res == [3, 83, 5, 106, 33, 599, 5, 0, 33, 3, 3, 3, 10, 3], f"found: {res}" + assert res_noalone == [0, 37, 3, 41, 11, 409, 0, 0, 11, 0, 0, 0, 4, 0], f"found: {res_noalone}" + class SubMe2(TestActionSpace): + def get_nb_bus(self): + return 3 + def get_env_nm(self): + return "l2rpn_idf_2023" + tmp2 = SubMe2() + tmp2.setUp() + th_vals = [0, 0, 4, 7, 11, 0, 0, 10, 0, 0, 125, 2108, 0, 0, 1711, 0, 162, 3, 37, 0, 0, 0, 37, + 4, 4, 0, 125, 0, 0, 4, 4, 41, 0, 37, 0, 0, 41, 0, 0, 37, 0, 409, 0, 0, 10, 10, 4, 0] + for sub_id, th_val in zip(list(range(48)), th_vals): + res_noalone = tmp2.env.action_space.get_all_unitary_topologies_set(tmp2.env.action_space, + sub_id=sub_id, + add_alone_line=False, + _count_only=True) + assert res_noalone[0] == th_val, f"error for sub_id {sub_id}: {res_noalone} vs {th_val}" + + if HAS_TIME_AND_MEMORY: + # takes 850s (13 minutes) + res_noalone = tmp2.env.action_space.get_all_unitary_topologies_set(tmp2.env.action_space, + sub_id=48, + add_alone_line=False, + _count_only=True) + assert res_noalone == 20698545, f"error for sub_id {48}: {res_noalone}" + tmp2.tearDown() + + def test_legacy_all_unitary_line_set_behaviour(self): + """make sure nothing broke for 2 busbars per substation even if the implementation changes""" + class SubMe(TestActionSpace): + def get_nb_bus(self): + return 2 + + tmp = SubMe() + tmp.setUp() + res = len(tmp.env.action_space.get_all_unitary_line_set(tmp.env.action_space)) + res_simple = len(tmp.env.action_space.get_all_unitary_line_set_simple(tmp.env.action_space)) + tmp.tearDown() + assert res == 5 * 20, f"found: {res}" + assert res_simple == 2 * 20, f"found: {res_simple}" + + class SubMe2(TestActionSpace): + def get_nb_bus(self): + return 2 + def get_env_nm(self): + return "l2rpn_idf_2023" + + tmp2 = SubMe2() + tmp2.setUp() + res = len(tmp2.env.action_space.get_all_unitary_line_set(tmp2.env.action_space)) + res_simple = len(tmp2.env.action_space.get_all_unitary_line_set_simple(tmp2.env.action_space)) + tmp2.tearDown() + assert res == 5 * 186, f"found: {res}" + assert res_simple == 2 * 186, f"found: {res_simple}" + + def test_get_all_unitary_line_set(self): + """test the :func:`grid2op.Action.SerializableActionSpace.get_all_unitary_line_set` when 3 busbars""" + res = len(self.env.action_space.get_all_unitary_line_set(self.env.action_space)) + assert res == (1 + 3*3) * 20, f"found: {res}" + res = len(self.env.action_space.get_all_unitary_line_set_simple(self.env.action_space)) + assert res == 2 * 20, f"found: {res}" + class SubMe2(TestActionSpace): + def get_nb_bus(self): + return 3 + def get_env_nm(self): + return "l2rpn_idf_2023" + + tmp2 = SubMe2() + tmp2.setUp() + res = len(tmp2.env.action_space.get_all_unitary_line_set(tmp2.env.action_space)) + res_simple = len(tmp2.env.action_space.get_all_unitary_line_set_simple(tmp2.env.action_space)) + tmp2.tearDown() + assert res == (1 + 3*3) * 186, f"found: {res}" + assert res_simple == 2 * 186, f"found: {res_simple}" + + class TestBackendAction(unittest.TestCase): pass @@ -578,6 +905,12 @@ class TestPandapowerBackend(unittest.TestCase): pass +class TestObservation(unittest.TestCase): + def test_action_space_get_back_to_ref_state(self): + """test the :func:`grid2op.Action.SerializableActionSpace.get_back_to_ref_state` + when 3 busbars which could not be tested without observation""" + pass + if __name__ == "__main__": unittest.main() \ No newline at end of file From 4461fae4b81f171d69159d43a5bc598f0d208ca6 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 2 Feb 2024 14:13:51 +0100 Subject: [PATCH 09/24] fix a bug in the shunt modification --- CHANGELOG.rst | 2 ++ grid2op/Action/baseAction.py | 21 +++++++++++---------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c5238cbf7..8a41a84c0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -43,6 +43,8 @@ Change Log - [FIXED] a bug in `act.get_gen_modif` (vector of wrong size was used, could lead to some crashes if n_gen >= n_load) - [FIXED] a bug in `act.as_dict` when shunts were modified +- [FIXED] a bug affecting shunts: sometimes it was not possible to modify their p / q + values for certain values of p or q (an AmbiguousAction exception was raised wrongly) - [IMPROVED] handling of "compatibility" grid2op version (by calling the relevant things done in the base class in `BaseAction` and `BaseObservation`) and by using the `from packaging import version` diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 20347fbe3..bbd5266b3 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -1843,16 +1843,17 @@ def _digest_shunt(self, dict_): "Invalid shunt id {}. Shunt id should be less than the number " "of shunt {}".format(sh_id, cls.n_shunt) ) - if new_bus <= -2: - raise IllegalAction( - f"Cannot ask for a shunt id <= 2, found {new_bus} for shunt id {sh_id}" - ) - elif new_bus > cls.n_busbar_per_sub: - raise IllegalAction( - f"Cannot ask for a shunt id > {cls.n_busbar_per_sub} " - f"the maximum number of busbar per substations" - f", found {new_bus} for shunt id {sh_id}" - ) + if key_n == "shunt_bus" or key_n == "set_bus": + if new_bus <= -2: + raise IllegalAction( + f"Cannot ask for a shunt bus <= 2, found {new_bus} for shunt id {sh_id}" + ) + elif new_bus > cls.n_busbar_per_sub: + raise IllegalAction( + f"Cannot ask for a shunt bus > {cls.n_busbar_per_sub} " + f"the maximum number of busbar per substations" + f", found {new_bus} for shunt id {sh_id}" + ) vect_self[sh_id] = new_bus elif tmp is None: From 7e615aeda1edba86a691ac15292a0c959a01b39b Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 2 Feb 2024 15:26:13 +0100 Subject: [PATCH 10/24] testing support in BackendAction --- CHANGELOG.rst | 2 + grid2op/Action/_backendAction.py | 40 ++++++----- grid2op/Action/baseAction.py | 2 +- grid2op/Environment/baseEnv.py | 48 +++++++++----- grid2op/Exceptions/__init__.py | 3 + grid2op/Exceptions/backendExceptions.py | 7 ++ grid2op/tests/test_Agent.py | 3 + grid2op/tests/test_n_busbar_per_sub.py | 88 ++++++++++++++++++++++++- 8 files changed, 155 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8a41a84c0..1ec02203e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -45,6 +45,8 @@ Change Log - [FIXED] a bug in `act.as_dict` when shunts were modified - [FIXED] a bug affecting shunts: sometimes it was not possible to modify their p / q values for certain values of p or q (an AmbiguousAction exception was raised wrongly) +- [FIXED] a bug in the `_BackendAction`: the "last known topoolgy" was not properly computed + in some cases (especially at the time where a line was reconnected) - [IMPROVED] handling of "compatibility" grid2op version (by calling the relevant things done in the base class in `BaseAction` and `BaseObservation`) and by using the `from packaging import version` diff --git a/grid2op/Action/_backendAction.py b/grid2op/Action/_backendAction.py index 0e60d9c05..15fb93f3b 100644 --- a/grid2op/Action/_backendAction.py +++ b/grid2op/Action/_backendAction.py @@ -205,6 +205,10 @@ def force_unchanged(self, mask, local_bus): to_unchanged = local_bus == -1 to_unchanged[~mask] = False self.changed[to_unchanged] = False + + def register_new_topo(self, current_topo: "ValueStore"): + mask_co = current_topo.values >= 1 + self.values[mask_co] = current_topo.values[mask_co] class _BackendAction(GridObjects): @@ -221,39 +225,39 @@ def __init__(self): GridObjects.__init__(self) cls = type(self) # last connected registered - self.last_topo_registered = ValueStore(cls.dim_topo, dtype=dt_int) + self.last_topo_registered: ValueStore = ValueStore(cls.dim_topo, dtype=dt_int) # topo at time t - self.current_topo = ValueStore(cls.dim_topo, dtype=dt_int) + self.current_topo: ValueStore = ValueStore(cls.dim_topo, dtype=dt_int) # by default everything is on busbar 1 self.last_topo_registered.values[:] = 1 self.current_topo.values[:] = 1 # injection at time t - self.prod_p = ValueStore(cls.n_gen, dtype=dt_float) - self.prod_v = ValueStore(cls.n_gen, dtype=dt_float) - self.load_p = ValueStore(cls.n_load, dtype=dt_float) - self.load_q = ValueStore(cls.n_load, dtype=dt_float) - self.storage_power = ValueStore(cls.n_storage, dtype=dt_float) + self.prod_p: ValueStore = ValueStore(cls.n_gen, dtype=dt_float) + self.prod_v: ValueStore = ValueStore(cls.n_gen, dtype=dt_float) + self.load_p: ValueStore = ValueStore(cls.n_load, dtype=dt_float) + self.load_q: ValueStore = ValueStore(cls.n_load, dtype=dt_float) + self.storage_power: ValueStore = ValueStore(cls.n_storage, dtype=dt_float) self.activated_bus = np.full((cls.n_sub, cls.n_busbar_per_sub), dtype=dt_bool, fill_value=False) - self.big_topo_to_subid = np.repeat( + self.big_topo_to_subid: np.ndarray = np.repeat( list(range(cls.n_sub)), repeats=cls.sub_info ) # shunts if cls.shunts_data_available: - self.shunt_p = ValueStore(cls.n_shunt, dtype=dt_float) - self.shunt_q = ValueStore(cls.n_shunt, dtype=dt_float) - self.shunt_bus = ValueStore(cls.n_shunt, dtype=dt_int) - self.current_shunt_bus = ValueStore(cls.n_shunt, dtype=dt_int) + self.shunt_p: ValueStore = ValueStore(cls.n_shunt, dtype=dt_float) + self.shunt_q: ValueStore = ValueStore(cls.n_shunt, dtype=dt_float) + self.shunt_bus: ValueStore = ValueStore(cls.n_shunt, dtype=dt_int) + self.current_shunt_bus: ValueStore = ValueStore(cls.n_shunt, dtype=dt_int) self.current_shunt_bus.values[:] = 1 - self._status_or_before = np.ones(cls.n_line, dtype=dt_int) - self._status_ex_before = np.ones(cls.n_line, dtype=dt_int) - self._status_or = np.ones(cls.n_line, dtype=dt_int) - self._status_ex = np.ones(cls.n_line, dtype=dt_int) + self._status_or_before: np.ndarray = np.ones(cls.n_line, dtype=dt_int) + self._status_ex_before: np.ndarray = np.ones(cls.n_line, dtype=dt_int) + self._status_or: np.ndarray = np.ones(cls.n_line, dtype=dt_int) + self._status_ex: np.ndarray = np.ones(cls.n_line, dtype=dt_int) self._loads_bus = None self._gens_bus = None @@ -323,7 +327,7 @@ def reorder(self, no_load, no_gen, no_topo, no_storage, no_shunt) -> None: self.current_shunt_bus.reorder(no_shunt) def reset(self) -> None: - # last topo + # last known topo self.last_topo_registered.reset() # topo at time t @@ -346,6 +350,8 @@ def reset(self) -> None: self.shunt_q.reset() self.shunt_bus.reset() self.current_shunt_bus.reset() + + self.last_topo_registered.register_new_topo(self.current_topo) def all_changed(self) -> None: # last topo diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index bbd5266b3..4fca6bb79 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -1846,7 +1846,7 @@ def _digest_shunt(self, dict_): if key_n == "shunt_bus" or key_n == "set_bus": if new_bus <= -2: raise IllegalAction( - f"Cannot ask for a shunt bus <= 2, found {new_bus} for shunt id {sh_id}" + f"Cannot ask for a shunt bus <= -2, found {new_bus} for shunt id {sh_id}" ) elif new_bus > cls.n_busbar_per_sub: raise IllegalAction( diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index 10f2d24e2..29339ecd7 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -36,7 +36,8 @@ InvalidRedispatching, GeneratorTurnedOffTooSoon, GeneratorTurnedOnTooSoon, - AmbiguousActionRaiseAlert) + AmbiguousActionRaiseAlert, + ImpossibleTopology) from grid2op.Parameters import Parameters from grid2op.Reward import BaseReward, RewardHelper from grid2op.Opponent import OpponentSpace, NeverAttackBudget, BaseOpponent @@ -523,11 +524,11 @@ def __init__( self._voltage_controler = None # backend action - self._backend_action_class = None - self._backend_action = None + self._backend_action_class : type = None + self._backend_action : _BackendAction = None # specific to Basic Env, do not change - self.backend :Backend = None + self.backend : Backend = None self.__is_init = False self.debug_dispatch = False @@ -2949,10 +2950,14 @@ def _aux_apply_redisp(self, action, new_p, new_p_th, gen_curtailed, except_): res_action = action return res_action, is_illegal_redisp, is_illegal_reco, is_done - def _aux_update_backend_action(self, action, action_storage_power, init_disp): + def _aux_update_backend_action(self, + action: BaseAction, + action_storage_power: np.ndarray, + init_disp: np.ndarray): # make sure the dispatching action is not implemented "as is" by the backend. # the environment must make sure it's a zero-sum action. # same kind of limit for the storage + res_exc_ = None action._redispatch[:] = 0.0 action._storage_power[:] = self._storage_power self._backend_action += action @@ -2961,6 +2966,7 @@ def _aux_update_backend_action(self, action, action_storage_power, init_disp): # TODO storage: check the original action, even when replaced by do nothing is not modified self._backend_action += self._env_modification self._backend_action.set_redispatch(self._actual_dispatch) + return res_exc_ def _update_alert_properties(self, action, lines_attacked, subs_attacked): # update the environment with the alert information from the @@ -3230,6 +3236,7 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, float, bool, dict]: beg_step = time.perf_counter() self._last_obs : Optional[BaseObservation] = None self._forecasts = None # force reading the forecast from the time series + cls = type(self) try: beg_ = time.perf_counter() @@ -3243,12 +3250,12 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, float, bool, dict]: ) # battery information is_ambiguous = True - if type(self).dim_alerts > 0: + if cls.dim_alerts > 0: # keep the alert even if the rest is ambiguous (if alert is non ambiguous) is_ambiguous_alert = isinstance(except_tmp, AmbiguousActionRaiseAlert) if is_ambiguous_alert: # reset the alert - init_alert = np.zeros(type(self).dim_alerts, dtype=dt_bool) + init_alert = np.zeros(cls.dim_alerts, dtype=dt_bool) else: action.raise_alert = init_alert except_.append(except_tmp) @@ -3262,13 +3269,13 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, float, bool, dict]: 1.0 * action._storage_power ) # battery information except_.append(reason) - if type(self).dim_alerts > 0: + if cls.dim_alerts > 0: # keep the alert even if the rest is illegal action.raise_alert = init_alert is_illegal = True if self._has_attention_budget: - if type(self).assistant_warning_type == "zonal": + if cls.assistant_warning_type == "zonal": # this feature is implemented, so i do it reason_alarm_illegal = self._attention_budget.register_action( self, action, is_illegal, is_ambiguous @@ -3284,7 +3291,7 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, float, bool, dict]: new_p_th = 1.0 * new_p # storage unit - if self.n_storage > 0: + if cls.n_storage > 0: # limiting the storage units is done in `_aux_apply_redisp` # this only ensure the Emin / Emax and all the actions self._compute_storage(action_storage_power) @@ -3295,7 +3302,7 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, float, bool, dict]: gen_curtailed = self._aux_handle_curtailment_without_limit(action, new_p) beg__redisp = time.perf_counter() - if self.redispatching_unit_commitment_availble or self.n_storage > 0.0: + if cls.redispatching_unit_commitment_availble or cls.n_storage > 0.0: # 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" @@ -3321,16 +3328,23 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, float, bool, dict]: tock = time.perf_counter() self._time_opponent += tock - tick self._time_create_bk_act += tock - beg_ - - self.backend.apply_action(self._backend_action) + try: + self.backend.apply_action(self._backend_action) + except ImpossibleTopology as exc_: + has_error = True + except_.append(exc_) + is_done = True + # TODO in this case: cancel the topological action of the agent + # and continue instead of "game over" self._time_apply_act += time.perf_counter() - beg_ # now it's time to run the powerflow properly # and to update the time dependant properties - self._update_alert_properties(action, lines_attacked, subs_attacked) - detailed_info, has_error = self._aux_run_pf_after_state_properly_set( - action, init_line_status, new_p, except_ - ) + if not is_done: + self._update_alert_properties(action, lines_attacked, subs_attacked) + detailed_info, has_error = self._aux_run_pf_after_state_properly_set( + action, init_line_status, new_p, except_ + ) else: has_error = True diff --git a/grid2op/Exceptions/__init__.py b/grid2op/Exceptions/__init__.py index f25ca1d26..f75a3bba6 100644 --- a/grid2op/Exceptions/__init__.py +++ b/grid2op/Exceptions/__init__.py @@ -52,6 +52,7 @@ "IsolatedElement", "DisconnectedLoad", "DisconnectedGenerator", + "ImpossibleTopology", "PlotError", "OpponentError", "UsedRunnerError", @@ -124,6 +125,8 @@ IsolatedElement, DisconnectedLoad, DisconnectedGenerator, + ImpossibleTopology, + ) DivergingPowerFlow = DivergingPowerflow # for compatibility with lightsim2grid diff --git a/grid2op/Exceptions/backendExceptions.py b/grid2op/Exceptions/backendExceptions.py index 297c63d69..e70cd645b 100644 --- a/grid2op/Exceptions/backendExceptions.py +++ b/grid2op/Exceptions/backendExceptions.py @@ -53,3 +53,10 @@ class DisconnectedLoad(BackendError): class DisconnectedGenerator(BackendError): """Specific error raised by the backend when a generator is disconnected""" pass + + +class ImpossibleTopology(BackendError): + """Specific error raised by the backend :func:`grid2op.Backend.Backend.apply_action` + when the player asked a topology (for example using `set_bus`) that + cannot be applied by the backend. + """ \ No newline at end of file diff --git a/grid2op/tests/test_Agent.py b/grid2op/tests/test_Agent.py index 30195af39..f66c7d5be 100644 --- a/grid2op/tests/test_Agent.py +++ b/grid2op/tests/test_Agent.py @@ -142,6 +142,9 @@ def test_2_busswitch(self): expected_reward = dt_float(12075.389) expected_reward = dt_float(12277.632) expected_reward = dt_float(12076.35644531 / 12.) + # 1006.363037109375 + #: Breaking change in 1.9.9: topology are not in the same order + expected_reward = dt_float(1006.34924) assert ( np.abs(cum_reward - expected_reward) <= self.tol_one ), f"The reward has not been properly computed {cum_reward} instead of {expected_reward}" diff --git a/grid2op/tests/test_n_busbar_per_sub.py b/grid2op/tests/test_n_busbar_per_sub.py index ce252a2e0..d96f204ab 100644 --- a/grid2op/tests/test_n_busbar_per_sub.py +++ b/grid2op/tests/test_n_busbar_per_sub.py @@ -22,7 +22,8 @@ import pdb -HAS_TIME_AND_MEMORY = False # test on a big computer only with lots of RAM, and lots of time available... +# test on a big computer only with lots of RAM, and lots of time available... +HAS_TIME_AND_MEMORY = False class _AuxFakeBackendSupport(PandaPowerBackend): @@ -893,14 +894,95 @@ def get_env_nm(self): res = len(tmp2.env.action_space.get_all_unitary_line_set(tmp2.env.action_space)) res_simple = len(tmp2.env.action_space.get_all_unitary_line_set_simple(tmp2.env.action_space)) tmp2.tearDown() - assert res == (1 + 3*3) * 186, f"found: {res}" + assert res == (1 + 3 * 3) * 186, f"found: {res}" assert res_simple == 2 * 186, f"found: {res_simple}" class TestBackendAction(unittest.TestCase): - pass + def get_nb_bus(self): + return 3 + + def get_env_nm(self): + return "educ_case14_storage" + + def setUp(self) -> None: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make(self.get_env_nm(), + backend=_AuxFakeBackendSupport(), + action_class=CompleteAction, + test=True, + n_busbar=self.get_nb_bus(), + _add_to_name=type(self).__name__ + f'_{self.get_nb_bus()}') + return super().setUp() + + def tearDown(self) -> None: + self.env.close() + return super().tearDown() + def test_correct_last_topo(self): + line_id = 0 + id_topo_or = type(self.env).line_or_pos_topo_vect[line_id] + id_topo_ex = type(self.env).line_ex_pos_topo_vect[line_id] + + backend_action = self.env._backend_action + act = self.env.action_space({"set_bus": {"lines_or_id": [(line_id, -1)]}}) + backend_action += act + backend_action.reset() + assert backend_action.current_topo.values[id_topo_or] == -1, f"{backend_action.current_topo.values[id_topo_or]} vs -1" + assert backend_action.current_topo.values[id_topo_ex] == -1, f"{backend_action.current_topo.values[id_topo_ex]} vs -1" + assert backend_action.last_topo_registered.values[id_topo_or] == 1, f"{backend_action.last_topo_registered.values[id_topo_or]} vs 1" + assert backend_action.last_topo_registered.values[id_topo_ex] == 1, f"{backend_action.last_topo_registered.values[id_topo_or]} vs 1" + + act = self.env.action_space({"set_bus": {"lines_or_id": [(line_id, 2)]}}) + backend_action += act + backend_action.reset() + assert backend_action.current_topo.values[id_topo_or] == 2, f"{backend_action.current_topo.values[id_topo_or]} vs 2" + assert backend_action.current_topo.values[id_topo_ex] == 1, f"{backend_action.current_topo.values[id_topo_ex]} vs 1" + assert backend_action.last_topo_registered.values[id_topo_or] == 2, f"{backend_action.last_topo_registered.values[id_topo_or]} vs 2" + assert backend_action.last_topo_registered.values[id_topo_ex] == 1, f"{backend_action.last_topo_registered.values[id_topo_or]} vs 1" + + act = self.env.action_space({"set_bus": {"lines_or_id": [(line_id, -1)]}}) + backend_action += act + backend_action.reset() + assert backend_action.current_topo.values[id_topo_or] == -1, f"{backend_action.current_topo.values[id_topo_or]} vs -1" + assert backend_action.current_topo.values[id_topo_ex] == -1, f"{backend_action.current_topo.values[id_topo_ex]} vs -1" + assert backend_action.last_topo_registered.values[id_topo_or] == 2, f"{backend_action.last_topo_registered.values[id_topo_or]} vs 2" + assert backend_action.last_topo_registered.values[id_topo_ex] == 1, f"{backend_action.last_topo_registered.values[id_topo_or]} vs 1" + + act = self.env.action_space({"set_bus": {"lines_ex_id": [(line_id, 3)]}}) + backend_action += act + backend_action.reset() + assert backend_action.current_topo.values[id_topo_or] == 2, f"{backend_action.current_topo.values[id_topo_or]} vs 2" + assert backend_action.current_topo.values[id_topo_ex] == 3, f"{backend_action.current_topo.values[id_topo_ex]} vs 3" + assert backend_action.last_topo_registered.values[id_topo_or] == 2, f"{backend_action.last_topo_registered.values[id_topo_or]} vs 2" + assert backend_action.last_topo_registered.values[id_topo_ex] == 3, f"{backend_action.last_topo_registered.values[id_topo_or]} vs 3" + + act = self.env.action_space({"set_bus": {"lines_or_id": [(line_id, -1)]}}) + backend_action += act + backend_action.reset() + assert backend_action.current_topo.values[id_topo_or] == -1, f"{backend_action.current_topo.values[id_topo_or]} vs -1" + assert backend_action.current_topo.values[id_topo_ex] == -1, f"{backend_action.current_topo.values[id_topo_ex]} vs -1" + assert backend_action.last_topo_registered.values[id_topo_or] == 2, f"{backend_action.last_topo_registered.values[id_topo_or]} vs 2" + assert backend_action.last_topo_registered.values[id_topo_ex] == 3, f"{backend_action.last_topo_registered.values[id_topo_or]} vs 3" + + act = self.env.action_space({"set_bus": {"lines_or_id": [(line_id, -1)]}}) + backend_action += act + backend_action.reset() + assert backend_action.current_topo.values[id_topo_or] == -1, f"{backend_action.current_topo.values[id_topo_or]} vs -1" + assert backend_action.current_topo.values[id_topo_ex] == -1, f"{backend_action.current_topo.values[id_topo_ex]} vs -1" + assert backend_action.last_topo_registered.values[id_topo_or] == 2, f"{backend_action.last_topo_registered.values[id_topo_or]} vs 2" + assert backend_action.last_topo_registered.values[id_topo_ex] == 3, f"{backend_action.last_topo_registered.values[id_topo_or]} vs 3" + act = self.env.action_space({"set_bus": {"lines_or_id": [(line_id, 1)]}}) + backend_action += act + backend_action.reset() + assert backend_action.current_topo.values[id_topo_or] == 1, f"{backend_action.current_topo.values[id_topo_or]} vs 1" + assert backend_action.current_topo.values[id_topo_ex] == 3, f"{backend_action.current_topo.values[id_topo_ex]} vs 3" + assert backend_action.last_topo_registered.values[id_topo_or] == 1, f"{backend_action.last_topo_registered.values[id_topo_or]} vs 1" + assert backend_action.last_topo_registered.values[id_topo_ex] == 3, f"{backend_action.last_topo_registered.values[id_topo_or]} vs 3" + + class TestPandapowerBackend(unittest.TestCase): pass From 814a8ed0350cbde503caa33d8c715cbc2836ca5e Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 2 Feb 2024 16:09:26 +0100 Subject: [PATCH 11/24] bump to medium+ in circleci to see if it solves the 'received killed signal' --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b5c496bce..301166578 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -28,7 +28,7 @@ executors: jobs: test: executor: grid2op-executor - resource_class: medium + resource_class: medium+ parallelism: 4 steps: - checkout From 7c12cbe66f5bae59c52da06e0a62fd0c385ebb49 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 2 Feb 2024 18:23:50 +0100 Subject: [PATCH 12/24] some tests are made for PandaPowerBackend --- grid2op/Backend/pandaPowerBackend.py | 24 +- grid2op/tests/test_n_busbar_per_sub.py | 324 ++++++++++++++++++++++++- 2 files changed, 332 insertions(+), 16 deletions(-) diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index 142f47ce1..fd8d44726 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -864,22 +864,18 @@ def apply_action(self, backendAction: Union["grid2op.Action._backendAction._Back # topology of the storage stor_bus = backendAction.get_storages_bus() - new_bus_id = stor_bus.values[stor_bus.changed] # id of the busbar 1 or 2 if - activated = new_bus_id > 0 # mask of storage that have been activated - new_bus_num = ( - cls.storage_to_subid[stor_bus.changed] + (new_bus_id - 1) * cls.n_sub - ) # bus number - new_bus_num[~activated] = cls.storage_to_subid[stor_bus.changed][ - ~activated - ] - self._grid.storage["in_service"].values[stor_bus.changed] = activated - self._grid.storage["bus"].values[stor_bus.changed] = new_bus_num - self._topo_vect[cls.storage_pos_topo_vect[stor_bus.changed]] = new_bus_num + new_bus_num = dt_int(1) * self._grid.storage["bus"].values + new_bus_id = stor_bus.values[stor_bus.changed] + new_bus_num[stor_bus.changed] = cls.local_bus_to_global(new_bus_id, cls.storage_to_subid[stor_bus.changed]) + deactivated = new_bus_num <= -1 + deact_and_changed = deactivated & stor_bus.changed + new_bus_num[deact_and_changed] = cls.storage_to_subid[deact_and_changed] + self._grid.storage["in_service"][stor_bus.changed & deactivated] = False + self._grid.storage["bus"] = new_bus_num + self._topo_vect[cls.storage_pos_topo_vect[stor_bus.changed]] = new_bus_num[stor_bus.changed] self._topo_vect[ - cls.storage_pos_topo_vect[stor_bus.changed][~activated] + cls.storage_pos_topo_vect[deact_and_changed] ] = -1 - # new_bus_num = cls.local_bus_to_global(cls.storage_pos_topo_vect[stor_bus.changed], cls.storage_to_subid[stor_bus.changed]) - # TODO n_busbar_per_sub if type(backendAction).shunts_data_available: shunt_p, shunt_q, shunt_bus = shunts__ diff --git a/grid2op/tests/test_n_busbar_per_sub.py b/grid2op/tests/test_n_busbar_per_sub.py index d96f204ab..a414feb66 100644 --- a/grid2op/tests/test_n_busbar_per_sub.py +++ b/grid2op/tests/test_n_busbar_per_sub.py @@ -981,18 +981,338 @@ def test_correct_last_topo(self): assert backend_action.current_topo.values[id_topo_ex] == 3, f"{backend_action.current_topo.values[id_topo_ex]} vs 3" assert backend_action.last_topo_registered.values[id_topo_or] == 1, f"{backend_action.last_topo_registered.values[id_topo_or]} vs 1" assert backend_action.last_topo_registered.values[id_topo_ex] == 3, f"{backend_action.last_topo_registered.values[id_topo_or]} vs 3" + + def test_call(self): + cls = type(self.env) + line_id = 0 + id_topo_or = cls.line_or_pos_topo_vect[line_id] + id_topo_ex = cls.line_ex_pos_topo_vect[line_id] + backend_action = self.env._backend_action + act = self.env.action_space({"set_bus": {"lines_or_id": [(line_id, -1)]}}) + backend_action += act + ( + active_bus, + (prod_p, prod_v, load_p, load_q, storage), + topo__, + shunts__, + ) = backend_action() + assert topo__.values[cls.line_or_pos_topo_vect[line_id]] == -1 + assert topo__.values[cls.line_ex_pos_topo_vect[line_id]] == -1 + backend_action.reset() -class TestPandapowerBackend(unittest.TestCase): - pass + act = self.env.action_space({"set_bus": {"lines_or_id": [(line_id, 2)]}}) + backend_action += act + ( + active_bus, + (prod_p, prod_v, load_p, load_q, storage), + topo__, + shunts__, + ) = backend_action() + assert topo__.values[cls.line_or_pos_topo_vect[line_id]] == 2 + assert topo__.values[cls.line_ex_pos_topo_vect[line_id]] == 1 + backend_action.reset() + + act = self.env.action_space({"set_bus": {"lines_or_id": [(line_id, -1)]}}) + backend_action += act + ( + active_bus, + (prod_p, prod_v, load_p, load_q, storage), + topo__, + shunts__, + ) = backend_action() + assert topo__.values[cls.line_or_pos_topo_vect[line_id]] == -1 + assert topo__.values[cls.line_ex_pos_topo_vect[line_id]] == -1 + backend_action.reset() + + act = self.env.action_space({"set_bus": {"lines_ex_id": [(line_id, 3)]}}) + backend_action += act + ( + active_bus, + (prod_p, prod_v, load_p, load_q, storage), + topo__, + shunts__, + ) = backend_action() + assert topo__.values[cls.line_or_pos_topo_vect[line_id]] == 2 + assert topo__.values[cls.line_ex_pos_topo_vect[line_id]] == 3 + backend_action.reset() + + act = self.env.action_space({"set_bus": {"lines_or_id": [(line_id, -1)]}}) + backend_action += act + ( + active_bus, + (prod_p, prod_v, load_p, load_q, storage), + topo__, + shunts__, + ) = backend_action() + assert topo__.values[cls.line_or_pos_topo_vect[line_id]] == -1 + assert topo__.values[cls.line_ex_pos_topo_vect[line_id]] == -1 + backend_action.reset() + act = self.env.action_space({"set_bus": {"lines_or_id": [(line_id, 1)]}}) + backend_action += act + ( + active_bus, + (prod_p, prod_v, load_p, load_q, storage), + topo__, + shunts__, + ) = backend_action() + assert topo__.values[cls.line_or_pos_topo_vect[line_id]] == 1 + assert topo__.values[cls.line_ex_pos_topo_vect[line_id]] == 3 + backend_action.reset() + + +class TestPandapowerBackend_3busbars(unittest.TestCase): + def get_nb_bus(self): + return 3 + + def get_env_nm(self): + return "educ_case14_storage" + + def setUp(self) -> None: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make(self.get_env_nm(), + backend=PandaPowerBackend(), + action_class=CompleteAction, + test=True, + n_busbar=self.get_nb_bus(), + _add_to_name=type(self).__name__ + f'_{self.get_nb_bus()}') + self.list_loc_bus = [-1] + list(range(1, type(self.env).n_busbar_per_sub + 1)) + return super().setUp() + + def tearDown(self) -> None: + self.env.close() + return super().tearDown() + + def test_right_bus_made(self): + assert self.env.backend._grid.bus.shape[0] == self.get_nb_bus() * type(self.env).n_sub + assert (~self.env.backend._grid.bus.iloc[type(self.env).n_sub:]["in_service"]).all() + @staticmethod + def _aux_find_sub(env, obj_col): + """find a sub with 4 elements, the type of elements and at least 2 lines""" + cls = type(env) + res = None + for sub_id in range(cls.n_sub): + this_sub_mask = cls.grid_objects_types[:,cls.SUB_COL] == sub_id + this_sub = cls.grid_objects_types[this_sub_mask, :] + if this_sub.shape[0] <= 3: + # not enough element + continue + if (this_sub[:, obj_col] == -1).all(): + # no load + continue + if ((this_sub[:, cls.LOR_COL] != -1) | (this_sub[:, cls.LEX_COL] != -1)).sum() <= 1: + # only 1 line + continue + el_id = this_sub[this_sub[:, obj_col] != -1, obj_col][0] + if (this_sub[:, cls.LOR_COL] != -1).any(): + line_or_id = this_sub[this_sub[:, cls.LOR_COL] != -1, cls.LOR_COL][0] + line_ex_id = None + else: + line_or_id = None + line_ex_id = this_sub[this_sub[:, cls.LEX_COL] != -1, cls.LEX_COL][0] + res = (sub_id, el_id, line_or_id, line_ex_id) + break + return res + + @staticmethod + def _aux_find_sub_shunt(env): + """find a sub with 4 elements, the type of elements and at least 2 lines""" + cls = type(env) + res = None + for el_id in range(cls.n_shunt): + sub_id = cls.shunt_to_subid[el_id] + this_sub_mask = cls.grid_objects_types[:,cls.SUB_COL] == sub_id + this_sub = cls.grid_objects_types[this_sub_mask, :] + if this_sub.shape[0] <= 3: + # not enough element + continue + if ((this_sub[:, cls.LOR_COL] != -1) | (this_sub[:, cls.LEX_COL] != -1)).sum() <= 1: + # only 1 line + continue + if (this_sub[:, cls.LOR_COL] != -1).any(): + line_or_id = this_sub[this_sub[:, cls.LOR_COL] != -1, cls.LOR_COL][0] + line_ex_id = None + else: + line_or_id = None + line_ex_id = this_sub[this_sub[:, cls.LEX_COL] != -1, cls.LEX_COL][0] + res = (sub_id, el_id, line_or_id, line_ex_id) + break + return res + + def test_move_load(self): + cls = type(self.env) + res = self._aux_find_sub(self.env, cls.LOA_COL) + if res is None: + raise RuntimeError(f"Cannot carry the test 'test_move_load' as " + "there are no suitable subastation in your grid.") + (sub_id, el_id, line_or_id, line_ex_id) = res + for new_bus in self.list_loc_bus: + if line_or_id is not None: + act = self.env.action_space({"set_bus": {"loads_id": [(el_id, new_bus)], "lines_or_id": [(line_or_id, new_bus)]}}) + else: + act = self.env.action_space({"set_bus": {"loads_id": [(el_id, new_bus)], "lines_ex_id": [(line_ex_id, new_bus)]}}) + bk_act = self.env._backend_action_class() + bk_act += act + self.env.backend.apply_action(bk_act) + global_bus = sub_id + (new_bus -1) * cls.n_sub + if new_bus >= 1: + assert self.env.backend._grid.load.iloc[el_id]["bus"] == global_bus + if line_or_id is not None: + assert self.env.backend._grid.line.iloc[line_or_id]["from_bus"] == global_bus + else: + assert self.env.backend._grid.line.iloc[line_ex_id]["to_bus"] == global_bus + assert self.env.backend._grid.bus.loc[global_bus]["in_service"] + else: + assert not self.env.backend._grid.load.iloc[el_id]["in_service"] + if line_or_id is not None: + assert not self.env.backend._grid.line.iloc[line_or_id]["in_service"] + else: + assert not self.env.backend._grid.line.iloc[line_ex_id]["in_service"] + + def test_move_gen(self): + cls = type(self.env) + res = self._aux_find_sub(self.env, cls.GEN_COL) + if res is None: + raise RuntimeError(f"Cannot carry the test 'test_move_gen' as " + "there are no suitable subastation in your grid.") + (sub_id, el_id, line_or_id, line_ex_id) = res + for new_bus in self.list_loc_bus: + if line_or_id is not None: + act = self.env.action_space({"set_bus": {"generators_id": [(el_id, new_bus)], "lines_or_id": [(line_or_id, new_bus)]}}) + else: + act = self.env.action_space({"set_bus": {"generators_id": [(el_id, new_bus)], "lines_ex_id": [(line_ex_id, new_bus)]}}) + bk_act = self.env._backend_action_class() + bk_act += act + self.env.backend.apply_action(bk_act) + global_bus = sub_id + (new_bus -1) * cls.n_sub + if new_bus >= 1: + assert self.env.backend._grid.gen.iloc[el_id]["bus"] == global_bus + if line_or_id is not None: + assert self.env.backend._grid.line.iloc[line_or_id]["from_bus"] == global_bus + else: + assert self.env.backend._grid.line.iloc[line_ex_id]["to_bus"] == global_bus + assert self.env.backend._grid.bus.loc[global_bus]["in_service"] + else: + assert not self.env.backend._grid.gen.iloc[el_id]["in_service"] + if line_or_id is not None: + assert not self.env.backend._grid.line.iloc[line_or_id]["in_service"] + else: + assert not self.env.backend._grid.line.iloc[line_ex_id]["in_service"] + + def test_move_storage(self): + cls = type(self.env) + res = self._aux_find_sub(self.env, cls.STORAGE_COL) + if res is None: + raise RuntimeError(f"Cannot carry the test 'test_move_storage' as " + "there are no suitable subastation in your grid.") + (sub_id, el_id, line_or_id, line_ex_id) = res + for new_bus in self.list_loc_bus: + if line_or_id is not None: + act = self.env.action_space({"set_bus": {"storages_id": [(el_id, new_bus)], "lines_or_id": [(line_or_id, new_bus)]}}) + else: + act = self.env.action_space({"set_bus": {"storages_id": [(el_id, new_bus)], "lines_ex_id": [(line_ex_id, new_bus)]}}) + bk_act = self.env._backend_action_class() + bk_act += act + self.env.backend.apply_action(bk_act) + global_bus = sub_id + (new_bus -1) * cls.n_sub + if new_bus >= 1: + assert self.env.backend._grid.storage.iloc[el_id]["bus"] == global_bus + if line_or_id is not None: + assert self.env.backend._grid.line.iloc[line_or_id]["from_bus"] == global_bus + else: + assert self.env.backend._grid.line.iloc[line_ex_id]["to_bus"] == global_bus + assert self.env.backend._grid.bus.loc[global_bus]["in_service"] + else: + assert not self.env.backend._grid.storage.iloc[el_id]["in_service"] + if line_or_id is not None: + assert not self.env.backend._grid.line.iloc[line_or_id]["in_service"] + else: + assert not self.env.backend._grid.line.iloc[line_ex_id]["in_service"] + + def test_move_line_or(self): + cls = type(self.env) + line_id = 0 + for new_bus in self.list_loc_bus: + act = self.env.action_space({"set_bus": {"lines_or_id": [(line_id, new_bus)]}}) + bk_act = self.env._backend_action_class() + bk_act += act + self.env.backend.apply_action(bk_act) + global_bus = cls.line_or_to_subid[line_id] + (new_bus -1) * cls.n_sub + if new_bus >= 1: + assert self.env.backend._grid.line.iloc[line_id]["from_bus"] == global_bus + assert self.env.backend._grid.bus.loc[global_bus]["in_service"] + else: + assert not self.env.backend._grid.line.iloc[line_id]["in_service"] + + def test_move_line_ex(self): + cls = type(self.env) + line_id = 0 + for new_bus in self.list_loc_bus: + act = self.env.action_space({"set_bus": {"lines_ex_id": [(line_id, new_bus)]}}) + bk_act = self.env._backend_action_class() + bk_act += act + self.env.backend.apply_action(bk_act) + global_bus = cls.line_ex_to_subid[line_id] + (new_bus -1) * cls.n_sub + if new_bus >= 1: + assert self.env.backend._grid.line.iloc[line_id]["to_bus"] == global_bus + assert self.env.backend._grid.bus.loc[global_bus]["in_service"] + else: + assert not self.env.backend._grid.line.iloc[line_id]["in_service"] + + def test_move_shunt(self): + cls = type(self.env) + res = self._aux_find_sub_shunt(self.env) + if res is None: + raise RuntimeError(f"Cannot carry the test 'test_move_load' as " + "there are no suitable subastation in your grid.") + (sub_id, el_id, line_or_id, line_ex_id) = res + for new_bus in self.list_loc_bus: + if line_or_id is not None: + act = self.env.action_space({"shunt": {"set_bus": [(el_id, new_bus)]}, "set_bus": {"lines_or_id": [(line_or_id, new_bus)]}}) + else: + act = self.env.action_space({"shunt": {"set_bus": [(el_id, new_bus)]}, "set_bus": {"lines_ex_id": [(line_ex_id, new_bus)]}}) + bk_act = self.env._backend_action_class() + bk_act += act + self.env.backend.apply_action(bk_act) + global_bus = sub_id + (new_bus -1) * cls.n_sub + if new_bus >= 1: + assert self.env.backend._grid.shunt.iloc[el_id]["bus"] == global_bus + if line_or_id is not None: + assert self.env.backend._grid.line.iloc[line_or_id]["from_bus"] == global_bus + else: + assert self.env.backend._grid.line.iloc[line_ex_id]["to_bus"] == global_bus + assert self.env.backend._grid.bus.loc[global_bus]["in_service"] + else: + assert not self.env.backend._grid.shunt.iloc[el_id]["in_service"] + if line_or_id is not None: + assert not self.env.backend._grid.line.iloc[line_or_id]["in_service"] + else: + assert not self.env.backend._grid.line.iloc[line_ex_id]["in_service"] + + +class TestPandapowerBackend_1busbar(TestPandapowerBackend_3busbars): + def get_nb_bus(self): + return 3 + + class TestObservation(unittest.TestCase): def test_action_space_get_back_to_ref_state(self): """test the :func:`grid2op.Action.SerializableActionSpace.get_back_to_ref_state` when 3 busbars which could not be tested without observation""" pass + +class TestEnv(unittest.TestCase): + pass + + +class TestGym(unittest.TestCase): + pass + + if __name__ == "__main__": unittest.main() \ No newline at end of file From f8081f148fcd5700641ffebebad703f5e9a63487 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Mon, 5 Feb 2024 09:36:28 +0100 Subject: [PATCH 13/24] silencing a warning for storage unit --- grid2op/Backend/pandaPowerBackend.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index fd8d44726..b01dc2bd7 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -223,6 +223,7 @@ def __init__( self._max_iter : bool = max_iter self._in_service_line_col_id = None self._in_service_trafo_col_id = None + self._in_service_storage_cold_id = None def _check_for_non_modeled_elements(self): """This function check for elements in the pandapower grid that will have no impact on grid2op. @@ -350,6 +351,7 @@ def load_grid(self, self._in_service_line_col_id = int(np.where(self._grid.line.columns == "in_service")[0][0]) self._in_service_trafo_col_id = int(np.where(self._grid.trafo.columns == "in_service")[0][0]) + self._in_service_storage_cold_id = int(np.where(self._grid.storage.columns == "in_service")[0][0]) # add the slack bus that is often not modeled as a generator, but i need it for this backend to work bus_gen_added = None @@ -870,7 +872,8 @@ def apply_action(self, backendAction: Union["grid2op.Action._backendAction._Back deactivated = new_bus_num <= -1 deact_and_changed = deactivated & stor_bus.changed new_bus_num[deact_and_changed] = cls.storage_to_subid[deact_and_changed] - self._grid.storage["in_service"][stor_bus.changed & deactivated] = False + # self._grid.storage["in_service"][stor_bus.changed & deactivated] = False + self._grid.storage.loc[stor_bus.changed & deactivated, self._in_service_storage_cold_id] = False self._grid.storage["bus"] = new_bus_num self._topo_vect[cls.storage_pos_topo_vect[stor_bus.changed]] = new_bus_num[stor_bus.changed] self._topo_vect[ From 8f5ddb5b695f7ad9fc2a807de0dd56198d616115 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Mon, 5 Feb 2024 10:14:18 +0100 Subject: [PATCH 14/24] did I say I really dislike pandas ? --- grid2op/Backend/pandaPowerBackend.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index b01dc2bd7..ae3c78647 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -348,10 +348,6 @@ def load_grid(self, warnings.filterwarnings("ignore", category=FutureWarning) self._grid = pp.from_json(full_path) self._check_for_non_modeled_elements() - - self._in_service_line_col_id = int(np.where(self._grid.line.columns == "in_service")[0][0]) - self._in_service_trafo_col_id = int(np.where(self._grid.trafo.columns == "in_service")[0][0]) - self._in_service_storage_cold_id = int(np.where(self._grid.storage.columns == "in_service")[0][0]) # add the slack bus that is often not modeled as a generator, but i need it for this backend to work bus_gen_added = None @@ -567,6 +563,11 @@ def load_grid(self, for ind, el in add_topo.iterrows(): pp.create_bus(self._grid, index=ind, **el) self._init_private_attrs() + + # do this at the end + self._in_service_line_col_id = int(np.where(self._grid.line.columns == "in_service")[0][0]) + self._in_service_trafo_col_id = int(np.where(self._grid.trafo.columns == "in_service")[0][0]) + self._in_service_storage_cold_id = int(np.where(self._grid.storage.columns == "in_service")[0][0]) def _init_private_attrs(self) -> None: # number of elements per substation @@ -873,7 +874,7 @@ def apply_action(self, backendAction: Union["grid2op.Action._backendAction._Back deact_and_changed = deactivated & stor_bus.changed new_bus_num[deact_and_changed] = cls.storage_to_subid[deact_and_changed] # self._grid.storage["in_service"][stor_bus.changed & deactivated] = False - self._grid.storage.loc[stor_bus.changed & deactivated, self._in_service_storage_cold_id] = False + self._grid.storage.loc[stor_bus.changed & deactivated, "in_service"] = False self._grid.storage["bus"] = new_bus_num self._topo_vect[cls.storage_pos_topo_vect[stor_bus.changed]] = new_bus_num[stor_bus.changed] self._topo_vect[ From 3b9bae4e9d3d600f621ffd30dd1dd468b3bb2366 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Mon, 5 Feb 2024 11:21:08 +0100 Subject: [PATCH 15/24] adding the function --- CHANGELOG.rst | 5 ++ grid2op/Space/GridObjects.py | 141 +++++++++++++++++++++++++++--- grid2op/tests/test_GridObjects.py | 40 ++++++++- 3 files changed, 174 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1ec02203e..aedfa6ac0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -47,12 +47,17 @@ Change Log values for certain values of p or q (an AmbiguousAction exception was raised wrongly) - [FIXED] a bug in the `_BackendAction`: the "last known topoolgy" was not properly computed in some cases (especially at the time where a line was reconnected) +- [ADDED] a method `gridobj.topo_vect_element()` that does the opposite of `gridobj.xxx_pos_topo_vect` - [IMPROVED] handling of "compatibility" grid2op version (by calling the relevant things done in the base class in `BaseAction` and `BaseObservation`) and by using the `from packaging import version` to check version (instead of comparing strings) - [IMPROVED] slightly the code of `check_kirchoff` to make it slightly clearer - [IMRPOVED] typing and doc for some of the main classes of the `Action` module +- [IMPROVED] methods `gridobj.get_lines_id`, `gridobj.get_generators_id`, `gridobj.get_loads_id` + `gridobj.get_storages_id` are now class methods and can be used with `type(env).get_lines_id(...)` + or `act.get_lines_id(...)` for example. + [1.9.8] - 2024-01-26 ---------------------- diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 1ca89f75a..6318c9730 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -21,7 +21,12 @@ import copy import numpy as np from packaging import version - +from typing import Dict, Union +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal + import grid2op from grid2op.dtypes import dt_int, dt_float, dt_bool from grid2op.Exceptions import * @@ -118,12 +123,13 @@ class GridObjects: - method 2 (not recommended): all of the above is stored (for the same powerline) in the :attr:`GridObjects.line_or_pos_topo_vect` [l_id]. In the example above, we will have: - :attr:`GridObjects.line_or_pos_topo_vect` [l_id] = 45 (=42+3: + :attr:`GridObjects.line_or_pos_topo_vect` [l_id] = 45 (=42+3): 42 being the index on which the substation started and 3 being the index of the object in the substation) - method 3 (recommended): use any of the function that computes it for you: :func:`grid2op.Observation.BaseObservation.state_of` is such an interesting method. The two previous methods "method 1" and "method 2" were presented as a way to give detailed and "concrete" example on how the modeling of the powergrid work. + - method 4 (recommended): use the :func:`GridObjects.topo_vect_element` For a given powergrid, this object should be initialized once in the :class:`grid2op.Backend.Backend` when the first call to :func:`grid2op.Backend.Backend.load_grid` is performed. In particular the following attributes @@ -2783,7 +2789,14 @@ def _get_grid2op_version_as_version_obj(cls): @classmethod def process_grid2op_compat(cls): - """This is called when the class is initialized, with `init_grid` to broadcast grid2op compatibility feature. + """ + INTERNAL + + .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ + This is done at the creation of the environment. Use of this class outside of this particular + use is really dangerous and will lead to undefined behaviours. **Do not use this function**. + + This is called when the class is initialized, with `init_grid` to broadcast grid2op compatibility feature. This function can be overloaded, but in this case it's best to call this original method too. @@ -2995,7 +3008,8 @@ def get_obj_substations(cls, _sentinel=None, substation_id=None): ] return res - def get_lines_id(self, _sentinel=None, from_=None, to_=None): + @classmethod + def get_lines_id(cls, _sentinel=None, from_=None, to_=None): """ Returns the list of all the powerlines id in the backend going from `from_` to `to_` @@ -3047,7 +3061,7 @@ def get_lines_id(self, _sentinel=None, from_=None, to_=None): ) for i, (ori, ext) in enumerate( - zip(self.line_or_to_subid, self.line_ex_to_subid) + zip(cls.line_or_to_subid, cls.line_ex_to_subid) ): if ori == from_ and ext == to_: res.append(i) @@ -3060,7 +3074,8 @@ def get_lines_id(self, _sentinel=None, from_=None, to_=None): return res - def get_generators_id(self, sub_id): + @classmethod + def get_generators_id(cls, sub_id): """ Returns the list of all generators id in the backend connected to the substation sub_id @@ -3100,7 +3115,7 @@ def get_generators_id(self, sub_id): 'Please modify "sub_id" parameter' ) - for i, s_id_gen in enumerate(self.gen_to_subid): + for i, s_id_gen in enumerate(cls.gen_to_subid): if s_id_gen == sub_id: res.append(i) @@ -3112,7 +3127,8 @@ def get_generators_id(self, sub_id): return res - def get_loads_id(self, sub_id): + @classmethod + def get_loads_id(cls, sub_id): """ Returns the list of all loads id in the backend connected to the substation sub_id @@ -3151,7 +3167,7 @@ def get_loads_id(self, sub_id): 'Please modify "sub_id" parameter' ) - for i, s_id_gen in enumerate(self.load_to_subid): + for i, s_id_gen in enumerate(cls.load_to_subid): if s_id_gen == sub_id: res.append(i) @@ -3164,7 +3180,8 @@ def get_loads_id(self, sub_id): return res - def get_storages_id(self, sub_id): + @classmethod + def get_storages_id(cls, sub_id): """ Returns the list of all storages element (battery or damp) id in the grid connected to the substation sub_id @@ -3203,7 +3220,7 @@ def get_storages_id(self, sub_id): 'Please modify "sub_id" parameter' ) - for i, s_id_gen in enumerate(self.storage_to_subid): + for i, s_id_gen in enumerate(cls.storage_to_subid): if s_id_gen == sub_id: res.append(i) @@ -3216,6 +3233,108 @@ def get_storages_id(self, sub_id): return res + @classmethod + def topo_vect_element(cls, topo_vect_id: int) -> Dict[Literal["load_id", "gen_id", "line_id", "storage_id", "line_or_id", "line_ex_id", "sub_id"], Union[int, Dict[Literal["or", "ex"], int]]]: + """ + This function aims to be the "opposite" of the + `cls.xxx_pos_topo_vect` (**eg** `cls.load_pos_topo_vect`) + + You give it an id in the topo_vect (*eg* 10) and it gives you + information about which element it is. More precisely, if + `type(env).topo_vect[topo_vect_id]` is: + + - a **load** then it will return `{'load_id': load_id}`, with `load_id` + being such that `type(env).load_pos_topo_vect[load_id] == topo_vect_id` + - a **generator** then it will return `{'gen_id': gen_id}`, with `gen_id` + being such that `type(env).gen_pos_topo_vect[gen_id] == topo_vect_id` + - a **storage** then it will return `{'storage_id': storage_id}`, with `storage_id` + being such that `type(env).storage_pos_topo_vect[storage_id] == topo_vect_id` + - a **line** (origin side) then it will return `{'line_id': {'or': line_id}, 'line_or_id': line_id}`, + with `line_id` + being such that `type(env).line_or_pos_topo_vect[line_id] == topo_vect_id` + - a **line** (ext side) then it will return `{'line_id': {'ex': line_id}, 'line_ex_id': line_id}`, + with `line_id` + being such that `type(env).line_or_pos_topo_vect[line_id] == topo_vect_id` + + .. seealso:: + The attributes :attr:`GridObjects.load_pos_topo_vect`, :attr:`GridObjects.gen_pos_topo_vect`, + :attr:`GridObjects.storage_pos_topo_vect`, :attr:`GridObjects.line_or_pos_topo_vect` and + :attr:`GridObjects.line_ex_pos_topo_vect` to do the opposite. + + And you can also have a look at :attr:`GridObjects.grid_objects_types` + + Parameters + ---------- + topo_vect_id: ``int`` + The element of the topo vect to which you want more information. + + Returns + ------- + res: ``dict`` + See details in the description + + Examples + -------- + It can be used like: + + .. code-block:: python + + import numpy as np + import grid2op + env = grid2op.make("l2rpn_case14_sandbox") + + env_cls = type(env) # or `type(act)` or` type(obs)` etc. or even `env.topo_vect_element(...)` or `obs.topo_vect_element(...)` + for load_id, pos_topo_vect in enumerate(env_cls.load_pos_topo_vect): + res = env_cls.topo_vect_element(pos_topo_vect) + assert "load_id" in res + assert res["load_id"] == load_id + + for gen_id, pos_topo_vect in enumerate(env_cls.gen_pos_topo_vect): + res = env_cls.topo_vect_element(pos_topo_vect) + assert "gen_id" in res + assert res["gen_id"] == gen_id + + for sto_id, pos_topo_vect in enumerate(env_cls.storage_pos_topo_vect): + res = env_cls.topo_vect_element(pos_topo_vect) + assert "storage_id" in res + assert res["storage_id"] == sto_id + + for line_id, pos_topo_vect in enumerate(env_cls.line_or_pos_topo_vect): + res = env_cls.topo_vect_element(pos_topo_vect) + assert "line_id" in res + assert res["line_id"] == {"or": line_id} + assert "line_or_id" in res + assert res["line_or_id"] == line_id + + for line_id, pos_topo_vect in enumerate(env_cls.line_ex_pos_topo_vect): + res = env_cls.topo_vect_element(pos_topo_vect) + assert "line_id" in res + assert res["line_id"] == {"ex": line_id} + assert "line_ex_id" in res + assert res["line_ex_id"] == line_id + + """ + elt = cls.grid_objects_types[topo_vect_id] + res = {"sub_id": int(elt[cls.SUB_COL])} + if elt[cls.LOA_COL] != -1: + res["load_id"] = int(elt[cls.LOA_COL]) + return res + if elt[cls.GEN_COL] != -1: + res["gen_id"] = int(elt[cls.GEN_COL]) + return res + if elt[cls.STORAGE_COL] != -1: + res["storage_id"] = int(elt[cls.STORAGE_COL]) + return res + if elt[cls.LOR_COL] != -1: + res["line_or_id"] = int(elt[cls.LOR_COL]) + res["line_id"] = {"or": int(elt[cls.LOR_COL])} + return res + if elt[cls.LEX_COL] != -1: + res["line_ex_id"] = int(elt[cls.LEX_COL]) + res["line_id"] = {"ex": int(elt[cls.LEX_COL])} + return res + raise Grid2OpException(f"Unknown element at position {topo_vect_id}") + @staticmethod def _make_cls_dict(cls, res, as_list=True, copy_=True): """NB: `cls` can be here a class or an object of a class...""" diff --git a/grid2op/tests/test_GridObjects.py b/grid2op/tests/test_GridObjects.py index a5ee0a493..63f4f2f19 100644 --- a/grid2op/tests/test_GridObjects.py +++ b/grid2op/tests/test_GridObjects.py @@ -152,7 +152,45 @@ def test_auxilliary_func(self): ) # this should pass bk_cls.assert_grid_correct_cls() - + + def test_topo_vect_element(self): + """ + .. newinversion:: 1.9.9 + Test this utilitary function + """ + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make( + "educ_case14_storage", + test=True, + _add_to_name=type(self).__name__+"test_gridobjects_testauxfunctions", + ) + cls = type(env) + for el_id, el_pos_topo_vect in enumerate(cls.load_pos_topo_vect): + res = cls.topo_vect_element(el_pos_topo_vect) + assert "load_id" in res + assert res["load_id"] == el_id + for el_id, el_pos_topo_vect in enumerate(cls.gen_pos_topo_vect): + res = cls.topo_vect_element(el_pos_topo_vect) + assert "gen_id" in res + assert res["gen_id"] == el_id + for el_id, el_pos_topo_vect in enumerate(cls.storage_pos_topo_vect): + res = cls.topo_vect_element(el_pos_topo_vect) + assert "storage_id" in res + assert res["storage_id"] == el_id + for el_id, el_pos_topo_vect in enumerate(cls.line_or_pos_topo_vect): + res = cls.topo_vect_element(el_pos_topo_vect) + assert "line_id" in res + assert res["line_id"] == {"or": el_id} + assert "line_or_id" in res + assert res["line_or_id"] == el_id + for el_id, el_pos_topo_vect in enumerate(cls.line_ex_pos_topo_vect): + res = cls.topo_vect_element(el_pos_topo_vect) + assert "line_id" in res + assert res["line_id"] == {"ex": el_id} + assert "line_ex_id" in res + assert res["line_ex_id"] == el_id + if __name__ == "__main__": unittest.main() From a8ed5c4d89b8b8206cbec5722d864147db0d8de6 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Wed, 7 Feb 2024 13:50:17 +0100 Subject: [PATCH 16/24] start support for observation, improve support by pandapower backend --- .circleci/config.yml | 3 +- CHANGELOG.rst | 5 +- grid2op/Backend/backend.py | 2 +- grid2op/Backend/pandaPowerBackend.py | 86 +--- grid2op/Environment/baseEnv.py | 20 +- grid2op/Environment/environment.py | 7 +- grid2op/Environment/multiMixEnv.py | 7 +- grid2op/Environment/timedOutEnv.py | 8 +- grid2op/Observation/baseObservation.py | 450 ++++++++++++-------- grid2op/gym_compat/discrete_gym_actspace.py | 2 +- grid2op/simulator/simulator.py | 11 +- grid2op/tests/test_gymnasium_compat.py | 7 +- grid2op/tests/test_n_busbar_per_sub.py | 149 ++++++- 13 files changed, 493 insertions(+), 264 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 301166578..717ef5d9d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -151,8 +151,7 @@ jobs: - run: command: | source venv_test/bin/activate - python -m pip install -U "numpy>=1.24,<1.25" "pandas<2.2" "scipy<1.12" numba - python -m pip install -U .[test] + python -m pip install -U "numpy>=1.24,<1.25" "pandas<2.2" "scipy<1.12" numba .[test] - run: command: | source venv_test/bin/activate diff --git a/CHANGELOG.rst b/CHANGELOG.rst index aedfa6ac0..5b3f8844c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -41,12 +41,14 @@ Change Log of actions and observation for wcci_2020 - [FIXED] 2 bugs detected by static code analysis (thanks sonar cloud) - [FIXED] a bug in `act.get_gen_modif` (vector of wrong size was used, could lead - to some crashes if n_gen >= n_load) + to some crashes if `n_gen >= n_load`) - [FIXED] a bug in `act.as_dict` when shunts were modified - [FIXED] a bug affecting shunts: sometimes it was not possible to modify their p / q values for certain values of p or q (an AmbiguousAction exception was raised wrongly) - [FIXED] a bug in the `_BackendAction`: the "last known topoolgy" was not properly computed in some cases (especially at the time where a line was reconnected) +- [FIXED] `MultiDiscreteActSpace` and `DiscreteActSpace` could be the same classes + on some cases (typo in the code). - [ADDED] a method `gridobj.topo_vect_element()` that does the opposite of `gridobj.xxx_pos_topo_vect` - [IMPROVED] handling of "compatibility" grid2op version (by calling the relevant things done in the base class @@ -54,6 +56,7 @@ Change Log to check version (instead of comparing strings) - [IMPROVED] slightly the code of `check_kirchoff` to make it slightly clearer - [IMRPOVED] typing and doc for some of the main classes of the `Action` module +- [IMRPOVED] typing and doc for some of the main classes of the `Observation` module - [IMPROVED] methods `gridobj.get_lines_id`, `gridobj.get_generators_id`, `gridobj.get_loads_id` `gridobj.get_storages_id` are now class methods and can be used with `type(env).get_lines_id(...)` or `act.get_lines_id(...)` for example. diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index 6adfc6a96..21c3380d9 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -1393,7 +1393,7 @@ def check_kirchoff(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray "Backend.check_kirchoff Impossible to get shunt information. Reactive information might be " "incorrect." ) - diff_v_bus = np.zeros((cls.n_sub, 2), dtype=dt_float) + 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 --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index ae3c78647..5a8439a1b 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -875,8 +875,9 @@ def apply_action(self, backendAction: Union["grid2op.Action._backendAction._Back new_bus_num[deact_and_changed] = cls.storage_to_subid[deact_and_changed] # self._grid.storage["in_service"][stor_bus.changed & deactivated] = False 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_num[stor_bus.changed] + 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 @@ -1391,70 +1392,29 @@ def get_topo_vect(self) -> np.ndarray: return self._topo_vect def _get_topo_vect(self): - res = np.full(self.dim_topo, fill_value=np.iinfo(dt_int).max, dtype=dt_int) + 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() - - i = 0 - for row in self._grid.line[["from_bus", "to_bus"]].values: - bus_or_id = row[0] - bus_ex_id = row[1] - if line_status[i]: - res[self.line_or_pos_topo_vect[i]] = ( - 1 if bus_or_id == self.line_or_to_subid[i] else 2 - ) - res[self.line_ex_pos_topo_vect[i]] = ( - 1 if bus_ex_id == self.line_ex_to_subid[i] else 2 - ) - else: - res[self.line_or_pos_topo_vect[i]] = -1 - res[self.line_ex_pos_topo_vect[i]] = -1 - i += 1 - - nb = self._number_true_line - i = 0 - for row in self._grid.trafo[["hv_bus", "lv_bus"]].values: - bus_or_id = row[0] - bus_ex_id = row[1] - - j = i + nb - if line_status[j]: - res[self.line_or_pos_topo_vect[j]] = ( - 1 if bus_or_id == self.line_or_to_subid[j] else 2 - ) - res[self.line_ex_pos_topo_vect[j]] = ( - 1 if bus_ex_id == self.line_ex_to_subid[j] else 2 - ) - else: - res[self.line_or_pos_topo_vect[j]] = -1 - res[self.line_ex_pos_topo_vect[j]] = -1 - i += 1 - - i = 0 - 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 - - 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 - - if self.n_storage: - # storage can be deactivated by the environment for backward compatibility - i = 0 - for bus_id in self._grid.storage["bus"].values: - status = self._grid.storage["in_service"].values[i] - if status: - res[self.storage_pos_topo_vect[i]] = ( - 1 if bus_id == self.storage_to_subid[i] else 2 - ) - else: - res[self.storage_pos_topo_vect[i]] = -1 - i += 1 - + 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 + 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 + # 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 + 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 + # 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 def _gens_info(self): diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index 29339ecd7..047def9cd 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -1367,7 +1367,7 @@ def set_id(self, id_: Union[int, str]) -> None: def reset(self, *, seed: Union[int, None] = None, - options: Union[Dict[Literal["time serie id"], Union[int, str]], None] = None): + options: Union[Dict[Union[str, Literal["time serie id"]], Union[int, str]], None] = None): """ Reset the base environment (set the appropriate variables to correct initialization). It is (and must be) overloaded in other :class:`grid2op.Environment` @@ -3104,7 +3104,23 @@ def _aux_run_pf_after_state_properly_set( ) return detailed_info, has_error - def step(self, action: BaseAction) -> Tuple[BaseObservation, float, bool, dict]: + def step(self, action: BaseAction) -> Tuple[BaseObservation, + float, + bool, + Dict[Literal["disc_lines", + "is_illegal", + "is_ambiguous", + "is_dispatching_illegal", + "is_illegal_reco", + "reason_alarm_illegal", + "reason_alert_illegal", + "opponent_attack_line", + "opponent_attack_sub", + "exception", + "detailed_infos_for_cascading_failures", + "rewards", + "time_series_id"], + Any]]: """ Run one timestep of the environment's dynamics. When end of episode is reached, you are responsible for calling `reset()` diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 5213c695f..f6ff9ca9b 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -11,6 +11,11 @@ import numpy as np import re from typing import Union, Any, Dict +try: + # Literal introduced in python 3.9 + from typing import Literal +except ImportError: + from typing_extensions import Literal import grid2op from grid2op.Opponent import OpponentSpace @@ -901,7 +906,7 @@ def add_text_logger(self, logger=None): def reset(self, *, seed: Union[int, None] = None, - options: Union[Dict[str, Any], None] = None) -> BaseObservation: + options: Union[Dict[Union[str, Literal["time serie id"]], Union[int, str]], None] = None) -> BaseObservation: """ Reset the environment to a clean state. It will reload the next chronics if any. And reset the grid to a clean state. diff --git a/grid2op/Environment/multiMixEnv.py b/grid2op/Environment/multiMixEnv.py index 5e86de132..0c5368c76 100644 --- a/grid2op/Environment/multiMixEnv.py +++ b/grid2op/Environment/multiMixEnv.py @@ -11,6 +11,11 @@ import numpy as np import copy from typing import Any, Dict, Tuple, Union, List +try: + # Literal introduced in python 3.9 + from typing import Literal +except ImportError: + from typing_extensions import Literal from grid2op.dtypes import dt_int, dt_float from grid2op.Space import GridObjects, RandomObject, DEFAULT_N_BUSBAR_PER_SUB @@ -370,7 +375,7 @@ def reset(self, *, seed: Union[int, None] = None, random=False, - options: Union[Dict[str, Any], None] = None) -> BaseObservation: + options: Union[Dict[Union[str, Literal["time serie id"]], Union[int, str]], None] = None) -> BaseObservation: if self.__closed: raise EnvError("This environment is closed, you cannot use it.") diff --git a/grid2op/Environment/timedOutEnv.py b/grid2op/Environment/timedOutEnv.py index bbf3593f3..d01991a55 100644 --- a/grid2op/Environment/timedOutEnv.py +++ b/grid2op/Environment/timedOutEnv.py @@ -9,6 +9,12 @@ import time from math import floor from typing import Any, Dict, Tuple, Union, List +try: + # Literal introduced in python 3.9 + from typing import Literal +except ImportError: + from typing_extensions import Literal + from grid2op.Environment.environment import Environment from grid2op.Action import BaseAction from grid2op.Observation import BaseObservation @@ -255,7 +261,7 @@ def init_obj_from_kwargs(cls, def reset(self, *, seed: Union[int, None] = None, - options: Union[Dict[str, Any], None] = None) -> BaseObservation: + options: Union[Dict[Union[str, Literal["time serie id"]], Union[int, str]], None] = None) -> BaseObservation: """Reset the environment. .. seealso:: diff --git a/grid2op/Observation/baseObservation.py b/grid2op/Observation/baseObservation.py index aba399e7d..055ec32ce 100644 --- a/grid2op/Observation/baseObservation.py +++ b/grid2op/Observation/baseObservation.py @@ -16,6 +16,13 @@ from typing import Optional from packaging import version +from typing import Dict, Union, Tuple, List, Optional, Any +try: + from typing import Self, Literal +except ImportError: + from typing_extensions import Self, Literal + +import grid2op # for type hints from grid2op.dtypes import dt_int, dt_float, dt_bool from grid2op.Exceptions import ( Grid2OpException, @@ -141,7 +148,7 @@ class BaseObservation(GridObjects): The capacity of each powerline. It is defined at the observed current flow divided by the thermal limit of each powerline (no unit) - topo_vect: :class:`numpy.ndarray`, dtype:int + topo_vect: :class:`numpy.ndarray`, dtype:int For each object (load, generator, ends of a powerline) it gives on which bus this object is connected in its substation. See :func:`grid2op.Backend.Backend.get_topo_vect` for more information. @@ -152,16 +159,16 @@ class BaseObservation(GridObjects): timestep_overflow: :class:`numpy.ndarray`, dtype:int Gives the number of time steps since a powerline is in overflow. - time_before_cooldown_line: :class:`numpy.ndarray`, dtype:int + time_before_cooldown_line: :class:`numpy.ndarray`, dtype:int For each powerline, it gives the number of time step the powerline is unavailable due to "cooldown" - (see :attr:`grid2op.Parameters.NB_TIMESTEP_COOLDOWN_LINE` for more information). 0 means the + (see :attr:`grid2op.Parameters.Parameters.NB_TIMESTEP_COOLDOWN_LINE` for more information). 0 means the an action will be able to act on this same powerline, a number > 0 (eg 1) means that an action at this time step cannot act on this powerline (in the example the agent have to wait 1 time step) time_before_cooldown_sub: :class:`numpy.ndarray`, dtype:int Same as :attr:`BaseObservation.time_before_cooldown_line` but for substations. For each substation, it gives the number of timesteps to wait before acting on this substation (see - see :attr:`grid2op.Parameters.NB_TIMESTEP_COOLDOWN_SUB` for more information). + see :attr:`grid2op.Parameters.Parameters.NB_TIMESTEP_COOLDOWN_SUB` for more information). time_next_maintenance: :class:`numpy.ndarray`, dtype:int For each powerline, it gives the time of the next planned maintenance. For example if there is: @@ -402,13 +409,13 @@ class BaseObservation(GridObjects): For each attackable line `i` it says: - obs.attack_under_alert[i] = 0 => attackable line i has not been attacked OR it - has been attacked before the relevant window (env.parameters.ALERT_TIME_WINDOW) + has been attacked before the relevant window (`env.parameters.ALERT_TIME_WINDOW`) - obs.attack_under_alert[i] = -1 => attackable line i has been attacked and (before the attack) no alert was sent (so your agent expects to survive at least - env.parameters.ALERT_TIME_WINDOW steps) + `env.parameters.ALERT_TIME_WINDOW` steps) - 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) + `env.parameters.ALERT_TIME_WINDOW` steps) _shunt_p: :class:`numpy.ndarray`, dtype:float Shunt active value (only available if shunts are available) (in MW) @@ -511,64 +518,65 @@ def __init__(self, self.minute_of_hour = dt_int(0) self.day_of_week = dt_int(0) - self.timestep_overflow = np.empty(shape=(self.n_line,), dtype=dt_int) + cls = type(self) + self.timestep_overflow = np.empty(shape=(cls.n_line,), dtype=dt_int) # 0. (line is disconnected) / 1. (line is connected) - self.line_status = np.empty(shape=self.n_line, dtype=dt_bool) + self.line_status = np.empty(shape=cls.n_line, dtype=dt_bool) # topological vector - self.topo_vect = np.empty(shape=self.dim_topo, dtype=dt_int) + self.topo_vect = np.empty(shape=cls.dim_topo, dtype=dt_int) # generators information - self.gen_p = np.empty(shape=self.n_gen, dtype=dt_float) - self.gen_q = np.empty(shape=self.n_gen, dtype=dt_float) - self.gen_v = np.empty(shape=self.n_gen, dtype=dt_float) - self.gen_margin_up = np.empty(shape=self.n_gen, dtype=dt_float) - self.gen_margin_down = np.empty(shape=self.n_gen, dtype=dt_float) + self.gen_p = np.empty(shape=cls.n_gen, dtype=dt_float) + self.gen_q = np.empty(shape=cls.n_gen, dtype=dt_float) + self.gen_v = np.empty(shape=cls.n_gen, dtype=dt_float) + self.gen_margin_up = np.empty(shape=cls.n_gen, dtype=dt_float) + self.gen_margin_down = np.empty(shape=cls.n_gen, dtype=dt_float) # loads information - self.load_p = np.empty(shape=self.n_load, dtype=dt_float) - self.load_q = np.empty(shape=self.n_load, dtype=dt_float) - self.load_v = np.empty(shape=self.n_load, dtype=dt_float) + self.load_p = np.empty(shape=cls.n_load, dtype=dt_float) + self.load_q = np.empty(shape=cls.n_load, dtype=dt_float) + self.load_v = np.empty(shape=cls.n_load, dtype=dt_float) # lines origin information - self.p_or = np.empty(shape=self.n_line, dtype=dt_float) - self.q_or = np.empty(shape=self.n_line, dtype=dt_float) - self.v_or = np.empty(shape=self.n_line, dtype=dt_float) - self.a_or = np.empty(shape=self.n_line, dtype=dt_float) + self.p_or = np.empty(shape=cls.n_line, dtype=dt_float) + self.q_or = np.empty(shape=cls.n_line, dtype=dt_float) + self.v_or = np.empty(shape=cls.n_line, dtype=dt_float) + self.a_or = np.empty(shape=cls.n_line, dtype=dt_float) # lines extremity information - self.p_ex = np.empty(shape=self.n_line, dtype=dt_float) - self.q_ex = np.empty(shape=self.n_line, dtype=dt_float) - self.v_ex = np.empty(shape=self.n_line, dtype=dt_float) - self.a_ex = np.empty(shape=self.n_line, dtype=dt_float) + self.p_ex = np.empty(shape=cls.n_line, dtype=dt_float) + self.q_ex = np.empty(shape=cls.n_line, dtype=dt_float) + self.v_ex = np.empty(shape=cls.n_line, dtype=dt_float) + self.a_ex = np.empty(shape=cls.n_line, dtype=dt_float) # lines relative flows - self.rho = np.empty(shape=self.n_line, dtype=dt_float) + self.rho = np.empty(shape=cls.n_line, dtype=dt_float) # cool down and reconnection time after hard overflow, soft overflow or cascading failure - self.time_before_cooldown_line = np.empty(shape=self.n_line, dtype=dt_int) - self.time_before_cooldown_sub = np.empty(shape=self.n_sub, dtype=dt_int) + self.time_before_cooldown_line = np.empty(shape=cls.n_line, dtype=dt_int) + self.time_before_cooldown_sub = np.empty(shape=cls.n_sub, dtype=dt_int) self.time_next_maintenance = 1 * self.time_before_cooldown_line self.duration_next_maintenance = 1 * self.time_before_cooldown_line # redispatching - self.target_dispatch = np.empty(shape=self.n_gen, dtype=dt_float) - self.actual_dispatch = np.empty(shape=self.n_gen, dtype=dt_float) + self.target_dispatch = np.empty(shape=cls.n_gen, dtype=dt_float) + self.actual_dispatch = np.empty(shape=cls.n_gen, dtype=dt_float) # storage unit - self.storage_charge = np.empty(shape=self.n_storage, dtype=dt_float) # in MWh + self.storage_charge = np.empty(shape=cls.n_storage, dtype=dt_float) # in MWh self.storage_power_target = np.empty( - shape=self.n_storage, dtype=dt_float + shape=cls.n_storage, dtype=dt_float ) # in MW - self.storage_power = np.empty(shape=self.n_storage, dtype=dt_float) # in MW + self.storage_power = np.empty(shape=cls.n_storage, dtype=dt_float) # in MW # attention budget self.is_alarm_illegal = np.ones(shape=1, dtype=dt_bool) self.time_since_last_alarm = np.empty(shape=1, dtype=dt_int) - self.last_alarm = np.empty(shape=self.dim_alarms, dtype=dt_int) + self.last_alarm = np.empty(shape=cls.dim_alarms, dtype=dt_int) self.attention_budget = np.empty(shape=1, dtype=dt_float) self.was_alarm_used_after_game_over = np.zeros(shape=1, dtype=dt_bool) # alert - dim_alert = type(self).dim_alerts + dim_alert = cls.dim_alerts self.active_alert = np.empty(shape=dim_alert, dtype=dt_bool) self.attack_under_alert = np.empty(shape=dim_alert, dtype=dt_int) self.time_since_last_alert = np.empty(shape=dim_alert, dtype=dt_int) @@ -584,33 +592,33 @@ def __init__(self, self._vectorized = None # for shunt (these are not stored!) - if type(self).shunts_data_available: - self._shunt_p = np.empty(shape=self.n_shunt, dtype=dt_float) - self._shunt_q = np.empty(shape=self.n_shunt, dtype=dt_float) - self._shunt_v = np.empty(shape=self.n_shunt, dtype=dt_float) - self._shunt_bus = np.empty(shape=self.n_shunt, dtype=dt_int) + if cls.shunts_data_available: + self._shunt_p = np.empty(shape=cls.n_shunt, dtype=dt_float) + self._shunt_q = np.empty(shape=cls.n_shunt, dtype=dt_float) + self._shunt_v = np.empty(shape=cls.n_shunt, dtype=dt_float) + self._shunt_bus = np.empty(shape=cls.n_shunt, dtype=dt_int) - self._thermal_limit = np.empty(shape=self.n_line, dtype=dt_float) + self._thermal_limit = np.empty(shape=cls.n_line, dtype=dt_float) - self.gen_p_before_curtail = np.empty(shape=self.n_gen, dtype=dt_float) - self.curtailment = np.empty(shape=self.n_gen, dtype=dt_float) - self.curtailment_limit = np.empty(shape=self.n_gen, dtype=dt_float) - self.curtailment_limit_effective = np.empty(shape=self.n_gen, dtype=dt_float) + self.gen_p_before_curtail = np.empty(shape=cls.n_gen, dtype=dt_float) + self.curtailment = np.empty(shape=cls.n_gen, dtype=dt_float) + self.curtailment_limit = np.empty(shape=cls.n_gen, dtype=dt_float) + self.curtailment_limit_effective = np.empty(shape=cls.n_gen, dtype=dt_float) # the "theta" (voltage angle, in degree) self.support_theta = False - self.theta_or = np.empty(shape=self.n_line, dtype=dt_float) - self.theta_ex = np.empty(shape=self.n_line, dtype=dt_float) - self.load_theta = np.empty(shape=self.n_load, dtype=dt_float) - self.gen_theta = np.empty(shape=self.n_gen, dtype=dt_float) - self.storage_theta = np.empty(shape=self.n_storage, dtype=dt_float) + self.theta_or = np.empty(shape=cls.n_line, dtype=dt_float) + self.theta_ex = np.empty(shape=cls.n_line, dtype=dt_float) + self.load_theta = np.empty(shape=cls.n_load, dtype=dt_float) + self.gen_theta = np.empty(shape=cls.n_gen, dtype=dt_float) + self.storage_theta = np.empty(shape=cls.n_storage, dtype=dt_float) # counter self.current_step = dt_int(0) self.max_step = dt_int(np.iinfo(dt_int).max) self.delta_time = dt_float(5.0) - def _aux_copy(self, other): + def _aux_copy(self, other : Self) -> None: attr_simple = [ "max_step", "current_step", @@ -685,12 +693,12 @@ def _aux_copy(self, other): attr_vect += ["_shunt_bus", "_shunt_v", "_shunt_q", "_shunt_p"] for attr_nm in attr_simple: - setattr(other, attr_nm, getattr(self, attr_nm)) + setattr(other, attr_nm, copy.deepcopy(getattr(self, attr_nm))) for attr_nm in attr_vect: getattr(other, attr_nm)[:] = getattr(self, attr_nm) - def __copy__(self): + def __copy__(self) -> Self: res = type(self)(obs_env=self._obs_env, action_helper=self.action_helper, kwargs_env=self._ptr_kwargs_env) @@ -711,7 +719,7 @@ def __copy__(self): return res - def __deepcopy__(self, memodict={}): + def __deepcopy__(self, memodict={}) -> Self: res = type(self)(obs_env=self._obs_env, action_helper=self.action_helper, kwargs_env=self._ptr_kwargs_env) @@ -742,7 +750,12 @@ def state_of( line_id=None, storage_id=None, substation_id=None, - ): + ) -> Dict[Literal["p", "q", "v", "theta", "bus", "sub_id", "actual_dispatch", "target_dispatch", + "maintenance", "cooldown_time", "storage_power", "storage_charge", + "storage_power_target", "storage_theta", + "topo_vect", "nb_bus", "origin", "extremity"], + Union[int, float, Dict[Literal["p", "q", "v", "a", "sub_id", "bus", "theta"], Union[int, float]]] + ]: """ Return the state of this action on a give unique load, generator unit, powerline of substation. Only one of load, gen, line or substation should be filled. @@ -849,7 +862,6 @@ def state_of( raise Grid2OpException( "action.effect_on should only be called with named argument." ) - if ( load_id is None and gen_id is None @@ -862,6 +874,7 @@ def state_of( 'Please provide "load_id", "gen_id", "line_id", "storage_id" or ' '"substation_id"' ) + cls = type(self) if load_id is not None: if ( @@ -883,7 +896,7 @@ def state_of( "q": self.load_q[load_id], "v": self.load_v[load_id], "bus": self.topo_vect[self.load_pos_topo_vect[load_id]], - "sub_id": self.load_to_subid[load_id], + "sub_id": cls.load_to_subid[load_id], } if self.support_theta: res["theta"] = self.load_theta[load_id] @@ -908,7 +921,7 @@ def state_of( "q": self.gen_q[gen_id], "v": self.gen_v[gen_id], "bus": self.topo_vect[self.gen_pos_topo_vect[gen_id]], - "sub_id": self.gen_to_subid[gen_id], + "sub_id": cls.gen_to_subid[gen_id], "target_dispatch": self.target_dispatch[gen_id], "actual_dispatch": self.target_dispatch[gen_id], "curtailment": self.curtailment[gen_id], @@ -939,8 +952,8 @@ def state_of( "q": self.q_or[line_id], "v": self.v_or[line_id], "a": self.a_or[line_id], - "bus": self.topo_vect[self.line_or_pos_topo_vect[line_id]], - "sub_id": self.line_or_to_subid[line_id], + "bus": self.topo_vect[cls.line_or_pos_topo_vect[line_id]], + "sub_id": cls.line_or_to_subid[line_id], } if self.support_theta: res["origin"]["theta"] = self.theta_or[line_id] @@ -950,8 +963,8 @@ def state_of( "q": self.q_ex[line_id], "v": self.v_ex[line_id], "a": self.a_ex[line_id], - "bus": self.topo_vect[self.line_ex_pos_topo_vect[line_id]], - "sub_id": self.line_ex_to_subid[line_id], + "bus": self.topo_vect[cls.line_ex_pos_topo_vect[line_id]], + "sub_id": cls.line_ex_to_subid[line_id], } if self.support_theta: res["origin"]["theta"] = self.theta_ex[line_id] @@ -968,7 +981,7 @@ def state_of( elif storage_id is not None: if substation_id is not None: raise Grid2OpException(ERROR_ONLY_SINGLE_EL) - if storage_id >= self.n_storage: + if storage_id >= cls.n_storage: raise Grid2OpException( 'There are no storage unit with id "storage_id={}" in this grid.'.format( storage_id @@ -978,23 +991,24 @@ def state_of( raise Grid2OpException("`storage_id` should be a positive integer") res = {} + res["p"] = self.storage_power[storage_id] res["storage_power"] = self.storage_power[storage_id] res["storage_charge"] = self.storage_charge[storage_id] res["storage_power_target"] = self.storage_power_target[storage_id] - res["bus"] = self.topo_vect[self.storage_pos_topo_vect[storage_id]] - res["sub_id"] = self.storage_to_subid[storage_id] + res["bus"] = self.topo_vect[cls.storage_pos_topo_vect[storage_id]] + res["sub_id"] = cls.storage_to_subid[storage_id] if self.support_theta: res["theta"] = self.storage_theta[storage_id] else: - if substation_id >= len(self.sub_info): + if substation_id >= len(cls.sub_info): raise Grid2OpException( 'There are no substation of id "substation_id={}" in this grid.'.format( substation_id ) ) - beg_ = int(self.sub_info[:substation_id].sum()) - end_ = int(beg_ + self.sub_info[substation_id]) + beg_ = int(cls.sub_info[:substation_id].sum()) + end_ = int(beg_ + cls.sub_info[substation_id]) topo_sub = self.topo_vect[beg_:end_] if (topo_sub > 0).any(): nb_bus = ( @@ -1011,7 +1025,7 @@ def state_of( return res @classmethod - def process_shunt_satic_data(cls): + def process_shunt_satic_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) @@ -1027,7 +1041,7 @@ def process_shunt_satic_data(cls): return super().process_shunt_satic_data() @classmethod - def process_grid2op_compat(cls): + def process_grid2op_compat(cls) -> None: super().process_grid2op_compat() glop_ver = cls._get_grid2op_version_as_version_obj() @@ -1141,13 +1155,13 @@ def process_grid2op_compat(cls): cls.attr_list_set = copy.deepcopy(cls.attr_list_set) cls.attr_list_set = set(cls.attr_list_vect) - def shape(self): + def shape(self) -> np.ndarray: return type(self).shapes() - def dtype(self): + def dtype(self) -> np.ndarray: return type(self).dtypes() - def reset(self): + def reset(self) -> None: """ INTERNAL @@ -1256,8 +1270,15 @@ def reset(self): self.max_step = dt_int(np.iinfo(dt_int).max) self.delta_time = dt_float(5.0) - def set_game_over(self, env=None): + def set_game_over(self, + env: Optional["grid2op.Environment.Environment"]=None) -> None: """ + INTERNAL + + .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ + This is used internally to reset an observation in a fixed state, possibly after + a game over. + Set the observation to the "game over" state: - all powerlines are disconnected @@ -1386,7 +1407,7 @@ def set_game_over(self, env=None): # was_alert_used_after_attack not updated here in this case # attack_under_alert not updated here in this case - def __compare_stats(self, other, name): + def __compare_stats(self, other: Self, name: str) -> bool: attr_me = getattr(self, name) attr_other = getattr(other, name) if attr_me is None and attr_other is not None: @@ -1416,7 +1437,7 @@ def __compare_stats(self, other, name): return False return True - def __eq__(self, other): + def __eq__(self, other : Self) -> bool: """ INTERNAL @@ -1477,13 +1498,31 @@ def __eq__(self, other): return True - def __sub__(self, other): + def __sub__(self, other : Self) -> Self: """ - computes the difference between two observation, and return an observation corresponding to + Computes the difference between two observations, and return an observation corresponding to this difference. This can be used to easily plot the difference between two observations at different step for example. + + + Examples + ---------- + + .. code-block:: python + + import grid2op + env = grid2op.make("l2rpn_case14_sandbox") + + obs_0 = env.reset() + + action = env.action_space() + obs_1, reward, done, info = env.step(action) + + diff_obs = obs_1 - obs_0 + + diff_obs.gen_p # the variation in generator between these steps """ same_grid = type(self).same_grid_class(type(other)) if not same_grid: @@ -1518,7 +1557,7 @@ def __sub__(self, other): res.__setattr__(stat_nm, diff_) return res - def where_different(self, other): + def where_different(self, other : Self) -> Tuple[Self, List]: """ Returns the difference between two observation. @@ -1529,7 +1568,7 @@ def where_different(self, other): Returns ------- - diff_: :class:`grid2op.Observation.BaseObservation` + diff_: :class:`BaseObservation` The observation showing the difference between `self` and `other` attr_nm: ``list`` List of string representing the names of the different attributes. It's [] if the two observations @@ -1549,7 +1588,7 @@ def where_different(self, other): return diff_, res @abstractmethod - def update(self, env, with_forecast=True): + def update(self, env: "grid2op.Environment.Environment", with_forecast: bool=True) -> None: """ INTERNAL @@ -1586,7 +1625,7 @@ def update(self, env, with_forecast=True): """ pass - def connectivity_matrix(self, as_csr_matrix=False): + def connectivity_matrix(self, as_csr_matrix: bool=False) -> Union[np.ndarray, csr_matrix]: """ Computes and return the "connectivity matrix" `con_mat`. Let "dim_topo := 2 * n_line + n_prod + n_conso + n_storage" (the total number of elements on the grid) @@ -1685,7 +1724,8 @@ def connectivity_matrix(self, as_csr_matrix=False): end_ = 0 row_ind = [] col_ind = [] - for sub_id, nb_obj in enumerate(self.sub_info): + cls = type(self) + for sub_id, nb_obj in enumerate(cls.sub_info): # it must be a vanilla python integer, otherwise it's not handled by some backend # especially if written in c++ nb_obj = int(nb_obj) @@ -1717,44 +1757,49 @@ def connectivity_matrix(self, as_csr_matrix=False): beg_ += nb_obj # both ends of a line are connected together (if line is connected) - for q_id in range(self.n_line): + for q_id in range(cls.n_line): if self.line_status[q_id]: # if powerline is connected connect both its side - row_ind.append(self.line_or_pos_topo_vect[q_id]) - col_ind.append(self.line_ex_pos_topo_vect[q_id]) - row_ind.append(self.line_ex_pos_topo_vect[q_id]) - col_ind.append(self.line_or_pos_topo_vect[q_id]) + row_ind.append(cls.line_or_pos_topo_vect[q_id]) + col_ind.append(cls.line_ex_pos_topo_vect[q_id]) + row_ind.append(cls.line_ex_pos_topo_vect[q_id]) + col_ind.append(cls.line_or_pos_topo_vect[q_id]) row_ind = np.array(row_ind).astype(dt_int) col_ind = np.array(col_ind).astype(dt_int) if not as_csr_matrix: self._connectivity_matrix_ = np.zeros( - shape=(self.dim_topo, self.dim_topo), dtype=dt_float + shape=(cls.dim_topo, cls.dim_topo), dtype=dt_float ) self._connectivity_matrix_[row_ind.T, col_ind] = 1.0 else: data = np.ones(row_ind.shape[0], dtype=dt_float) self._connectivity_matrix_ = csr_matrix( (data, (row_ind, col_ind)), - shape=(self.dim_topo, self.dim_topo), + shape=(cls.dim_topo, cls.dim_topo), dtype=dt_float, ) return self._connectivity_matrix_ def _aux_fun_get_bus(self): """see in bus_connectivity matrix""" - bus_or = self.topo_vect[self.line_or_pos_topo_vect] - bus_ex = self.topo_vect[self.line_ex_pos_topo_vect] + cls = type(self) + bus_or = self.topo_vect[cls.line_or_pos_topo_vect] + bus_ex = self.topo_vect[cls.line_ex_pos_topo_vect] connected = (bus_or > 0) & (bus_ex > 0) bus_or = bus_or[connected] bus_ex = bus_ex[connected] - bus_or = self.line_or_to_subid[connected] + (bus_or - 1) * self.n_sub - bus_ex = self.line_ex_to_subid[connected] + (bus_ex - 1) * self.n_sub + # bus_or = self.line_or_to_subid[connected] + (bus_or - 1) * self.n_sub + # bus_ex = self.line_ex_to_subid[connected] + (bus_ex - 1) * self.n_sub + bus_or = cls.local_bus_to_global(bus_or, cls.line_or_to_subid[connected]) + bus_ex = cls.local_bus_to_global(bus_ex, cls.line_ex_to_subid[connected]) unique_bus = np.unique(np.concatenate((bus_or, bus_ex))) unique_bus = np.sort(unique_bus) nb_bus = unique_bus.shape[0] return nb_bus, unique_bus, bus_or, bus_ex - def bus_connectivity_matrix(self, as_csr_matrix=False, return_lines_index=False): + def bus_connectivity_matrix(self, + as_csr_matrix: bool=False, + return_lines_index: bool=False) -> Tuple[Union[np.ndarray, csr_matrix], Optional[Tuple[np.ndarray, np.ndarray]]]: """ If we denote by `nb_bus` the total number bus of the powergrid (you can think of a "bus" being a "node" if you represent a powergrid as a graph [mathematical object, not a plot] with the lines @@ -1781,11 +1826,19 @@ def bus_connectivity_matrix(self, as_csr_matrix=False, return_lines_index=False) return_lines_index: ``bool`` Whether to also return the bus index associated to both side of each powerline. + ``False`` by default, meaning the indexes are not returned. Returns ------- res: ``numpy.ndarray``, shape: (nb_bus, nb_bus) dtype:float The bus connectivity matrix defined above. + + optional: + + - `lor_bus` : for each powerline, it gives the id (row / column of the matrix) + of the bus of the matrix to which its origin end is connected + - `lex_bus` : for each powerline, it gives the id (row / column of the matrix) + of the bus of the matrix to which its extremity end is connected Notes ------ @@ -1887,10 +1940,13 @@ def _get_bus_id(self, id_topo_vect, sub_id): """ bus_id = 1 * self.topo_vect[id_topo_vect] connected = bus_id > 0 - bus_id[connected] = sub_id[connected] + (bus_id[connected] - 1) * self.n_sub + # bus_id[connected] = sub_id[connected] + (bus_id[connected] - 1) * self.n_sub + bus_id[connected] = type(self).local_bus_to_global(bus_id[connected], sub_id[connected]) return bus_id, connected - def flow_bus_matrix(self, active_flow=True, as_csr_matrix=False): + def flow_bus_matrix(self, + active_flow: bool=True, + as_csr_matrix: bool=False) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: """ A matrix of size "nb bus" "nb bus". Each row and columns represent a "bus" of the grid ("bus" is a power system word that for computer scientist means "nodes" if the powergrid is represented as a graph). @@ -1954,7 +2010,7 @@ def flow_bus_matrix(self, active_flow=True, as_csr_matrix=False): flow on the origin (or extremity) side of the powerline connecting bus `i` to bus `j` You can also know how much power - (total generation + total storage discharging - total load - total storage charging - ) + (total generation + total storage discharging - total load - total storage charging) is injected at each bus `i` by looking at the `i` th diagonal coefficient. @@ -1963,11 +2019,11 @@ def flow_bus_matrix(self, active_flow=True, as_csr_matrix=False): matrix. `flow_mat.sum(axis=1)` """ + cls = type(self) if self._is_done: flow_mat = csr_matrix((1,1), dtype=dt_float) if not as_csr_matrix: flow_mat = flow_mat.toarray() - cls = type(self) load_bus = np.zeros(cls.n_load, dtype=dt_int) prod_bus = np.zeros(cls.n_gen, dtype=dt_int) stor_bus = np.zeros(cls.n_storage, dtype=dt_int) @@ -1977,26 +2033,26 @@ def flow_bus_matrix(self, active_flow=True, as_csr_matrix=False): nb_bus, unique_bus, bus_or, bus_ex = self._aux_fun_get_bus() prod_bus, prod_conn = self._get_bus_id( - self.gen_pos_topo_vect, self.gen_to_subid + cls.gen_pos_topo_vect, cls.gen_to_subid ) load_bus, load_conn = self._get_bus_id( - self.load_pos_topo_vect, self.load_to_subid + cls.load_pos_topo_vect, cls.load_to_subid ) stor_bus, stor_conn = self._get_bus_id( - self.storage_pos_topo_vect, self.storage_to_subid + cls.storage_pos_topo_vect, cls.storage_to_subid ) lor_bus, lor_conn = self._get_bus_id( - self.line_or_pos_topo_vect, self.line_or_to_subid + cls.line_or_pos_topo_vect, cls.line_or_to_subid ) lex_bus, lex_conn = self._get_bus_id( - self.line_ex_pos_topo_vect, self.line_ex_to_subid + cls.line_ex_pos_topo_vect, cls.line_ex_to_subid ) - if type(self).shunts_data_available: + if cls.shunts_data_available: sh_bus = 1 * self._shunt_bus sh_bus[sh_bus > 0] = ( - self.shunt_to_subid[sh_bus > 0] * (sh_bus[sh_bus > 0] - 1) - + self.shunt_to_subid[sh_bus > 0] + cls.shunt_to_subid[sh_bus > 0] * (sh_bus[sh_bus > 0] - 1) + + cls.shunt_to_subid[sh_bus > 0] ) sh_conn = self._shunt_bus != -1 @@ -2016,15 +2072,15 @@ def flow_bus_matrix(self, active_flow=True, as_csr_matrix=False): or_vect = self.p_or ex_vect = self.p_ex stor_vect = self.storage_power - if type(self).shunts_data_available: + if cls.shunts_data_available: sh_vect = self._shunt_p else: prod_vect = self.gen_q load_vect = self.load_q or_vect = self.q_or ex_vect = self.q_ex - stor_vect = np.zeros(self.n_storage, dtype=dt_float) - if type(self).shunts_data_available: + stor_vect = np.zeros(cls.n_storage, dtype=dt_float) + if cls.shunts_data_available: sh_vect = self._shunt_q nb_lor = lor_conn.sum() @@ -2065,7 +2121,7 @@ def flow_bus_matrix(self, active_flow=True, as_csr_matrix=False): ) data[bus_stor] -= map_mat.dot(stor_vect[stor_conn]) - if type(self).shunts_data_available: + if cls.shunts_data_available: # handle shunts nb_shunt = sh_conn.sum() if nb_shunt: @@ -2419,8 +2475,9 @@ def get_energy_graph(self) -> networkx.Graph: def _aux_get_connected_buses(self): res = np.full(2 * self.n_sub, fill_value=False) - global_bus = type(self).local_bus_to_global(self.topo_vect, - self._topo_vect_to_sub) + cls = type(self) + global_bus = cls.local_bus_to_global(self.topo_vect, + cls._topo_vect_to_sub) res[np.unique(global_bus[global_bus != -1])] = True return res @@ -2749,6 +2806,7 @@ def get_elements_graph(self) -> networkx.DiGraph: ------- networkx.DiGraph The "elements graph", see :ref:`elmnt-graph-gg` . + """ cls = type(self) @@ -2816,7 +2874,7 @@ def get_elements_graph(self) -> networkx.DiGraph: networkx.freeze(graph) return graph - def get_forecasted_inj(self, time_step=1): + def get_forecasted_inj(self, time_step:int =1) -> np.ndarray: """ This function allows you to retrieve directly the "forecast" injections for the step `time_step`. @@ -2845,11 +2903,12 @@ def get_forecasted_inj(self, time_step=1): time_step ) ) + cls = type(self) t, a = self._forecasted_inj[time_step] - prod_p_f = np.full(self.n_gen, fill_value=np.NaN, dtype=dt_float) - prod_v_f = np.full(self.n_gen, fill_value=np.NaN, dtype=dt_float) - load_p_f = np.full(self.n_load, fill_value=np.NaN, dtype=dt_float) - load_q_f = np.full(self.n_load, fill_value=np.NaN, dtype=dt_float) + prod_p_f = np.full(cls.n_gen, fill_value=np.NaN, dtype=dt_float) + prod_v_f = np.full(cls.n_gen, fill_value=np.NaN, dtype=dt_float) + load_p_f = np.full(cls.n_load, fill_value=np.NaN, dtype=dt_float) + load_q_f = np.full(cls.n_load, fill_value=np.NaN, dtype=dt_float) if "prod_p" in a["injection"]: prod_p_f = a["injection"]["prod_p"] @@ -2869,7 +2928,7 @@ def get_forecasted_inj(self, time_step=1): load_q_f[tmp_arg] = self.load_q[tmp_arg] return prod_p_f, prod_v_f, load_p_f, load_q_f - def get_time_stamp(self): + def get_time_stamp(self) -> datetime.datetime: """ Get the time stamp of the current observation as a `datetime.datetime` object """ @@ -2882,7 +2941,23 @@ def get_time_stamp(self): ) return res - def simulate(self, action, time_step=1): + def simulate(self, action : "grid2op.Action.BaseAction", time_step:int=1) -> Tuple["BaseObservation", + float, + bool, + Dict[Literal["disc_lines", + "is_illegal", + "is_ambiguous", + "is_dispatching_illegal", + "is_illegal_reco", + "reason_alarm_illegal", + "reason_alert_illegal", + "opponent_attack_line", + "opponent_attack_sub", + "exception", + "detailed_infos_for_cascading_failures", + "rewards", + "time_series_id"], + Any]]: """ This method is used to simulate the effect of an action on a forecast powergrid state. This forecast state is built upon the current observation. @@ -3155,7 +3230,7 @@ def simulate(self, action, time_step=1): sim_obs._update_internal_env_params(self._obs_env) return (sim_obs, *rest) # parentheses are needed for python 3.6 at least. - def copy(self): + def copy(self) -> Self: """ INTERNAL @@ -3195,7 +3270,7 @@ def copy(self): return res @property - def line_or_bus(self): + def line_or_bus(self) -> np.ndarray: """ Retrieve the busbar at which each origin end of powerline is connected. @@ -3217,7 +3292,7 @@ def line_or_bus(self): return res @property - def line_ex_bus(self): + def line_ex_bus(self) -> np.ndarray: """ Retrieve the busbar at which each extremity end of powerline is connected. @@ -3239,7 +3314,7 @@ def line_ex_bus(self): return res @property - def gen_bus(self): + def gen_bus(self) -> np.ndarray: """ Retrieve the busbar at which each generator is connected. @@ -3261,7 +3336,7 @@ def gen_bus(self): return res @property - def load_bus(self): + def load_bus(self) -> np.ndarray: """ Retrieve the busbar at which each load is connected. @@ -3283,7 +3358,7 @@ def load_bus(self): return res @property - def storage_bus(self): + def storage_bus(self) -> np.ndarray: """ Retrieve the busbar at which each storage unit is connected. @@ -3305,7 +3380,7 @@ def storage_bus(self): return res @property - def prod_p(self): + def prod_p(self) -> np.ndarray: """ As of grid2op version 1.5.0, for better consistency, the "prod_p" attribute has been renamed "gen_p", see the doc of :attr:`BaseObservation.gen_p` for more information. @@ -3320,7 +3395,7 @@ def prod_p(self): return self.gen_p @property - def prod_q(self): + def prod_q(self) -> np.ndarray: """ As of grid2op version 1.5.0, for better consistency, the "prod_q" attribute has been renamed "gen_q", see the doc of :attr:`BaseObservation.gen_q` for more information. @@ -3335,7 +3410,7 @@ def prod_q(self): return self.gen_q @property - def prod_v(self): + def prod_v(self) -> np.ndarray: """ As of grid2op version 1.5.0, for better consistency, the "prod_v" attribute has been renamed "gen_v", see the doc of :attr:`BaseObservation.gen_v` for more information. @@ -3349,7 +3424,7 @@ def prod_v(self): """ return self.gen_v - def sub_topology(self, sub_id): + def sub_topology(self, sub_id) -> np.ndarray: """ Returns the topology of the given substation. @@ -3523,7 +3598,7 @@ def to_dict(self): return self._dictionnarized - def add_act(self, act, issue_warn=True): + def add_act(self, act : "grid2op.Action.BaseAction", issue_warn=True) -> Self: """ Easier access to the impact on the observation if an action were applied. @@ -3612,8 +3687,11 @@ def add_act(self, act, issue_warn=True): if not isinstance(act, BaseAction): raise RuntimeError("You can only add actions to observation at the moment") + cls = type(self) + cls_act = type(act) + act = copy.deepcopy(act) - res = type(self)() + res = cls() res.set_game_over(env=None) res.topo_vect[:] = self.topo_vect @@ -3627,14 +3705,14 @@ def add_act(self, act, issue_warn=True): ) # if a powerline has been reconnected without specific bus, i issue a warning - if "set_line_status" in act.authorized_keys: + if "set_line_status" in cls_act.authorized_keys: reco_powerline = act.line_set_status - if "set_bus" in act.authorized_keys: + if "set_bus" in cls_act.authorized_keys: line_ex_set_bus = act.line_ex_set_bus line_or_set_bus = act.line_or_set_bus else: - line_ex_set_bus = np.zeros(res.n_line, dtype=dt_int) - line_or_set_bus = np.zeros(res.n_line, dtype=dt_int) + line_ex_set_bus = np.zeros(cls.n_line, dtype=dt_int) + line_or_set_bus = np.zeros(cls.n_line, dtype=dt_int) error_no_bus_set = ( "You reconnected a powerline with your action but did not specify on which bus " "to reconnect both its end. This behaviour, also perfectly fine for an environment " @@ -3645,85 +3723,85 @@ def add_act(self, act, issue_warn=True): tmp = ( (reco_powerline == 1) & (line_ex_set_bus <= 0) - & (res.topo_vect[self.line_ex_pos_topo_vect] == -1) + & (res.topo_vect[cls.line_ex_pos_topo_vect] == -1) ) if tmp.any(): id_issue_ex = np.where(tmp)[0] if issue_warn: warnings.warn(error_no_bus_set.format(id_issue_ex)) - if "set_bus" in act.authorized_keys: + if "set_bus" in cls_act.authorized_keys: # assign 1 in the bus in this case act.line_ex_set_bus = [(el, 1) for el in id_issue_ex] tmp = ( (reco_powerline == 1) & (line_or_set_bus <= 0) - & (res.topo_vect[self.line_or_pos_topo_vect] == -1) + & (res.topo_vect[cls.line_or_pos_topo_vect] == -1) ) if tmp.any(): id_issue_or = np.where(tmp)[0] if issue_warn: warnings.warn(error_no_bus_set.format(id_issue_or)) - if "set_bus" in act.authorized_keys: + if "set_bus" in cls_act.authorized_keys: # assign 1 in the bus in this case act.line_or_set_bus = [(el, 1) for el in id_issue_or] # topo vect - if "set_bus" in act.authorized_keys: + if "set_bus" in cls_act.authorized_keys: res.topo_vect[act.set_bus != 0] = act.set_bus[act.set_bus != 0] - if "change_bus" in act.authorized_keys: + if "change_bus" in cls_act.authorized_keys: do_change_bus_on = act.change_bus & ( res.topo_vect > 0 ) # change bus of elements that were on res.topo_vect[do_change_bus_on] = 3 - res.topo_vect[do_change_bus_on] # topo vect: reco of powerline that should be - res.line_status = (res.topo_vect[self.line_or_pos_topo_vect] >= 1) & ( - res.topo_vect[self.line_ex_pos_topo_vect] >= 1 + res.line_status = (res.topo_vect[cls.line_or_pos_topo_vect] >= 1) & ( + res.topo_vect[cls.line_ex_pos_topo_vect] >= 1 ) # powerline status - if "set_line_status" in act.authorized_keys: + if "set_line_status" in cls_act.authorized_keys: disco_line = (act.line_set_status == -1) & res.line_status - res.topo_vect[res.line_or_pos_topo_vect[disco_line]] = -1 - res.topo_vect[res.line_ex_pos_topo_vect[disco_line]] = -1 + res.topo_vect[cls.line_or_pos_topo_vect[disco_line]] = -1 + res.topo_vect[cls.line_ex_pos_topo_vect[disco_line]] = -1 res.line_status[disco_line] = False reco_line = (act.line_set_status >= 1) & (~res.line_status) # i can do that because i already "fixed" the action to have it put 1 in case it # bus were not provided - if "set_bus" in act.authorized_keys: + if "set_bus" in cls_act.authorized_keys: # I assign previous bus (because it could have been modified) res.topo_vect[ - res.line_or_pos_topo_vect[reco_line] + cls.line_or_pos_topo_vect[reco_line] ] = act.line_or_set_bus[reco_line] res.topo_vect[ - res.line_ex_pos_topo_vect[reco_line] + cls.line_ex_pos_topo_vect[reco_line] ] = act.line_ex_set_bus[reco_line] else: # I assign one (action do not allow me to modify the bus) - res.topo_vect[res.line_or_pos_topo_vect[reco_line]] = 1 - res.topo_vect[res.line_ex_pos_topo_vect[reco_line]] = 1 + res.topo_vect[cls.line_or_pos_topo_vect[reco_line]] = 1 + res.topo_vect[cls.line_ex_pos_topo_vect[reco_line]] = 1 res.line_status[reco_line] = True - if "change_line_status" in act.authorized_keys: + if "change_line_status" in cls_act.authorized_keys: disco_line = act.line_change_status & res.line_status reco_line = act.line_change_status & (~res.line_status) # handle disconnected powerlines - res.topo_vect[res.line_or_pos_topo_vect[disco_line]] = -1 - res.topo_vect[res.line_ex_pos_topo_vect[disco_line]] = -1 + res.topo_vect[cls.line_or_pos_topo_vect[disco_line]] = -1 + res.topo_vect[cls.line_ex_pos_topo_vect[disco_line]] = -1 res.line_status[disco_line] = False # handle reconnected powerlines if reco_line.any(): - if "set_bus" in act.authorized_keys: + if "set_bus" in cls_act.authorized_keys: line_ex_set_bus = 1 * act.line_ex_set_bus line_or_set_bus = 1 * act.line_or_set_bus else: - line_ex_set_bus = np.zeros(res.n_line, dtype=dt_int) - line_or_set_bus = np.zeros(res.n_line, dtype=dt_int) + line_ex_set_bus = np.zeros(cls.n_line, dtype=dt_int) + line_or_set_bus = np.zeros(cls.n_line, dtype=dt_int) if issue_warn and ( (line_or_set_bus[reco_line] == 0).any() @@ -3739,15 +3817,15 @@ def add_act(self, act, issue_warn=True): line_or_set_bus[reco_line & (line_or_set_bus == 0)] = 1 line_ex_set_bus[reco_line & (line_ex_set_bus == 0)] = 1 - res.topo_vect[res.line_or_pos_topo_vect[reco_line]] = line_or_set_bus[ + res.topo_vect[cls.line_or_pos_topo_vect[reco_line]] = line_or_set_bus[ reco_line ] - res.topo_vect[res.line_ex_pos_topo_vect[reco_line]] = line_ex_set_bus[ + res.topo_vect[cls.line_ex_pos_topo_vect[reco_line]] = line_ex_set_bus[ reco_line ] res.line_status[reco_line] = True - if "redispatch" in act.authorized_keys: + if "redispatch" in cls_act.authorized_keys: redisp = act.redispatch if (redisp != 0).any() and issue_warn: warnings.warn( @@ -3756,7 +3834,7 @@ def add_act(self, act, issue_warn=True): "generators for example) so we will not even try to mimic this here." ) - if "set_storage" in act.authorized_keys: + if "set_storage" in cls_act.authorized_keys: storage_p = act.storage_p if (storage_p != 0).any() and issue_warn: warnings.warn( @@ -3767,7 +3845,7 @@ def add_act(self, act, issue_warn=True): ) return res - def __add__(self, act): + def __add__(self, act: "grid2op.Action.BaseAction") -> Self: from grid2op.Action import BaseAction if isinstance(act, BaseAction): @@ -3777,7 +3855,7 @@ def __add__(self, act): ) @property - def thermal_limit(self): + def thermal_limit(self) -> np.ndarray: """ Return the thermal limit of the powergrid, given in Amps (A) @@ -3798,7 +3876,7 @@ def thermal_limit(self): return res @property - def curtailment_mw(self): + def curtailment_mw(self) -> np.ndarray: """ return the curtailment, expressed in MW rather than in ratio of pmax. @@ -3817,7 +3895,7 @@ def curtailment_mw(self): return self.curtailment * self.gen_pmax @property - def curtailment_limit_mw(self): + def curtailment_limit_mw(self) -> np.ndarray: """ return the limit of production of a generator in MW rather in ratio @@ -3835,7 +3913,7 @@ def curtailment_limit_mw(self): """ return self.curtailment_limit * self.gen_pmax - def _update_attr_backend(self, backend): + def _update_attr_backend(self, backend: "grid2op.Backend.Backend") -> None: """This function updates the attribute of the observation that depends only on the backend. @@ -3843,8 +3921,10 @@ def _update_attr_backend(self, backend): ---------- backend : The backend from which to update the observation + """ - + cls = type(self) + self.line_status[:] = backend.get_line_status() self.topo_vect[:] = backend.get_topo_vect() @@ -3857,15 +3937,15 @@ def _update_attr_backend(self, backend): self.rho[:] = backend.get_relative_flow().astype(dt_float) # margin up and down - if type(self).redispatching_unit_commitment_availble: + if cls.redispatching_unit_commitment_availble: self.gen_margin_up[:] = np.minimum( - type(self).gen_pmax - self.gen_p, self.gen_max_ramp_up + cls.gen_pmax - self.gen_p, self.gen_max_ramp_up ) - self.gen_margin_up[type(self).gen_renewable] = 0.0 + self.gen_margin_up[cls.gen_renewable] = 0.0 self.gen_margin_down[:] = np.minimum( - self.gen_p - type(self).gen_pmin, self.gen_max_ramp_down + self.gen_p - cls.gen_pmin, self.gen_max_ramp_down ) - self.gen_margin_down[type(self).gen_renewable] = 0.0 + self.gen_margin_down[cls.gen_renewable] = 0.0 # because of the slack, sometimes it's negative... # see https://github.com/rte-france/Grid2Op/issues/313 @@ -3876,7 +3956,7 @@ def _update_attr_backend(self, backend): self.gen_margin_down[:] = 0.0 # handle shunts (if avaialble) - if type(self).shunts_data_available: + if cls.shunts_data_available: sh_p, sh_q, sh_v, sh_bus = backend.shunt_info() self._shunt_p[:] = sh_p self._shunt_q[:] = sh_q @@ -3900,7 +3980,7 @@ def _update_attr_backend(self, backend): self.gen_theta[:] = 0. self.storage_theta[:] = 0. - def _update_internal_env_params(self, env): + def _update_internal_env_params(self, env: "grid2op.Environment.BaseEnv"): # this is only done if the env supports forecast # some parameters used for the "forecast env" # but not directly accessible in the observation @@ -3925,7 +4005,7 @@ def _update_internal_env_params(self, env): # (self._env_internal_params["opp_space_state"], # self._env_internal_params["opp_state"]) = env._oppSpace._get_state() - def _update_obs_complete(self, env, with_forecast=True): + def _update_obs_complete(self, env: "grid2op.Environment.BaseEnv", with_forecast:bool=True): """ update all the observation attributes as if it was a complete, fully observable and without noise observation @@ -4001,7 +4081,7 @@ def _update_obs_complete(self, env, with_forecast=True): self._update_alert(env) - def _update_forecast(self, env, with_forecast): + def _update_forecast(self, env: "grid2op.Environment.BaseEnv", with_forecast: bool) -> None: if not with_forecast: return @@ -4020,7 +4100,7 @@ def _update_forecast(self, env, with_forecast): self._env_internal_params = {} self._update_internal_env_params(env) - def _update_alarm(self, env): + def _update_alarm(self, env: "grid2op.Environment.BaseEnv"): if not (self.dim_alarms and env._has_attention_budget): return @@ -4035,7 +4115,7 @@ def _update_alarm(self, env): self.last_alarm[:] = env._attention_budget.last_successful_alarm_raised self.attention_budget[:] = env._attention_budget.current_budget - def _update_alert(self, env): + def _update_alert(self, env: "grid2op.Environment.BaseEnv"): self.active_alert[:] = env._last_alert self.time_since_last_alert[:] = env._time_since_last_alert self.alert_duration[:] = env._alert_duration @@ -4102,7 +4182,7 @@ def get_simulator(self) -> "grid2op.simulator.Simulator": self._obs_env.highres_sim_counter._HighResSimCounter__nb_highres_called = nb_highres_called return res - def _get_array_from_forecast(self, name): + def _get_array_from_forecast(self, name: str) -> np.ndarray: if len(self._forecasted_inj) <= 1: # self._forecasted_inj already embed the current step raise NoForecastAvailable("It appears this environment does not support any forecast at all.") @@ -4120,7 +4200,7 @@ def _get_array_from_forecast(self, name): res[h,:] = this_row return res - def _generate_forecasted_maintenance_for_simenv(self, nb_h: int): + def _generate_forecasted_maintenance_for_simenv(self, nb_h: int) -> np.ndarray: n_line = type(self).n_line res = np.full((nb_h, n_line), fill_value=False, dtype=dt_bool) for l_id in range(n_line): @@ -4240,7 +4320,7 @@ def get_forecast_env(self) -> "grid2op.Environment.Environment": maintenance = self._generate_forecasted_maintenance_for_simenv(prod_v.shape[0]) return self._make_env_from_arays(load_p, load_q, prod_p, prod_v, maintenance) - def get_forecast_arrays(self): + def get_forecast_arrays(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: """ This functions allows to retrieve (as numpy arrays) the values for all the loads / generators / maintenance for the forseable future (they are the forecast availble in :func:`BaseObservation.simulate` and @@ -4460,7 +4540,7 @@ def _make_env_from_arays(self, res.highres_sim_counter._HighResSimCounter__nb_highres_called = nb_highres_called return res - def change_forecast_parameters(self, params): + def change_forecast_parameters(self, params: "grid2op.Parameters.Parameters") -> None: """This function allows to change the parameters (see :class:`grid2op.Parameters.Parameters` for more information) that are used for the `obs.simulate()` and `obs.get_forecast_env()` method. @@ -4500,7 +4580,7 @@ def change_forecast_parameters(self, params): self._obs_env.change_parameters(params) self._obs_env._parameters = params - def update_after_reward(self, env): + def update_after_reward(self, env: "grid2op.Environment.BaseEnv") -> None: """Only called for the regular environment (so not available for :func:`BaseObservation.get_forecast_env` or :func:`BaseObservation.simulate`) diff --git a/grid2op/gym_compat/discrete_gym_actspace.py b/grid2op/gym_compat/discrete_gym_actspace.py index e059a04b8..d3f8010fe 100644 --- a/grid2op/gym_compat/discrete_gym_actspace.py +++ b/grid2op/gym_compat/discrete_gym_actspace.py @@ -363,7 +363,7 @@ def close(self): from gymnasium.spaces import Discrete from grid2op.gym_compat.box_gym_actspace import BoxGymnasiumActSpace from grid2op.gym_compat.continuous_to_discrete import ContinuousToDiscreteConverterGymnasium - DiscreteActSpaceGymnasium = type("MultiDiscreteActSpaceGymnasium", + DiscreteActSpaceGymnasium = type("DiscreteActSpaceGymnasium", (__AuxDiscreteActSpace, Discrete, ), {"_gymnasium": True, "_DiscreteType": Discrete, diff --git a/grid2op/simulator/simulator.py b/grid2op/simulator/simulator.py index 142097944..c7493b6bf 100644 --- a/grid2op/simulator/simulator.py +++ b/grid2op/simulator/simulator.py @@ -7,6 +7,11 @@ # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. import copy from typing import Optional, Tuple +try: + from typing import Self +except ImportError: + from typing_extensions import Self + import numpy as np import os from scipy.optimize import minimize @@ -70,7 +75,7 @@ def __init__( f"inheriting from BaseEnv" ) if env.backend._can_be_copied: - self.backend = env.backend.copy() + self.backend: Backend = env.backend.copy() else: raise SimulatorError("Impossible to make a Simulator when you " "cannot copy the backend of the environment.") @@ -100,7 +105,7 @@ def converged(self) -> bool: def converged(self, values): raise SimulatorError("Cannot set this property.") - def copy(self) -> "Simulator": + def copy(self) -> Self: """Allows to perform a (deep) copy of the simulator. Returns @@ -126,7 +131,7 @@ def copy(self) -> "Simulator": res._highres_sim_counter = self._highres_sim_counter return res - def change_backend(self, backend: Backend): + def change_backend(self, backend: Backend) -> None: """You can use this function in case you want to change the "solver" use to perform the computation. For example, you could use a machine learning based model to do the computation (to accelerate them), provided diff --git a/grid2op/tests/test_gymnasium_compat.py b/grid2op/tests/test_gymnasium_compat.py index c7417e26b..dd06153b3 100644 --- a/grid2op/tests/test_gymnasium_compat.py +++ b/grid2op/tests/test_gymnasium_compat.py @@ -93,7 +93,12 @@ class TestMultiDiscreteGymnasiumActSpace(_AuxTestMultiDiscreteGymActSpace, Auxil pass class TestDiscreteGymnasiumActSpace(_AuxTestDiscreteGymActSpace, AuxilliaryForTestGymnasium, unittest.TestCase): - pass + def test_class_different_from_multi_discrete(self): + from grid2op.gym_compat import (DiscreteActSpaceGymnasium, + MultiDiscreteActSpaceGymnasium) + assert DiscreteActSpaceGymnasium is not MultiDiscreteActSpaceGymnasium + assert DiscreteActSpaceGymnasium.__doc__ != MultiDiscreteActSpaceGymnasium.__doc__ + assert DiscreteActSpaceGymnasium.__name__ != MultiDiscreteActSpaceGymnasium.__name__ class TestAllGymnasiumActSpaceWithAlarm(_AuxTestAllGymActSpaceWithAlarm, AuxilliaryForTestGymnasium, unittest.TestCase): pass diff --git a/grid2op/tests/test_n_busbar_per_sub.py b/grid2op/tests/test_n_busbar_per_sub.py index a414feb66..b70392173 100644 --- a/grid2op/tests/test_n_busbar_per_sub.py +++ b/grid2op/tests/test_n_busbar_per_sub.py @@ -1171,6 +1171,8 @@ def test_move_load(self): assert not self.env.backend._grid.line.iloc[line_or_id]["in_service"] else: assert not self.env.backend._grid.line.iloc[line_ex_id]["in_service"] + topo_vect = self.env.backend._get_topo_vect() + assert topo_vect[cls.load_pos_topo_vect[el_id]] == new_bus, f"{topo_vect[cls.load_pos_topo_vect[el_id]]} vs {new_bus}" def test_move_gen(self): cls = type(self.env) @@ -1201,6 +1203,8 @@ def test_move_gen(self): assert not self.env.backend._grid.line.iloc[line_or_id]["in_service"] else: assert not self.env.backend._grid.line.iloc[line_ex_id]["in_service"] + topo_vect = self.env.backend._get_topo_vect() + assert topo_vect[cls.gen_pos_topo_vect[el_id]] == new_bus, f"{topo_vect[cls.gen_pos_topo_vect[el_id]]} vs {new_bus}" def test_move_storage(self): cls = type(self.env) @@ -1220,17 +1224,20 @@ def test_move_storage(self): global_bus = sub_id + (new_bus -1) * cls.n_sub if new_bus >= 1: assert self.env.backend._grid.storage.iloc[el_id]["bus"] == global_bus + assert self.env.backend._grid.storage.iloc[el_id]["in_service"], f"storage should not be deactivated" if line_or_id is not None: assert self.env.backend._grid.line.iloc[line_or_id]["from_bus"] == global_bus else: assert self.env.backend._grid.line.iloc[line_ex_id]["to_bus"] == global_bus assert self.env.backend._grid.bus.loc[global_bus]["in_service"] else: - assert not self.env.backend._grid.storage.iloc[el_id]["in_service"] + assert not self.env.backend._grid.storage.iloc[el_id]["in_service"], f"storage should be deactivated" if line_or_id is not None: assert not self.env.backend._grid.line.iloc[line_or_id]["in_service"] else: assert not self.env.backend._grid.line.iloc[line_ex_id]["in_service"] + topo_vect = self.env.backend._get_topo_vect() + assert topo_vect[cls.storage_pos_topo_vect[el_id]] == new_bus, f"{topo_vect[cls.storage_pos_topo_vect[el_id]]} vs {new_bus}" def test_move_line_or(self): cls = type(self.env) @@ -1246,6 +1253,9 @@ 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 + 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}" def test_move_line_ex(self): cls = type(self.env) @@ -1261,6 +1271,9 @@ 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 + 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}" def test_move_shunt(self): cls = type(self.env) @@ -1291,18 +1304,150 @@ def test_move_shunt(self): assert not self.env.backend._grid.line.iloc[line_or_id]["in_service"] else: assert not self.env.backend._grid.line.iloc[line_ex_id]["in_service"] + + def test_check_kirchoff(self): + cls = type(self.env) + res = self._aux_find_sub(self.env, cls.LOA_COL) + if res is None: + raise RuntimeError("Cannot carry the test 'test_move_load' as " + "there are no suitable subastation in your grid.") + (sub_id, el_id, line_or_id, line_ex_id) = res + for new_bus in self.list_loc_bus: + if new_bus <= -1: + continue + if line_or_id is not None: + act = self.env.action_space({"set_bus": {"loads_id": [(el_id, new_bus)], "lines_or_id": [(line_or_id, new_bus)]}}) + else: + act = self.env.action_space({"set_bus": {"loads_id": [(el_id, new_bus)], "lines_ex_id": [(line_ex_id, new_bus)]}}) + bk_act = self.env._backend_action_class() + bk_act += act + self.env.backend.apply_action(bk_act) + conv, maybe_exc = self.env.backend.runpf() + assert conv, f"error : {maybe_exc}" + p_subs, q_subs, p_bus, q_bus, diff_v_bus = self.env.backend.check_kirchoff() + # assert laws are met + assert np.abs(p_subs).max() <= 1e-5, f"error for busbar {new_bus}: {np.abs(p_subs).max():.2e}" + assert np.abs(q_subs).max() <= 1e-5, f"error for busbar {new_bus}: {np.abs(q_subs).max():.2e}" + assert np.abs(p_bus).max() <= 1e-5, f"error for busbar {new_bus}: {np.abs(p_bus).max():.2e}" + assert np.abs(q_bus).max() <= 1e-5, f"error for busbar {new_bus}: {np.abs(q_bus).max():.2e}" + assert np.abs(diff_v_bus).max() <= 1e-5, f"error for busbar {new_bus}: {np.abs(diff_v_bus).max():.2e}" class TestPandapowerBackend_1busbar(TestPandapowerBackend_3busbars): def get_nb_bus(self): - return 3 + return 1 class TestObservation(unittest.TestCase): + def get_nb_bus(self): + return 3 + + def get_env_nm(self): + return "educ_case14_storage" + + def get_reset_kwargs(self) -> dict: + return dict(seed=0, options={"time serie id": 0}) + + def setUp(self) -> None: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make(self.get_env_nm(), + backend=PandaPowerBackend(), + action_class=CompleteAction, + test=True, + n_busbar=self.get_nb_bus(), + _add_to_name=type(self).__name__ + f'_{self.get_nb_bus()}') + self.list_loc_bus = list(range(1, type(self.env).n_busbar_per_sub + 1)) + return super().setUp() + + def tearDown(self) -> None: + self.env.close() + return super().tearDown() + + def test_get_simulator(self): + obs = self.env.reset(**self.get_reset_kwargs()) + sim = obs.get_simulator() + assert type(sim.backend).n_busbar_per_sub == self.get_nb_bus() + res = TestPandapowerBackend_3busbars._aux_find_sub(self.env, type(self.env).LOA_COL) + if res is None: + raise RuntimeError(f"Cannot carry the test 'test_get_simulator' as " + "there are no suitable subastation in your grid.") + (sub_id, el_id, line_or_id, line_ex_id) = res + for new_bus in self.list_loc_bus: + if line_or_id is not None: + act = self.env.action_space({"set_bus": {"loads_id": [(el_id, new_bus)], "lines_or_id": [(line_or_id, new_bus)]}}) + else: + act = self.env.action_space({"set_bus": {"loads_id": [(el_id, new_bus)], "lines_ex_id": [(line_ex_id, new_bus)]}}) + sim2 = sim.predict(act) + global_bus = sub_id + (new_bus -1) * type(self.env).n_sub + assert sim2.backend._grid.load["bus"].iloc[el_id] == global_bus + + def _aux_build_act(self, res, new_bus, el_keys): + """res: output of TestPandapowerBackend_3busbars._aux_find_sub""" + if res is None: + raise RuntimeError(f"Cannot carry the test as " + "there are no suitable subastation in your grid.") + (sub_id, el_id, line_or_id, line_ex_id) = res + if line_or_id is not None: + act = self.env.action_space({"set_bus": {el_keys: [(el_id, new_bus)], "lines_or_id": [(line_or_id, new_bus)]}}) + else: + act = self.env.action_space({"set_bus": {el_keys: [(el_id, new_bus)], "lines_ex_id": [(line_ex_id, new_bus)]}}) + return act + + def test_get_forecasted_env(self): + obs = self.env.reset(**self.get_reset_kwargs()) + for_env = obs.get_forecast_env() + assert type(for_env).n_busbar_per_sub == self.get_nb_bus() + for_obs = for_env.reset() + assert type(for_obs).n_busbar_per_sub == self.get_nb_bus() + res = TestPandapowerBackend_3busbars._aux_find_sub(self.env, type(self.env).LOA_COL) + (sub_id, el_id, line_or_id, line_ex_id) = res + for new_bus in self.list_loc_bus: + for_env = obs.get_forecast_env() + act = self._aux_build_act(res, new_bus, "loads_id") + sim_obs, sim_r, sim_d, sim_info = for_env.step(act) + assert not sim_d, f"{sim_info['exception']}" + assert sim_obs.load_bus[el_id] == new_bus, f"{sim_obs.load_bus[el_id]} vs {new_bus}" + + def test_add(self): + obs = self.env.reset(**self.get_reset_kwargs()) + res = TestPandapowerBackend_3busbars._aux_find_sub(self.env, type(self.env).LOA_COL) + (sub_id, el_id, line_or_id, line_ex_id) = res + for new_bus in self.list_loc_bus: + act = self._aux_build_act(res, new_bus, "loads_id") + obs_pus_act = obs + act + assert obs_pus_act.load_bus[el_id] == new_bus, f"{obs_pus_act.load_bus[el_id]} vs {new_bus}" + + def test_simulate(self): + obs = self.env.reset(**self.get_reset_kwargs()) + res = TestPandapowerBackend_3busbars._aux_find_sub(self.env, type(self.env).LOA_COL) + (sub_id, el_id, line_or_id, line_ex_id) = res + for new_bus in self.list_loc_bus: + act = self._aux_build_act(res, new_bus, "loads_id") + sim_obs, sim_r, sim_d, sim_info = obs.simulate(act) + assert not sim_d, f"{sim_info['exception']}" + assert sim_obs.load_bus[el_id] == new_bus, f"{sim_obs.load_bus[el_id]} vs {new_bus}" + def test_action_space_get_back_to_ref_state(self): """test the :func:`grid2op.Action.SerializableActionSpace.get_back_to_ref_state` when 3 busbars which could not be tested without observation""" pass + + def test_connectivity_matrix(self): + pass + + def test_bus_connectivity_matrix(self): + pass + + def test_flow_bus_matrix(self): + pass + + def test_get_energy_graph(self): + pass + + def test_get_elements_graph(self): + pass + class TestEnv(unittest.TestCase): From 0096ff3c43019f81330d36fb24544001e2841850 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 8 Feb 2024 14:56:08 +0100 Subject: [PATCH 17/24] observations should work with n_busbar_per_sub --- CHANGELOG.rst | 5 +- docs/grid_graph.rst | 32 +++- grid2op/Action/serializableActionSpace.py | 18 +- grid2op/Environment/baseEnv.py | 7 +- grid2op/Environment/environment.py | 7 +- grid2op/Environment/multiMixEnv.py | 7 +- grid2op/Environment/timedOutEnv.py | 7 +- grid2op/Observation/baseObservation.py | 203 ++++++++++++++++------ grid2op/Space/GridObjects.py | 6 +- grid2op/tests/test_n_busbar_per_sub.py | 193 ++++++++++++++++++-- 10 files changed, 380 insertions(+), 105 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5b3f8844c..c165ce823 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -60,7 +60,10 @@ Change Log - [IMPROVED] methods `gridobj.get_lines_id`, `gridobj.get_generators_id`, `gridobj.get_loads_id` `gridobj.get_storages_id` are now class methods and can be used with `type(env).get_lines_id(...)` or `act.get_lines_id(...)` for example. - +- [IMPROVED] `obs.get_energy_graph()` by giving the "local_bus_id" and the "global_bus_id" + of the bus that represents each node of this graph. +- [IMPROVED] `obs.get_elements_graph()` by giving access to the bus id (local, global and + id of the node) where each element is connected. [1.9.8] - 2024-01-26 ---------------------- diff --git a/docs/grid_graph.rst b/docs/grid_graph.rst index 8d2834cfa..6e3b77fe8 100644 --- a/docs/grid_graph.rst +++ b/docs/grid_graph.rst @@ -645,7 +645,14 @@ The next `n_load` nodes of the "elements graph" represent the "loads" of the gri - `id`: which load does this node represent (between 0 and `n_load - 1`) - `type`: always "loads" - `name`: the name of this load (equal to `obs.name_load[id]`) -- `connected`: whether or not this load is connected to the grid. +- `connected`: whether or not this load is connected to the grid +- `local_bus`: (from version 1.9.9) the id (local, so between `1, 2, ..., obs.n_busbar_per_sub`) + of the bus to which this load is connected +- `global_bus`: (from version 1.9.9) the id (global, so between `0, 1, ..., obs.n_busbar_per_sub * obs.n_sub`) + of the bus to which this load is connected +- `bus_node_id`: (from version 1.9.9) the id of the node of this graph representing the bus to which the + load is connected. This means that if the load is connected, then (node_load_id, bus_node_id) is the + outgoing edge in this graph. The outgoing edges from the nodes representing loads tell at which bus this load is connected (for each load, there is only one outgoing edge). They have attributes: @@ -676,6 +683,13 @@ The next `n_gen` nodes of the "elements graph" represent the "generators" of the - `curtailment_limit`: same as `obs.curtailment_limit[id]`, see :attr:`grid2op.Observation.BaseObservation.curtailment_limit` - `gen_margin_up`: same as `obs.gen_margin_up[id]`, see :attr:`grid2op.Observation.BaseObservation.gen_margin_up` - `gen_margin_down`: same as `obs.gen_margin_down[id]`, see :attr:`grid2op.Observation.BaseObservation.gen_margin_down` +- `local_bus`: (from version 1.9.9) the id (local, so between `1, 2, ..., obs.n_busbar_per_sub`) + of the bus to which this generator is connected +- `global_bus`: (from version 1.9.9) the id (global, so between `0, 1, ..., obs.n_busbar_per_sub * obs.n_sub`) + of the bus to which this generator is connected +- `bus_node_id`: (from version 1.9.9) the id of the node of this graph representing the bus to which the + generator is connected. This means that if the generator is connected, then (node_gen_id, bus_node_id) is the + outgoing edge in this graph. The outgoing edges from the nodes representing generators tell at which bus this generator is connected (for each generator, there is only one outgoing edge). They have attributes: @@ -740,6 +754,14 @@ The next `n_storage` nodes represent the storage units. They have attributes: - `connected`: whether or not this storage unit is connected to the grid2op - `storage_charge`: same as `obs.storage_charge[id]`, see :attr:`grid2op.Observation.BaseObservation.storage_charge` - `storage_power_target`: same as `obs.storage_power_target[id]`, see :attr:`grid2op.Observation.BaseObservation.storage_power_target` +- `local_bus`: (from version 1.9.9) the id (local, so between `1, 2, ..., obs.n_busbar_per_sub`) + of the bus to which this storage unit is connected +- `global_bus`: (from version 1.9.9) the id (global, so between `0, 1, ..., obs.n_busbar_per_sub * obs.n_sub`) + of the bus to which this storage unit is connected +- `bus_node_id`: (from version 1.9.9) the id of the node of this graph representing the bus to which the + storage unit is connected. This means that if the storage unit is connected, + then (node_storage_id, bus_node_id) is the + outgoing edge in this graph. The outgoing edges from the nodes representing storage units tells at which bus this load is connected (for each load, there is only one outgoing edge). They have attributes: @@ -759,6 +781,14 @@ The next `n_shunt` nodes represent the storage units. They have attributes: - `type`: always "shunt" - `name`: the name of this shunt (equal to `obs.name_shunt[id]`) - `connected`: whether or not this shunt is connected to the grid2op +- `local_bus`: (from version 1.9.9) the id (local, so between `1, 2, ..., obs.n_busbar_per_sub`) + of the bus to which this shunt is connected +- `global_bus`: (from version 1.9.9) the id (global, so between `0, 1, ..., obs.n_busbar_per_sub * obs.n_sub`) + of the bus to which this shunt is connected +- `bus_node_id`: (from version 1.9.9) the id of the node of this graph representing the bus to which the + shunt is connected. This means that if the shunt is connected, + then (node_shunt_id, bus_node_id) is the + outgoing edge in this graph. The outgoing edges from the nodes representing sthuns tell at which bus this shunt is connected (for each load, there is only one outgoing edge). They have attributes: diff --git a/grid2op/Action/serializableActionSpace.py b/grid2op/Action/serializableActionSpace.py index 55a164139..a050bf112 100644 --- a/grid2op/Action/serializableActionSpace.py +++ b/grid2op/Action/serializableActionSpace.py @@ -9,12 +9,13 @@ import warnings import numpy as np import itertools -from typing import Dict, List +from typing import Dict, List, Literal try: - from typing import Literal, Self + from typing import Self except ImportError: - from typing_extensions import Literal, Self + from typing_extensions import Self +import grid2op from grid2op.dtypes import dt_int, dt_float, dt_bool from grid2op.Exceptions import AmbiguousAction, Grid2OpException from grid2op.Space import SerializableSpace @@ -1148,14 +1149,14 @@ def get_all_unitary_topologies_set(action_space: Self, - if `add_alone_line=False` (not the default) then there must be at least two elements in a substation - .. info:: + .. note:: We try to make the result of this function as small as possible. This means that if at any substation the number of "valid" topology is only 1, it is ignored and will not be added in the result. This imply that when `env.n_busbar_per_sub=1` then this function returns the empty list. - .. info:: + .. note:: If `add_alone_line` is True (again NOT the default) then if any substation counts less than 3 elements or less then no action will be added for this substation. @@ -1670,7 +1671,12 @@ def get_back_to_ref_state( obs: "grid2op.Observation.BaseObservation", storage_setpoint: float=0.5, precision: int=5, - ) -> Dict[str, List[BaseAction]]: + ) -> Dict[Literal["powerline", + "substation", + "redispatching", + "storage", + "curtailment"], + List[BaseAction]]: """ This function returns the list of unary actions that you can perform in order to get back to the "fully meshed" / "initial" topology. diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index 047def9cd..90e664a7b 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -13,12 +13,7 @@ import copy import os import json -from typing import Optional, Tuple, Union, Dict, Any -try: - # Literal introduced in python 3.9 - from typing import Literal -except ImportError: - from typing_extensions import Literal +from typing import Optional, Tuple, Union, Dict, Any, Literal import warnings import numpy as np diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index f6ff9ca9b..ef2db3904 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -10,12 +10,7 @@ import warnings import numpy as np import re -from typing import Union, Any, Dict -try: - # Literal introduced in python 3.9 - from typing import Literal -except ImportError: - from typing_extensions import Literal +from typing import Union, Any, Dict, Literal import grid2op from grid2op.Opponent import OpponentSpace diff --git a/grid2op/Environment/multiMixEnv.py b/grid2op/Environment/multiMixEnv.py index 0c5368c76..e6ba1a646 100644 --- a/grid2op/Environment/multiMixEnv.py +++ b/grid2op/Environment/multiMixEnv.py @@ -10,12 +10,7 @@ import warnings import numpy as np import copy -from typing import Any, Dict, Tuple, Union, List -try: - # Literal introduced in python 3.9 - from typing import Literal -except ImportError: - from typing_extensions import Literal +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 diff --git a/grid2op/Environment/timedOutEnv.py b/grid2op/Environment/timedOutEnv.py index d01991a55..2b7c16d85 100644 --- a/grid2op/Environment/timedOutEnv.py +++ b/grid2op/Environment/timedOutEnv.py @@ -8,12 +8,7 @@ import time from math import floor -from typing import Any, Dict, Tuple, Union, List -try: - # Literal introduced in python 3.9 - from typing import Literal -except ImportError: - from typing_extensions import Literal +from typing import Any, Dict, Tuple, Union, List, Literal from grid2op.Environment.environment import Environment from grid2op.Action import BaseAction diff --git a/grid2op/Observation/baseObservation.py b/grid2op/Observation/baseObservation.py index 055ec32ce..65071a7ea 100644 --- a/grid2op/Observation/baseObservation.py +++ b/grid2op/Observation/baseObservation.py @@ -16,11 +16,11 @@ from typing import Optional from packaging import version -from typing import Dict, Union, Tuple, List, Optional, Any +from typing import Dict, Union, Tuple, List, Optional, Any, Literal try: - from typing import Self, Literal + from typing import Self except ImportError: - from typing_extensions import Self, Literal + from typing_extensions import Self import grid2op # for type hints from grid2op.dtypes import dt_int, dt_float, dt_bool @@ -2230,6 +2230,21 @@ def get_energy_graph(self) -> networkx.Graph: Convert this observation as a networkx graph. This graph is the graph "seen" by "the electron" / "the energy" of the power grid. + .. versionchanged:: 1.9.9 + Addition of the attribute `local_bus_id` and `global_bus_id` for the nodes of the returned graph. + + `local_bus_id` give the local bus id (from 1 to `obs.n_busbar_per_sub`) id of the + bus represented by this node. + + `global_bus_id` give the global bus id (from 0 to `obs.n_busbar_per_sub * obs.n_sub - 1`) id of the + bus represented by this node. + + .. versionchanged:: 1.9.9 + Addition of the attribute `global_bus_or` and `global_bus_ex` for the edges of the returned graph. + + These provides the global id of the `origin` / `ext` side to which powerline(s) represented by + this edge is (are) connected. + Notes ------ The resulting graph is "frozen" this means that you cannot add / remove attribute on nodes or edges, nor add / @@ -2237,7 +2252,7 @@ def get_energy_graph(self) -> networkx.Graph: This graphs has the following properties: - - it counts as many nodes as the number of buses of the grid + - it counts as many nodes as the number of buses of the grid (so it has a dynamic size !) - it counts less edges than the number of lines of the grid (two lines connecting the same buses are "merged" into one single edge - this is the case for parallel line, that are hence "merged" into the same edge) - nodes (represents "buses" of the grid) have attributes: @@ -2248,9 +2263,14 @@ def get_energy_graph(self) -> networkx.Graph: - `v`: the voltage magnitude at this node - `cooldown`: how much longer you need to wait before being able to merge / split or change this node - 'sub_id': the id of the substation to which it is connected (typically between `0` and `obs.n_sub - 1`) - - (optional) `theta`: the voltage angle (in degree) at this nodes + - 'local_bus_id': the local bus id (from 1 to `obs.n_busbar_per_sub`) of the bus represented by this node + (new in version 1.9.9) + - 'global_bus_id': the global bus id (from 0 to `obs.n_busbar_per_sub * obs.n_sub - 1`) + of the bus represented by this node + (new in version 1.9.9) - `cooldown` : the time you need to wait (in number of steps) before being able to act on the substation to which this bus is connected. + - (optional) `theta`: the voltage angle (in degree) at this nodes - edges have attributes too (in this modeling an edge might represent more than one powerline, all parallel powerlines are represented by the same edge): @@ -2269,16 +2289,26 @@ def get_energy_graph(self) -> networkx.Graph: - `p`: active power injected at the "or" side (equal to p_or) (in MW) - `v_or`: voltage magnitude at the "or" bus (in kV) - `v_ex`: voltage magnitude at the "ex" bus (in kV) - - (optional) `theta_or`: voltage angle at the "or" bus (in deg) - - (optional) `theta_ex`: voltage angle at the "ex" bus (in deg) - `time_next_maintenance`: see :attr:`BaseObservation.time_next_maintenance` (min over all powerline) - `duration_next_maintenance` see :attr:`BaseObservation.duration_next_maintenance` (max over all powerlines) - `sub_id_or`: id of the substation of the "or" side of the powerlines - `sub_id_ex`: id of the substation of the "ex" side of the powerlines - `node_id_or`: id of the node (in this graph) of the "or" side of the powergraph - `node_id_ex`: id of the node (in this graph) of the "ex" side of the powergraph - - `bus_or`: on which bus [1 or 2] is this powerline connected to at its "or" substation - - `bus_ex`: on which bus [1 or 2] is this powerline connected to at its "ex" substation + - `bus_or`: on which bus [1 or 2 or 3, etc.] is this powerline connected to at its "or" substation + (this is the local id of the bus) + - `bus_ex`: on which bus [1 or 2 or 3, etc.] is this powerline connected to at its "ex" substation + (this is the local id of the bus) + - 'global_bus_or': the global bus id (from 0 to `obs.n_busbar_per_sub * obs.n_sub - 1`) + of the bus to which the origin side of the line(s) represented by this edge + is (are) connected + (new in version 1.9.9) + - 'global_bus_ex': the global bus id (from 0 to `obs.n_busbar_per_sub * obs.n_sub - 1`) + of the bus to which the ext side of the line(s) represented by this edge + is (are) connected + (new in version 1.9.9) + - (optional) `theta_or`: voltage angle at the "or" bus (in deg) + - (optional) `theta_ex`: voltage angle at the "ex" bus (in deg) .. danger:: **IMPORTANT NOTE** edges represents "fusion" of 1 or more powerlines. This graph is intended to be @@ -2369,6 +2399,10 @@ def get_energy_graph(self) -> networkx.Graph: bus_subid = np.zeros(mat_p.shape[0], dtype=dt_int) bus_subid[lor_bus[self.line_status]] = cls.line_or_to_subid[self.line_status] bus_subid[lex_bus[self.line_status]] = cls.line_ex_to_subid[self.line_status] + loc_bus_id = np.zeros(mat_p.shape[0], dtype=int) + loc_bus_id[lor_bus[self.line_status]] = self.topo_vect[cls.line_or_pos_topo_vect[self.line_status]] + loc_bus_id[lex_bus[self.line_status]] = self.topo_vect[cls.line_ex_pos_topo_vect[self.line_status]] + glob_bus_id = cls.local_bus_to_global(loc_bus_id, bus_subid) if self.support_theta: bus_theta[lor_bus[self.line_status]] = self.theta_or[self.line_status] bus_theta[lex_bus[self.line_status]] = self.theta_ex[self.line_status] @@ -2408,7 +2442,14 @@ def get_energy_graph(self) -> networkx.Graph: networkx.set_node_attributes(graph, {el: self.time_before_cooldown_sub[val] for el, val in enumerate(bus_subid)}, "cooldown") - + # add local_id and global_id as attribute to the node of this graph + networkx.set_node_attributes( + graph, {el: val for el, val in enumerate(loc_bus_id)}, "local_bus_id" + ) + networkx.set_node_attributes( + graph, {el: val for el, val in enumerate(glob_bus_id)}, "global_bus_id" + ) + # add the edges attributes self._add_edges_multi(self.p_or, self.p_ex, "p", lor_bus, lex_bus, graph) self._add_edges_multi(self.q_or, self.q_ex, "q", lor_bus, lex_bus, graph) @@ -2468,17 +2509,25 @@ def get_energy_graph(self) -> networkx.Graph: self.line_ex_bus, "bus_ex", lor_bus, lex_bus, graph ) + self._add_edges_simple( + glob_bus_id[lor_bus], + "global_bus_or", lor_bus, lex_bus, graph + ) + self._add_edges_simple( + glob_bus_id[lex_bus], + "global_bus_ex", lor_bus, lex_bus, graph + ) # extra layer of security: prevent accidental modification of this graph networkx.freeze(graph) return graph def _aux_get_connected_buses(self): - res = np.full(2 * self.n_sub, fill_value=False) cls = type(self) + res = np.full(cls.n_busbar_per_sub * cls.n_sub, fill_value=False) global_bus = cls.local_bus_to_global(self.topo_vect, cls._topo_vect_to_sub) - res[np.unique(global_bus[global_bus != -1])] = True + res[global_bus[global_bus != -1]] = True return res def _aux_add_edges(self, @@ -2508,6 +2557,7 @@ def _aux_add_edges(self, li_el_edges[ed_num][-1][prop_nm] = prop_vect[el_id] ed_num += 1 graph.add_edges_from(li_el_edges) + return li_el_edges def _aux_add_el_to_comp_graph(self, graph, @@ -2543,30 +2593,37 @@ def _aux_add_el_to_comp_graph(self, el_connected = np.array(el_global_bus) >= 0 for el_id in range(nb_el): li_el_node[el_id][-1]["connected"] = el_connected[el_id] + li_el_node[el_id][-1]["local_bus"] = el_bus[el_id] + li_el_node[el_id][-1]["global_bus"] = el_global_bus[el_id] if nodes_prop is not None: for el_id in range(nb_el): for prop_nm, prop_vect in nodes_prop: li_el_node[el_id][-1][prop_nm] = prop_vect[el_id] - graph.add_nodes_from(li_el_node) - graph.graph[f"{el_name}_nodes_id"] = el_ids if el_bus is None and el_to_sub_id is None: + graph.add_nodes_from(li_el_node) + graph.graph[f"{el_name}_nodes_id"] = el_ids return el_ids # add the edges - self._aux_add_edges(el_ids, - cls, - el_global_bus, - nb_el, - el_connected, - el_name, - edges_prop, - graph) + li_el_edges = self._aux_add_edges(el_ids, + cls, + el_global_bus, + nb_el, + el_connected, + el_name, + edges_prop, + graph) + for el_id, (el_node_id, edege_id, *_) in enumerate(li_el_edges): + li_el_node[el_id][-1]["bus_node_id"] = edege_id + + graph.add_nodes_from(li_el_node) + graph.graph[f"{el_name}_nodes_id"] = el_ids return el_ids def _aux_add_buses(self, graph, cls, first_id): - bus_ids = first_id + np.arange(2 * cls.n_sub) + bus_ids = first_id + np.arange(cls.n_busbar_per_sub * cls.n_sub) conn_bus = self._aux_get_connected_buses() bus_li = [ (bus_ids[bus_id], @@ -2576,7 +2633,7 @@ def _aux_add_buses(self, graph, cls, first_id): "type": "bus", "connected": conn_bus[bus_id]} ) - for bus_id in range(2 * cls.n_sub) + for bus_id in range(cls.n_busbar_per_sub * cls.n_sub) ] graph.add_nodes_from(bus_li) edge_bus_li = [(bus_id, @@ -2676,15 +2733,32 @@ def _aux_add_edge_line_side(self, ] if theta_vect is not None: edges_prop.append(("theta", theta_vect)) - self._aux_add_edges(line_node_ids, - cls, - global_bus, - cls.n_line, - conn_, - "line", - edges_prop, - graph) - + res = self._aux_add_edges(line_node_ids, + cls, + global_bus, + cls.n_line, + conn_, + "line", + edges_prop, + graph) + return res + + def _aux_add_local_global(self, cls, graph, lin_ids, el_loc_bus, xxx_subid, side): + el_global_bus = cls.local_bus_to_global(el_loc_bus, + xxx_subid) + dict_ = {} + for el_node_id, loc_bus in zip(lin_ids, el_loc_bus): + dict_[el_node_id] = loc_bus + networkx.set_node_attributes( + graph, dict_, f"local_bus_{side}" + ) + dict_ = {} + for el_node_id, glob_bus in zip(lin_ids, el_global_bus): + dict_[el_node_id] = glob_bus + networkx.set_node_attributes( + graph, dict_, f"global_bus_{side}" + ) + def _aux_add_lines(self, graph, cls, first_id): nodes_prop = [("rho", self.rho), ("connected", self.line_status), @@ -2693,6 +2767,7 @@ def _aux_add_lines(self, graph, cls, first_id): ("time_next_maintenance", self.time_next_maintenance), ("duration_next_maintenance", self.duration_next_maintenance), ] + # only add the nodes, not the edges right now lin_ids = self._aux_add_el_to_comp_graph(graph, first_id, @@ -2704,32 +2779,47 @@ def _aux_add_lines(self, graph, cls, first_id): nodes_prop=nodes_prop, edges_prop=None ) + self._aux_add_local_global(cls, graph, lin_ids, self.line_or_bus, cls.line_or_to_subid, "or") + self._aux_add_local_global(cls, graph, lin_ids, self.line_ex_bus, cls.line_ex_to_subid, "ex") # add "or" edges - self._aux_add_edge_line_side(cls, - graph, - self.line_or_bus, - cls.line_or_to_subid, - lin_ids, - "or", - self.p_or, - self.q_or, - self.v_or, - self.a_or, - self.theta_or if self.support_theta else None) + li_el_edges_or = self._aux_add_edge_line_side(cls, + graph, + self.line_or_bus, + cls.line_or_to_subid, + lin_ids, + "or", + self.p_or, + self.q_or, + self.v_or, + self.a_or, + self.theta_or if self.support_theta else None) + dict_or = {} + for el_id, (el_node_id, edege_id, *_) in enumerate(li_el_edges_or): + dict_or[el_node_id] = edege_id + networkx.set_node_attributes( + graph, dict_or, "bus_node_id_or" + ) # add "ex" edges - self._aux_add_edge_line_side(cls, - graph, - self.line_ex_bus, - cls.line_ex_to_subid, - lin_ids, - "ex", - self.p_ex, - self.q_ex, - self.v_ex, - self.a_ex, - self.theta_ex if self.support_theta else None) + li_el_edges_ex = self._aux_add_edge_line_side(cls, + graph, + self.line_ex_bus, + cls.line_ex_to_subid, + lin_ids, + "ex", + self.p_ex, + self.q_ex, + self.v_ex, + self.a_ex, + self.theta_ex if self.support_theta else None) + dict_ex = {} + for el_id, (el_node_id, edege_id, *_) in enumerate(li_el_edges_ex): + dict_ex[el_node_id] = edege_id + networkx.set_node_attributes( + graph, dict_ex, "bus_node_id_ex" + ) + return lin_ids def _aux_add_shunts(self, graph, cls, first_id): @@ -2756,7 +2846,8 @@ def get_elements_graph(self) -> networkx.DiGraph: """This function returns the "elements graph" as a networkx object. .. seealso:: - This object is extensively described in the documentation, see :ref:`elmnt-graph-gg` for more information. + This object is extensively described in the documentation, + see :ref:`elmnt-graph-gg` for more information. Basically, each "element" of the grid (element = a substation, a bus, a load, a generator, a powerline, a storate unit or a shunt) is represented by a node in this graph. diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 6318c9730..1ec0f8a1d 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -21,11 +21,7 @@ import copy import numpy as np from packaging import version -from typing import Dict, Union -try: - from typing import Literal -except ImportError: - from typing_extensions import Literal +from typing import Dict, Union, Literal import grid2op from grid2op.dtypes import dt_int, dt_float, dt_bool diff --git a/grid2op/tests/test_n_busbar_per_sub.py b/grid2op/tests/test_n_busbar_per_sub.py index b70392173..5d1e794f4 100644 --- a/grid2op/tests/test_n_busbar_per_sub.py +++ b/grid2op/tests/test_n_busbar_per_sub.py @@ -1338,7 +1338,7 @@ def get_nb_bus(self): return 1 -class TestObservation(unittest.TestCase): +class TestObservation_3busbars(unittest.TestCase): def get_nb_bus(self): return 3 @@ -1357,6 +1357,14 @@ def setUp(self) -> None: test=True, n_busbar=self.get_nb_bus(), _add_to_name=type(self).__name__ + f'_{self.get_nb_bus()}') + param = self.env.parameters + param.NB_TIMESTEP_COOLDOWN_SUB = 0 + param.NB_TIMESTEP_COOLDOWN_LINE = 0 + param.MAX_LINE_STATUS_CHANGED = 99999 + param.MAX_SUB_CHANGED = 99999 + self.env.change_parameters(param) + self.env.change_forecast_parameters(param) + self.env.reset(**self.get_reset_kwargs()) self.list_loc_bus = list(range(1, type(self.env).n_busbar_per_sub + 1)) return super().setUp() @@ -1431,25 +1439,186 @@ def test_simulate(self): def test_action_space_get_back_to_ref_state(self): """test the :func:`grid2op.Action.SerializableActionSpace.get_back_to_ref_state` when 3 busbars which could not be tested without observation""" - pass + obs = self.env.reset(**self.get_reset_kwargs()) + res = TestPandapowerBackend_3busbars._aux_find_sub(self.env, type(self.env).LOA_COL) + (sub_id, el_id, line_or_id, line_ex_id) = res + for new_bus in self.list_loc_bus: + if new_bus == 1: + # nothing to do if everything is moved to bus 1 + continue + act = self._aux_build_act(res, new_bus, "loads_id") + obs, reward, done, info = self.env.step(act) + assert not done + acts = self.env.action_space.get_back_to_ref_state(obs) + assert "substation" in acts + assert len(acts["substation"]) == 1 + act_to_ref = acts["substation"][0] + assert act_to_ref.load_set_bus[el_id] == 1 + if line_or_id is not None: + assert act_to_ref.line_or_set_bus[line_or_id] == 1 + if line_ex_id is not None: + assert act_to_ref.line_ex_set_bus[line_ex_id] == 1 def test_connectivity_matrix(self): - pass + cls = type(self.env) + obs = self.env.reset(**self.get_reset_kwargs()) + res = TestPandapowerBackend_3busbars._aux_find_sub(self.env, type(self.env).LOA_COL) + (sub_id, el_id, line_or_id, line_ex_id) = res + for new_bus in self.list_loc_bus: + act = self._aux_build_act(res, new_bus, "loads_id") + obs, reward, done, info = self.env.step(act) + assert not done + assert not info["exception"], "there should not have any exception (action should be legal)" + conn_mat = obs.connectivity_matrix() + assert conn_mat.shape == (cls.dim_topo, cls.dim_topo) + if new_bus == 1: + min_sub = np.sum(cls.sub_info[:sub_id]) + max_sub = min_sub + cls.sub_info[sub_id] + assert (conn_mat[min_sub:max_sub, min_sub:max_sub] == 1.).all() + else: + el_topov = cls.load_pos_topo_vect[el_id] + line_pos_topov = cls.line_or_pos_topo_vect[line_or_id] if line_or_id is not None else cls.line_ex_pos_topo_vect[line_ex_id] + line_pos_topo_other = cls.line_ex_pos_topo_vect[line_or_id] if line_or_id is not None else cls.line_or_pos_topo_vect[line_ex_id] + assert conn_mat[el_topov, line_pos_topov] == 1. + assert conn_mat[line_pos_topov, el_topov] == 1. + for el in range(cls.dim_topo): + if el == line_pos_topov: + continue + if el == el_topov: + continue + if el == line_pos_topo_other: + # other side of the line is connected to it + continue + assert conn_mat[el_topov, el] == 0., f"error for {new_bus}: ({el_topov}, {el}) appears to be connected: {conn_mat[el_topov, el]}" + assert conn_mat[el, el_topov] == 0., f"error for {new_bus}: ({el}, {el_topov}) appears to be connected: {conn_mat[el, el_topov]}" + assert conn_mat[line_pos_topov, el] == 0., f"error for {new_bus}: ({line_pos_topov}, {el}) appears to be connected: {conn_mat[line_pos_topov, el]}" + assert conn_mat[el, line_pos_topov] == 0., f"error for {new_bus}: ({el}, {line_pos_topov}) appears to be connected: {conn_mat[el, line_pos_topov]}" def test_bus_connectivity_matrix(self): - pass - + cls = type(self.env) + obs = self.env.reset(**self.get_reset_kwargs()) + res = TestPandapowerBackend_3busbars._aux_find_sub(self.env, type(self.env).LOA_COL) + (sub_id, el_id, line_or_id, line_ex_id) = res + for new_bus in self.list_loc_bus: + act = self._aux_build_act(res, new_bus, "loads_id") + obs, reward, done, info = self.env.step(act) + assert not done + assert not info["exception"], "there should not have any exception (action should be legal)" + conn_mat, (lor_ind, lex_ind) = obs.bus_connectivity_matrix(return_lines_index=True) + if new_bus == 1: + assert conn_mat.shape == (cls.n_sub, cls.n_sub) + else: + assert conn_mat.shape == (cls.n_sub + 1, cls.n_sub + 1) + new_bus_id = lor_ind[line_or_id] if line_or_id else lex_ind[line_ex_id] + bus_other = lex_ind[line_or_id] if line_or_id else lor_ind[line_ex_id] + assert conn_mat[new_bus_id, bus_other] == 1. + assert conn_mat[bus_other, new_bus_id] == 1. + assert conn_mat[new_bus_id, sub_id] == 0. + assert conn_mat[sub_id, new_bus_id] == 0. + def test_flow_bus_matrix(self): - pass + cls = type(self.env) + obs = self.env.reset(**self.get_reset_kwargs()) + res = TestPandapowerBackend_3busbars._aux_find_sub(self.env, type(self.env).LOA_COL) + (sub_id, el_id, line_or_id, line_ex_id) = res + for new_bus in self.list_loc_bus: + act = self._aux_build_act(res, new_bus, "loads_id") + obs, reward, done, info = self.env.step(act) + assert not done + assert not info["exception"], "there should not have any exception (action should be legal)" + conn_mat, (load_bus, prod_bus, stor_bus, lor_ind, lex_ind) = obs.flow_bus_matrix() + if new_bus == 1: + assert conn_mat.shape == (cls.n_sub, cls.n_sub) + else: + assert conn_mat.shape == (cls.n_sub + 1, cls.n_sub + 1) + new_bus_id = lor_ind[line_or_id] if line_or_id else lex_ind[line_ex_id] + bus_other = lex_ind[line_or_id] if line_or_id else lor_ind[line_ex_id] + assert conn_mat[new_bus_id, bus_other] != 0. # there are some flows from these 2 buses + assert conn_mat[bus_other, new_bus_id] != 0. # there are some flows from these 2 buses + assert conn_mat[new_bus_id, sub_id] == 0. + assert conn_mat[sub_id, new_bus_id] == 0. def test_get_energy_graph(self): - pass - + cls = type(self.env) + obs = self.env.reset(**self.get_reset_kwargs()) + res = TestPandapowerBackend_3busbars._aux_find_sub(self.env, type(self.env).LOA_COL) + (sub_id, el_id, line_or_id, line_ex_id) = res + for new_bus in self.list_loc_bus: + act = self._aux_build_act(res, new_bus, "loads_id") + obs, reward, done, info = self.env.step(act) + assert not done + assert not info["exception"], "there should not have any exception (action should be legal)" + graph = obs.get_energy_graph() + if new_bus == 1: + assert len(graph.nodes) == cls.n_sub + continue + # if I end up here it's because new_bus >= 2 + assert len(graph.nodes) == cls.n_sub + 1 + new_bus_id = cls.n_sub # this bus has been added + bus_other = cls.line_ex_to_subid[line_or_id] if line_or_id else cls.line_or_to_subid[line_ex_id] + assert (new_bus_id, bus_other) in graph.edges + edge = graph.edges[(new_bus_id, bus_other)] + node = graph.nodes[new_bus_id] + assert node["local_bus_id"] == new_bus + assert node["global_bus_id"] == sub_id + (new_bus - 1) * cls.n_sub + if line_or_id is not None: + assert edge["bus_or"] == new_bus + assert edge["global_bus_or"] == sub_id + (new_bus - 1) * cls.n_sub + else: + assert edge["bus_ex"] == new_bus + assert edge["global_bus_ex"] == sub_id + (new_bus - 1) * cls.n_sub + def test_get_elements_graph(self): - pass - - - + cls = type(self.env) + obs = self.env.reset(**self.get_reset_kwargs()) + res = TestPandapowerBackend_3busbars._aux_find_sub(self.env, type(self.env).LOA_COL) + (sub_id, el_id, line_or_id, line_ex_id) = res + for new_bus in self.list_loc_bus: + act = self._aux_build_act(res, new_bus, "loads_id") + obs, reward, done, info = self.env.step(act) + assert not done + assert not info["exception"], "there should not have any exception (action should be legal)" + graph = obs.get_elements_graph() + global_bus_id = sub_id + (new_bus - 1) * cls.n_sub + node_bus_id = graph.graph['bus_nodes_id'][global_bus_id] + node_load_id = graph.graph['load_nodes_id'][el_id] + node_line_id = graph.graph['line_nodes_id'][line_or_id] if line_or_id is not None else graph.graph['line_nodes_id'][line_ex_id] + node_load = graph.nodes[node_load_id] + node_line = graph.nodes[node_line_id] + assert len(graph.graph["bus_nodes_id"]) == cls.n_busbar_per_sub * cls.n_sub + + # check the bus + for node_id in graph.graph["bus_nodes_id"]: + assert "global_id" in graph.nodes[node_id], "key 'global_id' should be in the node" + if new_bus == 1: + for node_id in graph.graph["bus_nodes_id"][cls.n_sub:]: + assert not graph.nodes[node_id]["connected"], f"bus (global id {graph.nodes[node_id]['global_id']}) represented by node {node_id} should not be connected" + else: + for node_id in graph.graph["bus_nodes_id"][cls.n_sub:]: + if graph.nodes[node_id]['global_id'] != global_bus_id: + assert not graph.nodes[node_id]["connected"], f"bus (global id {graph.nodes[node_id]['global_id']}) represented by node {node_id} should not be connected" + else: + assert graph.nodes[node_id]["connected"], f"bus (global id {graph.nodes[node_id]['global_id']}) represented by node {node_id} should be connected" + + # check the load + edge_load_id = node_load["bus_node_id"] + assert node_load["local_bus"] == new_bus + assert node_load["global_bus"] == global_bus_id + assert (node_load_id, edge_load_id) in graph.edges + + # check lines + side = "or" if line_or_id is not None else "ex" + edge_line_id = node_line[f"bus_node_id_{side}"] + assert node_line[f"local_bus_{side}"] == new_bus + assert node_line[f"global_bus_{side}"] == global_bus_id + assert (node_line_id, edge_line_id) in graph.edges + + +class TestObservation_1busbar(TestObservation_3busbars): + def get_nb_bus(self): + return 1 + + class TestEnv(unittest.TestCase): pass From 2ab5c0ad8e21009c1a1f6908d97aa28d92283434 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 8 Feb 2024 15:40:04 +0100 Subject: [PATCH 18/24] improving docs about observation as graph [skip ci] --- CHANGELOG.rst | 1 + docs/grid_graph.rst | 94 ++++++++++++++++++++++++++++++++++----------- 2 files changed, 73 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c165ce823..4b36f2241 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -64,6 +64,7 @@ Change Log of the bus that represents each node of this graph. - [IMPROVED] `obs.get_elements_graph()` by giving access to the bus id (local, global and id of the node) where each element is connected. +- [IMPROVED] description of the different graph of the grid in the documentation. [1.9.8] - 2024-01-26 ---------------------- diff --git a/docs/grid_graph.rst b/docs/grid_graph.rst index 6e3b77fe8..ccfdbc615 100644 --- a/docs/grid_graph.rst +++ b/docs/grid_graph.rst @@ -326,6 +326,8 @@ To know what element of the grid is the "42nd", you can: is not of that type, and otherwise and id > 0. Taking the same example as for the above bullet point! `env.grid_objects_types[42,:] = [sub_id, -1, -1, -1, line_id, -1]` meaning the "42nd" element of the grid if the extremity end (because it's the 5th column) of id `line_id` (the other element being marked as "-1"). +3) refer to the :func:`grid2op.Space.GridObject.topo_vect_element` for an "easier" way to retrieve information + about this element. .. note:: As of a few versions of grid2op, if you are interested at the busbar to which (say) load 5 is connected, then Instead @@ -363,15 +365,15 @@ Type of graph described in grid2op method And their respective properties: -======================== ================ ======================== ===================== -Type of graph always same size encode all observation has flow information -======================== ================ ======================== ===================== -"energy graph" no almost yes -"elements graph" yes for nodes yes yes -"connectivity graph" yes no no -"bus connectivity graph" no no no -"flow bus graph" no no yes -======================== ================ ======================== ===================== +======================== =================== ==================== ======================= ===================== +Type of graph same number of node same number of edges encode all observation has flow information +======================== =================== ==================== ======================= ===================== +"energy graph" no no almost yes +"elements graph" yes no yes yes +"connectivity graph" yes no no no +"bus connectivity graph" no no no no +"flow bus graph" no no no yes +======================== =================== ==================== ======================= ===================== .. _graph1-gg: @@ -505,7 +507,7 @@ the two red powerlines, another where there are the two green) .. note:: On this example, for this visualization, lots of elements of the grid are not displayed. This is the case - for the load, generator and storage units for example. + for the loads, generators and storage units for example. For an easier to read representation, feel free to consult the :ref:`grid2op-plot-module` @@ -516,10 +518,10 @@ Graph2: the "elements graph" As opposed to the previous graph, this one has a fixed number of **nodes**: each nodes will represent an "element" of the powergrid. In this graph, there is -`n_sub` nodes each representing a substation and `2 * n_sub` nodes, each +`n_sub` nodes each representing a substation and `env.n_busbar_per_sub * n_sub` nodes, each representing a "busbar" and `n_load` nodes each representing a load etc. In total, there is then: -`n_sub + 2*n_sub + n_load + n_gen + n_line + n_storage + n_shunt` nodes. +`n_sub + env.n_busbar_per_sub*n_sub + n_load + n_gen + n_line + n_storage + n_shunt` nodes. Depending on its type, a node can have different properties. @@ -619,15 +621,16 @@ There are no outgoing edges from substation. Bus properties +++++++++++++++++++++++ -The next `2 * n_sub` nodes of the "elements graph" represent the "buses" of the grid. They have the attributes: +The next `env.n_busbar_per_sub * n_sub` nodes of the "elements graph" represent the "buses" of the grid. They have the attributes: -- `id`: which bus does this node represent (global id: `0 <= id < 2*env.n_sub`) +- `id`: which bus does this node represent (global id: `0 <= id < env.n_busbar_per_sub*env.n_sub`) - `global_id`: same as "id" -- `local_id`: which bus (in the substation) does this busbar represents (local id: `1 <= local_id <= 2`) +- `local_id`: which bus (in the substation) does this busbar represents (local id: `1 <= local_id <= env.n_busbar_per_sub`) - `type`: always "bus" - `connected`: whether or not this bus is "connected" to the grid. - `v`: the voltage magnitude of this bus (in kV, optional only when the bus is connected) -- `theta`: the voltage angle of this bus (in deg, optional only when the bus is connected) +- `theta`: the voltage angle of this bus (in deg, optional only when the bus is connected and + if the backend supports it) The outgoing edges from the nodes representing buses tells at which substation this bus is connected. These edges are "fixed": if they are present (meaning the bus is connected) they always connect the bus to the same substation. They have only @@ -803,9 +806,25 @@ there is only one outgoing edge). They have attributes: Graph3: the "connectivity graph" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -TODO: Work in progress, any help welcome +This graph is represented by a matrix (numpy 2d array or sicpy sparse matrix) of +floating point: `0.` means there are no connection between the elements and `1.`. -In the mean time, some documentation are available at :func:`grid2op.Observation.BaseObservation.connectivity_matrix` +Each row / column of the matrix represent an element modeled in the `topo_vect` vector. To know +more about the element represented by the row / column, you can have a look at the +:func:`grid2op.Space.GridObjects.topo_vect_element` function. + +In short, this graph gives the information of "this object" and "this other object" are connected +together: either they are the two side of the same powerline or they are connected to the same bus +in the grid. + +In other words the `node` of this graph are the element of the grid (side of line, load, gen and storage) +and the `edge` of this non oriented (undirected / symmetrical) non weighted graph represent the connectivity +of the grid. + +It has a fixed number of nodes (number of elements is fixed) but the number of edges can vary. + +You can consult the documentation of the :func:`grid2op.Observation.BaseObservation.connectivity_matrix` +for complement of information an some examples on how to retrieve this graph. .. note:: @@ -818,9 +837,24 @@ In the mean time, some documentation are available at :func:`grid2op.Observation Graph4: the "bus connectivity graph" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -TODO: Work in progress, any help welcome -In the mean time, some documentation are available at :func:`grid2op.Observation.BaseObservation.bus_connectivity_matrix` +This graph is represented by a matrix (numpy 2d array or sicpy sparse matrix) of +floating point: `0.` means there are no connection between the elements and `1.`. + +As opposed to the previous "graph" the row / column of this matrix has as many elements as the number of +independant buses on the grid. There are 0. if no powerlines connects the two buses +or one if at least a powerline connects these two buses. + +In other words the `nodes` of this graph are the buse of the grid +and the `edges` of this non oriented (undirected / symmetrical) non weighted graph represent the presence +of powerline connected two buses (basically if there are line with one of its side connecting one of the bus +and the other side connecting the other). + +It has a variable number of nodes and edges. In case of game over we chose to represent this graph as +an graph with 1 node and 0 edge. + +You can consult the documentation of the :func:`grid2op.Observation.BaseObservation.bus_connectivity_matrix` +for complement of information an some examples on how to retrieve this graph. .. note:: @@ -834,9 +868,25 @@ In the mean time, some documentation are available at :func:`grid2op.Observation Graph5: the "flow bus graph" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -TODO: Work in progress, any help welcome +This graph is also represented by a matrix (numpy 2d array or scipy sparse matrix) of float. It is quite similar +to the graph described in :ref:`graph4-gg`. The main difference is that instead of simply giving +information about connectivity (0. or 1.) this one gives information about flows +(either active flows or reactive flows). + +It is a directed graph (matrix is not symmetric) and it has weights. The weight associated to each node +(representing a bus) is the power (in MW for active or MVAr for reactive) injected at this bus +(generator convention: if the power is positive the power is injected at this graph). The weight associated +at each edge going from `i` to `j` is the sum of the active (or reactive) power of all +the lines connecting bus `i` to bus `j`. + +It has a variable number of nodes and edges. In case of game over we chose to represent this graph as +an graph with 1 node and 0 edge. + +You can consult the documentation of the :func:`grid2op.Observation.BaseObservation.flow_bus_matrix` +for complement of information an some examples on how to retrieve this graph. + +It is a simplified version of the :ref:`graph1-gg` described previously. -In the mean time, some documentation are available at :func:`grid2op.Observation.BaseObservation.flow_bus_matrix` .. note:: From ed79fdc877eeea1ada165c794039903f4453c71f Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 8 Feb 2024 15:50:50 +0100 Subject: [PATCH 19/24] start support for gym_compat, need test for gym_compat and regular env --- grid2op/gym_compat/box_gym_actspace.py | 4 ++-- grid2op/gym_compat/box_gym_obsspace.py | 4 ++-- grid2op/gym_compat/gym_act_space.py | 4 +++- grid2op/gym_compat/gym_obs_space.py | 4 +++- grid2op/gym_compat/gymenv.py | 2 +- grid2op/gym_compat/multidiscrete_gym_actspace.py | 6 +++--- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/grid2op/gym_compat/box_gym_actspace.py b/grid2op/gym_compat/box_gym_actspace.py index aed07d132..8f6d4cc01 100644 --- a/grid2op/gym_compat/box_gym_actspace.py +++ b/grid2op/gym_compat/box_gym_actspace.py @@ -225,7 +225,7 @@ def __init__( self._attr_to_keep = sorted(attr_to_keep) - act_sp = grid2op_action_space + act_sp = type(grid2op_action_space) self._act_space = copy.deepcopy(grid2op_action_space) low_gen = -1.0 * act_sp.gen_max_ramp_down[act_sp.gen_redispatchable] @@ -249,7 +249,7 @@ def __init__( ), "set_bus": ( np.full(shape=(act_sp.dim_topo,), fill_value=-1, dtype=dt_int), - np.full(shape=(act_sp.dim_topo,), fill_value=1, dtype=dt_int), + np.full(shape=(act_sp.dim_topo,), fill_value=act_sp.n_busbar_per_sub, dtype=dt_int), (act_sp.dim_topo,), dt_int, ), diff --git a/grid2op/gym_compat/box_gym_obsspace.py b/grid2op/gym_compat/box_gym_obsspace.py index 0277a1517..568ebb0b6 100644 --- a/grid2op/gym_compat/box_gym_obsspace.py +++ b/grid2op/gym_compat/box_gym_obsspace.py @@ -213,7 +213,7 @@ def __init__( ) self._attr_to_keep = sorted(attr_to_keep) - ob_sp = grid2op_observation_space + ob_sp = type(grid2op_observation_space) tol_redisp = ( ob_sp.obs_env._tol_poly ) # add to gen_p otherwise ... well it can crash @@ -408,7 +408,7 @@ def __init__( ), "topo_vect": ( np.full(shape=(ob_sp.dim_topo,), fill_value=-1, dtype=dt_int), - np.full(shape=(ob_sp.dim_topo,), fill_value=2, dtype=dt_int), + np.full(shape=(ob_sp.dim_topo,), fill_value=ob_sp.n_busbar_per_sub, dtype=dt_int), (ob_sp.dim_topo,), dt_int, ), diff --git a/grid2op/gym_compat/gym_act_space.py b/grid2op/gym_compat/gym_act_space.py index 8bc428e2e..5cf2da3e5 100644 --- a/grid2op/gym_compat/gym_act_space.py +++ b/grid2op/gym_compat/gym_act_space.py @@ -248,7 +248,9 @@ def _fill_dict_act_space(self, dict_, action_space, dict_variables): if attr_nm == "_set_line_status": my_type = type(self)._BoxType(low=-1, high=1, shape=shape, dtype=dt) elif attr_nm == "_set_topo_vect": - my_type = type(self)._BoxType(low=-1, high=2, shape=shape, dtype=dt) + my_type = type(self)._BoxType(low=-1, + high=type(action_space).n_busbar_per_sub, + shape=shape, dtype=dt) elif dt == dt_bool: # boolean observation space my_type = self._boolean_type(sh) diff --git a/grid2op/gym_compat/gym_obs_space.py b/grid2op/gym_compat/gym_obs_space.py index d427f4230..f74b3e43a 100644 --- a/grid2op/gym_compat/gym_obs_space.py +++ b/grid2op/gym_compat/gym_obs_space.py @@ -252,7 +252,9 @@ def _fill_dict_obs_space( elif attr_nm == "day_of_week": my_type = type(self)._DiscreteType(n=8) elif attr_nm == "topo_vect": - my_type = type(self)._BoxType(low=-1, high=2, shape=shape, dtype=dt) + my_type = type(self)._BoxType(low=-1, + high=observation_space.n_busbar_per_sub, + shape=shape, dtype=dt) elif attr_nm == "time_before_cooldown_line": my_type = type(self)._BoxType( low=0, diff --git a/grid2op/gym_compat/gymenv.py b/grid2op/gym_compat/gymenv.py index 7531e52e8..9f4252f52 100644 --- a/grid2op/gym_compat/gymenv.py +++ b/grid2op/gym_compat/gymenv.py @@ -45,7 +45,7 @@ class behave differently depending on the version of gym you have installed ! - :class:`GymEnv_Modern` for gym >= 0.26 .. warning:: - Depending on the presence absence of gymnasium and gym packages this class might behave differently. + Depending on the presence absence of `gymnasium` and `gym` packages this class might behave differently. In grid2op we tried to maintain compatibility both with gymnasium (newest) and gym (legacy, no more maintained) RL packages. The behaviour is the following: diff --git a/grid2op/gym_compat/multidiscrete_gym_actspace.py b/grid2op/gym_compat/multidiscrete_gym_actspace.py index a92620389..d6503cba5 100644 --- a/grid2op/gym_compat/multidiscrete_gym_actspace.py +++ b/grid2op/gym_compat/multidiscrete_gym_actspace.py @@ -39,7 +39,7 @@ class __AuxMultiDiscreteActSpace: - "change_line_status": `n_line` dimensions, each containing 2 elements "CHANGE", "DONT CHANGE" and affecting the powerline status (connected / disconnected) - "set_bus": `dim_topo` dimensions, each containing 4 choices: "DISCONNECT", "DONT AFFECT", "CONNECT TO BUSBAR 1", - or "CONNECT TO BUSBAR 2" and affecting to which busbar an object is connected + or "CONNECT TO BUSBAR 2", "CONNECT TO BUSBAR 3", ... and affecting to which busbar an object is connected - "change_bus": `dim_topo` dimensions, each containing 2 choices: "CHANGE", "DONT CHANGE" and affect to which busbar an element is connected - "redispatch": `sum(env.gen_redispatchable)` dimensions, each containing a certain number of choices depending on the value @@ -201,7 +201,7 @@ def __init__(self, grid2op_action_space, attr_to_keep=ALL_ATTR, nb_bins=None): self._attr_to_keep = sorted(attr_to_keep) - act_sp = grid2op_action_space + act_sp = type(grid2op_action_space) self._act_space = copy.deepcopy(grid2op_action_space) low_gen = -1.0 * act_sp.gen_max_ramp_down @@ -222,7 +222,7 @@ def __init__(self, grid2op_action_space, attr_to_keep=ALL_ATTR, nb_bins=None): self.ATTR_CHANGE, ), "set_bus": ( - [4 for _ in range(act_sp.dim_topo)], + [2 + act_sp.n_busbar_per_sub for _ in range(act_sp.dim_topo)], act_sp.dim_topo, self.ATTR_SET, ), From 9f658cc5e2ba13c4c107af742cd9b070abe89c94 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 8 Feb 2024 16:21:53 +0100 Subject: [PATCH 20/24] fix some doc and some issues spotted by sonar cube --- grid2op/Action/_backendAction.py | 107 +++++++++++++------------ grid2op/Action/actionSpace.py | 9 +-- grid2op/Action/baseAction.py | 133 +++++++++++++++++-------------- grid2op/Backend/backend.py | 22 ++++- 4 files changed, 152 insertions(+), 119 deletions(-) diff --git a/grid2op/Action/_backendAction.py b/grid2op/Action/_backendAction.py index 15fb93f3b..bf99c05af 100644 --- a/grid2op/Action/_backendAction.py +++ b/grid2op/Action/_backendAction.py @@ -377,6 +377,58 @@ def all_changed(self) -> None: def set_redispatch(self, new_redispatching): self.prod_p.change_val(new_redispatching) + def _aux_iadd_inj(self, dict_injection): + if "load_p" in dict_injection: + tmp = dict_injection["load_p"] + self.load_p.set_val(tmp) + if "load_q" in dict_injection: + tmp = dict_injection["load_q"] + self.load_q.set_val(tmp) + if "prod_p" in dict_injection: + tmp = dict_injection["prod_p"] + self.prod_p.set_val(tmp) + if "prod_v" in dict_injection: + tmp = dict_injection["prod_v"] + self.prod_v.set_val(tmp) + + def _aux_iadd_shunt(self, other): + shunts = {} + if type(other).shunts_data_available: + shunts["shunt_p"] = other.shunt_p + shunts["shunt_q"] = other.shunt_q + shunts["shunt_bus"] = other.shunt_bus + + arr_ = shunts["shunt_p"] + self.shunt_p.set_val(arr_) + arr_ = shunts["shunt_q"] + self.shunt_q.set_val(arr_) + arr_ = shunts["shunt_bus"] + self.shunt_bus.set_val(arr_) + self.current_shunt_bus.values[self.shunt_bus.changed] = self.shunt_bus.values[self.shunt_bus.changed] + + def _aux_iadd_reconcile_disco_reco(self): + disco_or = (self._status_or_before == -1) | (self._status_or == -1) + disco_ex = (self._status_ex_before == -1) | (self._status_ex == -1) + disco_now = ( + disco_or | disco_ex + ) # a powerline is disconnected if at least one of its extremity is + # added + reco_or = (self._status_or_before == -1) & (self._status_or >= 1) + reco_ex = (self._status_or_before == -1) & (self._status_ex >= 1) + reco_now = reco_or | reco_ex + # Set nothing + set_now = np.zeros_like(self._status_or) + # Force some disconnections + set_now[disco_now] = -1 + set_now[reco_now] = 1 + + self.current_topo.set_status( + set_now, + self.line_or_pos_topo_vect, + self.line_ex_pos_topo_vect, + self.last_topo_registered, + ) + def __iadd__(self, other : BaseAction) -> Self: """ .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ @@ -392,7 +444,6 @@ def __iadd__(self, other : BaseAction) -> Self: """ - dict_injection = other._dict_inj set_status = other._set_line_status switch_status = other._switch_line_status set_topo_vect = other._set_topo_vect @@ -403,19 +454,8 @@ def __iadd__(self, other : BaseAction) -> Self: # I deal with injections # Ia set the injection if other._modif_inj: - if "load_p" in dict_injection: - tmp = dict_injection["load_p"] - self.load_p.set_val(tmp) - if "load_q" in dict_injection: - tmp = dict_injection["load_q"] - self.load_q.set_val(tmp) - if "prod_p" in dict_injection: - tmp = dict_injection["prod_p"] - self.prod_p.set_val(tmp) - if "prod_v" in dict_injection: - tmp = dict_injection["prod_v"] - self.prod_v.set_val(tmp) - + self._aux_iadd_inj(other._dict_inj) + # Ib change the injection aka redispatching if other._modif_redispatch: self.prod_p.change_val(redispatching) @@ -426,20 +466,8 @@ def __iadd__(self, other : BaseAction) -> Self: # II shunts if type(self).shunts_data_available: - shunts = {} - if type(other).shunts_data_available: - shunts["shunt_p"] = other.shunt_p - shunts["shunt_q"] = other.shunt_q - shunts["shunt_bus"] = other.shunt_bus - - arr_ = shunts["shunt_p"] - self.shunt_p.set_val(arr_) - arr_ = shunts["shunt_q"] - self.shunt_q.set_val(arr_) - arr_ = shunts["shunt_bus"] - self.shunt_bus.set_val(arr_) - self.current_shunt_bus.values[self.shunt_bus.changed] = self.shunt_bus.values[self.shunt_bus.changed] - + self._aux_iadd_shunt(other) + # 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. @@ -480,28 +508,7 @@ def __iadd__(self, other : BaseAction) -> Self: # At least one disconnected extremity if other._modif_change_bus or other._modif_set_bus: - disco_or = (self._status_or_before == -1) | (self._status_or == -1) - disco_ex = (self._status_ex_before == -1) | (self._status_ex == -1) - disco_now = ( - disco_or | disco_ex - ) # a powerline is disconnected if at least one of its extremity is - # added - reco_or = (self._status_or_before == -1) & (self._status_or >= 1) - reco_ex = (self._status_or_before == -1) & (self._status_ex >= 1) - reco_now = reco_or | reco_ex - # Set nothing - set_now = np.zeros_like(self._status_or) - # Force some disconnections - set_now[disco_now] = -1 - set_now[reco_now] = 1 - - self.current_topo.set_status( - set_now, - self.line_or_pos_topo_vect, - self.line_ex_pos_topo_vect, - self.last_topo_registered, - ) - + self._aux_iadd_reconcile_disco_reco() return self def _assign_0_to_disco_el(self) -> None: diff --git a/grid2op/Action/actionSpace.py b/grid2op/Action/actionSpace.py index 137c9e93a..b8f870062 100644 --- a/grid2op/Action/actionSpace.py +++ b/grid2op/Action/actionSpace.py @@ -8,12 +8,7 @@ import warnings import copy -from typing import Dict, List, Any -try: - from typing import Literal -except ImportError: - from typing_extensions import Literal - +from typing import Dict, List, Any, Literal from grid2op.Action.baseAction import BaseAction from grid2op.Action.serializableActionSpace import SerializableActionSpace @@ -94,7 +89,7 @@ def __call__( check_legal: bool = False, env: "grid2op.Environment.BaseEnv" = None, *, - injection=None, # TODO n_busbar_per_sub + injection=None, ) -> BaseAction: """ This utility allows you to build a valid action, with the proper sizes if you provide it with a valid diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 4fca6bb79..9ed4966d7 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -10,6 +10,11 @@ import numpy as np import warnings from typing import Tuple, Dict, Literal, Any +try: + from typing import Self +except ImportError: + from typing_extensions import Self + from packaging import version from grid2op.dtypes import dt_int, dt_bool, dt_float @@ -74,9 +79,11 @@ class BaseAction(GridObjects): interpretation: - 0 -> don't change + - -1 -> disconnect the object. - 1 -> connect to bus 1 - 2 -> connect to bus 2 - - -1 -> disconnect the object. + - 3 -> connect to bus 3 (added in version 1.9.9) + - etc. (added in version 1.9.9) - the fifth element changes the buses to which the object is connected. It's a boolean vector interpreted as: @@ -757,7 +764,7 @@ def alarm_raised(self) -> np.ndarray: The indexes of the areas where the agent has raised an alarm. """ - return np.where(self._raise_alarm)[0] + return np.nonzero(self._raise_alarm)[0] def alert_raised(self) -> np.ndarray: """ @@ -771,8 +778,38 @@ def alert_raised(self) -> np.ndarray: The indexes of the lines where the agent has raised an alert. """ - return np.where(self._raise_alert)[0] + return np.nonzero(self._raise_alert)[0] + + @classmethod + def _aux_process_old_compat(cls): + # this is really important, otherwise things from grid2op base types will be affected + cls.authorized_keys = copy.deepcopy(cls.authorized_keys) + cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) + + # deactivate storage + cls.set_no_storage() + if "set_storage" in cls.authorized_keys: + cls.authorized_keys.remove("set_storage") + if "_storage_power" in cls.attr_list_vect: + cls.attr_list_vect.remove("_storage_power") + cls.attr_list_set = set(cls.attr_list_vect) + # remove the curtailment + if "curtail" in cls.authorized_keys: + cls.authorized_keys.remove("curtail") + if "_curtail" in cls.attr_list_vect: + cls.attr_list_vect.remove("_curtail") + + @classmethod + def _aux_process_n_busbar_per_sub(cls): + cls.authorized_keys = copy.deepcopy(cls.authorized_keys) + cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) + + if "change_bus" in cls.authorized_keys: + cls.authorized_keys.remove("change_bus") + if "_change_bus_vect" in cls.attr_list_vect: + cls.attr_list_vect.remove("_change_bus_vect") + @classmethod def process_grid2op_compat(cls): super().process_grid2op_compat() @@ -780,25 +817,8 @@ def process_grid2op_compat(cls): if cls.glop_version == cls.BEFORE_COMPAT_VERSION: # oldest version: no storage and no curtailment available - - # this is really important, otherwise things from grid2op base types will be affected - cls.authorized_keys = copy.deepcopy(cls.authorized_keys) - cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) - - # deactivate storage - cls.set_no_storage() - if "set_storage" in cls.authorized_keys: - cls.authorized_keys.remove("set_storage") - if "_storage_power" in cls.attr_list_vect: - cls.attr_list_vect.remove("_storage_power") - cls.attr_list_set = set(cls.attr_list_vect) - - # remove the curtailment - if "curtail" in cls.authorized_keys: - cls.authorized_keys.remove("curtail") - if "_curtail" in cls.attr_list_vect: - cls.attr_list_vect.remove("_curtail") - + cls._aux_process_old_compat + if glop_ver < version.parse("1.6.0"): # this feature did not exist before. cls.dim_alarms = 0 @@ -813,13 +833,7 @@ def process_grid2op_compat(cls): # or if there are only one busbar (cannot change anything) # 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.authorized_keys = copy.deepcopy(cls.authorized_keys) - cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) - - if "change_bus" in cls.authorized_keys: - cls.authorized_keys.remove("change_bus") - if "_change_bus_vect" in cls.attr_list_vect: - cls.attr_list_vect.remove("_change_bus_vect") + 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) @@ -1506,8 +1520,8 @@ def _assign_iadd_or_warn(self, attr_name, new_value): ) else: getattr(self, attr_name)[:] = new_value - - def __iadd__(self, other): + + def __iadd__(self, other: Self): """ Add an action to this one. @@ -1551,6 +1565,7 @@ def __iadd__(self, other): val = other._dict_inj[el] ok_ind = np.isfinite(val) self._dict_inj[el][ok_ind] = val[ok_ind] + # warning if the action cannot be added for el in other._dict_inj: if not el in self.attr_list_set: @@ -2560,7 +2575,7 @@ def _check_for_ambiguity(self): "environment. Please set up the proper costs for generator" ) - if (self._redispatch[~cls.gen_redispatchable] != 0.0).any(): + if (np.abs(self._redispatch[~cls.gen_redispatchable]) >= 1e-7).any(): raise InvalidRedispatching( "Trying to apply a redispatching action on a non redispatchable generator" ) @@ -2655,10 +2670,10 @@ def _check_for_ambiguity(self): # if i disconnected of a line, but i modify also the bus where it's connected if self._modif_set_bus or self._modif_change_bus: idx = self._set_line_status == -1 - id_disc = np.where(idx)[0] + id_disc = np.nonzero(idx)[0] idx2 = self._set_line_status == 1 - id_reco = np.where(idx2)[0] + id_reco = np.nonzero(idx2)[0] if self._modif_set_bus: if "set_bus" not in cls.authorized_keys: @@ -2793,13 +2808,13 @@ def _is_storage_ambiguous(self): "units affected" ) if (self._storage_power < -cls.storage_max_p_prod).any(): - where_bug = np.where(self._storage_power < -cls.storage_max_p_prod)[0] + where_bug = np.nonzero(self._storage_power < -cls.storage_max_p_prod)[0] raise InvalidStorage( f"you asked a storage unit to absorb more than what it can: " f"self._storage_power[{where_bug}] < -self.storage_max_p_prod[{where_bug}]." ) if (self._storage_power > cls.storage_max_p_absorb).any(): - where_bug = np.where(self._storage_power > cls.storage_max_p_absorb)[0] + where_bug = np.nonzero(self._storage_power > cls.storage_max_p_absorb)[0] raise InvalidStorage( f"you asked a storage unit to produce more than what it can: " f"self._storage_power[{where_bug}] > self.storage_max_p_absorb[{where_bug}]." @@ -2834,14 +2849,14 @@ def _is_curtailment_ambiguous(self): ) if ((self._curtail < 0.0) & (self._curtail != -1.0)).any(): - where_bug = np.where((self._curtail < 0.0) & (self._curtail != -1.0))[0] + where_bug = np.nonzero((self._curtail < 0.0) & (self._curtail != -1.0))[0] raise InvalidCurtailment( f"you asked to perform a negative curtailment: " f"self._curtail[{where_bug}] < 0. " f"Curtailment should be a real number between 0.0 and 1.0" ) if (self._curtail > 1.0).any(): - where_bug = np.where(self._curtail > 1.0)[0] + where_bug = np.nonzero(self._curtail > 1.0)[0] raise InvalidCurtailment( f"you asked a storage unit to produce more than what it can: " f"self._curtail[{where_bug}] > 1. " @@ -3060,7 +3075,7 @@ def __str__(self) -> str: if my_cls.dim_alarms > 0: if self._modif_alarm: li_area = np.array(my_cls.alarms_area_names)[ - np.where(self._raise_alarm)[0] + np.nonzero(self._raise_alarm)[0] ] if len(li_area) == 1: area_str = ": " + li_area[0] @@ -3072,7 +3087,7 @@ def __str__(self) -> str: if my_cls.dim_alerts > 0: if self._modif_alert: - i_alert = np.where(self._raise_alert)[0] + i_alert = np.nonzero(self._raise_alert)[0] li_line = np.array(my_cls.alertable_line_names)[i_alert] if len(li_line) == 1: line_str = f": {i_alert[0]} (on line {li_line[0]})" @@ -3118,7 +3133,7 @@ def impact_on_objects(self) -> dict: force_line_status["reconnections"]["count"] = ( self._set_line_status == 1 ).sum() - force_line_status["reconnections"]["powerlines"] = np.where( + force_line_status["reconnections"]["powerlines"] = np.nonzero( self._set_line_status == 1 )[0] @@ -3128,7 +3143,7 @@ def impact_on_objects(self) -> dict: force_line_status["disconnections"]["count"] = ( self._set_line_status == -1 ).sum() - force_line_status["disconnections"]["powerlines"] = np.where( + force_line_status["disconnections"]["powerlines"] = np.nonzero( self._set_line_status == -1 )[0] @@ -3138,7 +3153,7 @@ def impact_on_objects(self) -> dict: switch_line_status["changed"] = True has_impact = True switch_line_status["count"] = self._switch_line_status.sum() - switch_line_status["powerlines"] = np.where(self._switch_line_status)[0] + switch_line_status["powerlines"] = np.nonzero(self._switch_line_status)[0] topology = { "changed": False, @@ -3333,10 +3348,10 @@ def as_dict(self) -> Dict[Literal["load_p", "load_q", "prod_p", "prod_v", res["set_line_status"]["nb_disconnected"] = ( self._set_line_status == -1 ).sum() - res["set_line_status"]["connected_id"] = np.where( + res["set_line_status"]["connected_id"] = np.nonzero( self._set_line_status == 1 )[0] - res["set_line_status"]["disconnected_id"] = np.where( + res["set_line_status"]["disconnected_id"] = np.whnonzeroere( self._set_line_status == -1 )[0] @@ -3344,7 +3359,7 @@ def as_dict(self) -> Dict[Literal["load_p", "load_q", "prod_p", "prod_v", if self._switch_line_status.sum(): res["change_line_status"] = {} res["change_line_status"]["nb_changed"] = self._switch_line_status.sum() - res["change_line_status"]["changed_id"] = np.where( + res["change_line_status"]["changed_id"] = np.nonzero( self._switch_line_status )[0] @@ -3394,11 +3409,11 @@ def as_dict(self) -> Dict[Literal["load_p", "load_q", "prod_p", "prod_v", res["set_bus_vect"]["modif_subs_id"] = sorted(all_subs) if self._hazards.any(): - res["hazards"] = np.where(self._hazards)[0] + res["hazards"] = np.nonzero(self._hazards)[0] res["nb_hazards"] = self._hazards.sum() if self._maintenance.any(): - res["maintenance"] = np.where(self._maintenance)[0] + res["maintenance"] = np.nonzero(self._maintenance)[0] res["nb_maintenance"] = self._maintenance.sum() if (self._redispatch != 0.0).any(): @@ -3950,7 +3965,7 @@ def _aux_affect_object_int( ) el_id, new_bus = el if isinstance(el_id, str) and name_els is not None: - tmp = np.where(name_els == el_id)[0] + tmp = np.nonzero(name_els == el_id)[0] if len(tmp) == 0: raise IllegalAction(f"No known {name_el} with name {el_id}") el_id = tmp[0] @@ -3968,7 +3983,7 @@ def _aux_affect_object_int( # 2 cases: either key = load_id and value = new_bus or key = load_name and value = new bus for key, new_bus in values.items(): if isinstance(key, str) and name_els is not None: - tmp = np.where(name_els == key)[0] + tmp = np.nonzero(name_els == key)[0] if len(tmp) == 0: raise IllegalAction(f"No known {name_el} with name {key}") key = tmp[0] @@ -4657,7 +4672,7 @@ def _aux_affect_object_bool( # (note: i cannot convert to numpy array other I could mix types...) for el_id_or_name in values: if isinstance(el_id_or_name, str): - tmp = np.where(name_els == el_id_or_name)[0] + tmp = np.nonzero(name_els == el_id_or_name)[0] if len(tmp) == 0: raise IllegalAction( f'No known {name_el} with name "{el_id_or_name}"' @@ -5348,7 +5363,7 @@ def _aux_affect_object_float( ) el_id, new_val = el if isinstance(el_id, str): - tmp = np.where(name_els == el_id)[0] + tmp = np.nonzero(name_els == el_id)[0] if len(tmp) == 0: raise IllegalAction(f"No known {name_el} with name {el_id}") el_id = tmp[0] @@ -5364,7 +5379,7 @@ def _aux_affect_object_float( # 2 cases: either key = load_id and value = new_bus or key = load_name and value = new bus for key, new_val in values.items(): if isinstance(key, str): - tmp = np.where(name_els == key)[0] + tmp = np.nonzero(name_els == key)[0] if len(tmp) == 0: raise IllegalAction(f"No known {name_el} with name {key}") key = tmp[0] @@ -5861,7 +5876,7 @@ def _aux_change_bus_sub(self, values): def _aux_sub_when_dict_get_id(self, sub_id): if isinstance(sub_id, str): - tmp = np.where(self.name_sub == sub_id)[0] + tmp = np.nonzero(self.name_sub == sub_id)[0] if len(tmp) == 0: raise IllegalAction(f"No substation named {sub_id}") sub_id = tmp[0] @@ -6173,7 +6188,7 @@ def _aux_decompose_as_unary_actions_change_ls(self, cls, group_line_status, res) tmp._switch_line_status = copy.deepcopy(self._switch_line_status) res["change_line_status"] = [tmp] else: - lines_changed = np.where(self._switch_line_status)[0] + lines_changed = np.nonzero(self._switch_line_status)[0] res["change_line_status"] = [] for l_id in lines_changed: tmp = cls() @@ -6205,7 +6220,7 @@ def _aux_decompose_as_unary_actions_set_ls(self, cls, group_line_status, res): tmp._set_line_status = 1 * self._set_line_status res["set_line_status"] = [tmp] else: - lines_changed = np.where(self._set_line_status != 0)[0] + lines_changed = np.nonzero(self._set_line_status != 0)[0] res["set_line_status"] = [] for l_id in lines_changed: tmp = cls() @@ -6220,7 +6235,7 @@ def _aux_decompose_as_unary_actions_redisp(self, cls, group_redispatch, res): tmp._redispatch = 1. * self._redispatch res["redispatch"] = [tmp] else: - gen_changed = np.where(self._redispatch != 0.)[0] + gen_changed = np.whernonzeroe(np.abs(self._redispatch) >= 1e-7)[0] res["redispatch"] = [] for g_id in gen_changed: tmp = cls() @@ -6235,7 +6250,7 @@ def _aux_decompose_as_unary_actions_storage(self, cls, group_storage, res): tmp._storage_power = 1. * self._storage_power res["set_storage"] = [tmp] else: - sto_changed = np.where(self._storage_power != 0.)[0] + sto_changed = np.nonzero(np.abs(self._storage_power) >= 1e-7)[0] res["set_storage"] = [] for s_id in sto_changed: tmp = cls() @@ -6250,7 +6265,7 @@ def _aux_decompose_as_unary_actions_curtail(self, cls, group_curtailment, res): tmp._curtail = 1. * self._curtail res["curtail"] = [tmp] else: - gen_changed = np.where(self._curtail != -1.)[0] + gen_changed = np.nonzero(np.abs(self._curtail + 1.) >= 1e-7)[0] #self._curtail != -1 res["curtail"] = [] for g_id in gen_changed: tmp = cls() diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index 21c3380d9..89ec9e060 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -195,11 +195,19 @@ def can_handle_more_than_2_busbar(self): .. seealso:: :func:`Backend.cannot_handle_more_than_2_busbar` + + .. note:: + From grid2op 1.9.9 it is preferable that your backend calls one of + :func:`Backend.can_handle_more_than_2_busbar` or + :func:`Backend.cannot_handle_more_than_2_busbar`. + + If not, then the environments created with your backend will not be able to + "operate" grid with more than 2 busbars per substation. .. danger:: We highly recommend you do not try to override this function. - At time of writing I can't find any good reason to do so. + At least, at time of writing I can't find any good reason to do so. """ self._missing_two_busbars_support_info = False self.n_busbar_per_sub = type(self).n_busbar_per_sub @@ -208,7 +216,7 @@ def cannot_handle_more_than_2_busbar(self): """ .. versionadded:: 1.9.9 - This function should be called once in `load_grid` if your backend is able + This function should be called once in `load_grid` if your backend is **NOT** able to handle more than 2 busbars per substation. If not called, then the `environment` will not be able to use more than 2 busbars per substations. @@ -216,10 +224,18 @@ def cannot_handle_more_than_2_busbar(self): .. seealso:: :func:`Backend.cnot_handle_more_than_2_busbar` + .. note:: + From grid2op 1.9.9 it is preferable that your backend calls one of + :func:`Backend.can_handle_more_than_2_busbar` or + :func:`Backend.cannot_handle_more_than_2_busbar`. + + If not, then the environments created with your backend will not be able to + "operate" grid with more than 2 busbars per substation. + .. danger:: We highly recommend you do not try to override this function. - At time of writing I can't find any good reason to do so. + Atleast, at time of writing I can't find any good reason to do so. """ self._missing_two_busbars_support_info = False if type(self).n_busbar_per_sub != DEFAULT_N_BUSBAR_PER_SUB: From bc74ae6ffca56b35efd2217d66fde7a7cb62c675 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 8 Feb 2024 16:55:28 +0100 Subject: [PATCH 21/24] calling np.nonzero instead of np.where, fix a bug in gym_compat --- grid2op/Action/_backendAction.py | 12 +- grid2op/Action/baseAction.py | 2 +- grid2op/Action/serializableActionSpace.py | 10 +- grid2op/Agent/recoPowerLinePerArea.py | 2 +- grid2op/Agent/recoPowerlineAgent.py | 2 +- grid2op/Backend/pandaPowerBackend.py | 14 +- grid2op/Chronics/GSFFWFWM.py | 2 +- grid2op/Chronics/gridValue.py | 12 +- grid2op/Chronics/multiFolder.py | 2 +- grid2op/Converter/BackendConverter.py | 8 +- grid2op/Converter/ConnectivityConverter.py | 14 +- grid2op/Environment/baseEnv.py | 18 +- grid2op/Environment/environment.py | 2 +- grid2op/Observation/baseObservation.py | 4 +- grid2op/Opponent/geometricOpponent.py | 2 +- grid2op/Opponent/randomLineOpponent.py | 2 +- grid2op/Opponent/weightedRandomOpponent.py | 2 +- grid2op/Reward/alarmReward.py | 4 +- grid2op/Reward/alertReward.py | 2 +- grid2op/Rules/LookParam.py | 4 +- grid2op/Rules/PreventDiscoStorageModif.py | 2 +- grid2op/Rules/PreventReconnection.py | 4 +- grid2op/Rules/rulesByArea.py | 6 +- grid2op/Space/GridObjects.py | 34 +-- grid2op/gym_compat/box_gym_actspace.py | 54 ++-- grid2op/gym_compat/box_gym_obsspace.py | 279 ++++++++++---------- grid2op/tests/BaseBackendTest.py | 48 ++-- grid2op/tests/aaa_test_backend_interface.py | 4 +- grid2op/tests/test_Action.py | 4 +- 29 files changed, 278 insertions(+), 277 deletions(-) diff --git a/grid2op/Action/_backendAction.py b/grid2op/Action/_backendAction.py index bf99c05af..fbc05f52c 100644 --- a/grid2op/Action/_backendAction.py +++ b/grid2op/Action/_backendAction.py @@ -398,12 +398,12 @@ def _aux_iadd_shunt(self, other): shunts["shunt_q"] = other.shunt_q shunts["shunt_bus"] = other.shunt_bus - arr_ = shunts["shunt_p"] - self.shunt_p.set_val(arr_) - arr_ = shunts["shunt_q"] - self.shunt_q.set_val(arr_) - arr_ = shunts["shunt_bus"] - self.shunt_bus.set_val(arr_) + arr_ = shunts["shunt_p"] + self.shunt_p.set_val(arr_) + arr_ = shunts["shunt_q"] + self.shunt_q.set_val(arr_) + arr_ = shunts["shunt_bus"] + self.shunt_bus.set_val(arr_) self.current_shunt_bus.values[self.shunt_bus.changed] = self.shunt_bus.values[self.shunt_bus.changed] def _aux_iadd_reconcile_disco_reco(self): diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 9ed4966d7..dc8ddf47e 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -3351,7 +3351,7 @@ def as_dict(self) -> Dict[Literal["load_p", "load_q", "prod_p", "prod_v", res["set_line_status"]["connected_id"] = np.nonzero( self._set_line_status == 1 )[0] - res["set_line_status"]["disconnected_id"] = np.whnonzeroere( + res["set_line_status"]["disconnected_id"] = np.nonzero( self._set_line_status == -1 )[0] diff --git a/grid2op/Action/serializableActionSpace.py b/grid2op/Action/serializableActionSpace.py index a050bf112..aefd9a847 100644 --- a/grid2op/Action/serializableActionSpace.py +++ b/grid2op/Action/serializableActionSpace.py @@ -417,7 +417,7 @@ def disconnect_powerline(self, ) if line_id is None: - line_id = np.where(cls.name_line == line_name)[0] + line_id = np.nonzero(cls.name_line == line_name)[0] if not len(line_id): raise AmbiguousAction( 'Line with name "{}" is not on the grid. The powerlines names are:\n{}' @@ -519,7 +519,7 @@ def reconnect_powerline( ) cls = type(self) if line_id is None: - line_id = np.where(cls.name_line == line_name)[0] + line_id = np.nonzero(cls.name_line == line_name)[0] if previous_action is None: res = self.actionClass() @@ -1494,7 +1494,7 @@ def _aux_get_back_to_ref_state_curtail(self, res, obs): def _aux_get_back_to_ref_state_line(self, res, obs): disc_lines = ~obs.line_status if disc_lines.any(): - li_disc = np.where(disc_lines)[0] + li_disc = np.nonzero(disc_lines)[0] res["powerline"] = [] for el in li_disc: act = self.actionClass() @@ -1538,7 +1538,7 @@ def _aux_get_back_to_ref_state_redisp(self, res, obs, precision=1e-5): # TODO this is ugly, probably slow and could definitely be optimized notredisp_setpoint = obs.target_dispatch != 0.0 if notredisp_setpoint.any(): - need_redisp = np.where(notredisp_setpoint)[0] + need_redisp = np.nonzero(notredisp_setpoint)[0] res["redispatching"] = [] # combine generators and do not exceed ramps (up or down) rem = np.zeros(self.n_gen, dtype=dt_float) @@ -1603,7 +1603,7 @@ def _aux_get_back_to_ref_state_storage( notredisp_setpoint = obs.storage_charge / obs.storage_Emax != storage_setpoint delta_time_hour = dt_float(obs.delta_time / 60.0) if notredisp_setpoint.any(): - need_ajust = np.where(notredisp_setpoint)[0] + need_ajust = np.nonzero(notredisp_setpoint)[0] res["storage"] = [] # combine storage units and do not exceed maximum power rem = np.zeros(self.n_storage, dtype=dt_float) diff --git a/grid2op/Agent/recoPowerLinePerArea.py b/grid2op/Agent/recoPowerLinePerArea.py index bc28584e1..e6142124c 100644 --- a/grid2op/Agent/recoPowerLinePerArea.py +++ b/grid2op/Agent/recoPowerLinePerArea.py @@ -57,7 +57,7 @@ def act(self, observation: BaseObservation, reward: float, done : bool=False): return self.action_space() area_used = np.full(self.nb_area, fill_value=False, dtype=bool) reco_ids = [] - for l_id in np.where(can_be_reco)[0]: + for l_id in np.nonzero(can_be_reco)[0]: if not area_used[self.lines_to_area_id[l_id]]: reco_ids.append(l_id) area_used[self.lines_to_area_id[l_id]] = True diff --git a/grid2op/Agent/recoPowerlineAgent.py b/grid2op/Agent/recoPowerlineAgent.py index b4373f9bd..97ba1ed36 100644 --- a/grid2op/Agent/recoPowerlineAgent.py +++ b/grid2op/Agent/recoPowerlineAgent.py @@ -28,6 +28,6 @@ def _get_tested_action(self, observation): if can_be_reco.any(): res = [ self.action_space({"set_line_status": [(id_, +1)]}) - for id_ in np.where(can_be_reco)[0] + for id_ in np.nonzero(can_be_reco)[0] ] return res diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index 5a8439a1b..904a54f93 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -437,7 +437,7 @@ def load_grid(self, # TODO here i force the distributed slack bus too, by removing the other from the ext_grid... self._grid.ext_grid = self._grid.ext_grid.iloc[:1] else: - self.slack_id = np.where(self._grid.gen["slack"])[0] + self.slack_id = np.nonzero(self._grid.gen["slack"])[0] with warnings.catch_warnings(): warnings.filterwarnings("ignore") @@ -565,9 +565,9 @@ def load_grid(self, self._init_private_attrs() # do this at the end - self._in_service_line_col_id = int(np.where(self._grid.line.columns == "in_service")[0][0]) - self._in_service_trafo_col_id = int(np.where(self._grid.trafo.columns == "in_service")[0][0]) - self._in_service_storage_cold_id = int(np.where(self._grid.storage.columns == "in_service")[0][0]) + self._in_service_line_col_id = int(np.nonzero(self._grid.line.columns == "in_service")[0][0]) + self._in_service_trafo_col_id = int(np.nonzero(self._grid.trafo.columns == "in_service")[0][0]) + self._in_service_storage_cold_id = int(np.nonzero(self._grid.storage.columns == "in_service")[0][0]) def _init_private_attrs(self) -> None: # number of elements per substation @@ -1019,14 +1019,14 @@ def _aux_runpf_pp(self, is_dc: bool): 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"{np.where(~self._grid.load['in_service'])[0]}" + f"{np.nonzero(~self._grid.load['in_service'])[0]}" ) if (~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"{np.where(~self._grid.gen['in_service'])[0]}" + f"{np.nonzero(~self._grid.gen['in_service'])[0]}" ) try: if is_dc: @@ -1108,7 +1108,7 @@ def runpf(self, is_dc : bool=False) -> Tuple[bool, Union[Exception, None]]: # see https://github.com/e2nIEE/pandapower/issues/1996 for a fix for l_id in range(cls.n_load): if cls.load_to_subid[l_id] in cls.gen_to_subid: - ind_gens = np.where( + ind_gens = np.nonzero( cls.gen_to_subid == cls.load_to_subid[l_id] )[0] for g_id in ind_gens: diff --git a/grid2op/Chronics/GSFFWFWM.py b/grid2op/Chronics/GSFFWFWM.py index fc09e16e3..55d88196d 100644 --- a/grid2op/Chronics/GSFFWFWM.py +++ b/grid2op/Chronics/GSFFWFWM.py @@ -251,7 +251,7 @@ def _generate_matenance_static(name_line, size=n_Generated_Maintenance - maxDailyMaintenance, ) are_lines_in_maintenance[ - np.where(are_lines_in_maintenance)[0][not_chosen] + np.nonzero(are_lines_in_maintenance)[0][not_chosen] ] = False maintenance_me[ selected_rows_beg:selected_rows_end, are_lines_in_maintenance diff --git a/grid2op/Chronics/gridValue.py b/grid2op/Chronics/gridValue.py index 00bc8af50..90e3227e2 100644 --- a/grid2op/Chronics/gridValue.py +++ b/grid2op/Chronics/gridValue.py @@ -288,8 +288,8 @@ def get_maintenance_time_1d(maintenance): a = np.diff(maintenance) # +1 is because numpy does the diff `t+1` - `t` so to get index of the initial array # I need to "+1" - start = np.where(a == 1)[0] + 1 # start of maintenance - end = np.where(a == -1)[0] + 1 # end of maintenance + start = np.nonzero(a == 1)[0] + 1 # start of maintenance + end = np.nonzero(a == -1)[0] + 1 # end of maintenance prev_ = 0 # it's efficient here as i do a loop only on the number of time there is a maintenance # and maintenance are quite rare @@ -362,8 +362,8 @@ def get_maintenance_duration_1d(maintenance): a = np.diff(maintenance) # +1 is because numpy does the diff `t+1` - `t` so to get index of the initial array # I need to "+1" - start = np.where(a == 1)[0] + 1 # start of maintenance - end = np.where(a == -1)[0] + 1 # end of maintenance + start = np.nonzero(a == 1)[0] + 1 # start of maintenance + end = np.nonzero(a == -1)[0] + 1 # end of maintenance prev_ = 0 # it's efficient here as i do a loop only on the number of time there is a maintenance # and maintenance are quite rare @@ -440,8 +440,8 @@ def get_hazard_duration_1d(hazard): a = np.diff(hazard) # +1 is because numpy does the diff `t+1` - `t` so to get index of the initial array # I need to "+1" - start = np.where(a == 1)[0] + 1 # start of maintenance - end = np.where(a == -1)[0] + 1 # end of maintenance + start = np.nonzero(a == 1)[0] + 1 # start of maintenance + end = np.nonzero(a == -1)[0] + 1 # end of maintenance prev_ = 0 # it's efficient here as i do a loop only on the number of time there is a maintenance # and maintenance are quite rare diff --git a/grid2op/Chronics/multiFolder.py b/grid2op/Chronics/multiFolder.py index f948f94ac..7ab2be644 100644 --- a/grid2op/Chronics/multiFolder.py +++ b/grid2op/Chronics/multiFolder.py @@ -352,7 +352,7 @@ def sample_next_chronics(self, probabilities=None): probabilities /= sum_prob # take one at "random" among these selected = self.space_prng.choice(self._order, p=probabilities) - id_sel = np.where(self._order == selected)[0] + id_sel = np.nonzero(self._order == selected)[0] self._prev_cache_id = selected - 1 return id_sel diff --git a/grid2op/Converter/BackendConverter.py b/grid2op/Converter/BackendConverter.py index 44b381a23..a6db64614 100644 --- a/grid2op/Converter/BackendConverter.py +++ b/grid2op/Converter/BackendConverter.py @@ -206,13 +206,13 @@ def _init_myself(self): == sorted(self.target_backend.name_sub) ): for id_source, nm_source in enumerate(self.source_backend.name_sub): - id_target = np.where(self.target_backend.name_sub == nm_source)[0] + id_target = np.nonzero(self.target_backend.name_sub == nm_source)[0] self._sub_tg2sr[id_source] = id_target self._sub_sr2tg[id_target] = id_source else: for id_source, nm_source in enumerate(self.source_backend.name_sub): nm_target = self.sub_source_target[nm_source] - id_target = np.where(self.target_backend.name_sub == nm_target)[0] + id_target = np.nonzero(self.target_backend.name_sub == nm_target)[0] self._sub_tg2sr[id_source] = id_target self._sub_sr2tg[id_target] = id_source @@ -300,7 +300,7 @@ def _init_myself(self): def _get_possible_target_ids(self, id_source, source_2_id_sub, target_2_id_sub, nm): id_sub_source = source_2_id_sub[id_source] id_sub_target = self._sub_tg2sr[id_sub_source] - ids_target = np.where(target_2_id_sub == id_sub_target)[0] + ids_target = np.nonzero(target_2_id_sub == id_sub_target)[0] if ids_target.shape[0] == 0: raise RuntimeError( ERROR_ELEMENT_CONNECTED.format(nm, id_sub_target, id_sub_source) @@ -346,7 +346,7 @@ def _auto_fill_vect_powerline(self): idor_sub_target = self._sub_tg2sr[idor_sub_source] idex_sub_source = source_ex_2_id_sub[id_source] idex_sub_target = self._sub_tg2sr[idex_sub_source] - ids_target = np.where( + ids_target = np.nonzero( (target_or_2_id_sub == idor_sub_target) & (target_ex_2_id_sub == idex_sub_target) )[0] diff --git a/grid2op/Converter/ConnectivityConverter.py b/grid2op/Converter/ConnectivityConverter.py index 5826c1bcc..41eed4adc 100644 --- a/grid2op/Converter/ConnectivityConverter.py +++ b/grid2op/Converter/ConnectivityConverter.py @@ -188,11 +188,11 @@ def init_converter(self, all_actions=None, **kwargs): if nb_element < 4: continue - c_id = np.where(self.load_to_subid == sub_id)[0] - g_id = np.where(self.gen_to_subid == sub_id)[0] - lor_id = np.where(self.line_or_to_subid == sub_id)[0] - lex_id = np.where(self.line_ex_to_subid == sub_id)[0] - storage_id = np.where(self.storage_to_subid == sub_id)[0] + c_id = np.nonzero(self.load_to_subid == sub_id)[0] + g_id = np.nonzero(self.gen_to_subid == sub_id)[0] + lor_id = np.nonzero(self.line_or_to_subid == sub_id)[0] + lex_id = np.nonzero(self.line_ex_to_subid == sub_id)[0] + storage_id = np.nonzero(self.storage_to_subid == sub_id)[0] c_pos = self.load_to_sub_pos[self.load_to_subid == sub_id] g_pos = self.gen_to_sub_pos[self.gen_to_subid == sub_id] @@ -380,7 +380,7 @@ def convert_act(self, encoded_act, explore=None): ) if ((encoded_act < -1.0) | (encoded_act > 1.0)).any(): errors = (encoded_act < -1.0) | (encoded_act > 1.0) - indexes = np.where(errors)[0] + indexes = np.nonzero(errors)[0] raise RuntimeError( f'All elements of "encoded_act" must be in range [-1, 1]. Please check your ' f"encoded action at positions {indexes[:5]}... (only first 5 displayed)" @@ -393,7 +393,7 @@ def convert_act(self, encoded_act, explore=None): return super().__call__() argsort_changed = np.argsort(-np.abs(encoded_act_filtered)) - argsort = np.where(act_want_change)[0][argsort_changed] + argsort = np.nonzero(act_want_change)[0][argsort_changed] act, disag = self._aux_act_from_order(argsort, encoded_act) self.indx_sel = 0 if explore is None: diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index 90e664a7b..14742504d 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -1022,7 +1022,7 @@ def load_alert_data(self): alertable_line_names = copy.deepcopy(lines_attacked) alertable_line_ids = np.empty(len(alertable_line_names), dtype=dt_int) for i, el in enumerate(alertable_line_names): - indx = np.where(self.backend.name_line == el)[0] + indx = np.nonzero(self.backend.name_line == el)[0] if not len(indx): raise Grid2OpException(f"Attacked line {el} is not found in the grid.") alertable_line_ids[i] = indx[0] @@ -1751,7 +1751,7 @@ def set_thermal_limit(self, thermal_limit): f"names. We found: {key} which is not a line name. The names of the " f"powerlines are {self.name_line}" ) - ind_line = np.where(self.name_line == key)[0][0] + ind_line = np.nonzero(self.name_line == key)[0][0] if np.isfinite(tmp[ind_line]): raise Grid2OpException( f"Humm, there is a really strange bug, some lines are set twice." @@ -1861,7 +1861,7 @@ def _prepare_redisp(self, action, new_p, already_modified_gen): "invalid because, even if the sepoint is pmin, this dispatch would set it " "to a number higher than pmax, which is impossible]. Invalid dispatch for " "generator(s): " - "{}".format(np.where(cond_invalid)[0]) + "{}".format(np.nonzero(cond_invalid)[0]) ) self._target_dispatch -= redisp_act_orig return valid, except_, info_ @@ -1873,7 +1873,7 @@ def _prepare_redisp(self, action, new_p, already_modified_gen): "invalid because, even if the sepoint is pmax, this dispatch would set it " "to a number bellow pmin, which is impossible]. Invalid dispatch for " "generator(s): " - "{}".format(np.where(cond_invalid)[0]) + "{}".format(np.nonzero(cond_invalid)[0]) ) self._target_dispatch -= redisp_act_orig return valid, except_, info_ @@ -1893,7 +1893,7 @@ def _prepare_redisp(self, action, new_p, already_modified_gen): if (redisp_act_orig_cut != redisp_act_orig).any(): info_.append( { - "INFO: redispatching cut because generator will be turned_off": np.where( + "INFO: redispatching cut because generator will be turned_off": np.nonzero( redisp_act_orig_cut != redisp_act_orig )[ 0 @@ -2353,8 +2353,8 @@ def _handle_updown_times(self, gen_up_before, redisp_act): self._gen_downtime[gen_connected_this_timestep] < self.gen_min_downtime[gen_connected_this_timestep] ) - id_gen = np.where(id_gen)[0] - id_gen = np.where(gen_connected_this_timestep[id_gen])[0] + id_gen = np.nonzero(id_gen)[0] + id_gen = np.nonzero(gen_connected_this_timestep[id_gen])[0] except_ = GeneratorTurnedOnTooSoon( "Some generator has been connected too early ({})".format(id_gen) ) @@ -2375,8 +2375,8 @@ def _handle_updown_times(self, gen_up_before, redisp_act): self._gen_uptime[gen_disconnected_this] < self.gen_min_uptime[gen_disconnected_this] ) - id_gen = np.where(id_gen)[0] - id_gen = np.where(gen_connected_this_timestep[id_gen])[0] + id_gen = np.nonzero(id_gen)[0] + id_gen = np.nonzero(gen_connected_this_timestep[id_gen])[0] except_ = GeneratorTurnedOffTooSoon( "Some generator has been disconnected too early ({})".format(id_gen) ) diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index ef2db3904..0ea5592d8 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -524,7 +524,7 @@ def _handle_compat_glop_version(self, need_process_backend): # deals with the "sub_pos" vector for sub_id in range(cls_bk.n_sub): if (cls_bk.storage_to_subid == sub_id).any(): - stor_ids = np.where(cls_bk.storage_to_subid == sub_id)[0] + stor_ids = np.nonzero(cls_bk.storage_to_subid == sub_id)[0] stor_locs = cls_bk.storage_to_sub_pos[stor_ids] for stor_loc in sorted(stor_locs, reverse=True): for vect, sub_id_me in zip( diff --git a/grid2op/Observation/baseObservation.py b/grid2op/Observation/baseObservation.py index 65071a7ea..88b230f56 100644 --- a/grid2op/Observation/baseObservation.py +++ b/grid2op/Observation/baseObservation.py @@ -3817,7 +3817,7 @@ def add_act(self, act : "grid2op.Action.BaseAction", issue_warn=True) -> Self: & (res.topo_vect[cls.line_ex_pos_topo_vect] == -1) ) if tmp.any(): - id_issue_ex = np.where(tmp)[0] + id_issue_ex = np.nonzero(tmp)[0] if issue_warn: warnings.warn(error_no_bus_set.format(id_issue_ex)) if "set_bus" in cls_act.authorized_keys: @@ -3829,7 +3829,7 @@ def add_act(self, act : "grid2op.Action.BaseAction", issue_warn=True) -> Self: & (res.topo_vect[cls.line_or_pos_topo_vect] == -1) ) if tmp.any(): - id_issue_or = np.where(tmp)[0] + id_issue_or = np.nonzero(tmp)[0] if issue_warn: warnings.warn(error_no_bus_set.format(id_issue_or)) if "set_bus" in cls_act.authorized_keys: diff --git a/grid2op/Opponent/geometricOpponent.py b/grid2op/Opponent/geometricOpponent.py index 71253d4a7..ee0e23a00 100644 --- a/grid2op/Opponent/geometricOpponent.py +++ b/grid2op/Opponent/geometricOpponent.py @@ -109,7 +109,7 @@ def init( # Store attackable lines IDs self._lines_ids = [] for l_name in lines_attacked: - l_id = np.where(self.action_space.name_line == l_name) + l_id = np.nonzero(self.action_space.name_line == l_name) if len(l_id) and len(l_id[0]): self._lines_ids.append(l_id[0][0]) else: diff --git a/grid2op/Opponent/randomLineOpponent.py b/grid2op/Opponent/randomLineOpponent.py index f1c5ed256..da8ba3058 100644 --- a/grid2op/Opponent/randomLineOpponent.py +++ b/grid2op/Opponent/randomLineOpponent.py @@ -57,7 +57,7 @@ def init(self, partial_env, lines_attacked=[], **kwargs): # Store attackable lines IDs self._lines_ids = [] for l_name in lines_attacked: - l_id = np.where(self.action_space.name_line == l_name) + l_id = np.nonzero(self.action_space.name_line == l_name) if len(l_id) and len(l_id[0]): self._lines_ids.append(l_id[0][0]) else: diff --git a/grid2op/Opponent/weightedRandomOpponent.py b/grid2op/Opponent/weightedRandomOpponent.py index 35ad5f2be..d058f913f 100644 --- a/grid2op/Opponent/weightedRandomOpponent.py +++ b/grid2op/Opponent/weightedRandomOpponent.py @@ -73,7 +73,7 @@ def init( # Store attackable lines IDs self._lines_ids = [] for l_name in lines_attacked: - l_id = np.where(self.action_space.name_line == l_name) + l_id = np.whnonzeroere(self.action_space.name_line == l_name) if len(l_id) and len(l_id[0]): self._lines_ids.append(l_id[0][0]) else: diff --git a/grid2op/Reward/alarmReward.py b/grid2op/Reward/alarmReward.py index e114a7920..cee617d2c 100644 --- a/grid2op/Reward/alarmReward.py +++ b/grid2op/Reward/alarmReward.py @@ -107,7 +107,7 @@ def _mult_for_zone(self, alarm, disc_lines, env): """compute the multiplicative factor that increases the score if the right zone is predicted""" res = 1.0 # extract the lines that have been disconnected due to cascading failures - lines_disconnected_first = np.where(disc_lines == 0)[0] + lines_disconnected_first = np.nonzero(disc_lines == 0)[0] if ( alarm.sum() > 1 @@ -124,7 +124,7 @@ def _mult_for_zone(self, alarm, disc_lines, env): # now retrieve the id of the zones in which a powerline has been disconnected list_zone_names = list(zones_these_lines) - list_zone_ids = np.where(np.isin(env.alarms_area_names, list_zone_names))[0] + list_zone_ids = np.nonzero(np.isin(env.alarms_area_names, list_zone_names))[0] # and finally, award some extra points if one of the zone, containing one of the powerline disconnected # by protection is in the alarm if alarm[list_zone_ids].any(): diff --git a/grid2op/Reward/alertReward.py b/grid2op/Reward/alertReward.py index 1ab8d4d7c..aac6236d5 100644 --- a/grid2op/Reward/alertReward.py +++ b/grid2op/Reward/alertReward.py @@ -157,7 +157,7 @@ def _update_state(self, env, action): def _compute_score_attack_blackout(self, env, ts_attack_in_order, indexes_to_look): # retrieve the lines that have been attacked in the time window - ts_ind, line_ind = np.where(ts_attack_in_order) + ts_ind, line_ind = np.nonzero(ts_attack_in_order) line_first_attack, first_ind_line_attacked = np.unique(line_ind, return_index=True) ts_first_line_attacked = ts_ind[first_ind_line_attacked] # now retrieve the array starting at the correct place diff --git a/grid2op/Rules/LookParam.py b/grid2op/Rules/LookParam.py index 13445e612..797f42e5a 100644 --- a/grid2op/Rules/LookParam.py +++ b/grid2op/Rules/LookParam.py @@ -35,13 +35,13 @@ def __call__(self, action, env): aff_lines, aff_subs = action.get_topological_impact(powerline_status) if aff_lines.sum() > env._parameters.MAX_LINE_STATUS_CHANGED: - ids = np.where(aff_lines)[0] + ids = np.nonzero(aff_lines)[0] return False, IllegalAction( "More than {} line status affected by the action: {}" "".format(env.parameters.MAX_LINE_STATUS_CHANGED, ids) ) if aff_subs.sum() > env._parameters.MAX_SUB_CHANGED: - ids = np.where(aff_subs)[0] + ids = np.nonzero(aff_subs)[0] return False, IllegalAction( "More than {} substation affected by the action: {}" "".format(env.parameters.MAX_SUB_CHANGED, ids) diff --git a/grid2op/Rules/PreventDiscoStorageModif.py b/grid2op/Rules/PreventDiscoStorageModif.py index ba52472f1..8adff9d7c 100644 --- a/grid2op/Rules/PreventDiscoStorageModif.py +++ b/grid2op/Rules/PreventDiscoStorageModif.py @@ -41,6 +41,6 @@ def __call__(self, action, env): tmp_ = power_modif_disco & not_set_status & not_change_status return False, IllegalAction( f"Attempt to modify the power produced / absorbed by a storage unit " - f"without reconnecting it (check storage with id {np.where(tmp_)[0]}." + f"without reconnecting it (check storage with id {np.nonzero(tmp_)[0]}." ) return True, None diff --git a/grid2op/Rules/PreventReconnection.py b/grid2op/Rules/PreventReconnection.py index 464c3653e..354a77535 100644 --- a/grid2op/Rules/PreventReconnection.py +++ b/grid2op/Rules/PreventReconnection.py @@ -38,7 +38,7 @@ def __call__(self, action, env): if (env._times_before_line_status_actionable[aff_lines] > 0).any(): # i tried to act on a powerline too shortly after a previous action # or shut down due to an overflow or opponent or hazards or maintenance - ids = np.where((env._times_before_line_status_actionable > 0) & aff_lines)[ + ids = np.nonzero((env._times_before_line_status_actionable > 0) & aff_lines)[ 0 ] return False, IllegalAction( @@ -49,7 +49,7 @@ def __call__(self, action, env): if (env._times_before_topology_actionable[aff_subs] > 0).any(): # I tried to act on a topology too shortly after a previous action - ids = np.where((env._times_before_topology_actionable > 0) & aff_subs)[0] + ids = np.nonzero((env._times_before_topology_actionable > 0) & aff_subs)[0] return False, IllegalAction( "Substation with ids {} have been modified illegally (cooldown of {})".format( ids, env._times_before_topology_actionable[ids] diff --git a/grid2op/Rules/rulesByArea.py b/grid2op/Rules/rulesByArea.py index 66efe22b2..1338cb91f 100644 --- a/grid2op/Rules/rulesByArea.py +++ b/grid2op/Rules/rulesByArea.py @@ -87,7 +87,7 @@ def initialize(self, env): raise Grid2OpException("The number of listed ids of substations in rule initialization does not match the number of " "substations of the chosen environement. Look for missing ids or doublon") else: - self.lines_id_by_area = {key : sorted(list(chain(*[[item for item in np.where(env.line_or_to_subid == subid)[0] + self.lines_id_by_area = {key : sorted(list(chain(*[[item for item in np.nonzero(env.line_or_to_subid == subid)[0] ] for subid in subid_list]))) for key,subid_list in self.substations_id_by_area.items()} @@ -120,13 +120,13 @@ def _lookparam_byarea(self, action, env): aff_lines, aff_subs = action.get_topological_impact(powerline_status) if any([(aff_lines[line_ids]).sum() > env._parameters.MAX_LINE_STATUS_CHANGED for line_ids in self.lines_id_by_area.values()]): - ids = [[k for k in np.where(aff_lines)[0] if k in line_ids] for line_ids in self.lines_id_by_area.values()] + ids = [[k for k in np.nonzero(aff_lines)[0] if k in line_ids] for line_ids in self.lines_id_by_area.values()] return False, IllegalAction( "More than {} line status affected by the action in one area: {}" "".format(env.parameters.MAX_LINE_STATUS_CHANGED, ids) ) if any([(aff_subs[sub_ids]).sum() > env._parameters.MAX_SUB_CHANGED for sub_ids in self.substations_id_by_area.values()]): - ids = [[k for k in np.where(aff_subs)[0] if k in sub_ids] for sub_ids in self.substations_id_by_area.values()] + ids = [[k for k in np.nonzero(aff_subs)[0] if k in sub_ids] for sub_ids in self.substations_id_by_area.values()] return False, IllegalAction( "More than {} substation affected by the action in one area: {}" "".format(env.parameters.MAX_SUB_CHANGED, ids) diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 1ec0f8a1d..495d0de12 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -2036,7 +2036,7 @@ def assert_grid_correct_cls(cls): if not np.all(obj_per_sub == cls.sub_info): raise IncorrectNumberOfElements( - f"for substation(s): {np.where(obj_per_sub != cls.sub_info)[0]}" + f"for substation(s): {np.nonzero(obj_per_sub != cls.sub_info)[0]}" ) # test right number of element in substations @@ -2337,57 +2337,57 @@ def _check_validity_storage_data(cls): ) if (cls.storage_Emax < cls.storage_Emin).any(): - tmp = np.where(cls.storage_Emax < cls.storage_Emin)[0] + tmp = np.nonzero(cls.storage_Emax < cls.storage_Emin)[0] raise BackendError( f"storage_Emax < storage_Emin for storage units with ids: {tmp}" ) if (cls.storage_Emax < 0.0).any(): - tmp = np.where(cls.storage_Emax < 0.0)[0] + tmp = np.nonzero(cls.storage_Emax < 0.0)[0] raise BackendError( f"self.storage_Emax < 0. for storage units with ids: {tmp}" ) if (cls.storage_Emin < 0.0).any(): - tmp = np.where(cls.storage_Emin < 0.0)[0] + tmp = np.nonzero(cls.storage_Emin < 0.0)[0] raise BackendError( f"self.storage_Emin < 0. for storage units with ids: {tmp}" ) if (cls.storage_max_p_prod < 0.0).any(): - tmp = np.where(cls.storage_max_p_prod < 0.0)[0] + tmp = np.nonzero(cls.storage_max_p_prod < 0.0)[0] raise BackendError( f"self.storage_max_p_prod < 0. for storage units with ids: {tmp}" ) if (cls.storage_max_p_absorb < 0.0).any(): - tmp = np.where(cls.storage_max_p_absorb < 0.0)[0] + tmp = np.nonzero(cls.storage_max_p_absorb < 0.0)[0] raise BackendError( f"self.storage_max_p_absorb < 0. for storage units with ids: {tmp}" ) if (cls.storage_loss < 0.0).any(): - tmp = np.where(cls.storage_loss < 0.0)[0] + tmp = np.nonzero(cls.storage_loss < 0.0)[0] raise BackendError( f"self.storage_loss < 0. for storage units with ids: {tmp}" ) if (cls.storage_discharging_efficiency <= 0.0).any(): - tmp = np.where(cls.storage_discharging_efficiency <= 0.0)[0] + tmp = np.nonzero(cls.storage_discharging_efficiency <= 0.0)[0] raise BackendError( f"self.storage_discharging_efficiency <= 0. for storage units with ids: {tmp}" ) if (cls.storage_discharging_efficiency > 1.0).any(): - tmp = np.where(cls.storage_discharging_efficiency > 1.0)[0] + tmp = np.nonzero(cls.storage_discharging_efficiency > 1.0)[0] raise BackendError( f"self.storage_discharging_efficiency > 1. for storage units with ids: {tmp}" ) if (cls.storage_charging_efficiency < 0.0).any(): - tmp = np.where(cls.storage_charging_efficiency < 0.0)[0] + tmp = np.nonzero(cls.storage_charging_efficiency < 0.0)[0] raise BackendError( f"self.storage_charging_efficiency < 0. for storage units with ids: {tmp}" ) if (cls.storage_charging_efficiency > 1.0).any(): - tmp = np.where(cls.storage_charging_efficiency > 1.0)[0] + tmp = np.nonzero(cls.storage_charging_efficiency > 1.0)[0] raise BackendError( f"self.storage_charging_efficiency > 1. for storage units with ids: {tmp}" ) if (cls.storage_loss > cls.storage_max_p_absorb).any(): - tmp = np.where(cls.storage_loss > cls.storage_max_p_absorb)[0] + tmp = np.nonzero(cls.storage_loss > cls.storage_max_p_absorb)[0] raise BackendError( f"Some storage units are such that their loss (self.storage_loss) is higher " f"than the maximum power at which they can be charged (self.storage_max_p_absorb). " @@ -2884,11 +2884,11 @@ def get_obj_connect_to(cls, _sentinel=None, substation_id=None): "".format(substation_id) ) res = { - "loads_id": np.where(cls.load_to_subid == substation_id)[0], - "generators_id": np.where(cls.gen_to_subid == substation_id)[0], - "lines_or_id": np.where(cls.line_or_to_subid == substation_id)[0], - "lines_ex_id": np.where(cls.line_ex_to_subid == substation_id)[0], - "storages_id": np.where(cls.storage_to_subid == substation_id)[0], + "loads_id": np.nonzero(cls.load_to_subid == substation_id)[0], + "generators_id": np.nonzero(cls.gen_to_subid == substation_id)[0], + "lines_or_id": np.nonzero(cls.line_or_to_subid == substation_id)[0], + "lines_ex_id": np.nonzero(cls.line_ex_to_subid == substation_id)[0], + "storages_id": np.nonzero(cls.storage_to_subid == substation_id)[0], "nb_elements": cls.sub_info[substation_id], } return res diff --git a/grid2op/gym_compat/box_gym_actspace.py b/grid2op/gym_compat/box_gym_actspace.py index 8f6d4cc01..5cd4195a2 100644 --- a/grid2op/gym_compat/box_gym_actspace.py +++ b/grid2op/gym_compat/box_gym_actspace.py @@ -225,45 +225,45 @@ def __init__( self._attr_to_keep = sorted(attr_to_keep) - act_sp = type(grid2op_action_space) + act_sp_cls = type(grid2op_action_space) self._act_space = copy.deepcopy(grid2op_action_space) - low_gen = -1.0 * act_sp.gen_max_ramp_down[act_sp.gen_redispatchable] - high_gen = 1.0 * act_sp.gen_max_ramp_up[act_sp.gen_redispatchable] - nb_redisp = act_sp.gen_redispatchable.sum() - nb_curtail = act_sp.gen_renewable.sum() + low_gen = -1.0 * act_sp_cls.gen_max_ramp_down[act_sp_cls.gen_redispatchable] + high_gen = 1.0 * act_sp_cls.gen_max_ramp_up[act_sp_cls.gen_redispatchable] + nb_redisp = act_sp_cls.gen_redispatchable.sum() + nb_curtail = act_sp_cls.gen_renewable.sum() curtail = np.full(shape=(nb_curtail,), fill_value=0.0, dtype=dt_float) curtail_mw = np.full(shape=(nb_curtail,), fill_value=0.0, dtype=dt_float) self._dict_properties = { "set_line_status": ( - np.full(shape=(act_sp.n_line,), fill_value=-1, dtype=dt_int), - np.full(shape=(act_sp.n_line,), fill_value=1, dtype=dt_int), - (act_sp.n_line,), + np.full(shape=(act_sp_cls.n_line,), fill_value=-1, dtype=dt_int), + np.full(shape=(act_sp_cls.n_line,), fill_value=1, dtype=dt_int), + (act_sp_cls.n_line,), dt_int, ), "change_line_status": ( - np.full(shape=(act_sp.n_line,), fill_value=0, dtype=dt_int), - np.full(shape=(act_sp.n_line,), fill_value=1, dtype=dt_int), - (act_sp.n_line,), + np.full(shape=(act_sp_cls.n_line,), fill_value=0, dtype=dt_int), + np.full(shape=(act_sp_cls.n_line,), fill_value=1, dtype=dt_int), + (act_sp_cls.n_line,), dt_int, ), "set_bus": ( - np.full(shape=(act_sp.dim_topo,), fill_value=-1, dtype=dt_int), - np.full(shape=(act_sp.dim_topo,), fill_value=act_sp.n_busbar_per_sub, dtype=dt_int), - (act_sp.dim_topo,), + np.full(shape=(act_sp_cls.dim_topo,), fill_value=-1, dtype=dt_int), + np.full(shape=(act_sp_cls.dim_topo,), fill_value=act_sp_cls.n_busbar_per_sub, dtype=dt_int), + (act_sp_cls.dim_topo,), dt_int, ), "change_bus": ( - np.full(shape=(act_sp.dim_topo,), fill_value=0, dtype=dt_int), - np.full(shape=(act_sp.dim_topo,), fill_value=1, dtype=dt_int), - (act_sp.dim_topo,), + np.full(shape=(act_sp_cls.dim_topo,), fill_value=0, dtype=dt_int), + np.full(shape=(act_sp_cls.dim_topo,), fill_value=1, dtype=dt_int), + (act_sp_cls.dim_topo,), dt_int, ), "redispatch": (low_gen, high_gen, (nb_redisp,), dt_float), "set_storage": ( - -1.0 * act_sp.storage_max_p_prod, - 1.0 * act_sp.storage_max_p_absorb, - (act_sp.n_storage,), + -1.0 * act_sp_cls.storage_max_p_prod, + 1.0 * act_sp_cls.storage_max_p_absorb, + (act_sp_cls.n_storage,), dt_float, ), "curtail": ( @@ -274,20 +274,20 @@ def __init__( ), "curtail_mw": ( curtail_mw, - 1.0 * act_sp.gen_pmax[act_sp.gen_renewable], + 1.0 * act_sp_cls.gen_pmax[act_sp_cls.gen_renewable], (nb_curtail,), dt_float, ), "raise_alarm": ( - np.full(shape=(act_sp.dim_alarms,), fill_value=0, dtype=dt_int), - np.full(shape=(act_sp.dim_alarms,), fill_value=1, dtype=dt_int), - (act_sp.dim_alarms,), + np.full(shape=(act_sp_cls.dim_alarms,), fill_value=0, dtype=dt_int), + np.full(shape=(act_sp_cls.dim_alarms,), fill_value=1, dtype=dt_int), + (act_sp_cls.dim_alarms,), dt_int, ), "raise_alert": ( - np.full(shape=(act_sp.dim_alerts,), fill_value=0, dtype=dt_int), - np.full(shape=(act_sp.dim_alerts,), fill_value=1, dtype=dt_int), - (act_sp.dim_alerts,), + np.full(shape=(act_sp_cls.dim_alerts,), fill_value=0, dtype=dt_int), + np.full(shape=(act_sp_cls.dim_alerts,), fill_value=1, dtype=dt_int), + (act_sp_cls.dim_alerts,), dt_int, ), } diff --git a/grid2op/gym_compat/box_gym_obsspace.py b/grid2op/gym_compat/box_gym_obsspace.py index 568ebb0b6..76879ef9e 100644 --- a/grid2op/gym_compat/box_gym_obsspace.py +++ b/grid2op/gym_compat/box_gym_obsspace.py @@ -213,7 +213,8 @@ def __init__( ) self._attr_to_keep = sorted(attr_to_keep) - ob_sp = type(grid2op_observation_space) + ob_sp = grid2op_observation_space + ob_sp_cls = type(grid2op_observation_space) tol_redisp = ( ob_sp.obs_env._tol_poly ) # add to gen_p otherwise ... well it can crash @@ -263,113 +264,113 @@ def __init__( dt_int, ), "gen_p": ( - np.full(shape=(ob_sp.n_gen,), fill_value=0.0, dtype=dt_float) + np.full(shape=(ob_sp_cls.n_gen,), fill_value=0.0, dtype=dt_float) - tol_redisp - extra_for_losses, - ob_sp.gen_pmax + tol_redisp + extra_for_losses, - (ob_sp.n_gen,), + ob_sp_cls.gen_pmax + tol_redisp + extra_for_losses, + (ob_sp_cls.n_gen,), dt_float, ), "gen_q": ( - np.full(shape=(ob_sp.n_gen,), fill_value=-np.inf, dtype=dt_float), - np.full(shape=(ob_sp.n_gen,), fill_value=np.inf, dtype=dt_float), - (ob_sp.n_gen,), + np.full(shape=(ob_sp_cls.n_gen,), fill_value=-np.inf, dtype=dt_float), + np.full(shape=(ob_sp_cls.n_gen,), fill_value=np.inf, dtype=dt_float), + (ob_sp_cls.n_gen,), dt_float, ), "gen_v": ( - np.full(shape=(ob_sp.n_gen,), fill_value=0.0, dtype=dt_float), - np.full(shape=(ob_sp.n_gen,), fill_value=np.inf, dtype=dt_float), - (ob_sp.n_gen,), + np.full(shape=(ob_sp_cls.n_gen,), fill_value=0.0, dtype=dt_float), + np.full(shape=(ob_sp_cls.n_gen,), fill_value=np.inf, dtype=dt_float), + (ob_sp_cls.n_gen,), dt_float, ), "gen_margin_up": ( - np.full(shape=(ob_sp.n_gen,), fill_value=0.0, dtype=dt_float), - 1.0 * ob_sp.gen_max_ramp_up, - (ob_sp.n_gen,), + np.full(shape=(ob_sp_cls.n_gen,), fill_value=0.0, dtype=dt_float), + 1.0 * ob_sp_cls.gen_max_ramp_up, + (ob_sp_cls.n_gen,), dt_float, ), "gen_margin_down": ( - np.full(shape=(ob_sp.n_gen,), fill_value=0.0, dtype=dt_float), - 1.0 * ob_sp.gen_max_ramp_down, - (ob_sp.n_gen,), + np.full(shape=(ob_sp_cls.n_gen,), fill_value=0.0, dtype=dt_float), + 1.0 * ob_sp_cls.gen_max_ramp_down, + (ob_sp_cls.n_gen,), dt_float, ), "gen_theta": ( - np.full(shape=(ob_sp.n_gen,), fill_value=-180., dtype=dt_float), - np.full(shape=(ob_sp.n_gen,), fill_value=180., dtype=dt_float), - (ob_sp.n_gen,), + np.full(shape=(ob_sp_cls.n_gen,), fill_value=-180., dtype=dt_float), + np.full(shape=(ob_sp_cls.n_gen,), fill_value=180., dtype=dt_float), + (ob_sp_cls.n_gen,), dt_float, ), "load_p": ( - np.full(shape=(ob_sp.n_load,), fill_value=-np.inf, dtype=dt_float), - np.full(shape=(ob_sp.n_load,), fill_value=+np.inf, dtype=dt_float), - (ob_sp.n_load,), + np.full(shape=(ob_sp_cls.n_load,), fill_value=-np.inf, dtype=dt_float), + np.full(shape=(ob_sp_cls.n_load,), fill_value=+np.inf, dtype=dt_float), + (ob_sp_cls.n_load,), dt_float, ), "load_q": ( - np.full(shape=(ob_sp.n_load,), fill_value=-np.inf, dtype=dt_float), - np.full(shape=(ob_sp.n_load,), fill_value=+np.inf, dtype=dt_float), - (ob_sp.n_load,), + np.full(shape=(ob_sp_cls.n_load,), fill_value=-np.inf, dtype=dt_float), + np.full(shape=(ob_sp_cls.n_load,), fill_value=+np.inf, dtype=dt_float), + (ob_sp_cls.n_load,), dt_float, ), "load_v": ( - np.full(shape=(ob_sp.n_load,), fill_value=0.0, dtype=dt_float), - np.full(shape=(ob_sp.n_load,), fill_value=np.inf, dtype=dt_float), - (ob_sp.n_load,), + np.full(shape=(ob_sp_cls.n_load,), fill_value=0.0, dtype=dt_float), + np.full(shape=(ob_sp_cls.n_load,), fill_value=np.inf, dtype=dt_float), + (ob_sp_cls.n_load,), dt_float, ), "load_theta": ( - np.full(shape=(ob_sp.n_load,), fill_value=-180., dtype=dt_float), - np.full(shape=(ob_sp.n_load,), fill_value=180., dtype=dt_float), - (ob_sp.n_load,), + np.full(shape=(ob_sp_cls.n_load,), fill_value=-180., dtype=dt_float), + np.full(shape=(ob_sp_cls.n_load,), fill_value=180., dtype=dt_float), + (ob_sp_cls.n_load,), dt_float, ), "p_or": ( - np.full(shape=(ob_sp.n_line,), fill_value=-np.inf, dtype=dt_float), - np.full(shape=(ob_sp.n_line,), fill_value=np.inf, dtype=dt_float), - (ob_sp.n_line,), + np.full(shape=(ob_sp_cls.n_line,), fill_value=-np.inf, dtype=dt_float), + np.full(shape=(ob_sp_cls.n_line,), fill_value=np.inf, dtype=dt_float), + (ob_sp_cls.n_line,), dt_float, ), "q_or": ( - np.full(shape=(ob_sp.n_line,), fill_value=-np.inf, dtype=dt_float), - np.full(shape=(ob_sp.n_line,), fill_value=np.inf, dtype=dt_float), - (ob_sp.n_line,), + np.full(shape=(ob_sp_cls.n_line,), fill_value=-np.inf, dtype=dt_float), + np.full(shape=(ob_sp_cls.n_line,), fill_value=np.inf, dtype=dt_float), + (ob_sp_cls.n_line,), dt_float, ), "a_or": ( - np.full(shape=(ob_sp.n_line,), fill_value=0.0, dtype=dt_float), - np.full(shape=(ob_sp.n_line,), fill_value=np.inf, dtype=dt_float), - (ob_sp.n_line,), + np.full(shape=(ob_sp_cls.n_line,), fill_value=0.0, dtype=dt_float), + np.full(shape=(ob_sp_cls.n_line,), fill_value=np.inf, dtype=dt_float), + (ob_sp_cls.n_line,), dt_float, ), "v_or": ( - np.full(shape=(ob_sp.n_line,), fill_value=0.0, dtype=dt_float), - np.full(shape=(ob_sp.n_line,), fill_value=np.inf, dtype=dt_float), - (ob_sp.n_line,), + np.full(shape=(ob_sp_cls.n_line,), fill_value=0.0, dtype=dt_float), + np.full(shape=(ob_sp_cls.n_line,), fill_value=np.inf, dtype=dt_float), + (ob_sp_cls.n_line,), dt_float, ), "theta_or": ( - np.full(shape=(ob_sp.n_line,), fill_value=-180., dtype=dt_float), - np.full(shape=(ob_sp.n_line,), fill_value=180., dtype=dt_float), - (ob_sp.n_line,), + np.full(shape=(ob_sp_cls.n_line,), fill_value=-180., dtype=dt_float), + np.full(shape=(ob_sp_cls.n_line,), fill_value=180., dtype=dt_float), + (ob_sp_cls.n_line,), dt_float, ), "p_ex": ( - np.full(shape=(ob_sp.n_line,), fill_value=-np.inf, dtype=dt_float), - np.full(shape=(ob_sp.n_line,), fill_value=np.inf, dtype=dt_float), - (ob_sp.n_line,), + np.full(shape=(ob_sp_cls.n_line,), fill_value=-np.inf, dtype=dt_float), + np.full(shape=(ob_sp_cls.n_line,), fill_value=np.inf, dtype=dt_float), + (ob_sp_cls.n_line,), dt_float, ), "q_ex": ( - np.full(shape=(ob_sp.n_line,), fill_value=-np.inf, dtype=dt_float), - np.full(shape=(ob_sp.n_line,), fill_value=np.inf, dtype=dt_float), - (ob_sp.n_line,), + np.full(shape=(ob_sp_cls.n_line,), fill_value=-np.inf, dtype=dt_float), + np.full(shape=(ob_sp_cls.n_line,), fill_value=np.inf, dtype=dt_float), + (ob_sp_cls.n_line,), dt_float, ), "a_ex": ( - np.full(shape=(ob_sp.n_line,), fill_value=0.0, dtype=dt_float), - np.full(shape=(ob_sp.n_line,), fill_value=np.inf, dtype=dt_float), - (ob_sp.n_line,), + np.full(shape=(ob_sp_cls.n_line,), fill_value=0.0, dtype=dt_float), + np.full(shape=(ob_sp_cls.n_line,), fill_value=np.inf, dtype=dt_float), + (ob_sp_cls.n_line,), dt_float, ), "v_ex": ( @@ -379,135 +380,135 @@ def __init__( dt_float, ), "theta_ex": ( - np.full(shape=(ob_sp.n_line,), fill_value=-180., dtype=dt_float), - np.full(shape=(ob_sp.n_line,), fill_value=180., dtype=dt_float), - (ob_sp.n_line,), + np.full(shape=(ob_sp_cls.n_line,), fill_value=-180., dtype=dt_float), + np.full(shape=(ob_sp_cls.n_line,), fill_value=180., dtype=dt_float), + (ob_sp_cls.n_line,), dt_float, ), "rho": ( - np.full(shape=(ob_sp.n_line,), fill_value=0.0, dtype=dt_float), - np.full(shape=(ob_sp.n_line,), fill_value=np.inf, dtype=dt_float), - (ob_sp.n_line,), + np.full(shape=(ob_sp_cls.n_line,), fill_value=0.0, dtype=dt_float), + np.full(shape=(ob_sp_cls.n_line,), fill_value=np.inf, dtype=dt_float), + (ob_sp_cls.n_line,), dt_float, ), "line_status": ( - np.full(shape=(ob_sp.n_line,), fill_value=0, dtype=dt_int), - np.full(shape=(ob_sp.n_line,), fill_value=1, dtype=dt_int), - (ob_sp.n_line,), + np.full(shape=(ob_sp_cls.n_line,), fill_value=0, dtype=dt_int), + np.full(shape=(ob_sp_cls.n_line,), fill_value=1, dtype=dt_int), + (ob_sp_cls.n_line,), dt_int, ), "timestep_overflow": ( np.full( - shape=(ob_sp.n_line,), fill_value=np.iinfo(dt_int).min, dtype=dt_int + shape=(ob_sp_cls.n_line,), fill_value=np.iinfo(dt_int).min, dtype=dt_int ), np.full( - shape=(ob_sp.n_line,), fill_value=np.iinfo(dt_int).max, dtype=dt_int + shape=(ob_sp_cls.n_line,), fill_value=np.iinfo(dt_int).max, dtype=dt_int ), - (ob_sp.n_line,), + (ob_sp_cls.n_line,), dt_int, ), "topo_vect": ( - np.full(shape=(ob_sp.dim_topo,), fill_value=-1, dtype=dt_int), - np.full(shape=(ob_sp.dim_topo,), fill_value=ob_sp.n_busbar_per_sub, dtype=dt_int), - (ob_sp.dim_topo,), + np.full(shape=(ob_sp_cls.dim_topo,), fill_value=-1, dtype=dt_int), + np.full(shape=(ob_sp_cls.dim_topo,), fill_value=ob_sp_cls.n_busbar_per_sub, dtype=dt_int), + (ob_sp_cls.dim_topo,), dt_int, ), "time_before_cooldown_line": ( - np.full(shape=(ob_sp.n_line,), fill_value=0, dtype=dt_int), + np.full(shape=(ob_sp_cls.n_line,), fill_value=0, dtype=dt_int), np.full( - shape=(ob_sp.n_line,), fill_value=np.iinfo(dt_int).max, dtype=dt_int + shape=(ob_sp_cls.n_line,), fill_value=np.iinfo(dt_int).max, dtype=dt_int ), - (ob_sp.n_line,), + (ob_sp_cls.n_line,), dt_int, ), "time_before_cooldown_sub": ( - np.full(shape=(ob_sp.n_sub,), fill_value=0, dtype=dt_int), + np.full(shape=(ob_sp_cls.n_sub,), fill_value=0, dtype=dt_int), np.full( - shape=(ob_sp.n_sub,), fill_value=np.iinfo(dt_int).max, dtype=dt_int + shape=(ob_sp_cls.n_sub,), fill_value=np.iinfo(dt_int).max, dtype=dt_int ), - (ob_sp.n_sub,), + (ob_sp_cls.n_sub,), dt_int, ), "time_next_maintenance": ( - np.full(shape=(ob_sp.n_line,), fill_value=-1, dtype=dt_int), + np.full(shape=(ob_sp_cls.n_line,), fill_value=-1, dtype=dt_int), np.full( - shape=(ob_sp.n_line,), fill_value=np.iinfo(dt_int).max, dtype=dt_int + shape=(ob_sp_cls.n_line,), fill_value=np.iinfo(dt_int).max, dtype=dt_int ), - (ob_sp.n_line,), + (ob_sp_cls.n_line,), dt_int, ), "duration_next_maintenance": ( - np.full(shape=(ob_sp.n_line,), fill_value=0, dtype=dt_int), + np.full(shape=(ob_sp_cls.n_line,), fill_value=0, dtype=dt_int), np.full( - shape=(ob_sp.n_line,), fill_value=np.iinfo(dt_int).max, dtype=dt_int + shape=(ob_sp_cls.n_line,), fill_value=np.iinfo(dt_int).max, dtype=dt_int ), - (ob_sp.n_line,), + (ob_sp_cls.n_line,), dt_int, ), "target_dispatch": ( - np.minimum(ob_sp.gen_pmin, -ob_sp.gen_pmax), - np.maximum(-ob_sp.gen_pmin, +ob_sp.gen_pmax), - (ob_sp.n_gen,), + np.minimum(ob_sp_cls.gen_pmin, -ob_sp_cls.gen_pmax), + np.maximum(-ob_sp_cls.gen_pmin, +ob_sp_cls.gen_pmax), + (ob_sp_cls.n_gen,), dt_float, ), "actual_dispatch": ( - np.minimum(ob_sp.gen_pmin, -ob_sp.gen_pmax), - np.maximum(-ob_sp.gen_pmin, +ob_sp.gen_pmax), - (ob_sp.n_gen,), + np.minimum(ob_sp_cls.gen_pmin, -ob_sp_cls.gen_pmax), + np.maximum(-ob_sp_cls.gen_pmin, +ob_sp_cls.gen_pmax), + (ob_sp_cls.n_gen,), dt_float, ), "storage_charge": ( - np.full(shape=(ob_sp.n_storage,), fill_value=0, dtype=dt_float), - 1.0 * ob_sp.storage_Emax, - (ob_sp.n_storage,), + np.full(shape=(ob_sp_cls.n_storage,), fill_value=0, dtype=dt_float), + 1.0 * ob_sp_cls.storage_Emax, + (ob_sp_cls.n_storage,), dt_float, ), "storage_power_target": ( - -1.0 * ob_sp.storage_max_p_prod, - 1.0 * ob_sp.storage_max_p_absorb, - (ob_sp.n_storage,), + -1.0 * ob_sp_cls.storage_max_p_prod, + 1.0 * ob_sp_cls.storage_max_p_absorb, + (ob_sp_cls.n_storage,), dt_float, ), "storage_power": ( - -1.0 * ob_sp.storage_max_p_prod, - 1.0 * ob_sp.storage_max_p_absorb, - (ob_sp.n_storage,), + -1.0 * ob_sp_cls.storage_max_p_prod, + 1.0 * ob_sp_cls.storage_max_p_absorb, + (ob_sp_cls.n_storage,), dt_float, ), "storage_theta": ( - np.full(shape=(ob_sp.n_storage,), fill_value=-180., dtype=dt_float), - np.full(shape=(ob_sp.n_storage,), fill_value=180., dtype=dt_float), - (ob_sp.n_storage,), + np.full(shape=(ob_sp_cls.n_storage,), fill_value=-180., dtype=dt_float), + np.full(shape=(ob_sp_cls.n_storage,), fill_value=180., dtype=dt_float), + (ob_sp_cls.n_storage,), dt_float, ), "curtailment": ( - np.full(shape=(ob_sp.n_gen,), fill_value=0.0, dtype=dt_float), - np.full(shape=(ob_sp.n_gen,), fill_value=1.0, dtype=dt_float), - (ob_sp.n_gen,), + np.full(shape=(ob_sp_cls.n_gen,), fill_value=0.0, dtype=dt_float), + np.full(shape=(ob_sp_cls.n_gen,), fill_value=1.0, dtype=dt_float), + (ob_sp_cls.n_gen,), dt_float, ), "curtailment_limit": ( - np.full(shape=(ob_sp.n_gen,), fill_value=0.0, dtype=dt_float), - np.full(shape=(ob_sp.n_gen,), fill_value=1.0, dtype=dt_float), - (ob_sp.n_gen,), + np.full(shape=(ob_sp_cls.n_gen,), fill_value=0.0, dtype=dt_float), + np.full(shape=(ob_sp_cls.n_gen,), fill_value=1.0, dtype=dt_float), + (ob_sp_cls.n_gen,), dt_float, ), "curtailment_mw": ( - np.full(shape=(ob_sp.n_gen,), fill_value=0.0, dtype=dt_float), - 1.0 * ob_sp.gen_pmax, - (ob_sp.n_gen,), + np.full(shape=(ob_sp_cls.n_gen,), fill_value=0.0, dtype=dt_float), + 1.0 * ob_sp_cls.gen_pmax, + (ob_sp_cls.n_gen,), dt_float, ), "curtailment_limit_mw": ( - np.full(shape=(ob_sp.n_gen,), fill_value=0.0, dtype=dt_float), - 1.0 * ob_sp.gen_pmax, - (ob_sp.n_gen,), + np.full(shape=(ob_sp_cls.n_gen,), fill_value=0.0, dtype=dt_float), + 1.0 * ob_sp_cls.gen_pmax, + (ob_sp_cls.n_gen,), dt_float, ), "thermal_limit": ( - np.full(shape=(ob_sp.n_line,), fill_value=0.0, dtype=dt_float), - np.full(shape=(ob_sp.n_line,), fill_value=np.inf, dtype=dt_float), - (ob_sp.n_line,), + np.full(shape=(ob_sp_cls.n_line,), fill_value=0.0, dtype=dt_float), + np.full(shape=(ob_sp_cls.n_line,), fill_value=np.inf, dtype=dt_float), + (ob_sp_cls.n_line,), dt_float, ), "is_alarm_illegal": ( @@ -523,13 +524,13 @@ def __init__( dt_int, ), "last_alarm": ( - np.full(shape=(ob_sp.dim_alarms,), fill_value=-1, dtype=dt_int), + np.full(shape=(ob_sp_cls.dim_alarms,), fill_value=-1, dtype=dt_int), np.full( - shape=(ob_sp.dim_alarms,), + shape=(ob_sp_cls.dim_alarms,), fill_value=np.iinfo(dt_int).max, dtype=dt_int, ), - (ob_sp.dim_alarms,), + (ob_sp_cls.dim_alarms,), dt_int, ), "attention_budget": ( @@ -552,45 +553,45 @@ def __init__( ), # alert stuff "active_alert": ( - np.full(shape=(ob_sp.dim_alerts,), fill_value=False, dtype=dt_bool), - np.full(shape=(ob_sp.dim_alerts,), fill_value=True, dtype=dt_bool), - (ob_sp.dim_alerts,), + np.full(shape=(ob_sp_cls.dim_alerts,), fill_value=False, dtype=dt_bool), + np.full(shape=(ob_sp_cls.dim_alerts,), fill_value=True, dtype=dt_bool), + (ob_sp_cls.dim_alerts,), dt_bool, ), "time_since_last_alert": ( - np.full(shape=(ob_sp.dim_alerts,), fill_value=-1, dtype=dt_int), - np.full(shape=(ob_sp.dim_alerts,), fill_value=np.iinfo(dt_int).max, dtype=dt_int), - (ob_sp.dim_alerts,), + np.full(shape=(ob_sp_cls.dim_alerts,), fill_value=-1, dtype=dt_int), + np.full(shape=(ob_sp_cls.dim_alerts,), fill_value=np.iinfo(dt_int).max, dtype=dt_int), + (ob_sp_cls.dim_alerts,), dt_int, ), "alert_duration": ( - np.full(shape=(ob_sp.dim_alerts,), fill_value=-1, dtype=dt_int), - np.full(shape=(ob_sp.dim_alerts,), fill_value=np.iinfo(dt_int).max, dtype=dt_int), - (ob_sp.dim_alerts,), + np.full(shape=(ob_sp_cls.dim_alerts,), fill_value=-1, dtype=dt_int), + np.full(shape=(ob_sp_cls.dim_alerts,), fill_value=np.iinfo(dt_int).max, dtype=dt_int), + (ob_sp_cls.dim_alerts,), dt_int, ), "total_number_of_alert": ( - np.full(shape=(1 if ob_sp.dim_alerts else 0,), fill_value=-1, dtype=dt_int), - np.full(shape=(1 if ob_sp.dim_alerts else 0,), fill_value=np.iinfo(dt_int).max, dtype=dt_int), - (1 if ob_sp.dim_alerts else 0,), + np.full(shape=(1 if ob_sp_cls.dim_alerts else 0,), fill_value=-1, dtype=dt_int), + np.full(shape=(1 if ob_sp_cls.dim_alerts else 0,), fill_value=np.iinfo(dt_int).max, dtype=dt_int), + (1 if ob_sp_cls.dim_alerts else 0,), dt_int, ), "time_since_last_attack": ( - np.full(shape=(ob_sp.dim_alerts,), fill_value=-1, dtype=dt_int), - np.full(shape=(ob_sp.dim_alerts,), fill_value=np.iinfo(dt_int).max, dtype=dt_int), - (ob_sp.dim_alerts,), + np.full(shape=(ob_sp_cls.dim_alerts,), fill_value=-1, dtype=dt_int), + np.full(shape=(ob_sp_cls.dim_alerts,), fill_value=np.iinfo(dt_int).max, dtype=dt_int), + (ob_sp_cls.dim_alerts,), dt_int, ), "was_alert_used_after_attack": ( - np.full(shape=(ob_sp.dim_alerts,), fill_value=-1, dtype=dt_int), - np.full(shape=(ob_sp.dim_alerts,), fill_value=1, dtype=dt_int), - (ob_sp.dim_alerts,), + np.full(shape=(ob_sp_cls.dim_alerts,), fill_value=-1, dtype=dt_int), + np.full(shape=(ob_sp_cls.dim_alerts,), fill_value=1, dtype=dt_int), + (ob_sp_cls.dim_alerts,), dt_int, ), "attack_under_alert": ( - np.full(shape=(ob_sp.dim_alerts,), fill_value=-1, dtype=dt_int), - np.full(shape=(ob_sp.dim_alerts,), fill_value=1, dtype=dt_int), - (ob_sp.dim_alerts,), + np.full(shape=(ob_sp_cls.dim_alerts,), fill_value=-1, dtype=dt_int), + np.full(shape=(ob_sp_cls.dim_alerts,), fill_value=1, dtype=dt_int), + (ob_sp_cls.dim_alerts,), dt_int, ), } diff --git a/grid2op/tests/BaseBackendTest.py b/grid2op/tests/BaseBackendTest.py index b8f99b617..710db907e 100644 --- a/grid2op/tests/BaseBackendTest.py +++ b/grid2op/tests/BaseBackendTest.py @@ -350,7 +350,7 @@ def test_voltages_correct_load_gen(self): p_ex, q_ex, v_ex, a_ex = self.backend.lines_ex_info() for c_id, sub_id in enumerate(self.backend.load_to_subid): - l_ids = np.where(self.backend.line_or_to_subid == sub_id)[0] + l_ids = np.nonzero(self.backend.line_or_to_subid == sub_id)[0] if len(l_ids): l_id = l_ids[0] assert ( @@ -358,7 +358,7 @@ def test_voltages_correct_load_gen(self): ), "problem for load {}".format(c_id) continue - l_ids = np.where(self.backend.line_ex_to_subid == sub_id)[0] + l_ids = np.nonzero(self.backend.line_ex_to_subid == sub_id)[0] if len(l_ids): l_id = l_ids[0] assert ( @@ -368,7 +368,7 @@ def test_voltages_correct_load_gen(self): assert False, "load {} has not been checked".format(c_id) for g_id, sub_id in enumerate(self.backend.gen_to_subid): - l_ids = np.where(self.backend.line_or_to_subid == sub_id)[0] + l_ids = np.nonzero(self.backend.line_or_to_subid == sub_id)[0] if len(l_ids): l_id = l_ids[0] assert ( @@ -376,7 +376,7 @@ def test_voltages_correct_load_gen(self): ), "problem for generator {}".format(g_id) continue - l_ids = np.where(self.backend.line_ex_to_subid == sub_id)[0] + l_ids = np.nonzero(self.backend.line_ex_to_subid == sub_id)[0] if len(l_ids): l_id = l_ids[0] assert ( @@ -972,22 +972,22 @@ def test_topo_set1sub(self): assert np.max(topo_vect) == 2, "no buses have been changed" # check that the objects have been properly moved - load_ids = np.where(self.backend.load_to_subid == id_)[0] + load_ids = np.nonzero(self.backend.load_to_subid == id_)[0] assert np.all( topo_vect[self.backend.load_pos_topo_vect[load_ids]] == arr[self.backend.load_to_sub_pos[load_ids]] ) - lor_ids = np.where(self.backend.line_or_to_subid == id_)[0] + lor_ids = np.nonzero(self.backend.line_or_to_subid == id_)[0] assert np.all( topo_vect[self.backend.line_or_pos_topo_vect[lor_ids]] == arr[self.backend.line_or_to_sub_pos[lor_ids]] ) - lex_ids = np.where(self.backend.line_ex_to_subid == id_)[0] + lex_ids = np.nonzero(self.backend.line_ex_to_subid == id_)[0] assert np.all( topo_vect[self.backend.line_ex_pos_topo_vect[lex_ids]] == arr[self.backend.line_ex_to_sub_pos[lex_ids]] ) - gen_ids = np.where(self.backend.gen_to_subid == id_)[0] + gen_ids = np.nonzero(self.backend.gen_to_subid == id_)[0] assert np.all( topo_vect[self.backend.gen_pos_topo_vect[gen_ids]] == arr[self.backend.gen_to_sub_pos[gen_ids]] @@ -1071,22 +1071,22 @@ def test_topo_change1sub(self): assert np.max(topo_vect) == 2, "no buses have been changed" # check that the objects have been properly moved - load_ids = np.where(self.backend.load_to_subid == id_)[0] + load_ids = np.nonzero(self.backend.load_to_subid == id_)[0] assert np.all( topo_vect[self.backend.load_pos_topo_vect[load_ids]] == 1 + arr[self.backend.load_to_sub_pos[load_ids]] ) - lor_ids = np.where(self.backend.line_or_to_subid == id_)[0] + lor_ids = np.nonzero(self.backend.line_or_to_subid == id_)[0] assert np.all( topo_vect[self.backend.line_or_pos_topo_vect[lor_ids]] == 1 + arr[self.backend.line_or_to_sub_pos[lor_ids]] ) - lex_ids = np.where(self.backend.line_ex_to_subid == id_)[0] + lex_ids = np.nonzero(self.backend.line_ex_to_subid == id_)[0] assert np.all( topo_vect[self.backend.line_ex_pos_topo_vect[lex_ids]] == 1 + arr[self.backend.line_ex_to_sub_pos[lex_ids]] ) - gen_ids = np.where(self.backend.gen_to_subid == id_)[0] + gen_ids = np.nonzero(self.backend.gen_to_subid == id_)[0] assert np.all( topo_vect[self.backend.gen_pos_topo_vect[gen_ids]] == 1 + arr[self.backend.gen_to_sub_pos[gen_ids]] @@ -1146,22 +1146,22 @@ def test_topo_change_1sub_twice(self): assert np.max(topo_vect) == 2, "no buses have been changed" # check that the objects have been properly moved - load_ids = np.where(self.backend.load_to_subid == id_)[0] + load_ids = np.nonzero(self.backend.load_to_subid == id_)[0] assert np.all( topo_vect[self.backend.load_pos_topo_vect[load_ids]] == 1 + arr[self.backend.load_to_sub_pos[load_ids]] ) - lor_ids = np.where(self.backend.line_or_to_subid == id_)[0] + lor_ids = np.nonzero(self.backend.line_or_to_subid == id_)[0] assert np.all( topo_vect[self.backend.line_or_pos_topo_vect[lor_ids]] == 1 + arr[self.backend.line_or_to_sub_pos[lor_ids]] ) - lex_ids = np.where(self.backend.line_ex_to_subid == id_)[0] + lex_ids = np.nonzero(self.backend.line_ex_to_subid == id_)[0] assert np.all( topo_vect[self.backend.line_ex_pos_topo_vect[lex_ids]] == 1 + arr[self.backend.line_ex_to_sub_pos[lex_ids]] ) - gen_ids = np.where(self.backend.gen_to_subid == id_)[0] + gen_ids = np.nonzero(self.backend.gen_to_subid == id_)[0] assert np.all( topo_vect[self.backend.gen_pos_topo_vect[gen_ids]] == 1 + arr[self.backend.gen_to_sub_pos[gen_ids]] @@ -1236,44 +1236,44 @@ def test_topo_change_2sub(self): assert np.max(topo_vect) == 2, "no buses have been changed" # check that the objects have been properly moved - load_ids = np.where(self.backend.load_to_subid == id_1)[0] + load_ids = np.nonzero(self.backend.load_to_subid == id_1)[0] assert np.all( topo_vect[self.backend.load_pos_topo_vect[load_ids]] == 1 + arr1[self.backend.load_to_sub_pos[load_ids]] ) - lor_ids = np.where(self.backend.line_or_to_subid == id_1)[0] + lor_ids = np.nonzero(self.backend.line_or_to_subid == id_1)[0] assert np.all( topo_vect[self.backend.line_or_pos_topo_vect[lor_ids]] == 1 + arr1[self.backend.line_or_to_sub_pos[lor_ids]] ) - lex_ids = np.where(self.backend.line_ex_to_subid == id_1)[0] + lex_ids = np.nonzero(self.backend.line_ex_to_subid == id_1)[0] assert np.all( topo_vect[self.backend.line_ex_pos_topo_vect[lex_ids]] == 1 + arr1[self.backend.line_ex_to_sub_pos[lex_ids]] ) - gen_ids = np.where(self.backend.gen_to_subid == id_1)[0] + gen_ids = np.nonzero(self.backend.gen_to_subid == id_1)[0] assert np.all( topo_vect[self.backend.gen_pos_topo_vect[gen_ids]] == 1 + arr1[self.backend.gen_to_sub_pos[gen_ids]] ) - load_ids = np.where(self.backend.load_to_subid == id_2)[0] + load_ids = np.nonzero(self.backend.load_to_subid == id_2)[0] # TODO check the topology symmetry assert np.all( topo_vect[self.backend.load_pos_topo_vect[load_ids]] == arr2[self.backend.load_to_sub_pos[load_ids]] ) - lor_ids = np.where(self.backend.line_or_to_subid == id_2)[0] + lor_ids = np.nonzero(self.backend.line_or_to_subid == id_2)[0] assert np.all( topo_vect[self.backend.line_or_pos_topo_vect[lor_ids]] == arr2[self.backend.line_or_to_sub_pos[lor_ids]] ) - lex_ids = np.where(self.backend.line_ex_to_subid == id_2)[0] + lex_ids = np.nonzero(self.backend.line_ex_to_subid == id_2)[0] assert np.all( topo_vect[self.backend.line_ex_pos_topo_vect[lex_ids]] == arr2[self.backend.line_ex_to_sub_pos[lex_ids]] ) - gen_ids = np.where(self.backend.gen_to_subid == id_2)[0] + gen_ids = np.nonzero(self.backend.gen_to_subid == id_2)[0] assert np.all( topo_vect[self.backend.gen_pos_topo_vect[gen_ids]] == arr2[self.backend.gen_to_sub_pos[gen_ids]] diff --git a/grid2op/tests/aaa_test_backend_interface.py b/grid2op/tests/aaa_test_backend_interface.py index 9abf19761..5bb4bc3ce 100644 --- a/grid2op/tests/aaa_test_backend_interface.py +++ b/grid2op/tests/aaa_test_backend_interface.py @@ -1381,9 +1381,9 @@ def test_27_topo_vect_disconnect(self): def _aux_aux_get_line(self, el_id, el_to_subid, line_xx_to_subid): sub_id = el_to_subid[el_id] if (line_xx_to_subid == sub_id).sum() >= 2: - return True, np.where(line_xx_to_subid == sub_id)[0][0] + return True, np.nonzero(line_xx_to_subid == sub_id)[0][0] elif (line_xx_to_subid == sub_id).sum() == 1: - return False, np.where(line_xx_to_subid == sub_id)[0][0] + return False, np.nonzero(line_xx_to_subid == sub_id)[0][0] else: return None diff --git a/grid2op/tests/test_Action.py b/grid2op/tests/test_Action.py index 918cc47a0..b45a810a9 100644 --- a/grid2op/tests/test_Action.py +++ b/grid2op/tests/test_Action.py @@ -874,7 +874,7 @@ def test_to_vect(self): tmp[-action.n_gen :] = -1 # compute the "set_bus" vect - id_set = np.where(np.array(type(action).attr_list_vect) == "_set_topo_vect")[0][0] + id_set = np.nonzero(np.array(type(action).attr_list_vect) == "_set_topo_vect")[0][0] size_before = 0 for el in type(action).attr_list_vect[:id_set]: arr_ = action._get_array_from_attr_name(el) @@ -941,7 +941,7 @@ def test_to_vect(self): 0, ] ) - id_change = np.where(np.array(type(action).attr_list_vect) == "_change_bus_vect")[0][ + id_change = np.nonzero(np.array(type(action).attr_list_vect) == "_change_bus_vect")[0][ 0 ] size_before = 0 From 575772ca0df9111ce33c38deea29b88386eabcff Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 8 Feb 2024 18:06:08 +0100 Subject: [PATCH 22/24] still trying to fix some issues by sonarcloud --- CHANGELOG.rst | 2 + grid2op/Action/baseAction.py | 412 ++++++++++++--------- grid2op/Action/serializableActionSpace.py | 113 +++--- grid2op/Opponent/weightedRandomOpponent.py | 2 +- grid2op/Space/GridObjects.py | 40 ++ grid2op/tests/test_n_busbar_per_sub.py | 7 + 6 files changed, 341 insertions(+), 235 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4b36f2241..0976b5e54 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -50,6 +50,8 @@ Change Log - [FIXED] `MultiDiscreteActSpace` and `DiscreteActSpace` could be the same classes on some cases (typo in the code). - [ADDED] a method `gridobj.topo_vect_element()` that does the opposite of `gridobj.xxx_pos_topo_vect` +- [ADDED] a mthod `gridobj.get_powerline_id(sub_id)` that gives the + id of all powerlines connected to a given substation - [IMPROVED] handling of "compatibility" grid2op version (by calling the relevant things done in the base class in `BaseAction` and `BaseObservation`) and by using the `from packaging import version` diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index dc8ddf47e..2eecea462 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -817,7 +817,7 @@ def process_grid2op_compat(cls): if cls.glop_version == cls.BEFORE_COMPAT_VERSION: # oldest version: no storage and no curtailment available - cls._aux_process_old_compat + cls._aux_process_old_compat() if glop_ver < version.parse("1.6.0"): # this feature did not exist before. @@ -1521,42 +1521,7 @@ def _assign_iadd_or_warn(self, attr_name, new_value): else: getattr(self, attr_name)[:] = new_value - def __iadd__(self, other: Self): - """ - Add an action to this one. - - Adding an action to myself is equivalent to perform myself, and then perform other (but at the - same step) - - Parameters - ---------- - other: :class:`BaseAction` - - Examples - -------- - - .. code-block:: python - - import grid2op - env_name = "l2rpn_case14_sandbox" # or any other name - env = grid2op.make(env_name) - - act1 = env.action_space() - act1.set_bus = ... # for example - print("before += :") - print(act1) - - act2 = env.action_space() - act2.redispatch = ... # for example - print(act2) - - act1 += act 2 - print("after += ") - print(act1) - - """ - - # deal with injections + def _aux_iadd_inj(self, other): for el in self.attr_list_vect: if el in other._dict_inj: if el not in self._dict_inj: @@ -1565,14 +1530,14 @@ def __iadd__(self, other: Self): val = other._dict_inj[el] ok_ind = np.isfinite(val) self._dict_inj[el][ok_ind] = val[ok_ind] - # warning if the action cannot be added for el in other._dict_inj: if not el in self.attr_list_set: warnings.warn( type(self).ERR_ACTION_CUT.format(el) ) - # redispatching + + def _aux_iadd_redisp(self, other): redispatching = other._redispatch if (redispatching != 0.0).any(): if "_redispatch" not in self.attr_list_set: @@ -1582,19 +1547,8 @@ def __iadd__(self, other: Self): else: ok_ind = np.isfinite(redispatching) self._redispatch[ok_ind] += redispatching[ok_ind] - - # storage - set_storage = other._storage_power - ok_ind = np.isfinite(set_storage) & (set_storage != 0.0).any() - if ok_ind.any(): - if "_storage_power" not in self.attr_list_set: - warnings.warn( - type(self).ERR_ACTION_CUT.format("_storage_power") - ) - else: - self._storage_power[ok_ind] += set_storage[ok_ind] - - # curtailment + + def _aux_iadd_curtail(self, other): curtailment = other._curtail ok_ind = np.isfinite(curtailment) & (curtailment != -1.0) if ok_ind.any(): @@ -1607,8 +1561,57 @@ def __iadd__(self, other: Self): # the curtailment of rhs, only when rhs acts # on curtailment self._curtail[ok_ind] = curtailment[ok_ind] - - # set and change status + + def _aux_iadd_storage(self, other): + set_storage = other._storage_power + ok_ind = np.isfinite(set_storage) & (set_storage != 0.0).any() + if ok_ind.any(): + if "_storage_power" not in self.attr_list_set: + warnings.warn( + type(self).ERR_ACTION_CUT.format("_storage_power") + ) + else: + self._storage_power[ok_ind] += set_storage[ok_ind] + + def _aux_iadd_modif_flags(self, other): + self._modif_change_bus = self._modif_change_bus or other._modif_change_bus + self._modif_set_bus = self._modif_set_bus or other._modif_set_bus + self._modif_change_status = ( + self._modif_change_status or other._modif_change_status + ) + self._modif_set_status = self._modif_set_status or other._modif_set_status + self._modif_inj = self._modif_inj or other._modif_inj + self._modif_redispatch = self._modif_redispatch or other._modif_redispatch + self._modif_storage = self._modif_storage or other._modif_storage + 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 + + def _aux_iadd_shunt(self, other): + if not type(other).shunts_data_available: + warnings.warn("Trying to add an action that does not support " + "shunt with an action that does.") + return + + val = other.shunt_p + ok_ind = np.isfinite(val) + shunt_p = 1.0 * self.shunt_p + shunt_p[ok_ind] = val[ok_ind] + self._assign_iadd_or_warn("shunt_p", shunt_p) + + val = other.shunt_q + ok_ind = np.isfinite(val) + shunt_q = 1.0 * self.shunt_q + shunt_q[ok_ind] = val[ok_ind] + self._assign_iadd_or_warn("shunt_q", shunt_q) + + val = other.shunt_bus + ok_ind = val != 0 + shunt_bus = 1 * self.shunt_bus + shunt_bus[ok_ind] = val[ok_ind] + self._assign_iadd_or_warn("shunt_bus", shunt_bus) + + def _aux_iadd_set_change_status(self, other): other_set = other._set_line_status other_change = other._switch_line_status me_set = 1 * self._set_line_status @@ -1637,8 +1640,8 @@ def __iadd__(self, other: Self): self._assign_iadd_or_warn("_set_line_status", me_set) self._assign_iadd_or_warn("_switch_line_status", me_change) - - # set and change bus + + def _aux_iadd_set_change_bus(self, other): other_set = other._set_topo_vect other_change = other._change_bus_vect me_set = 1 * self._set_topo_vect @@ -1669,26 +1672,63 @@ def __iadd__(self, other: Self): self._assign_iadd_or_warn("_set_topo_vect", me_set) self._assign_iadd_or_warn("_change_bus_vect", me_change) + + def __iadd__(self, other: Self): + """ + Add an action to this one. + + Adding an action to myself is equivalent to perform myself, and then perform other (but at the + same step) + + Parameters + ---------- + other: :class:`BaseAction` + + Examples + -------- + + .. code-block:: python + + import grid2op + env_name = "l2rpn_case14_sandbox" # or any other name + env = grid2op.make(env_name) + + act1 = env.action_space() + act1.set_bus = ... # for example + print("before += :") + print(act1) + + act2 = env.action_space() + act2.redispatch = ... # for example + print(act2) + + act1 += act 2 + print("after += ") + print(act1) + + """ + + # deal with injections + self._aux_iadd_inj(other) + + # redispatching + self._aux_iadd_redisp(other) + + # storage + self._aux_iadd_storage(other) + + # curtailment + self._aux_iadd_curtail(other) + + # set and change status + self._aux_iadd_set_change_status(other) + + # set and change bus + self._aux_iadd_set_change_bus(other) # shunts if type(self).shunts_data_available: - val = other.shunt_p - ok_ind = np.isfinite(val) - shunt_p = 1.0 * self.shunt_p - shunt_p[ok_ind] = val[ok_ind] - self._assign_iadd_or_warn("shunt_p", shunt_p) - - val = other.shunt_q - ok_ind = np.isfinite(val) - shunt_q = 1.0 * self.shunt_q - shunt_q[ok_ind] = val[ok_ind] - self._assign_iadd_or_warn("shunt_q", shunt_q) - - val = other.shunt_bus - ok_ind = val != 0 - shunt_bus = 1 * self.shunt_bus - shunt_bus[ok_ind] = val[ok_ind] - self._assign_iadd_or_warn("shunt_bus", shunt_bus) + self._aux_iadd_shunt(other) # alarm feature self._raise_alarm[other._raise_alarm] = True @@ -1698,19 +1738,7 @@ def __iadd__(self, other: Self): # the modif flags - self._modif_change_bus = self._modif_change_bus or other._modif_change_bus - self._modif_set_bus = self._modif_set_bus or other._modif_set_bus - self._modif_change_status = ( - self._modif_change_status or other._modif_change_status - ) - self._modif_set_status = self._modif_set_status or other._modif_set_status - self._modif_inj = self._modif_inj or other._modif_inj - self._modif_redispatch = self._modif_redispatch or other._modif_redispatch - self._modif_storage = self._modif_storage or other._modif_storage - 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._aux_iadd_modif_flags(other) return self def __add__(self, other) -> "BaseAction": @@ -2874,49 +2902,55 @@ def _ignore_topo_action_if_disconnection(self, sel_): self._set_topo_vect[np.array(self.line_ex_pos_topo_vect[sel_])] = 0 self._change_bus_vect[np.array(self.line_ex_pos_topo_vect[sel_])] = False - def _obj_caract_from_topo_id(self, id_, with_name=False): - obj_id = None - objt_type = None - array_subid = None - cls = type(self) - for l_id, id_in_topo in enumerate(cls.load_pos_topo_vect): + def _aux_obj_caract(self, id_, with_name, xxx_pos_topo_vect, objt_type, xxx_subid, name_xxx): + for l_id, id_in_topo in enumerate(xxx_pos_topo_vect): if id_in_topo == id_: obj_id = l_id - objt_type = "load" - array_subid = cls.load_to_subid - obj_name = cls.name_load[l_id] - if obj_id is None: - for l_id, id_in_topo in enumerate(cls.gen_pos_topo_vect): - if id_in_topo == id_: - obj_id = l_id - objt_type = "generator" - array_subid = cls.gen_to_subid - obj_name = cls.name_gen[l_id] - if obj_id is None: - for l_id, id_in_topo in enumerate(cls.line_or_pos_topo_vect): - if id_in_topo == id_: - obj_id = l_id - objt_type = self._line_or_str - array_subid = cls.line_or_to_subid - obj_name = cls.name_line[l_id] - if obj_id is None: - for l_id, id_in_topo in enumerate(cls.line_ex_pos_topo_vect): - if id_in_topo == id_: - obj_id = l_id - objt_type = self._line_ex_str - array_subid = cls.line_ex_to_subid - obj_name = cls.name_line[l_id] - if obj_id is None: - for l_id, id_in_topo in enumerate(cls.storage_pos_topo_vect): - if id_in_topo == id_: - obj_id = l_id - objt_type = "storage" - array_subid = cls.storage_to_subid - obj_name = cls.name_storage[l_id] - substation_id = array_subid[obj_id] - if not with_name: - return obj_id, objt_type, substation_id - return obj_id, objt_type, substation_id, obj_name + obj_name = name_xxx[l_id] + substation_id = xxx_subid[obj_id] + if not with_name: + return obj_id, objt_type, substation_id + return obj_id, objt_type, substation_id, obj_name + return None + + def _aux_obj_caract_from_topo_id_load(self, cls, id_, with_name): + return self._aux_obj_caract(id_, with_name, cls.load_pos_topo_vect, "load", cls.load_to_subid, cls.name_load) + + def _aux_obj_caract_from_topo_id_gen(self, cls, id_, with_name): + return self._aux_obj_caract(id_, with_name, cls.gen_pos_topo_vect, + "generator", cls.gen_to_subid, cls.name_gen) + + def _aux_obj_caract_from_topo_id_lor(self, cls, id_, with_name): + return self._aux_obj_caract(id_, with_name, cls.line_or_pos_topo_vect, + self._line_or_str, cls.line_or_to_subid, cls.name_line) + + def _aux_obj_caract_from_topo_id_lex(self, cls, id_, with_name): + return self._aux_obj_caract(id_, with_name, cls.line_ex_pos_topo_vect, + self._line_ex_str, cls.line_ex_to_subid, cls.name_line) + + def _aux_obj_caract_from_topo_storage(self, cls, id_, with_name): + return self._aux_obj_caract(id_, with_name, cls.storage_pos_topo_vect, + "storage", cls.storage_to_subid, cls.name_storage) + + def _obj_caract_from_topo_id(self, id_, with_name=False): + # TODO refactor this with gridobj.topo_vect_element + cls = type(self) + tmp = self._aux_obj_caract_from_topo_id_load(cls, id_, with_name) + if tmp is not None: + return tmp + tmp = self._aux_obj_caract_from_topo_id_gen(cls, id_, with_name) + if tmp is not None: + return tmp + tmp = self._aux_obj_caract_from_topo_id_lor(cls, id_, with_name) + if tmp is not None: + return tmp + tmp = self._aux_obj_caract_from_topo_id_lex(cls, id_, with_name) + if tmp is not None: + return tmp + tmp = self._aux_obj_caract_from_topo_storage(cls, id_, with_name) + if tmp is not None: + return tmp + raise Grid2OpException(f"Unknown element in topovect with id {id_}") def __str__(self) -> str: """ @@ -3267,6 +3301,69 @@ def impact_on_objects(self) -> dict: "curtailment": curtailment, } + def _aux_as_dict_set_line(self, res): + res["set_line_status"] = {} + res["set_line_status"]["nb_connected"] = (self._set_line_status == 1).sum() + res["set_line_status"]["nb_disconnected"] = ( + self._set_line_status == -1 + ).sum() + res["set_line_status"]["connected_id"] = np.nonzero( + self._set_line_status == 1 + )[0] + res["set_line_status"]["disconnected_id"] = np.nonzero( + self._set_line_status == -1 + )[0] + + def _aux_as_dict_change_line(self, res): + res["change_line_status"] = {} + res["change_line_status"]["nb_changed"] = self._switch_line_status.sum() + res["change_line_status"]["changed_id"] = np.nonzero( + self._switch_line_status + )[0] + + def _aux_as_dict_change_bus(self, res): + res["change_bus_vect"] = {} + res["change_bus_vect"]["nb_modif_objects"] = self._change_bus_vect.sum() + all_subs = set() + for id_, k in enumerate(self._change_bus_vect): + if k: + obj_id, objt_type, substation_id, nm_ = self._obj_caract_from_topo_id( + id_, with_name=True + ) + sub_id = "{}".format(substation_id) + if not sub_id in res["change_bus_vect"]: + res["change_bus_vect"][sub_id] = {} + res["change_bus_vect"][sub_id][nm_] = { + "type": objt_type, + "id": obj_id, + } + all_subs.add(sub_id) + + res["change_bus_vect"]["nb_modif_subs"] = len(all_subs) + res["change_bus_vect"]["modif_subs_id"] = sorted(all_subs) + + def _aux_as_dict_set_bus(self, res): + res["set_bus_vect"] = {} + res["set_bus_vect"]["nb_modif_objects"] = (self._set_topo_vect != 0).sum() + all_subs = set() + for id_, k in enumerate(self._set_topo_vect): + if k != 0: + obj_id, objt_type, substation_id, nm_ = self._obj_caract_from_topo_id( + id_, with_name=True + ) + sub_id = "{}".format(substation_id) + if not sub_id in res["set_bus_vect"]: + res["set_bus_vect"][sub_id] = {} + res["set_bus_vect"][sub_id][nm_] = { + "type": objt_type, + "id": obj_id, + "new_bus": k, + } + all_subs.add(sub_id) + + res["set_bus_vect"]["nb_modif_subs"] = len(all_subs) + res["set_bus_vect"]["modif_subs_id"] = sorted(all_subs) + def as_dict(self) -> Dict[Literal["load_p", "load_q", "prod_p", "prod_v", "change_line_status", "set_line_status", "change_bus_vect", "set_bus_vect", @@ -3343,70 +3440,19 @@ def as_dict(self) -> Dict[Literal["load_p", "load_q", "prod_p", "prod_v", # handles actions on force line status if (self._set_line_status != 0).any(): - res["set_line_status"] = {} - res["set_line_status"]["nb_connected"] = (self._set_line_status == 1).sum() - res["set_line_status"]["nb_disconnected"] = ( - self._set_line_status == -1 - ).sum() - res["set_line_status"]["connected_id"] = np.nonzero( - self._set_line_status == 1 - )[0] - res["set_line_status"]["disconnected_id"] = np.nonzero( - self._set_line_status == -1 - )[0] + self._aux_as_dict_set_line(res) # handles action on swtich line status if self._switch_line_status.sum(): - res["change_line_status"] = {} - res["change_line_status"]["nb_changed"] = self._switch_line_status.sum() - res["change_line_status"]["changed_id"] = np.nonzero( - self._switch_line_status - )[0] + self._aux_as_dict_change_line(res) # handles topology change if (self._change_bus_vect).any(): - res["change_bus_vect"] = {} - res["change_bus_vect"]["nb_modif_objects"] = self._change_bus_vect.sum() - all_subs = set() - for id_, k in enumerate(self._change_bus_vect): - if k: - obj_id, objt_type, substation_id, nm_ = self._obj_caract_from_topo_id( - id_, with_name=True - ) - sub_id = "{}".format(substation_id) - if not sub_id in res["change_bus_vect"]: - res["change_bus_vect"][sub_id] = {} - res["change_bus_vect"][sub_id][nm_] = { - "type": objt_type, - "id": obj_id, - } - all_subs.add(sub_id) - - res["change_bus_vect"]["nb_modif_subs"] = len(all_subs) - res["change_bus_vect"]["modif_subs_id"] = sorted(all_subs) + self._aux_as_dict_change_bus(res) # handles topology set if (self._set_topo_vect!= 0).any(): - res["set_bus_vect"] = {} - res["set_bus_vect"]["nb_modif_objects"] = (self._set_topo_vect != 0).sum() - all_subs = set() - for id_, k in enumerate(self._set_topo_vect): - if k != 0: - obj_id, objt_type, substation_id, nm_ = self._obj_caract_from_topo_id( - id_, with_name=True - ) - sub_id = "{}".format(substation_id) - if not sub_id in res["set_bus_vect"]: - res["set_bus_vect"][sub_id] = {} - res["set_bus_vect"][sub_id][nm_] = { - "type": objt_type, - "id": obj_id, - "new_bus": k, - } - all_subs.add(sub_id) - - res["set_bus_vect"]["nb_modif_subs"] = len(all_subs) - res["set_bus_vect"]["modif_subs_id"] = sorted(all_subs) + self._aux_as_dict_set_bus(res) if self._hazards.any(): res["hazards"] = np.nonzero(self._hazards)[0] @@ -3416,7 +3462,7 @@ def as_dict(self) -> Dict[Literal["load_p", "load_q", "prod_p", "prod_v", res["maintenance"] = np.nonzero(self._maintenance)[0] res["nb_maintenance"] = self._maintenance.sum() - if (self._redispatch != 0.0).any(): + if (np.abs(self._redispatch) >= 1e-7).any(): res["redispatch"] = 1.0 * self._redispatch if self._modif_storage: diff --git a/grid2op/Action/serializableActionSpace.py b/grid2op/Action/serializableActionSpace.py index aefd9a847..ed22290da 100644 --- a/grid2op/Action/serializableActionSpace.py +++ b/grid2op/Action/serializableActionSpace.py @@ -176,11 +176,7 @@ def supports_type(self, action_type): f"You provided {action_type} which is not supported." ) cls = type(self) - if action_type == "storage_power": - return (cls.n_storage > 0) and ( - "set_storage" in self.actionClass.authorized_keys - ) - elif action_type == "set_storage": + if action_type == "storage_power" or action_type == "set_storage": return (cls.n_storage > 0) and ( "set_storage" in self.actionClass.authorized_keys ) @@ -1086,7 +1082,6 @@ def _is_ok_symmetry(cls, n_busbar_per_sub: int, tup: np.ndarray, bus_start: int= # which is alreay added somewhere else. The current topologie # is not valid. return False - return True @classmethod def _is_ok_line(cls, n_busbar_per_sub: int, tup: np.ndarray, lines_id: np.ndarray) -> bool: @@ -1099,14 +1094,16 @@ def _is_ok_line(cls, n_busbar_per_sub: int, tup: np.ndarray, lines_id: np.ndarra # So to make sure that every buses has at least a line connected to it # then I just check the number of unique buses (tup.max()) # and compare it to the number of buses where there are - # at least a line len(buses_with_lines) + # at least a line len(buses_with_lines) + + # NB the alternative implementation is slower + # >>> buses_with_lines = np.unique(tup[lines_id]) + # >>> return buses_with_lines.size == tup.max() nb = 0 only_line = tup[lines_id] for el in range(1, n_busbar_per_sub +1): nb += (only_line == el).any() return nb == tup.max() - # buses_with_lines = np.unique(tup[lines_id]) # slower than python code above - # return buses_with_lines.size == tup.max() @classmethod def _is_ok_2(cls, n_busbar_per_sub : int, tup) -> bool: @@ -1120,13 +1117,47 @@ def _is_ok_2(cls, n_busbar_per_sub : int, tup) -> bool: # then I just check the number of unique buses (tup.max()) # and compare it to the number of buses where there are # at least a line len(buses_with_lines) + + + # NB the alternative implementation is slower + # >>> un_, count = np.unique(tup, return_counts=True) + # >>> return (count >= 2).all() for el in range(1, tup.max() + 1): if (tup == el).sum() < 2: return False return True - # un_, count = np.unique(tup, return_counts=True) # slower than python code above - # return (count >= 2).all() + @staticmethod + def _aux_get_all_unitary_topologies_set_comp_topo(S, num_el, action_space, + cls, powerlines_id, add_alone_line, + _count_only, sub_id_): + if not _count_only: + tmp = [] + else: + tmp = 0 + + for tup in itertools.product(S, repeat=num_el - 1): + tup = np.array((1, *tup)) # force first el on bus 1 to break symmetry + + if not action_space._is_ok_symmetry(cls.n_busbar_per_sub, tup): + # already added (by symmetry) + continue + if not action_space._is_ok_line(cls.n_busbar_per_sub, tup, powerlines_id): + # check there is at least one line per busbars + continue + if not add_alone_line and not action_space._is_ok_2(cls.n_busbar_per_sub, tup): + # check there are at least 2 elements per buses + continue + + if not _count_only: + action = action_space( + {"set_bus": {"substations_id": [(sub_id_, tup)]}} + ) + tmp.append(action) + else: + tmp += 1 + return tmp + @staticmethod def get_all_unitary_topologies_set(action_space: Self, sub_id: int=None, @@ -1224,45 +1255,14 @@ def get_all_unitary_topologies_set(action_space: Self, res = [] S = list(range(1, cls.n_busbar_per_sub + 1)) - for sub_id_, num_el in enumerate(cls.sub_info): - if not _count_only: - tmp = [] - else: - tmp = 0 - - if sub_id is not None: - if sub_id_ != sub_id: - continue - - powerlines_or_id = cls.line_or_to_sub_pos[ - cls.line_or_to_subid == sub_id_ - ] - powerlines_ex_id = cls.line_ex_to_sub_pos[ - cls.line_ex_to_subid == sub_id_ - ] - powerlines_id = np.concatenate((powerlines_or_id, powerlines_ex_id)) - + if sub_id is not None: + num_el = cls.sub_info[sub_id] + powerlines_id = cls.get_powerline_id(sub_id) + # computes all the topologies at 2 buses for this substation - for tup in itertools.product(S, repeat=num_el - 1): - tup = np.array((1, *tup)) # force first el on bus 1 to break symmetry - - if not action_space._is_ok_symmetry(cls.n_busbar_per_sub, tup): - # already added (by symmetry) - continue - if not action_space._is_ok_line(cls.n_busbar_per_sub, tup, powerlines_id): - # check there is at least one line per busbars - continue - if not add_alone_line and not action_space._is_ok_2(cls.n_busbar_per_sub, tup): - # check there are at least 2 elements per buses - continue - - if not _count_only: - action = action_space( - {"set_bus": {"substations_id": [(sub_id_, tup)]}} - ) - tmp.append(action) - else: - tmp += 1 + tmp = action_space._aux_get_all_unitary_topologies_set_comp_topo(S, num_el, action_space, + cls, powerlines_id, add_alone_line, + _count_only, sub_id) if not _count_only and len(tmp) >= 2: # if i have only one single topology on this substation, it doesn't make any action @@ -1270,10 +1270,21 @@ def get_all_unitary_topologies_set(action_space: Self, res += tmp elif _count_only: if tmp >= 2: - res.append(tmp) + res = tmp else: # no real way to change if there is only one valid topology - res.append(0) + res = 0 + return res + + for sub_id in range(cls.n_sub): + this = cls.get_all_unitary_topologies_set(action_space, + sub_id, + add_alone_line, + _count_only) + if not _count_only: + res += this + else: + res.append(this) return res @staticmethod diff --git a/grid2op/Opponent/weightedRandomOpponent.py b/grid2op/Opponent/weightedRandomOpponent.py index d058f913f..c1298e1e1 100644 --- a/grid2op/Opponent/weightedRandomOpponent.py +++ b/grid2op/Opponent/weightedRandomOpponent.py @@ -73,7 +73,7 @@ def init( # Store attackable lines IDs self._lines_ids = [] for l_name in lines_attacked: - l_id = np.whnonzeroere(self.action_space.name_line == l_name) + l_id = np.nonzero(self.action_space.name_line == l_name) if len(l_id) and len(l_id[0]): self._lines_ids.append(l_id[0][0]) else: diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 495d0de12..57f5b137d 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -2893,6 +2893,46 @@ def get_obj_connect_to(cls, _sentinel=None, substation_id=None): } return res + @classmethod + def get_powerline_id(cls, sub_id: int) -> np.ndarray: + """ + Return the id of all powerlines connected to the substation `sub_id` + either "or" side or "ex" side + + Parameters + ----------- + sub_id: `int` + The id of the substation concerned + + Returns + ------- + res: np.ndarray, int + The id of all powerlines connected to this substation (either or side or ex side) + + Examples + -------- + + To get the id of all powerlines connected to substation with id 1, + you can do: + + .. code-block:: python + + import numpy as np + import grid2op + env = grid2op.make("l2rpn_case14_sandbox") + + all_lines_conn_to_sub_id_1 = type(env).get_powerline_id(1) + + """ + powerlines_or_id = cls.line_or_to_sub_pos[ + cls.line_or_to_subid == sub_id + ] + powerlines_ex_id = cls.line_ex_to_sub_pos[ + cls.line_ex_to_subid == sub_id + ] + powerlines_id = np.concatenate((powerlines_or_id, powerlines_ex_id)) + return powerlines_id + @classmethod def get_obj_substations(cls, _sentinel=None, substation_id=None): """ diff --git a/grid2op/tests/test_n_busbar_per_sub.py b/grid2op/tests/test_n_busbar_per_sub.py index 5d1e794f4..f05bb4c33 100644 --- a/grid2op/tests/test_n_busbar_per_sub.py +++ b/grid2op/tests/test_n_busbar_per_sub.py @@ -1627,6 +1627,13 @@ class TestGym(unittest.TestCase): pass +class TestRules(unittest.TestCase): + """test the rules for the reco / deco of line works also when >= 3 busbars, + also ttests the act.get_impact()... + """ + pass + + if __name__ == "__main__": unittest.main() \ No newline at end of file From cc5314c7c5a071a62df02407f7092a13bc2e1df6 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 9 Feb 2024 11:51:14 +0100 Subject: [PATCH 23/24] fixing some bugs after refacto + refacto --- grid2op/Action/baseAction.py | 23 +- grid2op/Action/serializableActionSpace.py | 13 +- grid2op/Backend/pandaPowerBackend.py | 5 +- grid2op/Observation/baseObservation.py | 606 ++++++++++++---------- 4 files changed, 345 insertions(+), 302 deletions(-) diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 2eecea462..11ec65282 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -3363,6 +3363,17 @@ def _aux_as_dict_set_bus(self, res): res["set_bus_vect"]["nb_modif_subs"] = len(all_subs) res["set_bus_vect"]["modif_subs_id"] = sorted(all_subs) + + def _aux_as_dict_shunt(self, res): + tmp = {} + if np.any(np.isfinite(self.shunt_p)): + tmp["shunt_p"] = 1.0 * self.shunt_p + if np.any(np.isfinite(self.shunt_q)): + tmp["shunt_q"] = 1.0 * self.shunt_q + if np.any(self.shunt_bus != 0): + tmp["shunt_bus"] = 1.0 * self.shunt_bus + if tmp: + res["shunt"] = tmp def as_dict(self) -> Dict[Literal["load_p", "load_q", "prod_p", "prod_v", "change_line_status", "set_line_status", @@ -3472,15 +3483,7 @@ def as_dict(self) -> Dict[Literal["load_p", "load_q", "prod_p", "prod_v", res["curtailment"] = 1.0 * self._curtail if type(self).shunts_data_available: - tmp = {} - if np.any(np.isfinite(self.shunt_p)): - tmp["shunt_p"] = 1.0 * self.shunt_p - if np.any(np.isfinite(self.shunt_q)): - tmp["shunt_q"] = 1.0 * self.shunt_q - if np.any(self.shunt_bus != 0): - tmp["shunt_bus"] = 1.0 * self.shunt_bus - if tmp: - res["shunt"] = tmp + self._aux_as_dict_shunt(res) return res def get_types(self) -> Tuple[bool, bool, bool, bool, bool, bool, bool]: @@ -6281,7 +6284,7 @@ def _aux_decompose_as_unary_actions_redisp(self, cls, group_redispatch, res): tmp._redispatch = 1. * self._redispatch res["redispatch"] = [tmp] else: - gen_changed = np.whernonzeroe(np.abs(self._redispatch) >= 1e-7)[0] + gen_changed = np.nonzero(np.abs(self._redispatch) >= 1e-7)[0] res["redispatch"] = [] for g_id in gen_changed: tmp = cls() diff --git a/grid2op/Action/serializableActionSpace.py b/grid2op/Action/serializableActionSpace.py index ed22290da..04a3a1720 100644 --- a/grid2op/Action/serializableActionSpace.py +++ b/grid2op/Action/serializableActionSpace.py @@ -1128,7 +1128,7 @@ def _is_ok_2(cls, n_busbar_per_sub : int, tup) -> bool: return True @staticmethod - def _aux_get_all_unitary_topologies_set_comp_topo(S, num_el, action_space, + def _aux_get_all_unitary_topologies_set_comp_topo(busbar_set, num_el, action_space, cls, powerlines_id, add_alone_line, _count_only, sub_id_): if not _count_only: @@ -1136,7 +1136,7 @@ def _aux_get_all_unitary_topologies_set_comp_topo(S, num_el, action_space, else: tmp = 0 - for tup in itertools.product(S, repeat=num_el - 1): + for tup in itertools.product(busbar_set, repeat=num_el - 1): tup = np.array((1, *tup)) # force first el on bus 1 to break symmetry if not action_space._is_ok_symmetry(cls.n_busbar_per_sub, tup): @@ -1274,17 +1274,16 @@ def get_all_unitary_topologies_set(action_space: Self, else: # no real way to change if there is only one valid topology res = 0 - return res + if not _count_only: + return res + return [res] # need to be a list still for sub_id in range(cls.n_sub): this = cls.get_all_unitary_topologies_set(action_space, sub_id, add_alone_line, _count_only) - if not _count_only: - res += this - else: - res.append(this) + res += this return res @staticmethod diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index 904a54f93..1ba80b16c 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -557,7 +557,7 @@ def load_grid(self, # "hack" to handle topological changes, for now only 2 buses per substation add_topo = copy.deepcopy(self._grid.bus) # TODO n_busbar: what if non contiguous indexing ??? - for busbar_supp in range(self.n_busbar_per_sub - 1): # self.n_busbar_per_sub and not type(self) here otherwise it erases can_handle_more_than_2_busbar / cannot_handle_more_than_2_busbar + for _ in range(self.n_busbar_per_sub - 1): # self.n_busbar_per_sub and not type(self) here otherwise it erases can_handle_more_than_2_busbar / cannot_handle_more_than_2_busbar add_topo.index += add_topo.shape[0] add_topo["in_service"] = False for ind, el in add_topo.iterrows(): @@ -818,8 +818,6 @@ def apply_action(self, backendAction: Union["grid2op.Action._backendAction._Back """ if backendAction is None: return - from grid2op.Action._backendAction import _BackendAction - backendAction : _BackendAction = backendAction cls = type(self) @@ -873,7 +871,6 @@ def apply_action(self, backendAction: Union["grid2op.Action._backendAction._Back deactivated = new_bus_num <= -1 deact_and_changed = deactivated & stor_bus.changed new_bus_num[deact_and_changed] = cls.storage_to_subid[deact_and_changed] - # self._grid.storage["in_service"][stor_bus.changed & deactivated] = False 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 diff --git a/grid2op/Observation/baseObservation.py b/grid2op/Observation/baseObservation.py index 88b230f56..d3c8f7024 100644 --- a/grid2op/Observation/baseObservation.py +++ b/grid2op/Observation/baseObservation.py @@ -1041,117 +1041,137 @@ def process_shunt_satic_data(cls) -> None: return super().process_shunt_satic_data() @classmethod - def process_grid2op_compat(cls) -> None: - super().process_grid2op_compat() - glop_ver = cls._get_grid2op_version_as_version_obj() - - if cls.glop_version == cls.BEFORE_COMPAT_VERSION: - # oldest version: no storage and no curtailment available - - # 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) - - # deactivate storage - cls.set_no_storage() - for el in ["storage_charge", "storage_power_target", "storage_power"]: - if el in cls.attr_list_vect: - try: - cls.attr_list_vect.remove(el) - except ValueError: - pass - - # remove the curtailment - for el in ["gen_p_before_curtail", "curtailment", "curtailment_limit"]: - if el in cls.attr_list_vect: - try: - cls.attr_list_vect.remove(el) - except ValueError: - pass - - cls.attr_list_set = set(cls.attr_list_vect) + def _aux_process_grid2op_compat_old(cls): + # 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) - if glop_ver < version.parse("1.6.0"): - # this feature did not exist before and was introduced in grid2op 1.6.0 - cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) - cls.dim_alarms = 0 - for el in [ - "is_alarm_illegal", - "time_since_last_alarm", - "last_alarm", - "attention_budget", - "was_alarm_used_after_game_over", - ]: + # deactivate storage + cls.set_no_storage() + for el in ["storage_charge", "storage_power_target", "storage_power"]: + if el in cls.attr_list_vect: try: cls.attr_list_vect.remove(el) - except ValueError as exc_: - # this attribute was not there in the first place + except ValueError: pass - for el in ["_shunt_p", "_shunt_q", "_shunt_v", "_shunt_bus"]: - # added in grid2op 1.6.0 mainly for the EpisodeReboot + # remove the curtailment + for el in ["gen_p_before_curtail", "curtailment", "curtailment_limit"]: + if el in cls.attr_list_vect: try: cls.attr_list_vect.remove(el) - except ValueError as exc_: - # this attribute was not there in the first place + except ValueError: pass + @classmethod + def _aux_process_grid2op_compat_160(cls): + cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) + cls.dim_alarms = 0 + for el in [ + "is_alarm_illegal", + "time_since_last_alarm", + "last_alarm", + "attention_budget", + "was_alarm_used_after_game_over", + ]: + try: + cls.attr_list_vect.remove(el) + except ValueError as exc_: + # this attribute was not there in the first place + pass + + for el in ["_shunt_p", "_shunt_q", "_shunt_v", "_shunt_bus"]: + # added in grid2op 1.6.0 mainly for the EpisodeReboot + try: + cls.attr_list_vect.remove(el) + except ValueError as exc_: + # this attribute was not there in the first place + pass + + @classmethod + def _aux_process_grid2op_compat_164(cls): + cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) + + for el in ["max_step", "current_step"]: + try: + cls.attr_list_vect.remove(el) + except ValueError as exc_: + # this attribute was not there in the first place + pass + + @classmethod + def _aux_process_grid2op_compat_165(cls): + cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) + + for el in ["delta_time"]: + try: + cls.attr_list_vect.remove(el) + except ValueError as exc_: + # this attribute was not there in the first place + pass + + @classmethod + def _aux_process_grid2op_compat_166(cls): + cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) - if glop_ver < version.parse("1.6.4"): - # "current_step", "max_step" were added in grid2Op 1.6.4 - cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) + for el in [ + "gen_margin_up", + "gen_margin_down", + "curtailment_limit_effective", + ]: + try: + cls.attr_list_vect.remove(el) + except ValueError as exc_: + # this attribute was not there in the first place + pass + + @classmethod + def _aux_process_grid2op_compat_191(cls): + cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) - for el in ["max_step", "current_step"]: - try: - cls.attr_list_vect.remove(el) - except ValueError as exc_: - # this attribute was not there in the first place - pass + for el in [ + "active_alert", + "attack_under_alert", + "time_since_last_alert", + "alert_duration", + "total_number_of_alert", + "time_since_last_attack", + "was_alert_used_after_attack" + ]: + 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: + super().process_grid2op_compat() + glop_ver = cls._get_grid2op_version_as_version_obj() + + if cls.glop_version == cls.BEFORE_COMPAT_VERSION: + # oldest version: no storage and no curtailment available + cls._aux_process_grid2op_compat_old() + + if glop_ver < version.parse("1.6.0"): + # this feature did not exist before and was introduced in grid2op 1.6.0 + cls._aux_process_grid2op_compat_160() + if glop_ver < version.parse("1.6.4"): + # "current_step", "max_step" were added in grid2Op 1.6.4 + cls._aux_process_grid2op_compat_164() + if glop_ver < version.parse("1.6.5"): # "current_step", "max_step" were added in grid2Op 1.6.5 - cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) - - for el in ["delta_time"]: - try: - cls.attr_list_vect.remove(el) - except ValueError as exc_: - # this attribute was not there in the first place - pass - + cls._aux_process_grid2op_compat_165() + if glop_ver < version.parse("1.6.6"): # "gen_margin_up", "gen_margin_down" were added in grid2Op 1.6.6 - cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) - - for el in [ - "gen_margin_up", - "gen_margin_down", - "curtailment_limit_effective", - ]: - try: - cls.attr_list_vect.remove(el) - except ValueError as exc_: - # this attribute was not there in the first place - pass - + cls._aux_process_grid2op_compat_166() + if glop_ver < version.parse("1.9.1"): # alert attributes have been added in 1.9.1 - cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) - - for el in [ - "active_alert", - "attack_under_alert", - "time_since_last_alert", - "alert_duration", - "total_number_of_alert", - "time_since_last_attack", - "was_alert_used_after_attack" - ]: - try: - cls.attr_list_vect.remove(el) - except ValueError as exc_: - # this attribute was not there in the first place - pass - + cls._aux_process_grid2op_compat_191() + cls.attr_list_set = copy.deepcopy(cls.attr_list_set) cls.attr_list_set = set(cls.attr_list_vect) @@ -1498,6 +1518,25 @@ def __eq__(self, other : Self) -> bool: return True + def _aux_sub_get_attr_diff(self, me_, oth_): + diff_ = None + if me_ is None and oth_ is None: + diff_ = None + elif me_ is not None and oth_ is None: + diff_ = me_ + elif me_ is None and oth_ is not None: + if oth_.dtype == dt_bool: + diff_ = np.full(oth_.shape, fill_value=False, dtype=dt_bool) + else: + diff_ = -oth_ + else: + # both are not None + if oth_.dtype == dt_bool: + diff_ = ~np.logical_xor(me_, oth_) + else: + diff_ = me_ - oth_ + return diff_ + def __sub__(self, other : Self) -> Self: """ Computes the difference between two observations, and return an observation corresponding to @@ -1539,21 +1578,7 @@ def __sub__(self, other : Self) -> Self: for stat_nm in self._attr_eq: me_ = getattr(self, stat_nm) oth_ = getattr(other, stat_nm) - if me_ is None and oth_ is None: - diff_ = None - elif me_ is not None and oth_ is None: - diff_ = me_ - elif me_ is None and oth_ is not None: - if oth_.dtype == dt_bool: - diff_ = np.full(oth_.shape, fill_value=False, dtype=dt_bool) - else: - diff_ = -oth_ - else: - # both are not None - if oth_.dtype == dt_bool: - diff_ = ~np.logical_xor(me_, oth_) - else: - diff_ = me_ - oth_ + diff_ = self._aux_sub_get_attr_diff(me_, oth_) res.__setattr__(stat_nm, diff_) return res @@ -1625,6 +1650,68 @@ def update(self, env: "grid2op.Environment.Environment", with_forecast: bool=Tru """ pass + def _aux_build_conn_mat(self, as_csr_matrix): + # self._connectivity_matrix_ = np.zeros(shape=(self.dim_topo, self.dim_topo), dtype=dt_float) + # fill it by block for the objects + beg_ = 0 + end_ = 0 + row_ind = [] + col_ind = [] + cls = type(self) + for sub_id, nb_obj in enumerate(cls.sub_info): + # it must be a vanilla python integer, otherwise it's not handled by some backend + # especially if written in c++ + nb_obj = int(nb_obj) + end_ += nb_obj + # tmp = np.zeros(shape=(nb_obj, nb_obj), dtype=dt_float) + for obj1 in range(nb_obj): + my_bus = self.topo_vect[beg_ + obj1] + if my_bus == -1: + # object is disconnected, nothing is done + continue + # connect an object to itself + row_ind.append(beg_ + obj1) + col_ind.append(beg_ + obj1) + + # connect the other objects to it + for obj2 in range(obj1 + 1, nb_obj): + my_bus2 = self.topo_vect[beg_ + obj2] + if my_bus2 == -1: + # object is disconnected, nothing is done + continue + if my_bus == my_bus2: + # objects are on the same bus + # tmp[obj1, obj2] = 1 + # tmp[obj2, obj1] = 1 + row_ind.append(beg_ + obj2) + col_ind.append(beg_ + obj1) + row_ind.append(beg_ + obj1) + col_ind.append(beg_ + obj2) + beg_ += nb_obj + + # both ends of a line are connected together (if line is connected) + for q_id in range(cls.n_line): + if self.line_status[q_id]: + # if powerline is connected connect both its side + row_ind.append(cls.line_or_pos_topo_vect[q_id]) + col_ind.append(cls.line_ex_pos_topo_vect[q_id]) + row_ind.append(cls.line_ex_pos_topo_vect[q_id]) + col_ind.append(cls.line_or_pos_topo_vect[q_id]) + row_ind = np.array(row_ind).astype(dt_int) + col_ind = np.array(col_ind).astype(dt_int) + if not as_csr_matrix: + self._connectivity_matrix_ = np.zeros( + shape=(cls.dim_topo, cls.dim_topo), dtype=dt_float + ) + self._connectivity_matrix_[row_ind.T, col_ind] = 1.0 + else: + data = np.ones(row_ind.shape[0], dtype=dt_float) + self._connectivity_matrix_ = csr_matrix( + (data, (row_ind, col_ind)), + shape=(cls.dim_topo, cls.dim_topo), + dtype=dt_float, + ) + def connectivity_matrix(self, as_csr_matrix: bool=False) -> Union[np.ndarray, csr_matrix]: """ Computes and return the "connectivity matrix" `con_mat`. @@ -1708,76 +1795,15 @@ def connectivity_matrix(self, as_csr_matrix: bool=False) -> Union[np.ndarray, cs # - assign bus 2 to load 0 [on substation 1] # -> one of them is on bus 1 [line (extremity) 0] and the other on bus 2 [load 0] """ - if ( - self._connectivity_matrix_ is None - or ( - isinstance(self._connectivity_matrix_, csr_matrix) and not as_csr_matrix - ) - or ( - (not isinstance(self._connectivity_matrix_, csr_matrix)) - and as_csr_matrix - ) - ): - # self._connectivity_matrix_ = np.zeros(shape=(self.dim_topo, self.dim_topo), dtype=dt_float) - # fill it by block for the objects - beg_ = 0 - end_ = 0 - row_ind = [] - col_ind = [] - cls = type(self) - for sub_id, nb_obj in enumerate(cls.sub_info): - # it must be a vanilla python integer, otherwise it's not handled by some backend - # especially if written in c++ - nb_obj = int(nb_obj) - end_ += nb_obj - # tmp = np.zeros(shape=(nb_obj, nb_obj), dtype=dt_float) - for obj1 in range(nb_obj): - my_bus = self.topo_vect[beg_ + obj1] - if my_bus == -1: - # object is disconnected, nothing is done - continue - # connect an object to itself - row_ind.append(beg_ + obj1) - col_ind.append(beg_ + obj1) - - # connect the other objects to it - for obj2 in range(obj1 + 1, nb_obj): - my_bus2 = self.topo_vect[beg_ + obj2] - if my_bus2 == -1: - # object is disconnected, nothing is done - continue - if my_bus == my_bus2: - # objects are on the same bus - # tmp[obj1, obj2] = 1 - # tmp[obj2, obj1] = 1 - row_ind.append(beg_ + obj2) - col_ind.append(beg_ + obj1) - row_ind.append(beg_ + obj1) - col_ind.append(beg_ + obj2) - beg_ += nb_obj - - # both ends of a line are connected together (if line is connected) - for q_id in range(cls.n_line): - if self.line_status[q_id]: - # if powerline is connected connect both its side - row_ind.append(cls.line_or_pos_topo_vect[q_id]) - col_ind.append(cls.line_ex_pos_topo_vect[q_id]) - row_ind.append(cls.line_ex_pos_topo_vect[q_id]) - col_ind.append(cls.line_or_pos_topo_vect[q_id]) - row_ind = np.array(row_ind).astype(dt_int) - col_ind = np.array(col_ind).astype(dt_int) - if not as_csr_matrix: - self._connectivity_matrix_ = np.zeros( - shape=(cls.dim_topo, cls.dim_topo), dtype=dt_float - ) - self._connectivity_matrix_[row_ind.T, col_ind] = 1.0 - else: - data = np.ones(row_ind.shape[0], dtype=dt_float) - self._connectivity_matrix_ = csr_matrix( - (data, (row_ind, col_ind)), - shape=(cls.dim_topo, cls.dim_topo), - dtype=dt_float, - ) + need_build_mat = (self._connectivity_matrix_ is None or + isinstance(self._connectivity_matrix_, csr_matrix) and not as_csr_matrix or + ( + (not isinstance(self._connectivity_matrix_, csr_matrix)) + and as_csr_matrix + ) + ) + if need_build_mat : + self._aux_build_conn_mat(as_csr_matrix) return self._connectivity_matrix_ def _aux_fun_get_bus(self): @@ -3689,6 +3715,110 @@ def to_dict(self): return self._dictionnarized + def _aux_add_act_set_line_status(self, cls, cls_act, act, res, issue_warn): + reco_powerline = act.line_set_status + if "set_bus" in cls_act.authorized_keys: + line_ex_set_bus = act.line_ex_set_bus + line_or_set_bus = act.line_or_set_bus + else: + line_ex_set_bus = np.zeros(cls.n_line, dtype=dt_int) + line_or_set_bus = np.zeros(cls.n_line, dtype=dt_int) + error_no_bus_set = ( + "You reconnected a powerline with your action but did not specify on which bus " + "to reconnect both its end. This behaviour, also perfectly fine for an environment " + "will not be accurate in the method obs + act. Consult the documentation for more " + "information. Problem arose for powerlines with id {}" + ) + + tmp = ( + (reco_powerline == 1) + & (line_ex_set_bus <= 0) + & (res.topo_vect[cls.line_ex_pos_topo_vect] == -1) + ) + if tmp.any(): + id_issue_ex = np.nonzero(tmp)[0] + if issue_warn: + warnings.warn(error_no_bus_set.format(id_issue_ex)) + if "set_bus" in cls_act.authorized_keys: + # assign 1 in the bus in this case + act.line_ex_set_bus = [(el, 1) for el in id_issue_ex] + tmp = ( + (reco_powerline == 1) + & (line_or_set_bus <= 0) + & (res.topo_vect[cls.line_or_pos_topo_vect] == -1) + ) + if tmp.any(): + id_issue_or = np.nonzero(tmp)[0] + if issue_warn: + warnings.warn(error_no_bus_set.format(id_issue_or)) + if "set_bus" in cls_act.authorized_keys: + # assign 1 in the bus in this case + act.line_or_set_bus = [(el, 1) for el in id_issue_or] + + def _aux_add_act_set_line_status2(self, cls, cls_act, act, res, issue_warn): + disco_line = (act.line_set_status == -1) & res.line_status + res.topo_vect[cls.line_or_pos_topo_vect[disco_line]] = -1 + res.topo_vect[cls.line_ex_pos_topo_vect[disco_line]] = -1 + res.line_status[disco_line] = False + + reco_line = (act.line_set_status >= 1) & (~res.line_status) + # i can do that because i already "fixed" the action to have it put 1 in case it + # bus were not provided + if "set_bus" in cls_act.authorized_keys: + # I assign previous bus (because it could have been modified) + res.topo_vect[ + cls.line_or_pos_topo_vect[reco_line] + ] = act.line_or_set_bus[reco_line] + res.topo_vect[ + cls.line_ex_pos_topo_vect[reco_line] + ] = act.line_ex_set_bus[reco_line] + else: + # I assign one (action do not allow me to modify the bus) + res.topo_vect[cls.line_or_pos_topo_vect[reco_line]] = 1 + res.topo_vect[cls.line_ex_pos_topo_vect[reco_line]] = 1 + + res.line_status[reco_line] = True + + def _aux_add_act_change_line_status2(self, cls, cls_act, act, res, issue_warn): + disco_line = act.line_change_status & res.line_status + reco_line = act.line_change_status & (~res.line_status) + + # handle disconnected powerlines + res.topo_vect[cls.line_or_pos_topo_vect[disco_line]] = -1 + res.topo_vect[cls.line_ex_pos_topo_vect[disco_line]] = -1 + res.line_status[disco_line] = False + + # handle reconnected powerlines + if reco_line.any(): + if "set_bus" in cls_act.authorized_keys: + line_ex_set_bus = 1 * act.line_ex_set_bus + line_or_set_bus = 1 * act.line_or_set_bus + else: + line_ex_set_bus = np.zeros(cls.n_line, dtype=dt_int) + line_or_set_bus = np.zeros(cls.n_line, dtype=dt_int) + + if issue_warn and ( + (line_or_set_bus[reco_line] == 0).any() + or (line_ex_set_bus[reco_line] == 0).any() + ): + warnings.warn( + 'A powerline has been reconnected with a "change_status" action without ' + "specifying on which bus it was supposed to be reconnected. This is " + "perfectly fine in regular grid2op environment, but this behaviour " + "cannot be properly implemented with the only information in the " + "observation. Please see the documentation for more information." + ) + line_or_set_bus[reco_line & (line_or_set_bus == 0)] = 1 + line_ex_set_bus[reco_line & (line_ex_set_bus == 0)] = 1 + + res.topo_vect[cls.line_or_pos_topo_vect[reco_line]] = line_or_set_bus[ + reco_line + ] + res.topo_vect[cls.line_ex_pos_topo_vect[reco_line]] = line_ex_set_bus[ + reco_line + ] + res.line_status[reco_line] = True + def add_act(self, act : "grid2op.Action.BaseAction", issue_warn=True) -> Self: """ Easier access to the impact on the observation if an action were applied. @@ -3797,45 +3927,8 @@ def add_act(self, act : "grid2op.Action.BaseAction", issue_warn=True) -> Self: # if a powerline has been reconnected without specific bus, i issue a warning if "set_line_status" in cls_act.authorized_keys: - reco_powerline = act.line_set_status - if "set_bus" in cls_act.authorized_keys: - line_ex_set_bus = act.line_ex_set_bus - line_or_set_bus = act.line_or_set_bus - else: - line_ex_set_bus = np.zeros(cls.n_line, dtype=dt_int) - line_or_set_bus = np.zeros(cls.n_line, dtype=dt_int) - error_no_bus_set = ( - "You reconnected a powerline with your action but did not specify on which bus " - "to reconnect both its end. This behaviour, also perfectly fine for an environment " - "will not be accurate in the method obs + act. Consult the documentation for more " - "information. Problem arose for powerlines with id {}" - ) - - tmp = ( - (reco_powerline == 1) - & (line_ex_set_bus <= 0) - & (res.topo_vect[cls.line_ex_pos_topo_vect] == -1) - ) - if tmp.any(): - id_issue_ex = np.nonzero(tmp)[0] - if issue_warn: - warnings.warn(error_no_bus_set.format(id_issue_ex)) - if "set_bus" in cls_act.authorized_keys: - # assign 1 in the bus in this case - act.line_ex_set_bus = [(el, 1) for el in id_issue_ex] - tmp = ( - (reco_powerline == 1) - & (line_or_set_bus <= 0) - & (res.topo_vect[cls.line_or_pos_topo_vect] == -1) - ) - if tmp.any(): - id_issue_or = np.nonzero(tmp)[0] - if issue_warn: - warnings.warn(error_no_bus_set.format(id_issue_or)) - if "set_bus" in cls_act.authorized_keys: - # assign 1 in the bus in this case - act.line_or_set_bus = [(el, 1) for el in id_issue_or] - + self._aux_add_act_set_line_status(cls, cls_act, act, res, issue_warn) + # topo vect if "set_bus" in cls_act.authorized_keys: res.topo_vect[act.set_bus != 0] = act.set_bus[act.set_bus != 0] @@ -3853,72 +3946,14 @@ def add_act(self, act : "grid2op.Action.BaseAction", issue_warn=True) -> Self: # powerline status if "set_line_status" in cls_act.authorized_keys: - disco_line = (act.line_set_status == -1) & res.line_status - res.topo_vect[cls.line_or_pos_topo_vect[disco_line]] = -1 - res.topo_vect[cls.line_ex_pos_topo_vect[disco_line]] = -1 - res.line_status[disco_line] = False - - reco_line = (act.line_set_status >= 1) & (~res.line_status) - # i can do that because i already "fixed" the action to have it put 1 in case it - # bus were not provided - if "set_bus" in cls_act.authorized_keys: - # I assign previous bus (because it could have been modified) - res.topo_vect[ - cls.line_or_pos_topo_vect[reco_line] - ] = act.line_or_set_bus[reco_line] - res.topo_vect[ - cls.line_ex_pos_topo_vect[reco_line] - ] = act.line_ex_set_bus[reco_line] - else: - # I assign one (action do not allow me to modify the bus) - res.topo_vect[cls.line_or_pos_topo_vect[reco_line]] = 1 - res.topo_vect[cls.line_ex_pos_topo_vect[reco_line]] = 1 - - res.line_status[reco_line] = True - + self._aux_add_act_set_line_status2(cls, cls_act, act, res, issue_warn) + if "change_line_status" in cls_act.authorized_keys: - disco_line = act.line_change_status & res.line_status - reco_line = act.line_change_status & (~res.line_status) - - # handle disconnected powerlines - res.topo_vect[cls.line_or_pos_topo_vect[disco_line]] = -1 - res.topo_vect[cls.line_ex_pos_topo_vect[disco_line]] = -1 - res.line_status[disco_line] = False - - # handle reconnected powerlines - if reco_line.any(): - if "set_bus" in cls_act.authorized_keys: - line_ex_set_bus = 1 * act.line_ex_set_bus - line_or_set_bus = 1 * act.line_or_set_bus - else: - line_ex_set_bus = np.zeros(cls.n_line, dtype=dt_int) - line_or_set_bus = np.zeros(cls.n_line, dtype=dt_int) - - if issue_warn and ( - (line_or_set_bus[reco_line] == 0).any() - or (line_ex_set_bus[reco_line] == 0).any() - ): - warnings.warn( - 'A powerline has been reconnected with a "change_status" action without ' - "specifying on which bus it was supposed to be reconnected. This is " - "perfectly fine in regular grid2op environment, but this behaviour " - "cannot be properly implemented with the only information in the " - "observation. Please see the documentation for more information." - ) - line_or_set_bus[reco_line & (line_or_set_bus == 0)] = 1 - line_ex_set_bus[reco_line & (line_ex_set_bus == 0)] = 1 - - res.topo_vect[cls.line_or_pos_topo_vect[reco_line]] = line_or_set_bus[ - reco_line - ] - res.topo_vect[cls.line_ex_pos_topo_vect[reco_line]] = line_ex_set_bus[ - reco_line - ] - res.line_status[reco_line] = True + self._aux_add_act_change_line_status2(cls, cls_act, act, res, issue_warn) if "redispatch" in cls_act.authorized_keys: redisp = act.redispatch - if (redisp != 0).any() and issue_warn: + if (np.abs(redisp) >= 1e-7).any() and issue_warn: warnings.warn( "You did redispatching on this action. Redispatching is heavily transformed " "by the environment (consult the documentation about the modeling of the " @@ -3927,7 +3962,16 @@ def add_act(self, act : "grid2op.Action.BaseAction", issue_warn=True) -> Self: if "set_storage" in cls_act.authorized_keys: storage_p = act.storage_p - if (storage_p != 0).any() and issue_warn: + if (np.abs(storage_p) >= 1e-7).any() and issue_warn: + warnings.warn( + "You did action on storage units in this action. This implies performing some " + "redispatching which is heavily transformed " + "by the environment (consult the documentation about the modeling of the " + "generators for example) so we will not even try to mimic this here." + ) + if "curtail" in cls_act.authorized_keys: + curt = act.curtail + if (np.abs(curt + 1) >= 1e-7).any() and issue_warn: # curtail == -1. warnings.warn( "You did action on storage units in this action. This implies performing some " "redispatching which is heavily transformed " From a0010ba1fc1e71805f344f72d58e97132f75b0e0 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 9 Feb 2024 12:10:37 +0100 Subject: [PATCH 24/24] trying to fix an issue with the regex in make_release [skip ci] --- utils/make_release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/make_release.py b/utils/make_release.py index 057f9342c..c91346dd1 100644 --- a/utils/make_release.py +++ b/utils/make_release.py @@ -84,7 +84,7 @@ def modify_and_push_docker(version, # grid2op version version)) # TODO re.search(reg_, "0.0.4-rc1").group("prerelease") -> rc1 (if regex_version is the official one) - if re.search(f".*\.(rc|pre|dev)[0-9]+$", version) is not None: + if re.search(f".*(\\.|-)(rc|pre|dev)[0-9]+$", version) is not None: is_prerelease = True print("This is a pre release, docker will NOT be pushed, github tag will NOT be made") time.sleep(2)