Creating a new backend

This page is organized as follow:

Objectives

Warning

Backends are internal to grid2op.

This page details how to create a backend from an available solver (a “things” that is able to compute flows and voltages). This is an advanced usage.

You will also find in this file the complete description on how the “powergrid” is represented in grid2op.

Backend is an abstraction that represents the physical system (the powergrid). In theory every powerflow can be used as a backend. And example of a backend using Pandapower is available with the grid2op.Backend.EducPandaPowerBackend.EducPandaPowerBackend (only for demonstration purpose)

If you want working backend, please use the grid2op.Backend.PandaPowerBackend that uses Pandapower and a port in c++ to a subset of pandapower called LightSim2Grid .

To implement completely a backend, you should implement all the abstract function defined here Backend. This file is an overview of what is needed and aims at contextualizing these requirements to make them clearer and easier to implement.

This section assume you have already a “code” that is able to compute some powerflow given a file stored somewhere, typically on a hard drive representing a powergrid. If you have that, you can probably use it to implement a grid2op backend and benefit from the whole grid2op ecosystem (code once a backend, reuse your “powerflow” everywhere). This includes, but is not limited to:

  • Save “logs” of your experiment in a standard format (with the runner) to be reused and analyzed graphically with grid2viz

  • Save “logs” of your experiments and compare the results with “reference” solver available

  • Act on your powergrid with a unified and “somewhat standard” fashion thanks to grid2op actions

  • Reuse agents that other people have trained in the context of L2RPN competitions

  • Train new grid controlers using the grid2op gym_compat module

  • etc.

Note

Grid2Op do not care about the modeling of the grid (static / steady state or dyanmic / transient) and both Any types of solver could be implemented as backend.

At time of writing (december 2020), only steady state powerflow are available.

Note

The previous note entails that grid2op is also independent on the format used to store a powergrid. It’s expected that the “backend” fails to initialize if it cannot found any file to initialize the grid.

At time of writing (december 2020), all environments are shipped with grid represented as “json” format, which is the default format for the default backend based on PandaPower. If you want your “Backend” to have support for all previous environment, you might need to initialize it from this format or convert the IEEE files to the representation that suits you.

Note

If your backend is somehow available in a c++ library (static or dynamic) and you can link program against it, you can use the interface “lightsim2grid” if you don’t want to worry about grid2op representation (and skip this entire files)

Indeed, lightsim2grid takes care of of exposing a model of the grid (Ybus and Sbus) from the grid2op data and is able to directly expose the results from the internal state to valid grid2op vectors.

It has the advantage of being able to read the data from the default grid2op backend (based on PandaPower) and to allow to focus on the solver rather to focus on grid2op “representation” of the grid. It is also optimize for speed and (work in progress) aims at not copying any data from python -> c++ -> python when it can be avoided.

The code is also relatively well organized in:

  • modification of the elements

  • generation of the Ybus (sparse) complex matrix and Sbus complex vector

  • solving for the complex voltage V (and part of the Sbus vector) the equation V.(Ybus.V)* = Sbus with the “standard” “powerflow constraints”

  • computing the active power, reactive power, flow on powerllines etc. from the V

So you if you have a solver that can somehow solves the equation V.(Ybus.V)* = Sbus (and meeting the constraints of a standard powerflow) then lightsim2grid might be a good way to make this solver available in grid2op easily.

On the other hands, this “backend” also comes with a special model of each powersystem elements (loads, generators, lines, transformers or shunts for example) that you cannot modify and has some limitation to what it supports.

Main methods to implement

Typically, a backend has a internal “modeling” / “representation” of the powergrid stored in the attribute self._grid that can be anything.

Note

self._grid is a “private” attribute. Only people that knows what it does and how it works should be able to use it.

Grid2op being fully generic, you can assume that all the classes of grid2op will never access self._grid. For example, when building the observation of the grid, grid2op will only use the information given in the *_infos() methods (eg grid2op.Backend.Backend.loads_info()) and never by directly accessing self._grid

In other words, self._grid can be anything: a PandaPower Network, a GridCal MultiCircuit, a lightsim2grid GridModel, a pypowsybl Network (or SortedNetwork), a powerfactory <https://www.digsilent.de/en/scripting-and-automation.html> Project etc. Grid2op will never attempt to access self._grid

(Though, to be perfectly honest, some agents might rely on some type _grid, if that’s the case, too bad for these agents they will need to implement special methods to be compatible with your backend. Hopefully this should be extremely rare… The whole idea of grid2op being to make the different “entities” (agent, environment, data, backend) as independant as possible this “corner” cases should be rare.)

An more detailed example, with some “working minimal code” is given in the “example/backend_integration” of the grid2op repository.

There are 4 __main__ types of method you need to implement if you want to use a custom powerflow (eg from a physical solver, from a neural network, or any other methods):

Warning

With grid2op version before <1.7.1, you were also required to implement the grid2op.Backend.Backend.copy() method.

As of grid2op >= 1.7.1 this is no longer required. Note however that if you don’t implement it, some features might not be available. This will be the case for eg grid2op.Observation.BaseObservation.simulate() or for grid2op.simulator.Simulator.

load_grid: Grid description

In this section we explicit what attributes need to be implemented to have a valid backend instance. We focus on the attribute of the Backend you have to set. But don’t forget you also need to load a powergrid and store it in the _grid attribute.

Basically the load_grid function would look something like:

def load_grid(self, path=None, filename=None):
    # simply handles different way of inputing the data
    full_path = self.make_complete_path(path, filename)

    # from grid2op 1.10.0 you need to call one of
    self.can_handle_more_than_2_busbar()  # see doc for more information
    OR
    self.cannot_handle_more_than_2_busbar()  # see doc for more information
    # It is important you include it at the top of this method, otherwise you
    # will not have access to self.n_busbar_per_sub

    # load the grid in your favorite format, located at `full_path`:
    self._grid = ... # the way you do that depends on the "solver" you use

    # and now initialize the attributes (see list bellow)
    self.n_line = ...  # number of lines in the grid should be read from self._grid
    self.n_gen = ...  # number of generators in the grid should be read from self._grid
    self.n_load = ...  # number of generators in the grid should be read from self._grid
    self.n_sub = ...  # number of generators in the grid should be read from self._grid

    # other attributes should be read from self._grid (see table below for a full list of the attributes)
    self.load_to_subid = ...
    self.gen_to_subid = ...
    self.line_or_to_subid = ...
    self.line_ex_to_subid = ...

    # and finish the initialization with a call to this function
    self._compute_pos_big_topo()

    # the initial thermal limit
    self.thermal_limit_a = ...

The grid2op attributes that need to be implemented in the grid2op.Backend.Backend.load_grid() function are given in the table bellow:

Name

See paragraph

Type

Size

Description

n_line

Number of elements (n_line, n_load, n_gen, n_sub)

int

NA

Number of powerline on the grid (remember, in grid2op framework a powerline includes both “powerlines” and “transformer”)

n_gen

Number of elements (n_line, n_load, n_gen, n_sub)

int

NA

Number of generators on the grid

n_load

Number of elements (n_line, n_load, n_gen, n_sub)

int

NA

Number of loads on the grid

n_sub

Number of elements (n_line, n_load, n_gen, n_sub)

int

NA

Number of substations on the grid

load_to_subid

Substation id (*_to_subid)

vect, int

n_load

For each load, it gives the substation id to which it is connected

gen_to_subid

Substation id (*_to_subid)

vect, int

n_gen

For each generator, it gives the substation id to which it is connected

line_or_to_subid

Substation id (*_to_subid)

vect, int

n_line

For each powerline, it gives the substation id to which its origin end is connected

line_ex_to_subid

Substation id (*_to_subid)

vect, int

n_line

For each powerline, it gives the substation id to which its extremity end is connected

name_load

vect, str

n_load

(optional) name of each load on the grid [if not set, by default it will be “load_$LoadSubID_$LoadID” for example “load_1_10” if the load with id 10 is connected to substation with id 1]

name_gen

vect, str

n_gen

(optional) name of each generator on the grid [if not set, by default it will be “gen_$GenSubID_$GenID” for example “gen_2_42” if the generator with id 42 is connected to substation with id 2]

name_line

vect, str

n_line

(optional) name of each powerline (and transformers !) on the grid [if not set, by default it will be “$SubOrID_SubExID_LineID” for example “1_4_57” if the powerline with id 57 has its origin side connected to substation with id 1 and its extremity side connected to substation with id 4]

name_sub

