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
def get_voltage_at_bus_bar(grid, tree): """ Determine voltage level at bus bar of MV-LV substation Parameters ---------- grid : LVGridDing0 Ding0 grid object tree : :networkx:`NetworkX Graph Obj< >` Tree of grid topology: Returns ------- :any:`list` Voltage at bus bar. First item refers to load case, second item refers to voltage in feedin (generation) case """ # voltage at substation bus bar r_mv_grid, x_mv_grid = get_mv_impedance(grid) r_trafo = sum([tr.r for tr in grid._station._transformers]) x_trafo = sum([tr.x for tr in grid._station._transformers]) cos_phi_load = cfg_ding0.get('assumptions', 'cos_phi_load') cos_phi_feedin = cfg_ding0.get('assumptions', 'cos_phi_gen') v_nom = cfg_ding0.get('assumptions', 'lv_nominal_voltage') # loads and generators connected to bus bar bus_bar_load = sum([ node.peak_load for node in tree.successors(grid._station) if isinstance(node, LVLoadDing0) ]) / cos_phi_load bus_bar_generation = sum([ node.capacity for node in tree.successors(grid._station) if isinstance(node, GeneratorDing0) ]) / cos_phi_feedin v_delta_load_case_bus_bar = voltage_delta_vde(v_nom, bus_bar_load, (r_mv_grid + r_trafo), (x_mv_grid + x_trafo), cos_phi_load) v_delta_gen_case_bus_bar = voltage_delta_vde(v_nom, bus_bar_generation, (r_mv_grid + r_trafo), -(x_mv_grid + x_trafo), cos_phi_feedin) return v_delta_load_case_bus_bar, v_delta_gen_case_bus_bar
def get_voltage_delta_branch(grid, tree, node, r_preceeding, x_preceeding): """ Determine voltage for a preceeding branch (edge) of node Parameters ---------- grid : LVGridDing0 Ding0 grid object tree : :networkx:`NetworkX Graph Obj< >` Tree of grid topology node : graph node Node to determine voltage level at r_preceeding : float Resitance of preceeding grid x_preceeding : float Reactance of preceeding grid Return ------ :any:`float` Delta voltage for node """ cos_phi_load = cfg_ding0.get('assumptions', 'cos_phi_load') cos_phi_feedin = cfg_ding0.get('assumptions', 'cos_phi_gen') v_nom = cfg_ding0.get('assumptions', 'lv_nominal_voltage') omega = 2 * math.pi * 50 # add resitance/ reactance to preceeding in_edge = [ _ for _ in grid.graph_branches_from_node(node) if _[0] in tree.predecessors(node) ][0][1] r = r_preceeding + (in_edge['branch'].type['R'] * in_edge['branch'].length) x = x_preceeding + (in_edge['branch'].type['L'] / 1e3 * omega * in_edge['branch'].length) # get apparent power for load and generation case peak_load, gen_capacity = get_house_conn_gen_load(tree, node) s_max_load = peak_load / cos_phi_load s_max_feedin = gen_capacity / cos_phi_feedin # determine voltage increase/ drop a node voltage_delta_load = voltage_delta_vde(v_nom, s_max_load, r, x, cos_phi_load) voltage_delta_gen = voltage_delta_vde(v_nom, s_max_feedin, r, -x, cos_phi_feedin) return [voltage_delta_load, voltage_delta_gen, r, x]
def get_mv_impedance_at_voltage_level(grid, voltage_level): """ Determine MV grid impedance (resistance and reactance separately) Parameters ---------- grid : :class:`~.ding0.core.network.grids.LVGridDing0` voltage_level: float voltage level to which impedance is rescaled (normally 0.4 kV for LV) Returns ------- :obj:`list` List containing resistance and reactance of MV grid """ freq = cfg_ding0.get('assumptions', 'frequency') omega = 2 * math.pi * freq mv_grid = grid.grid_district.lv_load_area.mv_grid_district.mv_grid edges = mv_grid.find_path(grid._station, mv_grid._station, type='edges') r_mv_grid = sum([ e[2]['branch'].type['R_per_km'] * e[2]['branch'].length / 1e3 for e in edges ]) x_mv_grid = sum([ e[2]['branch'].type['L_per_km'] / 1e3 * omega * e[2]['branch'].length / 1e3 for e in edges ]) # rescale to voltage level r_mv_grid_vl = r_mv_grid * (voltage_level / mv_grid.v_level)**2 x_mv_grid_vl = x_mv_grid * (voltage_level / mv_grid.v_level)**2 return [r_mv_grid_vl, x_mv_grid_vl]
def __init__(self, **kwargs): output_animation_file_prefix = cfg_ding0.get('output', 'animation_file_prefix') self.file_path = os.path.join(get_default_home_dir(), 'animation/') self.file_prefix = output_animation_file_prefix self.counter = 1
def calc_geo_dist_vincenty(node_source, node_target): """ Calculates the geodesic distance between `node_source` and `node_target` incorporating the detour factor specified in :file:`ding0/ding0/config/config_calc.cfg`. Parameters ---------- node_source: LVStationDing0, GeneratorDing0, or CableDistributorDing0 source node, member of GridDing0._graph node_target: LVStationDing0, GeneratorDing0, or CableDistributorDing0 target node, member of GridDing0._graph Returns ------- :any:`float` Distance in m """ branch_detour_factor = cfg_ding0.get('assumptions', 'branch_detour_factor') # notice: vincenty takes (lat,lon) branch_length = branch_detour_factor * vincenty( (node_source.geo_data.y, node_source.geo_data.x), (node_target.geo_data.y, node_target.geo_data.x)).m # ========= BUG: LINE LENGTH=0 WHEN CONNECTING GENERATORS =========== # When importing generators, the geom_new field is used as position. If it is empty, EnergyMap's geom # is used and so there are a couple of generators at the same position => length of interconnecting # line is 0. See issue #76 if branch_length == 0: branch_length = 1 logger.warning('Geo distance is zero, check objects\' positions. ' 'Distance is set to 1m') # =================================================================== return branch_length
def solve(self, graph, savings_solution, timeout, debug=False, anim=None): """Improve initial savings solution using local search Parameters ---------- graph: :networkx:`NetworkX Graph Obj< >` Graph instance savings_solution: SavingsSolution initial solution of CVRP problem (instance of `SavingsSolution` class) timeout: :obj:`int` max processing time in seconds debug: bool, defaults to False If True, information is printed while routing anim: AnimationDing0 AnimationDing0 object Returns ------- LocalSearchSolution A solution (LocalSearchSolution class) """ # TODO: If necessary, use timeout to set max processing time of local search # load threshold for operator (see exchange or relocate operator's description for more information) op_diff_round_digits = int( cfg_ding0.get('mv_routing', 'operator_diff_round_digits')) solution = LocalSearchSolution(graph, savings_solution) # FOR BENCHMARKING OF OPERATOR'S ORDER: #self.benchmark_operator_order(graph, savings_solution, op_diff_round_digits) for run in range(10): start = time.time() solution = self.operator_exchange(graph, solution, op_diff_round_digits, anim) time1 = time.time() if debug: logger.debug('Elapsed time (exchange, run {1}): {0}, ' 'Solution\'s length: {2}'.format( time1 - start, str(run), solution.length())) solution = self.operator_relocate(graph, solution, op_diff_round_digits, anim) time2 = time.time() if debug: logger.debug('Elapsed time (relocate, run {1}): {0}, ' 'Solution\'s length: {2}'.format( time2 - time1, str(run), solution.length())) solution = self.operator_oropt(graph, solution, op_diff_round_digits, anim) time3 = time.time() if debug: logger.debug('Elapsed time (oropt, run {1}): {0}, ' 'Solution\'s length: {2}'.format( time3 - time2, str(run), solution.length())) return solution
def can_add_lv_load_area(self, node): # TODO: check docstring """Sums up peak load of LV stations That is, total peak load for satellite string Parameters ---------- node: :class:`~.ding0.core.GridDing0` Descr Returns ------- :obj: `bool` True if ???? """ # get power factor for loads cos_phi_load = cfg_ding0.get('assumptions', 'cos_phi_load') lv_load_area = node.lv_load_area if lv_load_area not in self.lv_load_areas( ): # and isinstance(lv_load_area, LVLoadAreaDing0): path_length_to_root = lv_load_area.mv_grid_district.mv_grid.graph_path_length( self.root_node, node) if ((path_length_to_root <= self.branch_length_max) and (lv_load_area.peak_load + self.peak_load) / cos_phi_load <= self.peak_load_max): return True else: return False
def extend_substation(grid, critical_stations, grid_level): """ Reinforce MV or LV substation by exchanging the existing trafo and installing a parallel one if necessary. First, all available transformers in a `critical_stations` are extended to maximum power. If this does not solve all present issues, additional transformers are build. Parameters ---------- grid: GridDing0 Ding0 grid container critical_stations : :any:`list` List of stations with overloading grid_level : str Either "LV" or "MV". Basis to select right equipment. Notes ----- Curently straight forward implemented for LV stations Returns ------- type #TODO: Description of return. Change type in the previous line accordingly """ 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') trafo_params = grid.network._static_data['{grid_level}_trafos'.format( grid_level=grid_level)] trafo_s_max_max = max(trafo_params['S_nom']) for station in critical_stations: # determine if load or generation case and apply load factor if station['s_max'][0] > station['s_max'][1]: case = 'load' lf_lv_trans_normal = load_factor_lv_trans_lc_normal else: case = 'gen' lf_lv_trans_normal = load_factor_lv_trans_fc_normal
def __init__(self, **kwargs): output_animation_file_prefix = cfg_ding0.get('output', 'animation_file_prefix') package_path = ding0.__path__[0] self.file_path = os.path.join(package_path, 'output/animation/') self.file_prefix = output_animation_file_prefix self.counter = 1
def set_operation_voltage_level(self): """Set operation voltage level """ mv_station_v_level_operation = float(cfg_ding0.get('mv_routing_tech_constraints', 'mv_station_v_level_operation')) self.v_level_operation = mv_station_v_level_operation * self.grid.v_level
def __init__(self, **kwargs): self.id_db = kwargs.get('id_db', None) self.mv_grid_district = kwargs.get('mv_grid_district', None) self._lv_load_areas = [] self.peak_load = 0 self.branch_length_sum = 0 # threshold: max. allowed peak load of satellite string self.peak_load_max = cfg_ding0.get( 'mv_connect', 'load_area_sat_string_load_threshold') self.branch_length_max = cfg_ding0.get( 'mv_connect', 'load_area_sat_string_length_threshold') self.root_node = kwargs.get( 'root_node', None ) # root node (Ding0 object) = start of string on MV main route # TODO: Value is read from file every time a LV load_area is created -> move to associated NetworkDing0 class? # get id from count of load area groups in associated MV grid district self.id_db = self.mv_grid_district.lv_load_area_groups_count() + 1
def reinforce_lv_branches_overloading(grid, crit_branches): """ Choose appropriate cable type for branches with line overloading Parameters ---------- grid : LVGridDing0 Ding0 LV grid object crit_branches : :any:`list` List of critical branches incl. its line loading Notes ----- If maximum size cable is not capable to resolve issue due to line overloading largest available cable type is assigned to branch. Returns ------- :any:`list` unsolved_branches : List of braches no suitable cable could be found """ unsolved_branches = [] cable_lf = cfg_ding0.get('assumptions', 'load_factor_lv_cable_lc_normal') cables = grid.network.static_data['LV_cables'] # resolve overloading issues for each branch segment for branch in crit_branches: I_max_branch_load = branch['s_max'][0] I_max_branch_gen = branch['s_max'][1] I_max_branch = max([I_max_branch_load, I_max_branch_gen]) suitable_cables = cables[(cables['I_max_th'] * cable_lf) > I_max_branch] if not suitable_cables.empty: cable_type = suitable_cables.ix[suitable_cables['I_max_th'].idxmin()] branch['branch'].type = cable_type crit_branches.remove(branch) else: cable_type_max = cables.ix[cables['I_max_th'].idxmax()] unsolved_branches.append(branch) branch['branch'].type = cable_type_max logger.error("No suitable cable type could be found for {branch} " "with I_th_max = {current}. " "Cable of type {cable} is chosen during " "reinforcement.".format( branch=branch['branch'], cable=cable_type_max.name, current=I_max_branch )) return unsolved_branches
def get_voltage_delta_branch(tree, node, r, x): """ Determine voltage for a branch with impedance r + jx Parameters ---------- tree : :networkx:`NetworkX Graph Obj< >` Tree of grid topology node : graph node Node to determine voltage level at r : float Resistance of preceeding branch x : float Reactance of preceeding branch Return ------ :any:`float` Delta voltage for branch """ cos_phi_load = cfg_ding0.get('assumptions', 'cos_phi_load') cos_phi_feedin = cfg_ding0.get('assumptions', 'cos_phi_gen') cos_phi_load_mode = cfg_ding0.get('assumptions', 'cos_phi_load_mode') cos_phi_feedin_mode = cfg_ding0.get( 'assumptions', 'cos_phi_gen_mode' ) #ToDo: Check if this is true. Why would generator run in a way that aggravates voltage issues? v_nom = cfg_ding0.get('assumptions', 'lv_nominal_voltage') # get apparent power for load and generation case peak_load, gen_capacity = get_cumulated_conn_gen_load(tree, node) s_max_load = peak_load / cos_phi_load s_max_feedin = gen_capacity / cos_phi_feedin # determine voltage increase/ drop a node x_sign_load = q_sign(cos_phi_load_mode, 'load') voltage_delta_load = voltage_delta_vde(v_nom, s_max_load, r, x_sign_load * x, cos_phi_load) x_sign_gen = q_sign(cos_phi_feedin_mode, 'load') voltage_delta_gen = voltage_delta_vde(v_nom, s_max_feedin, r, x_sign_gen * x, cos_phi_feedin) return [voltage_delta_load, voltage_delta_gen]
def get_default_home_dir(): """ Return default home directory of Ding0 Returns ------- :any:`str` Default home directory including its path """ ding0_dir = str(cfg_ding0.get('config', 'config_dir')) return os.path.join(os.path.expanduser('~'), ding0_dir)
def calc_geo_dist_matrix_vincenty(nodes_pos): """ Calculates the geodesic distance between all nodes in `nodes_pos` incorporating the detour factor in config_calc.cfg. For every two points/coord it uses geopy's vincenty function (formula devised by Thaddeus Vincenty, with an accurate ellipsoidal model of the earth). As default ellipsoidal model of the earth WGS-84 is used. For more options see https://geopy.readthedocs.org/en/1.10.0/index.html?highlight=vincenty#geopy.distance.vincenty Parameters ---------- nodes_pos: dict dictionary of nodes with positions, with x=longitude, y=latitude, and the following format:: { 'node_1': (x_1, y_1), ..., 'node_n': (x_n, y_n) } Returns ------- :obj:`dict` dictionary with distances between all nodes (in km), with the following format:: { 'node_1': {'node_1': dist_11, ..., 'node_n': dist_1n}, ..., 'node_n': {'node_1': dist_n1, ..., 'node_n': dist_nn} } """ branch_detour_factor = cfg_ding0.get('assumptions', 'branch_detour_factor') matrix = {} for i in nodes_pos: pos_origin = tuple(nodes_pos[i]) matrix[i] = {} for j in nodes_pos: pos_dest = tuple(nodes_pos[j]) # notice: vincenty takes (lat,lon), thus the (x,y)/(lon,lat) tuple is reversed distance = branch_detour_factor * vincenty( tuple(reversed(pos_origin)), tuple(reversed(pos_dest))).km matrix[i][j] = distance return matrix
def __init__(self, **kwargs): # inherit branch parameters from Region super().__init__(**kwargs) # more params self._lv_grid_districts = [] self.ring = kwargs.get('ring', None) self.mv_grid_district = kwargs.get('mv_grid_district', None) self.lv_load_area_centre = kwargs.get('lv_load_area_centre', None) self.lv_load_area_group = kwargs.get('lv_load_area_group', None) self.is_satellite = kwargs.get('is_satellite', False) self.is_aggregated = kwargs.get('is_aggregated', False) # threshold: load area peak load, if peak load < threshold => treat load area as satellite load_area_sat_load_threshold = cfg_ding0.get( 'mv_connect', 'load_area_sat_load_threshold') # TODO: Value is read from file every time a LV load_area is created -> move to associated NetworkDing0 class? db_data = kwargs.get('db_data', None) # dangerous: attributes are created for any passed argument in `db_data` # load values into attributes if db_data is not None: for attribute in list(db_data.keys()): setattr(self, attribute, db_data[attribute]) # convert geo attributes to to shapely objects if hasattr(self, 'geo_area'): self.geo_area = wkt_loads(self.geo_area) if hasattr(self, 'geo_centre'): self.geo_centre = wkt_loads(self.geo_centre) # convert load values (rounded floats) to int if hasattr(self, 'peak_load_residential'): self.peak_load_residential = self.peak_load_residential if hasattr(self, 'peak_load_retail'): self.peak_load_retail = self.peak_load_retail if hasattr(self, 'peak_load_industrial'): self.peak_load_industrial = self.peak_load_industrial if hasattr(self, 'peak_load_agricultural'): self.peak_load_agricultural = self.peak_load_agricultural if hasattr(self, 'peak_load'): self.peak_load = self.peak_load # if load area has got a peak load less than load_area_sat_threshold, it's a satellite if self.peak_load < load_area_sat_load_threshold: self.is_satellite = True
def get_delta_voltage_preceding_line(grid, tree, node): """ Parameters ---------- grid : :class:`~.ding0.core.network.grids.LVGridDing0` Ding0 grid object tree: :networkx:`NetworkX Graph Obj< >` Tree of grid topology node: graph node Node at end of line Return ------ :any:`float` Voltage drop over preceding line of node """ # get impedance of preceding line freq = cfg_ding0.get('assumptions', 'frequency') omega = 2 * math.pi * freq # choose preceding branch branch = [ _ for _ in grid.graph_branches_from_node(node) if _[0] in list(tree.predecessors(node)) ][0][1] # calculate impedance of preceding branch r_line = (branch['branch'].type['R_per_km'] * branch['branch'].length / 1e3) x_line = (branch['branch'].type['L_per_km'] / 1e3 * omega * branch['branch'].length / 1e3) # get voltage drop over preceeding line voltage_delta_load, voltage_delta_gen = \ get_voltage_delta_branch(tree, node, r_line, x_line) return voltage_delta_load, voltage_delta_gen
def transformer(grid): """ Choose transformer and add to grid's station Parameters ---------- grid: LVGridDing0 LV grid data """ 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=id, 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)
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
def set_default_branch_type(self, debug=False): """ Determines default branch type according to grid district's peak load and standard equipment. Args ---- debug: bool, defaults to False If True, information is printed during process Returns ------- :pandas:`pandas.Series<series>` default branch type: pandas Series object. If no appropriate type is found, return largest possible one. :pandas:`pandas.Series<series>` default branch type max: pandas Series object. Largest available line/cable type Note ----- Parameter values for cables and lines are taken from [#]_, [#]_ and [#]_. Lines are chosen to have 60 % load relative to their nominal capacity according to [#]_. Decision on usage of overhead lines vs. cables is determined by load density of the considered region. Urban areas usually are equipped with underground cables whereas rural areas often have overhead lines as MV distribution system [#]_. References ---------- .. [#] Klaus Heuck et al., "Elektrische Energieversorgung", Vieweg+Teubner, Wiesbaden, 2007 .. [#] René Flosdorff et al., "Elektrische Energieverteilung", Vieweg+Teubner, 2005 .. [#] Südkabel GmbH, "Einadrige VPE-isolierte Mittelspannungskabel", http://www.suedkabel.de/cms/upload/pdf/Garnituren/Einadrige_VPE-isolierte_Mittelspannungskabel.pdf, 2017 .. [#] Deutsche Energie-Agentur GmbH (dena), "dena-Verteilnetzstudie. Ausbau- und Innovationsbedarf der Stromverteilnetze in Deutschland bis 2030.", 2012 .. [#] Tao, X., "Automatisierte Grundsatzplanung von Mittelspannungsnetzen", Dissertation, RWTH Aachen, 2007 """ # decide whether cable or line is used (initially for entire grid) and set grid's attribute if self.v_level == 20: self.default_branch_kind = 'line' elif self.v_level == 10: self.default_branch_kind = 'cable' # get power factor for loads cos_phi_load = cfg_ding0.get('assumptions', 'cos_phi_load') # get max. count of half rings per MV grid district mv_half_ring_count_max = int( cfg_ding0.get('mv_routing_tech_constraints', 'mv_half_ring_count_max')) #mv_half_ring_count_max=20 # load cable/line assumptions, file_names and parameter if self.default_branch_kind == 'line': load_factor_normal = float( cfg_ding0.get('assumptions', 'load_factor_mv_line_lc_normal')) branch_parameters = self.network.static_data['MV_overhead_lines'] # load cables as well to use it within settlements branch_parameters_settle = self.network.static_data['MV_cables'] # select types with appropriate voltage level branch_parameters_settle = branch_parameters_settle[ branch_parameters_settle['U_n'] == self.v_level] elif self.default_branch_kind == 'cable': load_factor_normal = float( cfg_ding0.get('assumptions', 'load_factor_mv_cable_lc_normal')) branch_parameters = self.network.static_data['MV_cables'] else: raise ValueError( 'Grid\'s default_branch_kind is invalid, could not set branch parameters.' ) # select appropriate branch params according to voltage level, sorted ascending by max. current # use <240mm2 only (ca. 420A) for initial rings and for disambiguation of agg. LA branch_parameters = branch_parameters[branch_parameters['U_n'] == self.v_level] branch_parameters = branch_parameters[ branch_parameters['reinforce_only'] == 0].sort_values('I_max_th') # get largest line/cable type branch_type_max = branch_parameters.loc[ branch_parameters['I_max_th'].idxmax()] # set aggregation flag using largest available line/cable self.set_nodes_aggregation_flag(branch_type_max['I_max_th'] * load_factor_normal) # calc peak current sum (= "virtual" current) of whole grid (I = S / sqrt(3) / U) excluding load areas of type # satellite and aggregated peak_current_sum = ( (self.grid_district.peak_load - self.grid_district.peak_load_satellites - self.grid_district.peak_load_aggregated) / cos_phi_load / (3**0.5) / self.v_level) # units: kVA / kV = A branch_type_settle = branch_type_settle_max = None # search the smallest possible line/cable for MV grid district in equipment datasets for all load areas # excluding those of type satellite and aggregated for idx, row in branch_parameters.iterrows(): # calc number of required rings using peak current sum of grid district, # load factor and max. current of line/cable half_ring_count = round(peak_current_sum / (row['I_max_th'] * load_factor_normal)) if debug: logger.debug( '=== Selection of default branch type in {} ==='.format( self)) logger.debug('Peak load= {} kVA'.format( self.grid_district.peak_load)) logger.debug('Peak current={}'.format(peak_current_sum)) logger.debug('I_max_th={}'.format(row['I_max_th'])) logger.debug('Half ring count={}'.format(half_ring_count)) # if count of half rings is below or equal max. allowed count, use current branch type as default if half_ring_count <= mv_half_ring_count_max: if self.default_branch_kind == 'line': # take only cables that can handle at least the current of the line branch_parameters_settle_filter = branch_parameters_settle[\ branch_parameters_settle['I_max_th'] - row['I_max_th'] > 0] # get cable type with similar (but greater) I_max_th # note: only grids with lines as default branch kind get cables in settlements # (not required in grids with cables as default branch kind) branch_type_settle = branch_parameters_settle_filter.loc[\ branch_parameters_settle_filter['I_max_th'].idxmin()] return row, branch_type_max, branch_type_settle # no equipment was found, return largest available line/cable if debug: logger.debug( 'No appropriate line/cable type could be found for ' '{}, declare some load areas as aggregated.'.format(self)) if self.default_branch_kind == 'line': branch_type_settle_max = branch_parameters_settle.loc[ branch_parameters_settle['I_max_th'].idxmax()] return branch_type_max, branch_type_max, branch_type_settle_max
def set_voltage_level(self, mode='distance'): """ Sets voltage level of MV grid according to load density of MV Grid District or max. distance between station and Load Area. Parameters ---------- mode: :obj:`str` method to determine voltage level * 'load_density': Decision on voltage level is determined by load density of the considered region. Urban areas (load density of >= 1 MW/km2 according to [#]_) usually got a voltage of 10 kV whereas rural areas mostly use 20 kV. * 'distance' (default): Decision on voltage level is determined by the max. distance between Grid District's HV-MV station and Load Areas (LA's centre is used). According to [#]_ a value of 1kV/kV can be assumed. The `voltage_per_km_threshold` defines the distance threshold for distinction. (default in config = (20km+10km)/2 = 15km) References ---------- .. [#] Falk Schaller et al., "Modellierung realitätsnaher zukünftiger Referenznetze im Verteilnetzsektor zur Überprüfung der Elektroenergiequalität", Internationaler ETG-Kongress Würzburg, 2011 .. [#] Klaus Heuck et al., "Elektrische Energieversorgung", Vieweg+Teubner, Wiesbaden, 2007 """ if mode == 'load_density': # get power factor for loads cos_phi_load = cfg_ding0.get('assumptions', 'cos_phi_load') # get load density load_density_threshold = float( cfg_ding0.get('assumptions', 'load_density_threshold')) # transform MVGD's area to epsg 3035 # to achieve correct area calculation projection = partial( pyproj.transform, pyproj.Proj(init='epsg:4326'), # source coordinate system pyproj.Proj(init='epsg:3035')) # destination coordinate system # calculate load density kw2mw = 1e-3 sqm2sqkm = 1e6 load_density = ( (self.grid_district.peak_load * kw2mw / cos_phi_load) / (transform(projection, self.grid_district.geo_data).area / sqm2sqkm)) # unit MVA/km^2 # identify voltage level if load_density < load_density_threshold: self.v_level = 20 elif load_density >= load_density_threshold: self.v_level = 10 else: raise ValueError('load_density is invalid!') elif mode == 'distance': # get threshold for 20/10kV disambiguation voltage_per_km_threshold = float( cfg_ding0.get('assumptions', 'voltage_per_km_threshold')) # initial distance dist_max = 0 import time start = time.time() for node in self.graph_nodes_sorted(): if isinstance(node, LVLoadAreaCentreDing0): # calc distance from MV-LV station to LA centre dist_node = calc_geo_dist_vincenty(self.station(), node) / 1e3 if dist_node > dist_max: dist_max = dist_node # max. occurring distance to a Load Area exceeds threshold => grid operates at 20kV if dist_max >= voltage_per_km_threshold: self.v_level = 20 # not: grid operates at 10kV else: self.v_level = 10 else: raise ValueError('parameter \'mode\' is invalid!')
def tech_constraints_satisfied(self): """ Check route validity according to technical constraints (voltage and current rating) It considers constraints as * current rating of cable/line * voltage stability at all nodes Notes ----- The validation is done for every tested MV grid configuration during CVRP algorithm. The current rating is checked using load factors from [#]_. Due to the high amount of steps the voltage rating cannot be checked using load flow calculation. Therefore we use a simple method which determines the voltage change between two consecutive nodes according to [#]_. Furthermore it is checked if new route has got more nodes than allowed (typ. 2*10 according to [#]_). References ---------- .. [#] Deutsche Energie-Agentur GmbH (dena), "dena-Verteilnetzstudie. Ausbau- und Innovationsbedarf der Stromverteilnetze in Deutschland bis 2030.", 2012 .. [#] M. Sakulin, W. Hipp, "Netzaspekte von dezentralen Erzeugungseinheiten, Studie im Auftrag der E-Control GmbH", TU Graz, 2004 .. [#] Klaus Heuck et al., "Elektrische Energieversorgung", Vieweg+Teubner, Wiesbaden, 2007 .. [#] FGH e.V.: "Technischer Bericht 302: Ein Werkzeug zur Optimierung der Störungsbeseitigung für Planung und Betrieb von Mittelspannungsnetzen", Tech. rep., 2008 """ # load parameters load_area_count_per_ring = float( cfg_ding0.get('mv_routing', 'load_area_count_per_ring')) max_half_ring_length = float( cfg_ding0.get('mv_routing', 'max_half_ring_length')) if self._problem._branch_kind == 'line': load_factor_normal = float( cfg_ding0.get('assumptions', 'load_factor_mv_line_lc_normal')) load_factor_malfunc = float( cfg_ding0.get('assumptions', 'load_factor_mv_line_lc_malfunc')) elif self._problem._branch_kind == 'cable': load_factor_normal = float( cfg_ding0.get('assumptions', 'load_factor_mv_cable_lc_normal')) load_factor_malfunc = float( cfg_ding0.get('assumptions', 'load_factor_mv_cable_lc_malfunc')) else: raise ValueError( 'Grid\'s _branch_kind is invalid, could not use branch parameters.' ) mv_max_v_level_lc_diff_normal = float( cfg_ding0.get('mv_routing_tech_constraints', 'mv_max_v_level_lc_diff_normal')) mv_max_v_level_lc_diff_malfunc = float( cfg_ding0.get('mv_routing_tech_constraints', 'mv_max_v_level_lc_diff_malfunc')) cos_phi_load = cfg_ding0.get('assumptions', 'cos_phi_load') # step 0: check if route has got more nodes than allowed if len(self._nodes) > load_area_count_per_ring: return False # step 1: calc circuit breaker position position = self.calc_circuit_breaker_position() # step 2: calc required values for checking current & voltage # get nodes of half-rings nodes_hring1 = [self._problem._depot] + self._nodes[0:position] nodes_hring2 = list( reversed(self._nodes[position:len(self._nodes)] + [self._problem._depot])) # get all nodes of full ring for both directions nodes_ring1 = [self._problem._depot] + self._nodes nodes_ring2 = list(reversed(self._nodes + [self._problem._depot])) # factor to calc reactive from active power Q_factor = tan(acos(cos_phi_load)) # line/cable params per km r = self._problem._branch_type['R'] # unit for r: ohm/km x = self._problem._branch_type[ 'L'] * 2 * pi * 50 / 1e3 # unit for x: ohm/km # step 3: check if total lengths of half-rings exceed max. allowed distance if (self.length_from_nodelist(nodes_hring1) > max_half_ring_length or self.length_from_nodelist(nodes_hring2) > max_half_ring_length): return False # step 4a: check if current rating of default cable/line is violated # (for every of the 2 half-rings using load factor for normal operation) demand_hring_1 = sum( [node.demand() for node in self._nodes[0:position]]) demand_hring_2 = sum( [node.demand() for node in self._nodes[position:len(self._nodes)]]) peak_current_sum_hring1 = demand_hring_1 / ( 3**0.5) / self._problem._v_level # units: kVA / kV = A peak_current_sum_hring2 = demand_hring_2 / ( 3**0.5) / self._problem._v_level # units: kVA / kV = A if (peak_current_sum_hring1 > (self._problem._branch_type['I_max_th'] * load_factor_normal) or peak_current_sum_hring2 > (self._problem._branch_type['I_max_th'] * load_factor_normal)): return False # step 4b: check if current rating of default cable/line is violated # (for full ring using load factor for malfunction operation) peak_current_sum_ring = self._demand / ( 3**0.5) / self._problem._v_level # units: kVA / kV = A if peak_current_sum_ring > (self._problem._branch_type['I_max_th'] * load_factor_malfunc): return False # step 5a: check voltage stability at all nodes # (for every of the 2 half-rings using max. voltage difference for normal operation) # get operation voltage level from station v_level_hring1 =\ v_level_hring2 =\ v_level_ring_dir1 =\ v_level_ring_dir2 =\ v_level_op =\ self._problem._v_level * 1e3 # set initial r and x r_hring1 =\ r_hring2 =\ x_hring1 =\ x_hring2 =\ r_ring_dir1 =\ r_ring_dir2 =\ x_ring_dir1 =\ x_ring_dir2 = 0 for n1, n2 in zip(nodes_hring1[0:len(nodes_hring1) - 1], nodes_hring1[1:len(nodes_hring1)]): r_hring1 += self._problem.distance(n1, n2) * r x_hring1 += self._problem.distance(n1, n2) * x v_level_hring1 -= n2.demand() * 1e3 * ( r_hring1 + x_hring1 * Q_factor) / v_level_op if (v_level_op - v_level_hring1) > (v_level_op * mv_max_v_level_lc_diff_normal): return False for n1, n2 in zip(nodes_hring2[0:len(nodes_hring2) - 1], nodes_hring2[1:len(nodes_hring2)]): r_hring2 += self._problem.distance(n1, n2) * r x_hring2 += self._problem.distance(n1, n2) * x v_level_hring2 -= n2.demand() * 1e3 * ( r_hring2 + x_hring2 * Q_factor) / v_level_op if (v_level_op - v_level_hring2) > (v_level_op * mv_max_v_level_lc_diff_normal): return False # step 5b: check voltage stability at all nodes # (for full ring calculating both directions simultaneously using max. voltage diff. for malfunction operation) for (n1, n2), (n3, n4) in zip( zip(nodes_ring1[0:len(nodes_ring1) - 1], nodes_ring1[1:len(nodes_ring1)]), zip(nodes_ring2[0:len(nodes_ring2) - 1], nodes_ring2[1:len(nodes_ring2)])): r_ring_dir1 += self._problem.distance(n1, n2) * r r_ring_dir2 += self._problem.distance(n3, n4) * r x_ring_dir1 += self._problem.distance(n1, n2) * x x_ring_dir2 += self._problem.distance(n3, n4) * x v_level_ring_dir1 -= (n2.demand() * 1e3 * (r_ring_dir1 + x_ring_dir1 * Q_factor) / v_level_op) v_level_ring_dir2 -= (n4.demand() * 1e3 * (r_ring_dir2 + x_ring_dir2 * Q_factor) / v_level_op) if ((v_level_op - v_level_ring_dir1) > (v_level_op * mv_max_v_level_lc_diff_malfunc) or (v_level_op - v_level_ring_dir2) > (v_level_op * mv_max_v_level_lc_diff_malfunc)): return False return True
def nodes_to_dict_of_dataframes(grid, nodes, lv_transformer=True): """ Creates dictionary of dataframes containing grid Parameters ---------- grid: ding0.Network nodes: list of ding0 grid components objects Nodes of the grid graph lv_transformer: bool, True Toggle transformer representation in power flow analysis Returns: components: dict of pandas.DataFrame DataFrames contain components attributes. Dict is keyed by components type components_data: dict of pandas.DataFrame DataFrame containing components time-varying data """ generator_instances = [MVStationDing0, GeneratorDing0] # TODO: MVStationDing0 has a slack generator cos_phi_load = cfg_ding0.get('assumptions', 'cos_phi_load') cos_phi_feedin = cfg_ding0.get('assumptions', 'cos_phi_gen') srid = int(cfg_ding0.get('geo', 'srid')) load_in_generation_case = cfg_ding0.get('assumptions', 'load_in_generation_case') generation_in_load_case = cfg_ding0.get('assumptions', 'generation_in_load_case') Q_factor_load = tan(acos(cos_phi_load)) Q_factor_generation = tan(acos(cos_phi_feedin)) voltage_set_slack = cfg_ding0.get("mv_routing_tech_constraints", "mv_station_v_level_operation") kw2mw = 1e-3 # define dictionaries buses = {'bus_id': [], 'v_nom': [], 'geom': [], 'grid_id': []} bus_v_mag_set = { 'bus_id': [], 'temp_id': [], 'v_mag_pu_set': [], 'grid_id': [] } generator = { 'generator_id': [], 'bus': [], 'control': [], 'grid_id': [], 'p_nom': [] } generator_pq_set = { 'generator_id': [], 'temp_id': [], 'p_set': [], 'grid_id': [], 'q_set': [] } load = {'load_id': [], 'bus': [], 'grid_id': []} load_pq_set = { 'load_id': [], 'temp_id': [], 'p_set': [], 'grid_id': [], 'q_set': [] } # # TODO: consider other implications of `lv_transformer is True` # if lv_transformer is True: # bus_instances.append(Transformer) # # TODO: only for debugging, remove afterwards # import csv # nodeslist = sorted([node.__repr__() for node in nodes # if node not in grid.graph_isolated_nodes()]) # with open('/home/guido/ding0_debug/nodes_via_dataframe.csv', 'w', newline='') as csvfile: # writer = csv.writer(csvfile, delimiter='\n') # writer.writerow(nodeslist) for node in nodes: if node not in grid.graph_isolated_nodes(): # buses only if isinstance(node, MVCableDistributorDing0): buses['bus_id'].append(node.pypsa_id) buses['v_nom'].append(grid.v_level) buses['geom'].append(from_shape(node.geo_data, srid=srid)) buses['grid_id'].append(grid.id_db) bus_v_mag_set['bus_id'].append(node.pypsa_id) bus_v_mag_set['temp_id'].append(1) bus_v_mag_set['v_mag_pu_set'].append([1, 1]) bus_v_mag_set['grid_id'].append(grid.id_db) # bus + generator elif isinstance(node, tuple(generator_instances)): # slack generator if isinstance(node, MVStationDing0): logger.info('Only MV side bus of MVStation will be added.') generator['generator_id'].append('_'.join( ['MV', str(grid.id_db), 'slack'])) generator['control'].append('Slack') generator['p_nom'].append(0) bus_v_mag_set['v_mag_pu_set'].append( [voltage_set_slack, voltage_set_slack]) # other generators if isinstance(node, GeneratorDing0): generator['generator_id'].append('_'.join( ['MV', str(grid.id_db), 'gen', str(node.id_db)])) generator['control'].append('PQ') generator['p_nom'].append(node.capacity * node.capacity_factor) generator_pq_set['generator_id'].append('_'.join( ['MV', str(grid.id_db), 'gen', str(node.id_db)])) generator_pq_set['temp_id'].append(1) generator_pq_set['p_set'].append([ node.capacity * node.capacity_factor * kw2mw * generation_in_load_case, node.capacity * node.capacity_factor * kw2mw ]) generator_pq_set['q_set'].append([ node.capacity * node.capacity_factor * kw2mw * Q_factor_generation * generation_in_load_case, node.capacity * node.capacity_factor * kw2mw * Q_factor_generation ]) generator_pq_set['grid_id'].append(grid.id_db) bus_v_mag_set['v_mag_pu_set'].append([1, 1]) buses['bus_id'].append(node.pypsa_id) buses['v_nom'].append(grid.v_level) buses['geom'].append(from_shape(node.geo_data, srid=srid)) buses['grid_id'].append(grid.id_db) bus_v_mag_set['bus_id'].append(node.pypsa_id) bus_v_mag_set['temp_id'].append(1) bus_v_mag_set['grid_id'].append(grid.id_db) generator['grid_id'].append(grid.id_db) generator['bus'].append(node.pypsa_id) # aggregated load at hv/mv substation elif isinstance(node, LVLoadAreaCentreDing0): load['load_id'].append(node.pypsa_id) load['bus'].append('_'.join(['HV', str(grid.id_db), 'trd'])) load['grid_id'].append(grid.id_db) load_pq_set['load_id'].append(node.pypsa_id) load_pq_set['temp_id'].append(1) load_pq_set['p_set'].append([ node.lv_load_area.peak_load * kw2mw, node.lv_load_area.peak_load * kw2mw * load_in_generation_case ]) load_pq_set['q_set'].append([ node.lv_load_area.peak_load * kw2mw * Q_factor_load, node.lv_load_area.peak_load * kw2mw * Q_factor_load * load_in_generation_case ]) load_pq_set['grid_id'].append(grid.id_db) # generator representing generation capacity of aggregate LA # analogously to load, generation is connected directly to # HV-MV substation generator['generator_id'].append('_'.join( ['MV', str(grid.id_db), 'lcg', str(node.id_db)])) generator['control'].append('PQ') generator['p_nom'].append(node.lv_load_area.peak_generation) generator['grid_id'].append(grid.id_db) generator['bus'].append('_'.join( ['HV', str(grid.id_db), 'trd'])) generator_pq_set['generator_id'].append('_'.join( ['MV', str(grid.id_db), 'lcg', str(node.id_db)])) generator_pq_set['temp_id'].append(1) generator_pq_set['p_set'].append([ node.lv_load_area.peak_generation * kw2mw * generation_in_load_case, node.lv_load_area.peak_generation * kw2mw ]) generator_pq_set['q_set'].append([ node.lv_load_area.peak_generation * kw2mw * Q_factor_generation * generation_in_load_case, node.lv_load_area.peak_generation * kw2mw * Q_factor_generation ]) generator_pq_set['grid_id'].append(grid.id_db) # bus + aggregate load of lv grids (at mv/ls substation) elif isinstance(node, LVStationDing0): # Aggregated load representing load in LV grid load['load_id'].append('_'.join( ['MV', str(grid.id_db), 'loa', str(node.id_db)])) load['bus'].append(node.pypsa_id) load['grid_id'].append(grid.id_db) load_pq_set['load_id'].append('_'.join( ['MV', str(grid.id_db), 'loa', str(node.id_db)])) load_pq_set['temp_id'].append(1) load_pq_set['p_set'].append([ node.peak_load * kw2mw, node.peak_load * kw2mw * load_in_generation_case ]) load_pq_set['q_set'].append([ node.peak_load * kw2mw * Q_factor_load, node.peak_load * kw2mw * Q_factor_load * load_in_generation_case ]) load_pq_set['grid_id'].append(grid.id_db) # bus at primary MV-LV transformer side buses['bus_id'].append(node.pypsa_id) buses['v_nom'].append(grid.v_level) buses['geom'].append(from_shape(node.geo_data, srid=srid)) buses['grid_id'].append(grid.id_db) bus_v_mag_set['bus_id'].append(node.pypsa_id) bus_v_mag_set['temp_id'].append(1) bus_v_mag_set['v_mag_pu_set'].append([1, 1]) bus_v_mag_set['grid_id'].append(grid.id_db) # generator representing generation capacity in LV grid generator['generator_id'].append('_'.join( ['MV', str(grid.id_db), 'gen', str(node.id_db)])) generator['control'].append('PQ') generator['p_nom'].append(node.peak_generation) generator['grid_id'].append(grid.id_db) generator['bus'].append(node.pypsa_id) generator_pq_set['generator_id'].append('_'.join( ['MV', str(grid.id_db), 'gen', str(node.id_db)])) generator_pq_set['temp_id'].append(1) generator_pq_set['p_set'].append([ node.peak_generation * kw2mw * generation_in_load_case, node.peak_generation * kw2mw ]) generator_pq_set['q_set'].append([ node.peak_generation * kw2mw * Q_factor_generation * generation_in_load_case, node.peak_generation * kw2mw * Q_factor_generation ]) generator_pq_set['grid_id'].append(grid.id_db) elif isinstance(node, CircuitBreakerDing0): # TODO: remove this elif-case if CircuitBreaker are removed from graph continue else: raise TypeError("Node of type", node, "cannot be handled here") else: if not isinstance(node, CircuitBreakerDing0): add_info = "LA is aggr. {0}".format( node.lv_load_area.is_aggregated) else: add_info = "" logger.warning("Node {0} is not connected to the graph and will " \ "be omitted in power flow analysis. {1}".format( node, add_info)) components = { 'Bus': DataFrame(buses).set_index('bus_id'), 'Generator': DataFrame(generator).set_index('generator_id'), 'Load': DataFrame(load).set_index('load_id') } components_data = { 'Bus': DataFrame(bus_v_mag_set).set_index('bus_id'), 'Generator': DataFrame(generator_pq_set).set_index('generator_id'), 'Load': DataFrame(load_pq_set).set_index('load_id') } # with open('/home/guido/ding0_debug/number_of_nodes_buses.csv', 'a') as csvfile: # csvfile.write(','.join(['\n', str(len(nodes)), str(len(grid.graph_isolated_nodes())), str(len(components['Bus']))])) return components, components_data
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', 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', 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', 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', type=cable_type_stub, id_db='stub_{sector}{branch}_{load}'.format( branch=branch_no, load=load_no, sector=sector_short)))
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', 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', 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', 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', 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()
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
def select_transformers(self): """ Selects appropriate transformers for the HV-MV substation. The transformers are chosen according to max. of load case and feedin-case considering load factors. The HV-MV transformer with the next higher available nominal apparent power is chosen. If one trafo is not sufficient, multiple trafos are used. Additionally, in a second step an redundant trafo is installed with max. capacity of the selected trafos of the first step according to general planning principles for MV distribution grids (n-1). Parameters ---------- transformers : dict Contains technical information of p hv/mv transformers **kwargs : dict Should contain a value behind the key 'peak_load' Notes ----- Parametrization of transformers bases on [#]_. Potential hv-mv-transformers are chosen according to [#]_. References ---------- .. [#] Deutsche Energie-Agentur GmbH (dena), "dena-Verteilnetzstudie. Ausbau- und Innovationsbedarf der Stromverteilnetze in Deutschland bis 2030.", 2012 .. [#] X. Tao, "Automatisierte Grundsatzplanung von Mittelspannungsnetzen", Dissertation, 2006 """ # get power factor for loads and generators cos_phi_load = cfg_ding0.get('assumptions', 'cos_phi_load') cos_phi_feedin = cfg_ding0.get('assumptions', 'cos_phi_gen') # get trafo load factors load_factor_mv_trans_lc_normal = float(cfg_ding0.get('assumptions', 'load_factor_mv_trans_lc_normal')) load_factor_mv_trans_fc_normal = float(cfg_ding0.get('assumptions', 'load_factor_mv_trans_fc_normal')) # get equipment parameters of MV transformers trafo_parameters = self.grid.network.static_data['MV_trafos'] # get peak load and peak generation cum_peak_load = self.peak_load / cos_phi_load cum_peak_generation = self.peak_generation(mode='MVLV') / cos_phi_feedin # check if load or generation is greater respecting corresponding load factor if (cum_peak_load / load_factor_mv_trans_lc_normal) > \ (cum_peak_generation / load_factor_mv_trans_fc_normal): # use peak load and load factor from load case load_factor_mv_trans = load_factor_mv_trans_lc_normal residual_apparent_power = cum_peak_load else: # use peak generation and load factor for feedin case load_factor_mv_trans = load_factor_mv_trans_fc_normal residual_apparent_power = cum_peak_generation # determine number and size of required transformers # get max. trafo transformer_max = trafo_parameters.iloc[trafo_parameters['S_nom'].idxmax()] while residual_apparent_power > 0: if residual_apparent_power > load_factor_mv_trans * transformer_max['S_nom']: transformer = transformer_max else: # choose trafo transformer = trafo_parameters.iloc[ trafo_parameters[trafo_parameters['S_nom'] * load_factor_mv_trans > residual_apparent_power]['S_nom'].idxmin()] # add transformer on determined size with according parameters self.add_transformer(TransformerDing0(**{'grid': self.grid, 'v_level': self.grid.v_level, 's_max_longterm': transformer['S_nom']})) # calc residual load residual_apparent_power -= (load_factor_mv_trans * transformer['S_nom']) # if no transformer was selected (no load in grid district), use smallest one if len(self._transformers) == 0: transformer = trafo_parameters.iloc[trafo_parameters['S_nom'].idxmin()] self.add_transformer( TransformerDing0(grid=self.grid, v_level=self.grid.v_level, s_max_longterm=transformer['S_nom'])) # add redundant transformer of the size of the largest transformer s_max_max = max((o.s_max_a for o in self._transformers)) self.add_transformer(TransformerDing0(**{'grid': self.grid, 'v_level': self.grid.v_level, 's_max_longterm': s_max_max}))
def ding0_graph_to_routing_specs(graph): """ Build data dictionary from graph nodes for routing (translation) Args ---- graph: :networkx:`NetworkX Graph Obj< >` NetworkX graph object with nodes Returns ------- :obj:`dict` Data dictionary for routing. See Also -------- ding0.grid.mv_grid.models.models.Graph : for keys of return dict """ # get power factor for loads cos_phi_load = cfg_ding0.get('assumptions', 'cos_phi_load') specs = {} nodes_demands = {} nodes_pos = {} nodes_agg = {} # check if there are only load areas of type aggregated and satellite # -> treat satellites as normal load areas (allow for routing) satellites_only = True for node in graph.nodes(): if isinstance(node, LVLoadAreaCentreDing0): if not node.lv_load_area.is_satellite and not node.lv_load_area.is_aggregated: satellites_only = False for node in graph.nodes(): # station is LV station if isinstance(node, LVLoadAreaCentreDing0): # only major stations are connected via MV ring # (satellites in case of there're only satellites in grid district) if not node.lv_load_area.is_satellite or satellites_only: # get demand and position of node # convert node's demand to int for performance purposes and to avoid that node # allocation with subsequent deallocation results in demand<0 due to rounding errors. nodes_demands[str(node)] = int(node.lv_load_area.peak_load / cos_phi_load) nodes_pos[str(node)] = (node.geo_data.x, node.geo_data.y) # get aggregation flag if node.lv_load_area.is_aggregated: nodes_agg[str(node)] = True else: nodes_agg[str(node)] = False # station is MV station elif isinstance(node, MVStationDing0): nodes_demands[str(node)] = 0 nodes_pos[str(node)] = (node.geo_data.x, node.geo_data.y) specs['DEPOT'] = str(node) specs['BRANCH_KIND'] = node.grid.default_branch_kind specs['BRANCH_TYPE'] = node.grid.default_branch_type specs['V_LEVEL'] = node.grid.v_level specs['NODE_COORD_SECTION'] = nodes_pos specs['DEMAND'] = nodes_demands specs['MATRIX'] = calc_geo_dist_matrix_vincenty(nodes_pos) specs['IS_AGGREGATED'] = nodes_agg return specs
def extend_substation_voltage(crit_stations, grid_level='LV'): """ Extend substation if voltage issues at the substation occur Follows a two-step procedure: i) Existing transformers are extended by replacement with large nominal apparent power ii) New additional transformers added to substation (see 'Notes') Parameters ---------- crit_stations : :any:`list` List of stations with overloading or voltage issues. grid_level : str Specifiy grid level: 'MV' or 'LV' Notes ----- At maximum 2 new of largest (currently 630 kVA) transformer are additionally built to resolve voltage issues at MV-LV substation bus bar. """ grid = crit_stations[0]['node'].grid trafo_params = grid.network._static_data['{grid_level}_trafos'.format( grid_level=grid_level)] trafo_s_max_max = max(trafo_params['S_nom']) trafo_min_size = trafo_params.ix[trafo_params['S_nom'].idxmin()] v_diff_max_fc = cfg_ding0.get('assumptions', 'lv_max_v_level_fc_diff_normal') v_diff_max_lc = cfg_ding0.get('assumptions', 'lv_max_v_level_lc_diff_normal') tree = nx.dfs_tree(grid._graph, grid._station) for station in crit_stations: v_delta = max(station['v_diff']) # get list of nodes of main branch in right order extendable_trafos = [_ for _ in station['node']._transformers if _.s_max_a < trafo_s_max_max] v_delta_initially_lc = v_delta[0] v_delta_initially_fc = v_delta[1] new_transformers_cnt = 0 # extend existing trafo power while voltage issues exist and larger trafos # are available while (v_delta[0] > v_diff_max_lc) or (v_delta[1] > v_diff_max_fc): if extendable_trafos: # extend power of first trafo to next higher size available extend_trafo_power(extendable_trafos, trafo_params) elif new_transformers_cnt < 2: # build a new transformer lv_transformer = TransformerDing0( grid=grid, id_db=id, v_level=0.4, s_max_longterm=trafo_min_size['S_nom'], r=trafo_min_size['R'], x=trafo_min_size['X']) # add each transformer to its station grid._station.add_transformer(lv_transformer) new_transformers_cnt += 1 # update break criteria v_delta = get_voltage_at_bus_bar(grid, tree) extendable_trafos = [_ for _ in station['node']._transformers if _.s_max_a < trafo_s_max_max] if (v_delta[0] == v_delta_initially_lc) or ( v_delta[1] == v_delta_initially_fc): logger.warning("Extension of {station} has no effect on " "voltage delta at bus bar. Transformation power " "extension is halted.".format( station=station['node'])) break