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

Implement n-1 security #423

Draft
wants to merge 12 commits into
base: dev
Choose a base branch
from
Prev Previous commit
Next Next commit
Allow checking for n-1 of stations
  • Loading branch information
birgits committed Feb 21, 2024
commit 3a179cc0e0eaf47b49203c0192ec2f13cc12d3e0
80 changes: 60 additions & 20 deletions edisgo/flex_opt/check_tech_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def mv_line_max_relative_overload(edisgo_obj, n_minus_one=False):
n_minus_one : bool
Determines which allowed load factors to use (see :py:attr:`~lines_allowed_load`
for more information).
Default: False.

Returns
-------
Expand Down Expand Up @@ -71,6 +72,7 @@ def lv_line_max_relative_overload(edisgo_obj, n_minus_one=False, lv_grid_id=None
lv_grid_id : str or int or None
If None, checks overloading for all LV lines. Otherwise, only lines in given
LV grid are checked. Default: None.
Default: False.

Returns
-------
Expand Down Expand Up @@ -224,6 +226,7 @@ def lines_allowed_load(edisgo_obj, lines=None, n_minus_one=False):
'config_grid_expansion' in section 'grid_expansion_load_factors_n_minus_one'
are used for lines in rings. For all other lines, 'grid_expansion_load_factors'
are used.
Default: False.

Returns
-------
Expand Down Expand Up @@ -380,13 +383,17 @@ def lines_relative_load(edisgo_obj, lines=None, n_minus_one=False):
return loading / allowed_loading


def hv_mv_station_max_overload(edisgo_obj):
def hv_mv_station_max_overload(edisgo_obj, n_minus_one=False):
"""
Checks for over-loading of HV/MV station.

Parameters
----------
edisgo_obj : :class:`~.EDisGo`
n_minus_one : bool
Determines which allowed load factors to use. See
:py:attr:`~stations_allowed_load` for more information.
Default: False.

Returns
-------
Expand All @@ -403,10 +410,11 @@ def hv_mv_station_max_overload(edisgo_obj):
-----
Over-load is determined based on allowed load factors for feed-in and
load cases that are defined in the config file 'config_grid_expansion' in
section 'grid_expansion_load_factors'.
section 'grid_expansion_load_factors' or in case of n-1 security
'grid_expansion_load_factors_n_minus_one'.

"""
crit_stations = _station_max_overload(edisgo_obj, edisgo_obj.topology.mv_grid)
crit_stations = _station_max_overload(edisgo_obj, edisgo_obj.topology.mv_grid, n_minus_one=n_minus_one)
if not crit_stations.empty:
logger.debug("==> HV/MV station has load issues.")
else:
Expand All @@ -415,7 +423,7 @@ def hv_mv_station_max_overload(edisgo_obj):
return crit_stations


def mv_lv_station_max_overload(edisgo_obj, lv_grid_id=None):
def mv_lv_station_max_overload(edisgo_obj, lv_grid_id=None, n_minus_one=False):
"""
Checks for over-loading of MV/LV stations.

Expand All @@ -425,6 +433,10 @@ def mv_lv_station_max_overload(edisgo_obj, lv_grid_id=None):
lv_grid_id : str or int or None
If None, checks overloading for all MV/LV stations. Otherwise, only station
in given LV grid is checked. Default: None.
n_minus_one : bool
Determines which allowed load factors to use. See
:py:attr:`~stations_allowed_load` for more information.
Default: False.

Returns
-------
Expand All @@ -441,7 +453,8 @@ def mv_lv_station_max_overload(edisgo_obj, lv_grid_id=None):
-----
Over-load is determined based on allowed load factors for feed-in and
load cases that are defined in the config file 'config_grid_expansion' in
section 'grid_expansion_load_factors'.
section 'grid_expansion_load_factors' or in case of n-1 security
'grid_expansion_load_factors_n_minus_one'.

"""
crit_stations = pd.DataFrame(dtype=float)
Expand All @@ -454,7 +467,7 @@ def mv_lv_station_max_overload(edisgo_obj, lv_grid_id=None):
crit_stations = pd.concat(
[
crit_stations,
_station_max_overload(edisgo_obj, lv_grid),
_station_max_overload(edisgo_obj, lv_grid, n_minus_one=n_minus_one),
]
)
if not crit_stations.empty:
Expand All @@ -469,14 +482,18 @@ def mv_lv_station_max_overload(edisgo_obj, lv_grid_id=None):
return crit_stations


def _station_max_overload(edisgo_obj, grid):
def _station_max_overload(edisgo_obj, grid, n_minus_one=False):
"""
Checks for over-loading of stations.

Parameters
----------
edisgo_obj : :class:`~.EDisGo`
grid : :class:`~.network.grids.LVGrid` or :class:`~.network.grids.MVGrid`
n_minus_one : bool
Determines which allowed load factors to use. See
:py:attr:`~stations_allowed_load` for more information.
Default: False.

Returns
-------
Expand All @@ -501,7 +518,7 @@ def _station_max_overload(edisgo_obj, grid):
s_station_pfa = _station_load(edisgo_obj, grid)

# get maximum allowed apparent power of station in each time step
s_station_allowed = _station_allowed_load(edisgo_obj, grid)
s_station_allowed = _station_allowed_load(edisgo_obj, grid, n_minus_one=n_minus_one)

# calculate residual apparent power (if negative, station is over-loaded)
s_res = s_station_allowed - s_station_pfa
Expand Down Expand Up @@ -583,10 +600,9 @@ def _station_load(edisgo_obj, grid):
raise ValueError("Inserted grid is invalid.")


def _station_allowed_load(edisgo_obj, grid):
def _station_allowed_load(edisgo_obj, grid, n_minus_one=False):
"""
Returns allowed loading of grid's station to the overlying voltage level per time
step in MVA.
Returns allowed loading of grid's station per time step in MVA.

Allowed loading considers allowed load factors in heavy load flow case ('load case')
and reverse power flow case ('feed-in case') that are defined in the config file
Expand All @@ -597,6 +613,9 @@ def _station_allowed_load(edisgo_obj, grid):
edisgo_obj : :class:`~.EDisGo`
grid : :class:`~.network.grids.LVGrid` or :class:`~.network.grids.MVGrid`
Grid to get allowed station loading for.
n_minus_one : bool
Determines which allowed load factors to use. See
:py:attr:`~stations_allowed_load` for more information.

Returns
-------
Expand All @@ -620,32 +639,52 @@ def _station_allowed_load(edisgo_obj, grid):

# get maximum allowed apparent power of station in each time step
s_station = sum(transformers_df.s_nom)
if n_minus_one is True:
which = "grid_expansion_load_factors_n_minus_one"
else:
which = "grid_expansion_load_factors"
load_factor = edisgo_obj.timeseries.timesteps_load_feedin_case.apply(
lambda _: edisgo_obj.config["grid_expansion_load_factors"][
f"{voltage_level}_{_}_transformer"
]
lambda _: edisgo_obj.config[which][f"{voltage_level}_{_}_transformer"]
)

return pd.DataFrame(
{grid.station_name: s_station * load_factor}, index=load_factor.index
)


def stations_allowed_load(edisgo_obj, grids=None):
def stations_allowed_load(edisgo_obj, grids=None, n_minus_one=False):
"""
Returns allowed loading of specified grids stations to the overlying voltage level
Returns allowed loading of specified grid's stations to the overlying voltage level
per time step in MVA.

Allowed loading considers allowed load factors in heavy load flow case ('load case')
and reverse power flow case ('feed-in case') that are defined in the config file
'config_grid_expansion' in section 'grid_expansion_load_factors'.
'config_grid_expansion' in section 'grid_expansion_load_factors' or in case of n-1
security 'grid_expansion_load_factors_n_minus_one'.

Whether load factors for the load or feed-in case apply is determined using the
residual load in the grid. In case different load factors are used in the different
cases for MV-LV stations this may not be the best way, as it could be the case that
the residual load in the entire grid is positive while it is negative in some LV
grids.
If you want to check for n-1 and have different allowed load factors for the load
and feed-in case it is suggested to use the reinforcement function
:func:`~.flex_opt.reinforce_grid.reinforce_for_n_minus_one`.

Parameters
----------
edisgo_obj : :class:`~.EDisGo`
grids : list(:class:`~.network.grids.Grid`)
List of MV and LV grids to get allowed station loading for. Per default
allowed loading is returned for all stations in the network. Default: None.
n_minus_one : bool
Determines which allowed load factors to use. In case it is set to False,
allowed load factors defined in the config file 'config_grid_expansion' in
section 'grid_expansion_load_factors' are used. This is the default.
In case it is set to True, allowed load factors defined in the config file
'config_grid_expansion' in section 'grid_expansion_load_factors_n_minus_one'
are used.
Default: False.

Returns
-------
Expand All @@ -663,12 +702,12 @@ def stations_allowed_load(edisgo_obj, grids=None):
allowed_loading = pd.DataFrame()
for grid in grids:
allowed_loading = pd.concat(
[allowed_loading, _station_allowed_load(edisgo_obj, grid)], axis=1
[allowed_loading, _station_allowed_load(edisgo_obj, grid, n_minus_one=n_minus_one)], axis=1
)
return allowed_loading


def stations_relative_load(edisgo_obj, grids=None):
def stations_relative_load(edisgo_obj, grids=None, n_minus_one=False):
"""
Returns relative loading of specified grids stations to the overlying voltage level
per time step in p.u..
Expand Down Expand Up @@ -729,6 +768,7 @@ def components_relative_load(edisgo_obj, n_minus_one=False):
n_minus_one : bool
Determines which allowed load factors to use. See :py:attr:`~lines_allowed_load`
for more information.
ToDo stations

Returns
-------
Expand All @@ -743,7 +783,7 @@ def components_relative_load(edisgo_obj, n_minus_one=False):
:attr:`~.network.grids.Grid.station_name`).

