"""This file is part of DING0, the DIstribution Network GeneratOr.
DING0 is a tool to generate synthetic medium and low voltage power
distribution grids based on open data.
It is developed in the project open_eGo: https://openegoproject.wordpress.com
DING0 lives at github: https://github.com/openego/ding0/
The documentation is available on RTD: http://ding0.readthedocs.io"""
__copyright__ = "Reiner Lemoine Institut gGmbH"
__license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)"
__url__ = "https://github.com/openego/ding0/blob/master/LICENSE"
__author__ = "nesnoj, gplssm"
import os
import time
import logging
from pyproj import Transformer
from ding0.core.network.stations import *
from ding0.core.network import BranchDing0, GeneratorDing0
from ding0.core import MVCableDistributorDing0
from ding0.core.structure.groups import LoadAreaGroupDing0
from ding0.core.structure.regions import LVLoadAreaCentreDing0
from ding0.tools import config as cfg_ding0
from ding0.tools.geo import calc_geo_branches_in_buffer,calc_geo_dist,\
calc_geo_centre_point, calc_geo_branches_in_polygon
if not 'READTHEDOCS' in os.environ:
from shapely.geometry import LineString
from shapely.ops import transform
logger = logging.getLogger('ding0')
[docs]def find_nearest_conn_objects(node_shp, branches, proj, conn_dist_weight, debug, branches_only=False):
"""Searches all `branches` for the nearest possible connection object per branch.
Picks out 1 object out of 3 possible objects:
* 2 branch-adjacent stations and
* 1 potentially created cable distributor on the line (perpendicular projection)).
The resulting stack (list) is sorted ascending by distance from node.
Parameters
----------
node_shp: :shapely:`Shapely Point object<points>`
Shapely Point object of node
branches: BranchDing0
BranchDing0 objects of MV region
proj: :pyproj:`pyproj Proj object< >`
nodes' CRS to equidistant CRS (e.g. WGS84 -> ETRS)
conn_dist_weight: float
length weighting to prefer stations instead of direct line connection.
debug: bool
If True, information is printed during process
branches_only: bool, defaults to False
If True, only branch objects are considered as connection objects
Returns
-------
:obj:`list`
List of connection objects.
Each object is represented by dict with Ding0 object,
shapely object, and distance to node.
See Also
--------
mv_connect_satellites : for details on `conn_dist_weight` param
"""
# threshold which is used to determine if 2 objects are on the same position (see below for details on usage)
conn_diff_tolerance = cfg_ding0.get('mv_routing', 'conn_diff_tolerance')
conn_objects_min_stack = []
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(proj, stations[0].geo_data)
station2_shp = transform(proj, stations[1].geo_data)
line_shp = LineString([station1_shp, station2_shp])
# create dict with DING0 objects (line & 2 adjacent stations), shapely objects and distances
if not branches_only:
conn_objects = {'s1': {'obj': stations[0],
'shp': station1_shp,
'dist': node_shp.distance(station1_shp) * conn_dist_weight * 0.999},
's2': {'obj': stations[1],
'shp': station2_shp,
'dist': node_shp.distance(station2_shp) * conn_dist_weight * 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'], MVStationDing0):
del conn_objects['s1']
elif isinstance(conn_objects['s2']['obj'], MVStationDing0):
del conn_objects['s2']
else:
conn_objects = {'b': {'obj': branch,
'shp': line_shp,
'dist': node_shp.distance(line_shp)}}
# 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'])
#if not branches_only:
# conn_objects_min_stack.append(conn_objects_min)
#elif isinstance(conn_objects_min['shp'], LineString):
# conn_objects_min_stack.append(conn_objects_min)
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'])]
if debug:
logger.debug('Stack length: {}'.format(len(conn_objects_min_stack)))
return conn_objects_min_stack
[docs]def get_lv_load_area_group_from_node_pair(node1, node2):
lv_load_area_group = None
# both nodes are LV stations -> get group from 1 or 2
if (isinstance(node1, LVLoadAreaCentreDing0) and
isinstance(node2, LVLoadAreaCentreDing0)):
if not node1.lv_load_area.lv_load_area_group:
lv_load_area_group = node2.lv_load_area.lv_load_area_group
else:
lv_load_area_group = node1.lv_load_area.lv_load_area_group
# node 1 is LV station and node 2 not -> get group from node 1
elif (isinstance(node1, LVLoadAreaCentreDing0) and
isinstance(node2, (MVStationDing0, MVCableDistributorDing0))):
lv_load_area_group = node1.lv_load_area.lv_load_area_group
# node 2 is LV station and node 1 not -> get group from node 2
elif (isinstance(node1, (MVStationDing0, MVCableDistributorDing0)) and
isinstance(node2, LVLoadAreaCentreDing0)):
lv_load_area_group = node2.lv_load_area.lv_load_area_group
# both nodes are not a LV station -> no group
elif (isinstance(node1, (MVStationDing0, MVCableDistributorDing0)) and
isinstance(node2, (MVStationDing0, MVCableDistributorDing0))):
lv_load_area_group = None
return lv_load_area_group
[docs]def find_connection_point(node, node_shp, graph, proj, conn_objects_min_stack, conn_dist_ring_mod, debug):
""" Goes through the possible target connection objects in `conn_objects_min_stack` (from nearest to most far
object) and tries to connect `node` to one of them.
Function searches from nearest to most far object.
Parameters
----------
node: LVLoadAreaCentreDing0, i.e.
Origin node - Ding0 graph object (e.g. LVLoadAreaCentreDing0)
node_shp: :shapely:`Shapely Point object<points>`
Shapely Point object of node
graph: :networkx:`NetworkX Graph Obj< >`
NetworkX graph object with nodes
proj: :pyproj:`pyproj Proj object< >`
equidistant CRS to conformal CRS (e.g. ETRS -> WGS84)
conn_objects_min_stack: :obj:`list`
List of connection objects.
Each object is represented by dict with Ding0 object, shapely object,
and distance to node, sorted ascending by distance.
conn_dist_ring_mod: type
Max. distance when nodes are included into route instead of creating a
new line.
debug: bool
If True, information is printed during process
See Also
--------
ding0.grid.mv_grid.mv_connect : for details on the `conn_dist_ring_mod` parameter.
"""
node_connected = False
# go through the stack (from nearest to most far connection target object)
for dist_min_obj in conn_objects_min_stack:
nodes_are_members_of_ring = False
# target object is branch
if isinstance(dist_min_obj['shp'], LineString):
# rename for readability
node1 = dist_min_obj['obj']['adj_nodes'][0]
node2 = dist_min_obj['obj']['adj_nodes'][1]
lv_load_area_group = get_lv_load_area_group_from_node_pair(node1, node2)
# check if target branch belongs to a main ring
nodes_are_members_of_ring = any(node1 in ring and node2 in ring for ring in node.grid.rings_nodes())
branch_ring = dist_min_obj['obj']['branch'].ring
# target object is node
else:
if isinstance(dist_min_obj['obj'], MVCableDistributorDing0):
lv_load_area_group = dist_min_obj['obj'].lv_load_area_group
else:
lv_load_area_group = dist_min_obj['obj'].lv_load_area.lv_load_area_group
# target object doesn't belong to a satellite string (is not a member of a Load Area group)
if not lv_load_area_group:
# connect node
target_obj_result = connect_node(node,
node_shp,
node.lv_load_area.mv_grid_district.mv_grid,
dist_min_obj,
proj,
graph,
conn_dist_ring_mod,
debug)
# if node was connected via branch (target line not re-routed and not member of aggregated load area):
# create new LV load_area group for current node
if (target_obj_result is not None) and (target_obj_result != 're-routed'):
lv_load_area_group = LoadAreaGroupDing0(mv_grid_district=node.lv_load_area.mv_grid_district,
root_node=target_obj_result)
lv_load_area_group.add_lv_load_area(lv_load_area=node.lv_load_area)
node.lv_load_area.lv_load_area_group = lv_load_area_group
node.lv_load_area.mv_grid_district.add_lv_load_area_group(lv_load_area_group)
if debug:
logger.debug('New LV load_area group {} created!'.format(
lv_load_area_group))
# node connected, stop connection for current node
node_connected = True
break
# node was inserted into line (target line was re-routed)
elif target_obj_result == 're-routed':
# if main ring was re-routed to include node => node is not a satellite anymore
if nodes_are_members_of_ring:
node.lv_load_area.is_satellite = False
node.lv_load_area.ring = branch_ring
# node connected, stop connection for current node
node_connected = True
break
# target object is member of a Load Area group
else:
# connect node
target_obj_result = connect_node(node,
node_shp,
node.lv_load_area.mv_grid_district.mv_grid,
dist_min_obj,
proj,
graph,
conn_dist_ring_mod,
debug)
# if node was connected via branch (target line not re-routed and not member of aggregated load area):
# create new LV load_area group for current node
if (target_obj_result is not None) and (target_obj_result != 're-routed'):
# node can join LV load_area group
if lv_load_area_group.can_add_lv_load_area(node=node):
# add node to LV load_area group
lv_load_area_group.add_lv_load_area(lv_load_area=node.lv_load_area)
node.lv_load_area.lv_load_area_group = lv_load_area_group
if isinstance(target_obj_result, MVCableDistributorDing0):
lv_load_area_group.add_lv_load_area(lv_load_area=target_obj_result)
target_obj_result.lv_load_area_group = lv_load_area_group
if debug:
logger.debug('LV load_area group {} joined!'.format(
lv_load_area_group))
# node connected, stop connection for current node
node_connected = True
break
# cannot join LV load_area group
else:
if debug:
logger.debug('Node {0} could not be added to '
'load_area group {1}'.format(
node, lv_load_area_group))
# rollback changes in graph
disconnect_node(node, target_obj_result, graph, debug)
# continue with next possible connection point
continue
# node was inserted into line (target line was re-routed)
elif target_obj_result == 're-routed':
# add node to LV load_area group
lv_load_area_group.add_lv_load_area(lv_load_area=node.lv_load_area)
node.lv_load_area.lv_load_area_group = lv_load_area_group
# if main ring was re-routed to include node => node is not a satellite anymore
if nodes_are_members_of_ring:
node.lv_load_area.is_satellite = False
node.lv_load_area.ring = branch_ring
# node inserted into existing route, stop connection for current node
node_connected = True
break
# else: node could not be connected because target object belongs to load area of aggregated type
if not node_connected and debug:
logger.debug(
'Node {} could not be connected, try to increase the parameter '
'`load_area_sat_buffer_radius` in config file `config_calc.cfg` '
'to gain more possible connection points.'.format(node))
[docs]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(adj_node1, node)
branch = BranchDing0(length=branch_length,
circuit_breaker=circ_breaker,
kind=branch_kind,
grid=mv_grid,
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(adj_node2, node)
graph.add_edge(adj_node2, node, branch=BranchDing0(length=branch_length,
kind=branch_kind,
grid=mv_grid,
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(adj_node1, cable_dist)
branch = BranchDing0(length=branch_length,
circuit_breaker=circ_breaker,
kind=branch_kind,
grid=mv_grid,
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(adj_node2, cable_dist)
graph.add_edge(adj_node2, cable_dist, branch=BranchDing0(length=branch_length,
kind=branch_kind,
grid=mv_grid,
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(node, cable_dist)
graph.add_edge(node, cable_dist, branch=BranchDing0(length=branch_length,
kind=branch_kind,
grid=mv_grid,
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(node, target_obj['obj'])
graph.add_edge(node, target_obj['obj'], branch=BranchDing0(length=branch_length,
kind=branch_kind,
grid=mv_grid,
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
[docs]def disconnect_node(node, target_obj_result, graph, debug):
""" Disconnects `node` from `target_obj`
Parameters
----------
node: LVLoadAreaCentreDing0, i.e.
Origin node - Ding0 graph object (e.g. LVLoadAreaCentreDing0)
target_obj_result: LVLoadAreaCentreDing0, i.e.
Origin node - Ding0 graph object (e.g. LVLoadAreaCentreDing0)
graph: :networkx:`NetworkX Graph Obj< >`
NetworkX graph object with nodes and newly created branches
debug: bool
If True, information is printed during process
"""
# backup kind and type of branch
branch_kind = graph.adj[node][target_obj_result]['branch'].kind
branch_type = graph.adj[node][target_obj_result]['branch'].type
branch_ring = graph.adj[node][target_obj_result]['branch'].ring
graph.remove_edge(node, target_obj_result)
if isinstance(target_obj_result, MVCableDistributorDing0):
neighbor_nodes = list(graph.neighbors(target_obj_result))
if len(neighbor_nodes) == 2:
node.grid.remove_cable_distributor(target_obj_result)
branch_length = calc_geo_dist(neighbor_nodes[0], neighbor_nodes[1])
graph.add_edge(neighbor_nodes[0], neighbor_nodes[1], branch=BranchDing0(length=branch_length,
kind=branch_kind,
grid=node.grid,
type=branch_type,
ring=branch_ring))
if debug:
logger.debug('disconnect edge {0}-{1}'.format(node, target_obj_result))
[docs]def parametrize_lines(mv_grid):
""" Set unparametrized branches to default branch type
Parameters
----------
mv_grid: MVGridDing0
MV grid instance
Note
-----
During the connection process of satellites, new branches are created -
these have to be parametrized.
"""
for branch in mv_grid.graph_edges():
if branch['branch'].kind is None:
branch['branch'].kind = mv_grid.default_branch_kind
if branch['branch'].type is None:
branch['branch'].type = mv_grid.default_branch_type
[docs]def mv_connect_satellites(mv_grid, graph, mode='normal', debug=False):
""" Connect satellites (small Load Areas) to MV grid
Parameters
----------
mv_grid: MVGridDing0
MV grid instance
graph: :networkx:`NetworkX Graph Obj< >`
NetworkX graph object with nodes
mode: :obj:`str`, defaults to 'normal'
Specify mode how satellite `LVLoadAreaCentreDing0` are connected to the
grid. Mode normal (default) considers for restrictions like max.
string length, max peak load per string.
The mode 'isolated' disregards any connection restrictions and connects
the node `LVLoadAreaCentreDing0` to the next connection point.
debug: bool, defaults to False
If True, information is printed during process
Note
-----
conn_dist_weight: The satellites can be connected to line (new terminal is
created) or to one station where the line ends, depending on the distance
from satellite to the objects. This threshold is a length weighting to
prefer stations instead of direct line connection to respect grid planning
principles.
Example: The distance from satellite to line is 1km, to station1 1.2km, to
station2 2km. With conn_dist_threshold=0.75, the 'virtual' distance to
station1 would be 1.2km * 0.75 = 0.9km, so this conn. point would be
preferred.
Returns
-------
:networkx:`NetworkX Graph Obj< >`
NetworkX graph object with nodes and newly created branches
"""
# conn_dist_weight: The satellites can be connected to line (new terminal is created) or to one station where the
# line ends, depending on the distance from satellite to the objects. This threshold is a length weighting to prefer
# stations instead of direct line connection to respect grid planning principles.
# Example: The distance from satellite to line is 1km, to station1 1.2km, to station2 2km.
# With conn_dist_threshold=0.75, the 'virtual' distance to station1 would be 1.2km * 0.75 = 0.9km, so this conn.
# point would be preferred.
conn_dist_weight = cfg_ding0.get('mv_connect', 'load_area_sat_conn_dist_weight')
# conn_dist_ring_mod: Allow re-routing of ring main route if node is closer than this threshold (in m) to ring.
conn_dist_ring_mod = cfg_ding0.get('mv_connect', 'load_area_sat_conn_dist_ring_mod')
load_area_sat_buffer_radius = cfg_ding0.get('mv_connect', 'load_area_sat_buffer_radius')
load_area_sat_buffer_radius_inc = cfg_ding0.get('mv_connect', 'load_area_sat_buffer_radius_inc')
start = time.time()
# WGS84 (conformal) to ETRS (equidistant) projection
proj1 = Transformer.from_crs("epsg:4326", "epsg:3035", always_xy=True).transform
# ETRS (equidistant) to WGS84 (conformal) projection
proj2 = Transformer.from_crs("epsg:3035", "epsg:4326", always_xy=True).transform
# check all nodes
if mode == 'normal':
#nodes = sorted(graph.nodes(), key=lambda x: repr(x))
nodes = mv_grid.graph_isolated_nodes()
elif mode == 'isolated':
nodes = mv_grid.graph_isolated_nodes()
else:
raise ValueError('\'mode\' is invalid.')
for node in nodes:
# node is Load Area centre
if isinstance(node, LVLoadAreaCentreDing0):
# satellites only
if node.lv_load_area.is_satellite:
node_shp = transform(proj1, node.geo_data)
if mode == 'normal':
# get branches within a the predefined radius `load_area_sat_buffer_radius`
branches = calc_geo_branches_in_buffer(node,
mv_grid,
load_area_sat_buffer_radius,
load_area_sat_buffer_radius_inc, proj1)
elif mode == 'isolated':
# get nodes of all MV rings
nodes = set()
[nodes.update(ring_nodes) for ring_nodes in list(mv_grid.rings_nodes(include_root_node=True))]
nodes = list(nodes)
# get branches of these nodes
branches = []
[branches.append(mv_grid.graph_branches_from_node(node_branches)) for node_branches in nodes]
# reformat branches
branches = [_ for _ in list(mv_grid.graph_edges())
if (_['adj_nodes'][0] in nodes and _['adj_nodes'][1] in nodes)]
# calc distance between node and grid's lines -> find nearest line
conn_objects_min_stack = find_nearest_conn_objects(node_shp, branches, proj1,
conn_dist_weight, debug,
branches_only=False)
# iterate over object stack
find_connection_point(node, node_shp, graph, proj2, conn_objects_min_stack,
conn_dist_ring_mod, debug)
# parametrize newly created branches
parametrize_lines(mv_grid)
if debug:
logger.debug('Elapsed time (mv_connect): {}'.format(time.time() - start))
return graph
[docs]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 = Transformer.from_crs("epsg:4326", "epsg:3035", always_xy=True).transform
# ETRS (equidistant) to WGS84 (conformal) projection
proj2 = Transformer.from_crs("epsg:3035", "epsg:4326", always_xy=True).transform
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(lv_station, node)
branch = BranchDing0(length=branch_length,
circuit_breaker=circ_breaker,
kind=branch_kind,
grid=mv_grid_district.mv_grid,
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(cable_dist, node)
branch = BranchDing0(length=branch_length,
circuit_breaker=circ_breaker,
kind=branch_kind,
grid=mv_grid_district.mv_grid,
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
[docs]def mv_connect_generators(mv_grid_district, graph, debug=False):
"""Connect MV generators 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
"""
generator_buffer_radius = cfg_ding0.get('mv_connect', 'generator_buffer_radius')
generator_buffer_radius_inc = cfg_ding0.get('mv_connect', 'generator_buffer_radius_inc')
# WGS84 (conformal) to ETRS (equidistant) projection
proj1 = Transformer.from_crs("epsg:4326", "epsg:3035", always_xy=True).transform
# ETRS (equidistant) to WGS84 (conformal) projection
proj2 = Transformer.from_crs("epsg:3035", "epsg:4326", always_xy=True).transform
for generator in sorted(mv_grid_district.mv_grid.generators(), key=lambda x: repr(x)):
# ===== voltage level 4: generator has to be connected to MV station =====
if generator.v_level == 4:
mv_station = mv_grid_district.mv_grid.station()
branch_length = calc_geo_dist(generator, mv_station)
# TODO: set branch type to something reasonable (to be calculated)
branch_kind = mv_grid_district.mv_grid.default_branch_kind
branch_type = mv_grid_district.mv_grid.default_branch_type
branch = BranchDing0(length=branch_length,
kind=branch_kind,
grid=mv_grid_district.mv_grid,
type=branch_type,
ring=None)
graph.add_edge(generator, mv_station, branch=branch)
if debug:
logger.debug('Generator {0} was connected to {1}'.format(
generator, mv_station))
# ===== voltage level 5: generator has to be connected to MV grid (next-neighbor) =====
elif generator.v_level == 5:
generator_shp = transform(proj1, generator.geo_data)
# get branches within a the predefined radius `generator_buffer_radius`
branches = calc_geo_branches_in_buffer(generator,
mv_grid_district.mv_grid,
generator_buffer_radius,
generator_buffer_radius_inc, proj1)
# calc distance between generator and grid's lines -> find nearest line
conn_objects_min_stack = find_nearest_conn_objects(generator_shp,
branches,
proj1,
conn_dist_weight=1,
debug=debug,
branches_only=False)
# 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:
# Note 1: conn_dist_ring_mod=0 to avoid re-routing of existent lines
# Note 2: In connect_node(), the default cable/line type of grid is used. This is reasonable since
# the max. allowed power of the smallest possible cable/line type (3.64 MVA for overhead
# line of type 48-AL1/8-ST1A) exceeds the max. allowed power of a generator (4.5 MVA (dena))
# (if connected separately!)
target_obj_result = connect_node(generator,
generator_shp,
mv_grid_district.mv_grid,
dist_min_obj,
proj2,
graph,
conn_dist_ring_mod=0,
debug=debug)
if target_obj_result is not None:
if debug:
logger.debug(
'Generator {0} was connected to {1}'.format(
generator, target_obj_result))
generator_connected = True
break
if not generator_connected and debug:
logger.debug(
'Generator {0} could not be connected, try to '
'increase the parameter `generator_buffer_radius` in '
'config file `config_calc.cfg` to gain more possible '
'connection points.'.format(generator))
return graph