Source code for ding0.grid.lv_grid.build_grid

"""This file is part of DING0, the DIstribution Network GeneratOr.
DING0 is a tool to generate synthetic medium and low voltage power
distribution grids based on open data.

It is developed in the project open_eGo: https://openegoproject.wordpress.com

DING0 lives at github: https://github.com/openego/ding0/
The documentation is available on RTD: http://ding0.readthedocs.io"""

__copyright__  = "Reiner Lemoine Institut gGmbH"
__license__    = "GNU Affero General Public License Version 3 (AGPL-3.0)"
__url__        = "https://github.com/openego/ding0/blob/master/LICENSE"
__author__     = "nesnoj, gplssm"


from ding0.tools import config as cfg_ding0

from ding0.core.network import TransformerDing0, BranchDing0
from ding0.core.network.cable_distributors import LVCableDistributorDing0
from ding0.core.network.loads import LVLoadDing0
import logging
import math

logger = logging.getLogger(__name__)


[docs]def select_transformers(grid, s_max=None): """Selects LV transformer according to peak load of LV grid district. The transformers are chosen according to max. of load case and feedin-case considering load factors and power factor. The MV-LV transformer with the next higher available nominal apparent power is chosen. Therefore, a max. allowed transformer loading of 100% is implicitly assumed. If the peak load exceeds the max. power of a single available transformer, multiple transformer are build. By default `peak_load` and `peak_generation` are taken from `grid` instance. The behavior can be overridden providing `s_max` as explained in ``Arguments``. Parameters ---------- grid: LVGridDing0 LV grid data Arguments --------- s_max : dict dict containing maximum apparent power of load or generation case and str describing the case. For example .. code-block:: python { 's_max': 480, 'case': 'load' } or .. code-block:: python { 's_max': 120, 'case': 'gen' } s_max passed overrides `grid.grid_district.peak_load` respectively `grid.station().peak_generation`. Returns ------- :pandas:`pandas.DataFrame<dataframe>` Parameters of chosen Transformer :obj:`int` Count of transformers Note ----- The LV transformer with the next higher available nominal apparent power is chosen. Therefore, a max. allowed transformer loading of 100% is implicitly assumed. If the peak load exceeds the max. power of a single available transformer, use multiple trafos. """ load_factor_lv_trans_lc_normal = cfg_ding0.get('assumptions', 'load_factor_lv_trans_lc_normal') load_factor_lv_trans_fc_normal = cfg_ding0.get('assumptions', 'load_factor_lv_trans_fc_normal') cos_phi_load = cfg_ding0.get('assumptions', 'cos_phi_load') cos_phi_gen = cfg_ding0.get('assumptions', 'cos_phi_gen') # get equipment parameters of LV transformers trafo_parameters = grid.network.static_data['LV_trafos'] # determine s_max from grid object if not provided via arguments if s_max is None: # get maximum from peak load and peak generation s_max_load = grid.grid_district.peak_load / cos_phi_load s_max_gen = grid.station().peak_generation / cos_phi_gen # check if load or generation is greater respecting corresponding load factor if s_max_load > s_max_gen: # use peak load and load factor from load case load_factor_lv_trans = load_factor_lv_trans_lc_normal s_max = s_max_load else: # use peak generation and load factor for feedin case load_factor_lv_trans = load_factor_lv_trans_fc_normal s_max = s_max_gen else: if s_max['case'] == 'load': load_factor_lv_trans = load_factor_lv_trans_lc_normal elif s_max['case'] == 'gen': load_factor_lv_trans = load_factor_lv_trans_fc_normal else: logger.error('No proper \'case\' provided for argument s_max') raise ValueError('Please provide proper \'case\' for argument ' '`s_max`.') s_max = s_max['s_max'] # get max. trafo transformer_max = trafo_parameters.iloc[trafo_parameters['S_nom'].idxmax()] # peak load is smaller than max. available trafo if s_max < (transformer_max['S_nom'] * load_factor_lv_trans ): # choose trafo transformer = trafo_parameters.iloc[ trafo_parameters[ trafo_parameters['S_nom'] * load_factor_lv_trans > s_max][ 'S_nom'].idxmin()] transformer_cnt = 1 # peak load is greater than max. available trafo -> use multiple trafos else: transformer_cnt = 2 # increase no. of trafos until peak load can be supplied while not any(trafo_parameters['S_nom'] * load_factor_lv_trans > ( s_max / transformer_cnt)): transformer_cnt += 1 transformer = trafo_parameters.iloc[ trafo_parameters[ trafo_parameters['S_nom'] * load_factor_lv_trans > (s_max / transformer_cnt)]['S_nom'].idxmin()] return transformer, transformer_cnt
[docs]def transformer(grid): """ Choose transformer and add to grid's station Parameters ---------- grid: LVGridDing0 LV grid data s_max_from_buildings: boolean new approach if s_max_from_buildings """ v_nom = cfg_ding0.get('assumptions', 'lv_nominal_voltage') / 1e3 # v_nom in kV # choose size and amount of transformers transformer, transformer_cnt = select_transformers(grid) # create transformers and add them to station of LVGD for t in range(0, transformer_cnt): lv_transformer = TransformerDing0( grid=grid, id_db=t + 1, v_level=v_nom, s_max_longterm=transformer['S_nom'], r_pu=transformer['r_pu'], x_pu=transformer['x_pu']) # add each transformer to its station grid._station.add_transformer(lv_transformer)
[docs]def select_grid_model_ria(lvgd, sector): """Select a typified grid for retail/industrial and agricultural Parameters ---------- lvgd : ding0.core.structure.regions.LVGridDistrictDing0 Low-voltage grid district object sector : :obj:`str` Either 'retail/industrial' or 'agricultural'. Depending on choice different parameters to grid topology apply Returns ------- :obj:`dict` Parameters that describe branch lines of a sector """ cable_lf = cfg_ding0.get('assumptions', 'load_factor_lv_cable_lc_normal') cos_phi_load = cfg_ding0.get('assumptions', 'cos_phi_load') max_lv_branch_line_load = cfg_ding0.get('assumptions', 'max_lv_branch_line') # make a distinction between sectors if sector == 'retail/industrial': max_branch_length = cfg_ding0.get( "assumptions", "branch_line_length_retail_industrial") peak_load = lvgd.peak_load_retail + \ lvgd.peak_load_industrial count_sector_areas = lvgd.sector_count_retail + \ lvgd.sector_count_industrial elif sector == 'agricultural': max_branch_length = cfg_ding0.get( "assumptions", "branch_line_length_agricultural") peak_load = lvgd.peak_load_agricultural count_sector_areas = lvgd.sector_count_agricultural else: raise ValueError('Sector {} does not exist!'.format(sector)) # determine size of a single load single_peak_load = peak_load / count_sector_areas # if this single load exceeds threshold of 300 kVA it is splitted while single_peak_load > (max_lv_branch_line_load * (cable_lf * cos_phi_load)): single_peak_load = single_peak_load / 2 count_sector_areas = count_sector_areas * 2 grid_model = {} # determine parameters of branches and loads connected to the branch # line if 0 < single_peak_load: grid_model['max_loads_per_branch'] = math.floor( (max_lv_branch_line_load * (cable_lf * cos_phi_load)) / single_peak_load) grid_model['single_peak_load'] = single_peak_load grid_model['full_branches'] = math.floor( count_sector_areas / grid_model['max_loads_per_branch']) grid_model['remaining_loads'] = count_sector_areas - ( grid_model['full_branches'] * grid_model['max_loads_per_branch'] ) grid_model['load_distance'] = max_branch_length / ( grid_model['max_loads_per_branch'] + 1) grid_model['load_distance_remaining'] = max_branch_length / ( grid_model['remaining_loads'] + 1) else: if count_sector_areas > 0: logger.warning( 'LVGD {lvgd} has in sector {sector} no load but area count' 'is {count}. This is maybe related to #153'.format( lvgd=lvgd, sector=sector, count=count_sector_areas)) grid_model = None # add consumption to grid_model for assigning it to the load object # consumption is given per sector and per individual load if sector == 'retail/industrial': grid_model['consumption'] = { 'retail': lvgd.sector_consumption_retail / ( grid_model['full_branches'] * grid_model['max_loads_per_branch'] + grid_model['remaining_loads']), 'industrial': lvgd.sector_consumption_industrial / ( grid_model['full_branches'] * grid_model['max_loads_per_branch'] + grid_model['remaining_loads'])} elif sector == 'agricultural': grid_model['consumption'] = { 'agricultural': lvgd.sector_consumption_agricultural / ( grid_model['full_branches'] * grid_model['max_loads_per_branch'] + grid_model['remaining_loads'])} return grid_model
[docs]def grid_model_params_ria(lvgd): """Determine grid model parameters for LV grids of sectors retail/industrial and agricultural Parameters ---------- lvgd : LVGridDistrictDing0 Low-voltage grid district object Returns ------- :obj:`dict` Structural description of (parts of) LV grid topology """ # Choose retail/industrial and agricultural grid model model_params_ria = {} if ((lvgd.sector_count_retail + lvgd.sector_count_industrial > 0) or (lvgd.peak_load_retail + lvgd.peak_load_industrial > 0)): model_params_ria['retail/industrial'] = select_grid_model_ria( lvgd, 'retail/industrial') else: model_params_ria['retail/industrial'] = None if ((lvgd.sector_count_agricultural > 0) or (lvgd.peak_load_agricultural > 0)): model_params_ria['agricultural'] = select_grid_model_ria(lvgd, 'agricultural') else: model_params_ria['agricultural'] = None return model_params_ria
[docs]def build_lv_graph_ria(lvgd, grid_model_params): """Build graph for LV grid of sectors retail/industrial and agricultural Based on structural description of LV grid topology for sectors retail/industrial and agricultural (RIA) branches for these sectors are created and attached to the LV grid's MV-LV substation bus bar. LV loads of the sectors retail/industrial and agricultural are located in separat branches for each sector (in case of large load multiple of these). These loads are distributed across the branches by an equidistant distribution. This function accepts the dict `grid_model_params` with particular structure >>> grid_model_params = { >>> ... 'agricultural': { >>> ... 'max_loads_per_branch': 2 >>> ... 'single_peak_load': 140, >>> ... 'full_branches': 2, >>> ... 'remaining_loads': 1, >>> ... 'load_distance': 800/3, >>> ... 'load_distance_remaining': 400}} Parameters ---------- lvgd : LVGridDistrictDing0 Low-voltage grid district object grid_model_params : dict Dict of structural information of sectoral LV grid branchwith particular structure, e.g.:: grid_model_params = { 'agricultural': { 'max_loads_per_branch': 2 'single_peak_load': 140, 'full_branches': 2, 'remaining_loads': 1, 'load_distance': 800/3, 'load_distance_remaining': 400 } } Note ----- We assume a distance from the load to the branch it is connected to of 30 m. This assumption is defined in the config files. """ def lv_graph_attach_branch(): """Attach a single branch including its equipment (cable dist, loads and line segments) to graph of `lv_grid` """ # determine maximum current occuring due to peak load # of this load load_no I_max_load = val['single_peak_load'] / (3 ** 0.5 * v_nom) / cos_phi_load # determine suitable cable for this current suitable_cables_stub = lvgd.lv_grid.network.static_data['LV_cables'][ (lvgd.lv_grid.network.static_data['LV_cables'][ 'I_max_th'] * cable_lf) > I_max_load] cable_type_stub = suitable_cables_stub.loc[ suitable_cables_stub['I_max_th'].idxmin(), : ] # cable distributor to divert from main branch lv_cable_dist = LVCableDistributorDing0( grid=lvgd.lv_grid, branch_no=branch_no, load_no=load_no) # add lv_cable_dist to graph lvgd.lv_grid.add_cable_dist(lv_cable_dist) # cable distributor within building (to connect load+geno) lv_cable_dist_building = LVCableDistributorDing0( grid=lvgd.lv_grid, branch_no=branch_no, load_no=load_no, in_building=True) # add lv_cable_dist_building to graph lvgd.lv_grid.add_cable_dist(lv_cable_dist_building) # create an instance of Ding0 LV load lv_load = LVLoadDing0(grid=lvgd.lv_grid, branch_no=branch_no, load_no=load_no, peak_load=val['single_peak_load'], consumption=val['consumption']) # add lv_load to graph lvgd.lv_grid.add_load(lv_load) # create branch line segment between either (a) station # and cable distributor or (b) between neighboring cable # distributors if load_no == 1: # case a: cable dist <-> station lvgd.lv_grid.graph.add_edge( lvgd.lv_grid.station(), lv_cable_dist, branch=BranchDing0( length=val['load_distance'], kind='cable', grid=lvgd.lv_grid, type=cable_type, id_db='branch_{sector}{branch}_{load}'.format( branch=branch_no, load=load_no, sector=sector_short) )) else: # case b: cable dist <-> cable dist lvgd.lv_grid.graph.add_edge( lvgd.lv_grid._cable_distributors[-4], lv_cable_dist, branch=BranchDing0( length=val['load_distance'], kind='cable', grid=lvgd.lv_grid, type=cable_type, id_db='branch_{sector}{branch}_{load}'.format( branch=branch_no, load=load_no, sector=sector_short))) # create branch stub that connects the load to the # lv_cable_dist located in the branch line lvgd.lv_grid.graph.add_edge( lv_cable_dist, lv_cable_dist_building, branch=BranchDing0( length=cfg_ding0.get( 'assumptions', 'lv_ria_branch_connection_distance'), kind='cable', grid=lvgd.lv_grid, type=cable_type_stub, id_db='stub_{sector}{branch}_{load}'.format( branch=branch_no, load=load_no, sector=sector_short)) ) lvgd.lv_grid.graph.add_edge( lv_cable_dist_building, lv_load, branch=BranchDing0( length=1, kind='cable', grid=lvgd.lv_grid, type=cable_type_stub, id_db='stub_{sector}{branch}_{load}'.format( branch=branch_no, load=load_no, sector=sector_short)) ) cable_lf = cfg_ding0.get('assumptions', 'load_factor_lv_cable_lc_normal') cos_phi_load = cfg_ding0.get('assumptions', 'cos_phi_load') v_nom = cfg_ding0.get('assumptions', 'lv_nominal_voltage') / 1e3 # v_nom in kV # iterate over branches for sectors retail/industrial and agricultural for sector, val in grid_model_params.items(): if sector == 'retail/industrial': sector_short = 'RETIND' elif sector == 'agricultural': sector_short = 'AGR' else: sector_short = '' if val is not None: for branch_no in list(range(1, val['full_branches'] + 1)): # determine maximum current occuring due to peak load of branch I_max_branch = (val['max_loads_per_branch'] * val['single_peak_load']) / (3 ** 0.5 * v_nom) / ( cos_phi_load) # determine suitable cable for this current suitable_cables = lvgd.lv_grid.network.static_data['LV_cables'][ (lvgd.lv_grid.network.static_data['LV_cables'][ 'I_max_th'] * cable_lf) > I_max_branch] cable_type = suitable_cables.loc[ suitable_cables['I_max_th'].idxmin(), : ] # create Ding0 grid objects and add to graph for load_no in list(range(1, val['max_loads_per_branch'] + 1)): # create a LV grid string and attached to station lv_graph_attach_branch() # add remaining branch if val['remaining_loads'] > 0: if 'branch_no' not in locals(): branch_no = 0 # determine maximum current occuring due to peak load of branch I_max_branch = (val['max_loads_per_branch'] * val['single_peak_load']) / (3 ** 0.5 * v_nom) / ( cos_phi_load) # determine suitable cable for this current suitable_cables = lvgd.lv_grid.network.static_data['LV_cables'][ (lvgd.lv_grid.network.static_data['LV_cables'][ 'I_max_th'] * cable_lf) > I_max_branch] cable_type = suitable_cables.loc[ suitable_cables['I_max_th'].idxmin(), : ] branch_no += 1 for load_no in list(range(1, val['remaining_loads'] + 1)): # create a LV grid string and attach to station lv_graph_attach_branch()
[docs]def build_ret_ind_agr_branches(lvgd): """Determine topology of LV grid for retail/industrial and agricultural sector and create representative graph of the grid Parameters ---------- lvgd : LVGridDistrictDing0 Low-voltage grid district object """ # determine topology of grid branches model_params = grid_model_params_ria(lvgd) # attach branches for sectors retail/industrial and agricultural build_lv_graph_ria(lvgd, model_params)
[docs]def select_grid_model_residential(lvgd): """Selects typified model grid based on population Parameters ---------- lvgd : LVGridDistrictDing0 Low-voltage grid district object Returns ------- :pandas:`pandas.DataFrame<dataframe>` Selected string of typified model grid :pandas:`pandas.DataFrame<dataframe>` Parameters of chosen Transformer Note ----- In total 196 distinct LV grid topologies are available that are chosen by population in the LV grid district. Population is translated to number of house branches. Each grid model fits a number of house branches. If this number exceeds 196, still the grid topology of 196 house branches is used. The peak load of the LV grid district is uniformly distributed across house branches. """ # Load properties of LV typified model grids string_properties = lvgd.lv_grid.network.static_data['LV_model_grids_strings'] # Load relational table of apartment count and strings of model grid apartment_string = lvgd.lv_grid.network.static_data[ 'LV_model_grids_strings_per_grid'] # load assumtions apartment_house_branch_ratio = cfg_ding0.get("assumptions", "apartment_house_branch_ratio") population_per_apartment = cfg_ding0.get("assumptions", "population_per_apartment") # calc count of apartments to select string types apartments = round(lvgd.population / population_per_apartment) if apartments > 196: apartments = 196 # select set of strings that represent one type of model grid strings = apartment_string.loc[apartments] selected_strings = [int(s) for s in strings[strings >= 1].index.tolist()] # slice dataframe of string parameters selected_strings_df = string_properties.loc[selected_strings] # add number of occurences of each branch to df occurence_selector = [str(i) for i in selected_strings] selected_strings_df['occurence'] = strings.loc[occurence_selector].tolist() return selected_strings_df
[docs]def build_lv_graph_residential(lvgd, selected_string_df): """Builds nxGraph based on the LV grid model Parameters ---------- lvgd : LVGridDistrictDing0 Low-voltage grid district object selected_string_df: :pandas:`pandas.DataFrame<dataframe>` Table of strings of the selected grid model Note ----- To understand what is happening in this method a few data table columns are explained here * `count house branch`: number of houses connected to a string * `distance house branch`: distance on a string between two house branches * `string length`: total length of a string * `length house branch A|B`: cable from string to connection point of a house A|B in general brings some variation in to the typified model grid and refer to different length of house branches and different cable types respectively different cable widths. """ houses_connected = ( selected_string_df['occurence'] * selected_string_df[ 'count house branch']).sum() average_load = lvgd.peak_load_residential / \ houses_connected average_consumption = lvgd.sector_consumption_residential / \ houses_connected hh_branch = 0 # iterate over each type of branch for i, row in selected_string_df.iterrows(): # get overall count of branches to set unique branch_no branch_count_sum = len(list( lvgd.lv_grid.graph.neighbors(lvgd.lv_grid.station()))) # iterate over it's occurences for branch_no in range(1, int(row['occurence']) + 1): hh_branch += 1 # iterate over house branches for house_branch in range(1, row['count house branch'] + 1): if house_branch % 2 == 0: variant = 'B' else: variant = 'A' # cable distributor to divert from main branch lv_cable_dist = LVCableDistributorDing0( grid=lvgd.lv_grid, string_id=i, branch_no=branch_no + branch_count_sum, load_no=house_branch) # add lv_cable_dist to graph lvgd.lv_grid.add_cable_dist(lv_cable_dist) # cable distributor within building (to connect load+geno) lv_cable_dist_building = LVCableDistributorDing0( grid=lvgd.lv_grid, string_id=i, branch_no=branch_no + branch_count_sum, load_no=house_branch, in_building=True) # add lv_cable_dist_building to graph lvgd.lv_grid.add_cable_dist(lv_cable_dist_building) lv_load = LVLoadDing0(grid=lvgd.lv_grid, string_id=i, branch_no=branch_no + branch_count_sum, load_no=house_branch, peak_load=average_load, consumption={ 'residential': average_consumption}) # add lv_load to graph lvgd.lv_grid.add_load(lv_load) cable_name = row['cable type'] + \ ' 4x1x{}'.format(row['cable width']) cable_type = lvgd.lv_grid.network.static_data[ 'LV_cables'].loc[cable_name] # connect current lv_cable_dist to station if house_branch == 1: # edge connect first house branch in branch with the station lvgd.lv_grid.graph.add_edge( lvgd.lv_grid.station(), lv_cable_dist, branch=BranchDing0( length=row['distance house branch'], kind='cable', grid=lvgd.lv_grid, type=cable_type, id_db='branch_{sector}{branch}_{load}'.format( branch=hh_branch, load=house_branch, sector='HH') )) # connect current lv_cable_dist to last one else: lvgd.lv_grid.graph.add_edge( lvgd.lv_grid._cable_distributors[-4], lv_cable_dist, branch=BranchDing0( length=row['distance house branch'], kind='cable', grid=lvgd.lv_grid, type=lvgd.lv_grid.network.static_data[ 'LV_cables'].loc[cable_name], id_db='branch_{sector}{branch}_{load}'.format( branch=hh_branch, load=house_branch, sector='HH'))) # connect house to cable distributor house_cable_name = row['cable type {}'.format(variant)] + \ ' 4x1x{}'.format( row['cable width {}'.format(variant)]) lvgd.lv_grid.graph.add_edge( lv_cable_dist, lv_cable_dist_building, branch=BranchDing0( length=row['length house branch {}'.format( variant)], kind='cable', grid=lvgd.lv_grid, type=lvgd.lv_grid.network.static_data['LV_cables']. \ loc[house_cable_name], id_db='branch_{sector}{branch}_{load}'.format( branch=hh_branch, load=house_branch, sector='HH')) ) lvgd.lv_grid.graph.add_edge( lv_cable_dist_building, lv_load, branch=BranchDing0( length=1, kind='cable', grid=lvgd.lv_grid, type=lvgd.lv_grid.network.static_data['LV_cables']. \ loc[house_cable_name], id_db='branch_{sector}{branch}_{load}'.format( branch=hh_branch, load=house_branch, sector='HH')) )
[docs]def build_residential_branches(lvgd): """Based on population and identified peak load data, the according grid topology for residential sector is determined and attached to the grid graph Parameters ---------- lvgd : LVGridDistrictDing0 Low-voltage grid district object """ # Choice of typified lv model grid depends on population within lv # grid district. If no population is given, lv grid is omitted and # load is represented by lv station's peak load if lvgd.population > 0 \ and lvgd.peak_load_residential > 0: model_grid = select_grid_model_residential(lvgd) build_lv_graph_residential(lvgd, model_grid) # no residential load but population elif lvgd.population > 0 \ and lvgd.peak_load_residential == 0: logger.warning( '{} has population but no residential load. ' 'No grid is created.'.format( repr(lvgd))) # residential load but no population elif lvgd.population == 0 \ and lvgd.peak_load_residential > 0: logger.warning( '{} has no population but residential load. ' 'No grid is created and thus this load is ' 'missing in overall balance!'.format( repr(lvgd))) else: logger.info( '{} has got no residential load. ' 'No grid is created.'.format( repr(lvgd)))