Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Load and Generation Shedding #655

Open
wants to merge 15 commits into
base: dev_turnoff_gen_load
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions getting_started/13_DetachmentOfLoadsAndGenerators.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Detachment of Loads and Generators\n",
"In emergency conditions, it may be possible / necessary for a grid operator to detach certain loads, generators, or other components in order to prevent a larger blackout. This notebook explores how this can be achieved in Grid2OP. \n",
"\n",
"By default shedding is disabled in all environments, to provide the keyword argument allow_detachment when initializing the environment."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"import grid2op\n",
"from lightsim2grid import LightSimBackend\n",
"from grid2op.Parameters import Parameters\n",
"\n",
"from grid2op.PlotGrid import PlotMatplot\n",
"from pathlib import Path\n",
"\n",
"data_path = Path.cwd() / \"grid2op\" / \"data\"\n",
"p = Parameters()\n",
"p.MAX_SUB_CHANGED = 5\n",
"env = grid2op.make(data_path / \"rte_case5_example\", param=p, allow_detachment=True)\n",
"plotter = PlotMatplot(env.observation_space, load_name=True, gen_name=True, dpi=150)\n",
"print(f\"Loads: {env.n_load}, Generators: {env.n_gen}, Storage: {env.n_storage}, Allow Detachment: {env.allow_detachment}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Disconnect the load at substation 4\n",
"load_lookup = {name:i for i,name in enumerate(env.name_load)}\n",
"gen_lookup = {name:i for i,name in enumerate(env.name_gen)}\n",
"act = env.action_space({\"set_bus\":[(env.load_pos_topo_vect[load_lookup[\"load_4_2\"]], -1),\n",
" (env.load_pos_topo_vect[load_lookup[\"load_3_1\"]], -1)]})\n",
"# act = env.action_space({\"set_bus\":[(env.gen_pos_topo_vect[gen_lookup[\"gen_0_0\"]], -1)]})\n",
"print(act)\n",
"env.set_id(\"00\")\n",
"init_obs = env.reset()\n",
"obs, reward, done, info = env.step(act)\n",
"plotter.plot_obs(obs, figure=plt.figure(figsize=(8,5)))\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import pandapower as pp\n",
"network = env.backend._grid.deepcopy()\n",
"display(network.res_line.loc[:, [\"p_from_mw\", \"p_to_mw\", \"q_from_mvar\", \"q_to_mvar\"]])\n",
"pp.runpp(network,\n",
" check_connectivity=False,\n",
" init=\"dc\",\n",
" lightsim2grid=False,\n",
" max_iteration=10,\n",
" distributed_slack=False,\n",
")\n",
"display(network.res_line.loc[:, [\"p_from_mw\", \"p_to_mw\", \"q_from_mvar\", \"q_to_mvar\"]])"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"env.backend.loads_info()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"env.backend.generators_info()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"topo_vect = env.backend.get_topo_vect()\n",
"topo_vect[env.backend.load_pos_topo_vect]"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "venv_grid2op",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.7"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
12 changes: 6 additions & 6 deletions grid2op/Action/baseAction.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ def __init__(self, _names_chronics_to_backend: Optional[Dict[Literal["loads", "p
self._modif_alert = False

@classmethod
def process_shunt_satic_data(cls):
def process_shunt_static_data(cls):
if not cls.shunts_data_available:
# this is really important, otherwise things from grid2op base types will be affected
cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect)
Expand All @@ -506,7 +506,7 @@ def process_shunt_satic_data(cls):
except ValueError:
pass
cls.attr_list_set = set(cls.attr_list_vect)
return super().process_shunt_satic_data()
return super().process_shunt_static_data()

def copy(self) -> "BaseAction":
# sometimes this method is used...
Expand Down Expand Up @@ -573,8 +573,8 @@ def __copy__(self) -> "BaseAction":
return res

@classmethod
def process_shunt_satic_data(cls):
return super().process_shunt_satic_data()
def process_shunt_static_data(cls):
return super().process_shunt_static_data()

def __deepcopy__(self, memodict={}) -> "BaseAction":
res = type(self)()
Expand Down Expand Up @@ -845,7 +845,7 @@ def process_grid2op_compat(cls):
# if there are only one busbar, the "set_bus" action can still be used
# to disconnect the element, this is why it's not removed
cls._aux_process_n_busbar_per_sub()

cls.attr_list_set = copy.deepcopy(cls.attr_list_set)
cls.attr_list_set = set(cls.attr_list_vect)

Expand Down Expand Up @@ -1098,7 +1098,7 @@ def __eq__(self, other) -> bool:
self._change_bus_vect == other._change_bus_vect
):
return False

# shunts are the same
if type(self).shunts_data_available:
if self.n_shunt != other.n_shunt:
Expand Down
108 changes: 104 additions & 4 deletions grid2op/Backend/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
DivergingPowerflow,
Grid2OpException,
)
from grid2op.Space import GridObjects, DEFAULT_N_BUSBAR_PER_SUB
from grid2op.Space import GridObjects, DEFAULT_N_BUSBAR_PER_SUB, DEFAULT_ALLOW_DETACHMENT


# TODO method to get V and theta at each bus, could be in the same shape as check_kirchoff
Expand Down Expand Up @@ -122,8 +122,8 @@ class Backend(GridObjects, ABC):

ERR_INIT_POWERFLOW : str = "Power cannot be computed on the first time step, please check your data."
def __init__(self,
detailed_infos_for_cascading_failures: bool=False,
can_be_copied: bool=True,
detailed_infos_for_cascading_failures:bool=False,
can_be_copied:bool=True,
**kwargs):
"""
Initialize an instance of Backend. This does nothing per se. Only the call to :func:`Backend.load_grid`
Expand Down Expand Up @@ -179,6 +179,10 @@ def __init__(self,
#: There is a difference between this and the class attribute.
#: You should not worry about the class attribute of the backend in :func:`Backend.apply_action`
self.n_busbar_per_sub: int = DEFAULT_N_BUSBAR_PER_SUB

# .. versionadded: 1.11.0
self._missing_detachment_support:bool = True
self._allow_detachment:bool = DEFAULT_ALLOW_DETACHMENT

def can_handle_more_than_2_busbar(self):
"""
Expand Down Expand Up @@ -240,7 +244,65 @@ def cannot_handle_more_than_2_busbar(self):
"'fix' this issue, you need to change the implementation of your backend or "
"upgrade it to a newer version.")
self.n_busbar_per_sub = DEFAULT_N_BUSBAR_PER_SUB



