From be8c9798c1dbffef5c470e707da2e0a0141c1e09 Mon Sep 17 00:00:00 2001 From: Jonas Hoersch Date: Mon, 12 Aug 2019 12:42:33 +0200 Subject: [PATCH] plot_network: Split plotting into several functions to make sphinx happy Also addresses part of issue #50: plot_map works again with cartopy. --- scripts/plot_network.py | 394 +++++++++++++++++++++------------------- 1 file changed, 207 insertions(+), 187 deletions(-) diff --git a/scripts/plot_network.py b/scripts/plot_network.py index 9ea4bb47..4c666f55 100644 --- a/scripts/plot_network.py +++ b/scripts/plot_network.py @@ -15,27 +15,6 @@ Description """ -# Dirty work-around so that sphinx can import this module and get the -# doc-string should be refactored in the style of the other scripts, ideally -# several functions for the different plots -if __name__ != "__main__": - import sys - sys.exit(0) - -if 'snakemake' not in globals(): - from vresutils.snakemake import MockSnakemake, Dict - from snakemake.rules import expand - import yaml - snakemake = Dict() - snakemake = MockSnakemake( - path='..', - wildcards=dict(network='elec', simpl='', clusters='90', lv='1.25', opts='Co2L-3H', attr='p_nom', ext="pdf"), - input=dict(network="results/networks/{network}_s{simpl}_{clusters}_lv{lv}_{opts}.nc", - tech_costs="data/costs.csv"), - output=dict(only_map="results/plots/{network}_s{simpl}_{clusters}_lv{lv}_{opts}_{attr}.{ext}", - ext="results/plots/{network}_s{simpl}_{clusters}_lv{lv}_{opts}_{attr}_ext.{ext}") - ) - import pypsa from _helpers import load_network, aggregate_p, aggregate_costs @@ -50,7 +29,9 @@ from itertools import product, chain from six.moves import map, zip from six import itervalues, iterkeys from collections import OrderedDict as odict +import logging +import cartopy.crs as ccrs import matplotlib.pyplot as plt import matplotlib as mpl from matplotlib.patches import Circle, Ellipse @@ -83,203 +64,242 @@ def make_handler_map_to_scale_circles_as_in(ax, dont_resize_actively=False): def make_legend_circles_for(sizes, scale=1.0, **kw): return [Circle((0,0), radius=(s/scale)**0.5, **kw) for s in sizes] -plt.style.use(['classic', 'seaborn-white', - {'axes.grid': False, 'grid.linestyle': '--', 'grid.color': u'0.6', - 'hatch.color': 'white', - 'patch.linewidth': 0.5, - 'font.size': 12, - 'legend.fontsize': 'medium', - 'lines.linewidth': 1.5, - 'pdf.fonttype': 42, - # 'font.family': 'Times New Roman' - }]) +def set_plot_style(): + plt.style.use(['classic', 'seaborn-white', + {'axes.grid': False, 'grid.linestyle': '--', 'grid.color': u'0.6', + 'hatch.color': 'white', + 'patch.linewidth': 0.5, + 'font.size': 12, + 'legend.fontsize': 'medium', + 'lines.linewidth': 1.5, + 'pdf.fonttype': 42, + # 'font.family': 'Times New Roman' + }]) -opts = snakemake.config['plotting'] -map_figsize = opts['map']['figsize'] -map_boundaries = opts['map']['boundaries'] +def plot_map(n, ax=None, attribute='p_nom', opts={}): + if ax is None: + ax = plt.gca() -n = load_network(snakemake.input.network, snakemake.input.tech_costs, snakemake.config) + ## DATA + line_colors = {'cur': "purple", + 'exp': to_rgba("red", 0.7)} + tech_colors = opts['tech_colors'] -scenario_opts = snakemake.wildcards.opts.split('-') - -## DATA -line_colors = {'cur': "purple", - 'exp': to_rgba("red", 0.7)} -tech_colors = opts['tech_colors'] - -if snakemake.wildcards.attr == 'p_nom': - # bus_sizes = n.generators_t.p.sum().loc[n.generators.carrier == "load"].groupby(n.generators.bus).sum() - bus_sizes = pd.concat((n.generators.query('carrier != "load"').groupby(['bus', 'carrier']).p_nom_opt.sum(), - n.storage_units.groupby(['bus', 'carrier']).p_nom_opt.sum())) - line_widths_exp = dict(Line=n.lines.s_nom_opt, Link=n.links.p_nom_opt) - line_widths_cur = dict(Line=n.lines.s_nom_min, Link=n.links.p_nom_min) -else: - raise 'plotting of {} has not been implemented yet'.format(plot) + if attribute == 'p_nom': + # bus_sizes = n.generators_t.p.sum().loc[n.generators.carrier == "load"].groupby(n.generators.bus).sum() + bus_sizes = pd.concat((n.generators.query('carrier != "load"').groupby(['bus', 'carrier']).p_nom_opt.sum(), + n.storage_units.groupby(['bus', 'carrier']).p_nom_opt.sum())) + line_widths_exp = dict(Line=n.lines.s_nom_opt, Link=n.links.p_nom_opt) + line_widths_cur = dict(Line=n.lines.s_nom_min, Link=n.links.p_nom_min) + else: + raise 'plotting of {} has not been implemented yet'.format(plot) -line_colors_with_alpha = \ -dict(Line=(line_widths_cur['Line'] / n.lines.s_nom > 1e-3) - .map({True: line_colors['cur'], False: to_rgba(line_colors['cur'], 0.)}), - Link=(line_widths_cur['Link'] / n.links.p_nom > 1e-3) - .map({True: line_colors['cur'], False: to_rgba(line_colors['cur'], 0.)})) + line_colors_with_alpha = \ + dict(Line=(line_widths_cur['Line'] / n.lines.s_nom > 1e-3) + .map({True: line_colors['cur'], False: to_rgba(line_colors['cur'], 0.)}), + Link=(line_widths_cur['Link'] / n.links.p_nom > 1e-3) + .map({True: line_colors['cur'], False: to_rgba(line_colors['cur'], 0.)})) -## FORMAT -linewidth_factor = opts['map'][snakemake.wildcards.attr]['linewidth_factor'] -bus_size_factor = opts['map'][snakemake.wildcards.attr]['bus_size_factor'] + ## FORMAT + linewidth_factor = opts['map'][attribute]['linewidth_factor'] + bus_size_factor = opts['map'][attribute]['bus_size_factor'] -## PLOT -fig, ax = plt.subplots(figsize=map_figsize) -n.plot(line_widths=pd.concat(line_widths_exp)/linewidth_factor, - line_colors=dict(Line=line_colors['exp'], Link=line_colors['exp']), - bus_sizes=bus_sizes/bus_size_factor, - bus_colors=tech_colors, - boundaries=map_boundaries, - basemap=True, - ax=ax) -n.plot(line_widths=pd.concat(line_widths_cur)/linewidth_factor, - line_colors=pd.concat(line_colors_with_alpha), - bus_sizes=0, - bus_colors=tech_colors, - boundaries=map_boundaries, - basemap=False, - ax=ax) -ax.set_aspect('equal') -ax.axis('off') + ## PLOT + n.plot(line_widths=pd.concat(line_widths_exp)/linewidth_factor, + line_colors=dict(Line=line_colors['exp'], Link=line_colors['exp']), + bus_sizes=bus_sizes/bus_size_factor, + bus_colors=tech_colors, + boundaries=map_boundaries, + geomap=True, + ax=ax) + n.plot(line_widths=pd.concat(line_widths_cur)/linewidth_factor, + line_colors=pd.concat(line_colors_with_alpha), + bus_sizes=0, + bus_colors=tech_colors, + boundaries=map_boundaries, + geomap=True, # TODO : Turn to False, after the release of PyPSA 0.14.2 (refer to https://github.com/PyPSA/PyPSA/issues/75) + ax=ax) + ax.set_aspect('equal') + ax.axis('off') -# x1, y1, x2, y2 = map_boundaries -# ax.set_xlim(x1, x2) -# ax.set_ylim(y1, y2) + # x1, y1, x2, y2 = map_boundaries + # ax.set_xlim(x1, x2) + # ax.set_ylim(y1, y2) -# Rasterize basemap -for c in ax.collections[:2]: c.set_rasterized(True) + # Rasterize basemap + # TODO : Check if this also works with cartopy + for c in ax.collections[:2]: c.set_rasterized(True) -# LEGEND -handles = [] -labels = [] + # LEGEND + handles = [] + labels = [] -for s in (10, 1): - handles.append(plt.Line2D([0],[0],color=line_colors['exp'], - linewidth=s*1e3/linewidth_factor)) - labels.append("{} GW".format(s)) -l1 = l1_1 = ax.legend(handles, labels, - loc="upper left", bbox_to_anchor=(0.24, 1.01), - frameon=False, - labelspacing=0.8, handletextpad=1.5, - title='Transmission Exist./Exp. ') -ax.add_artist(l1_1) + for s in (10, 1): + handles.append(plt.Line2D([0],[0],color=line_colors['exp'], + linewidth=s*1e3/linewidth_factor)) + labels.append("{} GW".format(s)) + l1 = l1_1 = ax.legend(handles, labels, + loc="upper left", bbox_to_anchor=(0.24, 1.01), + frameon=False, + labelspacing=0.8, handletextpad=1.5, + title='Transmission Exist./Exp. ') + ax.add_artist(l1_1) -handles = [] -labels = [] -for s in (10, 5): - handles.append(plt.Line2D([0],[0],color=line_colors['cur'], - linewidth=s*1e3/linewidth_factor)) - labels.append("/") -l1_2 = ax.legend(handles, labels, - loc="upper left", bbox_to_anchor=(0.26, 1.01), - frameon=False, - labelspacing=0.8, handletextpad=0.5, - title=' ') -ax.add_artist(l1_2) + handles = [] + labels = [] + for s in (10, 5): + handles.append(plt.Line2D([0],[0],color=line_colors['cur'], + linewidth=s*1e3/linewidth_factor)) + labels.append("/") + l1_2 = ax.legend(handles, labels, + loc="upper left", bbox_to_anchor=(0.26, 1.01), + frameon=False, + labelspacing=0.8, handletextpad=0.5, + title=' ') + ax.add_artist(l1_2) -handles = make_legend_circles_for([10e3, 5e3, 1e3], scale=bus_size_factor, facecolor="w") -labels = ["{} GW".format(s) for s in (10, 5, 3)] -l2 = ax.legend(handles, labels, - loc="upper left", bbox_to_anchor=(0.01, 1.01), - frameon=False, labelspacing=1.0, - title='Generation', - handler_map=make_handler_map_to_scale_circles_as_in(ax)) -ax.add_artist(l2) + handles = make_legend_circles_for([10e3, 5e3, 1e3], scale=bus_size_factor, facecolor="w") + labels = ["{} GW".format(s) for s in (10, 5, 3)] + l2 = ax.legend(handles, labels, + loc="upper left", bbox_to_anchor=(0.01, 1.01), + frameon=False, labelspacing=1.0, + title='Generation', + handler_map=make_handler_map_to_scale_circles_as_in(ax)) + ax.add_artist(l2) -techs = (bus_sizes.index.levels[1]) & pd.Index(opts['vre_techs'] + opts['conv_techs'] + opts['storage_techs']) -handles = [] -labels = [] -for t in techs: - handles.append(plt.Line2D([0], [0], color=tech_colors[t], marker='o', markersize=8, linewidth=0)) - labels.append(opts['nice_names'].get(t, t)) -l3 = ax.legend(handles, labels, loc="upper center", bbox_to_anchor=(0.5, -0.), # bbox_to_anchor=(0.72, -0.05), - handletextpad=0., columnspacing=0.5, ncol=4, title='Technology') + techs = (bus_sizes.index.levels[1]) & pd.Index(opts['vre_techs'] + opts['conv_techs'] + opts['storage_techs']) + handles = [] + labels = [] + for t in techs: + handles.append(plt.Line2D([0], [0], color=tech_colors[t], marker='o', markersize=8, linewidth=0)) + labels.append(opts['nice_names'].get(t, t)) + l3 = ax.legend(handles, labels, loc="upper center", bbox_to_anchor=(0.5, -0.), # bbox_to_anchor=(0.72, -0.05), + handletextpad=0., columnspacing=0.5, ncol=4, title='Technology') - -fig.savefig(snakemake.output.only_map, dpi=150, - bbox_inches='tight', bbox_extra_artists=[l1,l2,l3]) + return fig #n = load_network(snakemake.input.network, opts, combine_hydro_ps=False) -## Add total energy p -ax1 = ax = fig.add_axes([-0.115, 0.625, 0.2, 0.2]) -ax.set_title('Energy per technology', fontdict=dict(fontsize="medium")) +def plot_total_energy_pie(n, ax=None): + """Add total energy pie plot""" + if ax is None: + ax = plt.gca() -e_primary = aggregate_p(n).drop('load', errors='ignore').loc[lambda s: s>0] + ax.set_title('Energy per technology', fontdict=dict(fontsize="medium")) -patches, texts, autotexts = ax.pie(e_primary, - startangle=90, - labels = e_primary.rename(opts['nice_names_n']).index, - autopct='%.0f%%', - shadow=False, - colors = [tech_colors[tech] for tech in e_primary.index]) -for t1, t2, i in zip(texts, autotexts, e_primary.index): - if e_primary.at[i] < 0.04 * e_primary.sum(): - t1.remove() - t2.remove() + e_primary = aggregate_p(n).drop('load', errors='ignore').loc[lambda s: s>0] -## Add average system cost bar plot -# ax2 = ax = fig.add_axes([-0.1, 0.2, 0.1, 0.33]) -# ax2 = ax = fig.add_axes([-0.1, 0.15, 0.1, 0.37]) -ax2 = ax = fig.add_axes([-0.075, 0.1, 0.1, 0.45]) -total_load = (n.snapshot_weightings * n.loads_t.p.sum(axis=1)).sum() + patches, texts, autotexts = ax.pie(e_primary, + startangle=90, + labels = e_primary.rename(opts['nice_names_n']).index, + autopct='%.0f%%', + shadow=False, + colors = [tech_colors[tech] for tech in e_primary.index]) + for t1, t2, i in zip(texts, autotexts, e_primary.index): + if e_primary.at[i] < 0.04 * e_primary.sum(): + t1.remove() + t2.remove() -def split_costs(n): - costs = aggregate_costs(n).reset_index(level=0, drop=True) - costs_ex = aggregate_costs(n, existing_only=True).reset_index(level=0, drop=True) - return (costs['capital'].add(costs['marginal'], fill_value=0.), - costs_ex['capital'], costs['capital'] - costs_ex['capital'], costs['marginal']) +def plot_total_cost_bar(n, ax=None): + """Add average system cost bar plot""" + if ax is None: + ax = plt.gca() -costs, costs_cap_ex, costs_cap_new, costs_marg = split_costs(n) + total_load = (n.snapshot_weightings * n.loads_t.p.sum(axis=1)).sum() -costs_graph = pd.DataFrame(dict(a=costs.drop('load', errors='ignore')), - index=['AC-AC', 'AC line', 'onwind', 'offwind-ac', 'offwind-dc', 'solar', 'OCGT','CCGT', 'battery', 'H2']).dropna() -bottom = np.array([0., 0.]) -texts = [] + def split_costs(n): + costs = aggregate_costs(n).reset_index(level=0, drop=True) + costs_ex = aggregate_costs(n, existing_only=True).reset_index(level=0, drop=True) + return (costs['capital'].add(costs['marginal'], fill_value=0.), + costs_ex['capital'], costs['capital'] - costs_ex['capital'], costs['marginal']) -for i,ind in enumerate(costs_graph.index): - data = np.asarray(costs_graph.loc[ind])/total_load - ax.bar([0.5], data, bottom=bottom, color=tech_colors[ind], width=0.7, zorder=-1) - bottom_sub = bottom - bottom = bottom+data + costs, costs_cap_ex, costs_cap_new, costs_marg = split_costs(n) - if ind in opts['conv_techs'] + ['AC line']: - for c in [costs_cap_ex, costs_marg]: - if ind in c: - data_sub = np.asarray([c.loc[ind]])/total_load - ax.bar([0.5], data_sub, linewidth=0, - bottom=bottom_sub, color=tech_colors[ind], - width=0.7, zorder=-1, alpha=0.8) - bottom_sub += data_sub + costs_graph = pd.DataFrame(dict(a=costs.drop('load', errors='ignore')), + index=['AC-AC', 'AC line', 'onwind', 'offwind-ac', 'offwind-dc', 'solar', 'OCGT','CCGT', 'battery', 'H2']).dropna() + bottom = np.array([0., 0.]) + texts = [] - if abs(data[-1]) < 5: - continue + for i,ind in enumerate(costs_graph.index): + data = np.asarray(costs_graph.loc[ind])/total_load + ax.bar([0.5], data, bottom=bottom, color=tech_colors[ind], width=0.7, zorder=-1) + bottom_sub = bottom + bottom = bottom+data - text = ax.text(1.1,(bottom-0.5*data)[-1]-3,opts['nice_names_n'].get(ind,ind)) - texts.append(text) + if ind in opts['conv_techs'] + ['AC line']: + for c in [costs_cap_ex, costs_marg]: + if ind in c: + data_sub = np.asarray([c.loc[ind]])/total_load + ax.bar([0.5], data_sub, linewidth=0, + bottom=bottom_sub, color=tech_colors[ind], + width=0.7, zorder=-1, alpha=0.8) + bottom_sub += data_sub -ax.set_ylabel("Average system cost [Eur/MWh]") -ax.set_ylim([0,80]) # opts['costs_max']]) -ax.set_xlim([0,1]) -#ax.set_xticks([0.5]) -ax.set_xticklabels([]) #["w/o\nEp", "w/\nEp"]) -ax.grid(True, axis="y", color='k', linestyle='dotted') + if abs(data[-1]) < 5: + continue -#fig.tight_layout() + text = ax.text(1.1,(bottom-0.5*data)[-1]-3,opts['nice_names_n'].get(ind,ind)) + texts.append(text) -ll = snakemake.wildcards.ll -ll_type = ll[0] -ll_factor = ll[1:] -lbl = dict(c='line cost', v='line volume')[ll_type] -amnt = '{ll} x today\'s'.format(ll=ll_factor) if ll_factor != 'opt' else 'optimal' -fig.suptitle('Expansion to {amount} {label} at {clusters} clusters' - .format(amount=amnt, label=lbl, clusters=snakemake.wildcards.clusters)) + ax.set_ylabel("Average system cost [Eur/MWh]") + ax.set_ylim([0,80]) # opts['costs_max']]) + ax.set_xlim([0,1]) + #ax.set_xticks([0.5]) + ax.set_xticklabels([]) #["w/o\nEp", "w/\nEp"]) + ax.grid(True, axis="y", color='k', linestyle='dotted') -fig.savefig(snakemake.output.ext, transparent=True, - bbox_inches='tight', bbox_extra_artists=[l1, l2, l3, ax1, ax2]) + +if __name__ == "__main__": + if 'snakemake' not in globals(): + from vresutils.snakemake import MockSnakemake, Dict + from snakemake.rules import expand + + snakemake = Dict() + snakemake = MockSnakemake( + path='..', + wildcards=dict(network='elec', simpl='', clusters='90', lv='1.25', opts='Co2L-3H', attr='p_nom', ext="pdf"), + input=dict(network="results/networks/{network}_s{simpl}_{clusters}_lv{lv}_{opts}.nc", + tech_costs="data/costs.csv"), + output=dict(only_map="results/plots/{network}_s{simpl}_{clusters}_lv{lv}_{opts}_{attr}.{ext}", + ext="results/plots/{network}_s{simpl}_{clusters}_lv{lv}_{opts}_{attr}_ext.{ext}") + ) + + logging.basicConfig(level=snakemake.config['logging_level']) + + set_plot_style() + + opts = snakemake.config['plotting'] + map_figsize = opts['map']['figsize'] + map_boundaries = opts['map']['boundaries'] + + n = load_network(snakemake.input.network, snakemake.input.tech_costs, snakemake.config) + + scenario_opts = snakemake.wildcards.opts.split('-') + + fig, ax = plt.subplots(figsize=map_figsize, subplot_kw={"projection": ccrs.PlateCarree()}) + plot_map(n, ax, snakemake.wildcards.attr, opts) + + fig.savefig(snakemake.output.only_map, dpi=150, + bbox_inches='tight', bbox_extra_artists=[l1,l2,l3]) + + ax1 = fig.add_axes([-0.115, 0.625, 0.2, 0.2]) + plot_total_energy_pie(n, ax1) + + ax2 = fig.add_axes([-0.075, 0.1, 0.1, 0.45]) + plot_total_cost_bar(n, ax2) + + #fig.tight_layout() + + ll = snakemake.wildcards.ll + ll_type = ll[0] + ll_factor = ll[1:] + lbl = dict(c='line cost', v='line volume')[ll_type] + amnt = '{ll} x today\'s'.format(ll=ll_factor) if ll_factor != 'opt' else 'optimal' + fig.suptitle('Expansion to {amount} {label} at {clusters} clusters' + .format(amount=amnt, label=lbl, clusters=snakemake.wildcards.clusters)) + + fig.savefig(snakemake.output.ext, transparent=True, + bbox_inches='tight', bbox_extra_artists=[l1, l2, l3, ax1, ax2])