diff --git a/.circleci/config.yml b/.circleci/config.yml index f54f17919..b5c496bce 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,27 +3,27 @@ version: 2.1 executors: grid2op-executor: docker: - - image: python:3.10-buster + - image: python:3.10 working_directory: /Grid2Op python37: docker: - - image: python:3.7-buster + - image: python:3.7 python38: docker: - - image: python:3.8-buster + - image: python:3.8 python39: docker: - - image: python:3.9-buster + - image: python:3.9 python310: docker: - - image: python:3.10-buster + - image: python:3.10 python311: docker: - - image: python:3.11-buster + - image: python:3.11 python312: docker: - - image: cimg/python:3.12.0 + - image: cimg/python:3.12 jobs: test: @@ -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] - export _GRID2OP_FORCE_TEST=1 - grid2op.testinstall + python -m pip install -U "numpy>=1.20,<1.21" "pandas<2.2" "scipy<1.12" numba .[test] + pip freeze - run: - command: | + 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 + 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 "numpy>=1.24,<1.25" "pandas<2.2" "scipy<1.12" numba python -m pip install -U .[test] - export _GRID2OP_FORCE_TEST=1 - grid2op.testinstall - run: - command: | + 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 + pip freeze - run: - command: | + 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 + cd /tmp grid2op.testinstall install39: @@ -190,56 +185,24 @@ jobs: - 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 + 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 - 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: | + 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 + cd /tmp 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 + python -m pip install -U "numpy>=1.26,<1.27" "pandas<2.2" "scipy<1.12" numba .[test] + pip freeze - run: - command: | + command: | source venv_test/bin/activate - python -m pip install -U "numpy>=1.26,<1.27" - python -m pip install -U .[test] 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] - export _GRID2OP_FORCE_TEST=1 - grid2op.testinstall + 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 - 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: | + 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 + cd /tmp 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 + python -m pip install -U "numpy>=1.26,<1.27" "pandas<2.2" "scipy<1.12" numba .[test] + pip freeze - run: - command: | + command: | source venv_test/bin/activate - python -m pip install -U "numpy>=1.26,<1.27" - python -m pip install -U .[test] 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] - export _GRID2OP_FORCE_TEST=1 - grid2op.testinstall + python -m pip install -U "numpy>=1.23,<1.24" "pandas<2.2" "scipy<1.12" numba .[test] + pip freeze - run: - command: | + 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 + cd /tmp 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 + python -m pip install -U "numpy>=1.26,<1.27" "pandas<2.2" "scipy<1.12" numba .[test] + pip freeze - run: - command: | + command: | source venv_test/bin/activate - python -m pip install -U "numpy>=1.26,<1.27" - python -m pip install -U .[test] 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/.gitignore b/.gitignore index 2a03d9711..84e7e7bd5 100644 --- a/.gitignore +++ b/.gitignore @@ -397,6 +397,8 @@ grid2op/tests/test_failing_simulator.txt old_pyproject.toml pp_bug_gen_alone.py test_dunder.py +grid2op/tests/test_fail_ci.txt +saved_multiepisode_agent_36bus_DN_4/ # profiling files **.prof diff --git a/.readthedocs.yml b/.readthedocs.yml index 6f2d283a9..8dbbe353f 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,7 +1,14 @@ -version: 2 +version: "2" + +build: + os: "ubuntu-22.04" + tools: + python: "3.10" + +sphinx: + configuration: docs/conf.py python: - version: 3.8 install: - method: pip path: . diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f21dc67a6..821c3365b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -32,6 +32,29 @@ Change Log - [???] properly model interconnecting powerlines +[1.9.8] - 2024-01-26 +---------------------- +- [FIXED] the `backend.check_kirchoff` function was not correct when some elements were disconnected + (the wrong columns of the p_bus and q_bus was set in case of disconnected elements) +- [FIXED] `PandapowerBackend`, when no slack was present +- [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` +- [IMPROVED] the bahviour of the generic class `MakeBackend` used for the test suite. +- [IMPROVED] re introducing python 12 testing +- [IMPROVED] error messages in the automatic test suite (`AAATestBackendAPI`) + [1.9.7] - 2023-12-01 ---------------------- - [BREAKING] removal of the `grid2op/Exceptions/PowerflowExceptions.py` file and move the 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/action.rst b/docs/action.rst index a370d4d8b..90abdaa57 100644 --- a/docs/action.rst +++ b/docs/action.rst @@ -85,7 +85,7 @@ you want to perform on the grid. For more information you can consult the help o To avoid extremely verbose things, as of grid2op 1.5.0, we introduced some convenience functions to allow easier action construction. You can now do `act.load_set_bus = ...` instead of the previously way -more verbose `act.update({"set_bus": {"loads_id": ...}}` +more verbose `act.update({"set_bus": {"loads_id": ...}})` .. _action-module-examples: diff --git a/docs/conf.py b/docs/conf.py index 18cee2d9a..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.7' +release = '1.9.8.dev1' version = '1.9' diff --git a/docs/environment.rst b/docs/environment.rst index 11cac0a59..88213ffec 100644 --- a/docs/environment.rst +++ b/docs/environment.rst @@ -101,7 +101,7 @@ be equivalent to starting into the "middle" of a video game. If that is the case Finally, you might have noticed that each call to "env.reset" might take a while. This can dramatically increase the training time, especially at the beginning. This is due to the fact that each time `env.reset` is called, the whole chronics is read from the hard drive. If you want to lower this -impact then you might consult the `Optimize the data pipeline`_ section. +impact then you might consult the :ref:`environment-module-data-pipeline` page of the doc. .. _environment-module-chronics-info: diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index bf291aaf3..a06fc00b0 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -1023,10 +1023,12 @@ def next_grid_state(self, ] = True # disconnect the current power lines - if to_disc[lines_status].sum() == 0: - # no powerlines have been disconnected at this time step, i stop the computation there + if to_disc[lines_status].any() == 0: + # no powerlines have been disconnected at this time step, + # i stop the computation there break disconnected_during_cf[to_disc] = ts + # perform the disconnection action for i, el in enumerate(to_disc): if el: @@ -1124,18 +1126,19 @@ def check_kirchoff(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray p_ex, q_ex, v_ex, *_ = self.lines_ex_info() p_gen, q_gen, v_gen = self.generators_info() p_load, q_load, v_load = self.loads_info() - if self.n_storage > 0: + cls = type(self) + if cls.n_storage > 0: p_storage, q_storage, v_storage = self.storages_info() # fist check the "substation law" : nothing is created at any substation - p_subs = np.zeros(self.n_sub, dtype=dt_float) - q_subs = np.zeros(self.n_sub, dtype=dt_float) + p_subs = np.zeros(cls.n_sub, dtype=dt_float) + q_subs = np.zeros(cls.n_sub, dtype=dt_float) # check for each bus - p_bus = np.zeros((self.n_sub, 2), dtype=dt_float) - q_bus = np.zeros((self.n_sub, 2), dtype=dt_float) + p_bus = np.zeros((cls.n_sub, 2), dtype=dt_float) + q_bus = np.zeros((cls.n_sub, 2), dtype=dt_float) v_bus = ( - np.zeros((self.n_sub, 2, 2), dtype=dt_float) - 1.0 + np.zeros((cls.n_sub, 2, 2), dtype=dt_float) - 1.0 ) # sub, busbar, [min,max] topo_vect = self.get_topo_vect() @@ -1143,11 +1146,15 @@ def check_kirchoff(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray # for example, if two powerlines are such that line_or_to_subid is equal (eg both connected to substation 0) # then numpy do not guarantee that `p_subs[self.line_or_to_subid] += p_or` will add the two "corresponding p_or" # TODO this can be vectorized with matrix product, see example in obs.flow_bus_matrix (BaseObervation.py) - for i in range(self.n_line): - sub_or_id = self.line_or_to_subid[i] - sub_ex_id = self.line_ex_to_subid[i] - loc_bus_or = topo_vect[self.line_or_pos_topo_vect[i]] - 1 - loc_bus_ex = topo_vect[self.line_ex_pos_topo_vect[i]] - 1 + for i in range(cls.n_line): + sub_or_id = cls.line_or_to_subid[i] + sub_ex_id = cls.line_ex_to_subid[i] + if (topo_vect[cls.line_or_pos_topo_vect[i]] == -1 or + topo_vect[cls.line_ex_pos_topo_vect[i]] == -1): + # line is disconnected + continue + loc_bus_or = topo_vect[cls.line_or_pos_topo_vect[i]] - 1 + loc_bus_ex = topo_vect[cls.line_ex_pos_topo_vect[i]] - 1 # for substations p_subs[sub_or_id] += p_or[i] @@ -1184,92 +1191,104 @@ def check_kirchoff(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray v_bus[sub_ex_id,loc_bus_ex,][0] = min(v_bus[sub_ex_id,loc_bus_ex,][0],v_ex[i],) v_bus[sub_ex_id,loc_bus_ex,][1] = max(v_bus[sub_ex_id,loc_bus_ex,][1],v_ex[i],) - for i in range(self.n_gen): + for i in range(cls.n_gen): + if topo_vect[cls.gen_pos_topo_vect[i]] == -1: + # gen is disconnected + continue + # for substations - p_subs[self.gen_to_subid[i]] -= p_gen[i] - q_subs[self.gen_to_subid[i]] -= q_gen[i] + p_subs[cls.gen_to_subid[i]] -= p_gen[i] + q_subs[cls.gen_to_subid[i]] -= q_gen[i] # for bus p_bus[ - self.gen_to_subid[i], topo_vect[self.gen_pos_topo_vect[i]] - 1 + cls.gen_to_subid[i], topo_vect[cls.gen_pos_topo_vect[i]] - 1 ] -= p_gen[i] q_bus[ - self.gen_to_subid[i], topo_vect[self.gen_pos_topo_vect[i]] - 1 + cls.gen_to_subid[i], topo_vect[cls.gen_pos_topo_vect[i]] - 1 ] -= q_gen[i] # compute max and min values if v_gen[i]: # but only if gen is connected - v_bus[self.gen_to_subid[i], topo_vect[self.gen_pos_topo_vect[i]] - 1][ + v_bus[cls.gen_to_subid[i], topo_vect[cls.gen_pos_topo_vect[i]] - 1][ 0 ] = min( v_bus[ - self.gen_to_subid[i], topo_vect[self.gen_pos_topo_vect[i]] - 1 + cls.gen_to_subid[i], topo_vect[cls.gen_pos_topo_vect[i]] - 1 ][0], v_gen[i], ) - v_bus[self.gen_to_subid[i], topo_vect[self.gen_pos_topo_vect[i]] - 1][ + v_bus[cls.gen_to_subid[i], topo_vect[cls.gen_pos_topo_vect[i]] - 1][ 1 ] = max( v_bus[ - self.gen_to_subid[i], topo_vect[self.gen_pos_topo_vect[i]] - 1 + cls.gen_to_subid[i], topo_vect[cls.gen_pos_topo_vect[i]] - 1 ][1], v_gen[i], ) - for i in range(self.n_load): + for i in range(cls.n_load): + if topo_vect[cls.load_pos_topo_vect[i]] == -1: + # load is disconnected + continue + # for substations - p_subs[self.load_to_subid[i]] += p_load[i] - q_subs[self.load_to_subid[i]] += q_load[i] + p_subs[cls.load_to_subid[i]] += p_load[i] + q_subs[cls.load_to_subid[i]] += q_load[i] # for buses p_bus[ - self.load_to_subid[i], topo_vect[self.load_pos_topo_vect[i]] - 1 + cls.load_to_subid[i], topo_vect[cls.load_pos_topo_vect[i]] - 1 ] += p_load[i] q_bus[ - self.load_to_subid[i], topo_vect[self.load_pos_topo_vect[i]] - 1 + cls.load_to_subid[i], topo_vect[cls.load_pos_topo_vect[i]] - 1 ] += q_load[i] # compute max and min values if v_load[i]: # but only if load is connected - v_bus[self.load_to_subid[i], topo_vect[self.load_pos_topo_vect[i]] - 1][ + v_bus[cls.load_to_subid[i], topo_vect[cls.load_pos_topo_vect[i]] - 1][ 0 ] = min( v_bus[ - self.load_to_subid[i], topo_vect[self.load_pos_topo_vect[i]] - 1 + cls.load_to_subid[i], topo_vect[cls.load_pos_topo_vect[i]] - 1 ][0], v_load[i], ) - v_bus[self.load_to_subid[i], topo_vect[self.load_pos_topo_vect[i]] - 1][ + v_bus[cls.load_to_subid[i], topo_vect[cls.load_pos_topo_vect[i]] - 1][ 1 ] = max( v_bus[ - self.load_to_subid[i], topo_vect[self.load_pos_topo_vect[i]] - 1 + cls.load_to_subid[i], topo_vect[cls.load_pos_topo_vect[i]] - 1 ][1], v_load[i], ) - for i in range(self.n_storage): - p_subs[self.storage_to_subid[i]] += p_storage[i] - q_subs[self.storage_to_subid[i]] += q_storage[i] + for i in range(cls.n_storage): + if topo_vect[cls.storage_pos_topo_vect[i]] == -1: + # storage is disconnected + continue + + p_subs[cls.storage_to_subid[i]] += p_storage[i] + q_subs[cls.storage_to_subid[i]] += q_storage[i] p_bus[ - self.storage_to_subid[i], topo_vect[self.storage_pos_topo_vect[i]] - 1 + cls.storage_to_subid[i], topo_vect[cls.storage_pos_topo_vect[i]] - 1 ] += p_storage[i] q_bus[ - self.storage_to_subid[i], topo_vect[self.storage_pos_topo_vect[i]] - 1 + cls.storage_to_subid[i], topo_vect[cls.storage_pos_topo_vect[i]] - 1 ] += q_storage[i] # compute max and min values if v_storage[i] > 0: # the storage unit is connected v_bus[ - self.storage_to_subid[i], - topo_vect[self.storage_pos_topo_vect[i]] - 1, + cls.storage_to_subid[i], + topo_vect[cls.storage_pos_topo_vect[i]] - 1, ][0] = min( v_bus[ - self.storage_to_subid[i], - topo_vect[self.storage_pos_topo_vect[i]] - 1, + cls.storage_to_subid[i], + topo_vect[cls.storage_pos_topo_vect[i]] - 1, ][0], v_storage[i], ) @@ -1278,29 +1297,33 @@ def check_kirchoff(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray topo_vect[self.storage_pos_topo_vect[i]] - 1, ][1] = max( v_bus[ - self.storage_to_subid[i], - topo_vect[self.storage_pos_topo_vect[i]] - 1, + cls.storage_to_subid[i], + topo_vect[cls.storage_pos_topo_vect[i]] - 1, ][1], v_storage[i], ) - if type(self).shunts_data_available: + if cls.shunts_data_available: p_s, q_s, v_s, bus_s = self.shunt_info() - for i in range(self.n_shunt): + for i in range(cls.n_shunt): + if bus_s[i] == -1: + # shunt is disconnected + continue + # for substations - p_subs[self.shunt_to_subid[i]] += p_s[i] - q_subs[self.shunt_to_subid[i]] += q_s[i] + p_subs[cls.shunt_to_subid[i]] += p_s[i] + q_subs[cls.shunt_to_subid[i]] += q_s[i] # for buses - p_bus[self.shunt_to_subid[i], bus_s[i] - 1] += p_s[i] - q_bus[self.shunt_to_subid[i], bus_s[i] - 1] += q_s[i] + p_bus[cls.shunt_to_subid[i], bus_s[i] - 1] += p_s[i] + q_bus[cls.shunt_to_subid[i], bus_s[i] - 1] += q_s[i] # compute max and min values - v_bus[self.shunt_to_subid[i], bus_s[i] - 1][0] = min( - v_bus[self.shunt_to_subid[i], bus_s[i] - 1][0], v_s[i] + v_bus[cls.shunt_to_subid[i], bus_s[i] - 1][0] = min( + v_bus[cls.shunt_to_subid[i], bus_s[i] - 1][0], v_s[i] ) - v_bus[self.shunt_to_subid[i], bus_s[i] - 1][1] = max( - v_bus[self.shunt_to_subid[i], bus_s[i] - 1][1], v_s[i] + v_bus[cls.shunt_to_subid[i], bus_s[i] - 1][1] = max( + v_bus[cls.shunt_to_subid[i], bus_s[i] - 1][1], v_s[i] ) else: warnings.warn( 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/__init__.py b/grid2op/Environment/__init__.py index 1375aad0a..a9a4197b3 100644 --- a/grid2op/Environment/__init__.py +++ b/grid2op/Environment/__init__.py @@ -5,7 +5,8 @@ "SingleEnvMultiProcess", "MultiEnvMultiProcess", "MultiMixEnvironment", - "TimedOutEnvironment" + "TimedOutEnvironment", + "MaskedEnvironment" ] from grid2op.Environment.baseEnv import BaseEnv @@ -15,3 +16,4 @@ from grid2op.Environment.multiEnvMultiProcess import MultiEnvMultiProcess from grid2op.Environment.multiMixEnv import MultiMixEnvironment from grid2op.Environment.timedOutEnv import TimedOutEnvironment +from grid2op.Environment.maskedEnvironment import MaskedEnvironment diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index 40aaf5252..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 @@ -84,6 +92,65 @@ class BaseEnv(GridObjects, RandomObject, ABC): The documentation is showed here to document the common attributes of an "BaseEnvironment". + .. _danger-env-ownership: + + Notes + ------------------------ + + Note en environment data ownership + + .. danger:: + + + A non pythonic decision has been implemented in grid2op for various reasons: an environment + owns everything created from it. + + This means that if you (or the python interpreter) deletes the environment, you might not + use some data generate with this environment. + + More precisely, you cannot do something like: + + .. code-block:: python + + import grid2op + env = grid2op.make("l2rpn_case14_sandbox") + + saved_obs = [] + + obs = env.reset() + saved_obs.append(obs) + obs2, reward, done, info = env.step(env.action_space()) + saved_obs.append(obs2) + + saved_obs[0].simulate(env.action_space()) # works + del env + saved_obs[0].simulate(env.action_space()) # DOES NOT WORK + + It will raise an error like `Grid2OpException EnvError "This environment is closed. You cannot use it anymore."` + + This will also happen if you do things inside functions, for example like this: + + .. code-block:: python + + import grid2op + + def foo(manager): + env = grid2op.make("l2rpn_case14_sandbox") + obs = env.reset() + manager.append(obs) + obs2, reward, done, info = env.step(env.action_space()) + manager.append(obs2) + manager[0].simulate(env.action_space()) # works + return manager + + manager = [] + manager = foo(manager) + manager[0].simulate(env.action_space()) # DOES NOT WORK + + The same error is raised because the environment `env` is automatically deleted by python when the function `foo` ends + (well it might work on some cases, if the function is called before the variable `env` is actually deleted but you + should not rely on this behaviour.) + Attributes ---------- @@ -234,6 +301,11 @@ class BaseEnv(GridObjects, RandomObject, ABC): 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, @@ -342,7 +414,7 @@ def __init__( ) self._timestep_overflow: np.ndarray = None self._nb_timestep_overflow_allowed: np.ndarray = None - self._hard_overflow_threshold: float = self._parameters.HARD_OVERFLOW_THRESHOLD + self._hard_overflow_threshold: np.ndarray = None # store actions "cooldown" self._times_before_line_status_actionable: np.ndarray = None @@ -626,7 +698,7 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None): new_obj._nb_timestep_overflow_allowed = copy.deepcopy( self._nb_timestep_overflow_allowed ) - new_obj._hard_overflow_threshold = self._hard_overflow_threshold + new_obj._hard_overflow_threshold = copy.deepcopy(self._hard_overflow_threshold) # store actions "cooldown" new_obj._times_before_line_status_actionable = copy.deepcopy( @@ -1204,7 +1276,6 @@ def _has_been_initialized(self): self._gen_downtime = np.zeros(self.n_gen, dtype=dt_int) self._gen_activeprod_t = np.zeros(self.n_gen, dtype=dt_float) self._gen_activeprod_t_redisp = np.zeros(self.n_gen, dtype=dt_float) - self._nb_timestep_overflow_allowed = np.ones(shape=self.n_line, dtype=dt_int) self._max_timestep_line_status_deactivated = ( self._parameters.NB_TIMESTEP_COOLDOWN_LINE ) @@ -1220,6 +1291,11 @@ def _has_been_initialized(self): fill_value=self._parameters.NB_TIMESTEP_OVERFLOW_ALLOWED, dtype=dt_int, ) + self._hard_overflow_threshold = np.full( + shape=(self.n_line,), + fill_value=self._parameters.HARD_OVERFLOW_THRESHOLD, + dtype=dt_float, + ) self._timestep_overflow = np.zeros(shape=(self.n_line,), dtype=dt_int) # update the parameters @@ -1261,7 +1337,6 @@ def _update_parameters(self): # type of power flow to play # if True, then it will not disconnect lines above their thermal limits self._no_overflow_disconnection = self._parameters.NO_OVERFLOW_DISCONNECTION - self._hard_overflow_threshold = self._parameters.HARD_OVERFLOW_THRESHOLD # store actions "cooldown" self._max_timestep_line_status_deactivated = ( @@ -1275,20 +1350,34 @@ def _update_parameters(self): self._nb_timestep_overflow_allowed[ : ] = self._parameters.NB_TIMESTEP_OVERFLOW_ALLOWED - + self._hard_overflow_threshold[:] = self._parameters.HARD_OVERFLOW_THRESHOLD # hard overflow part self._env_dc = self._parameters.ENV_DC 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 @@ -1309,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() @@ -1356,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`` @@ -2957,16 +3064,19 @@ def _aux_register_env_converged(self, disc_lines, action, init_line_status, new_ # TODO is non zero and disconnected, this should be ok. self._time_extract_obs += time.perf_counter() - beg_res + def _backend_next_grid_state(self): + """overlaoded in MaskedEnv""" + return self.backend.next_grid_state(env=self, is_dc=self._env_dc) + def _aux_run_pf_after_state_properly_set( self, action, init_line_status, new_p, except_ ): has_error = True + detailed_info = None try: # compute the next _grid state beg_pf = time.perf_counter() - disc_lines, detailed_info, conv_ = self.backend.next_grid_state( - env=self, is_dc=self._env_dc - ) + disc_lines, detailed_info, conv_ = self._backend_next_grid_state() self._disc_lines[:] = disc_lines self._time_powerflow += time.perf_counter() - beg_pf if conv_ is None: @@ -3327,7 +3437,7 @@ def _reset_vectors_and_timings(self): ] = self._parameters.NB_TIMESTEP_OVERFLOW_ALLOWED self.nb_time_step = 0 # to have the first step at 0 - self._hard_overflow_threshold = self._parameters.HARD_OVERFLOW_THRESHOLD + self._hard_overflow_threshold[:] = self._parameters.HARD_OVERFLOW_THRESHOLD self._env_dc = self._parameters.ENV_DC self._times_before_line_status_actionable[:] = 0 diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index f7047204a..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 @@ -37,6 +38,14 @@ class Environment(BaseEnv): """ This class is the grid2op implementation of the "Environment" entity in the RL framework. + .. danger:: + + Long story short, once a environment is deleted, you cannot use anything it "holds" including, + but not limited to the capacity to perform `obs.simulate(...)` even if the `obs` is still + referenced. + + See :ref:`danger-env-ownership` (first danger block). + Attributes ---------- @@ -418,7 +427,7 @@ def _init_backend( raise Grid2OpException( "Impossible to initialize the powergrid, the powerflow diverge at iteration 0. " "Available information are: {}".format(info) - ) + ) from info["exception"][0] # test the backend returns object of the proper size if need_process_backend: @@ -672,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`. @@ -680,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`` @@ -862,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. @@ -881,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/maskedEnvironment.py b/grid2op/Environment/maskedEnvironment.py new file mode 100644 index 000000000..b97bf986c --- /dev/null +++ b/grid2op/Environment/maskedEnvironment.py @@ -0,0 +1,159 @@ +# Copyright (c) 2023, RTE (https://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + +import copy +import numpy as np +from typing import Tuple, Union, List +from grid2op.Environment.environment import Environment +from grid2op.Action import BaseAction +from grid2op.Observation import BaseObservation +from grid2op.Exceptions import EnvError +from grid2op.dtypes import dt_bool, dt_float, dt_int + + +class MaskedEnvironment(Environment): # TODO heritage ou alors on met un truc de base + """This class is the grid2op implementation of a "maked" environment: lines not in the + `lines_of_interest` mask will NOT be deactivated by the environment is the flow is too high + (or moderately high for too long.) + + .. warning:: + This class might not behave normally if used with TimeOutEnvironment, MultiEnv, MultiMixEnv etc. + + .. warning:: + At time of writing, the behaviour of "obs.simulate" is not modified + """ + # some kind of infinity value + # NB we multiply np.finfo(dt_float).max by a small number (1e-7) to avoid overflow + # indeed, _hard_overflow_threshold is multiply by the flow on the lines + INF_VAL_THM_LIM = 1e-7 * np.finfo(dt_float).max + + # some kind of infinity value + INF_VAL_TS_OVERFLOW_ALLOW = np.iinfo(dt_int).max - 1 + + def __init__(self, + grid2op_env: Union[Environment, dict], + lines_of_interest): + + self._lines_of_interest = self._make_lines_of_interest(lines_of_interest) + if isinstance(grid2op_env, Environment): + super().__init__(**grid2op_env.get_kwargs()) + elif isinstance(grid2op_env, dict): + super().__init__(**grid2op_env) + else: + raise EnvError(f"For MaskedEnvironment you need to provide " + f"either an Environment or a dict " + f"for grid2op_env. You provided: {type(grid2op_env)}") + + def _make_lines_of_interest(self, lines_of_interest): + # NB is called BEFORE the env has been created... + if isinstance(lines_of_interest, np.ndarray): + # if lines_of_interest.size() != type(self).n_line: + # raise EnvError("Impossible to init A masked environment when the number of lines " + # "of the mask do not match the number of lines on the grid.") + res = lines_of_interest.astype(dt_bool) + if res.sum() == 0: + raise EnvError("You cannot use MaskedEnvironment and masking all " + "the grid. If you don't want to simulate powerline " + "disconnection when they are game over, please " + "set params.NO_OVERFLOW_DISCONNECT=True (see doc)") + else: + raise EnvError("Format of lines_of_interest is not understood. " + "Please provide a vector of the size of the " + "number of lines on the grid.") + return res + + def _reset_vectors_and_timings(self): + super()._reset_vectors_and_timings() + self._hard_overflow_threshold[~self._lines_of_interest] = type(self).INF_VAL_THM_LIM + self._nb_timestep_overflow_allowed[~self._lines_of_interest] = type(self).INF_VAL_TS_OVERFLOW_ALLOW + + def get_kwargs(self, with_backend=True, with_chronics_handler=True): + res = {} + res["lines_of_interest"] = copy.deepcopy(self._lines_of_interest) + res["grid2op_env"] = super().get_kwargs(with_backend, with_chronics_handler) + return res + + def get_params_for_runner(self): + res = super().get_params_for_runner() + res["envClass"] = MaskedEnvironment + res["other_env_kwargs"] = {"lines_of_interest": copy.deepcopy(self._lines_of_interest)} + return res + + def _custom_deepcopy_for_copy(self, new_obj): + super()._custom_deepcopy_for_copy(new_obj) + new_obj._lines_of_interest = copy.deepcopy(self._lines_of_interest) + + @classmethod + def init_obj_from_kwargs(cls, + other_env_kwargs, + init_env_path, + init_grid_path, + chronics_handler, + backend, + parameters, + name, + names_chronics_to_backend, + actionClass, + observationClass, + rewardClass, + legalActClass, + voltagecontrolerClass, + other_rewards, + opponent_space_type, + opponent_action_class, + opponent_class, + opponent_init_budget, + opponent_budget_per_ts, + opponent_budget_class, + opponent_attack_duration, + opponent_attack_cooldown, + kwargs_opponent, + with_forecast, + attention_budget_cls, + kwargs_attention_budget, + has_attention_budget, + logger, + kwargs_observation, + observation_bk_class, + observation_bk_kwargs, + _raw_backend_class, + _read_from_local_dir): + res = MaskedEnvironment(grid2op_env={"init_env_path": init_env_path, + "init_grid_path": init_grid_path, + "chronics_handler": chronics_handler, + "backend": backend, + "parameters": parameters, + "name": name, + "names_chronics_to_backend": names_chronics_to_backend, + "actionClass": actionClass, + "observationClass": observationClass, + "rewardClass": rewardClass, + "legalActClass": legalActClass, + "voltagecontrolerClass": voltagecontrolerClass, + "other_rewards": other_rewards, + "opponent_space_type": opponent_space_type, + "opponent_action_class": opponent_action_class, + "opponent_class": opponent_class, + "opponent_init_budget": opponent_init_budget, + "opponent_budget_per_ts": opponent_budget_per_ts, + "opponent_budget_class": opponent_budget_class, + "opponent_attack_duration": opponent_attack_duration, + "opponent_attack_cooldown": opponent_attack_cooldown, + "kwargs_opponent": kwargs_opponent, + "with_forecast": with_forecast, + "attention_budget_cls": attention_budget_cls, + "kwargs_attention_budget": kwargs_attention_budget, + "has_attention_budget": has_attention_budget, + "logger": logger, + "kwargs_observation": kwargs_observation, + "observation_bk_class": observation_bk_class, + "observation_bk_kwargs": observation_bk_kwargs, + "_raw_backend_class": _raw_backend_class, + "_read_from_local_dir": _read_from_local_dir}, + **other_env_kwargs) + return res 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 fcccd7641..84fafef58 100644 --- a/grid2op/Environment/timedOutEnv.py +++ b/grid2op/Environment/timedOutEnv.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2020, RTE (https://www.rte-france.com) +# Copyright (c) 2023, RTE (https://www.rte-france.com) # See AUTHORS.txt # This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. # If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, @@ -8,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 @@ -23,7 +23,10 @@ class TimedOutEnvironment(Environment): # TODO heritage ou alors on met un truc of the `step` function. For more information, see the documentation of - :func:`TimedOutEnvironment.step` for + :func:`TimedOutEnvironment.step` + + .. warning:: + This class might not behave normally if used with MaskedEnvironment, MultiEnv, MultiMixEnv etc. Attributes ---------- @@ -244,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 @@ -257,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/Episode/EpisodeReplay.py b/grid2op/Episode/EpisodeReplay.py index 6213bf450..b21f21fc7 100644 --- a/grid2op/Episode/EpisodeReplay.py +++ b/grid2op/Episode/EpisodeReplay.py @@ -102,15 +102,15 @@ def replay_episode( load_info: ``str`` Defaults to "p". What kind of values to show on loads. - Can be oneof `["p", "v", None]` + Can be one of `["p", "v", None]` gen_info: ``str`` Defaults to "p". What kind of values to show on generators. - Can be oneof `["p", "v", None]` + Can be one of `["p", "v", None]` line_info: ``str`` Defaults to "rho". What kind of values to show on lines. - Can be oneof `["rho", "a", "p", "v", None]` + Can be one of `["rho", "a", "p", "v", None]` resolution: ``tuple`` Defaults to (1280, 720). The resolution to use for the gif. @@ -187,7 +187,12 @@ def replay_episode( # Export all frames as gif if enabled if gif_name is not None and len(frames) > 0: try: - imageio.mimwrite(gif_path, frames, fps=fps) + try: + # with imageio > 2.5 you need to compute the duration + imageio.mimwrite(gif_path, frames, duration=1000./fps) + except TypeError: + # imageio <= 2.5 can be given fps directly + imageio.mimwrite(gif_path, frames, fps=fps) # Try to compress try: from pygifsicle import optimize diff --git a/grid2op/Observation/baseObservation.py b/grid2op/Observation/baseObservation.py index 1c0a259fa..6b401502b 100644 --- a/grid2op/Observation/baseObservation.py +++ b/grid2op/Observation/baseObservation.py @@ -4207,7 +4207,18 @@ def get_forecast_env(self) -> "grid2op.Environment.Environment": f_obs_3, *_ = forecast_env.step(act_3) sim_obs_3, *_ = sim_obs_2.simulate(act_3) # f_obs_3 should be sim_obs_3 - + + .. danger:: + + Long story short, once a environment (and a forecast_env is one) + is deleted, you cannot use anything it "holds" including, + but not limited to the capacity to perform `obs.simulate(...)` even if the `obs` is still + referenced. + + See :ref:`danger-env-ownership` (first danger block). + + This caused issue https://github.com/rte-france/Grid2Op/issues/568 for example. + Returns ------- grid2op.Environment.Environment @@ -4339,8 +4350,26 @@ def get_env_from_external_forecasts(self, you have 100 rows then you have 100 steps. .. warning:: - We remind that, if you provide some forecasts, it is expected that + We remind that, if you provide some forecasts, it is expected that they allow some powerflow to converge. + The balance between total generation on one side and total demand and losses on the other should also + make "as close as possible" to reduce some modeling artifact (by the backend, grid2op does not check + anything here). + + Finally, make sure that your input data meet the constraints on the generators (pmin, pmax and ramps) + otherwise you might end up with incorrect behaviour. Grid2op supposes that data fed to it + is consistent with its model. If not it's "undefined behaviour". + + .. danger:: + + Long story short, once a environment (and a forecast_env is one) + is deleted, you cannot use anything it "holds" including, + but not limited to the capacity to perform `obs.simulate(...)` even if the `obs` is still + referenced. + + See :ref:`danger-env-ownership` (first danger block). + This caused issue https://github.com/rte-france/Grid2Op/issues/568 for example. + Examples -------- A typical use might look like diff --git a/grid2op/Runner/runner.py b/grid2op/Runner/runner.py index c790b0883..59747a116 100644 --- a/grid2op/Runner/runner.py +++ b/grid2op/Runner/runner.py @@ -1137,6 +1137,7 @@ def run( returned list are not necessarily sorted by this value) - "cum_reward" the cumulative reward obtained by the :attr:`Runner.Agent` on this episode i - "nb_time_step": the number of time steps played in this episode. + - "total_step": the total number of time steps possible in this episode. - "episode_data" : [Optional] The :class:`EpisodeData` corresponding to this episode run only if `add_detailed_output=True` - "add_nb_highres_sim": [Optional] The estimated number of calls to high resolution simulator made diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 48439dca6..f14eb3a46 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -650,70 +650,8 @@ def tell_dim_alert(cls, dim_alerts): @classmethod def _clear_class_attribute(cls): - cls.glop_version = grid2op.__version__ - cls._PATH_ENV = None - - cls.SUB_COL = 0 - cls.LOA_COL = 1 - cls.GEN_COL = 2 - cls.LOR_COL = 3 - cls.LEX_COL = 4 - cls.STORAGE_COL = 5 - - cls.attr_list_vect = None - cls.attr_list_set = {} - cls.attr_list_json = [] - cls.attr_nan_list_set = set() - - # class been init - cls._IS_INIT = False - - # name of the objects - cls.env_name = "unknown" - cls.name_load = None - cls.name_gen = None - cls.name_line = None - cls.name_sub = None - cls.name_storage = None - - cls.n_gen = -1 - cls.n_load = -1 - cls.n_line = -1 - cls.n_sub = -1 - cls.n_storage = -1 - - cls.sub_info = None - cls.dim_topo = -1 - - # to which substation is connected each element - cls.load_to_subid = None - cls.gen_to_subid = None - cls.line_or_to_subid = None - cls.line_ex_to_subid = None - cls.storage_to_subid = None - - # which index has this element in the substation vector - cls.load_to_sub_pos = None - cls.gen_to_sub_pos = None - cls.line_or_to_sub_pos = None - cls.line_ex_to_sub_pos = None - cls.storage_to_sub_pos = None - - # which index has this element in the topology vector - cls.load_pos_topo_vect = None - cls.gen_pos_topo_vect = None - cls.line_or_pos_topo_vect = None - cls.line_ex_pos_topo_vect = None - cls.storage_pos_topo_vect = None - - # "convenient" way to retrieve information of the grid - cls.grid_objects_types = None - # to which substation each element of the topovect is connected - cls._topo_vect_to_sub = None - - # list of attribute to convert it from/to a vector - cls._vectorized = None - + cls.shunts_data_available = False + # for redispatching / unit commitment cls._li_attr_disp = [ "gen_type", @@ -744,56 +682,9 @@ def _clear_class_attribute(cls): float, bool, ] - - # redispatch data, not available in all environment - cls.redispatching_unit_commitment_availble = False - cls.gen_type = None - cls.gen_pmin = None - cls.gen_pmax = None - cls.gen_redispatchable = None - cls.gen_max_ramp_up = None - cls.gen_max_ramp_down = None - cls.gen_min_uptime = None - cls.gen_min_downtime = None - cls.gen_cost_per_MW = None # marginal cost (in currency / (power.step) and not in $/(MW.h) it would be $ / (MW.5mins) ) - cls.gen_startup_cost = None # start cost (in currency) - cls.gen_shutdown_cost = None # shutdown cost (in currency) - cls.gen_renewable = None - - # storage unit static data - cls.storage_type = None - cls.storage_Emax = None - cls.storage_Emin = None - cls.storage_max_p_prod = None - cls.storage_max_p_absorb = None - cls.storage_marginal_cost = None - cls.storage_loss = None - cls.storage_charging_efficiency = None - cls.storage_discharging_efficiency = None - - # grid layout - cls.grid_layout = None - - # shunt data, not available in every backend - cls.shunts_data_available = False - cls.n_shunt = None - cls.name_shunt = None - cls.shunt_to_subid = None - - # alarm / alert - cls.assistant_warning_type = None - # alarms - cls.dim_alarms = 0 - cls.alarms_area_names = [] - cls.alarms_lines_area = {} - cls.alarms_area_lines = [] - - # alerts - cls.dim_alerts = 0 - cls.alertable_line_names = [] - cls.alertable_line_ids = [] - + cls._clear_grid_dependant_class_attributes() + @classmethod def _clear_grid_dependant_class_attributes(cls): cls.glop_version = grid2op.__version__ @@ -856,10 +747,10 @@ def _clear_grid_dependant_class_attributes(cls): cls.grid_objects_types = None # to which substation each element of the topovect is connected cls._topo_vect_to_sub = None - + # list of attribute to convert it from/to a vector cls._vectorized = None - + # redispatch data, not available in all environment cls.redispatching_unit_commitment_availble = False cls.gen_type = None @@ -885,7 +776,15 @@ def _clear_grid_dependant_class_attributes(cls): cls.storage_loss = None cls.storage_charging_efficiency = None cls.storage_discharging_efficiency = None - + + # grid layout + cls.grid_layout = None + + # shunt data, not available in every backend + cls.n_shunt = None + cls.name_shunt = None + cls.shunt_to_subid = None + # alarm / alert cls.assistant_warning_type = None @@ -900,11 +799,6 @@ def _clear_grid_dependant_class_attributes(cls): cls.alertable_line_names = [] cls.alertable_line_ids = [] - # shunt data, not available in every backend - cls.n_shunt = None - cls.name_shunt = None - cls.shunt_to_subid = None - @classmethod def _update_value_set(cls): """ diff --git a/grid2op/__init__.py b/grid2op/__init__.py index 8597490cb..89c8140fe 100644 --- a/grid2op/__init__.py +++ b/grid2op/__init__.py @@ -11,7 +11,7 @@ Grid2Op """ -__version__ = '1.9.7' +__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/box_gym_actspace.py b/grid2op/gym_compat/box_gym_actspace.py index 1838a4f33..aed07d132 100644 --- a/grid2op/gym_compat/box_gym_actspace.py +++ b/grid2op/gym_compat/box_gym_actspace.py @@ -77,14 +77,14 @@ class __AuxBoxGymActSpace: .. code-block:: python - gym_env.observation_space = BoxGymActSpace(env.observation_space, + gym_env.action_space = BoxGymActSpace(env.action_space, attr_to_keep=['redispatch', "curtail"]) You can also apply some basic transformation to the attribute of the action. This can be done with: .. code-block:: python - gym_env.observation_space = BoxGymActSpace(env.observation_space, + gym_env.action_space = BoxGymActSpace(env.action_space, attr_to_keep=['redispatch', "curtail"], multiply={"redispatch": env.gen_max_ramp_up}, add={"redispatch": 0.5 * env.gen_max_ramp_up}) @@ -654,7 +654,7 @@ def normalize_attr(self, attr_nm: str): both_finite &= curr_high > curr_low if (~both_finite).any(): - warnings.warn(f"The normalization of attribute \"{both_finite}\" cannot be performed entirely as " + warnings.warn(f"The normalization of attribute \"{attr_tmp}\" cannot be performed entirely as " f"there are some non finite value, or `high == `low` " f"for some components.") diff --git a/grid2op/gym_compat/box_gym_obsspace.py b/grid2op/gym_compat/box_gym_obsspace.py index d0aecf761..0277a1517 100644 --- a/grid2op/gym_compat/box_gym_obsspace.py +++ b/grid2op/gym_compat/box_gym_obsspace.py @@ -908,7 +908,7 @@ def normalize_attr(self, attr_nm: str): both_finite &= curr_high > curr_low if (~both_finite).any(): - warnings.warn(f"The normalization of attribute \"{both_finite}\" cannot be performed entirely as " + warnings.warn(f"The normalization of attribute \"{attr_nm}\" cannot be performed entirely as " f"there are some non finite value, or `high == `low` " f"for some components.") 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/BaseBackendTest.py b/grid2op/tests/BaseBackendTest.py index 9603e0839..b8f99b617 100644 --- a/grid2op/tests/BaseBackendTest.py +++ b/grid2op/tests/BaseBackendTest.py @@ -74,7 +74,7 @@ def get_path(self): def test_properNames(self): self.skip_if_needed() - backend = self.make_backend() + backend = self.make_backend_with_glue_code() path = self.get_path() with warnings.catch_warnings(): @@ -97,7 +97,7 @@ def get_casefile(self): return "test_case14.json" def test_load_file(self): - backend = self.make_backend() + backend = self.make_backend_with_glue_code() path_matpower = self.get_path() case_file = self.get_casefile() with warnings.catch_warnings(): @@ -177,8 +177,8 @@ def test_load_file(self): assert np.all(backend.get_topo_vect() == np.ones(np.sum(backend.sub_info))) - conv = backend.runpf() - assert conv, "powerflow diverge it is not supposed to!" + conv, *_ = backend.runpf() + assert conv, f"powerflow diverge it is not supposed to! Error {_}" with warnings.catch_warnings(): warnings.filterwarnings("ignore") @@ -191,7 +191,7 @@ def test_load_file(self): assert np.max(np.abs(q_bus.flatten())) <= self.tolvect def test_assert_grid_correct(self): - backend = self.make_backend() + backend = self.make_backend_with_glue_code() path_matpower = self.get_path() case_file = self.get_casefile() with warnings.catch_warnings(): @@ -199,8 +199,8 @@ def test_assert_grid_correct(self): backend.load_grid(path_matpower, case_file) type(backend).set_env_name("TestLoadingCase_env2_test_assert_grid_correct") backend.assert_grid_correct() - conv = backend.runpf() - assert conv, "powerflow diverge it is not supposed to!" + conv, *_ = backend.runpf() + assert conv, f"powerflow diverge it is not supposed to! Error {_}" backend.assert_grid_correct_after_powerflow() @@ -212,7 +212,7 @@ def get_casefile(self): return "test_case14.json" def setUp(self): - self.backend = self.make_backend() + self.backend = self.make_backend_with_glue_code() self.path_matpower = self.get_path() self.case_file = self.get_casefile() with warnings.catch_warnings(): @@ -262,8 +262,8 @@ def test_theta_ok(self): def test_runpf_dc(self): self.skip_if_needed() - conv = self.backend.runpf(is_dc=True) - assert conv + conv, *_ = self.backend.runpf(is_dc=True) + assert conv, f"powerflow diverge with error {_}" true_values_dc = np.array( [ 147.83859556, @@ -317,7 +317,8 @@ def test_runpf(self): 2.80741759e01, ] ) - conv = self.backend.runpf(is_dc=False) + conv, *_ = self.backend.runpf(is_dc=False) + assert conv, f"powerflow diverge with error {_}" assert conv p_or, *_ = self.backend.lines_or_info() assert self.compare_vect(p_or, true_values_ac) @@ -325,8 +326,8 @@ def test_runpf(self): def test_voltage_convert_powerlines(self): self.skip_if_needed() # i have the correct voltages in powerlines if the formula to link mw, mvar, kv and amps is correct - conv = self.backend.runpf(is_dc=False) - assert conv, "powerflow diverge at loading" + conv, *_ = self.backend.runpf(is_dc=False) + assert conv, f"powerflow diverge at loading with error {_}" p_or, q_or, v_or, a_or = self.backend.lines_or_info() a_th = np.sqrt(p_or**2 + q_or**2) * 1e3 / (np.sqrt(3) * v_or) @@ -341,8 +342,8 @@ def test_voltages_correct_load_gen(self): # i have the right voltages to generators and load, if it's the same as the voltage (correct from the above test) # of the powerline connected to it. - conv = self.backend.runpf(is_dc=False) - assert conv, "powerflow diverge at loading" + conv, *_ = self.backend.runpf(is_dc=False) + assert conv, f"powerflow diverge at loading with error {_}" load_p, load_q, load_v = self.backend.loads_info() gen_p, gen__q, gen_v = self.backend.generators_info() p_or, q_or, v_or, a_or = self.backend.lines_or_info() @@ -384,33 +385,37 @@ def test_voltages_correct_load_gen(self): continue assert False, "generator {} has not been checked".format(g_id) - def test_copy(self): + def test_copy_ac(self, is_dc=False): self.skip_if_needed() - conv = self.backend.runpf(is_dc=False) - assert conv, "powerflow diverge at loading" + conv, *_ = self.backend.runpf(is_dc=is_dc) + assert conv, f"powerflow diverge at loading with error {_}" l_id = 3 p_or_orig, *_ = self.backend.lines_or_info() - adn_backend_cpy = self.backend.copy() + backend_cpy = self.backend.copy() self.backend._disconnect_line(l_id) - conv = self.backend.runpf(is_dc=False) - assert conv - conv2 = adn_backend_cpy.runpf(is_dc=False) - assert conv2 + conv, *_ = self.backend.runpf(is_dc=is_dc) + assert conv, f"original backend diverged with error {_}" + conv2 = backend_cpy.runpf(is_dc=is_dc) + assert conv2, f"copied backend diverged with error {_}" p_or_ref, *_ = self.backend.lines_or_info() - p_or, *_ = adn_backend_cpy.lines_or_info() + p_or, *_ = backend_cpy.lines_or_info() assert self.compare_vect( p_or_orig, p_or ), "the copied object affects its original 'parent'" assert ( np.abs(p_or_ref[l_id]) <= self.tol_one - ), "powerline {} has not been disconnected".format(l_id) + ), "powerline {} has not been disconnected in orig backend".format(l_id) + + def test_copy_dc(self): + self.skip_if_needed() + self.test_copy_ac(True) def test_copy2(self): self.skip_if_needed() self.backend._disconnect_line(8) - conv = self.backend.runpf(is_dc=False) + conv, *_ = self.backend.runpf(is_dc=False) p_or_orig, *_ = self.backend.lines_or_info() adn_backend_cpy = self.backend.copy() @@ -520,12 +525,12 @@ def test_pf_ac_dc(self): 5.77869057, ] ) - conv = self.backend.runpf(is_dc=True) - assert conv + conv, *_ = self.backend.runpf(is_dc=True) + assert conv, f"error {_}" p_or_orig, q_or_orig, *_ = self.backend.lines_or_info() assert np.all(q_or_orig == 0.0), "in dc mode all q must be zero" - conv = self.backend.runpf(is_dc=False) - assert conv + conv, *_ = self.backend.runpf(is_dc=False) + assert conv, f"error {_}" p_or_orig, q_or_orig, *_ = self.backend.lines_or_info() assert self.compare_vect(q_or_orig, true_values_ac) @@ -567,11 +572,11 @@ def test_disconnect_line(self): continue backend_cpy = self.backend.copy() backend_cpy._disconnect_line(i) - conv = backend_cpy.runpf() + conv, *_ = backend_cpy.runpf() assert ( conv - ), "Power flow computation does not converge if line {} is removed".format( - i + ), "Power flow computation does not converge if line {} is removed with error ".format( + i, _ ) flows = backend_cpy.get_line_status() assert not flows[i] @@ -579,7 +584,8 @@ def test_disconnect_line(self): def test_donothing_action(self): self.skip_if_needed() - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() + assert conv, f"error {_}" init_flow = self.backend.get_line_flow() init_lp, *_ = self.backend.loads_info() init_gp, *_ = self.backend.generators_info() @@ -596,8 +602,8 @@ def test_donothing_action(self): # assert self.compare_vect(init_gp, after_gp) # check i didn't modify the generators # TODO here !!! problem with steady state P=C+L assert np.all(init_ls == after_ls) # check i didn't disconnect any powerlines - conv = self.backend.runpf() - assert conv, "Cannot perform a powerflow after doing nothing" + conv, *_ = self.backend.runpf() + assert conv, f"Cannot perform a powerflow after doing nothing with error {_}" after_flow = self.backend.get_line_flow() assert self.compare_vect(init_flow, after_flow) @@ -608,8 +614,8 @@ def test_apply_action_active_value(self): # also multiply by 2 # i set up the stuff to have exactly 0 losses - conv = self.backend.runpf(is_dc=True) - assert conv, "powergrid diverge after loading (even in DC)" + conv, *_ = self.backend.runpf(is_dc=True) + assert conv, f"powergrid diverge after loading (even in DC) with error {_}" init_flow, *_ = self.backend.lines_or_info() init_lp, init_l_q, *_ = self.backend.loads_info() init_gp, *_ = self.backend.generators_info() @@ -623,7 +629,8 @@ def test_apply_action_active_value(self): bk_action = self.bkact_class() bk_action += action self.backend.apply_action(bk_action) - conv = self.backend.runpf(is_dc=True) + conv, *_ = self.backend.runpf(is_dc=True) + assert conv, f"powergrid diverge with error {_}" # now the system has exactly 0 losses (ie sum load = sum gen) # i check that if i divide by 2, then everything is divided by 2 @@ -641,8 +648,8 @@ def test_apply_action_active_value(self): bk_action = self.bkact_class() bk_action += action self.backend.apply_action(bk_action) - conv = self.backend.runpf(is_dc=True) - assert conv, "Cannot perform a powerflow after doing nothing" + conv, *_ = self.backend.runpf(is_dc=True) + assert conv, "Cannot perform a powerflow after doing nothing (dc)" after_lp, after_lq, *_ = self.backend.loads_info() after_gp, *_ = self.backend.generators_info() @@ -656,10 +663,10 @@ def test_apply_action_active_value(self): # i'm in DC mode, i can't check for reactive values... assert ( np.max(np.abs(p_subs)) <= self.tolvect - ), "problem with active values, at substation" + ), "problem with active values, at substation (kirchoff for DC)" assert ( np.max(np.abs(p_bus.flatten())) <= self.tolvect - ), "problem with active values, at a bus" + ), "problem with active values, at a bus (kirchoff for DC)" assert self.compare_vect( new_pp, after_gp @@ -673,8 +680,8 @@ def test_apply_action_active_value(self): def test_apply_action_prod_v(self): self.skip_if_needed() - conv = self.backend.runpf(is_dc=False) - assert conv, "powergrid diverge after loading" + conv, *_ = self.backend.runpf(is_dc=False) + assert conv, f"powergrid diverge after loading with error {_}" prod_p_init, prod_q_init, prod_v_init = self.backend.generators_info() ratio = 1.05 action = self.action_env( @@ -683,8 +690,8 @@ def test_apply_action_prod_v(self): bk_action = self.bkact_class() bk_action += action self.backend.apply_action(bk_action) - conv = self.backend.runpf(is_dc=False) - assert conv, "Cannot perform a powerflow after modifying the powergrid" + conv, *_ = self.backend.runpf(is_dc=False) + assert conv, f"Cannot perform a powerflow after modifying the powergrid with error {_}" prod_p_after, prod_q_after, prod_v_after = self.backend.generators_info() assert self.compare_vect( @@ -694,7 +701,8 @@ def test_apply_action_prod_v(self): def test_apply_action_maintenance(self): self.skip_if_needed() # retrieve some initial data to be sure only a subpart of the _grid is modified - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() + assert conv, f"powerflow diverge with , error: {_}" init_lp, *_ = self.backend.loads_info() init_gp, *_ = self.backend.generators_info() @@ -709,8 +717,8 @@ def test_apply_action_maintenance(self): self.backend.apply_action(bk_action) # compute a load flow an performs more tests - conv = self.backend.runpf() - assert conv, "Power does not converge if line {} is removed".format(19) + conv, *_ = self.backend.runpf() + assert conv, "Power does not converge if line {} is removed with error {}".format(19, _) # performs basic check after_lp, *_ = self.backend.loads_info() @@ -728,8 +736,8 @@ def test_apply_action_maintenance(self): def test_apply_action_hazard(self): self.skip_if_needed() - conv = self.backend.runpf() - assert conv, "powerflow did not converge at iteration 0" + conv, *_ = self.backend.runpf() + assert conv, f"powerflow did not converge at iteration 0, with error {_}" init_lp, *_ = self.backend.loads_info() init_gp, *_ = self.backend.generators_info() @@ -743,8 +751,8 @@ def test_apply_action_hazard(self): self.backend.apply_action(bk_action) # compute a load flow an performs more tests - conv = self.backend.runpf() - assert conv, "Power does not converge if line {} is removed".format(19) + conv, *_ = self.backend.runpf() + assert conv, "Power does not converge if line {} is removed with error {}".format(19, _) # performs basic check after_lp, *_ = self.backend.loads_info() @@ -759,7 +767,8 @@ def test_apply_action_hazard(self): def test_apply_action_disconnection(self): self.skip_if_needed() # retrieve some initial data to be sure only a subpart of the _grid is modified - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() + assert conv, f"powerflow diverge with , error: {_}" init_lp, *_ = self.backend.loads_info() init_gp, *_ = self.backend.generators_info() @@ -779,10 +788,10 @@ def test_apply_action_disconnection(self): self.backend.apply_action(bk_action) # compute a load flow an performs more tests - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() assert ( conv - ), "Powerflow does not converge if lines {} and {} are removed".format(17, 19) + ), "Powerflow does not converge if lines {} and {} are removed with error {}".format(17, 19, _) # performs basic check after_lp, *_ = self.backend.loads_info() @@ -813,7 +822,7 @@ def get_casefile(self): return "test_case14.json" def setUp(self): - self.backend = self.make_backend() + self.backend = self.make_backend_with_glue_code() self.path_matpower = self.get_path() self.case_file = self.get_casefile() with warnings.catch_warnings(): @@ -858,7 +867,8 @@ def _check_kirchoff(self): def test_get_topo_vect_speed(self): # retrieve some initial data to be sure only a subpart of the _grid is modified self.skip_if_needed() - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() + assert conv, f"powerflow diverge with , error: {_}" init_amps_flow = self.backend.get_line_flow() # check that maintenance vector is properly taken into account @@ -869,8 +879,8 @@ def test_get_topo_vect_speed(self): bk_action += action # apply the action here self.backend.apply_action(bk_action) - conv = self.backend.runpf() - assert conv + conv, *_ = self.backend.runpf() + assert conv, f"powerflow diverge with , error: {_}" after_amps_flow = self.backend.get_line_flow() topo_vect = self.backend.get_topo_vect() @@ -940,7 +950,8 @@ def test_get_topo_vect_speed(self): def test_topo_set1sub(self): # retrieve some initial data to be sure only a subpart of the _grid is modified self.skip_if_needed() - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() + assert conv, f"powerflow diverge with , error: {_}" init_amps_flow = self.backend.get_line_flow() # check that maintenance vector is properly taken into account @@ -952,8 +963,8 @@ def test_topo_set1sub(self): # apply the action here self.backend.apply_action(bk_action) - conv = self.backend.runpf() - assert conv + conv, *_ = self.backend.runpf() + assert conv, f"powerflow diverge with , error: {_}" after_amps_flow = self.backend.get_line_flow() topo_vect = self.backend.get_topo_vect() @@ -1037,7 +1048,8 @@ def test_topo_set1sub(self): def test_topo_change1sub(self): # check that switching the bus of 3 object is equivalent to set them to bus 2 (as above) self.skip_if_needed() - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() + assert conv, f"powerflow diverge with , error: {_}" init_amps_flow = self.backend.get_line_flow() # check that maintenance vector is properly taken into account @@ -1050,8 +1062,8 @@ def test_topo_change1sub(self): self.backend.apply_action(bk_action) # run the powerflow - conv = self.backend.runpf() - assert conv + conv, *_ = self.backend.runpf() + assert conv, f"powerflow diverge with , error: {_}" after_amps_flow = self.backend.get_line_flow() topo_vect = self.backend.get_topo_vect() @@ -1111,7 +1123,8 @@ def test_topo_change_1sub_twice(self): # check that switching the bus of 3 object is equivalent to set them to bus 2 (as above) # and that setting it again is equivalent to doing nothing self.skip_if_needed() - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() + assert conv, f"powerflow diverge with , error: {_}" init_amps_flow = copy.deepcopy(self.backend.get_line_flow()) # check that maintenance vector is properly taken into account @@ -1123,9 +1136,9 @@ def test_topo_change_1sub_twice(self): # apply the action here self.backend.apply_action(bk_action) - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() bk_action.reset() - assert conv + assert conv, f"powerflow diverge with , error: {_}" after_amps_flow = self.backend.get_line_flow() topo_vect = self.backend.get_topo_vect() @@ -1186,8 +1199,8 @@ def test_topo_change_1sub_twice(self): # apply the action here self.backend.apply_action(bk_action) - conv = self.backend.runpf() - assert conv + conv, *_ = self.backend.runpf() + assert conv, f"powerflow diverge with error: {_}" after_amps_flow = self.backend.get_line_flow() assert self.compare_vect(after_amps_flow, init_amps_flow) @@ -1214,8 +1227,8 @@ def test_topo_change_2sub(self): # apply the action here self.backend.apply_action(bk_action) - conv = self.backend.runpf() - assert conv, "powerflow diverge it should not" + conv, *_ = self.backend.runpf() + assert conv, f"powerflow diverge it should not, error: {_}" # check the _grid is correct topo_vect = self.backend.get_topo_vect() @@ -1422,13 +1435,13 @@ def test_get_action_to_set_storage(self): env = grid2op.make( "educ_case14_storage", test=True, - backend=self.make_backend(), + backend=self.make_backend_with_glue_code(), _add_to_name=type(self).__name__ ) env2 = grid2op.make( "educ_case14_storage", test=True, - backend=self.make_backend(), + backend=self.make_backend_with_glue_code(), _add_to_name=type(self).__name__ ) obs, *_ = env.step(env.action_space({"set_storage": [-1.0, 1.0]})) @@ -1453,7 +1466,7 @@ def test_update_from_obs(self): env = grid2op.make( "rte_case14_realistic", test=True, - backend=self.make_backend(), + backend=self.make_backend_with_glue_code(), _add_to_name=type(self).__name__ ) @@ -1559,8 +1572,7 @@ def get_path(self): return PATH_DATA_TEST def setUp(self): - self.backend = self.make_backend(detailed_infos_for_cascading_failures=True) - type(self.backend)._clear_grid_dependant_class_attributes() + self.backend = self.make_backend_with_glue_code(detailed_infos_for_cascading_failures=True) self.path_matpower = self.get_path() self.case_file = self.get_casefile() with warnings.catch_warnings(): @@ -1685,8 +1697,8 @@ def test_next_grid_state_1overflow_envNoCF(self): self.backend.load_grid(self.path_matpower, case_file) type(self.backend).set_no_storage() self.backend.assert_grid_correct() - conv = self.backend.runpf() - assert conv, "powerflow should converge at loading" + conv, *_ = self.backend.runpf() + assert conv, f"powerflow should converge at loading, error: {_}" lines_flows_init = self.backend.get_line_flow() thermal_limit = 10 * lines_flows_init thermal_limit[self.id_first_line_disco] = ( @@ -1729,8 +1741,8 @@ def test_nb_timestep_overflow_disc0(self): self.backend.load_grid(self.path_matpower, case_file) type(self.backend).set_no_storage() self.backend.assert_grid_correct() - conv = self.backend.runpf() - assert conv, "powerflow should converge at loading" + conv, *_ = self.backend.runpf() + assert conv, f"powerflow should converge at loading, error: {_}" lines_flows_init = self.backend.get_line_flow() thermal_limit = 10 * lines_flows_init @@ -1885,12 +1897,11 @@ class BaseTestChangeBusAffectRightBus(MakeBackend): def test_set_bus(self): self.skip_if_needed() # print("test_set_bus") - backend = self.make_backend() - type(backend)._clear_grid_dependant_class_attributes() + backend = self.make_backend_with_glue_code() with warnings.catch_warnings(): warnings.filterwarnings("ignore") env = grid2op.make("rte_case14_realistic", test=True, backend=backend, - _add_to_name=type(self).__name__) + _add_to_name=type(self).__name__) env.reset() action = env.action_space({"set_bus": {"lines_or_id": [(17, 2)]}}) obs, reward, done, info = env.step(action) @@ -1901,8 +1912,7 @@ def test_set_bus(self): def test_change_bus(self): self.skip_if_needed() # print("test_change_bus") - backend = self.make_backend() - type(backend)._clear_grid_dependant_class_attributes() + backend = self.make_backend_with_glue_code() with warnings.catch_warnings(): warnings.filterwarnings("ignore") env = grid2op.make("rte_case14_realistic", test=True, backend=backend, @@ -1916,8 +1926,7 @@ def test_change_bus(self): def test_change_bustwice(self): self.skip_if_needed() # print("test_change_bustwice") - backend = self.make_backend() - type(backend)._clear_grid_dependant_class_attributes() + backend = self.make_backend_with_glue_code() with warnings.catch_warnings(): warnings.filterwarnings("ignore") env = grid2op.make("rte_case14_realistic", test=True, backend=backend, @@ -1938,8 +1947,7 @@ def test_change_bustwice(self): def test_isolate_load(self): self.skip_if_needed() # print("test_isolate_load") - backend = self.make_backend() - type(backend)._clear_grid_dependant_class_attributes() + backend = self.make_backend_with_glue_code() with warnings.catch_warnings(): warnings.filterwarnings("ignore") env = grid2op.make("rte_case14_realistic", test=True, backend=backend, @@ -1951,8 +1959,7 @@ def test_isolate_load(self): def test_reco_disco_bus(self): self.skip_if_needed() # print("test_reco_disco_bus") - backend = self.make_backend() - type(backend)._clear_grid_dependant_class_attributes() + backend = self.make_backend_with_glue_code() with warnings.catch_warnings(): warnings.filterwarnings("ignore") env_case1 = grid2op.make( @@ -1978,8 +1985,7 @@ def test_reco_disco_bus(self): def test_reco_disco_bus2(self): self.skip_if_needed() # print("test_reco_disco_bus2") - backend = self.make_backend() - type(backend)._clear_grid_dependant_class_attributes() + backend = self.make_backend_with_glue_code() with warnings.catch_warnings(): warnings.filterwarnings("ignore") env_case2 = grid2op.make( @@ -2005,8 +2011,7 @@ def test_reco_disco_bus2(self): def test_reco_disco_bus3(self): self.skip_if_needed() # print("test_reco_disco_bus3") - backend = self.make_backend() - type(backend)._clear_grid_dependant_class_attributes() + backend = self.make_backend_with_glue_code() with warnings.catch_warnings(): warnings.filterwarnings("ignore") env_case2 = grid2op.make( @@ -2030,8 +2035,7 @@ def test_reco_disco_bus3(self): def test_reco_disco_bus4(self): self.skip_if_needed() # print("test_reco_disco_bus4") - backend = self.make_backend() - type(backend)._clear_grid_dependant_class_attributes() + backend = self.make_backend_with_glue_code() with warnings.catch_warnings(): warnings.filterwarnings("ignore") env_case2 = grid2op.make( @@ -2055,8 +2059,7 @@ def test_reco_disco_bus4(self): def test_reco_disco_bus5(self): self.skip_if_needed() # print("test_reco_disco_bus5") - backend = self.make_backend() - type(backend)._clear_grid_dependant_class_attributes() + backend = self.make_backend_with_glue_code() with warnings.catch_warnings(): warnings.filterwarnings("ignore") env_case2 = grid2op.make( @@ -2078,7 +2081,7 @@ def test_reco_disco_bus5(self): class BaseTestShuntAction(MakeBackend): def test_shunt_ambiguous_id_incorrect(self): self.skip_if_needed() - backend = self.make_backend() + backend = self.make_backend_with_glue_code() with warnings.catch_warnings(): warnings.filterwarnings("ignore") with grid2op.make( @@ -2094,8 +2097,8 @@ def test_shunt_ambiguous_id_incorrect(self): def test_shunt_effect(self): self.skip_if_needed() - backend1 = self.make_backend() - backend2 = self.make_backend() + backend1 = self.make_backend_with_glue_code() + backend2 = self.make_backend_with_glue_code() with warnings.catch_warnings(): warnings.filterwarnings("ignore") env_ref = grid2op.make( @@ -2170,9 +2173,8 @@ def test_shunt_effect(self): class BaseTestResetEqualsLoadGrid(MakeBackend): def setUp(self): - backend1 = self.make_backend() - backend2 = self.make_backend() - type(backend1)._clear_grid_dependant_class_attributes() + backend1 = self.make_backend_with_glue_code() + backend2 = self.make_backend_with_glue_code() with warnings.catch_warnings(): warnings.filterwarnings("ignore") self.env1 = grid2op.make("rte_case5_example", test=True, backend=backend1, _add_to_name=type(self).__name__) @@ -2304,8 +2306,7 @@ def test_obs_from_same_chronic(self): def test_combined_changes(self): # Unlimited sub changes - backend = self.make_backend() - type(backend)._clear_grid_dependant_class_attributes() + backend = self.make_backend_with_glue_code() params = grid2op.Parameters.Parameters() params.MAX_SUB_CHANGED = 999 @@ -2376,8 +2377,7 @@ def aux_random_topos_act(self, env, n=128, r=2): class BaseTestVoltageOWhenDisco(MakeBackend): def test_this(self): self.skip_if_needed() - backend = self.make_backend() - type(backend)._clear_grid_dependant_class_attributes() + backend = self.make_backend_with_glue_code() with warnings.catch_warnings(): warnings.filterwarnings("ignore") with grid2op.make("rte_case14_realistic", test=True, backend=backend, _add_to_name=type(self).__name__) as env: @@ -2392,8 +2392,7 @@ def test_this(self): class BaseTestChangeBusSlack(MakeBackend): def test_change_slack_case14(self): self.skip_if_needed() - backend = self.make_backend() - type(backend)._clear_grid_dependant_class_attributes() + backend = self.make_backend_with_glue_code() with warnings.catch_warnings(): warnings.filterwarnings("ignore") env = grid2op.make("rte_case14_realistic", test=True, backend=backend, _add_to_name=type(self).__name__) @@ -2439,8 +2438,7 @@ def _aux_test_kirchoff(self): def test_there_are_storage(self): """test the backend properly loaded the storage units""" self.skip_if_needed() - backend = self.make_backend() - type(backend)._clear_grid_dependant_class_attributes() + backend = self.make_backend_with_glue_code() with warnings.catch_warnings(): warnings.filterwarnings("ignore") self.env = grid2op.make("educ_case14_storage", test=True, backend=backend, _add_to_name=type(self).__name__) @@ -2449,8 +2447,7 @@ def test_there_are_storage(self): def test_storage_action_mw(self): """test the actions are properly implemented in the backend""" self.skip_if_needed() - backend = self.make_backend() - type(backend)._clear_grid_dependant_class_attributes() + backend = self.make_backend_with_glue_code() with warnings.catch_warnings(): warnings.filterwarnings("ignore") self.env = grid2op.make("educ_case14_storage", test=True, backend=backend, _add_to_name=type(self).__name__) @@ -2520,8 +2517,7 @@ def test_storage_action_topo(self): param = Parameters() param.NB_TIMESTEP_COOLDOWN_SUB = 0 param.NB_TIMESTEP_COOLDOWN_LINE = 0 - backend = self.make_backend() - type(backend)._clear_grid_dependant_class_attributes() + backend = self.make_backend_with_glue_code() with warnings.catch_warnings(): warnings.filterwarnings("ignore") self.env = grid2op.make( @@ -2669,8 +2665,7 @@ class BaseIssuesTest(MakeBackend): def test_issue_125(self): # https://github.com/rte-france/Grid2Op/issues/125 self.skip_if_needed() - backend = self.make_backend() - type(backend)._clear_grid_dependant_class_attributes() + backend = self.make_backend_with_glue_code() with warnings.catch_warnings(): warnings.filterwarnings("ignore") env = grid2op.make("rte_case14_realistic", test=True, backend=backend, _add_to_name=type(self).__name__) @@ -2691,8 +2686,7 @@ def test_issue_125(self): def test_issue_134(self): self.skip_if_needed() - backend = self.make_backend() - type(backend)._clear_grid_dependant_class_attributes() + backend = self.make_backend_with_glue_code() param = Parameters() param.NB_TIMESTEP_COOLDOWN_LINE = 0 @@ -2747,7 +2741,7 @@ def test_issue_134(self): } ) obs, reward, done, info = env.step(action) - assert not done + assert not done, f"Episode should not have ended here, error : {info['exception']}" assert obs.line_status[LINE_ID] == False assert obs.topo_vect[obs.line_or_pos_topo_vect[LINE_ID]] == -1 assert obs.topo_vect[obs.line_ex_pos_topo_vect[LINE_ID]] == -1 @@ -2769,8 +2763,7 @@ def test_issue_134(self): def test_issue_134_check_ambiguity(self): self.skip_if_needed() - backend = self.make_backend() - type(backend)._clear_grid_dependant_class_attributes() + backend = self.make_backend_with_glue_code() param = Parameters() param.MAX_LINE_STATUS_CHANGED = 9999 @@ -2799,8 +2792,7 @@ def test_issue_134_check_ambiguity(self): def test_issue_134_withcooldown_forrules(self): self.skip_if_needed() - backend = self.make_backend() - type(backend)._clear_grid_dependant_class_attributes() + backend = self.make_backend_with_glue_code() param = Parameters() param.NB_TIMESTEP_COOLDOWN_LINE = 20 @@ -2941,8 +2933,7 @@ def test_issue_134_withcooldown_forrules(self): def test_issue_copyenv(self): # https://github.com/BDonnot/lightsim2grid/issues/10 - backend = self.make_backend() - type(backend)._clear_grid_dependant_class_attributes() + backend = self.make_backend_with_glue_code() with warnings.catch_warnings(): warnings.filterwarnings("ignore") env1 = grid2op.make("rte_case14_realistic", test=True, backend=backend, _add_to_name=type(self).__name__) @@ -2954,8 +2945,7 @@ def test_issue_copyenv(self): class BaseStatusActions(MakeBackend): def _make_my_env(self): - backend = self.make_backend() - type(backend)._clear_grid_dependant_class_attributes() + backend = self.make_backend_with_glue_code() param = Parameters() param.NB_TIMESTEP_COOLDOWN_LINE = 0 param.NB_TIMESTEP_COOLDOWN_SUB = 0 diff --git a/grid2op/tests/BaseRedispTest.py b/grid2op/tests/BaseRedispTest.py index f12087dd4..b6a4b6567 100644 --- a/grid2op/tests/BaseRedispTest.py +++ b/grid2op/tests/BaseRedispTest.py @@ -33,7 +33,7 @@ def get_casefile(self): def setUp(self): super().setUp() # powergrid - self.backend = self.make_backend() + self.backend = self.make_backend_with_glue_code() self.path_matpower = self.get_path() self.case_file = self.get_casefile() @@ -374,7 +374,7 @@ def get_casefile(self): def setUp(self): super().setUp() # powergrid - self.backend = self.make_backend() + self.backend = self.make_backend_with_glue_code() self.path_matpower = self.get_path() self.case_file = self.get_casefile() @@ -476,7 +476,7 @@ class BaseTestRedispTooLowHigh(MakeBackend): # test bug reported in issues https://github.com/rte-france/Grid2Op/issues/44 def setUp(self) -> None: super().setUp() - backend = self.make_backend() + backend = self.make_backend_with_glue_code() with warnings.catch_warnings(): warnings.filterwarnings("ignore") self.env = grid2op.make("rte_case14_redisp", @@ -574,7 +574,7 @@ class BaseTestDispatchRampingIllegalETC(MakeBackend): def setUp(self): super().setUp() # powergrid - backend = self.make_backend() + backend = self.make_backend_with_glue_code() with warnings.catch_warnings(): warnings.filterwarnings("ignore") self.env = grid2op.make("rte_case14_test", test=True, backend=backend, @@ -842,7 +842,7 @@ class BaseTestLoadingAcceptAlmostZeroSumRedisp(MakeBackend): def setUp(self): super().setUp() # powergrid - backend = self.make_backend() + backend = self.make_backend_with_glue_code() with warnings.catch_warnings(): warnings.filterwarnings("ignore") self.env = grid2op.make("rte_case14_test", test=True, backend=backend, 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/aaa_test_backend_interface.py b/grid2op/tests/aaa_test_backend_interface.py index 16d469b82..9abf19761 100644 --- a/grid2op/tests/aaa_test_backend_interface.py +++ b/grid2op/tests/aaa_test_backend_interface.py @@ -40,7 +40,7 @@ def aux_get_env_name(self): def aux_make_backend(self) -> Backend: """do not run nor modify ! (used for this test class only)""" - backend = self.make_backend() + backend = self.make_backend_with_glue_code() backend.load_grid(self.get_path(), self.get_casefile()) backend.load_redispacthing_data("tmp") # pretend there is no generator backend.load_storage_data(self.get_path()) @@ -52,7 +52,7 @@ def aux_make_backend(self) -> Backend: def test_00create_backend(self): """Tests the backend can be created (not integrated in a grid2op environment yet)""" self.skip_if_needed() - backend = self.make_backend() + backend = self.make_backend_with_glue_code() def test_01load_grid(self): """Tests the grid can be loaded (supposes that your backend can read the grid.json in educ_case14_storage)* @@ -405,7 +405,7 @@ def test_11_modify_load_pf_getter(self): backend.apply_action(bk_act) # modification of load_p, load_q and gen_p res2 = backend.runpf(is_dc=False) - assert res2[0], "backend should not have diverge after such a little perturbation" + assert res2[0], f"backend should not have diverged after such a little perturbation. It diverges with error {res2[1]}" tmp2 = backend.loads_info() assert len(tmp) == 3, "loads_info() should return 3 elements: load_p, load_q, load_v (see doc)" load_p_after, load_q_after, load_v_after = tmp2 @@ -428,7 +428,8 @@ def test_11_modify_load_pf_getter(self): bk_act += action backend.apply_action(bk_act) # modification of load_p, load_q and gen_p res_tmp = backend.runpf(is_dc=False) - assert res_tmp[0], "backend should not have diverge after such a little perturbation" + assert res_tmp[0], (f"backend should not have diverged after such a little perturbation. " + f"It diverges with error {res_tmp[1]} for load {load_id}") tmp = backend.loads_info() assert np.abs(tmp[0][load_id] - load_p_init[load_id]) >= delta_mw / 2., f"error when trying to modify load {load_id}: check the consistency between backend.loads_info() and backend.apply_action for load_p" assert np.abs(tmp[1][load_id] - load_q_init[load_id]) >= delta_mvar / 2., f"error when trying to modify load {load_id}: check the consistency between backend.loads_info() and backend.apply_action for load_q" @@ -463,12 +464,16 @@ def test_12_modify_gen_pf_getter(self): backend.apply_action(bk_act) # modification of load_p, load_q and gen_p res2 = backend.runpf(is_dc=False) - assert res2[0], "backend should not have diverge after such a little perturbation" + assert res2[0], f"backend should not have diverged after such a little perturbation. It diverges with error {res2[1]}" tmp2 = backend.generators_info() assert len(tmp) == 3, "generators_info() should return 3 elements: gen_p, gen_q, gen_v (see doc)" gen_p_after, gen_q_after, gen_v_after = tmp2 - assert not np.allclose(gen_p_after, gen_p_init), f"gen_p does not seemed to be modified by apply_action when generators are impacted (active value): check `apply_action` for gen_p / prod_p" - assert not np.allclose(gen_v_after, gen_v_init), f"gen_v does not seemed to be modified by apply_action when generators are impacted (voltage setpoint value): check `apply_action` for gen_v / prod_v" + assert not np.allclose(gen_p_after, gen_p_init), (f"gen_p does not seemed to be modified by apply_action when " + "generators are impacted (active value): check `apply_action` " + "for gen_p / prod_p") + assert not np.allclose(gen_v_after, gen_v_init), (f"gen_v does not seemed to be modified by apply_action when " + "generators are impacted (voltage setpoint value): check `apply_action` " + "for gen_v / prod_v") # now a basic check for "one gen at a time" # NB this test cannot be done like this for "prod_v" / gen_v because two generators might be connected to the same @@ -486,7 +491,8 @@ def test_12_modify_gen_pf_getter(self): bk_act += action backend.apply_action(bk_act) res_tmp = backend.runpf(is_dc=False) - assert res_tmp[0], "backend should not have diverge after such a little perturbation" + assert res_tmp[0], (f"backend should not have diverged after such a little " + f"perturbation. It diverges with error {res_tmp[1]} for gen {gen_id}") tmp = backend.generators_info() if np.abs(tmp[0][gen_id] - gen_p_init[gen_id]) <= delta_mw / 2.: # in case of non distributed slack, backend cannot control the generator acting as the slack. @@ -541,7 +547,8 @@ def test_13_disco_reco_lines_pf_getter(self): bk_act += action1 backend.apply_action(bk_act) # disconnection of line 0 only res_disco = backend.runpf(is_dc=False) - assert res_disco[0], f"your backend diverge after disconnection of line {line_id}, which should not be the case" + # backend._grid.tell_solver_need_reset() + assert res_disco[0], f"your backend diverges after disconnection of line {line_id}, which should not be the case" tmp_or_disco = backend.lines_or_info() tmp_ex_disco = backend.lines_ex_info() assert not np.allclose(tmp_or_disco[0], p_or), f"p_or does not seemed to be modified by apply_action when a powerline is disconnected (active value): check `apply_action` for line connection disconnection" @@ -565,7 +572,7 @@ def test_13_disco_reco_lines_pf_getter(self): bk_act += action2 backend.apply_action(bk_act) # disconnection of line 0 only res_disco = backend.runpf(is_dc=False) - assert res_disco[0], f"your backend diverge after disconnection of line {line_id}, which should not be the case" + assert res_disco[0], f"your backend diverges after disconnection of line {line_id}, which should not be the case" tmp_or_reco = backend.lines_or_info() tmp_ex_reco = backend.lines_ex_info() assert not np.allclose(tmp_or_disco[0], tmp_or_reco[0]), f"p_or does not seemed to be modified by apply_action when a powerline is reconnected (active value): check `apply_action` for line connection reconnection" @@ -648,7 +655,8 @@ def test_14change_topology(self): bk_act += action1 backend.apply_action(bk_act) # everything on busbar 2 at sub 0 res = backend.runpf(is_dc=False) - assert res[0], "Your powerflow has diverged after the loading of the file, which should not happen" + assert res[0], (f"Your powerflow has diverged after a topological change at substation {sub_id} with error {res[1]}." + f"\nCheck `apply_action` for topology.") if not cls.shunts_data_available: warnings.warn(f"{type(self).__name__} test_14change_topology: This test is not performed in depth as your backend does not support shunts") @@ -1080,7 +1088,7 @@ def test_22_islanded_grid_stops_computation(self): bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 res = backend.runpf(is_dc=False) - assert not res[0], "It is expected that your backend return `False` in case of non connected grid in AC." + assert not res[0], f"It is expected that your backend return `(False, _)` in case of non connected grid in AC." error = res[1] assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" if not isinstance(error, BackendError): @@ -1096,7 +1104,7 @@ def test_22_islanded_grid_stops_computation(self): bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 res = backend.runpf(is_dc=True) - assert not res[0], "It is expected that your backend throws an exception inheriting from BackendError in case of non connected grid in DC." + assert not res[0], f"It is expected that your backend return `(False, _)` in case of non connected grid in DC." error = res[1] assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" if not isinstance(error, BackendError): @@ -1125,6 +1133,7 @@ def test_23_disco_line_v_null(self): backend.apply_action(bk_act) res = backend.runpf(is_dc=False) + assert res[0], f"Your backend diverged in AC after a line disconnection, error was {res[1]}" p_or, q_or, v_or, a_or = backend.lines_or_info() p_ex, q_ex, v_ex, a_ex = backend.lines_ex_info() assert np.allclose(v_or[line_id], 0.), f"v_or should be 0. for disconnected line, but is currently {v_or[line_id]} (AC)" @@ -1141,6 +1150,7 @@ def test_23_disco_line_v_null(self): backend.apply_action(bk_act) res = backend.runpf(is_dc=True) + assert res[0], f"Your backend diverged in DC after a line disconnection, error was {res[1]}" p_or, q_or, v_or, a_or = backend.lines_or_info() p_ex, q_ex, v_ex, a_ex = backend.lines_ex_info() assert np.allclose(v_or[line_id], 0.), f"v_or should be 0. for disconnected line, but is currently {v_or[line_id]} (DC)" @@ -1177,6 +1187,7 @@ def test_24_disco_shunt_v_null(self): bk_act += action backend.apply_action(bk_act) res = backend.runpf(is_dc=False) + assert res[0], f"Your backend diverged in AC after a shunt disconnection, error was {res[1]}" p_, q_, v_, bus_ = backend.shunt_info() assert np.allclose(v_[shunt_id], 0.), f"v should be 0. for disconnected shunt, but is currently {v_[shunt_id]} (AC)" assert bus_[shunt_id] == -1, f"bus_ should be -1 for disconnected shunt, but is currently {bus_[shunt_id]} (AC)" @@ -1189,6 +1200,7 @@ def test_24_disco_shunt_v_null(self): bk_act += action backend.apply_action(bk_act) res = backend.runpf(is_dc=True) + assert res[0], f"Your backend diverged in DC after a shunt disconnection, error was {res[1]}" p_, q_, v_, bus_ = backend.shunt_info() assert np.allclose(v_[shunt_id], 0.), f"v should be 0. for disconnected shunt, but is currently {v_[shunt_id]} (DC)" assert bus_[shunt_id] == -1, f"bus_ should be -1 for disconnected shunt, but is currently {bus_[shunt_id]} (DC)" @@ -1221,6 +1233,7 @@ def test_25_disco_storage_v_null(self): backend.apply_action(bk_act) res = backend.runpf(is_dc=False) + assert res[0], f"Your backend diverged in AC after a storage disconnection, error was {res[1]}" p_, q_, v_ = backend.storages_info() assert np.allclose(v_[storage_id], 0.), f"v should be 0. for disconnected storage, but is currently {v_[storage_id]} (AC)" @@ -1232,6 +1245,7 @@ def test_25_disco_storage_v_null(self): bk_act += action backend.apply_action(bk_act) res = backend.runpf(is_dc=True) + assert res[0], f"Your backend diverged in DC after a storage disconnection, error was {res[1]}" p_, q_, v_ = backend.storages_info() assert np.allclose(v_[storage_id], 0.), f"v should be 0. for disconnected storage, but is currently {v_[storage_id]} (AC)" @@ -1261,7 +1275,8 @@ def test_26_copy(self): # backend can be copied backend_cpy = backend.copy() assert isinstance(backend_cpy, type(backend)), f"backend.copy() is supposed to return an object of the same type as your backend. Check backend.copy()" - backend.runpf(is_dc=False) + res = backend.runpf(is_dc=False) + assert res[0], f"Your backend diverged in AC after a copy, error was {res[1]}" # now modify original one init_gen_p, *_ = backend.generators_info() init_load_p, *_ = backend.loads_info() @@ -1274,6 +1289,7 @@ def test_26_copy(self): backend.apply_action(bk_act) res = backend.runpf(is_dc=True) res_cpy = backend_cpy.runpf(is_dc=True) + assert res_cpy[0], f"Your backend diverged in DC after a copy, error was {res_cpy[1]}" p_or, *_ = backend.lines_or_info() p_or_cpy, *_ = backend_cpy.lines_or_info() @@ -1302,6 +1318,7 @@ def test_27_topo_vect_disconnect(self): cls = type(backend) res = backend.runpf(is_dc=False) + assert res[0], f"Your backend diverged in AC after loading, error was {res[1]}" topo_vect_orig = self._aux_check_topo_vect(backend) # disconnect line @@ -1313,6 +1330,7 @@ def test_27_topo_vect_disconnect(self): bk_act += action backend.apply_action(bk_act) res = backend.runpf(is_dc=False) + assert res[0], f"Your backend diverged in AC after a line disconnection, error was {res[1]}" topo_vect = self._aux_check_topo_vect(backend) error_msg = (f"Line {line_id} has been disconnected, yet according to 'topo_vect' " f"is still connected (origin side) to busbar {topo_vect[cls.line_or_pos_topo_vect[line_id]]}") @@ -1331,6 +1349,7 @@ def test_27_topo_vect_disconnect(self): bk_act += action backend.apply_action(bk_act) res = backend.runpf(is_dc=False) + assert res[0], f"Your backend diverged in AC after a storage disconnection, error was {res[1]}" topo_vect = self._aux_check_topo_vect(backend) error_msg = (f"Storage {sto_id} has been disconnected, yet according to 'topo_vect' " f"is still connected (origin side) to busbar {topo_vect[cls.storage_pos_topo_vect[line_id]]}") @@ -1353,6 +1372,7 @@ def test_27_topo_vect_disconnect(self): bk_act += action backend.apply_action(bk_act) res = backend.runpf(is_dc=False) + assert res[0], f"Your backend diverged in AC after a shunt disconnection, error was {res[1]}" topo_vect = self._aux_check_topo_vect(backend) error_msg = (f"Disconnecting a shunt should have no impact on the topo_vect vector " f"as shunt are not taken into account in this") @@ -1439,6 +1459,7 @@ def _aux_check_el_generic(self, backend, busbar_id, bk_act += action backend.apply_action(bk_act) # apply the action res = backend.runpf(is_dc=False) + assert res[0], f"Your backend diverged in AC after setting a {el_nm} on busbar {busbar_id}, error was {res[1]}" # now check the topology vector topo_vect = self._aux_check_topo_vect(backend) error_msg = (f"{el_nm} {el_id} has been moved to busbar {busbar_id}, yet according to 'topo_vect' " @@ -1464,6 +1485,7 @@ def test_28_topo_vect_set(self): cls = type(backend) res = backend.runpf(is_dc=False) + assert res[0], f"Your backend diverged in AC after loading the grid state, error was {res[1]}" topo_vect_orig = self._aux_check_topo_vect(backend) # line or @@ -1476,6 +1498,7 @@ def test_28_topo_vect_set(self): bk_act += action backend.apply_action(bk_act) res = backend.runpf(is_dc=False) + assert res[0], f"Your backend diverged in AC after setting a line (or side) on busbar 2, error was {res[1]}" topo_vect = self._aux_check_topo_vect(backend) error_msg = (f"Line {line_id} (or. side) has been moved to busbar {busbar_id}, yet according to 'topo_vect' " f"is still connected (origin side) to busbar {topo_vect[cls.line_or_pos_topo_vect[line_id]]}") @@ -1491,6 +1514,7 @@ def test_28_topo_vect_set(self): bk_act += action backend.apply_action(bk_act) res = backend.runpf(is_dc=False) + assert res[0], f"Your backend diverged in AC after setting a line (ex side) on busbar 2, error was {res[1]}" topo_vect = self._aux_check_topo_vect(backend) error_msg = (f"Line {line_id} (ex. side) has been moved to busbar {busbar_id}, yet according to 'topo_vect' " f"is still connected (ext side) to busbar {topo_vect[cls.line_ex_pos_topo_vect[line_id]]}") diff --git a/grid2op/tests/helper_path_test.py b/grid2op/tests/helper_path_test.py index f1ad50281..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 @@ -63,6 +67,13 @@ class MakeBackend(ABC, HelperTests): def make_backend(self, detailed_infos_for_cascading_failures=False) -> Backend: pass + def make_backend_with_glue_code(self, detailed_infos_for_cascading_failures=False, extra_name="") -> Backend: + Backend._clear_class_attribute() + bk = self.make_backend(detailed_infos_for_cascading_failures=detailed_infos_for_cascading_failures) + type(bk)._clear_grid_dependant_class_attributes() + type(bk).set_env_name(type(self).__name__ + extra_name) + return bk + def get_path(self) -> str: raise NotImplementedError( "This function should be implemented for the test suit you are developping" diff --git a/grid2op/tests/test_Environment.py b/grid2op/tests/test_Environment.py index ac1e96df5..055f3e865 100644 --- a/grid2op/tests/test_Environment.py +++ b/grid2op/tests/test_Environment.py @@ -845,7 +845,7 @@ def _check_env_param(self, env, param): # type of power flow to play # if True, then it will not disconnect lines above their thermal limits assert env._no_overflow_disconnection == param.NO_OVERFLOW_DISCONNECTION - assert env._hard_overflow_threshold == param.HARD_OVERFLOW_THRESHOLD + assert (env._hard_overflow_threshold == param.HARD_OVERFLOW_THRESHOLD).all() # store actions "cooldown" assert ( diff --git a/grid2op/tests/test_MaskedEnvironment.py b/grid2op/tests/test_MaskedEnvironment.py new file mode 100644 index 000000000..41ed76110 --- /dev/null +++ b/grid2op/tests/test_MaskedEnvironment.py @@ -0,0 +1,230 @@ +# Copyright (c) 2019-2023, RTE (https://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + +import warnings +import unittest +import numpy as np + +import grid2op +from grid2op.Environment import MaskedEnvironment +from grid2op.Runner import Runner +from grid2op.gym_compat import (GymEnv, + BoxGymActSpace, + BoxGymObsSpace, + DiscreteActSpace, + MultiDiscreteActSpace) + + +class TestMaskedEnvironment(unittest.TestCase): + @staticmethod + def get_mask(): + mask = np.full(20, fill_value=False, dtype=bool) + mask[[0, 1, 4, 2, 3, 6, 5]] = True # THT part + return mask + + def setUp(self) -> None: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env_in = MaskedEnvironment(grid2op.make("l2rpn_case14_sandbox", test=True, _add_to_name=type(self).__name__), + lines_of_interest=TestMaskedEnvironment.get_mask()) + self.env_out = MaskedEnvironment(grid2op.make("l2rpn_case14_sandbox", test=True, _add_to_name=type(self).__name__), + lines_of_interest=~TestMaskedEnvironment.get_mask()) + self.line_id = 3 + th_lim = self.env_in.get_thermal_limit() * 2. # avoid all problem in general + th_lim[self.line_id] /= 10. # make sure to get trouble in line 3 + # env_in: line is int the area + self.env_in.set_thermal_limit(th_lim) + # env_out: line is out of the area + self.env_out.set_thermal_limit(th_lim) + + TestMaskedEnvironment._init_env(self.env_in) + TestMaskedEnvironment._init_env(self.env_out) + + @staticmethod + def _init_env(env): + env.set_id(0) + env.seed(0) + env.reset() + + def tearDown(self) -> None: + self.env_in.close() + self.env_out.close() + return super().tearDown() + + def test_right_type(self): + assert isinstance(self.env_in, MaskedEnvironment) + assert isinstance(self.env_out, MaskedEnvironment) + assert hasattr(self.env_in, "_lines_of_interest") + assert hasattr(self.env_out, "_lines_of_interest") + assert self.env_in._lines_of_interest[self.line_id], "line_id should be in env_in" + assert not self.env_out._lines_of_interest[self.line_id], "line_id should not be in env_out" + + def test_ok(self): + act = self.env_in.action_space() + for i in range(10): + obs_in, reward, done, info = self.env_in.step(act) + obs_out, reward, done, info = self.env_out.step(act) + if i < 2: # 2 : 2 full steps already + assert obs_in.timestep_overflow[self.line_id] == i + 1, f"error for step {i}: {obs_in.timestep_overflow[self.line_id]}" + assert obs_out.timestep_overflow[self.line_id] == i + 1, f"error for step {i}: {obs_out.timestep_overflow[self.line_id]}" + else: + # cooldown applied for line 3: + # - it disconnect stuff in `self.env_in` + # - it does not affect anything in `self.env_out` + assert not obs_in.line_status[self.line_id] + assert obs_out.timestep_overflow[self.line_id] == i + 1, f"error for step {i}: {obs_out.timestep_overflow[self.line_id]}" + + def test_reset(self): + # timestep_overflow should be 0 initially even if the flow is too high + obs = self.env_in.reset() + assert obs.timestep_overflow[self.line_id] == 0 + assert obs.rho[self.line_id] > 1. + + +class TestMaskedEnvironmentCpy(TestMaskedEnvironment): + def setUp(self) -> None: + super().setUp() + init_int = self.env_in + init_out = self.env_out + self.env_in = self.env_in.copy() + self.env_out = self.env_out.copy() + init_int.close() + init_out.close() + + +class TestMaskedEnvironmentRunner(unittest.TestCase): + def setUp(self) -> None: + TestMaskedEnvironment.setUp(self) + self.max_iter = 10 + + def tearDown(self) -> None: + self.env_in.close() + self.env_out.close() + return super().tearDown() + + def test_runner_can_make(self): + runner = Runner(**self.env_in.get_params_for_runner()) + env2 = runner.init_env() + assert isinstance(env2, MaskedEnvironment) + assert (env2._lines_of_interest == self.env_in._lines_of_interest).all() + + def test_runner(self): + # create the runner + runner_in = Runner(**self.env_in.get_params_for_runner()) + runner_out = Runner(**self.env_out.get_params_for_runner()) + res_in, *_ = runner_in.run(nb_episode=1, max_iter=self.max_iter, env_seeds=[0], episode_id=[0], add_detailed_output=True) + res_out, *_ = runner_out.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_out[3] == 10 + assert np.allclose(res_in[2], 645.4992065) + assert np.allclose(res_in2[2], 645.4992065) + assert np.allclose(res_out[2], 645.7020874) + + # check detailed results + ep_data_in = res_in[-1] + ep_data_out = res_out[-1] + for i in range(self.max_iter + 1): + obs_in = ep_data_in.observations[i] + obs_out = ep_data_out.observations[i] + if i < 3: + assert obs_in.timestep_overflow[self.line_id] == i, f"error for step {i}: {obs_in.timestep_overflow[self.line_id]}" + assert obs_out.timestep_overflow[self.line_id] == i, f"error for step {i}: {obs_out.timestep_overflow[self.line_id]}" + else: + # cooldown applied for line 3: + # - it disconnect stuff in `self.env_in` + # - it does not affect anything in `self.env_out` + assert not obs_in.line_status[self.line_id], f"error for step {i}: line is not disconnected" + assert obs_out.timestep_overflow[self.line_id] == i, f"error for step {i}: {obs_out.timestep_overflow[self.line_id]}" + + + +class TestMaskedEnvironmentGym(unittest.TestCase): + def setUp(self) -> None: + TestMaskedEnvironment.setUp(self) + + def tearDown(self) -> None: + self.env_in.close() + self.env_out.close() + return super().tearDown() + + def _aux_run_envs(self, act, env_gym_in, env_gym_out): + for i in range(10): + obs_in, reward, done, truncated, info = env_gym_in.step(act) + obs_out, reward, done, truncated, info = env_gym_out.step(act) + if i < 2: # 2 : 2 full steps already + assert obs_in["timestep_overflow"][self.line_id] == i + 1, f"error for step {i}: {obs_in['timestep_overflow'][self.line_id]}" + assert obs_out['timestep_overflow'][self.line_id] == i + 1, f"error for step {i}: {obs_out['timestep_overflow'][self.line_id]}" + else: + # cooldown applied for line 3: + # - it disconnect stuff in `self.env_in` + # - it does not affect anything in `self.env_out` + assert not obs_in["line_status"][self.line_id] + assert obs_out["timestep_overflow"][self.line_id] == i + 1, f"error for step {i}: {obs_out['timestep_overflow'][self.line_id]}" + + def test_gym_with_step(self): + """test the step function also disconnects (or not) the lines""" + env_gym_in = GymEnv(self.env_in) + env_gym_out = GymEnv(self.env_out) + act = {} + self._aux_run_envs(act, env_gym_in, env_gym_out) + env_gym_in.reset() + env_gym_out.reset() + self._aux_run_envs(act, env_gym_in, env_gym_out) + + def test_gym_normal(self): + """test I can create the gym env""" + env_gym = GymEnv(self.env_in) + env_gym.reset() + + def test_gym_box(self): + """test I can create the gym env with box ob space and act space""" + env_gym_in = GymEnv(self.env_in) + env_gym_out = GymEnv(self.env_out) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env_gym_in.action_space = BoxGymActSpace(self.env_in.action_space) + env_gym_in.observation_space = BoxGymObsSpace(self.env_in.observation_space) + env_gym_out.action_space = BoxGymActSpace(self.env_out.action_space) + env_gym_out.observation_space = BoxGymObsSpace(self.env_out.observation_space) + env_gym_in.reset() + env_gym_out.reset() + + def test_gym_discrete(self): + """test I can create the gym env with discrete act space""" + env_gym_in = GymEnv(self.env_in) + env_gym_out = GymEnv(self.env_out) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env_gym_in.action_space = DiscreteActSpace(self.env_in.action_space) + env_gym_out.action_space = DiscreteActSpace(self.env_out.action_space) + env_gym_in.reset() + env_gym_out.reset() + act = 0 + self._aux_run_envs(act, env_gym_in, env_gym_out) + + + def test_gym_multidiscrete(self): + """test I can create the gym env with multi discrete act space""" + env_gym_in = GymEnv(self.env_in) + env_gym_out = GymEnv(self.env_out) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env_gym_in.action_space = MultiDiscreteActSpace(self.env_in.action_space) + env_gym_out.action_space = MultiDiscreteActSpace(self.env_out.action_space) + env_gym_in.reset() + env_gym_out.reset() + act = env_gym_in.action_space.sample() + act[:] = 0 + self._aux_run_envs(act, env_gym_in, env_gym_out) + + +if __name__ == "__main__": + unittest.main() diff --git a/grid2op/tests/test_Runner.py b/grid2op/tests/test_Runner.py index d4324015f..1d8dcd233 100644 --- a/grid2op/tests/test_Runner.py +++ b/grid2op/tests/test_Runner.py @@ -505,6 +505,8 @@ def test_backward_compatibility(self): "1.9.3", "1.9.4", "1.9.5", + "1.9.6", + "1.9.7", ] curr_version = "test_version" assert ( 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 5e4a176d0..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" ], @@ -96,7 +96,11 @@ def my_test_suite(): if sys.version_info.minor == 12: # numba is not available for python 3.12 at the moment - pkgs["extras"]["test"] = [el for el in pkgs["extras"]["test"] if not ("numba" in el)] + pkgs["extras"]["test"] = [el for el in pkgs["extras"]["test"] if (not ("numba" in el) and + not ("gym" in el) and + not ('stable-baselines3' in el) + ) + ] setup(description='An gymnasium compatible environment to model sequential decision making for powersystems', long_description=long_description,