From 8b70407ce3e282ad365690895f6bddc0b02b299c Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Tue, 26 Nov 2024 17:44:48 +0100 Subject: [PATCH] fixing broken tests (hopefully) and improve AAA backend test suite for new feature Signed-off-by: DONNOT Benjamin --- grid2op/Backend/backend.py | 46 ++-- grid2op/Backend/pandaPowerBackend.py | 17 +- grid2op/Environment/environment.py | 7 +- grid2op/tests/aaa_test_backend_interface.py | 285 +++++++++++++------- grid2op/tests/test_attached_envs_compat.py | 25 +- grid2op/tests/test_n_busbar_per_sub.py | 10 +- 6 files changed, 252 insertions(+), 138 deletions(-) diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index 9ba913fc..fd71dde5 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -121,6 +121,9 @@ class Backend(GridObjects, ABC): _complete_action_class : "Optional[grid2op.Action.CompleteAction]"= None ERR_INIT_POWERFLOW : str = "Power cannot be computed on the first time step, please check your data." + ERR_DETACHMENT : str = ("One or more {} were isolated from the grid " + "but this is not allowed or not supported (Game Over) (detachment_is_allowed is False), " + "check {} {}") def __init__(self, detailed_infos_for_cascading_failures:bool=False, can_be_copied:bool=True, @@ -181,7 +184,7 @@ def __init__(self, self.n_busbar_per_sub: int = DEFAULT_N_BUSBAR_PER_SUB #: .. versionadded: 1.11.0 - self._missing_detachment_support : bool = True + self._missing_detachment_support_info : bool = True self.detachment_is_allowed : bool = DEFAULT_ALLOW_DETACHMENT def can_handle_more_than_2_busbar(self): @@ -270,7 +273,7 @@ def can_handle_detachment(self): We highly recommend you do not try to override this function. At least, at time of writing there is no good reason to do so. """ - self._missing_detachment_support = False + self._missing_detachment_support_info = False self.detachment_is_allowed = type(self).detachment_is_allowed def cannot_handle_detachment(self): @@ -297,7 +300,7 @@ def cannot_handle_detachment(self): We highly recommend you do not try to override this function. At least, at time of writing there is no good reason to do so. """ - self._missing_detachment_support = False + self._missing_detachment_support_info = False if type(self).detachment_is_allowed != DEFAULT_ALLOW_DETACHMENT: warnings.warn("You asked in 'make' function to allow shedding. This is" f"not possible with a backend of type {type(self)}.") @@ -1079,7 +1082,7 @@ def _runpf_with_diverging_exception(self, is_dc : bool) -> Optional[Exception]: """ conv = False exc_me = None - + cls = type(self) try: conv, exc_me = self.runpf(is_dc=is_dc) # run powerflow @@ -1091,21 +1094,28 @@ def _runpf_with_diverging_exception(self, is_dc : bool) -> Optional[Exception]: # Check if loads/gens have been detached and if this is allowed, otherwise raise an error # .. versionadded:: 1.11.0 topo_vect = self.get_topo_vect() - load_buses = topo_vect[self.load_pos_topo_vect] - if not self.detachment_is_allowed and (load_buses == -1).any(): - raise Grid2OpException(f"One or more loads were detached before powerflow in Backend {type(self).__name__}" - "but this is not allowed or not supported (Game Over)") + load_buses = topo_vect[cls.load_pos_topo_vect] + if not cls.detachment_is_allowed and (load_buses == -1).any(): + raise BackendError(cls.ERR_DETACHMENT.format("loads", "loads", (load_buses == -1).nonzero()[0])) - gen_buses = topo_vect[self.gen_pos_topo_vect] + gen_buses = topo_vect[cls.gen_pos_topo_vect] + if not cls.detachment_is_allowed and (gen_buses == -1).any(): + raise BackendError(cls.ERR_DETACHMENT.format("gens", "gens", (gen_buses == -1).nonzero()[0])) - if not self.detachment_is_allowed and (gen_buses == -1).any(): - raise Grid2OpException(f"One or more generators were detached before powerflow in Backend {type(self).__name__}" - "but this is not allowed or not supported (Game Over)") + if cls.n_storage > 0: + storage_buses = topo_vect[cls.storage_pos_topo_vect] + storage_p, *_ = self.storages_info() + sto_maybe_error = (storage_buses == -1) & (np.abs(storage_p) >= 1e-6) + if not cls.detachment_is_allowed and sto_maybe_error.any(): + raise BackendError((cls.ERR_DETACHMENT.format("storages", "storages", sto_maybe_error.nonzero()[0]) + + " NB storage units are allowed to be disconnected even if " + "`detachment_is_allowed` is False but only if the don't produce active power.")) + except Grid2OpException as exc_: exc_me = exc_ if not conv and exc_me is None: - exc_me = DivergingPowerflow( + exc_me = BackendError( "GAME OVER: Powerflow has diverged during computation " "or a load has been disconnected or a generator has been disconnected." ) @@ -2181,8 +2191,8 @@ def assert_grid_correct(self, _local_dir_cls=None) -> None: "attribute. This is known issue in lightims2grid <= 0.7.5. Please " "upgrade your backend. This will raise an error in the future.") - if hasattr(self, "_missing_detachment_support"): - if self._missing_detachment_support: + if hasattr(self, "_missing_detachment_support_info"): + if self._missing_detachment_support_info: warnings.warn("The backend implementation you are using is probably too old to take advantage of the " "new feature added in grid2op 1.11.0: the possibility " "to detach loads or generators without leading to an immediate game over. " @@ -2194,12 +2204,12 @@ def assert_grid_correct(self, _local_dir_cls=None) -> None: "\nAnd of course, ideally, if the current implementation " "of your backend cannot handle detachment then change it :-)\n" "Your backend will behave as if it did not support it.") - self._missing_detachment_support = False + self._missing_detachment_support_info = False self.detachment_is_allowed = DEFAULT_ALLOW_DETACHMENT else: - self._missing_detachment_support = False + self._missing_detachment_support_info = False self.detachment_is_allowed = DEFAULT_ALLOW_DETACHMENT - warnings.warn("Your backend is missing the `_missing_detachment_support` " + warnings.warn("Your backend is missing the `_missing_detachment_support_info` " "attribute.") orig_type = type(self) diff --git a/grid2op/Backend/pandaPowerBackend.py b/grid2op/Backend/pandaPowerBackend.py index 1adda34d..533017b1 100644 --- a/grid2op/Backend/pandaPowerBackend.py +++ b/grid2op/Backend/pandaPowerBackend.py @@ -829,6 +829,9 @@ def storage_deact_for_backward_comaptibility(self) -> None: self.storage_p = np.full(cls.n_storage, dtype=dt_float, fill_value=np.NaN) self.storage_q = np.full(cls.n_storage, dtype=dt_float, fill_value=np.NaN) self.storage_v = np.full(cls.n_storage, dtype=dt_float, fill_value=np.NaN) + self._topo_vect.flags.writeable = True + self._topo_vect.resize(cls.dim_topo) + self._topo_vect.flags.writeable = False self._get_topo_vect() def _convert_id_topo(self, id_big_topo): @@ -899,7 +902,6 @@ def apply_action(self, backendAction: Union["grid2op.Action._backendAction._Back tmp_stor_p = self._grid.storage["p_mw"] if (storage.changed).any(): tmp_stor_p.iloc[storage.changed] = storage.values[storage.changed] - # topology of the storage stor_bus = backendAction.get_storages_bus() new_bus_num = dt_int(1) * self._grid.storage["bus"].values @@ -1183,11 +1185,8 @@ def runpf(self, is_dc : bool=False) -> Tuple[bool, Union[Exception, None]]: self.storage_v[:], self.storage_theta[:], ) = self._storages_info() + deact_storage = ~np.isfinite(self.storage_v) - if (np.abs(self.storage_p[deact_storage]) > self.tol).any(): - raise pp.powerflow.LoadflowNotConverged( - "Isolated storage set to absorb / produce something" - ) self.storage_p[deact_storage] = 0.0 self.storage_q[deact_storage] = 0.0 self.storage_v[deact_storage] = 0.0 @@ -1336,7 +1335,7 @@ def copy(self) -> "PandaPowerBackend": res._in_service_trafo_col_id = self._in_service_trafo_col_id res._missing_two_busbars_support_info = self._missing_two_busbars_support_info - res._missing_detachment_support = self._missing_detachment_support + res._missing_detachment_support_info = self._missing_detachment_support_info res.div_exception = self.div_exception return res @@ -1552,8 +1551,10 @@ def _storages_info(self): if self.n_storage: # this is because we support "backward comaptibility" feature. So the storage can be # deactivated from the Environment... - p_storage = self._grid.res_storage["p_mw"].values.astype(dt_float) - q_storage = self._grid.res_storage["q_mvar"].values.astype(dt_float) + # p_storage = self._grid.res_storage["p_mw"].values.astype(dt_float) + # q_storage = self._grid.res_storage["q_mvar"].values.astype(dt_float) + p_storage = self._grid.storage["p_mw"].values.astype(dt_float) + q_storage = self._grid.storage["q_mvar"].values.astype(dt_float) v_storage = ( self._grid.res_bus.loc[self._grid.storage["bus"].values][ "vm_pu" diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 431ea224..299e0104 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -265,7 +265,7 @@ def _init_backend( need_process_backend = False if not self.backend.is_loaded: if hasattr(self.backend, "init_pp_backend") and self.backend.init_pp_backend is not None: - # hack for lightsim2grid ... + # hack for legacy lightsim2grid ... if type(self.backend.init_pp_backend)._INIT_GRID_CLS is not None: type(self.backend.init_pp_backend)._INIT_GRID_CLS._clear_grid_dependant_class_attributes() type(self.backend.init_pp_backend)._clear_grid_dependant_class_attributes() @@ -282,7 +282,6 @@ def _init_backend( type(self.backend).set_env_name(self.name) type(self.backend).set_n_busbar_per_sub(self._n_busbar) type(self.backend).set_detachment_is_allowed(self._allow_detachment) - if self._compat_glop_version is not None: type(self.backend).glop_version = self._compat_glop_version @@ -296,8 +295,8 @@ def _init_backend( except BackendError as exc_: self.backend.redispatching_unit_commitment_availble = False warnings.warn(f"Impossible to load redispatching data. This is not an error but you will not be able " - f"to use all grid2op functionalities. " - f"The error was: \"{exc_}\"") + f"to use all grid2op functionalities. " + f"The error was: \"{exc_}\"") exc_ = self.backend.load_grid_layout(self.get_path_env()) if exc_ is not None: warnings.warn( diff --git a/grid2op/tests/aaa_test_backend_interface.py b/grid2op/tests/aaa_test_backend_interface.py index 40409eb5..71cabe00 100644 --- a/grid2op/tests/aaa_test_backend_interface.py +++ b/grid2op/tests/aaa_test_backend_interface.py @@ -66,7 +66,7 @@ def test_00create_backend(self): "or `self.cannot_handle_more_than_2_busbar()` in the `load_grid` " "method of your backend. Please refer to documentation for more information.") - if not backend._missing_detachment_support: + if not backend._missing_detachment_support_info: warnings.warn("You should call either `self.can_handle_detachment()` " "or `self.cannot_handle_detachment()` in the `load_grid` " "method of your backend. Please refer to documentation for more information.") @@ -803,8 +803,40 @@ def test_15_reset(self): assert np.allclose(q2_or, q_or), f"The q_or flow differ between its original value and after a reset. Check backend.reset()" assert np.allclose(v2_or, v_or), f"The v_or differ between its original value and after a reset. Check backend.reset()" assert np.allclose(a2_or, a_or), f"The a_or flow differ between its original value and after a reset. Check backend.reset()" - - def test_16_isolated_load_stops_computation(self): + + def _aux_aux_test_detachment_should_fail(self, maybe_exc): + assert maybe_exc is not None, "When your backend diverges, we expect it throws an exception (second return value)" + assert isinstance(maybe_exc, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(maybe_exc)}" + if not isinstance(maybe_exc, BackendError): + warnings.warn("The error returned by your backend when it stopped (due to isolated element) should preferably inherit from BackendError") + + def _aux_test_detachment(self, backend : Backend, is_dc=True, detachment_should_pass = False): + """auxilliary method to handle the "legacy" code, when the backend was expected to + handle the error """ + str_ = "DC" if is_dc else "AC" + if backend._missing_detachment_support_info: + # legacy behaviour, should behave as if it diverges + # for new (>= 1.11.0) behaviour, it is catched in the method `_runpf_with_diverging_exception` + res = backend.runpf(is_dc=is_dc) + assert not res[0], f"It is expected (at time of writing) that your backend returns `False` in case of isolated loads in {str_}." + maybe_exc = res[1] + detachment_allowed = False + else: + # new (1.11.0) test here + maybe_exc = backend._runpf_with_diverging_exception(is_dc=is_dc) + detachment_allowed = type(backend).detachment_is_allowed + if not detachment_allowed: + # should raise in all cases as the backend prevent detachment + self._aux_aux_test_detachment_should_fail(maybe_exc) + elif not detachment_should_pass: + # it expected that even if the backend supports detachment, + # this test should fail (kwargs detachment_should_pass set to False) + self._aux_aux_test_detachment_should_fail(maybe_exc) + else: + # detachment should not make things diverge + assert maybe_exc is None, f"Your backend supports detachment of loads or generator, yet it diverges when some loads / generators are disconnected." + + def test_16_isolated_load_stops_computation(self, allow_detachment=DEFAULT_ALLOW_DETACHMENT): """Tests that an isolated load will be spotted by the `run_pf` method and forwarded to grid2op by returining `False, an_exception` (in AC and DC) This test supposes that : @@ -818,9 +850,12 @@ def test_16_isolated_load_stops_computation(self): Currently this stops the computation of the environment and lead to a game over. This behaviour might change in the future. + + .. note:: + This test is also used in `attr:AAATestBackendAPI.test_33_allow_detachment` """ self.skip_if_needed() - backend = self.aux_make_backend() + backend = self.aux_make_backend(allow_detachment=allow_detachment) cls = type(backend) # a load alone on a bus @@ -830,13 +865,8 @@ def test_16_isolated_load_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 (at time of writing) that your backend returns `False` in case of isolated loads in AC." - assert res[1] is not None, "When your backend diverges, we expect it throws an exception (second return value)" - 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): - warnings.warn("The error returned by your backend when it stopped (due to isolated shunt) should preferably inherit from BackendError") - + self._aux_test_detachment(backend, is_dc=False) + backend.reset(self.get_path(), self.get_casefile()) # a load alone on a bus action = type(backend)._complete_action_class() @@ -844,15 +874,9 @@ def test_16_isolated_load_stops_computation(self): bk_act = type(backend).my_bk_act_class() 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 (at time of writing) that your backend returns `False` in case of isolated loads in DC." - assert res[1] is not None, "When your backend diverges, we expect it throws an exception (second return value)" - 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): - warnings.warn("The error returned by your backend when it stopped (due to isolated shunt) should preferably inherit from BackendError") - - def test_17_isolated_gen_stops_computation(self): + self._aux_test_detachment(backend, is_dc=True) + + def test_17_isolated_gen_stops_computation(self, allow_detachment=DEFAULT_ALLOW_DETACHMENT): """Tests that an isolated generator will be spotted by the `run_pf` method and forwarded to grid2op by returining `False, an_exception` (in AC and DC) This test supposes that : @@ -866,9 +890,12 @@ def test_17_isolated_gen_stops_computation(self): Currently this stops the computation of the environment and lead to a game over. This behaviour might change in the future. + + .. note:: + This test is also used in `attr:AAATestBackendAPI.test_33_allow_detachment` """ self.skip_if_needed() - backend = self.aux_make_backend() + backend = self.aux_make_backend(allow_detachment=allow_detachment) cls = type(backend) # disconnect a gen @@ -877,14 +904,8 @@ def test_17_isolated_gen_stops_computation(self): bk_act = type(backend).my_bk_act_class() 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 (at time of writing) that your backend returns `False` in case of isolated gen." - assert res[1] is not None, "When your backend diverges, we expect it throws an exception (second return value)" - 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): - warnings.warn("The error returned by your backend when it stopped (due to isolated shunt) should preferably inherit from BackendError") - + self._aux_test_detachment(backend, is_dc=False) + backend.reset(self.get_path(), self.get_casefile()) # disconnect a gen action = type(backend)._complete_action_class() @@ -892,15 +913,9 @@ def test_17_isolated_gen_stops_computation(self): bk_act = type(backend).my_bk_act_class() 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 (at time of writing) that your backend returns `False` in case of isolated gen." - assert res[1] is not None, "When your backend diverges, we expect it throws an exception (second return value)" - 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): - warnings.warn("The error returned by your backend when it stopped (due to isolated shunt) should preferably inherit from BackendError") - - def test_18_isolated_shunt_stops_computation(self): + self._aux_test_detachment(backend, is_dc=True) + + def test_18_isolated_shunt_stops_computation(self, allow_detachment=DEFAULT_ALLOW_DETACHMENT): """Tests test that an isolated shunt will be spotted by the `run_pf` method and forwarded to grid2op by returining `False, an_exception` (in AC and DC) This test supposes that : @@ -916,9 +931,12 @@ def test_18_isolated_shunt_stops_computation(self): Currently this stops the computation of the environment and lead to a game over. This behaviour might change in the future. + + .. note:: + This test is also used in `attr:AAATestBackendAPI.test_33_allow_detachment` """ self.skip_if_needed() - backend = self.aux_make_backend() + backend = self.aux_make_backend(allow_detachment=allow_detachment) cls = type(backend) if not cls.shunts_data_available: self.skipTest("Your backend does not support shunts") @@ -931,14 +949,8 @@ def test_18_isolated_shunt_stops_computation(self): bk_act = type(backend).my_bk_act_class() 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 (at time of writing) that your backend returns `False` in case of isolated shunt." - assert res[1] is not None, "When your backend diverges, we expect it throws an exception (second return value)" - 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): - warnings.warn("The error returned by your backend when it stopped (due to isolated shunt) should preferably inherit from BackendError") - + self._aux_test_detachment(backend, is_dc=False) + backend.reset(self.get_path(), self.get_casefile()) # make a shunt alone on a bus action = type(backend)._complete_action_class() @@ -946,15 +958,9 @@ def test_18_isolated_shunt_stops_computation(self): bk_act = type(backend).my_bk_act_class() 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 (at time of writing) that your backend returns `False` in case of isolated shunt in DC." - assert res[1] is not None, "When your backend stops, we expect it throws an exception (second return value)" - error = res[1] - assert isinstance(error, Grid2OpException), f"When your backend returns `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" - if not isinstance(error, BackendError): - warnings.warn("The error returned by your backend when it stopped (due to isolated shunt) should preferably inherit from BackendError") - - def test_19_isolated_storage_stops_computation(self): + self._aux_test_detachment(backend, is_dc=True) + + def test_19_isolated_storage_stops_computation(self, allow_detachment=DEFAULT_ALLOW_DETACHMENT): """Teststest that an isolated storage unit will be spotted by the `run_pf` method and forwarded to grid2op by returining `False, an_exception` (in AC and DC) This test supposes that : @@ -969,10 +975,11 @@ def test_19_isolated_storage_stops_computation(self): .. note:: Currently this stops the computation of the environment and lead to a game over. - This behaviour might change in the future. + .. note:: + This test is also used in `attr:AAATestBackendAPI.test_33_allow_detachment` """ self.skip_if_needed() - backend = self.aux_make_backend() + backend = self.aux_make_backend(allow_detachment=allow_detachment) cls = type(backend) if cls.n_storage == 0: self.skipTest("Your backend does not support storage units") @@ -982,29 +989,17 @@ def test_19_isolated_storage_stops_computation(self): bk_act = type(backend).my_bk_act_class() 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 (at time of writing) that your backend returns `False` in case of isolated storage units in AC." - assert res[1] is not None, "When your backend stops, we expect it throws an exception (second return value)" - 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): - warnings.warn("The error returned by your backend when it stopped (due to isolated storage units) should preferably inherit from BackendError") - + self._aux_test_detachment(backend, is_dc=False) + backend.reset(self.get_path(), self.get_casefile()) action = type(backend)._complete_action_class() action.update({"set_bus": {"storages_id": [(0, 2)]}}) bk_act = type(backend).my_bk_act_class() 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 (at time of writing) that your backend returns `False` in case of isolated storage unit." - assert res[1] is not None, "When your backend stops, we expect it throws an exception (second return value)" - 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): - warnings.warn("The error returned by your backend when it stopped (due to isolated storage units) should preferably inherit from BackendError") - - def test_20_disconnected_load_stops_computation(self): + self._aux_test_detachment(backend, is_dc=True) + + def test_20_disconnected_load_stops_computation(self, allow_detachment=DEFAULT_ALLOW_DETACHMENT): """ Tests that a disconnected load unit will be caught by the `_runpf_with_diverging_exception` method. @@ -1020,10 +1015,11 @@ def test_20_disconnected_load_stops_computation(self): .. note:: Currently this stops the computation of the environment and lead to a game over. - Behaviour changed in version 1.11.0 (no longer caught by runpf() itelf) + .. note:: + This test is also used in `attr:AAATestBackendAPI.test_33_allow_detachment` """ self.skip_if_needed() - backend = self.aux_make_backend() + backend = self.aux_make_backend(allow_detachment=allow_detachment) # a load alone on a bus action = type(backend)._complete_action_class() @@ -1031,9 +1027,7 @@ def test_20_disconnected_load_stops_computation(self): bk_act = type(backend).my_bk_act_class() bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 - error = backend._runpf_with_diverging_exception(is_dc=False) - assert error is not None - assert isinstance(error, Grid2OpException) + self._aux_test_detachment(backend, is_dc=False, detachment_should_pass=True) backend.reset(self.get_path(), self.get_casefile()) # a load alone on a bus @@ -1042,11 +1036,9 @@ def test_20_disconnected_load_stops_computation(self): bk_act = type(backend).my_bk_act_class() bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 - error = backend._runpf_with_diverging_exception(is_dc=True) - assert error is not None - assert isinstance(error, Grid2OpException) + self._aux_test_detachment(backend, is_dc=True, detachment_should_pass=True) - def test_21_disconnected_gen_stops_computation(self): + def test_21_disconnected_gen_stops_computation(self, allow_detachment=DEFAULT_ALLOW_DETACHMENT): """ Tests that a disconnected generator will be caught by the `_runpf_with_diverging_exception` method @@ -1062,10 +1054,11 @@ def test_21_disconnected_gen_stops_computation(self): .. note:: Currently this stops the computation of the environment and lead to a game over. - Behaviour changed in version 1.11.0 (no longer caught by runpf() itelf) + .. note:: + This test is also used in `attr:AAATestBackendAPI.test_33_allow_detachment` """ self.skip_if_needed() - backend = self.aux_make_backend() + backend = self.aux_make_backend(allow_detachment=allow_detachment) # a disconnected generator action = type(backend)._complete_action_class() @@ -1073,9 +1066,7 @@ def test_21_disconnected_gen_stops_computation(self): bk_act = type(backend).my_bk_act_class() bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 - error = backend._runpf_with_diverging_exception(is_dc=False) - assert error is not None - assert isinstance(error, Grid2OpException) + self._aux_test_detachment(backend, is_dc=False, detachment_should_pass=True) backend.reset(self.get_path(), self.get_casefile()) # a disconnected generator @@ -1084,9 +1075,7 @@ def test_21_disconnected_gen_stops_computation(self): bk_act = type(backend).my_bk_act_class() bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 - res = backend._runpf_with_diverging_exception(is_dc=True) - assert error is not None - assert isinstance(error, Grid2OpException) + self._aux_test_detachment(backend, is_dc=True, detachment_should_pass=True) def test_22_islanded_grid_stops_computation(self): """Tests that when the grid is split in two different "sub_grid" is spotted by the `run_pf` method and forwarded to grid2op by returining `False, an_exception` (in AC and DC) @@ -1119,7 +1108,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], f"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): @@ -1135,7 +1124,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], f"It is expected that your backend return `(False, _)` 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): @@ -1278,7 +1267,7 @@ def test_25_disco_storage_v_null(self): 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)" + assert np.allclose(v_[storage_id], 0.), f"v should be 0. for disconnected storage, but is currently {v_[storage_id]} (DC)" def test_26_copy(self): """Tests that the backend can be copied (and that the copied backend and the @@ -1701,4 +1690,110 @@ def test_30_n_busbar_per_sub_ok(self): el_nm, el_key, el_pos_topo_vect) else: warnings.warn(f"{type(self).__name__} test_30_n_busbar_per_sub_ok: This test is not performed in depth as your backend does not support storage units (or there are none on the grid)") + + def _aux_disco_sto_then_add_sto_p(self, backend: Backend): + action = type(backend)._complete_action_class() + action.update({"set_bus": {"storages_id": [(0, -1)]}}) + bk_act = type(backend).my_bk_act_class() + bk_act += action + backend.apply_action(bk_act) + action = type(backend)._complete_action_class() + action.update({"set_storage": [(0, 0.1)]}) + bk_act = type(backend).my_bk_act_class() + bk_act += action + backend.apply_action(bk_act) + + def test_31_disconnected_storage_with_p_stops_computation(self, allow_detachment=DEFAULT_ALLOW_DETACHMENT): + """ + Tests that a disconnected storage unit that is asked to produce active power + raise an error if the backend does not support `allow_detachment` + + This test supposes that : + + - backend.load_grid(...) is implemented + - backend.runpf() (AC and DC mode) is implemented + - backend.apply_action() for topology modification + - backend.reset() is implemented + + NB: this test is skipped if your backend does not (yet :-) ) supports storage units + + .. note:: + Currently this stops the computation of the environment and lead to a game over. + + .. note:: + This test is also used in `attr:AAATestBackendAPI.test_31_allow_detachment` + + """ + self.skip_if_needed() + backend = self.aux_make_backend(allow_detachment=allow_detachment) + if type(backend).n_storage == 0: + self.skipTest("Your backend does not support storage unit") + + # a disconnected generator + self._aux_disco_sto_then_add_sto_p(backend) + self._aux_test_detachment(backend, is_dc=False, detachment_should_pass=True) + + backend.reset(self.get_path(), self.get_casefile()) + # a disconnected generator + self._aux_disco_sto_then_add_sto_p(backend) + self._aux_test_detachment(backend, is_dc=True, detachment_should_pass=True) + + def test_32_xxx_handle_detachment_called(self): + """Tests that at least one of the function: + + - :func:`grid2op.Backend.Backend.can_handle_detachment` + - :func:`grid2op.Backend.Backend.cannot_handle_detachment` + + has been implemented in the :func:`grid2op.Backend.Backend.load_grid` + implementation. + + This test supposes that : + + - backend.load_grid(...) is implemented + + .. versionadded:: 1.11.0 + + """ + self.skip_if_needed() + backend = self.aux_make_backend() + assert not backend._missing_detachment_support_info + + def test_33_allow_detachment(self): + """Tests that your backend model disconnected load / generator (is the proper flag is present.) + + Concretely it will run the tests + + - :attr:`TestBackendAPI.test_16_isolated_load_stops_computation` + - :attr:`TestBackendAPI.test_17_isolated_gen_stops_computation` + - :attr:`TestBackendAPI.test_18_isolated_shunt_stops_computation` + - :attr:`TestBackendAPI.test_19_isolated_storage_stops_computation` + - :attr:`TestBackendAPI.test_20_disconnected_load_stops_computation` + - :attr:`TestBackendAPI.test_21_disconnected_gen_stops_computation` + + When your backend is initialized with "allow_detachment". + + NB: of course these tests have been modified such that things that should pass + will pass and things that should fail will fail. + + .. versionadded:: 1.11.0 + + """ + self.skip_if_needed() + backend = self.aux_make_backend(allow_detachment=True) + if backend._missing_detachment_support_info: + self.skipTest("Cannot perform this test as you have not specified whether " + "the backend class supports the 'detachement' of loads and " + "generators. Falling back to default grid2op behaviour, which " + "is to fail if a load or a generator is disconnected.") + if not type(backend).detachment_is_allowed: + self.skipTest("Cannot perform this test as your backend does not appear " + "to support the `detachment` information: a disconnect load " + "or generator is necessarily causing a game over.") + self.test_16_isolated_load_stops_computation(allow_detachment=True) + self.test_17_isolated_gen_stops_computation(allow_detachment=True) + self.test_18_isolated_shunt_stops_computation(allow_detachment=True) + self.test_19_isolated_storage_stops_computation(allow_detachment=True) + self.test_20_disconnected_load_stops_computation(allow_detachment=True) + self.test_21_disconnected_gen_stops_computation(allow_detachment=True) + self.test_31_disconnected_storage_with_p_stops_computation(allow_detachment=True) \ No newline at end of file diff --git a/grid2op/tests/test_attached_envs_compat.py b/grid2op/tests/test_attached_envs_compat.py index 9b790497..6418b56e 100644 --- a/grid2op/tests/test_attached_envs_compat.py +++ b/grid2op/tests/test_attached_envs_compat.py @@ -12,6 +12,7 @@ import grid2op import numpy as np +from grid2op.Backend import Backend, PandaPowerBackend from grid2op.Space import GridObjects from grid2op.Action import PowerlineSetAction, DontAct, PlayableAction from grid2op.Observation import CompleteObservation @@ -46,11 +47,11 @@ def test_opponent(self): def test_action_space(self): assert issubclass(self.env.action_space.subtype, PlayableAction) - assert self.env.action_space.n == 494 + assert self.env.action_space.n == 494, f"{self.env.action_space.n}" def test_observation_space(self): assert issubclass(self.env.observation_space.subtype, CompleteObservation) - assert self.env.observation_space.n == 1266 + assert self.env.observation_space.n == 1266, f"{self.env.observation_space.n}" def test_random_action(self): """test i can perform some step (random)""" @@ -91,14 +92,14 @@ def test_opponent(self): def test_action_space(self): assert issubclass(self.env.action_space.subtype, PlayableAction) - assert self.env.action_space.n == 1500 + assert self.env.action_space.n == 1500, f"{self.env.action_space.n}" def test_observation_space(self): assert issubclass(self.env.observation_space.subtype, CompleteObservation) assert ( "curtailment" not in self.env.observation_space.subtype.attr_list_vect ), "curtailment should not be there" - assert self.env.observation_space.n == 3868 + assert self.env.observation_space.n == 3868, f"{self.env.observation_space.n}" def test_random_action(self): """test i can perform some step (random)""" @@ -139,11 +140,11 @@ def test_opponent(self): def test_action_space(self): assert issubclass(self.env.action_space.subtype, PlayableAction) - assert self.env.action_space.n == 160 + assert self.env.action_space.n == 160, f"{self.env.action_space.n}" def test_observation_space(self): assert issubclass(self.env.observation_space.subtype, CompleteObservation) - assert self.env.observation_space.n == 420 + assert self.env.observation_space.n == 420, f"{self.env.observation_space.n}" def test_random_action(self): """test i can perform some step (random)""" @@ -184,11 +185,11 @@ def test_opponent(self): def test_action_space(self): assert issubclass(self.env.action_space.subtype, PlayableAction) - assert self.env.action_space.n == 26 + assert self.env.action_space.n == 26, f"{self.env.action_space.n}" def test_observation_space(self): assert issubclass(self.env.observation_space.subtype, CompleteObservation) - assert self.env.observation_space.n == 420 + assert self.env.observation_space.n == 420, f"{self.env.observation_space.n}" def test_random_action(self): """test i can perform some step (random)""" @@ -225,15 +226,17 @@ def test_elements(self): def test_opponent(self): assert issubclass(self.env._opponent_action_class, DontAct) - assert self.env._opponent_action_space.n == 0 + assert self.env._opponent_action_space.n == 0, f"{self.env._opponent_action_space.n}" def test_action_space(self): assert issubclass(self.env.action_space.subtype, PlayableAction) - assert self.env.action_space.n == 26 + assert self.env.action_space.n == 26, f"{self.env.action_space.n}" def test_observation_space(self): assert issubclass(self.env.observation_space.subtype, CompleteObservation) - assert self.env.observation_space.n == 420 + import pdb + pdb.set_trace() + assert self.env.observation_space.n == 420, f"{self.env.observation_space.n}" def test_same_env_as_no_storage(self): res = 0 diff --git a/grid2op/tests/test_n_busbar_per_sub.py b/grid2op/tests/test_n_busbar_per_sub.py index f1e59b0c..778e83c0 100644 --- a/grid2op/tests/test_n_busbar_per_sub.py +++ b/grid2op/tests/test_n_busbar_per_sub.py @@ -1254,7 +1254,10 @@ def test_move_line_or(self): assert self.env.backend._grid.bus.loc[global_bus]["in_service"] else: assert not self.env.backend._grid.line.iloc[line_id]["in_service"] - self.env.backend.line_status[:] = self.env.backend._get_line_status() # otherwise it's not updated + tmp = self.env.backend._get_line_status() # otherwise it's not updated + self.env.backend.line_status.flags.writeable = True + self.env.backend.line_status[:] = tmp + self.env.backend.line_status.flags.writeable = False topo_vect = self.env.backend._get_topo_vect() assert topo_vect[cls.line_or_pos_topo_vect[line_id]] == new_bus, f"{topo_vect[cls.line_or_pos_topo_vect[line_id]]} vs {new_bus}" @@ -1272,7 +1275,10 @@ def test_move_line_ex(self): assert self.env.backend._grid.bus.loc[global_bus]["in_service"] else: assert not self.env.backend._grid.line.iloc[line_id]["in_service"] - self.env.backend.line_status[:] = self.env.backend._get_line_status() # otherwise it's not updated + tmp = self.env.backend._get_line_status() # otherwise it's not updated + self.env.backend.line_status.flags.writeable = True + self.env.backend.line_status[:] = tmp + self.env.backend.line_status.flags.writeable = False topo_vect = self.env.backend._get_topo_vect() assert topo_vect[cls.line_ex_pos_topo_vect[line_id]] == new_bus, f"{topo_vect[cls.line_ex_pos_topo_vect[line_id]]} vs {new_bus}"