Skip to content

Commit 95e26f4

Browse files
author
yassine abdou
committed
feat : add unit test for protection and thermallimits
1 parent 4d340b6 commit 95e26f4

File tree

6 files changed

+267
-59
lines changed

6 files changed

+267
-59
lines changed

grid2op/Backend/protectionScheme.py

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ class DefaultProtection:
1717
def __init__(
1818
self,
1919
backend: Backend,
20-
parameters: Parameters,
21-
thermal_limits: ThermalLimits,
20+
parameters: Optional[Parameters] = None,
21+
thermal_limits: Optional[ThermalLimits] = None,
2222
is_dc: bool = False,
2323
logger: Optional[logging.Logger] = None,
2424
):
@@ -38,9 +38,11 @@ def __init__(
3838
self._hard_overflow_threshold = self._get_value_from_parameters("HARD_OVERFLOW_THRESHOLD")
3939
self._soft_overflow_threshold = self._get_value_from_parameters("SOFT_OVERFLOW_THRESHOLD")
4040
self._nb_timestep_overflow_allowed = self._get_value_from_parameters("NB_TIMESTEP_OVERFLOW_ALLOWED")
41+
self._no_overflow_disconnection = self._get_value_from_parameters("NO_OVERFLOW_DISCONNECTION")
4142

4243
self.disconnected_during_cf = np.full(self.thermal_limits.n_line, fill_value=-1, dtype=dt_int)
4344
self._timestep_overflow = np.zeros(self.thermal_limits.n_line, dtype=dt_int)
45+
self.conv_ = self._run_power_flow()
4446
self.infos: List[str] = []
4547

4648
if logger is None:
@@ -115,37 +117,21 @@ def next_grid_state(self) -> Tuple[np.ndarray, List[Any], Union[None, Exception]
115117

116118
except Exception as e:
117119
if self.logger is not None:
118-
self.exception("Erreur inattendue dans le calcul de l'état du réseau.")
120+
self.logger.exception("Erreur inattendue dans le calcul de l'état du réseau.")
119121
return self.disconnected_during_cf, self.infos, e
120122

121-
class NoProtection:
123+
class NoProtection(DefaultProtection):
122124
"""
123-
Classe qui gère le cas où les protections de débordement sont désactivées.
125+
Classe qui désactive les protections de débordement tout en conservant la structure de DefaultProtection.
124126
"""
125-
def __init__(
126-
self,
127-
backend: Backend,
128-
thermal_limits: ThermalLimits
129-
):
130-
self.backend = backend
131-
self._validate_input(self.backend)
132-
133-
self.thermal_limits = thermal_limits
127+
def __init__(self, backend: Backend, thermal_limits: ThermalLimits, is_dc: bool = False):
128+
super().__init__(backend, parameters=None, thermal_limits=thermal_limits, is_dc=is_dc)
134129

135-
self._thermal_limit_a = self.thermal_limits.limits if self.thermal_limits else None
136-
self.backend.thermal_limit_a = self._thermal_limit_a
137-
138-
self.disconnected_during_cf = np.full(self.thermal_limits.n_line, fill_value=-1, dtype=dt_int)
139-
self.infos = []
140-
141-
def _validate_input(self, backend: Backend) -> None:
142-
if not isinstance(backend, Backend):
143-
raise Grid2OpException(f"Argument 'backend' doit être de type 'Backend', reçu : {type(backend)}")
144130
def next_grid_state(self) -> Tuple[np.ndarray, List[Any], None]:
145131
"""
146-
Retourne l'état du réseau sans effectuer de déconnexions dues aux débordements.
132+
Ignore les protections et retourne l'état du réseau sans déconnexions.
147133
"""
148-
return self.disconnected_during_cf, self.infos, None
134+
return self.disconnected_during_cf, self.infos, self.conv_
149135

150136
class BlablaProtection:
151137
pass

grid2op/Backend/thermalLimits.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def limits(self, new_limits: Union[np.ndarray, Dict[str, float]]):
122122
el, self.name_line
123123
)
124124
)
125-
for i, el in self.name_line:
125+
for i, el in enumerate(self.name_line):
126126
if el in new_limits:
127127
try:
128128
tmp = dt_float(new_limits[el])

grid2op/Environment/baseEnv.py

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -644,23 +644,39 @@ def _init_thermal_limit(self):
644644
)
645645

