Skip to content

Commit 84a4a4c

Browse files
committed
fix some doc issues + improve backend doc
Signed-off-by: DONNOT Benjamin <[email protected]>
1 parent e7ec8f0 commit 84a4a4c

File tree

4 files changed

+265
-19
lines changed

4 files changed

+265
-19
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -208,11 +208,7 @@ Native multi agents support:
208208
- [IMPROVED] `ForecastEnv` is now part of the public API.
209209
- [IMPROVED] no need to call `self._compute_pos_big_top()` at the end of the implementation of `backend.load_grid()`
210210
- [IMPROVED] type hints in various files.
211-
212-
[1.10.5] - 2025-03-07
213-
------------------------
214-
- [FIXED] force pandapower < 3 otherwise pandapower backend does not work and
215-
lots of tests are failing.
211+
- [IMPROVED] documentation of the backend
216212

217213
[1.10.5] - 2025-03-07
218214
------------------------

docs/developer/createbackend.rst

Lines changed: 259 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -215,11 +215,29 @@ Basically the `load_grid` function would look something like:
215215
216216
# from grid2op 1.10.0 you need to call one of
217217
self.can_handle_more_than_2_busbar() # see doc for more information
218-
OR
218+
# OR
219219
self.cannot_handle_more_than_2_busbar() # see doc for more information
220220
# It is important you include it at the top of this method, otherwise you
221221
# will not have access to self.n_busbar_per_sub
222222
223+
# from grid2op 1.11.0 you need to call one of
224+
self.can_handle_detachment() # see doc for more information
225+
# OR
226+
self.cannot_handle_detachment() # see doc for more information
227+
# It is important you include it at the top of this method,
228+
# if you don't, by default grid2op will suppose that your backend
229+
# cannot disconnect element
230+
231+
# from grid2op 1.11.0, "detachment" feature is implemented. This
232+
# means the agent is able to disconnect element from the grid.
233+
# But it is still "required" (post processing) that the backend
234+
# does not automatically disconnect elements.
235+
# If you backend disconnects automatically elements, you probably want
236+
# to turn off the internal checks (so that if your backend disconnect
237+
# things, the environment is not "done")
238+
# To do that, set the flags
239+
# self._prevent_automatic_disconnection = False
240+
223241
# load the grid in your favorite format, located at `full_path`:
224242
self._grid = ... # the way you do that depends on the "solver" you use
225243
@@ -539,10 +557,8 @@ At the end, the `apply_action` function of the backend should look something lik
539557

540558
.. code-block:: python
541559
542-
def apply_action(self, backendAction=None):
543-
if backendAction is None:
544-
return
545-
active_bus, (prod_p, prod_v, load_p, load_q), _, shunts__ = backendAction()
560+
def apply_action(self, backend_action=None):
561+
active_bus, (prod_p, prod_v, load_p, load_q, storage_p), _, shunts__ = backend_action()
546562
547563
# modify the injections [see paragraph "Modifying the injections (productions and loads)"]
548564
for gen_id, new_p in prod_p:
@@ -560,7 +576,7 @@ At the end, the `apply_action` function of the backend should look something lik
560576
... # the way you do that depends on the `internal representation of the grid`
561577
562578
# modify the topology [see paragraph "Modifying the topology (status and busbar)"]
563-
loads_bus = backendAction.get_loads_bus()
579+
loads_bus = backend_action.get_loads_bus()
564580
for load_id, new_bus in loads_bus:
565581
# modify the "busbar" of the loads
566582
if new_bus == -1:
@@ -570,7 +586,7 @@ At the end, the `apply_action` function of the backend should look something lik
570586
# the load is moved to either busbar 1 (in this case `new_bus` will be `1`)
571587
# or to busbar 2 (in this case `new_bus` will be `2`)
572588
... # the way you do that depends on the `internal representation of the grid`
573-
gens_bus = backendAction.get_gens_bus()
589+
gens_bus = backend_action.get_gens_bus()
574590
for gen_id, new_bus in gens_bus:
575591
# modify the "busbar" of the generators
576592
if new_bus == -1:
@@ -580,7 +596,7 @@ At the end, the `apply_action` function of the backend should look something lik
580596
# the gen is moved to either busbar 1 (in this case `new_bus` will be `1`)
581597
# or to busbar 2 (in this case `new_bus` will be `2`)
582598
... # the way you do that depends on the `internal representation of the grid`
583-
lines_or_bus = backendAction.get_lines_or_bus()
599+
lines_or_bus = backend_action.get_lines_or_bus()
584600
for line_id, new_bus in lines_or_bus:
585601
# modify the "busbar" of the origin side of powerline line_id
586602
if new_bus == -1:
@@ -590,7 +606,7 @@ At the end, the `apply_action` function of the backend should look something lik
590606
# the origin side of powerline is moved to either busbar 1 (in this case `new_bus` will be `1`)
591607
# or to busbar 2 (in this case `new_bus` will be `2`)
592608
... # the way you do that depends on the `internal representation of the grid`
593-
lines_ex_bus = backendAction.get_lines_ex_bus()
609+
lines_ex_bus = backend_action.get_lines_ex_bus()
594610
for line_id, new_bus in lines_ex_bus:
595611
# modify the "busbar" of the extremity side of powerline line_id
596612
if new_bus == -1:
@@ -979,8 +995,242 @@ TODO there are ways to use the `create_test_suite` but they have not been tested
979995

