diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1849890..f92998c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -26,6 +26,23 @@ TODO: in `main.cpp` check the returned policy of pybind11 and also the `py::call TODO: a cpp class that is able to compute (DC powerflow) ContingencyAnalysis and TimeSeries using PTDF and LODF TODO: integration test with pandapower (see `pandapower/contingency/contingency.py` and import `lightsim2grid_installed` and check it's True) +[0.10.0] 2024-12-17 +------------------- +- [BREAKING] disconnected storage now raises errors if some power is produced / absorbed, when using legacy grid2op version, + you can retrieve the previous behaviour by initializing the `LightSimBackend` with + `backend = LightSimBackend(..., stop_if_storage_disco=False)` +- [BREAKING] with the new `detachment_is_allowed` feature in grid2op, the kwargs `stop_if_load_disco`, + `stop_if_gen_disco` (and `stop_if_storage_disco`) are now optional. They are set up from the + call to `grid2op.make(...)` and are erased by the `allow_detachment` kwargs. In other words, + you don't need to set `stop_if_load_disco`, `stop_if_gen_disco` or `stop_if_storage_disco`. It is + automatically set by `grid2op.make(..., allow_detachment=XXX)` to have the correct bahaviour. +- [FIXED] an issue with the storage units (when asking it to produce / consume + but deactivating them with the same action the grid did not diverge) +- [IMPROVED] add the grid2op "detachement" support (loads and generators are allowed + to be disconnected from the grid) +- [ADDED] a kwargs `stop_if_storage_disco` to control (in legacy grid2op version) the behaviour + of the backend when a storage unit is disconnected. + [0.9.2.post2] 2024-11-29 -------------------------- - [FIXED] The attribute `can_output_theta` (of base `Backend` class) diff --git a/basic_perf.py b/basic_perf.py new file mode 100644 index 0000000..686db5d --- /dev/null +++ b/basic_perf.py @@ -0,0 +1,109 @@ +import warnings +import pandapower as pp +import numpy as np +from grid2op import make, Parameters +from grid2op.Chronics import FromNPY +from lightsim2grid import LightSimBackend +import tempfile +import os + +try: + from tabulate import tabulate + TABULATE_AVAIL = True +except ImportError: + print("The tabulate package is not installed. Some output might not work properly") + TABULATE_AVAIL = False + + +case_names = [ + # "case14.json", + "case118.json", + # "case_illinois200.json", + # "case300.json", + # "case1354pegase.json", + "case1888rte.json", + # "GBnetwork.json", # 2224 buses + "case2848rte.json", + # "case2869pegase.json", + # "case3120sp.json", + "case6495rte.json", + "case6515rte.json", + "case9241pegase.json" + ] + +case_name = "case6495rte.json" +case_name = "case14.json" + +def make_grid2op_env(pp_case, case_name, load_p, load_q, gen_p, sgen_p): + param = Parameters.Parameters() + param.init_from_dict({"NO_OVERFLOW_DISCONNECTION": True}) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env_lightsim = make("blank", + param=param, + test=True, + backend=LightSimBackend(), + chronics_class=FromNPY, + data_feeding_kwargs={"load_p": load_p, + "load_q": load_q, + "prod_p": gen_p + }, + grid_path=case_name, + _add_to_name=f"{case_name}", + ) + return env_lightsim + +if __name__ == "__main__": + + import pandapower.networks as pn + for case_name in case_names: + tmp_nm = os.path.splitext(case_name)[0] + print(f"====================== {tmp_nm} ======================") + case = getattr(pn,tmp_nm)() + pp.runpp(case) # for slack + + load_p_init = 1.0 * case.load["p_mw"].values + load_q_init = 1.0 * case.load["q_mvar"].values + gen_p_init = 1.0 * case.gen["p_mw"].values + sgen_p_init = 1.0 * case.sgen["p_mw"].values + + nb_ts = 1 + # add slack ! + slack_gens = np.zeros((nb_ts, case.ext_grid.shape[0])) + if "res_ext_grid" in case: + slack_gens += np.tile(case.res_ext_grid["p_mw"].values.reshape(1,-1), (nb_ts, 1)) + gen_p_g2op = np.concatenate((gen_p_init.reshape(1,-1), slack_gens), axis=1) + + with tempfile.TemporaryDirectory() as tmpdirname: + pp.to_json(case, os.path.join(tmpdirname, case_name)) + with open(os.path.join(tmpdirname, "config.py"), "w") as f: + f.write("config = {}") + + env = make_grid2op_env(None, + os.path.join(tmpdirname, case_name), + load_p=load_p_init.reshape(1,-1), + load_q=load_q_init.reshape(1,-1), + gen_p=gen_p_g2op.reshape(1,-1), + sgen_p=None) + + env.backend._grid.tell_solver_need_reset() + _ = env.step(env.action_space()) + ls_solver = env.backend._grid.get_solver() + nb_iter_solver = ls_solver.get_nb_iter() + timers = ls_solver.get_timers_jacobian() + (timer_Fx, timer_solve, timer_init, timer_check, + timer_compute_dS, timer_fillJ, timer_compVa_Vm, timer_preproc, timer_total) = timers + print(f"Total time for the powerflow (=pre proc + NR + post proc): {env.backend._grid.timer_last_ac_pf:.2e}s") + print(f"Total time spent in the Newton Raphson: {timer_total:.2e}s") + print(f"Time to pre process input data: {timer_preproc:.2e}s") + print(f"Time to intialize linear solver: {timer_init:.2e}s") + print(f"Then for all iterations (cumulated time over all {nb_iter_solver} iterations)") + print(f"\ttotal time to compute dS/dVm and dS/dVa: {timer_compute_dS:.2e}s") + print(f"\ttotal time fill jacobian matrix (from dS/dVm and dS/dVa): {timer_fillJ:.2e}s") + print(f"\ttotal time to solve J.x = b: {timer_solve:.2e}s") + print(f"\ttotal time to compute V, Va and Vm: {timer_compVa_Vm:.2e}s") + print(f"\ttotal time to compute p, q mismatch at buses: {timer_Fx:.2e}s") + print(f"\ttotal time to check if p,q mismatch at buses are within tolerance: {timer_check:.2e}s") + print(f"====================== {' '*len(tmp_nm)} ======================") + \ No newline at end of file diff --git a/benchmarks/benchmark_gauss_seidel.py b/benchmarks/benchmark_gauss_seidel.py new file mode 100644 index 0000000..18be5ee --- /dev/null +++ b/benchmarks/benchmark_gauss_seidel.py @@ -0,0 +1,192 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of LightSim2grid, LightSim2grid a implements a c++ backend targeting the Grid2Op platform. + +import warnings +import copy +import pandapower as pp +import numpy as np +import hashlib +from scipy.interpolate import interp1d +import matplotlib.pyplot as plt +from grid2op import make, Parameters +from grid2op.Chronics import FromNPY, ChangeNothing +from grid2op.Backend import PandaPowerBackend +from grid2op.Exceptions import Grid2OpException +import lightsim2grid +from lightsim2grid import LightSimBackend +from benchmark_grid_size import (get_loads_gens, + make_grid2op_env_pp, + run_grid2op_env, + make_grid2op_env) +from benchmark_solvers import solver_gs, solver_names, order_solver_print + +from tqdm import tqdm +import os +from utils_benchmark import print_configuration, get_env_name_displayed +from benchmark_solvers import solver_names + +try: + from tabulate import tabulate + TABULATE_AVAIL = True +except ImportError: + print("The tabulate package is not installed. Some output might not work properly") + TABULATE_AVAIL = False + +VERBOSE = False +MAKE_PLOT = False +WITH_PP = False +DEBUG = False + +case_names = [ + "case14.json", + "case118.json", + "case_illinois200.json", + "case300.json", + "case1354pegase.json", + "case1888rte.json", + # # "GBnetwork.json", # 2224 buses + # "case2848rte.json", + # "case2869pegase.json", + # "case3120sp.json", + # "case6495rte.json", + # "case6515rte.json", + # "case9241pegase.json" + ] + + +if __name__ == "__main__": + prng = np.random.default_rng(42) + case_names_displayed = [get_env_name_displayed(el) for el in case_names] + nb_iters = [] + ts_sizes = [] + errors = {} + for case_name in tqdm(case_names): + + if not os.path.exists(case_name): + import pandapower.networks as pn + case = getattr(pn, os.path.splitext(case_name)[0])() + pp.to_json(case, case_name) + + # load the case file + case = pp.from_json(case_name) + ts_sizes.append(case.bus.shape[0]) + pp.runpp(case) # for slack + + # create the env + param = Parameters.Parameters() + param.init_from_dict({"NO_OVERFLOW_DISCONNECTION": True}) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env_pp = make("blank", + param=param, test=True, + backend=PandaPowerBackend(lightsim2grid=False), + chronics_class=ChangeNothing, + grid_path=case_name, + _add_to_name=f"{case_name}", + ) + env_ls = make("blank", + param=param, test=True, + backend=LightSimBackend(), + chronics_class=ChangeNothing, + grid_path=case_name, + _add_to_name=f"{case_name}", + ) + env_ls.backend.set_solver_type(lightsim2grid.SolverType.GaussSeidel) + all_iters = [1, 3, 10, 30, 100, 300, 1_000, 3_000, + 10_000, 30_000, + 100_000, 300_000 + ] + iters = [] + errors_p = [] + errors_q = [] + for max_iter in all_iters: + env_ls.backend.set_solver_max_iter(max_iter) + env_ls.backend._grid.tell_solver_need_reset() + conv = True + try: + obs = env_ls.reset() + except Grid2OpException as exc_: + conv = False + iters.append(env_ls.backend._grid.get_solver().get_nb_iter()) + v_tmp = env_ls.backend._grid.get_solver().get_V() + res_tmp = env_ls.backend._grid.check_solution(v_tmp, False) + error_p = 1. * np.abs(res_tmp.real).max() + error_q = 1. * np.abs(res_tmp.imag).max() + errors_p.append(error_p) + errors_q.append(error_q) + if conv: + break + if conv: + nb_iters.append(iters[-1]) + else: + nb_iters.append(None) + + errors[case.bus.shape[0]] = (errors_p, errors_q) + + print("Configuration:") + print_configuration() + print(f"Solver used for linear algebra: {lightsim2grid.SolverType.GaussSeidel}") + print() + hds = ["grid size (nb bus)", "gauss seidel max iter"] + tab = [] + for sz, nb_it in zip(ts_sizes, nb_iters): + tab.append([sz, nb_it]) + + if TABULATE_AVAIL: + res_use_with_grid2op_2 = tabulate(tab, headers=hds, tablefmt="rst") + print(res_use_with_grid2op_2) + else: + print(tab) + + print(errors[118][0]) + print(errors[118][1]) + import pickle + with open("res_gauss_seidel.pickle", "wb") as f: + pickle.dump(errors, file=f) + with open("res_gauss_seidel_nb_iters.pickle", "wb") as f: + pickle.dump(nb_iters, file=f) + print() + print() + + +# total computation time : 1h27min16s +# Configuration: + +# - date: 2024-12-02 18:46 CET +# - system: Linux 5.15.0-56-generic +# - OS: ubuntu 20.04 +# - processor: Intel(R) Core(TM) i7-6820HQ CPU @ 2.70GHz +# - python version: 3.8.10.final.0 (64 bit) +# - numpy version: 1.24.3 +# - pandas version: 2.0.3 +# - pandapower version: 2.14.0 +# - grid2op version: 1.11.0.dev2 +# - lightsim2grid version: 0.9.2.post2 +# - lightsim2grid extra information: + +# - klu_solver_available: True +# - nicslu_solver_available: False +# - cktso_solver_available: False +# - compiled_march_native: False +# - compiled_o3_optim: False + +# Solver used for linear algebra: SolverType.GaussSeidel + +# ==================== ======================= +# grid size (nb bus) gauss seidel max iter +# ==================== ======================= +# 14 278 +# 118 3274 +# 200 8360 +# 300 40783 +# 1354 122169 +# 1888 +# ==================== ======================= +# [31.858705410410803, 13.801689961508492, 7.912199121114395, 6.387621207822959, 4.5494311573542525, 1.3539274305627065, 0.01652457790687702, 5.5928201247405206e-08, 9.957519963773673e-09] +# [111.7637849724719, 52.1105433668106, 6.3902552555152345, 1.1851759157023143, 0.8457897295792693, 0.25197455746676584, 0.0030761171444685202, 1.0415372012959338e-08, 1.8561325626140559e-09] diff --git a/benchmarks/benchmark_grid_size.py b/benchmarks/benchmark_grid_size.py index bbc2b62..5269b3f 100644 --- a/benchmarks/benchmark_grid_size.py +++ b/benchmarks/benchmark_grid_size.py @@ -190,7 +190,9 @@ def run_grid2op_env(env_lightsim, case, reset_solver, g2op_step_time, ls_solver_time, ls_gridmodel_time, - g2op_sizes + g2op_sizes, + sgen_p, + nb_ts ): _ = env_lightsim.reset() done = False @@ -330,7 +332,7 @@ def run_grid2op_env(env_lightsim, case, reset_solver, g2op_step_time_reset, ls_solver_time_reset, ls_gridmodel_time_reset, - g2op_sizes_reset + g2op_sizes_reset, sgen_p, nb_ts ) reset_solver = False # default @@ -340,7 +342,7 @@ def run_grid2op_env(env_lightsim, case, reset_solver, g2op_step_time, ls_solver_time, ls_gridmodel_time, - g2op_sizes + g2op_sizes, sgen_p, nb_ts ) # Perform the computation using TimeSerie diff --git a/docs/conf.py b/docs/conf.py index 93f2a29..512eedd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,8 +22,8 @@ author = 'Benjamin DONNOT' # The full version, including alpha/beta/rc tags -release = "0.9.2.post2" -version = '0.9' +release = "0.10.0" +version = '0.10' # -- General configuration --------------------------------------------------- diff --git a/lightsim2grid/__init__.py b/lightsim2grid/__init__.py index d1c3abe..f5c5ccd 100644 --- a/lightsim2grid/__init__.py +++ b/lightsim2grid/__init__.py @@ -6,7 +6,7 @@ # SPDX-License-Identifier: MPL-2.0 # This file is part of LightSim2grid, LightSim2grid implements a c++ backend targeting the Grid2Op platform. -__version__ = "0.9.2.post2" +__version__ = "0.10.0" __all__ = ["newtonpf", "SolverType", "ErrorType", "solver", "compilation_options"] diff --git a/lightsim2grid/lightSimBackend.py b/lightsim2grid/lightSimBackend.py index 8fe4ce3..1813442 100644 --- a/lightsim2grid/lightSimBackend.py +++ b/lightsim2grid/lightSimBackend.py @@ -36,6 +36,12 @@ except ImportError: # for backward compatibility with grid2op <= 1.9.8 DEFAULT_N_BUSBAR_PER_SUB = 2 + +try: + from grid2op.Space import DEFAULT_ALLOW_DETACHMENT +except ImportError: + # for backward compatibility with grid2op < 1.11.0 + DEFAULT_ALLOW_DETACHMENT = False try: from typing import Literal @@ -61,6 +67,9 @@ class LightSimBackend(Backend): # for legacy grid2op n_busbar_per_sub = DEFAULT_N_BUSBAR_PER_SUB + if not hasattr(Backend, "detachment_is_allowed"): + # for legacy grid2op (< 1.11.0) + detachment_is_allowed = DEFAULT_ALLOW_DETACHMENT def __init__(self, detailed_infos_for_cascading_failures: bool=False, @@ -73,8 +82,9 @@ def __init__(self, use_static_gen: bool=False, # add the static generators as generator gri2dop side loader_method: Literal["pandapower", "pypowsybl"] = "pandapower", loader_kwargs : Optional[dict] = None, - stop_if_load_disco : Optional[bool] = True, - stop_if_gen_disco : Optional[bool] = True, + stop_if_load_disco : Optional[bool] = None, + stop_if_gen_disco : Optional[bool] = None, + stop_if_storage_disco : Optional[bool] = None, ): #: ``int`` maximum number of iteration allowed for the solver #: if the solver has not converge after this, it will @@ -131,13 +141,32 @@ def __init__(self, #: #: if set to ``True`` (default) then the backend will raise a #: BackendError in case of disconnected load + #: + #: .. note:: When using grid2op >= 1.11.0 and lightsim2grid >= 0.10.0 this + #: is automatically set-up with the call to grid2op.make, + #: to match the behaviour expected by `allow_detachment` self._stop_if_load_disco = stop_if_load_disco #: .. versionadded:: 0.8.0 #: #: if set to ``True`` (default) then the backend will raise a #: BackendError in case of disconnected generator + #: + #: .. note:: When using grid2op >= 1.11.0 and lightsim2grid >= 0.10.0 this + #: is automatically set-up with the call to grid2op.make, + #: to match the behaviour expected by `allow_detachment` self._stop_if_gen_disco = stop_if_gen_disco + + #: .. versionadded:: 0.10.0 + #: + #: if set to ``True`` (default) then the backend will raise a + #: BackendError in case of disconnected storage that are + #: asked to produce / absorb something + #: + #: .. note:: When using grid2op >= 1.11.0 and lightsim2grid >= 0.10.0 this + #: is automatically set-up with the call to grid2op.make, + #: to match the behaviour expected by `allow_detachment` + self._stop_if_storage_disco = stop_if_storage_disco self._aux_init_super(detailed_infos_for_cascading_failures, can_be_copied, @@ -150,7 +179,8 @@ def __init__(self, loader_method, loader_kwargs, stop_if_load_disco, - stop_if_gen_disco) + stop_if_gen_disco, + stop_if_storage_disco) # backward compat: need to define it if not done by grid2op if not hasattr(self, "_can_be_copied"): @@ -298,6 +328,13 @@ def __init__(self, # backend SHOULD not do these kind of stuff self._idx_hack_storage = [] + #: ..versionadded: 0.9.3 + #: sometimes some actions will make the grid fails + #: but grid2op expect it to fail not on "apply_action" + #: but rather when calling `runpf` + #: this flags remembers it + self._next_pf_fails : Optional[BackendError] = None + # speed optimization self._lineor_res = None self._lineex_res = None @@ -323,7 +360,8 @@ def _aux_init_super(self, loader_method, loader_kwargs, stop_if_load_disco, - stop_if_gen_disco): + stop_if_gen_disco, + stop_if_storage_disco): try: # for grid2Op >= 1.7.1 Backend.__init__(self, @@ -339,6 +377,7 @@ def _aux_init_super(self, loader_kwargs=loader_kwargs, stop_if_load_disco=stop_if_load_disco, stop_if_gen_disco=stop_if_gen_disco, + stop_if_storage_disco=stop_if_storage_disco ) except TypeError as exc_: warnings.warn("Please use grid2op >= 1.7.1: with older grid2op versions, " @@ -350,6 +389,10 @@ def _aux_init_super(self, # do not forget to propagate this if needed self.can_handle_more_than_2_busbar() + if hasattr(type(self), "can_handle_detachment"): + # do not forget to propagate this if needed + self.can_handle_detachment() + def turnedoff_no_pv(self): self._turned_off_pv = False self._grid.turnedoff_no_pv() @@ -516,7 +559,9 @@ def _handle_turnedoff_pv(self): self._grid.turnedoff_no_pv() def _assign_right_solver(self): - has_single_slack = np.where(np.array([el.slack_weight for el in self._grid.get_generators()]) != 0.)[0].shape[0] == 1 + slack_weights = np.array([el.slack_weight for el in self._grid.get_generators()]) + nb_slack_nonzero = (np.abs(slack_weights) > 0.).sum() + has_single_slack = nb_slack_nonzero == 1 if has_single_slack and not self._dist_slack_non_renew: if SolverType.KLUSingleSlack in self.available_solvers: # use the faster KLU if available @@ -530,14 +575,96 @@ def _assign_right_solver(self): self._grid.change_solver(SolverType.KLU) else: self._grid.change_solver(SolverType.SparseLU) + + def _aux_set_correct_detach_flags_d_allowed(self): + # user allowed detachment, I check the correct flags + if self._stop_if_gen_disco is None: + # user did not specify anything + self._stop_if_gen_disco = False + elif not self._stop_if_gen_disco: + # force conversion to proper type + self._stop_if_gen_disco = False + elif self._stop_if_gen_disco: + # erase default values and continue like the grid2op call specifies + warnings.warn("Call to `grid2op.make(..., allow_detachement=True)` will erase the lightsim2grid kwargs `stop_if_gen_disco=True`") + self._stop_if_gen_disco = False + + if self._stop_if_load_disco is None: + # user did not specify anything + self._stop_if_load_disco = False + elif not self._stop_if_load_disco: + # force conversion to the proper type + self._stop_if_load_disco = False + elif self._stop_if_load_disco: + # erase default values and continue like the grid2op call specifies + warnings.warn("Call to `grid2op.make(..., allow_detachement=True)` will erase the lightsim2grid kwargs `stop_if_load_disco=True`") + self._stop_if_load_disco = False + + if self._stop_if_storage_disco is None: + # user did not specify anything + self._stop_if_storage_disco = False + elif not self._stop_if_storage_disco: + # force conversion to the proper type + self._stop_if_storage_disco = False + elif self._stop_if_storage_disco: + # erase default values and continue like the grid2op call specifies + warnings.warn("Call to `grid2op.make(..., allow_detachement=True)` will erase the lightsim2grid kwargs `stop_if_storage_disco=True`") + self._stop_if_storage_disco = False + + def _aux_set_correct_detach_flags_d_not_allowed(self):# user did not allow detachment (or it's a legacy grid2op version), I check the correct flags + if self._stop_if_gen_disco is None: + # user did not specify anything + self._stop_if_gen_disco = True + elif self._stop_if_gen_disco: + # force conversion to proper type + self._stop_if_gen_disco = True + elif not self._stop_if_gen_disco: + # erase default values and continue like the grid2op call specifies + warnings.warn("Call to `grid2op.make(..., allow_detachement=False)` will erase the lightsim2grid kwargs `stop_if_gen_disco=False`") + self._stop_if_gen_disco = True + + if self._stop_if_load_disco is None: + # user did not specify anything + self._stop_if_load_disco = True + elif self._stop_if_load_disco: + # force conversion to proper type + self._stop_if_load_disco = True + elif not self._stop_if_load_disco: + # erase default values and continue like the grid2op call specifies + warnings.warn("Call to `grid2op.make(..., allow_detachement=False)` will erase the lightsim2grid kwargs `stop_if_load_disco=False`") + self._stop_if_load_disco = True + + if self._stop_if_storage_disco is None: + # user did not specify anything + self._stop_if_storage_disco = True + elif self._stop_if_storage_disco: + # force conversion to proper type + self._stop_if_storage_disco = True + elif not self._stop_if_storage_disco: + # erase default values and continue like the grid2op call specifies + warnings.warn("Call to `grid2op.make(..., allow_detachement=False)` will erase the lightsim2grid kwargs `stop_if_storage_disco=False`") + self._stop_if_storage_disco = True + def _aux_set_correct_detach_flags(self): + if self.detachment_is_allowed: + self._aux_set_correct_detach_flags_d_allowed() + else: + self._aux_set_correct_detach_flags_d_not_allowed() + def load_grid(self, path : Union[os.PathLike, str], filename : Optional[Union[os.PathLike, str]]=None) -> None: - if hasattr(type(self), "can_handle_more_than_2_busbar"): + cls = type(self) + if hasattr(cls, "can_handle_more_than_2_busbar"): # grid2op version >= 1.10.0 then we use this self.can_handle_more_than_2_busbar() - + + if hasattr(cls, "can_handle_detachment"): + # grid2op version >= 1.11.0 then we use this + self.can_handle_detachment() + + self._aux_set_correct_detach_flags() + if self._loader_method == "pandapower": self._load_grid_pandapower(path, filename) elif self._loader_method == "pypowsybl": @@ -546,6 +673,7 @@ def load_grid(self, raise BackendError(f"Impossible to initialize the backend with '{self._loader_method}'") self._grid.tell_solver_need_reset() self._reset_res_pointers() # force the re reading of the accessors at next powerflow + self.V = np.ones(self.nb_bus_total, dtype=np.complex_) def _should_not_have_to_do_this(self, path=None, filename=None): # included in grid2op now ! @@ -1148,6 +1276,7 @@ def apply_action(self, backendAction: Union["grid2op.Action._backendAction._Back Specific implementation of the method to apply an action modifying a powergrid in the pandapower format. """ tick = time.perf_counter() + self._next_pf_fails = None active_bus, *_, topo__, shunts__ = backendAction() # change the overall topology @@ -1178,7 +1307,11 @@ def apply_action(self, backendAction: Union["grid2op.Action._backendAction._Back backendAction.storage_power.values) except RuntimeError as exc_: # modification of power of disconnected storage has no effect in lightsim2grid - pass + if self.detachment_is_allowed: + # a storage units is allowed to be disconnected in this case + pass + else: + self._next_pf_fails = BackendError("Some storage units would be disconnected") # handle shunts if type(self).shunts_data_available: @@ -1231,7 +1364,10 @@ def _fetch_grid_data(self): def runpf(self, is_dc : bool=False) -> Tuple[bool, Union[Exception, None]]: my_exc_ = None res = False - try: + try: + if self._next_pf_fails is not None: + raise self._next_pf_fails + beg_preproc = time.perf_counter() if is_dc: # somehow, when asked to do a powerflow in DC, pandapower assign Vm to be @@ -1331,20 +1467,31 @@ def runpf(self, is_dc : bool=False) -> Tuple[bool, Union[Exception, None]]: self.next_prod_p[:] = self.prod_p if self._stop_if_load_disco and ((~np.isfinite(self.load_v)).any() or (self.load_v <= 0.).any()): disco = (~np.isfinite(self.load_v)) | (self.load_v <= 0.) - load_disco = np.where(disco)[0] + load_disco = disco.nonzero()[0] self._timer_postproc += time.perf_counter() - beg_postroc raise BackendError(f"At least one load is disconnected (check loads {load_disco})") if self._stop_if_gen_disco and ((~np.isfinite(self.prod_v)).any() or (self.prod_v <= 0.).any()): disco = (~np.isfinite(self.prod_v)) | (self.prod_v <= 0.) - gen_disco = np.where(disco)[0] + gen_disco = disco.nonzero()[0] self._timer_postproc += time.perf_counter() - beg_postroc raise BackendError(f"At least one generator is disconnected (check gen {gen_disco})") + + if self.__has_storage: + sto_active = (np.abs(self.storage_p) > 0.) + sto_act_disco = (((~np.isfinite(self.storage_v)) & sto_active).any() or + ((self.storage_v <= 0.) & sto_active).any() + ) + if self._stop_if_storage_disco and sto_act_disco: + disco = ((~np.isfinite(self.storage_v)) | (self.storage_v <= 0.)) & sto_active + sto_disco = disco.nonzero()[0] + self._timer_postproc += time.perf_counter() - beg_postroc + raise BackendError(f"At least one storage unit is disconnected (check gen {sto_disco})") # TODO storage case of divergence ! if type(self).shunts_data_available: self._set_shunt_info() - if (self.line_or_theta >= 1e6).any() or (self.line_ex_theta >= 1e6).any(): + if (np.abs(self.line_or_theta) >= 1e6).any() or (np.abs(self.line_ex_theta) >= 1e6).any(): raise BackendError(f"Some theta are above 1e6 which should not be happening !") res = True self._grid.unset_changes() @@ -1369,6 +1516,7 @@ def runpf(self, is_dc : bool=False) -> Tuple[bool, Union[Exception, None]]: def _fill_nans(self): """fill the results vectors with nans""" + self._next_pf_fails = None self.p_or[:] = np.NaN self.q_or[:] = np.NaN self.v_or[:] = np.NaN @@ -1449,7 +1597,8 @@ def copy(self) -> Self: self._loader_method, self._loader_kwargs, self._stop_if_load_disco, - self._stop_if_gen_disco) + self._stop_if_gen_disco, + self._stop_if_storage_disco) # for backward compat (attribute was not necessarily present in early grid2op) if not hasattr(res, "_can_be_copied"): @@ -1473,8 +1622,8 @@ def copy(self) -> Self: "supported_grid_format", "max_it", "tol", "_turned_off_pv", "_dist_slack_non_renew", "_use_static_gen", "_loader_method", "_loader_kwargs", - "_stop_if_load_disco", "_stop_if_gen_disco", - "_timer_fetch_data_cpp" + "_stop_if_load_disco", "_stop_if_gen_disco", "_stop_if_storage_disco", + "_timer_fetch_data_cpp", "_next_pf_fails" ] for attr_nm in li_regular_attr: if hasattr(self, attr_nm): diff --git a/lightsim2grid/tests/test_issue_66.py b/lightsim2grid/tests/test_issue_66.py index be2cc08..2643bd4 100644 --- a/lightsim2grid/tests/test_issue_66.py +++ b/lightsim2grid/tests/test_issue_66.py @@ -83,14 +83,15 @@ def test_disco_storage(self): obs = self.env.reset() act = self.env.action_space({"set_bus": {"storages_id": [(0, -1)]}}) obs, reward, done, info = self.env.step(act) - assert len(info['exception']) == 0 + assert len(info['exception']) == 0, f"{len(info['exception'])} vs 0" assert not done # should not raise any RuntimeError act = self.env.action_space({"set_storage": [(0, -1)]}) obs, reward, done, info = self.env.step(act) - assert len(info['exception']) == 0 - assert not done + # grid2op >= 1.11.0 requires this + assert len(info['exception']) == 1, f"{len(info['exception'])} vs 1" + assert done # should not raise any RuntimeError diff --git a/setup.py b/setup.py index 8667f7c..2cff340 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ from pybind11.setup_helpers import Pybind11Extension, build_ext -__version__ = "0.9.2.post2" +__version__ = "0.10.0" KLU_SOLVER_AVAILABLE = False # Try to link against SuiteSparse (if available) diff --git a/src/GridModel.h b/src/GridModel.h index 0c50403..e16e829 100644 --- a/src/GridModel.h +++ b/src/GridModel.h @@ -494,9 +494,9 @@ class GridModel : public GenericContainer // reactivate_storage(storage_id); // requirement from grid2op, might be discussed // storages_.change_p(storage_id, new_p, need_reset_); // } - storages_.change_p(storage_id, new_p, solver_control_); + storages_.change_p_nothrow(storage_id, new_p, solver_control_); } - void change_q_storage(int storage_id, real_type new_q) {storages_.change_q(storage_id, new_q, solver_control_); } + void change_q_storage(int storage_id, real_type new_q) {storages_.change_q_nothrow(storage_id, new_q, solver_control_); } int get_bus_storage(int storage_id) {return storages_.get_bus(storage_id);} //deactivate a powerline (disconnect it) diff --git a/src/element_container/LoadContainer.cpp b/src/element_container/LoadContainer.cpp index e7c7483..fedf1f6 100644 --- a/src/element_container/LoadContainer.cpp +++ b/src/element_container/LoadContainer.cpp @@ -117,10 +117,7 @@ void LoadContainer::change_p(int load_id, real_type new_p, SolverControl & solve exc_ << ")"; throw std::runtime_error(exc_.str()); } - if (p_mw_(load_id) != new_p) { - solver_control.tell_recompute_sbus(); - p_mw_(load_id) = new_p; - } + change_p_nothrow(load_id, new_p, solver_control); } void LoadContainer::change_q(int load_id, real_type new_q, SolverControl & solver_control) @@ -134,10 +131,7 @@ void LoadContainer::change_q(int load_id, real_type new_q, SolverControl & solve exc_ << ")"; throw std::runtime_error(exc_.str()); } - if (q_mvar_(load_id) != new_q) { - solver_control.tell_recompute_sbus(); - q_mvar_(load_id) = new_q; - } + change_q_nothrow(load_id, new_q, solver_control); } void LoadContainer::reconnect_connected_buses(std::vector<bool> & bus_status) const { diff --git a/src/element_container/LoadContainer.h b/src/element_container/LoadContainer.h index 9a6710b..edfea92 100644 --- a/src/element_container/LoadContainer.h +++ b/src/element_container/LoadContainer.h @@ -153,7 +153,21 @@ class LoadContainer : public GenericContainer void change_bus(int load_id, int new_bus_id, SolverControl & solver_control, int nb_bus) {_change_bus(load_id, new_bus_id, bus_id_, solver_control, nb_bus);} int get_bus(int load_id) {return _get_bus(load_id, status_, bus_id_);} void change_p(int load_id, real_type new_p, SolverControl & solver_control); + void change_p_nothrow(int load_id, real_type new_p, SolverControl & solver_control) + { + if (p_mw_(load_id) != new_p) { + solver_control.tell_recompute_sbus(); + p_mw_(load_id) = new_p; + } + } void change_q(int load_id, real_type new_q, SolverControl & solver_control); + void change_q_nothrow(int load_id, real_type new_q, SolverControl & solver_control) + { + if (q_mvar_(load_id) != new_q) { + solver_control.tell_recompute_sbus(); + q_mvar_(load_id) = new_q; + } + } virtual void reconnect_connected_buses(std::vector<bool> & bus_status) const; virtual void disconnect_if_not_in_main_component(std::vector<bool> & busbar_in_main_component);