Skip to content

Commit 3ebd690

Browse files
author
yassine abdou
committed
feat : first part (use defaultProtection) add protection schemes and thermal limit
1 parent 2a7caa0 commit 3ebd690

File tree

7 files changed

+579
-258
lines changed

7 files changed

+579
-258
lines changed

grid2op/Backend/backend.py

Lines changed: 190 additions & 189 deletions
Large diffs are not rendered by default.
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import copy
2+
from loguru import logger
3+
from typing import Tuple, Union, Any, List, Optional
4+
import numpy as np
5+
6+
from grid2op.dtypes import dt_int
7+
from grid2op.Backend.backend import Backend
8+
from grid2op.Parameters import Parameters
9+
from grid2op.Exceptions import Grid2OpException
10+
from grid2op.Backend.thermalLimits import ThermalLimits
11+
12+
class DefaultProtection:
13+
"""
14+
Classe avancée pour gérer les protections réseau et les déconnexions.
15+
"""
16+
17+
def __init__(
18+
self,
19+
backend: Backend,
20+
parameters: Parameters,
21+
thermal_limits: ThermalLimits,
22+
is_dc: bool = False,
23+
):
24+
"""
25+
Initialise l'état du réseau avec des protections personnalisables.
26+
"""
27+
self.backend = backend
28+
self._parameters = copy.deepcopy(parameters) if parameters else Parameters()
29+
self._validate_input(self.backend, self._parameters)
30+
31+
self.is_dc = is_dc
32+
self.thermal_limits = thermal_limits
33+
34+
self._thermal_limit_a = self.thermal_limits.limits if self.thermal_limits else None
35+
36+
self._hard_overflow_threshold = self._get_value_from_parameters("HARD_OVERFLOW_THRESHOLD")
37+
self._soft_overflow_threshold = self._get_value_from_parameters("SOFT_OVERFLOW_THRESHOLD")
38+
self._nb_timestep_overflow_allowed = self._get_value_from_parameters("NB_TIMESTEP_OVERFLOW_ALLOWED")
39+
self._no_overflow_disconnection = self._get_value_from_parameters("NO_OVERFLOW_DISCONNECTION")
40+
41+
self.disconnected_during_cf = np.full(self.thermal_limits.n_line, fill_value=-1, dtype=dt_int)
42+
self._timestep_overflow = np.zeros(self.thermal_limits.n_line, dtype=dt_int)
43+
self.infos: List[str] = []
44+
45+
def _validate_input(self, backend: Backend, parameters: Optional[Parameters]) -> None:
46+
if not isinstance(backend, Backend):
47+
raise Grid2OpException(f"Argument 'backend' doit être de type 'Backend', reçu : {type(backend)}")
48+
if parameters and not isinstance(parameters, Parameters):
49+
raise Grid2OpException(f"Argument 'parameters' doit être de type 'Parameters', reçu : {type(parameters)}")
50+
51+
def _get_value_from_parameters(self, parameter_name: str) -> Any:
52+
return getattr(self._parameters, parameter_name, None)
53+
54+
def _run_power_flow(self) -> Optional[Exception]:
55+
try:
56+
return self.backend._runpf_with_diverging_exception(self.is_dc)
57+
except Exception as e:
58+
logger.error(f"Erreur flux de puissance : {e}")
59+
return e
60+
61+
def _update_overflows(self, lines_flows: np.ndarray) -> np.ndarray:
62+
if self._thermal_limit_a is None:
63+
logger.error("Thermal limits must be provided for overflow calculations.")
64+
raise ValueError("Thermal limits must be provided for overflow calculations.")
65+
66+
lines_status = self.backend.get_line_status() # self._thermal_limit_a reste fixe. self._soft_overflow_threshold = 1
67+
is_overflowing = (lines_flows >= self._thermal_limit_a * self._soft_overflow_threshold) & lines_status
68+
self._timestep_overflow[is_overflowing] += 1
69+
# self._hard_overflow_threshold = 1.5
70+
exceeds_hard_limit = (lines_flows > self._thermal_limit_a * self._hard_overflow_threshold) & lines_status
71+
exceeds_allowed_time = self._timestep_overflow > self._nb_timestep_overflow_allowed
72+
73+
lines_to_disconnect = exceeds_hard_limit | (exceeds_allowed_time & lines_status)
74+
return lines_to_disconnect
75+
76+
def _disconnect_lines(self, lines_to_disconnect: np.ndarray, timestep: int) -> None:
77+
for line_idx in np.where(lines_to_disconnect)[0]:
78+
self.backend._disconnect_line(line_idx)
79+
self.disconnected_during_cf[line_idx] = timestep
80+
logger.warning(f"Ligne {line_idx} déconnectée au pas de temps {timestep}.")
81+
82+
def next_grid_state(self) -> Tuple[np.ndarray, List[Any], Union[None, Exception]]:
83+
try:
84+
if self._no_overflow_disconnection: # détaché cela d'ici et si on simule pas
85+
return self._handle_no_protection()
86+
87+
timestep = 0
88+
while True:
89+
power_flow_result = self._run_power_flow()
90+
if power_flow_result:
91+
return self.disconnected_during_cf, self.infos, power_flow_result
92+
93+
lines_flows = self.backend.get_line_flow()
94+
lines_to_disconnect = self._update_overflows(lines_flows)
95+
96+
if not lines_to_disconnect.any():
97+
break
98+
99+
self._disconnect_lines(lines_to_disconnect, timestep)
100+
timestep += 1
101+
102+
return self.disconnected_during_cf, self.infos, None
103+
104+
except Exception as e:
105+
logger.exception("Erreur inattendue dans le calcul de l'état du réseau.")
106+
return self.disconnected_during_cf, self.infos, e
107+
108+
def _handle_no_protection(self) -> Tuple[np.ndarray, List[Any], None]:
109+
no_protection = NoProtection(self.thermal_limits)
110+
return no_protection.handle_no_protection()
111+
112+
class NoProtection:
113+
"""
114+
Classe qui gère le cas où les protections de débordement sont désactivées.
115+
"""
116+
def __init__(
117+
self,
118+
thermal_limits: ThermalLimits
119+
):
120+
121+
self.thermal_limits = thermal_limits
122+
self.disconnected_during_cf = np.full(self.thermal_limits.n_line, fill_value=-1, dtype=dt_int)
123+
self.infos = []
124+
125+
def handle_no_protection(self) -> Tuple[np.ndarray, List[Any], None]:
126+
"""
127+
Retourne l'état du réseau sans effectuer de déconnexions dues aux débordements.
128+
"""
129+
return self.disconnected_during_cf, self.infos, None
130+
131+
class BlablaProtection:
132+
pass