980996
Advanced usage and speed optimization
981997
--------------------------------------
998+
999+
Storage units (feature)
1000+
++++++++++++++++++++++++
1001+
1002+
You can deactivate the support of storage units by calling `self.set_no_storage()` in the
1003+
`load_grid()` implementation.
1004+
1005+
If you want to support storage units, you need to implement:
1006+
1007+
- in `load_grid()`:
1008+
1009+
- `self.n_storage` : number of storage unit on the grid
1010+
- `self.storages_to_subid` : for each storage units, at which substation they are connected on the grid
1011+
- (optional): `self.name_storage`, `self.storage_to_sub_pos`, `self.storage_pos_topo_vect`
1012+
1013+
- in `apply_action()`:
1014+
1015+
- you receive same kind of information for which bus storages needs to be
1016+
connected to with `stos_bus = backend_action.get_storages_bus()` (use it like
1017+
you would other element)
1018+
- you receive the information in the `storage_p` variable about the amount of
1019+
power that the storage unit is asked to absorb / produce. This is given in MW
1020+
with load convention (positive power = power is absorbed by the storage unit)
1021+
1022+
- in the "getters" you need to implement the :func:`grid2op.Backend.Backend.storages_info()` methods that returns
1023+
3 vectors: `storage_p`, `storage_q`, `storage_v` (similar to `loads_info` or `generators_info`)
1024+
1025+
.. note::
1026+
In order for the "storage units" to be configured correctly, for now, the default behaviour
1027+
is to have a separate file giving the storage information. Please
1028+
consult the documentation of :func:`grid2op.Backend.Backend.load_storage_data` for
1029+
more information.
1030+
1031+
Shunts (feature)
1032+
+++++++++++++++++
1033+
1034+
It is also possible to support shunts in grid2op.
1035+
1036+
For now the support of shunts is pretty basic. User provides a target in p and q as well
1037+
as a busbar on which to connect it.
1038+
1039+
There is no (yet) notion of `tap` or anything more realistic at the moment.
1040+
1041+
.. note::
1042+
The above sentence does NOT mean grid2op cannot handle this or that
1043+
if your backend supports it then you cannot make it a grid2op backend.
1044+
1045+
This means that a grid2op agent, using only grid2op functions will not
1046+
be able to operate the shunts in a "fine grained" / "more realistic"
1047+
fashion.
1048+
1049+
If you don't do anything about shunts, they will be deactivated, the agent
1050+
will not be able to directly operate the shunts using grid2op functions (
1051+
this does not mean shunts will be (or should be) removed from the powerflow
1052+
computation)
1053+
1054+
If you want to support them you need to define:
1055+
1056+
- in `load_grid()`:
1057+
1058+
- call `self.shunts_data_available = True`
1059+
- `self.n_shunt` : number of shunts on the grid
1060+
- `self.shunts_to_subid` : for each shunts, at which substation they are connected on the grid
1061+
- (optional): `self.name_shunt`. There is nothing comparable to
1062+
`self.gen_to_sub_pos` or `self.gen_pos_topo_vect` for shunts.
1063+
1064+
- in `apply_action()`:
1065+
1066+
- you receive most of the information on the shunt with `shunts_` variable. It is a
1067+
tuple with 3 elements (`shunt_p, shunt_q, shunt_bus = shunts__`) with:
1068+
1069+
- `shunt_p` the each shunt MW (given in the form a `ValueStore` as always)
1070+
- `shunt_q` the each shunt MVAr (given in the form a `ValueStore` as always)
1071+
- `shunt_bus` the each shunt MVAr (given in the form a `ValueStore` as always)
1072+
1073+
- in the "getters" you need to implement the :func:`grid2op.Backend.Backend.shunt_info()` methods that returns
1074+
4 vectors: `shunt_p`, `shunt_q`, `shunt_v` and `shunt_bus`
1075+
This is similar to :func:`grid2op.Backend.Backend.loads_info` or
1076+
:func:`grid2op.Backend.Backend.generators_info` for the `p`, `q` and `v` attributes.
1077+
The `bus` gives the bus to which each shunt is connected.
1078+
1079+
1080+
Voltage angle (feature)
1081+
++++++++++++++++++++++++
1082+
1083+
If you want your backend to output voltage angle values, you can specify it by invoking
1084+
(in the `__init__` or in `load_grid`) `self.can_output_theta = True`
1085+
1086+
In this case you need to implement :func:`grid2op.Backend.Backend.get_theta()`
1087+
1088+
.. note::
1089+
At time of writing, grid2op does not offer a way to retrieve the theta values
1090+
of shunts. Shunts theta should not be retrieved by `get_theta`
1091+
1092+
.. warning::
1093+
Even if your backend does not support storage units, you are expected to
1094+
return a `storage_theta` vector which would be empty, for example
1095+
`storage_theta = np.empty(0, dtype=float)`
1096+
1097+
.. note::
1098+
Voltage angles are expected to be given in degree and not in radian
1099+
1100+
1101+
Copy (feature)
1102+
+++++++++++++++++
1103+
1104+
By default, your backend is supposed to be "copy able".
1105+
1106+
The default implementation of :func:`grid2op.Backend.Backend.copy` might
1107+
not be optimal for all backend.
1108+
1109+
You might want to overload it for a faster copy.
1110+
1111+
Non copy-able (feature)
1112+
++++++++++++++++++++++++
1113+
1114+
If your backend cannot be copied (for example if it relies on a software
1115+
with license check), you can specify that `self._can_be_copied = False`
1116+
in the `load_grid` implementation.
1117+
1118+
Faster line disconnection (speed)
1119+
++++++++++++++++++++++++++++++++++
1120+
1121+
During the emulation of the "cascading failure" / "protections", performed
1122+
in :func:`grid2op.Backend.Backend.next_grid_state`, the special function
1123+
:func:`grid2op.Backend.Backend._disconnect_line` is called.
1124+
1125+
It has a default implementation relying on the building
1126+
of a `backend_action` and then calling
1127+
:func:`grid2op.Backend.Backend.apply_action` which might be rather slow.
1128+
1129+
You can bypass everything by overloading this function
1130+
with a faster implementation.
1131+
1132+
.. note::
1133+
This is definitely a "marginal gain".
1134+
1135+
Number of independant buses per substation
1136+
+++++++++++++++++++++++++++++++++++++++++++
1137+
1138+
Before grid2op <1.10.0, the "grid2op modelling" imposed that every substations
1139+
counted exactly 2 independant buses, and that each elements could be either connected
1140+
to one busbar OR the other.
1141+
1142+
From grid2op >=1.10.0 onwargs, the user might indicate the maximum number of
1143+
busbars wanted per substation with a call to `grid2op.make(..., n_busbar_per_sub=XXX)`.
1144+
1145+
If your backend does not provide this feature (yet ?) then you need to call
1146+
`self.cannot_handle_more_than_2_busbar()` in the `load_grid` implementation, preferably
1147+
at the beginning of it.
1148+
1149+
However, if your implementation can handle this (which is the case for most real
1150+
powerflow...) you need to call `self.can_handle_more_than_2_busbar()`
1151+
1152+
.. warning::
1153+
1154+
In all cases then, you should either call :
1155+
1156+
- :func:`grid2op.Backend.Backend.cannot_handle_more_than_2_busbar`
1157+
- OR
1158+
- :func:`grid2op.Backend.Backend.can_handle_more_than_2_busbar`
1159+
1160+
In the implementation of `load_grid`
1161+
1162+
If you don't, the default grid2op behaviour is to fall back to "legacy mode"
1163+
where only 2 independant busbars per substation was supported.
1164+
1165+
This fall back erases the user defined behaviour in the call to env.make
1166+
1167+
1168+
Detachment
1169+
+++++++++++
1170+
1171+
Before grid2op <1.11, if an element (load, gen, storage units producing power)
1172+
was disconnected, then the episode stopped (done=True).
1173+
1174+
Starting from grid2op 1.11, the user can decide to allow the detachement of some
1175+
elements (load, gen, storage units) by calling `grid2op.make(..., allow_detachment=XXX)`
1176+
1177+
If your backend has been coded before this feature, then you need to call
1178+
`self.cannot_handle_detachment()`, preferably at the top of the `load_grid` function.
1179+
1180+
If however your backend can support this feature, then you need to call
1181+
`self.can_handle_detachment()`, preferably at the top of the `load_grid` function.
1182+
1183+
.. warning::
1184+
1185+
In all cases then, you should either call :
1186+
1187+
- :func:`grid2op.Backend.Backend.cannot_handle_detachment`
1188+
- OR
1189+
- :func:`grid2op.Backend.Backend.can_handle_detachment`
1190+
1191+
In the implementation of `load_grid`
1192+
1193+
If you don't, the default grid2op behaviour is to fall back to "legacy mode"
1194+
where detachment was not supported: agent will not have the possibility
1195+
to disconnect things.
1196+
1197+
This fall back erases the user defined behaviour in the call to env.make
1198+
1199+
Automatic disconnection of elements
1200+
++++++++++++++++++++++++++++++++++++
1201+
1202+
One of the stopping criteria of a grid2op episode is that the grid is split in
1203+
2 or more independant connected component.
1204+
1205+
This is (at time of writing, grid2op 1.11.0) still "more or less" the case (but might evolve in the
1206+
next major release).
1207+
1208+
Realistic backends (used for real powergrid) might be "robust" to such cases, for example
1209+
by running different powerflow, one for each connected component, or by running a
1210+
powerflow only on the "main" connected component.
1211+
1212+
If that is the case, then the backend will "automatically" disconnect element not in the
1213+
"main" connected component before performing the computation for example.
1214+
1215+
As of writing, grid2op will stop an episode if the backend automatically disconnects
1216+
elements (loads, generators, lines etc.) by default.
1217+
1218+
To bypass the default behaviour, a "partial" support for this feature (=possibility for
1219+
a backend to continue a episode even if part of the grid is in blackout) is possible
1220+
if the backend implementation calls `self._prevent_automatic_disconnection = False`
1221+
1222+
Internally, this will deactivate some checking that ensure "done=True" is returned.
1223+
1224+
Vectorization in `apply_action` (speed)
1225+
++++++++++++++++++++++++++++++++++++++++
9821226
TODO this will be explained "soon".
9831227

