diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e4a63b0f..5a773177 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -113,6 +113,16 @@ Native multi agents support: - [FIXED] the `obs.get_forecast_env` : in some cases the resulting first observation (obtained from `for_env.reset()`) did not have the correct topology. +- [FIXED] issue https://github.com/Grid2op/grid2op/issues/665 (`obs.reset()` + was not correctly implemented: some attributes were forgotten) +- [FIXED] issue https://github.com/Grid2op/grid2op/issues/667 (`act.as_serializable_dict()` + was not correctly implemented AND the `_aux_affect_object_int` and `_aux_affect_object_float` + have been also fixed - weird behaviour when you give them a list with the exact length of the + object you tried to modified (for example a list with a size of `n_load` that affected the loads)) +- [FIXED] a bug when using the `DoNothingHandler` for the maintenance and the + environment data +- [FIXED] an issue preventing to set the thermal limit in the options + if the last simulated action lead to a game over - [ADDED] possibility to set the "thermal limits" when calling `env.reset(..., options={"thermal limit": xxx})` - [ADDED] possibility to retrieve some structural information about elements with with `gridobj.get_line_info(...)`, `gridobj.get_load_info(...)`, `gridobj.get_gen_info(...)` @@ -138,7 +148,13 @@ Native multi agents support: does not have shunt information but there are not shunts on the grid. - [IMPROVED] consistency of `MultiMixEnv` in case of automatic_classes (only one class is generated for all mixes) - +- [IMPROVED] the `act.as_serializable_dict()` to be more 'backend agnostic'as + it nows tries to use the name of the elements in the json output +- [IMPROVED] the way shunt data are digested in the `BaseAction` class (it is now + possible to use the same things as for the other types of element) +- [IMPROVED] grid2op does not require the `chronics` folder when using the `FromHandlers` + class + [1.10.4] - 2024-10-15 ------------------------- - [FIXED] new pypi link (no change in code) diff --git a/docs/conf.py b/docs/conf.py index fc753b64..5205f702 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ author = 'Benjamin Donnot' # The full version, including alpha/beta/rc tags -release = '1.11.0.dev1' +release = '1.11.0.dev2' version = '1.11' diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 3bc564a8..587ee00c 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -592,13 +592,13 @@ def __deepcopy__(self, memodict={}) -> "BaseAction": return res - def _aux_serialize_add_key_change(self, attr_nm, dict_key, res): - tmp_ = [int(id_) for id_, val in enumerate(getattr(self, attr_nm)) if val] + def _aux_serialize_add_key_change(self, attr_nm, dict_key, res, vect_id_to_name): + tmp_ = [str(vect_id_to_name[id_]) for id_, val in enumerate(getattr(self, attr_nm)) if val] if tmp_: res[dict_key] = tmp_ - def _aux_serialize_add_key_set(self, attr_nm, dict_key, res): - tmp_ = [(int(id_), int(val)) for id_, val in enumerate(getattr(self, attr_nm)) if np.abs(val) >= 1e-7] + def _aux_serialize_add_key_set(self, attr_nm, dict_key, res, vect_id_to_name): + tmp_ = [(str(vect_id_to_name[id_]), int(val)) for id_, val in enumerate(getattr(self, attr_nm)) if np.abs(val) >= 1e-7] if tmp_: res[dict_key] = tmp_ @@ -651,18 +651,18 @@ def as_serializable_dict(self) -> dict: if self._modif_change_bus: res["change_bus"] = {} - self._aux_serialize_add_key_change("load_change_bus", "loads_id", res["change_bus"]) - self._aux_serialize_add_key_change("gen_change_bus", "generators_id", res["change_bus"]) - self._aux_serialize_add_key_change("line_or_change_bus", "lines_or_id", res["change_bus"]) - self._aux_serialize_add_key_change("line_ex_change_bus", "lines_ex_id", res["change_bus"]) + self._aux_serialize_add_key_change("load_change_bus", "loads_id", res["change_bus"], cls.name_load) + self._aux_serialize_add_key_change("gen_change_bus", "generators_id", res["change_bus"], cls.name_gen) + self._aux_serialize_add_key_change("line_or_change_bus", "lines_or_id", res["change_bus"], cls.name_line) + self._aux_serialize_add_key_change("line_ex_change_bus", "lines_ex_id", res["change_bus"], cls.name_line) if hasattr(cls, "n_storage") and cls.n_storage: - self._aux_serialize_add_key_change("storage_change_bus", "storages_id", res["change_bus"]) + self._aux_serialize_add_key_change("storage_change_bus", "storages_id", res["change_bus"], cls.name_storage) if not res["change_bus"]: del res["change_bus"] if self._modif_change_status: res["change_line_status"] = [ - int(id_) for id_, val in enumerate(self._switch_line_status) if val + str(cls.name_line[id_]) for id_, val in enumerate(self._switch_line_status) if val ] if not res["change_line_status"]: del res["change_line_status"] @@ -670,18 +670,18 @@ def as_serializable_dict(self) -> dict: # int elements if self._modif_set_bus: res["set_bus"] = {} - self._aux_serialize_add_key_set("load_set_bus", "loads_id", res["set_bus"]) - self._aux_serialize_add_key_set("gen_set_bus", "generators_id", res["set_bus"]) - self._aux_serialize_add_key_set("line_or_set_bus", "lines_or_id", res["set_bus"]) - self._aux_serialize_add_key_set("line_ex_set_bus", "lines_ex_id", res["set_bus"]) + self._aux_serialize_add_key_set("load_set_bus", "loads_id", res["set_bus"], cls.name_load) + self._aux_serialize_add_key_set("gen_set_bus", "generators_id", res["set_bus"], cls.name_gen) + self._aux_serialize_add_key_set("line_or_set_bus", "lines_or_id", res["set_bus"], cls.name_line) + self._aux_serialize_add_key_set("line_ex_set_bus", "lines_ex_id", res["set_bus"], cls.name_line) if hasattr(cls, "n_storage") and cls.n_storage: - self._aux_serialize_add_key_set("storage_set_bus", "storages_id", res["set_bus"]) + self._aux_serialize_add_key_set("storage_set_bus", "storages_id", res["set_bus"], cls.name_storage) if not res["set_bus"]: del res["set_bus"] if self._modif_set_status: res["set_line_status"] = [ - (int(id_), int(val)) + (str(cls.name_line[id_]), int(val)) for id_, val in enumerate(self._set_line_status) if val != 0 ] @@ -691,7 +691,7 @@ def as_serializable_dict(self) -> dict: # float elements if self._modif_redispatch: res["redispatch"] = [ - (int(id_), float(val)) + (str(cls.name_gen[id_]), float(val)) for id_, val in enumerate(self._redispatch) if np.abs(val) >= 1e-7 ] @@ -700,7 +700,7 @@ def as_serializable_dict(self) -> dict: if self._modif_storage: res["set_storage"] = [ - (int(id_), float(val)) + (str(cls.name_storage[id_]), float(val)) for id_, val in enumerate(self._storage_power) if np.abs(val) >= 1e-7 ] @@ -709,7 +709,7 @@ def as_serializable_dict(self) -> dict: if self._modif_curtailment: res["curtail"] = [ - (int(id_), float(val)) + (str(cls.name_gen[id_]), float(val)) for id_, val in enumerate(self._curtail) if np.abs(val + 1.) >= 1e-7 ] @@ -719,9 +719,10 @@ def as_serializable_dict(self) -> dict: # more advanced options if self._modif_inj: res["injection"] = {} - for ky in ["prod_p", "prod_v", "load_p", "load_q"]: + for ky, vect_nm in zip(["prod_p", "prod_v", "load_p", "load_q"], + [cls.name_gen, cls.name_gen, cls.name_load, cls.name_load]): if ky in self._dict_inj: - res["injection"][ky] = [float(val) for val in self._dict_inj[ky]] + res["injection"][ky] = {str(vect_nm[i]): float(val) for i, val in enumerate(self._dict_inj[ky])} if not res["injection"]: del res["injection"] @@ -729,15 +730,15 @@ def as_serializable_dict(self) -> dict: res["shunt"] = {} if np.isfinite(self.shunt_p).any(): res["shunt"]["shunt_p"] = [ - (int(sh_id), float(val)) for sh_id, val in enumerate(self.shunt_p) if np.isfinite(val) + (str(cls.name_shunt[sh_id]), float(val)) for sh_id, val in enumerate(self.shunt_p) if np.isfinite(val) ] if np.isfinite(self.shunt_q).any(): res["shunt"]["shunt_q"] = [ - (int(sh_id), float(val)) for sh_id, val in enumerate(self.shunt_q) if np.isfinite(val) + (str(cls.name_shunt[sh_id]), float(val)) for sh_id, val in enumerate(self.shunt_q) if np.isfinite(val) ] if (self.shunt_bus != 0).any(): res["shunt"]["shunt_bus"] = [ - (int(sh_id), int(val)) + (str(cls.name_shunt[sh_id]), int(val)) for sh_id, val in enumerate(self.shunt_bus) if val != 0 ] @@ -1860,60 +1861,51 @@ def __call__(self) -> Tuple[dict, np.ndarray, np.ndarray, np.ndarray, np.ndarray def _digest_shunt(self, dict_): cls = type(self) - if "shunt" in dict_: - ddict_ = dict_["shunt"] - - key_shunt_reco = {"set_bus", "shunt_p", "shunt_q", "shunt_bus"} - for k in ddict_: - if k not in key_shunt_reco: - warn = "The key {} is not recognized by BaseAction when trying to modify the shunt.".format( - k + if "shunt" not in dict_: + return + ddict_ = dict_["shunt"] + + key_shunt_reco = {"set_bus", "shunt_p", "shunt_q", "shunt_bus"} + for k in ddict_: + if k not in key_shunt_reco: + warn = "The key {} is not recognized by BaseAction when trying to modify the shunt.".format( + k + ) + warn += " Recognized keys are {}".format(sorted(key_shunt_reco)) + warnings.warn(warn) + + for key_n, vect_self in zip( + ["shunt_bus", "shunt_p", "shunt_q", "set_bus"], + [self.shunt_bus, self.shunt_p, self.shunt_q, self.shunt_bus], + ): + if key_n in ddict_: + tmp = ddict_[key_n] + if tmp is None: + pass + elif key_n == "shunt_bus" or key_n == "set_bus": + self._aux_affect_object_int( + tmp, + key_n, + cls.n_shunt, + cls.name_shunt, + np.arange(cls.n_shunt), + vect_self, + max_val=cls.n_busbar_per_sub + ) + elif key_n == "shunt_p" or key_n == "shunt_q": + self._aux_affect_object_float( + tmp, + key_n, + cls.n_shunt, + cls.name_shunt, + np.arange(cls.n_shunt), + vect_self + ) + else: + raise AmbiguousAction( + "Invalid way to modify {} for shunts. It should be a numpy array or a " + "list, found {}.".format(key_n, type(tmp)) ) - warn += " Recognized keys are {}".format(sorted(key_shunt_reco)) - warnings.warn(warn) - for key_n, vect_self in zip( - ["shunt_bus", "shunt_p", "shunt_q", "set_bus"], - [self.shunt_bus, self.shunt_p, self.shunt_q, self.shunt_bus], - ): - if key_n in ddict_: - tmp = ddict_[key_n] - if isinstance(tmp, np.ndarray): - # complete shunt vector is provided - vect_self[:] = tmp - elif isinstance(tmp, list): - # expected a list: (id shunt, new bus) - for (sh_id, new_bus) in tmp: - if sh_id < 0: - raise AmbiguousAction( - "Invalid shunt id {}. Shunt id should be positive".format( - sh_id - ) - ) - if sh_id >= cls.n_shunt: - raise AmbiguousAction( - "Invalid shunt id {}. Shunt id should be less than the number " - "of shunt {}".format(sh_id, cls.n_shunt) - ) - 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: - pass - else: - raise AmbiguousAction( - "Invalid way to modify {} for shunts. It should be a numpy array or a " - "dictionary.".format(key_n) - ) def _digest_injection(self, dict_): # I update the action @@ -2265,6 +2257,7 @@ def update(self, - "curtail" : TODO - "raise_alarm" : TODO - "raise_alert": TODO + - "shunt": 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. @@ -4059,7 +4052,7 @@ def _aux_affect_object_int( if len(values) == nb_els: # 2 cases: either i set all loads in the form [(0,..), (1,..), (2,...)] # or i should have converted the list to np array - if isinstance(values[0], tuple): + if isinstance(values[0], (tuple, list)): # list of tuple, handled below # TODO can be somewhat "hacked" if the type of the object on the list is not always the same pass @@ -5492,7 +5485,7 @@ def _aux_affect_object_float( raise IllegalAction( f"Impossible to set {name_el} values with a single float." ) - elif isinstance(values[0], tuple): + elif isinstance(values[0], (tuple, list)): # list of tuple, handled below # TODO can be somewhat "hacked" if the type of the object on the list is not always the same pass diff --git a/grid2op/Chronics/time_series_from_handlers.py b/grid2op/Chronics/time_series_from_handlers.py index 646cf3de..b26baccb 100644 --- a/grid2op/Chronics/time_series_from_handlers.py +++ b/grid2op/Chronics/time_series_from_handlers.py @@ -272,15 +272,17 @@ def load_next(self): res["injection"] = dict_inj # load maintenance + maintenance_time, maintenance_duration = None, None if self.maintenance_handler is not None: tmp_ = self.maintenance_handler.load_next(res) if tmp_ is not None: res["maintenance"] = tmp_ maintenance_time, maintenance_duration = self.maintenance_handler.load_next_maintenance() - else: + if maintenance_time is None: maintenance_time = self._no_mh_time + if maintenance_duration is None: maintenance_duration = self._no_mh_duration - + # load hazards if self.hazard_duration is not None: res["hazards"] = self.hazards_handler.load_next(res) diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 5468db5e..ce4ccd90 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -1297,6 +1297,8 @@ def reset(self, if ambiguous: raise Grid2OpException("You provided an invalid (ambiguous) action to set the 'init state'") from except_tmp init_state.remove_change() + if self.observation_space.obs_env is not None: + self.observation_space.obs_env.reset() super().reset(seed=seed, options=options) if options is not None and "max step" in options: diff --git a/grid2op/MakeEnv/MakeFromPath.py b/grid2op/MakeEnv/MakeFromPath.py index 640c93be..39abd725 100644 --- a/grid2op/MakeEnv/MakeFromPath.py +++ b/grid2op/MakeEnv/MakeFromPath.py @@ -25,6 +25,7 @@ FromNPY, FromChronix2grid, GridStateFromFile, + FromHandlers, GridValue) from grid2op.Space import GRID2OP_CLASSES_ENV_FOLDER from grid2op.Action import BaseAction, DontAct @@ -342,7 +343,7 @@ def make_from_dataset_path( print(exc_) raise EnvError( "Invalid dataset config file: {}".format(config_path_abs) - ) from None + ) from exc_ # Get graph layout graph_layout = None @@ -558,7 +559,6 @@ def make_from_dataset_path( chronics_class_cfg = ChangeNothing if "chronics_class" in config_data and config_data["chronics_class"] is not None: chronics_class_cfg = config_data["chronics_class"] - # Get default Grid class grid_value_class_cfg = GridStateFromFile if ( @@ -605,7 +605,9 @@ def make_from_dataset_path( if ( ((chronics_class_used != ChangeNothing) and (chronics_class_used != FromNPY) and - (chronics_class_used != FromChronix2grid)) + (chronics_class_used != FromChronix2grid) and + (chronics_class_used != FromHandlers) + ) ) and exc_chronics is not None: raise EnvError( f"Impossible to find the chronics for your environment. Please make sure to provide " diff --git a/grid2op/__init__.py b/grid2op/__init__.py index 35522b93..95050dfb 100644 --- a/grid2op/__init__.py +++ b/grid2op/__init__.py @@ -8,10 +8,10 @@ # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. """ -Grid2Op - +Grid2Op a testbed platform to model sequential decision making in power systems. """ -__version__ = '1.11.0.dev1' + +__version__ = '1.11.0.dev2' __all__ = [ "Action", diff --git a/grid2op/tests/BaseBackendTest.py b/grid2op/tests/BaseBackendTest.py index 37fba0b9..2feb91c8 100644 --- a/grid2op/tests/BaseBackendTest.py +++ b/grid2op/tests/BaseBackendTest.py @@ -2092,7 +2092,7 @@ def test_shunt_ambiguous_id_incorrect(self): backend=backend, _add_to_name=type(self).__name__ + "_1" ) as env_case2: - with self.assertRaises(AmbiguousAction): + with self.assertRaises(IllegalAction): act = env_case2.action_space({"shunt": {"set_bus": [(0, 2)]}}) def test_shunt_effect(self): diff --git a/grid2op/tests/test_act_as_serializable_dict.py b/grid2op/tests/test_act_as_serializable_dict.py index 3ac3df59..fc274191 100644 --- a/grid2op/tests/test_act_as_serializable_dict.py +++ b/grid2op/tests/test_act_as_serializable_dict.py @@ -398,14 +398,14 @@ def tearDown(self) -> None: def test_can_make_lineor(self): act : BaseAction = self.env1.action_space({"set_bus": {"lines_or_id": [(0, 2), (5, 1), (15, 2)]}}) dict_ = act.as_serializable_dict() - assert dict_ == {'set_bus': {'lines_or_id': [(0, 2), (5, 1), (15, 2)]}} + assert dict_ == {'set_bus': {'lines_or_id': [('0_1_0', 2), ('2_3_5', 1), ('3_6_15', 2)]}} act2 = self.env2.action_space(dict_) dict_2 = act2.as_serializable_dict() assert dict_ == dict_2 act : BaseAction = self.env1.action_space({"change_bus": {"lines_or_id": [0, 5, 15]}}) dict_ = act.as_serializable_dict() - assert dict_ == {'change_bus': {'lines_or_id': [0, 5, 15]}} + assert dict_ == {'change_bus': {'lines_or_id': ['0_1_0', '2_3_5', '3_6_15']}} act2 = self.env2.action_space(dict_) dict_2 = act2.as_serializable_dict() assert dict_ == dict_2 @@ -413,14 +413,14 @@ def test_can_make_lineor(self): def test_can_make_lineex(self): act : BaseAction = self.env1.action_space({"set_bus": {"lines_ex_id": [(0, 2), (5, 1), (15, 2)]}}) dict_ = act.as_serializable_dict() - assert dict_ == {'set_bus': {'lines_ex_id': [(0, 2), (5, 1), (15, 2)]}} + assert dict_ == {'set_bus': {'lines_ex_id': [('0_1_0', 2), ('2_3_5', 1), ('3_6_15', 2)]}} act2 = self.env2.action_space(dict_) dict_2 = act2.as_serializable_dict() assert dict_ == dict_2 act : BaseAction = self.env1.action_space({"change_bus": {"lines_ex_id": [0, 5, 15]}}) dict_ = act.as_serializable_dict() - assert dict_ == {'change_bus': {'lines_ex_id': [0, 5, 15]}} + assert dict_ == {'change_bus': {'lines_ex_id': ['0_1_0', '2_3_5', '3_6_15']}} act2 = self.env2.action_space(dict_) dict_2 = act2.as_serializable_dict() assert dict_ == dict_2 @@ -428,14 +428,14 @@ def test_can_make_lineex(self): def test_can_make_gen(self): act : BaseAction = self.env1.action_space({"set_bus": {"generators_id": [(0, 2), (5, 1)]}}) dict_ = act.as_serializable_dict() - assert dict_ == {'set_bus': {'generators_id': [(0, 2), (5, 1)]}} + assert dict_ == {'set_bus': {'generators_id': [('gen_1_0', 2), ('gen_0_5', 1)]}} act2 = self.env2.action_space(dict_) dict_2 = act2.as_serializable_dict() assert dict_ == dict_2 act : BaseAction = self.env1.action_space({"change_bus": {"generators_id": [0, 5]}}) dict_ = act.as_serializable_dict() - assert dict_ == {'change_bus': {'generators_id': [0, 5]}} + assert dict_ == {'change_bus': {'generators_id': ['gen_1_0', 'gen_0_5']}} act2 = self.env2.action_space(dict_) dict_2 = act2.as_serializable_dict() assert dict_ == dict_2 @@ -443,14 +443,14 @@ def test_can_make_gen(self): def test_can_make_load(self): act : BaseAction = self.env1.action_space({"set_bus": {"loads_id": [(0, 2), (5, 1)]}}) dict_ = act.as_serializable_dict() - assert dict_ == {'set_bus': {'loads_id': [(0, 2), (5, 1)]}} + assert dict_ == {'set_bus': {'loads_id': [('load_1_0', 2), ('load_8_5', 1)]}} act2 = self.env2.action_space(dict_) dict_2 = act2.as_serializable_dict() assert dict_ == dict_2 act : BaseAction = self.env1.action_space({"change_bus": {"loads_id": [0, 5]}}) dict_ = act.as_serializable_dict() - assert dict_ == {'change_bus': {'loads_id': [0, 5]}} + assert dict_ == {'change_bus': {'loads_id': ['load_1_0', 'load_8_5']}} act2 = self.env2.action_space(dict_) dict_2 = act2.as_serializable_dict() assert dict_ == dict_2 @@ -462,10 +462,10 @@ def test_with_gen_load_lineor_lineex(self): "lines_or_id": [(0, 2), (5, 1), (15, 2)] }}) dict_ = act.as_serializable_dict() - assert dict_ == {'set_bus': {'loads_id': [(0, 2), (5, 1)], - 'generators_id': [(0, 2), (5, 1)], - 'lines_ex_id': [(0, 2), (5, 1), (15, 2)], - 'lines_or_id': [(0, 2), (5, 1), (15, 2)] + assert dict_ == {'set_bus': {'loads_id': [('load_1_0', 2), ('load_8_5', 1)], + 'generators_id': [('gen_1_0', 2), ('gen_0_5', 1)], + 'lines_ex_id': [('0_1_0', 2), ('2_3_5', 1), ('3_6_15', 2)], + 'lines_or_id': [('0_1_0', 2), ('2_3_5', 1), ('3_6_15', 2)] }} act2 = self.env2.action_space(dict_) dict_2 = act2.as_serializable_dict() @@ -477,10 +477,10 @@ def test_with_gen_load_lineor_lineex(self): 'lines_or_id': [0, 5, 15] }}) dict_ = act.as_serializable_dict() - assert dict_ == {'change_bus': {'loads_id': [0, 5], - 'generators_id': [0, 5], - 'lines_ex_id': [0, 5, 15], - 'lines_or_id': [0, 5, 15] + assert dict_ == {'change_bus': {'loads_id': ["load_1_0", "load_8_5"], + 'generators_id': ["gen_1_0", "gen_0_5"], + 'lines_ex_id': ["0_1_0", "2_3_5", "3_6_15"], + 'lines_or_id': ["0_1_0", "2_3_5", "3_6_15"] }} act2 = self.env2.action_space(dict_) dict_2 = act2.as_serializable_dict() diff --git a/grid2op/tests/test_issue_665.py b/grid2op/tests/test_issue_665.py new file mode 100644 index 00000000..bd5f24c6 --- /dev/null +++ b/grid2op/tests/test_issue_665.py @@ -0,0 +1,67 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# See AUTHORS.txt and https://github.com/Grid2Op/grid2op/pull/319 +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + +import numpy as np +from logging import Logger +import unittest +import warnings + + +from helper_path_test import PATH_DATA_TEST +import grid2op +from grid2op.dtypes import dt_int, dt_float +from grid2op.gym_compat import BoxGymObsSpace +from grid2op.gym_compat.utils import _compute_extra_power_for_losses +from grid2op.Exceptions import ChronicsError, EnvError + + +class Issue665Tester(unittest.TestCase): + def setUp(self): + self.env_name = "l2rpn_idf_2023" + # create first env + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make("l2rpn_idf_2023", test=True) + self.dict_properties = BoxGymObsSpace(self.env.observation_space)._dict_properties + + def tearDown(self) -> None: + self.env.close() + return super().tearDown() + + def test_issue_665(self): + attributes_names = set(self.dict_properties.keys()) + attr_with_a_problem = set() # I put an attribute here if at least one bound has been exceeded at least once + attr_without_a_problem = set(self.dict_properties.keys()) # I remove an attribute from here if at least one bound has been exceeded at least once + + i = 0 + while i < 5 and not attr_without_a_problem: + obs = self.env.reset() + obs_temp = self.env.observation_space._template_obj + + # I check only attributes which has not exceeded their bounds yet + for attr_name in attr_without_a_problem: + attr = getattr(obs_temp, attr_name) + low = self.dict_properties[attr_name][0] + high = self.dict_properties[attr_name][1] + + ids = np.where((attr < low) | (attr > high))[0] + if ids.shape[0] > 0: # Case where at least a bound has been exceeded + # I uppdate my set + attr_with_a_problem.add(attr_name) + # I print a value (the one with the lower index) that exceeded its bounds + id0 = ids[0] + print(f"The {attr_name} attribute is out of the bounds with index {id0}. Bounds : {low[id0]} <= {high[id0]}, value: {attr[id0]}.") + + # I uppdate my set + attr_without_a_problem = attributes_names - attr_with_a_problem + i+=1 + + assert not attr_with_a_problem + +if __name__ == "__main__": + unittest.main() diff --git a/grid2op/tests/test_issue_667.py b/grid2op/tests/test_issue_667.py new file mode 100644 index 00000000..1ef16908 --- /dev/null +++ b/grid2op/tests/test_issue_667.py @@ -0,0 +1,161 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# See AUTHORS.txt and https://github.com/Grid2Op/grid2op/pull/319 +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + +import json +import tempfile +import warnings +import grid2op +from grid2op.Action import BaseAction +from pathlib import Path +import unittest + + +class TestIssue667(unittest.TestCase): + def setUp(self) -> None: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make("educ_case14_storage", + test=True, + action_class=BaseAction, + _add_to_name=type(self).__name__) + self.init_obs = self.env.reset(seed=0, options={"time serie id":0}) + return super().setUp() + + def test_set_line_status(self): + all_but_one_lines_on = self.env.action_space({"set_line_status":[(name, 1) for name in self.env.name_line[0:-1]]}) + all_lines_on = self.env.action_space({"set_line_status":[(name, 1) for name in self.env.name_line[:]]}) + + with tempfile.TemporaryDirectory() as tmpdirname: + tmp_path = Path(tmpdirname) + for act in [all_but_one_lines_on, all_lines_on]: + with open(tmp_path/ "act.json", "w") as f: + json.dump(act.as_serializable_dict(), f) + with open(tmp_path / "act.json", "r") as f: + dict_ = json.load(f) + act2 = self.env.action_space(dict_) + assert act == act2 + + def test_change_line_status(self): + all_but_one_lines_on = self.env.action_space({"change_line_status":[name for name in self.env.name_line[0:-1]]}) + all_lines_on = self.env.action_space({"change_line_status":[name for name in self.env.name_line[:]]}) + + with tempfile.TemporaryDirectory() as tmpdirname: + tmp_path = Path(tmpdirname) + for act in [all_but_one_lines_on, all_lines_on]: + with open(tmp_path/ "act.json", "w") as f: + json.dump(act.as_serializable_dict(), f) + with open(tmp_path / "act.json", "r") as f: + dict_ = json.load(f) + act2 = self.env.action_space(dict_) + assert act == act2 + + def test_redispatching(self): + all_gens_but_one = self.env.action_space({"redispatch":[(name, 0.01) for name in self.env.name_gen[0:-1]]}) + all_gens = self.env.action_space({"redispatch":[(name, 0.01) for name in self.env.name_gen[:]]}) + + with tempfile.TemporaryDirectory() as tmpdirname: + for act in [all_gens_but_one, all_gens]: + with open(Path(tmpdirname) / "act.json", "w") as f: + json.dump(act.as_serializable_dict(), f) + with open(Path(tmpdirname) / "act.json", "r") as f: + dict_ = json.load(f) + act2 = self.env.action_space(dict_) + assert act == act2 + + def test_curtail(self): + all_gens_but_one = self.env.action_space({"curtail":[(name, 0.01) for name in self.env.name_gen[0:-1]]}) + all_gens = self.env.action_space({"curtail":[(name, 0.01) for name in self.env.name_gen[:]]}) + + with tempfile.TemporaryDirectory() as tmpdirname: + for act in [all_gens_but_one, all_gens]: + with open(Path(tmpdirname) / "act.json", "w") as f: + json.dump(act.as_serializable_dict(), f) + with open(Path(tmpdirname) / "act.json", "r") as f: + dict_ = json.load(f) + act2 = self.env.action_space(dict_) + assert act == act2 + + def test_storage(self): + all_gens_but_one = self.env.action_space({"set_storage":[(name, 0.01) for name in self.env.name_storage[0:-1]]}) + all_gens = self.env.action_space({"set_storage":[(name, 0.01) for name in self.env.name_storage[:]]}) + + with tempfile.TemporaryDirectory() as tmpdirname: + for act in [all_gens_but_one, all_gens]: + with open(Path(tmpdirname) / "act.json", "w") as f: + json.dump(act.as_serializable_dict(), f) + with open(Path(tmpdirname) / "act.json", "r") as f: + dict_ = json.load(f) + act2 = self.env.action_space(dict_) + assert act == act2 + + def test_set_bus(self): + for el_type, el_vect in zip(["loads_id", "generators_id", "lines_or_id", "lines_ex_id", "storages_id"], + [self.env.name_load, self.env.name_gen, self.env.name_line, self.env.name_line, self.env.name_storage]): + all_but_one_lines_on = self.env.action_space({"set_bus": {el_type: [(name, 2) for name in el_vect[0:-1]]}}) + all_lines_on = self.env.action_space({"set_bus": {el_type: [(name, 1) for name in el_vect[:]]}}) + + with tempfile.TemporaryDirectory() as tmpdirname: + tmp_path = Path(tmpdirname) + for act in [all_but_one_lines_on, all_lines_on]: + with open(tmp_path/ "act.json", "w") as f: + json.dump(act.as_serializable_dict(), f) + with open(tmp_path / "act.json", "r") as f: + dict_ = json.load(f) + act2 = self.env.action_space(dict_) + assert act == act2 + + def test_change_bus(self): + for el_type, el_vect in zip(["loads_id", "generators_id", "lines_or_id", "lines_ex_id", "storages_id"], + [self.env.name_load, self.env.name_gen, self.env.name_line, self.env.name_line, self.env.name_storage]): + all_but_one_lines_on = self.env.action_space({"change_bus": {el_type: [name for name in el_vect[0:-1]]}}) + all_lines_on = self.env.action_space({"change_bus": {el_type: [name for name in el_vect[:]]}}) + + with tempfile.TemporaryDirectory() as tmpdirname: + tmp_path = Path(tmpdirname) + for act in [all_but_one_lines_on, all_lines_on]: + with open(tmp_path/ "act.json", "w") as f: + json.dump(act.as_serializable_dict(), f) + with open(tmp_path / "act.json", "r") as f: + dict_ = json.load(f) + act2 = self.env.action_space(dict_) + assert act == act2 + + def test_injection(self): + for el_type, el_vect in zip(["prod_p", "prod_v", "load_p", "load_q"], + [self.env.name_gen, self.env.name_gen, self.env.name_load, self.env.name_load]): + all_but_one_lines_on = self.env.action_space({"injection": {el_type: {name: 1.0 for name in el_vect[0:-1]}}}) + all_lines_on = self.env.action_space({"injection": {el_type: {name: 2. for name in el_vect[:]}}}) + + with tempfile.TemporaryDirectory() as tmpdirname: + tmp_path = Path(tmpdirname) + for act in [all_but_one_lines_on, all_lines_on]: + with open(tmp_path/ "act.json", "w") as f: + json.dump(act.as_serializable_dict(), f) + with open(tmp_path / "act.json", "r") as f: + dict_ = json.load(f) + act2 = self.env.action_space(dict_) + assert act == act2 + + def test_shunt(self): + for el_type in ["set_bus", "shunt_p", "shunt_q", "shunt_bus"]: + all_but_one_lines_on = self.env.action_space({"shunt": {el_type: {name: 1 for name in self.env.name_shunt[0:-1]}}}) + all_lines_on = self.env.action_space({"shunt": {el_type: {name: 2 for name in self.env.name_shunt[:]}}}) + + with tempfile.TemporaryDirectory() as tmpdirname: + tmp_path = Path(tmpdirname) + for act in [all_but_one_lines_on, all_lines_on]: + with open(tmp_path/ "act.json", "w") as f: + json.dump(act.as_serializable_dict(), f) + with open(tmp_path / "act.json", "r") as f: + dict_ = json.load(f) + act2 = self.env.action_space(dict_) + assert act == act2 + + +if __name__ == "__main__": + unittest.main() diff --git a/grid2op/tests/test_n_busbar_per_sub.py b/grid2op/tests/test_n_busbar_per_sub.py index f1e59b0c..8a5f7f17 100644 --- a/grid2op/tests/test_n_busbar_per_sub.py +++ b/grid2op/tests/test_n_busbar_per_sub.py @@ -506,18 +506,18 @@ def _aux_test_act_consistent_as_dict(self, act_as_dict, name_xxx, el_id, bus_val 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): + def _aux_test_act_consistent_as_serializable_dict(self, act_as_dict, el_nms, el_id, bus_val, nm_els): 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)] + assert tmp == [(nm_els[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)] + assert tmp == [(nm_els[el_id], bus_val)] def _aux_test_action(self, act : BaseAction, name_xxx, el_id, bus_val, el_nms): assert act.can_affect_something() @@ -526,7 +526,7 @@ def _aux_test_action(self, act : BaseAction, name_xxx, el_id, bus_val, el_nms): 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) + self._aux_test_act_consistent_as_serializable_dict(tmp, el_nms, el_id, bus_val, name_xxx) def _aux_test_set_bus_onebus(self, nm_prop, el_id, bus_val, name_xxx, el_nms): act = self.env.action_space() @@ -610,7 +610,7 @@ def _aux_test_action_shunt(self, act : BaseAction, el_id, bus_val): 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) + self._aux_test_act_consistent_as_serializable_dict(tmp, el_nms, el_id, bus_val, type(act).name_shunt) def test_shunt(self): el_id = 0