646646
def _init_protection(self):
647-
# Initialize the protection system with the specified parameters
648-
self._no_overflow_disconnection: bool = (
649-
self._parameters.NO_OVERFLOW_DISCONNECTION
650-
)
651-
if self._no_overflow_disconnection:
652-
self.protection = protectionScheme.NoProtection(
653-
backend=self.backend,
654-
thermal_limits=self.ts_manager,
655-
)
656-
else:
657-
self.protection = protectionScheme.DefaultProtection(
647+
"""
648+
Initialise le système de protection du réseau avec gestion des erreurs et logs.
649+
"""
650+
try:
651+
self.logger.info("Initialisation du système de protection...")
652+
653+
initializerProtection = protectionScheme.DefaultProtection(
658654
backend=self.backend,
659655
parameters=self.parameters,
660656
thermal_limits=self.ts_manager,
661-
is_dc=self._env_dc
657+
is_dc=self._env_dc,
658+
logger=self.logger
662659
)
663660

661+
if self._no_overflow_disconnection or initializerProtection.conv_ is not None:
662+
self.logger.warning("Utilisation de NoProtection car _no_overflow_disconnection est activé "
663+
"ou la convergence du power flow a échoué.")
664+
self.protection = protectionScheme.NoProtection(
665+
backend=self.backend,
666+
thermal_limits=self.ts_manager,
667+
is_dc=self._env_dc
668+
)
669+
else:
670+
self.logger.info("Utilisation de DefaultProtection avec succès.")
671+
self.protection = initializerProtection
672+
673+
except Grid2OpException as e:
674+
self.logger.error(f"Erreur spécifique à Grid2Op lors de l'initialisation de la protection : {e}")
675+
raise
676+
except Exception as e:
677+
self.logger.exception("Erreur inattendue lors de l'initialisation de la protection.")
678+
raise
679+
664680
@property
665681
def highres_sim_counter(self):
666682
return self._highres_sim_counter

grid2op/tests/BaseBackendTest.py

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1637,7 +1637,7 @@ def next_grid_state_no_overflow(self):
16371637

16381638
)
16391639

1640-
disco, infos, conv_ = env.protection.next_grid_state()
1640+
disco, infos, conv_ = self.backend.next_grid_state(env, is_dc=False)
16411641
assert conv_ is None
16421642
assert not infos
16431643

@@ -1667,10 +1667,9 @@ def test_next_grid_state_1overflow(self):
16671667
thermal_limit[self.id_first_line_disco] = (
16681668
self.lines_flows_init[self.id_first_line_disco] / 2
16691669
)
1670-
env.ts_manager.limits = thermal_limit
1671-
env._init_protection()
1672-
1673-
disco, infos, conv_ = env.protection.next_grid_state()
1670+
self.backend.set_thermal_limit(thermal_limit)
1671+
1672+
disco, infos, conv_ = self.backend.next_grid_state(env, is_dc=False)
16741673
assert conv_ is None
16751674
assert len(infos) == 1 # check that i have only one overflow
16761675
assert np.sum(disco >= 0) == 1
@@ -1705,10 +1704,9 @@ def test_next_grid_state_1overflow_envNoCF(self):
17051704
thermal_limit[self.id_first_line_disco] = (
17061705
lines_flows_init[self.id_first_line_disco] / 2
17071706
)
1708-
env.ts_manager.limits = thermal_limit
1709-
env._init_protection()
1707+
self.backend.set_thermal_limit(thermal_limit)
17101708

1711-
disco, infos, conv_ = env.protection.next_grid_state()
1709+
disco, infos, conv_ = self.backend.next_grid_state(env, is_dc=False)
17121710
assert conv_ is None
17131711
assert not infos # check that don't simulate a cascading failure
17141712
assert np.sum(disco >= 0) == 0
@@ -1752,10 +1750,9 @@ def test_nb_timestep_overflow_disc0(self):
17521750
lines_flows_init[self.id_first_line_disco] / 2
17531751
)
17541752
thermal_limit[self.id_2nd_line_disco] = 400
1755-
env.ts_manager.limits = thermal_limit
1756-
env._init_protection()
1753+
self.backend.set_thermal_limit(thermal_limit)
17571754

1758-
disco, infos, conv_ = env.protection.next_grid_state()
1755+
disco, infos, conv_ = self.backend.next_grid_state(env, is_dc=False)
17591756
assert conv_ is None
17601757
assert len(infos) == 2 # check that there is a cascading failure of length 2
17611758
assert disco[self.id_first_line_disco] >= 0
@@ -1796,10 +1793,9 @@ def test_nb_timestep_overflow_nodisc(self):
17961793
self.lines_flows_init[self.id_first_line_disco] / 2
17971794
)
17981795
thermal_limit[self.id_2nd_line_disco] = 400
1799-
env.ts_manager.limits = thermal_limit
1800-
env._init_protection()
1796+
self.backend.set_thermal_limit(thermal_limit)
18011797