grid2op/Backend/thermalLimits.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
from loguru import logger
2+
from typing import Union, List, Optional, Dict
3+
import numpy as np
4+
5+
import grid2op
6+
from grid2op.dtypes import dt_float
7+
from grid2op.Exceptions import Grid2OpException
8+
9+
class ThermalLimits:
10+
"""
11+
Class for managing the thermal limits of power grid lines.
12+
"""
13+
14+
def __init__(
15+
self,
16+
_thermal_limit_a: Optional[np.ndarray] = None,
17+
line_names: Optional[List[str]] = None,
18+
n_line: Optional[int] = None
19+
):
20+
"""
21+
Initializes the thermal limits manager.
22+
23+
:param thermal_limits: Optional[np.ndarray]
24+
Array of thermal limits for each power line. Must have the same length as the number of lines.
25+
:param line_names: Optional[List[str]]
26+
List of power line names.
27+
:param n_line: Optional[int]
28+
Number of lines (can be passed explicitly or inferred from `thermal_limits` or `line_names`).
29+
30+
:raises ValueError:
31+
If neither `thermal_limits` nor `n_line` and `line_names` are provided.
32+
"""
33+
if _thermal_limit_a is None and (n_line is None and line_names is None):
34+
raise ValueError("Must provide thermal_limits or both n_line and line_names.")
35+
36+
self._thermal_limit_a = _thermal_limit_a
37+
self._n_line = n_line
38+
self._name_line = line_names
39+
40+
logger.info(f"ThermalLimits initialized with {self.n_line} limits.")
41+
42+
@property
43+
def n_line(self) -> int:
44+
return self._n_line
45+
46+
@n_line.setter
47+
def n_line(self, new_n_line: int) -> None:
48+
if new_n_line <= 0:
49+
raise ValueError("Number of lines must be a positive integer.")
50+
self._n_line = new_n_line
51+
logger.info(f"Number of lines updated to {self._n_line}.")
52+
53+
@property
54+
def name_line(self) -> Union[List[str], np.ndarray]:
55+
return self._name_line
56+
57+
@name_line.setter
58+
def name_line(self, new_name_line: Union[List[str], np.ndarray]) -> None:
59+
if isinstance(new_name_line, np.ndarray):
60+
if not np.all([isinstance(name, str) for name in new_name_line]):
61+
raise ValueError("All elements in name_line must be strings.")
62+
elif isinstance(new_name_line, list):
63+
if not all(isinstance(name, str) for name in new_name_line):
64+
raise ValueError("All elements in name_line must be strings.")
65+
else:
66+
raise ValueError("Line names must be a list or numpy array of non-empty strings.")
67+
68+
if self._n_line is not None and len(new_name_line) != self._n_line:
69+
raise ValueError("Length of name list must match the number of lines.")
70+
71+
self._name_line = new_name_line
72+
logger.info(f"Power line names updated")
73+
74+
@property
75+
def limits(self) -> np.ndarray:
76+
"""
77+
Gets the current thermal limits of the power lines.
78+
79+
:return: np.ndarray
80+
The array containing thermal limits for each power line.
81+
"""
82+
return self._thermal_limit_a
83+
84+
@limits.setter
85+
def limits(self, new_limits: Union[np.ndarray, Dict[str, float]]):
86+
"""
87+
Sets new thermal limits.
88+
89+
:param new_limits: Union[np.ndarray, Dict[str, float]]
90+
Either a numpy array or a dictionary mapping line names to new thermal limits.
91+
92+
:raises ValueError:
93+
If the new limits array size does not match the number of lines.
94+
:raises Grid2OpException:
95+
If invalid power line names are provided in the dictionary.
96+
If the new thermal limit values are invalid (non-positive or non-convertible).
97+
:raises TypeError:
98+
If the input type is not an array or dictionary.
99+
"""
100+
if isinstance(new_limits, np.ndarray):
101+
if new_limits.shape[0] == self.n_line:
102+
self._thermal_limit_a = 1.0 * new_limits.astype(dt_float)
103+
elif isinstance(new_limits, dict):
104+
for el in new_limits.keys():
105+
if not el in self.name_line:
106+
raise Grid2OpException(
107+
'You asked to modify the thermal limit of powerline named "{}" that is not '
108+
"on the grid. Names of powerlines are {}".format(
109+
el, self.name_line
110+
)
111+
)
112+
for i, el in self.name_line:
113+
if el in new_limits:
114+
try:
115+
tmp = dt_float(new_limits[el])
116+
except Exception as exc_:
117+
raise Grid2OpException(
118+
'Impossible to convert data ({}) for powerline named "{}" into float '
119+
"values".format(new_limits[el], el)
120+
) from exc_
121+
if tmp <= 0:
122+
raise Grid2OpException(
123+
'New thermal limit for powerlines "{}" is not positive ({})'
124+
"".format(el, tmp)
125+
)
126+
self._thermal_limit_a[i] = tmp
127+
128+
def env_limits(self, thermal_limit):
129+
if isinstance(thermal_limit, dict):
130+
tmp = np.full(self.n_line, fill_value=np.NaN, dtype=dt_float)
131+
for key, val in thermal_limit.items():
132+
if key not in self.name_line:
133+
raise Grid2OpException(
134+
f"When setting a thermal limit with a dictionary, the keys should be line "
135+
f"names. We found: {key} which is not a line name. The names of the "
136+
f"powerlines are {self.name_line}"
137+
)
138+
ind_line = (self.name_line == key).nonzero()[0][0]
139+
if np.isfinite(tmp[ind_line]):
140+
raise Grid2OpException(
141+
f"Humm, there is a really strange bug, some lines are set twice."
142+
)
143+
try:
144+
val_fl = float(val)
145+
except Exception as exc_:
146+
raise Grid2OpException(
147+
f"When setting thermal limit with a dictionary, the keys should be "
148+
f"the values of the thermal limit (in amps) you provided something that "
149+
f'cannot be converted to a float. Error was "{exc_}".'
150+
)
151+
tmp[ind_line] = val_fl
152+
153+
elif isinstance(thermal_limit, (np.ndarray, list)):
154+
try:
155+
tmp = np.array(thermal_limit).flatten().astype(dt_float)
156+
except Exception as exc_:
157+
raise Grid2OpException(
158+
f"Impossible to convert the vector as input into a 1d numpy float array. "
159+
f"Error was: \n {exc_}"
160+
)
161+
if tmp.shape[0] != self.n_line:
162+
raise Grid2OpException(
163+
"Attempt to set thermal limit on {} powerlines while there are {}"
164+
"on the grid".format(tmp.shape[0], self.n_line)
165+
)
166+
if (~np.isfinite(tmp)).any():
167+
raise Grid2OpException(
168+
"Impossible to use non finite value for thermal limits."
169+
)
170+
else:
171+
raise Grid2OpException(
172+
f"You can only set the thermal limits of the environment with a dictionary (in that "
173+
f"case the keys are the line names, and the values the thermal limits) or with "
174+
f"a numpy array that has as many components of the number of powerlines on "
175+
f'the grid. You provided something with type "{type(thermal_limit)}" which '
176+
f"is not supported."
177+
)
178+
179+
self._thermal_limit_a = tmp
180+
logger.info("Env thermal limits successfully set.")
181+
182+
def update_limits(self, thermal_limit_a: np.ndarray) -> None:
183+
"""
184+
Updates the thermal limits using a numpy array.
185+
186+
:param thermal_limit_a: np.ndarray
187+
The new array of thermal limits (in Amperes).
188+
"""
189+
thermal_limit_a = np.array(thermal_limit_a).astype(dt_float)
190+
self._thermal_limit_a = thermal_limit_a
191+
logger.info("Thermal limits updated from vector.")

