From 3ed4bad4aa4c126f5c42c964534b6d10cc078952 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 7 Mar 2024 11:32:50 +0100 Subject: [PATCH] adding support for more than 2 busbars per substation --- CHANGELOG.rst | 5 +- benchmarks/benchmark_grid_size.py | 51 ++- lightsim2grid/lightSimBackend.py | 134 ++++---- lightsim2grid/tests/test_n_busbar_per_sub.py | 307 +++++++++++++++++++ lightsim2grid/tests/test_solver_control.py | 1 - src/GridModel.cpp | 9 +- src/GridModel.h | 16 +- src/main.cpp | 6 + 8 files changed, 463 insertions(+), 66 deletions(-) create mode 100644 lightsim2grid/tests/test_n_busbar_per_sub.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d5c04d2..666a2a4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,7 +21,7 @@ Change Log [0.7.6] 2023-xx-yy -------------------- - [BREAKING] now able to retrieve `dcSbus` with a dedicated method (and not with the old `get_Sbus`). - If you previously used `gridmodel.get_Subus()` to retrieve the Sbus used for DC powerflow, please use + If you previously used `gridmodel.get_Sbus()` to retrieve the Sbus used for DC powerflow, please use `gridmodel.get_dcSbus()` instead. - [DEPRECATED] in the cpp class: the old `SecurityAnalysisCPP` has been renamed `ContingencyAnalysisCPP` (you should not import it, but it you do you can `from lightsim2grid.securityAnalysis import ContingencyAnalysisCPP` now) @@ -45,6 +45,9 @@ Change Log - [ADDED] embed in the generator models the "non pv" behaviour. (TODO need to be able to change Q from python side) - [ADDED] computation of PTPF (Power Transfer Distribution Factor) is now possible - [ADDED] (not tested) support for more than 2 busbars per substation +- [ADDED] a timer to get the time spent in the gridmodel for the powerflow (env.backend.timer_gridmodel_xx_pf) + which also include the time +- [ADDED] support for more than 2 busbars per substation (requires grid2op >= 1.10.0) - [IMPROVED] now performing the new grid2op `create_test_suite` - [IMPROVED] now lightsim2grid properly throw `BackendError` - [IMPROVED] clean ce cpp side by refactoring: making clearer the difference (linear) solver diff --git a/benchmarks/benchmark_grid_size.py b/benchmarks/benchmark_grid_size.py index b7a15ad..09d627e 100644 --- a/benchmarks/benchmark_grid_size.py +++ b/benchmarks/benchmark_grid_size.py @@ -13,7 +13,12 @@ import matplotlib.pyplot as plt from grid2op import make, Parameters from grid2op.Chronics import FromNPY -from lightsim2grid import LightSimBackend, TimeSerie, SecurityAnalysis +from lightsim2grid import LightSimBackend, TimeSerie +try: + from lightsim2grid import ContingencyAnalysis +except ImportError: + from lightsim2grid import SecurityAnalysis as ContingencyAnalysis + from tqdm import tqdm import os from utils_benchmark import print_configuration, get_env_name_displayed @@ -157,6 +162,8 @@ def get_loads_gens(load_p_init, load_q_init, gen_p_init, sgen_p_init): g2op_speeds = [] g2op_sizes = [] g2op_step_time = [] + ls_solver_time = [] + ls_gridmodel_time = [] ts_times = [] ts_speeds = [] @@ -227,13 +234,18 @@ def get_loads_gens(load_p_init, load_q_init, gen_p_init, sgen_p_init): g2op_times.append(None) g2op_speeds.append(None) g2op_step_time.append(None) + ls_solver_time.append(None) + ls_gridmodel_time.append(None) g2op_sizes.append(env_lightsim.n_sub) else: total_time = env_lightsim.backend._timer_preproc + env_lightsim.backend._timer_solver # + env_lightsim.backend._timer_postproc + total_time = env_lightsim._time_step g2op_times.append(total_time) g2op_speeds.append(1.0 * nb_step / total_time) g2op_step_time.append(1.0 * env_lightsim._time_step / nb_step) - g2op_sizes.append(env_lightsim.n_sub) + ls_solver_time.append(env_lightsim.backend.comp_time) + ls_gridmodel_time.append(env_lightsim.backend.timer_gridmodel_xx_pf) + g2op_sizes.append(env_lightsim.n_sub) # Perform the computation using TimeSerie env_lightsim.reset() @@ -274,7 +286,7 @@ def get_loads_gens(load_p_init, load_q_init, gen_p_init, sgen_p_init): # Perform a securtiy analysis (up to 1000 contingencies) env_lightsim.reset() - sa = SecurityAnalysis(env_lightsim) + sa = ContingencyAnalysis(env_lightsim) for i in range(env_lightsim.n_line): sa.add_single_contingency(i) if i >= 1000: @@ -297,11 +309,24 @@ def get_loads_gens(load_p_init, load_q_init, gen_p_init, sgen_p_init): print("Results using grid2op.steps (288 consecutive steps, only measuring 'dc pf [init] + ac pf')") tab_g2op = [] for i, nm_ in enumerate(case_names_displayed): - tab_g2op.append((nm_, ts_sizes[i], 1000. / g2op_speeds[i] if g2op_speeds[i] else None, g2op_speeds[i], - 1000. * g2op_step_time[i] if g2op_step_time[i] else None)) + tab_g2op.append((nm_, + ts_sizes[i], + 1000. / g2op_speeds[i] if g2op_speeds[i] else None, + g2op_speeds[i], + 1000. * g2op_step_time[i] if g2op_step_time[i] else None, + 1000. * ls_gridmodel_time[i] / nb_step if ls_gridmodel_time[i] else None, + 1000. * ls_solver_time[i] / nb_step if ls_solver_time[i] else None, + )) if TABULATE_AVAIL: res_use_with_grid2op_2 = tabulate(tab_g2op, - headers=["grid", "size (nb bus)", "time (ms / pf)", "speed (pf / s)", "avg step duration (ms)"], + headers=["grid", + "size (nb bus)", + "step time (ms / pf)", + "speed (pf / s)", + "avg step duration (ms)", + "time in 'gridmodel' (ms / pf)", + "time in 'pf algo' (ms / pf)", + ], tablefmt="rst") print(res_use_with_grid2op_2) else: @@ -349,6 +374,20 @@ def get_loads_gens(load_p_init, load_q_init, gen_p_init, sgen_p_init): plt.title(f"Computation speed using Grid2Op.step (dc pf [init] + ac pf)") plt.yscale("log") plt.show() + + plt.plot(g2op_sizes, ls_solver_time, linestyle='solid', marker='+', markersize=8) + plt.xlabel("Size (number of substation)") + plt.ylabel("Speed (solver time)") + plt.title(f"Computation speed for solving the powerflow only") + plt.yscale("log") + plt.show() + + plt.plot(g2op_sizes, ls_gridmodel_time, linestyle='solid', marker='+', markersize=8) + plt.xlabel("Size (number of substation)") + plt.ylabel("Speed (solver time)") + plt.title(f"Computation speed for solving the powerflow only") + plt.yscale("log") + plt.show() # make the plot summarizing all results plt.plot(ts_sizes, ts_times, linestyle='solid', marker='+', markersize=8) diff --git a/lightsim2grid/lightSimBackend.py b/lightsim2grid/lightSimBackend.py index 81732db..bbecd58 100644 --- a/lightsim2grid/lightSimBackend.py +++ b/lightsim2grid/lightSimBackend.py @@ -168,7 +168,17 @@ def __init__(self, # available solver in lightsim self.available_solvers = [] - self.comp_time = 0. # computation time of just the powerflow + + # computation time of just the powerflow (when the grid is formatted + # by the gridmodel already) + # it takes only into account the time spend in the powerflow algorithm + self.comp_time = 0. + + # computation time of the powerflow + # it takes into account everything in the gridmodel, including the mapping + # to the solver, building of Ybus and Sbus AND the time to solve the powerflow + self.timer_gridmodel_xx_pf = 0. + self._timer_postproc = 0. self._timer_preproc = 0. self._timer_solver = 0. @@ -195,22 +205,6 @@ def __init__(self, # add the static gen to the list of controlable gen in grid2Op self._use_static_gen = use_static_gen # TODO implement it - - # storage data for this object (otherwise it's in the class) - # self.n_storage = None - # self.storage_to_subid = None - # self.storage_pu_to_kv = None - # self.name_storage = None - # self.storage_to_sub_pos = None - # self.storage_type = None - # self.storage_Emin = None - # self.storage_Emax = None - # self.storage_max_p_prod = None - # self.storage_max_p_absorb = None - # self.storage_marginal_cost = None - # self.storage_loss = None - # self.storage_discharging_efficiency = None - # self.storage_charging_efficiency = None def turnedoff_no_pv(self): self._turned_off_pv = False @@ -411,6 +405,10 @@ def _assign_right_solver(self): self._grid.change_solver(SolverType.SparseLU) def load_grid(self, path=None, filename=None): + if hasattr(type(self), "can_handle_more_than_2_busbar"): + # grid2op version >= 1.10.0 then we use this + self.can_handle_more_than_2_busbar() + if self._loader_method == "pandapower": self._load_grid_pandapower(path, filename) elif self._loader_method == "pypowsybl": @@ -418,6 +416,42 @@ def load_grid(self, path=None, filename=None): else: raise BackendError(f"Impossible to initialize the backend with '{self._loader_method}'") + def _should_not_have_to_do_this(self, path=None, filename=None): + # included in grid2op now ! + # but before `make_complete_path` was introduced we need to still + # be able to use lightsim2grid + import os + from grid2op.Exceptions import Grid2OpException + if path is None and filename is None: + raise Grid2OpException( + "You must provide at least one of path or file to load a powergrid." + ) + if path is None: + full_path = filename + elif filename is None: + full_path = path + else: + full_path = os.path.join(path, filename) + if not os.path.exists(full_path): + raise Grid2OpException('There is no powergrid at "{}"'.format(full_path)) + return full_path + + def _aux_pypowsybl_init_substations(self, loader_kwargs): + # now handle the legacy "make as if there are 2 busbars per substation" + # as it is done with grid2Op simulated environment + if (("double_bus_per_sub" in loader_kwargs and loader_kwargs["double_bus_per_sub"]) or + ("n_busbar_per_sub" in loader_kwargs and loader_kwargs["n_busbar_per_sub"])): + bus_init = self._grid.get_bus_vn_kv() + orig_to_ls = np.array(self._grid._orig_to_ls) + bus_doubled = np.concatenate([bus_init for _ in range(self.n_busbar_per_sub)]) + self._grid.init_bus(bus_doubled, 0, 0) + for i in range(self.__nb_bus_before): + self._grid.deactivate_bus(i + self.__nb_bus_before) + new_orig_to_ls = np.concatenate([orig_to_ls + i * self.__nb_bus_before + for i in range(self.n_busbar_per_sub)] + ) + self._grid._orig_to_ls = new_orig_to_ls + def _load_grid_pypowsybl(self, path=None, filename=None): from lightsim2grid.gridmodel.from_pypowsybl import init as init_pypow import pypowsybl.network as pypow_net @@ -429,28 +463,15 @@ def _load_grid_pypowsybl(self, path=None, filename=None): full_path = self.make_complete_path(path, filename) except AttributeError as exc_: warnings.warn("Please upgrade your grid2op version") - import os - from grid2op.Exceptions import Grid2OpException - def make_complete_path(path, filename): - if path is None and filename is None: - raise Grid2OpException( - "You must provide at least one of path or file to load a powergrid." - ) - if path is None: - full_path = filename - elif filename is None: - full_path = path - else: - full_path = os.path.join(path, filename) - if not os.path.exists(full_path): - raise Grid2OpException('There is no powergrid at "{}"'.format(full_path)) - return full_path - full_path = make_complete_path(path, filename) + full_path = self._should_not_have_to_do_this(path, filename) + grid_tmp = pypow_net.load(full_path) gen_slack_id = None if "gen_slack_id" in loader_kwargs: gen_slack_id = int(loader_kwargs["gen_slack_id"]) self._grid = init_pypow(grid_tmp, gen_slack_id=gen_slack_id, sort_index=True) + self.__nb_bus_before = len(self._grid.get_bus_vn_kv()) + self._aux_pypowsybl_init_substations(loader_kwargs) self._aux_setup_right_after_grid_init() # mandatory for the backend @@ -482,7 +503,11 @@ def make_complete_path(path, filename): self.shunt_to_subid = np.array([el.bus_id for el in self._grid.get_shunts()], dtype=dt_int) else: # TODO get back the sub id from the grid_tmp.get_substations() - raise NotImplementedError("TODO") + raise NotImplementedError("Today the only supported behaviour is to consider the 'buses' of the powsybl grid " + "are the 'substation' of this backend. " + "This will change in the future, but in the meantime please add " + "'use_buses_for_sub' = True in the `loader_kwargs` when loading " + "a lightsim2grid grid.") # the names self.name_load = np.array([f"load_{el.bus_id}_{id_obj}" for id_obj, el in enumerate(self._grid.get_loads())]) @@ -496,25 +521,10 @@ def make_complete_path(path, filename): self._compute_pos_big_topo() self.__nb_powerline = len(self._grid.get_lines()) - self.__nb_bus_before = len(self._grid.get_bus_vn_kv()) # init this self.prod_p = np.array([el.target_p_mw for el in self._grid.get_generators()], dtype=dt_float) self.next_prod_p = np.array([el.target_p_mw for el in self._grid.get_generators()], dtype=dt_float) - - # now handle the legacy "make as if there are 2 busbars per substation" - # as it is done with grid2Op simulated environment - if "double_bus_per_sub" in loader_kwargs and loader_kwargs["double_bus_per_sub"]: - bus_init = self._grid.get_bus_vn_kv() - orig_to_ls = np.array(self._grid._orig_to_ls) - bus_doubled = np.concatenate((bus_init, bus_init)) - self._grid.init_bus(bus_doubled, 0, 0) - for i in range(self.__nb_bus_before): - self._grid.deactivate_bus(i + self.__nb_bus_before) - new_orig_to_ls = np.concatenate((orig_to_ls, - orig_to_ls + self.__nb_bus_before) - ) - self._grid._orig_to_ls = new_orig_to_ls self.nb_bus_total = len(self._grid.get_bus_vn_kv()) # and now things needed by the backend (legacy) @@ -531,6 +541,7 @@ def make_complete_path(path, filename): self._aux_finish_setup_after_reading() def _aux_setup_right_after_grid_init(self): + self._grid.set_n_sub(self.__nb_bus_before) self._handle_turnedoff_pv() self.available_solvers = self._grid.available_solvers() @@ -549,12 +560,19 @@ def _aux_setup_right_after_grid_init(self): # check that the solver type provided is installed with lightsim2grid self._check_suitable_solver_type(self.__current_solver_type) self._grid.change_solver(self.__current_solver_type) + + # handle multiple busbar per substations + if hasattr(type(self), "can_handle_more_than_2_busbar"): + # grid2op version >= 1.10.0 then we use this + self._grid._max_nb_bus_per_sub = self.n_busbar_per_sub def _load_grid_pandapower(self, path=None, filename=None): + type(self.init_pp_backend).n_busbar_per_sub = self.n_busbar_per_sub self.init_pp_backend.load_grid(path, filename) self.can_output_theta = True # i can compute the "theta" and output it to grid2op - self._grid = init(self.init_pp_backend._grid) + self._grid = init(self.init_pp_backend._grid) + self.__nb_bus_before = self.init_pp_backend.get_nb_active_bus() self._aux_setup_right_after_grid_init() self.n_line = self.init_pp_backend.n_line @@ -653,7 +671,6 @@ def _aux_finish_setup_after_reading(self): # set up the "lightsim grid" accordingly cls = type(self) - self._grid.set_n_sub(self.__nb_bus_before) self._grid.set_load_pos_topo_vect(cls.load_pos_topo_vect) self._grid.set_gen_pos_topo_vect(cls.gen_pos_topo_vect) self._grid.set_line_or_pos_topo_vect(cls.line_or_pos_topo_vect[:self.__nb_powerline]) @@ -913,8 +930,18 @@ def runpf(self, is_dc=False): beg_postroc = time.perf_counter() if is_dc: self.comp_time += self._grid.get_dc_computation_time() + self.timer_gridmodel_xx_pf += self._grid.timer_last_dc_pf else: self.comp_time += self._grid.get_computation_time() + # NB get_computation_time returns "time_total_nr", which is + # defined in the powerflow algorithm and not on the linear solver. + # it takes into account everything needed to solve the powerflow + # once everything is passed to the solver. + # It does not take into account the time to format the data in the + # from the GridModel + + self.timer_gridmodel_xx_pf += self._grid.timer_last_ac_pf + # timer_gridmodel_xx_pf takes all the time within the gridmodel "ac_pf" self.V[:] = V (self.p_or[:self.__nb_powerline], @@ -1046,6 +1073,8 @@ def copy(self): #################### # res = copy.deepcopy(self) # super slow res = type(self).__new__(type(self)) + res.comp_time = self.comp_time + res.timer_gridmodel_xx_pf = self.timer_gridmodel_xx_pf # copy the regular attribute res.__has_storage = self.__has_storage @@ -1197,6 +1226,7 @@ def reset(self, grid_path, grid_filename=None): self._handle_turnedoff_pv() self.topo_vect[:] = self.__init_topo_vect self.comp_time = 0. + self.timer_gridmodel_xx_pf = 0. self._timer_postproc = 0. self._timer_preproc = 0. self._timer_solver = 0. diff --git a/lightsim2grid/tests/test_n_busbar_per_sub.py b/lightsim2grid/tests/test_n_busbar_per_sub.py new file mode 100644 index 0000000..aa51bb0 --- /dev/null +++ b/lightsim2grid/tests/test_n_busbar_per_sub.py @@ -0,0 +1,307 @@ +# Copyright (c) 2020, 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 LightSim2grid, LightSim2grid a implements a c++ backend targeting the Grid2Op platform. +import unittest +import warnings +import numpy as np + +import grid2op +from grid2op.Action import CompleteAction + +from lightsim2grid import LightSimBackend + +class TestLightSimBackend_3busbars(unittest.TestCase): + def get_nb_bus(self): + return 3 + + def get_env_nm(self): + return "educ_case14_storage" + + def get_backend_kwargs(self): + return dict() + + def setUp(self) -> None: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make(self.get_env_nm(), + backend=LightSimBackend(**self.get_backend_kwargs()), + action_class=CompleteAction, + test=True, + n_busbar=self.get_nb_bus(), + _add_to_name=type(self).__name__ + f'_{self.get_nb_bus()}') + self.list_loc_bus = [-1] + list(range(1, type(self.env).n_busbar_per_sub + 1)) + return super().setUp() + + def tearDown(self) -> None: + self.env.close() + return super().tearDown() + + def test_right_bus_made(self): + assert len(self.env.backend._grid.get_bus_vn_kv()) == self.get_nb_bus() * type(self.env).n_sub + assert (~np.array(self.env.backend._grid.get_bus_status())[type(self.env).n_sub:]).all() + assert (np.array(self.env.backend._grid.get_bus_status())[:type(self.env).n_sub]).all() + + @staticmethod + def _aux_find_sub(env, obj_col): + """find a sub with 4 elements, the type of elements and at least 2 lines""" + cls = type(env) + res = None + for sub_id in range(cls.n_sub): + this_sub_mask = cls.grid_objects_types[:,cls.SUB_COL] == sub_id + this_sub = cls.grid_objects_types[this_sub_mask, :] + if this_sub.shape[0] <= 3: + # not enough element + continue + if (this_sub[:, obj_col] == -1).all(): + # no load + continue + if ((this_sub[:, cls.LOR_COL] != -1) | (this_sub[:, cls.LEX_COL] != -1)).sum() <= 1: + # only 1 line + continue + el_id = this_sub[this_sub[:, obj_col] != -1, obj_col][0] + if (this_sub[:, cls.LOR_COL] != -1).any(): + line_or_id = this_sub[this_sub[:, cls.LOR_COL] != -1, cls.LOR_COL][0] + line_ex_id = None + else: + line_or_id = None + line_ex_id = this_sub[this_sub[:, cls.LEX_COL] != -1, cls.LEX_COL][0] + res = (sub_id, el_id, line_or_id, line_ex_id) + break + return res + + @staticmethod + def _aux_find_sub_shunt(env): + """find a sub with 4 elements, the type of elements and at least 2 lines""" + cls = type(env) + res = None + for el_id in range(cls.n_shunt): + sub_id = cls.shunt_to_subid[el_id] + this_sub_mask = cls.grid_objects_types[:,cls.SUB_COL] == sub_id + this_sub = cls.grid_objects_types[this_sub_mask, :] + if this_sub.shape[0] <= 3: + # not enough element + continue + if ((this_sub[:, cls.LOR_COL] != -1) | (this_sub[:, cls.LEX_COL] != -1)).sum() <= 1: + # only 1 line + continue + if (this_sub[:, cls.LOR_COL] != -1).any(): + line_or_id = this_sub[this_sub[:, cls.LOR_COL] != -1, cls.LOR_COL][0] + line_ex_id = None + else: + line_or_id = None + line_ex_id = this_sub[this_sub[:, cls.LEX_COL] != -1, cls.LEX_COL][0] + res = (sub_id, el_id, line_or_id, line_ex_id) + break + return res + + def test_move_load(self): + cls = type(self.env) + res = self._aux_find_sub(self.env, cls.LOA_COL) + if res is None: + raise RuntimeError(f"Cannot carry the test 'test_move_load' as " + "there are no suitable subastation in your grid.") + (sub_id, el_id, line_or_id, line_ex_id) = res + for new_bus in self.list_loc_bus: + if line_or_id is not None: + act = self.env.action_space({"set_bus": {"loads_id": [(el_id, new_bus)], "lines_or_id": [(line_or_id, new_bus)]}}) + else: + act = self.env.action_space({"set_bus": {"loads_id": [(el_id, new_bus)], "lines_ex_id": [(line_ex_id, new_bus)]}}) + bk_act = self.env._backend_action_class() + bk_act += act + self.env.backend.apply_action(bk_act) + global_bus = sub_id + (new_bus -1) * cls.n_sub + if new_bus >= 1: + assert self.env.backend._grid.get_loads()[el_id].bus_id == global_bus, f"error for new_bus {new_bus}: {self.env.backend._grid.get_loads()[el_id].bus_id} vs {global_bus}" + if line_or_id is not None: + assert self.env.backend._grid.get_lines()[line_or_id].bus_or_id == global_bus + else: + assert self.env.backend._grid.get_lines()[line_ex_id].bus_ex_id == global_bus + assert self.env.backend._grid.get_bus_status()[global_bus] + else: + assert not self.env.backend._grid.get_loads()[el_id].connected + if line_or_id is not None: + assert not self.env.backend._grid.get_lines()[line_or_id].connected + else: + assert not self.env.backend._grid.get_lines()[line_ex_id].connected + topo_vect = 1 * self.env.backend.get_topo_vect() + assert topo_vect[cls.load_pos_topo_vect[el_id]] == new_bus, f"{topo_vect[cls.load_pos_topo_vect[el_id]]} vs {new_bus}" + + def test_move_gen(self): + cls = type(self.env) + res = self._aux_find_sub(self.env, cls.GEN_COL) + if res is None: + raise RuntimeError(f"Cannot carry the test 'test_move_gen' as " + "there are no suitable subastation in your grid.") + (sub_id, el_id, line_or_id, line_ex_id) = res + for new_bus in self.list_loc_bus: + if line_or_id is not None: + act = self.env.action_space({"set_bus": {"generators_id": [(el_id, new_bus)], "lines_or_id": [(line_or_id, new_bus)]}}) + else: + act = self.env.action_space({"set_bus": {"generators_id": [(el_id, new_bus)], "lines_ex_id": [(line_ex_id, new_bus)]}}) + bk_act = self.env._backend_action_class() + bk_act += act + self.env.backend.apply_action(bk_act) + global_bus = sub_id + (new_bus -1) * cls.n_sub + if new_bus >= 1: + assert self.env.backend._grid.get_generators()[el_id].bus_id == global_bus, f"error for new_bus {new_bus}: {self.env.backend._grid.get_loads()[el_id].bus_id} vs {global_bus}" + if line_or_id is not None: + assert self.env.backend._grid.get_lines()[line_or_id].bus_or_id == global_bus + else: + assert self.env.backend._grid.get_lines()[line_ex_id].bus_ex_id == global_bus + assert self.env.backend._grid.get_bus_status()[global_bus] + else: + assert not self.env.backend._grid.get_generators()[el_id].connected + if line_or_id is not None: + assert not self.env.backend._grid.get_lines()[line_or_id].connected + else: + assert not self.env.backend._grid.get_lines()[line_ex_id].connected + topo_vect = 1 * self.env.backend.get_topo_vect() + assert topo_vect[cls.gen_pos_topo_vect[el_id]] == new_bus, f"{topo_vect[cls.gen_pos_topo_vect[el_id]]} vs {new_bus}" + + def test_move_storage(self): + cls = type(self.env) + res = self._aux_find_sub(self.env, cls.STORAGE_COL) + if res is None: + raise RuntimeError(f"Cannot carry the test 'test_move_storage' as " + "there are no suitable subastation in your grid.") + (sub_id, el_id, line_or_id, line_ex_id) = res + for new_bus in self.list_loc_bus: + if line_or_id is not None: + act = self.env.action_space({"set_bus": {"storages_id": [(el_id, new_bus)], "lines_or_id": [(line_or_id, new_bus)]}}) + else: + act = self.env.action_space({"set_bus": {"storages_id": [(el_id, new_bus)], "lines_ex_id": [(line_ex_id, new_bus)]}}) + bk_act = self.env._backend_action_class() + bk_act += act + self.env.backend.apply_action(bk_act) + global_bus = sub_id + (new_bus -1) * cls.n_sub + if new_bus >= 1: + assert self.env.backend._grid.get_storages()[el_id].bus_id == global_bus, f"error for new_bus {new_bus}: {self.env.backend._grid.get_loads()[el_id].bus_id} vs {global_bus}" + if line_or_id is not None: + assert self.env.backend._grid.get_lines()[line_or_id].bus_or_id == global_bus + else: + assert self.env.backend._grid.get_lines()[line_ex_id].bus_ex_id == global_bus + assert self.env.backend._grid.get_bus_status()[global_bus] + else: + assert not self.env.backend._grid.get_storages()[el_id].connected + if line_or_id is not None: + assert not self.env.backend._grid.get_lines()[line_or_id].connected + else: + assert not self.env.backend._grid.get_lines()[line_ex_id].connected + topo_vect = 1 * self.env.backend.get_topo_vect() + assert topo_vect[cls.storage_pos_topo_vect[el_id]] == new_bus, f"{topo_vect[cls.storage_pos_topo_vect[el_id]]} vs {new_bus}" + + def test_move_line_or(self): + cls = type(self.env) + line_id = 0 + for new_bus in self.list_loc_bus: + act = self.env.action_space({"set_bus": {"lines_or_id": [(line_id, new_bus)]}}) + bk_act = self.env._backend_action_class() + bk_act += act + self.env.backend.apply_action(bk_act) + global_bus = cls.line_or_to_subid[line_id] + (new_bus -1) * cls.n_sub + if new_bus >= 1: + assert self.env.backend._grid.get_lines()[line_id].bus_or_id == global_bus + assert self.env.backend._grid.get_bus_status()[global_bus] + else: + assert not self.env.backend._grid.get_lines()[line_id].connected + topo_vect = 1 * self.env.backend.get_topo_vect() + assert topo_vect[cls.line_or_pos_topo_vect[line_id]] == new_bus, f"{topo_vect[cls.line_or_pos_topo_vect[line_id]]} vs {new_bus}" + + def test_move_line_ex(self): + cls = type(self.env) + line_id = 0 + for new_bus in self.list_loc_bus: + act = self.env.action_space({"set_bus": {"lines_ex_id": [(line_id, new_bus)]}}) + bk_act = self.env._backend_action_class() + bk_act += act + self.env.backend.apply_action(bk_act) + global_bus = cls.line_ex_to_subid[line_id] + (new_bus -1) * cls.n_sub + if new_bus >= 1: + assert self.env.backend._grid.get_lines()[line_id].bus_ex_id == global_bus + assert self.env.backend._grid.get_bus_status()[global_bus] + else: + assert not self.env.backend._grid.get_lines()[line_id].connected + topo_vect = 1 * self.env.backend.get_topo_vect() + assert topo_vect[cls.line_ex_pos_topo_vect[line_id]] == new_bus, f"{topo_vect[cls.line_ex_pos_topo_vect[line_id]]} vs {new_bus}" + + def test_move_shunt(self): + cls = type(self.env) + res = self._aux_find_sub_shunt(self.env) + if res is None: + raise RuntimeError(f"Cannot carry the test 'test_move_load' as " + "there are no suitable subastation in your grid.") + (sub_id, el_id, line_or_id, line_ex_id) = res + for new_bus in self.list_loc_bus: + if line_or_id is not None: + act = self.env.action_space({"shunt": {"set_bus": [(el_id, new_bus)]}, "set_bus": {"lines_or_id": [(line_or_id, new_bus)]}}) + else: + act = self.env.action_space({"shunt": {"set_bus": [(el_id, new_bus)]}, "set_bus": {"lines_ex_id": [(line_ex_id, new_bus)]}}) + bk_act = self.env._backend_action_class() + bk_act += act + self.env.backend.apply_action(bk_act) + global_bus = sub_id + (new_bus -1) * cls.n_sub + if new_bus >= 1: + assert self.env.backend._grid.get_shunts()[el_id].bus_id == global_bus, f"error for new_bus {new_bus}: {self.env.backend._grid.get_loads()[el_id].bus_id} vs {global_bus}" + if line_or_id is not None: + assert self.env.backend._grid.get_lines()[line_or_id].bus_or_id == global_bus + else: + assert self.env.backend._grid.get_lines()[line_ex_id].bus_ex_id == global_bus + assert self.env.backend._grid.get_bus_status()[global_bus] + else: + assert not self.env.backend._grid.get_shunts()[el_id].connected + if line_or_id is not None: + assert not self.env.backend._grid.get_lines()[line_or_id].connected + else: + assert not self.env.backend._grid.get_lines()[line_ex_id].connected + + def test_check_kirchoff(self): + cls = type(self.env) + res = self._aux_find_sub(self.env, cls.LOA_COL) + if res is None: + raise RuntimeError("Cannot carry the test 'test_move_load' as " + "there are no suitable subastation in your grid.") + (sub_id, el_id, line_or_id, line_ex_id) = res + for new_bus in self.list_loc_bus: + if new_bus <= -1: + continue + if line_or_id is not None: + act = self.env.action_space({"set_bus": {"loads_id": [(el_id, new_bus)], "lines_or_id": [(line_or_id, new_bus)]}}) + else: + act = self.env.action_space({"set_bus": {"loads_id": [(el_id, new_bus)], "lines_ex_id": [(line_ex_id, new_bus)]}}) + bk_act = self.env._backend_action_class() + bk_act += act + self.env.backend.apply_action(bk_act) + conv, maybe_exc = self.env.backend.runpf() + assert conv, f"error : {maybe_exc}" + p_subs, q_subs, p_bus, q_bus, diff_v_bus = self.env.backend.check_kirchoff() + # assert laws are met + assert np.abs(p_subs).max() <= 1e-5, f"error for busbar {new_bus}: {np.abs(p_subs).max():.2e}" + assert np.abs(q_subs).max() <= 1e-5, f"error for busbar {new_bus}: {np.abs(q_subs).max():.2e}" + assert np.abs(p_bus).max() <= 1e-5, f"error for busbar {new_bus}: {np.abs(p_bus).max():.2e}" + assert np.abs(q_bus).max() <= 1e-5, f"error for busbar {new_bus}: {np.abs(q_bus).max():.2e}" + assert np.abs(diff_v_bus).max() <= 1e-5, f"error for busbar {new_bus}: {np.abs(diff_v_bus).max():.2e}" + + +class TestLightSimBackend_1busbar(TestLightSimBackend_3busbars): + def get_nb_bus(self): + return 1 + + +class TestLightSimBackend_3busbars_iidm(TestLightSimBackend_3busbars): + def get_env_nm(self): + return "./case_14_storage_iidm" + + def get_backend_kwargs(self): + return dict(loader_method="pypowsybl", + loader_kwargs={"use_buses_for_sub": True, + "double_bus_per_sub": True, + "gen_slack_id": 5} + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/lightsim2grid/tests/test_solver_control.py b/lightsim2grid/tests/test_solver_control.py index ec9a897..ae2a8f1 100644 --- a/lightsim2grid/tests/test_solver_control.py +++ b/lightsim2grid/tests/test_solver_control.py @@ -25,7 +25,6 @@ import numpy as np import grid2op from grid2op.Action import CompleteAction -from grid2op.Parameters import Parameters from lightsim2grid import LightSimBackend from lightsim2grid.solver import SolverType diff --git a/src/GridModel.cpp b/src/GridModel.cpp index ba83336..67b4853 100644 --- a/src/GridModel.cpp +++ b/src/GridModel.cpp @@ -330,6 +330,9 @@ void GridModel::reset(bool reset_solver, bool reset_ac, bool reset_dc) Ybus_dc_ = Eigen::SparseMatrix(); } + timer_last_ac_pf_= 0.; + timer_last_dc_pf_ = 0.; + acSbus_ = CplxVect(); dcSbus_ = CplxVect(); bus_pv_ = Eigen::VectorXi(); @@ -356,6 +359,7 @@ CplxVect GridModel::ac_pf(const CplxVect & Vinit, int max_iter, real_type tol) { + auto timer = CustTimer(); const int nb_bus = static_cast(bus_vn_kv_.size()); if(Vinit.size() != nb_bus){ std::ostringstream exc_; @@ -399,6 +403,7 @@ CplxVect GridModel::ac_pf(const CplxVect & Vinit, // std::cout << "\tbefore process_results" << std::endl; process_results(conv, res, Vinit, true, id_me_to_ac_solver_); + timer_last_ac_pf_ = timer.duration(); // return the vector of complex voltage at each bus return res; }; @@ -882,6 +887,8 @@ CplxVect GridModel::dc_pf(const CplxVect & Vinit, // the idea is to "mess" with the Sbus beforehand to split the "losses" // ie fake the action of generators to adjust Sbus such that sum(Sbus) = 0 // and the slack contribution factors are met. + auto timer = CustTimer(); + const int nb_bus = static_cast(bus_vn_kv_.size()); if(Vinit.size() != nb_bus){ //TODO DEBUG MODE: @@ -929,7 +936,7 @@ CplxVect GridModel::dc_pf(const CplxVect & Vinit, // std::cout << "\tprocess_results (dc) \n"; // store results (fase -> because I am in dc mode) process_results(conv, res, Vinit, is_ac, id_me_to_dc_solver_); - // std::cout << "\tafter compute_pf\n"; + timer_last_dc_pf_ = timer.duration(); return res; } diff --git a/src/GridModel.h b/src/GridModel.h index 7a0a104..4fefb2f 100644 --- a/src/GridModel.h +++ b/src/GridModel.h @@ -92,6 +92,8 @@ class GridModel : public GenericContainer > StateRes; GridModel(): + timer_last_ac_pf_(0.), + timer_last_dc_pf_(0.), solver_control_(), compute_results_(true), init_vm_pu_(1.04), @@ -113,6 +115,8 @@ class GridModel : public GenericContainer void set_orig_to_ls(const IntVect & orig_to_ls); // set both _orig_to_ls and _ls_to_orig const IntVect & get_ls_to_orig(void) const {return _ls_to_orig;} const IntVect & get_orig_to_ls(void) const {return _orig_to_ls;} + double timer_last_ac_pf() const {return timer_last_ac_pf_;} + double timer_last_dc_pf() const {return timer_last_dc_pf_;} Eigen::Index total_bus() const {return bus_vn_kv_.size();} const std::vector & id_me_to_ac_solver() const {return id_me_to_ac_solver_;} @@ -613,8 +617,7 @@ class GridModel : public GenericContainer } void set_max_nb_bus_per_sub(int max_nb_bus_per_sub) { - max_nb_bus_per_sub_ = max_nb_bus_per_sub; - if(bus_vn_kv_.size() != n_sub_ * max_nb_bus_per_sub_){ + if(bus_vn_kv_.size() != n_sub_ * max_nb_bus_per_sub){ std::ostringstream exc_; exc_ << "GridModel::set_max_nb_bus_per_sub: "; exc_ << "your model counts "; @@ -624,7 +627,9 @@ class GridModel : public GenericContainer exc_ << "substations / buses per substations with `set_n_sub` / `set_max_nb_bus_per_sub`"; throw std::runtime_error(exc_.str()); } + max_nb_bus_per_sub_ = max_nb_bus_per_sub; } + int get_max_nb_bus_per_sub() const { return max_nb_bus_per_sub_;} void fillSbus_other(CplxVect & res, bool ac, const std::vector& id_me_to_solver){ fillSbus_me(res, ac, id_me_to_solver); @@ -746,8 +751,8 @@ class GridModel : public GenericContainer int new_bus = new_values(el_pos); if(new_bus > 0){ // new bus is a real bus, so i need to make sure to have it turned on, and then change the bus - int init_bus_me = vect_subid(el_id); - int new_bus_backend = new_bus == 1 ? init_bus_me : init_bus_me + n_sub_ * (max_nb_bus_per_sub_ - 1); + int sub_id = vect_subid(el_id); + int new_bus_backend = sub_id + (new_bus - 1) * n_sub_; bus_status_[new_bus_backend] = true; (this->*fun_react)(el_id); // eg reactivate_load(load_id); (this->*fun_change)(el_id, new_bus_backend); // eg change_bus_load(load_id, new_bus_backend); @@ -774,7 +779,8 @@ class GridModel : public GenericContainer IntVect _orig_to_ls; // for converter from bus in lightsim2grid index to bus in original file format (*eg* pandapower or pypowsybl) // member of the grid - // static const int _deactivated_bus_id; + double timer_last_ac_pf_; + double timer_last_dc_pf_; // bool need_reset_solver_; // some matrices change size, needs to be computed // bool need_recompute_sbus_; // some coeff of sbus changed, need to recompute it diff --git a/src/main.cpp b/src/main.cpp index a1c9a6b..dd001e1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -613,6 +613,12 @@ PYBIND11_MODULE(lightsim2grid_cpp, m) .def("copy", &GridModel::copy) .def_property("_ls_to_orig", &GridModel::get_ls_to_orig, &GridModel::set_ls_to_orig, "remember the conversion from bus index in lightsim2grid to bus index in original file format (*eg* pandapower of pypowsybl).") .def_property("_orig_to_ls", &GridModel::get_orig_to_ls, &GridModel::set_orig_to_ls, "remember the conversion from bus index in original file format (*eg* pandapower of pypowsybl) to bus index in lightsim2grid.") + .def_property("_max_nb_bus_per_sub", + &GridModel::get_max_nb_bus_per_sub, + &GridModel::set_max_nb_bus_per_sub, + "do not modify it after loading !") + .def_property_readonly("timer_last_ac_pf", &GridModel::timer_last_ac_pf, "TODO") + .def_property_readonly("timer_last_dc_pf", &GridModel::timer_last_dc_pf, "TODO") // pickle .def(py::pickle(