1228+
Overriding the public methods (speed)
1229+
++++++++++++++++++++++++++++++++++++++++
1230+
TODO this will be explained "soon".
1231+
1232+
*eg* `load_grid_public`
1233+
9841234
Detailed Documentation by class
9851235
-------------------------------
9861236
A first example of a working backend that can be easily understood (without nasty gory speed optimization)

grid2op/Action/_backendAction.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class ValueStore:
3838
3939
There are two correct uses for this class:
4040
41-
#. by iterating manually with the `for xxx in value_stor_instance: `
41+
#. by iterating manually with the `for xxx in value_stor_instance:`
4242
#. by checking which objects have been changed (with :attr:`ValueStore.changed` ) and then check the
4343
new value of the elements **changed** with :attr:`ValueStore.values` [el_id]
4444

grid2op/Space/GridObjects.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5124,7 +5124,7 @@ def get_line_info(cls, *, line_id : Optional[int]=None, line_name : Optional[str
51245124
It does not accept any positional argument (only key-word argument) and you need
51255125
to specify only one of `line_name` OR `line_id` but not both.
51265126
5127-
.. info::
5127+
.. note::
51285128
`line_id` is a python id, so it should be between `0` and `env.n_line - 1`
51295129
51305130
Examples
@@ -5197,7 +5197,7 @@ def get_load_info(cls, *, load_id : Optional[int]=None, load_name : Optional[str
51975197
It does not accept any positional argument (only key-word argument) and you need
51985198
to specify only one of `load_name` OR `load_id` but not both.
51995199
5200-
.. info::
5200+
.. note::
52015201
`load_id` is a python id, so it should be between `0` and `env.n_load - 1`
52025202
52035203
Examples
@@ -5234,7 +5234,7 @@ def get_gen_info(cls, *, gen_id : Optional[int]=None, gen_name : Optional[str]=N
52345234
It does not accept any positional argument (only key-word argument) and you need
52355235
to specify only one of `gen_name` OR `gen_id` but not both.
52365236
5237-
.. info::
5237+
.. note::
52385238
`gen_id` is a python id, so it should be between `0` and `env.n_gen - 1`
52395239
52405240
Examples
@@ -5271,7 +5271,7 @@ def get_storage_info(cls, *, storage_id : Optional[int]=None, storage_name : Opt
52715271
It does not accept any positional argument (only key-word argument) and you need
52725272
to specify only one of `storage_name` OR `storage_id` but not both.
52735273
5274-
.. info::
5274+
.. note::
52755275
`storage_id` is a python id, so it should be between `0` and `env.n_storage - 1`
52765276
52775277
Examples

0 commit comments

Comments
 (0)