From 5b375a9bebb1d3bf85d8c9632351638f53a0740d Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Tue, 11 Jun 2024 10:33:19 +0200 Subject: [PATCH 01/11] adding some todos for different number of busbar per substation --- grid2op/Environment/environment.py | 8 ++++---- grid2op/MakeEnv/MakeFromPath.py | 4 ++-- grid2op/Space/GridObjects.py | 28 +++++++++++++++++++++------- grid2op/typing_variables.py | 6 ++++++ 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 113b2048..4e4dc3d0 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -10,7 +10,7 @@ import warnings import numpy as np import re -from typing import Optional, Union, Any, Dict, Literal +from typing import Optional, Union, Literal import grid2op from grid2op.Opponent import OpponentSpace @@ -33,7 +33,7 @@ from grid2op.Opponent import BaseOpponent, NeverAttackBudget from grid2op.operator_attention import LinearAttentionBudget from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB -from grid2op.typing_variables import RESET_OPTIONS_TYPING +from grid2op.typing_variables import RESET_OPTIONS_TYPING, N_BUSBAR_PER_SUB_TYPING class Environment(BaseEnv): @@ -84,7 +84,7 @@ def __init__( backend, parameters, name="unknown", - n_busbar=DEFAULT_N_BUSBAR_PER_SUB, + n_busbar : N_BUSBAR_PER_SUB_TYPING=DEFAULT_N_BUSBAR_PER_SUB, names_chronics_to_backend=None, actionClass=TopologyAction, observationClass=CompleteObservation, @@ -152,7 +152,7 @@ def __init__( observation_bk_kwargs=observation_bk_kwargs, highres_sim_counter=highres_sim_counter, update_obs_after_reward=_update_obs_after_reward, - n_busbar=n_busbar, + n_busbar=n_busbar, # TODO n_busbar_per_sub different num per substations: read from a config file maybe (if not provided by the user) _init_obs=_init_obs, _is_test=_is_test, # is this created with "test=True" # TODO not implemented !! ) diff --git a/grid2op/MakeEnv/MakeFromPath.py b/grid2op/MakeEnv/MakeFromPath.py index 81f31d21..c550261b 100644 --- a/grid2op/MakeEnv/MakeFromPath.py +++ b/grid2op/MakeEnv/MakeFromPath.py @@ -937,7 +937,7 @@ def make_from_dataset_path( attention_budget_cls=attention_budget_class, kwargs_attention_budget=kwargs_attention_budget, logger=logger, - n_busbar=n_busbar, + n_busbar=n_busbar, # TODO n_busbar_per_sub different num per substations: read from a config file maybe (if not provided by the user) _compat_glop_version=_compat_glop_version, _read_from_local_dir=None, # first environment to generate the classes and save them kwargs_observation=kwargs_observation, @@ -1004,7 +1004,7 @@ def make_from_dataset_path( attention_budget_cls=attention_budget_class, kwargs_attention_budget=kwargs_attention_budget, logger=logger, - n_busbar=n_busbar, + n_busbar=n_busbar, # TODO n_busbar_per_sub different num per substations: read from a config file maybe (if not provided by the user) _compat_glop_version=_compat_glop_version, _read_from_local_dir=classes_path, _allow_loaded_backend=allow_loaded_backend, diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 76261089..11c87c65 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -25,7 +25,7 @@ import grid2op from grid2op.dtypes import dt_int, dt_float, dt_bool -from grid2op.typing_variables import CLS_AS_DICT_TYPING +from grid2op.typing_variables import CLS_AS_DICT_TYPING, N_BUSBAR_PER_SUB_TYPING from grid2op.Exceptions import * from grid2op.Space.space_utils import extract_from_dict, save_to_dict @@ -635,7 +635,8 @@ def __init__(self): pass @classmethod - def set_n_busbar_per_sub(cls, n_busbar_per_sub: int) -> None: + def set_n_busbar_per_sub(cls, n_busbar_per_sub: N_BUSBAR_PER_SUB_TYPING) -> None: + # TODO n_busbar_per_sub different num per substations cls.n_busbar_per_sub = n_busbar_per_sub @classmethod @@ -2023,10 +2024,21 @@ def assert_grid_correct_cls(cls): # TODO refactor this method with the `_check***` methods. # TODO refactor the `_check***` to use the same "base functions" that would be coded only once. - if cls.n_busbar_per_sub != int(cls.n_busbar_per_sub): - raise EnvError(f"`n_busbar_per_sub` should be convertible to an integer, found {cls.n_busbar_per_sub}") - cls.n_busbar_per_sub = int(cls.n_busbar_per_sub) - if cls.n_busbar_per_sub < 1: + # TODO n_busbar_per_sub different num per substations + if isinstance(cls.n_busbar_per_sub, (int, dt_int, np.int32, np.int64)): + cls.n_busbar_per_sub = dt_int(cls.n_busbar_per_sub) + # np.full(cls.n_sub, + # fill_value=cls.n_busbar_per_sub, + # dtype=dt_int) + else: + # cls.n_busbar_per_sub = np.array(cls.n_busbar_per_sub) + # cls.n_busbar_per_sub = cls.n_busbar_per_sub.astype(dt_int) + raise EnvError("Grid2op cannot handle a different number of busbar per substations at the moment.") + + # if cls.n_busbar_per_sub != int(cls.n_busbar_per_sub): + # raise EnvError(f"`n_busbar_per_sub` should be convertible to an integer, found {cls.n_busbar_per_sub}") + # cls.n_busbar_per_sub = int(cls.n_busbar_per_sub) + if (cls.n_busbar_per_sub < 1).any(): raise EnvError(f"`n_busbar_per_sub` should be >= 1 found {cls.n_busbar_per_sub}") if cls.n_gen <= 0: @@ -2904,7 +2916,9 @@ def init_grid(cls, gridobj, force=False, extra_name=None, force_module=None): # with shunt and without shunt, then # there might be issues name_res += "_noshunt" - + + # TODO n_busbar_per_sub different num per substations: if it's a vector, use some kind of hash of it + # for the name of the class ! if gridobj.n_busbar_per_sub != DEFAULT_N_BUSBAR_PER_SUB: # to be able to load same environment with # different `n_busbar_per_sub` diff --git a/grid2op/typing_variables.py b/grid2op/typing_variables.py index 463d9adb..224969f6 100644 --- a/grid2op/typing_variables.py +++ b/grid2op/typing_variables.py @@ -54,3 +54,9 @@ np.ndarray, # eg load_to_subid, gen_pos_topo_vect List[Union[int, str, float, bool]]] ] + +#: n_busbar_per_sub +N_BUSBAR_PER_SUB_TYPING = Union[int, # one for all substation + List[int], # give info for all substations + Dict[str, int] # give information for some substation + ] From 6742b2b25279dae8a3a6b78c56be9e2d8e801ffc Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Tue, 11 Jun 2024 13:42:03 +0200 Subject: [PATCH 02/11] adding feature to specify the initial time step when reset --- CHANGELOG.rst | 5 +- grid2op/Environment/baseEnv.py | 22 +++- grid2op/Environment/environment.py | 84 ++++++++++++++- grid2op/tests/test_resest_options.py | 151 +++++++++++++++++++++++++++ 4 files changed, 258 insertions(+), 4 deletions(-) create mode 100644 grid2op/tests/test_resest_options.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ed7af776..4b86a865 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -36,7 +36,10 @@ Change Log - TODO A number of max buses per sub - TODO Automatic "experimental_read_from_local_dir" - TODO Notebook for stable baselines -- TODO in the reset options: datetime start and max number of steps +- TODO in the reset options: and max number of steps + +- [ADDED] possibility to skip some step when calling `env.reset(..., options={"init ts": ...})` + [1.10.2] - 2024-05-27 ------------------------- diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index 8dca86a3..2af3d213 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -299,7 +299,7 @@ def foo(manager): #: this are the keys of the dictionnary `options` #: that can be used when calling `env.reset(..., options={})` - KEYS_RESET_OPTIONS = {"time serie id", "init state"} + KEYS_RESET_OPTIONS = {"time serie id", "init state", "init ts", "max ts"} def __init__( @@ -3776,6 +3776,11 @@ def fast_forward_chronics(self, nb_timestep): 00:00). This can lead to suboptimal exploration, as during this phase, only a few time steps are managed by the agent, so in general these few time steps will correspond to grid state around Jan 1st at 00:00. + .. seealso:: + From grid2op version 1.10.3, a similar objective can be + obtained directly by calling :func:`grid2op.Environment.Environment.reset` with `"init ts"` + as option, for example like `obs = env.reset(options={"init ts": 12})` + Parameters ---------- nb_timestep: ``int`` @@ -3783,7 +3788,20 @@ def fast_forward_chronics(self, nb_timestep): Examples --------- - This can be used like this: + + From grid2op version 1.10.3 we recommend not to use this function (which will be deprecated) + but to use the :func:`grid2op.Environment.Environment.reset` functon with the `"init ts"` + option. + + .. code-block:: python + + import grid2op + env_name = "l2rpn_case14_sandbox" + env = grid2op.make(env_name) + + obs = env.reset(options={"init ts": 123}) + + For the legacy usave, this can be used like this: .. code-block:: python diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 4e4dc3d0..2fdc9201 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -913,7 +913,11 @@ def reset(self, options: dict Some options to "customize" the reset call. For example specifying the "time serie id" (grid2op >= 1.9.8) to use - or the "initial state of the grid" (grid2op >= 1.10.2). See examples for more information about this. Ignored if + or the "initial state of the grid" (grid2op >= 1.10.2) or to + start the episode at some specific time in the time series (grid2op >= 1.10.3) with the + "init ts" key. + + See examples for more information about this. Ignored if not set. Examples @@ -1035,7 +1039,63 @@ def reset(self, init_state_dict = {"set_line_status": [(0, -1)], "method": "force"} obs = env.reset(options={"init state": init_state_dict}) obs.line_status[0] is False + + .. versionadded:: 1.10.3 + + Another feature has been added in version 1.10.3, the possibility to skip the + some steps of the time series and starts at some given steps. + + The time series often always start at a given day of the week (*eg* Monday) + and at a given time (*eg* midnight). But for some reason you notice that your + agent performs poorly on other day of the week or time of the day. This might be + because it has seen much more data from Monday at midnight that from any other + day and hour of the day. + + To alleviate this issue, you can now easily reset an episode and ask grid2op + to start this episode after xxx steps have "passed". + + Concretely, you can do it with: + + .. code-block:: python + + import grid2op + env_name = "l2rpn_case14_sandbox" + env = grid2op.make(env_name) + + obs = env.reset(options={"init ts": 1}) + + Doing that your agent will start its episode not at midnight (which + is the case for this environment), but at 00:05 + + If you do: + + .. code-block:: python + + obs = env.reset(options={"init ts": 12}) + In this case, you start the episode at 01:00 and not at midnight (you + start at what would have been the 12th steps) + + If you want to start the "next day", you can do: + + .. code-block:: python + + obs = env.reset(options={"init ts": 288}) + + etc. + + .. note:: + On this feature, if a powerline is on soft overflow (meaning its flow is above + the limit but below the :attr:`grid2op.Parameters.Parameters.HARD_OVERFLOW_THRESHOLD` * `the limit`) + then it is still connected (of course) and the counter + :attr:`grid2op.Observation.BaseObservation.timestep_overflow` is at 0. + + If a powerline is on "hard overflow" (meaning its flow would be above + :attr:`grid2op.Parameters.Parameters.HARD_OVERFLOW_THRESHOLD` * `the limit`), then, as it is + the case for a "normal" (without options) reset, this line is disconnected, but can be reconnected + directly (:attr:`grid2op.Observation.BaseObservation.time_before_cooldown_line` == 0) + + """ # process the "options" kwargs # (if there is an init state then I need to process it to remove the @@ -1079,6 +1139,28 @@ def reset(self, if self.viewer_fig is not None: del self.viewer_fig self.viewer_fig = None + + if options is not None and "init ts" in options: + try: + skip_ts = int(options["init ts"]) + except ValueError as exc_: + raise Grid2OpException("In `env.reset` the kwargs `init ts` should be convertible to an int") from exc_ + + if skip_ts != options["init ts"]: + raise Grid2OpException(f"In `env.reset` the kwargs `init ts` should be convertible to an int, found {options['init ts']}") + + self._reset_vectors_and_timings() + + if skip_ts < 1: + raise Grid2OpException(f"In `env.reset` the kwargs `init ts` should be an int >= 1, found {options['init ts']}") + if skip_ts == 1: + self._init_obs = None + self.step(self.action_space()) + elif skip_ts == 2: + self.fast_forward_chronics(1) + else: + self.fast_forward_chronics(skip_ts) + # if True, then it will not disconnect lines above their thermal limits self._reset_vectors_and_timings() # and it needs to be done AFTER to have proper timings at tbe beginning # the attention budget is reset above diff --git a/grid2op/tests/test_resest_options.py b/grid2op/tests/test_resest_options.py new file mode 100644 index 00000000..843785d2 --- /dev/null +++ b/grid2op/tests/test_resest_options.py @@ -0,0 +1,151 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + +import warnings +import grid2op +from grid2op.Exceptions import Grid2OpException +import unittest +import pdb + + +class InitTSOptions(unittest.TestCase): + """test the "init ts" options in env.reset() """ + 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, + _add_to_name=type(self).__name__) + + def test_function_ok(self): + obs = self.env.reset() # normal reset + assert obs.year == 2019 + assert obs.month == 1 + assert obs.day == 6 + assert obs.hour_of_day == 0 + assert obs.minute_of_hour == 0 + + obs = self.env.reset(options={"init ts": 1}) # skip the first step, start at 5 minutes + assert obs.year == 2019 + assert obs.month == 1 + assert obs.day == 6 + assert obs.hour_of_day == 0 + assert obs.minute_of_hour == 5, f"{ obs.minute_of_hour} vs 5" + + obs = self.env.reset(options={"init ts": 2}) # start after 10 minutes, 2 steps + assert obs.year == 2019 + assert obs.month == 1 + assert obs.day == 6 + assert obs.hour_of_day == 0 + assert obs.minute_of_hour == 10, f"{ obs.minute_of_hour} vs 10" + + obs = self.env.reset(options={"init ts": 6}) # start after 6steps (30 minutes) + assert obs.year == 2019 + assert obs.month == 1 + assert obs.day == 6 + assert obs.hour_of_day == 0 + assert obs.minute_of_hour == 30, f"{ obs.minute_of_hour} vs 30" + + obs = self.env.reset(options={"init ts": 12}) # start at the 12th step + assert obs.year == 2019 + assert obs.month == 1 + assert obs.day == 6 + assert obs.hour_of_day == 1, f"{ obs.minute_of_hour} vs 1" + assert obs.minute_of_hour == 0, f"{ obs.minute_of_hour} vs 0" + + obs = self.env.reset(options={"init ts": 12 * 24}) # start after exactly 1 day + assert obs.year == 2019 + assert obs.month == 1 + assert obs.day == 7, f"{ obs.day} vs 7" + assert obs.hour_of_day == 0, f"{ obs.hour_of_day} vs 1" + assert obs.minute_of_hour == 0, f"{ obs.minute_of_hour} vs 0" + + def check_soft_overflow(self): + """check that the lines are not on soft overflow (obs.timestep_overflow == 0 just after reset)""" + line_id = 3 + obs = self.env.reset(options={"time serie id": 0}) + th_lim = 1. * self.env.get_thermal_limit() + th_lim[line_id] = 0.6 * obs.a_or[line_id] + self.env.set_thermal_limit(th_lim) + obs = self.env.reset(options={"time serie id": 0}) + assert (obs.timestep_overflow == 0).all() + assert obs.rho[line_id] > 1. + assert obs.line_status[line_id] + + obs = self.env.reset(options={"time serie id": 0}) + assert (obs.timestep_overflow == 0).all() + assert obs.rho[line_id] > 1. + assert obs.line_status[line_id] + + obs = self.env.reset(options={"time serie id": 0, "init ts": 1}) + assert (obs.timestep_overflow == 0).all() + assert obs.rho[line_id] > 1. + assert obs.line_status[line_id] + + obs = self.env.reset(options={"time serie id": 0, "init ts": 2}) + assert (obs.timestep_overflow == 0).all() + assert obs.rho[line_id] > 1. + assert obs.line_status[line_id] + + obs = self.env.reset(options={"time serie id": 0, "init ts": 6}) + assert (obs.timestep_overflow == 0).all() + assert obs.rho[line_id] > 1. + assert obs.line_status[line_id] + + def check_hard_overflow(self): + """check lines are disconnected if on hard overflow at the beginning""" + line_id = 3 + obs = self.env.reset(options={"time serie id": 0}) + th_lim = 1. * self.env.get_thermal_limit() + th_lim[line_id] = 0.4 * obs.a_or[line_id] + self.env.set_thermal_limit(th_lim) + obs = self.env.reset(options={"time serie id": 0}) + assert (obs.timestep_overflow == 0).all() + assert obs.rho[line_id] == 0. + assert not obs.line_status[line_id] + assert obs.time_before_cooldown_line[line_id] == 0 + + obs = self.env.reset(options={"time serie id": 0}) + assert (obs.timestep_overflow == 0).all() + assert obs.rho[line_id] == 0. + assert not obs.line_status[line_id] + assert obs.time_before_cooldown_line[line_id] == 0 + + obs = self.env.reset(options={"time serie id": 0, "init ts": 1}) + assert (obs.timestep_overflow == 0).all() + assert obs.rho[line_id] == 0. + assert not obs.line_status[line_id] + assert obs.time_before_cooldown_line[line_id] == 0 + + obs = self.env.reset(options={"time serie id": 0, "init ts": 2}) + assert (obs.timestep_overflow == 0).all() + assert obs.rho[line_id] == 0. + assert not obs.line_status[line_id] + assert obs.time_before_cooldown_line[line_id] == 0 + + obs = self.env.reset(options={"time serie id": 0, "init ts": 6}) + assert (obs.timestep_overflow == 0).all() + assert obs.rho[line_id] == 0. + assert not obs.line_status[line_id] + assert obs.time_before_cooldown_line[line_id] == 0 + + + def test_raise_if_args_not_correct(self): + with self.assertRaises(Grid2OpException): + # string and not int + obs = self.env.reset(options={"init ts": "treliug"}) + with self.assertRaises(Grid2OpException): + # float which is not an int + obs = self.env.reset(options={"init ts": 1.5}) + + # should work with a float convertible to an int + obs = self.env.reset(options={"time serie id": 0, "init ts": 6.}) + + +if __name__ == "__main__": + unittest.main() From 6340de28cb40061a822926feea30feb8ea4f704d Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Wed, 12 Jun 2024 17:08:05 +0200 Subject: [PATCH 03/11] adding possibility to use env.reset(..., options={'max step': ...}) --- CHANGELOG.rst | 5 +- grid2op/Chronics/chronicsHandler.py | 4 +- grid2op/Chronics/multiFolder.py | 1 + grid2op/Chronics/multifolderWithCache.py | 1 + grid2op/Environment/baseEnv.py | 9 +- grid2op/Environment/environment.py | 173 +++++++++++++++++++++-- grid2op/tests/test_resest_options.py | 144 ++++++++++++++++++- grid2op/typing_variables.py | 2 + 8 files changed, 319 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4b86a865..f940232e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -36,9 +36,12 @@ Change Log - TODO A number of max buses per sub - TODO Automatic "experimental_read_from_local_dir" - TODO Notebook for stable baselines -- TODO in the reset options: and max number of steps +- TODO reset options in the runner +- [FIXED] a bug in the `MultiFolder` and `MultifolderWithCache` leading to the wrong + computation of `max_iter` on some corner cases - [ADDED] possibility to skip some step when calling `env.reset(..., options={"init ts": ...})` +- [ADDED] possibility to limit the duration of an episode with `env.reset(..., options={"max step": ...})` [1.10.2] - 2024-05-27 diff --git a/grid2op/Chronics/chronicsHandler.py b/grid2op/Chronics/chronicsHandler.py index 44ad9256..0a125bec 100644 --- a/grid2op/Chronics/chronicsHandler.py +++ b/grid2op/Chronics/chronicsHandler.py @@ -175,9 +175,9 @@ def set_max_iter(self, max_iter: int): """ - if not isinstance(max_iter, int): + if not isinstance(max_iter, (int, dt_int, np.int64)): raise Grid2OpException( - "The maximum number of iterations possible for this chronics, before it ends." + "The maximum number of iterations possible for this time series, before it ends should be an int" ) if max_iter == 0: raise Grid2OpException( diff --git a/grid2op/Chronics/multiFolder.py b/grid2op/Chronics/multiFolder.py index 47ed2fa5..be2d360b 100644 --- a/grid2op/Chronics/multiFolder.py +++ b/grid2op/Chronics/multiFolder.py @@ -441,6 +441,7 @@ def initialize( ) if self.action_space is not None: self.data.action_space = self.action_space + self._max_iter = self.data.max_iter def done(self): """ diff --git a/grid2op/Chronics/multifolderWithCache.py b/grid2op/Chronics/multifolderWithCache.py index e5a5755b..a26568b0 100644 --- a/grid2op/Chronics/multifolderWithCache.py +++ b/grid2op/Chronics/multifolderWithCache.py @@ -239,6 +239,7 @@ def initialize( id_scenario = self._order[self._prev_cache_id] self.data = self._cached_data[id_scenario] self.data.next_chronics() + self._max_iter = self.data.max_iter @property def max_iter(self): diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index 2af3d213..ef1f024b 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -299,8 +299,7 @@ def foo(manager): #: this are the keys of the dictionnary `options` #: that can be used when calling `env.reset(..., options={})` - KEYS_RESET_OPTIONS = {"time serie id", "init state", "init ts", "max ts"} - + KEYS_RESET_OPTIONS = {"time serie id", "init state", "init ts", "max step"} def __init__( self, @@ -3780,6 +3779,12 @@ def fast_forward_chronics(self, nb_timestep): From grid2op version 1.10.3, a similar objective can be obtained directly by calling :func:`grid2op.Environment.Environment.reset` with `"init ts"` as option, for example like `obs = env.reset(options={"init ts": 12})` + + + .. danger:: + The usage of both :func:`BaseEnv.fast_forward_chronics` and :func:`Environment.set_max_iter` + is not recommended at all and might not behave correctly. Please use `env.reset` with + `obs = env.reset(options={"max step": xxx, "init ts": yyy})` for a correct behaviour. Parameters ---------- diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 2fdc9201..5db3d8c6 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -164,6 +164,10 @@ def __init__( self.name = name self._read_from_local_dir = _read_from_local_dir + # to remember if the user specified a "max_iter" at some point + self._max_iter = -1 + self._max_step = None + #: starting grid2Op 1.11 classes are stored on the disk when an environment is created #: so the "environment" is created twice (one to generate the class and then correctly to load them) self._allow_loaded_backend : bool = _allow_loaded_backend @@ -492,20 +496,97 @@ def max_episode_duration(self): to the maximum 32 bit integer (usually `2147483647`) """ + if self._max_step is not None: + return self._max_step tmp = dt_int(self.chronics_handler.max_episode_duration()) if tmp < 0: tmp = dt_int(np.iinfo(dt_int).max) return tmp + def _aux_check_max_iter(self, max_iter): + try: + max_iter_int = int(max_iter) + except ValueError as exc_: + raise EnvError("Impossible to set 'max_iter' by providing something that is not an integer.") from exc_ + if max_iter_int != max_iter: + raise EnvError("Impossible to set 'max_iter' by providing something that is not an integer.") + if max_iter_int < 1 and max_iter_int != -1: + raise EnvError("'max_iter' should be an int >= 1 or -1") + return max_iter_int + def set_max_iter(self, max_iter): """ - + Set the maximum duration of an episode for all the next episodes. + + .. seealso:: + The option `max step` when calling the :func:`Environment.reset` function + used like `obs = env.reset(options={"max step": 288})` (see examples of + `env.reset` for more information) + + .. note:: + The real maximum duration of a duration depends on this parameter but also on the + size of the time series used. For example, if you use an environment with + time series lasting 8064 steps and you call `env.set_max_iter(9000)` + the maximum number of iteration will still be 8064. + + .. warning:: + It only has an impact on future episode. Said differently it also has an impact AFTER + `env.reset` has been called. + + .. danger:: + The usage of both :func:`BaseEnv.fast_forward_chronics` and :func:`Environment.set_max_iter` + is not recommended at all and might not behave correctly. Please use `env.reset` with + `obs = env.reset(options={"max step": xxx, "init ts": yyy})` for a correct behaviour. + Parameters ---------- max_iter: ``int`` - The maximum number of iteration you can do before reaching the end of the episode. Set it to "-1" for + The maximum number of iterations you can do before reaching the end of the episode. Set it to "-1" for possibly infinite episode duration. + + Examples + -------- + It can be used like this: + + .. code-block:: python + + import grid2op + env_name = "l2rpn_case14_sandbox" + + env = grid2op.make(env_name) + + obs = env.reset() + obs.max_step == 8064 # default for this environment + + env.set_max_iter(288) + # no impact here + + obs = env.reset() + obs.max_step == 288 + + # the limitation still applies to the next episode + obs = env.reset() + obs.max_step == 288 + + If you want to "unset" your limitation, you can do: + + .. code-block:: python + + env.set_max_iter(-1) + obs = env.reset() + obs.max_step == 8064 + + Finally, you cannot limit it to something larger than the duration + of the time series of the environment: + + .. code-block:: python + + env.set_max_iter(9000) + obs = env.reset() + obs.max_step == 8064 + # the call to env.set_max_iter has no impact here + Notes ------- @@ -513,7 +594,9 @@ def set_max_iter(self, max_iter): more information """ - self.chronics_handler.set_max_iter(max_iter) + max_iter_int = self._aux_check_max_iter(max_iter) + self._max_iter = max_iter_int + self.chronics_handler.set_max_iter(max_iter_int) @property def _helper_observation(self): @@ -892,6 +975,18 @@ def add_text_logger(self, logger=None): self.logger = logger return self + def _aux_get_skip_ts(self, options): + skip_ts = None + if options is not None and "init ts" in options: + try: + skip_ts = int(options["init ts"]) + except ValueError as exc_: + raise Grid2OpException("In `env.reset` the kwargs `init ts` should be convertible to an int") from exc_ + + if skip_ts != options["init ts"]: + raise Grid2OpException(f"In `env.reset` the kwargs `init ts` should be convertible to an int, found {options['init ts']}") + return skip_ts + def reset(self, *, seed: Union[int, None] = None, @@ -1095,13 +1190,57 @@ def reset(self, the case for a "normal" (without options) reset, this line is disconnected, but can be reconnected directly (:attr:`grid2op.Observation.BaseObservation.time_before_cooldown_line` == 0) + .. seealso:: + The function :func:`Environment.fast_forward_chronics` for an alternative usage (that will be + deprecated at some point) + + Yet another feature has been added in grid2op version 1.10.3 in this `env.reset` function. It is + the capacity to limit the duration of an episode. + + .. code-block:: python + + import grid2op + env_name = "l2rpn_case14_sandbox" + env = grid2op.make(env_name) + + obs = env.reset(options={"max step": 288}) + + This will limit the duration to 288 steps (1 day), meaning your agent + will have successfully managed the entire episode if it manages to keep + the grid in a safe state for a whole day (depending on the environment you are + using the default duration is either one week - roughly 2016 steps or 4 weeks) + + .. note:: + This option only affect the current episode. It will have no impact on the + next episode (after reset) + + For example: + + .. code-block:: python + obs = env.reset() + obs.max_step == 8064 # default for this environment + + obs = env.reset(options={"max step": 288}) + obs.max_step == 288 # specified by the option + + obs = env.reset() + obs.max_step == 8064 # retrieve the default behaviour + + .. seealso:: + The function :func:`Environment.set_max_iter` for an alternative usage with the different + that `set_max_iter` is permenanent: it impacts all the future episodes and not only + the next one. + """ # process the "options" kwargs # (if there is an init state then I need to process it to remove the # some keys) + self._max_step = None method = "combine" init_state = None + skip_ts = self._aux_get_skip_ts(options) + max_iter_int = None if options is not None and "init state" in options: act_as_dict = options["init state"] if isinstance(act_as_dict, dict): @@ -1121,7 +1260,18 @@ def reset(self, init_state.remove_change() super().reset(seed=seed, options=options) - + + if options is not None and "max step" in options: + # use the "max iter" provided in the options + max_iter_int = self._aux_check_max_iter(options["max step"]) + if skip_ts is not None: + max_iter_chron = max_iter_int + skip_ts + else: + max_iter_chron = max_iter_int + self.chronics_handler.set_max_iter(max_iter_chron) + else: + # reset previous max iter to value set with `env.set_max_iter(...)` (or -1 by default) + self.chronics_handler.set_max_iter(self._max_iter) self.chronics_handler.next_chronics() self.chronics_handler.initialize( self.backend.name_load, @@ -1130,6 +1280,10 @@ def reset(self, self.backend.name_sub, names_chronics_to_backend=self._names_chronics_to_backend, ) + if max_iter_int is not None: + self._max_step = min(max_iter_int, self.chronics_handler.real_data.max_iter - (skip_ts if skip_ts is not None else 0)) + else: + self._max_step = None self._env_modification = None self._reset_maintenance() self._reset_redispatching() @@ -1140,15 +1294,7 @@ def reset(self, del self.viewer_fig self.viewer_fig = None - if options is not None and "init ts" in options: - try: - skip_ts = int(options["init ts"]) - except ValueError as exc_: - raise Grid2OpException("In `env.reset` the kwargs `init ts` should be convertible to an int") from exc_ - - if skip_ts != options["init ts"]: - raise Grid2OpException(f"In `env.reset` the kwargs `init ts` should be convertible to an int, found {options['init ts']}") - + if skip_ts is not None: self._reset_vectors_and_timings() if skip_ts < 1: @@ -1250,6 +1396,7 @@ def _custom_deepcopy_for_copy(self, new_obj): new_obj._compat_glop_version = self._compat_glop_version new_obj._actionClass_orig = self._actionClass_orig new_obj._observationClass_orig = self._observationClass_orig + new_obj._max_iter = self._max_iter def copy(self) -> "Environment": """ diff --git a/grid2op/tests/test_resest_options.py b/grid2op/tests/test_resest_options.py index 843785d2..5a5d6b2b 100644 --- a/grid2op/tests/test_resest_options.py +++ b/grid2op/tests/test_resest_options.py @@ -65,7 +65,7 @@ def test_function_ok(self): assert obs.hour_of_day == 0, f"{ obs.hour_of_day} vs 1" assert obs.minute_of_hour == 0, f"{ obs.minute_of_hour} vs 0" - def check_soft_overflow(self): + def test_soft_overflow(self): """check that the lines are not on soft overflow (obs.timestep_overflow == 0 just after reset)""" line_id = 3 obs = self.env.reset(options={"time serie id": 0}) @@ -97,7 +97,7 @@ def check_soft_overflow(self): assert obs.rho[line_id] > 1. assert obs.line_status[line_id] - def check_hard_overflow(self): + def test_hard_overflow(self): """check lines are disconnected if on hard overflow at the beginning""" line_id = 3 obs = self.env.reset(options={"time serie id": 0}) @@ -142,10 +142,150 @@ def test_raise_if_args_not_correct(self): with self.assertRaises(Grid2OpException): # float which is not an int obs = self.env.reset(options={"init ts": 1.5}) + with self.assertRaises(Grid2OpException): + # value too small + obs = self.env.reset(options={"init ts": 0}) # should work with a float convertible to an int obs = self.env.reset(options={"time serie id": 0, "init ts": 6.}) + +class MaxStepOptions(unittest.TestCase): + """test the "max step" options in env.reset() """ + 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, + _add_to_name=type(self).__name__) + + def test_raise_if_args_not_correct(self): + with self.assertRaises(Grid2OpException): + # string and not int + obs = self.env.reset(options={"max step": "treliug"}) + with self.assertRaises(Grid2OpException): + # float which is not an int + obs = self.env.reset(options={"max step": 1.5}) + + with self.assertRaises(Grid2OpException): + # value too small + obs = self.env.reset(options={"max step": 0}) + + # should work with a float convertible to an int + obs = self.env.reset(options={"time serie id": 0, "max step": 6.}) + + def test_function_ok(self): + obs = self.env.reset() # normal reset + assert obs.max_step == 575, f"{obs.max_step} vs 575" + + # enough data to be limited + obs = self.env.reset(options={"max step": 5}) + assert obs.max_step == 5, f"{obs.max_step} vs 5" + + # limit has no effect: not enough data anyway + obs = self.env.reset(options={"max step": 800}) + assert obs.max_step == 575, f"{obs.max_step} vs 575" + def test_no_impact_next_reset(self): + obs = self.env.reset() # normal reset + assert obs.max_step == 575, f"{obs.max_step} vs 575" + + # enough data to be limited + obs = self.env.reset(options={"max step": 5}) + assert obs.max_step == 5, f"{obs.max_step} vs 5" + + obs = self.env.reset() # normal reset + assert obs.max_step == 575, f"{obs.max_step} vs 575" + + def test_remember_previous_max_iter(self): + obs = self.env.reset() # normal reset + assert obs.max_step == 575, f"{obs.max_step} vs 575" + + self.env.set_max_iter(200) + obs = self.env.reset() # normal reset + assert obs.max_step == 200, f"{obs.max_step} vs 200" + + # use the option to limit + obs = self.env.reset(options={"max step": 5}) + assert obs.max_step == 5, f"{obs.max_step} vs 5" + + # check it remembers the previous limit + obs = self.env.reset() # normal reset (but 200 were set) + assert obs.max_step == 200, f"{obs.max_step} vs 200" + + # set back the limit to "maximum in the time serie" + self.env.set_max_iter(-1) + obs = self.env.reset() # normal reset + assert obs.max_step == 575, f"{obs.max_step} vs 575" + + # limit for this reset only + obs = self.env.reset(options={"max step": 5}) + assert obs.max_step == 5, f"{obs.max_step} vs 5" + + # check again the right limit was applied + obs = self.env.reset() # normal reset (but 575 were set back) + assert obs.max_step == 575, f"{obs.max_step} vs 575" + + def test_max_step_and_init_ts(self): + """test that episode duration is properly computed and updated in + the observation when both max step and init ts are set at the same time""" + obs = self.env.reset() # normal reset + assert obs.max_step == 575, f"{obs.max_step} vs 575" + assert obs.year == 2019 + assert obs.month == 1 + assert obs.day == 6 + assert obs.hour_of_day == 0 + assert obs.minute_of_hour == 0 + + obs = self.env.reset(options={"init ts": 12 * 24, "max step": 24}) # start after exactly 1 day for 2 hours + assert obs.year == 2019 + assert obs.month == 1 + assert obs.day == 7, f"{ obs.day} vs 7" + assert obs.hour_of_day == 0, f"{ obs.hour_of_day} vs 1" + assert obs.minute_of_hour == 0, f"{ obs.minute_of_hour} vs 0" + assert obs.max_step == 24, f"{obs.max_step} vs 24" + + obs = self.env.reset(options={"init ts": 12 * 24}) # start after exactly 1 day without any max + assert obs.year == 2019 + assert obs.month == 1 + assert obs.day == 7, f"{ obs.day} vs 7" + assert obs.hour_of_day == 0, f"{ obs.hour_of_day} vs 1" + assert obs.minute_of_hour == 0, f"{ obs.minute_of_hour} vs 0" + assert obs.max_step == 575, f"{obs.max_step} vs 575" + + obs = self.env.reset() # normal reset + assert obs.max_step == 575, f"{obs.max_step} vs 575" + assert obs.year == 2019 + assert obs.month == 1 + assert obs.day == 6 + assert obs.hour_of_day == 0 + assert obs.minute_of_hour == 0 + + obs = self.env.reset(options={"max step": 288}) # don't skip anything, but last only 1 day + assert obs.year == 2019 + assert obs.month == 1 + assert obs.day == 6, f"{ obs.day} vs 6" + assert obs.hour_of_day == 0, f"{ obs.hour_of_day} vs 1" + assert obs.minute_of_hour == 0, f"{ obs.minute_of_hour} vs 0" + assert obs.max_step == 288, f"{obs.max_step} vs 288" + + obs = self.env.reset(options={"init ts": 12 * 24, "max step": 700}) # start after exactly 1 day for too much steps + assert obs.year == 2019 + assert obs.month == 1 + assert obs.day == 7, f"{ obs.day} vs 7" + assert obs.hour_of_day == 0, f"{ obs.hour_of_day} vs 1" + assert obs.minute_of_hour == 0, f"{ obs.minute_of_hour} vs 0" + # 288 here because the limit is the time series ! + assert obs.max_step == 287, f"{obs.max_step} vs 287" + + obs = self.env.reset() # normal reset + assert obs.max_step == 575, f"{obs.max_step} vs 575" + assert obs.year == 2019 + assert obs.month == 1 + assert obs.day == 6 + assert obs.hour_of_day == 0 + assert obs.minute_of_hour == 0 + + if __name__ == "__main__": unittest.main() diff --git a/grid2op/typing_variables.py b/grid2op/typing_variables.py index 224969f6..0d0c0396 100644 --- a/grid2op/typing_variables.py +++ b/grid2op/typing_variables.py @@ -45,6 +45,8 @@ #: type hints for the "options" flag of reset function RESET_OPTIONS_TYPING = Union[Dict[Literal["time serie id"], int], Dict[Literal["init state"], DICT_ACT_TYPING], + Dict[Literal["init ts"], int], + Dict[Literal["max step"], int], None] #: type hints for a "GridObject" when converted to a dictionary From 3eb1ae2a3f991d6fb46200b250c3507c669a2eb5 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Wed, 12 Jun 2024 17:42:04 +0200 Subject: [PATCH 04/11] fixing a bug when using the runner and env.chronics_handler.set_max_iter, which is now private --- CHANGELOG.rst | 3 +++ grid2op/Chronics/chronicsHandler.py | 10 +++++++++- grid2op/Environment/environment.py | 3 ++- grid2op/Runner/aux_fun.py | 20 +++++++++----------- grid2op/tests/test_basic_env_ls.py | 4 ++-- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f940232e..f26f8392 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -38,6 +38,9 @@ Change Log - TODO Notebook for stable baselines - TODO reset options in the runner +- [BREAKING] `env.chronics_hander.set_max_iter()` is now a private function. Use + `env.set_max_iter()` instead. Indeed, `env.chronics_hander.set_max_iter()` will likely has + no effect - [FIXED] a bug in the `MultiFolder` and `MultifolderWithCache` leading to the wrong computation of `max_iter` on some corner cases - [ADDED] possibility to skip some step when calling `env.reset(..., options={"init ts": ...})` diff --git a/grid2op/Chronics/chronicsHandler.py b/grid2op/Chronics/chronicsHandler.py index 0a125bec..08004ce7 100644 --- a/grid2op/Chronics/chronicsHandler.py +++ b/grid2op/Chronics/chronicsHandler.py @@ -160,13 +160,21 @@ def get_name(self): """ return str(os.path.split(self.get_id())[-1]) - def set_max_iter(self, max_iter: int): + def _set_max_iter(self, max_iter: int): """ This function is used to set the maximum number of iterations possible before the chronics ends. You can reset this by setting it to `-1`. + .. danger:: + As for grid2op 1.10.3, due to the fix of a bug when + max_iter and fast_forward were used at the same time + you should not use this function anymore. + + Please use `env.set_max_iter()` instead of + `env.chronics_hander.set_max_iter()` + Parameters ---------- max_iter: ``int`` diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 5db3d8c6..5b0bbb34 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -596,7 +596,7 @@ def set_max_iter(self, max_iter): """ max_iter_int = self._aux_check_max_iter(max_iter) self._max_iter = max_iter_int - self.chronics_handler.set_max_iter(max_iter_int) + self.chronics_handler._set_max_iter(max_iter_int) @property def _helper_observation(self): @@ -1397,6 +1397,7 @@ def _custom_deepcopy_for_copy(self, new_obj): new_obj._actionClass_orig = self._actionClass_orig new_obj._observationClass_orig = self._observationClass_orig new_obj._max_iter = self._max_iter + new_obj._max_step = self._max_step def copy(self) -> "Environment": """ diff --git a/grid2op/Runner/aux_fun.py b/grid2op/Runner/aux_fun.py index b9839f5c..c24d4d99 100644 --- a/grid2op/Runner/aux_fun.py +++ b/grid2op/Runner/aux_fun.py @@ -122,20 +122,18 @@ def _aux_run_one_episode( cum_reward = dt_float(0.0) # set the environment to use the proper chronic - env.set_id(indx) - # set the seed - if env_seed is not None: - env.seed(env_seed) - + # env.set_id(indx) + + options = {"time serie id": indx} # handle max_iter if max_iter is not None: - env.chronics_handler.set_max_iter(max_iter) - + options["max step"] = max_iter + # handle init state + if init_state is not None: + options["init state"] = init_state + # reset it - if init_state is None: - obs = env.reset() - else: - obs = env.reset(options={"init state": init_state}) + obs = env.reset(seed=env_seed, options=options) # reset the number of calls to high resolution simulator env._highres_sim_counter._HighResSimCounter__nb_highres_called = 0 diff --git a/grid2op/tests/test_basic_env_ls.py b/grid2op/tests/test_basic_env_ls.py index c3214a26..1e1496ae 100644 --- a/grid2op/tests/test_basic_env_ls.py +++ b/grid2op/tests/test_basic_env_ls.py @@ -132,8 +132,8 @@ def test_runner(self): res_in, *_ = runner_in.run(nb_episode=1, max_iter=self.max_iter, env_seeds=[0], episode_id=[0], add_detailed_output=True) res_in2, *_ = runner_in.run(nb_episode=1, max_iter=self.max_iter, env_seeds=[0], episode_id=[0]) # check correct results are obtained when agregated - assert res_in[3] == 10 - assert res_in2[3] == 10 + assert res_in[3] == self.max_iter, f"{res_in[3]} vs {self.max_iter}" + assert res_in2[3] == self.max_iter, f"{res_in[3]} vs {self.max_iter}" assert np.allclose(res_in[2], 645.4992065) assert np.allclose(res_in2[2], 645.4992065) From d2d7803c3e2bb85859c8dcc576ae1a4b2aef107b Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 13 Jun 2024 08:58:28 +0200 Subject: [PATCH 05/11] fix some broken tests --- CHANGELOG.rst | 10 +++++++--- grid2op/Chronics/handlers/baseHandler.py | 2 +- grid2op/Chronics/handlers/csvForecastHandler.py | 4 ++-- grid2op/Chronics/multifolderWithCache.py | 2 +- grid2op/Chronics/time_series_from_handlers.py | 6 +++--- grid2op/Environment/environment.py | 4 ++-- grid2op/Runner/runner.py | 2 +- grid2op/tests/test_ChronicsHandler.py | 4 ++-- grid2op/tests/test_multi_steps_forecasts.py | 2 +- 9 files changed, 20 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f26f8392..a34991c5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -38,9 +38,13 @@ Change Log - TODO Notebook for stable baselines - TODO reset options in the runner -- [BREAKING] `env.chronics_hander.set_max_iter()` is now a private function. Use - `env.set_max_iter()` instead. Indeed, `env.chronics_hander.set_max_iter()` will likely has - no effect +- [BREAKING] `env.chronics_hander.set_max_iter(xxx)` is now a private function. Use + `env.set_max_iter(xxx)` or even better `env.reset(options={"max step": xxx})`. + Indeed, `env.chronics_hander.set_max_iter()` will likely have + no effect at all on your environment. +- [BREAKING] for all the `Handler` (*eg* `CSVForecastHandler`) the method `set_max_iter` is + now private (for the same reason as the `env.chronics_handler`). We do not recommend to + use it (will likely have no effect). Prefer using `env.set_max_iter` instead. - [FIXED] a bug in the `MultiFolder` and `MultifolderWithCache` leading to the wrong computation of `max_iter` on some corner cases - [ADDED] possibility to skip some step when calling `env.reset(..., options={"init ts": ...})` diff --git a/grid2op/Chronics/handlers/baseHandler.py b/grid2op/Chronics/handlers/baseHandler.py index 0cb51d9a..d4acf1d6 100644 --- a/grid2op/Chronics/handlers/baseHandler.py +++ b/grid2op/Chronics/handlers/baseHandler.py @@ -73,7 +73,7 @@ def __init__(self, array_name, max_iter=-1, h_forecast=(5, )): self.path : Optional[os.PathLike] = None self.max_episode_duration : Optional[int] = None - def set_max_iter(self, max_iter: Optional[int]) -> None: + def _set_max_iter(self, max_iter: Optional[int]) -> None: """ INTERNAL diff --git a/grid2op/Chronics/handlers/csvForecastHandler.py b/grid2op/Chronics/handlers/csvForecastHandler.py index 046ac870..cf08a0ea 100644 --- a/grid2op/Chronics/handlers/csvForecastHandler.py +++ b/grid2op/Chronics/handlers/csvForecastHandler.py @@ -93,8 +93,8 @@ def load_next(self, dict_): def set_chunk_size(self, chunk_size): super().set_chunk_size(self._nb_row_per_step * int(chunk_size)) - def set_max_iter(self, max_iter): - super().set_max_iter(self._nb_row_per_step * int(max_iter)) + def _set_max_iter(self, max_iter): + super()._set_max_iter(self._nb_row_per_step * int(max_iter)) def set_h_forecast(self, h_forecast): super().set_h_forecast(h_forecast) diff --git a/grid2op/Chronics/multifolderWithCache.py b/grid2op/Chronics/multifolderWithCache.py index a26568b0..a7f09ea0 100644 --- a/grid2op/Chronics/multifolderWithCache.py +++ b/grid2op/Chronics/multifolderWithCache.py @@ -70,7 +70,7 @@ class MultifolderWithCache(Multifolder): env = make(...,chronics_class=MultifolderWithCache) # set the chronics to limit to one week of data (lower memory footprint) - env.chronics_handler.set_max_iter(7*288) + env.set_max_iter(7*288) # assign a filter, use only chronics that have "december" in their name env.chronics_handler.real_data.set_filter(lambda x: re.match(".*december.*", x) is not None) # create the cache diff --git a/grid2op/Chronics/time_series_from_handlers.py b/grid2op/Chronics/time_series_from_handlers.py index d3a3af4a..99715281 100644 --- a/grid2op/Chronics/time_series_from_handlers.py +++ b/grid2op/Chronics/time_series_from_handlers.py @@ -204,7 +204,7 @@ def __init__( self.set_chunk_size(chunk_size) if max_iter != -1: - self.set_max_iter(max_iter) + self._set_max_iter(max_iter) self.init_datetime() self.current_inj = None @@ -389,10 +389,10 @@ def set_chunk_size(self, new_chunk_size): for el in self._active_handlers: el.set_chunk_size(new_chunk_size) - def set_max_iter(self, max_iter): + def _set_max_iter(self, max_iter): self.max_iter = int(max_iter) for el in self._active_handlers: - el.set_max_iter(max_iter) + el._set_max_iter(max_iter) def init_datetime(self): for handl in self._active_handlers: diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 5b0bbb34..540d687a 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -1268,10 +1268,10 @@ def reset(self, max_iter_chron = max_iter_int + skip_ts else: max_iter_chron = max_iter_int - self.chronics_handler.set_max_iter(max_iter_chron) + self.chronics_handler._set_max_iter(max_iter_chron) else: # reset previous max iter to value set with `env.set_max_iter(...)` (or -1 by default) - self.chronics_handler.set_max_iter(self._max_iter) + self.chronics_handler._set_max_iter(self._max_iter) self.chronics_handler.next_chronics() self.chronics_handler.initialize( self.backend.name_load, diff --git a/grid2op/Runner/runner.py b/grid2op/Runner/runner.py index 647630ae..4715e4a1 100644 --- a/grid2op/Runner/runner.py +++ b/grid2op/Runner/runner.py @@ -789,7 +789,7 @@ def run_one_episode( init_state=init_state, ) if max_iter is not None: - env.chronics_handler.set_max_iter(-1) + env.chronics_handler._set_max_iter(-1) id_chron = env.chronics_handler.get_id() # `res` here necessarily contains detailed_output and nb_highres_call diff --git a/grid2op/tests/test_ChronicsHandler.py b/grid2op/tests/test_ChronicsHandler.py index c19ad216..1fefb2bc 100644 --- a/grid2op/tests/test_ChronicsHandler.py +++ b/grid2op/tests/test_ChronicsHandler.py @@ -1122,7 +1122,7 @@ def setUp(self): with warnings.catch_warnings(): warnings.filterwarnings("ignore") self.env = grid2op.make("rte_case14_realistic", test=True, _add_to_name=type(self).__name__) - self.env.chronics_handler.set_max_iter(self.max_iter) + self.env.set_max_iter(self.max_iter) def tearDown(self): self.env.close() @@ -1183,7 +1183,7 @@ def test_load_still(self): ) as env: # test a first time without chunks env.set_id(0) - env.chronics_handler.set_max_iter(max_iter) + env.set_max_iter(max_iter) obs = env.reset() # check that simulate is working diff --git a/grid2op/tests/test_multi_steps_forecasts.py b/grid2op/tests/test_multi_steps_forecasts.py index 2608f3cb..0dc7ac68 100644 --- a/grid2op/tests/test_multi_steps_forecasts.py +++ b/grid2op/tests/test_multi_steps_forecasts.py @@ -80,7 +80,7 @@ def test_chunk_size(self): def test_max_iter(self): max_iter = 4 - self.env.chronics_handler.set_max_iter(max_iter) + self.env.set_max_iter(max_iter) obs = self.env.reset() self.aux_test_for_consistent(obs) From b2b7690afa7a108f4ca7929accb10d31cc3d82b8 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 13 Jun 2024 10:02:57 +0200 Subject: [PATCH 06/11] trying to fix another issue in the CI --- .gitignore | 3 +++ grid2op/Environment/environment.py | 4 ++-- grid2op/Runner/aux_fun.py | 1 + grid2op/Runner/runner.py | 11 ----------- 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index ba9e6e67..7f06d9ea 100644 --- a/.gitignore +++ b/.gitignore @@ -410,6 +410,9 @@ grid2op/tests/req_38_np121 test_make_2_envs.py getting_started/env_py38_grid2op110_ray110.ipynb getting_started/env_py38_grid2op110_ray210.ipynb +grid2op/tests/req_chronix2grid +grid2op/tests/venv_test_chronix2grid/ + # profiling files **.prof diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 540d687a..51cdcc68 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -165,8 +165,8 @@ def __init__( self._read_from_local_dir = _read_from_local_dir # to remember if the user specified a "max_iter" at some point - self._max_iter = -1 - self._max_step = None + self._max_iter = chronics_handler.max_iter # for all episode, set in the chronics_handler or by a call to `env.set_max_iter` + self._max_step = None # for the current episode #: starting grid2Op 1.11 classes are stored on the disk when an environment is created #: so the "environment" is created twice (one to generate the class and then correctly to load them) diff --git a/grid2op/Runner/aux_fun.py b/grid2op/Runner/aux_fun.py index c24d4d99..406de2bf 100644 --- a/grid2op/Runner/aux_fun.py +++ b/grid2op/Runner/aux_fun.py @@ -128,6 +128,7 @@ def _aux_run_one_episode( # handle max_iter if max_iter is not None: options["max step"] = max_iter + # handle init state if init_state is not None: options["init state"] = init_state diff --git a/grid2op/Runner/runner.py b/grid2op/Runner/runner.py index 4715e4a1..2dd7207a 100644 --- a/grid2op/Runner/runner.py +++ b/grid2op/Runner/runner.py @@ -539,11 +539,6 @@ def __init__( self.max_iter = max_iter if max_iter > 0: self.gridStateclass_kwargs["max_iter"] = max_iter - # self.chronics_handler = ChronicsHandler( - # chronicsClass=self.gridStateclass, - # path=self.path_chron, - # **self.gridStateclass_kwargs - # ) self.verbose = verbose self.thermal_limit_a = thermal_limit_a @@ -636,12 +631,6 @@ def _make_new_backend(self): return res def _new_env(self, parameters) -> Tuple[BaseEnv, BaseAgent]: - # the same chronics_handler is used for all the environments. - # make sure to "reset" it properly - # (this is handled elsewhere in case of "multi chronics") - # ch_used = copy.deepcopy(chronics_handler) - # if not ch_used.chronicsClass.MULTI_CHRONICS: - # ch_used.next_chronics() chronics_handler = ChronicsHandler( chronicsClass=self.gridStateclass, path=self.path_chron, From a0f1b4458bfd3d9611f780de2a61b2f17fea6e58 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 13 Jun 2024 14:31:22 +0200 Subject: [PATCH 07/11] prepare to add the possibility to specify reset_options in the runner --- grid2op/Runner/aux_fun.py | 39 +++++++-- grid2op/Runner/runner.py | 163 ++++++++++++++++++++++++++++++++++---- 2 files changed, 178 insertions(+), 24 deletions(-) diff --git a/grid2op/Runner/aux_fun.py b/grid2op/Runner/aux_fun.py index 406de2bf..e103e3b8 100644 --- a/grid2op/Runner/aux_fun.py +++ b/grid2op/Runner/aux_fun.py @@ -36,6 +36,7 @@ def _aux_add_data(reward, env, episode, ) return reward + def _aux_one_process_parrallel( runner, episode_this_process, @@ -46,7 +47,8 @@ def _aux_one_process_parrallel( max_iter=None, add_detailed_output=False, add_nb_highres_sim=False, - init_states=None + init_states=None, + reset_options=None, ): """this is out of the runner, otherwise it does not work on windows / macos""" # chronics_handler = ChronicsHandler( @@ -75,7 +77,11 @@ def _aux_one_process_parrallel( init_state = init_states[i] else: init_state = None - + + if reset_options is not None: + reset_option = reset_options[i] + else: + reset_option = None tmp_ = _aux_run_one_episode( env, agent, @@ -87,7 +93,8 @@ def _aux_one_process_parrallel( agent_seed=agt_seed, detailed_output=add_detailed_output, use_compact_episode_data=runner.use_compact_episode_data, - init_state=init_state + init_state=init_state, + reset_option=reset_option ) (name_chron, cum_reward, nb_time_step, max_ts, episode_data, nb_highres_sim) = tmp_ id_chron = env.chronics_handler.get_id() @@ -114,7 +121,8 @@ def _aux_run_one_episode( max_iter=None, detailed_output=False, use_compact_episode_data=False, - init_state=None + init_state=None, + reset_option=None, ): done = False time_step = int(0) @@ -123,18 +131,33 @@ def _aux_run_one_episode( # set the environment to use the proper chronic # env.set_id(indx) + if reset_option is None: + reset_option = {} + + if "time serie id" in reset_option: + warnings.warn("You provided both `episode_id` and the key `'time serie id'` is present " + "in the provided `reset_options`. In this case, grid2op will ignore the " + "`time serie id` of the `reset_options` and keep the value in `episode_id`.") + reset_option["time serie id"] = indx - options = {"time serie id": indx} # handle max_iter if max_iter is not None: - options["max step"] = max_iter + if "max step" in reset_option: + warnings.warn("You provided both `max_iter` and the key `'max step'` is present " + "in the provided `reset_options`. In this case, grid2op will ignore the " + "`max step` of the `reset_options` and keep the value in `max_iter`.") + reset_option["max step"] = max_iter # handle init state if init_state is not None: - options["init state"] = init_state + if "init state" in reset_option: + warnings.warn("You provided both `init_state` and the key `'init state'` is present " + "in the provided `reset_options`. In this case, grid2op will ignore the " + "`init state` of the `reset_options` and keep the value in `init_state`.") + reset_option["init state"] = init_state # reset it - obs = env.reset(seed=env_seed, options=options) + obs = env.reset(seed=env_seed, options=reset_option) # reset the number of calls to high resolution simulator env._highres_sim_counter._HighResSimCounter__nb_highres_called = 0 diff --git a/grid2op/Runner/runner.py b/grid2op/Runner/runner.py index 2dd7207a..1ec06c4c 100644 --- a/grid2op/Runner/runner.py +++ b/grid2op/Runner/runner.py @@ -55,6 +55,8 @@ # TODO use gym logger if specified by the user. # TODO: if chronics are "loop through" multiple times, only last results are saved. :-/ +KEY_TIME_SERIE_ID = "time serie id" + class Runner(object): """ @@ -725,7 +727,8 @@ def run_one_episode( episode_id=None, detailed_output=False, add_nb_highres_sim=False, - init_state=None + init_state=None, + reset_options=None, ) -> runner_returned_type: """ INTERNAL @@ -762,12 +765,23 @@ def run_one_episode( """ self.reset() - with self.init_env() as env: + with self.init_env() as env: + # small piece of code to detect the + # episode id + if episode_id is None: + # user did not provide any episode id, I check in the reset_options + if reset_options is not None: + if KEY_TIME_SERIE_ID in reset_options: + indx = int(reset_options[KEY_TIME_SERIE_ID]) + del reset_options[KEY_TIME_SERIE_ID] + else: + # user specified an episode id, I use it. + indx = episode_id res = _aux_run_one_episode( env, self.agent, self.logger, - indx if episode_id is None else episode_id, + indx, path_save, pbar=pbar, env_seed=env_seed, @@ -776,6 +790,7 @@ def run_one_episode( detailed_output=detailed_output, use_compact_episode_data = self.use_compact_episode_data, init_state=init_state, + reset_option=reset_options, ) if max_iter is not None: env.chronics_handler._set_max_iter(-1) @@ -802,7 +817,8 @@ def _run_sequential( episode_id=None, add_detailed_output=False, add_nb_highres_sim=False, - init_states=None + init_states=None, + reset_options=None, ) -> List[runner_returned_type]: """ INTERNAL @@ -875,9 +891,22 @@ def _run_sequential( init_state = None if init_states is not None: init_state = init_states[i] - ep_id = i # if no "episode_id" is provided i used the i th one + reset_opt = None + if reset_options is not None: + # we copy it because we might remove the "time serie id" + # from it + reset_opt = reset_options[i].copy() + # if no "episode_id" is provided i used the i th one + ep_id = i if episode_id is not None: + # if episode_id is provided, I use this one ep_id = episode_id[i] # otherwise i use the provided one + else: + # if it's not provided, I check if one is used in the `reset_options` + if reset_opt is not None: + if KEY_TIME_SERIE_ID in reset_opt: + ep_id = int(reset_opt[KEY_TIME_SERIE_ID]) + del reset_opt[KEY_TIME_SERIE_ID] ( id_chron, name_chron, @@ -896,6 +925,7 @@ def _run_sequential( detailed_output=True, add_nb_highres_sim=True, init_state=init_state, + reset_options=reset_opt ) res[i] = (id_chron, name_chron, @@ -921,7 +951,8 @@ def _run_parrallel( episode_id=None, add_detailed_output=False, add_nb_highres_sim=False, - init_states=None + init_states=None, + reset_options=None, ) -> List[runner_returned_type]: """ INTERNAL @@ -992,7 +1023,7 @@ def _run_parrallel( # if i start using parallel i need to continue using parallel # so i force the usage of the sequential mode self.logger.warn( - "Runner.run_parrallel: number of process set to 1. Failing back into sequential mod." + "Runner.run_parrallel: number of process set to 1. Failing back into sequential mode." ) return self._run_sequential( nb_episode, @@ -1004,6 +1035,7 @@ def _run_parrallel( add_detailed_output=add_detailed_output, add_nb_highres_sim=add_nb_highres_sim, init_states=init_states, + reset_options=reset_options ) else: self._clean_up() @@ -1012,8 +1044,22 @@ def _run_parrallel( process_ids = [[] for i in range(nb_process)] for i in range(nb_episode): if episode_id is None: - process_ids[i % nb_process].append(i) + # user does not provide episode_id + if reset_options is not None: + # we copy them, because we might delete some things from them + reset_options = [el.copy() for el in reset_options] + + # we check if the reset_options contains the "time serie id" + if KEY_TIME_SERIE_ID in reset_options[i]: + this_ep_id = int(reset_options[i][KEY_TIME_SERIE_ID]) + del reset_options[i][KEY_TIME_SERIE_ID] + else: + this_ep_id = i + else: + this_ep_id = i + process_ids[i % nb_process].append(this_ep_id) else: + # user provided episode_id, we use this one process_ids[i % nb_process].append(episode_id[i]) if env_seeds is None: @@ -1035,11 +1081,19 @@ def _run_parrallel( if init_states is None: init_states_res = [None for _ in range(nb_process)] else: - # split the seeds according to the process + # split the init states according to the process init_states_res = [[] for _ in range(nb_process)] for i in range(nb_episode): init_states_res[i % nb_process].append(init_states[i]) + if reset_options is None: + reset_options_res = [None for _ in range(nb_process)] + else: + # split the reset options according to the process + reset_options_res = [[] for _ in range(nb_process)] + for i in range(nb_episode): + reset_options_res[i % nb_process].append(reset_options[i]) + res = [] if _IS_LINUX: lists = [(self,) for _ in enumerate(process_ids)] @@ -1056,7 +1110,8 @@ def _run_parrallel( max_iter, add_detailed_output, add_nb_highres_sim, - init_states_res[i]) + init_states_res[i], + reset_options_res[i]) if get_start_method() == 'spawn': # https://github.com/rte-france/Grid2Op/issues/600 @@ -1139,6 +1194,7 @@ def run( add_detailed_output=False, add_nb_highres_sim=False, init_states=None, + reset_options=None, ) -> List[runner_returned_type]: """ Main method of the :class:`Runner` class. It will either call :func:`Runner._run_sequential` if "nb_process" is @@ -1159,7 +1215,11 @@ def run( max_iter: ``int`` Maximum number of iteration you want the runner to perform. - + + .. warning:: + (only for grid2op >= 1.10.3) If set in this parameters, it will + erase all values that may be present in the `reset_options` kwargs (key `"max step"`) + pbar: ``bool`` or ``type`` or ``object`` How to display the progress bar, understood as follow: @@ -1185,6 +1245,15 @@ def run( For each of the nb_episdeo you want to compute, it specifies the id of the chronix that will be used. By default ``None``, no seeds are set. If provided, its size should match ``nb_episode``. + + .. warning:: + (only for grid2op >= 1.10.3) If set in this parameters, it will + erase all values that may be present in the `reset_options` kwargs (key `"time serie id"`). + + .. danger:: + As of now, it's not properly handled to compute twice the same `episode_id` more than once using the runner + (more specifically, the computation will happen but file might not be saved correctly on the + hard drive: attempt to save all the results in the same location. We do not advise to do it) add_detailed_output: ``bool`` A flag to add an :class:`EpisodeData` object to the results, containing a lot of information about the run @@ -1204,6 +1273,43 @@ def run( If you provide a dictionary or a grid2op action, then this element will be used for all scenarios you want to run. + .. warning:: + (only for grid2op >= 1.10.3) If set in this parameters, it will + erase all values that may be present in the `reset_options` kwargs (key `"init state"`). + + reset_options: + (added in grid2op 1.10.3) Possibility to customize the call to `env.reset` made internally by + the Runner. More specifically, it will pass a custom `options` when the runner calls + `env.reset(..., options=XXX)`. + + It should either be: + + - a dictionary that can be used directly by :func:`grid2op.Environment.Environment.reset`. + In this case the same dictionary will be used for all the episodes computed by the runner. + - a list / tuple of one of the above with the same size as the number of episode you want to + compute which allow a full customization for each episode. + + .. warning:: + If the kwargs `max_iter` is present when calling `runner.run` function, then the key `max step` + will be ignored in all the `reset_options` dictionary. + + .. warning:: + If the kwargs `episode_id` is present when calling `runner.run` function, then the key `time serie id` + will be ignored in all the `reset_options` dictionary. + + .. warning:: + If the kwargs `init_states` is present when calling `runner.run` function, then the key `init state` + will be ignored in all the `reset_options` dictionary. + + .. danger:: + If you provide the key "time serie id" in one of the `reset_options` dictionary, we recommend + you do it for all `reset options` otherwise you might not end up computing the correct episodes. + + .. danger:: + As of now, it's not properly handled to compute twice the same `time serie` more than once using the runner + (more specifically, the computation will happen but file might not be saved correctly on the + hard drive: attempt to save all the results in the same location. We do not advise to do it) + Returns ------- res: ``list`` @@ -1343,8 +1449,30 @@ def run( f"You provided {type(el)} at position {i}.") else: raise RuntimeError("When using `init_state` in the runner, you should make sure to use " - "either use dictionnary, grid2op actions or list of actions.") - + "either use dictionnary, grid2op actions or list / tuple of actions.") + + if reset_options is not None: + if isinstance(reset_options, (dict)): + # user provided one initial state, I copy it to all + # evaluation + reset_options = [reset_options.copy() for _ in range(nb_episode)] + elif isinstance(reset_options, (list, tuple, np.ndarray)): + # user provided a list ofreset_options, it should match the + # number of scenarios + if len(reset_options) != nb_episode: + raise RuntimeError( + 'You want to compute "{}" run(s) but provide only "{}" different reset options.' + "".format(nb_episode, len(reset_options)) + ) + for i, el in enumerate(reset_options): + if not isinstance(el, dict): + raise RuntimeError("When specifying `reset_options` kwargs with a list (or a tuple) " + "it should be a list (or a tuple) of dictionary or BaseAction. " + f"You provided {type(el)} at position {i}.") + else: + raise RuntimeError("When using `reset_options` in the runner, you should make sure to use " + "either use dictionnary, grid2op actions or list / tuple of actions.") + if max_iter is not None: max_iter = int(max_iter) @@ -1367,7 +1495,8 @@ def run( episode_id=episode_id, add_detailed_output=add_detailed_output, add_nb_highres_sim=add_nb_highres_sim, - init_states=init_states + init_states=init_states, + reset_options=reset_options ) else: if add_detailed_output and (_IS_WINDOWS or _IS_MACOS): @@ -1386,7 +1515,8 @@ def run( episode_id=episode_id, add_detailed_output=add_detailed_output, add_nb_highres_sim=add_nb_highres_sim, - init_states=init_states + init_states=init_states, + reset_options=reset_options ) else: self.logger.info("Parallel runner used.") @@ -1400,7 +1530,8 @@ def run( episode_id=episode_id, add_detailed_output=add_detailed_output, add_nb_highres_sim=add_nb_highres_sim, - init_states=init_states + init_states=init_states, + reset_options=reset_options ) finally: self._clean_up() From 2f9d8461c1ced2fbcf5433bbce7838fd1d2bde6d Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 13 Jun 2024 16:56:26 +0200 Subject: [PATCH 08/11] adding base tests for the reset options in the runner, need to test the init_state now --- CHANGELOG.rst | 2 + grid2op/Runner/aux_fun.py | 7 +- grid2op/Runner/runner.py | 32 +- grid2op/tests/test_reset_options_runner.py | 525 +++++++++++++++++++++ 4 files changed, 552 insertions(+), 14 deletions(-) create mode 100644 grid2op/tests/test_reset_options_runner.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a34991c5..f6e8f8f2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -45,6 +45,8 @@ Change Log - [BREAKING] for all the `Handler` (*eg* `CSVForecastHandler`) the method `set_max_iter` is now private (for the same reason as the `env.chronics_handler`). We do not recommend to use it (will likely have no effect). Prefer using `env.set_max_iter` instead. +- [BREAKING] now the `runner.run()` method only accept kwargs argument + (because it should always have been like this) - [FIXED] a bug in the `MultiFolder` and `MultifolderWithCache` leading to the wrong computation of `max_iter` on some corner cases - [ADDED] possibility to skip some step when calling `env.reset(..., options={"init ts": ...})` diff --git a/grid2op/Runner/aux_fun.py b/grid2op/Runner/aux_fun.py index e103e3b8..22c38527 100644 --- a/grid2op/Runner/aux_fun.py +++ b/grid2op/Runner/aux_fun.py @@ -8,7 +8,7 @@ import copy import time - +import warnings import numpy as np from grid2op.Environment import Environment @@ -51,11 +51,6 @@ def _aux_one_process_parrallel( reset_options=None, ): """this is out of the runner, otherwise it does not work on windows / macos""" - # chronics_handler = ChronicsHandler( - # chronicsClass=runner.gridStateclass, - # path=runner.path_chron, - # **runner.gridStateclass_kwargs - # ) parameters = copy.deepcopy(runner.parameters) nb_episode_this_process = len(episode_this_process) res = [(None, None, None) for _ in range(nb_episode_this_process)] diff --git a/grid2op/Runner/runner.py b/grid2op/Runner/runner.py index 1ec06c4c..543b5d77 100644 --- a/grid2op/Runner/runner.py +++ b/grid2op/Runner/runner.py @@ -29,18 +29,20 @@ from grid2op.dtypes import dt_float from grid2op.Opponent import BaseOpponent, NeverAttackBudget from grid2op.operator_attention import LinearAttentionBudget +from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB +from grid2op.Episode import EpisodeData +# on windows if i start using sequential, i need to continue using sequential +# if i start using parallel i need to continue using parallel +# so i force the usage of the "starmap" stuff even if there is one process on windows +from grid2op._glop_platform_info import _IS_WINDOWS, _IS_LINUX, _IS_MACOS + from grid2op.Runner.aux_fun import ( _aux_run_one_episode, _aux_make_progress_bar, _aux_one_process_parrallel, ) from grid2op.Runner.basic_logger import DoNothingLog, ConsoleLog -from grid2op.Episode import EpisodeData -# on windows if i start using sequential, i need to continue using sequential -# if i start using parallel i need to continue using parallel -# so i force the usage of the "starmap" stuff even if there is one process on windows -from grid2op._glop_platform_info import _IS_WINDOWS, _IS_LINUX, _IS_MACOS runner_returned_type = Union[Tuple[str, str, float, int, int], Tuple[str, str, float, int, int, EpisodeData], @@ -57,7 +59,6 @@ KEY_TIME_SERIE_ID = "time serie id" - class Runner(object): """ A runner is a utility tool that allows to run simulations more easily. @@ -246,7 +247,7 @@ def __init__( init_env_path: str, init_grid_path: str, path_chron, # path where chronics of injections are stored - n_busbar=2, + n_busbar=DEFAULT_N_BUSBAR_PER_SUB, name_env="unknown", parameters_path=None, names_chronics_to_backend=None, @@ -918,6 +919,7 @@ def _run_sequential( ) = self.run_one_episode( path_save=path_save, indx=ep_id, + episode_id=ep_id, pbar=next_pbar[0], env_seed=env_seed, agent_seed=agt_seed, @@ -1184,6 +1186,7 @@ def _clean_up(self): def run( self, nb_episode, + *, # force kwargs nb_process=1, path_save=None, max_iter=None, @@ -1452,7 +1455,13 @@ def run( "either use dictionnary, grid2op actions or list / tuple of actions.") if reset_options is not None: - if isinstance(reset_options, (dict)): + if isinstance(reset_options, dict): + for k in reset_options: + if not k in self.envClass.KEYS_RESET_OPTIONS: + raise RuntimeError("Wehn specifying `reset options` all keys of the dictionary should " + "be compatible with the available reset options of your environment " + f"class. You provided the key \"{k}\" for the provided dictionary but" + f"possible keys are limited to {self.envClass.KEYS_RESET_OPTIONS}.") # user provided one initial state, I copy it to all # evaluation reset_options = [reset_options.copy() for _ in range(nb_episode)] @@ -1469,6 +1478,13 @@ def run( raise RuntimeError("When specifying `reset_options` kwargs with a list (or a tuple) " "it should be a list (or a tuple) of dictionary or BaseAction. " f"You provided {type(el)} at position {i}.") + for i, el in enumerate(reset_options): + for k in el: + if not k in self.envClass.KEYS_RESET_OPTIONS: + raise RuntimeError("Wehn specifying `reset options` all keys of the dictionary should " + "be compatible with the available reset options of your environment " + f"class. You provided the key \"{k}\" for the {i}th dictionary but" + f"possible keys are limited to {self.envClass.KEYS_RESET_OPTIONS}.") else: raise RuntimeError("When using `reset_options` in the runner, you should make sure to use " "either use dictionnary, grid2op actions or list / tuple of actions.") diff --git a/grid2op/tests/test_reset_options_runner.py b/grid2op/tests/test_reset_options_runner.py new file mode 100644 index 00000000..6aaf6821 --- /dev/null +++ b/grid2op/tests/test_reset_options_runner.py @@ -0,0 +1,525 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + + +import warnings +import unittest + +import grid2op +from grid2op.Runner import Runner +from grid2op.tests.helper_path_test import * + + +class TestResetOptionRunner(unittest.TestCase): + def _env_path(self): + return "l2rpn_case14_sandbox" + + def setUp(self) -> None: + self.env_nm = self._env_path() + self.max_iter = 5 + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make(self.env_nm, + test=True + ) + self.runner = Runner(**self.env.get_params_for_runner()) + + def tearDown(self) -> None: + self.env.close() + self.runner._clean_up() + return super().tearDown() + + def test_run_one_episode_ts_id(self): + with warnings.catch_warnings(): + warnings.filterwarnings("error") # check it does not raise any error + res = self.runner.run_one_episode(reset_options={"time serie id": 1}, + max_iter=self.max_iter, + detailed_output=True + ) + assert res[1]== '0001' + assert res[3] == self.max_iter + assert res[4] == self.max_iter + + with warnings.catch_warnings(): + warnings.filterwarnings("error") # check it does not raise any error + res = self.runner.run_one_episode(reset_options={}, + episode_id=1, + max_iter=self.max_iter, + detailed_output=True + ) + assert res[1]== '0001' + assert res[3] == self.max_iter + assert res[4] == self.max_iter + + # check the correct episode id is used + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + res = self.runner.run_one_episode(reset_options={"time serie id": 1}, + max_iter=self.max_iter, + episode_id=0, + detailed_output=True + ) + assert res[1]== '0000' + assert res[3] == self.max_iter + assert res[4] == self.max_iter + + def test_run_one_episode_warning_raised_ts_id(self): + # check it does raise an error + with self.assertRaises(UserWarning): + with warnings.catch_warnings(): + warnings.filterwarnings("error") + res = self.runner.run_one_episode(reset_options={"time serie id": 1}, + max_iter=self.max_iter, + episode_id=3, + detailed_output=True + ) + + def test_run_onesingle_ep_ts_id(self): + # one reset option + res = self.runner.run(nb_episode=1, + reset_options={"time serie id": 1}, + max_iter=self.max_iter + ) + assert res[0][1]== '0001' + assert res[0][3] == self.max_iter + assert res[0][4] == self.max_iter + + # one list (of one element here) + res = self.runner.run(nb_episode=1, + reset_options=[{"time serie id": 1}], + max_iter=self.max_iter + ) + assert res[0][1]== '0001' + assert res[0][3] == self.max_iter + assert res[0][4] == self.max_iter + + # one tuple (of one element here) + res = self.runner.run(nb_episode=1, + reset_options=({"time serie id": 1}, ), + max_iter=self.max_iter + ) + assert res[0][1]== '0001' + assert res[0][3] == self.max_iter + assert res[0][4] == self.max_iter + + # check the correct episode id is used + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + res = self.runner.run(nb_episode=1, + reset_options={"time serie id": 1}, + max_iter=self.max_iter, + episode_id=[0] + ) + assert res[0][1]== '0000' + assert res[0][3] == self.max_iter + assert res[0][4] == self.max_iter + + # check the warning is raised + with self.assertRaises(UserWarning): + with warnings.catch_warnings(): + warnings.filterwarnings("error") + res = self.runner.run(nb_episode=1, + reset_options={"time serie id": 1}, + max_iter=self.max_iter, + episode_id=[0] + ) + + def test_run_two_eps_seq_ts_id(self, nb_process=1): + # one reset option + res = self.runner.run(nb_episode=2, + reset_options={"time serie id": 1}, + max_iter=self.max_iter, + nb_process=nb_process + ) + for el in res: + assert el[1]== '0001' + assert el[3] == self.max_iter + assert el[4] == self.max_iter + + # one list (of one element here) + res = self.runner.run(nb_episode=2, + reset_options=[{"time serie id": 1}, {"time serie id": 1}], + max_iter=self.max_iter, + nb_process=nb_process + ) + for el in res: + assert el[1]== '0001' + assert el[3] == self.max_iter + assert el[4] == self.max_iter + + # one tuple (of one element here) + res = self.runner.run(nb_episode=2, + reset_options=({"time serie id": 1}, {"time serie id": 1}), + max_iter=self.max_iter, + nb_process=nb_process + ) + for el in res: + assert el[1]== '0001' + assert el[3] == self.max_iter + assert el[4] == self.max_iter + + # check the correct episode id is used + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + res = self.runner.run(nb_episode=2, + reset_options={"time serie id": 1}, + max_iter=self.max_iter, + episode_id=[0, 1], + nb_process=nb_process + ) + assert res[0][1]== '0000' + assert res[1][1]== '0001' + for el in res: + assert el[3] == self.max_iter + assert el[4] == self.max_iter + + # check the warning is raised + with self.assertRaises(UserWarning): + with warnings.catch_warnings(): + warnings.filterwarnings("error") + res = self.runner.run(nb_episode=2, + reset_options={"time serie id": 1}, + max_iter=self.max_iter, + episode_id=[0, 1], + nb_process=nb_process + ) + + def test_run_two_eps_seq_two_options_ts_id(self, nb_process=1): + # one list (of one element here) + res = self.runner.run(nb_episode=2, + reset_options=[{"time serie id": 0}, {"time serie id": 1}], + max_iter=self.max_iter, + nb_process=nb_process + ) + assert res[0][1]== '0000' + assert res[1][1]== '0001' + for el in res: + assert el[3] == self.max_iter + assert el[4] == self.max_iter + + # one tuple (of one element here) + res = self.runner.run(nb_episode=2, + reset_options=({"time serie id": 0}, {"time serie id": 1}), + max_iter=self.max_iter, + nb_process=nb_process + ) + assert res[0][1]== '0000' + assert res[1][1]== '0001' + for el in res: + assert el[3] == self.max_iter + assert el[4] == self.max_iter + + # check the correct episode id is used + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + res = self.runner.run(nb_episode=2, + reset_options={"time serie id": 1}, + max_iter=self.max_iter, + episode_id=[0, 1], + nb_process=nb_process + ) + assert res[0][1]== '0000' + assert res[1][1]== '0001' + for el in res: + assert el[3] == self.max_iter + assert el[4] == self.max_iter + + # check the warning is raised + with self.assertRaises(UserWarning): + with warnings.catch_warnings(): + warnings.filterwarnings("error") + res = self.runner.run(nb_episode=2, + reset_options={"time serie id": 1}, + max_iter=self.max_iter, + episode_id=[0, 1], + nb_process=nb_process + ) + + def test_run_two_eps_par_ts_id(self): + self.test_run_two_eps_seq_ts_id(nb_process=2) + + def test_run_two_eps_par_two_opts_ts_id(self): + self.test_run_two_eps_seq_two_options_ts_id(nb_process=2) + + def test_fail_when_needed(self): + # wrong type + with self.assertRaises(RuntimeError): + res = self.runner.run(nb_episode=2, + reset_options=1, + episode_id=[0, 1], + max_iter=self.max_iter, + add_detailed_output=True, + ) + with self.assertRaises(RuntimeError): + res = self.runner.run(nb_episode=2, + reset_options=[1, {"time serie id": 1}], + episode_id=[0, 1], + max_iter=self.max_iter, + add_detailed_output=True, + ) + with self.assertRaises(RuntimeError): + res = self.runner.run(nb_episode=2, + reset_options=[{"time serie id": 1}, 1], + episode_id=[0, 1], + max_iter=self.max_iter, + add_detailed_output=True, + ) + + # wrong size (too big) + with self.assertRaises(RuntimeError): + res = self.runner.run(nb_episode=2, + reset_options=[{"time serie id": 1}, + {"time serie id": 1}, + {"time serie id": 1}], + episode_id=[0, 1], + max_iter=self.max_iter, + add_detailed_output=True, + ) + # wrong size (too small) + with self.assertRaises(RuntimeError): + res = self.runner.run(nb_episode=2, + reset_options=[{"time serie id": 1}], + episode_id=[0, 1], + max_iter=self.max_iter, + add_detailed_output=True, + ) + # wrong key (beginning) + with self.assertRaises(RuntimeError): + res = self.runner.run(nb_episode=2, + reset_options=[{"bleurk": 1}, {"time serie id": 1}], + episode_id=[0, 1], + max_iter=self.max_iter, + add_detailed_output=True, + ) + # wrong key (end) + with self.assertRaises(RuntimeError): + res = self.runner.run(nb_episode=2, + reset_options=[{"time serie id": 1}, {"bleurk": 1}], + episode_id=[0, 1], + max_iter=self.max_iter, + add_detailed_output=True, + ) + # wrong key (when alone) + with self.assertRaises(RuntimeError): + res = self.runner.run(nb_episode=2, + reset_options={"bleurk": 1}, + episode_id=[0, 1], + max_iter=self.max_iter, + add_detailed_output=True, + ) + + def test_run_one_episode_max_it(self): + with warnings.catch_warnings(): + warnings.filterwarnings("error") # check it does not raise any error + res = self.runner.run_one_episode(reset_options={"max step": self.max_iter, "time serie id": 1}, + detailed_output=True + ) + assert res[1]== '0001' + assert res[3] == self.max_iter + assert res[4] == self.max_iter + + with warnings.catch_warnings(): + warnings.filterwarnings("error") # check it does not raise any error + res = self.runner.run_one_episode(reset_options={"time serie id": 1}, + max_iter=self.max_iter, + detailed_output=True + ) + assert res[1]== '0001' + assert res[3] == self.max_iter + assert res[4] == self.max_iter + + # check the correct max iter is used + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + res = self.runner.run_one_episode(reset_options={"time serie id": 1, "max step": self.max_iter + 1}, + max_iter=self.max_iter, + episode_id=0, + detailed_output=True + ) + assert res[1]== '0000' + assert res[3] == self.max_iter + assert res[4] == self.max_iter + + def test_run_one_episode_warning_raised_max_it(self): + # check it does raise an error + with self.assertRaises(UserWarning): + with warnings.catch_warnings(): + warnings.filterwarnings("error") + res = self.runner.run_one_episode(reset_options={"time serie id": 1, "max step": self.max_iter + 3}, + max_iter=self.max_iter + ) + + def test_run_onesingle_ep_max_it(self): + # one reset option + res = self.runner.run(nb_episode=1, + reset_options={"time serie id": 1, "max step": self.max_iter}, + ) + assert res[0][1]== '0001' + assert res[0][3] == self.max_iter + assert res[0][4] == self.max_iter + + # one list (of one element here) + res = self.runner.run(nb_episode=1, + reset_options=[{"time serie id": 1, "max step": self.max_iter}], + ) + assert res[0][1]== '0001' + assert res[0][3] == self.max_iter + assert res[0][4] == self.max_iter + + # one tuple (of one element here) + res = self.runner.run(nb_episode=1, + reset_options=({"time serie id": 1, "max step": self.max_iter}, ), + ) + assert res[0][1]== '0001' + assert res[0][3] == self.max_iter + assert res[0][4] == self.max_iter + + # check the correct episode id is used + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + res = self.runner.run(nb_episode=1, + reset_options={"time serie id": 0, "max step": self.max_iter + 3}, + max_iter=self.max_iter, + ) + assert res[0][1]== '0000' + assert res[0][3] == self.max_iter + assert res[0][4] == self.max_iter + + # check the warning is raised + with self.assertRaises(UserWarning): + with warnings.catch_warnings(): + warnings.filterwarnings("error") + res = self.runner.run(nb_episode=1, + reset_options={"time serie id": 0, "max step": self.max_iter + 3}, + max_iter=self.max_iter + ) + + def test_run_two_eps_seq_max_it(self, nb_process=1): + # one reset option + res = self.runner.run(nb_episode=2, + reset_options={"time serie id": 1, "max step": self.max_iter }, + nb_process=nb_process + ) + for el in res: + assert el[1]== '0001' + assert el[3] == self.max_iter + assert el[4] == self.max_iter + + # one list (of the same element here) + res = self.runner.run(nb_episode=2, + reset_options=[{"time serie id": 1, "max step": self.max_iter}, + {"time serie id": 1, "max step": self.max_iter}], + nb_process=nb_process + ) + for el in res: + assert el[1]== '0001' + assert el[3] == self.max_iter + assert el[4] == self.max_iter + + # one tuple (of the same element here) + res = self.runner.run(nb_episode=2, + reset_options=({"time serie id": 1, "max step": self.max_iter}, + {"time serie id": 1, "max step": self.max_iter}), + nb_process=nb_process + ) + for el in res: + assert el[1]== '0001' + assert el[3] == self.max_iter + assert el[4] == self.max_iter + + # check the correct "max iter" is used + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + res = self.runner.run(nb_episode=2, + reset_options={"max step": self.max_iter + 3}, + max_iter=self.max_iter, + episode_id=[0, 1], + nb_process=nb_process + ) + assert res[0][1]== '0000' + assert res[1][1]== '0001' + for el in res: + assert el[3] == self.max_iter + assert el[4] == self.max_iter + + # check the warning is raised + with self.assertRaises(UserWarning): + with warnings.catch_warnings(): + warnings.filterwarnings("error") + res = self.runner.run(nb_episode=2, + reset_options={"max step": self.max_iter + 3}, + max_iter=self.max_iter, + episode_id=[0, 1], + nb_process=nb_process + ) + + def test_run_two_eps_seq_two_options_max_it(self, nb_process=1): + # one list (of two different elements here) + res = self.runner.run(nb_episode=2, + reset_options=[{"time serie id": 0, "max step": self.max_iter + 1}, + {"time serie id": 1, "max step": self.max_iter + 2}], + nb_process=nb_process + ) + assert res[0][1]== '0000' + assert res[0][3] == self.max_iter + 1 + assert res[0][4] == self.max_iter + 1 + assert res[1][1]== '0001' + assert res[1][3] == self.max_iter + 2 + assert res[1][4] == self.max_iter + 2 + + # one tuple (of two different elements here) + res = self.runner.run(nb_episode=2, + reset_options=({"time serie id": 0, "max step": self.max_iter + 1}, + {"time serie id": 1, "max step": self.max_iter + 2}), + nb_process=nb_process + ) + assert res[0][1]== '0000' + assert res[0][3] == self.max_iter + 1 + assert res[0][4] == self.max_iter + 1 + assert res[1][1]== '0001' + assert res[1][3] == self.max_iter + 2 + assert res[1][4] == self.max_iter + 2 + + # check the correct max iter is used + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + res = self.runner.run(nb_episode=2, + reset_options={"max step": self.max_iter + 1}, + max_iter=self.max_iter, + episode_id=[0, 1], + nb_process=nb_process + ) + assert res[0][1]== '0000' + assert res[1][1]== '0001' + for el in res: + assert el[3] == self.max_iter + assert el[4] == self.max_iter + + # check the warning is raised + with self.assertRaises(UserWarning): + with warnings.catch_warnings(): + warnings.filterwarnings("error") + res = self.runner.run(nb_episode=2, + reset_options={"max step": self.max_iter + 1}, + max_iter=self.max_iter, + episode_id=[0, 1], + nb_process=nb_process + ) + + def test_run_two_eps_par_max_it(self): + self.test_run_two_eps_seq_max_it(nb_process=2) + + def test_run_two_eps_par_two_opts_max_it(self): + self.test_run_two_eps_seq_two_options_max_it(nb_process=2) + + + ##################### + + +if __name__ == "__main__": + unittest.main() From 8a38327dedfe44d61d706a34b3664f8fb21381fc Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 14 Jun 2024 09:46:16 +0200 Subject: [PATCH 09/11] completeting the tests for the reset_options in the runner --- grid2op/tests/test_reset_options_runner.py | 387 ++++++++++++++++++++- 1 file changed, 386 insertions(+), 1 deletion(-) diff --git a/grid2op/tests/test_reset_options_runner.py b/grid2op/tests/test_reset_options_runner.py index 6aaf6821..94da9ada 100644 --- a/grid2op/tests/test_reset_options_runner.py +++ b/grid2op/tests/test_reset_options_runner.py @@ -517,8 +517,393 @@ def test_run_two_eps_par_max_it(self): def test_run_two_eps_par_two_opts_max_it(self): self.test_run_two_eps_seq_two_options_max_it(nb_process=2) + def test_run_one_episode_init_act(self): + with warnings.catch_warnings(): + warnings.filterwarnings("error") # check it does not raise any error + res = self.runner.run_one_episode(reset_options={"max step": self.max_iter, "time serie id": 1, + "init state": {"set_line_status": [(1, -1)], "method": "ignore"}}, + detailed_output=True + ) + assert res[1]== '0001' + assert res[3] == self.max_iter + assert res[4] == self.max_iter + + ep_data = res[-1] + init_obs = ep_data.observations[0] + assert init_obs.topo_vect[init_obs.line_or_pos_topo_vect[1]] == -1 + assert init_obs.topo_vect[init_obs.line_ex_pos_topo_vect[1]] == -1 + assert not init_obs.line_status[1] + + with warnings.catch_warnings(): + warnings.filterwarnings("error") # check it does not raise any error + res = self.runner.run_one_episode(reset_options={"time serie id": 1}, + max_iter=self.max_iter, + init_state={"set_line_status": [(1, -1)], "method": "ignore"}, + detailed_output=True + ) + assert res[1]== '0001' + assert res[3] == self.max_iter + assert res[4] == self.max_iter + + ep_data = res[-1] + init_obs = ep_data.observations[0] + assert init_obs.topo_vect[init_obs.line_or_pos_topo_vect[1]] == -1 + assert init_obs.topo_vect[init_obs.line_ex_pos_topo_vect[1]] == -1 + assert not init_obs.line_status[1] + + # check the correct init state is used + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + res = self.runner.run_one_episode(reset_options={"time serie id": 1, + "max step": self.max_iter + 1, + "init state": {"set_line_status": [(0, -1)], "method": "ignore"} + }, + max_iter=self.max_iter, + episode_id=0, + init_state={"set_line_status": [(1, -1)], "method": "ignore"}, + detailed_output=True + ) + assert res[1]== '0000' + assert res[3] == self.max_iter + assert res[4] == self.max_iter + + ep_data = res[-1] + init_obs = ep_data.observations[0] + # line 1 is disco + assert init_obs.topo_vect[init_obs.line_or_pos_topo_vect[1]] == -1 + assert init_obs.topo_vect[init_obs.line_ex_pos_topo_vect[1]] == -1 + assert not init_obs.line_status[1] + # line 0 should not + assert init_obs.topo_vect[init_obs.line_or_pos_topo_vect[0]] == 1 + assert init_obs.topo_vect[init_obs.line_ex_pos_topo_vect[0]] == 1 + assert init_obs.line_status[0] + + def test_run_one_episode_warning_raised_init_act(self): + # check it does raise an error + with self.assertRaises(UserWarning): + with warnings.catch_warnings(): + warnings.filterwarnings("error") + res = self.runner.run_one_episode(reset_options={"time serie id": 1, + "max step": self.max_iter + 3, + "init state": {"set_line_status": [(0, -1)], "method": "ignore"}}, + init_state={"set_line_status": [(1, -1)], "method": "ignore"}, + ) + + def test_run_onesingle_ep_init_act(self): + # one reset option + res = self.runner.run(nb_episode=1, + reset_options={"time serie id": 1, + "max step": self.max_iter, + "init state": {"set_line_status": [(0, -1)], "method": "ignore"} + }, + add_detailed_output=True + ) + assert res[0][1]== '0001' + assert res[0][3] == self.max_iter + assert res[0][4] == self.max_iter + ep_data = res[0][-1] + init_obs = ep_data.observations[0] + assert init_obs.topo_vect[init_obs.line_or_pos_topo_vect[0]] == -1 + assert init_obs.topo_vect[init_obs.line_ex_pos_topo_vect[0]] == -1 + assert not init_obs.line_status[0] + + # one list (of one element here) + res = self.runner.run(nb_episode=1, + reset_options=[{"time serie id": 1, + "max step": self.max_iter, + "init state": {"set_line_status": [(0, -1)], "method": "ignore"} + }], + add_detailed_output=True + ) + assert res[0][1]== '0001' + assert res[0][3] == self.max_iter + assert res[0][4] == self.max_iter + ep_data = res[0][-1] + init_obs = ep_data.observations[0] + assert init_obs.topo_vect[init_obs.line_or_pos_topo_vect[0]] == -1 + assert init_obs.topo_vect[init_obs.line_ex_pos_topo_vect[0]] == -1 + assert not init_obs.line_status[0] + + # one tuple (of one element here) + res = self.runner.run(nb_episode=1, + reset_options=({"time serie id": 1, + "max step": self.max_iter, + "init state": {"set_line_status": [(0, -1)], "method": "ignore"} + }, ), + add_detailed_output=True + ) + assert res[0][1]== '0001' + assert res[0][3] == self.max_iter + assert res[0][4] == self.max_iter + ep_data = res[0][-1] + init_obs = ep_data.observations[0] + assert init_obs.topo_vect[init_obs.line_or_pos_topo_vect[0]] == -1 + assert init_obs.topo_vect[init_obs.line_ex_pos_topo_vect[0]] == -1 + assert not init_obs.line_status[0] + + # check the correct init action is used + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + res = self.runner.run(nb_episode=1, + reset_options={"time serie id": 0, + "max step": self.max_iter + 3, + "init state": {"set_line_status": [(0, -1)], "method": "ignore"}}, + max_iter=self.max_iter, + init_states={"set_line_status": [(1, -1)], "method": "ignore"}, + add_detailed_output=True + ) + assert res[0][1]== '0000' + assert res[0][3] == self.max_iter + assert res[0][4] == self.max_iter + ep_data = res[0][-1] + init_obs = ep_data.observations[0] + # line 1 is disco + assert init_obs.topo_vect[init_obs.line_or_pos_topo_vect[1]] == -1 + assert init_obs.topo_vect[init_obs.line_ex_pos_topo_vect[1]] == -1 + assert not init_obs.line_status[1] + # line 0 should not + assert init_obs.topo_vect[init_obs.line_or_pos_topo_vect[0]] == 1 + assert init_obs.topo_vect[init_obs.line_ex_pos_topo_vect[0]] == 1 + assert init_obs.line_status[0] + + # check the warning is raised + with self.assertRaises(UserWarning): + with warnings.catch_warnings(): + warnings.filterwarnings("error") + res = self.runner.run(nb_episode=1, + reset_options={"time serie id": 0, + "init state": {"set_line_status": [(0, -1)], "method": "ignore"}}, + max_iter=self.max_iter, + init_states={"set_line_status": [(1, -1)], "method": "ignore"}, + add_detailed_output=True + ) + + def test_run_two_eps_seq_init_act(self, nb_process=1): + # one reset option + res = self.runner.run(nb_episode=2, + reset_options={"time serie id": 1, + "max step": self.max_iter, + "init state": {"set_line_status": [(0, -1)], "method": "ignore"} + }, + nb_process=nb_process, + add_detailed_output=True + ) + for el in res: + assert el[1]== '0001' + assert el[3] == self.max_iter + assert el[4] == self.max_iter + ep_data = el[-1] + init_obs = ep_data.observations[0] + assert init_obs.topo_vect[init_obs.line_or_pos_topo_vect[0]] == -1 + assert init_obs.topo_vect[init_obs.line_ex_pos_topo_vect[0]] == -1 + assert not init_obs.line_status[0] + + # one list (of the same element here) + res = self.runner.run(nb_episode=2, + reset_options=[{"time serie id": 1, + "max step": self.max_iter, + "init state": {"set_line_status": [(0, -1)], "method": "ignore"}}, + {"time serie id": 1, + "max step": self.max_iter, + "init state": {"set_line_status": [(0, -1)], "method": "ignore"}}], + nb_process=nb_process, + add_detailed_output=True + ) + for el in res: + assert el[1]== '0001' + assert el[3] == self.max_iter + assert el[4] == self.max_iter + ep_data = el[-1] + init_obs = ep_data.observations[0] + assert init_obs.topo_vect[init_obs.line_or_pos_topo_vect[0]] == -1 + assert init_obs.topo_vect[init_obs.line_ex_pos_topo_vect[0]] == -1 + assert not init_obs.line_status[0] + + # one tuple (of the same element here) + res = self.runner.run(nb_episode=2, + reset_options=({"time serie id": 1, + "max step": self.max_iter, + "init state": {"set_line_status": [(0, -1)], "method": "ignore"}}, + {"time serie id": 1, + "max step": self.max_iter, + "init state": {"set_line_status": [(0, -1)], "method": "ignore"}}), + nb_process=nb_process, + add_detailed_output=True + ) + for el in res: + assert el[1]== '0001' + assert el[3] == self.max_iter + assert el[4] == self.max_iter + ep_data = el[-1] + init_obs = ep_data.observations[0] + assert init_obs.topo_vect[init_obs.line_or_pos_topo_vect[0]] == -1 + assert init_obs.topo_vect[init_obs.line_ex_pos_topo_vect[0]] == -1 + assert not init_obs.line_status[0] + + # check the correct "init state" is used + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + res = self.runner.run(nb_episode=2, + reset_options={"init state": {"set_line_status": [(0, -1)], "method": "ignore"}}, + max_iter=self.max_iter, + episode_id=[0, 1], + nb_process=nb_process, + init_states={"set_line_status": [(1, -1)], "method": "ignore"}, + add_detailed_output=True + ) + assert res[0][1]== '0000' + assert res[1][1]== '0001' + for el in res: + assert el[3] == self.max_iter + assert el[4] == self.max_iter + ep_data = el[-1] + init_obs = ep_data.observations[0] + # line 1 is disco + assert init_obs.topo_vect[init_obs.line_or_pos_topo_vect[1]] == -1 + assert init_obs.topo_vect[init_obs.line_ex_pos_topo_vect[1]] == -1 + assert not init_obs.line_status[1] + # line 0 should not + assert init_obs.topo_vect[init_obs.line_or_pos_topo_vect[0]] == 1 + assert init_obs.topo_vect[init_obs.line_ex_pos_topo_vect[0]] == 1 + assert init_obs.line_status[0] + + # check the warning is raised + with self.assertRaises(UserWarning): + with warnings.catch_warnings(): + warnings.filterwarnings("error") + res = self.runner.run(nb_episode=2, + reset_options={"init state": {"set_line_status": [(0, -1)], "method": "ignore"}}, + max_iter=self.max_iter, + episode_id=[0, 1], + nb_process=nb_process, + init_states={"set_line_status": [(1, -1)], "method": "ignore"}, + add_detailed_output=True + ) + + def test_run_two_eps_seq_two_options_init_act(self, nb_process=1): + # one list (of two different elements here) + res = self.runner.run(nb_episode=2, + reset_options=[{"time serie id": 0, + "max step": self.max_iter + 1, + "init state": {"set_line_status": [(0, -1)], "method": "ignore"}}, + {"time serie id": 1, + "max step": self.max_iter + 2, + "init state": {"set_line_status": [(1, -1)], "method": "ignore"}}], + nb_process=nb_process, + add_detailed_output=True + ) + assert res[0][1]== '0000' + assert res[0][3] == self.max_iter + 1 + assert res[0][4] == self.max_iter + 1 + ep_data = res[0][-1] + init_obs = ep_data.observations[0] + # line 0 is disco + assert init_obs.topo_vect[init_obs.line_or_pos_topo_vect[0]] == -1 + assert init_obs.topo_vect[init_obs.line_ex_pos_topo_vect[0]] == -1 + assert not init_obs.line_status[0] + # line 1 should not + assert init_obs.topo_vect[init_obs.line_or_pos_topo_vect[1]] == 1 + assert init_obs.topo_vect[init_obs.line_ex_pos_topo_vect[1]] == 1 + assert init_obs.line_status[1] + + assert res[1][1]== '0001' + assert res[1][3] == self.max_iter + 2 + assert res[1][4] == self.max_iter + 2 + ep_data = res[1][-1] + init_obs = ep_data.observations[0] + # line 1 is disco + assert init_obs.topo_vect[init_obs.line_or_pos_topo_vect[1]] == -1 + assert init_obs.topo_vect[init_obs.line_ex_pos_topo_vect[1]] == -1 + assert not init_obs.line_status[1] + # line 0 should not + assert init_obs.topo_vect[init_obs.line_or_pos_topo_vect[0]] == 1 + assert init_obs.topo_vect[init_obs.line_ex_pos_topo_vect[0]] == 1 + assert init_obs.line_status[0] + + # one tuple (of two different elements here) + res = self.runner.run(nb_episode=2, + reset_options=({"time serie id": 0, + "max step": self.max_iter + 1, + "init state": {"set_line_status": [(0, -1)], "method": "ignore"}}, + {"time serie id": 1, + "max step": self.max_iter + 2, + "init state": {"set_line_status": [(1, -1)], "method": "ignore"}}), + nb_process=nb_process, + add_detailed_output=True + ) + assert res[0][1]== '0000' + assert res[0][3] == self.max_iter + 1 + assert res[0][4] == self.max_iter + 1 + ep_data = res[0][-1] + init_obs = ep_data.observations[0] + # line 0 is disco + assert init_obs.topo_vect[init_obs.line_or_pos_topo_vect[0]] == -1 + assert init_obs.topo_vect[init_obs.line_ex_pos_topo_vect[0]] == -1 + assert not init_obs.line_status[0] + # line 1 should not + assert init_obs.topo_vect[init_obs.line_or_pos_topo_vect[1]] == 1 + assert init_obs.topo_vect[init_obs.line_ex_pos_topo_vect[1]] == 1 + assert init_obs.line_status[1] + + assert res[1][1]== '0001' + assert res[1][3] == self.max_iter + 2 + assert res[1][4] == self.max_iter + 2 + ep_data = res[1][-1] + init_obs = ep_data.observations[0] + # line 1 is disco + assert init_obs.topo_vect[init_obs.line_or_pos_topo_vect[1]] == -1 + assert init_obs.topo_vect[init_obs.line_ex_pos_topo_vect[1]] == -1 + assert not init_obs.line_status[1] + # line 0 should not + assert init_obs.topo_vect[init_obs.line_or_pos_topo_vect[0]] == 1 + assert init_obs.topo_vect[init_obs.line_ex_pos_topo_vect[0]] == 1 + assert init_obs.line_status[0] + + # check the correct init state is used + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + res = self.runner.run(nb_episode=2, + reset_options={"init state": {"set_line_status": [(0, -1)], "method": "ignore"}}, + max_iter=self.max_iter, + episode_id=[0, 1], + nb_process=nb_process, + add_detailed_output=True, + init_states={"set_line_status": [(1, -1)], "method": "ignore"} + ) + assert res[0][1]== '0000' + assert res[1][1]== '0001' + for el in res: + assert el[3] == self.max_iter + assert el[4] == self.max_iter + ep_data = el[-1] + init_obs = ep_data.observations[0] + # line 1 is disco + assert init_obs.topo_vect[init_obs.line_or_pos_topo_vect[1]] == -1 + assert init_obs.topo_vect[init_obs.line_ex_pos_topo_vect[1]] == -1 + assert not init_obs.line_status[1] + # line 0 should not + assert init_obs.topo_vect[init_obs.line_or_pos_topo_vect[0]] == 1 + assert init_obs.topo_vect[init_obs.line_ex_pos_topo_vect[0]] == 1 + assert init_obs.line_status[0] + + # check the warning is raised + with self.assertRaises(UserWarning): + with warnings.catch_warnings(): + warnings.filterwarnings("error") + res = self.runner.run(nb_episode=2, + reset_options={"init state": {"set_line_status": [(0, -1)], "method": "ignore"}}, + max_iter=self.max_iter, + episode_id=[0, 1], + nb_process=nb_process, + add_detailed_output=True, + init_states={"set_line_status": [(1, -1)], "method": "ignore"} + ) + + def test_run_two_eps_par_init_act(self): + self.test_run_two_eps_seq_init_act(nb_process=2) - ##################### + def test_run_two_eps_par_two_opts_init_act(self): + self.test_run_two_eps_seq_two_options_init_act(nb_process=2) if __name__ == "__main__": From 79988ff43adbe5af42a601769e27d5c49fea0af8 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 14 Jun 2024 10:06:48 +0200 Subject: [PATCH 10/11] added docs and readme [skip ci] --- CHANGELOG.rst | 3 +- docs/conf.py | 2 +- grid2op/Runner/runner.py | 86 ++++++++++++++++++++++++++++++++++++++-- grid2op/__init__.py | 2 +- 4 files changed, 86 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f6e8f8f2..9df52dba 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -51,7 +51,8 @@ Change Log computation of `max_iter` on some corner cases - [ADDED] possibility to skip some step when calling `env.reset(..., options={"init ts": ...})` - [ADDED] possibility to limit the duration of an episode with `env.reset(..., options={"max step": ...})` - +- [ADDED] possibility to specify the "reset_options" used in `env.reset` when + using the runner with `runner.run(..., reset_options=xxx)` [1.10.2] - 2024-05-27 ------------------------- diff --git a/docs/conf.py b/docs/conf.py index d7583ca4..8d3d22dd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ author = 'Benjamin Donnot' # The full version, including alpha/beta/rc tags -release = '1.10.3.dev0' +release = '1.10.3.dev1' version = '1.10' diff --git a/grid2op/Runner/runner.py b/grid2op/Runner/runner.py index 543b5d77..85408241 100644 --- a/grid2op/Runner/runner.py +++ b/grid2op/Runner/runner.py @@ -1333,7 +1333,7 @@ def run( You can use the runner this way: - .. code-block: python + .. code-block:: python import grid2op from gri2op.Runner import Runner @@ -1345,7 +1345,7 @@ def run( If you would rather to provide an agent instance (and not a class) you can do it this way: - .. code-block: python + .. code-block:: python import grid2op from gri2op.Runner import Runner @@ -1361,7 +1361,7 @@ def run( by passing `env_seeds` and `agent_seeds` parameters (on the example bellow, the agent will be seeded with 42 and the environment with 0. - .. code-block: python + .. code-block:: python import grid2op from gri2op.Runner import Runner @@ -1375,7 +1375,7 @@ def run( Since grid2op 1.10.2 you can also set the initial state of the grid when calling the runner. You can do that with the kwargs `init_states`, for example like this: - .. code-block: python + .. code-block:: python import grid2op from gri2op.Runner import Runner @@ -1405,7 +1405,85 @@ def run( that you can control what exactly is done (set the `"method"`) more information about this on the doc of the :func:`grid2op.Environment.Environment.reset` function. + + Since grid2op 1.10.3 you can also customize the way the runner will "reset" the + environment with the kwargs `reset_options`. + + Concretely, if you specify `runner.run(..., reset_options=XXX)` then the environment + will be reset with a call to `env.reset(options=reset_options)`. + + As for the init states kwargs, reset_options can be either a dictionnary, in this + case the same dict will be used for running all the episode or a list / tuple + of dictionnaries with the same size as the `nb_episode` kwargs. + + .. code-block:: python + + import grid2op + from gri2op.Runner import Runner + from grid2op.Agent import RandomAgent + + env = grid2op.make("l2rpn_case14_sandbox") + my_agent = RandomAgent(env.action_space) + runner = Runner(**env.get_params_for_runner(), agentClass=None, agentInstance=my_agent) + res = runner.run(nb_episode=2, + agent_seeds=[42, 43], + env_seeds=[0, 1], + reset_options={"init state": {"set_line_status": [(0, -1)]}} + ) + # same initial state will be used for the two epusode + + res2 = runner.run(nb_episode=2, + agent_seeds=[42, 43], + env_seeds=[0, 1], + reset_options=[{"init state": {"set_line_status": [(0, -1)]}}, + {"init state": {"set_line_status": [(1, -1)]}}] + ) + # two different initial states will be used: the first one for the + # first episode and the second one for the second + + .. note:: + In case of conflicting inputs, for example when you specify: + + .. code-block:: python + + runner.run(..., + init_states=XXX, + reset_options={"init state"=YYY} + ) + + or + .. code-block:: python + + runner.run(..., + max_iter=XXX, + reset_options={"max step"=YYY} + ) + + or + + .. code-block:: python + + runner.run(..., + episode_id=XXX, + reset_options={"time serie id"=YYY} + ) + + Then: 1) a warning is issued to inform you that you might have + done something wrong and 2) the value in `XXX` above (*ie* the + value provided in the `runner.run` kwargs) is always used + instead of the value `YYY` (*ie* the value present in the + reset_options). + + In other words, the arguments of the `runner.run` have the + priority over the arguments passed to the `reset_options`. + + .. danger:: + If you provide the key "time serie id" in one of the `reset_options` + dictionary, we recommend + you do it for all `reset_options` otherwise you might not end up + computing the correct episodes. + """ if nb_episode < 0: raise RuntimeError("Impossible to run a negative number of scenarios.") diff --git a/grid2op/__init__.py b/grid2op/__init__.py index 365a1420..32bbc659 100644 --- a/grid2op/__init__.py +++ b/grid2op/__init__.py @@ -11,7 +11,7 @@ Grid2Op """ -__version__ = '1.10.3.dev0' +__version__ = '1.10.3.dev1' __all__ = [ "Action", From 7d5cfc8a05055b754b88bde9075e74d347d7db51 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 14 Jun 2024 10:19:25 +0200 Subject: [PATCH 11/11] comment things for sonarcloud [skip ci] --- CHANGELOG.rst | 7 +++++-- examples/backend_integration/Step5_modify_topology.py | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9df52dba..ecc3124c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -31,12 +31,15 @@ Change Log - [???] "asynch" multienv - [???] properly model interconnecting powerlines + +- TODO A number of max buses per sub +- TODO in the runner, save multiple times the same sceanrio + + [1.10.3] - 2024-xx-yy ------------------------- -- TODO A number of max buses per sub - TODO Automatic "experimental_read_from_local_dir" - TODO Notebook for stable baselines -- TODO reset options in the runner - [BREAKING] `env.chronics_hander.set_max_iter(xxx)` is now a private function. Use `env.set_max_iter(xxx)` or even better `env.reset(options={"max step": xxx})`. diff --git a/examples/backend_integration/Step5_modify_topology.py b/examples/backend_integration/Step5_modify_topology.py index 7cc99ff3..4e84a58e 100644 --- a/examples/backend_integration/Step5_modify_topology.py +++ b/examples/backend_integration/Step5_modify_topology.py @@ -207,8 +207,8 @@ def get_topo_vect(self) -> np.ndarray: local_topo = (1, 2, 1, 2, 1, 2) elif env_name == "l2rpn_wcci_2022_dev": raise RuntimeError("Storage units are not handled by the example backend, and there are some on the grid.") - sub_id = 3 - local_topo = (1, 2, 1, 2, 1) + # sub_id = 3 + # local_topo = (1, 2, 1, 2, 1) else: raise RuntimeError(f"Unknown grid2op environment name {env_name}") action = env.action_space({"set_bus": {"substations_id": [(sub_id, local_topo)]}})