grid2op/Environment/_obsEnv.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ def __init__(
115115

116116
self.current_obs_init = None
117117
self.current_obs = None
118+
119+
self._init_thermal_limit()
120+
118121
self._init_backend(
119122
chronics_handler=_ObsCH(),
120123
backend=backend_instanciated,
@@ -125,6 +128,8 @@ def __init__(
125128
legalActClass=legalActClass,
126129
)
127130

131+
self.ts_manager = copy.deepcopy(self._observation_space.ts_manager)
132+
128133
self.delta_time_seconds = delta_time_seconds
129134
####
130135
# to be able to save and import (using env.generate_classes) correctly
@@ -193,9 +198,9 @@ def _init_backend(
193198
self._game_rules.initialize(self)
194199
self._legalActClass = legalActClass
195200

196-
# self._action_space = self._do_nothing
197-
self.backend.set_thermal_limit(self._thermal_limit_a)
198-
201+
# # self._action_space = self._do_nothing
202+
self.ts_manager.limits = self._thermal_limit_a # old code line : self.backend.set_thermal_limit(self._thermal_limit_a)
203+
199204
from grid2op.Observation import ObservationSpace
200205
from grid2op.Reward import FlatReward
201206
ob_sp_cls = ObservationSpace.init_grid(type(backend), _local_dir_cls=self._local_dir_cls)
@@ -207,7 +212,6 @@ def _init_backend(
207212
_local_dir_cls=self._local_dir_cls
208213
)
209214
self._observationClass = self._observation_space.subtype # not used anyway
210-
211215
# create the opponent
212216
self._create_opponent()
213217

0 commit comments

Comments
 (0)