Skip to content

Commit 0ce7b57

Browse files
committed
fix an issue with time overcurrent protection in case soft_overflow_threshold was not 1
Signed-off-by: DONNOT Benjamin <[email protected]>
1 parent 84a4a4c commit 0ce7b57

File tree

11 files changed

+287
-30
lines changed

11 files changed

+287
-30
lines changed

CHANGELOG.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,10 @@ Native multi agents support:
159159
- [FIXED] an issue when computing the cascading failure routine, in case multiple iterations were performed,
160160
the cooldowns were not updated correctly.
161161
- [FIXED] cascading failure could be started at the first observation (t=0, just after a reset).
162+
- [FIXED] a bug when "SOFT_OVERFLOW_THRESHOLD" was not 1.: it also impacted "instantaneous overcurrent protections"
163+
(it was triggered when `flow > SOFT_OVERFLOW_THRESHOLD * HARD_OVERFLOW_THRESHOLD * th_lim`)
164+
- [FIXED] a bug when "SOFT_OVERFLOW_THRESHOLD" was not 1.: the backend routine to compute the protections
165+
disconnected the lines with a counter based on `flow > th_lim` and not `flow > th_lim * SOFT_OVERFLOW_THRESHOLD`
162166
- [ADDED] Possibility to disconnect loads, generators and storage units (if proper flag set in the environment).
163167
See documentation.
164168
- [ADDED] possibility to set the "thermal limits" when calling `env.reset(..., options={"thermal limit": xxx})`
@@ -177,6 +181,9 @@ Native multi agents support:
177181
kwargs by using `env.reset(..., options={"init datetime": XXX})`
178182
- [ADDED] the `ChangeNothing` time series class now supports forecast
179183
- [ADDED] test coverage on the CI
184+
- [ADDED] the `obs.timestep_protection_triggered` counter which counts whether or not the
185+
"time overcurrent protection" (soft overflow) will be triggered: lines will be disconnected
186+
if `time overcurrent protection > parameters.NB_TIMESTEP_POWERFLOW_ALLOWED`
180187
- [IMPROVED] possibility to set the injections values with names
181188
to be consistent with other way to set the actions (*eg* set_bus)
182189
- [IMPROVED] error messages when creating an action which changes the injections
@@ -209,6 +216,7 @@ Native multi agents support:
209216
- [IMPROVED] no need to call `self._compute_pos_big_top()` at the end of the implementation of `backend.load_grid()`
210217
- [IMPROVED] type hints in various files.
211218
- [IMPROVED] documentation of the backend
219+
- [IMRPOVED] `SOFT_OVERFLOW_THRESHOLD` can now be lower than 1
212220

213221
[1.10.5] - 2025-03-07
214222
------------------------

docs/mdp.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -654,7 +654,7 @@ The solver is then called and there are 2 alternatives (again):
654654
terminal state :math:`s_{\emptyset}` (ignore all the steps bellow)
655655
#. or a physical solution is found and the process carries out in the next steps
656656

657-
.. _mdp-protection-emulation-step:
657+
.. _mdp--emulation-step:
658658

659659
Step 5: Emulation of the "protections"
660660
++++++++++++++++++++++++++++++++++++++++++

