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
1 change: 1 addition & 0 deletions edisgo/edisgo.py
Original file line number Diff line number Diff line change
Expand Up @@ -1348,6 +1348,7 @@ def reinforce(
changes, etc.

"""
# ToDo n-1 hier einbauen? Ansonsten Info dass es in extra Funktion gemacht wird
if copy_grid:
edisgo_obj = copy.deepcopy(self)
else:
Expand Down
2 changes: 1 addition & 1 deletion edisgo/flex_opt/charging_strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ def charging_strategy(
)

# get residual load
init_residual_load = edisgo_obj.timeseries.residual_load
init_residual_load = edisgo_obj.timeseries.residual_load()

len_residual_load = int(charging_processes_df.park_end_timesteps.max())

Expand Down
214 changes: 135 additions & 79 deletions edisgo/flex_opt/check_tech_constraints.py

Large diffs are not rendered by default.

119 changes: 100 additions & 19 deletions edisgo/flex_opt/reinforce_grid.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import copy
import datetime
import logging

Expand All @@ -12,6 +13,7 @@
from edisgo.flex_opt import exceptions, reinforce_measures
from edisgo.flex_opt.costs import grid_expansion_costs
from edisgo.flex_opt.reinforce_measures import separate_lv_grid
from edisgo.network.timeseries import TimeSeriesRaw
from edisgo.tools import tools
from edisgo.tools.temporal_complexity_reduction import get_most_critical_time_steps

Expand Down Expand Up @@ -68,6 +70,7 @@ def reinforce_grid(
If True, excludes lines that were added in the generator import to connect
new generators from calculation of network expansion costs. Default: False.
n_minus_one : bool
ToDo adapt
Determines whether n-1 security should be checked. Currently, n-1 security
cannot be handled correctly, wherefore the case where this parameter is set to
True will lead to an error being raised. Default: False.
Expand Down Expand Up @@ -130,9 +133,6 @@ def reinforce_grid(
reinforcement is conducted.

"""
if n_minus_one is True:
raise NotImplementedError("n-1 security can currently not be checked.")

# check if provided mode is valid
if mode and mode not in ["mv", "mvlv", "lv"]:
raise ValueError(f"Provided mode {mode} is not a valid mode.")
Expand Down Expand Up @@ -208,24 +208,28 @@ def reinforce_grid(
overloaded_mv_station = (
pd.DataFrame(dtype=float)
if mode == "lv" or kwargs.get("skip_mv_reinforcement", False)
else checks.hv_mv_station_max_overload(edisgo)
else checks.hv_mv_station_max_overload(edisgo, n_minus_one=n_minus_one)
)
if lv_grid_id or (mode == "mv"):
overloaded_lv_stations = pd.DataFrame(dtype=float)
else:
overloaded_lv_stations = checks.mv_lv_station_max_overload(edisgo)
overloaded_lv_stations = checks.mv_lv_station_max_overload(
edisgo, n_minus_one=n_minus_one
)

logger.debug("==> Check line load.")
crit_lines = (
pd.DataFrame(dtype=float)
if mode == "lv" or kwargs.get("skip_mv_reinforcement", False)
else checks.mv_line_max_relative_overload(edisgo)
else checks.mv_line_max_relative_overload(edisgo, n_minus_one=n_minus_one)
)
if not mode or mode == "lv":
crit_lines = pd.concat(
[
crit_lines,
checks.lv_line_max_relative_overload(edisgo, lv_grid_id=lv_grid_id),
checks.lv_line_max_relative_overload(
edisgo, lv_grid_id=lv_grid_id, n_minus_one=n_minus_one
),
]
)

Expand Down Expand Up @@ -289,22 +293,26 @@ def reinforce_grid(
overloaded_mv_station = (
pd.DataFrame(dtype=float)
if mode == "lv" or kwargs.get("skip_mv_reinforcement", False)
else checks.hv_mv_station_max_overload(edisgo)
else checks.hv_mv_station_max_overload(edisgo, n_minus_one=n_minus_one)
)
if mode != "mv" and (not lv_grid_id):
overloaded_lv_stations = checks.mv_lv_station_max_overload(edisgo)
overloaded_lv_stations = checks.mv_lv_station_max_overload(
edisgo, n_minus_one=n_minus_one
)

logger.debug("==> Recheck line load.")
crit_lines = (
pd.DataFrame(dtype=float)
if mode == "lv" or kwargs.get("skip_mv_reinforcement", False)
else checks.mv_line_max_relative_overload(edisgo)
else checks.mv_line_max_relative_overload(edisgo, n_minus_one=n_minus_one)
)
if not mode or mode == "lv":
crit_lines = pd.concat(
[
crit_lines,
checks.lv_line_max_relative_overload(edisgo, lv_grid_id=lv_grid_id),
checks.lv_line_max_relative_overload(
edisgo, lv_grid_id=lv_grid_id, n_minus_one=n_minus_one
),
]
)

Expand Down Expand Up @@ -532,24 +540,28 @@ def reinforce_grid(
overloaded_mv_station = (
pd.DataFrame(dtype=float)
if mode == "lv" or kwargs.get("skip_mv_reinforcement", False)
else checks.hv_mv_station_max_overload(edisgo)
else checks.hv_mv_station_max_overload(edisgo, n_minus_one=n_minus_one)
)
if (lv_grid_id) or (mode == "mv"):
overloaded_lv_stations = pd.DataFrame(dtype=float)
else:
overloaded_lv_stations = checks.mv_lv_station_max_overload(edisgo)
overloaded_lv_stations = checks.mv_lv_station_max_overload(
edisgo, n_minus_one=n_minus_one
)

logger.debug("==> Recheck line load.")
crit_lines = (
pd.DataFrame(dtype=float)
if mode == "lv" or kwargs.get("skip_mv_reinforcement", False)
else checks.mv_line_max_relative_overload(edisgo)
else checks.mv_line_max_relative_overload(edisgo, n_minus_one=n_minus_one)
)
if not mode or mode == "lv":
crit_lines = pd.concat(
[
crit_lines,
checks.lv_line_max_relative_overload(edisgo, lv_grid_id=lv_grid_id),
checks.lv_line_max_relative_overload(
edisgo, lv_grid_id=lv_grid_id, n_minus_one=n_minus_one
),
]
)

Expand Down Expand Up @@ -613,22 +625,26 @@ def reinforce_grid(
overloaded_mv_station = (
pd.DataFrame(dtype=float)
if mode == "lv" or kwargs.get("skip_mv_reinforcement", False)
else checks.hv_mv_station_max_overload(edisgo)
else checks.hv_mv_station_max_overload(edisgo, n_minus_one=n_minus_one)
)
if mode != "mv" and (not lv_grid_id):
overloaded_lv_stations = checks.mv_lv_station_max_overload(edisgo)
overloaded_lv_stations = checks.mv_lv_station_max_overload(
edisgo, n_minus_one=n_minus_one
)

logger.debug("==> Recheck line load.")
crit_lines = (
pd.DataFrame(dtype=float)
if mode == "lv" or kwargs.get("skip_mv_reinforcement", False)
else checks.mv_line_max_relative_overload(edisgo)
else checks.mv_line_max_relative_overload(edisgo, n_minus_one=n_minus_one)
)
if not mode or mode == "lv":
crit_lines = pd.concat(
[
crit_lines,
checks.lv_line_max_relative_overload(edisgo, lv_grid_id=lv_grid_id),
checks.lv_line_max_relative_overload(
edisgo, lv_grid_id=lv_grid_id, n_minus_one=n_minus_one
),
]
)

Expand Down Expand Up @@ -677,6 +693,71 @@ def reinforce_grid(
return edisgo.results


def reinforce_for_n_minus_one(
edisgo_obj: EDisGo,
**kwargs,
):
"""
Grid reinforcment to comply with n-1 security

Only means reduced allowed line loading. Voltage limits are not changed.
Fault conditions are not used. But load case is checked per MV feeder.

Per default LV is included. To change that set mode to "mv".

Parameters
----------
edisgo : :class:`~.EDisGo`
kwargs : dict
See parameters of function
:func:`edisgo.flex_opt.reinforce_grid.reinforce_grid`.

Returns
--------

"""
# ToDo HV/MV transformer needs to be checked separately

comp_types = ["loads", "generators", "storage_units"]
# get residual load in each MV feeder
edisgo_obj.topology.assign_feeders("mv_feeder")
residual_load_feeders = edisgo_obj.timeseries.residual_load(
feeder="mv_feeder", edisgo_obj=edisgo_obj
)
# save original time series
orig_ts = copy.deepcopy(edisgo_obj.timeseries)
orig_ts.time_series_raw = TimeSeriesRaw()
# per feeder, set all time series to zero, in case residual load in feeder is
# negative (feed-in case)
for feeder in residual_load_feeders.columns:
if feeder != "station_node":
# get time steps where residual load is negative
residual_load_feeder = residual_load_feeders[feeder]
ts_neg = residual_load_feeder[residual_load_feeder < 0].dropna()
# ToDo check if it works correctly if dataframe or ts_net is empty
for comp_type in comp_types:
# get all components of that type in feeder
groupby_bus = pd.merge(
getattr(edisgo_obj.topology, f"{comp_type}_df"),
edisgo_obj.topology.buses_df,
how="left",
left_on="bus",
right_index=True,
).groupby(feeder)
comps_feeder = groupby_bus.groups[feeder]
for ts_type in ["active_power", "reactive_power"]:
# set time series data to zero
getattr(edisgo_obj.topology, f"{comp_type}_{ts_type}").loc[
ts_neg, comps_feeder
] = 0.0

# reinforce with n-1 values
# n_minus_one = kwargs.pop("")
edisgo_obj.reinforce(**kwargs)

# reset time series


def catch_convergence_reinforce_grid(
edisgo: EDisGo,
**kwargs,
Expand Down
104 changes: 88 additions & 16 deletions edisgo/network/timeseries.py
Original file line number Diff line number Diff line change
Expand Up @@ -1795,10 +1795,9 @@ def _get_q_sign_and_power_factor_per_component(
"storage_units_reactive_power", reactive_power
)

@property
def residual_load(self):
def residual_load(self, feeder=None, edisgo_obj=None):
"""
Returns residual load in network.
Returns residual load in network or per feeder.

Residual load for each time step is calculated from total load
minus total generation minus storage active power (discharge is
Expand All @@ -1807,17 +1806,83 @@ def residual_load(self):
residual load here represents a feed-in case.
Grid losses are not considered.

Parameters
-----------
feeder : str
Specifies whether to determine the residual load for the entire grid
or per feeder. Feeder can be either the MV or grid feeder.
If set to None, which is the default, the residual load for the entire grid
is returned.
If set to "mv_feeder", the MV feeders the buses and lines are in are
determined. If mode is "grid_feeder", LV buses and lines are assigned the
LV feeder they are in and MV buses and lines are assigned the MV feeder
they are in. The residual load is in both cases returned per feeder.
Default: None.

Returns
-------
:pandas:`pandas.Series<Series>`
Series with residual load in MW.

"""
return (
self.loads_active_power.sum(axis=1)
- self.generators_active_power.sum(axis=1)
- self.storage_units_active_power.sum(axis=1)
)
:pandas:`pandas.Series<Series>` or :pandas:`pandas.DataFrame<DataFrame>`
Returns residual load per time step in MW. Index is a time index.
If `feeder` is None, a series with the residual load in the entire grid
is returned. If `feeder` is "mv_feeder" or "grid_feeder"
a dataframe is returned where the column names correspond to the feeder
name. As the station is not in any feeder, it is assigned the name
"station_node" and only components directly connected to the station are
considered in the calculation of its residual load. In case `feeder` is set
to "mv_grid" the "station_node" just includes the HV-MV station, but in case
`feeder` is set to "grid_feeder" all MV-LV stations as well as the HV-MV
station are included in the "station_node" (this could be changed at some
point).

"""
if feeder is None:
return (
self.loads_active_power.sum(axis=1)
- self.generators_active_power.sum(axis=1)
- self.storage_units_active_power.sum(axis=1)
)
else:
# check if feeder was already assigned and if not, assign it
if feeder not in edisgo_obj.topology.buses_df.columns:
edisgo_obj.topology.assign_feeders(mode=feeder)
# iterate over components and add/subtract to/from residual load
residual_load = pd.DataFrame(
data=0.0,
columns=edisgo_obj.topology.buses_df.loc[:, feeder].unique(),
index=self.timeindex,
)
sign_dict = {
"loads": 1.0,
"generators": -1.0,
"storage_units": -1.0,
}
for comp_type in sign_dict.keys():
# groupby feeder
groupby_bus = pd.merge(
getattr(edisgo_obj.topology, f"{comp_type}_df"),
edisgo_obj.topology.buses_df,
how="left",
left_on="bus",
right_index=True,
).groupby(feeder)
residual_load = residual_load.add(
sign_dict[comp_type]
* pd.concat(
[
pd.DataFrame(
{
k: getattr(self, f"{comp_type}_active_power")
.loc[:, v]
.sum(axis=1)
}
)
for k, v in groupby_bus.groups.items()
],
axis=1,
),
fill_value=0.0,
)
return residual_load

@property
def timesteps_load_feedin_case(self):
Expand Down Expand Up @@ -1845,7 +1910,7 @@ def timesteps_load_feedin_case(self):

"""

return self.residual_load.apply(
return self.residual_load().apply(
lambda _: "feed-in_case" if _ < 0.0 else "load_case"
)

Expand Down Expand Up @@ -2215,7 +2280,10 @@ def resample(self, method: str = "ffill", freq: str | pd.Timedelta = "15min"):
self._timeindex = index

def scale_timeseries(
self, p_scaling_factor: float = 1.0, q_scaling_factor: float = 1.0
self,
p_scaling_factor: float = 1.0,
q_scaling_factor: float = 1.0,
components: list | None = None,
):
"""
Scales component time series by given factors.
Expand All @@ -2232,15 +2300,19 @@ def scale_timeseries(
Scaling factor to use for reactive power time series. Values between 0 and 1
will scale down the time series and values above 1 will scale the
timeseries up. Default: 1.
components : list(str)
Components to scale. Possible options are "generators", "loads", and
"storage_units". Per default (if components is None), all are scaled.

"""
attributes_type = ["generators", "loads", "storage_units"]
if components is None:
components = ["generators", "loads", "storage_units"]
power_types = {
"active_power": p_scaling_factor,
"reactive_power": q_scaling_factor,
}
for suffix, scaling_factor in power_types.items():
for type in attributes_type:
for type in components:
attribute = f"{type}_{suffix}"
setattr(self, attribute, getattr(self, attribute) * scaling_factor)

Expand Down
Loading
Loading