vect, str

n_sub

(optional) name of each substation on the grid [if not set, by default it will be “sub_$SubID” for example “sub_41” for the substation with id 41]

sub_info

(optional) Substation information (sub_info, dim_topo)

vect, int

n_sub

(can be automatically set if you don’t initialize it) For each substation, it gives the number of elements connected to it (“elements” here denotes: powerline - and transformer- ends, load or generator)

dim_topo

(optional) Substation information (sub_info, dim_topo)

int

NA

(can be automatically set if you don’t initialize it) Total number of elements on the grid (“elements” here denotes: powerline - and transformer- ends, load or generator)

load_to_sub_pos

(optional) Position in substation (*_to_sub_pos)

vect, int

n_load

(can be automatically set if you don’t initialize it) See the description for more information (“a picture often speaks a thousand words”)

gen_to_sub_pos

(optional) Position in substation (*_to_sub_pos)

vect, int

n_gen

(can be automatically set if you don’t initialize it) See the description for more information (“a picture often speaks a thousand words”)

line_or_to_sub_pos

(optional) Position in substation (*_to_sub_pos)

vect, int

n_line

(can be automatically set if you don’t initialize it) See the description for more information (“a picture often speaks a thousand words”)

line_ex_to_sub_pos

(optional) Position in substation (*_to_sub_pos)

vect, int

n_line

(can be automatically set if you don’t initialize it) See the description for more information (“a picture often speaks a thousand words”)

load_pos_topo_vect

(optional) Position in the topology vector (*_pos_topo_vect)

vect, int

n_load

Automatically set with a call to self._compute_pos_big_topo

gen_pos_topo_vect

(optional) Position in the topology vector (*_pos_topo_vect)

vect, int

n_gen

Automatically set with a call to self._compute_pos_big_topo

line_or_pos_topo_vect

(optional) Position in the topology vector (*_pos_topo_vect)

vect, int

n_line

Automatically set with a call to self._compute_pos_big_topo

line_ex_pos_topo_vect

(optional) Position in the topology vector (*_pos_topo_vect)

vect, int

n_line

Automatically set with a call to self._compute_pos_big_topo

TODO storage doc and shunts doc !

Example on how to set them

