import os
import math
import numpy as np
import matplotlib.pyplot as plt
import networkx as nx
from pyproj import Proj, transform
import logging
logger = logging.getLogger('ding0')
from ding0.tools.logger import get_default_home_dir
from ding0.core.network.grids import MVGridDing0
use_gpd = False
use_ctx = False
if 'READTHEDOCS' not in os.environ:
use_gpd = True
try:
import geopandas as gpd
except:
use_gpd = False
use_ctx = True
try:
import contextily as ctx
except:
use_ctx = False
[docs]def plot_mv_topology(grid, subtitle='', filename=None, testcase='load',
line_color=None, node_color='type',
limits_cb_lines=None, limits_cb_nodes=None,
background_map=True):
""" Draws MV grid graph using networkx
Parameters
----------
grid : :obj:`MVGridDing0`
MV grid to plot.
subtitle : :obj:`str`
Extend plot's title by this string.
filename : :obj:`str`
If provided, the figure will be saved and not displayed (default path: ~/.ding0/).
A prefix is added to the file name.
testcase : :obj:`str`
Defines which case is to be used. Refer to config_calc.cfg to see further
assumptions for the cases. Possible options are:
* 'load' (default)
Heavy-load-flow case
* 'feedin'
Feedin-case
line_color : :obj:`str`
Defines whereby to choose line colors. Possible options are:
* 'loading'
Line color is set according to loading of the line in heavy load case.
You can use parameter `limits_cb_lines` to adjust the color range.
* None (default)
Lines are plotted in black. Is also the fallback option in case of
wrong input.
node_color : :obj:`str`
Defines whereby to choose node colors. Possible options are:
* 'type' (default)
Node color as well as size is set according to type of node
(generator, MV station, etc.). Is also the fallback option in case of
wrong input.
* 'voltage'
Node color is set according to voltage deviation from 1 p.u..
You can use parameter `limits_cb_nodes` to adjust the color range.
limits_cb_lines : :obj:`tuple`
Tuple with limits for colorbar of line color. First entry is the
minimum and second entry the maximum value. E.g. pass (0, 1) to
adjust the colorbar to 0..100% loading.
Default: None (min and max loading are used).
limits_cb_nodes : :obj:`tuple`
Tuple with limits for colorbar of nodes. First entry is the
minimum and second entry the maximum value. E.g. pass (0.9, 1) to
adjust the colorbar to 90%..100% voltage.
Default: None (min and max loading are used).
background_map : bool, optional
If True, a background map is plotted (default: stamen toner light).
The additional package `contextily` is needed for this functionality.
Default: True
Note
-----
WGS84 pseudo mercator (epsg:3857) is used as coordinate reference system (CRS).
Therefore, the drawn graph representation may be falsified!
"""
def set_nodes_style_and_position(nodes):
# TODO: MOVE settings to config
# node types (name of classes)
node_types = ['MVStationDing0',
'LVStationDing0',
'LVLoadAreaCentreDing0',
'MVCableDistributorDing0',
'GeneratorDing0',
'GeneratorFluctuatingDing0',
'CircuitBreakerDing0',
'n/a']
# node styles
colors_dict = {'MVStationDing0': '#f2ae00',
'LVStationDing0': 'grey',
'LVLoadAreaCentreDing0': '#fffc3d',
'MVCableDistributorDing0': '#000000',
'GeneratorDing0': '#00b023',
'GeneratorFluctuatingDing0': '#0078b0',
'CircuitBreakerDing0': '#c20000',
'n/a': 'orange'}
sizes_dict = {'MVStationDing0': 120,
'LVStationDing0': 7,
'LVLoadAreaCentreDing0': 30,
'MVCableDistributorDing0': 5,
'GeneratorDing0': 50,
'GeneratorFluctuatingDing0': 50,
'CircuitBreakerDing0': 50,
'n/a': 5}
zindex_by_type = {'MVStationDing0': 16,
'LVStationDing0': 12,
'LVLoadAreaCentreDing0': 11,
'MVCableDistributorDing0': 13,
'GeneratorDing0': 14,
'GeneratorFluctuatingDing0': 14,
'CircuitBreakerDing0': 15,
'n/a': 10}
# dict of node class names: list of nodes
nodes_by_type = {_: [] for _ in node_types}
# dict of node class names: list of node-individual color
node_colors_by_type = {_: [] for _ in node_types}
# dict of node class names: list of node-individual size
node_sizes_by_type = {_: [] for _ in node_types}
node_sizes_by_type['all'] = []
# dict of nodes:node-individual positions
nodes_pos = {}
for n in nodes:
if type(n).__name__ in node_types:
nodes_by_type[type(n).__name__].append(n)
node_colors_by_type[type(n).__name__].append(colors_dict[type(n).__name__])
node_sizes_by_type[type(n).__name__].append(sizes_dict[type(n).__name__])
node_sizes_by_type['all'].append(sizes_dict[type(n).__name__])
else:
nodes_by_type['n/a'].append(n)
node_colors_by_type['n/a'].append(colors_dict['n/a'])
node_sizes_by_type['n/a'].append(sizes_dict['n/a'])
node_sizes_by_type['all'].append(sizes_dict['n/a'])
nodes_pos[n] = (n.geo_data.x, n.geo_data.y)
return node_types, nodes_by_type, node_colors_by_type,\
node_sizes_by_type, zindex_by_type, nodes_pos
def reproject_nodes(nodes_pos, model_proj='4326'):
inProj = Proj(init='epsg:{srid}'.format(srid=model_proj))
outProj = Proj(init='epsg:3857')
nodes_pos2 = {}
for k, v in nodes_pos.items():
x2, y2 = transform(inProj, outProj,
v[0],
v[1])
nodes_pos2[k] = (x2, y2)
return nodes_pos2
def plot_background_map(ax):
url = ctx.sources.ST_TONER_LITE
xmin, xmax, ymin, ymax = ax.axis()
basemap, extent = ctx.bounds2img(xmin, ymin, xmax, ymax,
zoom=12, url=url)
ax.imshow(basemap, extent=extent, interpolation='bilinear', zorder=0)
ax.axis((xmin, xmax, ymin, ymax))
def plot_region_data(ax):
# get geoms of MV grid district, load areas and LV grid districts
mv_grid_district = gpd.GeoDataFrame({'geometry': grid.grid_district.geo_data},
crs={'init': 'epsg:{srid}'.format(srid=model_proj)})
load_areas = gpd.GeoDataFrame({'geometry': [la.geo_area for la in grid.grid_district.lv_load_areas()]},
crs={'init': 'epsg:{srid}'.format(srid=model_proj)})
lv_grid_districts = gpd.GeoDataFrame({'geometry': [lvgd.geo_data
for la in grid.grid_district.lv_load_areas()
for lvgd in la.lv_grid_districts()]},
crs={'init': 'epsg:{srid}'.format(srid=model_proj)})
# reproject to WGS84 pseudo mercator
mv_grid_district = mv_grid_district.to_crs(epsg=3857)
load_areas = load_areas.to_crs(epsg=3857)
lv_grid_districts = lv_grid_districts.to_crs(epsg=3857)
# plot
mv_grid_district.plot(ax=ax, color='#ffffff', alpha=0.2, edgecolor='k', linewidth=2, zorder=2)
load_areas.plot(ax=ax, color='#fffea3', alpha=0.1, edgecolor='k', linewidth=0.5, zorder=3)
lv_grid_districts.plot(ax=ax, color='#ffffff', alpha=0.05, edgecolor='k', linewidth=0.5, zorder=4)
if not isinstance(grid, MVGridDing0):
logger.warning('Sorry, but plotting is currently only available for MV grids but you did not pass an'
'instance of MVGridDing0. Plotting is skipped.')
return
g = grid.graph
model_proj = grid.network.config['geo']['srid']
if testcase == 'feedin':
case_idx = 1
else:
case_idx = 0
nodes_types, nodes_by_type, node_colors_by_type, node_sizes_by_type, zindex_by_type, nodes_pos =\
set_nodes_style_and_position(g.nodes())
# reproject to WGS84 pseudo mercator
nodes_pos = reproject_nodes(nodes_pos, model_proj=model_proj)
plt.figure(figsize=(9, 6))
ax = plt.gca()
if line_color == 'loading':
edges_color = []
for n1, n2 in g.edges():
edge = g.adj[n1][n2]
if hasattr(edge['branch'], 's_res'):
edges_color.append(edge['branch'].s_res[case_idx] * 1e3 /
(3 ** 0.5 * edge['branch'].type['U_n'] * edge['branch'].type['I_max_th']))
else:
edges_color.append(0)
edges_cmap = plt.get_cmap('jet')
#edges_cmap.set_over('#952eff')
else:
edges_color = ['black'] * len(list(grid.graph_edges()))
edges_cmap = None
# plot nodes by voltage
if node_color == 'voltage':
voltage_station = grid._station.voltage_res[case_idx]
nodes_color = []
for n in g.nodes():
if hasattr(n, 'voltage_res'):
nodes_color.append(n.voltage_res[case_idx])
else:
nodes_color.append(voltage_station)
if testcase == 'feedin':
nodes_cmap = plt.get_cmap('Reds')
nodes_vmax = voltage_station + float(grid.network.config
['mv_routing_tech_constraints']
['mv_max_v_level_fc_diff_normal'])
nodes_vmin = voltage_station
else:
nodes_cmap = plt.get_cmap('Reds_r')
nodes_vmin = voltage_station - float(grid.network.config
['mv_routing_tech_constraints']
['mv_max_v_level_lc_diff_normal'])
nodes_vmax = voltage_station
nodes = nx.draw_networkx_nodes(g,
pos=nodes_pos,
node_color=nodes_color,
# node_shape='o', # TODO: Add additional symbols here
cmap=nodes_cmap,
vmin=nodes_vmin,
vmax=nodes_vmax,
node_size=node_sizes_by_type['all'],
linewidths=0.25,
ax=ax)
nodes.set_zorder(10)
nodes.set_edgecolor('k')
# colorbar nodes
if limits_cb_nodes is None:
limits_cb_nodes = (math.floor(min(nodes_color)*100)/100,
math.ceil(max(nodes_color)*100)/100)
v_range = np.linspace(limits_cb_nodes[0], limits_cb_nodes[1], 101)
cb_voltage = plt.colorbar(nodes, boundaries=v_range,
ticks=v_range[0:101:10],
fraction=0.04, pad=0.1)
cb_voltage.set_clim(vmin=limits_cb_nodes[0],
vmax=limits_cb_nodes[1])
cb_voltage.set_label('Node voltage deviation in %', size='smaller')
cb_voltage.ax.tick_params(labelsize='smaller')
# plot nodes by type
else:
for node_type in nodes_types:
if len(nodes_by_type[node_type]) != 0:
nodes = nx.draw_networkx_nodes(g,
nodelist=nodes_by_type[node_type],
pos=nodes_pos,
# node_shape='o', # TODO: Add additional symbols here
node_color=node_colors_by_type[node_type],
cmap=None,
vmin=None,
vmax=None,
node_size=node_sizes_by_type[node_type],
linewidths=0.25,
label=node_type,
ax=ax)
nodes.set_zorder(zindex_by_type[node_type])
nodes.set_edgecolor('k')
edges = nx.draw_networkx_edges(g,
pos=nodes_pos,
edge_color=edges_color,
edge_cmap=edges_cmap,
edge_vmin=0,
edge_vmax=1,
#width=1,
ax=ax)
edges.set_zorder(5)
if line_color == 'loading':
# colorbar edges
if limits_cb_lines is None:
limits_cb_lines = (math.floor(min(edges_color)*100)/100,
math.ceil(max(edges_color)*100)/100)
loading_range = np.linspace(limits_cb_lines[0], limits_cb_lines[1], 101)
cb_loading = plt.colorbar(edges, boundaries=loading_range,
ticks=loading_range[0:101:10],
fraction=0.04, pad=0.04)
cb_loading.set_clim(vmin=limits_cb_lines[0],
vmax=limits_cb_lines[1])
cb_loading.set_label('Line loading in % of nominal capacity', size='smaller')
cb_loading.ax.tick_params(labelsize='smaller')
if use_ctx and background_map:
plot_background_map(ax=ax)
if use_gpd:
plot_region_data(ax=ax)
plt.legend(fontsize=7)
plt.title('MV Grid District {id} - {st}'.format(id=grid.id_db,
st=subtitle))
# hide axes labels (coords)
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
if filename is None:
plt.tight_layout()
plt.show()
else:
path = os.path.join(get_default_home_dir(), 'ding0_grid_{id}_{filename}'.format(id=str(grid.id_db),
filename=filename))
plt.savefig(path, dpi=300, bbox_inches='tight')
plt.close()
logger.info('==> Figure saved to {path}'.format(path=path))