1802-
disco, infos, conv_ = env.protection.next_grid_state()
1798+
disco, infos, conv_ = self.backend.next_grid_state(env, is_dc=False)
18031799
assert conv_ is None
18041800
assert len(infos) == 1 # check that don't simulate a cascading failure
18051801
assert disco[self.id_first_line_disco] >= 0
@@ -1840,10 +1836,9 @@ def test_nb_timestep_overflow_nodisc_2(self):
18401836
self.lines_flows_init[self.id_first_line_disco] / 2
18411837
)
18421838
thermal_limit[self.id_2nd_line_disco] = 400
1843-
env.ts_manager.limits = thermal_limit
1844-
env._init_protection()
1839+
self.backend.set_thermal_limit(thermal_limit)
18451840

1846-
disco, infos, conv_ = env.protection.next_grid_state()
1841+
disco, infos, conv_ = self.backend.next_grid_state(env, is_dc=False)
18471842
assert conv_ is None
18481843
assert len(infos) == 1 # check that don't simulate a cascading failure
18491844
assert disco[self.id_first_line_disco] >= 0
@@ -1884,10 +1879,9 @@ def test_nb_timestep_overflow_disc2(self):
18841879
self.lines_flows_init[self.id_first_line_disco] / 2
18851880
)
18861881
thermal_limit[self.id_2nd_line_disco] = 400
1887-
env.ts_manager.limits = thermal_limit
1888-
env._init_protection()
1882+
self.backend.set_thermal_limit(thermal_limit)
18891883

1890-
disco, infos, conv_ = env.protection.next_grid_state()
1884+
disco, infos, conv_ = self.backend.next_grid_state(env, is_dc=False)
18911885
assert conv_ is None
18921886
assert len(infos) == 2 # check that there is a cascading failure of length 2
18931887
assert disco[self.id_first_line_disco] >= 0
@@ -3224,4 +3218,4 @@ def test_chgtbus_prevDisc(self):
32243218
self._line_disconnected(LINE_ID, obs)
32253219

