def routing_solution_to_ding0_graph(graph, solution): """ Insert `solution` from routing into `graph` Args ---- graph: :networkx:`NetworkX Graph Obj< >` NetworkX graph object with nodes solution: BaseSolution Instance of `BaseSolution` or child class (e.g. `LocalSearchSolution`) (=solution from routing) Returns ------- :networkx:`NetworkX Graph Obj< >` NetworkX graph object with nodes and edges """ # TODO: Bisherige Herangehensweise (diese Funktion): Branches werden nach Routing erstellt um die Funktionsfähigkeit # TODO: des Routing-Tools auch für die TestCases zu erhalten. Es wird ggf. notwendig, diese direkt im Routing vorzunehmen. # build node dict (name: obj) from graph nodes to map node names on node objects node_list = {str(n): n for n in graph.nodes()} # add edges from solution to graph try: depot = solution._nodes[solution._problem._depot.name()] depot_node = node_list[depot.name()] for r in solution.routes(): circ_breaker_pos = None # if route has only one node and is not aggregated, it wouldn't be possible to add two lines from and to # this node (undirected graph of NetworkX). So, as workaround, an additional MV cable distributor is added # at nodes' position (resulting route: HV/MV_subst --- node --- cable_dist --- HV/MV_subst. if len(r._nodes) == 1: if not solution._problem._is_aggregated[r._nodes[0]._name]: # create new cable dist cable_dist = MVCableDistributorDing0( geo_data=node_list[r._nodes[0]._name].geo_data, grid=depot_node.grid) depot_node.grid.add_cable_distributor(cable_dist) # create new node (as dummy) an allocate to route r r.allocate([Node(name=repr(cable_dist), demand=0)]) # add it to node list and allocated-list manually node_list[str(cable_dist)] = cable_dist solution._problem._is_aggregated[str(cable_dist)] = False # set circ breaker pos manually circ_breaker_pos = 1 # build edge list n1 = r._nodes[0:len(r._nodes) - 1] n2 = r._nodes[1:len(r._nodes)] edges = list(zip(n1, n2)) edges.append((depot, r._nodes[0])) edges.append((r._nodes[-1], depot)) # create MV Branch object for every edge in `edges` mv_branches = [BranchDing0() for _ in edges] edges_with_branches = list(zip(edges, mv_branches)) # recalculate circuit breaker positions for final solution, create it and set associated branch. # if circ. breaker position is not set manually (routes with more than one load area, see above) if not circ_breaker_pos: circ_breaker_pos = r.calc_circuit_breaker_position() node1 = node_list[edges[circ_breaker_pos - 1][0].name()] node2 = node_list[edges[circ_breaker_pos - 1][1].name()] # ALTERNATIVE TO METHOD ABOVE: DO NOT CREATE 2 BRANCHES (NO RING) -> LA IS CONNECTED AS SATELLITE # IF THIS IS COMMENTED-IN, THE IF-BLOCK IN LINE 87 HAS TO BE COMMENTED-OUT # See issue #114 # =============================== # do not add circuit breaker for routes which are aggregated load areas or # routes that contain only one load area # if not (node1 == depot_node and solution._problem._is_aggregated[edges[circ_breaker_pos - 1][1].name()] or # node2 == depot_node and solution._problem._is_aggregated[edges[circ_breaker_pos - 1][0].name()] or # len(r._nodes) == 1): # =============================== # do not add circuit breaker for routes which are aggregated load areas if not (node1 == depot_node and solution._problem._is_aggregated[ edges[circ_breaker_pos - 1][1].name()] or node2 == depot_node and solution._problem. _is_aggregated[edges[circ_breaker_pos - 1][0].name()]): branch = mv_branches[circ_breaker_pos - 1] circ_breaker = CircuitBreakerDing0( grid=depot_node.grid, branch=branch, geo_data=calc_geo_centre_point(node1, node2)) branch.circuit_breaker = circ_breaker # create new ring object for route ring = RingDing0(grid=depot_node.grid) # translate solution's node names to graph node objects using dict created before # note: branch object is assigned to edge using an attribute ('branch' is used here), it can be accessed # using the method `graph_edges()` of class `GridDing0` edges_graph = [] for ((n1, n2), b) in edges_with_branches: # get node objects node1 = node_list[n1.name()] node2 = node_list[n2.name()] # set branch's ring attribute b.ring = ring # set LVLA's ring attribute if isinstance(node1, LVLoadAreaCentreDing0): node1.lv_load_area.ring = ring # set branch length b.length = calc_geo_dist_vincenty(node1, node2) # set branch kind and type # 1) default b.kind = depot_node.grid.default_branch_kind b.type = depot_node.grid.default_branch_type # 2) aggregated load area types if node1 == depot_node and solution._problem._is_aggregated[ n2.name()]: b.connects_aggregated = True b.kind = depot_node.grid.default_branch_kind_aggregated b.type = depot_node.grid.default_branch_type_aggregated elif node2 == depot_node and solution._problem._is_aggregated[ n1.name()]: b.connects_aggregated = True b.kind = depot_node.grid.default_branch_kind_aggregated b.type = depot_node.grid.default_branch_type_aggregated # append to branch list edges_graph.append((node1, node2, dict(branch=b))) # add branches to graph graph.add_edges_from(edges_graph) except: logger.exception( 'unexpected error while converting routing solution to DING0 graph (NetworkX).' ) return graph
def connect_node(node, node_shp, mv_grid, target_obj, proj, graph, conn_dist_ring_mod, debug): """ Connects `node` to `target_obj`. Parameters ---------- node: LVLoadAreaCentreDing0, i.e. Origin node - Ding0 graph object (e.g. LVLoadAreaCentreDing0) node_shp: :shapely:`Shapely Point object<points>` Shapely Point object of origin node target_obj: type object that node shall be connected to proj: :pyproj:`pyproj Proj object< >` equidistant CRS to conformal CRS (e.g. ETRS -> WGS84) graph: :networkx:`NetworkX Graph Obj< >` NetworkX graph object with nodes and newly created branches conn_dist_ring_mod: float Max. distance when nodes are included into route instead of creating a new line. debug: bool If True, information is printed during process. Returns ------- :obj:`LVLoadAreaCentreDing0` object that node was connected to. (instance of :obj:`LVLoadAreaCentreDing0` or :obj:`MVCableDistributorDing0`. If node is included into line instead of creating a new line (see arg `conn_dist_ring_mod`), `target_obj_result` is None. See Also -------- ding0.grid.mv_grid.mv_connect : for details on the `conn_dist_ring_mod` parameter. """ target_obj_result = None # 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(proj, conn_point_shp) # target MV line does currently not connect a load area of type aggregated if not target_obj['obj']['branch'].connects_aggregated: # Node is close to line # -> insert node into route (change existing route) if (target_obj['dist'] < conn_dist_ring_mod): # backup kind and type of branch branch_type = graph.adj[adj_node1][adj_node2]['branch'].type branch_kind = graph.adj[adj_node1][adj_node2]['branch'].kind branch_ring = graph.adj[adj_node1][adj_node2]['branch'].ring # check if there's a circuit breaker on current branch, # if yes set new position between first node (adj_node1) and newly inserted node circ_breaker = graph.adj[adj_node1][adj_node2][ 'branch'].circuit_breaker if circ_breaker is not None: circ_breaker.geo_data = calc_geo_centre_point( adj_node1, node) # split old ring main route into 2 segments (delete old branch and create 2 new ones # along node) graph.remove_edge(adj_node1, adj_node2) branch_length = calc_geo_dist_vincenty(adj_node1, node) branch = BranchDing0(length=branch_length, circuit_breaker=circ_breaker, kind=branch_kind, type=branch_type, ring=branch_ring) if circ_breaker is not None: circ_breaker.branch = branch graph.add_edge(adj_node1, node, branch=branch) branch_length = calc_geo_dist_vincenty(adj_node2, node) graph.add_edge(adj_node2, node, branch=BranchDing0(length=branch_length, kind=branch_kind, type=branch_type, ring=branch_ring)) target_obj_result = 're-routed' if debug: logger.debug('Ring main route modified to include ' 'node {}'.format(node)) # Node is too far away from route # => keep main route and create new line from node to (cable distributor on) route. else: # create cable distributor and add it to grid cable_dist = MVCableDistributorDing0(geo_data=conn_point_shp, grid=mv_grid) mv_grid.add_cable_distributor(cable_dist) # check if there's a circuit breaker on current branch, # if yes set new position between first node (adj_node1) and newly created cable distributor circ_breaker = graph.adj[adj_node1][adj_node2][ 'branch'].circuit_breaker if circ_breaker is not None: circ_breaker.geo_data = calc_geo_centre_point( adj_node1, cable_dist) # split old branch into 2 segments (delete old branch and create 2 new ones along cable_dist) # =========================================================================================== # backup kind and type of branch branch_kind = graph.adj[adj_node1][adj_node2]['branch'].kind branch_type = graph.adj[adj_node1][adj_node2]['branch'].type branch_ring = graph.adj[adj_node1][adj_node2]['branch'].ring graph.remove_edge(adj_node1, adj_node2) branch_length = calc_geo_dist_vincenty(adj_node1, cable_dist) branch = BranchDing0(length=branch_length, circuit_breaker=circ_breaker, kind=branch_kind, type=branch_type, ring=branch_ring) if circ_breaker is not None: circ_breaker.branch = branch graph.add_edge(adj_node1, cable_dist, branch=branch) branch_length = calc_geo_dist_vincenty(adj_node2, cable_dist) graph.add_edge(adj_node2, cable_dist, branch=BranchDing0(length=branch_length, kind=branch_kind, type=branch_type, ring=branch_ring)) # add new branch for satellite (station to cable distributor) # =========================================================== # get default branch kind and type from grid to use it for new branch branch_kind = mv_grid.default_branch_kind branch_type = mv_grid.default_branch_type branch_length = calc_geo_dist_vincenty(node, cable_dist) graph.add_edge(node, cable_dist, branch=BranchDing0(length=branch_length, kind=branch_kind, type=branch_type, ring=branch_ring)) target_obj_result = cable_dist # debug info if debug: logger.debug('Nearest connection point for object {0} ' 'is branch {1} (distance={2} m)'.format( node, target_obj['obj']['adj_nodes'], target_obj['dist'])) # node ist nearest connection point else: # what kind of node is to be connected? (which type is node of?) # LVLoadAreaCentreDing0: Connect to LVLoadAreaCentreDing0 only # LVStationDing0: Connect to LVLoadAreaCentreDing0, LVStationDing0 or MVCableDistributorDing0 # GeneratorDing0: Connect to LVLoadAreaCentreDing0, LVStationDing0, MVCableDistributorDing0 or GeneratorDing0 if isinstance(node, LVLoadAreaCentreDing0): valid_conn_objects = LVLoadAreaCentreDing0 elif isinstance(node, LVStationDing0): valid_conn_objects = (LVLoadAreaCentreDing0, LVStationDing0, MVCableDistributorDing0) elif isinstance(node, GeneratorDing0): valid_conn_objects = (LVLoadAreaCentreDing0, LVStationDing0, MVCableDistributorDing0, GeneratorDing0) else: raise ValueError( 'Oops, the node you are trying to connect is not a valid connection object' ) # if target is Load Area centre or LV station, check if it belongs to a load area of type aggregated # (=> connection not allowed) if isinstance(target_obj['obj'], (LVLoadAreaCentreDing0, LVStationDing0)): target_is_aggregated = target_obj['obj'].lv_load_area.is_aggregated 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: # get default branch kind and type from grid to use it for new branch branch_kind = mv_grid.default_branch_kind branch_type = mv_grid.default_branch_type # get branch ring obj branch_ring = mv_grid.get_ring_from_node(target_obj['obj']) # add new branch for satellite (station to station) branch_length = calc_geo_dist_vincenty(node, target_obj['obj']) graph.add_edge(node, target_obj['obj'], branch=BranchDing0(length=branch_length, kind=branch_kind, type=branch_type, ring=branch_ring)) target_obj_result = target_obj['obj'] # debug info if debug: logger.debug( 'Nearest connection point for object {0} is station {1} ' '(distance={2} m)'.format(node, target_obj['obj'], target_obj['dist'])) return target_obj_result
def mv_connect_stations(mv_grid_district, graph, debug=False): """ Connect LV stations to MV grid Parameters ---------- mv_grid_district: MVGridDistrictDing0 MVGridDistrictDing0 object for which the connection process has to be done graph: :networkx:`NetworkX Graph Obj< >` NetworkX graph object with nodes debug: bool, defaults to False If True, information is printed during process Returns ------- :networkx:`NetworkX Graph Obj< >` NetworkX graph object with nodes and newly created branches """ # WGS84 (conformal) to ETRS (equidistant) projection proj1 = partial( pyproj.transform, pyproj.Proj(init='epsg:4326'), # source coordinate system pyproj.Proj(init='epsg:3035')) # destination coordinate system # ETRS (equidistant) to WGS84 (conformal) projection proj2 = partial( pyproj.transform, pyproj.Proj(init='epsg:3035'), # source coordinate system pyproj.Proj(init='epsg:4326')) # destination coordinate system conn_dist_weight = cfg_ding0.get('mv_connect', 'load_area_sat_conn_dist_weight') conn_dist_ring_mod = cfg_ding0.get('mv_connect', 'load_area_stat_conn_dist_ring_mod') for lv_load_area in mv_grid_district.lv_load_areas(): # exclude aggregated Load Areas and choose only load areas that were connected to grid before if not lv_load_area.is_aggregated and \ lv_load_area.lv_load_area_centre not in mv_grid_district.mv_grid.graph_isolated_nodes(): lv_load_area_centre = lv_load_area.lv_load_area_centre # there's only one station: Replace Load Area centre by station in graph if lv_load_area.lv_grid_districts_count() == 1: # get station lv_station = list( lv_load_area.lv_grid_districts())[0].lv_grid.station() # get branches that are connected to Load Area centre branches = mv_grid_district.mv_grid.graph_branches_from_node( lv_load_area_centre) # connect LV station, delete Load Area centre for node, branch in branches: # backup kind and type of branch branch_kind = branch['branch'].kind branch_type = branch['branch'].type branch_ring = branch['branch'].ring # respect circuit breaker if existent circ_breaker = branch['branch'].circuit_breaker if circ_breaker is not None: branch[ 'branch'].circuit_breaker.geo_data = calc_geo_centre_point( lv_station, node) # delete old branch to Load Area centre and create a new one to LV station graph.remove_edge(lv_load_area_centre, node) branch_length = calc_geo_dist_vincenty(lv_station, node) branch = BranchDing0(length=branch_length, circuit_breaker=circ_breaker, kind=branch_kind, type=branch_type, ring=branch_ring) if circ_breaker is not None: circ_breaker.branch = branch graph.add_edge(lv_station, node, branch=branch) # delete Load Area centre from graph graph.remove_node(lv_load_area_centre) # there're more than one station: Do normal connection process (as in satellites) else: # connect LV stations of all grid districts # ========================================= for lv_grid_district in lv_load_area.lv_grid_districts(): # get branches that are partly or fully located in load area branches = calc_geo_branches_in_polygon( mv_grid_district.mv_grid, lv_load_area.geo_area, mode='intersects', proj=proj1) # filter branches that belong to satellites (load area groups) if Load Area is not a satellite # itself if not lv_load_area.is_satellite: branches_valid = [] for branch in branches: node1 = branch['adj_nodes'][0] node2 = branch['adj_nodes'][1] lv_load_area_group = get_lv_load_area_group_from_node_pair( node1, node2) # delete branch as possible conn. target if it belongs to a group (=satellite) or # if it belongs to a ring different from the ring of the current LVLA if (lv_load_area_group is None) and\ (branch['branch'].ring is lv_load_area.ring): branches_valid.append(branch) branches = branches_valid # find possible connection objects lv_station = lv_grid_district.lv_grid.station() lv_station_shp = transform(proj1, lv_station.geo_data) conn_objects_min_stack = find_nearest_conn_objects( lv_station_shp, branches, proj1, conn_dist_weight, debug, branches_only=False) # connect! connect_node(lv_station, lv_station_shp, mv_grid_district.mv_grid, conn_objects_min_stack[0], proj2, graph, conn_dist_ring_mod, debug) # Replace Load Area centre by cable distributor # ================================================ # create cable distributor and add it to grid cable_dist = MVCableDistributorDing0( geo_data=lv_load_area_centre.geo_data, grid=mv_grid_district.mv_grid) mv_grid_district.mv_grid.add_cable_distributor(cable_dist) # get branches that are connected to Load Area centre branches = mv_grid_district.mv_grid.graph_branches_from_node( lv_load_area_centre) # connect LV station, delete Load Area centre for node, branch in branches: # backup kind and type of branch branch_kind = branch['branch'].kind branch_type = branch['branch'].type branch_ring = branch['branch'].ring # respect circuit breaker if existent circ_breaker = branch['branch'].circuit_breaker if circ_breaker is not None: branch[ 'branch'].circuit_breaker.geo_data = calc_geo_centre_point( cable_dist, node) # delete old branch to Load Area centre and create a new one to LV station graph.remove_edge(lv_load_area_centre, node) branch_length = calc_geo_dist_vincenty(cable_dist, node) branch = BranchDing0(length=branch_length, circuit_breaker=circ_breaker, kind=branch_kind, type=branch_type, ring=branch_ring) if circ_breaker is not None: circ_breaker.branch = branch graph.add_edge(cable_dist, node, branch=branch) # delete Load Area centre from graph graph.remove_node(lv_load_area_centre) # Replace all overhead lines by cables # ==================================== # if grid's default type is overhead line if mv_grid_district.mv_grid.default_branch_kind == 'line': # get all branches in load area branches = calc_geo_branches_in_polygon( mv_grid_district.mv_grid, lv_load_area.geo_area, mode='contains', proj=proj1) # set type for branch in branches: branch[ 'branch'].kind = mv_grid_district.mv_grid.default_branch_kind_settle branch[ 'branch'].type = mv_grid_district.mv_grid.default_branch_type_settle return graph
def set_circuit_breakers(mv_grid, mode='load', debug=False): """ Calculates the optimal position of a circuit breaker on all routes of mv_grid, adds and connects them to graph. Args ---- mv_grid: MVGridDing0 Description#TODO debug: bool, defaults to False If True, information is printed during process Notes ----- According to planning principles of MV grids, a MV ring is run as two strings (half-rings) separated by a circuit breaker which is open at normal operation [#]_, [#]_. Assuming a ring (route which is connected to the root node at either sides), the optimal position of a circuit breaker is defined as the position (virtual cable) between two nodes where the conveyed current is minimal on the route. Instead of the peak current, the peak load is used here (assuming a constant voltage). If a ring is dominated by loads (peak load > peak capacity of generators), only loads are used for determining the location of circuit breaker. If generators are prevailing (peak load < peak capacity of generators), only generator capacities are considered for relocation. The core of this function (calculation of the optimal circuit breaker position) is the same as in ding0.grid.mv_grid.models.Route.calc_circuit_breaker_position but here it is 1. applied to a different data type (NetworkX Graph) and it 2. adds circuit breakers to all rings. The re-location of circuit breakers is necessary because the original position (calculated during routing with method mentioned above) shifts during the connection of satellites and therefore it is no longer valid. References ---------- .. [#] X. Tao, "Automatisierte Grundsatzplanung von Mittelspannungsnetzen", Dissertation, 2006 .. [#] FGH e.V.: "Technischer Bericht 302: Ein Werkzeug zur Optimierung der Störungsbeseitigung für Planung und Betrieb von Mittelspannungsnetzen", Tech. rep., 2008 """ # 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') # iterate over all rings and circuit breakers for ring, circ_breaker in zip(mv_grid.rings_nodes(include_root_node=False), mv_grid.circuit_breakers()): nodes_peak_load = [] nodes_peak_generation = [] # iterate over all nodes of ring for node in ring: # node is LV station -> get peak load and peak generation if isinstance(node, LVStationDing0): nodes_peak_load.append(node.peak_load / cos_phi_load) nodes_peak_generation.append(node.peak_generation / cos_phi_feedin) # node is cable distributor -> get all connected nodes of subtree using graph_nodes_from_subtree() elif isinstance(node, CableDistributorDing0): nodes_subtree = mv_grid.graph_nodes_from_subtree(node) nodes_subtree_peak_load = 0 nodes_subtree_peak_generation = 0 for node_subtree in nodes_subtree: # node is LV station -> get peak load and peak generation if isinstance(node_subtree, LVStationDing0): nodes_subtree_peak_load += node_subtree.peak_load / \ cos_phi_load nodes_subtree_peak_generation += node_subtree.peak_generation / \ cos_phi_feedin # node is LV station -> get peak load and peak generation if isinstance(node_subtree, GeneratorDing0): nodes_subtree_peak_generation += node_subtree.capacity / \ cos_phi_feedin nodes_peak_load.append(nodes_subtree_peak_load) nodes_peak_generation.append(nodes_subtree_peak_generation) else: raise ValueError('Ring node has got invalid type.') if mode == 'load': node_peak_data = nodes_peak_load elif mode == 'loadgen': # is ring dominated by load or generation? # (check if there's more load than generation in ring or vice versa) if sum(nodes_peak_load) > sum(nodes_peak_generation): node_peak_data = nodes_peak_load else: node_peak_data = nodes_peak_generation else: raise ValueError('parameter \'mode\' is invalid!') # calc optimal circuit breaker position # set init value diff_min = 10e6 # check where difference of demand/generation in two half-rings is minimal for ctr in range(len(node_peak_data)): # split route and calc demand difference route_data_part1 = sum(node_peak_data[0:ctr]) route_data_part2 = sum(node_peak_data[ctr:len(node_peak_data)]) diff = abs(route_data_part1 - route_data_part2) # equality has to be respected, otherwise comparison stops when demand/generation=0 if diff <= diff_min: diff_min = diff position = ctr else: break # relocate circuit breaker node1 = ring[position - 1] node2 = ring[position] circ_breaker.branch = mv_grid._graph.edge[node1][node2]['branch'] circ_breaker.branch_nodes = (node1, node2) circ_breaker.branch.circuit_breaker = circ_breaker circ_breaker.geo_data = calc_geo_centre_point(node1, node2) if debug: logger.debug('Ring: {}'.format(ring)) logger.debug('Circuit breaker {0} was relocated to edge {1}-{2} ' '(position on route={3})'.format( circ_breaker, node1, node2, position)) logger.debug('Peak load sum: {}'.format(sum(nodes_peak_load))) logger.debug('Peak loads: {}'.format(nodes_peak_load))