From 100d1db87d574106d96036d5fc6c813b5c2ade58 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Mon, 27 May 2024 10:26:30 +0200 Subject: [PATCH 1/2] adding parameters to deactivate initial state reading from the time series --- CHANGELOG.rst | 10 ++- grid2op/Chronics/gridValue.py | 5 ++ grid2op/Environment/environment.py | 13 ++- grid2op/Parameters.py | 76 ++++++++++++----- grid2op/tests/test_action_set_orig_state.py | 94 +++++++++++++++++++++ 5 files changed, 173 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 621adc96..6d4271a1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -31,7 +31,7 @@ Change Log - [???] "asynch" multienv - [???] properly model interconnecting powerlines -[1.10.2] - 2024-xx-yy +[1.10.2] - 2024-05-27 ------------------------- - [BREAKING] the `runner.run_one_episode` now returns an extra first argument: `chron_id, chron_name, cum_reward, timestep, max_ts = runner.run_one_episode()` which @@ -39,6 +39,9 @@ Change Log `chron_name, cum_reward, timestep, max_ts = runner.run_one_episode()`) - [BREAKING] the runner now has no `chronics_handler` attribute (`runner.chronics_handler` is not defined) +- [BREAKING] now grid2op forces everything to be connected at busbar 1 if + `param.IGNORE_INITIAL_STATE_TIME_SERIE == True` (**NOT** the default) and + no initial state is provided in `env.reset(..., options={"init state": ...})` - [ADDED] it is now possible to call `change_reward` directly from an observation (no need to do it from the Observation Space) - [ADDED] method to change the reward from the observation (observation_space @@ -55,6 +58,11 @@ Change Log - [ADDED] some more type hints in the `GridObject` class - [ADDED] Possibility to deactive the support of shunts if subclassing `PandaPowerBackend` (and add some basic tests) +- [ADDED] a parameters (`param.IGNORE_INITIAL_STATE_TIME_SERIE`) which defaults to + `False` that tells the environment whether it should ignore the + initial state of the grid provided in the time series. + By default it is NOT ignored, it is taken into account + (for the environment that supports this feature) - [FIXED] a small issue that could lead to having "redispatching_unit_commitment_availble" flag set even if the redispatching data was not loaded correctly diff --git a/grid2op/Chronics/gridValue.py b/grid2op/Chronics/gridValue.py index 0cc8d1c0..e49c6bb5 100644 --- a/grid2op/Chronics/gridValue.py +++ b/grid2op/Chronics/gridValue.py @@ -833,6 +833,11 @@ def get_init_action(self, names_chronics_to_backend: Dict[Literal["loads", "prod For later version, we let the possibility to set, in the "time series folder" (or time series generators) the possibility to change the initial condition of the grid. + Notes + ----- + If the environment parameters :attr:`grid2op.Parameters.Parameters.IGNORE_INITIAL_STATE_TIME_SERIE` + is set to `True` (not its default value) then this is ignored. + Returns ------- grid2op.Action.playableAction.PlayableAction diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 033d75b6..113b2048 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -834,11 +834,22 @@ def reset_grid(self, self._backend_action = self._backend_action_class() self.nb_time_step = -1 # to have init obs at step 1 (and to prevent 'setting to proper state' "action" to be illegal) - init_action : BaseAction = self.chronics_handler.get_init_action(self._names_chronics_to_backend) + init_action = None + if not self._parameters.IGNORE_INITIAL_STATE_TIME_SERIE: + # load the initial state from the time series (default) + # TODO logger: log that + init_action : BaseAction = self.chronics_handler.get_init_action(self._names_chronics_to_backend) + else: + # do as if everything was connected to busbar 1 + # TODO logger: log that + init_action = self._helper_action_env({"set_bus": np.ones(type(self).dim_topo, dtype=dt_int)}) + if type(self).shunts_data_available: + init_action += self._helper_action_env({"shunt": {"set_bus": np.ones(type(self).n_shunt, dtype=dt_int)}}) if init_action is None: # default behaviour for grid2op < 1.10.2 init_action = self._helper_action_env({}) else: + # remove the "change part" of the action init_action.remove_change() if init_act_opt is not None: diff --git a/grid2op/Parameters.py b/grid2op/Parameters.py index c16d9a93..c5ec67b2 100644 --- a/grid2op/Parameters.py +++ b/grid2op/Parameters.py @@ -148,6 +148,19 @@ class Parameters: MAX_SIMULATE_PER_EPISODE: ``int`` Maximum number of calls to `obs.simuate(...)` allowed per episode (reset each "env.simulate(...)"). Defaults to -1 meaning "as much as you want". + IGNORE_INITIAL_STATE_TIME_SERIE: ``bool`` + If set to True (which is NOT the default), then the initial state of the grid + will always be "everything connected" and "everything connected to busbar 1" + regardless of the information present in the time series (see + :func:`grid2op.Chronics.GridValue.get_init_action`) + + .. versionadded:: 1.10.2 + + .. note:: + This flag has no impact if an initial state is set through a call to + `env.reset(options={"init state": ...})` (see doc of :func:`grid2op.Environment.Environment.reset` + for more information) + """ def __init__(self, parameters_path=None): @@ -227,6 +240,8 @@ def __init__(self, parameters_path=None): else: warn_msg = "Parameters: the file {} is not found. Continuing with default parameters." warnings.warn(warn_msg.format(parameters_path)) + + self.IGNORE_INITIAL_STATE_TIME_SERIE = False @staticmethod def _isok_txt(arg): @@ -368,6 +383,11 @@ def init_from_dict(self, dict_): if "MAX_SIMULATE_PER_EPISODE" in dict_: self.MAX_SIMULATE_PER_EPISODE = dt_int(dict_["MAX_SIMULATE_PER_EPISODE"]) + if "IGNORE_INITIAL_STATE_TIME_SERIE" in dict_: + self.IGNORE_INITIAL_STATE_TIME_SERIE = Parameters._isok_txt( + dict_["IGNORE_INITIAL_STATE_TIME_SERIE"] + ) + authorized_keys = set(self.__dict__.keys()) authorized_keys = authorized_keys | { "NB_TIMESTEP_POWERFLOW_ALLOWED", @@ -416,6 +436,7 @@ def to_dict(self): res["ALERT_TIME_WINDOW"] = int(self.ALERT_TIME_WINDOW) res["MAX_SIMULATE_PER_STEP"] = int(self.MAX_SIMULATE_PER_STEP) res["MAX_SIMULATE_PER_EPISODE"] = int(self.MAX_SIMULATE_PER_EPISODE) + res["IGNORE_INITIAL_STATE_TIME_SERIE"] = int(self.IGNORE_INITIAL_STATE_TIME_SERIE) return res def init_from_json(self, json_path): @@ -470,8 +491,10 @@ def check_valid(self): Raises ------- - An exception if the parameter is not valid + An exception (`RuntimeError`) if the parameter is not valid + """ + try: if not isinstance(self.NO_OVERFLOW_DISCONNECTION, (bool, dt_bool)): raise RuntimeError("NO_OVERFLOW_DISCONNECTION should be a boolean") @@ -479,7 +502,7 @@ def check_valid(self): except Exception as exc_: raise RuntimeError( f'Impossible to convert NO_OVERFLOW_DISCONNECTION to bool with error \n:"{exc_}"' - ) + ) from exc_ try: self.NB_TIMESTEP_OVERFLOW_ALLOWED = int( @@ -491,7 +514,7 @@ def check_valid(self): except Exception as exc_: raise RuntimeError( f'Impossible to convert NB_TIMESTEP_OVERFLOW_ALLOWED to int with error \n:"{exc_}"' - ) + ) from exc_ if self.NB_TIMESTEP_OVERFLOW_ALLOWED < 0: raise RuntimeError( @@ -505,7 +528,7 @@ def check_valid(self): except Exception as exc_: raise RuntimeError( f'Impossible to convert NB_TIMESTEP_RECONNECTION to int with error \n:"{exc_}"' - ) + ) from exc_ if self.NB_TIMESTEP_RECONNECTION < 0: raise RuntimeError("NB_TIMESTEP_RECONNECTION < 0., this should be >= 0.") try: @@ -514,7 +537,7 @@ def check_valid(self): except Exception as exc_: raise RuntimeError( f'Impossible to convert NB_TIMESTEP_COOLDOWN_LINE to int with error \n:"{exc_}"' - ) + ) from exc_ if self.NB_TIMESTEP_COOLDOWN_LINE < 0: raise RuntimeError("NB_TIMESTEP_COOLDOWN_LINE < 0., this should be >= 0.") try: @@ -525,7 +548,7 @@ def check_valid(self): except Exception as exc_: raise RuntimeError( f'Impossible to convert NB_TIMESTEP_COOLDOWN_SUB to int with error \n:"{exc_}"' - ) + ) from exc_ if self.NB_TIMESTEP_COOLDOWN_SUB < 0: raise RuntimeError("NB_TIMESTEP_COOLDOWN_SUB < 0., this should be >= 0.") try: @@ -536,7 +559,7 @@ def check_valid(self): except Exception as exc_: raise RuntimeError( f'Impossible to convert HARD_OVERFLOW_THRESHOLD to float with error \n:"{exc_}"' - ) + ) from exc_ if self.HARD_OVERFLOW_THRESHOLD < 1.0: raise RuntimeError( "HARD_OVERFLOW_THRESHOLD < 1., this should be >= 1. (use env.set_thermal_limit " @@ -551,7 +574,7 @@ def check_valid(self): except Exception as exc_: raise RuntimeError( f'Impossible to convert SOFT_OVERFLOW_THRESHOLD to float with error \n:"{exc_}"' - ) + ) from exc_ if self.SOFT_OVERFLOW_THRESHOLD < 1.0: raise RuntimeError( "SOFT_OVERFLOW_THRESHOLD < 1., this should be >= 1. (use env.set_thermal_limit " @@ -570,14 +593,14 @@ def check_valid(self): except Exception as exc_: raise RuntimeError( f'Impossible to convert ENV_DC to bool with error \n:"{exc_}"' - ) + ) from exc_ try: self.MAX_SUB_CHANGED = int(self.MAX_SUB_CHANGED) # to raise if numpy array self.MAX_SUB_CHANGED = dt_int(self.MAX_SUB_CHANGED) except Exception as exc_: raise RuntimeError( f'Impossible to convert MAX_SUB_CHANGED to int with error \n:"{exc_}"' - ) + ) from exc_ if self.MAX_SUB_CHANGED < 0: raise RuntimeError( "MAX_SUB_CHANGED should be >=0 (or -1 if you want to be able to change every " @@ -591,7 +614,7 @@ def check_valid(self): except Exception as exc_: raise RuntimeError( f'Impossible to convert MAX_LINE_STATUS_CHANGED to int with error \n:"{exc_}"' - ) + ) from exc_ if self.MAX_LINE_STATUS_CHANGED < 0: raise RuntimeError( "MAX_LINE_STATUS_CHANGED should be >=0 " @@ -604,7 +627,7 @@ def check_valid(self): except Exception as exc_: raise RuntimeError( f'Impossible to convert IGNORE_MIN_UP_DOWN_TIME to bool with error \n:"{exc_}"' - ) + ) from exc_ try: if not isinstance(self.ALLOW_DISPATCH_GEN_SWITCH_OFF, (bool, dt_bool)): raise RuntimeError("ALLOW_DISPATCH_GEN_SWITCH_OFF should be a boolean") @@ -614,7 +637,7 @@ def check_valid(self): except Exception as exc_: raise RuntimeError( f'Impossible to convert ALLOW_DISPATCH_GEN_SWITCH_OFF to bool with error \n:"{exc_}"' - ) + ) from exc_ try: if not isinstance( self.LIMIT_INFEASIBLE_CURTAILMENT_STORAGE_ACTION, (bool, dt_bool) @@ -628,7 +651,7 @@ def check_valid(self): except Exception as exc_: raise RuntimeError( f'Impossible to convert LIMIT_INFEASIBLE_CURTAILMENT_STORAGE_ACTION to bool with error \n:"{exc_}"' - ) + ) from exc_ try: self.INIT_STORAGE_CAPACITY = float( @@ -638,16 +661,16 @@ def check_valid(self): except Exception as exc_: raise RuntimeError( f'Impossible to convert INIT_STORAGE_CAPACITY to float with error \n:"{exc_}"' - ) + ) from exc_ if self.INIT_STORAGE_CAPACITY < 0.0: raise RuntimeError( "INIT_STORAGE_CAPACITY < 0., this should be within range [0., 1.]" - ) + ) from exc_ if self.INIT_STORAGE_CAPACITY > 1.0: raise RuntimeError( "INIT_STORAGE_CAPACITY > 1., this should be within range [0., 1.]" - ) + ) from exc_ try: if not isinstance(self.ACTIVATE_STORAGE_LOSS, (bool, dt_bool)): @@ -656,26 +679,26 @@ def check_valid(self): except Exception as exc_: raise RuntimeError( f'Impossible to convert ACTIVATE_STORAGE_LOSS to bool with error \n:"{exc_}"' - ) + ) from exc_ try: self.ALARM_WINDOW_SIZE = dt_int(self.ALARM_WINDOW_SIZE) except Exception as exc_: raise RuntimeError( f'Impossible to convert ALARM_WINDOW_SIZE to int with error \n:"{exc_}"' - ) + ) from exc_ try: self.ALARM_BEST_TIME = dt_int(self.ALARM_BEST_TIME) except Exception as exc_: raise RuntimeError( f'Impossible to convert ALARM_BEST_TIME to int with error \n:"{exc_}"' - ) + ) from exc_ try: self.ALERT_TIME_WINDOW = dt_int(self.ALERT_TIME_WINDOW) except Exception as exc_: raise RuntimeError( f'Impossible to convert ALERT_TIME_WINDOW to int with error \n:"{exc_}"' - ) + ) from exc_ if self.ALARM_WINDOW_SIZE <= 0: raise RuntimeError("self.ALARM_WINDOW_SIZE should be a positive integer !") @@ -692,7 +715,7 @@ def check_valid(self): except Exception as exc_: raise RuntimeError( f'Impossible to convert MAX_SIMULATE_PER_STEP to int with error \n:"{exc_}"' - ) + ) from exc_ if self.MAX_SIMULATE_PER_STEP <= -2: raise RuntimeError( f"self.MAX_SIMULATE_PER_STEP should be a positive integer or -1, we found {self.MAX_SIMULATE_PER_STEP}" @@ -706,8 +729,15 @@ def check_valid(self): except Exception as exc_: raise RuntimeError( f'Impossible to convert MAX_SIMULATE_PER_EPISODE to int with error \n:"{exc_}"' - ) + ) from exc_ if self.MAX_SIMULATE_PER_EPISODE <= -2: raise RuntimeError( f"self.MAX_SIMULATE_PER_EPISODE should be a positive integer or -1, we found {self.MAX_SIMULATE_PER_EPISODE}" ) + + try: + self.IGNORE_INITIAL_STATE_TIME_SERIE = dt_bool(self.IGNORE_INITIAL_STATE_TIME_SERIE) + except Exception as exc_: + raise RuntimeError( + f'Impossible to convert IGNORE_INITIAL_STATE_TIME_SERIE to bool with error \n:"{exc_}"' + ) from exc_ diff --git a/grid2op/tests/test_action_set_orig_state.py b/grid2op/tests/test_action_set_orig_state.py index ee35f1ac..4f1be835 100644 --- a/grid2op/tests/test_action_set_orig_state.py +++ b/grid2op/tests/test_action_set_orig_state.py @@ -66,6 +66,9 @@ def _get_backend(self): def _get_gridpath(self): return None + def _get_envparams(self, env): + return None + def setUp(self) -> None: self.env_nm = self._env_path() tmp_path = self._get_gridpath() @@ -85,9 +88,14 @@ def setUp(self) -> None: with warnings.catch_warnings(): warnings.filterwarnings("ignore") self.env = grid2op.make(self.env_nm, **env_params) + env_params = self._get_envparams(self.env) + if env_params is not None: + self.env.change_parameters(env_params) + self.env.change_forecast_parameters(env_params) if issubclass(self._get_ch_cls(), MultifolderWithCache): self.env.chronics_handler.set_filter(lambda x: True) self.env.chronics_handler.reset() + self.env.reset(seed=0) # some test to make sure the tests are correct assert issubclass(self.env.action_space.subtype, self._get_act_cls()) assert isinstance(self.env.chronics_handler.real_data, self._get_ch_cls()) @@ -605,5 +613,91 @@ def test_set_storage_state(self): deltagen_p_th = ((obs_stor.gen_p - obs_stor.actual_dispatch) - obs_nostor.gen_p) assert (np.abs(deltagen_p_th[:slack_id]) <= 1e-6).all() + +class TestSetActOrigIgnoredParams(TestSetActOrigDefault): + """This class test that the new feature (setting the initial state in the time series + is properly ignored if the parameter says so)""" + + def _get_envparams(self, env): + param = env.parameters + param.IGNORE_INITIAL_STATE_TIME_SERIE = True + return param + + def test_working_setbus(self): + """test that it's ignored even if the action is set_status""" + self.obs = self._aux_reset_env(seed=0, ep_id=0) + assert self.obs.topo_vect[self.obs.line_or_pos_topo_vect[1]] == 1 + assert self.obs.topo_vect[self.obs.load_pos_topo_vect[0]] == 1 + assert (self.obs.time_before_cooldown_line == 0).all() + assert (self.obs.time_before_cooldown_sub == 0).all() + + obs, reward, done, info = self._aux_make_step() + assert not done + assert obs.topo_vect[self.obs.line_or_pos_topo_vect[1]] == 1 + assert obs.topo_vect[self.obs.load_pos_topo_vect[0]] == 1 + assert (obs.time_before_cooldown_line == 0).all() + assert (obs.time_before_cooldown_sub == 0).all() + # check the action in the time series folder is valid + self._aux_get_act_valid() + + def test_working_setstatus(self): + """test that it's ignored even if the action is set_status""" + self.obs = self._aux_reset_env(seed=0, ep_id=1) + + assert self.obs.topo_vect[self.obs.line_or_pos_topo_vect[1]] == 1 + assert self.obs.topo_vect[self.obs.line_ex_pos_topo_vect[1]] == 1 + assert self.obs.line_status[1] + assert (self.obs.time_before_cooldown_line == 0).all() + assert (self.obs.time_before_cooldown_sub == 0).all() + + obs, reward, done, info = self._aux_make_step() + assert not done + assert obs.topo_vect[self.obs.line_or_pos_topo_vect[1]] == 1 + assert obs.topo_vect[self.obs.line_ex_pos_topo_vect[1]] == 1 + assert obs.line_status[1] + assert (obs.time_before_cooldown_line == 0).all() + assert (obs.time_before_cooldown_sub == 0).all() + # check the action in the time series folder is valid + self._aux_get_act_valid() + + def test_rules_ok(self): + """that it's ignored even if the action is illegal""" + self.obs = self._aux_reset_env(seed=0, ep_id=2) + + assert self.obs.topo_vect[self.obs.line_or_pos_topo_vect[1]] == 1 + assert self.obs.topo_vect[self.obs.line_ex_pos_topo_vect[5]] == 1 + assert (self.obs.time_before_cooldown_line == 0).all() + assert (self.obs.time_before_cooldown_sub == 0).all() + act_init = self._aux_get_init_act() + if act_init is None: + # test not correct for multiprocessing, I stop here + return + obs, reward, done, info = self._aux_make_step(act_init) + assert info["exception"] is not None + assert info["is_illegal"] + assert obs.topo_vect[self.obs.line_or_pos_topo_vect[1]] == 1 + assert obs.topo_vect[self.obs.line_ex_pos_topo_vect[5]] == 1 + assert (obs.time_before_cooldown_line == 0).all() + assert (obs.time_before_cooldown_sub == 0).all() + # check the action in the time series folder is valid + self._aux_get_act_valid() + + def test_change_bus_ignored(self, catch_warning=True): + """test that if the action to set uses change_bus then nothing is done""" + # no warning in the main process in multiprocessing + self.obs = self._aux_reset_env(seed=0, ep_id=3) + assert self.obs.topo_vect[self.obs.line_or_pos_topo_vect[1]] == 1 + assert self.obs.topo_vect[self.obs.line_ex_pos_topo_vect[1]] == 1 + assert self.obs.topo_vect[self.obs.line_or_pos_topo_vect[2]] == 1 + assert self.obs.topo_vect[self.obs.line_ex_pos_topo_vect[2]] == 1 + assert self.obs.topo_vect[self.obs.line_or_pos_topo_vect[5]] == 1 + assert self.obs.topo_vect[self.obs.line_ex_pos_topo_vect[5]] == 1 + assert self.obs.line_status[1] == 1 + assert self.obs.line_status[2] == 1 + assert self.obs.line_status[5] == 1 + # check the action in the time series folder is valid + self._aux_get_act_valid() + + if __name__ == "__main__": unittest.main() From b109000e8bbbb2d05f9895bf508b7f20a5ed2ee2 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Mon, 27 May 2024 10:49:25 +0200 Subject: [PATCH 2/2] fix broken tests --- grid2op/tests/test_Observation.py | 1 + grid2op/tests/test_action_set_orig_state.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/grid2op/tests/test_Observation.py b/grid2op/tests/test_Observation.py index 2210ffe2..dff0b205 100644 --- a/grid2op/tests/test_Observation.py +++ b/grid2op/tests/test_Observation.py @@ -2975,6 +2975,7 @@ def setUp(self): "educ_case14_storage", test=True, action_class=PlayableAction, _add_to_name=type(self).__name__ ) + self.env.reset(seed=0, options={"time serie id": 0}) self.obs = self._make_forecast_perfect(self.env) self.sim_obs = None self.step_obs = None diff --git a/grid2op/tests/test_action_set_orig_state.py b/grid2op/tests/test_action_set_orig_state.py index 4f1be835..22832022 100644 --- a/grid2op/tests/test_action_set_orig_state.py +++ b/grid2op/tests/test_action_set_orig_state.py @@ -17,9 +17,10 @@ LS_AVAIL = False import grid2op -from grid2op.Environment import TimedOutEnvironment, MaskedEnvironment, SingleEnvMultiProcess +from grid2op.Environment import (TimedOutEnvironment, + MaskedEnvironment, + SingleEnvMultiProcess) from grid2op.Backend import PandaPowerBackend -from grid2op.Backend.educPandaPowerBackend import EducPandaPowerBackend from grid2op.Episode import EpisodeData from grid2op.Opponent import FromEpisodeDataOpponent from grid2op.Runner import Runner