"""
stations_rel_load = stations_relative_load(edisgo_obj)
stations_rel_load = stations_relative_load(edisgo_obj, n_minus_one=n_minus_one)
lines_rel_load = lines_relative_load(
edisgo_obj, lines=None, n_minus_one=n_minus_one
)
Expand Down
21 changes: 21 additions & 0 deletions tests/flex_opt/test_check_tech_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,17 @@ def test_hv_mv_station_max_overload(self):
)
assert df.at["MVGrid_1_station", "time_index"] == self.timesteps[0]

# check n-1
df = check_tech_constraints.hv_mv_station_max_overload(self.edisgo, n_minus_one=True)
# check shape of dataframe
assert (1, 3) == df.shape
# check missing transformer capacity
assert np.isclose(
df.at["MVGrid_1_station", "s_missing"],
(np.hypot(30, 30) - 20),
)
assert df.at["MVGrid_1_station", "time_index"] == self.timesteps[0]

def test_mv_lv_station_max_overload(self):
# implicitly checks function _station_overload

Expand Down Expand Up @@ -291,6 +302,16 @@ def test__station_allowed_load(self):
]
assert np.isclose(40.0, df.loc[feed_in_cases.values].values).all()

# check MV grid n-1
df = check_tech_constraints._station_allowed_load(
self.edisgo, grid, n_minus_one=True
)
# check shape of dataframe
assert (4, 1) == df.shape
# check values
assert np.isclose(20.0, df.loc[load_cases.values].values).all()
assert np.isclose(40.0, df.loc[feed_in_cases.values].values).all()

def test_stations_allowed_load(self):
# check without specifying a grid
df = check_tech_constraints.stations_allowed_load(self.edisgo)
Expand Down