From 68bb54487301be4c5f57ce590270cc2caafe17f0 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Tue, 11 Jul 2023 13:37:29 +0200 Subject: [PATCH 01/13] first commit, need tests and docs --- grid2op/Chronics/__init__.py | 4 +- grid2op/Chronics/fromEpisodeData.py | 286 +++++++++++++++++++++++++ grid2op/tests/test_env_from_episode.py | 38 ++++ 3 files changed, 327 insertions(+), 1 deletion(-) create mode 100644 grid2op/Chronics/fromEpisodeData.py create mode 100644 grid2op/tests/test_env_from_episode.py diff --git a/grid2op/Chronics/__init__.py b/grid2op/Chronics/__init__.py index 9be16a3f0..671c880a2 100644 --- a/grid2op/Chronics/__init__.py +++ b/grid2op/Chronics/__init__.py @@ -11,7 +11,8 @@ "GridStateFromFileWithForecastsWithoutMaintenance", "FromNPY", "FromChronix2grid", - "FromHandlers" + "FromHandlers", + "FromOneEpisodeData" ] from grid2op.Chronics.chronicsHandler import ChronicsHandler @@ -30,3 +31,4 @@ from grid2op.Chronics.fromNPY import FromNPY from grid2op.Chronics.fromChronix2grid import FromChronix2grid from grid2op.Chronics.time_series_from_handlers import FromHandlers +from grid2op.Chronics.fromEpisodeData import FromOneEpisodeData diff --git a/grid2op/Chronics/fromEpisodeData.py b/grid2op/Chronics/fromEpisodeData.py new file mode 100644 index 000000000..88a32f0fd --- /dev/null +++ b/grid2op/Chronics/fromEpisodeData.py @@ -0,0 +1,286 @@ +# Copyright (c) 2019-2023, RTE (https://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + +from datetime import datetime, timedelta +import os +import numpy as np +import copy +from typing import Optional, Union +from pathlib import Path + +from grid2op.Exceptions import ( + ChronicsError, ChronicsNotFoundError +) + +from grid2op.Chronics.gridValue import GridValue + +from grid2op.dtypes import dt_int, dt_float +from grid2op.Episode import EpisodeData + + +class FromOneEpisodeData(GridValue): + """This class allows to use the :class:`grid2op.Chronics.handlers.BaseHandler` to read back data + stored in :class:`grid2op.Episode.EpisodeData` + + .. newinversion:: 1.9.2 + + TODO make sure to use the opponent from OppFromEpisodeData if your initial environment had an opponent ! + + TODO there will be "perfect" forecast, as original forecasts are not stored ! + + Examples + --------- + You can use this class this way: + + .. code-block:: python + + import grid2op + from grid2op.Chronics import FromHandlers + from grid2op.Chronics.handlers import CSVHandler, DoNothingHandler, PerfectForecastHandler + env_name = "l2rpn_case14_sandbox" + + env = grid2op.make(env_name, + data_feeding_kwargs={"gridvalueClass": FromEpisodeData, + "ep_data": EpData + } + ) + + obs = env.reset() + + # and now you can use "env" as any grid2op environment. + + """ + MULTI_CHRONICS = False + + def __init__( + self, + path, # can be None ! + ep_data: Union[str, Path], + time_interval=timedelta(minutes=5), + sep=";", # here for compatibility with grid2op, but not used + max_iter=-1, + start_datetime=datetime(year=2019, month=1, day=1), + chunk_size=None, + h_forecast=(5,), + ): + GridValue.__init__( + self, + time_interval=time_interval, + max_iter=max_iter, + start_datetime=start_datetime, + chunk_size=chunk_size, + ) + + self.path = path + 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)) + except Exception as exc_: + raise ChronicsError("Impossible to build the FromOneEpisodeData with the `ep_data` provided.") from exc_ + elif isinstance(ep_data, (tuple, list)): + if len(ep_data) != 2: + 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) + except Exception as exc_: + raise ChronicsError("Impossible to build the FromOneEpisodeData with the `ep_data` provided.") from exc_ + else: + raise ChronicsError("FromOneEpisodeData can only read data either directly from an EpisodeData, from a path pointing to one, or from a tuple") + self.current_inj = None + + def initialize( + self, + order_backend_loads, + order_backend_prods, + order_backend_lines, + order_backend_subs, + names_chronics_to_backend=None, + ): + # set the current path of the time series + self._set_path(self.path) + + self.n_gen = len(order_backend_prods) + self.n_load = len(order_backend_loads) + self.n_line = len(order_backend_lines) + self.curr_iter = 0 + self.current_inj = None + + # TODO check if consistent, and compute the order ! + + # when there are no maintenance / hazards, build this only once + self._no_mh_time = np.full(self.n_line, fill_value=-1, dtype=dt_int) + self._no_mh_duration = np.full(self.n_line, fill_value=0, dtype=dt_int) + + def load_next(self): + self.current_datetime += self.time_interval + self.curr_iter += 1 + + res = {} + # load the injection + dict_inj, prod_v = self._load_injection() + res["injection"] = dict_inj + + # load maintenance + obs = self._episode_data.observations[self.curr_iter] + res["maintenance"] = obs.time_next_maintenance == 0 + maintenance_time = self._no_mh_time.copy() + maintenance_duration = self._no_mh_duration.copy() + # TODO ! + maintenance_time[res["maintenance"] ] = 0 + maintenance_duration[res["maintenance"] ] = 1 + + self.current_inj = res + return ( + self.current_datetime, + res, + maintenance_time, + maintenance_duration, + self._no_mh_duration, + prod_v, + ) + + def max_timestep(self): + if self.max_iter > 0: + return min(self.max_iter, len(self._episode_data)) + return len(self._episode_data) + + def next_chronics(self): + self.current_datetime = self.start_datetime + self.curr_iter = 0 + + def done(self): + # I am done if the part I control is "over" + if self._max_iter > 0 and self.curr_iter > self._max_iter: + return True + # TODO + return False + + def check_validity(self, backend): + # TODO + return True + + def _aux_forecasts(self, h_id, dict_, key, + for_handler, base_handler, handlers): + if for_handler is not None: + tmp_ = for_handler.forecast(h_id, self.current_inj, dict_, base_handler, handlers) + if tmp_ is not None: + dict_[key] = dt_float(1.0) * tmp_ + + def forecasts(self): + res = [] + if not self._forcast_handlers: + # nothing to handle forecast in this class + return res + + handlers = (self.load_p_handler, self.load_q_handler, self.gen_p_handler, self.gen_v_handler) + for h_id, h in enumerate(self._forcast_handlers[0].get_available_horizons()): + dict_ = {} + self._aux_forecasts(h_id, dict_, "load_p", self.load_p_for_handler, self.load_p_handler, handlers) + self._aux_forecasts(h_id, dict_, "load_q", self.load_q_for_handler, self.load_q_handler, handlers) + self._aux_forecasts(h_id, dict_, "prod_p", self.gen_p_for_handler, self.gen_p_handler, handlers) + self._aux_forecasts(h_id, dict_, "prod_v", self.gen_v_for_handler, self.gen_v_handler, handlers) + + res_d = {} + if dict_: + res_d["injection"] = dict_ + + forecast_datetime = self.current_datetime + timedelta(minutes=h) + res.append((forecast_datetime, res_d)) + return res + + def get_kwargs(self, dict_): + dict_["ep_data"] = copy.deepcopy(self._episode_data) + return dict_ + + def get_id(self) -> str: + if self.path is not None: + return self.path + else: + # TODO EpisodeData.path !!! + return "" + raise NotImplementedError() + + def shuffle(self, shuffler=None): + # TODO + pass + + def sample_next_chronics(self, probabilities=None): + # TODO + pass + + def seed(self, seed): + # nothing to do in this case, environment is purely deterministic + super().seed(seed) + + def _load_injection(self): + dict_ = {} + obs = self._episode_data.observations[self.curr_iter] + prod_v = None + + tmp_ = obs.load_p + if tmp_ is not None: + dict_["load_p"] = dt_float(1.0) * tmp_ + + tmp_ = obs.load_q + if tmp_ is not None: + dict_["load_q"] = dt_float(1.0) * tmp_ + + tmp_ = obs.gen_p + if tmp_ is not None: + dict_["prod_p"] = dt_float(1.0) * tmp_ + + tmp_ = obs.gen_v + if tmp_ is not None: + prod_v = dt_float(1.0) * tmp_ + + return dict_, prod_v + + def _init_date_time(self): # in csv handler + if os.path.exists(os.path.join(self.path, "start_datetime.info")): + with open(os.path.join(self.path, "start_datetime.info"), "r") as f: + a = f.read().rstrip().lstrip() + try: + tmp = datetime.strptime(a, "%Y-%m-%d %H:%M") + except ValueError: + tmp = datetime.strptime(a, "%Y-%m-%d") + except Exception: + raise ChronicsNotFoundError( + 'Impossible to understand the content of "start_datetime.info". Make sure ' + 'it\'s composed of only one line with a datetime in the "%Y-%m-%d %H:%M"' + "format." + ) + self.start_datetime = tmp + self.current_datetime = tmp + + if os.path.exists(os.path.join(self.path, "time_interval.info")): + with open(os.path.join(self.path, "time_interval.info"), "r") as f: + a = f.read().rstrip().lstrip() + try: + tmp = datetime.strptime(a, "%H:%M") + except ValueError: + tmp = datetime.strptime(a, "%M") + except Exception: + raise ChronicsNotFoundError( + 'Impossible to understand the content of "time_interval.info". Make sure ' + 'it\'s composed of only one line with a datetime in the "%H:%M"' + "format." + ) + self.time_interval = timedelta(hours=tmp.hour, minutes=tmp.minute) + + def fast_forward(self, nb_timestep): + for _ in range(nb_timestep): + self.load_next() + # for this class I suppose the real data AND the forecast are read each step + self.forecasts() diff --git a/grid2op/tests/test_env_from_episode.py b/grid2op/tests/test_env_from_episode.py new file mode 100644 index 000000000..1badb09e6 --- /dev/null +++ b/grid2op/tests/test_env_from_episode.py @@ -0,0 +1,38 @@ +# Copyright (c) 2019-2023, RTE (https://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + +import unittest +import warnings +import grid2op +from grid2op.Runner import Runner + + +class TestTSFromEpisode(unittest.TestCase): + def setUp(self) -> None: + env_name = "l2rpn_idf_2023" # with maintenance and attacks ! + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make(env_name) + return super().setUp() + def tearDown(self) -> None: + self.env.close() + return super().tearDown() + + def test_basic(self): + obs = self.env.reset() + runner = Runner( + **self.env.get_params_for_runner(), + agentClass=None + ) + + # test that the right seeds are assigned to the agent + res = runner.run(nb_episode=1, max_iter=self.max_iter, add_detailed_output=True) + ep_stat = res[-1] + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 5b8e74a44335cf567a2e73528660f08cda239344 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Wed, 12 Jul 2023 11:36:31 +0200 Subject: [PATCH 02/13] initial implementation, need docs, tests etc. etc. --- grid2op/Chronics/__init__.py | 2 +- ...omEpisodeData.py => fromOneEpisodeData.py} | 33 ++-- grid2op/Chronics/multiFolder.py | 2 - grid2op/Episode/EpisodeData.py | 16 +- grid2op/Runner/aux_fun.py | 4 +- grid2op/tests/test_env_from_episode.py | 187 +++++++++++++++++- 6 files changed, 212 insertions(+), 32 deletions(-) rename grid2op/Chronics/{fromEpisodeData.py => fromOneEpisodeData.py} (92%) diff --git a/grid2op/Chronics/__init__.py b/grid2op/Chronics/__init__.py index 671c880a2..c4ec2ca52 100644 --- a/grid2op/Chronics/__init__.py +++ b/grid2op/Chronics/__init__.py @@ -31,4 +31,4 @@ from grid2op.Chronics.fromNPY import FromNPY from grid2op.Chronics.fromChronix2grid import FromChronix2grid from grid2op.Chronics.time_series_from_handlers import FromHandlers -from grid2op.Chronics.fromEpisodeData import FromOneEpisodeData +from grid2op.Chronics.fromOneEpisodeData import FromOneEpisodeData diff --git a/grid2op/Chronics/fromEpisodeData.py b/grid2op/Chronics/fromOneEpisodeData.py similarity index 92% rename from grid2op/Chronics/fromEpisodeData.py rename to grid2op/Chronics/fromOneEpisodeData.py index 88a32f0fd..7a285e75e 100644 --- a/grid2op/Chronics/fromEpisodeData.py +++ b/grid2op/Chronics/fromOneEpisodeData.py @@ -40,14 +40,12 @@ class FromOneEpisodeData(GridValue): .. code-block:: python import grid2op - from grid2op.Chronics import FromHandlers - from grid2op.Chronics.handlers import CSVHandler, DoNothingHandler, PerfectForecastHandler + from grid2op.Chronics import FromOneEpisodeData env_name = "l2rpn_case14_sandbox" env = grid2op.make(env_name, - data_feeding_kwargs={"gridvalueClass": FromEpisodeData, - "ep_data": EpData - } + chronics_class=FromOneEpisodeData, + data_feeding_kwargs={"ep_data": EpData} ) obs = env.reset() @@ -67,6 +65,7 @@ def __init__( start_datetime=datetime(year=2019, month=1, day=1), chunk_size=None, h_forecast=(5,), + **kwargs, # unused ): GridValue.__init__( self, @@ -109,7 +108,6 @@ def initialize( names_chronics_to_backend=None, ): # set the current path of the time series - self._set_path(self.path) self.n_gen = len(order_backend_prods) self.n_load = len(order_backend_loads) @@ -123,23 +121,20 @@ def initialize( self._no_mh_time = np.full(self.n_line, fill_value=-1, dtype=dt_int) self._no_mh_duration = np.full(self.n_line, fill_value=0, dtype=dt_int) - def load_next(self): + def load_next(self): + obs = self._episode_data.observations[self.curr_iter] self.current_datetime += self.time_interval self.curr_iter += 1 - + res = {} # load the injection - dict_inj, prod_v = self._load_injection() + dict_inj, prod_v = self._load_injection(obs) res["injection"] = dict_inj # load maintenance - obs = self._episode_data.observations[self.curr_iter] res["maintenance"] = obs.time_next_maintenance == 0 - maintenance_time = self._no_mh_time.copy() - maintenance_duration = self._no_mh_duration.copy() - # TODO ! - maintenance_time[res["maintenance"] ] = 0 - maintenance_duration[res["maintenance"] ] = 1 + maintenance_time = 1 * obs.time_next_maintenance + maintenance_duration = 1 * obs.duration_next_maintenance self.current_inj = res return ( @@ -164,7 +159,8 @@ def done(self): # I am done if the part I control is "over" if self._max_iter > 0 and self.curr_iter > self._max_iter: return True - # TODO + if self.curr_iter > len(self._episode_data): + return True return False def check_validity(self, backend): @@ -180,6 +176,8 @@ def _aux_forecasts(self, h_id, dict_, key, def forecasts(self): res = [] + return res + # TODO if not self._forcast_handlers: # nothing to handle forecast in this class return res @@ -224,9 +222,8 @@ def seed(self, seed): # nothing to do in this case, environment is purely deterministic super().seed(seed) - def _load_injection(self): + def _load_injection(self, obs): dict_ = {} - obs = self._episode_data.observations[self.curr_iter] prod_v = None tmp_ = obs.load_p diff --git a/grid2op/Chronics/multiFolder.py b/grid2op/Chronics/multiFolder.py index f4612ea50..2d0fd1a8f 100644 --- a/grid2op/Chronics/multiFolder.py +++ b/grid2op/Chronics/multiFolder.py @@ -496,8 +496,6 @@ def tell_id(self, id_num, previous=False): Do you want to set to the previous value of this one or not (note that in general you want to set to the previous value, as calling this function as an impact only after `env.reset()` is called) """ - import pdb - if isinstance(id_num, str): # new accepted behaviour starting 1.6.4 # new in version 1.6.5: you only need to specify the chronics folder id and not the full path diff --git a/grid2op/Episode/EpisodeData.py b/grid2op/Episode/EpisodeData.py index 830e2bf44..0fe0c2bfd 100644 --- a/grid2op/Episode/EpisodeData.py +++ b/grid2op/Episode/EpisodeData.py @@ -415,7 +415,10 @@ def get_observations(self): return self.observations.collection def __len__(self): - return int(self.meta["chronics_max_timestep"]) + 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"): @@ -816,6 +819,12 @@ def get_grid2op_version(path_episode): if "version" in dict_: version = dict_["version"] return version + + def set_game_over(self, game_over_step: int): + self.observations.set_game_over(game_over_step + 1) + self.actions.set_game_over(game_over_step) + self.attacks.set_game_over(game_over_step) + self.env_actions.set_game_over(game_over_step) class CollectionWrapper: @@ -906,7 +915,10 @@ def __init__( except EnvError as exc_: self._game_over = i break - + + def set_game_over(self, game_over_step: int): + self._game_over = game_over_step + def __len__(self): if self._game_over is None: return self.collection.shape[0] diff --git a/grid2op/Runner/aux_fun.py b/grid2op/Runner/aux_fun.py index 1cee259eb..2f69d520f 100644 --- a/grid2op/Runner/aux_fun.py +++ b/grid2op/Runner/aux_fun.py @@ -119,7 +119,7 @@ def _aux_run_one_episode( # handle max_iter if max_iter is not None: env.chronics_handler.set_max_iter(max_iter) - + # reset it obs = env.reset() # reset the number of calls to high resolution simulator @@ -263,7 +263,7 @@ def _aux_run_one_episode( obs, info, time_step, opp_attack) pbar_.update(1) - + episode.set_game_over(time_step) end_ = time.perf_counter() episode.set_meta(env, time_step, float(cum_reward), env_seed, agent_seed) li_text = [ diff --git a/grid2op/tests/test_env_from_episode.py b/grid2op/tests/test_env_from_episode.py index 1badb09e6..272d6bf78 100644 --- a/grid2op/tests/test_env_from_episode.py +++ b/grid2op/tests/test_env_from_episode.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2023, RTE (https://www.rte-france.com) +# Copyright (c) 2023, RTE (https://www.rte-france.com) # See AUTHORS.txt # This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. # If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, @@ -8,31 +8,204 @@ import unittest import warnings +import numpy as np +import copy + import grid2op +from grid2op.Action import BaseAction, DontAct, TopologyAction +from grid2op.Backend import PandaPowerBackend +from grid2op.Environment import BaseEnv, Environment +from grid2op.Observation import BaseObservation, CompleteObservation +from grid2op.Opponent import BaseOpponent, NeverAttackBudget +from grid2op.Opponent.OpponentSpace import OpponentSpace +from grid2op.Reward import FlatReward +from grid2op.Rules import AlwaysLegal from grid2op.Runner import Runner +from grid2op.Chronics import FromOneEpisodeData, Multifolder, GridStateFromFileWithForecasts, GridStateFromFile, ChronicsHandler +from grid2op.Agent import BaseAgent, DoNothingAgent +from grid2op.Exceptions import Grid2OpException + +import pdb + +from grid2op.VoltageControler import ControlVoltageFromFile +from grid2op.operator_attention import LinearAttentionBudget + + +class GameOverTestAgent(BaseAgent): + def act(self, observation: BaseObservation, reward: float, done: bool = False) -> BaseAction: + if observation.current_step == 5: + return self.action_space({"set_bus": {"loads_id": [(0, -1)]}}) + return self.action_space() + + +class SpecialChronicsHandler(ChronicsHandler): + pass + +class SpecialMultifolder(Multifolder): + pass + + +class SpecialRunnerAddMaintenance(Runner): + def __init__(self, *args, data_ref, **kwargs): + super().__init__(*args, **kwargs) + self.data_ref = copy.deepcopy(data_ref) + + def init_env(special_runner_obj) -> BaseEnv: + res = super().init_env() + res.chronics_handler.__class__ = SpecialChronicsHandler + res.chronics_handler.real_data.__class__ = SpecialMultifolder + + def custom_initialize(multifolder_obj, + order_backend_loads, + order_backend_prods, + order_backend_lines, + order_backend_subs, + names_chronics_to_backend=None): + + # use original implementation + Multifolder.initialize(multifolder_obj, + order_backend_loads, + order_backend_prods, + order_backend_lines, + order_backend_subs, + names_chronics_to_backend) + # and then assign maintenance from the data ref + max_iter = (multifolder_obj._max_iter + 1) if multifolder_obj._max_iter > 0 else 2018 + multifolder_obj.data.maintenance[:, :] = special_runner_obj.data_ref.maintenance[:max_iter, :] + multifolder_obj.data.maintenance_time[:, :] = special_runner_obj.data_ref.maintenance_time[:max_iter, :] + multifolder_obj.data.maintenance_duration[:, :] = special_runner_obj.data_ref.maintenance_duration[:max_iter, :] + type(res.chronics_handler.real_data).initialize = custom_initialize + return res class TestTSFromEpisode(unittest.TestCase): def setUp(self) -> None: - env_name = "l2rpn_idf_2023" # with maintenance and attacks ! + self.env_name = "l2rpn_idf_2023" # with maintenance and attacks ! with warnings.catch_warnings(): warnings.filterwarnings("ignore") - self.env = grid2op.make(env_name) + self.env = grid2op.make(self.env_name) + self.env.set_id(0) + self.env.seed(0) + self.max_iter = 10 return super().setUp() + def tearDown(self) -> None: self.env.close() return super().tearDown() + def _aux_obs_equal(self, obs1, obs2, error_msg=""): + assert np.abs(obs1.gen_p - obs2.gen_p).max() <= 1e-5, f"{error_msg}: {np.abs(obs1.gen_p - obs2.gen_p).max()}" + assert np.array_equal(obs1.load_p, obs2.load_p), f"{error_msg}: {np.abs(obs1.load_p - obs2.load_p).max()}" + assert np.array_equal(obs1.time_next_maintenance, obs2.time_next_maintenance), f"{error_msg}" + assert np.array_equal(obs1.duration_next_maintenance, obs2.duration_next_maintenance), f"{error_msg}" + assert np.abs(obs1.a_or - obs2.a_or).max() <= 1e-3, f"{error_msg}: {np.abs(obs1.a_or - obs2.a_or).max()}" + def test_basic(self): + """test injection, without opponent nor maintenance""" + obs = self.env.reset() + runner = Runner( + **self.env.get_params_for_runner() + ) + res = runner.run(nb_episode=1, max_iter=self.max_iter, add_detailed_output=True) + ep_data = res[0][-1] + env = grid2op.make(self.env_name, + chronics_class=FromOneEpisodeData, + data_feeding_kwargs={"ep_data": ep_data}) + obs = env.reset() + self._aux_obs_equal(obs, ep_data.observations[0]) + for i in range(10): + obs, reward, done, info = env.step(env.action_space()) + self._aux_obs_equal(obs, ep_data.observations[i+1], f"at it. {i}") + assert done + with self.assertRaises(Grid2OpException): + obs, reward, done, info = env.step(env.action_space()) + + # again :-) + obs = env.reset() + self._aux_obs_equal(obs, ep_data.observations[0], "after reset") + for i in range(10): + obs, reward, done, info = env.step(env.action_space()) + self._aux_obs_equal(obs, ep_data.observations[i+1], f"at it. {i} (after reset)") + assert done + + def test_when_game_over(self): + """test I can load from a runner that used an agent that games over""" obs = self.env.reset() runner = Runner( **self.env.get_params_for_runner(), - agentClass=None + agentClass=GameOverTestAgent ) - - # test that the right seeds are assigned to the agent res = runner.run(nb_episode=1, max_iter=self.max_iter, add_detailed_output=True) - ep_stat = res[-1] + ep_data = res[0][-1] + env = grid2op.make(self.env_name, + chronics_class=FromOneEpisodeData, + data_feeding_kwargs={"ep_data": ep_data}) + obs = env.reset() + self._aux_obs_equal(obs, ep_data.observations[0]) + for i in range(6): + obs, reward, done, info = env.step(env.action_space()) + self._aux_obs_equal(obs, ep_data.observations[i+1]) + assert done + + def test_maintenance(self): + """test the maintenance are correct""" + obs = self.env.reset() + + # hack for generating maintenance + dataref = copy.deepcopy(self.env.chronics_handler.real_data.data) + dataref.maintenance[2:7, 2] = True + + for line_id in range(dataref.n_line): + dataref.maintenance_time[:, line_id] = dataref.get_maintenance_time_1d( + dataref.maintenance[:, line_id] + ) + dataref.maintenance_duration[ + :, line_id + ] = dataref.get_maintenance_duration_1d(dataref.maintenance[:, line_id]) + env_dict_params = self.env.get_params_for_runner() + env_dict_params["data_ref"] = dataref + runner = SpecialRunnerAddMaintenance( + **env_dict_params + ) + # now run as usual + res = runner.run(nb_episode=1, max_iter=self.max_iter, add_detailed_output=True) + ep_data = res[0][-1] + assert len(ep_data) == self.max_iter, f"{len(ep_data)} vs {self.max_iter}" + assert ep_data.observations[2].time_next_maintenance[2] == 0 # check right maintenance is applied + assert ep_data.observations[2].duration_next_maintenance[2] == 5 # check right maintenance is applied + env = grid2op.make(self.env_name, + chronics_class=FromOneEpisodeData, + data_feeding_kwargs={"ep_data": ep_data}) + obs = env.reset() + self._aux_obs_equal(obs, ep_data.observations[0], f"after reset") + for i in range(10): + obs, reward, done, info = env.step(env.action_space()) + self._aux_obs_equal(obs, ep_data.observations[i+1], f"at it. {i}") + assert done + + # now with an agent that games over + runner2 = SpecialRunnerAddMaintenance( + **env_dict_params, + agentClass=GameOverTestAgent + ) + # now run as usual + res2 = runner2.run(nb_episode=1, max_iter=self.max_iter, add_detailed_output=True) + ep_data2 = res2[0][-1] + import pdb + pdb.set_trace() + assert len(ep_data2) == 7, f"{len(ep_data2)} vs 7" + assert ep_data2.observations[2].time_next_maintenance[2] == 0 # check right maintenance is applied + assert ep_data2.observations[2].duration_next_maintenance[2] == 5 # check right maintenance is applied + env = grid2op.make(self.env_name, + chronics_class=FromOneEpisodeData, + data_feeding_kwargs={"ep_data": ep_data2}) + obs = env.reset() + self._aux_obs_equal(obs, ep_data2.observations[0], f"after reset (after game over)") + for i in range(6): + obs, reward, done, info = env.step(env.action_space()) + self._aux_obs_equal(obs, ep_data2.observations[i+1], f"at it. {i} (after game over)") + assert done + if __name__ == "__main__": unittest.main() \ No newline at end of file From 244365a3125073791781bbac8bf01a697d57b463 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 13 Jul 2023 13:49:19 +0200 Subject: [PATCH 03/13] adding the non test opponent from logs --- grid2op/Environment/Environment.py | 2 +- grid2op/MakeEnv/MakeFromPath.py | 2 +- grid2op/Opponent/__init__.py | 20 +++--- ...aseActionBudget.py => baseActionBudget.py} | 0 .../{BaseOpponent.py => baseOpponent.py} | 0 grid2op/Opponent/fromEpisodeDataOpponent.py | 64 +++++++++++++++++++ ...metricOpponent.py => geometricOpponent.py} | 2 +- .../Opponent/geometricOpponentMultiArea.py | 4 +- ...erAttackBudget.py => neverAttackBudget.py} | 2 +- .../{OpponentSpace.py => opponentSpace.py} | 0 ...mLineOpponent.py => randomLineOpponent.py} | 2 +- ...{UnlimitedBudget.py => unlimitedBudget.py} | 2 +- ...mOpponent.py => weightedRandomOpponent.py} | 2 +- grid2op/Runner/runner.py | 2 +- grid2op/tests/test_EpisodeData.py | 2 +- grid2op/tests/test_Opponent.py | 4 +- grid2op/tests/test_env_from_episode.py | 4 +- grid2op/tests/test_opp_with_area.py | 2 +- 18 files changed, 90 insertions(+), 26 deletions(-) rename grid2op/Opponent/{BaseActionBudget.py => baseActionBudget.py} (100%) rename grid2op/Opponent/{BaseOpponent.py => baseOpponent.py} (100%) create mode 100644 grid2op/Opponent/fromEpisodeDataOpponent.py rename grid2op/Opponent/{GeometricOpponent.py => geometricOpponent.py} (99%) rename grid2op/Opponent/{NeverAttackBudget.py => neverAttackBudget.py} (93%) rename grid2op/Opponent/{OpponentSpace.py => opponentSpace.py} (100%) rename grid2op/Opponent/{RandomLineOpponent.py => randomLineOpponent.py} (98%) rename grid2op/Opponent/{UnlimitedBudget.py => unlimitedBudget.py} (93%) rename grid2op/Opponent/{WeightedRandomOpponent.py => weightedRandomOpponent.py} (99%) diff --git a/grid2op/Environment/Environment.py b/grid2op/Environment/Environment.py index 00ffac583..316d3f202 100644 --- a/grid2op/Environment/Environment.py +++ b/grid2op/Environment/Environment.py @@ -12,7 +12,7 @@ import re import grid2op -from grid2op.Opponent.OpponentSpace import OpponentSpace +from grid2op.Opponent.opponentSpace import OpponentSpace from grid2op.dtypes import dt_float, dt_bool, dt_int from grid2op.Action import ( ActionSpace, diff --git a/grid2op/MakeEnv/MakeFromPath.py b/grid2op/MakeEnv/MakeFromPath.py index c50e8b55d..7cebcf2fb 100644 --- a/grid2op/MakeEnv/MakeFromPath.py +++ b/grid2op/MakeEnv/MakeFromPath.py @@ -14,7 +14,7 @@ from grid2op.Environment import Environment from grid2op.Backend import Backend, PandaPowerBackend -from grid2op.Opponent.OpponentSpace import OpponentSpace +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 diff --git a/grid2op/Opponent/__init__.py b/grid2op/Opponent/__init__.py index f3442abfd..c1a130f6b 100644 --- a/grid2op/Opponent/__init__.py +++ b/grid2op/Opponent/__init__.py @@ -7,15 +7,17 @@ "WeightedRandomOpponent", "NeverAttackBudget", "GeometricOpponent", - "GeometricOpponentMultiArea" + "GeometricOpponentMultiArea", + "FromEpisodeDataOpponent" ] -from grid2op.Opponent.OpponentSpace import OpponentSpace -from grid2op.Opponent.BaseActionBudget import BaseActionBudget -from grid2op.Opponent.BaseOpponent import BaseOpponent -from grid2op.Opponent.UnlimitedBudget import UnlimitedBudget -from grid2op.Opponent.RandomLineOpponent import RandomLineOpponent -from grid2op.Opponent.WeightedRandomOpponent import WeightedRandomOpponent -from grid2op.Opponent.NeverAttackBudget import NeverAttackBudget -from grid2op.Opponent.GeometricOpponent import GeometricOpponent +from grid2op.Opponent.opponentSpace import OpponentSpace +from grid2op.Opponent.baseActionBudget import BaseActionBudget +from grid2op.Opponent.baseOpponent import BaseOpponent +from grid2op.Opponent.unlimitedBudget import UnlimitedBudget +from grid2op.Opponent.randomLineOpponent import RandomLineOpponent +from grid2op.Opponent.weightedRandomOpponent import WeightedRandomOpponent +from grid2op.Opponent.neverAttackBudget import NeverAttackBudget +from grid2op.Opponent.geometricOpponent import GeometricOpponent from grid2op.Opponent.geometricOpponentMultiArea import GeometricOpponentMultiArea +from grid2op.Opponent.fromEpisodeDataOpponent import FromEpisodeDataOpponent diff --git a/grid2op/Opponent/BaseActionBudget.py b/grid2op/Opponent/baseActionBudget.py similarity index 100% rename from grid2op/Opponent/BaseActionBudget.py rename to grid2op/Opponent/baseActionBudget.py diff --git a/grid2op/Opponent/BaseOpponent.py b/grid2op/Opponent/baseOpponent.py similarity index 100% rename from grid2op/Opponent/BaseOpponent.py rename to grid2op/Opponent/baseOpponent.py diff --git a/grid2op/Opponent/fromEpisodeDataOpponent.py b/grid2op/Opponent/fromEpisodeDataOpponent.py new file mode 100644 index 000000000..23ac431ad --- /dev/null +++ b/grid2op/Opponent/fromEpisodeDataOpponent.py @@ -0,0 +1,64 @@ +# Copyright (c) 2019-2023, RTE (https://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + +import copy + +from grid2op.Opponent.baseOpponent import BaseOpponent +from grid2op.Chronics import FromOneEpisodeData +from grid2op.Exceptions import OpponentError + + +class FromEpisodeDataOpponent(BaseOpponent): + """ + .. warning:: + This can only be used if your environment uses :class:`grid2op.Chronics.FromOneEpisodeData` + or XXX (from a list of episode data or directory) + class otherwise it will NOT work. + + .. versionadded:: 1.9.2 + + Parameters + ---------- + BaseOpponent : _type_ + _description_ + """ + def __init__(self, action_space): + BaseOpponent.__init__(self, action_space) + self._attacks = [] + self._ptr_env = None + + def init(self, partial_env, **kwargs): + if not isinstance(partial_env.chronics_handler.real_data, FromOneEpisodeData): + raise OpponentError("FromEpisodeDataOpponent can only be used with FromOneEpisodeData time series !") + self._ptr_env = partial_env + self._attacks = copy.deepcopy(self._ptr_env.chronics_handler.real_data._episode_data.attacks) + + def reset(self, initial_budget): + self._attacks = copy.deepcopy(self._ptr_env.chronics_handler.real_data._episode_data.attacks) + + def attack(self, observation, agent_action, env_action, budget, previous_fails): + step = observation.current_step + attack = None + time = None + tmp = self._attacks[step] + if tmp.can_affect_something(): + # there as been an attack at this step + attack = tmp + time = 1 + return attack, time + + def tell_attack_continues(self, observation, agent_action, env_action, budget): + # should not be called at all + pass + + def get_state(self): + return (self._ptr_env, copy.deepcopy(self._attacks)) + + def set_state(self, state): + self._ptr_env = state[0] + self._attacks = state[1] diff --git a/grid2op/Opponent/GeometricOpponent.py b/grid2op/Opponent/geometricOpponent.py similarity index 99% rename from grid2op/Opponent/GeometricOpponent.py rename to grid2op/Opponent/geometricOpponent.py index e761ddb89..71253d4a7 100644 --- a/grid2op/Opponent/GeometricOpponent.py +++ b/grid2op/Opponent/geometricOpponent.py @@ -11,7 +11,7 @@ import numpy as np from grid2op.dtypes import dt_int -from grid2op.Opponent import BaseOpponent +from grid2op.Opponent.baseOpponent import BaseOpponent from grid2op.Exceptions import OpponentError diff --git a/grid2op/Opponent/geometricOpponentMultiArea.py b/grid2op/Opponent/geometricOpponentMultiArea.py index 38d55ad09..6a24f90db 100644 --- a/grid2op/Opponent/geometricOpponentMultiArea.py +++ b/grid2op/Opponent/geometricOpponentMultiArea.py @@ -11,8 +11,8 @@ from grid2op.dtypes import dt_int -from grid2op.Opponent import BaseOpponent -from grid2op.Opponent import GeometricOpponent +from grid2op.Opponent.baseOpponent import BaseOpponent +from grid2op.Opponent.geometricOpponent import GeometricOpponent from grid2op.Exceptions import OpponentError diff --git a/grid2op/Opponent/NeverAttackBudget.py b/grid2op/Opponent/neverAttackBudget.py similarity index 93% rename from grid2op/Opponent/NeverAttackBudget.py rename to grid2op/Opponent/neverAttackBudget.py index be142d46a..fa12a2921 100644 --- a/grid2op/Opponent/NeverAttackBudget.py +++ b/grid2op/Opponent/neverAttackBudget.py @@ -6,7 +6,7 @@ # SPDX-License-Identifier: MPL-2.0 # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. import numpy as np -from grid2op.Opponent.BaseActionBudget import BaseActionBudget +from grid2op.Opponent.baseActionBudget import BaseActionBudget class NeverAttackBudget(BaseActionBudget): diff --git a/grid2op/Opponent/OpponentSpace.py b/grid2op/Opponent/opponentSpace.py similarity index 100% rename from grid2op/Opponent/OpponentSpace.py rename to grid2op/Opponent/opponentSpace.py diff --git a/grid2op/Opponent/RandomLineOpponent.py b/grid2op/Opponent/randomLineOpponent.py similarity index 98% rename from grid2op/Opponent/RandomLineOpponent.py rename to grid2op/Opponent/randomLineOpponent.py index d91a9d560..f1c5ed256 100644 --- a/grid2op/Opponent/RandomLineOpponent.py +++ b/grid2op/Opponent/randomLineOpponent.py @@ -9,7 +9,7 @@ import numpy as np import copy -from grid2op.Opponent import BaseOpponent +from grid2op.Opponent.baseOpponent import BaseOpponent from grid2op.Exceptions import OpponentError diff --git a/grid2op/Opponent/UnlimitedBudget.py b/grid2op/Opponent/unlimitedBudget.py similarity index 93% rename from grid2op/Opponent/UnlimitedBudget.py rename to grid2op/Opponent/unlimitedBudget.py index 5b5110802..77c313482 100644 --- a/grid2op/Opponent/UnlimitedBudget.py +++ b/grid2op/Opponent/unlimitedBudget.py @@ -7,7 +7,7 @@ # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. from grid2op.dtypes import dt_float -from grid2op.Opponent.BaseActionBudget import BaseActionBudget +from grid2op.Opponent.baseActionBudget import BaseActionBudget class UnlimitedBudget(BaseActionBudget): diff --git a/grid2op/Opponent/WeightedRandomOpponent.py b/grid2op/Opponent/weightedRandomOpponent.py similarity index 99% rename from grid2op/Opponent/WeightedRandomOpponent.py rename to grid2op/Opponent/weightedRandomOpponent.py index ea970711c..54eadca0a 100644 --- a/grid2op/Opponent/WeightedRandomOpponent.py +++ b/grid2op/Opponent/weightedRandomOpponent.py @@ -9,7 +9,7 @@ import numpy as np import copy -from grid2op.Opponent import BaseOpponent +from grid2op.Opponent.baseOpponent import BaseOpponent from grid2op.Exceptions import OpponentError diff --git a/grid2op/Runner/runner.py b/grid2op/Runner/runner.py index 1fd04dced..6dad20fe0 100644 --- a/grid2op/Runner/runner.py +++ b/grid2op/Runner/runner.py @@ -16,7 +16,7 @@ from grid2op.Action import BaseAction, TopologyAction, DontAct from grid2op.Exceptions import Grid2OpException, EnvError from grid2op.Observation import CompleteObservation, BaseObservation -from grid2op.Opponent.OpponentSpace import OpponentSpace +from grid2op.Opponent.opponentSpace import OpponentSpace from grid2op.Reward import FlatReward, BaseReward from grid2op.Rules import AlwaysLegal, BaseRules from grid2op.Environment import Environment diff --git a/grid2op/tests/test_EpisodeData.py b/grid2op/tests/test_EpisodeData.py index 5c641818e..000209c7b 100644 --- a/grid2op/tests/test_EpisodeData.py +++ b/grid2op/tests/test_EpisodeData.py @@ -23,7 +23,7 @@ from grid2op.Action import TopologyAction from grid2op.Parameters import Parameters from grid2op.MakeEnv import make -from grid2op.Opponent.BaseActionBudget import BaseActionBudget +from grid2op.Opponent.baseActionBudget import BaseActionBudget from grid2op.Opponent import RandomLineOpponent diff --git a/grid2op/tests/test_Opponent.py b/grid2op/tests/test_Opponent.py index 5dec8b348..cdded1937 100644 --- a/grid2op/tests/test_Opponent.py +++ b/grid2op/tests/test_Opponent.py @@ -9,7 +9,7 @@ import tempfile import warnings import grid2op -from grid2op.Opponent.OpponentSpace import OpponentSpace +from grid2op.Opponent.opponentSpace import OpponentSpace from grid2op.tests.helper_path_test import * from grid2op.Chronics import ChangeNothing from grid2op.Opponent import ( @@ -21,7 +21,7 @@ from grid2op.Opponent.geometricOpponentMultiArea import GeometricOpponentMultiArea from grid2op.Action import TopologyAction from grid2op.MakeEnv import make -from grid2op.Opponent.BaseActionBudget import BaseActionBudget +from grid2op.Opponent.baseActionBudget import BaseActionBudget from grid2op.dtypes import dt_int from grid2op.Parameters import Parameters from grid2op.Runner import Runner diff --git a/grid2op/tests/test_env_from_episode.py b/grid2op/tests/test_env_from_episode.py index 272d6bf78..d1a142093 100644 --- a/grid2op/tests/test_env_from_episode.py +++ b/grid2op/tests/test_env_from_episode.py @@ -17,7 +17,7 @@ from grid2op.Environment import BaseEnv, Environment from grid2op.Observation import BaseObservation, CompleteObservation from grid2op.Opponent import BaseOpponent, NeverAttackBudget -from grid2op.Opponent.OpponentSpace import OpponentSpace +from grid2op.Opponent.opponentSpace import OpponentSpace from grid2op.Reward import FlatReward from grid2op.Rules import AlwaysLegal from grid2op.Runner import Runner @@ -191,8 +191,6 @@ def test_maintenance(self): # now run as usual res2 = runner2.run(nb_episode=1, max_iter=self.max_iter, add_detailed_output=True) ep_data2 = res2[0][-1] - import pdb - pdb.set_trace() assert len(ep_data2) == 7, f"{len(ep_data2)} vs 7" assert ep_data2.observations[2].time_next_maintenance[2] == 0 # check right maintenance is applied assert ep_data2.observations[2].duration_next_maintenance[2] == 5 # check right maintenance is applied diff --git a/grid2op/tests/test_opp_with_area.py b/grid2op/tests/test_opp_with_area.py index 10a205561..8462f0c67 100644 --- a/grid2op/tests/test_opp_with_area.py +++ b/grid2op/tests/test_opp_with_area.py @@ -15,7 +15,7 @@ ) from grid2op.Action import TopologyAction from grid2op.MakeEnv import make -from grid2op.Opponent.BaseActionBudget import BaseActionBudget +from grid2op.Opponent.baseActionBudget import BaseActionBudget from grid2op.dtypes import dt_int from grid2op.Parameters import Parameters from grid2op.Runner import Runner From ff3a71a6113f91a2b321d7da5cd0e782ddaa478f Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Mon, 21 Aug 2023 15:02:07 +0200 Subject: [PATCH 04/13] fixing an issue with read the doc and sphinx latest version --- CHANGELOG.rst | 6 ++++++ grid2op/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 03c220b8c..8b4e2262a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -31,6 +31,12 @@ Change Log - [???] "asynch" multienv - [???] properly model interconnecting powerlines +[1.9.4] - 2023-xx-yy +--------------------- +- [FIXED] read-the-docs template is not compatible with latest sphinx version (7.0.0) + see https://github.com/readthedocs/sphinx_rtd_theme/issues/1463 + + [1.9.3] - 2023-07-28 --------------------- - [BREAKING] the "chronix2grid" dependency now points to chronix2grid and not to the right branch diff --git a/grid2op/__init__.py b/grid2op/__init__.py index 85a96e44b..928990386 100644 --- a/grid2op/__init__.py +++ b/grid2op/__init__.py @@ -11,7 +11,7 @@ Grid2Op """ -__version__ = '1.9.3' +__version__ = '1.9.4.dev0' __all__ = [ "Action", diff --git a/setup.py b/setup.py index 798f67273..ca25c64d6 100644 --- a/setup.py +++ b/setup.py @@ -55,7 +55,7 @@ def my_test_suite(): ], "docs": [ "numpydoc>=0.9.2", - "sphinx>=2.4.4", + "sphinx<7.0.0,>=2.4.4", "sphinx-rtd-theme>=0.4.3", "sphinxcontrib-trio>=1.1.0", "autodocsumm>=0.1.13", From a015c66843c7d4ff2c8d7045136b0851d701c208 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 24 Aug 2023 11:45:38 +0200 Subject: [PATCH 05/13] now that lightsim2grid is released, check issue rte-france#508 --- grid2op/Backend/backend.py | 3 ++- grid2op/tests/test_issue_sim2real_storage.py | 21 ++++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index aa722c8df..917f41c2c 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -1753,9 +1753,10 @@ def assert_grid_correct(self): self._init_class_attr() # hack due to changing class of imported module in the module itself - self.__class__ = type(self).init_grid( + future_cls = orig_type.init_grid( type(self), force_module=type(self).__module__ ) + self.__class__ = future_cls setattr( sys.modules[type(self).__module__], self.__class__.__name__, diff --git a/grid2op/tests/test_issue_sim2real_storage.py b/grid2op/tests/test_issue_sim2real_storage.py index a78cb80b4..4d32a1e5f 100644 --- a/grid2op/tests/test_issue_sim2real_storage.py +++ b/grid2op/tests/test_issue_sim2real_storage.py @@ -10,18 +10,19 @@ import unittest import grid2op -from grid2op.Parameters import Parameters from grid2op.tests.helper_path_test import * +from grid2op.Backend import PandaPowerBackend from lightsim2grid import LightSimBackend -class TestSim2realStorage(unittest.TestCase): +class _AuxTestSim2realStorage: def setUp(self) -> None: + print(f"\n\n\nfor {type(self.get_backend())}") with warnings.catch_warnings(): warnings.filterwarnings("ignore") self.env = grid2op.make(os.path.join(PATH_DATA_TEST, "educ_case14_storage_diffgrid"), test=True, - backend=LightSimBackend()) + backend=self.get_backend()) self.env.seed(0) self.env.set_id(0) @@ -29,8 +30,20 @@ def tearDown(self) -> None: self.env.close() return super().tearDown() - def test_only_kwargs(self): + def test_reset(self): obs = self.env.reset() + assert obs.n_storage == 2 + +class TestSim2realStorageLS(_AuxTestSim2realStorage, unittest.TestCase): + def get_backend(self): + return LightSimBackend() + + +class TestSim2realStoragePP(_AuxTestSim2realStorage, unittest.TestCase): + def get_backend(self): + return PandaPowerBackend() + + if __name__ == '__main__': unittest.main() From 2a4801a0fe2961f165b7b3025f6214fe718ea338 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 24 Aug 2023 14:12:47 +0200 Subject: [PATCH 06/13] adressing issue rte-france#511 --- grid2op/Action/baseAction.py | 8 +++-- grid2op/tests/test_issue_511.py | 52 +++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 grid2op/tests/test_issue_511.py diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 17c680e6c..8c58c9145 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -3231,8 +3231,9 @@ def as_dict(self) -> dict: sub_id = "{}".format(substation_id) if not sub_id in res["change_bus_vect"]: res["change_bus_vect"][sub_id] = {} - res["change_bus_vect"][sub_id]["{}".format(obj_id)] = { - "type": objt_type + res["change_bus_vect"][sub_id]["{}_{}".format(objt_type, obj_id)] = { + "type": objt_type, + "id": obj_id, } all_subs.add(sub_id) @@ -3252,8 +3253,9 @@ def as_dict(self) -> dict: sub_id = "{}".format(substation_id) if not sub_id in res["set_bus_vect"]: res["set_bus_vect"][sub_id] = {} - res["set_bus_vect"][sub_id]["{}".format(obj_id)] = { + res["set_bus_vect"][sub_id]["{}_{}".format(objt_type, obj_id)] = { "type": objt_type, + "id": obj_id, "new_bus": k, } all_subs.add(sub_id) diff --git a/grid2op/tests/test_issue_511.py b/grid2op/tests/test_issue_511.py new file mode 100644 index 000000000..8499042a6 --- /dev/null +++ b/grid2op/tests/test_issue_511.py @@ -0,0 +1,52 @@ + +# Copyright (c) 2023, RTE (https://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + +import grid2op +from grid2op.Parameters import Parameters +import warnings +import unittest + +class Issue511Tester(unittest.TestCase): + def setUp(self) -> None: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make( + "l2rpn_idf_2023", + test=True + ) + return super().setUp() + + def test_issue_set_bus(self): + act = { + "set_bus": { + "lines_or_id": [(0, 2)], + "loads_id": [(0, 2)], + }, + } + + 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): + act = { + "change_bus": { + "lines_or_id": [0], + "loads_id": [0], + }, + } + + topo_action = self.env.action_space(act) + as_dict = topo_action.as_dict() + assert len(as_dict['change_bus_vect']['0']) == 2 # two objects modified + + +if __name__ == '__main__': + unittest.main() From c8824f39be6504310a61a16029b170ec62253eed Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 24 Aug 2023 14:27:38 +0200 Subject: [PATCH 07/13] improving docs following rte-france#509 --- grid2op/Observation/baseObservation.py | 11 +++++++++ grid2op/Space/GridObjects.py | 33 +++++++++++++++++++++----- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/grid2op/Observation/baseObservation.py b/grid2op/Observation/baseObservation.py index ca2aa11ae..872290c33 100644 --- a/grid2op/Observation/baseObservation.py +++ b/grid2op/Observation/baseObservation.py @@ -3404,6 +3404,17 @@ def to_dict(self): ------- The returned dictionary is not necessarily json serializable. To have a grid2op observation that you can serialize in a json fashion, please use the :func:`grid2op.Space.GridObjects.to_json` function. + + .. note:: + This function is different to the :func:`grid2op.Space.GridObjects.to_dict`. + Indeed the dictionnary resulting from this function will count as keys all the attributes + in :attr:`GridObjects.attr_list_vect` only. + + Concretely, if `obs` is an observation (:class:`grid2op.Observation.BaseObservation`) + then `obs.to_dict()` will have the keys `type(obs).attr_list_vect` and the values will + be numpy arrays whereas `obs.to_json()` will have the keys + `type(obs).attr_list_vect` and `type(obs).attr_list_json` and the values will be + lists (serializable). """ if self._dictionnarized is None: diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 99bbfaa43..e885328da 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -898,8 +898,21 @@ def to_vect(self): def to_json(self, convert=True): """ Convert this instance of GridObjects to a dictionary that can be json serialized. - - convert: do you convert the numpy types to standard python list (might take lots of time) + + .. note:: + This function is different to the :func:`grid2op.Observation.BaseObservation.to_dict`. + Indeed the dictionnary resulting from this function will count as keys all the attributes + in :attr:`GridObjects.attr_list_vect` and :attr:`GridObjects.attr_list_json`. + + Concretely, if `obs` is an observation (:class:`grid2op.Observation.BaseObservation`) + then `obs.to_dict()` will have the keys `type(obs).attr_list_vect` and the values will + be numpy arrays whereas `obs.to_json()` will have the keys + `type(obs).attr_list_vect` and `type(obs).attr_list_json` and the values will be + lists (serializable) + + .. warning:: + convert: do you convert the numpy types to standard python list (might take lots of time) + TODO doc and example """ @@ -909,11 +922,12 @@ def to_json(self, convert=True): # or even storing the things in [id, value] for these types of attributes (time_before_cooldown_line, # time_before_cooldown_sub, time_next_maintenance, duration_next_maintenance etc.) + cls = type(self) res = {} - for attr_nm in self.attr_list_vect + self.attr_list_json: + for attr_nm in cls.attr_list_vect + cls.attr_list_json: res[attr_nm] = self._get_array_from_attr_name(attr_nm) if convert: - self._convert_to_json(res) # TODO ! + cls._convert_to_json(res) return res def from_json(self, dict_): @@ -945,8 +959,9 @@ def from_json(self, dict_): type_ = type(my_attr) setattr(self, key, type_(array_[0])) - def _convert_to_json(self, dict_): - for attr_nm in self.attr_list_vect + self.attr_list_json: + @classmethod + def _convert_to_json(cls, dict_): + for attr_nm in cls.attr_list_vect + cls.attr_list_json: tmp = dict_[attr_nm] dtype = tmp.dtype if dtype == dt_float: @@ -955,6 +970,12 @@ def _convert_to_json(self, dict_): dict_[attr_nm] = [int(el) for el in tmp] elif dtype == dt_bool: dict_[attr_nm] = [bool(el) for el in tmp] + elif dtype == float: + dict_[attr_nm] = [float(el) for el in tmp] + elif dtype == int: + dict_[attr_nm] = [int(el) for el in tmp] + elif dtype == bool: + dict_[attr_nm] = [bool(el) for el in tmp] def shape(self): """ From 62e5aa5607fc341a57f62e8c6ce0a1e04d6a42e8 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 24 Aug 2023 14:28:43 +0200 Subject: [PATCH 08/13] adressing issue rte-france#507 [skip ci] --- grid2op/Agent/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/grid2op/Agent/__init__.py b/grid2op/Agent/__init__.py index a3b7a4165..6fa173429 100644 --- a/grid2op/Agent/__init__.py +++ b/grid2op/Agent/__init__.py @@ -19,7 +19,8 @@ "MLAgent", "RecoPowerlineAgent", "FromActionsListAgent", - "RecoPowerlinePerArea" + "RecoPowerlinePerArea", + "AlertAgent" ] from grid2op.Agent.baseAgent import BaseAgent @@ -35,3 +36,4 @@ from grid2op.Agent.recoPowerlineAgent import RecoPowerlineAgent from grid2op.Agent.fromActionsListAgent import FromActionsListAgent from grid2op.Agent.recoPowerLinePerArea import RecoPowerlinePerArea +from grid2op.Agent.alertAgent import AlertAgent From 13a5a049d11112203fb9382f062f195906fbae57 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 25 Aug 2023 11:08:46 +0200 Subject: [PATCH 09/13] add an helper function to disable the opponent --- CHANGELOG.rst | 1 + docs/opponent.rst | 7 ++++++ grid2op/Opponent/__init__.py | 1 + grid2op/Opponent/utils.py | 44 ++++++++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+) create mode 100644 grid2op/Opponent/utils.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c1e93247b..a0e8acefe 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -39,6 +39,7 @@ Change Log - [FIXED] issue https://github.com/rte-france/Grid2Op/issues/508 - [ADDED] some classes that can be used to reproduce exactly what happened in a previously run environment see `grid2op.Chronics.FromOneEpisodeData` and `grid2op.Opponent.FromEpisodeDataOpponent` +- [ADDED] An helper function to get the kwargs to disable the opponent (see `grid2op.Opponent.get_kwargs_no_opponent()`) - [IMPROVED] doc of `obs.to_dict` and `obs.to_json` (see https://github.com/rte-france/Grid2Op/issues/509) [1.9.3] - 2023-07-28 diff --git a/docs/opponent.rst b/docs/opponent.rst index 9731963bb..71aa5b171 100644 --- a/docs/opponent.rst +++ b/docs/opponent.rst @@ -171,6 +171,13 @@ deactivate it, you can do this by customization the call to "grid2op.make" like from grid2op.Opponent import BaseOpponent, NeverAttackBudget env_name = ... + + # if you want to disable the opponent you can do (grid2op >= 1.9.4) + kwargs_no_opp = grid2op.Opponent.get_kwargs_no_opponent() + env_no_opp = grid2op.make(env_name, **kwargs_no_opp) + # and there the opponent is disabled + + # or, in a more complex fashion (or for older grid2op version <= 1.9.3) env_without_opponent = grid2op.make(env_name, opponent_attack_cooldown=999999, opponent_attack_duration=0, diff --git a/grid2op/Opponent/__init__.py b/grid2op/Opponent/__init__.py index c1a130f6b..53a25c9a0 100644 --- a/grid2op/Opponent/__init__.py +++ b/grid2op/Opponent/__init__.py @@ -21,3 +21,4 @@ from grid2op.Opponent.geometricOpponent import GeometricOpponent from grid2op.Opponent.geometricOpponentMultiArea import GeometricOpponentMultiArea from grid2op.Opponent.fromEpisodeDataOpponent import FromEpisodeDataOpponent +from grid2op.Opponent.utils import get_kwargs_no_opponent \ No newline at end of file diff --git a/grid2op/Opponent/utils.py b/grid2op/Opponent/utils.py new file mode 100644 index 000000000..2512f91c4 --- /dev/null +++ b/grid2op/Opponent/utils.py @@ -0,0 +1,44 @@ +# Copyright (c) 2023, RTE (https://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + +from typing import Dict +from grid2op.Opponent.neverAttackBudget import NeverAttackBudget +from grid2op.Opponent.baseOpponent import BaseOpponent +from grid2op.Action import DontAct + + +def get_kwargs_no_opponent() -> Dict: + """This dict allows to retrieve a dictionnary you can use as kwargs to disable the opponent. + + Examples + -------- + + You can use it like + + .. code-block:: + + import grid2op + env_name = "l2rpn_case14_sandbox" # or any other name + env = grid2op.make(env_name) # en with possibly an opponent + + # if you want to disable the opponent you can do + kwargs_no_opp = grid2op.Opponent.get_kwargs_no_opponent() + env_no_opp = grid2op.make(env_name, **kwargs_no_opp) + # and there the opponent is disabled + + """ + res = { + "opponent_attack_cooldown": 99999999, + "opponent_attack_duration": 0, + "opponent_budget_per_ts": 0., + "opponent_init_budget": 0., + "opponent_action_class": DontAct, + "opponent_class": BaseOpponent, + "opponent_budget_class": NeverAttackBudget, + } + return res From 3ba325eda9ea1b678a8ffb4c69c863d0bec4952a Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 25 Aug 2023 11:09:53 +0200 Subject: [PATCH 10/13] change the name of the default env in the doc --- grid2op/Reward/_alarmScore.py | 2 +- grid2op/Reward/alarmReward.py | 2 +- grid2op/Reward/baseReward.py | 2 +- grid2op/Reward/bridgeReward.py | 2 +- grid2op/Reward/closeToOverflowReward.py | 2 +- grid2op/Reward/constantReward.py | 2 +- grid2op/Reward/distanceReward.py | 2 +- grid2op/Reward/economicReward.py | 2 +- grid2op/Reward/episodeDurationReward.py | 2 +- grid2op/Reward/flatReward.py | 2 +- grid2op/Reward/gameplayReward.py | 2 +- grid2op/Reward/increasingFlatReward.py | 2 +- grid2op/Reward/l2RPNReward.py | 2 +- grid2op/Reward/linesCapacityReward.py | 2 +- grid2op/Reward/linesReconnectedReward.py | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/grid2op/Reward/_alarmScore.py b/grid2op/Reward/_alarmScore.py index b4c69ff5d..d35b67cb6 100644 --- a/grid2op/Reward/_alarmScore.py +++ b/grid2op/Reward/_alarmScore.py @@ -44,7 +44,7 @@ class _AlarmScore(AlarmReward): import grid2op from grid2op.Reward import AlarmReward # then you create your environment with it: - NAME_OF_THE_ENVIRONMENT = "rte_case14_realistic" + NAME_OF_THE_ENVIRONMENT = "l2rpn_case14_sandbox" env = grid2op.make(NAME_OF_THE_ENVIRONMENT,reward_class=AlarmScore) # and do a step with a "do nothing" action obs = env.reset() diff --git a/grid2op/Reward/alarmReward.py b/grid2op/Reward/alarmReward.py index d9d2cf87a..cd9d8bd73 100644 --- a/grid2op/Reward/alarmReward.py +++ b/grid2op/Reward/alarmReward.py @@ -35,7 +35,7 @@ class AlarmReward(BaseReward): from grid2op.Reward import AlarmReward # then you create your environment with it: - NAME_OF_THE_ENVIRONMENT = "rte_case14_realistic" + NAME_OF_THE_ENVIRONMENT = "l2rpn_case14_sandbox" env = grid2op.make(NAME_OF_THE_ENVIRONMENT,reward_class=AlarmReward) # and do a step with a "do nothing" action obs = env.reset() diff --git a/grid2op/Reward/baseReward.py b/grid2op/Reward/baseReward.py index 164f54e0b..882dd348a 100644 --- a/grid2op/Reward/baseReward.py +++ b/grid2op/Reward/baseReward.py @@ -72,7 +72,7 @@ def __call__(action, env, has_error, is_done, is_illegal, is_ambiguous): return res # then you create your environment with it: - NAME_OF_THE_ENVIRONMENT = "rte_case14_realistic" + NAME_OF_THE_ENVIRONMENT = "l2rpn_case14_sandbox" env = grid2op.make(NAME_OF_THE_ENVIRONMENT,reward_class=SumOfFlowReward) # and do a step with a "do nothing" action obs = env.reset() diff --git a/grid2op/Reward/bridgeReward.py b/grid2op/Reward/bridgeReward.py index c9866d497..70709534a 100644 --- a/grid2op/Reward/bridgeReward.py +++ b/grid2op/Reward/bridgeReward.py @@ -28,7 +28,7 @@ class BridgeReward(BaseReward): from grid2op.Reward import BridgeReward # then you create your environment with it: - NAME_OF_THE_ENVIRONMENT = "rte_case14_realistic" + NAME_OF_THE_ENVIRONMENT = "l2rpn_case14_sandbox" env = grid2op.make(NAME_OF_THE_ENVIRONMENT,reward_class=BridgeReward) # and do a step with a "do nothing" action obs = env.reset() diff --git a/grid2op/Reward/closeToOverflowReward.py b/grid2op/Reward/closeToOverflowReward.py index 3bb74a16f..d74a9b7b2 100644 --- a/grid2op/Reward/closeToOverflowReward.py +++ b/grid2op/Reward/closeToOverflowReward.py @@ -26,7 +26,7 @@ class CloseToOverflowReward(BaseReward): from grid2op.Reward import CloseToOverflowReward # then you create your environment with it: - NAME_OF_THE_ENVIRONMENT = "rte_case14_realistic" + NAME_OF_THE_ENVIRONMENT = "l2rpn_case14_sandbox" env = grid2op.make(NAME_OF_THE_ENVIRONMENT,reward_class=CloseToOverflowReward) # and do a step with a "do nothing" action obs = env.reset() diff --git a/grid2op/Reward/constantReward.py b/grid2op/Reward/constantReward.py index b5ac0196f..3b1edb8c0 100644 --- a/grid2op/Reward/constantReward.py +++ b/grid2op/Reward/constantReward.py @@ -28,7 +28,7 @@ class ConstantReward(BaseReward): from grid2op.Reward import ConstantReward # then you create your environment with it: - NAME_OF_THE_ENVIRONMENT = "rte_case14_realistic" + NAME_OF_THE_ENVIRONMENT = "l2rpn_case14_sandbox" env = grid2op.make(NAME_OF_THE_ENVIRONMENT,reward_class=ConstantReward) # and do a step with a "do nothing" action obs = env.reset() diff --git a/grid2op/Reward/distanceReward.py b/grid2op/Reward/distanceReward.py index 310421a7c..a50c8f81f 100644 --- a/grid2op/Reward/distanceReward.py +++ b/grid2op/Reward/distanceReward.py @@ -26,7 +26,7 @@ class DistanceReward(BaseReward): from grid2op.Reward import DistanceReward # then you create your environment with it: - NAME_OF_THE_ENVIRONMENT = "rte_case14_realistic" + NAME_OF_THE_ENVIRONMENT = "l2rpn_case14_sandbox" env = grid2op.make(NAME_OF_THE_ENVIRONMENT,reward_class=DistanceReward) # and do a step with a "do nothing" action obs = env.reset() diff --git a/grid2op/Reward/economicReward.py b/grid2op/Reward/economicReward.py index ff46d994b..bc03e4570 100644 --- a/grid2op/Reward/economicReward.py +++ b/grid2op/Reward/economicReward.py @@ -31,7 +31,7 @@ class EconomicReward(BaseReward): from grid2op.Reward import EconomicReward # then you create your environment with it: - NAME_OF_THE_ENVIRONMENT = "rte_case14_realistic" + NAME_OF_THE_ENVIRONMENT = "l2rpn_case14_sandbox" env = grid2op.make(NAME_OF_THE_ENVIRONMENT,reward_class=EconomicReward) # and do a step with a "do nothing" action obs = env.reset() diff --git a/grid2op/Reward/episodeDurationReward.py b/grid2op/Reward/episodeDurationReward.py index 966218a80..b855abb69 100644 --- a/grid2op/Reward/episodeDurationReward.py +++ b/grid2op/Reward/episodeDurationReward.py @@ -26,7 +26,7 @@ class EpisodeDurationReward(BaseReward): from grid2op.Reward import EpisodeDurationReward # then you create your environment with it: - NAME_OF_THE_ENVIRONMENT = "rte_case14_realistic" + NAME_OF_THE_ENVIRONMENT = "l2rpn_case14_sandbox" env = grid2op.make(NAME_OF_THE_ENVIRONMENT,reward_class=EpisodeDurationReward) # and do a step with a "do nothing" action obs = env.reset() diff --git a/grid2op/Reward/flatReward.py b/grid2op/Reward/flatReward.py index 9166b43d8..02e9e09ce 100644 --- a/grid2op/Reward/flatReward.py +++ b/grid2op/Reward/flatReward.py @@ -24,7 +24,7 @@ class FlatReward(BaseReward): from grid2op.Reward import FlatReward # then you create your environment with it: - NAME_OF_THE_ENVIRONMENT = "rte_case14_realistic" + NAME_OF_THE_ENVIRONMENT = "l2rpn_case14_sandbox" env = grid2op.make(NAME_OF_THE_ENVIRONMENT,reward_class=FlatReward) # and do a step with a "do nothing" action obs = env.reset() diff --git a/grid2op/Reward/gameplayReward.py b/grid2op/Reward/gameplayReward.py index 02c74d399..22eef2db1 100644 --- a/grid2op/Reward/gameplayReward.py +++ b/grid2op/Reward/gameplayReward.py @@ -27,7 +27,7 @@ class GameplayReward(BaseReward): from grid2op.Reward import GameplayReward # then you create your environment with it: - NAME_OF_THE_ENVIRONMENT = "rte_case14_realistic" + NAME_OF_THE_ENVIRONMENT = "l2rpn_case14_sandbox" env = grid2op.make(NAME_OF_THE_ENVIRONMENT,reward_class=GameplayReward) # and do a step with a "do nothing" action obs = env.reset() diff --git a/grid2op/Reward/increasingFlatReward.py b/grid2op/Reward/increasingFlatReward.py index 33ba1e21f..5bd29ac8b 100644 --- a/grid2op/Reward/increasingFlatReward.py +++ b/grid2op/Reward/increasingFlatReward.py @@ -27,7 +27,7 @@ class IncreasingFlatReward(BaseReward): from grid2op.Reward import IncreasingFlatReward # then you create your environment with it: - NAME_OF_THE_ENVIRONMENT = "rte_case14_realistic" + NAME_OF_THE_ENVIRONMENT = "l2rpn_case14_sandbox" env = grid2op.make(NAME_OF_THE_ENVIRONMENT,reward_class=IncreasingFlatReward) # and do a step with a "do nothing" action obs = env.reset() diff --git a/grid2op/Reward/l2RPNReward.py b/grid2op/Reward/l2RPNReward.py index c15e5ade7..889f7b789 100644 --- a/grid2op/Reward/l2RPNReward.py +++ b/grid2op/Reward/l2RPNReward.py @@ -36,7 +36,7 @@ class L2RPNReward(BaseReward): from grid2op.Reward import L2RPNReward # then you create your environment with it: - NAME_OF_THE_ENVIRONMENT = "rte_case14_realistic" + NAME_OF_THE_ENVIRONMENT = "l2rpn_case14_sandbox" env = grid2op.make(NAME_OF_THE_ENVIRONMENT,reward_class=L2RPNReward) # and do a step with a "do nothing" action obs = env.reset() diff --git a/grid2op/Reward/linesCapacityReward.py b/grid2op/Reward/linesCapacityReward.py index e251f3870..c8ff7f0ae 100644 --- a/grid2op/Reward/linesCapacityReward.py +++ b/grid2op/Reward/linesCapacityReward.py @@ -31,7 +31,7 @@ class LinesCapacityReward(BaseReward): from grid2op.Reward import LinesCapacityReward # then you create your environment with it: - NAME_OF_THE_ENVIRONMENT = "rte_case14_realistic" + NAME_OF_THE_ENVIRONMENT = "l2rpn_case14_sandbox" env = grid2op.make(NAME_OF_THE_ENVIRONMENT,reward_class=LinesCapacityReward) # and do a step with a "do nothing" action obs = env.reset() diff --git a/grid2op/Reward/linesReconnectedReward.py b/grid2op/Reward/linesReconnectedReward.py index 3715961e1..be9f1c4d2 100644 --- a/grid2op/Reward/linesReconnectedReward.py +++ b/grid2op/Reward/linesReconnectedReward.py @@ -28,7 +28,7 @@ class LinesReconnectedReward(BaseReward): from grid2op.Reward import LinesReconnectedReward # then you create your environment with it: - NAME_OF_THE_ENVIRONMENT = "rte_case14_realistic" + NAME_OF_THE_ENVIRONMENT = "l2rpn_case14_sandbox" env = grid2op.make(NAME_OF_THE_ENVIRONMENT,reward_class=LinesReconnectedReward) # and do a step with a "do nothing" action obs = env.reset() From 37fd31a75985262ba5657e7f03336ba4622ec695 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 25 Aug 2023 11:12:26 +0200 Subject: [PATCH 11/13] the class FromOneEpisodeData seems almost over and tested --- grid2op/Action/serializableActionSpace.py | 2 +- grid2op/Chronics/fromOneEpisodeData.py | 64 +++-- grid2op/Converter/ConnectivityConverter.py | 2 +- grid2op/Download/download.py | 2 +- grid2op/Environment/baseEnv.py | 4 +- grid2op/Environment/environment.py | 4 +- grid2op/Opponent/fromEpisodeDataOpponent.py | 2 +- grid2op/tests/test_env_from_episode.py | 274 ++++++++++++++------ 8 files changed, 250 insertions(+), 104 deletions(-) diff --git a/grid2op/Action/serializableActionSpace.py b/grid2op/Action/serializableActionSpace.py index 7c11d2312..4264b839e 100644 --- a/grid2op/Action/serializableActionSpace.py +++ b/grid2op/Action/serializableActionSpace.py @@ -145,7 +145,7 @@ def supports_type(self, action_type): import grid2op from grid2op.Converter import ConnectivityConverter - env = grid2op.make("rte_case14_realistic", test=True) + env = grid2op.make("l2rpn_case14_sandbox", test=True) can_i_use_set_bus = env.action_space.supports_type("set_bus") # this is True env2 = grid2op.make("educ_case14_storage", test=True) diff --git a/grid2op/Chronics/fromOneEpisodeData.py b/grid2op/Chronics/fromOneEpisodeData.py index 7123de48e..7ae603110 100644 --- a/grid2op/Chronics/fromOneEpisodeData.py +++ b/grid2op/Chronics/fromOneEpisodeData.py @@ -192,7 +192,27 @@ def __init__( raise ChronicsError("FromOneEpisodeData can only read data either directly from an EpisodeData, " "from a path pointing to one, or from a tuple") self.current_inj = None - + + if list_perfect_forecasts is not None: + self.list_perfect_forecasts = list_perfect_forecasts + else: + self.list_perfect_forecasts = [] + self._check_list_perfect_forecasts() + + def _check_list_perfect_forecasts(self): + if not self.list_perfect_forecasts: + return + self.list_perfect_forecasts = [int(el) for el in self.list_perfect_forecasts] + for horizon in self.list_perfect_forecasts: + tmp = horizon * 60. / self.time_interval.total_seconds() + if tmp - int(tmp) != 0: + raise ChronicsError(f"All forecast horizons should be multiple of self.time_interval (and given in minutes), found {horizon}") + + for h_id, horizon in enumerate(self.list_perfect_forecasts): + if horizon * 60 != (h_id + 1) * (self.time_interval.total_seconds()): + raise ChronicsError("For now all horizons should be consecutive, you cannot 'skip' a forecast: (5, 10, 15) " + "is ok but (5, 15, 30) is NOT.") + def initialize( self, order_backend_loads, @@ -286,25 +306,33 @@ def _aux_forecasts(self, h_id, dict_, key, dict_[key] = dt_float(1.0) * tmp_ def forecasts(self): - res = [] - return res - # TODO - if not self._forcast_handlers: - # nothing to handle forecast in this class - return res + """Retrieve PERFECT forecast from this time series generator. - handlers = (self.load_p_handler, self.load_q_handler, self.gen_p_handler, self.gen_v_handler) - for h_id, h in enumerate(self._forcast_handlers[0].get_available_horizons()): - dict_ = {} - self._aux_forecasts(h_id, dict_, "load_p", self.load_p_for_handler, self.load_p_handler, handlers) - self._aux_forecasts(h_id, dict_, "load_q", self.load_q_for_handler, self.load_q_handler, handlers) - self._aux_forecasts(h_id, dict_, "prod_p", self.gen_p_for_handler, self.gen_p_handler, handlers) - self._aux_forecasts(h_id, dict_, "prod_v", self.gen_v_for_handler, self.gen_v_handler, handlers) + .. alert:: + These are perfect forecast and not the original forecasts. - res_d = {} - if dict_: - res_d["injection"] = dict_ + Notes + ----- + As in grid2op the forecast information is not stored by the runner, + it is NOT POSSIBLE to retrieve the forecast informations used by the + "original" env (the one that generated the EpisodeData). + + This class however, thanks to the `list_perfect_forecasts` kwarg you + can set at building time, can generate perfect forecasts: the agent will + see into the future if using these forecasts. + """ + if not self.list_perfect_forecasts: + return [] + + res = [] + for h_id, h in enumerate(self.list_perfect_forecasts): + res_d = {} + obs = self._episode_data.observations[min(self.curr_iter + h_id, len(self._episode_data) - 1)] + # load the injection + dict_inj, prod_v = self._load_injection(obs) + dict_inj["prod_v"] = prod_v + res_d["injection"] = dict_inj forecast_datetime = self.current_datetime + timedelta(minutes=h) res.append((forecast_datetime, res_d)) return res @@ -355,7 +383,7 @@ def _load_injection(self, obs): return dict_, prod_v - def _init_date_time(self): # in csv handler + def _init_date_time(self): # from csv handler if os.path.exists(os.path.join(self.path, "start_datetime.info")): with open(os.path.join(self.path, "start_datetime.info"), "r") as f: a = f.read().rstrip().lstrip() diff --git a/grid2op/Converter/ConnectivityConverter.py b/grid2op/Converter/ConnectivityConverter.py index 7aaaa15ea..5826c1bcc 100644 --- a/grid2op/Converter/ConnectivityConverter.py +++ b/grid2op/Converter/ConnectivityConverter.py @@ -68,7 +68,7 @@ class ConnectivityConverter(Converter): import numpy as np from grid2op.Converter import ConnectivityConverter - env = grid2op.make("rte_case14_realistic", test=True) + env = grid2op.make("l2rpn_case14_sandbox", test=True) converter = ConnectivityConverter(env.action_space) # it's a good practice to seed the element that can be, for reproducibility converter.seed(0) diff --git a/grid2op/Download/download.py b/grid2op/Download/download.py index 061536498..2f7b1f07e 100755 --- a/grid2op/Download/download.py +++ b/grid2op/Download/download.py @@ -53,7 +53,7 @@ def download_cli(): ) parser.add_argument( "--name", - default="rte_case14_redisp", + default="l2rpn_case14_sandbox", type=str, help="The name of the dataset (one of {} )." "".format(",".join(LI_VALID_ENV)), ) diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index a25a90e73..a71542416 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -1478,7 +1478,7 @@ def deactivate_forecast(self): import grid2op from grid2op.Chronics import GridStateFromFile # tell grid2op not to read the "forecast" - env = grid2op.make("rte_case14_realistic", data_feeding_kwargs={"gridvalueClass": GridStateFromFile}) + env = grid2op.make("l2rpn_case14_sandbox", data_feeding_kwargs={"gridvalueClass": GridStateFromFile}) do_nothing_action = env.action_space() @@ -1523,7 +1523,7 @@ def reactivate_forecast(self): import grid2op from grid2op.Chronics import GridStateFromFile # tell grid2op not to read the "forecast" - env = grid2op.make("rte_case14_realistic", data_feeding_kwargs={"gridvalueClass": GridStateFromFile}) + env = grid2op.make("l2rpn_case14_sandbox", data_feeding_kwargs={"gridvalueClass": GridStateFromFile}) do_nothing_action = env.action_space() diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index ca77cb4f9..08763efba 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -697,7 +697,7 @@ def set_id(self, id_): from grid2op import make from grid2op.BaseAgent import DoNothingAgent - env = make("rte_case14_realistic") # create an environment + env = make("l2rpn_case14_sandbox") # create an environment agent = DoNothingAgent(env.action_space) # create an BaseAgent for i in range(10): @@ -717,7 +717,7 @@ def set_id(self, id_): from grid2op import make from grid2op.BaseAgent import DoNothingAgent - env = make("rte_case14_realistic") # create an environment + env = make("l2rpn_case14_sandbox") # create an environment agent = DoNothingAgent(env.action_space) # create an BaseAgent scenario_order = [1,2,3,4,5,10,8,6,5,7,78, 8] for id_ in scenario_order: diff --git a/grid2op/Opponent/fromEpisodeDataOpponent.py b/grid2op/Opponent/fromEpisodeDataOpponent.py index ff2a08e1d..4e296f5b8 100644 --- a/grid2op/Opponent/fromEpisodeDataOpponent.py +++ b/grid2op/Opponent/fromEpisodeDataOpponent.py @@ -82,7 +82,7 @@ def init(self, partial_env, **kwargs): def reset(self, initial_budget): self._attacks = copy.deepcopy(self._ptr_env.chronics_handler.real_data._episode_data.attacks) - if self._ptr_env._oppSpace.attack_cooldown != 1: + if self._ptr_env._oppSpace.attack_cooldown > 1: if not self._warning_cooldown_issued: self._warning_cooldown_issued = True warnings.warn('When using FromEpisodeDataOpponent, make sure that your ' diff --git a/grid2op/tests/test_env_from_episode.py b/grid2op/tests/test_env_from_episode.py index fc9df2b8f..b6a1621fb 100644 --- a/grid2op/tests/test_env_from_episode.py +++ b/grid2op/tests/test_env_from_episode.py @@ -13,18 +13,13 @@ import tempfile import grid2op -from grid2op.Action import BaseAction, DontAct, TopologyAction, PowerlineSetAction -from grid2op.Backend import PandaPowerBackend -from grid2op.Environment import BaseEnv, Environment -from grid2op.Observation import BaseObservation, CompleteObservation -from grid2op.Opponent import BaseOpponent, NeverAttackBudget -from grid2op.Opponent.opponentSpace import OpponentSpace -from grid2op.Reward import FlatReward -from grid2op.Rules import AlwaysLegal +from grid2op.Action import BaseAction, DontAct, PowerlineSetAction +from grid2op.Environment import BaseEnv +from grid2op.Observation import BaseObservation from grid2op.Runner import Runner -from grid2op.Chronics import FromOneEpisodeData, Multifolder, GridStateFromFileWithForecasts, GridStateFromFile, ChronicsHandler -from grid2op.Agent import BaseAgent, DoNothingAgent -from grid2op.Exceptions import Grid2OpException +from grid2op.Chronics import FromOneEpisodeData, Multifolder, ChronicsHandler +from grid2op.Agent import BaseAgent +from grid2op.Exceptions import Grid2OpException, ChronicsError from grid2op.Agent import RecoPowerlineAgent from grid2op.Episode import EpisodeData from grid2op.Opponent import (FromEpisodeDataOpponent, @@ -33,9 +28,6 @@ import pdb -from grid2op.VoltageControler import ControlVoltageFromFile -from grid2op.operator_attention import LinearAttentionBudget - class GameOverTestAgent(BaseAgent): def act(self, observation: BaseObservation, reward: float, done: bool = False) -> BaseAction: @@ -84,12 +76,18 @@ def custom_initialize(multifolder_obj, return res -class TestTSFromEpisode(unittest.TestCase): +class TestTSFromEpisodeMaintenance(unittest.TestCase): def setUp(self) -> None: - self.env_name = "l2rpn_idf_2023" # with maintenance and attacks ! + self.env_name = "l2rpn_idf_2023" # with maintenance but without attacks (attacks are tested elsewhere) with warnings.catch_warnings(): warnings.filterwarnings("ignore") - self.env = grid2op.make(self.env_name, test=True) + self.env = grid2op.make(self.env_name, + test=True, + opponent_attack_cooldown=99999999, + opponent_attack_duration=0, + opponent_budget_per_ts=0., + opponent_init_budget=0., + opponent_action_class=DontAct,) self.env.set_id(0) self.env.seed(0) self.max_iter = 10 @@ -115,10 +113,17 @@ def test_basic(self): ) res = runner.run(nb_episode=1, max_iter=self.max_iter, add_detailed_output=True) ep_data = res[0][-1] - env = grid2op.make(self.env_name, - test=True, - chronics_class=FromOneEpisodeData, - data_feeding_kwargs={"ep_data": ep_data}) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make(self.env_name, + test=True, + chronics_class=FromOneEpisodeData, + data_feeding_kwargs={"ep_data": ep_data}, + opponent_attack_cooldown=99999999, + opponent_attack_duration=0, + opponent_budget_per_ts=0., + opponent_init_budget=0., + opponent_action_class=DontAct) obs = env.reset() self._aux_obs_equal(obs, ep_data.observations[0]) for i in range(10): @@ -135,7 +140,86 @@ def test_basic(self): obs, reward, done, info = env.step(env.action_space()) self._aux_obs_equal(obs, ep_data.observations[i+1], f"at it. {i} (after reset)") assert done - + + def test_fast_forward(self): + """test the correct behaviour of env.fast_forward_chronics""" + obs = self.env.reset() + runner = Runner( + **self.env.get_params_for_runner() + ) + res = runner.run(nb_episode=1, max_iter=self.max_iter, add_detailed_output=True) + ep_data = res[0][-1] + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make(self.env_name, + test=True, + chronics_class=FromOneEpisodeData, + data_feeding_kwargs={"ep_data": ep_data}, + opponent_attack_cooldown=99999999, + opponent_attack_duration=0, + opponent_budget_per_ts=0., + opponent_init_budget=0., + opponent_action_class=DontAct) + obs = env.reset() + env.fast_forward_chronics(2) + obs = env.get_obs() + self._aux_obs_equal(obs, ep_data.observations[2]) + for i in range(8): + obs, reward, done, info = env.step(env.action_space()) + self._aux_obs_equal(obs, ep_data.observations[i+3], f"at it. {i} (after reset)") + + def test_forecasts(self): + """test the forecasts property""" + obs = self.env.reset() + runner = Runner( + **self.env.get_params_for_runner() + ) + res = runner.run(nb_episode=1, max_iter=self.max_iter, add_detailed_output=True) + ep_data = res[0][-1] + with self.assertRaises(ChronicsError): + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make(self.env_name, + test=True, + chronics_class=FromOneEpisodeData, + data_feeding_kwargs={"ep_data": ep_data, + "list_perfect_forecasts": [1]}, + opponent_attack_cooldown=99999999, + opponent_attack_duration=0, + opponent_budget_per_ts=0., + opponent_init_budget=0., + opponent_action_class=DontAct,) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make(self.env_name, + test=True, + chronics_class=FromOneEpisodeData, + data_feeding_kwargs={"ep_data": ep_data, + "list_perfect_forecasts": [5]}, + opponent_attack_cooldown=99999999, + opponent_attack_duration=0, + opponent_budget_per_ts=0., + opponent_init_budget=0., + opponent_action_class=DontAct,) + + obs = env.reset() + self._aux_obs_equal(obs, ep_data.observations[0]) + for i in range(8): + obs, reward, done, info = env.step(env.action_space()) + self._aux_obs_equal(obs, ep_data.observations[i+1], f"at it. {i} test_forecasts") + sim_o, *_ = obs.simulate(env.action_space()) + self._aux_obs_equal(sim_o, ep_data.observations[i+2], f"at it. {i} test_forecasts / forecasts") + + # test final observation (the one just before the game over) + obs, reward, done, info = env.step(env.action_space()) + self._aux_obs_equal(obs, ep_data.observations[9], f"final test_forecasts") + sim_o, *_ = obs.simulate(env.action_space()) + self._aux_obs_equal(sim_o, ep_data.observations[9], f"final test_forecasts forecast / forecasts") + + obs, reward, done, info = env.step(env.action_space()) + assert done + def test_when_game_over(self): """test I can load from a runner that used an agent that games over""" obs = self.env.reset() @@ -143,12 +227,21 @@ def test_when_game_over(self): **self.env.get_params_for_runner(), agentClass=GameOverTestAgent ) - res = runner.run(nb_episode=1, max_iter=self.max_iter, add_detailed_output=True) + res = runner.run(nb_episode=1, max_iter=self.max_iter, + env_seeds=[0], add_detailed_output=True) ep_data = res[0][-1] - env = grid2op.make(self.env_name, - test=True, - chronics_class=FromOneEpisodeData, - data_feeding_kwargs={"ep_data": ep_data}) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make(self.env_name, + test=True, + chronics_class=FromOneEpisodeData, + data_feeding_kwargs={"ep_data": ep_data}, + opponent_attack_cooldown=99999999, + opponent_attack_duration=0, + opponent_budget_per_ts=0., + opponent_init_budget=0., + opponent_action_class=DontAct) obs = env.reset() self._aux_obs_equal(obs, ep_data.observations[0]) for i in range(6): @@ -177,15 +270,24 @@ def test_maintenance(self): **env_dict_params ) # now run as usual - res = runner.run(nb_episode=1, max_iter=self.max_iter, add_detailed_output=True) + res = runner.run(nb_episode=1, max_iter=self.max_iter, + add_detailed_output=True, env_seeds=[0]) ep_data = res[0][-1] assert len(ep_data) == self.max_iter, f"{len(ep_data)} vs {self.max_iter}" assert ep_data.observations[2].time_next_maintenance[2] == 0 # check right maintenance is applied assert ep_data.observations[2].duration_next_maintenance[2] == 5 # check right maintenance is applied - env = grid2op.make(self.env_name, - test=True, - chronics_class=FromOneEpisodeData, - data_feeding_kwargs={"ep_data": ep_data}) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make(self.env_name, + test=True, + chronics_class=FromOneEpisodeData, + data_feeding_kwargs={"ep_data": ep_data}, + opponent_attack_cooldown=99999999, + opponent_attack_duration=0, + opponent_budget_per_ts=0., + opponent_init_budget=0., + opponent_action_class=DontAct) obs = env.reset() self._aux_obs_equal(obs, ep_data.observations[0], f"after reset") for i in range(10): @@ -199,15 +301,24 @@ def test_maintenance(self): agentClass=GameOverTestAgent ) # now run as usual - res2 = runner2.run(nb_episode=1, max_iter=self.max_iter, add_detailed_output=True) + res2 = runner2.run(nb_episode=1, max_iter=self.max_iter, + add_detailed_output=True, env_seeds=[0]) ep_data2 = res2[0][-1] assert len(ep_data2) == 7, f"{len(ep_data2)} vs 7" assert ep_data2.observations[2].time_next_maintenance[2] == 0 # check right maintenance is applied assert ep_data2.observations[2].duration_next_maintenance[2] == 5 # check right maintenance is applied - env = grid2op.make(self.env_name, - test=True, - chronics_class=FromOneEpisodeData, - data_feeding_kwargs={"ep_data": ep_data2}) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make(self.env_name, + test=True, + chronics_class=FromOneEpisodeData, + data_feeding_kwargs={"ep_data": ep_data2}, + opponent_attack_cooldown=99999999, + opponent_attack_duration=0, + opponent_budget_per_ts=0., + opponent_init_budget=0., + opponent_action_class=DontAct) obs = env.reset() self._aux_obs_equal(obs, ep_data2.observations[0], f"after reset (after game over)") for i in range(6): @@ -239,22 +350,25 @@ def test_given_example(self): # load li_episode = EpisodeData.list_episode(f) ep_data = li_episode[0] - env2 = grid2op.make(env_name, - test=True, - chronics_class=FromOneEpisodeData, - data_feeding_kwargs={"ep_data": ep_data}, - opponent_class=FromEpisodeDataOpponent, - ) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env2 = grid2op.make(env_name, + test=True, + chronics_class=FromOneEpisodeData, + data_feeding_kwargs={"ep_data": ep_data}, + opponent_class=FromEpisodeDataOpponent, + ) ep_data2 = EpisodeData.from_disk(*ep_data) obs = env2.reset() for i in range(10): obs, reward, done, info = env2.step(env.action_space()) - TestTSFromEpisode._aux_obs_equal(obs, ep_data2.observations[i+1], f"at it. {i} (TestExamples)") + TestTSFromEpisodeMaintenance._aux_obs_equal(obs, ep_data2.observations[i+1], f"at it. {i} (TestExamples)") env.close() env2.close() - + class TestWithOpp(unittest.TestCase): def test_load_with_opp(self): @@ -264,14 +378,14 @@ def test_load_with_opp(self): warnings.filterwarnings("ignore") env = grid2op.make(env_name, test=True, - opponent_attack_cooldown=12*24, - opponent_attack_duration=7, - opponent_budget_per_ts=0.5, - opponent_init_budget=100000., - opponent_action_class=PowerlineSetAction, - opponent_class=RandomLineOpponent, - opponent_budget_class=BaseActionBudget, - kwargs_opponent={"lines_attacked": + opponent_attack_cooldown=12*24, + opponent_attack_duration=7, + opponent_budget_per_ts=0.5, + opponent_init_budget=100000., + opponent_action_class=PowerlineSetAction, + opponent_class=RandomLineOpponent, + opponent_budget_class=BaseActionBudget, + kwargs_opponent={"lines_attacked": ["1_3_3", "1_4_4", "3_6_15", "9_10_12", "11_12_13", "12_13_14"]}) runner = Runner( **env.get_params_for_runner(), agentClass=RecoPowerlineAgent @@ -284,24 +398,27 @@ def test_load_with_opp(self): ep_data = res[0][-1] assert ep_data.attacks[0].set_line_status[4] == -1 # assert I got an attack assert ep_data.attacks[1].set_line_status[4] == -1 # assert I got an attack - env2 = grid2op.make(env_name, - test=True, - chronics_class=FromOneEpisodeData, - data_feeding_kwargs={"ep_data": ep_data}, - opponent_class=FromEpisodeDataOpponent, - opponent_attack_cooldown=1, - opponent_attack_duration=7, - opponent_budget_per_ts=0.5, - opponent_init_budget=100000., - opponent_action_class=PowerlineSetAction, - opponent_budget_class=BaseActionBudget, - ) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env2 = grid2op.make(env_name, + test=True, + chronics_class=FromOneEpisodeData, + data_feeding_kwargs={"ep_data": ep_data}, + opponent_class=FromEpisodeDataOpponent, + opponent_attack_cooldown=1, + opponent_attack_duration=7, + opponent_budget_per_ts=0.5, + opponent_init_budget=100000., + opponent_action_class=PowerlineSetAction, + opponent_budget_class=BaseActionBudget, + ) obs = env2.reset() agent = RecoPowerlineAgent(env2.action_space) for i in range(10): act = agent.act(obs, None) obs, reward, done, info = env2.step(act) - TestTSFromEpisode._aux_obs_equal(obs, ep_data.observations[i+1], f"at it. {i} (TestWithOpp)") + TestTSFromEpisodeMaintenance._aux_obs_equal(obs, ep_data.observations[i+1], f"at it. {i} (TestWithOpp)") def test_assert_warnings(self): env_name = "l2rpn_case14_sandbox" @@ -310,15 +427,16 @@ def test_assert_warnings(self): warnings.filterwarnings("ignore") env = grid2op.make(env_name, test=True, - opponent_attack_cooldown=12*24, - opponent_attack_duration=7, - opponent_budget_per_ts=0.5, - opponent_init_budget=100000., - opponent_action_class=PowerlineSetAction, - opponent_class=RandomLineOpponent, - opponent_budget_class=BaseActionBudget, - kwargs_opponent={"lines_attacked": - ["1_3_3", "1_4_4", "3_6_15", "9_10_12", "11_12_13", "12_13_14"]}) + opponent_attack_cooldown=12*24, + opponent_attack_duration=7, + opponent_budget_per_ts=0.5, + opponent_init_budget=100000., + opponent_action_class=PowerlineSetAction, + opponent_class=RandomLineOpponent, + opponent_budget_class=BaseActionBudget, + kwargs_opponent={"lines_attacked": + ["1_3_3", "1_4_4", "3_6_15", + "9_10_12", "11_12_13", "12_13_14"]}) runner = Runner( **env.get_params_for_runner(), agentClass=RecoPowerlineAgent ) @@ -357,9 +475,9 @@ def test_assert_warnings(self): opponent_budget_class=BaseActionBudget, ) -# TODO test with forecasts + # TODO test the FromMultiEpisodeData # TODO test the get_id of FromMultiEpisodeData -# TODO test fast_forward of FromMultiEpisodeData and of FromOneEpisodeData +# TODO test fast_forward of FromMultiEpisodeData if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() From 43454673116a4fba3f4abf55c00a4012e04789f8 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 25 Aug 2023 14:44:10 +0200 Subject: [PATCH 12/13] finishing the class to loop through episode data --- CHANGELOG.rst | 3 +- grid2op/Agent/recoPowerLinePerArea.py | 2 +- grid2op/Chronics/__init__.py | 5 +- grid2op/Chronics/fromMultiEpisodeData.py | 87 +++++++++++++- grid2op/Chronics/fromOneEpisodeData.py | 5 +- grid2op/Environment/baseMultiProcessEnv.py | 2 +- grid2op/Opponent/__init__.py | 2 +- grid2op/Opponent/fromEpisodeDataOpponent.py | 4 +- grid2op/Opponent/utils.py | 2 +- grid2op/Reward/_alarmScore.py | 2 +- grid2op/Reward/alarmReward.py | 2 +- grid2op/Reward/alertReward.py | 2 +- grid2op/Reward/baseReward.py | 2 +- grid2op/Reward/bridgeReward.py | 2 +- grid2op/Reward/closeToOverflowReward.py | 2 +- grid2op/Reward/constantReward.py | 2 +- grid2op/Reward/distanceReward.py | 2 +- grid2op/Reward/economicReward.py | 2 +- grid2op/Reward/episodeDurationReward.py | 2 +- grid2op/Reward/flatReward.py | 2 +- grid2op/Reward/gameplayReward.py | 2 +- grid2op/Reward/increasingFlatReward.py | 2 +- grid2op/Reward/l2RPNReward.py | 2 +- grid2op/Reward/linesCapacityReward.py | 2 +- grid2op/Reward/linesReconnectedReward.py | 2 +- grid2op/Reward/redispReward.py | 2 +- grid2op/Rules/rulesByArea.py | 3 +- grid2op/tests/test_env_from_episode.py | 123 +++++++++++++++++++- 28 files changed, 235 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a0e8acefe..b79e0ef29 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -38,7 +38,8 @@ Change Log - [FIXED] issue https://github.com/rte-france/Grid2Op/issues/511 - [FIXED] issue https://github.com/rte-france/Grid2Op/issues/508 - [ADDED] some classes that can be used to reproduce exactly what happened in a previously run environment - see `grid2op.Chronics.FromOneEpisodeData` and `grid2op.Opponent.FromEpisodeDataOpponent` + see `grid2op.Chronics.FromOneEpisodeData` and `grid2op.Opponent.FromEpisodeDataOpponent` + and `grid2op.Chronics.FromMultiEpisodeData` - [ADDED] An helper function to get the kwargs to disable the opponent (see `grid2op.Opponent.get_kwargs_no_opponent()`) - [IMPROVED] doc of `obs.to_dict` and `obs.to_json` (see https://github.com/rte-france/Grid2Op/issues/509) diff --git a/grid2op/Agent/recoPowerLinePerArea.py b/grid2op/Agent/recoPowerLinePerArea.py index 31c79d047..bc28584e1 100644 --- a/grid2op/Agent/recoPowerLinePerArea.py +++ b/grid2op/Agent/recoPowerLinePerArea.py @@ -26,7 +26,7 @@ class RecoPowerlinePerArea(BaseAgent): You can use it like: - .. code-block:: + .. code-block:: python import grid2op from grid2op.Agent import RecoPowerlinePerArea diff --git a/grid2op/Chronics/__init__.py b/grid2op/Chronics/__init__.py index c4ec2ca52..04f403238 100644 --- a/grid2op/Chronics/__init__.py +++ b/grid2op/Chronics/__init__.py @@ -12,7 +12,8 @@ "FromNPY", "FromChronix2grid", "FromHandlers", - "FromOneEpisodeData" + "FromOneEpisodeData", + "FromMultiEpisodeData" ] from grid2op.Chronics.chronicsHandler import ChronicsHandler @@ -31,4 +32,6 @@ from grid2op.Chronics.fromNPY import FromNPY from grid2op.Chronics.fromChronix2grid import FromChronix2grid from grid2op.Chronics.time_series_from_handlers import FromHandlers + from grid2op.Chronics.fromOneEpisodeData import FromOneEpisodeData +from grid2op.Chronics.fromMultiEpisodeData import FromMultiEpisodeData diff --git a/grid2op/Chronics/fromMultiEpisodeData.py b/grid2op/Chronics/fromMultiEpisodeData.py index 071ac5298..309f990e6 100644 --- a/grid2op/Chronics/fromMultiEpisodeData.py +++ b/grid2op/Chronics/fromMultiEpisodeData.py @@ -25,6 +25,82 @@ class FromMultiEpisodeData(GridValue): + """This class allows to redo some episode that have been previously run using a runner. + + It is an extension of the class :class:`FromOneEpisodeData` but with multiple episodes. + + .. seealso:: + :class:`grid2op.Chronics.FromOneEpisodeData`if you want to use only one episode + + .. warning:: + It has the same limitation as :class:`grid2op.Chronics.FromOneEpisodeData`, including: + + - forecasts are not saved so cannot be retrieved with this class. You can however + use `obs.simulate` and in this case it will lead perfect forecasts. + - to make sure you are running the exact same episode, you need to create the environment + with the :class:`grid2op.Opponent.FromEpisodeDataOpponent` opponent + + Examples + --------- + You can use this class this way: + + First, you generate some data by running an episode with do nothing or reco powerline agent, + preferably episode that go until the end of your time series + + .. code-block:: python + + import grid2op + from grid2op.Runner import Runner + from grid2op.Agent import RecoPowerlineAgent + + path_agent = .... + nb_episode = ... + env_name = "l2rpn_case14_sandbox" # or any other name + env = grid2op.make(env_name, etc.) + + # optional (change the parameters to allow the ) + param = env.parameters + param.NO_OVERFLOW_DISCONNECTION = True + env.change_parameters(param) + env.reset() + # end optional + + runner = Runner(**env.get_params_for_runner(), + agentClass=RecoPowerlineAgent) + runner.run(nb_episode=nb_episode, + path_save=path_agent) + + And then you can load it back and run the exact same environment with the same + time series, the same attacks etc. with: + + .. code-block:: python + + import grid2op + from grid2op.Chronics import FromMultiEpisodeData + from grid2op.Opponent import FromEpisodeDataOpponent + from grid2op.Episode import EpisodeData + + path_agent = .... # same as above + env_name = .... # same as above + + # path_agent is the path where data coming from a grid2op runner are stored + # NB it should come from a do nothing agent, or at least + # an agent that does not modify the injections (no redispatching, curtailment, storage) + li_episode = EpisodeData.list_episode(path_agent) + + env = grid2op.make(env_name, + chronics_class=FromMultiEpisodeData, + data_feeding_kwargs={"li_ep_data": li_episode}, + opponent_class=FromEpisodeDataOpponent, + opponent_attack_cooldown=1, + ) + # li_ep_data in this case is a list of anything that is accepted by `FromOneEpisodeData` + + obs = env.reset() + + # and now you can use "env" as any grid2op environment. + + """ MULTI_CHRONICS = True def __init__(self, path, # can be None ! @@ -47,8 +123,9 @@ def __init__(self, start_datetime=start_datetime) for el in li_ep_data ] - self._prev_cache_id = 0 - self.data = self.li_ep_data[0] + self._prev_cache_id = len(self.li_ep_data) - 1 + self.data = self.li_ep_data[self._prev_cache_id] + self._episode_data = self.data._episode_data # used by the fromEpisodeDataOpponent def next_chronics(self): self._prev_cache_id += 1 @@ -74,6 +151,7 @@ def initialize( order_backend_subs, names_chronics_to_backend=names_chronics_to_backend, ) + self._episode_data = self.data._episode_data def done(self): return self.data.done() @@ -88,6 +166,7 @@ def forecasts(self): return self.data.forecasts() def tell_id(self, id_num, previous=False): + 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 " "at the moment. Feel free to write a feature request if you want more.") @@ -99,8 +178,8 @@ def tell_id(self, id_num, previous=False): self._prev_cache_id -= 1 self._prev_cache_id %= len(self.li_ep_data) - def get_id(self) -> int: - return self._prev_cache_id - 1 # TODO check + def get_id(self) -> str: + return f'{self._prev_cache_id }' def max_timestep(self): return self.data.max_timestep() diff --git a/grid2op/Chronics/fromOneEpisodeData.py b/grid2op/Chronics/fromOneEpisodeData.py index 7ae603110..39ba20426 100644 --- a/grid2op/Chronics/fromOneEpisodeData.py +++ b/grid2op/Chronics/fromOneEpisodeData.py @@ -62,6 +62,9 @@ class FromOneEpisodeData(GridValue): you to build this class with a complete episode (and not using an agent that games over after a few steps), for example by using the "RecoPowerlineAgent" and the `NO_OVERFLOW_DISCONNECTION` parameters (see example below) + + .. seealso:: + :class:`grid2op.Chronics.FromMultiEpisodeData`if you want to use multiple episode data Examples --------- @@ -128,7 +131,7 @@ class FromOneEpisodeData(GridValue): If you want to include perfect forecast (unfortunately you cannot retrieve the original forecasts) you can do: - .. code-block:: + .. code-block:: python # same as above diff --git a/grid2op/Environment/baseMultiProcessEnv.py b/grid2op/Environment/baseMultiProcessEnv.py index 2d09ef88e..aa32fea2b 100644 --- a/grid2op/Environment/baseMultiProcessEnv.py +++ b/grid2op/Environment/baseMultiProcessEnv.py @@ -616,7 +616,7 @@ def simulate(self, actions): You can use this feature like: - .. code-block:: + .. code-block:: python import grid2op from grid2op.Environment import BaseMultiProcessEnvironment diff --git a/grid2op/Opponent/__init__.py b/grid2op/Opponent/__init__.py index 53a25c9a0..9e1641ec8 100644 --- a/grid2op/Opponent/__init__.py +++ b/grid2op/Opponent/__init__.py @@ -21,4 +21,4 @@ from grid2op.Opponent.geometricOpponent import GeometricOpponent from grid2op.Opponent.geometricOpponentMultiArea import GeometricOpponentMultiArea from grid2op.Opponent.fromEpisodeDataOpponent import FromEpisodeDataOpponent -from grid2op.Opponent.utils import get_kwargs_no_opponent \ No newline at end of file +from grid2op.Opponent.utils import get_kwargs_no_opponent diff --git a/grid2op/Opponent/fromEpisodeDataOpponent.py b/grid2op/Opponent/fromEpisodeDataOpponent.py index 4e296f5b8..63a8811d1 100644 --- a/grid2op/Opponent/fromEpisodeDataOpponent.py +++ b/grid2op/Opponent/fromEpisodeDataOpponent.py @@ -10,7 +10,7 @@ import warnings from grid2op.Opponent.baseOpponent import BaseOpponent -from grid2op.Chronics import FromOneEpisodeData +from grid2op.Chronics import FromOneEpisodeData, FromMultiEpisodeData from grid2op.Exceptions import OpponentError @@ -75,7 +75,7 @@ def __init__(self, action_space): self._warning_cooldown_issued = False def init(self, partial_env, **kwargs): - if not isinstance(partial_env.chronics_handler.real_data, FromOneEpisodeData): + if not isinstance(partial_env.chronics_handler.real_data, (FromOneEpisodeData, FromMultiEpisodeData)): raise OpponentError("FromEpisodeDataOpponent can only be used with FromOneEpisodeData time series !") self._ptr_env = partial_env self._attacks = copy.deepcopy(self._ptr_env.chronics_handler.real_data._episode_data.attacks) diff --git a/grid2op/Opponent/utils.py b/grid2op/Opponent/utils.py index 2512f91c4..913348cc4 100644 --- a/grid2op/Opponent/utils.py +++ b/grid2op/Opponent/utils.py @@ -20,7 +20,7 @@ def get_kwargs_no_opponent() -> Dict: You can use it like - .. code-block:: + .. code-block:: python import grid2op env_name = "l2rpn_case14_sandbox" # or any other name diff --git a/grid2op/Reward/_alarmScore.py b/grid2op/Reward/_alarmScore.py index d35b67cb6..82cc5a591 100644 --- a/grid2op/Reward/_alarmScore.py +++ b/grid2op/Reward/_alarmScore.py @@ -39,7 +39,7 @@ class _AlarmScore(AlarmReward): --------- You can use this reward in any environment with: - .. code-block: + .. code-block:: python import grid2op from grid2op.Reward import AlarmReward diff --git a/grid2op/Reward/alarmReward.py b/grid2op/Reward/alarmReward.py index cd9d8bd73..e114a7920 100644 --- a/grid2op/Reward/alarmReward.py +++ b/grid2op/Reward/alarmReward.py @@ -29,7 +29,7 @@ class AlarmReward(BaseReward): --------- You can use this reward in any environment with: - .. code-block: + .. code-block:: python import grid2op from grid2op.Reward import AlarmReward diff --git a/grid2op/Reward/alertReward.py b/grid2op/Reward/alertReward.py index 3f7d90924..1ab8d4d7c 100644 --- a/grid2op/Reward/alertReward.py +++ b/grid2op/Reward/alertReward.py @@ -54,7 +54,7 @@ class AlertReward(BaseReward): --------- You can use this reward in any environment with: - .. code-block: + .. code-block:: python import grid2op from grid2op.Reward import AlertReward diff --git a/grid2op/Reward/baseReward.py b/grid2op/Reward/baseReward.py index 882dd348a..2f9dec6dd 100644 --- a/grid2op/Reward/baseReward.py +++ b/grid2op/Reward/baseReward.py @@ -41,7 +41,7 @@ class BaseReward(ABC): If you want the environment to compute a reward that is the sum of the flow (this is not a good reward, but we use it as an example on how to do it) you can achieve it with: - .. code-block: + .. code-block:: python import grid2op from grid2op.Reward import BaseReward diff --git a/grid2op/Reward/bridgeReward.py b/grid2op/Reward/bridgeReward.py index 70709534a..3e259793f 100644 --- a/grid2op/Reward/bridgeReward.py +++ b/grid2op/Reward/bridgeReward.py @@ -22,7 +22,7 @@ class BridgeReward(BaseReward): --------- You can use this reward in any environment with: - .. code-block: + .. code-block:: python import grid2op from grid2op.Reward import BridgeReward diff --git a/grid2op/Reward/closeToOverflowReward.py b/grid2op/Reward/closeToOverflowReward.py index d74a9b7b2..43ae4ab98 100644 --- a/grid2op/Reward/closeToOverflowReward.py +++ b/grid2op/Reward/closeToOverflowReward.py @@ -20,7 +20,7 @@ class CloseToOverflowReward(BaseReward): --------- You can use this reward in any environment with: - .. code-block: + .. code-block:: python import grid2op from grid2op.Reward import CloseToOverflowReward diff --git a/grid2op/Reward/constantReward.py b/grid2op/Reward/constantReward.py index 3b1edb8c0..1aa63d6e4 100644 --- a/grid2op/Reward/constantReward.py +++ b/grid2op/Reward/constantReward.py @@ -22,7 +22,7 @@ class ConstantReward(BaseReward): --------- You can use this reward in any environment with: - .. code-block: + .. code-block:: python import grid2op from grid2op.Reward import ConstantReward diff --git a/grid2op/Reward/distanceReward.py b/grid2op/Reward/distanceReward.py index a50c8f81f..3a15c3cbf 100644 --- a/grid2op/Reward/distanceReward.py +++ b/grid2op/Reward/distanceReward.py @@ -20,7 +20,7 @@ class DistanceReward(BaseReward): --------- You can use this reward in any environment with: - .. code-block: + .. code-block:: python import grid2op from grid2op.Reward import DistanceReward diff --git a/grid2op/Reward/economicReward.py b/grid2op/Reward/economicReward.py index bc03e4570..e9986b4f2 100644 --- a/grid2op/Reward/economicReward.py +++ b/grid2op/Reward/economicReward.py @@ -25,7 +25,7 @@ class EconomicReward(BaseReward): --------- You can use this reward in any environment with: - .. code-block: + .. code-block:: python import grid2op from grid2op.Reward import EconomicReward diff --git a/grid2op/Reward/episodeDurationReward.py b/grid2op/Reward/episodeDurationReward.py index b855abb69..8e241dc67 100644 --- a/grid2op/Reward/episodeDurationReward.py +++ b/grid2op/Reward/episodeDurationReward.py @@ -20,7 +20,7 @@ class EpisodeDurationReward(BaseReward): --------- You can use this reward in any environment with: - .. code-block: + .. code-block:: python import grid2op from grid2op.Reward import EpisodeDurationReward diff --git a/grid2op/Reward/flatReward.py b/grid2op/Reward/flatReward.py index 02e9e09ce..f94e36869 100644 --- a/grid2op/Reward/flatReward.py +++ b/grid2op/Reward/flatReward.py @@ -18,7 +18,7 @@ class FlatReward(BaseReward): --------- You can use this reward in any environment with: - .. code-block: + .. code-block:: python import grid2op from grid2op.Reward import FlatReward diff --git a/grid2op/Reward/gameplayReward.py b/grid2op/Reward/gameplayReward.py index 22eef2db1..a9f7a6cb9 100644 --- a/grid2op/Reward/gameplayReward.py +++ b/grid2op/Reward/gameplayReward.py @@ -21,7 +21,7 @@ class GameplayReward(BaseReward): --------- You can use this reward in any environment with: - .. code-block: + .. code-block:: python import grid2op from grid2op.Reward import GameplayReward diff --git a/grid2op/Reward/increasingFlatReward.py b/grid2op/Reward/increasingFlatReward.py index 5bd29ac8b..4beccf258 100644 --- a/grid2op/Reward/increasingFlatReward.py +++ b/grid2op/Reward/increasingFlatReward.py @@ -21,7 +21,7 @@ class IncreasingFlatReward(BaseReward): --------- You can use this reward in any environment with: - .. code-block: + .. code-block:: python import grid2op from grid2op.Reward import IncreasingFlatReward diff --git a/grid2op/Reward/l2RPNReward.py b/grid2op/Reward/l2RPNReward.py index 889f7b789..3a4a4a11e 100644 --- a/grid2op/Reward/l2RPNReward.py +++ b/grid2op/Reward/l2RPNReward.py @@ -30,7 +30,7 @@ class L2RPNReward(BaseReward): --------- You can use this reward in any environment with: - .. code-block: + .. code-block:: python import grid2op from grid2op.Reward import L2RPNReward diff --git a/grid2op/Reward/linesCapacityReward.py b/grid2op/Reward/linesCapacityReward.py index c8ff7f0ae..4a4de6e93 100644 --- a/grid2op/Reward/linesCapacityReward.py +++ b/grid2op/Reward/linesCapacityReward.py @@ -25,7 +25,7 @@ class LinesCapacityReward(BaseReward): --------- You can use this reward in any environment with: - .. code-block: + .. code-block:: python import grid2op from grid2op.Reward import LinesCapacityReward diff --git a/grid2op/Reward/linesReconnectedReward.py b/grid2op/Reward/linesReconnectedReward.py index be9f1c4d2..eeda86389 100644 --- a/grid2op/Reward/linesReconnectedReward.py +++ b/grid2op/Reward/linesReconnectedReward.py @@ -22,7 +22,7 @@ class LinesReconnectedReward(BaseReward): --------- You can use this reward in any environment with: - .. code-block: + .. code-block:: python import grid2op from grid2op.Reward import LinesReconnectedReward diff --git a/grid2op/Reward/redispReward.py b/grid2op/Reward/redispReward.py index a51ec00ec..fb8d494c5 100644 --- a/grid2op/Reward/redispReward.py +++ b/grid2op/Reward/redispReward.py @@ -27,7 +27,7 @@ class RedispReward(BaseReward): --------- You can use this reward in any environment with: - .. code-block: + .. code-block:: python import grid2op from grid2op.Reward import RedispReward diff --git a/grid2op/Rules/rulesByArea.py b/grid2op/Rules/rulesByArea.py index 8335a1697..66efe22b2 100644 --- a/grid2op/Rules/rulesByArea.py +++ b/grid2op/Rules/rulesByArea.py @@ -36,7 +36,8 @@ class RulesByArea(BaseRules): --------- If you want the environment to take into account the rules by area, you can achieve it with: - .. code-block: + .. code-block:: python + import grid2op from grid2op.Rules.rulesByArea import RulesByArea diff --git a/grid2op/tests/test_env_from_episode.py b/grid2op/tests/test_env_from_episode.py index b6a1621fb..aeebc0e1c 100644 --- a/grid2op/tests/test_env_from_episode.py +++ b/grid2op/tests/test_env_from_episode.py @@ -17,7 +17,7 @@ from grid2op.Environment import BaseEnv from grid2op.Observation import BaseObservation from grid2op.Runner import Runner -from grid2op.Chronics import FromOneEpisodeData, Multifolder, ChronicsHandler +from grid2op.Chronics import FromOneEpisodeData, Multifolder, ChronicsHandler, FromMultiEpisodeData from grid2op.Agent import BaseAgent from grid2op.Exceptions import Grid2OpException, ChronicsError from grid2op.Agent import RecoPowerlineAgent @@ -329,7 +329,7 @@ def test_maintenance(self): class TestExamples(unittest.TestCase): """test the example given in the doc""" - def test_given_example(self): + def test_given_example_oneepdata(self): env_name = "l2rpn_case14_sandbox" # or any other name with warnings.catch_warnings(): warnings.filterwarnings("ignore") @@ -368,7 +368,45 @@ def test_given_example(self): env.close() env2.close() - + + def test_given_example_multiepdata(self): + env_name = "l2rpn_case14_sandbox" # or any other name + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make(env_name, test=True) + + param = env.parameters + param.NO_OVERFLOW_DISCONNECTION = True + env.change_parameters(param) + env.reset() + + with tempfile.TemporaryDirectory() as path_agent: + + runner = Runner(**env.get_params_for_runner(), + agentClass=RecoPowerlineAgent) + runner.run(nb_episode=2, + path_save=path_agent, + max_iter=10) + + # path_agent is the path where data coming from a grid2op runner are stored + # NB it should come from a do nothing agent, or at least + # an agent that does not modify the injections (no redispatching, curtailment, storage) + li_episode = EpisodeData.list_episode(path_agent) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env2 = grid2op.make(env_name, + test=True, + chronics_class=FromMultiEpisodeData, + data_feeding_kwargs={"li_ep_data": li_episode}, + opponent_class=FromEpisodeDataOpponent, + opponent_attack_cooldown=1, + ) + # li_ep_data in this case is a list of anything that is accepted by `FromOneEpisodeData` + + obs = env.reset() + env.reset() + env2.reset() class TestWithOpp(unittest.TestCase): def test_load_with_opp(self): @@ -476,8 +514,81 @@ def test_assert_warnings(self): ) -# TODO test the FromMultiEpisodeData -# TODO test the get_id of FromMultiEpisodeData -# TODO test fast_forward of FromMultiEpisodeData +class TestTSFromMultieEpisode(unittest.TestCase): + def setUp(self) -> None: + self.env_name = "l2rpn_case14_sandbox" + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make(self.env_name, + test=True, + opponent_attack_cooldown=99999999, + opponent_attack_duration=0, + opponent_budget_per_ts=0., + opponent_init_budget=0., + opponent_action_class=DontAct,) + self.env.set_id(0) + self.env.seed(0) + self.max_iter = 10 + return super().setUp() + + def tearDown(self) -> None: + self.env.close() + return super().tearDown() + + def test_basic(self): + """test injection, without opponent nor maintenance""" + obs = self.env.reset() + runner = Runner( + **self.env.get_params_for_runner() + ) + res = runner.run(nb_episode=2, max_iter=self.max_iter, add_detailed_output=True) + ep_data = [el[-1] for el in res] + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make(self.env_name, + test=True, + chronics_class=FromMultiEpisodeData, + data_feeding_kwargs={"li_ep_data": ep_data}, + opponent_attack_cooldown=99999999, + opponent_attack_duration=0, + opponent_budget_per_ts=0., + opponent_init_budget=0., + opponent_action_class=DontAct) + # test init data + obs = env.reset() + TestTSFromEpisodeMaintenance._aux_obs_equal(obs, ep_data[0].observations[0]) + for i in range(10): + obs, reward, done, info = env.step(env.action_space()) + TestTSFromEpisodeMaintenance._aux_obs_equal(obs, ep_data[0].observations[i+1], f"at it. {i}") + assert done + with self.assertRaises(Grid2OpException): + obs, reward, done, info = env.step(env.action_space()) + assert env.chronics_handler.get_id() == "0" + + # test when reset, that it moves to next data + obs = env.reset() + TestTSFromEpisodeMaintenance._aux_obs_equal(obs, ep_data[1].observations[0]) + for i in range(10): + obs, reward, done, info = env.step(env.action_space()) + TestTSFromEpisodeMaintenance._aux_obs_equal(obs, ep_data[1].observations[i+1], f"at it. {i}") + assert done + with self.assertRaises(Grid2OpException): + obs, reward, done, info = env.step(env.action_space()) + assert env.chronics_handler.get_id() == "1" + + # test the set_id + env.set_id("1") + obs = env.reset() + assert env.chronics_handler.get_id() == "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()) + TestTSFromEpisodeMaintenance._aux_obs_equal(obs, ep_data[1].observations[i+1], f"at it. {i}") + assert done + with self.assertRaises(Grid2OpException): + obs, reward, done, info = env.step(env.action_space()) + assert env.chronics_handler.get_id() == "1" + + if __name__ == "__main__": unittest.main() From 7adb61febf9c7229fc1ca6fc3cb28a56a03759ef Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Mon, 4 Sep 2023 16:01:56 +0200 Subject: [PATCH 13/13] updating changelog before release [skip ci] --- CHANGELOG.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b79e0ef29..56735a746 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -31,7 +31,12 @@ Change Log - [???] "asynch" multienv - [???] properly model interconnecting powerlines -[1.9.4] - 2023-xx-yy +[1.9.5] - 2023-xx-yy +--------------------- +- XXX + + +[1.9.4] - 2023-09-04 --------------------- - [FIXED] read-the-docs template is not compatible with latest sphinx version (7.0.0) see https://github.com/readthedocs/sphinx_rtd_theme/issues/1463