From 80a54f91c139b782ef0286e7453a07427689761b Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 7 Nov 2024 17:08:37 +0100 Subject: [PATCH 1/8] adding the 'caching' kwargs to FromMultiEpisodeData Signed-off-by: DONNOT Benjamin --- CHANGELOG.rst | 4 ++ grid2op/Chronics/fromMultiEpisodeData.py | 72 +++++++++++++++++------- grid2op/tests/test_env_from_episode.py | 16 ++++-- 3 files changed, 68 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 30d8e6fea..21f64dd40 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -99,6 +99,8 @@ Native multi agents support: [1.11.0] - 202x-yy-zz ----------------------- +- [BREAKING] Change for `FromMultiEpisodeData` that disables the caching by default + when creating the data. - [FIXED] issue https://github.com/Grid2op/grid2op/issues/657 - [FIXED] missing an import on the `MaskedEnvironment` class - [ADDED] possibility to set the "thermal limits" when calling `env.reset(..., options={"thermal limit": xxx})` @@ -113,6 +115,8 @@ Native multi agents support: "chronics_hander" in the ObsEnv behaves (it now fully implements the public interface of a "real" chronic_handler) - [IMPROVED] error message in the `FromNPY` class when the backend is checked +- [IMRPOVED] the `FromMultiEpisodeData` class with the addition of the `caching` + kwargs to allow / disable caching (which was default behavior in previous version) [1.10.4] - 2024-10-15 ------------------------- diff --git a/grid2op/Chronics/fromMultiEpisodeData.py b/grid2op/Chronics/fromMultiEpisodeData.py index d7f77d227..e3e6f2321 100644 --- a/grid2op/Chronics/fromMultiEpisodeData.py +++ b/grid2op/Chronics/fromMultiEpisodeData.py @@ -7,16 +7,9 @@ # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. from datetime import datetime, timedelta -import os -import numpy as np -import copy -import warnings from typing import Optional, Union, List, Dict, Literal -from pathlib import Path -from grid2op.Exceptions import ( - ChronicsError, ChronicsNotFoundError -) +from grid2op.Exceptions import ChronicsError from grid2op.Chronics.gridValue import GridValue @@ -40,6 +33,17 @@ class FromMultiEpisodeData(GridValue): - to make sure you are running the exact same episode, you need to create the environment with the :class:`grid2op.Opponent.FromEpisodeDataOpponent` opponent + .. versionchanged:: 1.11.0 + Before versin 1.11.0 this class would load all the data in memory at the creation of the environment, + which could take lots of time and memory but once done a call to `env.reset` would be really fast. + + From grid2op >= 1.11.0 a kwargs `caching` has been added (default value is ``FALSE``) which + does not load everything in memory which makes it more memory efficient and (maybe) more time saving + (if some data happened to be loaded but never used). The default behaviour has then + changed. + + You can still benefit from previous behaviour by loading with `caching=True` + Examples --------- You can use this class this way: @@ -110,21 +114,39 @@ def __init__(self, max_iter=-1, start_datetime=datetime(year=2019, month=1, day=1), chunk_size=None, - list_perfect_forecasts=None, # TODO + list_perfect_forecasts=None, + caching : bool=False, **kwargs, # unused ): super().__init__(time_interval, max_iter, start_datetime, chunk_size) - self.li_ep_data = [FromOneEpisodeData(path, - ep_data=el, - time_interval=time_interval, - max_iter=max_iter, - chunk_size=chunk_size, - list_perfect_forecasts=list_perfect_forecasts, - start_datetime=start_datetime) - for el in li_ep_data - ] + self._caching : bool = bool(caching) + self._path = path + self._chunk_size = chunk_size + self._list_perfect_forecasts = list_perfect_forecasts + if self._caching: + self.li_ep_data = [FromOneEpisodeData(path, + ep_data=el, + time_interval=time_interval, + max_iter=max_iter, + chunk_size=chunk_size, + list_perfect_forecasts=list_perfect_forecasts, + start_datetime=start_datetime) + for el in li_ep_data + ] + self._input_li_ep_data = None + else: + self.li_ep_data = [None for el in li_ep_data] + self._input_li_ep_data = li_ep_data self._prev_cache_id = len(self.li_ep_data) - 1 self.data = self.li_ep_data[self._prev_cache_id] + if self.data is None: + self.data = FromOneEpisodeData(self._path, + ep_data=self._input_li_ep_data[self._prev_cache_id], + time_interval=self.time_interval, + max_iter=self.max_iter, + chunk_size=self._chunk_size, + list_perfect_forecasts=self._list_perfect_forecasts, + start_datetime=self.start_datetime) self._episode_data = self.data._episode_data # used by the fromEpisodeDataOpponent def next_chronics(self): @@ -144,6 +166,15 @@ def initialize( ): self.data = self.li_ep_data[self._prev_cache_id] + if self.data is None: + # data was not in cache: + self.data = FromOneEpisodeData(self._path, + ep_data=self._input_li_ep_data[self._prev_cache_id], + time_interval=self.time_interval, + max_iter=self.max_iter, + chunk_size=self._chunk_size, + list_perfect_forecasts=self._list_perfect_forecasts, + start_datetime=self.start_datetime) self.data.initialize( order_backend_loads, order_backend_prods, @@ -168,7 +199,8 @@ def check_validity(self, backend): def forecasts(self): return self.data.forecasts() - def tell_id(self, id_num, previous=False): + def tell_id(self, id_num: str, previous=False): + path_, id_num = id_num.split("@") id_num = int(id_num) if not isinstance(id_num, (int, dt_int)): raise ChronicsError("FromMultiEpisodeData can only be used with `tell_id` being an integer " @@ -182,7 +214,7 @@ def tell_id(self, id_num, previous=False): self._prev_cache_id %= len(self.li_ep_data) def get_id(self) -> str: - return f'{self._prev_cache_id }' + return f'{self._path}@{self._prev_cache_id}' def max_timestep(self): return self.data.max_timestep() diff --git a/grid2op/tests/test_env_from_episode.py b/grid2op/tests/test_env_from_episode.py index b71aed247..3a4af57d4 100644 --- a/grid2op/tests/test_env_from_episode.py +++ b/grid2op/tests/test_env_from_episode.py @@ -407,7 +407,7 @@ def test_given_example_multiepdata(self): env2 = grid2op.make(env_name, test=True, chronics_class=FromMultiEpisodeData, - data_feeding_kwargs={"li_ep_data": li_episode}, + data_feeding_kwargs={"li_ep_data": li_episode, "caching": True}, opponent_class=FromEpisodeDataOpponent, opponent_attack_cooldown=1, _add_to_name=type(self).__name__, @@ -551,7 +551,10 @@ def setUp(self) -> None: def tearDown(self) -> None: self.env.close() return super().tearDown() - + + def do_i_cache(self): + return False + def test_basic(self): """test injection, without opponent nor maintenance""" obs = self.env.reset() @@ -565,7 +568,7 @@ def test_basic(self): env = grid2op.make(self.env_name, test=True, chronics_class=FromMultiEpisodeData, - data_feeding_kwargs={"li_ep_data": ep_data}, + data_feeding_kwargs={"li_ep_data": ep_data, "caching": self.do_i_cache()}, opponent_attack_cooldown=99999999, opponent_attack_duration=0, opponent_budget_per_ts=0., @@ -607,6 +610,11 @@ def test_basic(self): obs, reward, done, info = env.step(env.action_space()) assert env.chronics_handler.get_id() == "1" - + +class TestTSFromMultieEpisodeWithCache(TestTSFromMultieEpisode): + def do_i_cache(self): + return True + + if __name__ == "__main__": unittest.main() From 914e003099dd9ab0fcb7b6054182a01312637cdf Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 7 Nov 2024 17:49:53 +0100 Subject: [PATCH 2/8] fixing some bugs after changing the get_id / set_id of MultiEpisodeData Signed-off-by: DONNOT Benjamin --- CHANGELOG.rst | 1 + grid2op/Chronics/fromMultiEpisodeData.py | 16 +++++++++++----- grid2op/tests/test_env_from_episode.py | 10 ++++++---- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 21f64dd40..4d9d4db4e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -117,6 +117,7 @@ Native multi agents support: - [IMPROVED] error message in the `FromNPY` class when the backend is checked - [IMRPOVED] the `FromMultiEpisodeData` class with the addition of the `caching` kwargs to allow / disable caching (which was default behavior in previous version) +- [IMPROVED] the `FromMultiEpisodeData` class that now returns also the path of the data [1.10.4] - 2024-10-15 ------------------------- diff --git a/grid2op/Chronics/fromMultiEpisodeData.py b/grid2op/Chronics/fromMultiEpisodeData.py index e3e6f2321..45fb8c420 100644 --- a/grid2op/Chronics/fromMultiEpisodeData.py +++ b/grid2op/Chronics/fromMultiEpisodeData.py @@ -200,12 +200,18 @@ def forecasts(self): return self.data.forecasts() def tell_id(self, id_num: str, previous=False): - path_, id_num = id_num.split("@") - id_num = int(id_num) - if not isinstance(id_num, (int, dt_int)): + try: + id_num = int(id_num) + path_ = None + except ValueError: + path_, id_num = id_num.split("@") + id_num = int(id_num) + + if path_ is not None and path_ != self._path: raise ChronicsError("FromMultiEpisodeData can only be used with `tell_id` being an integer " - "at the moment. Feel free to write a feature request if you want more.") - + "or if tell_id has the same path as the original file. " + "Feel free to write a feature request if you want more.") + self._prev_cache_id = id_num self._prev_cache_id %= len(self.li_ep_data) diff --git a/grid2op/tests/test_env_from_episode.py b/grid2op/tests/test_env_from_episode.py index 3a4af57d4..b55e53ed2 100644 --- a/grid2op/tests/test_env_from_episode.py +++ b/grid2op/tests/test_env_from_episode.py @@ -6,6 +6,7 @@ # SPDX-License-Identifier: MPL-2.0 # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. +import os import unittest import warnings import numpy as np @@ -577,6 +578,7 @@ def test_basic(self): _add_to_name=type(self).__name__) # test init data obs = env.reset() + path_ = os.path.join(env.get_path_env(), "chronics") TestTSFromEpisodeMaintenance._aux_obs_equal(obs, ep_data[0].observations[0]) for i in range(10): obs, reward, done, info = env.step(env.action_space()) @@ -584,7 +586,7 @@ def test_basic(self): assert done with self.assertRaises(Grid2OpException): obs, reward, done, info = env.step(env.action_space()) - assert env.chronics_handler.get_id() == "0" + assert env.chronics_handler.get_id() == f"{path_}@0", f"{env.chronics_handler.get_id()} vs {path_}@0" # test when reset, that it moves to next data obs = env.reset() @@ -595,12 +597,12 @@ def test_basic(self): assert done with self.assertRaises(Grid2OpException): obs, reward, done, info = env.step(env.action_space()) - assert env.chronics_handler.get_id() == "1" + assert env.chronics_handler.get_id() == f"{path_}@1", f"{env.chronics_handler.get_id()} vs {path_}@1" # test the set_id env.set_id("1") obs = env.reset() - assert env.chronics_handler.get_id() == "1" + assert env.chronics_handler.get_id() == f"{path_}@1", f"{env.chronics_handler.get_id()} vs {path_}@1" TestTSFromEpisodeMaintenance._aux_obs_equal(obs, ep_data[1].observations[0]) for i in range(10): obs, reward, done, info = env.step(env.action_space()) @@ -608,7 +610,7 @@ def test_basic(self): assert done with self.assertRaises(Grid2OpException): obs, reward, done, info = env.step(env.action_space()) - assert env.chronics_handler.get_id() == "1" + assert env.chronics_handler.get_id() == f"{path_}@1", f"{env.chronics_handler.get_id()} vs {path_}@1" class TestTSFromMultieEpisodeWithCache(TestTSFromMultieEpisode): From c94c1da41d5426dc591867de06c3588c11b45d4d Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 7 Nov 2024 17:56:43 +0100 Subject: [PATCH 3/8] now failing if the init state options raises a warning Signed-off-by: DONNOT Benjamin --- grid2op/Environment/environment.py | 8 ++++++-- grid2op/tests/test_action_set_orig_state_options.py | 6 ++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 16d9cf0d1..8c11286d2 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -951,7 +951,9 @@ def reset_grid(self, if not self._parameters.IGNORE_INITIAL_STATE_TIME_SERIE: # load the initial state from the time series (default) # TODO logger: log that - init_action : BaseAction = self.chronics_handler.get_init_action(self._names_chronics_to_backend) + with warnings.catch_warnings(): + warnings.filterwarnings("error") + init_action : BaseAction = self.chronics_handler.get_init_action(self._names_chronics_to_backend) else: # do as if everything was connected to busbar 1 # TODO logger: log that @@ -1278,7 +1280,9 @@ def reset(self, if "method" in act_as_dict: method = act_as_dict["method"] del act_as_dict["method"] - init_state : BaseAction = self._helper_action_env(act_as_dict) + with warnings.catch_warnings(): + warnings.filterwarnings("error") + init_state : BaseAction = self._helper_action_env(act_as_dict) elif isinstance(act_as_dict, BaseAction): init_state = act_as_dict else: diff --git a/grid2op/tests/test_action_set_orig_state_options.py b/grid2op/tests/test_action_set_orig_state_options.py index e42dcf680..03f272886 100644 --- a/grid2op/tests/test_action_set_orig_state_options.py +++ b/grid2op/tests/test_action_set_orig_state_options.py @@ -38,6 +38,12 @@ def _aux_reset_env(self, seed, ep_id, init_state): "init state": init_state}) return obs + def test_incorrect_action_error(self): + """test that when an action raised a warning then grid2op fails""" + with self.assertRaises(UserWarning): + obs = self.env.reset(options={"time serie id": 1, + "init state": {"toto": 1}}) + def _aux_make_step(self, act=None): if act is None: act = self.env.action_space() From 5c14343654c67fcbaadce56ed4d571cbc8b809b6 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 7 Nov 2024 17:57:07 +0100 Subject: [PATCH 4/8] now failing if the init state options raises a warning Signed-off-by: DONNOT Benjamin --- grid2op/tests/test_action_set_orig_state_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grid2op/tests/test_action_set_orig_state_options.py b/grid2op/tests/test_action_set_orig_state_options.py index 03f272886..edd168c19 100644 --- a/grid2op/tests/test_action_set_orig_state_options.py +++ b/grid2op/tests/test_action_set_orig_state_options.py @@ -42,7 +42,7 @@ def test_incorrect_action_error(self): """test that when an action raised a warning then grid2op fails""" with self.assertRaises(UserWarning): obs = self.env.reset(options={"time serie id": 1, - "init state": {"toto": 1}}) + "init state": {"toto": 1}}) def _aux_make_step(self, act=None): if act is None: From f259521b614e64a392175e1d8b58b0405b6f0091 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 8 Nov 2024 08:39:28 +0100 Subject: [PATCH 5/8] fix broken tests Signed-off-by: DONNOT Benjamin --- grid2op/Action/baseAction.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 1b54e77e9..082b6e71b 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -1859,9 +1859,7 @@ def __call__(self) -> Tuple[dict, np.ndarray, np.ndarray, np.ndarray, np.ndarray ) def _digest_shunt(self, dict_): - if not type(self).shunts_data_available: - return - + cls = type(self) if "shunt" in dict_: ddict_ = dict_["shunt"] @@ -1884,7 +1882,6 @@ def _digest_shunt(self, dict_): vect_self[:] = tmp elif isinstance(tmp, list): # expected a list: (id shunt, new bus) - cls = type(self) for (sh_id, new_bus) in tmp: if sh_id < 0: raise AmbiguousAction( @@ -2380,18 +2377,36 @@ def update(self, """ self._reset_vect() - + cls = type(self) + if dict_ is not None: for kk in dict_.keys(): - if kk not in self.authorized_keys: + if kk not in cls.authorized_keys: + if kk == "shunt" and not cls.shunts_data_available: + # no warnings are raised in this case because if a warning + # were raised it could crash some environment + # with shunt in "init_state.json" with a backend that does not + # handle shunt + continue + if kk == "set_storage" and cls.n_storage == 0: + # no warnings are raised in this case because if a warning + # were raised it could crash some environment + # with storage in "init_state.json" but if the backend did not + # handle storage units + continue warn = 'The key "{}" used to update an action will be ignored. Valid keys are {}' - warn = warn.format(kk, self.authorized_keys) + warn = warn.format(kk, cls.authorized_keys) warnings.warn(warn) - self._digest_shunt(dict_) + if cls.shunts_data_available: + # do not digest shunt when backend does not support it + self._digest_shunt(dict_) self._digest_injection(dict_) self._digest_redispatching(dict_) - self._digest_storage(dict_) # ADDED for battery + if cls.n_storage > 0: + # do not digest storage when backend does not + # support it + self._digest_storage(dict_) # ADDED for battery self._digest_curtailment(dict_) # ADDED for curtailment self._digest_setbus(dict_) self._digest_change_bus(dict_) From b65879305af57aa6a3138f6d19b2c43616c87ddf Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 8 Nov 2024 09:05:04 +0100 Subject: [PATCH 6/8] improve reading speed of FromEpisodeData by not reading everything, see issue #659 Signed-off-by: DONNOT Benjamin --- grid2op/Chronics/fromOneEpisodeData.py | 5 +- grid2op/Episode/EpisodeData.py | 142 ++++++++++++++++--------- grid2op/tests/test_env_from_episode.py | 4 +- 3 files changed, 93 insertions(+), 58 deletions(-) diff --git a/grid2op/Chronics/fromOneEpisodeData.py b/grid2op/Chronics/fromOneEpisodeData.py index 9dbe959ec..bd6c85b25 100644 --- a/grid2op/Chronics/fromOneEpisodeData.py +++ b/grid2op/Chronics/fromOneEpisodeData.py @@ -177,12 +177,11 @@ def __init__( if self.path is not None: # logger: this has no impact pass - if isinstance(ep_data, EpisodeData): self._episode_data = ep_data elif isinstance(ep_data, (str, Path)): try: - self._episode_data = EpisodeData.from_disk(*os.path.split(ep_data)) + self._episode_data = EpisodeData.from_disk(*os.path.split(ep_data), _only_act_obs=True) except Exception as exc_: raise ChronicsError("Impossible to build the FromOneEpisodeData with the `ep_data` provided.") from exc_ elif isinstance(ep_data, (tuple, list)): @@ -190,7 +189,7 @@ def __init__( raise ChronicsError("When you provide a tuple, or a list, FromOneEpisodeData can only be used if this list has length 2. " f"Length {len(ep_data)} found.") try: - self._episode_data = EpisodeData.from_disk(*ep_data) + self._episode_data = EpisodeData.from_disk(*ep_data, _only_act_obs=True) except Exception as exc_: raise ChronicsError("Impossible to build the FromOneEpisodeData with the `ep_data` provided.") from exc_ else: diff --git a/grid2op/Episode/EpisodeData.py b/grid2op/Episode/EpisodeData.py index 1925fd7ba..6e5b4e7ff 100644 --- a/grid2op/Episode/EpisodeData.py +++ b/grid2op/Episode/EpisodeData.py @@ -204,24 +204,33 @@ def __init__( observations, observation_space, "observations", init_me=_init_collections ) - self.env_actions = CollectionWrapper( - env_actions, - helper_action_env, - "env_actions", - check_legit=False, - init_me=_init_collections, - ) + if env_actions is not None: + self.env_actions = CollectionWrapper( + env_actions, + helper_action_env, + "env_actions", + check_legit=False, + init_me=_init_collections, + ) + else: + self.env_actions = None - self.attacks = CollectionWrapper( - attack, attack_space, "attacks", init_me=_init_collections - ) + if attack is not None: + self.attacks = CollectionWrapper( + attack, attack_space, "attacks", init_me=_init_collections + ) + else: + self.attacks = None self.meta = meta # gives a unique game over for everyone # TODO this needs testing! action_go = self.actions._game_over obs_go = self.observations._game_over - env_go = self.env_actions._game_over + if self.env_actions is not None: + env_go = self.env_actions._game_over + else: + env_go = None # raise RuntimeError("Add the attaks game over too !") real_go = action_go if self.meta is not None: @@ -247,7 +256,8 @@ def __init__( # there is a real game over, i assign the proper value for each collection self.actions._game_over = real_go self.observations._game_over = real_go + 1 - self.env_actions._game_over = real_go + if self.env_actions is not None: + self.env_actions._game_over = real_go self.other_rewards = other_rewards self.observation_space = observation_space @@ -401,12 +411,14 @@ def reboot(self): """ self.actions.reboot() self.observations.reboot() - self.env_actions.reboot() + if self.env_actions is not None: + self.env_actions.reboot() def go_to(self, index): self.actions.go_to(index) self.observations.go_to(index + 1) - self.env_actions.go_to(index) + if self.env_actions is not None: + self.env_actions.go_to(index) def get_actions(self): return self.actions.collection @@ -415,13 +427,17 @@ def get_observations(self): return self.observations.collection def __len__(self): - tmp = int(self.meta["chronics_max_timestep"]) - if tmp > 0: - return min(tmp, len(self.observations)) + if self.meta is not None: + tmp = int(self.meta["chronics_max_timestep"]) + if tmp > 0: + return min(tmp, len(self.observations)) return len(self.observations) @classmethod - def from_disk(cls, agent_path, name="1"): + def from_disk(cls, + agent_path: os.PathLike, + name:str="1", + _only_act_obs :bool =False): """ This function allows you to reload an episode stored using the runner. @@ -434,6 +450,9 @@ def from_disk(cls, agent_path, name="1"): name: ``str`` The name of the episode you want to reload. + + _only_act_obs: bool + Load only part of the episode data Returns ------- @@ -448,44 +467,58 @@ def from_disk(cls, agent_path, name="1"): episode_path = os.path.abspath(os.path.join(agent_path, name)) try: - with open(os.path.join(episode_path, EpisodeData.PARAMS)) as f: - _parameters = json.load(fp=f) - with open(os.path.join(episode_path, EpisodeData.META)) as f: - episode_meta = json.load(fp=f) - with open(os.path.join(episode_path, EpisodeData.TIMES)) as f: - episode_times = json.load(fp=f) - with open(os.path.join(episode_path, EpisodeData.OTHER_REWARDS)) as f: - other_rewards = json.load(fp=f) - - times = np.load(os.path.join(episode_path, EpisodeData.AG_EXEC_TIMES))[ - "data" - ] + path_legal_ambiguous = os.path.join(episode_path, cls.LEGAL_AMBIGUOUS) + if _only_act_obs: + _parameters = None + episode_meta = None + episode_times = None + other_rewards = None + times = None + env_actions = None + disc_lines = None + attack = None + rewards = None + has_legal_ambiguous = False + legal = None + ambiguous = None + else: + with open(os.path.join(episode_path, cls.PARAMS)) as f: + _parameters = json.load(fp=f) + with open(os.path.join(episode_path, cls.META)) as f: + episode_meta = json.load(fp=f) + with open(os.path.join(episode_path, cls.TIMES)) as f: + episode_times = json.load(fp=f) + with open(os.path.join(episode_path, cls.OTHER_REWARDS)) as f: + other_rewards = json.load(fp=f) + + times = np.load(os.path.join(episode_path, cls.AG_EXEC_TIMES))[ + "data" + ] + env_actions = np.load(os.path.join(episode_path, cls.ENV_ACTIONS_FILE))[ + "data" + ] + disc_lines = np.load( + os.path.join(episode_path, cls.LINES_FAILURES) + )["data"] + rewards = np.load(os.path.join(episode_path, cls.REWARDS))["data"] + has_legal_ambiguous = False + if os.path.exists(path_legal_ambiguous): + legal_ambiguous = np.load(path_legal_ambiguous)["data"] + legal = copy.deepcopy(legal_ambiguous[:, 0]) + ambiguous = copy.deepcopy(legal_ambiguous[:, 1]) + has_legal_ambiguous = True + else: + legal = None + ambiguous = None + actions = np.load(os.path.join(episode_path, EpisodeData.ACTIONS_FILE))["data"] - env_actions = np.load(os.path.join(episode_path, EpisodeData.ENV_ACTIONS_FILE))[ - "data" - ] observations = np.load( os.path.join(episode_path, EpisodeData.OBSERVATIONS_FILE) )["data"] - disc_lines = np.load( - os.path.join(episode_path, EpisodeData.LINES_FAILURES) - )["data"] attack = np.load(os.path.join(episode_path, EpisodeData.ATTACK))["data"] - rewards = np.load(os.path.join(episode_path, EpisodeData.REWARDS))["data"] - - path_legal_ambiguous = os.path.join(episode_path, EpisodeData.LEGAL_AMBIGUOUS) - has_legal_ambiguous = False - if os.path.exists(path_legal_ambiguous): - legal_ambiguous = np.load(path_legal_ambiguous)["data"] - legal = copy.deepcopy(legal_ambiguous[:, 0]) - ambiguous = copy.deepcopy(legal_ambiguous[:, 1]) - has_legal_ambiguous = True - else: - legal = None - ambiguous = None - except FileNotFoundError as ex: - raise Grid2OpException(f"EpisodeData file not found \n {str(ex)}") + except FileNotFoundError as exc_: + raise Grid2OpException(f"EpisodeData failed to load the file. Some data are not found.") from exc_ observation_space = ObservationSpace.from_dict( os.path.join(agent_path, EpisodeData.OBS_SPACE) @@ -493,12 +526,15 @@ def from_disk(cls, agent_path, name="1"): action_space = ActionSpace.from_dict( os.path.join(agent_path, EpisodeData.ACTION_SPACE) ) - helper_action_env = ActionSpace.from_dict( - os.path.join(agent_path, EpisodeData.ENV_MODIF_SPACE) - ) attack_space = ActionSpace.from_dict( os.path.join(agent_path, EpisodeData.ATTACK_SPACE) ) + if _only_act_obs: + helper_action_env = None + else: + helper_action_env = ActionSpace.from_dict( + os.path.join(agent_path, EpisodeData.ENV_MODIF_SPACE) + ) if observation_space.glop_version != grid2op.__version__: warnings.warn( 'You are using a "grid2op compatibility" feature (the data you saved ' diff --git a/grid2op/tests/test_env_from_episode.py b/grid2op/tests/test_env_from_episode.py index b55e53ed2..72681d7b1 100644 --- a/grid2op/tests/test_env_from_episode.py +++ b/grid2op/tests/test_env_from_episode.py @@ -531,7 +531,7 @@ def test_assert_warnings(self): ) -class TestTSFromMultieEpisode(unittest.TestCase): +class TestTSFromMultiEpisode(unittest.TestCase): def setUp(self) -> None: self.env_name = "l2rpn_case14_sandbox" with warnings.catch_warnings(): @@ -613,7 +613,7 @@ def test_basic(self): assert env.chronics_handler.get_id() == f"{path_}@1", f"{env.chronics_handler.get_id()} vs {path_}@1" -class TestTSFromMultieEpisodeWithCache(TestTSFromMultieEpisode): +class TestTSFromMultiEpisodeWithCache(TestTSFromMultiEpisode): def do_i_cache(self): return True From ccdc626b28247600d594bb5c0cb8f1758cb99616 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 8 Nov 2024 09:37:44 +0100 Subject: [PATCH 7/8] some improvment for base agents Signed-off-by: DONNOT Benjamin --- CHANGELOG.rst | 3 +++ grid2op/Agent/baseAgent.py | 2 +- grid2op/Agent/greedyAgent.py | 15 ++++++++++----- grid2op/Agent/powerlineSwitch.py | 16 +++++++--------- grid2op/Agent/recoPowerlineAgent.py | 12 ++++++++---- grid2op/Agent/topologyGreedy.py | 11 +++++++---- grid2op/tests/test_Agent.py | 25 +++++++++++++++++++++++++ 7 files changed, 61 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4d9d4db4e..c04ddcebd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -118,6 +118,9 @@ Native multi agents support: - [IMRPOVED] the `FromMultiEpisodeData` class with the addition of the `caching` kwargs to allow / disable caching (which was default behavior in previous version) - [IMPROVED] the `FromMultiEpisodeData` class that now returns also the path of the data +- [IMPROVED] the classes inherited from `GreedyAgent` with the added possibility to + do the `obs.simulate` on a different time horizon (kwarg `simulated_time_step`) +- [IMPROVED] some type hints for some agent class [1.10.4] - 2024-10-15 ------------------------- diff --git a/grid2op/Agent/baseAgent.py b/grid2op/Agent/baseAgent.py index efdc3f816..ed5e41237 100644 --- a/grid2op/Agent/baseAgent.py +++ b/grid2op/Agent/baseAgent.py @@ -32,7 +32,7 @@ class BaseAgent(RandomObject, ABC): def __init__(self, action_space: ActionSpace): RandomObject.__init__(self) - self.action_space = copy.deepcopy(action_space) + self.action_space : ActionSpace = copy.deepcopy(action_space) def reset(self, obs: BaseObservation): """ diff --git a/grid2op/Agent/greedyAgent.py b/grid2op/Agent/greedyAgent.py index 405dc4b7a..619ce1916 100644 --- a/grid2op/Agent/greedyAgent.py +++ b/grid2op/Agent/greedyAgent.py @@ -7,10 +7,14 @@ # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. from abc import abstractmethod +from typing import List import numpy as np -from grid2op.Agent.baseAgent import BaseAgent +from grid2op.Action import BaseAction, ActionSpace +from grid2op.Observation import BaseObservation from grid2op.dtypes import dt_float +from grid2op.Agent.baseAgent import BaseAgent + class GreedyAgent(BaseAgent): """ @@ -23,12 +27,13 @@ class GreedyAgent(BaseAgent): override this class. Examples are provided with :class:`PowerLineSwitch` and :class:`TopologyGreedy`. """ - def __init__(self, action_space): + def __init__(self, action_space: ActionSpace, simulated_time_step : int =1): BaseAgent.__init__(self, action_space) self.tested_action = None self.resulting_rewards = None + self.simulated_time_step = int(simulated_time_step) - def act(self, observation, reward, done=False): + def act(self, observation: BaseObservation, reward: float, done : bool=False) -> BaseAction: """ By definition, all "greedy" agents are acting the same way. The only thing that can differentiate multiple agents is the actions that are tested. @@ -64,7 +69,7 @@ def act(self, observation, reward, done=False): simul_reward, simul_has_error, simul_info, - ) = observation.simulate(action) + ) = observation.simulate(action, time_step=self.simulated_time_step) self.resulting_rewards[i] = simul_reward reward_idx = int( np.argmax(self.resulting_rewards) @@ -75,7 +80,7 @@ def act(self, observation, reward, done=False): return best_action @abstractmethod - def _get_tested_action(self, observation): + def _get_tested_action(self, observation: BaseObservation) -> List[BaseAction]: """ Returns the list of all the candidate actions. diff --git a/grid2op/Agent/powerlineSwitch.py b/grid2op/Agent/powerlineSwitch.py index f8662ed72..8dd831874 100644 --- a/grid2op/Agent/powerlineSwitch.py +++ b/grid2op/Agent/powerlineSwitch.py @@ -6,9 +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. +from typing import List import numpy as np from grid2op.dtypes import dt_bool +from grid2op.Observation import BaseObservation +from grid2op.Action import BaseAction, ActionSpace + from grid2op.Agent.greedyAgent import GreedyAgent @@ -27,20 +31,14 @@ class PowerLineSwitch(GreedyAgent): """ - def __init__(self, action_space): - GreedyAgent.__init__(self, action_space) + def __init__(self, action_space: ActionSpace, simulated_time_step : int =1): + GreedyAgent.__init__(self, action_space, simulated_time_step=simulated_time_step) - def _get_tested_action(self, observation): + def _get_tested_action(self, observation: BaseObservation) -> List[BaseAction]: res = [self.action_space({})] # add the do nothing for i in range(self.action_space.n_line): tmp = np.full(self.action_space.n_line, fill_value=False, dtype=dt_bool) tmp[i] = True action = self.action_space({"change_line_status": tmp}) - if not observation.line_status[i]: - # so the action consisted in reconnecting the powerline - # i need to say on which bus (always on bus 1 for this type of agent) - action = action.update( - {"set_bus": {"lines_or_id": [(i, 1)], "lines_ex_id": [(i, 1)]}} - ) res.append(action) return res diff --git a/grid2op/Agent/recoPowerlineAgent.py b/grid2op/Agent/recoPowerlineAgent.py index c7462877f..a11a1fc47 100644 --- a/grid2op/Agent/recoPowerlineAgent.py +++ b/grid2op/Agent/recoPowerlineAgent.py @@ -5,7 +5,11 @@ # you can obtain one at http://mozilla.org/MPL/2.0/. # SPDX-License-Identifier: MPL-2.0 # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. -import numpy as np + +from typing import List +from grid2op.Observation import BaseObservation +from grid2op.Action import BaseAction, ActionSpace + from grid2op.Agent.greedyAgent import GreedyAgent @@ -17,10 +21,10 @@ class RecoPowerlineAgent(GreedyAgent): """ - def __init__(self, action_space): - GreedyAgent.__init__(self, action_space) + def __init__(self, action_space: ActionSpace, simulated_time_step : int =1): + GreedyAgent.__init__(self, action_space, simulated_time_step=simulated_time_step) - def _get_tested_action(self, observation): + def _get_tested_action(self, observation: BaseObservation) -> List[BaseAction]: res = [self.action_space({})] # add the do nothing line_stat_s = observation.line_status cooldown = observation.time_before_cooldown_line diff --git a/grid2op/Agent/topologyGreedy.py b/grid2op/Agent/topologyGreedy.py index 3ca4a5178..a6f842392 100644 --- a/grid2op/Agent/topologyGreedy.py +++ b/grid2op/Agent/topologyGreedy.py @@ -6,6 +6,9 @@ # SPDX-License-Identifier: MPL-2.0 # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. +from typing import List +from grid2op.Observation import BaseObservation +from grid2op.Action import BaseAction, ActionSpace from grid2op.Agent.greedyAgent import GreedyAgent @@ -22,11 +25,11 @@ class TopologyGreedy(GreedyAgent): """ - def __init__(self, action_space): - GreedyAgent.__init__(self, action_space) - self.tested_action = None + def __init__(self, action_space: ActionSpace, simulated_time_step : int =1): + GreedyAgent.__init__(self, action_space, simulated_time_step=simulated_time_step) + self.tested_action : List[BaseAction]= None - def _get_tested_action(self, observation): + def _get_tested_action(self, observation: BaseObservation) -> List[BaseAction]: if self.tested_action is None: res = [self.action_space({})] # add the do nothing # better use "get_all_unitary_topologies_set" and not "get_all_unitary_topologies_change" diff --git a/grid2op/tests/test_Agent.py b/grid2op/tests/test_Agent.py index db42395a1..e799b5b66 100644 --- a/grid2op/tests/test_Agent.py +++ b/grid2op/tests/test_Agent.py @@ -131,6 +131,20 @@ def test_1_powerlineswitch(self): np.abs(cum_reward - expected_reward) <= self.tol_one ), f"The reward has not been properly computed {cum_reward} instead of {expected_reward}" + def test_1_powerlineswitch2(self): + agent = PowerLineSwitch(self.env.action_space, simulated_time_step=0) + with warnings.catch_warnings(): + warnings.filterwarnings("error") + i, cum_reward, all_acts = self._aux_test_agent(agent, i_max=5) + assert ( + i == 6 + ), "The powerflow diverged before step 6 for powerline switch agent" + # switch to using df_float in the reward, change then the results + expected_reward = dt_float(541.0180053710938) + assert ( + np.abs(cum_reward - expected_reward) <= self.tol_one + ), f"The reward has not been properly computed {cum_reward} instead of {expected_reward}" + def test_2_busswitch(self): agent = TopologyGreedy(self.env.action_space) with warnings.catch_warnings(): @@ -148,6 +162,17 @@ def test_2_busswitch(self): assert ( np.abs(cum_reward - expected_reward) <= self.tol_one ), f"The reward has not been properly computed {cum_reward} instead of {expected_reward}" + + def test_2_busswitch2(self): + agent = TopologyGreedy(self.env.action_space, simulated_time_step=0) + with warnings.catch_warnings(): + warnings.filterwarnings("error") + i, cum_reward, all_acts = self._aux_test_agent(agent, i_max=5) + assert i == 6, "The powerflow diverged before step 6 for greedy agent" + expected_reward = dt_float(541.0657348632812) + assert ( + np.abs(cum_reward - expected_reward) <= self.tol_one + ), f"The reward has not been properly computed {cum_reward} instead of {expected_reward}" class TestMake2Agents(HelperTests, unittest.TestCase): From 7acf06601af21347e0439601e6c8d898db2590db Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 8 Nov 2024 09:48:22 +0100 Subject: [PATCH 8/8] fix an non issue spotted by sonarcube [skip ci] Signed-off-by: DONNOT Benjamin --- grid2op/Chronics/fromMultiEpisodeData.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/grid2op/Chronics/fromMultiEpisodeData.py b/grid2op/Chronics/fromMultiEpisodeData.py index 45fb8c420..82bc55255 100644 --- a/grid2op/Chronics/fromMultiEpisodeData.py +++ b/grid2op/Chronics/fromMultiEpisodeData.py @@ -123,6 +123,7 @@ def __init__(self, self._path = path self._chunk_size = chunk_size self._list_perfect_forecasts = list_perfect_forecasts + self._input_li_ep_data = li_ep_data if self._caching: self.li_ep_data = [FromOneEpisodeData(path, ep_data=el, @@ -131,12 +132,10 @@ def __init__(self, chunk_size=chunk_size, list_perfect_forecasts=list_perfect_forecasts, start_datetime=start_datetime) - for el in li_ep_data - ] - self._input_li_ep_data = None + for el in li_ep_data + ] else: - self.li_ep_data = [None for el in li_ep_data] - self._input_li_ep_data = li_ep_data + self.li_ep_data = [None for _ in li_ep_data] self._prev_cache_id = len(self.li_ep_data) - 1 self.data = self.li_ep_data[self._prev_cache_id] if self.data is None: