From 3ebd6903d4366562f6f5409dd5e1391920771a6d Mon Sep 17 00:00:00 2001 From: yassine abdou Date: Mon, 17 Feb 2025 16:49:08 +0100 Subject: [PATCH 1/8] feat : first part (use defaultProtection) add protection schemes and thermal limit --- grid2op/Backend/backend.py | 379 ++++++++++++------------ grid2op/Backend/protectionScheme.py | 132 +++++++++ grid2op/Backend/thermalLimits.py | 191 ++++++++++++ grid2op/Environment/_obsEnv.py | 12 +- grid2op/Environment/baseEnv.py | 100 +++---- grid2op/Environment/environment.py | 8 +- grid2op/Observation/observationSpace.py | 15 +- 7 files changed, 579 insertions(+), 258 deletions(-) create mode 100644 grid2op/Backend/protectionScheme.py create mode 100644 grid2op/Backend/thermalLimits.py diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index 24d789f6..ce2f3aab 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -186,6 +186,7 @@ def __init__(self, self._missing_detachment_support_info : bool = True self.detachment_is_allowed : bool = DEFAULT_ALLOW_DETACHMENT + def can_handle_more_than_2_busbar(self): """ .. versionadded:: 1.10.0 @@ -763,135 +764,135 @@ def get_line_flow(self) -> np.ndarray: p_or, q_or, v_or, a_or = self.lines_or_info() return a_or - def set_thermal_limit(self, limits : Union[np.ndarray, Dict["str", float]]) -> None: - """ - INTERNAL - - .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ - - You can set the thermal limit directly in the environment. - - This function is used as a convenience function to set the thermal limits :attr:`Backend.thermal_limit_a` - in amperes. - - It can be used at the beginning of an episode if the thermal limit are not present in the original data files - or alternatively if the thermal limits depends on the period of the year (one in winter and one in summer - for example). - - Parameters - ---------- - limits: ``object`` - It can be understood differently according to its type: - - - If it's a ``numpy.ndarray``, then it is assumed the thermal limits are given in amperes in the same order - as the powerlines computed in the backend. In that case it modifies all the thermal limits of all - the powerlines at once. - - If it's a ``dict`` it must have: - - - as key the powerline names (not all names are mandatory, in that case only the powerlines with the name - in this dictionnary will be modified) - - as value the new thermal limit (should be a strictly positive float). - - """ - if isinstance(limits, np.ndarray): - if limits.shape[0] == self.n_line: - self.thermal_limit_a = 1.0 * limits.astype(dt_float) - elif isinstance(limits, dict): - for el in limits.keys(): - if not el in self.name_line: - raise BackendError( - 'You asked to modify the thermal limit of powerline named "{}" that is not ' - "on the grid. Names of powerlines are {}".format( - el, self.name_line - ) - ) - for i, el in self.name_line: - if el in limits: - try: - tmp = dt_float(limits[el]) - except Exception as exc_: - raise BackendError( - 'Impossible to convert data ({}) for powerline named "{}" into float ' - "values".format(limits[el], el) - ) from exc_ - if tmp <= 0: - raise BackendError( - 'New thermal limit for powerlines "{}" is not positive ({})' - "".format(el, tmp) - ) - self.thermal_limit_a[i] = tmp - - def update_thermal_limit_from_vect(self, thermal_limit_a : np.ndarray) -> None: - """You can use it if your backend stores the thermal limits - of the grid in a vector (see :class:`PandaPowerBackend` for example) + # def set_thermal_limit(self, limits : Union[np.ndarray, Dict["str", float]]) -> None: + # """ + # INTERNAL + + # .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ + + # You can set the thermal limit directly in the environment. + + # This function is used as a convenience function to set the thermal limits :attr:`Backend.thermal_limit_a` + # in amperes. + + # It can be used at the beginning of an episode if the thermal limit are not present in the original data files + # or alternatively if the thermal limits depends on the period of the year (one in winter and one in summer + # for example). + + # Parameters + # ---------- + # limits: ``object`` + # It can be understood differently according to its type: + + # - If it's a ``numpy.ndarray``, then it is assumed the thermal limits are given in amperes in the same order + # as the powerlines computed in the backend. In that case it modifies all the thermal limits of all + # the powerlines at once. + # - If it's a ``dict`` it must have: + + # - as key the powerline names (not all names are mandatory, in that case only the powerlines with the name + # in this dictionnary will be modified) + # - as value the new thermal limit (should be a strictly positive float). + + # """ + # if isinstance(limits, np.ndarray): + # if limits.shape[0] == self.n_line: + # self.thermal_limit_a = 1.0 * limits.astype(dt_float) + # elif isinstance(limits, dict): + # for el in limits.keys(): + # if not el in self.name_line: + # raise BackendError( + # 'You asked to modify the thermal limit of powerline named "{}" that is not ' + # "on the grid. Names of powerlines are {}".format( + # el, self.name_line + # ) + # ) + # for i, el in self.name_line: + # if el in limits: + # try: + # tmp = dt_float(limits[el]) + # except Exception as exc_: + # raise BackendError( + # 'Impossible to convert data ({}) for powerline named "{}" into float ' + # "values".format(limits[el], el) + # ) from exc_ + # if tmp <= 0: + # raise BackendError( + # 'New thermal limit for powerlines "{}" is not positive ({})' + # "".format(el, tmp) + # ) + # self.thermal_limit_a[i] = tmp + + # def update_thermal_limit_from_vect(self, thermal_limit_a : np.ndarray) -> None: + # """You can use it if your backend stores the thermal limits + # of the grid in a vector (see :class:`PandaPowerBackend` for example) - .. warning:: - This is not called by the environment and cannot be used to - model Dynamic Line Rating. For such purpose please use `update_thermal_limit` + # .. warning:: + # This is not called by the environment and cannot be used to + # model Dynamic Line Rating. For such purpose please use `update_thermal_limit` - This function is used to create a "Simulator" from a backend for example. + # This function is used to create a "Simulator" from a backend for example. - Parameters - ---------- - vect : np.ndarray - The thermal limits (in A) - """ - thermal_limit_a = np.array(thermal_limit_a).astype(dt_float) - self.thermal_limit_a[:] = thermal_limit_a + # Parameters + # ---------- + # vect : np.ndarray + # The thermal limits (in A) + # """ + # thermal_limit_a = np.array(thermal_limit_a).astype(dt_float) + # self.thermal_limit_a[:] = thermal_limit_a - def update_thermal_limit(self, env : "grid2op.Environment.BaseEnv") -> None: - """ - INTERNAL + # def update_thermal_limit(self, env : "grid2op.Environment.BaseEnv") -> None: + # """ + # INTERNAL - .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ + # .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ - This is done in a call to `env.step` in case of DLR for example. + # This is done in a call to `env.step` in case of DLR for example. - If you don't want this feature, do not implement it. + # If you don't want this feature, do not implement it. - Update the new thermal limit in case of DLR for example. + # Update the new thermal limit in case of DLR for example. - By default it does nothing. + # By default it does nothing. - Depending on the operational strategy, it is also possible to implement some - `Dynamic Line Rating `_ (DLR) - strategies. - In this case, this function will give the thermal limit for a given time step provided the flows and the - weather condition are accessible by the backend. Our methodology doesn't make any assumption on the method - used to get these thermal limits. + # Depending on the operational strategy, it is also possible to implement some + # `Dynamic Line Rating `_ (DLR) + # strategies. + # In this case, this function will give the thermal limit for a given time step provided the flows and the + # weather condition are accessible by the backend. Our methodology doesn't make any assumption on the method + # used to get these thermal limits. - Parameters - ---------- - env: :class:`grid2op.Environment.Environment` - The environment used to compute the thermal limit + # Parameters + # ---------- + # env: :class:`grid2op.Environment.Environment` + # The environment used to compute the thermal limit - """ - pass + # """ + # pass - def get_thermal_limit(self) -> np.ndarray: - """ - INTERNAL + # def get_thermal_limit(self) -> np.ndarray: + # """ + # INTERNAL - .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ + # .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ - Retrieve the thermal limit directly from the environment instead (with a call - to :func:`grid2op.Environment.BaseEnc.get_thermal_limit` for example) + # Retrieve the thermal limit directly from the environment instead (with a call + # to :func:`grid2op.Environment.BaseEnc.get_thermal_limit` for example) - Gives the thermal limit (in amps) for each powerline of the _grid. Only one value per powerline is returned. + # Gives the thermal limit (in amps) for each powerline of the _grid. Only one value per powerline is returned. - It is assumed that both :func:`Backend.get_line_flow` and *_get_thermal_limit* gives the value of the same - end of the powerline. + # It is assumed that both :func:`Backend.get_line_flow` and *_get_thermal_limit* gives the value of the same + # end of the powerline. - See the help of *_get_line_flow* for a more detailed description of this problem. + # See the help of *_get_line_flow* for a more detailed description of this problem. - For assumption about the order of the powerline flows return in this vector, see the help of the - :func:`Backend.get_line_status` method. + # For assumption about the order of the powerline flows return in this vector, see the help of the + # :func:`Backend.get_line_status` method. - :return: An array giving the thermal limit of the powerlines. - :rtype: np.array, dtype:float - """ - return self.thermal_limit_a + # :return: An array giving the thermal limit of the powerlines. + # :rtype: np.array, dtype:float + # """ + # return self.thermal_limit_a def get_relative_flow(self) -> np.ndarray: """ @@ -914,7 +915,7 @@ def get_relative_flow(self) -> np.ndarray: The relative flow in each powerlines of the grid. """ num_ = self.get_line_flow() - denom_ = self.get_thermal_limit() + denom_ = self.thermal_limit_a res = np.divide(num_, denom_) return res @@ -942,7 +943,7 @@ def get_line_overflow(self) -> np.ndarray: :return: An array saying if a powerline is overflow or not :rtype: np.array, dtype:bool """ - th_lim = self.get_thermal_limit() + th_lim = self.thermal_limit_a flow = self.get_line_flow() return flow > th_lim @@ -1120,87 +1121,87 @@ def _runpf_with_diverging_exception(self, is_dc : bool) -> Optional[Exception]: ) return exc_me - def next_grid_state(self, - env: "grid2op.Environment.BaseEnv", - is_dc: Optional[bool]=False): - """ - INTERNAL - - .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ - - This is called by `env.step` - - This method is called by the environment to compute the next\\_grid\\_states. - It allows to compute the powerline and approximate the "cascading failures" if there are some overflows. - - Attributes - ---------- - env: :class:`grid2op.Environment.Environment` - the environment in which the powerflow is ran. - - is_dc: ``bool`` - mode of power flow (AC : False, DC: is_dc is True) - - Returns - -------- - disconnected_during_cf: ``numpy.ndarray``, dtype=bool - For each powerlines, it returns ``True`` if the powerline has been disconnected due to a cascading failure - or ``False`` otherwise. - - infos: ``list`` - If :attr:`Backend.detailed_infos_for_cascading_failures` is ``True`` then it returns the different - state computed by the powerflow (can drastically slow down this function, as it requires - deep copy of backend object). Otherwise the list is always empty. - - """ - infos = [] - disconnected_during_cf = np.full(self.n_line, fill_value=-1, dtype=dt_int) - conv_ = self._runpf_with_diverging_exception(is_dc) - if env._no_overflow_disconnection or conv_ is not None: - return disconnected_during_cf, infos, conv_ - - # the environment disconnect some powerlines - init_time_step_overflow = copy.deepcopy(env._timestep_overflow) - ts = 0 - while True: - # simulate the cascading failure - lines_flows = 1.0 * self.get_line_flow() - thermal_limits = self.get_thermal_limit() * env._parameters.SOFT_OVERFLOW_THRESHOLD # SOFT_OVERFLOW_THRESHOLD new in grid2op 1.9.3 - lines_status = self.get_line_status() - - # a) disconnect lines on hard overflow (that are still connected) - to_disc = ( - lines_flows > env._hard_overflow_threshold * thermal_limits - ) & lines_status - - # b) deals with soft overflow (disconnect them if lines still connected) - init_time_step_overflow[(lines_flows >= thermal_limits) & lines_status] += 1 - to_disc[ - (init_time_step_overflow > env._nb_timestep_overflow_allowed) - & lines_status - ] = True - - # disconnect the current power lines - 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 + # def next_grid_state(self, + # env: "grid2op.Environment.BaseEnv", + # is_dc: Optional[bool]=False): + # """ + # INTERNAL + + # .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ + + # This is called by `env.step` + + # This method is called by the environment to compute the next\\_grid\\_states. + # It allows to compute the powerline and approximate the "cascading failures" if there are some overflows. + + # Attributes + # ---------- + # env: :class:`grid2op.Environment.Environment` + # the environment in which the powerflow is ran. + + # is_dc: ``bool`` + # mode of power flow (AC : False, DC: is_dc is True) + + # Returns + # -------- + # disconnected_during_cf: ``numpy.ndarray``, dtype=bool + # For each powerlines, it returns ``True`` if the powerline has been disconnected due to a cascading failure + # or ``False`` otherwise. + + # infos: ``list`` + # If :attr:`Backend.detailed_infos_for_cascading_failures` is ``True`` then it returns the different + # state computed by the powerflow (can drastically slow down this function, as it requires + # deep copy of backend object). Otherwise the list is always empty. + + # """ + # infos = [] + # disconnected_during_cf = np.full(self.n_line, fill_value=-1, dtype=dt_int) + # conv_ = self._runpf_with_diverging_exception(is_dc) + # if env._no_overflow_disconnection or conv_ is not None: + # return disconnected_during_cf, infos, conv_ + + # # the environment disconnect some powerlines + # init_time_step_overflow = copy.deepcopy(env._timestep_overflow) + # ts = 0 + # while True: + # # simulate the cascading failure + # lines_flows = 1.0 * self.get_line_flow() + # thermal_limits = self.get_thermal_limit() * env._parameters.SOFT_OVERFLOW_THRESHOLD # SOFT_OVERFLOW_THRESHOLD new in grid2op 1.9.3 + # lines_status = self.get_line_status() + + # # a) disconnect lines on hard overflow (that are still connected) + # to_disc = ( + # lines_flows > env._hard_overflow_threshold * thermal_limits + # ) & lines_status + + # # b) deals with soft overflow (disconnect them if lines still connected) + # init_time_step_overflow[(lines_flows >= thermal_limits) & lines_status] += 1 + # to_disc[ + # (init_time_step_overflow > env._nb_timestep_overflow_allowed) + # & lines_status + # ] = True + + # # disconnect the current power lines + # 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: - self._disconnect_line(i) + # # perform the disconnection action + # for i, el in enumerate(to_disc): + # if el: + # self._disconnect_line(i) - # start a powerflow on this new state - conv_ = self._runpf_with_diverging_exception(is_dc) - if self.detailed_infos_for_cascading_failures: - infos.append(self.copy()) + # # start a powerflow on this new state + # conv_ = self._runpf_with_diverging_exception(is_dc) + # if self.detailed_infos_for_cascading_failures: + # infos.append(self.copy()) - if conv_ is not None: - break - ts += 1 - return disconnected_during_cf, infos, conv_ + # if conv_ is not None: + # break + # ts += 1 + # return disconnected_during_cf, infos, conv_ def storages_info(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """ @@ -2131,7 +2132,7 @@ def assert_grid_correct_after_powerflow(self) -> None: raise IncorrectNumberOfLines('returned by "backend.get_line_flow()"') if (~np.isfinite(tmp)).any(): raise EnvError(type(self).ERR_INIT_POWERFLOW) - tmp = self.get_thermal_limit() + tmp = self.thermal_limit_a if tmp.shape[0] != self.n_line: raise IncorrectNumberOfLines('returned by "backend.get_thermal_limit()"') if (~np.isfinite(tmp)).any(): diff --git a/grid2op/Backend/protectionScheme.py b/grid2op/Backend/protectionScheme.py new file mode 100644 index 00000000..9ecacf07 --- /dev/null +++ b/grid2op/Backend/protectionScheme.py @@ -0,0 +1,132 @@ +import copy +from loguru import logger +from typing import Tuple, Union, Any, List, Optional +import numpy as np + +from grid2op.dtypes import dt_int +from grid2op.Backend.backend import Backend +from grid2op.Parameters import Parameters +from grid2op.Exceptions import Grid2OpException +from grid2op.Backend.thermalLimits import ThermalLimits + +class DefaultProtection: + """ + Classe avancée pour gérer les protections réseau et les déconnexions. + """ + + def __init__( + self, + backend: Backend, + parameters: Parameters, + thermal_limits: ThermalLimits, + is_dc: bool = False, + ): + """ + Initialise l'état du réseau avec des protections personnalisables. + """ + self.backend = backend + self._parameters = copy.deepcopy(parameters) if parameters else Parameters() + self._validate_input(self.backend, self._parameters) + + self.is_dc = is_dc + self.thermal_limits = thermal_limits + + self._thermal_limit_a = self.thermal_limits.limits if self.thermal_limits else None + + self._hard_overflow_threshold = self._get_value_from_parameters("HARD_OVERFLOW_THRESHOLD") + self._soft_overflow_threshold = self._get_value_from_parameters("SOFT_OVERFLOW_THRESHOLD") + self._nb_timestep_overflow_allowed = self._get_value_from_parameters("NB_TIMESTEP_OVERFLOW_ALLOWED") + self._no_overflow_disconnection = self._get_value_from_parameters("NO_OVERFLOW_DISCONNECTION") + + self.disconnected_during_cf = np.full(self.thermal_limits.n_line, fill_value=-1, dtype=dt_int) + self._timestep_overflow = np.zeros(self.thermal_limits.n_line, dtype=dt_int) + self.infos: List[str] = [] + + def _validate_input(self, backend: Backend, parameters: Optional[Parameters]) -> None: + if not isinstance(backend, Backend): + raise Grid2OpException(f"Argument 'backend' doit être de type 'Backend', reçu : {type(backend)}") + if parameters and not isinstance(parameters, Parameters): + raise Grid2OpException(f"Argument 'parameters' doit être de type 'Parameters', reçu : {type(parameters)}") + + def _get_value_from_parameters(self, parameter_name: str) -> Any: + return getattr(self._parameters, parameter_name, None) + + def _run_power_flow(self) -> Optional[Exception]: + try: + return self.backend._runpf_with_diverging_exception(self.is_dc) + except Exception as e: + logger.error(f"Erreur flux de puissance : {e}") + return e + + def _update_overflows(self, lines_flows: np.ndarray) -> np.ndarray: + if self._thermal_limit_a is None: + logger.error("Thermal limits must be provided for overflow calculations.") + raise ValueError("Thermal limits must be provided for overflow calculations.") + + lines_status = self.backend.get_line_status() # self._thermal_limit_a reste fixe. self._soft_overflow_threshold = 1 + is_overflowing = (lines_flows >= self._thermal_limit_a * self._soft_overflow_threshold) & lines_status + self._timestep_overflow[is_overflowing] += 1 + # self._hard_overflow_threshold = 1.5 + exceeds_hard_limit = (lines_flows > self._thermal_limit_a * self._hard_overflow_threshold) & lines_status + exceeds_allowed_time = self._timestep_overflow > self._nb_timestep_overflow_allowed + + lines_to_disconnect = exceeds_hard_limit | (exceeds_allowed_time & lines_status) + return lines_to_disconnect + + def _disconnect_lines(self, lines_to_disconnect: np.ndarray, timestep: int) -> None: + for line_idx in np.where(lines_to_disconnect)[0]: + self.backend._disconnect_line(line_idx) + self.disconnected_during_cf[line_idx] = timestep + logger.warning(f"Ligne {line_idx} déconnectée au pas de temps {timestep}.") + + def next_grid_state(self) -> Tuple[np.ndarray, List[Any], Union[None, Exception]]: + try: + if self._no_overflow_disconnection: # détaché cela d'ici et si on simule pas + return self._handle_no_protection() + + timestep = 0 + while True: + power_flow_result = self._run_power_flow() + if power_flow_result: + return self.disconnected_during_cf, self.infos, power_flow_result + + lines_flows = self.backend.get_line_flow() + lines_to_disconnect = self._update_overflows(lines_flows) + + if not lines_to_disconnect.any(): + break + + self._disconnect_lines(lines_to_disconnect, timestep) + timestep += 1 + + return self.disconnected_during_cf, self.infos, None + + except Exception as e: + logger.exception("Erreur inattendue dans le calcul de l'état du réseau.") + return self.disconnected_during_cf, self.infos, e + + def _handle_no_protection(self) -> Tuple[np.ndarray, List[Any], None]: + no_protection = NoProtection(self.thermal_limits) + return no_protection.handle_no_protection() + +class NoProtection: + """ + Classe qui gère le cas où les protections de débordement sont désactivées. + """ + def __init__( + self, + thermal_limits: ThermalLimits + ): + + self.thermal_limits = thermal_limits + self.disconnected_during_cf = np.full(self.thermal_limits.n_line, fill_value=-1, dtype=dt_int) + self.infos = [] + + def handle_no_protection(self) -> Tuple[np.ndarray, List[Any], None]: + """ + Retourne l'état du réseau sans effectuer de déconnexions dues aux débordements. + """ + return self.disconnected_during_cf, self.infos, None + +class BlablaProtection: + pass \ No newline at end of file diff --git a/grid2op/Backend/thermalLimits.py b/grid2op/Backend/thermalLimits.py new file mode 100644 index 00000000..1a2c3436 --- /dev/null +++ b/grid2op/Backend/thermalLimits.py @@ -0,0 +1,191 @@ +from loguru import logger +from typing import Union, List, Optional, Dict +import numpy as np + +import grid2op +from grid2op.dtypes import dt_float +from grid2op.Exceptions import Grid2OpException + +class ThermalLimits: + """ + Class for managing the thermal limits of power grid lines. + """ + + def __init__( + self, + _thermal_limit_a: Optional[np.ndarray] = None, + line_names: Optional[List[str]] = None, + n_line: Optional[int] = None + ): + """ + Initializes the thermal limits manager. + + :param thermal_limits: Optional[np.ndarray] + Array of thermal limits for each power line. Must have the same length as the number of lines. + :param line_names: Optional[List[str]] + List of power line names. + :param n_line: Optional[int] + Number of lines (can be passed explicitly or inferred from `thermal_limits` or `line_names`). + + :raises ValueError: + If neither `thermal_limits` nor `n_line` and `line_names` are provided. + """ + if _thermal_limit_a is None and (n_line is None and line_names is None): + raise ValueError("Must provide thermal_limits or both n_line and line_names.") + + self._thermal_limit_a = _thermal_limit_a + self._n_line = n_line + self._name_line = line_names + + logger.info(f"ThermalLimits initialized with {self.n_line} limits.") + + @property + def n_line(self) -> int: + return self._n_line + + @n_line.setter + def n_line(self, new_n_line: int) -> None: + if new_n_line <= 0: + raise ValueError("Number of lines must be a positive integer.") + self._n_line = new_n_line + logger.info(f"Number of lines updated to {self._n_line}.") + + @property + def name_line(self) -> Union[List[str], np.ndarray]: + return self._name_line + + @name_line.setter + def name_line(self, new_name_line: Union[List[str], np.ndarray]) -> None: + if isinstance(new_name_line, np.ndarray): + if not np.all([isinstance(name, str) for name in new_name_line]): + raise ValueError("All elements in name_line must be strings.") + elif isinstance(new_name_line, list): + if not all(isinstance(name, str) for name in new_name_line): + raise ValueError("All elements in name_line must be strings.") + else: + raise ValueError("Line names must be a list or numpy array of non-empty strings.") + + if self._n_line is not None and len(new_name_line) != self._n_line: + raise ValueError("Length of name list must match the number of lines.") + + self._name_line = new_name_line + logger.info(f"Power line names updated") + + @property + def limits(self) -> np.ndarray: + """ + Gets the current thermal limits of the power lines. + + :return: np.ndarray + The array containing thermal limits for each power line. + """ + return self._thermal_limit_a + + @limits.setter + def limits(self, new_limits: Union[np.ndarray, Dict[str, float]]): + """ + Sets new thermal limits. + + :param new_limits: Union[np.ndarray, Dict[str, float]] + Either a numpy array or a dictionary mapping line names to new thermal limits. + + :raises ValueError: + If the new limits array size does not match the number of lines. + :raises Grid2OpException: + If invalid power line names are provided in the dictionary. + If the new thermal limit values are invalid (non-positive or non-convertible). + :raises TypeError: + If the input type is not an array or dictionary. + """ + if isinstance(new_limits, np.ndarray): + if new_limits.shape[0] == self.n_line: + self._thermal_limit_a = 1.0 * new_limits.astype(dt_float) + elif isinstance(new_limits, dict): + for el in new_limits.keys(): + if not el in self.name_line: + raise Grid2OpException( + 'You asked to modify the thermal limit of powerline named "{}" that is not ' + "on the grid. Names of powerlines are {}".format( + el, self.name_line + ) + ) + for i, el in self.name_line: + if el in new_limits: + try: + tmp = dt_float(new_limits[el]) + except Exception as exc_: + raise Grid2OpException( + 'Impossible to convert data ({}) for powerline named "{}" into float ' + "values".format(new_limits[el], el) + ) from exc_ + if tmp <= 0: + raise Grid2OpException( + 'New thermal limit for powerlines "{}" is not positive ({})' + "".format(el, tmp) + ) + self._thermal_limit_a[i] = tmp + + def env_limits(self, thermal_limit): + if isinstance(thermal_limit, dict): + tmp = np.full(self.n_line, fill_value=np.NaN, dtype=dt_float) + for key, val in thermal_limit.items(): + if key not in self.name_line: + raise Grid2OpException( + f"When setting a thermal limit with a dictionary, the keys should be line " + f"names. We found: {key} which is not a line name. The names of the " + f"powerlines are {self.name_line}" + ) + ind_line = (self.name_line == key).nonzero()[0][0] + if np.isfinite(tmp[ind_line]): + raise Grid2OpException( + f"Humm, there is a really strange bug, some lines are set twice." + ) + try: + val_fl = float(val) + except Exception as exc_: + raise Grid2OpException( + f"When setting thermal limit with a dictionary, the keys should be " + f"the values of the thermal limit (in amps) you provided something that " + f'cannot be converted to a float. Error was "{exc_}".' + ) + tmp[ind_line] = val_fl + + elif isinstance(thermal_limit, (np.ndarray, list)): + try: + tmp = np.array(thermal_limit).flatten().astype(dt_float) + except Exception as exc_: + raise Grid2OpException( + f"Impossible to convert the vector as input into a 1d numpy float array. " + f"Error was: \n {exc_}" + ) + if tmp.shape[0] != self.n_line: + raise Grid2OpException( + "Attempt to set thermal limit on {} powerlines while there are {}" + "on the grid".format(tmp.shape[0], self.n_line) + ) + if (~np.isfinite(tmp)).any(): + raise Grid2OpException( + "Impossible to use non finite value for thermal limits." + ) + else: + raise Grid2OpException( + f"You can only set the thermal limits of the environment with a dictionary (in that " + f"case the keys are the line names, and the values the thermal limits) or with " + f"a numpy array that has as many components of the number of powerlines on " + f'the grid. You provided something with type "{type(thermal_limit)}" which ' + f"is not supported." + ) + + self._thermal_limit_a = tmp + logger.info("Env thermal limits successfully set.") + + def update_limits(self, thermal_limit_a: np.ndarray) -> None: + """ + Updates the thermal limits using a numpy array. + + :param thermal_limit_a: np.ndarray + The new array of thermal limits (in Amperes). + """ + thermal_limit_a = np.array(thermal_limit_a).astype(dt_float) + self._thermal_limit_a = thermal_limit_a + logger.info("Thermal limits updated from vector.") \ No newline at end of file diff --git a/grid2op/Environment/_obsEnv.py b/grid2op/Environment/_obsEnv.py index c8b8ac79..d4da53df 100644 --- a/grid2op/Environment/_obsEnv.py +++ b/grid2op/Environment/_obsEnv.py @@ -115,6 +115,9 @@ def __init__( self.current_obs_init = None self.current_obs = None + + self._init_thermal_limit() + self._init_backend( chronics_handler=_ObsCH(), backend=backend_instanciated, @@ -125,6 +128,8 @@ def __init__( legalActClass=legalActClass, ) + self.ts_manager = copy.deepcopy(self._observation_space.ts_manager) + self.delta_time_seconds = delta_time_seconds #### # to be able to save and import (using env.generate_classes) correctly @@ -193,9 +198,9 @@ def _init_backend( self._game_rules.initialize(self) self._legalActClass = legalActClass - # self._action_space = self._do_nothing - self.backend.set_thermal_limit(self._thermal_limit_a) - + # # self._action_space = self._do_nothing + self.ts_manager.limits = self._thermal_limit_a # old code line : self.backend.set_thermal_limit(self._thermal_limit_a) + from grid2op.Observation import ObservationSpace from grid2op.Reward import FlatReward ob_sp_cls = ObservationSpace.init_grid(type(backend), _local_dir_cls=self._local_dir_cls) @@ -207,7 +212,6 @@ def _init_backend( _local_dir_cls=self._local_dir_cls ) self._observationClass = self._observation_space.subtype # not used anyway - # create the opponent self._create_opponent() diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index e3c32323..e2204ac0 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -26,7 +26,7 @@ from grid2op.Observation import (BaseObservation, ObservationSpace, HighResSimCounter) -from grid2op.Backend import Backend +from grid2op.Backend import Backend, thermalLimits, protectionScheme from grid2op.dtypes import dt_int, dt_float, dt_bool from grid2op.Space import (GridObjects, RandomObject, @@ -569,6 +569,10 @@ def __init__( self.__is_init = False self.debug_dispatch = False + # Thermal limit and protection + self.ts_manager : thermalLimits.ThermalLimits = None + self.protection = protectionScheme.NoProtection = None + # to change the parameters self.__new_param = None self.__new_forecast_param = None @@ -667,6 +671,33 @@ def __init__( # slack (1.11.0) self._delta_gen_p = None + + def _init_thermal_limit(self): + + if self._thermal_limit_a is None: + if hasattr(self.ts_manager, '_thermal_limit_a') and isinstance(self.ts_manager.limits, np.ndarray): + _thermal_limits = self.ts_manager.limits.astype(dt_float) + else: + raise ValueError("Thermal limits not provided and 'self.ts_manager.limits' is unavailable or invalid.") + else: + _thermal_limits = self._thermal_limit_a + + # Update the thermal limits manager for protection scheme + self.ts_manager = thermalLimits.ThermalLimits( + _thermal_limit_a = _thermal_limits, + n_line=self.n_line, + line_names=self.name_line + ) + + def _init_protection(self): + # Initialize the protection system with the specified parameters + self.protection = protectionScheme.DefaultProtection( + backend=self.backend, + parameters=self.parameters, + thermal_limits=self.ts_manager, + is_dc=self._env_dc + ) + @property def highres_sim_counter(self): return self._highres_sim_counter @@ -739,9 +770,9 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None): new_obj._backend_action = copy.deepcopy(self._backend_action) # specific to Basic Env, do not change - new_obj.backend = self.backend.copy() + new_obj.ts_manager = self.ts_manager.copy() if self._thermal_limit_a is not None: - new_obj.backend.set_thermal_limit(self._thermal_limit_a) + new_obj.ts_manager.limits = self._thermal_limit_a new_obj._thermal_limit_a = copy.deepcopy(self._thermal_limit_a) new_obj.__is_init = self.__is_init @@ -1889,57 +1920,9 @@ def set_thermal_limit(self, thermal_limit): "Impossible to set the thermal limit to a non initialized Environment. " "Have you called `env.reset()` after last game over ?" ) - if isinstance(thermal_limit, dict): - tmp = np.full(self.n_line, fill_value=np.NaN, dtype=dt_float) - for key, val in thermal_limit.items(): - if key not in self.name_line: - raise Grid2OpException( - f"When setting a thermal limit with a dictionary, the keys should be line " - f"names. We found: {key} which is not a line name. The names of the " - f"powerlines are {self.name_line}" - ) - ind_line = (self.name_line == key).nonzero()[0][0] - if np.isfinite(tmp[ind_line]): - raise Grid2OpException( - f"Humm, there is a really strange bug, some lines are set twice." - ) - try: - val_fl = float(val) - except Exception as exc_: - raise Grid2OpException( - f"When setting thermal limit with a dictionary, the keys should be " - f"the names of the lines and the values the thermal limit (in amps) " - f"you provided something that " - f'cannot be converted to a float {type(val)}' - ) from exc_ - tmp[ind_line] = val_fl - - elif isinstance(thermal_limit, (np.ndarray, list)): - try: - tmp = np.array(thermal_limit).flatten().astype(dt_float) - except Exception as exc_: - raise Grid2OpException( - f"Impossible to convert the vector as input into a 1d numpy float array. " - ) from exc_ - if tmp.shape[0] != self.n_line: - raise Grid2OpException( - "Attempt to set thermal limit on {} powerlines while there are {}" - "on the grid".format(tmp.shape[0], self.n_line) - ) - if (~np.isfinite(tmp)).any(): - raise Grid2OpException( - "Impossible to use non finite value for thermal limits." - ) - else: - raise Grid2OpException( - f"You can only set the thermal limits of the environment with a dictionary (in that " - f"case the keys are the line names, and the values the thermal limits) or with " - f"a numpy array that has as many components of the number of powerlines on " - f'the grid. You provided something with type "{type(thermal_limit)}" which ' - f"is not supported." - ) - self._thermal_limit_a[:] = tmp - self.backend.set_thermal_limit(self._thermal_limit_a) + # update n_line and name_line of ts_manager (old : self.ts_manager.n_line = -1 and self.ts_manager.name_line = None) + self.ts_manager.env_limits(thermal_limit=thermal_limit) + self.ts_manager.limits = self._thermal_limit_a self.observation_space.set_thermal_limit(self._thermal_limit_a) def _reset_redispatching(self): @@ -3172,7 +3155,8 @@ def _update_alert_properties(self, action, lines_attacked, subs_attacked): def _aux_register_env_converged(self, disc_lines, action, init_line_status, new_p) -> Optional[Grid2OpException]: beg_res = time.perf_counter() # update the thermal limit, for DLR for example - self.backend.update_thermal_limit(self) + self.ts_manager.update_limits(thermal_limit_a=self._thermal_limit_a) # old code : self.backend.update_thermal_limit(self) + overflow_lines = self.backend.get_line_overflow() # save the current topology as "last" topology (for connected powerlines) # and update the state of the disconnected powerline due to cascading failure @@ -3270,7 +3254,9 @@ def _aux_register_env_converged(self, disc_lines, action, init_line_status, new_ def _backend_next_grid_state(self): """overlaoded in MaskedEnv""" - return self.backend.next_grid_state(env=self, is_dc=self._env_dc) + self._init_thermal_limit() + self._init_protection() + return self.protection.next_grid_state() def _aux_run_pf_after_state_properly_set( self, action, init_line_status, new_p, except_ @@ -4529,7 +4515,7 @@ def _reset_to_orig_state(self, obs): update the value of the "time dependant" attributes, used mainly for the "_ObsEnv" (simulate) or the "Forecasted env" (obs.get_forecast_env()) """ - self.backend.set_thermal_limit(obs._thermal_limit) + self.ts_manager.limits = obs._thermal_limit if "opp_space_state" in obs._env_internal_params: self._oppSpace._set_state(obs._env_internal_params["opp_space_state"], obs._env_internal_params["opp_state"]) diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 6f13d926..31243c43 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -26,7 +26,7 @@ from grid2op.Observation import CompleteObservation, ObservationSpace, BaseObservation from grid2op.Reward import FlatReward, RewardHelper, BaseReward from grid2op.Rules import RulesChecker, AlwaysLegal, BaseRules -from grid2op.Backend import Backend +from grid2op.Backend import Backend, protectionScheme, thermalLimits from grid2op.Chronics import ChronicsHandler from grid2op.VoltageControler import ControlVoltageFromFile, BaseVoltageController from grid2op.Environment.baseEnv import BaseEnv @@ -199,10 +199,10 @@ def __init__( self.spec = None self._compat_glop_version = _compat_glop_version - # needs to be done before "_init_backend" otherwise observationClass is not defined in the # observation space (real_env_kwargs) self._observationClass_orig = observationClass + # for plotting self._init_backend( chronics_handler, @@ -213,6 +213,8 @@ def __init__( rewardClass, legalActClass, ) + self.ts_manager : thermalLimits.ThermalLimits = copy.deepcopy(self.ts_manager) + self.protection : protectionScheme.DefaultProtection = copy.deepcopy(self.protection) def _init_backend( self, @@ -418,6 +420,7 @@ def _init_backend( # this needs to be done after the chronics handler: rewards might need information # about the chronics to work properly. self._helper_observation_class = ObservationSpace.init_grid(gridobj=bk_type, _local_dir_cls=self._local_dir_cls) + # FYI: this try to copy the backend if it fails it will modify the backend # and the environment to force the deactivation of the # forecasts @@ -442,7 +445,6 @@ def _init_backend( self._reward_helper.initialize(self) for k, v in self.other_rewards.items(): v.initialize(self) - # controller for voltage if not issubclass(self._voltagecontrolerClass, BaseVoltageController): raise Grid2OpException( diff --git a/grid2op/Observation/observationSpace.py b/grid2op/Observation/observationSpace.py index 1bc0290a..b6a6e005 100644 --- a/grid2op/Observation/observationSpace.py +++ b/grid2op/Observation/observationSpace.py @@ -10,6 +10,8 @@ import copy import logging import os +from grid2op.Backend import thermalLimits + from grid2op.Exceptions.envExceptions import EnvError from grid2op.Observation.serializableObservationSpace import ( @@ -133,7 +135,10 @@ def __init__( self._real_env_kwargs = {} self._observation_bk_class = observation_bk_class self._observation_bk_kwargs = observation_bk_kwargs - + + # Initialisation de la class thermal limits + self.ts_manager : thermalLimits.ThermalLimits = copy.deepcopy(env.ts_manager) + def set_real_env_kwargs(self, env): if not self.with_forecast: return @@ -217,8 +222,8 @@ def _aux_create_backend(self, env, observation_bk_class, observation_bk_kwargs, self._backend_obs.assert_grid_correct() self._backend_obs.runpf() self._backend_obs.assert_grid_correct_after_powerflow() - self._backend_obs.set_thermal_limit(env.get_thermal_limit()) - + self.ts_manager.limits = env.get_thermal_limit() + def _create_backend_obs(self, env, observation_bk_class, observation_bk_kwargs, _local_dir_cls): _with_obs_env = True path_sim_bk = os.path.join(env.get_path_env(), "grid_forecast.json") @@ -397,8 +402,8 @@ def change_reward(self, reward_func): def set_thermal_limit(self, thermal_limit_a): if self.obs_env is not None: self.obs_env.set_thermal_limit(thermal_limit_a) - if self._backend_obs is not None: - self._backend_obs.set_thermal_limit(thermal_limit_a) + # if self._backend_obs is not None: + # self.ts_manager.limits = thermal_limit_a def reset_space(self): if self.with_forecast: From a9d6ff06ced496ff16c7d1b0f4f6cc9266d11a3e Mon Sep 17 00:00:00 2001 From: yassine abdou Date: Wed, 19 Feb 2025 16:24:27 +0100 Subject: [PATCH 2/8] feat : use defaultprotection in place of next_grid function --- grid2op/Backend/thermalLimits.py | 26 ++++- grid2op/Environment/_obsEnv.py | 2 - grid2op/Environment/baseEnv.py | 136 +++++++++++------------- grid2op/Environment/environment.py | 29 +++-- grid2op/Episode/EpisodeReboot.py | 4 +- grid2op/MakeEnv/MakeFromPath.py | 5 +- grid2op/Observation/baseObservation.py | 2 +- grid2op/Observation/observationSpace.py | 12 +-- grid2op/Runner/runner.py | 5 +- 9 files changed, 121 insertions(+), 100 deletions(-) diff --git a/grid2op/Backend/thermalLimits.py b/grid2op/Backend/thermalLimits.py index 1a2c3436..f7ac6d4f 100644 --- a/grid2op/Backend/thermalLimits.py +++ b/grid2op/Backend/thermalLimits.py @@ -1,6 +1,12 @@ -from loguru import logger from typing import Union, List, Optional, Dict +try: + from typing import Self +except ImportError: + from typing_extensions import Self + +from loguru import logger import numpy as np +import copy import grid2op from grid2op.dtypes import dt_float @@ -126,6 +132,8 @@ def limits(self, new_limits: Union[np.ndarray, Dict[str, float]]): self._thermal_limit_a[i] = tmp def env_limits(self, thermal_limit): + """ + """ if isinstance(thermal_limit, dict): tmp = np.full(self.n_line, fill_value=np.NaN, dtype=dt_float) for key, val in thermal_limit.items(): @@ -179,7 +187,7 @@ def env_limits(self, thermal_limit): self._thermal_limit_a = tmp logger.info("Env thermal limits successfully set.") - def update_limits(self, thermal_limit_a: np.ndarray) -> None: + def update_limits_from_vector(self, thermal_limit_a: np.ndarray) -> None: """ Updates the thermal limits using a numpy array. @@ -188,4 +196,16 @@ def update_limits(self, thermal_limit_a: np.ndarray) -> None: """ thermal_limit_a = np.array(thermal_limit_a).astype(dt_float) self._thermal_limit_a = thermal_limit_a - logger.info("Thermal limits updated from vector.") \ No newline at end of file + logger.info("Thermal limits updated from vector.") + + def update_limits(self, env : "grid2op.Environment.BaseEnv") -> None: + pass + + def copy(self) -> Self: + """ + Creates a deep copy of the current ThermalLimits instance. + + :return: ThermalLimits + A new instance with the same attributes. + """ + return copy.deepcopy(self) \ No newline at end of file diff --git a/grid2op/Environment/_obsEnv.py b/grid2op/Environment/_obsEnv.py index d4da53df..4a174f74 100644 --- a/grid2op/Environment/_obsEnv.py +++ b/grid2op/Environment/_obsEnv.py @@ -116,7 +116,6 @@ def __init__( self.current_obs_init = None self.current_obs = None - self._init_thermal_limit() self._init_backend( chronics_handler=_ObsCH(), @@ -128,7 +127,6 @@ def __init__( legalActClass=legalActClass, ) - self.ts_manager = copy.deepcopy(self._observation_space.ts_manager) self.delta_time_seconds = delta_time_seconds #### diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index e2204ac0..f8b84471 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -570,8 +570,8 @@ def __init__( self.debug_dispatch = False # Thermal limit and protection - self.ts_manager : thermalLimits.ThermalLimits = None - self.protection = protectionScheme.NoProtection = None + self._init_thermal_limit() + self.protection : protectionScheme.DefaultProtection = None # to change the parameters self.__new_param = None @@ -673,18 +673,8 @@ def __init__( def _init_thermal_limit(self): - - if self._thermal_limit_a is None: - if hasattr(self.ts_manager, '_thermal_limit_a') and isinstance(self.ts_manager.limits, np.ndarray): - _thermal_limits = self.ts_manager.limits.astype(dt_float) - else: - raise ValueError("Thermal limits not provided and 'self.ts_manager.limits' is unavailable or invalid.") - else: - _thermal_limits = self._thermal_limit_a - - # Update the thermal limits manager for protection scheme self.ts_manager = thermalLimits.ThermalLimits( - _thermal_limit_a = _thermal_limits, + _thermal_limit_a = self._thermal_limit_a, n_line=self.n_line, line_names=self.name_line ) @@ -770,6 +760,7 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None): new_obj._backend_action = copy.deepcopy(self._backend_action) # specific to Basic Env, do not change + new_obj.backend = self.backend.copy() new_obj.ts_manager = self.ts_manager.copy() if self._thermal_limit_a is not None: new_obj.ts_manager.limits = self._thermal_limit_a @@ -1878,52 +1869,51 @@ def _init_backend( """ pass - def set_thermal_limit(self, thermal_limit): - """ - Set the thermal limit effectively. + # def set_thermal_limit(self, thermal_limit): + # """ + # Set the thermal limit effectively. - Parameters - ---------- - thermal_limit: ``numpy.ndarray`` - The new thermal limit. It must be a numpy ndarray vector (or convertible to it). For each powerline it - gives the new thermal limit. + # Parameters + # ---------- + # thermal_limit: ``numpy.ndarray`` + # The new thermal limit. It must be a numpy ndarray vector (or convertible to it). For each powerline it + # gives the new thermal limit. - Alternatively, this can be a dictionary mapping the line names (keys) to its thermal limits (values). In - that case, all thermal limits for all powerlines should be specified (this is a safety measure - to reduce the odds of misuse). + # Alternatively, this can be a dictionary mapping the line names (keys) to its thermal limits (values). In + # that case, all thermal limits for all powerlines should be specified (this is a safety measure + # to reduce the odds of misuse). - Examples - --------- + # Examples + # --------- - This function can be used like this: + # This function can be used like this: - .. code-block:: python + # .. code-block:: python - import grid2op + # import grid2op - # I create an environment - env = grid2op.make(""l2rpn_case14_sandbox"", test=True) + # # I create an environment + # env = grid2op.make(""l2rpn_case14_sandbox"", test=True) - # i set the thermal limit of each powerline to 20000 amps - env.set_thermal_limit([20000 for _ in range(env.n_line)]) + # # i set the thermal limit of each powerline to 20000 amps + # env.set_thermal_limit([20000 for _ in range(env.n_line)]) - Notes - ----- - As of grid2op > 1.5.0, it is possible to set the thermal limit by using a dictionary with the keys being - the name of the powerline and the values the thermal limits. + # Notes + # ----- + # As of grid2op > 1.5.0, it is possible to set the thermal limit by using a dictionary with the keys being + # the name of the powerline and the values the thermal limits. - """ - if self.__closed: - raise EnvError("This environment is closed, you cannot use it.") - if not self.__is_init: - raise Grid2OpException( - "Impossible to set the thermal limit to a non initialized Environment. " - "Have you called `env.reset()` after last game over ?" - ) - # update n_line and name_line of ts_manager (old : self.ts_manager.n_line = -1 and self.ts_manager.name_line = None) - self.ts_manager.env_limits(thermal_limit=thermal_limit) - self.ts_manager.limits = self._thermal_limit_a - self.observation_space.set_thermal_limit(self._thermal_limit_a) + # """ + # if self.__closed: + # raise EnvError("This environment is closed, you cannot use it.") + # if not self.__is_init: + # raise Grid2OpException( + # "Impossible to set the thermal limit to a non initialized Environment. " + # "Have you called `env.reset()` after last game over ?" + # ) + # # update n_line and name_line of ts_manager (old : self.ts_manager.n_line = -1 and self.ts_manager.name_line = None) + # self.ts_manager.env_limits(thermal_limit=thermal_limit) + # self.observation_space.set_thermal_limit(thermal_limit=thermal_limit) def _reset_redispatching(self): # redispatching @@ -2588,33 +2578,33 @@ def get_obs(self, _update_state=True, _do_copy=True): else: return self._last_obs - def get_thermal_limit(self): - """ - Get the current thermal limit in amps registered for the environment. + # def get_thermal_limit(self): + # """ + # Get the current thermal limit in amps registered for the environment. - Examples - --------- + # Examples + # --------- - It can be used like this: + # It can be used like this: - .. code-block:: python + # .. code-block:: python - import grid2op + # import grid2op - # I create an environment - env = grid2op.make("l2rpn_case14_sandbox") + # # I create an environment + # env = grid2op.make("l2rpn_case14_sandbox") - thermal_limits = env.get_thermal_limit() + # thermal_limits = env.get_thermal_limit() - """ - if self.__closed: - raise EnvError("This environment is closed, you cannot use it.") - if not self.__is_init: - raise EnvError( - "This environment is not initialized. It has no thermal limits. " - "Have you called `env.reset()` after last game over ?" - ) - return 1.0 * self._thermal_limit_a + # """ + # if self.__closed: + # raise EnvError("This environment is closed, you cannot use it.") + # if not self.__is_init: + # raise EnvError( + # "This environment is not initialized. It has no thermal limits. " + # "Have you called `env.reset()` after last game over ?" + # ) + # return 1.0 * self._thermal_limit_a def _withdraw_storage_losses(self): """ @@ -3155,7 +3145,7 @@ def _update_alert_properties(self, action, lines_attacked, subs_attacked): def _aux_register_env_converged(self, disc_lines, action, init_line_status, new_p) -> Optional[Grid2OpException]: beg_res = time.perf_counter() # update the thermal limit, for DLR for example - self.ts_manager.update_limits(thermal_limit_a=self._thermal_limit_a) # old code : self.backend.update_thermal_limit(self) + self.ts_manager.update_limits(self) # old code : self.backend.update_thermal_limit(self) overflow_lines = self.backend.get_line_overflow() # save the current topology as "last" topology (for connected powerlines) @@ -3252,11 +3242,10 @@ def _aux_register_env_converged(self, disc_lines, action, init_line_status, new_ self._time_extract_obs += time.perf_counter() - beg_res return None - def _backend_next_grid_state(self): + def _protection_next_grid_state(self): """overlaoded in MaskedEnv""" self._init_thermal_limit() self._init_protection() - return self.protection.next_grid_state() def _aux_run_pf_after_state_properly_set( self, action, init_line_status, new_p, except_ @@ -3286,7 +3275,8 @@ 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() + self._protection_next_grid_state() + disc_lines, detailed_info, conv_ = self.protection.next_grid_state() self._disc_lines[:] = disc_lines self._time_powerflow += time.perf_counter() - beg_pf if conv_ is None: diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 31243c43..cd46c49a 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -26,7 +26,7 @@ from grid2op.Observation import CompleteObservation, ObservationSpace, BaseObservation from grid2op.Reward import FlatReward, RewardHelper, BaseReward from grid2op.Rules import RulesChecker, AlwaysLegal, BaseRules -from grid2op.Backend import Backend, protectionScheme, thermalLimits +from grid2op.Backend import Backend, thermalLimits from grid2op.Chronics import ChronicsHandler from grid2op.VoltageControler import ControlVoltageFromFile, BaseVoltageController from grid2op.Environment.baseEnv import BaseEnv @@ -85,6 +85,7 @@ def __init__( chronics_handler, backend, parameters, + ts_manager: thermalLimits.ThermalLimits = None, name="unknown", n_busbar:N_BUSBAR_PER_SUB_TYPING=DEFAULT_N_BUSBAR_PER_SUB, allow_detachment:bool=DEFAULT_ALLOW_DETACHMENT, @@ -207,19 +208,19 @@ def __init__( self._init_backend( chronics_handler, backend, + ts_manager, names_chronics_to_backend, actionClass, observationClass, rewardClass, legalActClass, ) - self.ts_manager : thermalLimits.ThermalLimits = copy.deepcopy(self.ts_manager) - self.protection : protectionScheme.DefaultProtection = copy.deepcopy(self.protection) def _init_backend( self, chronics_handler, backend, + ts_manager, names_chronics_to_backend, actionClass, observationClass, @@ -249,7 +250,13 @@ def _init_backend( type(rewardClass) ) ) - + #thermalLimits: + ts_manager = ts_manager or self.ts_manager + if not isinstance(ts_manager, thermalLimits.ThermalLimits): + raise Grid2OpException( + 'Parameter "ts_manager" used to build the Environment should derived form the ' + 'grid2op.Backend.thermalLimits class, type provided is "{}"'.format(type(ts_manager)) + ) # backend if not isinstance(backend, Backend): raise Grid2OpException( @@ -326,11 +333,13 @@ def _init_backend( self._line_status = np.ones(shape=self.n_line, dtype=dt_bool) self._disc_lines = np.zeros(shape=self.n_line, dtype=dt_int) - 1 + self.ts_manager.n_line = self.n_line + self.ts_manager.name_line = self.name_line if self._thermal_limit_a is None: - self._thermal_limit_a = self.backend.thermal_limit_a.astype(dt_float) + self.ts_manager.limits = self.backend.thermal_limit_a.astype(dt_float) + self._thermal_limit_a = self.ts_manager.limits else: - self.backend.set_thermal_limit(self._thermal_limit_a.astype(dt_float)) - + self.ts_manager.limits = self._thermal_limit_a.astype(dt_float) *_, tmp = self.backend.generators_info() # rules of the game @@ -1582,7 +1591,8 @@ def get_kwargs(self, res["legalActClass"] = self._legalActClass res["epsilon_poly"] = self._epsilon_poly res["tol_poly"] = self._tol_poly - res["thermal_limit_a"] = self._thermal_limit_a + # res["thermal_limit_a"] = self._thermal_limit_a + res["ts_manager"] = self.ts_manager res["voltagecontrolerClass"] = self._voltagecontrolerClass res["other_rewards"] = {k: v.rewardClass for k, v in self.other_rewards.items()} res["name"] = self.name @@ -2204,7 +2214,8 @@ def get_params_for_runner(self): if self.chronics_handler.max_iter is not None: res["max_iter"] = self.chronics_handler.max_iter res["gridStateclass_kwargs"] = dict_ - res["thermal_limit_a"] = self._thermal_limit_a + # res["thermal_limit_a"] = self._thermal_limit_a + res["ts_manager"] = self.ts_manager res["voltageControlerClass"] = self._voltagecontrolerClass res["other_rewards"] = {k: v.rewardClass for k, v in self.other_rewards.items()} res["grid_layout"] = self.grid_layout diff --git a/grid2op/Episode/EpisodeReboot.py b/grid2op/Episode/EpisodeReboot.py index bda5218d..4f09b6a7 100644 --- a/grid2op/Episode/EpisodeReboot.py +++ b/grid2op/Episode/EpisodeReboot.py @@ -244,9 +244,7 @@ def _assign_state(self, obs): # # TODO check that the "stored" "last bus for when the powerline were connected" are # # kept there (I might need to do a for loop) self.env.backend.update_from_obs(obs) - disc_lines, detailed_info, conv_ = self.env.backend.next_grid_state( - env=self.env - ) + disc_lines, detailed_info, conv_ = self.env.protection.next_grid_state() if conv_ is None: self.env._backend_action.update_state(disc_lines) self.env._backend_action.reset() diff --git a/grid2op/MakeEnv/MakeFromPath.py b/grid2op/MakeEnv/MakeFromPath.py index 6692edfa..45468e64 100644 --- a/grid2op/MakeEnv/MakeFromPath.py +++ b/grid2op/MakeEnv/MakeFromPath.py @@ -1069,8 +1069,9 @@ def make_from_dataset_path( env._do_not_erase_local_dir_cls = do_not_erase_cls # Update the thermal limit if any if thermal_limits is not None: - env.set_thermal_limit(thermal_limits) - + env.ts_manager.env_limits(thermal_limits) + env.observation_space.ts_manager.env_limits(env.ts_manager.limits) + # Set graph layout if not None and not an empty dict if graph_layout is not None and graph_layout: try: diff --git a/grid2op/Observation/baseObservation.py b/grid2op/Observation/baseObservation.py index df727e19..9cc7a48f 100644 --- a/grid2op/Observation/baseObservation.py +++ b/grid2op/Observation/baseObservation.py @@ -4405,7 +4405,7 @@ def _update_obs_complete(self, env: "grid2op.Environment.BaseEnv", with_forecast self.target_dispatch[:] = env._target_dispatch self.actual_dispatch[:] = env._actual_dispatch - self._thermal_limit[:] = env.get_thermal_limit() + self._thermal_limit[:] = env.ts_manager.limits if self.redispatching_unit_commitment_availble: self.gen_p_before_curtail[:] = env._gen_before_curtailment diff --git a/grid2op/Observation/observationSpace.py b/grid2op/Observation/observationSpace.py index b6a6e005..189db32e 100644 --- a/grid2op/Observation/observationSpace.py +++ b/grid2op/Observation/observationSpace.py @@ -189,7 +189,7 @@ def _create_obs_env(self, env, observationClass): parameters=self._simulate_parameters, reward_helper=self.reward_helper, action_helper=self.action_helper_env, - thermal_limit_a=env.get_thermal_limit(), + thermal_limit_a=env.ts_manager.limits, legalActClass=copy.deepcopy(env._legalActClass), other_rewards=other_rewards, helper_action_class=env._helper_action_class, @@ -399,11 +399,11 @@ def change_reward(self, reward_func): "function when you cannot simulate (because the " "backend could not be copied)") - def set_thermal_limit(self, thermal_limit_a): - if self.obs_env is not None: - self.obs_env.set_thermal_limit(thermal_limit_a) - # if self._backend_obs is not None: - # self.ts_manager.limits = thermal_limit_a + # def set_thermal_limit(self, thermal_limit_a): + # if self.obs_env is not None: + # self.obs_env.set_thermal_limit(thermal_limit_a) + # if self._backend_obs is not None: + # self.obs_env.ts_manager.limits = thermal_limit_a def reset_space(self): if self.with_forecast: diff --git a/grid2op/Runner/runner.py b/grid2op/Runner/runner.py index 9ee0e28c..370fb423 100644 --- a/grid2op/Runner/runner.py +++ b/grid2op/Runner/runner.py @@ -22,7 +22,7 @@ from grid2op.Rules import AlwaysLegal from grid2op.Environment import Environment from grid2op.Chronics import ChronicsHandler, GridStateFromFile, GridValue, MultifolderWithCache -from grid2op.Backend import Backend, PandaPowerBackend +from grid2op.Backend import Backend, PandaPowerBackend, thermalLimits from grid2op.Parameters import Parameters from grid2op.Agent import DoNothingAgent, BaseAgent from grid2op.VoltageControler import ControlVoltageFromFile @@ -375,6 +375,7 @@ def __init__( gridStateclass_kwargs={}, voltageControlerClass=ControlVoltageFromFile, thermal_limit_a=None, + ts_manager=thermalLimits.ThermalLimits, max_iter=-1, other_rewards={}, opponent_space_type=OpponentSpace, @@ -667,6 +668,7 @@ def __init__( self.verbose = verbose self.thermal_limit_a = thermal_limit_a + self.ts_manager = ts_manager # controler for voltage if not issubclass(voltageControlerClass, ControlVoltageFromFile): @@ -1278,6 +1280,7 @@ def _get_params(self): "gridStateclass_kwargs": copy.deepcopy(self.gridStateclass_kwargs), "voltageControlerClass": self.voltageControlerClass, "thermal_limit_a": self.thermal_limit_a, + "ts_manager": self.ts_manager, "max_iter": self.max_iter, "other_rewards": copy.deepcopy(self._other_rewards), "opponent_space_type": self._opponent_space_type, From 8934ddd7e5a1482029f96c0030eff8ba3763488b Mon Sep 17 00:00:00 2001 From: yassine abdou Date: Thu, 20 Feb 2025 10:00:49 +0100 Subject: [PATCH 3/8] fix : bug update thermal limit --- grid2op/Environment/environment.py | 2 +- grid2op/Episode/EpisodeReboot.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index cd46c49a..21a4c40c 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -962,7 +962,7 @@ def reset_grid(self, # self.backend.assert_grid_correct() if self._thermal_limit_a is not None: - self.backend.set_thermal_limit(self._thermal_limit_a.astype(dt_float)) + self.ts_manager.limits = self._thermal_limit_a.astype(dt_float) self._backend_action = self._backend_action_class() self.nb_time_step = -1 # to have init obs at step 1 (and to prevent 'setting to proper state' "action" to be illegal) diff --git a/grid2op/Episode/EpisodeReboot.py b/grid2op/Episode/EpisodeReboot.py index 4f09b6a7..3eda0b32 100644 --- a/grid2op/Episode/EpisodeReboot.py +++ b/grid2op/Episode/EpisodeReboot.py @@ -182,6 +182,8 @@ def load(self, backend, agent_path=None, name=None, data=None, env_kwargs={}): del env_kwargs["opponent_class"] if "name" in env_kwargs: del env_kwargs["name"] + if "ts_manager" in env_kwargs: + del env_kwargs["ts_manager"] seed = None with open(os.path.join(agent_path, name, "episode_meta.json")) as f: From 2209a308b6e4894a03718facd35242a362cd5a4b Mon Sep 17 00:00:00 2001 From: yassine abdou Date: Thu, 20 Feb 2025 15:56:39 +0100 Subject: [PATCH 4/8] feat : use default and no protection in baseEnv, use protection in place of next_grid function from backend --- grid2op/Backend/protectionScheme.py | 23 +++--- grid2op/Environment/baseEnv.py | 94 ++++++++++++++----------- grid2op/MakeEnv/MakeFromPath.py | 3 +- grid2op/Observation/observationSpace.py | 10 +-- grid2op/tests/BaseBackendTest.py | 34 +++++---- 5 files changed, 91 insertions(+), 73 deletions(-) diff --git a/grid2op/Backend/protectionScheme.py b/grid2op/Backend/protectionScheme.py index 9ecacf07..4a0c3658 100644 --- a/grid2op/Backend/protectionScheme.py +++ b/grid2op/Backend/protectionScheme.py @@ -32,11 +32,11 @@ def __init__( self.thermal_limits = thermal_limits self._thermal_limit_a = self.thermal_limits.limits if self.thermal_limits else None - + self.backend.thermal_limit_a = self._thermal_limit_a + self._hard_overflow_threshold = self._get_value_from_parameters("HARD_OVERFLOW_THRESHOLD") self._soft_overflow_threshold = self._get_value_from_parameters("SOFT_OVERFLOW_THRESHOLD") self._nb_timestep_overflow_allowed = self._get_value_from_parameters("NB_TIMESTEP_OVERFLOW_ALLOWED") - self._no_overflow_disconnection = self._get_value_from_parameters("NO_OVERFLOW_DISCONNECTION") self.disconnected_during_cf = np.full(self.thermal_limits.n_line, fill_value=-1, dtype=dt_int) self._timestep_overflow = np.zeros(self.thermal_limits.n_line, dtype=dt_int) @@ -81,9 +81,6 @@ def _disconnect_lines(self, lines_to_disconnect: np.ndarray, timestep: int) -> N def next_grid_state(self) -> Tuple[np.ndarray, List[Any], Union[None, Exception]]: try: - if self._no_overflow_disconnection: # détaché cela d'ici et si on simule pas - return self._handle_no_protection() - timestep = 0 while True: power_flow_result = self._run_power_flow() @@ -105,24 +102,30 @@ def next_grid_state(self) -> Tuple[np.ndarray, List[Any], Union[None, Exception] logger.exception("Erreur inattendue dans le calcul de l'état du réseau.") return self.disconnected_during_cf, self.infos, e - def _handle_no_protection(self) -> Tuple[np.ndarray, List[Any], None]: - no_protection = NoProtection(self.thermal_limits) - return no_protection.handle_no_protection() - class NoProtection: """ Classe qui gère le cas où les protections de débordement sont désactivées. """ def __init__( self, + backend: Backend, thermal_limits: ThermalLimits ): + self.backend = backend + self._validate_input(self.backend) self.thermal_limits = thermal_limits + + self._thermal_limit_a = self.thermal_limits.limits if self.thermal_limits else None + self.backend.thermal_limit_a = self._thermal_limit_a + self.disconnected_during_cf = np.full(self.thermal_limits.n_line, fill_value=-1, dtype=dt_int) self.infos = [] - def handle_no_protection(self) -> Tuple[np.ndarray, List[Any], None]: + def _validate_input(self, backend: Backend) -> None: + if not isinstance(backend, Backend): + raise Grid2OpException(f"Argument 'backend' doit être de type 'Backend', reçu : {type(backend)}") + def next_grid_state(self) -> Tuple[np.ndarray, List[Any], None]: """ Retourne l'état du réseau sans effectuer de déconnexions dues aux débordements. """ diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index f8b84471..c030b1d9 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -571,7 +571,7 @@ def __init__( # Thermal limit and protection self._init_thermal_limit() - self.protection : protectionScheme.DefaultProtection = None + self.protection : protectionScheme = None # to change the parameters self.__new_param = None @@ -681,12 +681,21 @@ def _init_thermal_limit(self): def _init_protection(self): # Initialize the protection system with the specified parameters - self.protection = protectionScheme.DefaultProtection( - backend=self.backend, - parameters=self.parameters, - thermal_limits=self.ts_manager, - is_dc=self._env_dc - ) + self._no_overflow_disconnection: bool = ( + self._parameters.NO_OVERFLOW_DISCONNECTION + ) + if self._no_overflow_disconnection: + self.protection = protectionScheme.NoProtection( + backend=self.backend, + thermal_limits=self.ts_manager, + ) + else: + self.protection = protectionScheme.DefaultProtection( + backend=self.backend, + parameters=self.parameters, + thermal_limits=self.ts_manager, + is_dc=self._env_dc + ) @property def highres_sim_counter(self): @@ -1869,51 +1878,52 @@ def _init_backend( """ pass - # def set_thermal_limit(self, thermal_limit): - # """ - # Set the thermal limit effectively. + def set_thermal_limit(self, thermal_limit): + """ + Set the thermal limit effectively. - # Parameters - # ---------- - # thermal_limit: ``numpy.ndarray`` - # The new thermal limit. It must be a numpy ndarray vector (or convertible to it). For each powerline it - # gives the new thermal limit. + Parameters + ---------- + thermal_limit: ``numpy.ndarray`` + The new thermal limit. It must be a numpy ndarray vector (or convertible to it). For each powerline it + gives the new thermal limit. - # Alternatively, this can be a dictionary mapping the line names (keys) to its thermal limits (values). In - # that case, all thermal limits for all powerlines should be specified (this is a safety measure - # to reduce the odds of misuse). + Alternatively, this can be a dictionary mapping the line names (keys) to its thermal limits (values). In + that case, all thermal limits for all powerlines should be specified (this is a safety measure + to reduce the odds of misuse). - # Examples - # --------- + Examples + --------- - # This function can be used like this: + This function can be used like this: - # .. code-block:: python + .. code-block:: python - # import grid2op + import grid2op - # # I create an environment - # env = grid2op.make(""l2rpn_case14_sandbox"", test=True) + # I create an environment + env = grid2op.make(""l2rpn_case14_sandbox"", test=True) - # # i set the thermal limit of each powerline to 20000 amps - # env.set_thermal_limit([20000 for _ in range(env.n_line)]) + # i set the thermal limit of each powerline to 20000 amps + env.set_thermal_limit([20000 for _ in range(env.n_line)]) - # Notes - # ----- - # As of grid2op > 1.5.0, it is possible to set the thermal limit by using a dictionary with the keys being - # the name of the powerline and the values the thermal limits. + Notes + ----- + As of grid2op > 1.5.0, it is possible to set the thermal limit by using a dictionary with the keys being + the name of the powerline and the values the thermal limits. - # """ - # if self.__closed: - # raise EnvError("This environment is closed, you cannot use it.") - # if not self.__is_init: - # raise Grid2OpException( - # "Impossible to set the thermal limit to a non initialized Environment. " - # "Have you called `env.reset()` after last game over ?" - # ) - # # update n_line and name_line of ts_manager (old : self.ts_manager.n_line = -1 and self.ts_manager.name_line = None) - # self.ts_manager.env_limits(thermal_limit=thermal_limit) - # self.observation_space.set_thermal_limit(thermal_limit=thermal_limit) + """ + if self.__closed: + raise EnvError("This environment is closed, you cannot use it.") + if not self.__is_init: + raise Grid2OpException( + "Impossible to set the thermal limit to a non initialized Environment. " + "Have you called `env.reset()` after last game over ?" + ) + # update n_line and name_line of ts_manager (old : self.ts_manager.n_line = -1 and self.ts_manager.name_line = None) + self.ts_manager.env_limits(thermal_limit=thermal_limit) + self.backend.thermal_limit_a = self.ts_manager.limits + self.observation_space.set_thermal_limit(self.ts_manager.limits) def _reset_redispatching(self): # redispatching diff --git a/grid2op/MakeEnv/MakeFromPath.py b/grid2op/MakeEnv/MakeFromPath.py index 45468e64..9a3702fc 100644 --- a/grid2op/MakeEnv/MakeFromPath.py +++ b/grid2op/MakeEnv/MakeFromPath.py @@ -1069,8 +1069,7 @@ def make_from_dataset_path( env._do_not_erase_local_dir_cls = do_not_erase_cls # Update the thermal limit if any if thermal_limits is not None: - env.ts_manager.env_limits(thermal_limits) - env.observation_space.ts_manager.env_limits(env.ts_manager.limits) + env.set_thermal_limit(thermal_limits) # Set graph layout if not None and not an empty dict if graph_layout is not None and graph_layout: diff --git a/grid2op/Observation/observationSpace.py b/grid2op/Observation/observationSpace.py index 189db32e..13f698ee 100644 --- a/grid2op/Observation/observationSpace.py +++ b/grid2op/Observation/observationSpace.py @@ -399,11 +399,11 @@ def change_reward(self, reward_func): "function when you cannot simulate (because the " "backend could not be copied)") - # def set_thermal_limit(self, thermal_limit_a): - # if self.obs_env is not None: - # self.obs_env.set_thermal_limit(thermal_limit_a) - # if self._backend_obs is not None: - # self.obs_env.ts_manager.limits = thermal_limit_a + def set_thermal_limit(self, thermal_limit_a): + if self.obs_env is not None: + self.obs_env.ts_manager.env_limits(thermal_limit_a) + if self._backend_obs is not None: + self._backend_obs.thermal_limit_a = thermal_limit_a def reset_space(self): if self.with_forecast: diff --git a/grid2op/tests/BaseBackendTest.py b/grid2op/tests/BaseBackendTest.py index 2feb91c8..ee8f8bc5 100644 --- a/grid2op/tests/BaseBackendTest.py +++ b/grid2op/tests/BaseBackendTest.py @@ -1637,7 +1637,7 @@ def next_grid_state_no_overflow(self): ) - disco, infos, conv_ = self.backend.next_grid_state(env, is_dc=False) + disco, infos, conv_ = env.protection.next_grid_state() assert conv_ is None assert not infos @@ -1667,9 +1667,10 @@ def test_next_grid_state_1overflow(self): thermal_limit[self.id_first_line_disco] = ( self.lines_flows_init[self.id_first_line_disco] / 2 ) - self.backend.set_thermal_limit(thermal_limit) - - disco, infos, conv_ = self.backend.next_grid_state(env, is_dc=False) + env.ts_manager.limits = thermal_limit + env._init_protection() + + disco, infos, conv_ = env.protection.next_grid_state() assert conv_ is None assert len(infos) == 1 # check that i have only one overflow assert np.sum(disco >= 0) == 1 @@ -1704,9 +1705,10 @@ def test_next_grid_state_1overflow_envNoCF(self): thermal_limit[self.id_first_line_disco] = ( lines_flows_init[self.id_first_line_disco] / 2 ) - self.backend.set_thermal_limit(thermal_limit) + env.ts_manager.limits = thermal_limit + env._init_protection() - disco, infos, conv_ = self.backend.next_grid_state(env, is_dc=False) + disco, infos, conv_ = env.protection.next_grid_state() assert conv_ is None assert not infos # check that don't simulate a cascading failure assert np.sum(disco >= 0) == 0 @@ -1750,9 +1752,10 @@ def test_nb_timestep_overflow_disc0(self): lines_flows_init[self.id_first_line_disco] / 2 ) thermal_limit[self.id_2nd_line_disco] = 400 - self.backend.set_thermal_limit(thermal_limit) + env.ts_manager.limits = thermal_limit + env._init_protection() - disco, infos, conv_ = self.backend.next_grid_state(env, is_dc=False) + disco, infos, conv_ = env.protection.next_grid_state() assert conv_ is None assert len(infos) == 2 # check that there is a cascading failure of length 2 assert disco[self.id_first_line_disco] >= 0 @@ -1793,9 +1796,10 @@ def test_nb_timestep_overflow_nodisc(self): self.lines_flows_init[self.id_first_line_disco] / 2 ) thermal_limit[self.id_2nd_line_disco] = 400 - self.backend.set_thermal_limit(thermal_limit) + env.ts_manager.limits = thermal_limit + env._init_protection() - disco, infos, conv_ = self.backend.next_grid_state(env, is_dc=False) + disco, infos, conv_ = env.protection.next_grid_state() assert conv_ is None assert len(infos) == 1 # check that don't simulate a cascading failure assert disco[self.id_first_line_disco] >= 0 @@ -1836,9 +1840,10 @@ def test_nb_timestep_overflow_nodisc_2(self): self.lines_flows_init[self.id_first_line_disco] / 2 ) thermal_limit[self.id_2nd_line_disco] = 400 - self.backend.set_thermal_limit(thermal_limit) + env.ts_manager.limits = thermal_limit + env._init_protection() - disco, infos, conv_ = self.backend.next_grid_state(env, is_dc=False) + disco, infos, conv_ = env.protection.next_grid_state() assert conv_ is None assert len(infos) == 1 # check that don't simulate a cascading failure assert disco[self.id_first_line_disco] >= 0 @@ -1879,9 +1884,10 @@ def test_nb_timestep_overflow_disc2(self): self.lines_flows_init[self.id_first_line_disco] / 2 ) thermal_limit[self.id_2nd_line_disco] = 400 - self.backend.set_thermal_limit(thermal_limit) + env.ts_manager.limits = thermal_limit + env._init_protection() - disco, infos, conv_ = self.backend.next_grid_state(env, is_dc=False) + disco, infos, conv_ = env.protection.next_grid_state() assert conv_ is None assert len(infos) == 2 # check that there is a cascading failure of length 2 assert disco[self.id_first_line_disco] >= 0 From 2b82bd9b1ef4cfccdfd6b37197e797f1acb911b0 Mon Sep 17 00:00:00 2001 From: yassine abdou Date: Thu, 20 Feb 2025 16:21:13 +0100 Subject: [PATCH 5/8] feat : Change -next_grid- to -protection- in all files containing this functionality, while the rest of the code remains unchanged. --- grid2op/Backend/backend.py | 378 ++++++++++++++--------------- grid2op/Environment/_obsEnv.py | 5 +- grid2op/Environment/baseEnv.py | 44 ++-- grid2op/Environment/environment.py | 4 +- 4 files changed, 214 insertions(+), 217 deletions(-) diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index ce2f3aab..f736959d 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -764,135 +764,135 @@ def get_line_flow(self) -> np.ndarray: p_or, q_or, v_or, a_or = self.lines_or_info() return a_or - # def set_thermal_limit(self, limits : Union[np.ndarray, Dict["str", float]]) -> None: - # """ - # INTERNAL - - # .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ - - # You can set the thermal limit directly in the environment. - - # This function is used as a convenience function to set the thermal limits :attr:`Backend.thermal_limit_a` - # in amperes. - - # It can be used at the beginning of an episode if the thermal limit are not present in the original data files - # or alternatively if the thermal limits depends on the period of the year (one in winter and one in summer - # for example). - - # Parameters - # ---------- - # limits: ``object`` - # It can be understood differently according to its type: - - # - If it's a ``numpy.ndarray``, then it is assumed the thermal limits are given in amperes in the same order - # as the powerlines computed in the backend. In that case it modifies all the thermal limits of all - # the powerlines at once. - # - If it's a ``dict`` it must have: - - # - as key the powerline names (not all names are mandatory, in that case only the powerlines with the name - # in this dictionnary will be modified) - # - as value the new thermal limit (should be a strictly positive float). - - # """ - # if isinstance(limits, np.ndarray): - # if limits.shape[0] == self.n_line: - # self.thermal_limit_a = 1.0 * limits.astype(dt_float) - # elif isinstance(limits, dict): - # for el in limits.keys(): - # if not el in self.name_line: - # raise BackendError( - # 'You asked to modify the thermal limit of powerline named "{}" that is not ' - # "on the grid. Names of powerlines are {}".format( - # el, self.name_line - # ) - # ) - # for i, el in self.name_line: - # if el in limits: - # try: - # tmp = dt_float(limits[el]) - # except Exception as exc_: - # raise BackendError( - # 'Impossible to convert data ({}) for powerline named "{}" into float ' - # "values".format(limits[el], el) - # ) from exc_ - # if tmp <= 0: - # raise BackendError( - # 'New thermal limit for powerlines "{}" is not positive ({})' - # "".format(el, tmp) - # ) - # self.thermal_limit_a[i] = tmp - - # def update_thermal_limit_from_vect(self, thermal_limit_a : np.ndarray) -> None: - # """You can use it if your backend stores the thermal limits - # of the grid in a vector (see :class:`PandaPowerBackend` for example) + def set_thermal_limit(self, limits : Union[np.ndarray, Dict["str", float]]) -> None: + """ + INTERNAL + + .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ + + You can set the thermal limit directly in the environment. + + This function is used as a convenience function to set the thermal limits :attr:`Backend.thermal_limit_a` + in amperes. + + It can be used at the beginning of an episode if the thermal limit are not present in the original data files + or alternatively if the thermal limits depends on the period of the year (one in winter and one in summer + for example). + + Parameters + ---------- + limits: ``object`` + It can be understood differently according to its type: + + - If it's a ``numpy.ndarray``, then it is assumed the thermal limits are given in amperes in the same order + as the powerlines computed in the backend. In that case it modifies all the thermal limits of all + the powerlines at once. + - If it's a ``dict`` it must have: + + - as key the powerline names (not all names are mandatory, in that case only the powerlines with the name + in this dictionnary will be modified) + - as value the new thermal limit (should be a strictly positive float). + + """ + if isinstance(limits, np.ndarray): + if limits.shape[0] == self.n_line: + self.thermal_limit_a = 1.0 * limits.astype(dt_float) + elif isinstance(limits, dict): + for el in limits.keys(): + if not el in self.name_line: + raise BackendError( + 'You asked to modify the thermal limit of powerline named "{}" that is not ' + "on the grid. Names of powerlines are {}".format( + el, self.name_line + ) + ) + for i, el in self.name_line: + if el in limits: + try: + tmp = dt_float(limits[el]) + except Exception as exc_: + raise BackendError( + 'Impossible to convert data ({}) for powerline named "{}" into float ' + "values".format(limits[el], el) + ) from exc_ + if tmp <= 0: + raise BackendError( + 'New thermal limit for powerlines "{}" is not positive ({})' + "".format(el, tmp) + ) + self.thermal_limit_a[i] = tmp + + def update_thermal_limit_from_vect(self, thermal_limit_a : np.ndarray) -> None: + """You can use it if your backend stores the thermal limits + of the grid in a vector (see :class:`PandaPowerBackend` for example) - # .. warning:: - # This is not called by the environment and cannot be used to - # model Dynamic Line Rating. For such purpose please use `update_thermal_limit` + .. warning:: + This is not called by the environment and cannot be used to + model Dynamic Line Rating. For such purpose please use `update_thermal_limit` - # This function is used to create a "Simulator" from a backend for example. + This function is used to create a "Simulator" from a backend for example. - # Parameters - # ---------- - # vect : np.ndarray - # The thermal limits (in A) - # """ - # thermal_limit_a = np.array(thermal_limit_a).astype(dt_float) - # self.thermal_limit_a[:] = thermal_limit_a + Parameters + ---------- + vect : np.ndarray + The thermal limits (in A) + """ + thermal_limit_a = np.array(thermal_limit_a).astype(dt_float) + self.thermal_limit_a[:] = thermal_limit_a - # def update_thermal_limit(self, env : "grid2op.Environment.BaseEnv") -> None: - # """ - # INTERNAL + def update_thermal_limit(self, env : "grid2op.Environment.BaseEnv") -> None: + """ + INTERNAL - # .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ + .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ - # This is done in a call to `env.step` in case of DLR for example. + This is done in a call to `env.step` in case of DLR for example. - # If you don't want this feature, do not implement it. + If you don't want this feature, do not implement it. - # Update the new thermal limit in case of DLR for example. + Update the new thermal limit in case of DLR for example. - # By default it does nothing. + By default it does nothing. - # Depending on the operational strategy, it is also possible to implement some - # `Dynamic Line Rating `_ (DLR) - # strategies. - # In this case, this function will give the thermal limit for a given time step provided the flows and the - # weather condition are accessible by the backend. Our methodology doesn't make any assumption on the method - # used to get these thermal limits. + Depending on the operational strategy, it is also possible to implement some + `Dynamic Line Rating `_ (DLR) + strategies. + In this case, this function will give the thermal limit for a given time step provided the flows and the + weather condition are accessible by the backend. Our methodology doesn't make any assumption on the method + used to get these thermal limits. - # Parameters - # ---------- - # env: :class:`grid2op.Environment.Environment` - # The environment used to compute the thermal limit + Parameters + ---------- + env: :class:`grid2op.Environment.Environment` + The environment used to compute the thermal limit - # """ - # pass + """ + pass - # def get_thermal_limit(self) -> np.ndarray: - # """ - # INTERNAL + def get_thermal_limit(self) -> np.ndarray: + """ + INTERNAL - # .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ + .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ - # Retrieve the thermal limit directly from the environment instead (with a call - # to :func:`grid2op.Environment.BaseEnc.get_thermal_limit` for example) + Retrieve the thermal limit directly from the environment instead (with a call + to :func:`grid2op.Environment.BaseEnc.get_thermal_limit` for example) - # Gives the thermal limit (in amps) for each powerline of the _grid. Only one value per powerline is returned. + Gives the thermal limit (in amps) for each powerline of the _grid. Only one value per powerline is returned. - # It is assumed that both :func:`Backend.get_line_flow` and *_get_thermal_limit* gives the value of the same - # end of the powerline. + It is assumed that both :func:`Backend.get_line_flow` and *_get_thermal_limit* gives the value of the same + end of the powerline. - # See the help of *_get_line_flow* for a more detailed description of this problem. + See the help of *_get_line_flow* for a more detailed description of this problem. - # For assumption about the order of the powerline flows return in this vector, see the help of the - # :func:`Backend.get_line_status` method. + For assumption about the order of the powerline flows return in this vector, see the help of the + :func:`Backend.get_line_status` method. - # :return: An array giving the thermal limit of the powerlines. - # :rtype: np.array, dtype:float - # """ - # return self.thermal_limit_a + :return: An array giving the thermal limit of the powerlines. + :rtype: np.array, dtype:float + """ + return self.thermal_limit_a def get_relative_flow(self) -> np.ndarray: """ @@ -915,7 +915,7 @@ def get_relative_flow(self) -> np.ndarray: The relative flow in each powerlines of the grid. """ num_ = self.get_line_flow() - denom_ = self.thermal_limit_a + denom_ = self.get_thermal_limit() res = np.divide(num_, denom_) return res @@ -943,7 +943,7 @@ def get_line_overflow(self) -> np.ndarray: :return: An array saying if a powerline is overflow or not :rtype: np.array, dtype:bool """ - th_lim = self.thermal_limit_a + th_lim = self.get_thermal_limit() flow = self.get_line_flow() return flow > th_lim @@ -1121,87 +1121,87 @@ def _runpf_with_diverging_exception(self, is_dc : bool) -> Optional[Exception]: ) return exc_me - # def next_grid_state(self, - # env: "grid2op.Environment.BaseEnv", - # is_dc: Optional[bool]=False): - # """ - # INTERNAL - - # .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ - - # This is called by `env.step` - - # This method is called by the environment to compute the next\\_grid\\_states. - # It allows to compute the powerline and approximate the "cascading failures" if there are some overflows. - - # Attributes - # ---------- - # env: :class:`grid2op.Environment.Environment` - # the environment in which the powerflow is ran. - - # is_dc: ``bool`` - # mode of power flow (AC : False, DC: is_dc is True) - - # Returns - # -------- - # disconnected_during_cf: ``numpy.ndarray``, dtype=bool - # For each powerlines, it returns ``True`` if the powerline has been disconnected due to a cascading failure - # or ``False`` otherwise. - - # infos: ``list`` - # If :attr:`Backend.detailed_infos_for_cascading_failures` is ``True`` then it returns the different - # state computed by the powerflow (can drastically slow down this function, as it requires - # deep copy of backend object). Otherwise the list is always empty. - - # """ - # infos = [] - # disconnected_during_cf = np.full(self.n_line, fill_value=-1, dtype=dt_int) - # conv_ = self._runpf_with_diverging_exception(is_dc) - # if env._no_overflow_disconnection or conv_ is not None: - # return disconnected_during_cf, infos, conv_ - - # # the environment disconnect some powerlines - # init_time_step_overflow = copy.deepcopy(env._timestep_overflow) - # ts = 0 - # while True: - # # simulate the cascading failure - # lines_flows = 1.0 * self.get_line_flow() - # thermal_limits = self.get_thermal_limit() * env._parameters.SOFT_OVERFLOW_THRESHOLD # SOFT_OVERFLOW_THRESHOLD new in grid2op 1.9.3 - # lines_status = self.get_line_status() - - # # a) disconnect lines on hard overflow (that are still connected) - # to_disc = ( - # lines_flows > env._hard_overflow_threshold * thermal_limits - # ) & lines_status - - # # b) deals with soft overflow (disconnect them if lines still connected) - # init_time_step_overflow[(lines_flows >= thermal_limits) & lines_status] += 1 - # to_disc[ - # (init_time_step_overflow > env._nb_timestep_overflow_allowed) - # & lines_status - # ] = True - - # # disconnect the current power lines - # 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 + def next_grid_state(self, + env: "grid2op.Environment.BaseEnv", + is_dc: Optional[bool]=False): + """ + INTERNAL + + .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ + + This is called by `env.step` + + This method is called by the environment to compute the next\\_grid\\_states. + It allows to compute the powerline and approximate the "cascading failures" if there are some overflows. + + Attributes + ---------- + env: :class:`grid2op.Environment.Environment` + the environment in which the powerflow is ran. + + is_dc: ``bool`` + mode of power flow (AC : False, DC: is_dc is True) + + Returns + -------- + disconnected_during_cf: ``numpy.ndarray``, dtype=bool + For each powerlines, it returns ``True`` if the powerline has been disconnected due to a cascading failure + or ``False`` otherwise. + + infos: ``list`` + If :attr:`Backend.detailed_infos_for_cascading_failures` is ``True`` then it returns the different + state computed by the powerflow (can drastically slow down this function, as it requires + deep copy of backend object). Otherwise the list is always empty. + + """ + infos = [] + disconnected_during_cf = np.full(self.n_line, fill_value=-1, dtype=dt_int) + conv_ = self._runpf_with_diverging_exception(is_dc) + if env._no_overflow_disconnection or conv_ is not None: + return disconnected_during_cf, infos, conv_ + + # the environment disconnect some powerlines + init_time_step_overflow = copy.deepcopy(env._timestep_overflow) + ts = 0 + while True: + # simulate the cascading failure + lines_flows = 1.0 * self.get_line_flow() + thermal_limits = self.get_thermal_limit() * env._parameters.SOFT_OVERFLOW_THRESHOLD # SOFT_OVERFLOW_THRESHOLD new in grid2op 1.9.3 + lines_status = self.get_line_status() + + # a) disconnect lines on hard overflow (that are still connected) + to_disc = ( + lines_flows > env._hard_overflow_threshold * thermal_limits + ) & lines_status + + # b) deals with soft overflow (disconnect them if lines still connected) + init_time_step_overflow[(lines_flows >= thermal_limits) & lines_status] += 1 + to_disc[ + (init_time_step_overflow > env._nb_timestep_overflow_allowed) + & lines_status + ] = True + + # disconnect the current power lines + 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: - # self._disconnect_line(i) + # perform the disconnection action + for i, el in enumerate(to_disc): + if el: + self._disconnect_line(i) - # # start a powerflow on this new state - # conv_ = self._runpf_with_diverging_exception(is_dc) - # if self.detailed_infos_for_cascading_failures: - # infos.append(self.copy()) + # start a powerflow on this new state + conv_ = self._runpf_with_diverging_exception(is_dc) + if self.detailed_infos_for_cascading_failures: + infos.append(self.copy()) - # if conv_ is not None: - # break - # ts += 1 - # return disconnected_during_cf, infos, conv_ + if conv_ is not None: + break + ts += 1 + return disconnected_during_cf, infos, conv_ def storages_info(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """ @@ -2132,7 +2132,7 @@ def assert_grid_correct_after_powerflow(self) -> None: raise IncorrectNumberOfLines('returned by "backend.get_line_flow()"') if (~np.isfinite(tmp)).any(): raise EnvError(type(self).ERR_INIT_POWERFLOW) - tmp = self.thermal_limit_a + tmp = self.get_thermal_limit() if tmp.shape[0] != self.n_line: raise IncorrectNumberOfLines('returned by "backend.get_thermal_limit()"') if (~np.isfinite(tmp)).any(): diff --git a/grid2op/Environment/_obsEnv.py b/grid2op/Environment/_obsEnv.py index 4a174f74..760b8eb8 100644 --- a/grid2op/Environment/_obsEnv.py +++ b/grid2op/Environment/_obsEnv.py @@ -115,8 +115,6 @@ def __init__( self.current_obs_init = None self.current_obs = None - - self._init_backend( chronics_handler=_ObsCH(), backend=backend_instanciated, @@ -127,7 +125,6 @@ def __init__( legalActClass=legalActClass, ) - self.delta_time_seconds = delta_time_seconds #### # to be able to save and import (using env.generate_classes) correctly @@ -557,4 +554,4 @@ def close(self): ]: if hasattr(self, attr_nm): delattr(self, attr_nm) - setattr(self, attr_nm, None) + setattr(self, attr_nm, None) \ No newline at end of file diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index c030b1d9..a6ff0417 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -2588,33 +2588,33 @@ def get_obs(self, _update_state=True, _do_copy=True): else: return self._last_obs - # def get_thermal_limit(self): - # """ - # Get the current thermal limit in amps registered for the environment. + def get_thermal_limit(self): + """ + Get the current thermal limit in amps registered for the environment. - # Examples - # --------- + Examples + --------- - # It can be used like this: + It can be used like this: - # .. code-block:: python + .. code-block:: python - # import grid2op + import grid2op - # # I create an environment - # env = grid2op.make("l2rpn_case14_sandbox") + # I create an environment + env = grid2op.make("l2rpn_case14_sandbox") - # thermal_limits = env.get_thermal_limit() + thermal_limits = env.get_thermal_limit() - # """ - # if self.__closed: - # raise EnvError("This environment is closed, you cannot use it.") - # if not self.__is_init: - # raise EnvError( - # "This environment is not initialized. It has no thermal limits. " - # "Have you called `env.reset()` after last game over ?" - # ) - # return 1.0 * self._thermal_limit_a + """ + if self.__closed: + raise EnvError("This environment is closed, you cannot use it.") + if not self.__is_init: + raise EnvError( + "This environment is not initialized. It has no thermal limits. " + "Have you called `env.reset()` after last game over ?" + ) + return 1.0 * self._thermal_limit_a def _withdraw_storage_losses(self): """ @@ -3256,6 +3256,7 @@ def _protection_next_grid_state(self): """overlaoded in MaskedEnv""" self._init_thermal_limit() self._init_protection() + return self.protection.next_grid_state() def _aux_run_pf_after_state_properly_set( self, action, init_line_status, new_p, except_ @@ -3285,8 +3286,7 @@ def _aux_run_pf_after_state_properly_set( try: # compute the next _grid state beg_pf = time.perf_counter() - self._protection_next_grid_state() - disc_lines, detailed_info, conv_ = self.protection.next_grid_state() + disc_lines, detailed_info, conv_ = self._protection_next_grid_state() self._disc_lines[:] = disc_lines self._time_powerflow += time.perf_counter() - beg_pf if conv_ is None: diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 21a4c40c..b0b2a87a 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -1591,7 +1591,7 @@ def get_kwargs(self, res["legalActClass"] = self._legalActClass res["epsilon_poly"] = self._epsilon_poly res["tol_poly"] = self._tol_poly - # res["thermal_limit_a"] = self._thermal_limit_a + res["thermal_limit_a"] = self._thermal_limit_a res["ts_manager"] = self.ts_manager res["voltagecontrolerClass"] = self._voltagecontrolerClass res["other_rewards"] = {k: v.rewardClass for k, v in self.other_rewards.items()} @@ -2214,7 +2214,7 @@ def get_params_for_runner(self): if self.chronics_handler.max_iter is not None: res["max_iter"] = self.chronics_handler.max_iter res["gridStateclass_kwargs"] = dict_ - # res["thermal_limit_a"] = self._thermal_limit_a + res["thermal_limit_a"] = self._thermal_limit_a res["ts_manager"] = self.ts_manager res["voltageControlerClass"] = self._voltagecontrolerClass res["other_rewards"] = {k: v.rewardClass for k, v in self.other_rewards.items()} From 6ebe24cc624c5290c81b6d3ecbcd11b68de16cca Mon Sep 17 00:00:00 2001 From: yassine abdou Date: Thu, 20 Feb 2025 16:50:17 +0100 Subject: [PATCH 6/8] fix : bug logur --- grid2op/Backend/protectionScheme.py | 26 +++++++++++++++++++++----- grid2op/Backend/thermalLimits.py | 27 ++++++++++++++++++--------- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/grid2op/Backend/protectionScheme.py b/grid2op/Backend/protectionScheme.py index 4a0c3658..08af6d03 100644 --- a/grid2op/Backend/protectionScheme.py +++ b/grid2op/Backend/protectionScheme.py @@ -1,5 +1,5 @@ import copy -from loguru import logger +import logging from typing import Tuple, Union, Any, List, Optional import numpy as np @@ -20,6 +20,7 @@ def __init__( parameters: Parameters, thermal_limits: ThermalLimits, is_dc: bool = False, + logger: Optional[logging.Logger] = None, ): """ Initialise l'état du réseau avec des protections personnalisables. @@ -42,6 +43,12 @@ def __init__( self._timestep_overflow = np.zeros(self.thermal_limits.n_line, dtype=dt_int) self.infos: List[str] = [] + if logger is None: + self.logger = logging.getLogger(__name__) + self.logger.disabled = True + else: + self.logger: logging.Logger = logger.getChild("grid2op_BaseEnv") + def _validate_input(self, backend: Backend, parameters: Optional[Parameters]) -> None: if not isinstance(backend, Backend): raise Grid2OpException(f"Argument 'backend' doit être de type 'Backend', reçu : {type(backend)}") @@ -55,12 +62,18 @@ def _run_power_flow(self) -> Optional[Exception]: try: return self.backend._runpf_with_diverging_exception(self.is_dc) except Exception as e: - logger.error(f"Erreur flux de puissance : {e}") + if self.logger is not None: + self.logger.error( + f"Erreur flux de puissance : {e}" + ) return e def _update_overflows(self, lines_flows: np.ndarray) -> np.ndarray: if self._thermal_limit_a is None: - logger.error("Thermal limits must be provided for overflow calculations.") + if self.logger is not None: + self.logger.error( + "Thermal limits must be provided for overflow calculations." + ) raise ValueError("Thermal limits must be provided for overflow calculations.") lines_status = self.backend.get_line_status() # self._thermal_limit_a reste fixe. self._soft_overflow_threshold = 1 @@ -77,7 +90,9 @@ def _disconnect_lines(self, lines_to_disconnect: np.ndarray, timestep: int) -> N for line_idx in np.where(lines_to_disconnect)[0]: self.backend._disconnect_line(line_idx) self.disconnected_during_cf[line_idx] = timestep - logger.warning(f"Ligne {line_idx} déconnectée au pas de temps {timestep}.") + if self.logger is not None: + self.logger.warning(f"Ligne {line_idx} déconnectée au pas de temps {timestep}.") + def next_grid_state(self) -> Tuple[np.ndarray, List[Any], Union[None, Exception]]: try: @@ -99,7 +114,8 @@ def next_grid_state(self) -> Tuple[np.ndarray, List[Any], Union[None, Exception] return self.disconnected_during_cf, self.infos, None except Exception as e: - logger.exception("Erreur inattendue dans le calcul de l'état du réseau.") + if self.logger is not None: + self.exception("Erreur inattendue dans le calcul de l'état du réseau.") return self.disconnected_during_cf, self.infos, e class NoProtection: diff --git a/grid2op/Backend/thermalLimits.py b/grid2op/Backend/thermalLimits.py index f7ac6d4f..5a8a0284 100644 --- a/grid2op/Backend/thermalLimits.py +++ b/grid2op/Backend/thermalLimits.py @@ -4,7 +4,7 @@ except ImportError: from typing_extensions import Self -from loguru import logger +import logging import numpy as np import copy @@ -21,7 +21,8 @@ def __init__( self, _thermal_limit_a: Optional[np.ndarray] = None, line_names: Optional[List[str]] = None, - n_line: Optional[int] = None + n_line: Optional[int] = None, + logger: Optional[logging.Logger] = None, ): """ Initializes the thermal limits manager. @@ -43,8 +44,12 @@ def __init__( self._n_line = n_line self._name_line = line_names - logger.info(f"ThermalLimits initialized with {self.n_line} limits.") - + if logger is None: + self.logger = logging.getLogger(__name__) + self.logger.disabled = True + else: + self.logger: logging.Logger = logger.getChild("grid2op_BaseEnv") + @property def n_line(self) -> int: return self._n_line @@ -54,8 +59,9 @@ def n_line(self, new_n_line: int) -> None: if new_n_line <= 0: raise ValueError("Number of lines must be a positive integer.") self._n_line = new_n_line - logger.info(f"Number of lines updated to {self._n_line}.") - + if self.logger is not None: + self.logger.info(f"Number of lines updated to {self._n_line}.") + @property def name_line(self) -> Union[List[str], np.ndarray]: return self._name_line @@ -75,7 +81,8 @@ def name_line(self, new_name_line: Union[List[str], np.ndarray]) -> None: raise ValueError("Length of name list must match the number of lines.") self._name_line = new_name_line - logger.info(f"Power line names updated") + if self.logger is not None: + self.logger.info("Power line names updated") @property def limits(self) -> np.ndarray: @@ -185,7 +192,8 @@ def env_limits(self, thermal_limit): ) self._thermal_limit_a = tmp - logger.info("Env thermal limits successfully set.") + if self.logger is not None: + self.logger.info("Env thermal limits successfully set.") def update_limits_from_vector(self, thermal_limit_a: np.ndarray) -> None: """ @@ -196,7 +204,8 @@ def update_limits_from_vector(self, thermal_limit_a: np.ndarray) -> None: """ thermal_limit_a = np.array(thermal_limit_a).astype(dt_float) self._thermal_limit_a = thermal_limit_a - logger.info("Thermal limits updated from vector.") + if self.logger is not None: + self.logger.info("Thermal limits updated from vector.") def update_limits(self, env : "grid2op.Environment.BaseEnv") -> None: pass From 21d215f93cede1f224b721b297ef2eedfd850944 Mon Sep 17 00:00:00 2001 From: yassine abdou Date: Wed, 5 Mar 2025 14:55:35 +0100 Subject: [PATCH 7/8] feat : add unit test for protection and thermallimits --- grid2op/Backend/protectionScheme.py | 36 ++----- grid2op/Backend/thermalLimits.py | 2 +- grid2op/Environment/baseEnv.py | 40 ++++--- grid2op/tests/BaseBackendTest.py | 36 +++---- grid2op/tests/test_protectionScheme.py | 142 +++++++++++++++++++++++++ grid2op/tests/test_thermalLimits.py | 70 ++++++++++++ 6 files changed, 267 insertions(+), 59 deletions(-) create mode 100644 grid2op/tests/test_protectionScheme.py create mode 100644 grid2op/tests/test_thermalLimits.py diff --git a/grid2op/Backend/protectionScheme.py b/grid2op/Backend/protectionScheme.py index 08af6d03..9d9edbc8 100644 --- a/grid2op/Backend/protectionScheme.py +++ b/grid2op/Backend/protectionScheme.py @@ -17,8 +17,8 @@ class DefaultProtection: def __init__( self, backend: Backend, - parameters: Parameters, - thermal_limits: ThermalLimits, + parameters: Optional[Parameters] = None, + thermal_limits: Optional[ThermalLimits] = None, is_dc: bool = False, logger: Optional[logging.Logger] = None, ): @@ -38,9 +38,11 @@ def __init__( self._hard_overflow_threshold = self._get_value_from_parameters("HARD_OVERFLOW_THRESHOLD") self._soft_overflow_threshold = self._get_value_from_parameters("SOFT_OVERFLOW_THRESHOLD") self._nb_timestep_overflow_allowed = self._get_value_from_parameters("NB_TIMESTEP_OVERFLOW_ALLOWED") + self._no_overflow_disconnection = self._get_value_from_parameters("NO_OVERFLOW_DISCONNECTION") self.disconnected_during_cf = np.full(self.thermal_limits.n_line, fill_value=-1, dtype=dt_int) self._timestep_overflow = np.zeros(self.thermal_limits.n_line, dtype=dt_int) + self.conv_ = self._run_power_flow() self.infos: List[str] = [] if logger is None: @@ -115,37 +117,21 @@ def next_grid_state(self) -> Tuple[np.ndarray, List[Any], Union[None, Exception] except Exception as e: if self.logger is not None: - self.exception("Erreur inattendue dans le calcul de l'état du réseau.") + self.logger.exception("Erreur inattendue dans le calcul de l'état du réseau.") return self.disconnected_during_cf, self.infos, e -class NoProtection: +class NoProtection(DefaultProtection): """ - Classe qui gère le cas où les protections de débordement sont désactivées. + Classe qui désactive les protections de débordement tout en conservant la structure de DefaultProtection. """ - def __init__( - self, - backend: Backend, - thermal_limits: ThermalLimits - ): - self.backend = backend - self._validate_input(self.backend) - - self.thermal_limits = thermal_limits + def __init__(self, backend: Backend, thermal_limits: ThermalLimits, is_dc: bool = False): + super().__init__(backend, parameters=None, thermal_limits=thermal_limits, is_dc=is_dc) - self._thermal_limit_a = self.thermal_limits.limits if self.thermal_limits else None - self.backend.thermal_limit_a = self._thermal_limit_a - - self.disconnected_during_cf = np.full(self.thermal_limits.n_line, fill_value=-1, dtype=dt_int) - self.infos = [] - - def _validate_input(self, backend: Backend) -> None: - if not isinstance(backend, Backend): - raise Grid2OpException(f"Argument 'backend' doit être de type 'Backend', reçu : {type(backend)}") def next_grid_state(self) -> Tuple[np.ndarray, List[Any], None]: """ - Retourne l'état du réseau sans effectuer de déconnexions dues aux débordements. + Ignore les protections et retourne l'état du réseau sans déconnexions. """ - return self.disconnected_during_cf, self.infos, None + return self.disconnected_during_cf, self.infos, self.conv_ class BlablaProtection: pass \ No newline at end of file diff --git a/grid2op/Backend/thermalLimits.py b/grid2op/Backend/thermalLimits.py index 5a8a0284..25b991e9 100644 --- a/grid2op/Backend/thermalLimits.py +++ b/grid2op/Backend/thermalLimits.py @@ -122,7 +122,7 @@ def limits(self, new_limits: Union[np.ndarray, Dict[str, float]]): el, self.name_line ) ) - for i, el in self.name_line: + for i, el in enumerate(self.name_line): if el in new_limits: try: tmp = dt_float(new_limits[el]) diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index a6ff0417..7d440b70 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -680,23 +680,39 @@ def _init_thermal_limit(self): ) def _init_protection(self): - # Initialize the protection system with the specified parameters - self._no_overflow_disconnection: bool = ( - self._parameters.NO_OVERFLOW_DISCONNECTION - ) - if self._no_overflow_disconnection: - self.protection = protectionScheme.NoProtection( - backend=self.backend, - thermal_limits=self.ts_manager, - ) - else: - self.protection = protectionScheme.DefaultProtection( + """ + Initialise le système de protection du réseau avec gestion des erreurs et logs. + """ + try: + self.logger.info("Initialisation du système de protection...") + + initializerProtection = protectionScheme.DefaultProtection( backend=self.backend, parameters=self.parameters, thermal_limits=self.ts_manager, - is_dc=self._env_dc + is_dc=self._env_dc, + logger=self.logger ) + if self._no_overflow_disconnection or initializerProtection.conv_ is not None: + self.logger.warning("Utilisation de NoProtection car _no_overflow_disconnection est activé " + "ou la convergence du power flow a échoué.") + self.protection = protectionScheme.NoProtection( + backend=self.backend, + thermal_limits=self.ts_manager, + is_dc=self._env_dc + ) + else: + self.logger.info("Utilisation de DefaultProtection avec succès.") + self.protection = initializerProtection + + except Grid2OpException as e: + self.logger.error(f"Erreur spécifique à Grid2Op lors de l'initialisation de la protection : {e}") + raise + except Exception as e: + self.logger.exception("Erreur inattendue lors de l'initialisation de la protection.") + raise + @property def highres_sim_counter(self): return self._highres_sim_counter diff --git a/grid2op/tests/BaseBackendTest.py b/grid2op/tests/BaseBackendTest.py index ee8f8bc5..9b02c700 100644 --- a/grid2op/tests/BaseBackendTest.py +++ b/grid2op/tests/BaseBackendTest.py @@ -1637,7 +1637,7 @@ def next_grid_state_no_overflow(self): ) - disco, infos, conv_ = env.protection.next_grid_state() + disco, infos, conv_ = self.backend.next_grid_state(env, is_dc=False) assert conv_ is None assert not infos @@ -1667,10 +1667,9 @@ def test_next_grid_state_1overflow(self): thermal_limit[self.id_first_line_disco] = ( self.lines_flows_init[self.id_first_line_disco] / 2 ) - env.ts_manager.limits = thermal_limit - env._init_protection() - - disco, infos, conv_ = env.protection.next_grid_state() + self.backend.set_thermal_limit(thermal_limit) + + disco, infos, conv_ = self.backend.next_grid_state(env, is_dc=False) assert conv_ is None assert len(infos) == 1 # check that i have only one overflow assert np.sum(disco >= 0) == 1 @@ -1705,10 +1704,9 @@ def test_next_grid_state_1overflow_envNoCF(self): thermal_limit[self.id_first_line_disco] = ( lines_flows_init[self.id_first_line_disco] / 2 ) - env.ts_manager.limits = thermal_limit - env._init_protection() + self.backend.set_thermal_limit(thermal_limit) - disco, infos, conv_ = env.protection.next_grid_state() + disco, infos, conv_ = self.backend.next_grid_state(env, is_dc=False) assert conv_ is None assert not infos # check that don't simulate a cascading failure assert np.sum(disco >= 0) == 0 @@ -1752,10 +1750,9 @@ def test_nb_timestep_overflow_disc0(self): lines_flows_init[self.id_first_line_disco] / 2 ) thermal_limit[self.id_2nd_line_disco] = 400 - env.ts_manager.limits = thermal_limit - env._init_protection() + self.backend.set_thermal_limit(thermal_limit) - disco, infos, conv_ = env.protection.next_grid_state() + disco, infos, conv_ = self.backend.next_grid_state(env, is_dc=False) assert conv_ is None assert len(infos) == 2 # check that there is a cascading failure of length 2 assert disco[self.id_first_line_disco] >= 0 @@ -1796,10 +1793,9 @@ def test_nb_timestep_overflow_nodisc(self): self.lines_flows_init[self.id_first_line_disco] / 2 ) thermal_limit[self.id_2nd_line_disco] = 400 - env.ts_manager.limits = thermal_limit - env._init_protection() + self.backend.set_thermal_limit(thermal_limit) - disco, infos, conv_ = env.protection.next_grid_state() + disco, infos, conv_ = self.backend.next_grid_state(env, is_dc=False) assert conv_ is None assert len(infos) == 1 # check that don't simulate a cascading failure assert disco[self.id_first_line_disco] >= 0 @@ -1840,10 +1836,9 @@ def test_nb_timestep_overflow_nodisc_2(self): self.lines_flows_init[self.id_first_line_disco] / 2 ) thermal_limit[self.id_2nd_line_disco] = 400 - env.ts_manager.limits = thermal_limit - env._init_protection() + self.backend.set_thermal_limit(thermal_limit) - disco, infos, conv_ = env.protection.next_grid_state() + disco, infos, conv_ = self.backend.next_grid_state(env, is_dc=False) assert conv_ is None assert len(infos) == 1 # check that don't simulate a cascading failure assert disco[self.id_first_line_disco] >= 0 @@ -1884,10 +1879,9 @@ def test_nb_timestep_overflow_disc2(self): self.lines_flows_init[self.id_first_line_disco] / 2 ) thermal_limit[self.id_2nd_line_disco] = 400 - env.ts_manager.limits = thermal_limit - env._init_protection() + self.backend.set_thermal_limit(thermal_limit) - disco, infos, conv_ = env.protection.next_grid_state() + disco, infos, conv_ = self.backend.next_grid_state(env, is_dc=False) assert conv_ is None assert len(infos) == 2 # check that there is a cascading failure of length 2 assert disco[self.id_first_line_disco] >= 0 @@ -3224,4 +3218,4 @@ def test_chgtbus_prevDisc(self): self._line_disconnected(LINE_ID, obs) # right way to count it - self._only_sub_impacted(LINE_ID, action, statuses) + self._only_sub_impacted(LINE_ID, action, statuses) \ No newline at end of file diff --git a/grid2op/tests/test_protectionScheme.py b/grid2op/tests/test_protectionScheme.py new file mode 100644 index 00000000..b87d245c --- /dev/null +++ b/grid2op/tests/test_protectionScheme.py @@ -0,0 +1,142 @@ +import numpy as np + +import unittest +from unittest.mock import MagicMock + +from grid2op.Backend.backend import Backend +from grid2op.Parameters import Parameters +from grid2op.Exceptions import Grid2OpException + +from grid2op.Backend.thermalLimits import ThermalLimits +from grid2op.Backend.protectionScheme import DefaultProtection, NoProtection + +class TestProtection(unittest.TestCase): + + def setUp(self): + """Initialisation des mocks et des paramètres de test.""" + self.mock_backend = MagicMock(spec=Backend) + self.mock_parameters = MagicMock(spec=Parameters) + self.mock_thermal_limits = MagicMock(spec=ThermalLimits) + + # Définition des limites thermiques + self.mock_thermal_limits.limits = np.array([100.0, 200.0]) + self.mock_thermal_limits.n_line = 2 + + # Mock des valeurs de paramètre avec des valeurs par défaut + self.mock_parameters.SOFT_OVERFLOW_THRESHOLD = 1.0 + self.mock_parameters.HARD_OVERFLOW_THRESHOLD = 1.5 + self.mock_parameters.NB_TIMESTEP_OVERFLOW_ALLOWED = 3 + + # Comportement du backend + self.mock_backend.get_line_status.return_value = np.array([True, True]) + self.mock_backend.get_line_flow.return_value = np.array([90.0, 210.0]) + self.mock_backend._runpf_with_diverging_exception.return_value = None + + # Initialisation de la classe testée + self.default_protection = DefaultProtection( + backend=self.mock_backend, + parameters=self.mock_parameters, + thermal_limits=self.mock_thermal_limits, + is_dc=False + ) + + def test_initialization(self): + """Test de l'initialisation de DefaultProtection.""" + self.assertIsInstance(self.default_protection, DefaultProtection) + self.assertEqual(self.default_protection.is_dc, False) + self.assertIsNotNone(self.default_protection._parameters) + + def test_validate_input(self): + """Test de validation des entrées.""" + with self.assertRaises(Grid2OpException): + DefaultProtection(backend=None, parameters=self.mock_parameters) + + def test_run_power_flow(self): + """Test de l'exécution du flux de puissance.""" + result = self.default_protection._run_power_flow() + self.assertIsNone(result) + + def test_update_overflows(self): + """Test de la mise à jour des surcharges et des lignes à déconnecter.""" + lines_flows = np.array([120.0, 310.0]) + lines_to_disconnect = self.default_protection._update_overflows(lines_flows) + self.assertTrue(lines_to_disconnect[1]) # Seule la deuxième ligne doit être déconnectée + self.assertFalse(lines_to_disconnect[0]) + + def test_disconnect_lines(self): + """Test de la déconnexion des lignes.""" + lines_to_disconnect = np.array([False, True]) + self.default_protection._disconnect_lines(lines_to_disconnect, timestep=1) + self.mock_backend._disconnect_line.assert_called_once_with(1) + + def test_next_grid_state(self): + """Test de la simulation de l'évolution du réseau.""" + disconnected, infos, error = self.default_protection.next_grid_state() + self.assertIsInstance(disconnected, np.ndarray) + self.assertIsInstance(infos, list) + self.assertIsNone(error) + + def test_no_protection(self): + """Test de la classe NoProtection.""" + no_protection = NoProtection(self.mock_backend, self.mock_thermal_limits) + disconnected, infos, conv = no_protection.next_grid_state() + self.assertIsInstance(disconnected, np.ndarray) + self.assertIsInstance(infos, list) + self.assertIsNone(conv) + +class TestFunctionalProtection(unittest.TestCase): + + def setUp(self): + """Initialisation des mocks pour un test fonctionnel.""" + self.mock_backend = MagicMock(spec=Backend) + self.mock_parameters = MagicMock(spec=Parameters) + self.mock_thermal_limits = MagicMock(spec=ThermalLimits) + + # Configuration des limites thermiques et des flux de lignes + self.mock_thermal_limits.limits = np.array([100.0, 200.0]) + self.mock_thermal_limits.n_line = 2 + self.mock_parameters.SOFT_OVERFLOW_THRESHOLD = 1.0 + self.mock_parameters.HARD_OVERFLOW_THRESHOLD = 1.5 + self.mock_parameters.NB_TIMESTEP_OVERFLOW_ALLOWED = 3 + + # Comportement du backend pour simuler les flux de lignes + self.mock_backend.get_line_status.return_value = np.array([True, True]) + self.mock_backend.get_line_flow.return_value = np.array([90.0, 210.0]) + self.mock_backend._runpf_with_diverging_exception.return_value = None + + # Initialisation de la classe de protection avec des paramètres + self.default_protection = DefaultProtection( + backend=self.mock_backend, + parameters=self.mock_parameters, + thermal_limits=self.mock_thermal_limits, + is_dc=False + ) + + # Initialisation de NoProtection + self.no_protection = NoProtection( + backend=self.mock_backend, + thermal_limits=self.mock_thermal_limits, + is_dc=False + ) + + def test_functional_default_protection(self): + """Test fonctionnel pour DefaultProtection.""" + + self.mock_backend.get_line_flow.return_value = np.array([90.0, 210.0]) # Lignes avec un débordement + disconnected, infos, error = self.default_protection.next_grid_state() + + self.assertTrue(np.any(disconnected == -1)) # Ligne 1 doit être déconnectée + self.assertIsNone(error) + self.assertEqual(len(infos), 0) + + def test_functional_no_protection(self): + """Test fonctionnel pour NoProtection.""" + self.mock_backend.get_line_flow.return_value = np.array([90.0, 180.0]) # Aucune ligne en débordement + disconnected, infos, error = self.no_protection.next_grid_state() + + self.assertTrue(np.all(disconnected == -1)) # Aucune ligne ne doit être déconnectée + self.assertIsNone(error) + self.assertEqual(len(infos), 0) + +if __name__ == "__main__": + unittest.main() diff --git a/grid2op/tests/test_thermalLimits.py b/grid2op/tests/test_thermalLimits.py new file mode 100644 index 00000000..7883d303 --- /dev/null +++ b/grid2op/tests/test_thermalLimits.py @@ -0,0 +1,70 @@ +import unittest +import numpy as np +import logging +from grid2op.Backend.protectionScheme import ThermalLimits +from grid2op.Exceptions import Grid2OpException + +class TestThermalLimits(unittest.TestCase): + + def setUp(self): + """Initialisation avant chaque test.""" + self.logger = logging.getLogger("test_logger") + self.logger.disabled = True + + self.n_lines = 3 + self.line_names = ["Line1", "Line2", "Line3"] + self.thermal_limits = np.array([100.0, 200.0, 300.0]) + + self.thermal_limit_instance = ThermalLimits( + _thermal_limit_a=self.thermal_limits, + line_names=self.line_names, + n_line=self.n_lines, + logger=self.logger + ) + + def test_initialization(self): + """Test de l'initialisation de ThermalLimits.""" + self.assertEqual(self.thermal_limit_instance.n_line, self.n_lines) + self.assertEqual(self.thermal_limit_instance.name_line, self.line_names) + np.testing.assert_array_equal(self.thermal_limit_instance.limits, self.thermal_limits) + + def test_set_n_line(self): + """Test du setter de n_line.""" + self.thermal_limit_instance.n_line = 5 + self.assertEqual(self.thermal_limit_instance.n_line, 5) + + with self.assertRaises(ValueError): + self.thermal_limit_instance.n_line = -1 # Doit lever une erreur + + def test_set_name_line(self): + """Test du setter de name_line.""" + new_names = ["L4", "L5", "L6"] + self.thermal_limit_instance.name_line = new_names + self.assertEqual(self.thermal_limit_instance.name_line, new_names) + + with self.assertRaises(ValueError): + self.thermal_limit_instance.name_line = ["L4", 123, "L6"] # Doit lever une erreur + + def test_set_limits(self): + """Test du setter de limits avec np.array et dict.""" + new_limits = np.array([400.0, 500.0, 600.0]) + self.thermal_limit_instance.limits = new_limits + np.testing.assert_array_equal(self.thermal_limit_instance.limits, new_limits) + + limit_dict = {"Line1": 110.0, "Line2": 220.0, "Line3": 330.0} + self.thermal_limit_instance.limits = limit_dict + np.testing.assert_array_equal( + self.thermal_limit_instance.limits, np.array([110.0, 220.0, 330.0]) + ) + + with self.assertRaises(Grid2OpException): + self.thermal_limit_instance.limits = {"InvalidLine": 100.0} # Ligne inexistante + + def test_copy(self): + """Test de la méthode copy.""" + copied_instance = self.thermal_limit_instance.copy() + self.assertIsNot(copied_instance, self.thermal_limit_instance) + np.testing.assert_array_equal(copied_instance.limits, self.thermal_limit_instance.limits) + +if __name__ == "__main__": + unittest.main() From 97bb3b7bfef36b2bc17234424e996a316fe13947 Mon Sep 17 00:00:00 2001 From: yassine abdou Date: Wed, 2 Apr 2025 10:54:53 +0000 Subject: [PATCH 8/8] feat : add new test and translate to english Signed-off-by: yassine abdou --- grid2op/Backend/protectionScheme.py | 21 +++---- grid2op/tests/test_protectionScheme.py | 84 +++++++++++++++++--------- grid2op/tests/test_thermalLimits.py | 18 +++--- 3 files changed, 76 insertions(+), 47 deletions(-) diff --git a/grid2op/Backend/protectionScheme.py b/grid2op/Backend/protectionScheme.py index 9d9edbc8..863a3440 100644 --- a/grid2op/Backend/protectionScheme.py +++ b/grid2op/Backend/protectionScheme.py @@ -11,7 +11,7 @@ class DefaultProtection: """ - Classe avancée pour gérer les protections réseau et les déconnexions. + Advanced class to manage network protections and disconnections. """ def __init__( @@ -23,7 +23,7 @@ def __init__( logger: Optional[logging.Logger] = None, ): """ - Initialise l'état du réseau avec des protections personnalisables. + Initializes the network state with customizable protections. """ self.backend = backend self._parameters = copy.deepcopy(parameters) if parameters else Parameters() @@ -53,9 +53,9 @@ def __init__( def _validate_input(self, backend: Backend, parameters: Optional[Parameters]) -> None: if not isinstance(backend, Backend): - raise Grid2OpException(f"Argument 'backend' doit être de type 'Backend', reçu : {type(backend)}") + raise Grid2OpException(f"Argument 'backend' must be of type 'Backend', received: {type(backend)}") if parameters and not isinstance(parameters, Parameters): - raise Grid2OpException(f"Argument 'parameters' doit être de type 'Parameters', reçu : {type(parameters)}") + raise Grid2OpException(f"Argument 'parameters' must be of type 'Parameters', received: {type(parameters)}") def _get_value_from_parameters(self, parameter_name: str) -> Any: return getattr(self._parameters, parameter_name, None) @@ -66,7 +66,7 @@ def _run_power_flow(self) -> Optional[Exception]: except Exception as e: if self.logger is not None: self.logger.error( - f"Erreur flux de puissance : {e}" + f"Power flow error: {e}" ) return e @@ -78,7 +78,7 @@ def _update_overflows(self, lines_flows: np.ndarray) -> np.ndarray: ) raise ValueError("Thermal limits must be provided for overflow calculations.") - lines_status = self.backend.get_line_status() # self._thermal_limit_a reste fixe. self._soft_overflow_threshold = 1 + lines_status = self.backend.get_line_status() # self._thermal_limit_a remains fixed. self._soft_overflow_threshold = 1 is_overflowing = (lines_flows >= self._thermal_limit_a * self._soft_overflow_threshold) & lines_status self._timestep_overflow[is_overflowing] += 1 # self._hard_overflow_threshold = 1.5 @@ -93,8 +93,7 @@ def _disconnect_lines(self, lines_to_disconnect: np.ndarray, timestep: int) -> N self.backend._disconnect_line(line_idx) self.disconnected_during_cf[line_idx] = timestep if self.logger is not None: - self.logger.warning(f"Ligne {line_idx} déconnectée au pas de temps {timestep}.") - + self.logger.warning(f"Line {line_idx} disconnected at timestep {timestep}.") def next_grid_state(self) -> Tuple[np.ndarray, List[Any], Union[None, Exception]]: try: @@ -117,19 +116,19 @@ def next_grid_state(self) -> Tuple[np.ndarray, List[Any], Union[None, Exception] except Exception as e: if self.logger is not None: - self.logger.exception("Erreur inattendue dans le calcul de l'état du réseau.") + self.logger.exception("Unexpected error in calculating the network state.") return self.disconnected_during_cf, self.infos, e class NoProtection(DefaultProtection): """ - Classe qui désactive les protections de débordement tout en conservant la structure de DefaultProtection. + Class that disables overflow protections while keeping the structure of DefaultProtection. """ def __init__(self, backend: Backend, thermal_limits: ThermalLimits, is_dc: bool = False): super().__init__(backend, parameters=None, thermal_limits=thermal_limits, is_dc=is_dc) def next_grid_state(self) -> Tuple[np.ndarray, List[Any], None]: """ - Ignore les protections et retourne l'état du réseau sans déconnexions. + Ignores protections and returns the network state without disconnections. """ return self.disconnected_during_cf, self.infos, self.conv_ diff --git a/grid2op/tests/test_protectionScheme.py b/grid2op/tests/test_protectionScheme.py index b87d245c..8cdb53cc 100644 --- a/grid2op/tests/test_protectionScheme.py +++ b/grid2op/tests/test_protectionScheme.py @@ -13,71 +13,102 @@ class TestProtection(unittest.TestCase): def setUp(self): - """Initialisation des mocks et des paramètres de test.""" + """Initialization of mocks and test parameters.""" self.mock_backend = MagicMock(spec=Backend) self.mock_parameters = MagicMock(spec=Parameters) self.mock_thermal_limits = MagicMock(spec=ThermalLimits) - # Définition des limites thermiques + # Define thermal limits self.mock_thermal_limits.limits = np.array([100.0, 200.0]) self.mock_thermal_limits.n_line = 2 - # Mock des valeurs de paramètre avec des valeurs par défaut + # Default parameters self.mock_parameters.SOFT_OVERFLOW_THRESHOLD = 1.0 self.mock_parameters.HARD_OVERFLOW_THRESHOLD = 1.5 self.mock_parameters.NB_TIMESTEP_OVERFLOW_ALLOWED = 3 - # Comportement du backend + # Backend behavior self.mock_backend.get_line_status.return_value = np.array([True, True]) - self.mock_backend.get_line_flow.return_value = np.array([90.0, 210.0]) + self.mock_backend.get_line_flow.return_value = np.array([90.0, 210.0]) # One line is overflowing self.mock_backend._runpf_with_diverging_exception.return_value = None - # Initialisation de la classe testée + # Initialize the DefaultProtection class self.default_protection = DefaultProtection( backend=self.mock_backend, parameters=self.mock_parameters, thermal_limits=self.mock_thermal_limits, is_dc=False ) + """ + soft overflow" : with two expected behaviour + either the line is on overflow for less than "NB_TIMESTEP_OVERFLOW_ALLOWED" (or a similar name not checked in the code) in this case the only consquence is that the overflow counter is increased by 1 + or the line has been on overflow for more than "NB_TIMESTEP_OVERFLOW_ALLOWED" in this case the line is disconnected + """ + def test_update_overflows_soft(self): + """Test for soft overflow.""" + lines_flows = np.array([90.0, 210.0]) + lines_to_disconnect = self.default_protection._update_overflows(lines_flows) + self.assertFalse(lines_to_disconnect[0]) # No disconnection for the first line + self.assertFalse(lines_to_disconnect[1]) # No disconnection yet, as the overflow is soft + + # il faut se relancer une deuxieme fois et une troiseieme fois pour le deconnecté =3 + + def test_update_overflows_hard(self): + """Test for hard overflow.""" + lines_flows = np.array([120.0, 310.0]) + lines_to_disconnect = self.default_protection._update_overflows(lines_flows) + self.assertFalse(lines_to_disconnect[0]) # The first line should not be disconnected + self.assertTrue(lines_to_disconnect[1]) # The second line should be disconnected + + def test_overflow_counter_increase(self): + """Test that the overflow counter does not exceed 1 per call.""" + self.mock_backend.get_line_flow.return_value = np.array([90.0, 210.0]) + self.default_protection._update_overflows(np.array([90.0, 210.0])) + self.assertEqual(self.default_protection._timestep_overflow[1], 1) # The overflow counter for line 1 should be 1 + + # Next call with different flow + self.mock_backend.get_line_flow.return_value = np.array([90.0, 220.0]) + self.default_protection._update_overflows(np.array([90.0, 220.0])) + self.assertEqual(self.default_protection._timestep_overflow[1], 2) # The overflow counter for line 1 should be 2 def test_initialization(self): - """Test de l'initialisation de DefaultProtection.""" + """Test the initialization of DefaultProtection.""" self.assertIsInstance(self.default_protection, DefaultProtection) self.assertEqual(self.default_protection.is_dc, False) self.assertIsNotNone(self.default_protection._parameters) def test_validate_input(self): - """Test de validation des entrées.""" + """Test input validation.""" with self.assertRaises(Grid2OpException): DefaultProtection(backend=None, parameters=self.mock_parameters) def test_run_power_flow(self): - """Test de l'exécution du flux de puissance.""" + """Test running the power flow.""" result = self.default_protection._run_power_flow() self.assertIsNone(result) def test_update_overflows(self): - """Test de la mise à jour des surcharges et des lignes à déconnecter.""" + """Test updating overflows and lines to disconnect.""" lines_flows = np.array([120.0, 310.0]) lines_to_disconnect = self.default_protection._update_overflows(lines_flows) - self.assertTrue(lines_to_disconnect[1]) # Seule la deuxième ligne doit être déconnectée + self.assertTrue(lines_to_disconnect[1]) # Only the second line should be disconnected self.assertFalse(lines_to_disconnect[0]) def test_disconnect_lines(self): - """Test de la déconnexion des lignes.""" + """Test disconnecting lines.""" lines_to_disconnect = np.array([False, True]) self.default_protection._disconnect_lines(lines_to_disconnect, timestep=1) self.mock_backend._disconnect_line.assert_called_once_with(1) def test_next_grid_state(self): - """Test de la simulation de l'évolution du réseau.""" + """Test simulating the network's evolution.""" disconnected, infos, error = self.default_protection.next_grid_state() self.assertIsInstance(disconnected, np.ndarray) self.assertIsInstance(infos, list) self.assertIsNone(error) def test_no_protection(self): - """Test de la classe NoProtection.""" + """Test the NoProtection class.""" no_protection = NoProtection(self.mock_backend, self.mock_thermal_limits) disconnected, infos, conv = no_protection.next_grid_state() self.assertIsInstance(disconnected, np.ndarray) @@ -87,24 +118,24 @@ def test_no_protection(self): class TestFunctionalProtection(unittest.TestCase): def setUp(self): - """Initialisation des mocks pour un test fonctionnel.""" + """Initialization of mocks for a functional test.""" self.mock_backend = MagicMock(spec=Backend) self.mock_parameters = MagicMock(spec=Parameters) self.mock_thermal_limits = MagicMock(spec=ThermalLimits) - # Configuration des limites thermiques et des flux de lignes + # Set thermal limits and line flows self.mock_thermal_limits.limits = np.array([100.0, 200.0]) self.mock_thermal_limits.n_line = 2 self.mock_parameters.SOFT_OVERFLOW_THRESHOLD = 1.0 self.mock_parameters.HARD_OVERFLOW_THRESHOLD = 1.5 self.mock_parameters.NB_TIMESTEP_OVERFLOW_ALLOWED = 3 - # Comportement du backend pour simuler les flux de lignes + # Backend behavior to simulate line flows self.mock_backend.get_line_status.return_value = np.array([True, True]) self.mock_backend.get_line_flow.return_value = np.array([90.0, 210.0]) self.mock_backend._runpf_with_diverging_exception.return_value = None - # Initialisation de la classe de protection avec des paramètres + # Initialize protection class with parameters self.default_protection = DefaultProtection( backend=self.mock_backend, parameters=self.mock_parameters, @@ -112,7 +143,7 @@ def setUp(self): is_dc=False ) - # Initialisation de NoProtection + # Initialize NoProtection self.no_protection = NoProtection( backend=self.mock_backend, thermal_limits=self.mock_thermal_limits, @@ -120,23 +151,22 @@ def setUp(self): ) def test_functional_default_protection(self): - """Test fonctionnel pour DefaultProtection.""" - - self.mock_backend.get_line_flow.return_value = np.array([90.0, 210.0]) # Lignes avec un débordement + """Functional test for DefaultProtection.""" + self.mock_backend.get_line_flow.return_value = np.array([90.0, 210.0]) # Lines with overflow disconnected, infos, error = self.default_protection.next_grid_state() - self.assertTrue(np.any(disconnected == -1)) # Ligne 1 doit être déconnectée + self.assertTrue(np.any(disconnected == -1)) # Line 1 should be disconnected self.assertIsNone(error) self.assertEqual(len(infos), 0) def test_functional_no_protection(self): - """Test fonctionnel pour NoProtection.""" - self.mock_backend.get_line_flow.return_value = np.array([90.0, 180.0]) # Aucune ligne en débordement + """Functional test for NoProtection.""" + self.mock_backend.get_line_flow.return_value = np.array([90.0, 180.0]) # No lines overflowing disconnected, infos, error = self.no_protection.next_grid_state() - self.assertTrue(np.all(disconnected == -1)) # Aucune ligne ne doit être déconnectée + self.assertTrue(np.all(disconnected == -1)) # No line should be disconnected self.assertIsNone(error) self.assertEqual(len(infos), 0) if __name__ == "__main__": - unittest.main() + unittest.main() \ No newline at end of file diff --git a/grid2op/tests/test_thermalLimits.py b/grid2op/tests/test_thermalLimits.py index 7883d303..1ed2033f 100644 --- a/grid2op/tests/test_thermalLimits.py +++ b/grid2op/tests/test_thermalLimits.py @@ -7,7 +7,7 @@ class TestThermalLimits(unittest.TestCase): def setUp(self): - """Initialisation avant chaque test.""" + """Initialization before each test.""" self.logger = logging.getLogger("test_logger") self.logger.disabled = True @@ -23,30 +23,30 @@ def setUp(self): ) def test_initialization(self): - """Test de l'initialisation de ThermalLimits.""" + """Test the initialization of ThermalLimits.""" self.assertEqual(self.thermal_limit_instance.n_line, self.n_lines) self.assertEqual(self.thermal_limit_instance.name_line, self.line_names) np.testing.assert_array_equal(self.thermal_limit_instance.limits, self.thermal_limits) def test_set_n_line(self): - """Test du setter de n_line.""" + """Test the setter for n_line.""" self.thermal_limit_instance.n_line = 5 self.assertEqual(self.thermal_limit_instance.n_line, 5) with self.assertRaises(ValueError): - self.thermal_limit_instance.n_line = -1 # Doit lever une erreur + self.thermal_limit_instance.n_line = -1 # Should raise an error def test_set_name_line(self): - """Test du setter de name_line.""" + """Test the setter for name_line.""" new_names = ["L4", "L5", "L6"] self.thermal_limit_instance.name_line = new_names self.assertEqual(self.thermal_limit_instance.name_line, new_names) with self.assertRaises(ValueError): - self.thermal_limit_instance.name_line = ["L4", 123, "L6"] # Doit lever une erreur + self.thermal_limit_instance.name_line = ["L4", 123, "L6"] # Should raise an error def test_set_limits(self): - """Test du setter de limits avec np.array et dict.""" + """Test the setter for limits with np.array and dict.""" new_limits = np.array([400.0, 500.0, 600.0]) self.thermal_limit_instance.limits = new_limits np.testing.assert_array_equal(self.thermal_limit_instance.limits, new_limits) @@ -58,10 +58,10 @@ def test_set_limits(self): ) with self.assertRaises(Grid2OpException): - self.thermal_limit_instance.limits = {"InvalidLine": 100.0} # Ligne inexistante + self.thermal_limit_instance.limits = {"InvalidLine": 100.0} # Non-existent line def test_copy(self): - """Test de la méthode copy.""" + """Test the copy method.""" copied_instance = self.thermal_limit_instance.copy() self.assertIsNot(copied_instance, self.thermal_limit_instance) np.testing.assert_array_equal(copied_instance.limits, self.thermal_limit_instance.limits)