# 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.
import copy
import numpy as np
from typing import Tuple, Union
try:
from typing import Self
except ImportError:
from typing_extensions import Self
from grid2op.Action.baseAction import BaseAction
from grid2op.dtypes import dt_int, dt_bool, dt_float
from grid2op.Space import GridObjects
# TODO see if it can be done in c++ easily
[docs]class ValueStore:
"""
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
:class:`grid2op.Action._backendAction._BackendAction`
when they are created or when instances of `_BackendAction` are process *eg* with :func:`_BackendAction.__call__` or
:func:`_BackendAction.get_loads_bus` etc.
There are two correct uses for this class:
#. by iterating manually with the `for xxx in value_stor_instance: `
#. by checking which objects have been changed (with :attr:`ValueStore.changed` ) and then check the
new value of the elements **changed** with :attr:`ValueStore.values` [el_id]
.. danger::
You should never trust the values in :attr:`ValueStore.values` [el_id] if :attr:`ValueStore.changed` [el_id] is `False`.
Access data (values) only when the corresponding "mask" (:attr:`ValueStore.changed`) is `True`.
This is, of course, ensured by default if you use the practical way of iterating through them with:
.. code-block:: python
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.
Attributes
----------
TODO
Examples
---------
Say you have a "ValueStore" `val_sto` (in :class:`grid2op.Action._backendAction._BackendAction` you will end up manipulating
pretty much all the time `ValueStore` if you use it correctly, with :func:`_BackendAction.__call__` but also is you call
:func:`_BackendAction.get_loads_bus`, :func:`_BackendAction.get_loads_bus_global`, :func:`_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 :class:`grid2op.Action._backendAction._BackendAction`
are all :class:`ValueStore`.
Recommended usage:
.. code-block:: python
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...):
.. code-block:: python
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...
"""
def __init__(self, size, dtype):
## TODO at the init it's mandatory to have everything at "1" here
# if topo is not "fully connected" it will not work
#: :class:`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).
self.values = np.empty(size, dtype=dtype)
#: :class:`np.ndarray` (bool)
#: Mask representing which values (stored in :attr:`ValueStore.values` ) are
#: meaningful. The other values (corresponding to `changed=False` ) are meaningless.
self.changed = np.full(size, dtype=dt_bool, fill_value=False)
#: used internally for iteration
self.last_index = 0
self.__size = size
if issubclass(dtype, dt_int):
self.set_val = self._set_val_int
self.change_val = self._change_val_int
elif issubclass(dtype, dt_float):
self.set_val = self._set_val_float
self.change_val = self._change_val_float
def _set_val_float(self, newvals):
changed_ = np.isfinite(newvals)
self.changed[changed_] = True
self.values[changed_] = newvals[changed_]
def _set_val_int(self, newvals):
changed_ = newvals != 0
self.changed[changed_] = True
self.values[changed_] = newvals[changed_]
def _change_val_int(self, newvals):
changed_ = newvals & (self.values > 0)
self.changed[changed_] = True
self.values[changed_] = (1 - self.values[changed_]) + 2
def _change_val_float(self, newvals):
changed_ = np.abs(newvals) >= 1e-7
self.changed[changed_] = True
self.values[changed_] += newvals[changed_]
def reset(self):
self.changed[:] = False
self.last_index = 0
def change_status(self, switch, lineor_id, lineex_id, old_vect):
if not switch.any():
# nothing is modified so i stop here
return
# changed
changed_ = switch
# make it to ids
id_chg_or = lineor_id[changed_]
id_chg_ex = lineex_id[changed_]
self.changed[id_chg_or] = True
self.changed[id_chg_ex] = True
# disconnect the powerlines
me_or_bus = self.values[id_chg_or]
me_ex_bus = self.values[id_chg_ex]
was_connected = (me_or_bus > 0) | (me_ex_bus > 0)
was_disco = ~was_connected
# it was connected, i disconnect it
self.values[id_chg_or[was_connected]] = -1
self.values[id_chg_ex[was_connected]] = -1
# it was disconnected, i reconnect it
reco_or = id_chg_or[was_disco]
reco_ex = id_chg_ex[was_disco]
self.values[reco_or] = old_vect[reco_or]
self.values[reco_ex] = old_vect[reco_ex]
def set_status(self, set_status, lineor_id, lineex_id, old_vect):
id_or = lineor_id
id_ex = lineex_id
# disco
disco_ = set_status == -1
reco_ = set_status == 1
# make it to ids
id_reco_or = id_or[reco_]
id_reco_ex = id_ex[reco_]
id_disco_or = id_or[disco_]
id_disco_ex = id_ex[disco_]
self.changed[id_reco_or] = True
self.changed[id_reco_ex] = True
self.changed[id_disco_or] = True
self.changed[id_disco_ex] = True
# disconnect the powerlines
self.values[id_disco_or] = -1
self.values[id_disco_ex] = -1
# reconnect the powerlines
# don't consider powerlines that have been already changed with topology
# ie reconnect to the old bus only powerline from which we don't know the status
id_reco_or = id_reco_or[self.values[id_reco_or] < 0]
id_reco_ex = id_reco_ex[self.values[id_reco_ex] < 0]
self.values[id_reco_or] = old_vect[id_reco_or]
self.values[id_reco_ex] = old_vect[id_reco_ex]
def get_line_status(self, lineor_id, lineex_id):
return self.values[lineor_id], self.values[lineex_id]
def update_connected(self, current_values):
indx_conn = current_values.values > 0
self.values[indx_conn] = current_values.values[indx_conn]
def all_changed(self):
self.reset()
self.changed[:] = True
def __getitem__(self, item):
return self.values[item]
def __setitem__(self, key, value):
self.values[key] = value
self.changed[key] = value
def __iter__(self):
return self
def __next__(self):
res = None
while self.last_index < self.values.shape[0]:
if self.changed[self.last_index]:
res = (self.last_index, self.values[self.last_index])
self.last_index += 1
if res is not None:
break
if res is not None:
return res
else:
raise StopIteration
def __len__(self):
return self.__size
[docs] def reorder(self, new_order):
"""reorder the element modified, this is use when converting backends only and should not be use
outside of this usecase"""
self.changed[:] = self.changed[new_order]
self.values[:] = self.values[new_order]
def copy_from_index(self, ref, index):
self.reset()
self.changed[:] = ref.changed[index]
self.values[:] = ref.values[index]
def __copy__(self):
res = type(self)(self.values.shape[0], self.values.dtype.type)
res.values[:] = self.values
res.changed[:] = self.changed
res.last_index = self.last_index
res.__size = self.__size
return res
def __deepcopy__(self, memodict={}):
res = type(self)(self.values.shape[0], self.values.dtype.type)
res.values[:] = self.values
res.changed[:] = self.changed
res.last_index = self.last_index
res.__size = self.__size
return res
[docs] def copy(self, other):
"""deepcopy, shallow or deep, without having to initialize everything again"""
self.values[:] = other.values
self.changed[:] = other.changed
self.last_index = other.last_index
self.__size = other.__size
def force_unchanged(self, mask, local_bus):
to_unchanged = local_bus == -1
to_unchanged[~mask] = False
self.changed[to_unchanged] = False
def register_new_topo(self, current_topo: "ValueStore"):
mask_co = current_topo.values >= 1
self.values[mask_co] = current_topo.values[mask_co]
[docs]class _BackendAction(GridObjects):
"""
.. 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 :func:`grid2op.Backend.Backend.apply_action`.
.. note::
In a :class:`_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 :class:`_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:
- :func:`_BackendAction.__call__`
- :func:`_BackendAction.get_loads_bus`
- :func:`_BackendAction.get_loads_bus_global`
- :func:`_BackendAction.get_gens_bus`
- :func:`_BackendAction.get_gens_bus_global`
- :func:`_BackendAction.get_lines_or_bus`
- :func:`_BackendAction.get_lines_or_bus_global`
- :func:`_BackendAction.get_lines_ex_bus`
- :func:`_BackendAction.get_lines_ex_bus_global`
- :func:`_BackendAction.get_storages_bus`
- :func:`_BackendAction.get_storages_bus_global`
- :func:`_BackendAction.get_shunts_bus_global`
And in this case, for usage examples, see the examples available in:
- https://github.com/rte-france/Grid2Op/tree/master/examples/backend_integration: a step by step guide to
code a new backend
- :class:`grid2op.Backend.educPandaPowerBackend.EducPandaPowerBackend` and especially the
:func:`grid2op.Backend.educPandaPowerBackend.EducPandaPowerBackend.apply_action`
- :ref:`create-backend-module` page of the documentation, especially the
:ref:`backend-action-create-backend` section
Otherwise, "TL;DR" (only relevant when you want to implement the :func:`grid2op.Backend.Backend.apply_action`
function, rest is not shown):
.. code-block:: python
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 :class:`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*"
"""
[docs] def __init__(self):
"""
.. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\
This is handled by the environment !
"""
GridObjects.__init__(self)
cls = type(self)
# last connected registered
self.last_topo_registered: ValueStore = ValueStore(cls.dim_topo, dtype=dt_int)
# topo at time t
self.current_topo: ValueStore = ValueStore(cls.dim_topo, dtype=dt_int)
# by default everything is on busbar 1
self.last_topo_registered.values[:] = 1
self.current_topo.values[:] = 1
# injection at time t
self.prod_p: ValueStore = ValueStore(cls.n_gen, dtype=dt_float)
self.prod_v: ValueStore = ValueStore(cls.n_gen, dtype=dt_float)
self.load_p: ValueStore = ValueStore(cls.n_load, dtype=dt_float)
self.load_q: ValueStore = ValueStore(cls.n_load, dtype=dt_float)
self.storage_power: ValueStore = ValueStore(cls.n_storage, dtype=dt_float)
self.activated_bus = np.full((cls.n_sub, cls.n_busbar_per_sub), dtype=dt_bool, fill_value=False)
self.big_topo_to_subid: np.ndarray = np.repeat(
list(range(cls.n_sub)), repeats=cls.sub_info
)
# shunts
if cls.shunts_data_available:
self.shunt_p: ValueStore = ValueStore(cls.n_shunt, dtype=dt_float)
self.shunt_q: ValueStore = ValueStore(cls.n_shunt, dtype=dt_float)
self.shunt_bus: ValueStore = ValueStore(cls.n_shunt, dtype=dt_int)
self.current_shunt_bus: ValueStore = ValueStore(cls.n_shunt, dtype=dt_int)
self.current_shunt_bus.values[:] = 1
self._status_or_before: np.ndarray = np.ones(cls.n_line, dtype=dt_int)
self._status_ex_before: np.ndarray = np.ones(cls.n_line, dtype=dt_int)
self._status_or: np.ndarray = np.ones(cls.n_line, dtype=dt_int)
self._status_ex: np.ndarray = np.ones(cls.n_line, dtype=dt_int)
self._loads_bus = None
self._gens_bus = None
self._lines_or_bus = None
self._lines_ex_bus = None
self._storage_bus = None
[docs] def __deepcopy__(self, memodict={}) -> Self:
"""
.. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\
"""
res = type(self)()
# last connected registered
res.last_topo_registered.copy(self.last_topo_registered)
res.current_topo.copy(self.current_topo)
res.prod_p.copy(self.prod_p)
res.prod_v.copy(self.prod_v)
res.load_p.copy(self.load_p)
res.load_q.copy(self.load_q)
res.storage_power.copy(self.storage_power)
res.activated_bus[:, :] = self.activated_bus
# res.big_topo_to_subid[:] = self.big_topo_to_subid # cste
cls = type(self)
if cls.shunts_data_available:
res.shunt_p.copy(self.shunt_p)
res.shunt_q.copy(self.shunt_q)
res.shunt_bus.copy(self.shunt_bus)
res.current_shunt_bus.copy(self.current_shunt_bus)
res._status_or_before[:] = self._status_or_before
res._status_ex_before[:] = self._status_ex_before
res._status_or[:] = self._status_or
res._status_ex[:] = self._status_ex
res._loads_bus = copy.deepcopy(self._loads_bus)
res._gens_bus = copy.deepcopy(self._gens_bus)
res._lines_or_bus = copy.deepcopy(self._lines_or_bus)
res._lines_ex_bus = copy.deepcopy(self._lines_ex_bus)
res._storage_bus = copy.deepcopy(self._storage_bus)
return res
[docs] def __copy__(self) -> Self:
"""
.. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\
"""
res = self.__deepcopy__() # nothing less to do
return res
[docs] def reorder(self, no_load, no_gen, no_topo, no_storage, no_shunt) -> None:
"""
.. 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"
"""
self.last_topo_registered.reorder(no_topo)
self.current_topo.reorder(no_topo)
self.prod_p.reorder(no_gen)
self.prod_v.reorder(no_gen)
self.load_p.reorder(no_load)
self.load_q.reorder(no_load)
self.storage_power.reorder(no_storage)
cls = type(self)
if cls.shunts_data_available:
self.shunt_p.reorder(no_shunt)
self.shunt_q.reorder(no_shunt)
self.shunt_bus.reorder(no_shunt)
self.current_shunt_bus.reorder(no_shunt)
[docs] def reset(self) -> None:
"""
.. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\
This is called by the environment, do not alter.
"""
# last known topo
self.last_topo_registered.reset()
# topo at time t
self.current_topo.reset()
# injection at time t
self.prod_p.reset()
self.prod_v.reset()
self.load_p.reset()
self.load_q.reset()
self.storage_power.reset()
# storage unit have their power reset to 0. each step
self.storage_power.changed[:] = True
self.storage_power.values[:] = 0.0
# shunts
cls = type(self)
if cls.shunts_data_available:
self.shunt_p.reset()
self.shunt_q.reset()
self.shunt_bus.reset()
self.current_shunt_bus.reset()
self.last_topo_registered.register_new_topo(self.current_topo)
[docs] def all_changed(self) -> None:
"""
.. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\
This is called by the environment, do not alter.
"""
# last topo
self.last_topo_registered.all_changed()
# topo at time t
self.current_topo.all_changed()
# injection at time t
self.prod_p.all_changed()
self.prod_v.all_changed()
self.load_p.all_changed()
self.load_q.all_changed()
self.storage_power.all_changed()
# TODO handle shunts
# shunts
# if self.shunts_data_available:
# self.shunt_p.all_changed()
# self.shunt_q.all_changed()
# self.shunt_bus.all_changed()
[docs] def set_redispatch(self, new_redispatching):
"""
.. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\
This is called by the environment, do not alter.
"""
self.prod_p.change_val(new_redispatching)
[docs] def _aux_iadd_inj(self, dict_injection):
"""
.. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\
Internal implementation of +=
"""
if "load_p" in dict_injection:
tmp = dict_injection["load_p"]
self.load_p.set_val(tmp)
if "load_q" in dict_injection:
tmp = dict_injection["load_q"]
self.load_q.set_val(tmp)
if "prod_p" in dict_injection:
tmp = dict_injection["prod_p"]
self.prod_p.set_val(tmp)
if "prod_v" in dict_injection:
tmp = dict_injection["prod_v"]
self.prod_v.set_val(tmp)
[docs] def _aux_iadd_shunt(self, other):
"""
.. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\
Internal implementation of +=
"""
shunts = {}
if type(other).shunts_data_available:
shunts["shunt_p"] = other.shunt_p
shunts["shunt_q"] = other.shunt_q
shunts["shunt_bus"] = other.shunt_bus
arr_ = shunts["shunt_p"]
self.shunt_p.set_val(arr_)
arr_ = shunts["shunt_q"]
self.shunt_q.set_val(arr_)
arr_ = shunts["shunt_bus"]
self.shunt_bus.set_val(arr_)
self.current_shunt_bus.values[self.shunt_bus.changed] = self.shunt_bus.values[self.shunt_bus.changed]
[docs] def _aux_iadd_reconcile_disco_reco(self):
"""
.. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\
Internal implementation of +=
"""
disco_or = (self._status_or_before == -1) | (self._status_or == -1)
disco_ex = (self._status_ex_before == -1) | (self._status_ex == -1)
disco_now = (
disco_or | disco_ex
) # a powerline is disconnected if at least one of its extremity is
# added
reco_or = (self._status_or_before == -1) & (self._status_or >= 1)
reco_ex = (self._status_or_before == -1) & (self._status_ex >= 1)
reco_now = reco_or | reco_ex
# Set nothing
set_now = np.zeros_like(self._status_or)
# Force some disconnections
set_now[disco_now] = -1
set_now[reco_now] = 1
self.current_topo.set_status(
set_now,
self.line_or_pos_topo_vect,
self.line_ex_pos_topo_vect,
self.last_topo_registered,
)
[docs] def __iadd__(self, other : BaseAction) -> Self:
"""
.. 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: :class:`grid2op.Action.BaseAction`
Returns
-------
The updated state of `self` after the new action `other` has been added to it.
"""
set_status = other._set_line_status
switch_status = other._switch_line_status
set_topo_vect = other._set_topo_vect
switcth_topo_vect = other._change_bus_vect
redispatching = other._redispatch
storage_power = other._storage_power
# I deal with injections
# Ia set the injection
if other._modif_inj:
self._aux_iadd_inj(other._dict_inj)
# Ib change the injection aka redispatching
if other._modif_redispatch:
self.prod_p.change_val(redispatching)
# Ic storage unit
if other._modif_storage:
self.storage_power.set_val(storage_power)
# II shunts
if type(self).shunts_data_available:
self._aux_iadd_shunt(other)
# III line status
# this need to be done BEFORE the topology, as a connected powerline will be connected to their old bus.
# regardless if the status is changed in the action or not.
if other._modif_change_status:
self.current_topo.change_status(
switch_status,
self.line_or_pos_topo_vect,
self.line_ex_pos_topo_vect,
self.last_topo_registered,
)
if other._modif_set_status:
self.current_topo.set_status(
set_status,
self.line_or_pos_topo_vect,
self.line_ex_pos_topo_vect,
self.last_topo_registered,
)
# if other._modif_change_status or other._modif_set_status:
(
self._status_or_before[:],
self._status_ex_before[:],
) = self.current_topo.get_line_status(
self.line_or_pos_topo_vect, self.line_ex_pos_topo_vect
)
# IV topo
if other._modif_change_bus:
self.current_topo.change_val(switcth_topo_vect)
if other._modif_set_bus:
self.current_topo.set_val(set_topo_vect)
# V Force disconnected status
# of disconnected powerlines extremities
self._status_or[:], self._status_ex[:] = self.current_topo.get_line_status(
self.line_or_pos_topo_vect, self.line_ex_pos_topo_vect
)
# At least one disconnected extremity
if other._modif_change_bus or other._modif_set_bus:
self._aux_iadd_reconcile_disco_reco()
return self
[docs] def _assign_0_to_disco_el(self) -> None:
"""
.. 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
"""
cls = type(self)
gen_changed = self.current_topo.changed[cls.gen_pos_topo_vect]
gen_bus = self.current_topo.values[cls.gen_pos_topo_vect]
self.prod_p.force_unchanged(gen_changed, gen_bus)
self.prod_v.force_unchanged(gen_changed, gen_bus)
load_changed = self.current_topo.changed[cls.load_pos_topo_vect]
load_bus = self.current_topo.values[cls.load_pos_topo_vect]
self.load_p.force_unchanged(load_changed, load_bus)
self.load_q.force_unchanged(load_changed, load_bus)
sto_changed = self.current_topo.changed[cls.storage_pos_topo_vect]
sto_bus = self.current_topo.values[cls.storage_pos_topo_vect]
self.storage_power.force_unchanged(sto_changed, sto_bus)
[docs] def __call__(self) -> Tuple[np.ndarray,
Tuple[ValueStore, ValueStore, ValueStore, ValueStore, ValueStore],
ValueStore,
Union[Tuple[ValueStore, ValueStore, ValueStore], None]]:
"""
This function should be called at the top of the :func:`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:
.. code-block:: python
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)
"""
self._assign_0_to_disco_el()
injections = (
self.prod_p,
self.prod_v,
self.load_p,
self.load_q,
self.storage_power,
)
topo = self.current_topo
shunts = None
if type(self).shunts_data_available:
shunts = self.shunt_p, self.shunt_q, self.shunt_bus
self._get_active_bus()
return self.activated_bus, injections, topo, shunts
[docs] def get_loads_bus(self) -> ValueStore:
"""
This function might be called in the implementation of :func:`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 :class:`_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 /!\\\\
.. seealso::
The other related functions:
- :func:`_BackendAction.get_loads_bus`
- :func:`_BackendAction.get_gens_bus`
- :func:`_BackendAction.get_lines_or_bus`
- :func:`_BackendAction.get_lines_ex_bus`
- :func:`_BackendAction.get_storages_bus`
Examples
-----------
A typical use of `get_loads_bus` in `apply_action` is:
.. code-block:: python
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`
"""
if self._loads_bus is None:
self._loads_bus = ValueStore(type(self).n_load, dtype=dt_int)
self._loads_bus.copy_from_index(self.current_topo, type(self).load_pos_topo_vect)
return self._loads_bus
def _aux_to_global(self, value_store, to_subid) -> ValueStore:
value_store = copy.deepcopy(value_store)
value_store.values = type(self).local_bus_to_global(value_store.values, to_subid)
return value_store
[docs] def get_loads_bus_global(self) -> ValueStore:
"""
This function might be called in the implementation of :func:`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 :class:`_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 /!\\\\
.. seealso::
The other related functions:
- :func:`_BackendAction.get_loads_bus_global`
- :func:`_BackendAction.get_gens_bus_global`
- :func:`_BackendAction.get_lines_or_bus_global`
- :func:`_BackendAction.get_lines_ex_bus_global`
- :func:`_BackendAction.get_storages_bus_global`
Examples
-----------
A typical use of `get_loads_bus_global` in `apply_action` is:
.. code-block:: python
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`
"""
tmp_ = self.get_loads_bus()
return self._aux_to_global(tmp_, type(self).load_to_subid)
[docs] def get_gens_bus(self) -> ValueStore:
"""
This function might be called in the implementation of :func:`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 :class:`_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 /!\\\\
.. seealso::
The other related functions:
- :func:`_BackendAction.get_loads_bus`
- :func:`_BackendAction.get_gens_bus`
- :func:`_BackendAction.get_lines_or_bus`
- :func:`_BackendAction.get_lines_ex_bus`
- :func:`_BackendAction.get_storages_bus`
Examples
---------
Some examples are given in the documentation of :func:`_BackendAction.get_loads_bus`
"""
if self._gens_bus is None:
self._gens_bus = ValueStore(type(self).n_gen, dtype=dt_int)
self._gens_bus.copy_from_index(self.current_topo, type(self).gen_pos_topo_vect)
return self._gens_bus
[docs] def get_gens_bus_global(self) -> ValueStore:
"""
This function might be called in the implementation of :func:`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 :class:`_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 /!\\\\
.. seealso::
The other related functions:
- :func:`_BackendAction.get_loads_bus_global`
- :func:`_BackendAction.get_gens_bus_global`
- :func:`_BackendAction.get_lines_or_bus_global`
- :func:`_BackendAction.get_lines_ex_bus_global`
- :func:`_BackendAction.get_storages_bus_global`
Examples
---------
Some examples are given in the documentation of :func:`_BackendAction.get_loads_bus_global`
"""
tmp_ = copy.deepcopy(self.get_gens_bus())
return self._aux_to_global(tmp_, type(self).gen_to_subid)
[docs] def get_lines_or_bus(self) -> ValueStore:
"""
This function might be called in the implementation of :func:`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 :class:`_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 /!\\\\
.. seealso::
The other related functions:
- :func:`_BackendAction.get_loads_bus`
- :func:`_BackendAction.get_gens_bus`
- :func:`_BackendAction.get_lines_or_bus`
- :func:`_BackendAction.get_lines_ex_bus`
- :func:`_BackendAction.get_storages_bus`
Examples
---------
Some examples are given in the documentation of :func:`_BackendAction.get_loads_bus`
"""
if self._lines_or_bus is None:
self._lines_or_bus = ValueStore(type(self).n_line, dtype=dt_int)
self._lines_or_bus.copy_from_index(
self.current_topo, type(self).line_or_pos_topo_vect
)
return self._lines_or_bus
[docs] def get_lines_or_bus_global(self) -> ValueStore:
"""
This function might be called in the implementation of :func:`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 :class:`_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 /!\\\\
.. seealso::
The other related functions:
- :func:`_BackendAction.get_loads_bus_global`
- :func:`_BackendAction.get_gens_bus_global`
- :func:`_BackendAction.get_lines_or_bus_global`
- :func:`_BackendAction.get_lines_ex_bus_global`
- :func:`_BackendAction.get_storages_bus_global`
Examples
---------
Some examples are given in the documentation of :func:`_BackendAction.get_loads_bus_global`
"""
tmp_ = self.get_lines_or_bus()
return self._aux_to_global(tmp_, type(self).line_or_to_subid)
[docs] def get_lines_ex_bus(self) -> ValueStore:
"""
This function might be called in the implementation of :func:`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 :class:`_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 /!\\\\
.. seealso::
The other related functions:
- :func:`_BackendAction.get_loads_bus`
- :func:`_BackendAction.get_gens_bus`
- :func:`_BackendAction.get_lines_or_bus`
- :func:`_BackendAction.get_lines_ex_bus`
- :func:`_BackendAction.get_storages_bus`
Examples
---------
Some examples are given in the documentation of :func:`_BackendAction.get_loads_bus`
"""
if self._lines_ex_bus is None:
self._lines_ex_bus = ValueStore(type(self).n_line, dtype=dt_int)
self._lines_ex_bus.copy_from_index(
self.current_topo, type(self).line_ex_pos_topo_vect
)
return self._lines_ex_bus
[docs] def get_lines_ex_bus_global(self) -> ValueStore:
"""
This function might be called in the implementation of :func:`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 :class:`_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 /!\\\\
.. seealso::
The other related functions:
- :func:`_BackendAction.get_loads_bus_global`
- :func:`_BackendAction.get_gens_bus_global`
- :func:`_BackendAction.get_lines_or_bus_global`
- :func:`_BackendAction.get_lines_ex_bus_global`
- :func:`_BackendAction.get_storages_bus_global`
Examples
---------
Some examples are given in the documentation of :func:`_BackendAction.get_loads_bus_global`
"""
tmp_ = self.get_lines_ex_bus()
return self._aux_to_global(tmp_, type(self).line_ex_to_subid)
[docs] def get_storages_bus(self) -> ValueStore:
"""
This function might be called in the implementation of :func:`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 :class:`_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 /!\\\\
.. seealso::
The other related functions:
- :func:`_BackendAction.get_loads_bus`
- :func:`_BackendAction.get_gens_bus`
- :func:`_BackendAction.get_lines_or_bus`
- :func:`_BackendAction.get_lines_ex_bus`
- :func:`_BackendAction.get_storages_bus`
Examples
---------
Some examples are given in the documentation of :func:`_BackendAction.get_loads_bus`
"""
if self._storage_bus is None:
self._storage_bus = ValueStore(type(self).n_storage, dtype=dt_int)
self._storage_bus.copy_from_index(self.current_topo, type(self).storage_pos_topo_vect)
return self._storage_bus
[docs] def get_storages_bus_global(self) -> ValueStore:
"""
This function might be called in the implementation of :func:`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 :class:`_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 /!\\\\
.. seealso::
The other related functions:
- :func:`_BackendAction.get_loads_bus_global`
- :func:`_BackendAction.get_gens_bus_global`
- :func:`_BackendAction.get_lines_or_bus_global`
- :func:`_BackendAction.get_lines_ex_bus_global`
- :func:`_BackendAction.get_storages_bus_global`
Examples
---------
Some examples are given in the documentation of :func:`_BackendAction.get_loads_bus_global`
"""
tmp_ = self.get_storages_bus()
return self._aux_to_global(tmp_, type(self).storage_to_subid)
[docs] def get_shunts_bus_global(self) -> ValueStore:
"""
This function might be called in the implementation of :func:`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 :class:`_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 /!\\\\
.. seealso::
The other related functions:
- :func:`_BackendAction.get_loads_bus_global`
- :func:`_BackendAction.get_gens_bus_global`
- :func:`_BackendAction.get_lines_or_bus_global`
- :func:`_BackendAction.get_lines_ex_bus_global`
- :func:`_BackendAction.get_storages_bus_global`
Examples
---------
Some examples are given in the documentation of :func:`_BackendAction.get_loads_bus_global`
"""
tmp_ = self.shunt_bus
return self._aux_to_global(tmp_, type(self).shunt_to_subid)
[docs] def _get_active_bus(self) -> None:
"""
.. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\
"""
self.activated_bus[:, :] = False
tmp = self.current_topo.values - 1
is_el_conn = tmp >= 0
self.activated_bus[self.big_topo_to_subid[is_el_conn], tmp[is_el_conn]] = True
if type(self).shunts_data_available:
is_el_conn = self.current_shunt_bus.values >= 0
tmp = self.current_shunt_bus.values - 1
self.activated_bus[type(self).shunt_to_subid[is_el_conn], tmp[is_el_conn]] = True
[docs] def update_state(self, powerline_disconnected) -> None:
"""
.. 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.
"""
if (powerline_disconnected >= 0).any():
arr_ = np.zeros(powerline_disconnected.shape, dtype=dt_int)
arr_[powerline_disconnected >= 0] = -1
self.current_topo.set_status(
arr_,
self.line_or_pos_topo_vect,
self.line_ex_pos_topo_vect,
self.last_topo_registered,
)
self.last_topo_registered.update_connected(self.current_topo)
self.current_topo.reset()