Skip to content

Commit

Permalink
Merge remote-tracking branch 'deuce1957/dev_shedding' into dev_shedding
Browse files Browse the repository at this point in the history
  • Loading branch information
BDonnot committed Nov 19, 2024
2 parents bbec434 + 486cffa commit f16ba3c
Show file tree
Hide file tree
Showing 21 changed files with 483 additions and 114 deletions.
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

0 comments on commit f16ba3c

Please sign in to comment.