Some concrete example on how to create a backend are given in the grid2op.Backend.PandaPowerBackend (for the default Backend) and in the “lightsim2grid” backend (available at https://github.com/BDonnot/lightsim2grid ). Feel free to consult any of these codes for more information.

In this example, we detail what is needed to create a backend and how to set the required attributes.

We explain step by step how to proceed with this powergid:

5subs_grid_layout

Prerequisite: Order and label everything

The first step is to give names and order to every object on the loaded grid.

For example, you can first assign order to substations this way:

5subs_grid_1_sub

Warning

To be consistent with python ecosystem, index are 0 based. So the first element should have id 0 (and not 1)

Then you decide an ordering of the loads:

5subs_grid_2_loads

Then the generators:

5subs_grid_3_gens

And then you deal with the powerlines. Which is a bit more “complex” as you need also to “assign side” ( “extremity” or “origin”) to each powerline end which is to define an “origin” end and and “extremity” end. This result in a possible ordering this way:

5subs_grid_4_lines

Optionally you also need to come up with a way of assign to each “element” an order in the substation. This is an extremely complex way to say you have to do this:

5subs_grid_5_obj_in_sub

Note the number for each element in the substation.

In this example, for substaion with id 0 (bottom left) you decided that the powerline with id 0 (connected at this substation at its origin side) will be the “first object of this substation”. Then the “Load 0” is the second object [remember index a 0 based, so the second object has id 1], generator 0 is the third object of this substation (you can know it with the “3” near it) etc.

Note

Grid2op assumes that if the same files is loaded multiple times, then the same grid is defined by the backend. This entails that the loads are in the same order, substations are in the same order, generators are in the same order, powerline are in the same order (and for each powerrline is oriented the same way: same “origin” and same “extremity”).

This powergrid will be used throughout this example. And in the next sections, we suppose that you have chosen a way to assign all these “order”.

Note

The order of the elements has absolutely no impact whatsoever on the solver and the state of the grid. In other words flows, voltages etc. do not depend on this (arbitrary) order.

We could have chosen a different representation of this data (for example by identifying objects with names instead of ID in vector) but it turns out this “index based representation” is extremely fast as it allows manipulation of most data using the numpy package.

This is a reason why grid2op is relatively fast in most cases: very little time is taken to map objects to there properties.

Note

This can be done automatically if you don’t want to take care of this labelling.

If you chose to adopt the “automatic” way it will result in the following ordering:

  • load (if any is connected to this substation) will be labeled first

  • gen will be labeled just after

  • then origin side of powerline

  • then extremity side of powerline

This will result in a different labelling that the one we adopted here! So the vector *_to_sub_pos will be different, and so will the *_to_pos_topo_vect

Final result

For the most impatient readers, the final representation is :

5subs_grid_layout_with_repr

In the next paragraphs we detail step by step why this is this way.

Number of elements (n_line, n_load, n_gen, n_sub)

Nothing much to say here, you count each object and assign the right val to the attributes. This gives:

5subs_grid_n_el

For example, n_line is 8 because there are 8 lines on the grid, labeled from 0 to 7.

Substation id (*_to_subid)

The attribute explained in this section are load_to_subid, gen_to_subid, line_or_to_subid and line_ex_to_subid.

Again, for each of these vector, you specify to which substation the objects are connected. For example, for the loads this gives:

5subs_grid_load_to_subid

Indeed, the load with id 0 is connected to substation with id 0, load with id 1 is connected to substation with id 3 and load with id 2 is connected to substation with id 4.

For the other attributes, you follow the same pattern:

5subs_grid_el_to_subid

(optional) Substation information (sub_info, dim_topo)

New in version 1.3.2: This is now done automatically if the user do not set it.

For these attributes too, there is nothing really surprising (we remember that these can be set automatically if you don’t do it). We show how to set them mainly to explain grid2op “encoding” for these attributes that you might want to reuse somewhere else.

For each component of sub_info you tell grid2op of the number of elements connected to it. And then you sum up each of these elements in the dim_topo attributes.

5subs_grid_5_sub_i

Note

Only the loads, line ends (“origin” or “extremity”) and generators are counted as “elements”.

(optional) Position in substation (*_to_sub_pos)

New in version 1.3.2: This is now done automatically if the user do not set it.

These are the least common (and “most complicated”) attributes to set. This is why we implemented a function that takes care of computing it if you need. This function will make this choice, for each substation:

  • load (if any is connected to this substation) will be labeled first

  • gen will be labeled just after

  • then origin side of powerline

  • then extremity side of powerline

You are free to chose any other ordering, and this is why we explain in detail how this feature works here.

Note

It’s a “take all or nothing” feature. It means that you either take the full ordering detailed above or you need to implement a complete ordering yourself.

This values allow to uniquely identified, inside each substation. These were represented by the “small” number near each element in the last image of the introductory paragraph Prerequisite: Order and label everything. If you have that image in mind, it’s simple: you set the number of each elements into its vector. And that is it.

If you are confused, we made a detailed example below.

First, have a look at substation 0:

5subs_grid_sub0

You know that, at this substation 0 there are 6 elements connected. In this example, these are:

  • origin side of Line 0

  • Load 0

  • gen 0

  • origin side of line 1

  • origin side of line 2

  • origin side of line 3

Given that, you can fill:

  • first component of line_or_to_sub_pos [origin of line 0 is connected at this substation]

  • first component of load_to_sub_pos [Load 0 is connected at this substation]

  • first component of gen_to_sub_pos [gen 0 is connected at this substation]

  • second component of line_or_to_sub_pos [origin of line 1 is connected at this substation]

  • third component of line_or_to_sub_pos [origin of line 2 is connected at this substation]

  • fourth component of line_or_to_sub_pos [origin of line 2 is connected at this substation]

These are indicated with the “??” on the figure above. (note that the XX cannot be set right now)

Once you know that, you just have to recall that you already give an order to each of these objects. You defined (in a purely arbitrary manner):

  • the element 0 of this substation to be “origin of line 0”

  • the element 1 of this substation to be “load 0”

  • the element 2 of this substation to be “gen 0”

  • the element 3 of this substation to be “origin of line 3”

  • the element 4 of this substation to be “origin of line 2”

  • the element 5 of this substation to be “origin of line 1”

So you get:

  • first component of line_or_to_sub_pos is 0 [because “origin side of line 0” is “element 0” of this substation]

  • first component of load_to_sub_pos is 1 [because “load 0” is “element 1” of this substation]

  • first component of gen_to_sub_pos is 2 [because “gen 0” is “element 2” of this substation]

  • fourth component of line_or_to_sub_pos is 3 [because “origin side of line 3” is “element 3” of this substation]

  • third component of line_or_to_sub_pos is 4 [because “origin side of line 2” is “element 4” of this substation]

  • second component of line_or_to_sub_pos is 5 [because “origin side of line 1” is “element 5” of this substation]

This is showed in the figure below:

5subs_grid_sub0_final

Then you do the same process with substation 1 which will result in the vectors showed in the following plot:

5subs_grid_sub1_final

When writing this, we realize it’s really verbose. But really, it simply consist on assigning, at each object, a unique ID to be able to retrieved it when querying something like “set the object of id 2 in subtation 0 to busbar 2”.

Finally, you proceed in the same manner for all substation and you get:

5subs_grid_layout_with_repr

(optional) Position in the topology vector (*_pos_topo_vect)

This information is redundant with the other vector. It can be initialized with a call to the function grid2op.Space.GridObjects._compute_pos_big_topo() that you will need to perform after having initialized all the other vectors as explained above (simply call self._compute_pos_big_topo() at the end of your implementation of load_grid function)

Note

Shunts are not “part of” the topology vector.

apply_action: underlying grid modification

In this section we detail step by step how to understand the specific format used by grid2op to “inform” the backend on how to modify its internal state before computing a powerflow.

A grid2op.Action._backendAction._BackendAction will tell the backend on what is modified among:

Note

Typically the apply_action function is called once per step of the environment. The implementation of this function should be rather optimized for the best performance.

In this section we detail the format of these particular “actions”. We assume in the following that backendAction is the action you need to perform.

At the end, the apply_action function of the backend should look something like:

def apply_action(self, backendAction=None):
    if backendAction is None:
        return
    active_bus, (prod_p, prod_v, load_p, load_q), _, shunts__ = backendAction()

    # modify the injections [see paragraph "Modifying the injections (productions and loads)"]
    for gen_id, new_p in prod_p:
        # modify the generator with id 'gen_id' to have the new setpoint "new_p" for production
        ...  # the way you do that depends on the `internal representation of the grid`
    for gen_id, new_v in prod_v:
        # modify the generator with id 'gen_id' to have the new value "new_v" as voltage setpoint
        # be careful here ! new_v are given in kV and NOT in pu. So you need to convert them accordingly
        ...  # the way you do that depends on the `internal representation of the grid`
    for load_id, new_p in load_p:
        # modify the load with id 'load_id' to have the new value "new_p" as consumption
        ...  # the way you do that depends on the `internal representation of the grid`
    for load_id, new_p in load_p:
        # modify the load with id 'load_id' to have the new value "new_p" as consumption
        ...  # the way you do that depends on the `internal representation of the grid`

    # modify the topology [see paragraph "Modifying the topology (status and busbar)"]
    loads_bus = backendAction.get_loads_bus()
    for load_id, new_bus in loads_bus:
        # modify the "busbar" of the loads
        if new_bus == -1:
            # the load is disconnected in the action, disconnect it on your internal representation of the grid
            ... # the way you do that depends on the `internal representation of the grid`
        else:
            # the load is moved to either busbar 1 (in this case `new_bus` will be `1`)
            # or to busbar 2 (in this case `new_bus` will be `2`)
            ... # the way you do that depends on the `internal representation of the grid`
    gens_bus = backendAction.get_gens_bus()
    for gen_id, new_bus in gens_bus:
        # modify the "busbar" of the generators
        if new_bus == -1:
            # the gen is disconnected in the action, disconnect it on your internal representation of the grid
            ... # the way you do that depends on the `internal representation of the grid`
        else:
            # the gen is moved to either busbar 1 (in this case `new_bus` will be `1`)
            # or to busbar 2 (in this case `new_bus` will be `2`)
            ... # the way you do that depends on the `internal representation of the grid`
    lines_or_bus = backendAction.get_lines_or_bus()
    for line_id, new_bus in lines_or_bus:
        # modify the "busbar" of the origin side of powerline line_id
        if new_bus == -1:
            # the origin side of powerline is disconnected in the action, disconnect it on your internal representation of the grid
            ... # the way you do that depends on the `internal representation of the grid`
        else:
            # the origin side of powerline is moved to either busbar 1 (in this case `new_bus` will be `1`)
            # or to busbar 2 (in this case `new_bus` will be `2`)
            ... # the way you do that depends on the `internal representation of the grid`
    lines_ex_bus = backendAction.get_lines_ex_bus()
    for line_id, new_bus in lines_ex_bus:
        # modify the "busbar" of the extremity side of powerline line_id
        if new_bus == -1:
            # the extremity side of powerline is disconnected in the action, disconnect it on your internal representation of the grid
            ... # the way you do that depends on the `internal representation of the grid`
        else:
            # the extremity side of powerline is moved to either busbar 1 (in this case `new_bus` will be `1`)
            # or to busbar 2 (in this case `new_bus` will be `2`)
            ... # the way you do that depends on the `internal representation of the grid`

New in version 1.3.2: Before version 1.3.2, you could not use the get_loads_bus, get_gens_bus, get_lines_or_bus or get_lines_ex_bus method. Please upgrade to grid2op 1.3.2 or later to use these.

Retrieve what has been modified

This is the first step you would need to perform to retrieve what are the modifications you need to implement in the backend. This is achieved with:

active_bus, (prod_p, prod_v, load_p, load_q), _, shunts__ = backendAction()

And all information needed to set the state of your backend is now available. We will explain them step by step in the following paragraphs.

Modifying the injections (productions and loads)

The new setpoints for the injections are given in the vectors (prod_p, prod_v, load_p, load_q) retrieved in the above paragraph (see Retrieve what has been modified for more information).

Each of the prod_p, prod_v, load_p and load_q are specific types of “iterable” that stores which values have been modified and what is the new value associated.

The first way to retrieve the modification is with a simple for loop:

for gen_id, new_p in prod_p:
    # modify the generator with id 'gen_id' to have the new setpoint "new_p"
    ...  # the way you do that depends on the `internal representation of the grid`

Note

If no changes have affected the “active production setpoint of generator” then it will not be “looped through”: only generators that have been modified between the steps will be showed here. So if you need to passe the values of all generators (for example) you need to remember these values yourself.

Of course it works the same way with the other “iterables”:

for gen_id, new_v in prod_v:
    # modify the generator with id 'gen_id' to have the new value "new_v" as voltage setpoint
    ...  # the way you do that depends on the `internal representation of the grid`
for load_id, new_p in load_p:
    # modify the load with id 'load_id' to have the new value "new_p" as consumption
    ...  # the way you do that depends on the `internal representation of the grid`
for load_id, new_p in load_p:
    # modify the load with id 'load_id' to have the new value "new_p" as consumption
    ...  # the way you do that depends on the `internal representation of the grid`

Modifying the topology (status and busbar)

This is probably the most “difficult” part of implementing a new backend based on your solver. This is because modification of topology is probably not as common as modifying the the production or consumption.

For this purpose we recommend to use the get_loads_bus, get_gens_bus, get_lines_or_bus and get_lines_ex_bus functions of the backendAction.

These functions can be used in the following manner:

# modify the topology
loads_bus = backendAction.get_loads_bus()
for load_id, new_bus in loads_bus:
    # modify the "busbar" of the loads
    if new_bus == -1:
        # the load is disconnected in the action, disconnect it on your internal representation of the grid
        ... # the way you do that depends on the `internal representation of the grid`
    else:
        # the load is moved to either busbar 1 (in this case `new_bus` will be `1`)
        # or to busbar 2 (in this case `new_bus` will be `2`)
        ... # the way you do that depends on the `internal representation of the grid`

And of course you do the same for generators and both ends of each powerline.

Note

About powerline, grid2op adopts the following convention: a powerline cannot be connected on one side and disconnected on the other.

That being said, it’s still possible to connect the extremity of a powerline “alone” on a busbar, which will have the same effect of having it “disconnected at one ends only”.

***_infos() : Read back the results (flows, voltages etc.)

This last “technical” part concerns what can be refer to as “getters” from the backend. These functions allow to read back the state of the grid and expose its results to grid2op in a standardize manner.

The main functions are:

Retrieving injections and flows

In this subsection we detail how we retrieve the information about the flows (at each side of all the powerlines and transformers) and how to retrieve the value associated with each “element” of the grid.

The functions you need to implement are described in the table below:

Name

Number of vectors

Description

grid2op.Backend.Backend.generators_info()

3

active setpoint, reactive absorption / production and voltage magnitude at the bus to which it’s connected

grid2op.Backend.Backend.loads_info()

3

active consumption, reactive consumption and voltage magnitude of the bus to which it’s connected

grid2op.Backend.Backend.lines_or_info()

4

active flow, reactive flow, voltage magnitude of the bus to which it’s connected and current flow

grid2op.Backend.Backend.lines_ex_info()

4

active flow, reactive flow, voltage magnitude of the bus to which it’s connected and current flow

It follows the same “representation” as anything else in grid2op : each of these values are represented as vectors. Vector should have fixed size, and each element (eg origin side of powerline L0) should be always at the same place in the vectors. For example, if you implement grid2op.Backend.Backend.lines_or_info(), it should return 4 vectors:

  • p_or: represents the active flows on each powerline (origin side)

  • q_or: represents the ractive flows on each powerline (origin side)

  • v_or: represents the voltage magnitude of each powerline (origin side)

  • a_or: represents the current flows on each powerline (origin side)

The first element of p_or vector expose the active flow on origin side of powerline l0, first component of a_or represents the current flow on origin side of l0.

This is further illustrated in the figure below (that concerns the the loads, but it’s the same ideas for all the other elements):

5subs_grid_loads_info

Note

The values that should be retrieved here are what is considered as “if i had placed a (perfect) sensor on this element what would this sensor display.

This is what is considered as “real world data” by grid2op. If you want to model delay in the measurements or some lack of precision in the sensors (for example) we don’t advise to do it on the Backend side, but rather to code a different types of “Observation” that would add some noise to the values returned by grid2op.

Note

Depending on the capacity of the Backend and its precision in modeling “real systems” the values returned in these vector may, or may not be different from their setpoint.

For example, if the backend do not model that “real” generators have limit for the reactive power they can absorb / produce then it’s likely that the “prod_v” you get from the backend is the same as the “prod_v” that has been used when the “apply_action” has been called.

Grid2Op is independent from the “model of grid” you are using. So grid2op itself will not be affected (and nothing will check) whether “prod_v” after a powerflow is the same as “prod_v” before.

Retrieving the topology

The last information you need to provide for your backend is tha ability to retrieve the internal topology of the powergrid.

This function is especially important if your solver is able to simulate the behaviour of some complex automatons on the grid. In this case, the topology after the powerflow (and the possible trigger of these automatons) can be different from the initial topology of the grid as provided by the backend action (see section load_grid: Grid description for more information).

Remember that to interface with grid2op, you needed to define orders of object in all the substation. To know the position of each element in the topology vector, grid2op simply concatenate all vectors of all substation.

Let’s start by substation 0:

5subs_grid_sub1_topo

Then you concatenate to it the vector representing substation 1:

5subs_grid_sub1_2_topo

And you do chat for all substations, giving:

5subs_grid_suball_topo

So in this simple example, the first element of the topology vector will represent the origin of powerline 0, the second element will represent the load 0, the 7th element (id 6, remember python index are 0 based) represent first element of substation 1, so in this case extremity side of powerline 3, the 8th element the generator 1, etc. up to element with id 20 whith is the last element of the last substation, in this case extremity of powerline 7.

Once you know the order, the encoding is pretty straightforward:

  • -1 means “the element is disconnected”

  • 1 means “the element is connected on busbar 1”

  • 2 means “the element is connected on busbar 2”

  • etc.

So, for example, we have, in our grid, if generator 1 is disconnected and powerline l7 is disconnected:

5subs_grid_ex_disco

And to represent a substation with multiple “nodes” (buses) you can encode it this way:

5subs_grid_ex_2buses

In this case, at substation 2 there are 2 connected busbars:

  • busbar 1 connects extremity of powerline 4 and origin of powerline 5

  • busbar 2 connects extremity of powerline 1 and origin of powerline 6

But there is no direct connection between busbar 1 and busbar 2.

Automatic testing and “Test Driven Programming”

New in version 1.9.6: Before this is was not specified in the code but only on some description what was expected from a backend.

In grid2op was added some convenience class to “make sure” that a backend candidate was working as expected, following the grid2op “expectation” in terms of grid representation and all. This test suite has been made independant of the solver used, you can use it directly if your backend can read pandapower “json” format but can also be used (thanks to some customization) even if it’s not the case.

The only requirement is that you install grid2op FROM SOURCE and in “editable” mode. You can do that (using a python virtual environment is a good idea and is not covered here):

git clone https://github.com/rte-france/grid2op.git grid2op_dev
cd grid2op_dev
pip install -e .

Warning

If it fails with some error like AttributeError: module ‘grid2op’ has no attribute ‘__version__’ or equivalent, you can remove the “pyproject.toml” file:

cd grid2op_dev  # same repo as above
rm -rf pyproject.toml
pip install -e .

Most simple way

Write a script (say “test_basic_api.py”) with this in your code:

from grid2op._create_test_suite import create_test_suite

def this_make_backend(self, detailed_infos_for_cascading_failures=False):
    # the function that will create your backend
    # do not put "PandaPowerBackend" of course, but the class you coded as a backend !
    return PandaPowerBackend(
            detailed_infos_for_cascading_failures=detailed_infos_for_cascading_failures
        )
add_name_cls = "test_PandaPowerBackend"  # h

res = create_test_suite(make_backend_fun=this_make_backend,
                        add_name_cls=add_name_cls,
                        add_to_module=__name__,
                        extended_test=False,  # for now keep `extended_test=False` until all problems are solved
                        )

if __name__ == "__main__":
    unittest.main()

Then you can run your test with:

python -m unittest test_basic_api.py

If all tests pass then you are done.

More verbose, but easier to debug

You can also do the equivalent script:

import unittest
from grid2op.tests.aaa_test_backend_interface import AAATestBackendAPI

class TestBackendAPI_PandaPowerBackend(AAATestBackendAPI, unittest.TestCase):
    def make_backend(self, detailed_infos_for_cascading_failures=False):
        # the function that will create your backend
        # do not put "PandaPowerBackend" of course, but the class you coded as a backend !
        return PandaPowerBackend(detailed_infos_for_cascading_failures=detailed_infos_for_cascading_failures)


if __name__ == "__main__":
    unittest.main()

Advantages:

  1. you can tell to python which test you want to run exactly, for example with python -m unittest test_basic_api.TestBackendAPI_PandaPowerBackend.test_01load_grid which makes it easier to debug (you can run only the failed test)

  2. it’s more clear what is being done and the name of everything

  3. it’s easier to customized

Drawbacks:

  1. Only tests in AAATestBackendAPI will be performed. Some other tests (integration test, backend more in depth tests) will not be performed

More customization

If you don’t have a backend that reads pandapower file format (or if you want to test part of your backend when initialized from another data format) it is also easy to customize it.

You need to:

  1. make a repo somewhere on you machine (say my_path_for_test which would be located at /home/user/Documents/my_path_for_test or C:\users\Documents\my_path_for_test or anywhere else)

  2. put there a grid with your specific format, for example grid.json or grid.xiidm or grid.xml. Two things are important here

    1. the file name should be grid and nothing else. Grid.json or my_grid.json or case14.xml will NOT work

    2. the extension should be set in the self.supported_grid_format, for example if you want your backend to be able to read grid.xml then the object you create in def make_backend(…) should have xml somewhere in its supported_grid_format attribute

    3. your grid should count at least 17 lines

    4. if your grid is not based on the educ_case14_storage (modification of the ieee case 14 with addition of 2 storage units and 2 generators), expect failure in the tests:

      1. the test_01load_grid

      2. test_22_islanded_grid_make_divergence

  3. Write a python script similar to this one:

import unittest
from grid2op.tests.aaa_test_backend_interface import AAATestBackendAPI
FILE_FORMAT = "xiidm"  # for this example, put whatever here !

class TestBackendAPI_PandaPowerBackend(AAATestBackendAPI, unittest.TestCase):
    def get_path(self):
        return "my_path_for_test"  # or /home/user/Documents/my_path_for_test

    def get_casefile(self):
        return "grid.xiidm"   # or `grid.xml` or any other format

    def make_backend(self, detailed_infos_for_cascading_failures=False):
        # the function that will create your backend
        # do not put "PandaPowerBackend" of course, but the class you coded as a backend !
        backend = PandaPowerBackend(detailed_infos_for_cascading_failures=detailed_infos_for_cascading_failures)
        assert FILE_FORMAT in backend.supported_grid_format, f"your backend does not recognize the '{FILE_FORMAT}' extension, grid2op will not work"
        return backend


if __name__ == "__main__":
    unittest.main()

TODO there are ways to use the create_test_suite but they have not been tested at the moment.

Advanced usage and speed optimization

TODO this will be explained “soon”.

Detailed Documentation by class

A first example of a working backend that can be easily understood (without nasty gory speed optimization) based on pandapower is available at :

class grid2op.Backend.educPandaPowerBackend.EducPandaPowerBackend(detailed_infos_for_cascading_failures: bool | None = False, can_be_copied: bool | None = True)[source]

INTERNAL

Warning

/!\ Internal, do not use unless you know what you are doing /!\

This class does not work.

It is mainly presented for educational purpose as example on coding your own backend.

It is derived from PandaPowerBackend, but without all the “optimization” that could make the resulting backend harder to read.

This module presents an example of an implementation of a grid2op.Backend when using the powerflow implementation “pandapower” available at PandaPower for more details about this backend. This file is provided as an example of a proper grid2op.Backend.Backend implementation.

This backend currently does not work with 3 winding transformers and other exotic object.

As explained in the grid2op.Backend module, every module must inherit the grid2op.Backend class.

We illustrate here how to set up a backend with what we think is “rather standard” in the powersystem eco system.

Please consult the documentation at Creating a new backend for more information.

You have at your disposal:

  • a tool that is able to compute power flows from a given grid in a given format (in this case call pandapower.runpf(pandapower_grid))

  • a tool that is able to load a powergrid from a file store on the hard drive.

We try to find a good compromise between the size of the code (for clarity) and the “closeness to a working code”.

For a complete working example, relatively optimized (but much less readable) please have a look at the real grid2op.Backend.PandaPowerBackend class.

Methods:

__init__([...])

Nothing much to do here except initializing what you would need (a tensorflow session, link to some external dependencies etc.)

_aux_get_line_info(colname_powerline, ...)

concatenate the information of powerlines and trafo using the convention that "powerlines go first"

_disconnect_line(id_)

you might consider implementing it

apply_action(backendAction)

Here the implementation of the "modify the grid" function.

close()

you might consider implementing it

copy()

you might consider implementing it

generators_info()

We chose to keep the same order in grid2op and in pandapower.

get_line_status()

you might consider implementing it

get_topo_vect()

Retrieve the bus to which the objects are connected based on the information stored on the grid.

lines_ex_info()

Main method to retrieve the information at the "extremity" side of the powerlines and transformers.

lines_or_info()

Main method to retrieve the information at the "origin" side of the powerlines and transformers.

load_grid(path[, filename])

Demonstration on how you can load a powergrid and then initialize the proper grid2op attributes.

loads_info()

We chose to keep the same order in grid2op and in pandapower.

reset(path[, grid_filename])

you might consider implementing it

runpf([is_dc])

Now we just perform a powerflow with pandapower which can be done with either rundcpp for dc powerflow or with runpp for AC powerflow.

__init__(detailed_infos_for_cascading_failures: bool | None = False, can_be_copied: bool | None = True)[source]

Nothing much to do here except initializing what you would need (a tensorflow session, link to some external dependencies etc.)

Nothing much for this example.

Parameters:

detailed_infos_for_cascading_failures (bool) – See the documentation of grid2op.Backend.Backend.__init__ for more information

_aux_get_line_info(colname_powerline, colname_trafo) ndarray[source]

concatenate the information of powerlines and trafo using the convention that “powerlines go first”

_disconnect_line(id_: int) None[source]

you might consider implementing it

Warning

/!\ This is a not a “main method” but you might want to implement it for a new backend (default implementation most likely not efficient at all). /!\

apply_action(backendAction: grid2op.Action._backendAction._BackendAction | None) None[source]

Here the implementation of the “modify the grid” function.

From the documentation, it’s pretty straightforward, even though the implementation takes ~70 lines of code. Most of them being direct copy paste from the examples in the documentation.

close() None[source]

you might consider implementing it

Warning

/!\ This is a not a “main method” but you might want to implement it for a new backend (default implementation most likely not efficient at all). /!\

Called when the grid2op;Environment has terminated, this function only reset the grid to a state where it has not been loaded.

copy() EducPandaPowerBackend[source]

you might consider implementing it

Warning

/!\ This is a not a “main method” but you might want to implement it for a new backend (default implementation most likely not efficient at all). /!\

Nothing really crazy here

Performs a deep copy of the power _grid. As pandapower is pure python, the deep copy operator is perfectly suited for the task.

generators_info() Tuple[ndarray, ndarray, ndarray][source]

We chose to keep the same order in grid2op and in pandapower. So we just return the correct values.

get_line_status() ndarray[source]

you might consider implementing it

Warning

/!\ This is a not a “main method” but you might want to implement it for a new backend (default implementation most likely not efficient at all). /!\

get_topo_vect() ndarray[source]

Retrieve the bus to which the objects are connected based on the information stored on the grid.

This is fairly simple, again, because we choose to explicitly represents 2 buses per substation.

Function is verbose (~40 lines of code), but pretty straightforward.

lines_ex_info() Tuple[ndarray, ndarray, ndarray, ndarray][source]

Main method to retrieve the information at the “extremity” side of the powerlines and transformers.

We simply need to follow the convention we adopted:

  • extremity side (grid2op) will be “to” side for pandapower powerline

  • extremity side (grid2op) will be “lv” side for pandapower trafo

  • we chose to first have powerlines, then transformers

(convention chosen in function EducPandaPowerBackend.load_grid())

lines_or_info() Tuple[ndarray, ndarray, ndarray, ndarray][source]

Main method to retrieve the information at the “origin” side of the powerlines and transformers.

We simply need to follow the convention we adopted:

  • origin side (grid2op) will be “from” side for pandapower powerline

  • origin side (grid2op) will be “hv” side for pandapower trafo

  • we chose to first have powerlines, then transformers

(convention chosen in EducPandaPowerBackend.load_grid())

load_grid(path: PathLike | str, filename: PathLike | str | None = None) None[source]

Demonstration on how you can load a powergrid and then initialize the proper grid2op attributes.

The only thing we have to do here is to “order” the objects in each substation. Note that we don’t even do it implicitly here (relying on default grid2op ordering).

The only decision we had to make was concerning “grid2op powerlines” which represents both “pandapower transformers” and “pandapower powerlines”.

We decided that:

  • powerline are “before” trafo (so in the grid2op line id I will put first all powerlines, then all trafo)

  • grid2op “origin” side will be “from” side for pandapower powerline and “hv” side for pandapower trafo

  • grid2op “extremity” side will be “to” side for pandapower powerline and “lv” side for pandapower trafo

Note

We use one “trick” here. Pandapower grid (as it will be the case for most format) will have one “bus” per substation. For grid2op, we want at least 2 busbar per substation. So we simply copy and paste the grid.

And we will deactivate the busbar that are not connected (no element connected to it).

This “coding” allows for easily mapping the bus id (each bus is represented with an id in pandapower) and whether its busbar 1 or busbar 2 (grid2op side). More precisely: busbar 1 of substation with id sub_id will have id sub_id and busbar 2 of the same substation will have id sub_id + n_sub (recall that n_sub is the number of substation on the grid).

This “coding” is not optimal in the ram it takes. But we recommend you to adopt a similar one. It’s pretty easy to change the topology using this trick, much easier than if you rely on “switches” for example. (But of course you can still use switches if you really want to)

loads_info() Tuple[ndarray, ndarray, ndarray][source]

We chose to keep the same order in grid2op and in pandapower. So we just return the correct values.

reset(path: PathLike | str, grid_filename: PathLike | str | None = None) None[source]

you might consider implementing it

Warning

/!\ This is a not a “main method” but you might want to implement it for a new backend (default implementation most likely not efficient at all). /!\

Reset the grid to the original state

runpf(is_dc: bool = False) Tuple[bool, Exception | None][source]

Now we just perform a powerflow with pandapower which can be done with either rundcpp for dc powerflow or with runpp for AC powerflow.

This is really a 2 lines code. It is a bit more verbose here because we took care of silencing some warnings and try / catch some possible exceptions.

And to understand better some key concepts, you can have a look at grid2op.Action._backendAction._BackendAction or the grid2op.Action._backendAction.ValueStore class:

class grid2op.Action._backendAction._BackendAction[source]

Warning

/!\ Internal, do not use unless you know what you are doing /!\

Internal class, use at your own risk.

This class “digest” the players / environment / opponent / voltage controlers “actions”, and transform it to one single “state” that can in turn be process by the backend in the function grid2op.Backend.Backend.apply_action().

Note

In a _BackendAction only the state of the element that have been modified by an “entity” (agent, environment, opponent, voltage controler etc.) is given.

We expect the backend to “remember somehow” the state of all the rest.

This is to save a lot of computation time for larger grid.

Note

You probably don’t need to import the _BackendAction class (this is why we “hide” it), but the backendAction you will receive in apply_action is indeed a _BackendAction, hence this documentation.

If you want to use grid2op to develop agents or new time series, this class should behave transparently for you and you don’t really need to spend time reading its documentation.

If you want to develop in grid2op and code a new backend, you might be interested in:

And in this case, for usage examples, see the examples available in:

Otherwise, “TL;DR” (only relevant when you want to implement the grid2op.Backend.Backend.apply_action() function, rest is not shown):

def apply_action(self, backendAction: Union["grid2op.Action._backendAction._BackendAction", None]) -> None:
    if backendAction is None:
        return

    (
        active_bus,
        (prod_p, prod_v, load_p, load_q, storage_p),
        topo__,
        shunts__,
    ) = backendAction()

    # change the active values of the loads
    for load_id, new_p in load_p:
        # do the real changes in self._grid

    # change the reactive values of the loads
    for load_id, new_q in load_q:
        # do the real changes in self._grid

    # change the active value of generators
    for gen_id, new_p in prod_p:
        # do the real changes in self._grid

    # for the voltage magnitude, pandapower expects pu but grid2op provides kV,
    # so we need a bit of change
    for gen_id, new_v in prod_v:
        # do the real changes in self._grid

    # process the topology :

    # option 1: you can directly set the element of the grid in the "topo_vect"
    # order, for example you can modify in your solver the busbar to which
    # element 17 of `topo_vect` is computed (this is necessarily a local view of
    # the buses )
    for el_topo_vect_id, new_el_bus in topo__:
        # connect this object to the `new_el_bus` (local) in self._grid

    # OR !!! (use either option 1 or option 2.a or option 2.b - exclusive OR)

    # option 2: use "per element type" view (this is usefull)
    # if your solver has organized its data by "type" and you can
    # easily access "all loads" and "all generators" etc.

    # option 2.a using "local view":
    # new_bus is either -1, 1, 2, ..., backendAction.n_busbar_per_sub
    lines_or_bus = backendAction.get_lines_or_bus()
    for line_id, new_bus in lines_or_bus:
        # connect "or" side of "line_id" to (local) bus `new_bus` in self._grid

    # OR !!! (use either option 1 or option 2.a or option 2.b - exclusive OR)

    # option 2.b using "global view":
    # new_bus is either 0, 1, 2, ..., backendAction.n_busbar_per_sub * backendAction.n_sub
    # (this suppose internally that your solver and grid2op have the same
    # "ways" of labelling the buses...)
    lines_or_bus = backendAction.get_lines_or_bus_global()
    for line_id, new_bus in lines_or_bus:
        # connect "or" side of "line_id" to (global) bus `new_bus` in self._grid

    # now repeat option a OR b calling the right methods
    # for each element types (*eg* get_lines_ex_bus, get_loads_bus, get_gens_bus,
    # get_storages_bus for "option a-like")

    ######## end processing of the topology  ###############

    # now implement the shunts:

    if shunts__ is not None:
        shunt_p, shunt_q, shunt_bus = shunts__

        if (shunt_p.changed).any():
            # p has changed for at least a shunt
            for shunt_id, new_shunt_p in shunt_p:
                # do the real changes in self._grid

        if (shunt_q.changed).any():
            # q has changed for at least a shunt
            for shunt_id, new_shunt_q in shunt_q:
                # do the real changes in self._grid

        if (shunt_bus.changed).any():
            # at least one shunt has been disconnected
            # or has changed the buses

            # do like for normal topology with:
            # option a -like (using local bus):
            for shunt_id, new_shunt_bus in shunt_bus:
                 ...
            # OR
            # option b -like (using global bus):
            shunt_global_bus = backendAction.get_shunts_bus_global()
            for shunt_id, new_shunt_bus in shunt_global_bus:
                # connect shunt_id to (global) bus `new_shunt_bus` in self._grid

Warning

The steps shown here are generic and might not be optimised for your backend. This is why you probably do not see any of them directly in grid2op.Backend.PandaPowerBackend (where everything is vectorized to make things fast with pandapower).

It is probably a good idea to first get this first implementation up and running, passing all the tests, and then to worry about optimization:

The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.

Donald Knuth, “The Art of Computer Programming

Methods:

__call__()

This function should be called at the top of the grid2op.Backend.Backend.apply_action() implementation when you decide to code a new backend.

__copy__()

Warning

/!\ Internal, do not use unless you know what you are doing /!\

__deepcopy__([memodict])

Warning

/!\ Internal, do not use unless you know what you are doing /!\

__iadd__(other)

Warning

/!\ Internal, do not use unless you know what you are doing /!\

__init__()

Warning

/!\ Internal, do not use unless you know what you are doing /!\

_assign_0_to_disco_el()

Warning

/!\ Internal, do not use unless you know what you are doing /!\

_aux_iadd_inj(dict_injection)

Warning

/!\ Internal, do not use unless you know what you are doing /!\

_aux_iadd_reconcile_disco_reco()

Warning

/!\ Internal, do not use unless you know what you are doing /!\

_aux_iadd_shunt(other)

Warning

/!\ Internal, do not use unless you know what you are doing /!\

_get_active_bus()

Warning

/!\ Internal, do not use unless you know what you are doing /!\

all_changed()

Warning

/!\ Internal, do not use unless you know what you are doing /!\

get_gens_bus()

This function might be called in the implementation of grid2op.Backend.Backend.apply_action().

get_gens_bus_global()

This function might be called in the implementation of grid2op.Backend.Backend.apply_action().

get_lines_ex_bus()

This function might be called in the implementation of grid2op.Backend.Backend.apply_action().

get_lines_ex_bus_global()

This function might be called in the implementation of grid2op.Backend.Backend.apply_action().

get_lines_or_bus()

This function might be called in the implementation of grid2op.Backend.Backend.apply_action().

get_lines_or_bus_global()

This function might be called in the implementation of grid2op.Backend.Backend.apply_action().

get_loads_bus()

This function might be called in the implementation of grid2op.Backend.Backend.apply_action().

get_loads_bus_global()

This function might be called in the implementation of grid2op.Backend.Backend.apply_action().

get_shunts_bus_global()

This function might be called in the implementation of grid2op.Backend.Backend.apply_action().

get_storages_bus()

This function might be called in the implementation of grid2op.Backend.Backend.apply_action().

get_storages_bus_global()

This function might be called in the implementation of grid2op.Backend.Backend.apply_action().

reorder(no_load, no_gen, no_topo, ...)

Warning

/!\ Internal, do not use unless you know what you are doing /!\

reset()

Warning

/!\ Internal, do not use unless you know what you are doing /!\

set_redispatch(new_redispatching)

Warning

/!\ Internal, do not use unless you know what you are doing /!\

update_state(powerline_disconnected)

Warning

/!\ Internal, do not use unless you know what you are doing /!\

__call__() Tuple[ndarray, Tuple[ValueStore, ValueStore, ValueStore, ValueStore, ValueStore], ValueStore, Tuple[ValueStore, ValueStore, ValueStore] | None][source]

This function should be called at the top of the grid2op.Backend.Backend.apply_action() implementation when you decide to code a new backend.

It processes the state of the backend into a form “easy to use” in the apply_action method.

Danger

It is mandatory to call it, otherwise some features might not work.

Warning

/!\ Do not alter / modify / change / override this implementation /!\

Examples

A typical implementation of apply_action will start with:

def apply_action(self, backendAction: Union["grid2op.Action._backendAction._BackendAction", None]) -> None:
    if backendAction is None:
        return

    (
        active_bus,
        (prod_p, prod_v, load_p, load_q, storage),
        topo__,
        shunts__,
    ) = backendAction()

    # process the backend action by updating `self._grid`
Returns:

  • - `active_bus` (matrix with type(self).n_sub rows and type(self).n_busbar_per_bus columns. Each elements) – represents a busbars of the grid. False indicates that nothing is connected to this busbar and True means that at least an element is connected to this busbar

  • - (prod_p, prod_v, load_p, load_q, storage) (5-tuple of Iterable to set the new values of generators, loads and storage units.)

  • - topo (iterable representing the target topology (in local bus, elements are ordered with their) – position in the topo_vect vector)

__copy__() Self[source]

Warning

/!\ Internal, do not use unless you know what you are doing /!\

__deepcopy__(memodict={}) Self[source]

Warning

/!\ Internal, do not use unless you know what you are doing /!\

__iadd__(other: BaseAction) Self[source]

Warning

/!\ Internal, do not use unless you know what you are doing /!\

This is called by the environment, do not alter.

The goal of this function is to “fused” together all the different types of modifications handled by:

  • the Agent

  • the opponent

  • the time series (part of the environment)

  • the voltage controler

It might be called multiple times per step.

Parameters:

other (grid2op.Action.BaseAction) –

Return type:

The updated state of self after the new action other has been added to it.

__init__()[source]

Warning

/!\ Internal, do not use unless you know what you are doing /!\

This is handled by the environment !

_assign_0_to_disco_el() None[source]

Warning

/!\ Internal, do not use unless you know what you are doing /!\

This is handled by the environment, do not alter.

Do not consider disconnected elements are modified for there active / reactive / voltage values

_aux_iadd_inj(dict_injection)[source]

Warning

/!\ Internal, do not use unless you know what you are doing /!\

Internal implementation of +=

_aux_iadd_reconcile_disco_reco()[source]

Warning

/!\ Internal, do not use unless you know what you are doing /!\

Internal implementation of +=

_aux_iadd_shunt(other)[source]

Warning

/!\ Internal, do not use unless you know what you are doing /!\

Internal implementation of +=

_get_active_bus() None[source]

Warning

/!\ Internal, do not use unless you know what you are doing /!\

all_changed() None[source]

Warning

/!\ Internal, do not use unless you know what you are doing /!\

This is called by the environment, do not alter.

get_gens_bus() ValueStore[source]

This function might be called in the implementation of grid2op.Backend.Backend.apply_action().

It is relevant when your solver expose API by “element types” for example you get the possibility to set and access all loads at once, all generators at once and your solver can easily move element from different busbar in a given substation.

This corresponds to option 2a described (shortly) in _BackendAction.

In this setting, this function will give you the “local bus” id for each generators that have been changed by the agent / time series / voltage controlers / opponent / etc.

Warning

/!\ Do not alter / modify / change / override this implementation /!\

Examples

Some examples are given in the documentation of _BackendAction.get_loads_bus()

get_gens_bus_global() ValueStore[source]

This function might be called in the implementation of grid2op.Backend.Backend.apply_action().

It is relevant when your solver expose API by “element types” for example you get the possibility to set and access all loads at once, all generators at once AND you can easily switch element from one “busbars” to another in the whole grid handled by your solver.

This corresponds to situation 2b described in _BackendAction.

In this setting, this function will give you the “local bus” id for each loads that have been changed by the agent / time series / voltage controlers / opponent / etc.

Warning

/!\ Do not alter / modify / change / override this implementation /!\

Examples

Some examples are given in the documentation of _BackendAction.get_loads_bus_global()

get_lines_ex_bus() ValueStore[source]

This function might be called in the implementation of grid2op.Backend.Backend.apply_action().

It is relevant when your solver expose API by “element types” for example you get the possibility to set and access all loads at once, all generators at once and your solver can easily move element from different busbar in a given substation.

This corresponds to option 2a described (shortly) in _BackendAction.

In this setting, this function will give you the “local bus” id for each line (ex side) that have been changed by the agent / time series / voltage controlers / opponent / etc.

Warning

/!\ Do not alter / modify / change / override this implementation /!\

Examples

Some examples are given in the documentation of _BackendAction.get_loads_bus()

get_lines_ex_bus_global() ValueStore[source]

This function might be called in the implementation of grid2op.Backend.Backend.apply_action().

It is relevant when your solver expose API by “element types” for example you get the possibility to set and access all loads at once, all generators at once AND you can easily switch element from one “busbars” to another in the whole grid handled by your solver.

This corresponds to situation 2b described in _BackendAction.

In this setting, this function will give you the “local bus” id for each loads that have been changed by the agent / time series / voltage controlers / opponent / etc.

Warning

/!\ Do not alter / modify / change / override this implementation /!\

Examples

Some examples are given in the documentation of _BackendAction.get_loads_bus_global()

get_lines_or_bus() ValueStore[source]

This function might be called in the implementation of grid2op.Backend.Backend.apply_action().

It is relevant when your solver expose API by “element types” for example you get the possibility to set and access all loads at once, all generators at once and your solver can easily move element from different busbar in a given substation.

This corresponds to option 2a described (shortly) in _BackendAction.

In this setting, this function will give you the “local bus” id for each line (or side) that have been changed by the agent / time series / voltage controlers / opponent / etc.

Warning

/!\ Do not alter / modify / change / override this implementation /!\

Examples

Some examples are given in the documentation of _BackendAction.get_loads_bus()

get_lines_or_bus_global() ValueStore[source]

This function might be called in the implementation of grid2op.Backend.Backend.apply_action().

It is relevant when your solver expose API by “element types” for example you get the possibility to set and access all loads at once, all generators at once AND you can easily switch element from one “busbars” to another in the whole grid handled by your solver.

This corresponds to situation 2b described in _BackendAction.

In this setting, this function will give you the “local bus” id for each loads that have been changed by the agent / time series / voltage controlers / opponent / etc.

Warning

/!\ Do not alter / modify / change / override this implementation /!\

Examples

Some examples are given in the documentation of _BackendAction.get_loads_bus_global()

get_loads_bus() ValueStore[source]

This function might be called in the implementation of grid2op.Backend.Backend.apply_action().

It is relevant when your solver expose API by “element types” for example you get the possibility to set and access all loads at once, all generators at once and your solver can easily move element from different busbar in a given substation.

This corresponds to option 2a described (shortly) in _BackendAction.

In this setting, this function will give you the “local bus” id for each loads that have been changed by the agent / time series / voltage controlers / opponent / etc.

Warning

/!\ Do not alter / modify / change / override this implementation /!\

Examples

A typical use of get_loads_bus in apply_action is:

def apply_action(self, backendAction: Union["grid2op.Action._backendAction._BackendAction", None]) -> None:
    if backendAction is None:
        return

    (
        active_bus,
        (prod_p, prod_v, load_p, load_q, storage),
        _,
        shunts__,
    ) = backendAction()

    # process the backend action by updating `self._grid`
    ...

    # now process the topology (called option 2.a in the doc):

    lines_or_bus = backendAction.get_lines_or_bus()
    for line_id, new_bus in lines_or_bus:
        # connect "or" side of "line_id" to (local) bus `new_bus` in self._grid
        self._grid.something(...)
        # or
        self._grid.something = ...

    lines_ex_bus = backendAction.get_lines_ex_bus()
    for line_id, new_bus in lines_ex_bus:
        # connect "ex" side of "line_id" to (local) bus `new_bus` in self._grid
        self._grid.something(...)
        # or
        self._grid.something = ...

    storages_bus = backendAction.get_storages_bus()
    for el_id, new_bus in storages_bus:
        # connect storage id `el_id` to (local) bus `new_bus` in self._grid
        self._grid.something(...)
        # or
        self._grid.something = ...

    gens_bus = backendAction.get_gens_bus()
    for el_id, new_bus in gens_bus:
        # connect generator id `el_id` to (local) bus `new_bus` in self._grid
        self._grid.something(...)
        # or
        self._grid.something = ...

    loads_bus = backendAction.get_loads_bus()
    for el_id, new_bus in loads_bus:
        # connect generator id `el_id` to (local) bus `new_bus` in self._grid
        self._grid.something(...)
        # or
        self._grid.something = ...

    # continue implementation of `apply_action`
get_loads_bus_global() ValueStore[source]

This function might be called in the implementation of grid2op.Backend.Backend.apply_action().

It is relevant when your solver expose API by “element types” for example you get the possibility to set and access all loads at once, all generators at once AND you can easily switch element from one “busbars” to another in the whole grid handled by your solver.

This corresponds to situation 2b described in _BackendAction.

In this setting, this function will give you the “local bus” id for each loads that have been changed by the agent / time series / voltage controlers / opponent / etc.

Warning

/!\ Do not alter / modify / change / override this implementation /!\

Examples

A typical use of get_loads_bus_global in apply_action is:

def apply_action(self, backendAction: Union["grid2op.Action._backendAction._BackendAction", None]) -> None:
    if backendAction is None:
        return

    (
        active_bus,
        (prod_p, prod_v, load_p, load_q, storage),
        _,
        shunts__,
    ) = backendAction()

    # process the backend action by updating `self._grid`
    ...

    # now process the topology (called option 2.a in the doc):

    lines_or_bus = backendAction.get_lines_or_bus_global()
    for line_id, new_bus in lines_or_bus:
        # connect "or" side of "line_id" to (global) bus `new_bus` in self._grid
        self._grid.something(...)
        # or
        self._grid.something = ...

    lines_ex_bus = backendAction.get_lines_ex_bus_global()
    for line_id, new_bus in lines_ex_bus:
        # connect "ex" side of "line_id" to (global) bus `new_bus` in self._grid
        self._grid.something(...)
        # or
        self._grid.something = ...

    storages_bus = backendAction.get_storages_bus_global()
    for el_id, new_bus in storages_bus:
        # connect storage id `el_id` to (global) bus `new_bus` in self._grid
        self._grid.something(...)
        # or
        self._grid.something = ...

    gens_bus = backendAction.get_gens_bus_global()
    for el_id, new_bus in gens_bus:
        # connect generator id `el_id` to (global) bus `new_bus` in self._grid
        self._grid.something(...)
        # or
        self._grid.something = ...

    loads_bus = backendAction.get_loads_bus_global()
    for el_id, new_bus in loads_bus:
        # connect generator id `el_id` to (global) bus `new_bus` in self._grid
        self._grid.something(...)
        # or
        self._grid.something = ...

    # continue implementation of `apply_action`
get_shunts_bus_global() ValueStore[source]

This function might be called in the implementation of grid2op.Backend.Backend.apply_action().

It is relevant when your solver expose API by “element types” for example you get the possibility to set and access all loads at once, all generators at once AND you can easily switch element from one “busbars” to another in the whole grid handled by your solver.

This corresponds to situation 2b described in _BackendAction.

In this setting, this function will give you the “local bus” id for each loads that have been changed by the agent / time series / voltage controlers / opponent / etc.

Warning

/!\ Do not alter / modify / change / override this implementation /!\

Examples

Some examples are given in the documentation of _BackendAction.get_loads_bus_global()

get_storages_bus() ValueStore[source]

This function might be called in the implementation of grid2op.Backend.Backend.apply_action().

It is relevant when your solver expose API by “element types” for example you get the possibility to set and access all loads at once, all generators at once and your solver can easily move element from different busbar in a given substation.

This corresponds to option 2a described (shortly) in _BackendAction.

In this setting, this function will give you the “local bus” id for each storage that have been changed by the agent / time series / voltage controlers / opponent / etc.

Warning

/!\ Do not alter / modify / change / override this implementation /!\

Examples

Some examples are given in the documentation of _BackendAction.get_loads_bus()

get_storages_bus_global() ValueStore[source]

This function might be called in the implementation of grid2op.Backend.Backend.apply_action().

It is relevant when your solver expose API by “element types” for example you get the possibility to set and access all loads at once, all generators at once AND you can easily switch element from one “busbars” to another in the whole grid handled by your solver.

This corresponds to situation 2b described in _BackendAction.

In this setting, this function will give you the “local bus” id for each loads that have been changed by the agent / time series / voltage controlers / opponent / etc.

Warning

/!\ Do not alter / modify / change / override this implementation /!\

Examples

Some examples are given in the documentation of _BackendAction.get_loads_bus_global()

reorder(no_load, no_gen, no_topo, no_storage, no_shunt) None[source]

Warning

/!\ Internal, do not use unless you know what you are doing /!\

This is handled by BackendConverter, do not alter

Reorder the element modified, this is use when converting backends only and should not be use outside of this usecase

no_* stands for “new order”

reset() None[source]

Warning

/!\ Internal, do not use unless you know what you are doing /!\

This is called by the environment, do not alter.

set_redispatch(new_redispatching)[source]

Warning

/!\ Internal, do not use unless you know what you are doing /!\

This is called by the environment, do not alter.

update_state(powerline_disconnected) None[source]

Warning

/!\ Internal, do not use unless you know what you are doing /!\

This is handled by the environment !

Update the internal state. Should be called after the cascading failures.

class grid2op.Action._backendAction.ValueStore(size, dtype)[source]

USE ONLY IF YOU WANT TO CODE A NEW BACKEND

Warning

/!\ Internal, do not modify, alter, change, override the implementation unless you know what you are doing /!\

If you override them you might even notice some extremely weird behaviour. It’s not “on purpose”, we are aware of it but we won’t change it (for now at least)

Warning

Objects from this class should never be created by anyone except by objects of the grid2op.Action._backendAction._BackendAction when they are created or when instances of _BackendAction are process eg with _BackendAction.__call__() or _BackendAction.get_loads_bus() etc.

There are two correct uses for this class:

  1. by iterating manually with the `for xxx in value_stor_instance: `

  2. by checking which objects have been changed (with ValueStore.changed ) and then check the new value of the elements changed with ValueStore.values [el_id]

Danger

You should never trust the values in ValueStore.values [el_id] if ValueStore.changed [el_id] is False.

Access data (values) only when the corresponding “mask” (ValueStore.changed) is True.

This is, of course, ensured by default if you use the practical way of iterating through them with:

load_p: ValueStore  # a ValueStore object named "load_p"

for load_id, new_p in load_p:
    # do something

In this case only “new_p” will be given if corresponding changed mask is true.

TODO

Examples

Say you have a “ValueStore” val_sto (in grid2op.Action._backendAction._BackendAction you will end up manipulating pretty much all the time ValueStore if you use it correctly, with _BackendAction.__call__() but also is you call _BackendAction.get_loads_bus(), _BackendAction.get_loads_bus_global(), _BackendAction.get_gens_bus(), …)

Basically, the “variables” named prod_p, prod_v, load_p, load_q, storage_p, topo__, shunt_p, shunt_q, shunt_bus, backendAction.get_lines_or_bus(), backendAction.get_lines_or_bus_global(), etc in the doc of grid2op.Action._backendAction._BackendAction are all ValueStore.

Recommended usage:

val_sto: ValueStore  # a ValueStore object named "val_sto"

for el_id, new_val in val_sto:
    # do something

# less abstractly, say `load_p` is a ValueStore:
# for load_id, new_p in load_p:
    # do the real changes of load active value in self._grid
    # load_id => id of loads for which the active consumption changed
    # new_p => new load active consumption for `load_id`
    # self._grid.change_load_active_value(load_id, new_p)  # fictive example of course...

More advanced / vectorized usage (only do that if you found out your backend was slow because of the iteration in python above, this is error-prone and in general might not be worth it…):

val_sto: ValueStore  # a ValueStore object named "val_sto"

# less abstractly, say `load_p` is a ValueStore:
# self._grid.change_all_loads_active_value(where_changed=load_p.changed,
                                           new_vals=load_p.values[load_p.changed])
