From af9a8544b960b4a04360587f25e68982d39dc854 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 15 Dec 2023 15:18:31 +0100 Subject: [PATCH 01/10] general improvement on some tests --- CHANGELOG.rst | 6 + grid2op/Backend/backend.py | 121 ++++++++++++-------- grid2op/Environment/baseEnv.py | 1 + grid2op/Environment/environment.py | 2 +- grid2op/tests/BaseBackendTest.py | 100 ++++++++-------- grid2op/tests/aaa_test_backend_interface.py | 48 ++++++-- 6 files changed, 167 insertions(+), 111 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f492ba4a1..7b585557a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -34,11 +34,17 @@ Change Log [1.9.8] - 20xx-yy-zz ---------------------- +- [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) - [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 ---------------------- diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index bf291aaf3..a72185ed5 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -1124,18 +1124,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 +1144,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 +1189,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 +1295,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/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index 40aaf5252..8d92d2d61 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -2961,6 +2961,7 @@ 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() diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index f7047204a..ed613c4a9 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -418,7 +418,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: diff --git a/grid2op/tests/BaseBackendTest.py b/grid2op/tests/BaseBackendTest.py index ad24c2ca6..1eea313de 100644 --- a/grid2op/tests/BaseBackendTest.py +++ b/grid2op/tests/BaseBackendTest.py @@ -177,7 +177,7 @@ def test_load_file(self): assert np.all(backend.get_topo_vect() == np.ones(np.sum(backend.sub_info))) - conv = backend.runpf() + conv, *_ = backend.runpf() assert conv, "powerflow diverge it is not supposed to!" with warnings.catch_warnings(): @@ -199,7 +199,7 @@ 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() + conv, *_ = backend.runpf() assert conv, "powerflow diverge it is not supposed to!" backend.assert_grid_correct_after_powerflow() @@ -262,7 +262,7 @@ def test_theta_ok(self): def test_runpf_dc(self): self.skip_if_needed() - conv = self.backend.runpf(is_dc=True) + conv, *_ = self.backend.runpf(is_dc=True) assert conv true_values_dc = np.array( [ @@ -317,7 +317,7 @@ def test_runpf(self): 2.80741759e01, ] ) - conv = self.backend.runpf(is_dc=False) + conv, *_ = self.backend.runpf(is_dc=False) assert conv p_or, *_ = self.backend.lines_or_info() assert self.compare_vect(p_or, true_values_ac) @@ -325,7 +325,7 @@ 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) + conv, *_ = self.backend.runpf(is_dc=False) assert conv, "powerflow diverge at loading" p_or, q_or, v_or, a_or = self.backend.lines_or_info() @@ -341,7 +341,7 @@ 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) + conv, *_ = self.backend.runpf(is_dc=False) assert conv, "powerflow diverge at loading" load_p, load_q, load_v = self.backend.loads_info() gen_p, gen__q, gen_v = self.backend.generators_info() @@ -384,33 +384,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,11 +524,11 @@ def test_pf_ac_dc(self): 5.77869057, ] ) - conv = self.backend.runpf(is_dc=True) + conv, *_ = self.backend.runpf(is_dc=True) assert conv 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) + conv, *_ = self.backend.runpf(is_dc=False) assert conv p_or_orig, q_or_orig, *_ = self.backend.lines_or_info() assert self.compare_vect(q_or_orig, true_values_ac) @@ -567,7 +571,7 @@ 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( @@ -579,7 +583,7 @@ def test_disconnect_line(self): def test_donothing_action(self): self.skip_if_needed() - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() init_flow = self.backend.get_line_flow() init_lp, *_ = self.backend.loads_info() init_gp, *_ = self.backend.generators_info() @@ -596,7 +600,7 @@ 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() + conv, *_ = self.backend.runpf() assert conv, "Cannot perform a powerflow after doing nothing" after_flow = self.backend.get_line_flow() assert self.compare_vect(init_flow, after_flow) @@ -608,7 +612,7 @@ 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) + conv, *_ = self.backend.runpf(is_dc=True) assert conv, "powergrid diverge after loading (even in DC)" init_flow, *_ = self.backend.lines_or_info() init_lp, init_l_q, *_ = self.backend.loads_info() @@ -623,7 +627,7 @@ 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) # 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 +645,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 +660,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,7 +677,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) + conv, *_ = self.backend.runpf(is_dc=False) assert conv, "powergrid diverge after loading" prod_p_init, prod_q_init, prod_v_init = self.backend.generators_info() ratio = 1.05 @@ -683,7 +687,7 @@ 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) + conv, *_ = self.backend.runpf(is_dc=False) assert conv, "Cannot perform a powerflow after modifying the powergrid" prod_p_after, prod_q_after, prod_v_after = self.backend.generators_info() @@ -694,7 +698,7 @@ 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() init_lp, *_ = self.backend.loads_info() init_gp, *_ = self.backend.generators_info() @@ -709,7 +713,7 @@ def test_apply_action_maintenance(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, "Power does not converge if line {} is removed".format(19) # performs basic check @@ -728,7 +732,7 @@ def test_apply_action_maintenance(self): def test_apply_action_hazard(self): self.skip_if_needed() - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() assert conv, "powerflow did not converge at iteration 0" init_lp, *_ = self.backend.loads_info() init_gp, *_ = self.backend.generators_info() @@ -743,7 +747,7 @@ def test_apply_action_hazard(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, "Power does not converge if line {} is removed".format(19) # performs basic check @@ -759,7 +763,7 @@ 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() init_lp, *_ = self.backend.loads_info() init_gp, *_ = self.backend.generators_info() @@ -779,7 +783,7 @@ 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) @@ -858,7 +862,7 @@ 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() init_amps_flow = self.backend.get_line_flow() # check that maintenance vector is properly taken into account @@ -869,7 +873,7 @@ def test_get_topo_vect_speed(self): bk_action += action # apply the action here self.backend.apply_action(bk_action) - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() assert conv after_amps_flow = self.backend.get_line_flow() @@ -940,7 +944,7 @@ 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() init_amps_flow = self.backend.get_line_flow() # check that maintenance vector is properly taken into account @@ -952,7 +956,7 @@ def test_topo_set1sub(self): # apply the action here self.backend.apply_action(bk_action) - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() assert conv after_amps_flow = self.backend.get_line_flow() @@ -1037,7 +1041,7 @@ 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() init_amps_flow = self.backend.get_line_flow() # check that maintenance vector is properly taken into account @@ -1050,7 +1054,7 @@ def test_topo_change1sub(self): self.backend.apply_action(bk_action) # run the powerflow - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() assert conv after_amps_flow = self.backend.get_line_flow() @@ -1111,7 +1115,7 @@ 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() init_amps_flow = copy.deepcopy(self.backend.get_line_flow()) # check that maintenance vector is properly taken into account @@ -1123,7 +1127,7 @@ 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 after_amps_flow = self.backend.get_line_flow() @@ -1186,7 +1190,7 @@ 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() assert conv after_amps_flow = self.backend.get_line_flow() @@ -1214,7 +1218,7 @@ def test_topo_change_2sub(self): # apply the action here self.backend.apply_action(bk_action) - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() assert conv, "powerflow diverge it should not" # check the _grid is correct @@ -1684,7 +1688,7 @@ 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() + conv, *_ = self.backend.runpf() assert conv, "powerflow should converge at loading" lines_flows_init = self.backend.get_line_flow() thermal_limit = 10 * lines_flows_init @@ -1728,7 +1732,7 @@ 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() + conv, *_ = self.backend.runpf() assert conv, "powerflow should converge at loading" lines_flows_init = self.backend.get_line_flow() diff --git a/grid2op/tests/aaa_test_backend_interface.py b/grid2op/tests/aaa_test_backend_interface.py index e45361b04..9abf19761 100644 --- a/grid2op/tests/aaa_test_backend_interface.py +++ b/grid2op/tests/aaa_test_backend_interface.py @@ -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]]}") From 03ad59c97be488da315bc0347f961f3cb1eada9d Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 15 Dec 2023 15:18:31 +0100 Subject: [PATCH 02/10] general improvement on some tests --- CHANGELOG.rst | 6 + grid2op/Backend/backend.py | 121 ++++++++++++-------- grid2op/Environment/baseEnv.py | 1 + grid2op/Environment/environment.py | 2 +- grid2op/tests/BaseBackendTest.py | 100 ++++++++-------- grid2op/tests/aaa_test_backend_interface.py | 48 ++++++-- 6 files changed, 167 insertions(+), 111 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f492ba4a1..7b585557a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -34,11 +34,17 @@ Change Log [1.9.8] - 20xx-yy-zz ---------------------- +- [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) - [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 ---------------------- diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index bf291aaf3..a72185ed5 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -1124,18 +1124,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 +1144,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 +1189,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 +1295,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/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index 40aaf5252..8d92d2d61 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -2961,6 +2961,7 @@ 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() diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index f7047204a..ed613c4a9 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -418,7 +418,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: diff --git a/grid2op/tests/BaseBackendTest.py b/grid2op/tests/BaseBackendTest.py index ad24c2ca6..1eea313de 100644 --- a/grid2op/tests/BaseBackendTest.py +++ b/grid2op/tests/BaseBackendTest.py @@ -177,7 +177,7 @@ def test_load_file(self): assert np.all(backend.get_topo_vect() == np.ones(np.sum(backend.sub_info))) - conv = backend.runpf() + conv, *_ = backend.runpf() assert conv, "powerflow diverge it is not supposed to!" with warnings.catch_warnings(): @@ -199,7 +199,7 @@ 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() + conv, *_ = backend.runpf() assert conv, "powerflow diverge it is not supposed to!" backend.assert_grid_correct_after_powerflow() @@ -262,7 +262,7 @@ def test_theta_ok(self): def test_runpf_dc(self): self.skip_if_needed() - conv = self.backend.runpf(is_dc=True) + conv, *_ = self.backend.runpf(is_dc=True) assert conv true_values_dc = np.array( [ @@ -317,7 +317,7 @@ def test_runpf(self): 2.80741759e01, ] ) - conv = self.backend.runpf(is_dc=False) + conv, *_ = self.backend.runpf(is_dc=False) assert conv p_or, *_ = self.backend.lines_or_info() assert self.compare_vect(p_or, true_values_ac) @@ -325,7 +325,7 @@ 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) + conv, *_ = self.backend.runpf(is_dc=False) assert conv, "powerflow diverge at loading" p_or, q_or, v_or, a_or = self.backend.lines_or_info() @@ -341,7 +341,7 @@ 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) + conv, *_ = self.backend.runpf(is_dc=False) assert conv, "powerflow diverge at loading" load_p, load_q, load_v = self.backend.loads_info() gen_p, gen__q, gen_v = self.backend.generators_info() @@ -384,33 +384,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,11 +524,11 @@ def test_pf_ac_dc(self): 5.77869057, ] ) - conv = self.backend.runpf(is_dc=True) + conv, *_ = self.backend.runpf(is_dc=True) assert conv 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) + conv, *_ = self.backend.runpf(is_dc=False) assert conv p_or_orig, q_or_orig, *_ = self.backend.lines_or_info() assert self.compare_vect(q_or_orig, true_values_ac) @@ -567,7 +571,7 @@ 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( @@ -579,7 +583,7 @@ def test_disconnect_line(self): def test_donothing_action(self): self.skip_if_needed() - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() init_flow = self.backend.get_line_flow() init_lp, *_ = self.backend.loads_info() init_gp, *_ = self.backend.generators_info() @@ -596,7 +600,7 @@ 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() + conv, *_ = self.backend.runpf() assert conv, "Cannot perform a powerflow after doing nothing" after_flow = self.backend.get_line_flow() assert self.compare_vect(init_flow, after_flow) @@ -608,7 +612,7 @@ 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) + conv, *_ = self.backend.runpf(is_dc=True) assert conv, "powergrid diverge after loading (even in DC)" init_flow, *_ = self.backend.lines_or_info() init_lp, init_l_q, *_ = self.backend.loads_info() @@ -623,7 +627,7 @@ 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) # 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 +645,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 +660,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,7 +677,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) + conv, *_ = self.backend.runpf(is_dc=False) assert conv, "powergrid diverge after loading" prod_p_init, prod_q_init, prod_v_init = self.backend.generators_info() ratio = 1.05 @@ -683,7 +687,7 @@ 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) + conv, *_ = self.backend.runpf(is_dc=False) assert conv, "Cannot perform a powerflow after modifying the powergrid" prod_p_after, prod_q_after, prod_v_after = self.backend.generators_info() @@ -694,7 +698,7 @@ 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() init_lp, *_ = self.backend.loads_info() init_gp, *_ = self.backend.generators_info() @@ -709,7 +713,7 @@ def test_apply_action_maintenance(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, "Power does not converge if line {} is removed".format(19) # performs basic check @@ -728,7 +732,7 @@ def test_apply_action_maintenance(self): def test_apply_action_hazard(self): self.skip_if_needed() - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() assert conv, "powerflow did not converge at iteration 0" init_lp, *_ = self.backend.loads_info() init_gp, *_ = self.backend.generators_info() @@ -743,7 +747,7 @@ def test_apply_action_hazard(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, "Power does not converge if line {} is removed".format(19) # performs basic check @@ -759,7 +763,7 @@ 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() init_lp, *_ = self.backend.loads_info() init_gp, *_ = self.backend.generators_info() @@ -779,7 +783,7 @@ 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) @@ -858,7 +862,7 @@ 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() init_amps_flow = self.backend.get_line_flow() # check that maintenance vector is properly taken into account @@ -869,7 +873,7 @@ def test_get_topo_vect_speed(self): bk_action += action # apply the action here self.backend.apply_action(bk_action) - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() assert conv after_amps_flow = self.backend.get_line_flow() @@ -940,7 +944,7 @@ 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() init_amps_flow = self.backend.get_line_flow() # check that maintenance vector is properly taken into account @@ -952,7 +956,7 @@ def test_topo_set1sub(self): # apply the action here self.backend.apply_action(bk_action) - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() assert conv after_amps_flow = self.backend.get_line_flow() @@ -1037,7 +1041,7 @@ 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() init_amps_flow = self.backend.get_line_flow() # check that maintenance vector is properly taken into account @@ -1050,7 +1054,7 @@ def test_topo_change1sub(self): self.backend.apply_action(bk_action) # run the powerflow - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() assert conv after_amps_flow = self.backend.get_line_flow() @@ -1111,7 +1115,7 @@ 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() init_amps_flow = copy.deepcopy(self.backend.get_line_flow()) # check that maintenance vector is properly taken into account @@ -1123,7 +1127,7 @@ 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 after_amps_flow = self.backend.get_line_flow() @@ -1186,7 +1190,7 @@ 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() assert conv after_amps_flow = self.backend.get_line_flow() @@ -1214,7 +1218,7 @@ def test_topo_change_2sub(self): # apply the action here self.backend.apply_action(bk_action) - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() assert conv, "powerflow diverge it should not" # check the _grid is correct @@ -1684,7 +1688,7 @@ 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() + conv, *_ = self.backend.runpf() assert conv, "powerflow should converge at loading" lines_flows_init = self.backend.get_line_flow() thermal_limit = 10 * lines_flows_init @@ -1728,7 +1732,7 @@ 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() + conv, *_ = self.backend.runpf() assert conv, "powerflow should converge at loading" lines_flows_init = self.backend.get_line_flow() diff --git a/grid2op/tests/aaa_test_backend_interface.py b/grid2op/tests/aaa_test_backend_interface.py index e45361b04..9abf19761 100644 --- a/grid2op/tests/aaa_test_backend_interface.py +++ b/grid2op/tests/aaa_test_backend_interface.py @@ -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]]}") From e867bc1909c79ee755d413c0756b1018d25341b5 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 15 Dec 2023 17:08:17 +0100 Subject: [PATCH 03/10] a new type of env that does not perform line disconection on a masked part of the grid, see rte-france#571 --- CHANGELOG.rst | 2 + grid2op/Backend/backend.py | 6 +- grid2op/Environment/__init__.py | 4 +- grid2op/Environment/baseEnv.py | 23 ++- grid2op/Environment/maskedEnvironment.py | 150 ++++++++++++++ grid2op/Environment/timedOutEnv.py | 7 +- grid2op/tests/BaseBackendTest.py | 59 +++--- grid2op/tests/test_MaskedEnvironment.py | 239 +++++++++++++++++++++++ 8 files changed, 451 insertions(+), 39 deletions(-) create mode 100644 grid2op/Environment/maskedEnvironment.py create mode 100644 grid2op/tests/test_MaskedEnvironment.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7b585557a..505515485 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 a72185ed5..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: 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 8d92d2d61..3f8ccf757 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 000000000..7b2ad5cea --- /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 fcccd7641..af5558ebe 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 1eea313de..3a3bb46e6 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 000000000..11cd2f96a --- /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() From bfe4798f0d575a54f525fe149a53977b22336848 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 15 Dec 2023 17:33:57 +0100 Subject: [PATCH 04/10] fix broken test --- CHANGELOG.rst | 2 +- grid2op/tests/test_Environment.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 505515485..e24666144 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -40,7 +40,7 @@ Change Log - [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`) + for some part of the grid (`MaskedEnvironment`) see https://github.com/rte-france/Grid2Op/issues/571 - [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/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 ( From 022ba0139b45392c92920f0430d97e5b753f24e0 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Wed, 10 Jan 2024 16:22:15 +0100 Subject: [PATCH 05/10] improving the doc to fix issue rte-france#568 [skip ci] --- docs/action.rst | 2 +- docs/environment.rst | 2 +- grid2op/Environment/baseEnv.py | 59 ++++++++++++++++++++++++++ grid2op/Environment/environment.py | 8 ++++ grid2op/Observation/baseObservation.py | 33 +++++++++++++- grid2op/tests/BaseBackendTest.py | 2 +- 6 files changed, 101 insertions(+), 5 deletions(-) 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/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/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index 3f8ccf757..e0cbeea38 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -84,6 +84,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 ---------- diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index ed613c4a9..09df00f97 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -37,6 +37,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 ---------- 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/tests/BaseBackendTest.py b/grid2op/tests/BaseBackendTest.py index 3a3bb46e6..b8f99b617 100644 --- a/grid2op/tests/BaseBackendTest.py +++ b/grid2op/tests/BaseBackendTest.py @@ -2741,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 From 3c5196f7fcf1e5b2a595889eb5158da8cbff1a6e Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Wed, 10 Jan 2024 16:37:04 +0100 Subject: [PATCH 06/10] adressing issue rte-france#569 [skip ci] --- CHANGELOG.rst | 1 + grid2op/Episode/EpisodeReplay.py | 13 +++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e24666144..1486819a0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -39,6 +39,7 @@ 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) +- [FIXED] an issue with imageio having deprecated the `fps` kwargs (see https://github.com/rte-france/Grid2Op/issues/569) - [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 - [IMPROVED] the CI speed: by not testing every possible numpy version but only most ancient and most recent 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 From 99e8fa28ff076479b497865c0736decd0b18033d Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Wed, 10 Jan 2024 17:37:15 +0100 Subject: [PATCH 07/10] fixing ray example training script see rte-france#535 [skip ci] --- examples/multi_agents/ray_example.py | 153 ++++++++++++++++++--------- 1 file changed, 105 insertions(+), 48 deletions(-) diff --git a/examples/multi_agents/ray_example.py b/examples/multi_agents/ray_example.py index b318c3b43..d165613e5 100644 --- a/examples/multi_agents/ray_example.py +++ b/examples/multi_agents/ray_example.py @@ -9,6 +9,7 @@ """example with centralized observation and local actions""" import warnings import numpy as np +import copy from gym.spaces import Discrete, Box @@ -16,7 +17,7 @@ from ray.rllib.policy.policy import PolicySpec, Policy import grid2op -from grid2op.Action.PlayableAction import PlayableAction +from grid2op.Action import PlayableAction from grid2op.multi_agent.multiAgentEnv import MultiAgentEnv from grid2op.gym_compat import GymEnv, BoxGymObsSpace, DiscreteActSpace @@ -41,9 +42,15 @@ class MAEnvWrapper(MAEnv): def __init__(self, env_config=None): super().__init__() + # you can customize stuff by using the "env config" if you want + backend = LightSimBackend() + if "backend_cls" in env_config: + backend = env_config["backend_cls"] + # you can do the same for other attribute to the environment + env = grid2op.make(ENV_NAME, action_class=PlayableAction, - backend=LightSimBackend()) + backend=backend) self.ma_env = MultiAgentEnv(env, ACTION_DOMAINS) @@ -55,41 +62,66 @@ def __init__(self, env_config=None): # with the grid2op / gym interface. self._gym_env = GymEnv(env) self._gym_env.observation_space.close() + + obs_attr_to_keep = ["gen_p", "rho"] + if "obs_attr_to_keep" in env_config: + obs_attr_to_keep = copy.deepcopy(env_config["obs_attr_to_keep"]) self._gym_env.observation_space = BoxGymObsSpace(env.observation_space, - attr_to_keep=["gen_p", - "rho"], + attr_to_keep=obs_attr_to_keep, replace_nan_by_0=True # replace Nan by 0. ) # we did not experiment yet with the "partially observable" setting # so for now we suppose all agents see the same observation # which is the full grid - self.observation_space = Box(shape=self._gym_env.observation_space.shape, - high=self._gym_env.observation_space.high, - low=self._gym_env.observation_space.low, - dtype=np.float32 - ) - - # we represent the action as discrete action for now. - # It should work to encode then differently using the - # gym_compat module for example - self._conv_action_space = { - agent_id : DiscreteActSpace(self.ma_env.action_spaces[agent_id]) + self._aux_observation_space = { + agent_id : BoxGymObsSpace(self.ma_env.observation_spaces[agent_id], + attr_to_keep=obs_attr_to_keep, + replace_nan_by_0=True # replace Nan by 0. + ) for agent_id in self.ma_env.agents } - # to avoid "weird" pickle issues - self.action_space = { - agent_id : Discrete(n=self.ma_env.action_spaces[agent_id].n) + self.observation_space = { + agent_id : Box(low=self._aux_observation_space[agent_id].low, + high=self._aux_observation_space[agent_id].high, + dtype=self._aux_observation_space[agent_id].dtype) for agent_id in self.ma_env.agents } - def reset(self): + # we represent the action as discrete action for now. + # It should work to encode then differently using the + # gym_compat module for example + act_type = "discrete" + if "act_type" in env_config: + act_type = env_config["act_type"] + + # for discrete actions + if act_type == "discrete": + self._conv_action_space = { + agent_id : DiscreteActSpace(self.ma_env.action_spaces[agent_id]) + for agent_id in self.ma_env.agents + } + + # to avoid "weird" pickle issues + self.action_space = { + agent_id : Discrete(n=self.ma_env.action_spaces[agent_id].n) + for agent_id in self.ma_env.agents + } + else: + raise NotImplementedError("Make the implementation in this case") + + def reset(self, *, seed=None, options=None): + if seed is not None: + self.seed(seed) + # reset the underlying multi agent environment obs = self.ma_env.reset() - return self._format_obs(obs) + return self._format_obs(obs), {} + def seed(self, seed): + return self.ma_env.seed(seed) def _format_obs(self, grid2op_obs): # NB we heavily use here that all agents see the same things @@ -132,7 +164,9 @@ def step(self, actions): # ignored for now info = {} - return gym_obs, r, done, info + truncateds = {k: False for k in self.ma_env.agents} + truncateds['__all__'] = truncateds[first_agent_id] + return gym_obs, r, done, truncateds, info def policy_mapping_fn(agent_id, episode, worker, **kwargs): @@ -141,7 +175,8 @@ def policy_mapping_fn(agent_id, episode, worker, **kwargs): if __name__ == "__main__": import ray - from ray.rllib.agents.ppo import ppo + # from ray.rllib.agents.ppo import ppo + from ray.rllib.algorithms.ppo import PPO, PPOConfig import json import os import shutil @@ -164,34 +199,56 @@ def policy_mapping_fn(agent_id, episode, worker, **kwargs): SELECT_ENV = MAEnvWrapper # Specifies the OpenAI Gym environment for Cart Pole N_ITER = 1000 # Number of training runs. - config = ppo.DEFAULT_CONFIG.copy() # PPO's default configuration. See the next code cell. - config["log_level"] = "WARN" # Suppress too many messages, but try "INFO" to see what can be printed. - - # Other settings we might adjust: - config["num_workers"] = 1 # Use > 1 for using more CPU cores, including over a cluster - config["num_sgd_iter"] = 10 # Number of SGD (stochastic gradient descent) iterations per training minibatch. - # I.e., for each minibatch of data, do this many passes over it to train. - config["sgd_minibatch_size"] = 64 # The amount of data records per minibatch - config["model"]["fcnet_hiddens"] = [100, 50] # - config["num_cpus_per_worker"] = 0 # This avoids running out of resources in the notebook environment when this cell is re-executed - config["vf_clip_param"] = 100 - - # multi agent specific config - config["multiagent"] = { - "policies" : { - "agent_0" : PolicySpec( - action_space=ray_ma_env.action_space["agent_0"] - ), - "agent_1" : PolicySpec( - action_space=ray_ma_env.action_space["agent_1"] - ) - }, - "policy_mapping_fn": policy_mapping_fn, - "policies_to_train": ["agent_0", "agent_1"], - } + # config = ppo.DEFAULT_CONFIG.copy() # PPO's default configuration. See the next code cell. + # config["log_level"] = "WARN" # Suppress too many messages, but try "INFO" to see what can be printed. + # # Other settings we might adjust: + # config["num_workers"] = 1 # Use > 1 for using more CPU cores, including over a cluster + # config["num_sgd_iter"] = 10 # Number of SGD (stochastic gradient descent) iterations per training minibatch. + # # I.e., for each minibatch of data, do this many passes over it to train. + # config["sgd_minibatch_size"] = 64 # The amount of data records per minibatch + # config["model"]["fcnet_hiddens"] = [100, 50] # + # config["num_cpus_per_worker"] = 0 # This avoids running out of resources in the notebook environment when this cell is re-executed + # config["vf_clip_param"] = 100 + + # # multi agent specific config + # config["multiagent"] = { + # "policies" : { + # "agent_0" : PolicySpec( + # action_space=ray_ma_env.action_space["agent_0"] + # ), + # "agent_1" : PolicySpec( + # action_space=ray_ma_env.action_space["agent_1"] + # ) + # }, + # "policy_mapping_fn": policy_mapping_fn, + # "policies_to_train": ["agent_0", "agent_1"], + # } + + # see ray doc for this... + # syntax changes every ray major version apparently... + config = PPOConfig() + config = config.training(gamma=0.9, lr=0.01, kl_coeff=0.3, + train_batch_size=128) + config = config.resources(num_gpus=0) + config = config.rollouts(num_rollout_workers=1) + + # multi agent parts + config.multi_agent(policies={ + "agent_0" : PolicySpec( + action_space=ray_ma_env.action_space["agent_0"], + observation_space=ray_ma_env.observation_space["agent_0"] + ), + "agent_1" : PolicySpec( + action_space=ray_ma_env.action_space["agent_1"], + observation_space=ray_ma_env.observation_space["agent_1"], + ) + }, + policy_mapping_fn = policy_mapping_fn, + policies_to_train= ["agent_0", "agent_1"]) + #Trainer - agent = ppo.PPOTrainer(config, env=SELECT_ENV) + agent = PPO(config=config, env=SELECT_ENV) results = [] episode_data = [] From f1310c5538e3f277e364e8816609483071271bda Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 11 Jan 2024 12:31:23 +0100 Subject: [PATCH 08/10] implementing some tests and some bugfix for MaskedEnvironment --- grid2op/Environment/maskedEnvironment.py | 21 +- grid2op/Runner/runner.py | 1 + grid2op/tests/test_MaskedEnvironment.py | 283 +++++++++++------------ 3 files changed, 153 insertions(+), 152 deletions(-) diff --git a/grid2op/Environment/maskedEnvironment.py b/grid2op/Environment/maskedEnvironment.py index 7b2ad5cea..b97bf986c 100644 --- a/grid2op/Environment/maskedEnvironment.py +++ b/grid2op/Environment/maskedEnvironment.py @@ -27,7 +27,14 @@ class MaskedEnvironment(Environment): # TODO heritage ou alors on met un truc d .. 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 + # 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): @@ -38,7 +45,7 @@ def __init__(self, elif isinstance(grid2op_env, dict): super().__init__(**grid2op_env) else: - raise EnvError(f"For TimedOutEnvironment you need to provide " + 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)}") @@ -62,10 +69,8 @@ def _make_lines_of_interest(self, lines_of_interest): 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 + 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 = {} @@ -79,6 +84,10 @@ def get_params_for_runner(self): 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, 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/tests/test_MaskedEnvironment.py b/grid2op/tests/test_MaskedEnvironment.py index 11cd2f96a..41ed76110 100644 --- a/grid2op/tests/test_MaskedEnvironment.py +++ b/grid2op/tests/test_MaskedEnvironment.py @@ -20,8 +20,9 @@ MultiDiscreteActSpace) -class TestMaskedEnvironment(unittest.TestCase): - def get_mask(self): +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 @@ -30,9 +31,9 @@ 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()) + 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=~self.get_mask()) + 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 @@ -41,10 +42,11 @@ def setUp(self) -> None: # 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): + 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() @@ -69,13 +71,13 @@ def test_ok(self): 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]}" + 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_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]}" def test_reset(self): # timestep_overflow should be 0 initially even if the flow is too high @@ -84,155 +86,144 @@ def test_reset(self): assert obs.rho[self.line_id] > 1. -class TestTimedOutEnvironmentCpy(TestMaskedEnvironment): +class TestMaskedEnvironmentCpy(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 = 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 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 +class TestMaskedEnvironmentRunner(unittest.TestCase): + def setUp(self) -> None: + TestMaskedEnvironment.setUp(self) + self.max_iter = 10 -# def tearDown(self) -> None: -# self.env1.close() -# return super().tearDown() + def tearDown(self) -> None: + self.env_in.close() + self.env_out.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 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]}" -# 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()) + + +class TestMaskedEnvironmentGym(unittest.TestCase): + def setUp(self) -> None: + TestMaskedEnvironment.setUp(self) -# def tearDown(self) -> None: -# self.env1.close() -# return super().tearDown() + def tearDown(self) -> None: + self.env_in.close() + self.env_out.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 _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.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_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 = GymEnv(self.env1) -# with warnings.catch_warnings(): -# warnings.filterwarnings("ignore") -# env_gym.action_space = MultiDiscreteActSpace(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_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__": From 47387b5e8a933e36b15be8c6a191a6d9ce346d49 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 11 Jan 2024 16:02:39 +0100 Subject: [PATCH 09/10] fix a 'bug' in the example for multi agent example due to not implemented function --- ...ibuted_actions_centralized_observations.py | 4 ++-- grid2op/multi_agent/subgridAction.py | 19 ++++++++++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/examples/multi_agents/distributed_actions_centralized_observations.py b/examples/multi_agents/distributed_actions_centralized_observations.py index 928bfba2e..e32aee454 100644 --- a/examples/multi_agents/distributed_actions_centralized_observations.py +++ b/examples/multi_agents/distributed_actions_centralized_observations.py @@ -17,9 +17,9 @@ # agent_name : controlled substation id zones = {"agent_0": [0, 1, 2, 3, 4], - "agent_1": [5,6,7,8,9,10,11,12,13]} + "agent_1": [5, 6, 7, 8, 9, 10, 11, 12, 13]} env = MultiAgentEnv(cent_env, action_domains=zones) - + env.seed(0) dict_obs = env.reset() # dict with: key=agent_name, value=the SubGridObservation diff --git a/grid2op/multi_agent/subgridAction.py b/grid2op/multi_agent/subgridAction.py index 55a54b399..afd6c5d78 100644 --- a/grid2op/multi_agent/subgridAction.py +++ b/grid2op/multi_agent/subgridAction.py @@ -381,16 +381,29 @@ def to_global(self, global_action_space: ActionSpace): "the target action type does not suppor it") if self._modif_interco_set_status: - raise NotImplementedError("What to do if I modified an interco status (set) ?") + # TODO not tested + if global_action_space.supports_type("set_line_status"): + global_action._modif_set_status = True + global_action._set_line_status[my_cls.interco_to_lineid] = self._set_interco_status + else: + warnings.warn("The set_line_status part of this local action has been removed because " + "the target action type does not suppor it") + if self._modif_interco_change_status: - raise NotImplementedError("What to do if I modified an interco status (change) ?") + # TODO not tested + if global_action_space.supports_type("change_line_status"): + global_action._modif_change_status = True + global_action._set_line_status[my_cls.interco_to_lineid] = self._set_interco_status + else: + warnings.warn("The change_line_status part of this local action has been removed because " + "the target action type does not suppor it") + if self._modif_inj: raise NotImplementedError("What to do if I modified an injection ?") if self._modif_alarm: raise NotImplementedError("What to do if I modified an alarm ?") return global_action - def impact_on_objects(self) -> dict: # TODO not tested res = super().impact_on_objects() From d6db9cd9d8fc0a2f5e28d74b7feb3a54c38a963d Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 12 Jan 2024 15:40:16 +0100 Subject: [PATCH 10/10] fixing read the docs --- .readthedocs.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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: .