From 0070b1c87cc31bedcd89b521d613a59a44388a90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20H=C3=B6rsch?= Date: Thu, 25 Oct 2018 16:43:24 +0200 Subject: [PATCH] plot_network: Add functionality to produce a network plot of each solution --- Snakefile | 12 +- scripts/plot_network.py | 256 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 263 insertions(+), 5 deletions(-) create mode 100644 scripts/plot_network.py diff --git a/Snakefile b/Snakefile index 5fa579af..10fa6e04 100644 --- a/Snakefile +++ b/Snakefile @@ -1,5 +1,7 @@ configfile: "config.yaml" +COSTS="data/costs.csv" + wildcard_constraints: lv="[0-9\.]+|inf", simpl="[a-zA-Z0-9]*", @@ -136,7 +138,7 @@ rule build_hydro_profile: rule add_electricity: input: base_network='networks/base.nc', - tech_costs='data/costs.csv', + tech_costs=COSTS, regions="resources/regions_onshore.geojson", powerplants='resources/powerplants.csv', hydro_capacities='data/bundle/hydro_capacities.csv', @@ -195,7 +197,7 @@ rule cluster_network: # script: "scripts/add_sectors.py" rule prepare_network: - input: 'networks/{network}_s{simpl}_{clusters}.nc', tech_costs='data/costs.csv' + input: 'networks/{network}_s{simpl}_{clusters}.nc', tech_costs=COSTS output: 'networks/{network}_s{simpl}_{clusters}_lv{lv}_{opts}.nc' threads: 1 resources: mem=1000 @@ -248,10 +250,10 @@ rule solve_operations_network: rule plot_network: input: network="results/networks/{network}_s{simpl}_{clusters}_lv{lv}_{opts}.nc", - costs='data/costs.csv' + tech_costs=COSTS output: - only_map="results/plots/{network}_s{simpl}_{clusters}_lv{lv}_{opts}_{attr}.pdf", - ext="results/plots/{network}_s{simpl}_{clusters}_lv{lv}_{opts}_{attr}_ext.pdf" + 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}" script: "scripts/plot_network.py" # rule plot_costs: diff --git a/scripts/plot_network.py b/scripts/plot_network.py new file mode 100644 index 00000000..7bcbf259 --- /dev/null +++ b/scripts/plot_network.py @@ -0,0 +1,256 @@ +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 +from vresutils import plot as vplot + +import os +import pypsa +import pandas as pd +import geopandas as gpd +import numpy as np +from itertools import product, chain +from six.moves import map, zip +from six import itervalues, iterkeys +from collections import OrderedDict as odict + +import matplotlib.pyplot as plt +import matplotlib as mpl +from matplotlib.patches import Circle, Ellipse +from matplotlib.legend_handler import HandlerPatch +import seaborn as sns +to_rgba = mpl.colors.colorConverter.to_rgba + +def make_handler_map_to_scale_circles_as_in(ax, dont_resize_actively=False): + fig = ax.get_figure() + def axes2pt(): + return np.diff(ax.transData.transform([(0,0), (1,1)]), axis=0)[0] * (72./fig.dpi) + + ellipses = [] + if not dont_resize_actively: + def update_width_height(event): + dist = axes2pt() + for e, radius in ellipses: e.width, e.height = 2. * radius * dist + fig.canvas.mpl_connect('resize_event', update_width_height) + ax.callbacks.connect('xlim_changed', update_width_height) + ax.callbacks.connect('ylim_changed', update_width_height) + + def legend_circle_handler(legend, orig_handle, xdescent, ydescent, + width, height, fontsize): + w, h = 2. * orig_handle.get_radius() * axes2pt() + e = Ellipse(xy=(0.5*width-0.5*xdescent, 0.5*height-0.5*ydescent), width=w, height=w) + ellipses.append((e, orig_handle.get_radius())) + return e + return {Circle: HandlerPatch(patch_func=legend_circle_handler)} + +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' + }]) + +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('-') + +## 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 = pd.concat(dict(Line=n.lines.s_nom_opt, Link=n.links.p_nom_opt)) + line_widths_cur = pd.concat(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 = \ +pd.concat(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'] + +## PLOT +fig, ax = plt.subplots(figsize=map_figsize) +n.plot(line_widths=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=line_widths_cur/linewidth_factor, + line_colors=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') + +# 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) + +# 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) + +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) + +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]) + +#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")) + +e_primary = aggregate_p(n).drop('load').loc[lambda s: s>0] + +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() + +## 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() + +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']) + +costs, costs_cap_ex, costs_cap_new, costs_marg = split_costs(n) + +costs_graph = pd.DataFrame(dict(a=costs.drop('load')), + index=['AC-AC', 'AC line', 'onwind', 'offwind', 'solar', 'OCGT', 'battery', 'H2']).dropna() +bottom = np.array([0., 0.]) +texts = [] + +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 + + 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 + + if abs(data[-1]) < 5: + continue + + text = ax.text(1.1,(bottom-0.5*data)[-1]-3,opts['nice_names_n'].get(ind,ind)) + texts.append(text) + +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.tight_layout() + +fig.suptitle('Expansion to {lv} x today\'s line volume at {clusters} clusters' + .format(lv=snakemake.wildcards.lv, clusters=snakemake.wildcards.clusters)) + +fig.savefig(snakemake.output.ext, transparent=True, + bbox_inches='tight', bbox_extra_artists=[l1, l2, l3, ax1, ax2])