diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 42ed401c..af984400 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -99,13 +99,20 @@ Native multi agents support: [1.11.0] - 202x-yy-zz ----------------------- +- [FIXED] issue https://github.com/Grid2op/grid2op/issues/657 +- [FIXED] missing an import on the `MaskedEnvironment` class - [ADDED] possibility to set the "thermal limits" when calling `env.reset(..., options={"thermal limit": xxx})` - [ADDED] possibility to retrieve some structural information about elements with with `gridobj.get_line_info(...)`, `gridobj.get_load_info(...)`, `gridobj.get_gen_info(...)` or , `gridobj.get_storage_info(...)` +- [ADDED] codacy badge on the readme - [IMPROVED] possibility to set the injections values with names to be consistent with other way to set the actions (*eg* set_bus) - [IMPROVED] error messages when creating an action which changes the injections +- [IMPROVED] (linked to https://github.com/Grid2op/grid2op/issues/657) the way the + "chronics_hander" in the ObsEnv behaves (it now fully implements the public interface of + a "real" chronic_handler) +- [IMPROVED] error message in the `FromNPY` class when the backend is checked [1.10.4] - 2024-10-15 ------------------------- diff --git a/README.md b/README.md index c1cddb23..32a4538b 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [![PyPi_Compat](https://img.shields.io/pypi/pyversions/grid2op.svg)](https://pypi.org/project/Grid2Op/) [![LICENSE](https://img.shields.io/pypi/l/grid2op.svg)](https://www.mozilla.org/en-US/MPL/2.0/) [![Documentation Status](https://readthedocs.org/projects/grid2op/badge/?version=latest)](https://grid2op.readthedocs.io/en/latest/?badge=latest) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/3a4e666ba20f4f20b9131e9a6081622c)](https://app.codacy.com/gh/Grid2op/grid2op/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![CircleCI](https://dl.circleci.com/status-badge/img/gh/Grid2op/grid2op/tree/master.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/Grid2op/grid2op/tree/master) [![discord](https://discord.com/api/guilds/698080905209577513/embed.png)](https://discord.gg/cYsYrPT) [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/grid2op/grid2op.git/master) diff --git a/grid2op/Chronics/_obs_fake_chronics_handler.py b/grid2op/Chronics/_obs_fake_chronics_handler.py new file mode 100644 index 00000000..79012011 --- /dev/null +++ b/grid2op/Chronics/_obs_fake_chronics_handler.py @@ -0,0 +1,256 @@ +# Copyright (c) 2019-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. + +from typing import Dict, Union, Literal + +import grid2op +from grid2op.Exceptions import EnvError, ChronicsError +from grid2op.Chronics import ChangeNothing + + +class _ObsCH(ChangeNothing): + """ + INTERNAL + + .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ + + This class is reserved to internal use. Do not attempt to do anything with it. + """ + + # properties that should not be accessed + @property + def chronicsClass(self): + raise EnvError("There are no time series in the observation from `obs.simulate`, so no `chronicsClass`") + + @property + def action_space(self): + raise EnvError("There are no time series in the observation from `obs.simulate`, so no `action_space`") + + @property + def path(self): + raise EnvError("There are no time series in the observation from `obs.simulate`, so no `path`") + + @property + def _real_data(self): + raise EnvError("There are no time series in the observation from `obs.simulate`, so no `_real_data`") + + @property + def kwargs(self): + return {} + + @kwargs.setter + def kwargs(self, new_value): + raise ChronicsError('Impossible to set the "kwargs" attribute') + + @property + def _kwargs(self): + return {} + + @property + def real_data(self): + raise EnvError("There are no time series in the observation from `obs.simulate`, so no `real_data`") + + # functions overriden from the ChronicsHandler class + def forecasts(self): + return [] + + def get_name(self): + return "" + + def next_time_step(self): + raise EnvError("There are no time series in the observation from `obs.simulate`, so no `next_time_step`") + + def max_episode_duration(self): + return 0 + + def seed(self, seed): + """.. warning:: This function is part of the public API of ChronicsHandler but should not do anything here""" + pass + + def cleanup_action_space(self): + """.. warning:: This function is part of the public API of ChronicsHandler but should not do anything here""" + pass + + # methods overriden from the ChronicsHandler class (__getattr__) so forwarded to the Chronics class + @property + def gridvalueClass(self): + raise EnvError("There are no time series in the observation from `obs.simulate`, so no `gridvalueClass`") + + @property + def data(self): + raise EnvError("There are no time series in the observation from `obs.simulate`, so no `data`") + + @property + def sep(self): + raise EnvError("There are no time series in the observation from `obs.simulate`, so no `sep`") + + @property + def subpaths(self): + raise EnvError("There are no time series in the observation from `obs.simulate`, so no `subpaths`") + + @property + def _order(self): + raise EnvError("There are no time series in the observation from `obs.simulate`, so no `_order`") + + @property + def chunk_size(self): + raise EnvError("There are no time series in the observation from `obs.simulate`, so no `chunk_size`") + + @property + def _order_backend_loads(self): + raise EnvError("There are no time series in the observation from `obs.simulate`, so no `_order_backend_loads`") + + @property + def _order_backend_prods(self): + raise EnvError("There are no time series in the observation from `obs.simulate`, so no `_order_backend_prods`") + + @property + def _order_backend_lines(self): + raise EnvError("There are no time series in the observation from `obs.simulate`, so no `_order_backend_lines`") + + @property + def _order_backend_subs(self): + raise EnvError("There are no time series in the observation from `obs.simulate`, so no `_order_backend_subs`") + + @property + def _names_chronics_to_backend(self): + raise EnvError("There are no time series in the observation from `obs.simulate`, so no `_names_chronics_to_backend`") + + @property + def _filter(self): + raise EnvError("There are no time series in the observation from `obs.simulate`, so no `_filter`") + + @property + def _prev_cache_id(self): + raise EnvError("There are no time series in the observation from `obs.simulate`, so no `_prev_cache_id`") + + def done(self): + return True + + def check_validity(self, backend): + return True + + def get_id(self) -> str: + return "" + + def shuffle(self, shuffler=None): + """ + .. warning:: + This function is part of the public API of ChronicsHandler, + by being accessible through the __getattr__ call that is + forwarded to the GridValue class + + It should not do anything here. + """ + pass + + def sample_next_chronics(self, probabilities=None): + """ + .. warning:: + This function is part of the public API of ChronicsHandler, + by being accessible through the __getattr__ call that is + forwarded to the GridValue class + + It should not do anything here. + """ + raise EnvError("There are no time series in the observation from `obs.simulate`, so no `sample_next_chronics`") + + def set_chunk_size(self, new_chunk_size): + """ + .. warning:: + This function is part of the public API of ChronicsHandler, + by being accessible through the __getattr__ call that is + forwarded to the GridValue class + + It should not do anything here. + """ + pass + + def init_datetime(self): + """ + .. warning:: + This function is part of the public API of ChronicsHandler, + by being accessible through the __getattr__ call that is + forwarded to the GridValue class + + It should not do anything here. + """ + pass + + def next_chronics(self): + """ + .. warning:: + This function is part of the public API of ChronicsHandler, + by being accessible through the __getattr__ call that is + forwarded to the GridValue class + + It should not do anything here. + """ + pass + + def tell_id(self, id_num, previous=False): + """ + .. warning:: + This function is part of the public API of ChronicsHandler, + by being accessible through the __getattr__ call that is + forwarded to the GridValue class + + It should not do anything here. + """ + pass + + def set_filter(self, filter_fun): + """ + .. warning:: + This function is part of the public API of ChronicsHandler, + by being accessible through the __getattr__ call that is + forwarded to the GridValue class + + It should not do anything here. + """ + pass + + def set_chunk_size(self, new_chunk_size): + """ + .. warning:: + This function is part of the public API of ChronicsHandler, + by being accessible through the __getattr__ call that is + forwarded to the GridValue class + + It should not do anything here. + """ + pass + + def fast_forward(self, nb_timestep): + """ + .. warning:: + This function is part of the public API of ChronicsHandler, + by being accessible through the __getattr__ call that is + forwarded to the GridValue class + + It should not do anything here. + """ + pass + + def get_init_action(self, names_chronics_to_backend: Dict[Literal["loads", "prods", "lines"], Dict[str, str]]) -> Union["grid2op.Action.playableAction.PlayableAction", None]: + raise EnvError("There are no time series in the observation from `obs.simulate`, so no `get_init_action`") + + def regenerate_with_new_seed(self): + """ + .. warning:: + This function is part of the public API of ChronicsHandler, + by being accessible through the __getattr__ call that is + forwarded to the GridValue class + + It should not do anything here. + """ + pass + + def max_timestep(self): + raise EnvError("There are no time series in the observation from `obs.simulate`, so no `max_timestep`") + \ No newline at end of file diff --git a/grid2op/Chronics/fromNPY.py b/grid2op/Chronics/fromNPY.py index 50d6e4c4..7ebb2ac6 100644 --- a/grid2op/Chronics/fromNPY.py +++ b/grid2op/Chronics/fromNPY.py @@ -232,12 +232,12 @@ def initialize( order_backend_subs, names_chronics_to_backend=None, ): - assert len(order_backend_prods) == self.n_gen - assert len(order_backend_loads) == self.n_load + assert len(order_backend_prods) == self.n_gen, f"len(order_backend_prods)={len(order_backend_prods)} vs self.n_gen={self.n_gen}" + assert len(order_backend_loads) == self.n_load, f"len(order_backend_loads)={len(order_backend_loads)} vs self.n_load={self.n_load}" if self.n_line is None: self.n_line = len(order_backend_lines) else: - assert len(order_backend_lines) == self.n_line + assert len(order_backend_lines) == self.n_line, f"len(order_backend_lines)={len(order_backend_lines)} vs self.n_line={self.n_line}" if self._forecasts is not None: self._forecasts.initialize( diff --git a/grid2op/Environment/_forecast_env.py b/grid2op/Environment/_forecast_env.py index 7378df7c..ab4d7056 100644 --- a/grid2op/Environment/_forecast_env.py +++ b/grid2op/Environment/_forecast_env.py @@ -7,6 +7,8 @@ # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. from typing import Tuple + +from grid2op.typing_variables import STEP_INFO_TYPING from grid2op.Action import BaseAction from grid2op.Observation import BaseObservation from grid2op.Environment.environment import Environment @@ -23,6 +25,6 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._do_not_erase_local_dir_cls = True - def step(self, action: BaseAction) -> Tuple[BaseObservation, float, bool, dict]: + def step(self, action: BaseAction) -> Tuple[BaseObservation, float, bool, STEP_INFO_TYPING]: self._highres_sim_counter += 1 return super().step(action) diff --git a/grid2op/Environment/_obsEnv.py b/grid2op/Environment/_obsEnv.py index 29abea03..2cf21404 100644 --- a/grid2op/Environment/_obsEnv.py +++ b/grid2op/Environment/_obsEnv.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2020, RTE (https://www.rte-france.com) +# Copyright (c) 2019-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, @@ -12,26 +12,15 @@ from typing import Dict, Union, Tuple, List, Optional, Any, Literal import grid2op -from grid2op.Exceptions.envExceptions import EnvError from grid2op.typing_variables import STEP_INFO_TYPING from grid2op.dtypes import dt_int, dt_float, dt_bool -from grid2op.Environment.baseEnv import BaseEnv +from grid2op.Exceptions import EnvError from grid2op.Chronics import ChangeNothing +from grid2op.Chronics._obs_fake_chronics_handler import _ObsCH from grid2op.Rules import RulesChecker from grid2op.operator_attention import LinearAttentionBudget - -class _ObsCH(ChangeNothing): - """ - INTERNAL - - .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ - - This class is reserved to internal use. Do not attempt to do anything with it. - """ - - def forecasts(self): - return [] +from grid2op.Environment.baseEnv import BaseEnv class _ObsEnv(BaseEnv): diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 938a2175..a74e5ec4 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -2135,7 +2135,7 @@ def get_params_for_runner(self): else: msg_ = ("You are probably using a legacy backend class that cannot " "be copied properly. Please upgrade your backend to the latest version.") - self.logger.warn(msg_) + self.logger.warning(msg_) warnings.warn(msg_) res["backend_kwargs"] = None diff --git a/grid2op/Environment/maskedEnvironment.py b/grid2op/Environment/maskedEnvironment.py index e3c55a7d..12bf0611 100644 --- a/grid2op/Environment/maskedEnvironment.py +++ b/grid2op/Environment/maskedEnvironment.py @@ -7,6 +7,7 @@ # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. import copy +import warnings import numpy as np import os from typing import Tuple, Union, List diff --git a/grid2op/tests/test_issue_657.py b/grid2op/tests/test_issue_657.py new file mode 100644 index 00000000..4e04bb2f --- /dev/null +++ b/grid2op/tests/test_issue_657.py @@ -0,0 +1,70 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# See AUTHORS.txt and https://github.com/Grid2Op/grid2op/pull/319 +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + +from logging import Logger +import unittest +import warnings + + +from helper_path_test import PATH_DATA_TEST +import grid2op +from grid2op.Exceptions import ChronicsError, EnvError +from grid2op.Action import BaseAction +from grid2op.Environment import BaseEnv +from grid2op.Reward import BaseReward + + +class WeirdReward(BaseReward): + def __init__(self, logger: Logger = None): + super().__init__(logger) + + def __call__(self, action: BaseAction, env:BaseEnv, has_error: bool, is_done: bool, is_illegal: bool, is_ambiguous: bool) -> float: + return len(env.chronics_handler.get_name()) + + +class Issue657Tester(unittest.TestCase): + def setUp(self): + self.env_name = "l2rpn_case14_sandbox" + # create first env + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make("l2rpn_case14_sandbox", test=True, reward_class=WeirdReward) + + def tearDown(self) -> None: + self.env.close() + return super().tearDown() + + def test_issue_657(self): + obs = self.env.reset() + obs.simulate(self.env.action_space()) + self.env.step(self.env.action_space()) + + def test_broader_names(self): + obs = self.env.reset() + obs_ch = obs._obs_env.chronics_handler + for attr_nm in self.env.chronics_handler.__dict__: + try: + getattr(obs_ch, attr_nm) + except (EnvError, ChronicsError) as exc_: + # access to some attributes / function might return these type of errors + pass + except AttributeError as exc_: + raise TypeError(f"No know attribute {attr_nm} for obs_chronics_handler") from exc_ + + for attr_nm in self.env.chronics_handler.real_data.__dict__: + try: + getattr(obs_ch, attr_nm) + except (EnvError, ChronicsError) as exc_: + # access to some attributes / function might return these type of errors + pass + except AttributeError as exc_: + raise TypeError(f"No know attribute {attr_nm} (from real_data / GridValue) for obs_chronics_handler") from exc_ + + +if __name__ == "__main__": + unittest.main()