# fictive example of couse, I highly doubt the self._grid
# implements a method named exactly `change_all_loads_active_value`

WARNING, DANGER AHEAD:
Never trust the data in load_p.values[~load_p.changed], they might even be un intialized...

Attributes:

changed

np.ndarray (bool) Mask representing which values (stored in ValueStore.values ) are meaningful.

last_index

used internally for iteration

values

np.ndarray The new target values to be set in backend._grid in apply_action never use the values if the corresponding mask is set to False (it might be non initialized).

Methods:

copy(other)

deepcopy, shallow or deep, without having to initialize everything again

reorder(new_order)

reorder the element modified, this is use when converting backends only and should not be use outside of this usecase

changed

np.ndarray (bool) Mask representing which values (stored in ValueStore.values ) are meaningful. The other values (corresponding to changed=False ) are meaningless.

copy(other)[source]

deepcopy, shallow or deep, without having to initialize everything again

last_index

used internally for iteration

reorder(new_order)[source]

reorder the element modified, this is use when converting backends only and should not be use outside of this usecase

values

np.ndarray The new target values to be set in backend._grid in apply_action never use the values if the corresponding mask is set to False (it might be non initialized).

If you still can’t find what you’re looking for, try in one of the following pages:

Still trouble finding the information ? Do not hesitate to send a github issue about the documentation at this link: Documentation issue template