Source code for edisgo.grid.connect

from ..grid.components import Line, MVStation, LVStation, MVDisconnectingPoint, Generator, Load, BranchTee
from ..tools.geo import calc_geo_dist_vincenty, \
                        calc_geo_lines_in_buffer, \
                        proj2equidistant, \
                        proj2conformal

import networkx as nx
import random
import pandas as pd

import os
if not 'READTHEDOCS' in os.environ:
    from shapely.geometry import LineString
    from shapely.ops import transform

import logging
logger = logging.getLogger('edisgo')


[docs]def connect_mv_generators(network): """Connect MV generators to existing grids. This function searches for unconnected generators in MV grids and connects them. It connects * generators of voltage level 4 * to HV-MV station * generators of voltage level 5 * with a nom. capacity of <=30 kW to LV loads of type residential * with a nom. capacity of >30 kW and <=100 kW to LV loads of type retail, industrial or agricultural * to the MV-LV station if no appropriate load is available (fallback) Parameters ---------- network : :class:`~.grid.network.Network` The eDisGo container object Notes ----- Adapted from `Ding0 <https://github.com/openego/ding0/blob/\ 21a52048f84ec341fe54e0204ac62228a9e8a32a/\ ding0/grid/mv_grid/mv_connect.py#L820>`_. """ # get params from config buffer_radius = int(network.config[ 'grid_connection']['conn_buffer_radius']) buffer_radius_inc = int(network.config[ 'grid_connection']['conn_buffer_radius_inc']) # get standard equipment std_line_type = network.equipment_data['mv_cables'].loc[ network.config['grid_expansion_standard_equipment']['mv_line']] for geno in sorted(network.mv_grid.graph.nodes_by_attribute('generator'), key=lambda _: repr(_)): if nx.is_isolate(network.mv_grid.graph, geno): # ===== voltage level 4: generator has to be connected to MV station ===== if geno.v_level == 4: line_length = calc_geo_dist_vincenty(network=network, node_source=geno, node_target=network.mv_grid.station) line = Line(id=random.randint(10**8, 10**9), type=std_line_type, kind='cable', quantity=1, length=line_length / 1e3, grid=network.mv_grid) network.mv_grid.graph.add_edge(network.mv_grid.station, geno, line=line, type='line') # add line to equipment changes to track costs _add_cable_to_equipment_changes(network=network, line=line) # ===== voltage level 5: generator has to be connected to MV grid (next-neighbor) ===== elif geno.v_level == 5: # get branches within a the predefined radius `generator_buffer_radius` branches = calc_geo_lines_in_buffer(network=network, node=geno, grid=network.mv_grid, radius=buffer_radius, radius_inc=buffer_radius_inc) # calc distance between generator and grid's lines -> find nearest line conn_objects_min_stack = _find_nearest_conn_objects(network=network, node=geno, branches=branches) # connect! # go through the stack (from nearest to most far connection target object) generator_connected = False for dist_min_obj in conn_objects_min_stack: target_obj_result = _connect_mv_node(network=network, node=geno, target_obj=dist_min_obj) if target_obj_result is not None: generator_connected = True break if not generator_connected: logger.debug( 'Generator {0} could not be connected, try to ' 'increase the parameter `conn_buffer_radius` in ' 'config file `config_grid.cfg` to gain more possible ' 'connection points.'.format(geno))
[docs]def connect_lv_generators(network, allow_multiple_genos_per_load=True): """Connect LV generators to existing grids. This function searches for unconnected generators in all LV grids and connects them. It connects * generators of voltage level 6 * to MV-LV station * generators of voltage level 7 * with a nom. capacity of <=30 kW to LV loads of type residential * with a nom. capacity of >30 kW and <=100 kW to LV loads of type retail, industrial or agricultural * to the MV-LV station if no appropriate load is available (fallback) Parameters ---------- network : :class:`~.grid.network.Network` The eDisGo container object allow_multiple_genos_per_load : :obj:`bool` If True, more than one generator can be connected to one load Notes ----- For the allocation, loads are selected randomly (sector-wise) using a predefined seed to ensure reproducibility. """ # get predefined random seed and initialize random generator seed = int(network.config['grid_connection']['random_seed']) #random.seed(a=seed) random.seed(a=1234) # ToDo: Switch back to 'seed' as soon as line ids are finished, #58 # get standard equipment std_line_type = network.equipment_data['lv_cables'].loc[ network.config['grid_expansion_standard_equipment']['lv_line']] std_line_kind = 'cable' # # TEMP: DEBUG STUFF # lv_grid_stats = pd.DataFrame(columns=('lv_grid', # 'load_count', # 'geno_count', # 'more_genos_than_loads') # ) # iterate over all LV grids for lv_grid in network.mv_grid.lv_grids: lv_loads = lv_grid.graph.nodes_by_attribute('load') # counter for genos in v_level 7 log_geno_count_vlevel7 = 0 # generate random list (without replacement => unique elements) # of loads (residential) to connect genos (P <= 30kW) to. lv_loads_res = sorted([lv_load for lv_load in lv_loads if 'residential' in list(lv_load.consumption.keys())], key=lambda _: repr(_)) if len(lv_loads_res) > 0: lv_loads_res_rnd = set(random.sample(lv_loads_res, len(lv_loads_res))) else: lv_loads_res_rnd = None # generate random list (without replacement => unique elements) # of loads (retail, industrial, agricultural) to connect genos # (30kW < P <= 100kW) to. lv_loads_ria = sorted([lv_load for lv_load in lv_loads if any([_ in list(lv_load.consumption.keys()) for _ in ['retail', 'industrial', 'agricultural']])], key=lambda _: repr(_)) if len(lv_loads_ria) > 0: lv_loads_ria_rnd = set(random.sample(lv_loads_ria, len(lv_loads_ria))) else: lv_loads_ria_rnd = None for geno in sorted(lv_grid.graph.nodes_by_attribute('generator'), key=lambda x: repr(x)): if nx.is_isolate(lv_grid.graph, geno): lv_station = lv_grid.station # generator is of v_level 6 -> connect to LV station if geno.v_level == 6: line_length = calc_geo_dist_vincenty(network=network, node_source=geno, node_target=lv_station) line = Line(id=random.randint(10 ** 8, 10 ** 9), length=line_length / 1e3, quantity=1, kind=std_line_kind, type=std_line_type, grid=lv_grid) lv_grid.graph.add_edge(geno, lv_station, line=line, type='line') # add line to equipment changes to track costs _add_cable_to_equipment_changes(network=network, line=line) # generator is of v_level 7 -> assign geno to load elif geno.v_level == 7: # counter for genos in v_level 7 log_geno_count_vlevel7 += 1 # connect genos with P <= 30kW to residential loads, if available if (geno.nominal_capacity <= 30) and (lv_loads_res_rnd is not None): if len(lv_loads_res_rnd) > 0: lv_load = lv_loads_res_rnd.pop() # if random load list is empty, create new one else: lv_loads_res_rnd = set(random.sample(lv_loads_res, len(lv_loads_res)) ) lv_load = lv_loads_res_rnd.pop() # get cable distributor of building lv_conn_target = list(lv_grid.graph.neighbors(lv_load))[0] if not allow_multiple_genos_per_load: # check if there's an existing generator connected to the load # if so, select next load. If no load is available, connect to station. while any([isinstance(_, Generator) for _ in lv_grid.graph.neighbors( list(lv_grid.graph.neighbors(lv_load))[0])]): if len(lv_loads_res_rnd) > 0: lv_load = lv_loads_res_rnd.pop() # get cable distributor of building lv_conn_target = list(lv_grid.graph.neighbors(lv_load))[0] else: lv_conn_target = lv_grid.station logger.debug( 'No valid conn. target found for {}. ' 'Connected to {}.'.format( repr(geno), repr(lv_conn_target) ) ) break # connect genos with 30kW <= P <= 100kW to residential loads # to retail, industrial, agricultural loads, if available elif (geno.nominal_capacity > 30) and (lv_loads_ria_rnd is not None): if len(lv_loads_ria_rnd) > 0: lv_load = lv_loads_ria_rnd.pop() # if random load list is empty, create new one else: lv_loads_ria_rnd = set(random.sample(lv_loads_ria, len(lv_loads_ria)) ) lv_load = lv_loads_ria_rnd.pop() # get cable distributor of building lv_conn_target = list(lv_grid.graph.neighbors(lv_load))[0] if not allow_multiple_genos_per_load: # check if there's an existing generator connected to the load # if so, select next load. If no load is available, connect to station. while any([isinstance(_, Generator) for _ in lv_grid.graph.neighbors( list(lv_grid.graph.neighbors(lv_load))[0])]): if len(lv_loads_ria_rnd) > 0: lv_load = lv_loads_ria_rnd.pop() # get cable distributor of building lv_conn_target = list(lv_grid.graph.neighbors(lv_load))[0] else: lv_conn_target = lv_grid.station logger.debug( 'No valid conn. target found for {}. ' 'Connected to {}.'.format( repr(geno), repr(lv_conn_target) ) ) break # fallback: connect to station else: lv_conn_target = lv_grid.station logger.debug( 'No valid conn. target found for {}. ' 'Connected to {}.'.format( repr(geno), repr(lv_conn_target) ) ) line = Line(id=random.randint(10 ** 8, 10 ** 9), length=1e-3, quantity=1, kind=std_line_kind, type=std_line_type, grid=lv_grid) lv_grid.graph.add_edge(geno, lv_station, line=line, type='line') # add line to equipment changes to track costs _add_cable_to_equipment_changes(network=network, line=line) # warn if there're more genos than loads in LV grid if log_geno_count_vlevel7 > len(lv_loads): logger.debug('The count of newly connected generators in voltage level 7 ({}) ' 'exceeds the count of loads ({}) in LV grid {}.' .format(str(log_geno_count_vlevel7), str(len(lv_loads)), repr(lv_grid) ) )
# # TEMP: DEBUG STUFF # lv_grid_stats.loc[len(lv_grid_stats)] = [repr(lv_grid), # len(lv_loads), # log_geno_count_vlevel7, # log_geno_count_vlevel7 > len(lv_loads)] def _add_cable_to_equipment_changes(network, line): """Add cable to the equipment changes All changes of equipment are stored in network.results.equipment_changes which is used later to determine grid expansion costs. Parameters ---------- network : :class:`~.grid.network.Network` The eDisGo container object line : class:`~.grid.components.Line` Line instance which is to be added """ network.results.equipment_changes = \ network.results.equipment_changes.append( pd.DataFrame( {'iteration_step': [0], 'change': ['added'], 'equipment': [line.type.name], 'quantity': [1] }, index=[line] ) ) def _del_cable_from_equipment_changes(network, line): """Delete cable from the equipment changes if existing This is needed if a cable was already added to network.results.equipment_changes but another node is connected later to this cable. Therefore, the cable needs to be split which changes the id (one cable id -> 2 new cable ids). Parameters ---------- network : :class:`~.grid.network.Network` The eDisGo container object line : class:`~.grid.components.Line` Line instance which is to be deleted """ if line in network.results.equipment_changes.index: network.results.equipment_changes = \ network.results.equipment_changes.drop(line) def _find_nearest_conn_objects(network, node, branches): """Searches all branches for the nearest possible connection object per branch It picks out 1 object out of 3 possible objects: 2 branch-adjacent stations and 1 potentially created branch tee on the line (using perpendicular projection). The resulting stack (list) is sorted ascending by distance from node. Parameters ---------- network : :class:`~.grid.network.Network` The eDisGo container object node : :class:`~.grid.components.Component` Node to connect (e.g. :class:`~.grid.components.Generator`) branches : List of branches (NetworkX branch objects) Returns ------- :obj:`list` of :obj:`dict` List of connection objects (each object is represented by dict with eDisGo object, shapely object and distance to node. Notes ----- Adapted from `Ding0 <https://github.com/openego/ding0/blob/\ 21a52048f84ec341fe54e0204ac62228a9e8a32a/\ ding0/grid/mv_grid/mv_connect.py#L38>`_. """ # threshold which is used to determine if 2 objects are on the same position (see below for details on usage) conn_diff_tolerance = network.config['grid_connection'][ 'conn_diff_tolerance'] conn_objects_min_stack = [] node_shp = transform(proj2equidistant(network), node.geom) for branch in branches: stations = branch['adj_nodes'] # create shapely objects for 2 stations and line between them, transform to equidistant CRS station1_shp = transform(proj2equidistant(network), stations[0].geom) station2_shp = transform(proj2equidistant(network), stations[1].geom) line_shp = LineString([station1_shp, station2_shp]) # create dict with DING0 objects (line & 2 adjacent stations), shapely objects and distances conn_objects = {'s1': {'obj': stations[0], 'shp': station1_shp, 'dist': node_shp.distance(station1_shp) * 0.999}, 's2': {'obj': stations[1], 'shp': station2_shp, 'dist': node_shp.distance(station2_shp) * 0.999}, 'b': {'obj': branch, 'shp': line_shp, 'dist': node_shp.distance(line_shp)}} # Remove branch from the dict of possible conn. objects if it is too close to a node. # Without this solution, the target object is not unique for different runs (and so # were the topology) if ( abs(conn_objects['s1']['dist'] - conn_objects['b']['dist']) < conn_diff_tolerance or abs(conn_objects['s2']['dist'] - conn_objects['b']['dist']) < conn_diff_tolerance ): del conn_objects['b'] # remove MV station as possible connection point if isinstance(conn_objects['s1']['obj'], MVStation): del conn_objects['s1'] elif isinstance(conn_objects['s2']['obj'], MVStation): del conn_objects['s2'] # find nearest connection point on given triple dict (2 branch-adjacent stations + cable dist. on line) conn_objects_min = min(conn_objects.values(), key=lambda v: v['dist']) conn_objects_min_stack.append(conn_objects_min) # sort all objects by distance from node conn_objects_min_stack = [_ for _ in sorted(conn_objects_min_stack, key=lambda x: x['dist'])] return conn_objects_min_stack def _connect_mv_node(network, node, target_obj): """Connects MV node to target object in MV grid If the target object is a node, a new line is created to it. If the target object is a line, the node is connected to a newly created branch tee (using perpendicular projection) on this line. New lines are created using standard equipment. Parameters ---------- network : :class:`~.grid.network.Network` The eDisGo container object node : :class:`~.grid.components.Component` Node to connect (e.g. :class:`~.grid.components.Generator`) Node must be a member of MV grid's graph (network.mv_grid.graph) target_obj : :class:`~.grid.components.Component` Object that node shall be connected to Returns ------- :class:`~.grid.components.Component` or None Node that node was connected to Notes ----- Adapted from `Ding0 <https://github.com/openego/ding0/blob/\ 21a52048f84ec341fe54e0204ac62228a9e8a32a/\ ding0/grid/mv_grid/mv_connect.py#L311>`_. """ # get standard equipment std_line_type = network.equipment_data['mv_cables'].loc[ network.config['grid_expansion_standard_equipment']['mv_line']] std_line_kind = 'cable' target_obj_result = None node_shp = transform(proj2equidistant(network), node.geom) # MV line is nearest connection point if isinstance(target_obj['shp'], LineString): adj_node1 = target_obj['obj']['adj_nodes'][0] adj_node2 = target_obj['obj']['adj_nodes'][1] # find nearest point on MV line conn_point_shp = target_obj['shp'].interpolate(target_obj['shp'].project(node_shp)) conn_point_shp = transform(proj2conformal(network), conn_point_shp) line = network.mv_grid.graph.edges[adj_node1,adj_node2] # target MV line does currently not connect a load area of type aggregated if not line['type'] == 'line_aggr': # create branch tee and add it to grid branch_tee = BranchTee(geom=conn_point_shp, grid=network.mv_grid, in_building=False) network.mv_grid.graph.add_node(branch_tee, type='branch_tee') # split old branch into 2 segments # (delete old branch and create 2 new ones along cable_dist) # ========================================================== # backup kind and type of branch line_kind = line['line'].kind line_type = line['line'].type # remove line from graph network.mv_grid.graph.remove_edge(adj_node1, adj_node2) # delete line from equipment changes if existing _del_cable_from_equipment_changes(network=network, line=line['line']) line_length = calc_geo_dist_vincenty(network=network, node_source=adj_node1, node_target=branch_tee) line = Line(id=random.randint(10 ** 8, 10 ** 9), length=line_length / 1e3, quantity=1, kind=line_kind, type=line_type, grid=network.mv_grid) network.mv_grid.graph.add_edge(adj_node1, branch_tee, line=line, type='line') # add line to equipment changes to track costs _add_cable_to_equipment_changes(network=network, line=line) line_length = calc_geo_dist_vincenty(network=network, node_source=adj_node2, node_target=branch_tee) line = Line(id=random.randint(10 ** 8, 10 ** 9), length=line_length / 1e3, quantity=1, kind=line_kind, type=line_type, grid=network.mv_grid) network.mv_grid.graph.add_edge(adj_node2, branch_tee, line=line, type='line') # add line to equipment changes to track costs _add_cable_to_equipment_changes(network=network, line=line) # add new branch for new node (node to branch tee) # ================================================ line_length = calc_geo_dist_vincenty(network=network, node_source=node, node_target=branch_tee) line = Line(id=random.randint(10 ** 8, 10 ** 9), length=line_length / 1e3, quantity=1, kind=std_line_kind, type=std_line_type, grid=network.mv_grid) network.mv_grid.graph.add_edge(node, branch_tee, line=line, type='line') # add line to equipment changes to track costs _add_cable_to_equipment_changes(network=network, line=line) target_obj_result = branch_tee # node ist nearest connection point else: # what kind of node is to be connected? (which type is node of?) # LVStation: Connect to LVStation or BranchTee # Generator: Connect to LVStation, BranchTee or Generator if isinstance(node, LVStation): valid_conn_objects = (LVStation, BranchTee) elif isinstance(node, Generator): valid_conn_objects = (LVStation, BranchTee, Generator) else: raise ValueError('Oops, the node you are trying to connect is not a valid connection object') # if target is generator or Load, check if it is aggregated (=> connection not allowed) if isinstance(target_obj['obj'], (Generator, Load)): target_is_aggregated = any([_ for _ in network.mv_grid.graph.adj[target_obj['obj']].values() if _['type'] == 'line_aggr']) else: target_is_aggregated = False # target node is not a load area of type aggregated if isinstance(target_obj['obj'], valid_conn_objects) and not target_is_aggregated: # add new branch for satellite (station to station) line_length = calc_geo_dist_vincenty(network=network, node_source=node, node_target=target_obj['obj']) line = Line(id=random.randint(10 ** 8, 10 ** 9), type=std_line_type, kind=std_line_kind, quantity=1, length=line_length / 1e3, grid=network.mv_grid) network.mv_grid.graph.add_edge(node, target_obj['obj'], line=line, type='line') # add line to equipment changes to track costs _add_cable_to_equipment_changes(network=network, line=line) target_obj_result = target_obj['obj'] return target_obj_result