def can_handle_detachment(self):
"""
.. versionadded:: 1.11.0

This function should be called once in :func:`Backend.load_grid` if your backend is able
to handle the detachment of loads and generators.

If not called, then the `environment` will not be able to detach loads and generators.

.. seealso::
:func:`Backend.cannot_handle_detachment`

.. note::
From grid2op 1.11.0 it is preferable that your backend calls one of
:func:`Backend.can_handle_detachment` or
:func:`Backend.cannot_handle_detachment`.

If not, then the environments created with your backend will not be able to
"operate" grid with load and generator detachment.

.. danger::
We highly recommend you do not try to override this function.
At least, at time of writing there is no good reason to do so.
"""
self._missing_detachment_support = False
self._allow_detachment = type(self)._allow_detachment

def cannot_handle_detachment(self):
"""
.. versionadded:: 1.11.0

This function should be called once in :func:`Backend.load_grid` if your backend is **NOT** able
to handle the detachment of loads and generators.

If not called, then the `environment` will not be able to detach loads and generators.

.. seealso::
:func:`Backend.cannot_handle_detachment`

.. note::
From grid2op 1.11.0 it is preferable that your backend calls one of
:func:`Backend.can_handle_detachment` or
:func:`Backend.cannot_handle_detachment`.

If not, then the environments created with your backend will not be able to
"operate" grid with load and generator detachment.

.. danger::
We highly recommend you do not try to override this function.
At least, at time of writing there is no good reason to do so.
"""
self._missing_detachment_support = False
if type(self._allow_detachment != DEFAULT_ALLOW_DETACHMENT):
warnings.warn("You asked in 'make' function to allow shedding. This is"
f"not possible with a backend of type {type(self)}.")
self._allow_detachment = DEFAULT_ALLOW_DETACHMENT

