# Source code for edisgo.flex_opt.check_tech_constraints

```
import itertools
import logging
from math import sqrt
import numpy as np
import pandas as pd
from edisgo.network.grids import LVGrid, MVGrid
logger = logging.getLogger(__name__)
[docs]def mv_line_load(edisgo_obj):
"""
Checks for over-loading issues in MV network.
Parameters
----------
edisgo_obj : :class:`~.EDisGo`
Returns
-------
:pandas:`pandas.DataFrame<DataFrame>`
Dataframe containing over-loaded MV lines, their maximum relative
over-loading (maximum calculated current over allowed current) and the
corresponding time step.
Index of the dataframe are the names of the over-loaded lines.
Columns are 'max_rel_overload' containing the maximum relative
over-loading as float, 'time_index' containing the corresponding
time step the over-loading occured in as
:pandas:`pandas.Timestamp<Timestamp>`, and 'voltage_level' specifying
the voltage level the line is in (either 'mv' or 'lv').
Notes
-----
Line 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'.
"""
crit_lines = _line_load(edisgo_obj, voltage_level="mv")
if not crit_lines.empty:
logger.debug(
"==> {} line(s) in MV network has/have load issues.".format(
crit_lines.shape[0]
)
)
else:
logger.debug("==> No line load issues in MV network.")
return crit_lines
[docs]def lv_line_load(edisgo_obj):
"""
Checks for over-loading issues in LV network.
Parameters
----------
edisgo_obj : :class:`~.EDisGo`
Returns
-------
:pandas:`pandas.DataFrame<DataFrame>`
Dataframe containing over-loaded LV lines, their maximum relative
over-loading (maximum calculated current over allowed current) and the
corresponding time step.
Index of the dataframe are the names of the over-loaded lines.
Columns are 'max_rel_overload' containing the maximum relative
over-loading as float, 'time_index' containing the corresponding
time step the over-loading occured in as
:pandas:`pandas.Timestamp<Timestamp>`, and 'voltage_level' specifying
the voltage level the line is in (either 'mv' or 'lv').
Notes
-----
Line 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'.
"""
crit_lines = _line_load(edisgo_obj, voltage_level="lv")
if not crit_lines.empty:
logger.debug(
"==> {} line(s) in LV networks has/have load issues.".format(
crit_lines.shape[0]
)
)
else:
logger.debug("==> No line load issues in LV networks.")
return crit_lines
[docs]def lines_allowed_load(edisgo_obj, voltage_level):
"""
Get allowed maximum current per line per time step
Parameters
----------
edisgo_obj : :class:`~.EDisGo`
voltage_level : str
Grid level, allowed line load is returned for. Possible options are
"mv" or "lv".
Returns
-------
:pandas:`pandas.DataFrame<DataFrame>`
Dataframe containing the maximum allowed current per line and time step
in kA. Index of the dataframe are all time steps power flow analysis
was conducted for of type :pandas:`pandas.Timestamp<Timestamp>`.
Columns are line names of all lines in the specified voltage level.
"""
# get lines and nominal voltage
mv_grid = edisgo_obj.topology.mv_grid
if voltage_level == "lv":
lines_df = edisgo_obj.topology.lines_df[
~edisgo_obj.topology.lines_df.index.isin(mv_grid.lines_df.index)
]
lv_grids = list(edisgo_obj.topology.lv_grids)
if len(lv_grids) > 0:
nominal_voltage = lv_grids[0].nominal_voltage
else:
nominal_voltage = np.NaN
elif voltage_level == "mv":
lines_df = mv_grid.lines_df
nominal_voltage = mv_grid.nominal_voltage
else:
raise ValueError(
"{} is not a valid option for input variable 'voltage_level' in "
"function lines_allowed_load. Try 'mv' or "
"'lv'.".format(voltage_level)
)
i_lines_allowed_per_case = {}
i_lines_allowed_per_case["feed-in_case"] = (
lines_df.s_nom
/ sqrt(3)
/ nominal_voltage
* edisgo_obj.config["grid_expansion_load_factors"][
"{}_feed-in_case_line".format(voltage_level)
]
)
# adapt i_lines_allowed for radial feeders
buses_in_cycles = list(
set(itertools.chain.from_iterable(edisgo_obj.topology.rings))
)
# Find lines in cycles
lines_in_cycles = list(
lines_df.loc[
lines_df[["bus0", "bus1"]].isin(buses_in_cycles).all(axis=1)
].index.values
)
lines_radial_feeders = list(
lines_df.loc[~lines_df.index.isin(lines_in_cycles)].index.values
)
# lines in cycles have to be n-1 secure
i_lines_allowed_per_case["load_case"] = (
lines_df.loc[lines_in_cycles].s_nom
/ sqrt(3)
/ nominal_voltage
* edisgo_obj.config["grid_expansion_load_factors"][
"{}_load_case_line".format(voltage_level)
]
)
# lines in radial feeders are not n-1 secure anyways
i_lines_allowed_per_case["load_case"] = pd.concat(
[
i_lines_allowed_per_case["load_case"],
lines_df.loc[lines_radial_feeders].s_nom / sqrt(3) / nominal_voltage,
]
)
i_lines_allowed = edisgo_obj.timeseries.timesteps_load_feedin_case.loc[
edisgo_obj.results.i_res.index
].apply(lambda _: i_lines_allowed_per_case[_])
return i_lines_allowed
[docs]def lines_relative_load(edisgo_obj, lines_allowed_load):
"""
Calculates relative line load based on specified allowed line load.
Parameters
----------
edisgo_obj : :class:`~.EDisGo`
lines_allowed_load : :pandas:`pandas.DataFrame<DataFrame>`
Dataframe containing the maximum allowed current per line and time step
in kA. Index of the dataframe are time steps of type
:pandas:`pandas.Timestamp<Timestamp>` and columns are line names.
Returns
--------
:pandas:`pandas.DataFrame<DataFrame>`
Dataframe containing the relative line load per line and time step.
Index and columns of the dataframe are the same as those of parameter
`lines_allowed_load`.
"""
# get line load from power flow analysis
i_lines_pfa = edisgo_obj.results.i_res.loc[
lines_allowed_load.index, lines_allowed_load.columns
]
return i_lines_pfa / lines_allowed_load
def _line_load(edisgo_obj, voltage_level):
"""
Checks for over-loading issues of lines.
Parameters
----------
edisgo_obj : :class:`~.EDisGo`
voltage_level : str
Voltage level, over-loading is checked for. Possible options are
"mv" or "lv".
Returns
-------
:pandas:`pandas.DataFrame<DataFrame>`
Dataframe containing over-loaded lines, their maximum relative
over-loading (maximum calculated current over allowed current) and the
corresponding time step.
Index of the dataframe are the names of the over-loaded lines.
Columns are 'max_rel_overload' containing the maximum relative
over-loading as float, 'time_index' containing the corresponding
time step the over-loading occured in as
:pandas:`pandas.Timestamp<Timestamp>`, and 'voltage_level' specifying
the voltage level the line is in (either 'mv' or 'lv').
"""
if edisgo_obj.results.i_res.empty:
raise Exception(
"No power flow results to check over-load for. Please perform "
"power flow analysis first."
)
# get allowed line load
i_lines_allowed = lines_allowed_load(edisgo_obj, voltage_level)
# calculate relative line load and keep maximum over-load of each line
relative_i_res = lines_relative_load(edisgo_obj, i_lines_allowed)
crit_lines_relative_load = relative_i_res[relative_i_res > 1].max().dropna()
if len(crit_lines_relative_load) > 0:
crit_lines = pd.concat(
[
crit_lines_relative_load,
relative_i_res.idxmax()[crit_lines_relative_load.index],
],
axis=1,
keys=["max_rel_overload", "time_index"],
sort=True,
)
crit_lines.loc[:, "voltage_level"] = voltage_level
else:
crit_lines = pd.DataFrame(dtype=float)
return crit_lines
[docs]def hv_mv_station_load(edisgo_obj):
"""
Checks for over-loading of HV/MV station.
Parameters
----------
edisgo_obj : :class:`~.EDisGo`
Returns
-------
:pandas:`pandas.DataFrame<DataFrame>`
Dataframe containing over-loaded HV/MV station, their apparent power
at maximal over-loading and the corresponding time step.
Index of the dataframe is the representative of the MVGrid.
Columns are 's_missing' containing the missing
apparent power at maximal over-loading in MVA as float and 'time_index'
containing the corresponding time step the over-loading occured in as
:pandas:`pandas.Timestamp<Timestamp>`.
Notes
-----
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'.
"""
crit_stations = _station_load(edisgo_obj, edisgo_obj.topology.mv_grid)
if not crit_stations.empty:
logger.debug("==> HV/MV station has load issues.")
else:
logger.debug("==> No HV/MV station load issues.")
return crit_stations
[docs]def mv_lv_station_load(edisgo_obj):
"""
Checks for over-loading of MV/LV stations.
Parameters
----------
edisgo_obj : :class:`~.EDisGo`
Returns
-------
:pandas:`pandas.DataFrame<DataFrame>`
Dataframe containing over-loaded MV/LV stations, their missing apparent
power at maximal over-loading and the corresponding time step.
Index of the dataframe are the representatives of the grids with
over-loaded stations. Columns are 's_missing' containing the missing
apparent power at maximal over-loading in MVA as float and 'time_index'
containing the corresponding time step the over-loading occured in as
:pandas:`pandas.Timestamp<Timestamp>`.
Notes
-----
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'.
"""
crit_stations = pd.DataFrame(dtype=float)
for lv_grid in edisgo_obj.topology.lv_grids:
crit_stations = pd.concat(
[
crit_stations,
_station_load(edisgo_obj, lv_grid),
]
)
if not crit_stations.empty:
logger.debug(
"==> {} MV/LV station(s) has/have load issues.".format(
crit_stations.shape[0]
)
)
else:
logger.debug("==> No MV/LV station load issues.")
return crit_stations
def _station_load(edisgo_obj, grid):
"""
Checks for over-loading of stations.
Parameters
----------
edisgo_obj : :class:`~.EDisGo`
grid : :class:`~.network.grids.LVGrid` or :class:`~.network.grids.MVGrid`
Returns
-------
:pandas:`pandas.DataFrame<DataFrame>`
Dataframe containing over-loaded stations, their missing apparent
power at maximal over-loading and the corresponding time step.
Index of the dataframe are the representatives of the grids with
over-loaded stations. Columns are 's_missing' containing the missing
apparent power at maximal over-loading in MVA as float and 'time_index'
containing the corresponding time step the over-loading occured in as
:pandas:`pandas.Timestamp<Timestamp>`.
"""
# get apparent power over station from power flow analysis
if isinstance(grid, LVGrid):
voltage_level = "lv"
transformers_df = grid.transformers_df
s_station_pfa = edisgo_obj.results.s_res.loc[:, transformers_df.index].sum(
axis=1
)
elif isinstance(grid, MVGrid):
voltage_level = "mv"
transformers_df = edisgo_obj.topology.transformers_hvmv_df
# ensure that power flow was conducted for MV
mv_lines = edisgo_obj.topology.mv_grid.lines_df.index
if not any(mv_lines.isin(edisgo_obj.results.i_res.columns)):
raise ValueError(
"MV was not included in power flow analysis, wherefore load "
"of HV/MV station cannot be calculated."
)
s_station_pfa = np.hypot(
edisgo_obj.results.pfa_slack.p,
edisgo_obj.results.pfa_slack.q,
)
else:
raise ValueError("Inserted grid is invalid.")
# get maximum allowed apparent power of station in each time step
s_station = sum(transformers_df.s_nom)
load_factor = edisgo_obj.timeseries.timesteps_load_feedin_case.apply(
lambda _: edisgo_obj.config["grid_expansion_load_factors"][
f"{voltage_level}_{_}_transformer"
]
)
s_station_allowed = s_station * load_factor
# calculate residual apparent power (if negative, station is over-loaded)
s_res = s_station_allowed - s_station_pfa
s_res = s_res[s_res < 0]
if not s_res.empty:
# calculate greatest apparent power missing (residual apparent power is
# devided by the load factor to account for load factors smaller than
# one, which lead to a higher needed additional capacity)
s_missing = (s_res / load_factor).dropna()
return pd.DataFrame(
{
"s_missing": abs(s_missing.min()),
"time_index": s_missing.idxmin(),
},
index=[repr(grid)],
)
else:
return pd.DataFrame(dtype=float)
[docs]def mv_voltage_deviation(edisgo_obj, voltage_levels="mv_lv"):
"""
Checks for voltage stability issues in MV network.
Returns buses with voltage issues and their maximum voltage deviation.
Parameters
----------
edisgo_obj : :class:`~.EDisGo`
voltage_levels : :obj:`str`
Specifies which allowed voltage deviations to use. Possible options
are:
* 'mv_lv'
This is the default. The allowed voltage deviations for buses in the
MV is the same as for buses in the LV. Further, load and feed-in case
are not distinguished.
* 'mv'
Use this to handle allowed voltage limits in the MV and LV
topology differently. In that case, load and feed-in case are
differentiated.
Returns
-------
:obj:`dict`
Dictionary with representative of :class:`~.network.grids.MVGrid` as
key and a :pandas:`pandas.DataFrame<DataFrame>` with voltage
deviations from allowed lower or upper voltage limits, sorted
descending from highest to lowest voltage deviation, as value.
Index of the dataframe are all buses with voltage issues.
Columns are 'v_diff_max' containing the maximum voltage deviation as
float and 'time_index' containing the corresponding time step the
voltage issue occured in as :pandas:`pandas.Timestamp<Timestamp>`.
Notes
-----
Voltage issues are determined based on allowed voltage deviations defined
in the config file 'config_grid_expansion' in section
'grid_expansion_allowed_voltage_deviations'.
"""
crit_buses = {}
# get allowed lower and upper voltage limits
v_limits_upper, v_limits_lower = _mv_allowed_voltage_limits(
edisgo_obj, voltage_levels
)
# find buses with voltage issues and their maximum voltage deviation
crit_buses_grid = _voltage_deviation(
edisgo_obj,
edisgo_obj.topology.mv_grid.buses_df.index,
v_limits_upper,
v_limits_lower,
)
if not crit_buses_grid.empty:
crit_buses[repr(edisgo_obj.topology.mv_grid)] = crit_buses_grid
logger.debug(
"==> {} bus(es) in MV topology has/have voltage issues.".format(
crit_buses_grid.shape[0]
)
)
else:
logger.debug("==> No voltage issues in MV topology.")
return crit_buses
[docs]def lv_voltage_deviation(edisgo_obj, mode=None, voltage_levels="mv_lv"):
"""
Checks for voltage stability issues in LV networks.
Returns buses with voltage issues and their maximum voltage deviation.
Parameters
----------
edisgo_obj : :class:`~.EDisGo`
mode : None or str
If None voltage at all buses in LV networks is checked. If mode is set
to 'stations' only voltage at bus bar is checked. Default: None.
voltage_levels : str
Specifies which allowed voltage deviations to use. Possible options
are:
* 'mv_lv'
This is the default. The allowed voltage deviations for buses in the
LV is the same as for buses in the MV. Further, load and feed-in case
are not distinguished.
* 'lv'
Use this to handle allowed voltage limits in the MV and LV
topology differently. In that case, load and feed-in case are
differentiated.
Returns
-------
dict
Dictionary with representative of :class:`~.network.grids.LVGrid` as
key and a :pandas:`pandas.DataFrame<DataFrame>` with voltage
deviations from allowed lower or upper voltage limits, sorted
descending from highest to lowest voltage deviation, as value.
Index of the dataframe are all buses with voltage issues.
Columns are 'v_diff_max' containing the maximum voltage deviation as
float and 'time_index' containing the corresponding time step the
voltage issue occured in as :pandas:`pandas.Timestamp<Timestamp>`.
Notes
-----
Voltage issues are determined based on allowed voltage deviations defined
in the config file 'config_grid_expansion' in section
'grid_expansion_allowed_voltage_deviations'.
"""
crit_buses = {}
if voltage_levels == "mv_lv":
v_limits_upper, v_limits_lower = _mv_allowed_voltage_limits(edisgo_obj, "mv_lv")
elif not "lv" == voltage_levels:
raise ValueError(
"{} is not a valid option for input variable 'voltage_levels' in "
"function lv_voltage_deviation. Try 'mv_lv' or "
"'lv'.".format(voltage_levels)
)
for lv_grid in edisgo_obj.topology.lv_grids:
if mode:
if mode == "stations":
buses = lv_grid.station.index
else:
raise ValueError(
"{} is not a valid option for input variable 'mode' in "
"function lv_voltage_deviation. Try 'stations' or "
"None.".format(mode)
)
else:
buses = lv_grid.buses_df.index
if voltage_levels == "lv":
v_limits_upper, v_limits_lower = _lv_allowed_voltage_limits(
edisgo_obj, lv_grid, mode
)
crit_buses_grid = _voltage_deviation(
edisgo_obj, buses, v_limits_upper, v_limits_lower
)
if not crit_buses_grid.empty:
crit_buses[str(lv_grid)] = crit_buses_grid
if crit_buses:
if mode == "stations":
logger.debug(
"==> {} LV station(s) has/have voltage issues.".format(len(crit_buses))
)
else:
logger.debug(
"==> {} LV topology(s) has/have voltage issues.".format(len(crit_buses))
)
else:
if mode == "stations":
logger.debug("==> No voltage issues in LV stations.")
else:
logger.debug("==> No voltage issues in LV grids.")
return crit_buses
def _mv_allowed_voltage_limits(edisgo_obj, voltage_levels):
"""
Calculates allowed upper and lower MV voltage limits in p.u..
Parameters
----------
edisgo_obj : :class:`~.EDisGo`
voltage_levels : :obj:`str`
Specifies which allowed voltage limits to use. Possible options
are:
* 'mv_lv'
The allowed voltage deviations for buses in the MV are the same as
for buses in the LV, namely $pm$ 10 %.
* 'mv'
Use this to handle allowed voltage limits in the MV and LV
differently. In that case load and feed-in case are differentiated.
Returns
-------
:pandas:`pandas.Series<Series>`
Series containing the allowed upper voltage limits in p.u..
Index of the series are all time steps power flow was last conducted
for of type :pandas:`pandas.Timestamp<Timestamp>`.
:pandas:`pandas.Series<Series>`
Series containing the allowed lower voltage limits in p.u..
Index of the series are all time steps power flow was last conducted
for of type :pandas:`pandas.Timestamp<Timestamp>`.
"""
v_allowed_per_case = {}
# get config values for lower voltage limit in feed-in case and upper
# voltage limit in load case
v_allowed_per_case["feed-in_case_lower"] = edisgo_obj.config[
"grid_expansion_allowed_voltage_deviations"
]["feed-in_case_lower"]
v_allowed_per_case["load_case_upper"] = edisgo_obj.config[
"grid_expansion_allowed_voltage_deviations"
]["load_case_upper"]
# calculate upper voltage limit in feed-in case and lower voltage limit in
# load case
offset = edisgo_obj.config["grid_expansion_allowed_voltage_deviations"][
"hv_mv_trafo_offset"
]
control_deviation = edisgo_obj.config["grid_expansion_allowed_voltage_deviations"][
"hv_mv_trafo_control_deviation"
]
if voltage_levels == "mv_lv" or voltage_levels == "mv":
v_allowed_per_case["feed-in_case_upper"] = (
1
+ offset
+ control_deviation
+ edisgo_obj.config["grid_expansion_allowed_voltage_deviations"][
"{}_feed-in_case_max_v_deviation".format(voltage_levels)
]
)
v_allowed_per_case["load_case_lower"] = (
1
+ offset
- control_deviation
- edisgo_obj.config["grid_expansion_allowed_voltage_deviations"][
"{}_load_case_max_v_deviation".format(voltage_levels)
]
)
else:
raise ValueError(
"Specified mode {} is not a valid option.".format(voltage_levels)
)
# create series with upper and lower voltage limits for each time step
v_limits_upper = edisgo_obj.timeseries.timesteps_load_feedin_case.apply(
lambda _: v_allowed_per_case["{}_upper".format(_)]
)
v_limits_lower = edisgo_obj.timeseries.timesteps_load_feedin_case.apply(
lambda _: v_allowed_per_case["{}_lower".format(_)]
)
return v_limits_upper, v_limits_lower
def _lv_allowed_voltage_limits(edisgo_obj, lv_grid, mode):
"""
Calculates allowed upper and lower voltage limits for given LV grid.
Parameters
----------
edisgo_obj : :class:`~.EDisGo`
lv_grid : :class:`~.network.grids.LVGrid`
LV grid to get voltage limits for.
mode : None or :obj:`str`
If None, voltage limits for buses in the LV network are returned. In
that case the reference bus is the LV stations' secondary side.
If mode is set to 'stations', voltage limits for stations' secondary
side (LV bus bar) are returned; the reference bus is the stations'
primary side.
Returns
-------
:pandas:`pandas.Series<Series>`
Series containing the allowed upper voltage limits in p.u..
Index of the series are all time steps power flow was last conducted
for of type :pandas:`pandas.Timestamp<Timestamp>`.
:pandas:`pandas.Series<Series>`
Series containing the allowed lower voltage limits in p.u..
Index of the series are all time steps power flow was last conducted
for of type :pandas:`pandas.Timestamp<Timestamp>`.
"""
v_allowed_per_case = {}
# get reference voltages for different modes
if mode == "stations":
# reference voltage is voltage at stations' primary side
bus_station_primary = lv_grid.transformers_df.iloc[0].bus0
voltage_base = edisgo_obj.results.v_res.loc[:, bus_station_primary]
config_string = "mv_lv_station"
else:
# reference voltage is voltage at stations' secondary side
voltage_base = edisgo_obj.results.v_res.loc[:, lv_grid.station.index.values[0]]
config_string = "lv"
# calculate upper voltage limit in feed-in case and lower voltage limit in
# load case
v_allowed_per_case["feed-in_case_upper"] = (
voltage_base
+ edisgo_obj.config["grid_expansion_allowed_voltage_deviations"][
"{}_feed-in_case_max_v_deviation".format(config_string)
]
)
v_allowed_per_case["load_case_lower"] = (
voltage_base
- edisgo_obj.config["grid_expansion_allowed_voltage_deviations"][
"{}_load_case_max_v_deviation".format(config_string)
]
)
timeindex = voltage_base.index
v_allowed_per_case["feed-in_case_lower"] = pd.Series(
edisgo_obj.config["grid_expansion_allowed_voltage_deviations"][
"feed-in_case_lower"
],
index=timeindex,
)
v_allowed_per_case["load_case_upper"] = pd.Series(
edisgo_obj.config["grid_expansion_allowed_voltage_deviations"][
"load_case_upper"
],
index=timeindex,
)
# create series with upper and lower voltage limits for each time step
v_limits_upper = []
v_limits_lower = []
load_feedin_case = edisgo_obj.timeseries.timesteps_load_feedin_case
for t in timeindex:
case = load_feedin_case.loc[t]
v_limits_upper.append(v_allowed_per_case["{}_upper".format(case)].loc[t])
v_limits_lower.append(v_allowed_per_case["{}_lower".format(case)].loc[t])
v_limits_upper = pd.Series(v_limits_upper, index=timeindex)
v_limits_lower = pd.Series(v_limits_lower, index=timeindex)
return v_limits_upper, v_limits_lower
[docs]def voltage_diff(edisgo_obj, buses, v_dev_allowed_upper, v_dev_allowed_lower):
"""
Function to detect under- and overvoltage at buses.
The function returns both under- and overvoltage deviations in p.u. from
the allowed lower and upper voltage limit, respectively, in separate
dataframes. In case of both under- and overvoltage issues at one bus,
only the highest voltage deviation is returned.
Parameters
----------
edisgo_obj : :class:`~.EDisGo`
buses : list(str)
List of buses to check voltage deviation for.
v_dev_allowed_upper : :pandas:`pandas.Series<Series>`
Series with time steps (of type :pandas:`pandas.Timestamp<Timestamp>`)
power flow analysis was conducted for and the allowed upper limit of
voltage deviation for each time step as float in p.u..
v_dev_allowed_lower : :pandas:`pandas.Series<Series>`
Series with time steps (of type :pandas:`pandas.Timestamp<Timestamp>`)
power flow analysis was conducted for and the allowed lower limit of
voltage deviation for each time step as float in p.u..
Returns
-------
:pandas:`pandas.DataFrame<DataFrame>`
Dataframe with deviations from allowed lower voltage level.
Columns of the dataframe are all time steps power flow analysis was
conducted for of type :pandas:`pandas.Timestamp<Timestamp>`; in the
index are all buses for which undervoltage was detected. In case of
a higher over- than undervoltage deviation for a bus, the bus does
not appear in this dataframe, but in the dataframe with overvoltage
deviations.
:pandas:`pandas.DataFrame<DataFrame>`
Dataframe with deviations from allowed upper voltage level.
Columns of the dataframe are all time steps power flow analysis was
conducted for of type :pandas:`pandas.Timestamp<Timestamp>`; in the
index are all buses for which overvoltage was detected. In case of
a higher under- than overvoltage deviation for a bus, the bus does
not appear in this dataframe, but in the dataframe with undervoltage
deviations.
"""
v_mag_pu_pfa = edisgo_obj.results.v_res.loc[:, buses]
v_dev_allowed_upper_format = np.tile(
(v_dev_allowed_upper.loc[v_mag_pu_pfa.index]).values,
(v_mag_pu_pfa.shape[1], 1),
)
v_dev_allowed_lower_format = np.tile(
(v_dev_allowed_lower.loc[v_mag_pu_pfa.index]).values,
(v_mag_pu_pfa.shape[1], 1),
)
overvoltage = v_mag_pu_pfa.T[v_mag_pu_pfa.T > v_dev_allowed_upper_format].dropna(
how="all"
)
undervoltage = v_mag_pu_pfa.T[v_mag_pu_pfa.T < v_dev_allowed_lower_format].dropna(
how="all"
)
# sort buses with under- and overvoltage issues in a way that
# worst case is saved
buses_both = v_mag_pu_pfa[
overvoltage[overvoltage.index.isin(undervoltage.index)].index
]
voltage_diff_ov = buses_both.T - v_dev_allowed_upper.loc[v_mag_pu_pfa.index].values
voltage_diff_uv = -buses_both.T + v_dev_allowed_lower.loc[v_mag_pu_pfa.index].values
voltage_diff_ov = voltage_diff_ov.loc[
voltage_diff_ov.max(axis=1) > voltage_diff_uv.max(axis=1)
]
voltage_diff_uv = voltage_diff_uv.loc[
~voltage_diff_uv.index.isin(voltage_diff_ov.index)
]
# handle buses with overvoltage issues and append to voltage_diff_ov
buses_ov = v_mag_pu_pfa[
overvoltage[~overvoltage.index.isin(buses_both.columns)].index
]
voltage_diff_ov = pd.concat(
[
voltage_diff_ov,
buses_ov.T - v_dev_allowed_upper.loc[v_mag_pu_pfa.index].values,
]
)
# handle buses with undervoltage issues and append to voltage_diff_uv
buses_uv = v_mag_pu_pfa[
undervoltage[~undervoltage.index.isin(buses_both.columns)].index
]
voltage_diff_uv = pd.concat(
[
voltage_diff_uv,
-buses_uv.T + v_dev_allowed_lower.loc[v_mag_pu_pfa.index].values,
]
)
return voltage_diff_uv, voltage_diff_ov
def _voltage_deviation(edisgo_obj, buses, v_limits_upper, v_limits_lower):
"""
Function to detect voltage issues at buses.
The function returns the highest voltage deviation from allowed lower
or upper voltage limit in p.u. for all buses with voltage issues.
Parameters
----------
edisgo_obj : :class:`~.EDisGo`
buses : list(str)
List of buses to check voltage deviation for.
v_limits_upper : :pandas:`pandas.Series<Series>`
Series with time steps (of type :pandas:`pandas.Timestamp<Timestamp>`)
power flow analysis was conducted for and the allowed upper limit of
voltage deviation for each time step as float in p.u..
v_limits_lower : :pandas:`pandas.Series<Series>`
Series with time steps (of type :pandas:`pandas.Timestamp<Timestamp>`)
power flow analysis was conducted for and the allowed lower limit of
voltage deviation for each time step as float in p.u..
Returns
-------
pandas:`pandas.DataFrame<DataFrame>`
Dataframe with deviations from allowed lower or upper voltage limits
sorted descending from highest to lowest voltage deviation
(it is not distinguished between over- or undervoltage).
Columns of the dataframe are 'v_diff_max' containing the maximum
absolute voltage deviation as float and 'time_index' containing the
corresponding time step the voltage issue occured in as
:pandas:`pandas.Timestamp<Timestamp>`. Index of the dataframe are the
names of all buses with voltage issues.
"""
def _append_crit_buses(df):
return pd.DataFrame(
{
"v_diff_max": df.max(axis=1).values,
"time_index": df.idxmax(axis=1).values,
},
index=df.index,
)
crit_buses_grid = pd.DataFrame(dtype=float)
voltage_diff_uv, voltage_diff_ov = voltage_diff(
edisgo_obj, buses, v_limits_upper, v_limits_lower
)
# append to crit buses dataframe
if not voltage_diff_ov.empty:
crit_buses_grid = pd.concat(
[
crit_buses_grid,
_append_crit_buses(voltage_diff_ov),
]
)
if not voltage_diff_uv.empty:
crit_buses_grid = pd.concat(
[
crit_buses_grid,
_append_crit_buses(voltage_diff_uv),
]
)
if not crit_buses_grid.empty:
crit_buses_grid.sort_values(by=["v_diff_max"], ascending=False, inplace=True)
return crit_buses_grid
[docs]def check_ten_percent_voltage_deviation(edisgo_obj):
"""
Checks if 10% criteria is exceeded.
Through the 10% criteria it is ensured that voltage is kept between 0.9
and 1.1 p.u.. In case of higher or lower voltages a ValueError is raised.
Parameters
----------
edisgo_obj : :class:`~.EDisGo`
"""
v_mag_pu_pfa = edisgo_obj.results.v_res
if (v_mag_pu_pfa > 1.1).any().any() or (v_mag_pu_pfa < 0.9).any().any():
message = "Maximum allowed voltage deviation of 10% exceeded."
raise ValueError(message)
```