diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 206723e3..f958fb1e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -37,14 +37,21 @@ Change Log an observation (no need to do it from the Observation Space) - [ADDED] method to change the reward from the observation (observation_space is not needed anymore): you can use `obs.change_reward` +- [ADDED] a way to automatically set the `experimental_read_from_local_dir` flags + (with automatic class creation). For now it is disable by default, but you can + activate it transparently (see doc) +- [ADDED] TODO the possibility to set the grid in an initial state (using an action) TODO - [FIXED] a small issue that could lead to having "redispatching_unit_commitment_availble" flag set even if the redispatching data was not loded correctly - [FIXED] EducPandaPowerBackend now properly sends numpy array in the class attributes (instead of pandas series) -- [FIXED] an issue when loading back data (with EpisodeData): when there were no storage units +- [FIXED] an issue when loading back data (with `EpisodeData`): when there were no storage units on the grid it did not set properly the "storage relevant" class attributes -- [FIXED] notebook 5 on loading back data. +- [FIXED] a bug in the "gridobj.generate_classes()" function which crashes when no + grid layout was set +- [FIXED] notebook 5 on loading back data with `EpisodeData`. +- [FIXED] converter between backends (could not handle more than 2 busbars) - [IMPROVED] documentation about `obs.simulate` to make it clearer the difference between env.step and obs.simulate on some cases - [IMPROVED] type hints on some methods of `GridObjects` @@ -52,6 +59,10 @@ Change Log save up a bit of computation time. - [IMPROVED] force class attributes to be numpy arrays of proper types when the classes are initialized from the backend. +- [IMPROVED] some (slight) speed improvments when comparing actions or deep copying objects +- [IMPROVED] the way the "grid2op compat" mode is handled +- [IMPROVED] the coverage of the tests in the "test_basic_env_ls.py" to test more in depth lightsim2grid + (creation of multiple environments, grid2op compatibility mode) [1.10.1] - 2024-03-xx ---------------------- diff --git a/docs/conf.py b/docs/conf.py index ed884174..9e0cf8fa 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.10.2.dev0' +release = '1.10.2.dev1' version = '1.10' diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index bc320c84..780f81ea 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -782,12 +782,13 @@ def alert_raised(self) -> np.ndarray: @classmethod def _aux_process_old_compat(cls): + super()._aux_process_old_compat() + # 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: diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index 81450a71..3e2b96d2 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -22,6 +22,7 @@ # python version is probably bellow 3.11 from typing_extensions import Self +import grid2op from grid2op.dtypes import dt_int, dt_float, dt_bool from grid2op.Exceptions import ( EnvError, @@ -66,19 +67,22 @@ class Backend(GridObjects, ABC): All the abstract methods (that need to be implemented for a backend to work properly) are (more information given in the :ref:`create-backend-module` page): - - :func:`Backend.load_grid` - - :func:`Backend.apply_action` - - :func:`Backend.runpf` - - :func:`Backend.get_topo_vect` - - :func:`Backend.generators_info` - - :func:`Backend.loads_info` - - :func:`Backend.lines_or_info` - - :func:`Backend.lines_ex_info` + - :func:`Backend.load_grid` (called once per episode, or if :func:`Backend.reset` is implemented, once for the entire + lifetime of the environment) + - :func:`Backend.apply_action` (called once per episode -initialization- and at least once per step) + - :func:`Backend.runpf` (called once per episode -initialization- and at least once per step) + - :func:`Backend.get_topo_vect` (called once per episode -initialization- and at least once per step) + - :func:`Backend.generators_info` (called once per episode -initialization- and at least once per step) + - :func:`Backend.loads_info` (called once per episode -initialization- and at least once per step) + - :func:`Backend.lines_or_info` (called once per episode -initialization- and at least once per step) + - :func:`Backend.lines_ex_info` (called once per episode -initialization- and at least once per step) And optionally: + - :func:`Backend.reset` will reload the powergrid from the hard drive by default. This is rather slow and we + recommend to overload it. - :func:`Backend.close` (this is mandatory if your backend implementation (`self._grid`) is relying on some - c / c++ code that do not free memory automatically. + c / c++ code that do not free memory automatically.) - :func:`Backend.copy` (not that this is mandatory if your backend implementation (in `self._grid`) cannot be deep copied using the python copy.deepcopy function) [as of grid2op >= 1.7.1 it is no more required. If not implemented, you won't be able to use some of grid2op feature however] @@ -88,8 +92,6 @@ class Backend(GridObjects, ABC): at the "origin" side and just return the "a_or" vector. You want to do something smarter here. - :func:`Backend._disconnect_line`: has a default slow implementation using "apply_action" that might can most likely be optimized in your backend. - - :func:`Backend.reset` will reload the powergrid from the hard drive by default. This is rather slow and we - recommend to overload it. And, if the flag :attr:Backend.shunts_data_available` is set to ``True`` the method :func:`Backend.shunt_info` should also be implemented. @@ -99,12 +101,6 @@ class Backend(GridObjects, ABC): `shunt_to_subid`, `name_shunt` and function `shunt_info` and handle the modification of shunts bus, active value and reactive value in the "apply_action" function). - - In order to be valid and carry out some computations, you should call :func:`Backend.load_grid` and later - :func:`grid2op.Spaces.GridObjects.assert_grid_correct`. It is also more than recommended to call - :func:`Backend.assert_grid_correct_after_powerflow` after the first powerflow. This is all carried ou in the - environment properly. - Attributes ---------- detailed_infos_for_cascading_failures: :class:`bool` @@ -119,9 +115,7 @@ class Backend(GridObjects, ABC): """ IS_BK_CONVERTER : bool = False - - env_name : str = "unknown" - + # action to set me my_bk_act_class : "Optional[grid2op.Action._backendAction._BackendAction]"= None _complete_action_class : "Optional[grid2op.Action.CompleteAction]"= None @@ -224,7 +218,7 @@ def cannot_handle_more_than_2_busbar(self): 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` + :func:`Backend.cannot_handle_more_than_2_busbar` .. note:: From grid2op 1.10.0 it is preferable that your backend calls one of @@ -1418,6 +1412,119 @@ def check_kirchoff(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray diff_v_bus[:, :] = v_bus[:, :, 1] - v_bus[:, :, 0] return p_subs, q_subs, p_bus, q_bus, diff_v_bus + def _fill_names_obj(self): + """fill the name vectors (**eg** name_line) if not done already in the backend. + This function is used to fill the name of an object of a class. It will also check the existence + of these vectors in the class. + """ + cls = type(self) + if self.name_line is None: + if cls.name_line is None: + line_or_to_subid = cls.line_or_to_subid if cls.line_or_to_subid is not None else self.line_or_to_subid + line_ex_to_subid = cls.line_ex_to_subid if cls.line_ex_to_subid is not None else self.line_ex_to_subid + self.name_line = [ + "{}_{}_{}".format(or_id, ex_id, l_id) + for l_id, (or_id, ex_id) in enumerate( + zip(line_or_to_subid, line_ex_to_subid) + ) + ] + self.name_line = np.array(self.name_line) + warnings.warn( + "name_line is None so default line names have been assigned to your grid. " + "(FYI: Line names are used to make the correspondence between the chronics and the backend)" + "This might result in impossibility to load data." + '\n\tIf "env.make" properly worked, you can safely ignore this warning.' + ) + else: + self.name_line = cls.name_line + + if self.name_load is None: + if cls.name_load is None: + load_to_subid = cls.load_to_subid if cls.load_to_subid is not None else self.load_to_subid + self.name_load = [ + "load_{}_{}".format(bus_id, load_id) + for load_id, bus_id in enumerate(load_to_subid) + ] + self.name_load = np.array(self.name_load) + warnings.warn( + "name_load is None so default load names have been assigned to your grid. " + "(FYI: load names are used to make the correspondence between the chronics and the backend)" + "This might result in impossibility to load data." + '\n\tIf "env.make" properly worked, you can safely ignore this warning.' + ) + else: + self.name_load = cls.name_load + + if self.name_gen is None: + if cls.name_gen is None: + gen_to_subid = cls.gen_to_subid if cls.gen_to_subid is not None else self.gen_to_subid + self.name_gen = [ + "gen_{}_{}".format(bus_id, gen_id) + for gen_id, bus_id in enumerate(gen_to_subid) + ] + self.name_gen = np.array(self.name_gen) + warnings.warn( + "name_gen is None so default generator names have been assigned to your grid. " + "(FYI: generator names are used to make the correspondence between the chronics and " + "the backend)" + "This might result in impossibility to load data." + '\n\tIf "env.make" properly worked, you can safely ignore this warning.' + ) + else: + self.name_gen = cls.name_gen + + if self.name_sub is None: + if cls.name_sub is None: + n_sub = cls.n_sub if cls.n_sub is not None and cls.n_sub > 0 else self.n_sub + self.name_sub = ["sub_{}".format(sub_id) for sub_id in range(n_sub)] + self.name_sub = np.array(self.name_sub) + warnings.warn( + "name_sub is None so default substation names have been assigned to your grid. " + "(FYI: substation names are used to make the correspondence between the chronics and " + "the backend)" + "This might result in impossibility to load data." + '\n\tIf "env.make" properly worked, you can safely ignore this warning.' + ) + else: + self.name_sub = cls.name_sub + + if self.name_storage is None: + if cls.name_storage is None: + storage_to_subid = cls.storage_to_subid if cls.storage_to_subid is not None else self.storage_to_subid + self.name_storage = [ + "storage_{}_{}".format(bus_id, sto_id) + for sto_id, bus_id in enumerate(storage_to_subid) + ] + self.name_storage = np.array(self.name_storage) + warnings.warn( + "name_storage is None so default storage unit names have been assigned to your grid. " + "(FYI: storage names are used to make the correspondence between the chronics and " + "the backend)" + "This might result in impossibility to load data." + '\n\tIf "env.make" properly worked, you can safely ignore this warning.' + ) + else: + self.name_storage = cls.name_storage + + if cls.shunts_data_available: + if self.name_shunt is None: + if cls.name_shunt is None: + shunt_to_subid = cls.shunt_to_subid if cls.shunt_to_subid is not None else self.shunt_to_subid + self.name_shunt = [ + "shunt_{}_{}".format(bus_id, sh_id) + for sh_id, bus_id in enumerate(shunt_to_subid) + ] + self.name_shunt = np.array(self.name_shunt) + warnings.warn( + "name_shunt is None so default storage unit names have been assigned to your grid. " + "(FYI: storage names are used to make the correspondence between the chronics and " + "the backend)" + "This might result in impossibility to load data." + '\n\tIf "env.make" properly worked, you can safely ignore this warning.' + ) + else: + self.name_shunt = cls.name_shunt + def load_redispacthing_data(self, path : Union[os.PathLike, str], name : Optional[str]="prods_charac.csv") -> None: @@ -1430,6 +1537,13 @@ def load_redispacthing_data(self, We don't recommend at all to modify this function. + Notes + ----- + Before you use this function, make sure the names of the generators are properly set. + + For example you can either read them from the grid (setting self.name_gen) or call + self._fill_names_obj() beforehand (this later is done in the environment.) + Parameters ---------- path: ``str`` @@ -1458,7 +1572,6 @@ def load_redispacthing_data(self, to change it. """ - self._fill_names() self.redispatching_unit_commitment_availble = False # for redispatching @@ -1574,6 +1687,13 @@ def load_storage_data(self, This method will load everything needed in presence of storage unit on the grid. We don't recommend at all to modify this function. + + Notes + ----- + Before you use this function, make sure the names of the generators are properly set. + + For example you can either read them from the grid (setting self.name_gen) or call + self._fill_names_obj() beforehand (this later is done in the environment.) Parameters ---------- @@ -1623,7 +1743,7 @@ def load_storage_data(self, fullpath = os.path.join(path, name) if not os.path.exists(fullpath): raise BackendError( - f"There are storage unit on the grid, yet we could not locate their description." + f"There are {self.n_storage} storage unit(s) on the grid, yet we could not locate their description." f'Please make sure to have a file "{name}" where the environment data are located.' f'For this environment the location is "{path}"' ) @@ -1983,19 +2103,44 @@ def assert_grid_correct(self) -> None: self.__class__.__name__, self.__class__, ) + # reset the attribute of the grid2op.Backend.Backend class # that can be messed up with depending on the initialization of the backend Backend._clear_class_attribute() # reset totally the grid2op Backend type - # orig_type._clear_class_attribute() - orig_type._clear_grid_dependant_class_attributes() # only reset the attributes that could be modified by user - + + # only reset the attributes that could be modified by the environment while keeping the + # attribute that can be defined in the Backend implementation (eg support of shunt) + orig_type._clear_grid_dependant_class_attributes() + my_cls = type(self) my_cls.my_bk_act_class = _BackendAction.init_grid(my_cls) my_cls._complete_action_class = CompleteAction.init_grid(my_cls) my_cls._complete_action_class._add_shunt_data() my_cls._complete_action_class._update_value_set() my_cls.assert_grid_correct_cls() + self._remove_my_attr_cls() + def _remove_my_attr_cls(self): + """ + INTERNAL + + .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ + + This function is called at the end of :func:`Backend.assert_grid_correct` and it "cleans" the attribute of the + backend object that are stored in the class now, to avoid discrepency between what has been read from the + grid and what have been processed by grid2op (for example in "compatibility" mode, storage are deactivated, so + `self.n_storage` would be different that `type(self).n_storage`) + + For this to work, the grid must first be initialized correctly, with the proper type (name of the environment + in the class name !) + """ + cls = type(self) + if cls._CLS_DICT_EXTENDED is not None: + for attr_nm, val in cls._CLS_DICT_EXTENDED.items(): + if hasattr(self, attr_nm) and hasattr(cls, attr_nm): + if id(getattr(self, attr_nm)) != id(getattr(cls, attr_nm)): + delattr(self, attr_nm) + def assert_grid_correct_after_powerflow(self) -> None: """ INTERNAL diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index 5f53f449..db7acf1a 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -176,7 +176,6 @@ def __init__( self._number_true_line = -1 self._corresp_name_fun = {} self._get_vector_inj = {} - self.dim_topo = -1 self._vars_action = BaseAction.attr_list_vect self._vars_action_set = BaseAction.attr_list_vect self.cst_1 = dt_float(1.0) @@ -554,6 +553,8 @@ def load_grid(self, self.name_sub = ["sub_{}".format(i) for i, row in self._grid.bus.iterrows()] self.name_sub = np.array(self.name_sub) + self.n_shunt = self._grid.shunt.shape[0] + # "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 ??? @@ -653,6 +654,20 @@ def _init_private_attrs(self) -> None: self._what_object_where[sub_id].append(("storage", "bus", i)) self.dim_topo = self.sub_info.sum() + + # shunts data + self.shunt_to_subid = np.zeros(self.n_shunt, dtype=dt_int) - 1 + name_shunt = [] + # TODO read name from the grid if provided + for i, (_, row) in enumerate(self._grid.shunt.iterrows()): + bus = int(row["bus"]) + name_shunt.append("shunt_{bus}_{index_shunt}".format(**row, index_shunt=i)) + self.shunt_to_subid[i] = bus + self.name_shunt = np.array(name_shunt).astype(str) + self._sh_vnkv = self._grid.bus["vn_kv"][self.shunt_to_subid].values.astype( + dt_float + ) + self._compute_pos_big_topo() # utilities for imeplementing apply_action @@ -717,21 +732,6 @@ def _init_private_attrs(self) -> None: self.storage_v = np.full(self.n_storage, dtype=dt_float, fill_value=np.NaN) self._nb_bus_before = None - # shunts data - self.n_shunt = self._grid.shunt.shape[0] - self.shunt_to_subid = np.zeros(self.n_shunt, dtype=dt_int) - 1 - name_shunt = [] - # TODO read name from the grid if provided - for i, (_, row) in enumerate(self._grid.shunt.iterrows()): - bus = int(row["bus"]) - name_shunt.append("shunt_{bus}_{index_shunt}".format(**row, index_shunt=i)) - self.shunt_to_subid[i] = bus - self.name_shunt = np.array(name_shunt) - self._sh_vnkv = self._grid.bus["vn_kv"][self.shunt_to_subid].values.astype( - dt_float - ) - # self.shunts_data_available = True # TODO shunts_data_available - # store the topoid -> objid self._big_topo_to_obj = [(None, None) for _ in range(self.dim_topo)] nm_ = "load" @@ -792,7 +792,12 @@ def _init_private_attrs(self) -> None: ) # will be initialized in the "assert_grid_correct" def storage_deact_for_backward_comaptibility(self) -> None: - self._init_private_attrs() + cls = type(self) + self.storage_theta = np.full(cls.n_storage, fill_value=np.NaN, dtype=dt_float) + self.storage_p = np.full(cls.n_storage, dtype=dt_float, fill_value=np.NaN) + self.storage_q = np.full(cls.n_storage, dtype=dt_float, fill_value=np.NaN) + self.storage_v = np.full(cls.n_storage, dtype=dt_float, fill_value=np.NaN) + self._topo_vect = self._get_topo_vect() def _convert_id_topo(self, id_big_topo): """ @@ -1178,16 +1183,6 @@ def runpf(self, is_dc : bool=False) -> Tuple[bool, Union[Exception, None]]: msg = exc_.__str__() return False, BackendError(f'powerflow diverged with error :"{msg}"') - def assert_grid_correct(self) -> None: - """ - INTERNAL - - .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ - - This is done as it should be by the Environment - """ - super().assert_grid_correct() - def _reset_all_nan(self) -> None: self.p_or[:] = np.NaN self.q_or[:] = np.NaN diff --git a/grid2op/Chronics/gridValue.py b/grid2op/Chronics/gridValue.py index bb8667cb..4e7bc9e2 100644 --- a/grid2op/Chronics/gridValue.py +++ b/grid2op/Chronics/gridValue.py @@ -10,6 +10,7 @@ from datetime import datetime, timedelta from abc import ABC, abstractmethod +import grid2op from grid2op.dtypes import dt_int from grid2op.Space import RandomObject from grid2op.Exceptions import EnvError @@ -800,3 +801,22 @@ def fast_forward(self, nb_timestep): """ for _ in range(nb_timestep): self.load_next() + + def get_init_action(self) -> "grid2op.Action.playableAction.PlayableAction": + """ + .. versionadded 1.10.2 + + It is used when the environment is reset (*ie* when :func:`grid2op.Environment.Environment.reset` is called) + to set the grid in its "original" state. + + Before grid2op 1.10.2 the original state is necessarily "everything connected together". + + For later version, we let the possibility to set, in the "time series folder" (or time series generators) + the possibility to change the initial condition of the grid. + + Returns + ------- + grid2op.Action.playableAction.PlayableAction + The desired intial configuration of the grid + """ + return None diff --git a/grid2op/Converter/BackendConverter.py b/grid2op/Converter/BackendConverter.py index ca0b431a..dfd0ced6 100644 --- a/grid2op/Converter/BackendConverter.py +++ b/grid2op/Converter/BackendConverter.py @@ -106,18 +106,24 @@ def __init__( difcf = detailed_infos_for_cascading_failures if kwargs_source_backend is None: kwargs_source_backend = {} - self.source_backend = source_backend_class( + + #: represents the backend used for the order / name of the elements + #: Agent will not see any difference between the converter and this backend + self.source_backend : Backend = source_backend_class( detailed_infos_for_cascading_failures=difcf, **kwargs_source_backend - ) # the one for the order of the elements + ) if kwargs_target_backend is None: kwargs_target_backend = {} - self.target_backend = target_backend_class( + + #: represents the backend used to compute the powerflows + self.target_backend : Backend = target_backend_class( detailed_infos_for_cascading_failures=difcf, **kwargs_target_backend ) # the one to computes powerflow - # if the target backend (the one performing the powerflows) needs a different file - self.target_backend_grid_path = target_backend_grid_path + + #: if the target backend (the one performing the powerflows) needs a different file + self.target_backend_grid_path :str = target_backend_grid_path # key: name in the source backend, value name in the target backend, for the substations self.sub_source_target = sub_source_target @@ -156,6 +162,14 @@ def __init__( # TODO storage check all this class ! + the doc of the backend def load_grid(self, path=None, filename=None): + # register the "n_busbar_per_sub" (set for the backend class) + # TODO in case source supports the "more than 2" feature but not target + # it's unclear how I can "reload" the grid... + from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB + type(self.source_backend).set_n_busbar_per_sub(DEFAULT_N_BUSBAR_PER_SUB) + type(self.target_backend).set_n_busbar_per_sub(DEFAULT_N_BUSBAR_PER_SUB) + self.cannot_handle_more_than_2_busbar() + self.source_backend.load_grid(path, filename) # and now i load the target backend if self.target_backend_grid_path is not None: @@ -163,7 +177,18 @@ def load_grid(self, path=None, filename=None): else: # both source and target backend understands the same format self.target_backend.load_grid(path, filename) - + + # TODO in case source supports the "more than 2" feature but not target + # it's unclear how I can "reload" the grid... + # if (not self.target_backend._missing_two_busbars_support_info and + # not self.source_backend._missing_two_busbars_support_info + # ): + # ??? + # else: + # # at least one backend cannot handle the number of busbars, so I deactivate it for all + # self.target_backend.cannot_handle_more_than_2_busbar() + # self.source_backend.cannot_handle_more_than_2_busbar() + def _assert_same_grid(self): """basic assertion that self and the target backend have the same grid but not necessarily the same object at the same place of course""" @@ -550,6 +575,12 @@ def assert_grid_correct_after_powerflow(self): super().assert_grid_correct_after_powerflow() self._sh_vnkv = self.target_backend._sh_vnkv + def _fill_names_obj(self): + self.target_backend._fill_names_obj() + self.source_backend._fill_names_obj() + for attr_nm in ["name_line", "name_gen", "name_load", "name_sub", "name_storage"]: + setattr(self, attr_nm, copy.deepcopy(getattr(self.source_backend, attr_nm))) + def reset(self, grid_path, grid_filename=None): """ Reload the power grid. diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index 8a08372e..b687899c 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -8,6 +8,7 @@ from datetime import datetime +import shutil import logging import time import copy @@ -3201,6 +3202,7 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, # somehow "env.step()" or "env.reset()" self._has_just_been_seeded = False + cls = type(self) has_error = True is_done = False is_illegal = False @@ -3217,7 +3219,7 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, detailed_info = [] init_disp = 1.0 * action._redispatch # dispatching action init_alert = None - if type(self).dim_alerts > 0: + if cls.dim_alerts > 0: init_alert = copy.deepcopy(action._raise_alert) action_storage_power = 1.0 * action._storage_power # battery information @@ -3231,7 +3233,6 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, 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() @@ -3342,7 +3343,6 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, ) else: has_error = True - except StopIteration: # episode is over is_done = True @@ -4085,6 +4085,50 @@ def generate_classes(self, *, local_dir_id=None, _guard=None, _is_base_env__=Tru with open(os.path.join(sys_path, "__init__.py"), mode, encoding="utf-8") as f: f.write(_init_txt) + def _forget_classes(self): + """ + This function allows python to "forget" the classes created at the initialization of the environment. + + It should not be used in most cases and is reserved for internal use only. + + .. versionadded: 1.10.2 + Function added following the new behaviour introduced in this version. + + """ + from grid2op.MakeEnv.PathUtils import USE_CLASS_IN_FILE + if not USE_CLASS_IN_FILE: + return + pass + + def remove_all_class_folders(self): + """ + This function allows python to remove all the files containing all the classes + in the environment. + + .. warning:: + If you have pending grid2op "job" using this environment, they will most likely crash + so use with extra care ! + + It should not be used in most cases and is reserved for internal use only. + + .. versionadded: 1.10.2 + Function added following the new behaviour introduced in this version. + + """ + directory_path = os.path.join(self.get_path_env(), "_grid2op_classes") + try: + with os.scandir(directory_path) as entries: + for entry in entries: + try: + if entry.is_file(): + os.unlink(entry.path) + else: + shutil.rmtree(entry.path) + except (OSError, FileNotFoundError): + pass + except OSError: + pass + def __del__(self): """when the environment is garbage collected, free all the memory, including cross reference to itself in the observation space.""" if hasattr(self, "_BaseEnv__closed") and not self.__closed: diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index acd1228d..530d9de4 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -118,6 +118,7 @@ def __init__( _compat_glop_version=None, _read_from_local_dir=True, _is_test=False, + _allow_loaded_backend=False, ): BaseEnv.__init__( self, @@ -161,10 +162,12 @@ def __init__( ) self.name = name self._read_from_local_dir = _read_from_local_dir + + #: starting grid2Op 1.11 classes are stored on the disk when an environment is created + #: so the "environment" is created twice (one to generate the class and then correctly to load them) + self._allow_loaded_backend : bool = _allow_loaded_backend - # for gym compatibility (initialized below) - # self.action_space = None - # self.observation_space = None + # for gym compatibility (action_spacen and observation_space initialized below) self.reward_range = None self._viewer = None self.metadata = None @@ -231,7 +234,7 @@ def _init_backend( 'grid2op.Backend class, type provided is "{}"'.format(type(backend)) ) self.backend = backend - if self.backend.is_loaded and self._init_obs is None: + if self.backend.is_loaded and self._init_obs is None and not self._allow_loaded_backend: raise EnvError( "Impossible to use the same backend twice. Please create your environment with a " "new backend instance (new object)." @@ -239,19 +242,29 @@ def _init_backend( need_process_backend = False if not self.backend.is_loaded: + if hasattr(self.backend, "init_pp_backend") and self.backend.init_pp_backend is not None: + # hack for lightsim2grid ... + if type(self.backend.init_pp_backend)._INIT_GRID_CLS is not None: + type(self.backend.init_pp_backend)._INIT_GRID_CLS._clear_grid_dependant_class_attributes() + type(self.backend.init_pp_backend)._clear_grid_dependant_class_attributes() + # usual case: the backend is not loaded # NB it is loaded when the backend comes from an observation for # example if self._read_from_local_dir is not None: # test to support pickle conveniently - self.backend._PATH_ENV = self.get_path_env() + self.backend._PATH_GRID_CLASSES = 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 type(self.backend).set_env_name(self.name) type(self.backend).set_n_busbar_per_sub(self._n_busbar) + if self._compat_glop_version is not None: + type(self.backend).glop_version = self._compat_glop_version self.backend.load_grid( self._init_grid_path ) # the real powergrid of the environment + self.backend.load_storage_data(self.get_path_env()) + self.backend._fill_names_obj() try: self.backend.load_redispacthing_data(self.get_path_env()) except BackendError as exc_: @@ -259,14 +272,12 @@ def _init_backend( warnings.warn(f"Impossible to load redispatching data. This is not an error but you will not be able " f"to use all grid2op functionalities. " f"The error was: \"{exc_}\"") - self.backend.load_storage_data(self.get_path_env()) exc_ = self.backend.load_grid_layout(self.get_path_env()) if exc_ is not None: warnings.warn( f"No layout have been found for you grid (or the layout provided was corrupted). You will " f'not be able to use the renderer, plot the grid etc. The error was "{exc_}"' ) - self.backend.is_loaded = True # alarm set up self.load_alarm_data() @@ -274,6 +285,7 @@ def _init_backend( # to force the initialization of the backend to the proper type self.backend.assert_grid_correct() + self.backend.is_loaded = True need_process_backend = True self._handle_compat_glop_version(need_process_backend) @@ -325,9 +337,8 @@ def _init_backend( ) # action affecting the grid that will be made by the agent - bk_type = type( - self.backend - ) # be careful here: you need to initialize from the class, and not from the object + # be careful here: you need to initialize from the class, and not from the object + bk_type = type(self.backend) self._rewardClass = rewardClass self._actionClass = actionClass.init_grid(gridobj=bk_type) self._actionClass._add_shunt_data() @@ -435,8 +446,25 @@ def _init_backend( # test the backend returns object of the proper size if need_process_backend: - self.backend.assert_grid_correct_after_powerflow() + + # hack to fix an issue with lightsim2grid... + # (base class is not reset correctly, will be fixed ASAP) + base_cls_ls = None + if hasattr(self.backend, "init_pp_backend") and self.backend.init_pp_backend is not None: + base_cls_ls = type(self.backend.init_pp_backend) + self.backend.assert_grid_correct_after_powerflow() + + # hack to fix an issue with lightsim2grid... + # (base class is not reset correctly, will be fixed ASAP) + if hasattr(self.backend, "init_pp_backend") and self.backend.init_pp_backend is not None: + if self.backend._INIT_GRID_CLS is not None: + # the init grid class has already been properly computed + self.backend._INIT_GRID_CLS._clear_grid_dependant_class_attributes() + elif base_cls_ls is not None: + # we need to clear the class of the original type as it has not been properly computed + base_cls_ls._clear_grid_dependant_class_attributes() + # for gym compatibility self.reward_range = self._reward_helper.range() self._viewer = None @@ -503,81 +531,10 @@ def _handle_compat_glop_version(self, need_process_backend): "read back data (for example with EpisodeData) that were stored with previous " "grid2op version." ) - if need_process_backend: - self.backend.set_env_name(f"{self.name}_{self._compat_glop_version}") - cls_bk = type(self.backend) - cls_bk.glop_version = self._compat_glop_version - if cls_bk.glop_version == cls_bk.BEFORE_COMPAT_VERSION: - # oldest version: no storage and no curtailment available - # deactivate storage - # recompute the topology vector (more or less everything need to be adjusted... - stor_locs = [pos for pos in cls_bk.storage_pos_topo_vect] - for stor_loc in sorted(stor_locs, reverse=True): - for vect in [ - cls_bk.load_pos_topo_vect, - cls_bk.gen_pos_topo_vect, - cls_bk.line_or_pos_topo_vect, - cls_bk.line_ex_pos_topo_vect, - ]: - vect[vect >= stor_loc] -= 1 - - # 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 = (cls_bk.storage_to_subid == sub_id).nonzero()[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( - [ - cls_bk.load_to_sub_pos, - cls_bk.gen_to_sub_pos, - cls_bk.line_or_to_sub_pos, - cls_bk.line_ex_to_sub_pos, - ], - [ - cls_bk.load_to_subid, - cls_bk.gen_to_subid, - cls_bk.line_or_to_subid, - cls_bk.line_ex_to_subid, - ], - ): - vect[(vect >= stor_loc) & (sub_id_me == sub_id)] -= 1 - - # remove storage from the number of element in the substation - for sub_id in range(cls_bk.n_sub): - cls_bk.sub_info[sub_id] -= (cls_bk.storage_to_subid == sub_id).sum() - # remove storage from the total number of element - cls_bk.dim_topo -= cls_bk.n_storage - - # recompute this private member - cls_bk._topo_vect_to_sub = np.repeat( - np.arange(cls_bk.n_sub), repeats=cls_bk.sub_info - ) - self.backend._topo_vect_to_sub = np.repeat( - np.arange(cls_bk.n_sub), repeats=cls_bk.sub_info - ) - new_grid_objects_types = cls_bk.grid_objects_types - new_grid_objects_types = new_grid_objects_types[ - new_grid_objects_types[:, cls_bk.STORAGE_COL] == -1, : - ] - cls_bk.grid_objects_types = 1 * new_grid_objects_types - self.backend.grid_objects_types = 1 * new_grid_objects_types - - # erase all trace of storage units - cls_bk.set_no_storage() - Environment.deactivate_storage(self.backend) - - if need_process_backend: - # the following line must be called BEFORE "self.backend.assert_grid_correct()" ! - self.backend.storage_deact_for_backward_comaptibility() - - # and recomputes everything while making sure everything is consistent - self.backend.assert_grid_correct() - type(self.backend)._topo_vect_to_sub = np.repeat( - np.arange(cls_bk.n_sub), repeats=cls_bk.sub_info - ) - type(self.backend).grid_objects_types = new_grid_objects_types + if need_process_backend: + # the following line must be called BEFORE "self.backend.assert_grid_correct()" ! + self.backend.storage_deact_for_backward_comaptibility() def _voltage_control(self, agent_action, prod_v_chronics): """ @@ -871,9 +828,12 @@ def reset_grid(self): self.backend.set_thermal_limit(self._thermal_limit_a.astype(dt_float)) self._backend_action = self._backend_action_class() - self.nb_time_step = -1 # to have init obs at step 1 - do_nothing = self._helper_action_env({}) - *_, fail_to_start, info = self.step(do_nothing) + self.nb_time_step = -1 # to have init obs at step 1 (and to prevent 'setting to proper state' "action" to be illegal) + init_action = self.chronics_handler.get_init_action() + if init_action is None: + # default behaviour for grid2op < 1.10.2 + init_action = self._helper_action_env({}) + *_, fail_to_start, info = self.step(init_action) if fail_to_start: raise Grid2OpException( "Impossible to initialize the powergrid, the powerflow diverge at iteration 0. " diff --git a/grid2op/Episode/CompactEpisodeData.py b/grid2op/Episode/CompactEpisodeData.py index 3ed6af14..30a13831 100644 --- a/grid2op/Episode/CompactEpisodeData.py +++ b/grid2op/Episode/CompactEpisodeData.py @@ -6,22 +6,13 @@ # 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 os -import warnings -import copy import numpy as np -import grid2op -from grid2op.Exceptions import ( - Grid2OpException, - EnvError, - IncorrectNumberOfElements, - NonFiniteElement, -) from grid2op.Action import ActionSpace from grid2op.Observation import ObservationSpace from pathlib import Path as p + class CompactEpisodeData(): """ @@ -222,7 +213,6 @@ def store_metadata(self): """ Store this Episode's meta data to disk. """ - print({k:(v,type(v)) for k,v in self.meta.items()}) with open(self.exp_dir / f"{self.ep_id}_metadata.json", "w", encoding="utf-8") as f: json.dump(self.meta, f, indent=4, sort_keys=True) diff --git a/grid2op/MakeEnv/MakeFromPath.py b/grid2op/MakeEnv/MakeFromPath.py index f72c4dc7..7da732c5 100644 --- a/grid2op/MakeEnv/MakeFromPath.py +++ b/grid2op/MakeEnv/MakeFromPath.py @@ -7,19 +7,25 @@ # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. import os +import time import importlib.util import numpy as np import json import warnings +from grid2op.MakeEnv.PathUtils import USE_CLASS_IN_FILE from grid2op.Environment import Environment from grid2op.Backend import Backend, PandaPowerBackend from grid2op.Opponent.opponentSpace import OpponentSpace from grid2op.Parameters import Parameters -from grid2op.Chronics import ChronicsHandler, ChangeNothing, FromNPY, FromChronix2grid -from grid2op.Chronics import GridStateFromFile, GridValue +from grid2op.Chronics import (ChronicsHandler, + ChangeNothing, + FromNPY, + FromChronix2grid, + GridStateFromFile, + GridValue) from grid2op.Action import BaseAction, DontAct -from grid2op.Exceptions import * +from grid2op.Exceptions import EnvError from grid2op.Observation import CompleteObservation, BaseObservation from grid2op.Reward import BaseReward, L2RPNReward from grid2op.Rules import BaseRules, DefaultRules @@ -864,31 +870,108 @@ def make_from_dataset_path( if observation_backend_kwargs is observation_backend_kwargs_cfg_: observation_backend_kwargs = None - if experimental_read_from_local_dir: + # new in 1.10.2 : + allow_loaded_backend = False + classes_path = None + if USE_CLASS_IN_FILE: sys_path = os.path.join(os.path.split(grid_path_abs)[0], "_grid2op_classes") if not os.path.exists(sys_path): - raise RuntimeError( - "Attempting to load the grid classes from the env path. Yet the directory " - "where they should be placed does not exists. Did you call `env.generate_classes()` " - "BEFORE creating an environment with `experimental_read_from_local_dir=True` ?" - ) - if not os.path.isdir(sys_path) or not os.path.exists( - os.path.join(sys_path, "__init__.py") - ): - raise RuntimeError( - f"Impossible to load the classes from the env path. There is something that is " - f"not a directory and that is called `_grid2op_classes`. " - f'Please remove "{sys_path}" and call `env.generate_classes()` where env is an ' - f"environment created with `experimental_read_from_local_dir=False` (default)" - ) - # sys_path = os.path.join(os.path.split(grid_path_abs)[0], "_grid2op_classes") - # if not os.path.exists(sys_path): - # try: - # os.mkdir(sys_path) - # except FileExistsError: - # pass - + try: + os.mkdir(sys_path) + except FileExistsError: + # if another process created it, no problem + pass + + # TODO: automatic delete the directory if needed + + # TODO: check the "new" path works + + # TODO: in the BaseEnv.generate_classes make sure the classes are added to the "__init__" if the file is created + # TODO: make that only if backend can be copied ! + + # TODO: check the hash thingy is working in baseEnv._aux_gen_classes (currently a pdb) + + # TODO: check that previous behaviour is working correctly + + # TODO: create again the environment with the proper "read from local_dir" + + # TODO check that it works if the backend changes, if shunt / no_shunt if name of env changes etc. + + # TODO: what if it cannot write on disk => fallback to previous behaviour + + # TODO: allow for a way to disable that (with env variable or config in grid2op) + # TODO: keep only one environment that will delete the files (with a flag in its constructor) + + # TODO: explain in doc new behaviour with regards to "class in file" + + # TODO: basic CI for this "new" mode + if not experimental_read_from_local_dir: + init_env = Environment(init_env_path=os.path.abspath(dataset_path), + init_grid_path=grid_path_abs, + chronics_handler=data_feeding, + backend=backend, + parameters=param, + name=name_env + _add_to_name, + names_chronics_to_backend=names_chronics_to_backend, + actionClass=action_class, + observationClass=observation_class, + rewardClass=reward_class, + legalActClass=gamerules_class, + voltagecontrolerClass=volagecontroler_class, + other_rewards=other_rewards, + opponent_space_type=opponent_space_type, + opponent_action_class=opponent_action_class, + opponent_class=opponent_class, + opponent_init_budget=opponent_init_budget, + opponent_attack_duration=opponent_attack_duration, + opponent_attack_cooldown=opponent_attack_cooldown, + opponent_budget_per_ts=opponent_budget_per_ts, + opponent_budget_class=opponent_budget_class, + kwargs_opponent=kwargs_opponent, + has_attention_budget=has_attention_budget, + attention_budget_cls=attention_budget_class, + kwargs_attention_budget=kwargs_attention_budget, + logger=logger, + n_busbar=n_busbar, + _compat_glop_version=_compat_glop_version, + _read_from_local_dir=None, # first environment to generate the classes and save them + kwargs_observation=kwargs_observation, + observation_bk_class=observation_backend_class, + observation_bk_kwargs=observation_backend_kwargs, + ) + this_local_dir = f"{time.time()}_{os.getpid()}" + init_env.generate_classes(local_dir_id=this_local_dir) + init_env.backend = None # to avoid to close the backend when init_env is deleted + classes_path = os.path.join(sys_path, this_local_dir) + # to force the reading back of the classes from the hard drive + init_env._forget_classes() # TODO not implemented + init_env.close() + else: + classes_path = sys_path + allow_loaded_backend = True + else: + # legacy behaviour (<= 1.10.1 behaviour) + classes_path = None if not experimental_read_from_local_dir else experimental_read_from_local_dir + if experimental_read_from_local_dir: + sys_path = os.path.join(os.path.split(grid_path_abs)[0], "_grid2op_classes") + if not os.path.exists(sys_path): + raise RuntimeError( + "Attempting to load the grid classes from the env path. Yet the directory " + "where they should be placed does not exists. Did you call `env.generate_classes()` " + "BEFORE creating an environment with `experimental_read_from_local_dir=True` ?" + ) + if not os.path.isdir(sys_path) or not os.path.exists( + os.path.join(sys_path, "__init__.py") + ): + raise RuntimeError( + f"Impossible to load the classes from the env path. There is something that is " + f"not a directory and that is called `_grid2op_classes`. " + f'Please remove "{sys_path}" and call `env.generate_classes()` where env is an ' + f"environment created with `experimental_read_from_local_dir=False` (default)" + ) + # Finally instantiate env from config & overrides + # including (if activated the new grid2op behaviour) env = Environment( init_env_path=os.path.abspath(dataset_path), init_grid_path=grid_path_abs, @@ -918,7 +1001,8 @@ def make_from_dataset_path( logger=logger, n_busbar=n_busbar, _compat_glop_version=_compat_glop_version, - _read_from_local_dir=experimental_read_from_local_dir, + _read_from_local_dir=classes_path, + _allow_loaded_backend=allow_loaded_backend, kwargs_observation=kwargs_observation, observation_bk_class=observation_backend_class, observation_bk_kwargs=observation_backend_kwargs, diff --git a/grid2op/MakeEnv/PathUtils.py b/grid2op/MakeEnv/PathUtils.py index 8551f39c..99db27b5 100644 --- a/grid2op/MakeEnv/PathUtils.py +++ b/grid2op/MakeEnv/PathUtils.py @@ -10,16 +10,47 @@ import os import json + DEFAULT_PATH_CONFIG = os.path.expanduser("~/.grid2opconfig.json") DEFAULT_PATH_DATA = os.path.expanduser("~/data_grid2op") +USE_CLASS_IN_FILE = False # set to True for new behaviour (will be set to True in grid2op 1.11) + + KEY_DATA_PATH = "data_path" +KEY_CLASS_IN_FILE = "class_in_file" + +def str_to_bool(string: str) -> bool: + """convert a "string" to a boolean, with the convention: + + - "t", "y", "yes", "true", "True", "TRUE" etc. returns True + - "false", "False", "FALSE" etc. returns False + - "1" returns True + - "0" returns False + + """ + string_ = string.lower() + if string_ in ["t", "true", "y", "yes", "on", "1"]: + return True + if string_ in ["f", "false", "n", "no", "off", "0"]: + return False + raise ValueError(f"Uknown way to convert `{string}` to a boolean. Please either set it to \"1\" or \"0\"") + + if os.path.exists(DEFAULT_PATH_CONFIG): with open(DEFAULT_PATH_CONFIG, "r") as f: dict_ = json.load(f) if KEY_DATA_PATH in dict_: DEFAULT_PATH_DATA = os.path.abspath(dict_[KEY_DATA_PATH]) + + if KEY_CLASS_IN_FILE in dict_: + USE_CLASS_IN_FILE = bool(dict_[KEY_CLASS_IN_FILE]) + if KEY_CLASS_IN_FILE in os.environ: + try: + USE_CLASS_IN_FILE = str_to_bool(os.environ[KEY_CLASS_IN_FILE]) + except ValueError as exc: + raise RuntimeError(f"Impossible to read the behaviour from `{KEY_CLASS_IN_FILE}` environment variable") from exc def _create_path_folder(data_path): diff --git a/grid2op/Rules/DefaultRules.py b/grid2op/Rules/DefaultRules.py index 4685c38a..9e4832a6 100644 --- a/grid2op/Rules/DefaultRules.py +++ b/grid2op/Rules/DefaultRules.py @@ -27,6 +27,11 @@ class DefaultRules(LookParam, PreventDiscoStorageModif, PreventReconnection): def __call__(self, action, env): """ See :func:`BaseRules.__call__` for a definition of the _parameters of this function. + + ..versionchanged:: 1.10.2 + In grid2op 1.10.2 this function is not called when the environment is reset: + The "action" made by the environment to set the environment in the desired state is always legal + """ is_legal, reason = LookParam.__call__(self, action, env) if not is_legal: diff --git a/grid2op/Rules/LookParam.py b/grid2op/Rules/LookParam.py index c2841233..e2e463fe 100644 --- a/grid2op/Rules/LookParam.py +++ b/grid2op/Rules/LookParam.py @@ -29,6 +29,11 @@ class LookParam(BaseRules): def __call__(self, action, env): """ See :func:`BaseRules.__call__` for a definition of the parameters of this function. + + ..versionchanged:: 1.10.2 + In grid2op 1.10.2 this function is not called when the environment is reset: + The "action" made by the environment to set the environment in the desired state is always legal + """ # at first iteration, env.current_obs is None... powerline_status = env.get_current_line_status() diff --git a/grid2op/Rules/PreventDiscoStorageModif.py b/grid2op/Rules/PreventDiscoStorageModif.py index 97071666..fb20ae34 100644 --- a/grid2op/Rules/PreventDiscoStorageModif.py +++ b/grid2op/Rules/PreventDiscoStorageModif.py @@ -23,6 +23,11 @@ class PreventDiscoStorageModif(BaseRules): def __call__(self, action, env): """ See :func:`BaseRules.__call__` for a definition of the parameters of this function. + + ..versionchanged:: 1.10.2 + In grid2op 1.10.2 this function is not called when the environment is reset: + The "action" made by the environment to set the environment in the desired state is always legal + """ env_cls = type(env) if env_cls.n_storage == 0: diff --git a/grid2op/Rules/PreventReconnection.py b/grid2op/Rules/PreventReconnection.py index 73e38a01..d1356ddd 100644 --- a/grid2op/Rules/PreventReconnection.py +++ b/grid2op/Rules/PreventReconnection.py @@ -27,6 +27,10 @@ def __call__(self, action, env): due to an overflow. See :func:`BaseRules.__call__` for a definition of the parameters of this function. + + ..versionchanged:: 1.10.2 + In grid2op 1.10.2 this function is not called when the environment is reset: + The "action" made by the environment to set the environment in the desired state is always legal """ # at first iteration, env.current_obs is None... diff --git a/grid2op/Rules/RulesChecker.py b/grid2op/Rules/RulesChecker.py index a362344a..6f857c45 100644 --- a/grid2op/Rules/RulesChecker.py +++ b/grid2op/Rules/RulesChecker.py @@ -81,4 +81,8 @@ def __call__(self, action, env): reason: A grid2op IllegalException given the reason for which the action is illegal """ + if env.nb_time_step <= 0: + # only access when env is reset + return True, None + return self.legal_action(action, env) diff --git a/grid2op/Rules/rulesByArea.py b/grid2op/Rules/rulesByArea.py index fd4978c1..4c01dcce 100644 --- a/grid2op/Rules/rulesByArea.py +++ b/grid2op/Rules/rulesByArea.py @@ -94,6 +94,11 @@ def initialize(self, env): def __call__(self, action, env): """ See :func:`BaseRules.__call__` for a definition of the _parameters of this function. + + ..versionchanged:: 1.10.2 + In grid2op 1.10.2 this function is not called when the environment is reset: + The "action" made by the environment to set the environment in the desired state is always legal + """ is_legal, reason = PreventDiscoStorageModif.__call__(self, action, env) if not is_legal: diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 4519aa24..93569b2f 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -475,7 +475,11 @@ class GridObjects: BEFORE_COMPAT_VERSION = "neurips_2020_compat" glop_version = grid2op.__version__ + + _INIT_GRID_CLS = None # do not modify that, this is handled by grid2op automatically _PATH_GRID_CLASSES = None # especially do not modify that + _CLS_DICT = None # init once to avoid yet another serialization of the class as dict (in make_cls_dict) + _CLS_DICT_EXTENDED = None # init once to avoid yet another serialization of the class as dict (in make_cls_dict) SUB_COL = 0 LOA_COL = 1 @@ -663,8 +667,18 @@ def tell_dim_alert(cls, dim_alerts: int) -> None: if dim_alerts: cls.assistant_warning_type = "by_line" + @classmethod + def _reset_cls_dict(cls): + cls._CLS_DICT = None + cls._CLS_DICT_EXTENDED = None + @classmethod def _clear_class_attribute(cls) -> None: + """Also calls :func:`GridObjects._clear_grid_dependant_class_attributes` : this clear the attribute that + may be backend dependant too (eg shunts_data) + + This clear the class as if it was defined in grid2op directly. + """ cls.shunts_data_available = False cls.n_busbar_per_sub = DEFAULT_N_BUSBAR_PER_SUB @@ -703,8 +717,12 @@ def _clear_class_attribute(cls) -> None: @classmethod def _clear_grid_dependant_class_attributes(cls) -> None: + """reset to an original state all the class attributes that depends on an environment""" + cls._reset_cls_dict() + cls._INIT_GRID_CLS = None # do not modify that, this is handled by grid2op automatically + cls._PATH_GRID_CLASSES = None # especially do not modify that + cls.glop_version = grid2op.__version__ - cls._PATH_GRID_CLASSES = None cls.SUB_COL = 0 cls.LOA_COL = 1 @@ -1295,25 +1313,54 @@ def _aux_pos_big_topo(cls, vect_to_subid, vect_to_sub_pos): res[i] = obj_before + my_pos return res - def _init_class_attr(self, obj=None): + def _init_class_attr(self, obj=None, _topo_vect_only=False): """Init the class attribute from an instance of the class THIS IS NOT A CLASS ATTR obj should be an object and NOT a class ! + + Notes + ------- + _topo_vect_only: this function is called once when the backend is initialized in `backend.load_grid` + (in `backend._compute_pos_big_topo`) and then once when everything is set up + (after redispatching and storage data are loaded). + + This is why I need the `_topo_vect_only` flag that tells this function when it's called only for + `topo_vect` related attributed """ + if obj is None: obj = self - cls = type(self) + cls = type(self) cls_as_dict = {} - GridObjects._make_cls_dict_extended(obj, cls_as_dict, as_list=False) + GridObjects._make_cls_dict_extended(obj, cls_as_dict, as_list=False, _topo_vect_only=_topo_vect_only) for attr_nm, attr in cls_as_dict.items(): - setattr(cls, attr_nm, attr) + if _topo_vect_only: + # safety guard: only set the attribute needed for the computation of the topo_vect vector + # this should be the only attribute in cls_as_dict but let's be sure + if (attr_nm.endswith("to_subid") or + attr_nm.endswith("to_sub_pos") or + attr_nm.startswith("n_") or + attr_nm.startswith("dim_topo") or + attr_nm.startswith("name_") or + attr_nm.startswith("shunts_data_available") + ): + setattr(cls, attr_nm, attr) + else: + # set all the attributes + setattr(cls, attr_nm, attr) + + # make sure to catch data intiialized even outside of this function + if not _topo_vect_only: + cls._reset_cls_dict() + tmp = {} + GridObjects._make_cls_dict_extended(obj, tmp, as_list=False, copy_=True, _topo_vect_only=False) def _compute_pos_big_topo(self): # move the object attribute as class attribute ! if not type(self)._IS_INIT: - self._init_class_attr() + self._init_class_attr(_topo_vect_only=True) cls = type(self) cls._compute_pos_big_topo_cls() @@ -1482,9 +1529,12 @@ def _check_sub_id(cls): "is greater than the number of substations of the grid, which is {}." "".format(np.max(cls.line_or_to_subid), cls.n_sub) ) - + @classmethod def _fill_names(cls): + """fill the name vectors (**eg** name_line) if not done already in the backend. + This function is used to fill the name of the class. + """ if cls.name_line is None: cls.name_line = [ "{}_{}_{}".format(or_id, ex_id, l_id) @@ -1499,6 +1549,8 @@ def _fill_names(cls): "This might result in impossibility to load data." '\n\tIf "env.make" properly worked, you can safely ignore this warning.' ) + cls._reset_cls_dict() + if cls.name_load is None: cls.name_load = [ "load_{}_{}".format(bus_id, load_id) @@ -1511,6 +1563,8 @@ def _fill_names(cls): "This might result in impossibility to load data." '\n\tIf "env.make" properly worked, you can safely ignore this warning.' ) + cls._reset_cls_dict() + if cls.name_gen is None: cls.name_gen = [ "gen_{}_{}".format(bus_id, gen_id) @@ -1524,6 +1578,8 @@ def _fill_names(cls): "This might result in impossibility to load data." '\n\tIf "env.make" properly worked, you can safely ignore this warning.' ) + cls._reset_cls_dict() + if cls.name_sub is None: cls.name_sub = ["sub_{}".format(sub_id) for sub_id in range(cls.n_sub)] cls.name_sub = np.array(cls.name_sub) @@ -1534,6 +1590,8 @@ def _fill_names(cls): "This might result in impossibility to load data." '\n\tIf "env.make" properly worked, you can safely ignore this warning.' ) + cls._reset_cls_dict() + if cls.name_storage is None: cls.name_storage = [ "storage_{}_{}".format(bus_id, sto_id) @@ -1547,6 +1605,22 @@ def _fill_names(cls): "This might result in impossibility to load data." '\n\tIf "env.make" properly worked, you can safely ignore this warning.' ) + cls._reset_cls_dict() + + if cls.shunts_data_available and cls.name_shunt is None: + cls.name_shunt = [ + "shunt_{}_{}".format(bus_id, sh_id) + for sh_id, bus_id in enumerate(cls.shunt_to_subid) + ] + cls.name_shunt = np.array(cls.name_shunt) + warnings.warn( + "name_shunt is None so default storage unit names have been assigned to your grid. " + "(FYI: storage names are used to make the correspondence between the chronics and " + "the backend)" + "This might result in impossibility to load data." + '\n\tIf "env.make" properly worked, you can safely ignore this warning.' + ) + cls._reset_cls_dict() @classmethod def _check_names(cls): @@ -1558,45 +1632,40 @@ def _check_names(cls): cls.name_line = cls.name_line.astype(str) except Exception as exc_: raise EnvError( - f"self.name_line should be convertible to a numpy array of type str. Error was " - f"{exc_}" - ) + f"self.name_line should be convertible to a numpy array of type str" + ) from exc_ if not isinstance(cls.name_load, np.ndarray): try: cls.name_load = np.array(cls.name_load) cls.name_load = cls.name_load.astype(str) except Exception as exc_: raise EnvError( - "self.name_load should be convertible to a numpy array of type str. Error was " - f"{exc_}" - ) + "self.name_load should be convertible to a numpy array of type str." + ) from exc_ if not isinstance(cls.name_gen, np.ndarray): try: cls.name_gen = np.array(cls.name_gen) cls.name_gen = cls.name_gen.astype(str) except Exception as exc_: raise EnvError( - "self.name_gen should be convertible to a numpy array of type str. Error was " - f"{exc_}" - ) + "self.name_gen should be convertible to a numpy array of type str." + ) from exc_ if not isinstance(cls.name_sub, np.ndarray): try: cls.name_sub = np.array(cls.name_sub) cls.name_sub = cls.name_sub.astype(str) except Exception as exc_: raise EnvError( - "self.name_sub should be convertible to a numpy array of type str. Error was " - f"{exc_}" - ) + "self.name_sub should be convertible to a numpy array of type str." + ) from exc_ if not isinstance(cls.name_storage, np.ndarray): try: cls.name_storage = np.array(cls.name_storage) cls.name_storage = cls.name_storage.astype(str) except Exception as exc_: raise EnvError( - "self.name_storage should be convertible to a numpy array of type str. Error was " - f"{exc_}" - ) + "self.name_storage should be convertible to a numpy array of type str." + ) from exc_ attrs_nms = [ cls.name_gen, @@ -1612,7 +1681,13 @@ def _check_names(cls): nms.append("shunts") for arr_, nm in zip(attrs_nms, nms): - tmp = np.unique(arr_) + try: + tmp = np.unique(arr_) + tmp.shape[0] + arr_.shape[0] + except AttributeError as exc_: + raise Grid2OpException(f"Error for {nm}: name is most likely None") from exc_ + if tmp.shape[0] != arr_.shape[0]: nms = "\n\t - ".join(sorted(arr_)) raise EnvError( @@ -1996,9 +2071,6 @@ def assert_grid_correct_cls(cls): # to which subtation they are connected cls._check_sub_id() - # for names - cls._check_names() - # compute the position in substation if not done already cls._compute_sub_pos() @@ -2065,6 +2137,10 @@ def assert_grid_correct_cls(cls): ) raise IncorrectNumberOfElements(err_msg) + + # for names + cls._check_names() + if len(cls.name_load) != cls.n_load: raise IncorrectNumberOfLoads("len(self.name_load) != self.n_load") if len(cls.name_gen) != cls.n_gen: @@ -2810,7 +2886,7 @@ def init_grid(cls, gridobj, force=False, extra_name=None, force_module=None): name_res = "{}_{}".format(cls.__name__, gridobj.env_name) if gridobj.glop_version != grid2op.__version__: name_res += f"_{gridobj.glop_version}" - + if gridobj._PATH_GRID_CLASSES is not None: # the configuration equires to initialize the classes from the local environment path # this might be usefull when using pickle module or multiprocessing on Windows for example @@ -2851,12 +2927,24 @@ 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() - res_cls.process_grid2op_compat() + compat_mode = res_cls.process_grid2op_compat() res_cls._check_convert_to_np_array() # convert everything to numpy array if force_module is not None: res_cls.__module__ = force_module # hack because otherwise it says "abc" which is not the case # best would be to have a look at https://docs.python.org/3/library/types.html - + + if not compat_mode: + # I can reuse the "cls" dictionnary as they did not changed + if cls._CLS_DICT is not None: + res_cls._CLS_DICT = cls._CLS_DICT + if cls._CLS_DICT_EXTENDED is not None: + res_cls._CLS_DICT_EXTENDED = cls._CLS_DICT_EXTENDED + else: + # I need to rewrite the _CLS_DICT and _CLS_DICT_EXTENDED + # as the class has been modified with a "compatibility version" mode + tmp = {} + res_cls._make_cls_dict_extended(res_cls, tmp, as_list=False) + # store the type created here in the "globals" to prevent the initialization of the same class over and over globals()[name_res] = res_cls del res_cls @@ -2884,24 +2972,98 @@ def process_grid2op_compat(cls): This function can be overloaded, but in this case it's best to call this original method too. """ + res = False 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_old_compat() + res = True + if glop_ver < version.parse("1.6.0"): # this feature did not exist before. cls.dim_alarms = 0 cls.assistant_warning_type = None + res = True 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 = [] + res = True if glop_ver < version.parse("1.10.0.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 + res = True + + if res: + cls._reset_cls_dict() # forget the previous class (stored as dict) + return res + @classmethod + def _aux_fix_topo_vect_removed_storage(cls): + if cls.n_storage == 0: + return + + stor_locs = [pos for pos in cls.storage_pos_topo_vect] + for stor_loc in sorted(stor_locs, reverse=True): + for vect in [ + cls.load_pos_topo_vect, + cls.gen_pos_topo_vect, + cls.line_or_pos_topo_vect, + cls.line_ex_pos_topo_vect, + ]: + vect[vect >= stor_loc] -= 1 + + # deals with the "sub_pos" vector + for sub_id in range(cls.n_sub): + if (cls.storage_to_subid == sub_id).any(): + stor_ids = (cls.storage_to_subid == sub_id).nonzero()[0] + stor_locs = cls.storage_to_sub_pos[stor_ids] + for stor_loc in sorted(stor_locs, reverse=True): + for vect, sub_id_me in zip( + [ + cls.load_to_sub_pos, + cls.gen_to_sub_pos, + cls.line_or_to_sub_pos, + cls.line_ex_to_sub_pos, + ], + [ + cls.load_to_subid, + cls.gen_to_subid, + cls.line_or_to_subid, + cls.line_ex_to_subid, + ], + ): + vect[(vect >= stor_loc) & (sub_id_me == sub_id)] -= 1 + + # remove storage from the number of element in the substation + for sub_id in range(cls.n_sub): + cls.sub_info[sub_id] -= (cls.storage_to_subid == sub_id).sum() + # remove storage from the total number of element + cls.dim_topo -= cls.n_storage + + # recompute this private member + cls._topo_vect_to_sub = np.repeat( + np.arange(cls.n_sub), repeats=cls.sub_info + ) + + new_grid_objects_types = cls.grid_objects_types + new_grid_objects_types = new_grid_objects_types[ + new_grid_objects_types[:, cls.STORAGE_COL] == -1, : + ] + cls.grid_objects_types = 1 * new_grid_objects_types + + @classmethod + def _aux_process_old_compat(cls): + # remove "storage dependant attributes (topo_vect etc.) that are modified !" + cls._aux_fix_topo_vect_removed_storage() + # deactivate storage + cls.set_no_storage() + @classmethod def get_obj_connect_to(cls, _sentinel=None, substation_id=None): """ @@ -3460,11 +3622,42 @@ def topo_vect_element(cls, topo_vect_id: int) -> Dict[Literal["load_id", "gen_id 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...""" - save_to_dict(res, cls, "glop_version", str, copy_) - res["_PATH_GRID_CLASSES"] = cls._PATH_GRID_CLASSES # i do that manually for more control - save_to_dict(res, cls, "n_busbar_per_sub", str, copy_) + def _make_cls_dict(cls, res, as_list=True, copy_=True, _topo_vect_only=False): + """ + INTERNAL + + .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ + + NB: `cls` can be here a class or an object of a class... + + Notes + ------- + _topo_vect_only: this function is called once when the backend is initialized in `backend.load_grid` + (in `backend._compute_pos_big_topo`) and then once when everything is set up + (after redispatching and storage data are loaded). + + This is why I need the `_topo_vect_only` flag that tells this function when it's called only for + `topo_vect` related attributed + + """ + if cls._CLS_DICT is not None and not as_list and not _topo_vect_only: + # speed optimization: it has already been computed, so + # I reuse it (class attr are const) + for k, v in cls._CLS_DICT.items(): + if copy_: + res[k] = copy.deepcopy(v) + else: + res[k] = v + return + + if not _topo_vect_only: + # all the attributes bellow are not needed for the "first call" + # to this function when the elements are put together in the topo_vect. + # Indeed, at this stage (first call in the backend.load_grid) these + # attributes are not (necessary) loaded yet + save_to_dict(res, cls, "glop_version", str, copy_) + res["_PATH_GRID_CLASSES"] = cls._PATH_GRID_CLASSES # i do that manually for more control + save_to_dict(res, cls, "n_busbar_per_sub", str, copy_) save_to_dict( res, @@ -3619,183 +3812,225 @@ def _make_cls_dict(cls, res, as_list=True, copy_=True): copy_, ) - # redispatching - if cls.redispatching_unit_commitment_availble: - for nm_attr, type_attr in zip(cls._li_attr_disp, cls._type_attr_disp): + # shunts (not in topo vect but still usefull) + if cls.shunts_data_available: + save_to_dict( + res, + cls, + "name_shunt", + (lambda li: [str(el) for el in li]) if as_list else None, + copy_, + ) + save_to_dict( + res, + cls, + "shunt_to_subid", + (lambda li: [int(el) for el in li]) if as_list else None, + copy_, + ) + else: + res["name_shunt"] = None + res["shunt_to_subid"] = None + + if not _topo_vect_only: + # all the attributes bellow are not needed for the "first call" + # to this function when the elements are put together in the topo_vect. + # Indeed, at this stage (first call in the backend.load_grid) these + # attributes are not loaded yet + + # redispatching + if cls.redispatching_unit_commitment_availble: + for nm_attr, type_attr in zip(cls._li_attr_disp, cls._type_attr_disp): + save_to_dict( + res, + cls, + nm_attr, + (lambda li: [type_attr(el) for el in li]) if as_list else None, + copy_, + ) + else: + for nm_attr in cls._li_attr_disp: + res[nm_attr] = None + + # layout (position of substation on a map of the grid) + if cls.grid_layout is not None: save_to_dict( res, cls, - nm_attr, - (lambda li: [type_attr(el) for el in li]) if as_list else None, + "grid_layout", + (lambda gl: {str(k): [float(x), float(y)] for k, (x, y) in gl.items()}) + if as_list + else None, copy_, ) - else: - for nm_attr in cls._li_attr_disp: - res[nm_attr] = None + else: + res["grid_layout"] = None - # shunts - if cls.grid_layout is not None: + # storage data save_to_dict( res, cls, - "grid_layout", - (lambda gl: {str(k): [float(x), float(y)] for k, (x, y) in gl.items()}) - if as_list - else None, + "storage_type", + (lambda li: [str(el) for el in li]) if as_list else None, copy_, ) - else: - res["grid_layout"] = None - - # shunts - if cls.shunts_data_available: save_to_dict( res, cls, - "name_shunt", - (lambda li: [str(el) for el in li]) if as_list else None, + "storage_Emax", + (lambda li: [float(el) for el in li]) if as_list else None, copy_, ) save_to_dict( res, cls, - "shunt_to_subid", - (lambda li: [int(el) for el in li]) if as_list else None, + "storage_Emin", + (lambda li: [float(el) for el in li]) if as_list else None, + copy_, + ) + save_to_dict( + res, + cls, + "storage_max_p_prod", + (lambda li: [float(el) for el in li]) if as_list else None, + copy_, + ) + save_to_dict( + res, + cls, + "storage_max_p_absorb", + (lambda li: [float(el) for el in li]) if as_list else None, + copy_, + ) + save_to_dict( + res, + cls, + "storage_marginal_cost", + (lambda li: [float(el) for el in li]) if as_list else None, + copy_, + ) + save_to_dict( + res, + cls, + "storage_loss", + (lambda li: [float(el) for el in li]) if as_list else None, + copy_, + ) + save_to_dict( + res, + cls, + "storage_charging_efficiency", + (lambda li: [float(el) for el in li]) if as_list else None, + copy_, + ) + save_to_dict( + res, + cls, + "storage_discharging_efficiency", + (lambda li: [float(el) for el in li]) if as_list else None, copy_, ) - else: - res["name_shunt"] = None - res["shunt_to_subid"] = None - - # storage data - save_to_dict( - res, - cls, - "storage_type", - (lambda li: [str(el) for el in li]) if as_list else None, - copy_, - ) - save_to_dict( - res, - cls, - "storage_Emax", - (lambda li: [float(el) for el in li]) if as_list else None, - copy_, - ) - save_to_dict( - res, - cls, - "storage_Emin", - (lambda li: [float(el) for el in li]) if as_list else None, - copy_, - ) - save_to_dict( - res, - cls, - "storage_max_p_prod", - (lambda li: [float(el) for el in li]) if as_list else None, - copy_, - ) - save_to_dict( - res, - cls, - "storage_max_p_absorb", - (lambda li: [float(el) for el in li]) if as_list else None, - copy_, - ) - save_to_dict( - res, - cls, - "storage_marginal_cost", - (lambda li: [float(el) for el in li]) if as_list else None, - copy_, - ) - save_to_dict( - res, - cls, - "storage_loss", - (lambda li: [float(el) for el in li]) if as_list else None, - copy_, - ) - save_to_dict( - res, - cls, - "storage_charging_efficiency", - (lambda li: [float(el) for el in li]) if as_list else None, - copy_, - ) - save_to_dict( - res, - cls, - "storage_discharging_efficiency", - (lambda li: [float(el) for el in li]) if as_list else None, - copy_, - ) - # alert or alarm - if cls.assistant_warning_type is not None: - res["assistant_warning_type"] = str(cls.assistant_warning_type) - else: - res["assistant_warning_type"] = None + # alert or alarm + if cls.assistant_warning_type is not None: + res["assistant_warning_type"] = str(cls.assistant_warning_type) + else: + res["assistant_warning_type"] = None + + # area for the alarm feature + res["dim_alarms"] = cls.dim_alarms - # area for the alarm feature - res["dim_alarms"] = cls.dim_alarms - - save_to_dict( - res, cls, "alarms_area_names", (lambda li: [str(el) for el in li]), copy_ - ) - save_to_dict( - res, - cls, - "alarms_lines_area", - ( - lambda dict_: { - str(l_nm): [str(ar_nm) for ar_nm in areas] - for l_nm, areas in dict_.items() - } - ), - copy_, - ) - save_to_dict( - res, - cls, - "alarms_area_lines", - (lambda lili: [[str(l_nm) for l_nm in lines] for lines in lili]), - copy_, - ) - - # number of line alert for the alert feature - res['dim_alerts'] = cls.dim_alerts - # save alert line names to dict - save_to_dict( - res, cls, "alertable_line_names", (lambda li: [str(el) for el in li]) if as_list else None, copy_ - ) - save_to_dict( - res, cls, "alertable_line_ids", (lambda li: [int(el) for el in li]) if as_list else None, copy_ - ) + save_to_dict( + res, cls, "alarms_area_names", (lambda li: [str(el) for el in li]), copy_ + ) + save_to_dict( + res, + cls, + "alarms_lines_area", + ( + lambda dict_: { + str(l_nm): [str(ar_nm) for ar_nm in areas] + for l_nm, areas in dict_.items() + } + ), + copy_, + ) + save_to_dict( + res, + cls, + "alarms_area_lines", + (lambda lili: [[str(l_nm) for l_nm in lines] for lines in lili]), + copy_, + ) + + # number of line alert for the alert feature + res['dim_alerts'] = cls.dim_alerts + # save alert line names to dict + save_to_dict( + res, cls, "alertable_line_names", (lambda li: [str(el) for el in li]) if as_list else None, copy_ + ) + save_to_dict( + res, cls, "alertable_line_ids", (lambda li: [int(el) for el in li]) if as_list else None, copy_ + ) + # avoid further computation and save it + if not as_list: + cls._CLS_DICT = res.copy() return res @staticmethod - def _make_cls_dict_extended(cls, res, as_list=True, copy_=True): - """add the n_gen and all in the class created""" - GridObjects._make_cls_dict(cls, res, as_list=as_list, copy_=copy_) + def _make_cls_dict_extended(cls, res, as_list=True, copy_=True, _topo_vect_only=False): + """add the n_gen and all in the class created + + Notes + ------- + _topo_vect_only: this function is called once when the backend is initialized in `backend.load_grid` + (in `backend._compute_pos_big_topo`) and then once when everything is set up + (after redispatching and storage data are loaded). + + This is why I need the `_topo_vect_only` flag that tells this function when it's called only for + `topo_vect` related attributed + + """ + if cls._CLS_DICT_EXTENDED is not None and not as_list and not _topo_vect_only: + # speed optimization: it has already been computed, so + # I reuse it (class attr are const) + for k, v in cls._CLS_DICT_EXTENDED.items(): + if copy_: + res[k] = copy.deepcopy(v) + else: + res[k] = v + return + + GridObjects._make_cls_dict(cls, res, as_list=as_list, copy_=copy_, _topo_vect_only=_topo_vect_only) res["n_gen"] = cls.n_gen res["n_load"] = cls.n_load res["n_line"] = cls.n_line res["n_sub"] = cls.n_sub res["dim_topo"] = 1 * cls.dim_topo - # shunt - res["n_shunt"] = cls.n_shunt - res["shunts_data_available"] = cls.shunts_data_available # storage res["n_storage"] = cls.n_storage - # redispatching / curtailment - res[ - "redispatching_unit_commitment_availble" - ] = cls.redispatching_unit_commitment_availble - # n_busbar_per_sub - res["n_busbar_per_sub"] = cls.n_busbar_per_sub + # shunt (not in topo vect but might be usefull) + res["shunts_data_available"] = cls.shunts_data_available + res["n_shunt"] = cls.n_shunt + + if not _topo_vect_only: + # all the attributes bellow are not needed for the "first call" + # to this function when the elements are put together in the topo_vect. + # Indeed, at this stage (first call in the backend.load_grid) these + # attributes are not loaded yet + + # redispatching / curtailment + res[ + "redispatching_unit_commitment_availble" + ] = cls.redispatching_unit_commitment_availble + + # n_busbar_per_sub + res["n_busbar_per_sub"] = cls.n_busbar_per_sub + + # avoid further computation and save it + if not as_list and not _topo_vect_only: + cls._CLS_DICT_EXTENDED = res.copy() @classmethod def cls_to_dict(cls): @@ -4068,7 +4303,11 @@ class res(GridObjects): else: cls.alertable_line_names = [] cls.alertable_line_ids = [] - + + # save the representation of this class as dict + tmp = {} + cls._make_cls_dict_extended(cls, tmp, as_list=False, copy_=True) + # retrieve the redundant information that are not stored (for efficiency) obj_ = cls() obj_._compute_pos_big_topo_cls() @@ -4130,11 +4369,9 @@ def same_grid_class(cls, other_cls) -> bool: # this implementation is 6 times faster than the "cls_to_dict" one below, so i kept it me_dict = {} - GridObjects._make_cls_dict_extended(cls, me_dict, as_list=False, copy_=False) # TODO serialize the dict of the class not to build this every time + GridObjects._make_cls_dict_extended(cls, me_dict, as_list=False, copy_=False) other_cls_dict = {} - GridObjects._make_cls_dict_extended( - other_cls, other_cls_dict, as_list=False, copy_=False - ) # TODO serialize the dict of the class not to build this every time + GridObjects._make_cls_dict_extended(other_cls, other_cls_dict, as_list=False, copy_=False) if me_dict.keys() - other_cls_dict.keys(): # one key is in me but not in other @@ -4537,9 +4774,12 @@ def format_el_int(values): def format_el(values): return ",".join([f'"{el}"' for el in values]) - tmp_tmp_ = [f'"{k}": [{format_el(v)}]' for k, v in cls.grid_layout.items()] - tmp_ = ",".join(tmp_tmp_) - grid_layout_str = f"{{{tmp_}}}" + if cls.grid_layout is not None: + tmp_tmp_ = [f'"{k}": [{format_el(v)}]' for k, v in cls.grid_layout.items()] + tmp_ = ",".join(tmp_tmp_) + grid_layout_str = f"{{{tmp_}}}" + else: + grid_layout_str = "None" name_shunt_str = ",".join([f'"{el}"' for el in cls.name_shunt]) shunt_to_subid_str = GridObjects._format_int_vect_to_cls_str(cls.shunt_to_subid) @@ -4589,8 +4829,10 @@ def format_el(values): class {cls.__name__}({cls._INIT_GRID_CLS.__name__}): BEFORE_COMPAT_VERSION = \"{cls.BEFORE_COMPAT_VERSION}\" glop_version = grid2op.__version__ # tells it's the installed grid2op version - _PATH_GRID_CLASSES = {_PATH_ENV_str} - _INIT_GRID_CLS = {cls._INIT_GRID_CLS.__name__} + _PATH_GRID_CLASSES = {_PATH_ENV_str} # especially do not modify that + _INIT_GRID_CLS = {cls._INIT_GRID_CLS.__name__} + _CLS_DICT = None # init once to avoid yet another serialization of the class as dict (in make_cls_dict) + _CLS_DICT_EXTENDED = None # init once to avoid yet another serialization of the class as dict (in make_cls_dict) SUB_COL = 0 LOA_COL = 1 diff --git a/grid2op/__init__.py b/grid2op/__init__.py index 90bc05e6..d4201078 100644 --- a/grid2op/__init__.py +++ b/grid2op/__init__.py @@ -11,7 +11,7 @@ Grid2Op """ -__version__ = '1.10.2.dev0' +__version__ = '1.10.2.dev1' __all__ = [ "Action", diff --git a/grid2op/tests/BaseBackendTest.py b/grid2op/tests/BaseBackendTest.py index 85d4e5c4..10c8b7e8 100644 --- a/grid2op/tests/BaseBackendTest.py +++ b/grid2op/tests/BaseBackendTest.py @@ -63,7 +63,7 @@ def comb(n, k): from grid2op.Rules import RulesChecker from grid2op.Rules import AlwaysLegal from grid2op.Action._backendAction import _BackendAction -from grid2op.Backend import Backend, PandaPowerBackend +from grid2op.Backend import PandaPowerBackend import pdb diff --git a/grid2op/tests/test_Action.py b/grid2op/tests/test_Action.py index cff8a1a3..059686f0 100644 --- a/grid2op/tests/test_Action.py +++ b/grid2op/tests/test_Action.py @@ -28,6 +28,7 @@ def _get_action_grid_class(): + GridObjects._clear_class_attribute() GridObjects.env_name = "test_action_env" GridObjects.n_busbar_per_sub = 2 GridObjects.n_gen = 5 @@ -338,6 +339,7 @@ def _get_action_grid_class(): } GridObjects.shunts_data_available = False my_cls = GridObjects.init_grid(GridObjects, force=True) + GridObjects._clear_class_attribute() return my_cls, json_ diff --git a/grid2op/tests/test_CompactEpisodeData.py b/grid2op/tests/test_CompactEpisodeData.py index e3dc8713..d37286c7 100644 --- a/grid2op/tests/test_CompactEpisodeData.py +++ b/grid2op/tests/test_CompactEpisodeData.py @@ -152,7 +152,6 @@ def test_one_episode_with_saving(self): episode_data = CompactEpisodeData.from_disk(path=f, ep_id=episode_name) assert int(episode_data.meta["chronics_max_timestep"]) == self.max_iter assert len(episode_data.other_rewards) == self.max_iter - print("\n\nOther Rewards:", episode_data.other_reward_names) other_reward_idx = episode_data.other_reward_names.index("test") other_reward = episode_data.other_rewards[:, other_reward_idx] assert np.all(np.abs(other_reward - episode_data.rewards) <= self.tol_one) @@ -206,12 +205,11 @@ def test_3_episode_with_saving(self): def test_3_episode_3process_with_saving(self): f = tempfile.mkdtemp() - nb_episode = 2 + nb_episode = 2 res = self.runner._run_parrallel( nb_episode=nb_episode, nb_process=2, path_save=f, ) assert len(res) == nb_episode - print(f"\n\n{f}\n",'\n'.join([str(elt) for elt in Path(f).glob('*')])) for i, episode_name, cum_reward, timestep, total_ts in res: episode_data = CompactEpisodeData.from_disk(path=f, ep_id=episode_name) assert int(episode_data.meta["chronics_max_timestep"]) == self.max_iter diff --git a/grid2op/tests/test_Observation.py b/grid2op/tests/test_Observation.py index 89ab2b6d..2210ffe2 100644 --- a/grid2op/tests/test_Observation.py +++ b/grid2op/tests/test_Observation.py @@ -2200,9 +2200,9 @@ def test_space_to_dict(self): val = dict_[el] val_res = self.dict_[el] if val is None and val_res is not None: - raise AssertionError(f"val is None and val_res is not None: val_res: {val_res}") + raise AssertionError(f"{el}: val is None and val_res is not None: val_res: {val_res}") if val is not None and val_res is None: - raise AssertionError(f"val is not None and val_res is None: val {val}") + raise AssertionError(f"{el}: val is not None and val_res is None: val {val}") if val is None and val_res is None: continue @@ -2978,6 +2978,9 @@ def setUp(self): self.obs = self._make_forecast_perfect(self.env) self.sim_obs = None self.step_obs = None + + def tearDown(self): + self.env.close() def test_storage_act(self): """test i can do storage actions in simulate""" diff --git a/grid2op/tests/test_act_as_serializable_dict.py b/grid2op/tests/test_act_as_serializable_dict.py index a1829aa8..3ac3df59 100644 --- a/grid2op/tests/test_act_as_serializable_dict.py +++ b/grid2op/tests/test_act_as_serializable_dict.py @@ -24,6 +24,7 @@ def _get_action_grid_class(): + GridObjects._clear_class_attribute() GridObjects.env_name = "test_action_serial_dict" GridObjects.n_gen = 5 GridObjects.name_gen = np.array(["gen_{}".format(i) for i in range(5)]) @@ -108,6 +109,7 @@ def _get_action_grid_class(): GridObjects.alarms_lines_area = {el: ["all"] for el in GridObjects.name_line} GridObjects.dim_alarms = 1 my_cls = GridObjects.init_grid(GridObjects, force=True) + GridObjects._clear_class_attribute() return my_cls diff --git a/grid2op/tests/test_attached_envs.py b/grid2op/tests/test_attached_envs.py index d9c0742b..0451dfb5 100644 --- a/grid2op/tests/test_attached_envs.py +++ b/grid2op/tests/test_attached_envs.py @@ -12,11 +12,12 @@ from grid2op.Action import (PowerlineSetAction, PlayableAction, DontAct) from grid2op.Observation import CompleteObservation -from grid2op.Opponent import GeometricOpponent +from grid2op.Opponent import GeometricOpponent, GeometricOpponentMultiArea import pdb # TODO refactor to have 1 base class, maybe +# TODO: test runner, gym_compat and EpisodeData class TestL2RPNNEURIPS2020_Track1(unittest.TestCase): @@ -28,11 +29,11 @@ def setUp(self) -> None: _ = self.env.reset() def test_elements(self): - assert self.env.n_sub == 36 - assert self.env.n_line == 59 - assert self.env.n_load == 37 - assert self.env.n_gen == 22 - assert self.env.n_storage == 0 + assert type(self.env).n_sub == 36 + assert type(self.env).n_line == 59 + assert type(self.env).n_load == 37 + assert type(self.env).n_gen == 22 + assert type(self.env).n_storage == 0 def test_opponent(self): assert issubclass(self.env._opponent_action_class, PowerlineSetAction) @@ -72,11 +73,11 @@ def setUp(self) -> None: _ = self.env.reset() def test_elements(self): - assert self.env.n_sub == 36 - assert self.env.n_line == 59 - assert self.env.n_load == 37 - assert self.env.n_gen == 22 - assert self.env.n_storage == 0 + assert type(self.env).n_sub == 36 + assert type(self.env).n_line == 59 + assert type(self.env).n_load == 37 + assert type(self.env).n_gen == 22 + assert type(self.env).n_storage == 0 def test_opponent(self): assert issubclass(self.env._opponent_action_class, PowerlineSetAction) @@ -121,11 +122,11 @@ def setUp(self) -> None: _ = self.env.reset() def test_elements(self): - assert self.env.n_sub == 118 - assert self.env.n_line == 186 - assert self.env.n_load == 99 - assert self.env.n_gen == 62 - assert self.env.n_storage == 0 + assert type(self.env).n_sub == 118 + assert type(self.env).n_line == 186 + assert type(self.env).n_load == 99 + assert type(self.env).n_gen == 62 + assert type(self.env).n_storage == 0 def test_opponent(self): assert issubclass(self.env._opponent_action_class, DontAct) @@ -165,11 +166,11 @@ def setUp(self) -> None: _ = self.env.reset() def test_elements(self): - assert self.env.n_sub == 14 - assert self.env.n_line == 20 - assert self.env.n_load == 11 - assert self.env.n_gen == 6 - assert self.env.n_storage == 0 + assert type(self.env).n_sub == 14 + assert type(self.env).n_line == 20 + assert type(self.env).n_load == 11 + assert type(self.env).n_gen == 6 + assert type(self.env).n_storage == 0 def test_opponent(self): assert issubclass(self.env._opponent_action_class, DontAct) @@ -209,11 +210,11 @@ def setUp(self) -> None: _ = self.env.reset() def test_elements(self): - assert self.env.n_sub == 14 - assert self.env.n_line == 20 - assert self.env.n_load == 11 - assert self.env.n_gen == 6 - assert self.env.n_storage == 0 + assert type(self.env).n_sub == 14 + assert type(self.env).n_line == 20 + assert type(self.env).n_load == 11 + assert type(self.env).n_gen == 6 + assert type(self.env).n_storage == 0 def test_opponent(self): assert issubclass(self.env._opponent_action_class, DontAct) @@ -253,11 +254,11 @@ def setUp(self) -> None: _ = self.env.reset() def test_elements(self): - assert self.env.n_sub == 14 - assert self.env.n_line == 20 - assert self.env.n_load == 11 - assert self.env.n_gen == 6 - assert self.env.n_storage == 2 + assert type(self.env).n_sub == 14 + assert type(self.env).n_line == 20 + assert type(self.env).n_load == 11 + assert type(self.env).n_gen == 6 + assert type(self.env).n_storage == 2 def test_opponent(self): assert issubclass(self.env._opponent_action_class, DontAct) @@ -288,5 +289,102 @@ def test_random_action(self): ) + +class TestL2RPNWCCI2022(unittest.TestCase): + def setUp(self) -> None: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make("l2rpn_wcci_2022", test=True, _add_to_name=type(self).__name__) + _ = self.env.reset(seed=0) + + def test_elements(self): + assert type(self.env).n_sub == 118, f"{type(self.env).n_sub} vs 118" + assert type(self.env).n_line == 186, f"{type(self.env).n_line} vs 186" + assert type(self.env).n_load == 91, f"{type(self.env).n_load} vs 91" + assert type(self.env).n_gen == 62, f"{type(self.env).n_gen} vs 62" + assert type(self.env).n_storage == 7, f"{type(self.env).n_storage} vs 7" + + def test_opponent(self): + assert issubclass(self.env._opponent_action_class, PowerlineSetAction) + assert isinstance(self.env._opponent, GeometricOpponent) + assert self.env._opponent_action_space.n == type(self.env).n_line + + def test_action_space(self): + assert issubclass(self.env.action_space.subtype, PlayableAction) + assert self.env.action_space.n == 1567, ( + f"act space size is {self.env.action_space.n}, should be {1567}" + ) + + def test_observation_space(self): + assert issubclass(self.env.observation_space.subtype, CompleteObservation) + size_th = 4295 + assert self.env.observation_space.n == size_th, ( + f"obs space size is " + f"{self.env.observation_space.n}, " + f"should be {size_th}" + ) + + def test_random_action(self): + """test i can perform some step (random)""" + i = 0 + for i in range(10): + act = self.env.action_space.sample() + obs, reward, done, info = self.env.step(act) + if done: + break + assert i >= 1, ( + "could not perform the random action test because it games over first time step. " + "Please fix the test and try again" + ) + + +class TestL2RPNIDF2023(unittest.TestCase): + def setUp(self) -> None: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make("l2rpn_idf_2023", test=True, _add_to_name=type(self).__name__) + _ = self.env.reset(seed=0) + + def test_elements(self): + assert type(self.env).n_sub == 118, f"{type(self.env).n_sub} vs 118" + assert type(self.env).n_line == 186, f"{type(self.env).n_line} vs 186" + assert type(self.env).n_load == 99, f"{type(self.env).n_load} vs 99" + assert type(self.env).n_gen == 62, f"{type(self.env).n_gen} vs 62" + assert type(self.env).n_storage == 7, f"{type(self.env).n_storage} vs 7" + + def test_opponent(self): + assert issubclass(self.env._opponent_action_class, PowerlineSetAction) + assert isinstance(self.env._opponent, GeometricOpponentMultiArea) + assert self.env._opponent_action_space.n == type(self.env).n_line + + def test_action_space(self): + assert issubclass(self.env.action_space.subtype, PlayableAction) + assert self.env.action_space.n == 1605, ( + f"act space size is {self.env.action_space.n}, should be {1605}" + ) + + def test_observation_space(self): + assert issubclass(self.env.observation_space.subtype, CompleteObservation) + size_th = 4460 + assert self.env.observation_space.n == size_th, ( + f"obs space size is " + f"{self.env.observation_space.n}, " + f"should be {size_th}" + ) + + def test_random_action(self): + """test i can perform some step (random)""" + i = 0 + for i in range(10): + act = self.env.action_space.sample() + obs, reward, done, info = self.env.step(act) + if done: + break + assert i >= 1, ( + "could not perform the random action test because it games over first time step. " + "Please fix the test and try again" + ) + + if __name__ == "__main__": unittest.main() diff --git a/grid2op/tests/test_attached_envs_compat.py b/grid2op/tests/test_attached_envs_compat.py index 65b170cd..9b790497 100644 --- a/grid2op/tests/test_attached_envs_compat.py +++ b/grid2op/tests/test_attached_envs_compat.py @@ -34,15 +34,15 @@ def setUp(self) -> None: self.env.seed(0) def test_elements(self): - assert self.env.n_sub == 36 - assert self.env.n_line == 59 - assert self.env.n_load == 37 - assert self.env.n_gen == 22 - assert self.env.n_storage == 0 + assert type(self.env).n_sub == 36 + assert type(self.env).n_line == 59 + assert type(self.env).n_load == 37 + assert type(self.env).n_gen == 22 + assert type(self.env).n_storage == 0 def test_opponent(self): assert issubclass(self.env._opponent_action_class, PowerlineSetAction) - assert self.env._opponent_action_space.n == self.env.n_line + assert self.env._opponent_action_space.n == type(self.env).n_line def test_action_space(self): assert issubclass(self.env.action_space.subtype, PlayableAction) @@ -79,11 +79,11 @@ def setUp(self) -> None: self.env.seed(0) def test_elements(self): - assert self.env.n_sub == 118 - assert self.env.n_line == 186 - assert self.env.n_load == 99 - assert self.env.n_gen == 62 - assert self.env.n_storage == 0 + assert type(self.env).n_sub == 118 + assert type(self.env).n_line == 186 + assert type(self.env).n_load == 99 + assert type(self.env).n_gen == 62 + assert type(self.env).n_storage == 0 def test_opponent(self): assert issubclass(self.env._opponent_action_class, DontAct) @@ -127,11 +127,11 @@ def setUp(self) -> None: self.env.seed(42) def test_elements(self): - assert self.env.n_sub == 14 - assert self.env.n_line == 20 - assert self.env.n_load == 11 - assert self.env.n_gen == 6 - assert self.env.n_storage == 0 + assert type(self.env).n_sub == 14 + assert type(self.env).n_line == 20 + assert type(self.env).n_load == 11 + assert type(self.env).n_gen == 6 + assert type(self.env).n_storage == 0 def test_opponent(self): assert issubclass(self.env._opponent_action_class, DontAct) @@ -172,11 +172,11 @@ def setUp(self) -> None: self.env.seed(0) def test_elements(self): - assert self.env.n_sub == 14 - assert self.env.n_line == 20 - assert self.env.n_load == 11 - assert self.env.n_gen == 6 - assert self.env.n_storage == 0 + assert type(self.env).n_sub == 14 + assert type(self.env).n_line == 20 + assert type(self.env).n_load == 11 + assert type(self.env).n_gen == 6 + assert type(self.env).n_storage == 0 def test_opponent(self): assert issubclass(self.env._opponent_action_class, DontAct) @@ -214,14 +214,14 @@ def setUp(self) -> None: _compat_glop_version=GridObjects.BEFORE_COMPAT_VERSION, _add_to_name=type(self).__name__+"test_attached_compat_4", ) - self.env.seed(0) + self.env.seed(3) # 0, 1 and 2 leads to "wrong action" (games over) def test_elements(self): - assert self.env.n_sub == 14 - assert self.env.n_line == 20 - assert self.env.n_load == 11 - assert self.env.n_gen == 6 - assert self.env.n_storage == 0 + assert type(self.env).n_sub == 14 + assert type(self.env).n_line == 20 + assert type(self.env).n_load == 11 + assert type(self.env).n_gen == 6 + assert type(self.env).n_storage == 0 def test_opponent(self): assert issubclass(self.env._opponent_action_class, DontAct) @@ -239,7 +239,9 @@ def test_same_env_as_no_storage(self): res = 0 with warnings.catch_warnings(): warnings.filterwarnings("ignore") - env = grid2op.make("educ_case14_redisp", test=True, _add_to_name=type(self).__name__+"test_same_env_as_no_storage") + env = grid2op.make("educ_case14_redisp", + test=True, + _add_to_name=type(self).__name__+"test_same_env_as_no_storage") for attr in self.env.observation_space.attr_list_vect: tmp = getattr(self.env.observation_space._template_obj, attr).shape tmp2 = getattr(env.observation_space._template_obj, attr).shape @@ -272,7 +274,6 @@ def test_random_action(self): act = self.env.action_space.sample() obs, reward, done, info = self.env.step(act) if done: - pdb.set_trace() break assert i >= 1, ( "could not perform the random action test because it games over first time step. " diff --git a/grid2op/tests/test_basic_env_ls.py b/grid2op/tests/test_basic_env_ls.py index 3b4b0b2a..c3214a26 100644 --- a/grid2op/tests/test_basic_env_ls.py +++ b/grid2op/tests/test_basic_env_ls.py @@ -9,14 +9,13 @@ import warnings import unittest import numpy as np +import tempfile +import os +import json +import packaging +from packaging import version import grid2op -try: - from lightsim2grid import LightSimBackend - LS_AVAIL = True -except ImportError: - LS_AVAIL = False - pass from grid2op.Environment import Environment from grid2op.Runner import Runner from grid2op.gym_compat import (GymEnv, @@ -24,6 +23,19 @@ BoxGymObsSpace, DiscreteActSpace, MultiDiscreteActSpace) +from grid2op.Action import PlayableAction +from grid2op.Parameters import Parameters +from grid2op.Observation import CompleteObservation +from grid2op.Agent import RandomAgent +from grid2op.tests.helper_path_test import data_test_dir +from grid2op.Episode import EpisodeData + +try: + from lightsim2grid import LightSimBackend + LS_AVAIL = True +except ImportError: + LS_AVAIL = False + pass class TestEnvironmentBasic(unittest.TestCase): @@ -73,7 +85,24 @@ def test_reset(self): obs = self.env.reset() assert obs.timestep_overflow[self.line_id] == 0 assert obs.rho[self.line_id] > 1. + + def test_can_make_2_envs(self): + env_name = "l2rpn_case14_sandbox" + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make(env_name, test=True, backend=LightSimBackend()) + + param = Parameters() + param.NO_OVERFLOW_DISCONNECTION = True + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env1 = grid2op.make("educ_case14_storage", + test=True, + action_class=PlayableAction, + param=param, + backend=LightSimBackend()) + class TestEnvironmentBasicCpy(TestEnvironmentBasic): def setUp(self) -> None: @@ -119,7 +148,143 @@ def test_runner(self): # - it disconnect stuff in `self.env_in` # - it does not affect anything in `self.env_out` assert not obs_in.line_status[self.line_id], f"error for step {i}: line is not disconnected" - + + def test_backward_compatibility(self): + # TODO copy paste from test_Runner + backward_comp_version = [ + "1.6.4", # minimum version for lightsim2grid + "1.6.5", + "1.7.0", + "1.7.1", + "1.7.2", + "1.8.1", + # "1.9.0", # this one is bugy I don"t know why + "1.9.1", + "1.9.2", + "1.9.3", + "1.9.4", + "1.9.5", + "1.9.6", + "1.9.7", + "1.9.8", + "1.10.0", + "1.10.1", + ] + # first check a normal run + curr_version = "test_version" + PATH_PREVIOUS_RUNNER = os.path.join(data_test_dir, "runner_data") + assert ( + "curtailment" in CompleteObservation.attr_list_vect + ), "error at the beginning" + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + with grid2op.make( + "rte_case5_example", test=True, + _add_to_name=type(self).__name__, + backend=LightSimBackend() + ) as env, tempfile.TemporaryDirectory() as path: + runner = Runner(**env.get_params_for_runner(), agentClass=RandomAgent) + runner.run( + nb_episode=2, + path_save=os.path.join(path, curr_version), + pbar=False, + max_iter=100, + env_seeds=[1, 0], + agent_seeds=[42, 69], + ) + # check that i can read this data generate for this runner + try: + self._aux_backward(path, curr_version, curr_version) + except Exception as exc_: + raise RuntimeError(f"error for {curr_version}") from exc_ + assert ( + "curtailment" in CompleteObservation.attr_list_vect + ), "error after the first runner" + + # check that it raises a warning if loaded on the compatibility version + grid2op_version = backward_comp_version[0] + with self.assertWarns(UserWarning, msg=f"error for {grid2op_version}"): + self._aux_backward( + PATH_PREVIOUS_RUNNER, f"res_agent_{grid2op_version}", grid2op_version + ) + + # now check the compat versions + for grid2op_version in backward_comp_version: + # check that i can read previous data stored from previous grid2Op version + # can be loaded properly + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + try: + self._aux_backward( + PATH_PREVIOUS_RUNNER, + f"res_agent_{grid2op_version}", + grid2op_version, + ) + except Exception as exc_: + raise RuntimeError(f"error for {grid2op_version}") from exc_ + assert "curtailment" in CompleteObservation.attr_list_vect, ( + f"error after the legacy version " f"{grid2op_version}" + ) + + def _aux_backward(self, base_path, g2op_version_txt, g2op_version): + # TODO copy paste from test_Runner + episode_studied = EpisodeData.list_episode( + os.path.join(base_path, g2op_version_txt) + ) + for base_path, episode_path in episode_studied: + assert "curtailment" in CompleteObservation.attr_list_vect, ( + f"error after the legacy version " f"{g2op_version}" + ) + this_episode = EpisodeData.from_disk(base_path, episode_path) + assert "curtailment" in CompleteObservation.attr_list_vect, ( + f"error after the legacy version " f"{g2op_version}" + ) + full_episode_path = os.path.join(base_path, episode_path) + with open( + os.path.join(full_episode_path, "episode_meta.json"), + "r", + encoding="utf-8", + ) as f: + meta_data = json.load(f) + nb_ts = int(meta_data["nb_timestep_played"]) + try: + assert len(this_episode.actions) == nb_ts, ( + f"wrong number of elements for actions for version " + f"{g2op_version_txt}: {len(this_episode.actions)} vs {nb_ts}" + ) + assert len(this_episode.observations) == nb_ts + 1, ( + f"wrong number of elements for observations " + f"for version {g2op_version_txt}: " + f"{len(this_episode.observations)} vs {nb_ts}" + ) + assert len(this_episode.env_actions) == nb_ts, ( + f"wrong number of elements for env_actions for " + f"version {g2op_version_txt}: " + f"{len(this_episode.env_actions)} vs {nb_ts}" + ) + except Exception as exc_: + raise exc_ + g2op_ver = "" + try: + g2op_ver = version.parse(g2op_version) + except packaging.version.InvalidVersion: + if g2op_version != "test_version": + g2op_ver = version.parse("0.0.1") + else: + g2op_ver = version.parse("1.4.1") + if g2op_ver <= version.parse("1.4.0"): + assert ( + EpisodeData.get_grid2op_version(full_episode_path) == "<=1.4.0" + ), "wrong grid2op version stored (grid2op version <= 1.4.0)" + elif g2op_version == "test_version": + assert ( + EpisodeData.get_grid2op_version(full_episode_path) + == grid2op.__version__ + ), "wrong grid2op version stored (test_version)" + else: + assert ( + EpisodeData.get_grid2op_version(full_episode_path) == g2op_version + ), "wrong grid2op version stored (>=1.5.0)" class TestBasicEnvironmentGym(unittest.TestCase): def setUp(self) -> None: diff --git a/grid2op/tests/test_gym_asynch_env.py b/grid2op/tests/test_gym_asynch_env.py index b4f400cd..e0cca4c7 100644 --- a/grid2op/tests/test_gym_asynch_env.py +++ b/grid2op/tests/test_gym_asynch_env.py @@ -172,11 +172,14 @@ def test_space_obs_vect_act_multidiscrete(self): template_env.close() -# class AsyncGymEnvTester_Spawn(AsyncGymEnvTester_Fork): -# Will be working when branch class_in_files will be merged -# def _aux_start_method(self): -# return "spawn" +class AsyncGymEnvTester_Spawn(AsyncGymEnvTester_Fork): + # Will be working when branch class_in_files will be merged + def _aux_start_method(self): + return "spawn" + def setUp(self) -> None: + self.skipTest("Not handled at the moment") + return super().setUp() if __name__ == "__main__": unittest.main() diff --git a/grid2op/tests/test_issue_446.py b/grid2op/tests/test_issue_446.py index dd0278a0..a194b1ca 100644 --- a/grid2op/tests/test_issue_446.py +++ b/grid2op/tests/test_issue_446.py @@ -11,12 +11,15 @@ from grid2op.gym_compat import BoxGymObsSpace import numpy as np import unittest +import warnings class Issue446Tester(unittest.TestCase): def test_box_action_space(self): # We considers only redispatching actions - env = grid2op.make("l2rpn_case14_sandbox", test=True, _add_to_name=type(self).__name__) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make("l2rpn_case14_sandbox", test=True, _add_to_name=type(self).__name__) divide = {"hour_of_day": np.ones(1)} subtract = {"hour_of_day": np.zeros(1)} diff --git a/grid2op/tests/test_issue_511.py b/grid2op/tests/test_issue_511.py index 5ff3db8d..4d5f558b 100644 --- a/grid2op/tests/test_issue_511.py +++ b/grid2op/tests/test_issue_511.py @@ -36,7 +36,6 @@ def test_issue_set_bus(self): topo_action = self.env.action_space(act) as_dict = topo_action.as_dict() - print(as_dict) assert len(as_dict['set_bus_vect']['0']) == 2 # two objects modified def test_issue_change_bus(self): diff --git a/setup.py b/setup.py index 57e00237..ec6d9f96 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ def my_test_suite(): pkgs = { "required": [ - "numpy>=1.20", + "numpy>=1.20,<2", # disable numpy 2 for now "scipy>=1.4.1", "pandas>=1.0.3", "pandapower>=2.2.2",