diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7b585557..50551548 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -39,6 +39,8 @@ Change Log - [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) +- [ADDED] A type of environment that does not perform the "emulation of the protections" + for some part of the grid (`MaskedEnvironment`) - [IMPROVED] the CI speed: by not testing every possible numpy version but only most ancient and most recent - [IMPROVED] Runner now test grid2op version 1.9.6 and 1.9.7 - [IMPROVED] refacto `gridobj_cls._clear_class_attribute` and `gridobj_cls._clear_grid_dependant_class_attributes` diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index a72185ed..a06fc00b 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: diff --git a/grid2op/Environment/__init__.py b/grid2op/Environment/__init__.py index 1375aad0..a9a4197b 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 8d92d2d6..3f8ccf75 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -342,7 +342,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 +626,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 +1204,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 +1219,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 +1265,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,7 +1278,7 @@ 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 @@ -2957,6 +2960,10 @@ 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_ ): @@ -2965,9 +2972,7 @@ def _aux_run_pf_after_state_properly_set( 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: @@ -3328,7 +3333,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/maskedEnvironment.py b/grid2op/Environment/maskedEnvironment.py new file mode 100644 index 00000000..7b2ad5ce --- /dev/null +++ b/grid2op/Environment/maskedEnvironment.py @@ -0,0 +1,150 @@ +# 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 + """ + CAN_SKIP_TS = False # some steps can be more than one time steps + 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 TimedOutEnvironment 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] = 1e-7 * np.finfo(dt_float).max # some kind of infinity value + # NB we multiply np.finfo(dt_float).max by a small number to avoid overflow + # indeed, _hard_overflow_threshold is multiply by the flow on the lines + self._nb_timestep_overflow_allowed[~self._lines_of_interest] = np.iinfo(dt_int).max - 1 # some kind of infinity value + + 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 + + @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/timedOutEnv.py b/grid2op/Environment/timedOutEnv.py index fcccd764..af5558eb 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, @@ -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 ---------- diff --git a/grid2op/tests/BaseBackendTest.py b/grid2op/tests/BaseBackendTest.py index 1eea313d..3a3bb46e 100644 --- a/grid2op/tests/BaseBackendTest.py +++ b/grid2op/tests/BaseBackendTest.py @@ -178,7 +178,7 @@ 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!" + assert conv, f"powerflow diverge it is not supposed to! Error {_}" with warnings.catch_warnings(): warnings.filterwarnings("ignore") @@ -200,7 +200,7 @@ def test_assert_grid_correct(self): 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!" + assert conv, f"powerflow diverge it is not supposed to! Error {_}" backend.assert_grid_correct_after_powerflow() @@ -263,7 +263,7 @@ def test_theta_ok(self): def test_runpf_dc(self): self.skip_if_needed() conv, *_ = self.backend.runpf(is_dc=True) - assert conv + assert conv, f"powerflow diverge with error {_}" true_values_dc = np.array( [ 147.83859556, @@ -318,6 +318,7 @@ def test_runpf(self): ] ) 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) @@ -326,7 +327,7 @@ 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" + 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) @@ -342,7 +343,7 @@ def test_voltages_correct_load_gen(self): # of the powerline connected to it. conv, *_ = self.backend.runpf(is_dc=False) - assert conv, "powerflow diverge at loading" + 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() @@ -525,11 +526,11 @@ def test_pf_ac_dc(self): ] ) conv, *_ = self.backend.runpf(is_dc=True) - assert conv + 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 + 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) @@ -574,8 +575,8 @@ def test_disconnect_line(self): 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] @@ -584,6 +585,7 @@ def test_disconnect_line(self): def test_donothing_action(self): self.skip_if_needed() 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() @@ -601,7 +603,7 @@ def test_donothing_action(self): 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" + 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) @@ -613,7 +615,7 @@ def test_apply_action_active_value(self): # 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)" + 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() @@ -628,6 +630,7 @@ def test_apply_action_active_value(self): bk_action += action self.backend.apply_action(bk_action) 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 @@ -678,7 +681,7 @@ 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" + 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( @@ -688,7 +691,7 @@ def test_apply_action_prod_v(self): 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" + 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( @@ -699,6 +702,7 @@ 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() + assert conv, f"powerflow diverge with , error: {_}" init_lp, *_ = self.backend.loads_info() init_gp, *_ = self.backend.generators_info() @@ -714,7 +718,7 @@ def test_apply_action_maintenance(self): # compute a load flow an performs more tests conv, *_ = self.backend.runpf() - assert conv, "Power does not converge if line {} is removed".format(19) + assert conv, "Power does not converge if line {} is removed with error {}".format(19, _) # performs basic check after_lp, *_ = self.backend.loads_info() @@ -733,7 +737,7 @@ 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" + assert conv, f"powerflow did not converge at iteration 0, with error {_}" init_lp, *_ = self.backend.loads_info() init_gp, *_ = self.backend.generators_info() @@ -748,7 +752,7 @@ def test_apply_action_hazard(self): # compute a load flow an performs more tests conv, *_ = self.backend.runpf() - assert conv, "Power does not converge if line {} is removed".format(19) + assert conv, "Power does not converge if line {} is removed with error {}".format(19, _) # performs basic check after_lp, *_ = self.backend.loads_info() @@ -764,6 +768,7 @@ 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() + assert conv, f"powerflow diverge with , error: {_}" init_lp, *_ = self.backend.loads_info() init_gp, *_ = self.backend.generators_info() @@ -786,7 +791,7 @@ def test_apply_action_disconnection(self): 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() @@ -863,6 +868,7 @@ 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() + assert conv, f"powerflow diverge with , error: {_}" init_amps_flow = self.backend.get_line_flow() # check that maintenance vector is properly taken into account @@ -874,7 +880,7 @@ def test_get_topo_vect_speed(self): # apply the action here self.backend.apply_action(bk_action) conv, *_ = self.backend.runpf() - assert conv + assert conv, f"powerflow diverge with , error: {_}" after_amps_flow = self.backend.get_line_flow() topo_vect = self.backend.get_topo_vect() @@ -945,6 +951,7 @@ 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() + assert conv, f"powerflow diverge with , error: {_}" init_amps_flow = self.backend.get_line_flow() # check that maintenance vector is properly taken into account @@ -957,7 +964,7 @@ def test_topo_set1sub(self): # apply the action here self.backend.apply_action(bk_action) conv, *_ = self.backend.runpf() - assert conv + assert conv, f"powerflow diverge with , error: {_}" after_amps_flow = self.backend.get_line_flow() topo_vect = self.backend.get_topo_vect() @@ -1042,6 +1049,7 @@ 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() + assert conv, f"powerflow diverge with , error: {_}" init_amps_flow = self.backend.get_line_flow() # check that maintenance vector is properly taken into account @@ -1055,7 +1063,7 @@ def test_topo_change1sub(self): # run the powerflow conv, *_ = self.backend.runpf() - assert conv + assert conv, f"powerflow diverge with , error: {_}" after_amps_flow = self.backend.get_line_flow() topo_vect = self.backend.get_topo_vect() @@ -1116,6 +1124,7 @@ def test_topo_change_1sub_twice(self): # and that setting it again is equivalent to doing nothing self.skip_if_needed() 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 @@ -1129,7 +1138,7 @@ def test_topo_change_1sub_twice(self): self.backend.apply_action(bk_action) 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() @@ -1191,7 +1200,7 @@ def test_topo_change_1sub_twice(self): # apply the action here self.backend.apply_action(bk_action) conv, *_ = self.backend.runpf() - assert conv + 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) @@ -1219,7 +1228,7 @@ 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" + assert conv, f"powerflow diverge it should not, error: {_}" # check the _grid is correct topo_vect = self.backend.get_topo_vect() @@ -1689,7 +1698,7 @@ def test_next_grid_state_1overflow_envNoCF(self): type(self.backend).set_no_storage() self.backend.assert_grid_correct() conv, *_ = self.backend.runpf() - assert conv, "powerflow should converge at loading" + 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] = ( @@ -1733,7 +1742,7 @@ def test_nb_timestep_overflow_disc0(self): type(self.backend).set_no_storage() self.backend.assert_grid_correct() conv, *_ = self.backend.runpf() - assert conv, "powerflow should converge at loading" + assert conv, f"powerflow should converge at loading, error: {_}" lines_flows_init = self.backend.get_line_flow() thermal_limit = 10 * lines_flows_init diff --git a/grid2op/tests/test_MaskedEnvironment.py b/grid2op/tests/test_MaskedEnvironment.py new file mode 100644 index 00000000..11cd2f96 --- /dev/null +++ b/grid2op/tests/test_MaskedEnvironment.py @@ -0,0 +1,239 @@ +# 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): + def get_mask(self): + 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=self.get_mask()) + self.env_out = MaskedEnvironment(grid2op.make("l2rpn_case14_sandbox", test=True, _add_to_name=type(self).__name__), + lines_of_interest=~self.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) + + self._init_env(self.env_in) + self._init_env(self.env_out) + + def _init_env(self, 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_in.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_in.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 TestTimedOutEnvironmentCpy(TestMaskedEnvironment): + def setUp(self) -> None: + super().setUp() + init_int = self.env_in.copy() + init_out = self.env_out.copy() + self.env0 = self.env_in.copy() + self.env1 = self.env_out.copy() + init_int.close() + init_out.close() + + +# class TestTOEnvRunner(unittest.TestCase): +# def get_timeout_ms(self): +# return 200 + +# def setUp(self) -> None: +# with warnings.catch_warnings(): +# warnings.filterwarnings("ignore") +# self.env1 = TimedOutEnvironment(grid2op.make("l2rpn_case14_sandbox", test=True, _add_to_name=type(self).__name__), +# time_out_ms=self.get_timeout_ms()) +# params = self.env1.parameters +# params.NO_OVERFLOW_DISCONNECTION = True +# self.env1.change_parameters(params) +# self.cum_reward = 645.70208 +# self.max_iter = 10 + +# def tearDown(self) -> None: +# self.env1.close() +# return super().tearDown() + +# def test_runner_can_make(self): +# runner = Runner(**self.env1.get_params_for_runner()) +# env2 = runner.init_env() +# assert isinstance(env2, TimedOutEnvironment) +# assert env2.time_out_ms == self.get_timeout_ms() + +# def test_runner_noskip(self): +# agent = AgentOK(self.env1) +# runner = Runner(**self.env1.get_params_for_runner(), +# agentClass=None, +# agentInstance=agent) +# res = runner.run(nb_episode=1, +# max_iter=self.max_iter) +# _, _, cum_reward, timestep, max_ts = res[0] +# assert abs(cum_reward - self.cum_reward) <= 1e-5 + +# def test_runner_skip1(self): +# agent = AgentKO(self.env1) +# runner = Runner(**self.env1.get_params_for_runner(), +# agentClass=None, +# agentInstance=agent) +# res = runner.run(nb_episode=1, +# max_iter=self.max_iter) +# _, _, cum_reward, timestep, max_ts = res[0] +# assert abs(cum_reward - self.cum_reward) <= 1e-5 + +# def test_runner_skip2(self): +# agent = AgentKO2(self.env1) +# runner = Runner(**self.env1.get_params_for_runner(), +# agentClass=None, +# agentInstance=agent) +# res = runner.run(nb_episode=1, +# max_iter=self.max_iter) +# _, _, cum_reward, timestep, max_ts = res[0] +# assert abs(cum_reward - self.cum_reward) <= 1e-5 + +# def test_runner_skip2_2ep(self): +# agent = AgentKO2(self.env1) +# runner = Runner(**self.env1.get_params_for_runner(), +# agentClass=None, +# agentInstance=agent) +# res = runner.run(nb_episode=2, +# max_iter=self.max_iter) +# _, _, cum_reward, timestep, max_ts = res[0] +# assert abs(cum_reward - self.cum_reward) <= 1e-5 +# _, _, cum_reward, timestep, max_ts = res[1] +# assert abs(cum_reward - 648.90795) <= 1e-5 + + +# class TestTOEnvGym(unittest.TestCase): +# def get_timeout_ms(self): +# return 400. + +# def setUp(self) -> None: +# with warnings.catch_warnings(): +# warnings.filterwarnings("ignore") +# self.env1 = TimedOutEnvironment(grid2op.make("l2rpn_case14_sandbox", test=True, _add_to_name=type(self).__name__), +# time_out_ms=self.get_timeout_ms()) + +# def tearDown(self) -> None: +# self.env1.close() +# return super().tearDown() + +# def test_gym_with_step(self): +# """test the step function also makes the 'do nothing'""" +# self.skipTest("On docker execution time is too unstable") +# env_gym = GymEnv(self.env1) +# env_gym.reset() + +# agentok = AgentOK(env_gym) +# for i in range(10): +# act = agentok.act_gym(None, None, None) +# for k in act: +# act[k][:] = 0 +# *_, info = env_gym.step(act) +# assert info["nb_do_nothing"] == 0 +# assert info["nb_do_nothing_made"] == 0 +# assert env_gym.init_env._nb_dn_last == 0 + +# env_gym.reset() +# agentko = AgentKO1(env_gym) +# for i in range(10): +# act = agentko.act_gym(None, None, None) +# for k in act: +# act[k][:] = 0 +# *_, info = env_gym.step(act) +# assert info["nb_do_nothing"] == 1 +# assert info["nb_do_nothing_made"] == 1 +# assert env_gym.init_env._nb_dn_last == 1 + +# def test_gym_normal(self): +# """test I can create the gym env""" +# env_gym = GymEnv(self.env1) +# env_gym.reset() + +# def test_gym_box(self): +# """test I can create the gym env with box ob space and act space""" +# env_gym = GymEnv(self.env1) +# with warnings.catch_warnings(): +# warnings.filterwarnings("ignore") +# env_gym.action_space = BoxGymActSpace(self.env1.action_space) +# env_gym.observation_space = BoxGymObsSpace(self.env1.observation_space) +# env_gym.reset() + +# def test_gym_discrete(self): +# """test I can create the gym env with discrete act space""" +# env_gym = GymEnv(self.env1) +# with warnings.catch_warnings(): +# warnings.filterwarnings("ignore") +# env_gym.action_space = DiscreteActSpace(self.env1.action_space) +# env_gym.reset() + +# def test_gym_multidiscrete(self): +# """test I can create the gym env with multi discrete act space""" +# env_gym = GymEnv(self.env1) +# with warnings.catch_warnings(): +# warnings.filterwarnings("ignore") +# env_gym.action_space = MultiDiscreteActSpace(self.env1.action_space) +# env_gym.reset() + + +if __name__ == "__main__": + unittest.main()