Source code for edisgo.flex_opt.check_tech_constraints

import logging

import numpy as np
import pandas as pd

from edisgo.network.grids import LVGrid, MVGrid

logger = logging.getLogger(__name__)


[docs] def mv_line_max_relative_overload(edisgo_obj, n_minus_one=False): """ Returns time step and value of most severe overloading of lines in MV network. Parameters ---------- edisgo_obj : :class:`~.EDisGo` n_minus_one : bool Determines which allowed load factors to use (see :py:attr:`~lines_allowed_load` for more information). 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. Returns ------- :pandas:`pandas.DataFrame<DataFrame>` Dataframe containing over-loaded MV lines, their maximum relative over-loading in p.u. (maximum calculated apparent power over allowed apparent power) 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 occurred 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_max_relative_overload( edisgo_obj, voltage_level="mv", n_minus_one=n_minus_one ) 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_max_relative_overload(edisgo_obj, n_minus_one=False, lv_grid_id=None): """ Returns time step and value of most severe overloading of lines in LV networks. Parameters ---------- edisgo_obj : :class:`~.EDisGo` n_minus_one : bool Determines which allowed load factors to use (see :py:attr:`~lines_allowed_load` for more information). 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. 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. Returns ------- :pandas:`pandas.DataFrame<DataFrame>` Dataframe containing over-loaded LV lines, their maximum relative over-loading in p.u. (maximum calculated apparent power over allowed apparent power) 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 occurred 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_max_relative_overload( edisgo_obj, voltage_level="lv", n_minus_one=n_minus_one, lv_grid_id=lv_grid_id ) 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
def _line_max_relative_overload( edisgo_obj, voltage_level, n_minus_one=False, lv_grid_id=None ): """ Returns time step and value of most severe overloading of lines. Parameters ---------- edisgo_obj : :class:`~.EDisGo` voltage_level : str Voltage level, over-loading is checked for. Possible options are 'mv' or 'lv'. n_minus_one : bool Determines which allowed load factors to use. See :py:attr:`~lines_allowed_load` for more information. lv_grid_id : str or int or None This parameter is only used in case `voltage_level` is "lv". If None, checks overloading for all LV lines. Otherwise, only lines in given LV grid are checked. Default: None. Returns ------- :pandas:`pandas.DataFrame<DataFrame>` Dataframe containing over-loaded lines, their maximum relative over-loading in p.u. (maximum calculated apparent power over allowed apparent power) 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 occurred 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 lines in voltage level mv_grid = edisgo_obj.topology.mv_grid if voltage_level == "lv": if lv_grid_id is None: lines = edisgo_obj.topology.lines_df[ ~edisgo_obj.topology.lines_df.index.isin(mv_grid.lines_df.index) ].index else: lv_grid = edisgo_obj.topology.get_lv_grid(lv_grid_id) lines = lv_grid.lines_df.index elif voltage_level == "mv": lines = mv_grid.lines_df.index else: raise ValueError( "{} is not a valid option for input variable 'voltage_level'. " "Try 'mv' or 'lv'.".format(voltage_level) ) # calculate relative line load and keep maximum over-load of each line relative_i_res = lines_relative_load(edisgo_obj, lines, n_minus_one=n_minus_one) 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 lines_allowed_load(edisgo_obj, lines=None, n_minus_one=False): """ Returns allowed loading of specified lines per time step in MVA. Allowed loading 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'. Parameters ---------- edisgo_obj : :class:`~.EDisGo` lines : list(str) List of line names to get allowed loading for. Per default allowed loading is returned for all lines 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. This case is currently not implemented. Returns ------- :pandas:`pandas.DataFrame<DataFrame>` Dataframe containing the maximum allowed apparent power per line and time step in MVA. Index of the dataframe are all time steps power flow analysis was conducted for of type :pandas:`pandas.Timestamp<Timestamp>`. Columns are line names as in index of :attr:`~.network.topology.Topology.loads_df`. """ allowed_load_lv = _lines_allowed_load_voltage_level( edisgo_obj, voltage_level="lv", n_minus_one=n_minus_one ) allowed_load_mv = _lines_allowed_load_voltage_level( edisgo_obj, voltage_level="mv", n_minus_one=n_minus_one ) allowed_load = pd.concat([allowed_load_lv, allowed_load_mv], axis=1) if lines is None: return allowed_load else: return allowed_load.loc[:, lines]
def _lines_allowed_load_voltage_level(edisgo_obj, voltage_level, n_minus_one=False): """ Returns allowed loading per line in the specified voltage level in MVA. Parameters ---------- edisgo_obj : :class:`~.EDisGo` voltage_level : str Grid level, allowed line load is returned for. Possible options are "mv" or "lv". 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. This case is currently not implemented. Returns ------- :pandas:`pandas.DataFrame<DataFrame>` Dataframe containing the maximum allowed apparent power per line and time step in MVA. Index of the dataframe are all time steps power flow analysis was conducted for of type :pandas:`pandas.Timestamp<Timestamp>`. Columns are line names as in index of :attr:`~.network.topology.Topology.loads_df` of all lines in the specified voltage level. """ # get lines in voltage level 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) ] elif voltage_level == "mv": lines_df = mv_grid.lines_df else: raise ValueError( "{} is not a valid option for input variable 'voltage_level' in " "function lines_allowed_load_voltage_level. Try 'mv' or " "'lv'.".format(voltage_level) ) allowed_load_per_case = {} # get allowed loads per case if n_minus_one is True: raise NotImplementedError("n-1 security can currently not be checked.") # # handle lines in cycles differently from lines in stubs # for case in ["feed-in_case", "load_case"]: # if ( # edisgo_obj.config["grid_expansion_load_factors_n_minus_one"][ # f"{voltage_level}_{case}_line" # ] # != 1.0 # ): # # 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 # allowed_load_per_case[case] = ( # lines_df.loc[lines_in_cycles].s_nom # * edisgo_obj.config["grid_expansion_load_factors_n_minus_one"][ # f"{voltage_level}_{case}_line" # ] # ) # # # lines in radial feeders are not n-1 secure anyway # allowed_load_per_case[case] = pd.concat( # [ # allowed_load_per_case[case], # lines_df.loc[lines_radial_feeders].s_nom, # ] # ) else: for case in ["feed-in_case", "load_case"]: allowed_load_per_case[case] = ( lines_df.s_nom * edisgo_obj.config["grid_expansion_load_factors"][ f"{voltage_level}_{case}_line" ] ) return edisgo_obj.timeseries.timesteps_load_feedin_case.loc[ edisgo_obj.results.s_res.index ].apply(lambda _: allowed_load_per_case[_])
[docs] def lines_relative_load(edisgo_obj, lines=None, n_minus_one=False): """ Returns relative line load. The relative line load is here defined as the apparent power over a line, obtained from power flow analysis, divided by the allowed load of a line, which is the nominal apparent power times a security factor (see :py:attr:`~lines_allowed_load` for more information). Parameters ---------- edisgo_obj : :class:`~.EDisGo` lines : list(str) or None List of line names to get relative loading for. Per default relative loading is returned for all lines included in the power flow analysis. Default: None. n_minus_one : bool Determines which allowed load factors to use. See :py:attr:`~lines_allowed_load` for more information. Returns -------- :pandas:`pandas.DataFrame<DataFrame>` Dataframe containing the relative loading per line and time step in p.u.. Index of the dataframe are all time steps power flow analysis was conducted for of type :pandas:`pandas.Timestamp<Timestamp>`. Columns are line names as in index of :attr:`~.network.topology.Topology.loads_df`. """ if lines is None: lines = edisgo_obj.results.s_res.columns.drop( edisgo_obj.topology.transformers_df.index, errors="ignore" ) # get allowed loading allowed_loading = lines_allowed_load(edisgo_obj, lines, n_minus_one=n_minus_one) # get line load from power flow analysis loading = edisgo_obj.results.s_res.loc[:, lines] return loading / allowed_loading
[docs] def hv_mv_station_max_overload(edisgo_obj): """ Checks for over-loading of HV/MV station. Parameters ---------- edisgo_obj : :class:`~.EDisGo` Returns ------- :pandas:`pandas.DataFrame<DataFrame>` In case there are no over-loading problems returns an empty dataframe. In case of over-loading problems the dataframe contains the name of the over-loaded station (grid's name with the extension '_station') in the index. Columns are 's_missing' containing the missing apparent power at maximal over-loading in MVA as float, 'time_index' containing the corresponding time step the over-loading occurred in as :pandas:`pandas.Timestamp<Timestamp>`, and 'grid' containing the grid object as :class:`~.network.grids.MVGrid`. 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_max_overload(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_max_overload(edisgo_obj, lv_grid_id=None): """ Checks for over-loading of MV/LV stations. Parameters ---------- edisgo_obj : :class:`~.EDisGo` 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. Returns ------- :pandas:`pandas.DataFrame<DataFrame>` In case there are no over-loading problems returns an empty dataframe. In case of over-loading problems the dataframe contains the name of the over-loaded station (grid's name with the extension '_station') in the index. Columns are 's_missing' containing the missing apparent power at maximal over-loading in MVA as float, 'time_index' containing the corresponding time step the over-loading occurred in as :pandas:`pandas.Timestamp<Timestamp>`, and 'grid' containing the grid object as :class:`~.network.grids.LVGrid`. 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) if lv_grid_id is not None: lv_grids = [edisgo_obj.topology.get_lv_grid(lv_grid_id)] else: lv_grids = list(edisgo_obj.topology.lv_grids) for lv_grid in lv_grids: crit_stations = pd.concat( [ crit_stations, _station_max_overload(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_max_overload(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>` In case there are no over-loading problems returns an empty dataframe. In case of over-loading problems the dataframe contains the name of the over-loaded station (grid's name with the extension '_station') in the index. Columns are 's_missing' containing the missing apparent power at maximal over-loading in MVA as float, 'time_index' containing the corresponding time step the over-loading occurred in as :pandas:`pandas.Timestamp<Timestamp>`, and 'grid' containing the grid object as :class:`~.network.grids.Grid`. """ if isinstance(grid, LVGrid): voltage_level = "lv" elif isinstance(grid, MVGrid): voltage_level = "mv" else: raise ValueError("Inserted grid is invalid.") # get apparent power over station from power flow analysis 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) # 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.dropna().empty: load_factor = edisgo_obj.timeseries.timesteps_load_feedin_case.apply( lambda _: edisgo_obj.config["grid_expansion_load_factors"][ f"{voltage_level}_{_}_transformer" ] ) # calculate the greatest apparent power missing (residual apparent power is # divided by the load factor to account for load factors smaller than # one, which lead to a higher needed additional capacity) s_missing = (s_res.iloc[:, 0] / load_factor).dropna() return pd.DataFrame( { "s_missing": abs(s_missing.min()), "time_index": s_missing.idxmin(), "grid": grid, }, index=[grid.station_name], ) else: return pd.DataFrame(dtype=float) def _station_load(edisgo_obj, grid): """ Returns loading of stations per time step from power flow analysis in MVA. In case of HV/MV transformers, which are not included in power flow analysis, loading is determined using slack results. Parameters ---------- edisgo_obj : :class:`~.EDisGo` grid : :class:`~.network.grids.LVGrid` or :class:`~.network.grids.MVGrid` Returns ------- :pandas:`pandas.DataFrame<DataFrame>` Dataframe containing loading of grid's station to the overlying voltage level per time step in MVA. Index of the dataframe are all time steps power flow analysis was conducted for of type :pandas:`pandas.Timestamp<Timestamp>`. Column name is grid's name with the extension '_station'. """ # get apparent power over station from power flow analysis if isinstance(grid, LVGrid): return pd.DataFrame( { grid.station_name: edisgo_obj.results.s_res.loc[ :, grid.transformers_df.index ].sum(axis=1) } ) elif isinstance(grid, MVGrid): # ensure that power flow was conducted for MV as slack could also be at MV/LV # station's secondary side 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." ) return pd.DataFrame( { grid.station_name: np.hypot( edisgo_obj.results.pfa_slack.p, edisgo_obj.results.pfa_slack.q, ) } ) else: raise ValueError("Inserted grid is invalid.") def _station_allowed_load(edisgo_obj, grid): """ Returns allowed loading of grid's station 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'. Parameters ---------- edisgo_obj : :class:`~.EDisGo` grid : :class:`~.network.grids.LVGrid` or :class:`~.network.grids.MVGrid` Grid to get allowed station loading for. Returns ------- :pandas:`pandas.DataFrame<DataFrame>` Dataframe containing the maximum allowed apparent power over the grid's transformers to the overlying voltage level per time step in MVA. Index of the dataframe are all time steps power flow analysis was conducted for of type :pandas:`pandas.Timestamp<Timestamp>`. Column name is grid's name with the extension '_station'. """ # get grid's voltage level and transformers to the overlying voltage level if isinstance(grid, LVGrid): voltage_level = "lv" transformers_df = grid.transformers_df elif isinstance(grid, MVGrid): voltage_level = "mv" transformers_df = edisgo_obj.topology.transformers_hvmv_df 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" ] ) return pd.DataFrame( {grid.station_name: s_station * load_factor}, index=load_factor.index )
[docs] def stations_allowed_load(edisgo_obj, grids=None): """ Returns allowed loading of specified grids 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'. 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. Returns ------- :pandas:`pandas.DataFrame<DataFrame>` Dataframe containing the maximum allowed apparent power over the grid's transformers to the overlying voltage level per time step in MVA. Index of the dataframe are all time steps power flow analysis was conducted for of type :pandas:`pandas.Timestamp<Timestamp>`. Column names are the respective grid's name with the extension '_station'. """ if grids is None: grids = edisgo_obj.topology.grids allowed_loading = pd.DataFrame() for grid in grids: allowed_loading = pd.concat( [allowed_loading, _station_allowed_load(edisgo_obj, grid)], axis=1 ) return allowed_loading
[docs] def stations_relative_load(edisgo_obj, grids=None): """ Returns relative loading of specified grids stations to the overlying voltage level per time step in p.u.. Stations relative loading is determined by dividing the stations loading (from power flow analysis) by the allowed loading (considering allowed load factors in heavy load flow case ('load case') and reverse power flow case ('feed-in case') from config files). Parameters ---------- edisgo_obj : :class:`~.EDisGo` grids : list(:class:`~.network.grids.Grid`) List of MV and LV grids to get relative station loading for. Per default relative loading is returned for all stations in the network that were included in the power flow analysis. Default: None. Returns ------- :pandas:`pandas.DataFrame<DataFrame>` Dataframe containing the relative loading of the grid's transformers to the overlying voltage level per time step in p.u.. Index of the dataframe are all time steps power flow analysis was conducted for of type :pandas:`pandas.Timestamp<Timestamp>`. Column names are the respective grid's name with the extension '_station'. """ if grids is None: grids = edisgo_obj.topology.grids # get allowed loading allowed_loading = stations_allowed_load(edisgo_obj, grids) # get loading from power flow results loading = pd.DataFrame() for grid in grids: # check that grid was included in power flow analysis try: loading = pd.concat([loading, _station_load(edisgo_obj, grid)], axis=1) except Exception: pass return loading / allowed_loading.loc[loading.index, loading.columns]
[docs] def components_relative_load(edisgo_obj, n_minus_one=False): """ Returns relative loading of all lines and stations included in power flow analysis. The component's relative loading is determined by dividing the stations loading (from power flow analysis) by the allowed loading (considering allowed load factors in heavy load flow case ('load case') and reverse power flow case ('feed-in case') from config files). Parameters ---------- edisgo_obj : :class:`~.EDisGo` n_minus_one : bool Determines which allowed load factors to use. See :py:attr:`~lines_allowed_load` for more information. Returns ------- :pandas:`pandas.DataFrame<DataFrame>` Dataframe containing the relative loading of lines and stations power flow results are available for per time step in p.u.. Index of the dataframe are all time steps power flow analysis was conducted for of type :pandas:`pandas.Timestamp<Timestamp>`. Columns are line names (as in index of :attr:`~.network.topology.Topology.loads_df`) and station names (respective grid's name with the extension '_station', see :attr:`~.network.grids.Grid.station_name`). """ stations_rel_load = stations_relative_load(edisgo_obj) lines_rel_load = lines_relative_load( edisgo_obj, lines=None, n_minus_one=n_minus_one ) return pd.concat([lines_rel_load, stations_rel_load], axis=1)
[docs] def voltage_issues(edisgo_obj, voltage_level, split_voltage_band=True, lv_grid_id=None): """ Gives buses with voltage issues and their maximum voltage deviation in p.u.. Parameters ---------- edisgo_obj : :class:`~.EDisGo` voltage_level : None or str Specifies voltage level for which to determine voltage issues. Possible options are 'mv' to check voltage deviations at MV buses, 'mv_lv' to check voltage deviations at MV-LV stations, and 'lv' to check voltage deviations at LV buses. If None voltage deviations in all voltage levels are checked. split_voltage_band : bool If True the allowed voltage band of +/-10 percent is allocated to the different voltage levels MV, MV/LV and LV according to config values set in section `grid_expansion_allowed_voltage_deviations`. If False, the same voltage limits are used for all voltage levels. Default: True. lv_grid_id : str or int or None This parameter is only used in case `voltage_level` is "mv_lv" or "lv". If None, checks voltage issues for all LV buses. Otherwise, only buses in given LV grid are checked. Default: None. Returns ------- :pandas:`pandas.DataFrame<DataFrame>` Dataframe with maximum deviations from allowed lower or upper voltage limits in p.u. sorted descending from highest to lowest voltage deviation (it is not distinguished between over- or undervoltage). Columns of the dataframe are 'abs_max_voltage_dev' containing the maximum absolute voltage deviation as float, 'time_index' containing the corresponding time step the maximum voltage issue occured in as :pandas:`pandas.Timestamp<Timestamp>`, and 'lv_grid_id' giving the LV grid ID the bus is in as integer. Index of the dataframe are the names of all buses with voltage issues as in index of :attr:`~.network.topology.Topology.buses_df`. 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'. """ if voltage_level: if voltage_level == "mv_lv": if lv_grid_id is None: buses = edisgo_obj.topology.transformers_df.bus1.unique() else: lv_grid = edisgo_obj.topology.get_lv_grid(lv_grid_id) buses = lv_grid.transformers_df.bus1.unique() elif voltage_level == "lv": if lv_grid_id is None: buses = edisgo_obj.topology.buses_df.index # drop MV buses and buses of stations secondary sides station_buses = edisgo_obj.topology.transformers_df.bus1.unique() buses = buses.drop( edisgo_obj.topology.mv_grid.buses_df.index.append( pd.Index(station_buses) ) ) else: lv_grid = edisgo_obj.topology.get_lv_grid(lv_grid_id) buses = lv_grid.buses_df.index # drop buses of station's secondary side station_buses = lv_grid.transformers_df.bus1.unique() buses = buses.drop(station_buses) elif voltage_level == "mv": buses = edisgo_obj.topology.mv_grid.buses_df.index else: raise ValueError( "{} is not a valid option for input variable 'voltage_level' in " "function voltage_issue. Possible options are 'mv', 'mv_lv', 'lv', " "or None.".format(voltage_level) ) else: mv_issues = voltage_issues( edisgo_obj, voltage_level="mv", split_voltage_band=split_voltage_band ) mv_lv_issues = voltage_issues( edisgo_obj, voltage_level="mv_lv", split_voltage_band=split_voltage_band ) lv_issues = voltage_issues( edisgo_obj, voltage_level="lv", split_voltage_band=split_voltage_band ) crit_buses = pd.concat([mv_issues, mv_lv_issues, lv_issues]) if not crit_buses.empty: crit_buses.sort_values( by=["abs_max_voltage_dev"], ascending=False, inplace=True ) return crit_buses crit_buses = _voltage_issues_helper(edisgo_obj, buses, split_voltage_band) # join LV grid information if voltage_level == "mv_lv" or voltage_level == "lv": crit_buses["lv_grid_id"] = edisgo_obj.topology.buses_df.loc[ crit_buses.index, "lv_grid_id" ] else: crit_buses["lv_grid_id"] = None if not crit_buses.empty: if voltage_level == "mv_lv": message = "==> {} MV-LV station(s) has/have voltage issues." elif voltage_level == "lv": message = "==> {} LV bus(es) has/have voltage issues." else: message = "==> {} bus(es) in MV topology has/have voltage issues." logger.debug(message.format(len(crit_buses))) else: if voltage_level == "mv_lv": message = "==> No voltage issues in MV-LV stations." elif voltage_level == "lv": message = "==> No voltage issues in LV grids." else: message = "==> No voltage issues in MV topology." logger.debug(message) return crit_buses
def _voltage_issues_helper(edisgo_obj, buses, split_voltage_band): """ 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. split_voltage_band : bool If True the allowed voltage band of +/-10 percent is allocated to the different voltage levels MV, MV/LV and LV according to config values set in section `grid_expansion_allowed_voltage_deviations`. If False, the same voltage limits are used for all voltage levels. Returns ------- pandas:`pandas.DataFrame<DataFrame>` Dataframe with maximum deviations from allowed lower or upper voltage limits in p.u. sorted descending from highest to lowest voltage deviation (it is not distinguished between over- or undervoltage). Columns of the dataframe are 'abs_max_voltage_dev' containing the maximum absolute voltage deviation as float and 'time_index' containing the corresponding time step the maximum voltage issue occured in as :pandas:`pandas.Timestamp<Timestamp>`. Index of the dataframe are the names of all buses with voltage issues as in index of :attr:`~.network.topology.Topology.buses_df`. """ crit_buses = pd.DataFrame(dtype=float) # get voltage deviations voltage_dev = voltage_deviation_from_allowed_voltage_limits( edisgo_obj, buses=buses, split_voltage_band=split_voltage_band ) # drop buses without voltage issues voltage_dev = voltage_dev[voltage_dev != 0].dropna(how="all", axis=1).abs() # determine absolute maximum voltage deviation and time step it occurs crit_buses["abs_max_voltage_dev"] = voltage_dev.max() crit_buses["time_index"] = voltage_dev.idxmax() # sort descending by maximum voltage deviation if not crit_buses.empty: crit_buses.sort_values( by=["abs_max_voltage_dev"], ascending=False, inplace=True ) return crit_buses
[docs] def allowed_voltage_limits(edisgo_obj, buses=None, split_voltage_band=True): """ Calculates allowed upper and lower voltage limits. Parameters ---------- edisgo_obj : :class:`~.EDisGo` buses : list(str) List of bus names to get allowed voltage limits for. Per default allowed voltage limits are returned for all buses in the network. Default: None. split_voltage_band : bool If True the allowed voltage band of +/-10 percent is allocated to the different voltage levels MV, MV/LV and LV according to config values set in section `grid_expansion_allowed_voltage_deviations`. If False, the same voltage limits are used for all voltage levels. Default: True. Returns ------- :pandas:`pandas.DataFrame<DataFrame>` Dataframe containing the maximum allowed apparent power per line and time step in MVA. Index of the dataframe are all time steps power flow analysis was conducted for of type :pandas:`pandas.Timestamp<Timestamp>`. Columns are bus names as in index of :attr:`~.network.topology.Topology.buses_df`. """ if buses is None: buses = edisgo_obj.results.v_res.columns if split_voltage_band: # MV limits mv_buses = edisgo_obj.topology.mv_grid.buses_df.index mv_upper, mv_lower = _mv_allowed_voltage_limits(edisgo_obj) mv_upper = pd.DataFrame( mv_upper, columns=mv_buses, index=edisgo_obj.results.v_res.index ) mv_lower = pd.DataFrame( mv_lower, columns=mv_buses, index=edisgo_obj.results.v_res.index ) # station limits stations_upper, stations_lower = _lv_allowed_voltage_limits( edisgo_obj, mode="stations" ) # LV limits lv_upper, lv_lower = _lv_allowed_voltage_limits(edisgo_obj, mode=None) # concat results and select relevant buses upper = pd.concat([mv_upper, stations_upper, lv_upper], axis=1) lower = pd.concat([mv_lower, stations_lower, lv_lower], axis=1) # check if allowed voltage limits could be determined for all specified buses allowed_buses = upper.columns buses_not_incl = list(set(buses) - set(allowed_buses)) if buses_not_incl: logger.warning( f"Allowed voltage limits cannot be determined for all given buses as " f"voltage information from power flow analysis is needed to calculate " f"allowed voltage for the MV/LV and LV level but the buses were not " f"included in the power flow analysis. " f"This concerns the following buses: {buses_not_incl}." ) buses = list(set(buses) - set(buses_not_incl)) return upper.loc[:, buses], lower.loc[:, buses] else: upper = pd.DataFrame(1.1, columns=buses, index=edisgo_obj.results.v_res.index) lower = pd.DataFrame(0.9, columns=buses, index=edisgo_obj.results.v_res.index) return upper, lower
def _mv_allowed_voltage_limits(edisgo_obj): """ Calculates allowed lower and upper voltage limits for MV nodes in p.u.. Parameters ---------- edisgo_obj : :class:`~.EDisGo` Returns ------- (float, float) Lower and upper voltage limit for MV nodes. """ # get values from config 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" ] upper_limit = ( 1 + offset - control_deviation + edisgo_obj.config["grid_expansion_allowed_voltage_deviations"][ "mv_max_v_rise" ] ) lower_limit = ( 1 + offset + control_deviation - edisgo_obj.config["grid_expansion_allowed_voltage_deviations"][ "mv_max_v_drop" ] ) return upper_limit, lower_limit def _lv_allowed_voltage_limits(edisgo_obj, lv_grids=None, mode=None): """ Calculates allowed lower and upper voltage limits for either buses or transformers in given LV grids. Voltage limits are determined relative to the station's secondary side, in case limits are determined for buses in the LV grid (default), or relative to the station's primary side, in case limits are determined for transformers. Limits can only be determined for grids included in power flow analysis. Parameters ---------- edisgo_obj : :class:`~.EDisGo` lv_grids : list(:class:`~.network.grids.LVGrid`) or None LV grids to get voltage limits for. If None, limits for all LV grids included in last power flow analysis are returned. mode : None or 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.DataFrame<DataFrame>`, :pandas:`pandas.DataFrame<DataFrame>`) Dataframe containing the allowed lower and upper voltage limits in p.u.. Index of the dataframe are all time steps power flow was last conducted for of type :pandas:`pandas.Timestamp<Timestamp>`. Columns are bus names as in index of :attr:`~.network.topology.Topology.buses_df` for all buses power flow results are available. If mode is 'stations' columns contain bus names of the stations secondary sides. """ if lv_grids is None: lv_grids = list(edisgo_obj.topology.mv_grid.lv_grids) upper_limits_df = pd.DataFrame() lower_limits_df = pd.DataFrame() voltages_pfa = edisgo_obj.results.v_res buses_in_pfa = voltages_pfa.columns if mode == "stations": config_string = "mv_lv_station" # get base voltage (voltage at primary side) for each station voltage_base = pd.DataFrame() for grid in lv_grids: transformers_df = grid.transformers_df primary_side = transformers_df.iloc[0].bus0 secondary_side = transformers_df.iloc[0].bus1 if primary_side in buses_in_pfa: voltage_base[secondary_side] = voltages_pfa.loc[:, primary_side] upper_limits_df = ( voltage_base + edisgo_obj.config["grid_expansion_allowed_voltage_deviations"][ "{}_max_v_rise".format(config_string) ] ) lower_limits_df = ( voltage_base - edisgo_obj.config["grid_expansion_allowed_voltage_deviations"][ "{}_max_v_drop".format(config_string) ] ) else: config_string = "lv" # get all secondary sides and buses in grids buses_dict = {} secondary_sides_dict = {} for grid in lv_grids: secondary_side = grid.station.index[0] if secondary_side in buses_in_pfa: secondary_sides_dict[grid] = secondary_side buses_dict[grid.station.index[0]] = grid.buses_df.index.drop( grid.station.index[0] ) secondary_sides = pd.Series(secondary_sides_dict) voltage_base = voltages_pfa.loc[:, secondary_sides.values] upper_limits_df_tmp = ( voltage_base + edisgo_obj.config["grid_expansion_allowed_voltage_deviations"][ "{}_max_v_rise".format(config_string) ] ) lower_limits_df_tmp = ( voltage_base - edisgo_obj.config["grid_expansion_allowed_voltage_deviations"][ "{}_max_v_drop".format(config_string) ] ) # rename columns to secondary side for colname, values in upper_limits_df_tmp.items(): tmp = pd.DataFrame( data=np.tile(values, (len(buses_dict[colname]), 1)).T, columns=buses_dict[colname], index=values.index, ) upper_limits_df = pd.concat([upper_limits_df, tmp], axis=1) for colname, values in lower_limits_df_tmp.items(): tmp = pd.DataFrame( data=np.tile(values, (len(buses_dict[colname]), 1)).T, columns=buses_dict[colname], index=values.index, ) lower_limits_df = pd.concat([lower_limits_df, tmp], axis=1) return upper_limits_df, lower_limits_df
[docs] def voltage_deviation_from_allowed_voltage_limits( edisgo_obj, buses=None, split_voltage_band=True ): """ 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) or None List of buses to check voltage deviation for. Per default voltage deviation is returned for all buses included in the power flow analysis. Default: None. split_voltage_band : bool If True the allowed voltage band of +/-10 percent is allocated to the different voltage levels MV, MV/LV and LV according to config values set in section `grid_expansion_allowed_voltage_deviations`. If False, the same voltage limits are used for all voltage levels. Default: True. Returns ------- :pandas:`pandas.DataFrame<DataFrame>` Dataframe with deviations from allowed lower voltage level in p.u.. Positive values signify an overvoltage whereas negative values signify an undervoltage. Zero values signify that the voltage is within the allowed limits. Index of the dataframe are all time steps power flow analysis was conducted for of type :pandas:`pandas.Timestamp<Timestamp>`. Columns are bus names as in index of :attr:`~.network.topology.Topology.buses_df`. """ if buses is None: buses = edisgo_obj.results.v_res.columns # get allowed voltage deviations v_dev_allowed_upper, v_dev_allowed_lower = allowed_voltage_limits( edisgo_obj, buses=buses, split_voltage_band=split_voltage_band ) # the following is needed in case the power flow was only conducted for one LV # grid - voltage at station node cannot be checked, warning is already raised # in allowed_voltage_limits() buses = v_dev_allowed_upper.columns # get voltages from power flow analysis v_mag_pu_pfa = edisgo_obj.results.v_res.loc[:, buses] # make all entries without voltage issues NaN values overvoltage = v_mag_pu_pfa[v_mag_pu_pfa > v_dev_allowed_upper] undervoltage = v_mag_pu_pfa[v_mag_pu_pfa < v_dev_allowed_lower] # determine deviation from allowed voltage limits for times with voltage issues # overvoltage deviations are positive, undervoltage deviations negative overvoltage_dev = overvoltage - v_dev_allowed_upper undervoltage_dev = undervoltage - v_dev_allowed_lower # combine overvoltage and undervoltage issues and set NaN values to zero voltage_dev = overvoltage_dev.fillna(0) + undervoltage_dev.fillna(0) return voltage_dev