32263220
# right way to count it
3227-
self._only_sub_impacted(LINE_ID, action, statuses)
3221+
self._only_sub_impacted(LINE_ID, action, statuses)
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import numpy as np
2+
3+
import unittest
4+
from unittest.mock import MagicMock
5+
6+
from grid2op.Backend.backend import Backend
7+
from grid2op.Parameters import Parameters
8+
from grid2op.Exceptions import Grid2OpException
9+
10+
from grid2op.Backend.thermalLimits import ThermalLimits
11+
from grid2op.Backend.protectionScheme import DefaultProtection, NoProtection
12+
13+
class TestProtection(unittest.TestCase):
14+
15+
def setUp(self):
16+
"""Initialisation des mocks et des paramètres de test."""
17+
self.mock_backend = MagicMock(spec=Backend)
18+
self.mock_parameters = MagicMock(spec=Parameters)
19+
self.mock_thermal_limits = MagicMock(spec=ThermalLimits)
20+
21+
# Définition des limites thermiques
22+
self.mock_thermal_limits.limits = np.array([100.0, 200.0])
23+
self.mock_thermal_limits.n_line = 2
24+
25+
# Mock des valeurs de paramètre avec des valeurs par défaut
26+
self.mock_parameters.SOFT_OVERFLOW_THRESHOLD = 1.0
27+
self.mock_parameters.HARD_OVERFLOW_THRESHOLD = 1.5
28+
self.mock_parameters.NB_TIMESTEP_OVERFLOW_ALLOWED = 3
29+
30+
# Comportement du backend
31+
self.mock_backend.get_line_status.return_value = np.array([True, True])
32+
self.mock_backend.get_line_flow.return_value = np.array([90.0, 210.0])
33+
self.mock_backend._runpf_with_diverging_exception.return_value = None
34+
35+
# Initialisation de la classe testée
36+
self.default_protection = DefaultProtection(
37+
backend=self.mock_backend,
38+
parameters=self.mock_parameters,
39+
thermal_limits=self.mock_thermal_limits,
40+
is_dc=False
41+
)
42+
43+
def test_initialization(self):
44+
"""Test de l'initialisation de DefaultProtection."""
45+
self.assertIsInstance(self.default_protection, DefaultProtection)
46+
self.assertEqual(self.default_protection.is_dc, False)
47+
self.assertIsNotNone(self.default_protection._parameters)
48+
49+
def test_validate_input(self):
50+
"""Test de validation des entrées."""
51+
with self.assertRaises(Grid2OpException):
52+
DefaultProtection(backend=None, parameters=self.mock_parameters)
53+
54+
def test_run_power_flow(self):
55+
"""Test de l'exécution du flux de puissance."""
56+
result = self.default_protection._run_power_flow()
57+
self.assertIsNone(result)
58+
59+
def test_update_overflows(self):
60+
"""Test de la mise à jour des surcharges et des lignes à déconnecter."""
61+
lines_flows = np.array([120.0, 310.0])
62+
lines_to_disconnect = self.default_protection._update_overflows(lines_flows)
63+
self.assertTrue(lines_to_disconnect[1]) # Seule la deuxième ligne doit être déconnectée
64+
self.assertFalse(lines_to_disconnect[0])
65+
66+
def test_disconnect_lines(self):
67+
"""Test de la déconnexion des lignes."""
68+
lines_to_disconnect = np.array([False, True])
69+
self.default_protection._disconnect_lines(lines_to_disconnect, timestep=1)
70+
self.mock_backend._disconnect_line.assert_called_once_with(1)
71+
72+
def test_next_grid_state(self):
73+
"""Test de la simulation de l'évolution du réseau."""
74+
disconnected, infos, error = self.default_protection.next_grid_state()
75+
self.assertIsInstance(disconnected, np.ndarray)
76+
self.assertIsInstance(infos, list)
77+
self.assertIsNone(error)
78+
79+
def test_no_protection(self):
80+
"""Test de la classe NoProtection."""
81+
no_protection = NoProtection(self.mock_backend, self.mock_thermal_limits)
82+
disconnected, infos, conv = no_protection.next_grid_state()
83+
self.assertIsInstance(disconnected, np.ndarray)
84+
self.assertIsInstance(infos, list)
85+
self.assertIsNone(conv)
86+
87+
class TestFunctionalProtection(unittest.TestCase):
88+
89+
def setUp(self):
90+
"""Initialisation des mocks pour un test fonctionnel."""
91+
self.mock_backend = MagicMock(spec=Backend)
92+
self.mock_parameters = MagicMock(spec=Parameters)
93+
self.mock_thermal_limits = MagicMock(spec=ThermalLimits)
94+
95+
# Configuration des limites thermiques et des flux de lignes
96+
self.mock_thermal_limits.limits = np.array([100.0, 200.0])
97+
self.mock_thermal_limits.n_line = 2
98+
self.mock_parameters.SOFT_OVERFLOW_THRESHOLD = 1.0
99+
self.mock_parameters.HARD_OVERFLOW_THRESHOLD = 1.5
100+
self.mock_parameters.NB_TIMESTEP_OVERFLOW_ALLOWED = 3
101+
102+
# Comportement du backend pour simuler les flux de lignes
103+
self.mock_backend.get_line_status.return_value = np.array([True, True])
104+
self.mock_backend.get_line_flow.return_value = np.array([90.0, 210.0])
105+
self.mock_backend._runpf_with_diverging_exception.return_value = None
106+
107+
# Initialisation de la classe de protection avec des paramètres
108+
self.default_protection = DefaultProtection(
109+
backend=self.mock_backend,
110+
parameters=self.mock_parameters,
111+
thermal_limits=self.mock_thermal_limits,
112+
is_dc=False
113+
)
114+
115+
# Initialisation de NoProtection
116+
self.no_protection = NoProtection(
117+
backend=self.mock_backend,
118+
thermal_limits=self.mock_thermal_limits,
119+
is_dc=False
120+
)
121+
122+
def test_functional_default_protection(self):
123+
"""Test fonctionnel pour DefaultProtection."""
124+
125+
self.mock_backend.get_line_flow.return_value = np.array([90.0, 210.0]) # Lignes avec un débordement
126+
disconnected, infos, error = self.default_protection.next_grid_state()
127+
128+
self.assertTrue(np.any(disconnected == -1)) # Ligne 1 doit être déconnectée
129+
self.assertIsNone(error)
130+
self.assertEqual(len(infos), 0)
131+
132+
def test_functional_no_protection(self):
133+
"""Test fonctionnel pour NoProtection."""
134+
self.mock_backend.get_line_flow.return_value = np.array([90.0, 180.0]) # Aucune ligne en débordement
135+
disconnected, infos, error = self.no_protection.next_grid_state()
136+
137+
self.assertTrue(np.all(disconnected == -1)) # Aucune ligne ne doit être déconnectée
138+
self.assertIsNone(error)
139+
self.assertEqual(len(infos), 0)
140+
141+
if __name__ == "__main__":
142+
unittest.main()

0 commit comments

Comments
 (0)