Source code for edisgo.flex_opt.check_tech_constraints

import pandas as pd
import logging

from edisgo.grid.grids import LVGrid
from edisgo.grid.components import LVStation

logger = logging.getLogger('edisgo')


[docs]def mv_line_load(network): """ Checks for over-loading issues in MV grid. Parameters ---------- network : :class:`~.grid.network.Network` Returns ------- :pandas:`pandas.DataFrame<dataframe>` Dataframe containing over-loaded MV lines, their maximum relative over-loading and the corresponding time step. Index of the dataframe are the over-loaded lines of type :class:`~.grid.components.Line`. Columns are 'max_rel_overload' containing the maximum relative over-loading as float and 'time_index' containing the corresponding time step the over-loading occured in as :pandas:`pandas.Timestamp<timestamp>`. 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 = pd.DataFrame() crit_lines = _line_load(network, network.mv_grid, crit_lines) if not crit_lines.empty: logger.debug('==> {} line(s) in MV grid has/have load issues.'.format( crit_lines.shape[0])) else: logger.debug('==> No line load issues in MV grid.') return crit_lines
[docs]def lv_line_load(network): """ Checks for over-loading issues in LV grids. Parameters ---------- network : :class:`~.grid.network.Network` Returns ------- :pandas:`pandas.DataFrame<dataframe>` Dataframe containing over-loaded LV lines, their maximum relative over-loading and the corresponding time step. Index of the dataframe are the over-loaded lines of type :class:`~.grid.components.Line`. Columns are 'max_rel_overload' containing the maximum relative over-loading as float and 'time_index' containing the corresponding time step the over-loading occured in as :pandas:`pandas.Timestamp<timestamp>`. 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 = pd.DataFrame() for lv_grid in network.mv_grid.lv_grids: crit_lines = _line_load(network, lv_grid, crit_lines) if not crit_lines.empty: logger.debug('==> {} line(s) in LV grids has/have load issues.'.format( crit_lines.shape[0])) else: logger.debug('==> No line load issues in LV grids.') return crit_lines
def _line_load(network, grid, crit_lines): """ Checks for over-loading issues of lines. Parameters ---------- network : :class:`~.grid.network.Network` grid : :class:`~.grid.grids.LVGrid` or :class:`~.grid.grids.MVGrid` crit_lines : :pandas:`pandas.DataFrame<dataframe>` Dataframe containing over-loaded lines, their maximum relative over-loading and the corresponding time step. Index of the dataframe are the over-loaded lines of type :class:`~.grid.components.Line`. Columns are 'max_rel_overload' containing the maximum relative over-loading as float and 'time_index' containing the corresponding time step the over-loading occured in as :pandas:`pandas.Timestamp<timestamp>`. Returns ------- :pandas:`pandas.DataFrame<dataframe>` Dataframe containing over-loaded lines, their maximum relative over-loading and the corresponding time step. Index of the dataframe are the over-loaded lines of type :class:`~.grid.components.Line`. Columns are 'max_rel_overload' containing the maximum relative over-loading as float and 'time_index' containing the corresponding time step the over-loading occured in as :pandas:`pandas.Timestamp<timestamp>`. """ if isinstance(grid, LVGrid): grid_level = 'lv' else: grid_level = 'mv' for line in list(grid.graph.lines()): i_line_allowed_per_case = {} i_line_allowed_per_case['feedin_case'] = \ line['line'].type['I_max_th'] * line['line'].quantity * \ network.config['grid_expansion_load_factors'][ '{}_feedin_case_line'.format(grid_level)] i_line_allowed_per_case['load_case'] = \ line['line'].type['I_max_th'] * line['line'].quantity * \ network.config['grid_expansion_load_factors'][ '{}_load_case_line'.format(grid_level)] # maximum allowed line load in each time step i_line_allowed = \ network.timeseries.timesteps_load_feedin_case.case.apply( lambda _: i_line_allowed_per_case[_]) try: # check if maximum current from power flow analysis exceeds # allowed maximum current i_line_pfa = network.results.i_res[repr(line['line'])] if any((i_line_allowed - i_line_pfa) < 0): # find out largest relative deviation relative_i_res = i_line_pfa / i_line_allowed crit_lines = crit_lines.append(pd.DataFrame( {'max_rel_overload': relative_i_res.max(), 'time_index': relative_i_res.idxmax()}, index=[line['line']])) except KeyError: logger.debug('No results for line {} '.format(str(line)) + 'to check overloading.') return crit_lines
[docs]def hv_mv_station_load(network): """ Checks for over-loading of HV/MV station. Parameters ---------- network : :class:`~.grid.network.Network` Returns ------- :pandas:`pandas.DataFrame<dataframe>` Dataframe containing over-loaded HV/MV stations, their apparent power at maximal over-loading and the corresponding time step. Index of the dataframe are the over-loaded stations of type :class:`~.grid.components.MVStation`. Columns are 's_pfa' containing the apparent power at maximal over-loading 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() crit_stations = _station_load(network, network.mv_grid.station, crit_stations) 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(network): """ Checks for over-loading of MV/LV stations. Parameters ---------- network : :class:`~.grid.network.Network` Returns ------- :pandas:`pandas.DataFrame<dataframe>` Dataframe containing over-loaded MV/LV stations, their apparent power at maximal over-loading and the corresponding time step. Index of the dataframe are the over-loaded stations of type :class:`~.grid.components.LVStation`. Columns are 's_pfa' containing the apparent power at maximal over-loading 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() for lv_grid in network.mv_grid.lv_grids: crit_stations = _station_load(network, lv_grid.station, crit_stations) 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(network, station, crit_stations): """ Checks for over-loading of stations. Parameters ---------- network : :class:`~.grid.network.Network` station : :class:`~.grid.components.LVStation` or :class:`~.grid.components.MVStation` crit_stations : :pandas:`pandas.DataFrame<dataframe>` Dataframe containing over-loaded stations, their apparent power at maximal over-loading and the corresponding time step. Index of the dataframe are the over-loaded stations either of type :class:`~.grid.components.LVStation` or :class:`~.grid.components.MVStation`. Columns are 's_pfa' containing the apparent power at maximal over-loading as float and 'time_index' containing the corresponding time step the over-loading occured in as :pandas:`pandas.Timestamp<timestamp>`. Returns ------- :pandas:`pandas.DataFrame<dataframe>` Dataframe containing over-loaded stations, their apparent power at maximal over-loading and the corresponding time step. Index of the dataframe are the over-loaded stations either of type :class:`~.grid.components.LVStation` or :class:`~.grid.components.MVStation`. Columns are 's_pfa' containing the apparent power at maximal over-loading as float and 'time_index' containing the corresponding time step the over-loading occured in as :pandas:`pandas.Timestamp<timestamp>`. """ if isinstance(station, LVStation): grid_level = 'lv' else: grid_level = 'mv' # maximum allowed apparent power of station for feed-in and load case s_station = sum([_.type.S_nom for _ in station.transformers]) s_station_allowed_per_case = {} s_station_allowed_per_case['feedin_case'] = s_station * network.config[ 'grid_expansion_load_factors']['{}_feedin_case_transformer'.format( grid_level)] s_station_allowed_per_case['load_case'] = s_station * network.config[ 'grid_expansion_load_factors']['{}_load_case_transformer'.format( grid_level)] # maximum allowed apparent power of station in each time step s_station_allowed = \ network.timeseries.timesteps_load_feedin_case.case.apply( lambda _: s_station_allowed_per_case[_]) try: if isinstance(station, LVStation): s_station_pfa = network.results.s_res( station.transformers).sum(axis=1) else: s_station_pfa = network.results.s_res([station]).iloc[:, 0] s_res = s_station_allowed - s_station_pfa s_res = s_res[s_res < 0] # check if maximum allowed apparent power of station exceeds # apparent power from power flow analysis at any time step if not s_res.empty: # find out largest relative deviation load_factor = \ network.timeseries.timesteps_load_feedin_case.case.apply( lambda _: network.config[ 'grid_expansion_load_factors'][ '{}_{}_transformer'.format(grid_level, _)]) relative_s_res = load_factor * s_res crit_stations = crit_stations.append(pd.DataFrame( {'s_pfa': s_station_pfa.loc[relative_s_res.idxmin()], 'time_index': relative_s_res.idxmin()}, index=[station])) except KeyError: logger.debug('No results for {} station to check overloading.'.format( grid_level.upper())) return crit_stations
[docs]def mv_voltage_deviation(network, voltage_levels='mv_lv'): """ Checks for voltage stability issues in MV grid. Parameters ---------- network : :class:`~.grid.network.Network` voltage_levels : :obj:`str` Specifies which allowed voltage deviations to use. Possible options are: * 'mv_lv' This is the default. The allowed voltage deviation for nodes in the MV grid is the same as for nodes in the LV grid. Further load and feed-in case are not distinguished. * 'mv' Use this to handle allowed voltage deviations in the MV and LV grid differently. Here, load and feed-in case are differentiated as well. Returns ------- :obj:`dict` Dictionary with :class:`~.grid.grids.MVGrid` as key and a :pandas:`pandas.DataFrame<dataframe>` with its critical nodes, sorted descending by voltage deviation, as value. Index of the dataframe are all nodes (of type :class:`~.grid.components.Generator`, :class:`~.grid.components.Load`, etc.) with over-voltage issues. Columns are 'v_mag_pu' containing the maximum voltage deviation as float and 'time_index' containing the corresponding time step the over-voltage 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_nodes = {} v_dev_allowed_per_case = {} v_dev_allowed_per_case['feedin_case_lower'] = 0.9 v_dev_allowed_per_case['load_case_upper'] = 1.1 offset = network.config[ 'grid_expansion_allowed_voltage_deviations']['hv_mv_trafo_offset'] control_deviation = network.config[ 'grid_expansion_allowed_voltage_deviations'][ 'hv_mv_trafo_control_deviation'] if voltage_levels == 'mv_lv': v_dev_allowed_per_case['feedin_case_upper'] = \ 1 + offset + control_deviation + network.config[ 'grid_expansion_allowed_voltage_deviations'][ 'mv_lv_feedin_case_max_v_deviation'] v_dev_allowed_per_case['load_case_lower'] = \ 1 + offset - control_deviation - network.config[ 'grid_expansion_allowed_voltage_deviations'][ 'mv_lv_load_case_max_v_deviation'] elif voltage_levels == 'mv': v_dev_allowed_per_case['feedin_case_upper'] = \ 1 + offset + control_deviation + network.config[ 'grid_expansion_allowed_voltage_deviations'][ 'mv_feedin_case_max_v_deviation'] v_dev_allowed_per_case['load_case_lower'] = \ 1 + offset - control_deviation - network.config[ 'grid_expansion_allowed_voltage_deviations'][ 'mv_load_case_max_v_deviation'] else: raise ValueError( 'Specified mode {} is not a valid option.'.format(voltage_levels)) # maximum allowed apparent power of station in each time step v_dev_allowed_upper = \ network.timeseries.timesteps_load_feedin_case.case.apply( lambda _: v_dev_allowed_per_case['{}_upper'.format(_)]) v_dev_allowed_lower = \ network.timeseries.timesteps_load_feedin_case.case.apply( lambda _: v_dev_allowed_per_case['{}_lower'.format(_)]) nodes = list(network.mv_grid.graph.nodes()) crit_nodes_grid = _voltage_deviation( network, nodes, v_dev_allowed_upper, v_dev_allowed_lower, voltage_level='mv') if not crit_nodes_grid.empty: crit_nodes[network.mv_grid] = crit_nodes_grid.sort_values( by=['v_mag_pu'], ascending=False) logger.debug( '==> {} node(s) in MV grid has/have voltage issues.'.format( crit_nodes[network.mv_grid].shape[0])) else: logger.debug('==> No voltage issues in MV grid.') return crit_nodes
[docs]def lv_voltage_deviation(network, mode=None, voltage_levels='mv_lv'): """ Checks for voltage stability issues in LV grids. Parameters ---------- network : :class:`~.grid.network.Network` mode : None or String If None voltage at all nodes in LV grid is checked. If mode is set to 'stations' only voltage at busbar is checked. voltage_levels : :obj:`str` Specifies which allowed voltage deviations to use. Possible options are: * 'mv_lv' This is the default. The allowed voltage deviation for nodes in the MV grid is the same as for nodes in the LV grid. Further load and feed-in case are not distinguished. * 'lv' Use this to handle allowed voltage deviations in the MV and LV grid differently. Here, load and feed-in case are differentiated as well. Returns ------- :obj:`dict` Dictionary with :class:`~.grid.grids.LVGrid` as key and a :pandas:`pandas.DataFrame<dataframe>` with its critical nodes, sorted descending by voltage deviation, as value. Index of the dataframe are all nodes (of type :class:`~.grid.components.Generator`, :class:`~.grid.components.Load`, etc.) with over-voltage issues. Columns are 'v_mag_pu' containing the maximum voltage deviation as float and 'time_index' containing the corresponding time step the over-voltage 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_nodes = {} v_dev_allowed_per_case = {} if voltage_levels == 'mv_lv': offset = network.config[ 'grid_expansion_allowed_voltage_deviations']['hv_mv_trafo_offset'] control_deviation = network.config[ 'grid_expansion_allowed_voltage_deviations'][ 'hv_mv_trafo_control_deviation'] v_dev_allowed_per_case['feedin_case_upper'] = \ 1 + offset + control_deviation + network.config[ 'grid_expansion_allowed_voltage_deviations'][ 'mv_lv_feedin_case_max_v_deviation'] v_dev_allowed_per_case['load_case_lower'] = \ 1 + offset - control_deviation - network.config[ 'grid_expansion_allowed_voltage_deviations'][ 'mv_lv_load_case_max_v_deviation'] v_dev_allowed_per_case['feedin_case_lower'] = 0.9 v_dev_allowed_per_case['load_case_upper'] = 1.1 v_dev_allowed_upper = \ network.timeseries.timesteps_load_feedin_case.case.apply( lambda _: v_dev_allowed_per_case['{}_upper'.format(_)]) v_dev_allowed_lower = \ network.timeseries.timesteps_load_feedin_case.case.apply( lambda _: v_dev_allowed_per_case['{}_lower'.format(_)]) elif voltage_levels == 'lv': pass else: raise ValueError( 'Specified mode {} is not a valid option.'.format(voltage_levels)) for lv_grid in network.mv_grid.lv_grids: if mode: if mode == 'stations': nodes = [lv_grid.station] else: raise ValueError( "{} is not a valid option for input variable 'mode' in " "function lv_voltage_deviation. Try 'stations' or " "None".format(mode)) else: nodes = list(lv_grid.graph.nodes()) if voltage_levels == 'lv': if mode == 'stations': # get voltage at primary side to calculate upper bound for # feed-in case and lower bound for load case v_lv_station_primary = network.results.v_res( nodes=[lv_grid.station], level='mv').iloc[:, 0] timeindex = v_lv_station_primary.index v_dev_allowed_per_case['feedin_case_upper'] = \ v_lv_station_primary + network.config[ 'grid_expansion_allowed_voltage_deviations'][ 'mv_lv_station_feedin_case_max_v_deviation'] v_dev_allowed_per_case['load_case_lower'] = \ v_lv_station_primary - network.config[ 'grid_expansion_allowed_voltage_deviations'][ 'mv_lv_station_load_case_max_v_deviation'] else: # get voltage at secondary side to calculate upper bound for # feed-in case and lower bound for load case v_lv_station_secondary = network.results.v_res( nodes=[lv_grid.station], level='lv').iloc[:, 0] timeindex = v_lv_station_secondary.index v_dev_allowed_per_case['feedin_case_upper'] = \ v_lv_station_secondary + network.config[ 'grid_expansion_allowed_voltage_deviations'][ 'lv_feedin_case_max_v_deviation'] v_dev_allowed_per_case['load_case_lower'] = \ v_lv_station_secondary - network.config[ 'grid_expansion_allowed_voltage_deviations'][ 'lv_load_case_max_v_deviation'] v_dev_allowed_per_case['feedin_case_lower'] = pd.Series( 0.9, index=timeindex) v_dev_allowed_per_case['load_case_upper'] = pd.Series( 1.1, index=timeindex) # maximum allowed voltage deviation in each time step v_dev_allowed_upper = [] v_dev_allowed_lower = [] for t in timeindex: case = \ network.timeseries.timesteps_load_feedin_case.loc[ t, 'case'] v_dev_allowed_upper.append( v_dev_allowed_per_case[ '{}_upper'.format(case)].loc[t]) v_dev_allowed_lower.append( v_dev_allowed_per_case[ '{}_lower'.format(case)].loc[t]) v_dev_allowed_upper = pd.Series(v_dev_allowed_upper, index=timeindex) v_dev_allowed_lower = pd.Series(v_dev_allowed_lower, index=timeindex) crit_nodes_grid = _voltage_deviation( network, nodes, v_dev_allowed_upper, v_dev_allowed_lower, voltage_level='lv') if not crit_nodes_grid.empty: crit_nodes[lv_grid] = crit_nodes_grid.sort_values( by=['v_mag_pu'], ascending=False) if crit_nodes: if mode == 'stations': logger.debug( '==> {} LV station(s) has/have voltage issues.'.format( len(crit_nodes))) else: logger.debug( '==> {} LV grid(s) has/have voltage issues.'.format( len(crit_nodes))) else: if mode == 'stations': logger.debug('==> No voltage issues in LV stations.') else: logger.debug('==> No voltage issues in LV grids.') return crit_nodes
def _voltage_deviation(network, nodes, v_dev_allowed_upper, v_dev_allowed_lower, voltage_level): """ Checks for voltage stability issues in LV grids. Parameters ---------- network : :class:`~.grid.network.Network` nodes : :obj:`list` List of nodes (of type :class:`~.grid.components.Generator`, :class:`~.grid.components.Load`, etc.) 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. 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. voltage_levels : :obj:`str` Specifies which voltage level to retrieve power flow analysis results for. Possible options are 'mv' and 'lv'. Returns ------- :pandas:`pandas.DataFrame<dataframe>` Dataframe with critical nodes, sorted descending by voltage deviation. Index of the dataframe are all nodes (of type :class:`~.grid.components.Generator`, :class:`~.grid.components.Load`, etc.) with over-voltage issues. Columns are 'v_mag_pu' containing the maximum voltage deviation as float and 'time_index' containing the corresponding time step the over-voltage occured in as :pandas:`pandas.Timestamp<timestamp>`. """ def _append_crit_node(series): return pd.DataFrame({'v_mag_pu': series.max(), 'time_index': series.idxmax()}, index=[node]) crit_nodes_grid = pd.DataFrame() v_mag_pu_pfa = network.results.v_res(nodes=nodes, level=voltage_level) for node in nodes: # check for over- and under-voltage overvoltage = v_mag_pu_pfa[repr(node)][ (v_mag_pu_pfa[repr(node)] > (v_dev_allowed_upper.loc[ v_mag_pu_pfa.index]))] undervoltage = v_mag_pu_pfa[repr(node)][ (v_mag_pu_pfa[repr(node)] < (v_dev_allowed_lower.loc[ v_mag_pu_pfa.index]))] # write greatest voltage deviation to dataframe if not overvoltage.empty: overvoltage_diff = overvoltage - v_dev_allowed_upper.loc[ overvoltage.index] if not undervoltage.empty: undervoltage_diff = v_dev_allowed_lower.loc[ undervoltage.index] - undervoltage if overvoltage_diff.max() > undervoltage_diff.max(): crit_nodes_grid = crit_nodes_grid.append( _append_crit_node(overvoltage_diff)) else: crit_nodes_grid = crit_nodes_grid.append( _append_crit_node(undervoltage_diff)) else: crit_nodes_grid = crit_nodes_grid.append( _append_crit_node(overvoltage_diff)) elif not undervoltage.empty: undervoltage_diff = v_dev_allowed_lower.loc[ undervoltage.index] - undervoltage crit_nodes_grid = crit_nodes_grid.append( _append_crit_node(undervoltage_diff)) return crit_nodes_grid
[docs]def check_ten_percent_voltage_deviation(network): """ Checks if 10% criteria is exceeded. Parameters ---------- network : :class:`~.grid.network.Network` """ v_mag_pu_pfa = network.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)