From 8271b94688c6b94cf5bf7e8a86e1e1e957726f76 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Mon, 3 Feb 2025 15:45:50 +0100 Subject: [PATCH 1/4] introduce support for detachment and more than 2 bubsbars per sub Signed-off-by: DONNOT Benjamin --- pypowsybl2grid/pypowsybl_backend.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pypowsybl2grid/pypowsybl_backend.py b/pypowsybl2grid/pypowsybl_backend.py index c9473c5..0c8e3db 100644 --- a/pypowsybl2grid/pypowsybl_backend.py +++ b/pypowsybl2grid/pypowsybl_backend.py @@ -83,7 +83,15 @@ def network(self) -> pp.network.Network: def load_grid(self, path: Union[os.PathLike, str], filename: Optional[Union[os.PathLike, str]] = None) -> None: - start_time = time.time() + start_time = time.perf_counter() + 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() # load network full_path = self.make_complete_path(path, filename) @@ -98,7 +106,7 @@ def load_grid(self, self.load_grid_from_iidm(network) - end_time = time.time() + end_time = time.perf_counter() elapsed_time = (end_time - start_time) * 1000 logger.info(f"Network '{network.id}' loaded in {elapsed_time:.2f} ms") From 37a7afe2231916e89313354c77e6c84dff41ad44 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 13 Feb 2025 11:24:49 +0100 Subject: [PATCH 2/4] adding support for detachment Signed-off-by: DONNOT Benjamin --- pypowsybl2grid/pypowsybl_backend.py | 58 ++++++++-- pyproject.toml | 5 +- tests/test_detachment_support_info.py | 148 ++++++++++++++++++++++++++ 3 files changed, 203 insertions(+), 8 deletions(-) create mode 100644 tests/test_detachment_support_info.py diff --git a/pypowsybl2grid/pypowsybl_backend.py b/pypowsybl2grid/pypowsybl_backend.py index 0c8e3db..1ea0d70 100644 --- a/pypowsybl2grid/pypowsybl_backend.py +++ b/pypowsybl2grid/pypowsybl_backend.py @@ -8,6 +8,7 @@ import os import time from typing import Optional, Tuple, Union +import warnings import grid2op from grid2op.dtypes import dt_float, dt_int @@ -27,15 +28,22 @@ class PyPowSyBlBackend(Backend): def __init__(self, - detailed_infos_for_cascading_failures=False, - check_isolated_and_disconnected_injections = True, - consider_open_branch_reactive_flow = False, - n_busbar_per_sub = DEFAULT_N_BUSBAR_PER_SUB, - connect_all_elements_to_first_bus = False, + detailed_infos_for_cascading_failures:bool=False, + check_isolated_and_disconnected_injections:Optional[bool]=None, + consider_open_branch_reactive_flow:bool = False, + n_busbar_per_sub:int = DEFAULT_N_BUSBAR_PER_SUB, + connect_all_elements_to_first_bus:bool = False, lf_parameters: pp.loadflow.Parameters = None): Backend.__init__(self, detailed_infos_for_cascading_failures=detailed_infos_for_cascading_failures, - can_be_copied=True) + can_be_copied=True, + # save this kwargs (might be needed) + check_isolated_and_disconnected_injections=check_isolated_and_disconnected_injections, + consider_open_branch_reactive_flow=consider_open_branch_reactive_flow, + connect_all_elements_to_first_bus=connect_all_elements_to_first_bus, + lf_parameters=lf_parameters + ) + self._check_isolated_and_disconnected_injections = check_isolated_and_disconnected_injections self._consider_open_branch_reactive_flow = consider_open_branch_reactive_flow self.n_busbar_per_sub = n_busbar_per_sub @@ -92,6 +100,11 @@ def load_grid(self, if hasattr(cls, "can_handle_detachment"): # grid2op version >= 1.11.0 then we use this self.can_handle_detachment() + self.check_detachment_coherent() + else: + if self._check_isolated_and_disconnected_injections is None: + # default behaviour in grid2op before detachment is allowed + self._check_isolated_and_disconnected_injections = True # load network full_path = self.make_complete_path(path, filename) @@ -110,6 +123,39 @@ def load_grid(self, elapsed_time = (end_time - start_time) * 1000 logger.info(f"Network '{network.id}' loaded in {elapsed_time:.2f} ms") + def check_detachment_coherent(self): + if self._check_isolated_and_disconnected_injections is None: + # user does not provide anything to the backend + if self.detachment_is_allowed: + self._check_isolated_and_disconnected_injections = False + else: + self._check_isolated_and_disconnected_injections = True + else: + # user provided something, I check if it's consistent + if self._check_isolated_and_disconnected_injections: + if self.detachment_is_allowed: + msg_ = ("You initialized the pypowsybl backend with \"check_isolated_and_disconnected_injections=True\" " + "and the environment with \"allow_detachment=True\" which is not consistent. " + "Discarding the call to env.make, the detachement is NOT supported for this env. " + "If you want to support detachment, either initialize the pypowsybl backend with " + "\"check_isolated_and_disconnected_injections=False\" or (preferably) with " + "\"check_isolated_and_disconnected_injections=None\"") + warnings.warn(msg_) + logger.warning(msg_) + type(self).detachment_is_allowed = False + self.detachment_is_allowed = False + else: + if not self.detachment_is_allowed: + msg_ = ("You initialized the pypowsybl backend with \"check_isolated_and_disconnected_injections=False\" " + "and the environment with \"allow_detachment=False\" which is not consistent. " + "The setting of \"check_isolated_and_disconnected_injections=False\" will have no effect. " + "Detachment will NOT be supported. If you want so, please consider initializing pypowsybl backend with " + "\"check_isolated_and_disconnected_injections=True\" or (preferably) with " + "\"check_isolated_and_disconnected_injections=None\"") + warnings.warn(msg_) + logger.warning(msg_) + + def load_grid_from_iidm(self, network: pp.network.Network) -> None: if self._grid: self._grid.close() diff --git a/pyproject.toml b/pyproject.toml index e2abb03..734a331 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,9 @@ [tool.poetry] name = "pypowsybl2grid" -version = "0.2.0" +version = "0.2.1" description = "An integration between Grid2op and PyPowSybl" -authors = ["Geoffroy Jamgotchian "] +authors = ["Geoffroy Jamgotchian ", + "Benjamin Donnot "] readme = "README.md" license = "MPL-2.0" diff --git a/tests/test_detachment_support_info.py b/tests/test_detachment_support_info.py new file mode 100644 index 0000000..97fd264 --- /dev/null +++ b/tests/test_detachment_support_info.py @@ -0,0 +1,148 @@ +# Copyright (c) 2025, RTE (http://www.rte-france.com) +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 + +import logging +from typing import Dict + +import grid2op +import grid2op.Space + +import pytest + +from pypowsybl2grid.pypowsybl_backend import PyPowSyBlBackend + +TOLERANCE = 1e-3 + +@pytest.fixture(autouse=True) +def setup(): + logging.basicConfig() + logging.getLogger('powsybl').setLevel(logging.WARN) + + +def create_env(check_isolated_and_disconnected_injections=None, allow_detachment=None): + if not hasattr(grid2op.Space, "DEFAULT_ALLOW_DETACHMENT"): + return None + backend = create_backend(check_isolated_and_disconnected_injections=check_isolated_and_disconnected_injections) + if allow_detachment is not None: + env = grid2op.make("l2rpn_case14_sandbox", test=True, allow_detachment=allow_detachment, backend=backend) + else: + env = grid2op.make("l2rpn_case14_sandbox", test=True, backend=backend) + return env + + +def create_backend(check_isolated_and_disconnected_injections=None): + return PyPowSyBlBackend(check_isolated_and_disconnected_injections=check_isolated_and_disconnected_injections, + consider_open_branch_reactive_flow=True, + connect_all_elements_to_first_bus=False, + ) + + +def test_None_False(): + env = create_env(check_isolated_and_disconnected_injections=None, allow_detachment=False) + if env is None: + # wrong grid2Op version + return + try: + assert not type(env.backend).detachment_is_allowed + assert not type(env).detachment_is_allowed + finally: + env.close() + + +def test_None_default(): + env = create_env(check_isolated_and_disconnected_injections=None) + if env is None: + # wrong grid2Op version + return + try: + assert not type(env.backend).detachment_is_allowed + assert not type(env).detachment_is_allowed + finally: + env.close() + + +def test_None_True(): + env = create_env(check_isolated_and_disconnected_injections=None, allow_detachment=True) + if env is None: + # wrong grid2Op version + return + try: + assert type(env.backend).detachment_is_allowed + assert type(env).detachment_is_allowed + finally: + env.close() + + +def test_False_False(): + env = create_env(check_isolated_and_disconnected_injections=False, allow_detachment=False) + if env is None: + # wrong grid2Op version + return + try: + assert not type(env.backend).detachment_is_allowed + assert not type(env).detachment_is_allowed + finally: + env.close() + + +def test_False_default(): + env = create_env(check_isolated_and_disconnected_injections=False) + if env is None: + # wrong grid2Op version + return + try: + assert not type(env.backend).detachment_is_allowed + assert not type(env).detachment_is_allowed + finally: + env.close() + + +def test_False_True(): + env = create_env(check_isolated_and_disconnected_injections=False, allow_detachment=True) + if env is None: + # wrong grid2Op version + return + try: + assert type(env.backend).detachment_is_allowed + assert type(env).detachment_is_allowed + finally: + env.close() + + +def test_True_False(): + env = create_env(check_isolated_and_disconnected_injections=True, allow_detachment=False) + if env is None: + # wrong grid2Op version + return + try: + assert not type(env.backend).detachment_is_allowed + assert not type(env).detachment_is_allowed + finally: + env.close() + + +def test_True_default(): + env = create_env(check_isolated_and_disconnected_injections=True) + if env is None: + # wrong grid2Op version + return + try: + assert not type(env.backend).detachment_is_allowed + assert not type(env).detachment_is_allowed + finally: + env.close() + + +def test_True_True(): + env = create_env(check_isolated_and_disconnected_injections=True, allow_detachment=True) + if env is None: + # wrong grid2Op version + return + try: + assert not type(env.backend).detachment_is_allowed + assert not type(env).detachment_is_allowed + finally: + env.close() From 45fcefc6b7c2ba45ca72a3e73fd870cdb33ddeae Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Thu, 13 Feb 2025 11:48:31 +0100 Subject: [PATCH 3/4] improving test coverage to test both env behaviour AND backend flag Signed-off-by: DONNOT Benjamin --- tests/test_detachment_support_info.py | 36 +++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/test_detachment_support_info.py b/tests/test_detachment_support_info.py index 97fd264..9514fb9 100644 --- a/tests/test_detachment_support_info.py +++ b/tests/test_detachment_support_info.py @@ -47,7 +47,11 @@ def test_None_False(): return try: assert not type(env.backend).detachment_is_allowed + assert env.backend._check_isolated_and_disconnected_injections assert not type(env).detachment_is_allowed + obs = env.reset() + obs, reward, done, info = env.step(env.action_space({'set_bus': {"loads_id": [(0, -1)]}})) + assert done finally: env.close() @@ -59,7 +63,11 @@ def test_None_default(): return try: assert not type(env.backend).detachment_is_allowed + assert env.backend._check_isolated_and_disconnected_injections assert not type(env).detachment_is_allowed + obs = env.reset() + obs, reward, done, info = env.step(env.action_space({'set_bus': {"loads_id": [(0, -1)]}})) + assert done finally: env.close() @@ -71,7 +79,11 @@ def test_None_True(): return try: assert type(env.backend).detachment_is_allowed + assert not env.backend._check_isolated_and_disconnected_injections assert type(env).detachment_is_allowed + obs = env.reset() + obs, reward, done, info = env.step(env.action_space({'set_bus': {"loads_id": [(0, -1)]}})) + assert not done finally: env.close() @@ -83,7 +95,11 @@ def test_False_False(): return try: assert not type(env.backend).detachment_is_allowed + assert not env.backend._check_isolated_and_disconnected_injections assert not type(env).detachment_is_allowed + obs = env.reset() + obs, reward, done, info = env.step(env.action_space({'set_bus': {"loads_id": [(0, -1)]}})) + assert done finally: env.close() @@ -95,7 +111,11 @@ def test_False_default(): return try: assert not type(env.backend).detachment_is_allowed + assert not env.backend._check_isolated_and_disconnected_injections assert not type(env).detachment_is_allowed + obs = env.reset() + obs, reward, done, info = env.step(env.action_space({'set_bus': {"loads_id": [(0, -1)]}})) + assert done finally: env.close() @@ -107,7 +127,11 @@ def test_False_True(): return try: assert type(env.backend).detachment_is_allowed + assert not env.backend._check_isolated_and_disconnected_injections assert type(env).detachment_is_allowed + obs = env.reset() + obs, reward, done, info = env.step(env.action_space({'set_bus': {"loads_id": [(0, -1)]}})) + assert not done finally: env.close() @@ -119,7 +143,11 @@ def test_True_False(): return try: assert not type(env.backend).detachment_is_allowed + assert env.backend._check_isolated_and_disconnected_injections assert not type(env).detachment_is_allowed + obs = env.reset() + obs, reward, done, info = env.step(env.action_space({'set_bus': {"loads_id": [(0, -1)]}})) + assert done finally: env.close() @@ -131,7 +159,11 @@ def test_True_default(): return try: assert not type(env.backend).detachment_is_allowed + assert env.backend._check_isolated_and_disconnected_injections assert not type(env).detachment_is_allowed + obs = env.reset() + obs, reward, done, info = env.step(env.action_space({'set_bus': {"loads_id": [(0, -1)]}})) + assert done finally: env.close() @@ -143,6 +175,10 @@ def test_True_True(): return try: assert not type(env.backend).detachment_is_allowed + assert env.backend._check_isolated_and_disconnected_injections assert not type(env).detachment_is_allowed + obs = env.reset() + obs, reward, done, info = env.step(env.action_space({'set_bus': {"loads_id": [(0, -1)]}})) + assert done finally: env.close() From 4efed8903480133c89b3a6271a79387363a869fa Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 14 Feb 2025 10:35:42 +0100 Subject: [PATCH 4/4] remove the second useless call to can_handle_more_than_2_busbars Signed-off-by: DONNOT Benjamin --- pypowsybl2grid/pypowsybl_backend.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pypowsybl2grid/pypowsybl_backend.py b/pypowsybl2grid/pypowsybl_backend.py index 1ea0d70..8e20b6a 100644 --- a/pypowsybl2grid/pypowsybl_backend.py +++ b/pypowsybl2grid/pypowsybl_backend.py @@ -170,8 +170,6 @@ def load_grid_from_iidm(self, network: pp.network.Network) -> None: self.name_sub = self._grid.get_string_value(pp.grid2op.StringValueType.VOLTAGE_LEVEL_NAME) self.n_sub = len(self.name_sub) - self.can_handle_more_than_2_busbar() - logger.info(f"{self.n_busbar_per_sub} busbars per substation") # loads