diff --git a/.circleci/config.yml b/.circleci/config.yml index 6dca4b27b..b5c496bce 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -47,13 +47,17 @@ jobs: export _GRID2OP_FORCE_TEST=1 cd grid2op/tests/ python3 helper_list_test.py | circleci tests split > /tmp/tests_run + - run: + command: | + source venv_test/bin/activate + pip freeze - run: cat /tmp/tests_run - run: command: | source venv_test/bin/activate cd grid2op/tests/ export _GRID2OP_FORCE_TEST=1 - python3 -m unittest $(cat /tmp/tests_run) + python3 -m unittest -v $(cat /tmp/tests_run) install36: executor: python36 @@ -136,37 +140,28 @@ jobs: - run: command: | source venv_test/bin/activate - python -m pip install -U "numpy>=1.20,<1.21" - python -m pip install -U .[test] + python -m pip install -U "numpy>=1.20,<1.21" "pandas<2.2" "scipy<1.12" numba .[test] + pip freeze + - run: + command: | + source venv_test/bin/activate export _GRID2OP_FORCE_TEST=1 + cd /tmp grid2op.testinstall - # - run: - # command: | - # source venv_test/bin/activate - # python -m pip install -U "numpy>=1.21,<1.22" - # python -m pip install -U .[test] - # export _GRID2OP_FORCE_TEST=1 - # grid2op.testinstall - # - run: - # command: | - # source venv_test/bin/activate - # python -m pip install -U "numpy>=1.22,<1.23" - # python -m pip install -U .[test] - # export _GRID2OP_FORCE_TEST=1 - # grid2op.testinstall - # - run: - # command: | - # source venv_test/bin/activate - # python -m pip install -U "numpy>=1.23,<1.24" - # python -m pip install -U .[test] - # export _GRID2OP_FORCE_TEST=1 - # grid2op.testinstall - run: command: | source venv_test/bin/activate - python -m pip install -U "numpy>=1.24,<1.25" + python -m pip install -U "numpy>=1.24,<1.25" "pandas<2.2" "scipy<1.12" numba python -m pip install -U .[test] + - run: + command: | + source venv_test/bin/activate + pip freeze + - run: + command: | + source venv_test/bin/activate export _GRID2OP_FORCE_TEST=1 + cd /tmp grid2op.testinstall install39: @@ -187,59 +182,27 @@ jobs: python -m pip install -U pip setuptools wheel python -m pip install chronix2grid>="1.1.0.post1" python -m pip uninstall -y grid2op - # - run: - # command: | - # source venv_test/bin/activate - # python -m pip install -U numba - # python -m pip install -U .[test] - # - run: - # command: | - # source venv_test/bin/activate - # python -m pip install -U "numpy>=1.20,<1.21" - # python -m pip install -U .[test] - # export _GRID2OP_FORCE_TEST=1 - # grid2op.testinstall - # - run: - # command: | - # source venv_test/bin/activate - # python -m pip install -U "numpy>=1.21,<1.22" - # python -m pip install -U .[test] - # export _GRID2OP_FORCE_TEST=1 - # grid2op.testinstall - # - run: - # command: | - # source venv_test/bin/activate - # python -m pip install -U "numpy>=1.22,<1.23" - # python -m pip install -U .[test] - # export _GRID2OP_FORCE_TEST=1 - # grid2op.testinstall - # - run: - # command: | - # source venv_test/bin/activate - # python -m pip install -U "numpy>=1.23,<1.24" - # python -m pip install -U .[test] - # export _GRID2OP_FORCE_TEST=1 - # grid2op.testinstall - # - run: - # command: | - # source venv_test/bin/activate - # python -m pip install -U "numpy>=1.24,<1.25" - # python -m pip install -U .[test] - # export _GRID2OP_FORCE_TEST=1 - # grid2op.testinstall - # - run: - # command: | - # source venv_test/bin/activate - # python -m pip install -U "numpy>=1.25,<1.26" - # python -m pip install -U .[test] - # export _GRID2OP_FORCE_TEST=1 - # grid2op.testinstall - run: command: | source venv_test/bin/activate - python -m pip install -U "numpy>=1.26,<1.27" - python -m pip install -U .[test] + python -m pip install -U "numpy>=1.20,<1.21" "pandas<2.2" "scipy<1.12" numba .[test] + pip freeze + - run: + command: | + source venv_test/bin/activate + export _GRID2OP_FORCE_TEST=1 + cd /tmp + grid2op.testinstall + - run: + command: | + source venv_test/bin/activate + python -m pip install -U "numpy>=1.26,<1.27" "pandas<2.2" "scipy<1.12" numba .[test] + pip freeze + - run: + command: | + source venv_test/bin/activate export _GRID2OP_FORCE_TEST=1 + cd /tmp grid2op.testinstall install310: @@ -261,44 +224,24 @@ jobs: - run: command: | source venv_test/bin/activate - python -m pip install -U "numpy>=1.21,<1.22" - python -m pip install -U .[test] + python -m pip install -U "numpy>=1.21,<1.22" "pandas<2.2" "scipy<1.12" numba .[test] + pip freeze + - run: + command: | + source venv_test/bin/activate export _GRID2OP_FORCE_TEST=1 + cd /tmp grid2op.testinstall - # - run: - # command: | - # source venv_test/bin/activate - # python -m pip install -U "numpy>=1.22,<1.23" - # python -m pip install -U .[test] - # export _GRID2OP_FORCE_TEST=1 - # grid2op.testinstall - # - run: - # command: | - # source venv_test/bin/activate - # python -m pip install -U "numpy>=1.23,<1.24" - # python -m pip install -U .[test] - # export _GRID2OP_FORCE_TEST=1 - # grid2op.testinstall - # - run: - # command: | - # source venv_test/bin/activate - # python -m pip install -U "numpy>=1.24,<1.25" - # python -m pip install -U .[test] - # export _GRID2OP_FORCE_TEST=1 - # grid2op.testinstall - # - run: - # command: | - # source venv_test/bin/activate - # python -m pip install -U "numpy>=1.25,<1.26" - # python -m pip install -U .[test] - # export _GRID2OP_FORCE_TEST=1 - # grid2op.testinstall - run: command: | source venv_test/bin/activate - python -m pip install -U "numpy>=1.26,<1.27" - python -m pip install -U .[test] + python -m pip install -U "numpy>=1.26,<1.27" "pandas<2.2" "scipy<1.12" numba .[test] + pip freeze + - run: + command: | + source venv_test/bin/activate export _GRID2OP_FORCE_TEST=1 + cd /tmp grid2op.testinstall install311: @@ -316,34 +259,27 @@ jobs: command: | source venv_test/bin/activate python -m pip install -U pip setuptools wheel - python -m pip install -U numba - run: command: | source venv_test/bin/activate - python -m pip install -U "numpy>=1.23,<1.24" - python -m pip install -U .[test] + python -m pip install -U "numpy>=1.23,<1.24" "pandas<2.2" "scipy<1.12" numba .[test] + pip freeze + - run: + command: | + source venv_test/bin/activate export _GRID2OP_FORCE_TEST=1 + cd /tmp grid2op.testinstall - # - run: - # command: | - # source venv_test/bin/activate - # python -m pip install -U "numpy>=1.24,<1.25" - # python -m pip install -U .[test] - # export _GRID2OP_FORCE_TEST=1 - # grid2op.testinstall - # - run: - # command: | - # source venv_test/bin/activate - # python -m pip install -U "numpy>=1.25,<1.26" - # python -m pip install -U .[test] - # export _GRID2OP_FORCE_TEST=1 - # grid2op.testinstall - run: command: | source venv_test/bin/activate - python -m pip install -U "numpy>=1.26,<1.27" - python -m pip install -U .[test] + python -m pip install -U "numpy>=1.26,<1.27" "pandas<2.2" "scipy<1.12" numba .[test] + pip freeze + - run: + command: | + source venv_test/bin/activate export _GRID2OP_FORCE_TEST=1 + cd /tmp grid2op.testinstall install312: executor: python312 @@ -364,9 +300,13 @@ jobs: - run: command: | source venv_test/bin/activate - python -m pip install -U "numpy>=1.26,<1.27" - python -m pip install -U .[test] + python -m pip install -U "numpy>=1.26,<1.27" "pandas<2.2" "scipy<1.12" .[test] + pip freeze + - run: + command: | + source venv_test/bin/activate export _GRID2OP_FORCE_TEST=1 + cd /tmp grid2op.testinstall workflows: @@ -380,4 +320,4 @@ workflows: - install39 - install310 - install311 - - install312 # failing because of dependencies of numba, torch etc. Tired of it so ignoring it ! + - install312 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1486819a0..87bf65caa 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -40,8 +40,14 @@ Change Log - [FIXED] the "BaseBackendTest" class did not correctly detect divergence in most cases (which lead to weird bugs in failing tests) - [FIXED] an issue with imageio having deprecated the `fps` kwargs (see https://github.com/rte-france/Grid2Op/issues/569) +- [FIXED] adding the "`loads_charac.csv`" in the package data +- [FIXED] a bug when using grid2op, not "utils.py" script could be used (see + https://github.com/rte-france/Grid2Op/issues/577). This was caused by the modification of + `sys.path` when importing the grid2op test suite. - [ADDED] A type of environment that does not perform the "emulation of the protections" for some part of the grid (`MaskedEnvironment`) see https://github.com/rte-france/Grid2Op/issues/571 +- [ADDED] a "gym like" API for reset allowing to set the seed and the time serie id directly when calling + `env.reset(seed=.., options={"time serie id": ...})` - [IMPROVED] the CI speed: by not testing every possible numpy version but only most ancient and most recent - [IMPROVED] Runner now test grid2op version 1.9.6 and 1.9.7 - [IMPROVED] refacto `gridobj_cls._clear_class_attribute` and `gridobj_cls._clear_grid_dependant_class_attributes` diff --git a/MANIFEST.in b/MANIFEST.in index 25337d7a1..3692f5526 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -recursive-include grid2op/data *.bz2 *.json *.zip prods_charac.csv *.py .multimix storage_units_charac.csv start_datetime.info time_interval.info +recursive-include grid2op/data *.bz2 *.json *.zip loads_charac.csv prods_charac.csv *.py .multimix storage_units_charac.csv start_datetime.info time_interval.info global-exclude */__pycache__/* global-exclude *.pyc global-exclude grid2op/data_test/* diff --git a/README.md b/README.md index cddf1f5a9..868150c19 100644 --- a/README.md +++ b/README.md @@ -312,6 +312,10 @@ but it is currently not on our priorities. A quick fix that is known to work include to set the `experimental_read_from_local_dir` when creating the environment with `grid2op.make(..., experimental_read_from_local_dir=True)` (see doc for more information) +Sometimes, on some configuration (python version) we do not recommend to use grid2op with pandas>=2.2 +If you encounter any trouble, please downgrade to pandas<2.2. This behaviour occured in our continuous +integration environment for python >=3.9 but could not be reproduced locally. + ### Perform tests locally Provided that Grid2Op is installed *from source*: diff --git a/docs/conf.py b/docs/conf.py index b9cbbc67d..da0703f09 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.9.8.dev0' +release = '1.9.8.dev1' version = '1.9' diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index 3dcef1d6c..e4b9c0ccf 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -21,7 +21,7 @@ from grid2op.dtypes import dt_int, dt_float, dt_bool from grid2op.Backend.backend import Backend from grid2op.Action import BaseAction -from grid2op.Exceptions import * +from grid2op.Exceptions import BackendError try: import numba @@ -118,6 +118,12 @@ def __init__( can_be_copied: bool=True, with_numba: bool=NUMBA_, ): + from grid2op.MakeEnv.Make import _force_test_dataset + if _force_test_dataset(): + if with_numba: + warnings.warn(f"Forcing `test=True` will disable numba for {type(self)}") + with_numba = False + Backend.__init__( self, detailed_infos_for_cascading_failures=detailed_infos_for_cascading_failures, @@ -214,6 +220,8 @@ def __init__( self._lightsim2grid : bool = lightsim2grid self._dist_slack : bool = dist_slack self._max_iter : bool = max_iter + self._in_service_line_col_id = None + self._in_service_trafo_col_id = None def _check_for_non_modeled_elements(self): """This function check for elements in the pandapower grid that will have no impact on grid2op. @@ -337,6 +345,9 @@ def load_grid(self, warnings.filterwarnings("ignore", category=FutureWarning) self._grid = pp.from_json(full_path) self._check_for_non_modeled_elements() + + self._in_service_line_col_id = int(np.where(self._grid.line.columns == "in_service")[0][0]) + self._in_service_trafo_col_id = int(np.where(self._grid.trafo.columns == "in_service")[0][0]) # add the slack bus that is often not modeled as a generator, but i need it for this backend to work bus_gen_added = None @@ -879,7 +890,7 @@ def apply_action(self, backendAction: Union["grid2op.Action._backendAction._Back self._grid.shunt["in_service"].iloc[shunt_bus.changed] = sh_service chg_and_in_service = sh_service & shunt_bus.changed self._grid.shunt["bus"].loc[chg_and_in_service] = cls.local_bus_to_global(shunt_bus.values[chg_and_in_service], - cls.shunt_to_subid[chg_and_in_service]) + cls.shunt_to_subid[chg_and_in_service]) # i made at least a real change, so i implement it in the backend for id_el, new_bus in topo__: @@ -1292,6 +1303,9 @@ def copy(self) -> "PandaPowerBackend": res.load_theta = copy.deepcopy(self.load_theta) res.gen_theta = copy.deepcopy(self.gen_theta) res.storage_theta = copy.deepcopy(self.storage_theta) + + res._in_service_line_col_id = self._in_service_line_col_id + res._in_service_trafo_col_id = self._in_service_trafo_col_id return res @@ -1347,18 +1361,18 @@ def get_line_flow(self) -> np.ndarray: def _disconnect_line(self, id_): if id_ < self._number_true_line: - self._grid.line["in_service"].iloc[id_] = False + self._grid.line.iloc[id_, self._in_service_line_col_id] = False else: - self._grid.trafo["in_service"].iloc[id_ - self._number_true_line] = False + self._grid.trafo.iloc[id_ - self._number_true_line, self._in_service_trafo_col_id] = False self._topo_vect[self.line_or_pos_topo_vect[id_]] = -1 self._topo_vect[self.line_ex_pos_topo_vect[id_]] = -1 self.line_status[id_] = False def _reconnect_line(self, id_): if id_ < self._number_true_line: - self._grid.line["in_service"].iloc[id_] = True + self._grid.line.iloc[id_, self._in_service_line_col_id] = True else: - self._grid.trafo["in_service"].iloc[id_ - self._number_true_line] = True + self._grid.trafo.iloc[id_ - self._number_true_line, self._in_service_trafo_col_id] = True self.line_status[id_] = True def get_topo_vect(self) -> np.ndarray: diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index e0cbeea38..613e3e409 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -13,30 +13,38 @@ import copy import os import json -from typing import Optional, Tuple +from typing import Optional, Tuple, Union, Dict, Any +try: + # Literal introduced in python 3.9 + from typing import Literal +except ImportError: + from typing_extensions import Literal + import warnings import numpy as np from scipy.optimize import (minimize, LinearConstraint) + from abc import ABC, abstractmethod -from grid2op.Action import ActionSpace from grid2op.Observation import (BaseObservation, ObservationSpace, HighResSimCounter) from grid2op.Backend import Backend from grid2op.dtypes import dt_int, dt_float, dt_bool from grid2op.Space import GridObjects, RandomObject -from grid2op.Exceptions import * +from grid2op.Exceptions import (Grid2OpException, + EnvError, + InvalidRedispatching, + GeneratorTurnedOffTooSoon, + GeneratorTurnedOnTooSoon, + AmbiguousActionRaiseAlert) from grid2op.Parameters import Parameters -from grid2op.Reward import BaseReward -from grid2op.Reward import RewardHelper -from grid2op.Opponent import OpponentSpace, NeverAttackBudget -from grid2op.Action import DontAct, BaseAction -from grid2op.Rules import AlwaysLegal -from grid2op.Opponent import BaseOpponent +from grid2op.Reward import BaseReward, RewardHelper +from grid2op.Opponent import OpponentSpace, NeverAttackBudget, BaseOpponent +from grid2op.Action import DontAct, BaseAction, ActionSpace from grid2op.operator_attention import LinearAttentionBudget from grid2op.Action._backendAction import _BackendAction from grid2op.Chronics import ChronicsHandler -from grid2op.Rules import AlwaysLegal, BaseRules +from grid2op.Rules import AlwaysLegal, BaseRules, AlwaysLegal # TODO put in a separate class the redispatching function @@ -293,6 +301,11 @@ def foo(manager): CAN_SKIP_TS = False # each step is exactly one time step + #: this are the keys of the dictionnary `options` + #: that can be used when calling `env.reset(..., options={})` + KEYS_RESET_OPTIONS = {"time serie id"} + + def __init__( self, init_env_path: os.PathLike, @@ -1343,14 +1356,28 @@ def _update_parameters(self): self.__new_param = None - def reset(self): + def set_id(self, id_: Union[int, str]) -> None: + # nothing to do in general, overloaded for real Environment + pass + + def reset(self, + *, + seed: Union[int, None] = None, + options: Union[Dict[Literal["time serie id"], Union[int, str]], None] = None): """ Reset the base environment (set the appropriate variables to correct initialization). It is (and must be) overloaded in other :class:`grid2op.Environment` """ if self.__closed: raise EnvError("This environment is closed. You cannot use it anymore.") - + if options is not None: + for el in options: + if el not in type(self).KEYS_RESET_OPTIONS: + raise EnvError(f"You tried to customize the `reset` call with some " + f"`options` using the key `{el}` which is invalid. " + f"Only keys in {sorted(list(type(self).KEYS_RESET_OPTIONS))} " + f"can be used.") + self.__is_init = True # current = None is an indicator that this is the first step of the environment # so don't change the setting of current_obs = None unless you are willing to change that @@ -1371,9 +1398,15 @@ def reset(self): self._last_obs = None - # seeds (so that next episode does not depend on what happened in previous episode) - if self.seed_used is not None and not self._has_just_been_seeded: + if options is not None and "time serie id" in options: + self.set_id(options["time serie id"]) + + if seed is not None: + self.seed(seed) + elif self.seed_used is not None and not self._has_just_been_seeded: + # seeds (so that next episode does not depend on what happened in previous episode) self.seed(None, _seed_me=False) + self._reset_storage() self._reset_curtailment() self._reset_alert() @@ -1418,6 +1451,18 @@ def seed(self, seed=None, _seed_me=True): """ Set the seed of this :class:`Environment` for a better control and to ease reproducible experiments. + .. seealso:: + function :func:`Environment.reset` for extra information + + .. versionchanged:: 1.9.8 + Starting from version 1.9.8 you can directly set the seed when calling + reset. + + .. warning:: + It is preferable to call this function `just before` a call to `env.reset()` otherwise + the seeding might not work properly (especially if some non standard "time serie generators" + *aka* chronics are used) + Parameters ---------- seed: ``int`` diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 09df00f97..c88b4f32b 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -10,6 +10,7 @@ import warnings import numpy as np import re +from typing import Union, Any, Dict import grid2op from grid2op.Opponent import OpponentSpace @@ -680,7 +681,7 @@ def simulate(self, action): """ return self.get_obs().simulate(action) - def set_id(self, id_): + def set_id(self, id_: Union[int, str]) -> None: """ Set the id that will be used at the next call to :func:`Environment.reset`. @@ -688,6 +689,29 @@ def set_id(self, id_): **NB** The environment need to be **reset** for this to take effect. + .. versionchanged:: 1.6.4 + `id_` can now be a string instead of an integer. You can call something like + `env.set_id("0000")` or `env.set_id("Scenario_april_000")` + or `env.set_id("2050-01-03_0")` (depending on your environment) + to use the right time series. + + .. seealso:: + function :func:`Environment.reset` for extra information + + .. versionchanged:: 1.9.8 + Starting from version 1.9.8 you can directly set the time serie id when calling + reset. + + .. warning:: + If the "time serie generator" you use is on standard (*eg* it is random in some sense) + and if you want fully reproducible results, you should first call `env.set_id(...)` and + then call `env.seed(...)` (and of course `env.reset()`) + + Calling `env.seed(...)` and then `env.set_id(...)` might not behave the way you want. + + In this case, it is much better to use the function + `reset(seed=..., options={"time serie id": ...})` directly. + Parameters ---------- id_: ``int`` @@ -870,7 +894,10 @@ def add_text_logger(self, logger=None): self.logger = logger return self - def reset(self) -> BaseObservation: + def reset(self, + *, + seed: Union[int, None] = None, + options: Union[Dict[str, Any], None] = None) -> BaseObservation: """ Reset the environment to a clean state. It will reload the next chronics if any. And reset the grid to a clean state. @@ -889,17 +916,59 @@ def reset(self) -> BaseObservation: import grid2op # create the environment - env = grid2op.make("l2rpn_case14_sandbox") + env_name = "l2rpn_case14_sandbox" + env = grid2op.make(env_name) - # and now you can "render" (plot) the state of the grid + # start a new episode obs = env.reset() done = False reward = env.reward_range[0] while not done: action = agent.act(obs, reward, done) obs, reward, done, info = env.step(action) + + .. versionadded:: 1.9.8 + It is now possible to set the seed and the time series you want to use at the new + episode by calling `env.reset(seed=..., options={"time serie id": ...})` + + Before version 1.9.8, if you wanted to use a fixed seed, you would need to (see + doc of :func:`Environment.seed` ): + + .. code-block:: python + + seed = ... + env.seed(seed) + obs = env.reset() + ... + + Starting from version 1.9.8 you can do this in one call: + + .. code-block:: python + + seed = ... + obs = env.reset(seed=seed) + + For the "time series id" it is the same concept. Before you would need to do (see + doc of :func:`Environment.set_id` for more information ): + + .. code-block:: python + + time_serie_id = ... + env.set_id(time_serie_id) + obs = env.reset() + ... + + And now (from version 1.9.8) you can more simply do: + + .. code-block:: python + + time_serie_id = ... + obs = env.reset(options={"time serie id": time_serie_id}) + ... + """ - super().reset() + super().reset(seed=seed, options=options) + self.chronics_handler.next_chronics() self.chronics_handler.initialize( self.backend.name_load, diff --git a/grid2op/Environment/multiEnvMultiProcess.py b/grid2op/Environment/multiEnvMultiProcess.py index 53c2cec18..00a6fc803 100644 --- a/grid2op/Environment/multiEnvMultiProcess.py +++ b/grid2op/Environment/multiEnvMultiProcess.py @@ -5,14 +5,12 @@ # 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 multiprocessing import Process, Pipe + import numpy as np from grid2op.dtypes import dt_int -from grid2op.Exceptions import Grid2OpException, MultiEnvException -from grid2op.Space import GridObjects +from grid2op.Exceptions import MultiEnvException from grid2op.Environment.baseMultiProcessEnv import BaseMultiProcessEnvironment -from grid2op.Action import BaseAction class MultiEnvMultiProcess(BaseMultiProcessEnvironment): diff --git a/grid2op/Environment/multiMixEnv.py b/grid2op/Environment/multiMixEnv.py index 56f251665..d20e73b75 100644 --- a/grid2op/Environment/multiMixEnv.py +++ b/grid2op/Environment/multiMixEnv.py @@ -10,10 +10,12 @@ import warnings import numpy as np import copy +from typing import Any, Dict, Tuple, Union, List from grid2op.dtypes import dt_int, dt_float from grid2op.Space import GridObjects, RandomObject from grid2op.Exceptions import EnvError, Grid2OpException +from grid2op.Observation import BaseObservation class MultiMixEnvironment(GridObjects, RandomObject): @@ -152,6 +154,8 @@ class MultiMixEnvironment(GridObjects, RandomObject): """ + KEYS_RESET_OPTIONS = {"time serie id"} + def __init__( self, envs_dir, @@ -359,17 +363,36 @@ def __getitem__(self, key): # Not found by name raise KeyError - def reset(self, random=False): + def reset(self, + *, + seed: Union[int, None] = None, + random=False, + options: Union[Dict[str, Any], None] = None) -> BaseObservation: + if self.__closed: raise EnvError("This environment is closed, you cannot use it.") + + if options is not None: + for el in options: + if el not in type(self).KEYS_RESET_OPTIONS: + raise EnvError(f"You tried to customize the `reset` call with some " + f"`options` using the key `{el}` which is invalid. " + f"Only keys in {sorted(list(type(self).KEYS_RESET_OPTIONS))} " + f"can be used.") + if random: self.env_index = self.space_prng.randint(len(self.mix_envs)) else: self.env_index = (self.env_index + 1) % len(self.mix_envs) self.current_env = self.mix_envs[self.env_index] - self.current_env.reset() - return self.get_obs() + + if options is not None and "time serie id" in options: + self.set_id(options["time serie id"]) + + if seed is not None: + self.seed(seed) + return self.current_env.reset() def seed(self, seed=None): """ diff --git a/grid2op/Environment/timedOutEnv.py b/grid2op/Environment/timedOutEnv.py index af5558ebe..84fafef58 100644 --- a/grid2op/Environment/timedOutEnv.py +++ b/grid2op/Environment/timedOutEnv.py @@ -8,7 +8,7 @@ import time from math import floor -from typing import Tuple, Union, List +from typing import Any, Dict, Tuple, Union, List from grid2op.Environment.environment import Environment from grid2op.Action import BaseAction from grid2op.Observation import BaseObservation @@ -247,10 +247,17 @@ def init_obj_from_kwargs(cls, "_read_from_local_dir": _read_from_local_dir}, **other_env_kwargs) return res - - def reset(self) -> BaseObservation: + + + def reset(self, + *, + seed: Union[int, None] = None, + options: Union[Dict[str, Any], None] = None) -> BaseObservation: """Reset the environment. + .. seealso:: + The doc of :func:`Environment.reset` for more information + Returns ------- BaseObservation @@ -260,7 +267,7 @@ def reset(self) -> BaseObservation: self.__last_act_send = time.perf_counter() self.__last_act_received = self.__last_act_send self._is_init_dn = False - res = super().reset() + res = super().reset(seed=seed, options=options) self.__last_act_send = time.perf_counter() self._is_init_dn = True return res diff --git a/grid2op/__init__.py b/grid2op/__init__.py index bd891c039..89c8140fe 100644 --- a/grid2op/__init__.py +++ b/grid2op/__init__.py @@ -11,7 +11,7 @@ Grid2Op """ -__version__ = '1.9.8.dev0' +__version__ = '1.9.8.dev1' __all__ = [ "Action", @@ -45,6 +45,7 @@ ] + from grid2op.MakeEnv import (make, update_env, list_available_remote_env, @@ -59,4 +60,4 @@ __all__.append("create_test_suite") except ImportError as exc_: # grid2op is most likely not installed in editable mode from source - pass \ No newline at end of file + pass diff --git a/grid2op/command_line.py b/grid2op/command_line.py index 1579a9cfb..970be2e73 100644 --- a/grid2op/command_line.py +++ b/grid2op/command_line.py @@ -61,7 +61,7 @@ def replay(): def testinstall(): """ - Performs aperforms basic tests to make sure grid2op is properly installed and working. + Performs basic tests to make sure grid2op is properly installed and working. It's not because these tests pass that grid2op will be fully functional however. """ @@ -76,15 +76,25 @@ def testinstall(): os.path.join(this_directory, "tests"), pattern=file_name ) ) - results = unittest.TextTestResult(stream=sys.stderr, descriptions=True, verbosity=1) + + def fun(first=None, *args, **kwargs): + if first is not None: + sys.stderr.write(first, *args, **kwargs) + sys.stderr.write("\n") + sys.stderr.writeln = fun + results = unittest.TextTestResult(stream=sys.stderr, + descriptions=True, + verbosity=2) test_suite.run(results) if results.wasSuccessful(): - sys.exit(0) + return 0 else: - for _, str_ in results.errors: - print(str_) - print("-------------------------\n") - for _, str_ in results.failures: - print(str_) - print("-------------------------\n") + print("\n") + results.printErrors() + # for _, str_ in results.errors: + # print(str_) + # print("-------------------------\n") + # for _, str_ in results.failures: + # print(str_) + # print("-------------------------\n") raise RuntimeError("Test not successful !") diff --git a/grid2op/gym_compat/gymenv.py b/grid2op/gym_compat/gymenv.py index 5a000ffc1..7531e52e8 100644 --- a/grid2op/gym_compat/gymenv.py +++ b/grid2op/gym_compat/gymenv.py @@ -154,15 +154,17 @@ def _aux_reset_new(self, seed=None, options=None): # used for gym > 0.26 if self._shuffle_chronics and isinstance( self.init_env.chronics_handler.real_data, Multifolder - ): + ) and (options is not None and "time serie id" not in options): self.init_env.chronics_handler.sample_next_chronics() - super().reset(seed=seed) + super().reset(seed=seed) # seed gymnasium env if seed is not None: self._aux_seed_spaces() seed, next_seed, underlying_env_seeds = self._aux_seed_g2op(seed) - - g2op_obs = self.init_env.reset() + + # we don't seed grid2op with reset as it is done + # earlier + g2op_obs = self.init_env.reset(seed=None, options=options) gym_obs = self.observation_space.to_gym(g2op_obs) chron_id = self.init_env.chronics_handler.get_id() diff --git a/grid2op/simulator/simulator.py b/grid2op/simulator/simulator.py index 41dd719e9..142097944 100644 --- a/grid2op/simulator/simulator.py +++ b/grid2op/simulator/simulator.py @@ -339,7 +339,7 @@ def _adjust_controlable_gen( scale_objective = np.round(scale_objective, decimals=4) tmp_zeros = np.zeros((1, nb_dispatchable), dtype=float) - + # wrap everything into the proper scipy form def target(actual_dispatchable): # define my real objective @@ -407,7 +407,7 @@ def f(init): denom_adjust = 1.0 x0[can_adjust] = -init_sum / (weights[can_adjust] * denom_adjust) - res = f(x0) + res = f(x0.astype(float)) if res.success: return res.x else: diff --git a/grid2op/tests/__init__.py b/grid2op/tests/__init__.py index 74d8f6a50..6885eeb44 100644 --- a/grid2op/tests/__init__.py +++ b/grid2op/tests/__init__.py @@ -5,5 +5,3 @@ # 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. - -__all__ = ["BaseBackendTest", "BaseIssuesTest", "BaseRedispTest"] diff --git a/grid2op/tests/_aux_test_gym_compat.py b/grid2op/tests/_aux_test_gym_compat.py index 66dcd9710..099e99ad2 100644 --- a/grid2op/tests/_aux_test_gym_compat.py +++ b/grid2op/tests/_aux_test_gym_compat.py @@ -793,9 +793,8 @@ def setUp(self) -> None: action_class=PlayableAction, _add_to_name=type(self).__name__ ) - self.env.seed(0) - self.env.reset() # seed part ! - self.obs_env = self.env.reset() + self.env.reset() + self.obs_env = self.env.reset(seed=0, options={"time serie id": 0}) self.env_gym = self._aux_GymEnv_cls()(self.env) def test_assert_raises_creation(self): @@ -888,7 +887,7 @@ def test_scaling(self): assert observation_space._attr_to_keep == kept_attr assert len(obs_gym) == 17 # the substract are calibrated so that the maximum is really close to 0 - assert obs_gym.max() <= 0 + assert obs_gym.max() <= 0, f"{obs_gym.max()} should be 0." assert obs_gym.max() >= -0.5 def test_functs(self): diff --git a/grid2op/tests/helper_path_test.py b/grid2op/tests/helper_path_test.py index 683b65bd8..59bf81ed2 100644 --- a/grid2op/tests/helper_path_test.py +++ b/grid2op/tests/helper_path_test.py @@ -10,6 +10,7 @@ # root package directory # Grid2Op subdirectory # Grid2Op/tests subdirectory + import sys import os import numpy as np @@ -24,7 +25,10 @@ data_test_dir = os.path.abspath(os.path.join(grid2op_dir, "data_test")) data_dir = os.path.abspath(os.path.join(grid2op_dir, "data")) -sys.path.insert(0, grid2op_dir) +# sys.path.insert(0, grid2op_dir) # cause https://github.com/rte-france/Grid2Op/issues/577 +# because the addition of `from grid2op._create_test_suite import create_test_suite` +# in grid2op "__init__.py" + PATH_DATA = data_dir PATH_DATA_TEST = data_test_dir diff --git a/grid2op/tests/test_new_reset.py b/grid2op/tests/test_new_reset.py new file mode 100644 index 000000000..9977ffb80 --- /dev/null +++ b/grid2op/tests/test_new_reset.py @@ -0,0 +1,82 @@ +# 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 grid2op +import unittest +import warnings +import numpy as np +from grid2op.Exceptions import EnvError +from grid2op.gym_compat import GymEnv + + +class TestNewReset(unittest.TestCase): + """ + This class tests the possibility to set the seed and the time + serie id directly when calling `env.reset` + """ + + def setUp(self): + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make("l2rpn_case14_sandbox", test=True, _add_to_name=type(self).__name__) + + def test_normal_env(self): + # original way + self.env.set_id(0) + self.env.seed(0) + obs = self.env.reset() + + # test with seed in reset + self.env.set_id(0) + obs_seed = self.env.reset(seed=0) + + # test with ts_id in reset + self.env.seed(0) + obs_ts = self.env.reset(options={"time serie id": 0}) + + # test with both + obs_both = self.env.reset(seed=0, options={"time serie id": 0}) + assert obs_seed == obs + assert obs_ts == obs + assert obs_both == obs + + def test_raise_if_wrong_key(self): + with self.assertRaises(EnvError): + obs_ts = self.env.reset(options={"time series id": 0}) + + with self.assertRaises(EnvError): + obs_ts = self.env.reset(options={"chronics id": 0}) + + def _aux_obs_equals(self, obs1, obs2): + assert obs1.keys() == obs2.keys(), f"not the same keys" + for el in obs1: + assert np.array_equal(obs1[el], obs2[el]), f"obs not equal for attribute {el}" + + def test_gym_env(self): + gym_env = GymEnv(self.env) + + # original way + gym_env.init_env.set_id(0) + gym_env.init_env.seed(0) + obs, *_ = gym_env.reset() + + # test with seed in reset + gym_env.init_env.set_id(0) + obs_seed, *_ = gym_env.reset(seed=0) + + # test with ts_id in reset + gym_env.init_env.seed(0) + obs_ts, *_ = gym_env.reset(options={"time serie id": 0}) + + # test with both + obs_both, *_ = gym_env.reset(seed=0, options={"time serie id": 0}) + + self._aux_obs_equals(obs_seed, obs) + self._aux_obs_equals(obs_ts, obs) + self._aux_obs_equals(obs_both, obs) + \ No newline at end of file diff --git a/setup.py b/setup.py index 68b3586f9..57e002376 100644 --- a/setup.py +++ b/setup.py @@ -73,7 +73,7 @@ def my_test_suite(): "numba", "gym>=0.26", "gymnasium", - "stable-baselines3>=2.0", + # "stable-baselines3>=2.0", "nbconvert", "jinja2" ],