def make_complete_path(self,
path : Union[os.PathLike, str],
filename : Optional[Union[os.PathLike, str]]=None) -> str:
Expand Down Expand Up @@ -1017,7 +1079,24 @@ def _runpf_with_diverging_exception(self, is_dc : bool) -> Optional[Exception]:
"""
conv = False
exc_me = None

try:
# Check if loads/gens have been detached and if this is allowed, otherwise raise an error
# .. versionadded:: 1.11.0
if hasattr(self, "_get_topo_vect"):
topo_vect = self._get_topo_vect()
else:
topo_vect = self.get_topo_vect()
load_buses = topo_vect[self.load_pos_topo_vect]
if not self._allow_detachment and (load_buses == -1).any():
raise Grid2OpException(f"One or more loads were detached before powerflow in Backend {type(self).__name__}"
"but this is not allowed or not supported (Game Over)")

gen_buses = topo_vect[self.gen_pos_topo_vect]
if not self._allow_detachment and (gen_buses == -1).any():
raise Grid2OpException(f"One or more generators were detached before powerflow in Backend {type(self).__name__}"
"but this is not allowed or not supported (Game Over)")

conv, exc_me = self.runpf(is_dc=is_dc) # run powerflow
except Grid2OpException as exc_:
exc_me = exc_
Expand Down Expand Up @@ -2081,6 +2160,27 @@ def assert_grid_correct(self, _local_dir_cls=None) -> None:
warnings.warn("Your backend is missing the `_missing_two_busbars_support_info` "
"attribute. This is known issue in lightims2grid <= 0.7.5. Please "
"upgrade your backend. This will raise an error in the future.")

if hasattr(self, "_missing_detachment_support"):
if self._missing_detachment_support:
warnings.warn("The backend implementation you are using is probably too old to take advantage of the "
"new feature added in grid2op 1.11.0: the possibility "
"to detach loads or generators without leading to an immediate game over. "
"To silence this warning, you can modify the `load_grid` implementation "
"of your backend and either call:\n"
"- self.can_handle_detachment if the current implementation "
" can handle detachments OR\n"
"- self.cannot_handle_detachment if not."
"\nAnd of course, ideally, if the current implementation "
"of your backend cannot handle detachment then change it :-)\n"
"Your backend will behave as if it did not support it.")
self._missing_detachment_support = False
self._allow_detachment = DEFAULT_ALLOW_DETACHMENT
else:
self._missing_detachment_support = False
self._allow_detachment = DEFAULT_ALLOW_DETACHMENT
warnings.warn("Your backend is missing the `_missing_detachment_support` "
"attribute.")

orig_type = type(self)
if orig_type.my_bk_act_class is None and orig_type._INIT_GRID_CLS is None:
Expand Down
31 changes: 6 additions & 25 deletions grid2op/Backend/pandaPowerBackend.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def __init__(
lightsim2grid=lightsim2grid,
dist_slack=dist_slack,
max_iter=max_iter,
with_numba=with_numba
with_numba=with_numba,
)
self.with_numba : bool = with_numba
self.prod_pu_to_kv : Optional[np.ndarray] = None
Expand Down Expand Up @@ -344,6 +344,7 @@ def load_grid(self,

"""
self.can_handle_more_than_2_busbar()
self.can_handle_detachment()
full_path = self.make_complete_path(path, filename)

with warnings.catch_warnings():
Expand Down Expand Up @@ -541,7 +542,7 @@ def load_grid(self,
pp.create_bus(self._grid, index=ind, **el)
self._init_private_attrs()
self._aux_run_pf_init() # run yet another powerflow with the added buses

# do this at the end
self._in_service_line_col_id = int((self._grid.line.columns == "in_service").nonzero()[0][0])
self._in_service_trafo_col_id = int((self._grid.trafo.columns == "in_service").nonzero()[0][0])
Expand Down Expand Up @@ -1016,28 +1017,7 @@ def _aux_runpf_pp(self, is_dc: bool):
warnings.filterwarnings("ignore", category=RuntimeWarning)
warnings.filterwarnings("ignore", category=DeprecationWarning)
self._pf_init = "dc"
# nb_bus = self.get_nb_active_bus()
# if self._nb_bus_before is None:
# self._pf_init = "dc"
# elif nb_bus == self._nb_bus_before:
# self._pf_init = "results"
# else:
# self._pf_init = "auto"

if (~self._grid.load["in_service"]).any():
# TODO see if there is a better way here -> do not handle this here, but rather in Backend._next_grid_state
raise pp.powerflow.LoadflowNotConverged("Disconnected load: for now grid2op cannot handle properly"
" disconnected load. If you want to disconnect one, say it"
" consumes 0. instead. Please check loads: "
f"{(~self._grid.load['in_service'].values).nonzero()[0]}"
)
if (~self._grid.gen["in_service"]).any():
# TODO see if there is a better way here -> do not handle this here, but rather in Backend._next_grid_state
raise pp.powerflow.LoadflowNotConverged("Disconnected gen: for now grid2op cannot handle properly"
" disconnected generators. If you want to disconnect one, say it"
" produces 0. instead. Please check generators: "
f"{(~self._grid.gen['in_service'].values).nonzero()[0]}"
)

try:
if is_dc:
pp.rundcpp(self._grid, check_connectivity=True, init="flat")
Expand Down Expand Up @@ -1325,6 +1305,7 @@ def copy(self) -> "PandaPowerBackend":
res._in_service_trafo_col_id = self._in_service_trafo_col_id

res._missing_two_busbars_support_info = self._missing_two_busbars_support_info
res._missing_detachment_support = self._missing_detachment_support
res.div_exception = self.div_exception
return res

Expand Down Expand Up @@ -1545,4 +1526,4 @@ def _storages_info(self):
def sub_from_bus_id(self, bus_id : int) -> int:
if bus_id >= self._number_true_line:
return bus_id - self._number_true_line
return bus_id
return bus_id
Loading