/
GridObjects.py
1459 lines (1202 loc) · 71.4 KB
/
GridObjects.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Copyright (c) 2019-2020, RTE (https://www.rte-france.com)
# See AUTHORS.txt
# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0.
# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file,
# you can obtain one at http://mozilla.org/MPL/2.0/.
# SPDX-License-Identifier: MPL-2.0
# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems.
"""
This class abstracts the main components of BaseAction, BaseObservation, ActionSpace, and ObservationSpace.
It represents a powergrid (the object in it) in a format completely agnostic to the solver used to compute
the power flows (:class:`grid2op.Backend.Backend`).
See :class:`grid2op.Converter` for a different type of Action / Observation. These can be used to transform
complex :class:`grid2op.Action.Action` or :class:`grid2op.Observation.Observaion` into more convient structures
to manipulate.
"""
import numpy as np
from grid2op.dtypes import dt_int, dt_float, dt_bool
from grid2op.Exceptions import *
from grid2op.Space.space_utils import extract_from_dict, save_to_dict
import pdb
# TODO better random stuff when random observation (seed in argument is really weird)
# TODO tests of these methods and this class in general
class GridObjects:
"""
This class stores in a Backend agnostic way some information about the powergrid.
It stores information about numbers of objects, and which objects are where, their names, etc.
The classes :class:`grid2op.Action.BaseAction`, :class:`grid2op.Action.ActionSpace`,
:class:`grid2op.Observation.BaseObservation`, :class:`grid2op.Observation.ObservationSpace` and
:class:`grid2op.Backend.Backend` all inherit from this class. This means that each of the above has its own
representation of the powergrid.
The modeling adopted for describing a powergrid is the following:
- only the main objects of a powergrid are represented. An "object" is either a load (consumption) a generator
(production), an end of a powerline (each powerline have exactly two extremities: "origin" (or)
and "extremity" (ext)).
- every "object" (see above) is connected to a unique substation. Each substation then counts a given (fixed)
number of objects connected to it. [in this platform we don't consider the possibility to build new "objects" as
of today]
For each object, the bus to which it is connected is given in the `*_to_subid` (for
example :attr:`GridObjects.load_to_subid` gives, for each load, the id of the substation to which it is
connected)
We suppose that, at every substation, each object (if connected) can be connected to either "busbar" 1 or
"busbar" 2. This means that, at maximum, there are 2 independent buses for each substation.
With this hypothesis, we can represent (thought experiment) each substation by a vector. This vector has as
many components than the number of objects in the substation (following the previous example, the vector
representing the first substation would have 5 components). And each component of this vector would represent
a fixed element in it. For example, if say, the load with id 1 is connected to the first element, there would be
a unique component saying if the load with id 1 is connected to busbar 1 or busbar 2. For the generators, this
id in this (fictive) vector is indicated in the :attr:`GridObjects.gen_to_sub_pos` vector. For example the first
position of :attr:`GridObjects.gen_to_sub_pos` indicates on which component of the (fictive) vector representing the
substation 1 to look to know on which bus the first generator is connected.
We define the "topology" as the busbar to which each object is connected: each object being connected to either
busbar 1 or busbar 2, this topology can be represented by a vector of fixed size (and it actually is in
:attr:`grid2op.Observation.BaseObservation.topo_vect` or in :func:`grid2op.Backend.Backend.get_topo_vect`). There are
multiple ways to make such a vector. We decided to concatenate all the (fictive) vectors described above. This
concatenation represents the actual topology of this powergrid at a given timestep. This class doesn't store this
information (see :class:`grid2op.Observation.BaseObservation` for such purpose).
This entails that:
- the bus to which each object on a substation will be stored in consecutive components of such a vector. For
example, if the first substation of the grid has 5 elements connected to it, then the first 5 elements of
:attr:`grid2op.Observation.BaseObservation.topo_vect` will represent these 5 elements. The number of elements
in each substation is given in :attr:`grid2op.Space.GridObjects.sub_info`.
- the substation are stored in "order": objects of the first substations are represented, then this is the objects
of the second substation etc. So in the example above, the 6th element of
:attr:`grid2op.Observation.BaseObservation.topo_vect` is an object connected to the second substation.
- to know on which position of this "topology vector" we can find the information relative a specific element
it is possible to:
- method 1 (not recommended):
i) retrieve the substation to which this object is connected (for example looking at
:attr:`GridObjects.line_or_to_subid` [l_id] to know on which substation is connected the origin of
powerline with id $l_id$.)
ii) once this substation id is known, compute which are the components of the topological vector that encodes
information about this substation. For example, if the substation id `sub_id` is 4, we a) count the number
of elements in substations with id 0, 1, 2 and 3 (say it's 42) we know, by definition that the substation
4 is encoded in ,:attr:`grid2op.Observation.BaseObservation.topo_vect` starting at component 42 and b) this
substations has :attr:`GridObjects.sub_info` [sub_id] elements (for the sake of the example say it's 5)
then the end of the vector for substation 4 will be 42+5 = 47. Finally, we got the representation of the
"local topology" of the substation 4 by looking at
:attr:`grid2op.Observation.BaseObservation.topo_vect` [42:47].
iii) retrieve which component of this vector of dimension 5 (remember we assumed substation 4 had 5 elements)
encodes information about the origin end of the line with id `l_id`. This information is given in
:attr:`GridObjects.line_or_to_sub_pos` [l_id]. This is a number between 0 and 4, say it's 3. 3 being
the index of the object in the substation)
- method 2 (not recommended): all of the above is stored (for the same powerline) in the
:attr:`GridObjects.line_or_pos_topo_vect` [l_id]. In the example above, we will have:
:attr:`GridObjects.line_or_pos_topo_vect` [l_id] = 45 (=42+3:
42 being the index on which the substation started and 3 being the index of the object in the substation)
- method 3 (recommended): use any of the function that computes it for you:
:func:`grid2op.Observation.BaseObservation.state_of` is such an interesting method. The two previous methods
"method 1" and "method 2" were presented as a way to give detailed and "concrete" example on how the
modeling of the powergrid work.
For a given powergrid, this object should be initialized once in the :class:`grid2op.Backend.Backend` when
the first call to :func:`grid2op.Backend.Backend.load_grid` is performed. In particular the following attributes
must necessarily be defined (see above for a detailed description of some of the attributes):
- :attr:`GridObjects.name_load`
- :attr:`GridObjects.name_gen`
- :attr:`GridObjects.name_line`
- :attr:`GridObjects.name_sub`
- :attr:`GridObjects.n_line`
- :attr:`GridObjects.n_gen`
- :attr:`GridObjects.n_load`
- :attr:`GridObjects.n_sub`
- :attr:`GridObjects.sub_info`
- :attr:`GridObjects.dim_topo`
- :attr:`GridObjects.load_to_subid`
- :attr:`GridObjects.gen_to_subid`
- :attr:`GridObjects.line_or_to_subid`
- :attr:`GridObjects.line_ex_to_subid`
- :attr:`GridObjects.load_to_sub_pos`
- :attr:`GridObjects.gen_to_sub_pos`
- :attr:`GridObjects.line_or_to_sub_pos`
- :attr:`GridObjects.line_ex_to_sub_pos`
A call to the function :func:`GridObjects._compute_pos_big_topo` allow to compute the \*_pos_topo_vect attributes
(for example :attr:`GridObjects.line_ex_pos_topo_vect`) can be computed from the above data:
- :attr:`GridObjects.load_pos_topo_vect`
- :attr:`GridObjects.gen_pos_topo_vect`
- :attr:`GridObjects.line_or_pos_topo_vect`
- :attr:`GridObjects.line_ex_pos_topo_vect`
Note that if you want to model an environment with unit commitment or redispatching capabilities, you also need
to provide the following attributes:
- :attr:`GridObjects.gen_type`
- :attr:`GridObjects.gen_pmin`
- :attr:`GridObjects.gen_pmax`
- :attr:`GridObjects.gen_redispatchable`
- :attr:`GridObjects.gen_max_ramp_up`
- :attr:`GridObjects.gen_max_ramp_down`
- :attr:`GridObjects.gen_min_uptime`
- :attr:`GridObjects.gen_min_downtime`
- :attr:`GridObjects.gen_cost_per_MW`
- :attr:`GridObjects.gen_startup_cost`
- :attr:`GridObjects.gen_shutdown_cost`
These information are loaded using the :func:`grid2op.Backend.Backend.load_redispacthing_data` method.
**NB** it does not store any information about the current state of the powergrid. It stores information that
cannot be modified by the BaseAgent, the Environment or any other entity.
Attributes
----------
n_line: :class:`int`
number of powerlines in the powergrid
n_gen: :class:`int`
number of generators in the powergrid
n_load: :class:`int`
number of loads in the
n_sub: :class:`int`
number of loads in the powergrid
dim_topo: :class:`int`
The total number of objects in the powergrid. This is also the dimension of the "topology vector" defined above.
sub_info: :class:`numpy.ndarray`, dtype:int
for each substation, gives the number of elements connected to it
load_to_subid: :class:`numpy.ndarray`, dtype:int
for each load, gives the id the substation to which it is connected. For example,
:attr:`GridObjects.load_to_subid` [load_id] gives the id of the substation to which the load of id
`load_id` is connected.
gen_to_subid: :class:`numpy.ndarray`, dtype:int
for each generator, gives the id the substation to which it is connected
line_or_to_subid: :class:`numpy.ndarray`, dtype:int
for each line, gives the id the substation to which its "origin" end is connected
line_ex_to_subid: :class:`numpy.ndarray`, dtype:int
for each line, gives the id the substation to which its "extremity" end is connected
load_to_sub_pos: :class:`numpy.ndarray`, dtype:int
Suppose you represent the topoology of the substation *s* with a vector (each component of this vector will
represent an object connected to this substation). This vector has, by definition the size
:attr:`GridObject.sub_info` [s]. `load_to_sub_pos` tells which component of this vector encodes the
current load. Suppose that load of id `l` is connected to the substation of id `s` (this information is
stored in :attr:`GridObjects.load_to_subid` [l]), then if you represent the topology of the substation
`s` with a vector `sub_topo_vect`, then "`sub_topo_vect` [ :attr:`GridObjects.load_to_subid` [l] ]" will encode
on which bus the load of id `l` is stored.
gen_to_sub_pos: :class:`numpy.ndarray`, dtype:int
same as :attr:`GridObjects.load_to_sub_pos` but for generators.
line_or_to_sub_pos: :class:`numpy.ndarray`, dtype:int
same as :attr:`GridObjects.load_to_sub_pos` but for "origin" end of powerlines.
line_ex_to_sub_pos: :class:`numpy.ndarray`, dtype:int
same as :attr:`GridObjects.load_to_sub_pos` but for "extremity" end of powerlines.
load_pos_topo_vect: :class:`numpy.ndarray`, dtype:int
The topology if the entire grid is given by a vector, say *topo_vect* of size
:attr:`GridObjects.dim_topo`. For a given load of id *l*,
:attr:`GridObjects.load_to_sub_pos` [l] is the index
of the load *l* in the vector :attr:`grid2op.BaseObservation.BaseObservation.topo_vect` . This means that, if
"`topo_vect` [ :attr:`GridObjects.load_pos_topo_vect` \[l\] ]=2"
then load of id *l* is connected to the second bus of the substation.
gen_pos_topo_vect: :class:`numpy.ndarray`, dtype:int
same as :attr:`GridObjects.load_pos_topo_vect` but for generators.
line_or_pos_topo_vect: :class:`numpy.ndarray`, dtype:int
same as :attr:`GridObjects.load_pos_topo_vect` but for "origin" end of powerlines.
line_ex_pos_topo_vect: :class:`numpy.ndarray`, dtype:int
same as :attr:`GridObjects.load_pos_topo_vect` but for "extremity" end of powerlines.
name_load: :class:`numpy.ndarray`, dtype:str
ordered names of the loads in the grid.
name_gen: :class:`numpy.ndarray`, dtype:str
ordered names of the productions in the grid.
name_line: :class:`numpy.ndarray`, dtype:str
ordered names of the powerline in the grid.
name_sub: :class:`numpy.ndarray`, dtype:str
ordered names of the substation in the grid
attr_list_vect: ``list``, static
List of string. It represents the attributes that will be stored to/from vector when the BaseObservation is converted
to/from it. This parameter is also used to compute automatically :func:`GridObjects.dtype` and
:func:`GridObjects.shape` as well as :func:`GridObjects.size`. If this class is derived, then it's really
important that this vector is properly set. All the attributes with the name on this vector should have
consistently the same size and shape, otherwise, some methods will not behave as expected.
_vectorized: :class:`numpy.ndarray`, dtype:float
The representation of the GridObject as a vector. See the help of :func:`GridObjects.to_vect` and
:func:`GridObjects.from_vect` for more information. **NB** for performance reason, the conversion of the internal
representation to a vector is not performed at any time. It is only performed when :func:`GridObjects.to_vect` is
called the first time. Otherwise, this attribute is set to ``None``.
gen_type: :class:`numpy.ndarray`, dtype:str
Type of the generators, among: "solar", "wind", "hydro", "thermal" and "nuclear". Optional. Used
for unit commitment problems or redispacthing action.
gen_pmin: :class:`numpy.ndarray`, dtype:float
Minimum active power production needed for a generator to work properly. Optional. Used
for unit commitment problems or redispacthing action.
gen_pmax: :class:`numpy.ndarray`, dtype:float
Maximum active power production needed for a generator to work properly. Optional. Used
for unit commitment problems or redispacthing action.
gen_redispatchable: :class:`numpy.ndarray`, dtype:bool
For each generator, it says if the generator is dispatchable or not. Optional. Used
for unit commitment problems or redispacthing action.
gen_max_ramp_up: :class:`numpy.ndarray`, dtype:float
Maximum active power variation possible between two consecutive timestep for each generator:
a redispatching action
on generator `g_id` cannot be above :attr:`GridObjects.gen_ramp_up_max` [`g_id`]. Optional. Used
for unit commitment problems or redispacthing action.
gen_max_ramp_down: :class:`numpy.ndarray`, dtype:float
Minimum active power variationpossible between two consecutive timestep for each generator: a redispatching
action
on generator `g_id` cannot be below :attr:`GridObjects.gen_ramp_down_min` [`g_id`]. Optional. Used
for unit commitment problems or redispacthing action.
gen_min_uptime: :class:`numpy.ndarray`, dtype:float
The minimum time (expressed in the number of timesteps) a generator needs to be turned on: it's not possible to
turn off generator `gen_id` that has been turned on less than `gen_min_time_on` [`gen_id`] timesteps
ago. Optional. Used
for unit commitment problems or redispacthing action.
gen_min_downtime: :class:`numpy.ndarray`, dtype:float
The minimum time (expressed in the number of timesteps) a generator needs to be turned off: it's not possible to
turn on generator `gen_id` that has been turned off less than `gen_min_time_on` [`gen_id`] timesteps
ago. Optional. Used
for unit commitment problems or redispacthing action.
gen_cost_per_MW: :class:`numpy.ndarray`, dtype:float
For each generator, it gives the "operating cost", eg the cost, in terms of "used currency" for the production
of one MW with this generator, if it is already turned on. It's a positive real number. It's the marginal cost
for each MW. Optional. Used
for unit commitment problems or redispacthing action.
gen_startup_cost: :class:`numpy.ndarray`, dtype:float
The cost to start a generator. It's a positive real number. Optional. Used
for unit commitment problems or redispacthing action.
gen_shutdown_cost: :class:`numpy.ndarray`, dtype:float
The cost to shut down a generator. It's a positive real number. Optional. Used
for unit commitment problems or redispacthing action.
redispatching_unit_commitment_availble: ``bool``
Does the current grid allow for redispatching and / or unit commit problem. If not, any attempt to use it
will raise a :class:`grid2op.Exceptions.UnitCommitorRedispachingNotAvailable` error.
For an environment to be compatible with this feature, you need to set up, when loading the backend:
- :attr:`GridObjects.gen_type`
- :attr:`GridObjects.gen_pmin`
- :attr:`GridObjects.gen_pmax`
- :attr:`GridObjects.gen_redispatchable`
- :attr:`GridObjects.gen_max_ramp_up`
- :attr:`GridObjects.gen_max_ramp_down`
- :attr:`GridObjects.gen_min_uptime`
- :attr:`GridObjects.gen_min_downtime`
- :attr:`GridObjects.gen_cost_per_MW`
- :attr:`GridObjects.gen_startup_cost`
- :attr:`GridObjects.gen_shutdown_cost`
grid_layout: ``dict`` or ``None``
The layout of the powergrid in a form of a dictionnary with keys the substation name, and value a tuple of
the coordinate of this substation. If no layout are provided, it defaults to ``None``
shunts_data_available: ``bool``
Whether or not the backend support the shunt data.
n_shunt: ``int`` or ``None``
Number of shunts on the grid. It might be ``None`` if the backend does not support shunts.
name_shunt: ``numpy.ndarray``, dtype:``str`` or ``None``
Name of each shunt on the grid, or ``None`` if the backend does not support shunts.
shunt_to_subid: :class:`numpy.ndarray`, dtype:int
for each shunt (if supported), gives the id the substation to which it is connected
"""
attr_list_vect = None
attr_list_set = {}
# class been init
# __is_init = False
# name of the objects
env_name = "unknown"
name_load = None
name_gen = None
name_line = None
name_sub = None
n_gen = -1
n_load = -1
n_line = -1
n_sub = -1
sub_info = None
dim_topo = -1
# to which substation is connected each element
load_to_subid = None
gen_to_subid = None
line_or_to_subid = None
line_ex_to_subid = None
# which index has this element in the substation vector
load_to_sub_pos = None
gen_to_sub_pos = None
line_or_to_sub_pos = None
line_ex_to_sub_pos = None
# which index has this element in the topology vector
load_pos_topo_vect = None
gen_pos_topo_vect = None
line_or_pos_topo_vect = None
line_ex_pos_topo_vect = None
# list of attribute to convert it from/to a vector
_vectorized = None
# for redispatching / unit commitment
# TODO = "TODO COMPLETE THAT BELLOW!!! AND UPDATE THE init methods"
_li_attr_disp = ["gen_type", "gen_pmin", "gen_pmax", "gen_redispatchable", "gen_max_ramp_up",
"gen_max_ramp_down", "gen_min_uptime", "gen_min_downtime", "gen_cost_per_MW",
"gen_startup_cost", "gen_shutdown_cost"]
_type_attr_disp = [str, float, float, bool, float, float, int, int, float, float, float]
# redispatch data, not available in all environment
redispatching_unit_commitment_availble = False
gen_type = None
gen_pmin = None
gen_pmax = None
gen_redispatchable = None
gen_max_ramp_up = None
gen_max_ramp_down = None
gen_min_uptime = None
gen_min_downtime = None
gen_cost_per_MW = None # marginal cost
gen_startup_cost = None # start cost
gen_shutdown_cost = None # shutdown cost
# grid layout
grid_layout = None
# shunt data, not available in every bakend
shunts_data_available = False
n_shunt = None
name_shunt = None
shunt_to_subid = None
def __init__(self):
pass
@classmethod
def _update_value_set(cls):
"""
Update the class attribute `attr_list_vect_set` from `attr_list_vect`
"""
cls.attr_list_set = set(cls.attr_list_vect)
def _raise_error_attr_list_none(self):
"""
Raise a "NotImplementedError" if :attr:`GridObjects.attr_list_vect` is not defined.
Raises
-------
``NotImplementedError``
"""
if self.attr_list_vect is None:
raise NotImplementedError("attr_list_vect attribute is not defined for class {}. "
"It is not possible to convert it from/to a vector, "
"nor to know its size, shape or dtype.".format(type(self)))
def _get_array_from_attr_name(self, attr_name):
"""
This function returns the proper attribute vector that can be inspected in the
:func:`GridObject.shape`, :func:`GridObject.size`, :func:`GridObject.dtype`,
:func:`GridObject.from_vect` and :func:`GridObject.to_vect` method.
If this function is overloaded, then the _assign_attr_from_name must be too.
Parameters
----------
attr_name: ``str``
Name of the attribute to inspect or set
Returns
-------
res: ``numpy.ndarray``
The attribute corresponding the name, flatten as a 1d vector.
"""
return np.array(getattr(self, attr_name)).flatten()
def to_vect(self):
"""
Convert this instance of GridObjects to a numpy ndarray.
The size of the array is always the same and is determined by the :func:`GridObject.size` method.
**NB**: in case the class GridObjects is derived,
either :attr:`GridObjects.attr_list_vect` is properly defined for the derived class, or this function must be
redefined.
Returns
-------
res: ``numpy.ndarray``
The representation of this action as a flat numpy ndarray
"""
if self._vectorized is None:
self._raise_error_attr_list_none()
li_vect = [self._get_array_from_attr_name(el).astype(dt_float) for el in self.attr_list_vect]
if li_vect:
self._vectorized = np.concatenate(li_vect)
else:
self._vectorized = np.array([], dtype=dt_float)
return self._vectorized
def shape(self):
"""
The shapes of all the components of the action, mainly used for gym compatibility is the shape of all
part of the action.
It is a numpy integer array.
This function must return a vector from which the sum is equal to the return value of "size()".
The shape vector must have the same number of components as the return value of the :func:`GridObjects.dtype()`
vector.
**NB**: in case the class GridObjects is derived,
either :attr:`GridObjects.attr_list_vect` is properly defined for the derived class, or this function must be
redefined.
Returns
-------
res: ``numpy.ndarray``
The shape of the :class:`GridObjects`
"""
self._raise_error_attr_list_none()
res = np.array([self._get_array_from_attr_name(el).shape[0] for el in self.attr_list_vect])
return res
def dtype(self):
"""
The types of the components of the GridObjects, mainly used for gym compatibility is the shape of all part
of the action.
It is a numpy array of objects.
The dtype vector must have the same number of components as the return value of the :func:`GridObjects.shape`
vector.
**NB**: in case the class GridObjects is derived,
either :attr:`GridObjects.attr_list_vect` is properly defined for the derived class, or this function must be
redefined.
Returns
-------
res: ``numpy.ndarray``
The dtype of the :class:`GridObjects`
"""
self._raise_error_attr_list_none()
res = np.array([self._get_array_from_attr_name(el).dtype for el in self.attr_list_vect])
return res
def _assign_attr_from_name(self, attr_nm, vect):
"""
Assign the proper attributes with name 'attr_nm' with the value of the vector vect
If this function is overloaded, then the _get_array_from_attr_name must be too.
Parameters
----------
attr_nm
vect
Returns
-------
``None``
"""
tmp = getattr(self, attr_nm)
if isinstance(tmp, (dt_bool, dt_int, dt_float)):
setattr(self, attr_nm, vect)
else:
tmp[:] = vect
def check_space_legit(self):
pass
def from_vect(self, vect):
"""
Convert a GridObjects, represented as a vector, into an GridObjects object.
**NB**: in case the class GridObjects is derived,
either :attr:`GridObjects.attr_list_vect` is properly defined for the derived class, or this function must be
redefined.
Only the size is checked. If it does not match, an :class:`grid2op.Exceptions.AmbiguousAction` is thrown.
Otherwise the component of the vector are coerced into the proper type silently.
It may results in an non deterministic behaviour if the input vector is not a real action, or cannot be
converted to one.
Parameters
----------
vect: ``numpy.ndarray``
A vector representing an BaseAction.
"""
if vect.shape[0] != self.size():
raise IncorrectNumberOfElements("Incorrect number of elements found while load a GridObjects "
"from a vector. Found {} elements instead of {}".format(
vect.shape[0], self.size()))
if np.any(~np.isfinite(vect)):
raise AmbiguousAction("The action you provided contained not finite number. It cannot be converted to an"
" action class.")
self._raise_error_attr_list_none()
prev_ = 0
for attr_nm, sh, dt in zip(self.attr_list_vect, self.shape(), self.dtype()):
self._assign_attr_from_name(attr_nm, vect[prev_:(prev_ + sh)].astype(dt))
prev_ += sh
self.check_space_legit()
def size(self):
"""
When the action is converted to a vector, this method return its size.
NB that it is a requirement that converting an GridObjects gives a vector of a fixed size throughout a training.
**NB**: in case the class GridObjects is derived,
either :attr:`GridObjects.attr_list_vect` is properly defined for the derived class, or this function must be
redefined.
Returns
-------
size: ``int``
The size of the GridObjects if it's converted to a flat vector.
"""
res = np.sum(self.shape())
return res
def _aux_pos_big_topo(self, vect_to_subid, vect_to_sub_pos):
"""
Return the proper "_pos_big_topo" vector given "to_subid" vector and "to_sub_pos" vectors.
This function is also called to performed sanity check after the load on the powergrid.
:param vect_to_subid: vector of int giving the id of the topology for this element
:type vect_to_subid: iterable int
:param vect_to_sub_pos: vector of int giving the id IN THE SUBSTATION for this element
:type vect_to_sub_pos: iterable int
:return:
"""
res = np.zeros(shape=vect_to_subid.shape, dtype=dt_int)
for i, (sub_id, my_pos) in enumerate(zip(vect_to_subid, vect_to_sub_pos)):
obj_before = np.sum(self.sub_info[:sub_id])
res[i] = obj_before + my_pos
return res
def _compute_pos_big_topo(self):
"""
Compute the position of each element in the big topological vector.
Topology action are represented by numpy vector of size np.sum(self.sub_info).
The vector self.load_pos_topo_vect will give the index of each load in this big topology vector.
For examaple, for load i, self.load_pos_topo_vect[i] gives the position in such a topology vector that
affect this load.
This position can be automatically deduced from self.sub_info, self.load_to_subid and self.load_to_sub_pos.
This is the same for generators and both end of powerlines
:return: ``None``
"""
# self.assert_grid_correct()
self.load_pos_topo_vect = self._aux_pos_big_topo(self.load_to_subid, self.load_to_sub_pos).astype(dt_int)
self.gen_pos_topo_vect = self._aux_pos_big_topo(self.gen_to_subid, self.gen_to_sub_pos).astype(dt_int)
self.line_or_pos_topo_vect = self._aux_pos_big_topo(self.line_or_to_subid, self.line_or_to_sub_pos).astype(dt_int)
self.line_ex_pos_topo_vect = self._aux_pos_big_topo(self.line_ex_to_subid, self.line_ex_to_sub_pos).astype(dt_int)
def assert_grid_correct(self):
"""
Performs some checking on the loaded _grid to make sure it is consistent.
It also makes sure that the vector such as *sub_info*, *load_to_subid* or *gen_to_sub_pos* are of the
right type eg. numpy.ndarray with dtype: dt_int
It is called after the _grid has been loaded.
These function is by default called by the :class:`grid2op.Environment` class after the initialization of the
environment.
If these tests are not successfull, no guarantee are given that the backend will return consistent computations.
In order for the backend to fully understand the structure of actions, it is strongly advised NOT to override
this method.
:return: ``None``
:raise: :class:`grid2op.EnvError` and possibly all of its derived class.
"""
if self.name_line is None:
raise EnvError("name_line is None. Powergrid is invalid. Line names are used to make the correspondance "
"between the chronics and the backend")
if self.name_load is None:
raise EnvError("name_load is None. Powergrid is invalid. Line names are used to make the correspondance "
"between the chronics and the backend")
if self.name_gen is None:
raise EnvError("name_gen is None. Powergrid is invalid. Line names are used to make the correspondance "
"between the chronics and the backend")
if self.name_sub is None:
raise EnvError("name_sub is None. Powergrid is invalid. Substation names are used to make the "
"correspondance between the chronics and the backend")
if self.n_gen <= 0:
raise EnvError("n_gen is negative. Powergrid is invalid: there are no generator")
if self.n_load <= 0:
raise EnvError("n_load is negative. Powergrid is invalid: there are no load")
if self.n_line <= 0:
raise EnvError("n_line is negative. Powergrid is invalid: there are no line")
if self.n_sub <= 0:
raise EnvError("n_sub is negative. Powergrid is invalid: there are no substation")
# test if vector can be properly converted
if not isinstance(self.sub_info, np.ndarray):
try:
self.sub_info = np.array(self.sub_info)
self.sub_info = self.sub_info.astype(dt_int)
except Exception as e:
raise EnvError("self.sub_info should be convertible to a numpy array")
if not isinstance(self.load_to_subid, np.ndarray):
try:
self.load_to_subid = np.array(self.load_to_subid)
self.load_to_subid = self.load_to_subid.astype(dt_int)
except Exception as e:
raise EnvError("self.load_to_subid should be convertible to a numpy array")
if not isinstance(self.gen_to_subid, np.ndarray):
try:
self.gen_to_subid = np.array(self.gen_to_subid)
self.gen_to_subid = self.gen_to_subid.astype(dt_int)
except Exception as e:
raise EnvError("self.gen_to_subid should be convertible to a numpy array")
if not isinstance(self.line_or_to_subid, np.ndarray):
try:
self.line_or_to_subid = np.array(self.line_or_to_subid)
self.line_or_to_subid = self.line_or_to_subid .astype(dt_int)
except Exception as e:
raise EnvError("self.line_or_to_subid should be convertible to a numpy array")
if not isinstance(self.line_ex_to_subid, np.ndarray):
try:
self.line_ex_to_subid = np.array(self.line_ex_to_subid)
self.line_ex_to_subid = self.line_ex_to_subid.astype(dt_int)
except Exception as e:
raise EnvError("self.line_ex_to_subid should be convertible to a numpy array")
if not isinstance(self.load_to_sub_pos, np.ndarray):
try:
self.load_to_sub_pos = np.array(self.load_to_sub_pos)
self.load_to_sub_pos = self.load_to_sub_pos.astype(dt_int)
except Exception as e:
raise EnvError("self.load_to_sub_pos should be convertible to a numpy array")
if not isinstance(self.gen_to_sub_pos, np.ndarray):
try:
self.gen_to_sub_pos = np.array(self.gen_to_sub_pos)
self.gen_to_sub_pos = self.gen_to_sub_pos.astype(dt_int)
except Exception as e:
raise EnvError("self.gen_to_sub_pos should be convertible to a numpy array")
if not isinstance(self.line_or_to_sub_pos, np.ndarray):
try:
self.line_or_to_sub_pos = np.array(self.line_or_to_sub_pos)
self.line_or_to_sub_pos = self.line_or_to_sub_pos.astype(dt_int)
except Exception as e:
raise EnvError("self.line_or_to_sub_pos should be convertible to a numpy array")
if not isinstance(self.line_ex_to_sub_pos, np.ndarray):
try:
self.line_ex_to_sub_pos = np.array(self.line_ex_to_sub_pos)
self.line_ex_to_sub_pos = self.line_ex_to_sub_pos .astype(dt_int)
except Exception as e:
raise EnvError("self.line_ex_to_sub_pos should be convertible to a numpy array")
if not isinstance(self.load_pos_topo_vect, np.ndarray):
try:
self.load_pos_topo_vect = np.array(self.load_pos_topo_vect)
self.load_pos_topo_vect = self.load_pos_topo_vect.astype(dt_int)
except Exception as e:
raise EnvError("self.load_pos_topo_vect should be convertible to a numpy array")
if not isinstance(self.gen_pos_topo_vect, np.ndarray):
try:
self.gen_pos_topo_vect = np.array(self.gen_pos_topo_vect)
self.gen_pos_topo_vect = self.gen_pos_topo_vect.astype(dt_int)
except Exception as e:
raise EnvError("self.gen_pos_topo_vect should be convertible to a numpy array")
if not isinstance(self.line_or_pos_topo_vect, np.ndarray):
try:
self.line_or_pos_topo_vect = np.array(self.line_or_pos_topo_vect)
self.line_or_pos_topo_vect = self.line_or_pos_topo_vect.astype(dt_int)
except Exception as e:
raise EnvError("self.line_or_pos_topo_vect should be convertible to a numpy array")
if not isinstance(self.line_ex_pos_topo_vect, np.ndarray):
try:
self.line_ex_pos_topo_vect = np.array(self.line_ex_pos_topo_vect)
self.line_ex_pos_topo_vect = self.line_ex_pos_topo_vect.astype(dt_int)
except Exception as e:
raise EnvError("self.line_ex_pos_topo_vect should be convertible to a numpy array")
# test that all numbers are finite:
tmp = np.concatenate((
self.sub_info.flatten(),
self.load_to_subid.flatten(),
self.gen_to_subid.flatten(),
self.line_or_to_subid.flatten(),
self.line_ex_to_subid.flatten(),
self.load_to_sub_pos.flatten(),
self.gen_to_sub_pos.flatten(),
self.line_or_to_sub_pos.flatten(),
self.line_ex_to_sub_pos.flatten(),
self.load_pos_topo_vect.flatten(),
self.gen_pos_topo_vect.flatten(),
self.line_or_pos_topo_vect.flatten(),
self.line_ex_pos_topo_vect.flatten()
))
try:
if np.any(~np.isfinite(tmp)):
raise EnvError("One of the vector is made of non finite elements")
except Exception as e:
raise EnvError("Impossible to check wheter or not vectors contains online finite elements (pobably one "
"or more topology related vector is not valid (None)")
# check sizes
if len(self.sub_info) != self.n_sub:
raise IncorrectNumberOfSubstation("The number of substation is not consistent in "
"self.sub_info (size \"{}\")"
" and self.n_sub ({})".format(len(self.sub_info), self.n_sub))
if np.sum(self.sub_info) != self.n_load + self.n_gen + 2*self.n_line:
err_msg = "The number of elements of elements is not consistent between self.sub_info where there are "
err_msg += "{} elements connected to all substations and the number of load, generators and lines in " \
"the _grid."
err_msg = err_msg.format(np.sum(self.sub_info))
raise IncorrectNumberOfElements(err_msg)
if len(self.load_to_subid) != self.n_load:
raise IncorrectNumberOfLoads()
if np.min(self.load_to_subid) < 0:
raise EnvError("Some shunt is connected to a negative substation id.")
if np.max(self.load_to_subid) > self.n_sub:
raise EnvError("Some load is supposed to be connected to substations with id {} which"
"is greater than the number of substations of the grid, which is {}."
"".format(np.max(self.load_to_subid), self.n_sub))
if len(self.gen_to_subid) != self.n_gen:
raise IncorrectNumberOfGenerators()
if np.min(self.gen_to_subid) < 0:
raise EnvError("Some shunt is connected to a negative substation id.")
if np.max(self.gen_to_subid) > self.n_sub:
raise EnvError("Some generator is supposed to be connected to substations with id {} which"
"is greater than the number of substations of the grid, which is {}."
"".format(np.max(self.gen_to_subid), self.n_sub))
if len(self.line_or_to_subid) != self.n_line:
raise IncorrectNumberOfLines()
if np.min(self.line_or_to_subid) < 0:
raise EnvError("Some shunt is connected to a negative substation id.")
if np.max(self.line_or_to_subid) > self.n_sub:
raise EnvError("Some powerline (or) is supposed to be connected to substations with id {} which"
"is greater than the number of substations of the grid, which is {}."
"".format(np.max(self.line_or_to_subid), self.n_sub))
if len(self.line_ex_to_subid) != self.n_line:
raise IncorrectNumberOfLines()
if np.min(self.line_ex_to_subid) < 0:
raise EnvError("Some shunt is connected to a negative substation id.")
if np.max(self.line_ex_to_subid) > self.n_sub:
raise EnvError("Some powerline (ex) is supposed to be connected to substations with id {} which"
"is greater than the number of substations of the grid, which is {}."
"".format(np.max(self.line_or_to_subid), self.n_sub))
if len(self.load_to_sub_pos) != self.n_load:
raise IncorrectNumberOfLoads()
if len(self.gen_to_sub_pos) != self.n_gen:
raise IncorrectNumberOfGenerators()
if len(self.line_or_to_sub_pos) != self.n_line:
raise IncorrectNumberOfLines()
if len(self.line_ex_to_sub_pos) != self.n_line:
raise IncorrectNumberOfLines()
if len(self.load_pos_topo_vect) != self.n_load:
raise IncorrectNumberOfLoads()
if len(self.gen_pos_topo_vect) != self.n_gen:
raise IncorrectNumberOfGenerators()
if len(self.line_or_pos_topo_vect) != self.n_line:
raise IncorrectNumberOfLines()
if len(self.line_ex_pos_topo_vect) != self.n_line:
raise IncorrectNumberOfLines()
# test if object are connected to right substation
obj_per_sub = np.zeros(shape=(self.n_sub,), dtype=dt_int)
for sub_id in self.load_to_subid:
obj_per_sub[sub_id] += 1
for sub_id in self.gen_to_subid:
obj_per_sub[sub_id] += 1
for sub_id in self.line_or_to_subid:
obj_per_sub[sub_id] += 1
for sub_id in self.line_ex_to_subid:
obj_per_sub[sub_id] += 1
if not np.all(obj_per_sub == self.sub_info):
raise IncorrectNumberOfElements()
# test right number of element in substations
# test that for each substation i don't have an id above the number of element of a substations
for i, (sub_id, sub_pos) in enumerate(zip(self.load_to_subid, self.load_to_sub_pos)):
if sub_pos >= self.sub_info[sub_id]:
raise IncorrectPositionOfLoads("for load {}".format(i))
for i, (sub_id, sub_pos) in enumerate(zip(self.gen_to_subid, self.gen_to_sub_pos)):
if sub_pos >= self.sub_info[sub_id]:
raise IncorrectPositionOfGenerators("for generator {}".format(i))
for i, (sub_id, sub_pos) in enumerate(zip(self.line_or_to_subid, self.line_or_to_sub_pos)):
if sub_pos >= self.sub_info[sub_id]:
raise IncorrectPositionOfLines("for line {} at origin end".format(i))
for i, (sub_id, sub_pos) in enumerate(zip(self.line_ex_to_subid, self.line_ex_to_sub_pos)):
if sub_pos >= self.sub_info[sub_id]:
# pdb.set_trace()
raise IncorrectPositionOfLines("for line {} at extremity end".format(i))
# check that i don't have 2 objects with the same id in the "big topo" vector
if len(np.unique(np.concatenate((self.load_pos_topo_vect.flatten(),
self.gen_pos_topo_vect.flatten(),
self.line_or_pos_topo_vect.flatten(),
self.line_ex_pos_topo_vect.flatten())))) != np.sum(self.sub_info):
raise EnvError("2 different objects would have the same id in the topology vector.")
# check that self.load_pos_topo_vect and co are consistent
load_pos_big_topo = self._aux_pos_big_topo(self.load_to_subid, self.load_to_sub_pos)
if not np.all(load_pos_big_topo == self.load_pos_topo_vect):
raise IncorrectPositionOfLoads()
gen_pos_big_topo = self._aux_pos_big_topo(self.gen_to_subid, self.gen_to_sub_pos)
if not np.all(gen_pos_big_topo == self.gen_pos_topo_vect):
raise IncorrectNumberOfGenerators()
lines_or_pos_big_topo = self._aux_pos_big_topo(self.line_or_to_subid, self.line_or_to_sub_pos)
if not np.all(lines_or_pos_big_topo == self.line_or_pos_topo_vect):
raise IncorrectPositionOfLines()
lines_ex_pos_big_topo = self._aux_pos_big_topo(self.line_ex_to_subid, self.line_ex_to_sub_pos)
if not np.all(lines_ex_pos_big_topo == self.line_ex_pos_topo_vect):
raise IncorrectPositionOfLines()
# no empty bus: at least one element should be present on each bus
if np.any(self.sub_info < 1):
raise BackendError("There are {} bus with 0 element connected to it.".format(np.sum(self.sub_info < 1)))
# redispatching / unit commitment
if self.redispatching_unit_commitment_availble:
if self.gen_type is None:
raise InvalidRedispatching("Impossible to recognize the type of generators (gen_type) when "
"redispatching is supposed to be available.")
if self.gen_pmin is None:
raise InvalidRedispatching("Impossible to recognize the pmin of generators (gen_pmin) when "
"redispatching is supposed to be available.")
if self.gen_pmax is None:
raise InvalidRedispatching("Impossible to recognize the pmax of generators (gen_pmax) when "
"redispatching is supposed to be available.")
if self.gen_redispatchable is None:
raise InvalidRedispatching("Impossible to know which generator can be dispatched (gen_redispatchable)"
" when redispatching is supposed to be available.")
if self.gen_max_ramp_up is None:
raise InvalidRedispatching("Impossible to recognize the ramp up of generators (gen_max_ramp_up)"
" when redispatching is supposed to be available.")
if self.gen_max_ramp_down is None:
raise InvalidRedispatching("Impossible to recognize the ramp up of generators (gen_max_ramp_down)"
" when redispatching is supposed to be available.")
if self.gen_min_uptime is None:
raise InvalidRedispatching("Impossible to recognize the min uptime of generators (gen_min_uptime)"
" when redispatching is supposed to be available.")
if self.gen_min_downtime is None:
raise InvalidRedispatching("Impossible to recognize the min downtime of generators (gen_min_downtime)"
" when redispatching is supposed to be available.")
if self.gen_cost_per_MW is None:
raise InvalidRedispatching("Impossible to recognize the marginal costs of generators (gen_cost_per_MW)"
" when redispatching is supposed to be available.")
if self.gen_startup_cost is None:
raise InvalidRedispatching("Impossible to recognize the start up cost of generators (gen_startup_cost)"
" when redispatching is supposed to be available.")
if self.gen_shutdown_cost is None:
raise InvalidRedispatching("Impossible to recognize the shut down cost of generators "
"(gen_shutdown_cost) when redispatching is supposed to be available.")
if len(self.gen_type) != self.n_gen:
raise InvalidRedispatching("Invalid length for the type of generators (gen_type) when "
"redispatching is supposed to be available.")
if len(self.gen_pmin) != self.n_gen:
raise InvalidRedispatching("Invalid length for the pmin of generators (gen_pmin) when "
"redispatching is supposed to be available.")
if len(self.gen_pmax) != self.n_gen:
raise InvalidRedispatching("Invalid length for the pmax of generators (gen_pmax) when "
"redispatching is supposed to be available.")
if len(self.gen_redispatchable) != self.n_gen:
raise InvalidRedispatching("Invalid length for which generator can be dispatched (gen_redispatchable)"
" when redispatching is supposed to be available.")
if len(self.gen_max_ramp_up) != self.n_gen:
raise InvalidRedispatching("Invalid length for the ramp up of generators (gen_max_ramp_up)"
" when redispatching is supposed to be available.")
if len(self.gen_max_ramp_down) != self.n_gen:
raise InvalidRedispatching("Invalid length for the ramp up of generators (gen_max_ramp_down)"
" when redispatching is supposed to be available.")
if len(self.gen_min_uptime) != self.n_gen:
raise InvalidRedispatching("Invalid length for the min uptime of generators (gen_min_uptime)"
" when redispatching is supposed to be available.")
if len(self.gen_min_downtime) != self.n_gen:
raise InvalidRedispatching("Invalid length for the min downtime of generators (gen_min_downtime)"
" when redispatching is supposed to be available.")
if len(self.gen_cost_per_MW) != self.n_gen:
raise InvalidRedispatching("Invalid length for the marginal costs of generators (gen_cost_per_MW)"
" when redispatching is supposed to be available.")
if len(self.gen_startup_cost) != self.n_gen:
raise InvalidRedispatching("Invalid length for the start up cost of generators (gen_startup_cost)"
" when redispatching is supposed to be available.")
if len(self.gen_shutdown_cost) != self.n_gen:
raise InvalidRedispatching("Invalid length for the shut down cost of generators "
"(gen_shutdown_cost) when redispatching is supposed to be available.")