grid2op/Backend/backend.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1459,13 +1459,13 @@ def next_grid_state(self,
14591459
return disconnected_during_cf, infos, conv_
14601460

14611461
# the environment disconnect some powerlines
1462-
init_time_step_overflow = copy.deepcopy(env._timestep_overflow)
1463-
counter_increased = np.zeros_like(init_time_step_overflow, dtype=dt_bool)
1462+
protection_counter = copy.deepcopy(env._protection_counter)
1463+
counter_increased = np.zeros_like(protection_counter, dtype=dt_bool)
14641464
iter_num = 0
14651465
while True:
14661466
# simulate the cascading failure
14671467
lines_flows = 1.0 * self.get_line_flow()
1468-
thermal_limits = self.get_thermal_limit() * env._parameters.SOFT_OVERFLOW_THRESHOLD # SOFT_OVERFLOW_THRESHOLD new in grid2op 1.9.3
1468+
thermal_limits = self.get_thermal_limit()
14691469
lines_status = self.get_line_status()
14701470

14711471
# a) disconnect lines on hard overflow (that are still connected)
@@ -1478,12 +1478,12 @@ def next_grid_state(self,
14781478
# no soft overflow after a reset
14791479
mask_inc = np.zeros_like(thermal_limits, dtype=dt_bool)
14801480
else:
1481-
mask_inc = (lines_flows >= thermal_limits) & lines_status
1481+
mask_inc = (lines_flows > env._parameters.SOFT_OVERFLOW_THRESHOLD * thermal_limits) & lines_status
14821482
mask_inc[counter_increased] = False
1483-
init_time_step_overflow[mask_inc] += 1
1483+
protection_counter[mask_inc] += 1
14841484
counter_increased[mask_inc] = True
14851485
to_disc[
1486-
(init_time_step_overflow > env._nb_timestep_overflow_allowed)
1486+
(protection_counter > env._nb_ts_max_protection_counter)
14871487
& lines_status
14881488
] = True
14891489

grid2op/Environment/_obsEnv.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,13 +364,13 @@ def init(
364364

365365
if time_step >= 1:
366366
is_overflow = obs.rho > 1.
367-
367+
protection_triggered = obs.rho > self._parameters.SOFT_OVERFLOW_THRESHOLD
368368
# handle the components that depends on the time
369369
(
370370
still_in_maintenance,
371371
reconnected,
372372
first_ts_maintenance,
373-
) = self._update_vector_with_timestep(time_step, is_overflow)
373+
) = self._update_vector_with_timestep(time_step, is_overflow, protection_triggered)
374374
if first_ts_maintenance.any():
375375
set_status = np.array(self._line_status_me, dtype=dt_int)
376376
set_status[first_ts_maintenance] = -1

grid2op/Environment/baseEnv.py

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,16 @@ def foo(manager):
226226
227227
Number of consecutive timesteps each powerline has been on overflow.
228228
229-
_nb_timestep_overflow_allowed: ``numpy.ndarray``, dtype: int
229+
_protection_counter: `numpy.ndarray``, dtype: int
230+
.. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\
231+
232+
Current state of the delayed protection. It is exacly :attr:`BaseEnv._timestep_overflow` unless
233+
:attr:`grid2op.Parameters.Parameters.SOFT_OVERFLOW_THRESHOLD` != 1.
234+
235+
If the soft overflow threshold is different than 1, it counts the number of steps
236+
since the soft overflow threshold is "activated" (flow > limits * soft_overflow_threshold)
237+
238+
_nb_ts_max_protection_counter: ``numpy.ndarray``, dtype: int
230239
.. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\
231240
232241
Number of consecutive timestep each powerline can be on overflow. It is usually read from
@@ -454,7 +463,8 @@ def __init__(
454463
self._parameters.NO_OVERFLOW_DISCONNECTION
455464
)
456465
self._timestep_overflow: np.ndarray = None
457-
self._nb_timestep_overflow_allowed: np.ndarray = None
466+
self._protection_counter: np.ndarray = None
467+
self._nb_ts_max_protection_counter: np.ndarray = None
458468
self._hard_overflow_threshold: np.ndarray = None
459469

460470
# store actions "cooldown"
@@ -767,8 +777,9 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None):
767777
# if True, then it will not disconnect lines above their thermal limits
768778
new_obj._no_overflow_disconnection = self._no_overflow_disconnection
769779
new_obj._timestep_overflow = copy.deepcopy(self._timestep_overflow)
770-
new_obj._nb_timestep_overflow_allowed = copy.deepcopy(
771-
self._nb_timestep_overflow_allowed
780+
new_obj._protection_counter = copy.deepcopy(self._protection_counter)
781+
new_obj._nb_ts_max_protection_counter = copy.deepcopy(
782+
self._nb_ts_max_protection_counter
772783
)
773784
new_obj._hard_overflow_threshold = copy.deepcopy(self._hard_overflow_threshold)
774785

@@ -1417,7 +1428,7 @@ def _has_been_initialized(self):
14171428
self._times_before_topology_actionable = np.zeros(
14181429
shape=(bk_type.n_sub,), dtype=dt_int
14191430
)
1420-
self._nb_timestep_overflow_allowed = np.full(
1431+
self._nb_ts_max_protection_counter = np.full(
14211432
shape=(bk_type.n_line,),
14221433
fill_value=self._parameters.NB_TIMESTEP_OVERFLOW_ALLOWED,
14231434
dtype=dt_int,
@@ -1428,6 +1439,7 @@ def _has_been_initialized(self):
14281439
dtype=dt_float,
14291440
)
14301441
self._timestep_overflow = np.zeros(shape=(bk_type.n_line,), dtype=dt_int)
1442+
self._protection_counter = np.zeros(shape=(bk_type.n_line,), dtype=dt_int)
14311443

14321444
# update the parameters
14331445
self.__new_param = self._parameters # small hack to have it working as expected
@@ -1531,7 +1543,7 @@ def _update_parameters(self):
15311543
)
15321544
self._nb_ts_reco = self._parameters.NB_TIMESTEP_RECONNECTION
15331545

1534-
self._nb_timestep_overflow_allowed[
1546+
self._nb_ts_max_protection_counter[
15351547
:
15361548
] = self._parameters.NB_TIMESTEP_OVERFLOW_ALLOWED
15371549
self._hard_overflow_threshold[:] = self._parameters.HARD_OVERFLOW_THRESHOLD
@@ -3267,6 +3279,7 @@ def _aux_register_env_converged(self,
32673279
# update the thermal limit, for DLR for example
32683280
self.backend.update_thermal_limit(self)
32693281
overflow_lines = self.backend.get_line_overflow()
3282+
current_flows = self.backend.get_line_flow()
32703283
# save the current topology as "last" topology (for connected powerlines)
32713284
# and update the state of the disconnected powerline due to cascading failure
32723285
self._backend_action.update_state(disc_lines)
@@ -3285,6 +3298,11 @@ def _aux_register_env_converged(self,
32853298

32863299
# set to 0 the number of timestep for lines that are not on overflow
32873300
self._timestep_overflow[~overflow_lines] = 0
3301+
3302+
# update protection counter
3303+
engaged_protection = current_flows > self.backend.get_thermal_limit() * self._parameters.SOFT_OVERFLOW_THRESHOLD
3304+
self._protection_counter[engaged_protection] += 1
3305+
self._protection_counter[~engaged_protection] = 0
32883306

32893307
# build the topological action "cooldown"
32903308
aff_lines, aff_subs = action.get_topological_impact(_read_from_cache=True)
@@ -3835,7 +3853,8 @@ def _reset_vectors_and_timings(self):
38353853
"""
38363854
self._no_overflow_disconnection = self._parameters.NO_OVERFLOW_DISCONNECTION
38373855
self._timestep_overflow[:] = 0
3838-
self._nb_timestep_overflow_allowed[
3856+
self._protection_counter[:] = 0
3857+
self._nb_ts_max_protection_counter[
38393858
:
38403859
] = self._parameters.NB_TIMESTEP_OVERFLOW_ALLOWED
38413860

@@ -3990,7 +4009,8 @@ def close(self):
39904009
"_forbid_dispatch_off",
39914010
"_no_overflow_disconnection",
39924011
"_timestep_overflow",
3993-
"_nb_timestep_overflow_allowed",
4012+
"_protection_counter",
4013+
"_nb_ts_max_protection_counter",
39944014
"_hard_overflow_threshold",
39954015
"_times_before_line_status_actionable",
39964016
"_max_timestep_line_status_deactivated",
@@ -4615,7 +4635,7 @@ def __del__(self):
46154635
if hasattr(self, "_BaseEnv__closed") and not self.__closed:
46164636
self.close()
46174637

4618-
def _update_vector_with_timestep(self, horizon, is_overflow):
4638+
def _update_vector_with_timestep(self, horizon, is_overflow, protection_triggered):
46194639
"""
46204640
INTERNAL
46214641
@@ -4671,9 +4691,10 @@ def _update_vector_with_timestep(self, horizon, is_overflow):
46714691
# this is tricky here because I have no model to predict the future...
46724692
# As i cannot do better, I simply do "if I am in overflow now, i will be later"
46734693
self._timestep_overflow[is_overflow] += (horizon - 1)
4694+
self._protection_counter[protection_triggered] += (horizon - 1)
46744695
return still_in_maintenance, reconnected, first_ts_maintenance
46754696

4676-
def _reset_to_orig_state(self, obs):
4697+
def _reset_to_orig_state(self, obs: BaseObservation):
46774698
"""
46784699
INTERNAL
46794700
@@ -4736,6 +4757,7 @@ def _reset_to_orig_state(self, obs):
47364757

47374758
# soft overflow
47384759
self._timestep_overflow[:] = obs.timestep_overflow
4760+
self._protection_counter[:] = obs.timestep_protection_engaged
47394761

47404762
def forecasts(self):
47414763
# ensure that the "env.chronics_handler.forecasts" is called at most once per step

grid2op/Environment/maskedEnvironment.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from grid2op.Exceptions import EnvError
1717
from grid2op.dtypes import dt_bool, dt_float, dt_int
1818
from grid2op.Space import DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_DETACHMENT
19-
from grid2op.MakeEnv.PathUtils import USE_CLASS_IN_FILE
2019

2120

2221
class MaskedEnvironment(Environment):
@@ -108,7 +107,7 @@ def _make_lines_of_interest(self, lines_of_interest):
108107
def _reset_vectors_and_timings(self):
109108
super()._reset_vectors_and_timings()
110109
self._hard_overflow_threshold[~self._lines_of_interest] = type(self).INF_VAL_THM_LIM
111-
self._nb_timestep_overflow_allowed[~self._lines_of_interest] = type(self).INF_VAL_TS_OVERFLOW_ALLOW
110+
self._nb_ts_max_protection_counter[~self._lines_of_interest] = type(self).INF_VAL_TS_OVERFLOW_ALLOW
112111

113112
def get_kwargs(self, with_backend=True, with_chronics_handler=True):
114113
res = {}

grid2op/Episode/EpisodeReboot.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from grid2op.Chronics import GridValue, ChronicsHandler
2121
from grid2op.Opponent import BaseOpponent
2222
from grid2op.Environment import Environment
23+
from grid2op.Observation import BaseObservation
2324

2425
from grid2op.Episode.EpisodeData import EpisodeData
2526

@@ -212,7 +213,7 @@ def load(self, backend, agent_path=None, name=None, data=None, env_kwargs={}):
212213
self._assign_state(current_obs)
213214
return self.env.get_obs()
214215

215-
def _assign_state(self, obs):
216+
def _assign_state(self, obs: BaseObservation):
216217
"""
217218
works only if observation store the complete state of the grid...
218219
"""
@@ -229,6 +230,7 @@ def _assign_state(self, obs):
229230
) + obs.actual_dispatch.astype(dt_float)
230231
self.env.current_obs = obs
231232
self.env._timestep_overflow[:] = obs.timestep_overflow.astype(dt_int)
233+
self.env._protection_counter[:] = obs.timestep_protection_engaged.astype(dt_int)
232234
self.env._times_before_line_status_actionable[
233235
:
234236
] = obs.time_before_cooldown_line.astype(dt_int)

grid2op/Observation/baseObservation.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,16 @@ class BaseObservation(GridObjects):
163163
timestep_overflow: :class:`numpy.ndarray`, dtype:int
164164
Gives the number of time steps since a powerline is in overflow.
165165
166+
timestep_protection_engaged: :class:`numpy.ndarray`, dtype:int
167+
.. versionadded:: 1.11.0
168+
169+
This counts the number of consecutive steps the "Time overcurrent protection" is too high.
170+
171+
It is exacly :attr:`BaseObservation.timestep_overflow` unless the parameter
172+
:attr:`grid2op.Parameters.Parameters.SOFT_OVERFLOW_THRESHOLD` != 1.
173+
174+
In that case it counts the consecutive steps for which `flow > limit * SOFT_OVERFLOW_THRESHOLD`
175+
166176
time_before_cooldown_line: :class:`numpy.ndarray`, dtype:int
167177
For each powerline, it gives the number of time step the powerline is unavailable due to "cooldown"
168178
(see :attr:`grid2op.Parameters.Parameters.NB_TIMESTEP_COOLDOWN_LINE` for more information). 0 means the
@@ -436,7 +446,7 @@ class BaseObservation(GridObjects):
436446
gen_p_detached: :class:`numpy.ndarray`, dtype:float
437447
438448
storage_p_detached: :class:`numpy.ndarray`, dtype:float
439-
449+
440450
_shunt_p: :class:`numpy.ndarray`, dtype:float
441451
Shunt active value (only available if shunts are available) (in MW)
442452
@@ -516,6 +526,8 @@ class BaseObservation(GridObjects):
516526
"load_q_detached",
517527
"gen_p_detached",
518528
"storage_p_detached",
529+
# better handling of soft_overflow_threshold (>= 1.11.0)
530+
"timestep_protection_engaged",
519531
]
520532

521533
attr_list_vect = None
@@ -551,6 +563,7 @@ def __init__(self,
551563

552564
cls = type(self)
553565
self.timestep_overflow = np.empty(shape=(cls.n_line,), dtype=dt_int)
566+
self.timestep_protection_engaged = np.empty(shape=(cls.n_line,), dtype=dt_int)
554567

555568
# 0. (line is disconnected) / 1. (line is connected)
556569
self.line_status = np.empty(shape=cls.n_line, dtype=dt_bool)
@@ -744,6 +757,8 @@ def _aux_copy(self, other : Self) -> None:
744757
"load_q_detached",
745758
"gen_p_detached",
746759
"storage_p_detached",
760+
# soft_overflow_threshold
761+
"timestep_protection_engaged"
747762
]
748763

749764
if type(self).shunts_data_available:
@@ -1376,6 +1391,7 @@ def reset(self) -> None:
13761391
self.time_next_maintenance[:] = 0
13771392
self.duration_next_maintenance[:] = 0
13781393
self.timestep_overflow[:] = 0
1394+
self.timestep_protection_engaged[:] = 0
13791395

13801396
# calendar data
13811397
self.year = dt_int(1970)
@@ -1534,6 +1550,7 @@ def set_game_over(self,
15341550

15351551
# overflow
15361552
self.timestep_overflow[:] = 0
1553+
self.timestep_protection_engaged[:] = 0
15371554

15381555
if type(self).shunts_data_available:
15391556
self._shunt_p[:] = 0.0
@@ -2485,6 +2502,7 @@ def get_energy_graph(self) -> networkx.Graph:
24852502
- `cooldown`: the number of step you need to wait before being able to act on this powerline (max over all powerlines)
24862503
- `thermal_limit`: maximum flow allowed on the the powerline (sum over all powerlines)
24872504
- `timestep_overflow`: number of time steps during which the powerline is on overflow (max over all powerlines)
2505+
- `timestep_protection_engaged`: number of time steps during which the "time overcurrent protection" are triggered (new in version 1.10.0)
24882506
- `p_or`: active power injected at this node at the "origin side" (in MW) (sum over all the powerlines).
24892507
- `p_ex`: active power injected at this node at the "extremity side" (in MW) (sum over all the powerlines).
24902508
- `q_or`: reactive power injected at this node at the "origin side" (in MVAr) (sum over all the powerlines).
@@ -2690,6 +2708,10 @@ def get_energy_graph(self) -> networkx.Graph:
26902708
self.timestep_overflow, "timestep_overflow", lor_bus, lex_bus, graph,
26912709
fun_reduce=max
26922710
)
2711+
self._add_edges_simple(
2712+
self.timestep_protection_engaged, "timestep_protection_engaged", lor_bus, lex_bus, graph,
2713+
fun_reduce=max
2714+
)
26932715
self._add_edges_simple(
26942716
self.line_or_to_subid,
26952717
"sub_id_or", lor_bus, lex_bus, graph
@@ -2995,6 +3017,7 @@ def _aux_add_lines(self, graph, cls, first_id):
29953017
nodes_prop = [("rho", self.rho),
29963018
("connected", self.line_status),
29973019
("timestep_overflow", self.timestep_overflow),
3020+
("timestep_protection_engaged", self.timestep_protection_engaged),
29983021
("time_before_cooldown_line", self.time_before_cooldown_line),
29993022
("time_next_maintenance", self.time_next_maintenance),
30003023
("duration_next_maintenance", self.duration_next_maintenance),
@@ -3844,6 +3867,7 @@ def to_dict(self):
38443867
if self._dictionnarized is None:
38453868
self._dictionnarized = {}
38463869
self._dictionnarized["timestep_overflow"] = self.timestep_overflow
3870+
self._dictionnarized["timestep_protection_engaged"] = self.timestep_protection_engaged
38473871
self._dictionnarized["line_status"] = self.line_status
38483872
self._dictionnarized["topo_vect"] = self.topo_vect
38493873
self._dictionnarized["loads"] = {}
@@ -4398,6 +4422,7 @@ def _update_obs_complete(self, env: "grid2op.Environment.BaseEnv", with_forecast
43984422

43994423
# get the values related to topology
44004424
self.timestep_overflow[:] = env._timestep_overflow
4425+
self.timestep_protection_engaged[:] = env._protection_counter
44014426

44024427
# attribute that depends only on the backend state
44034428
self._update_attr_backend(env.backend)

grid2op/Parameters.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -624,11 +624,11 @@ def check_valid(self):
624624
raise RuntimeError(
625625
f'Impossible to convert SOFT_OVERFLOW_THRESHOLD to float with error \n:"{exc_}"'
626626
) from exc_
627-
if self.SOFT_OVERFLOW_THRESHOLD < 1.0:
628-
raise RuntimeError(
629-
"SOFT_OVERFLOW_THRESHOLD < 1., this should be >= 1. (use env.set_thermal_limit "
630-
"to modify the thermal limit)"
631-
)
627+
# if self.SOFT_OVERFLOW_THRESHOLD < 1.0:
628+
# raise RuntimeError(
629+
# "SOFT_OVERFLOW_THRESHOLD < 1., this should be >= 1. (use env.set_thermal_limit "
630+
# "to modify the thermal limit)"
631+
# )
632632
if self.SOFT_OVERFLOW_THRESHOLD >= self.HARD_OVERFLOW_THRESHOLD:
633633
raise RuntimeError(
634634
"self.SOFT_OVERFLOW_THRESHOLD >= self.HARD_OVERFLOW_THRESHOLD this would that the"

0 commit comments

Comments
 (0)