def preprocess(plot: bool = False): # TODO: # - figure out what we really need # - converters? # - transformers? # - figure out if lines and/or links can be extended # - see prepare_network # - Probably best to do all these steps once in a preprocess function and then just # remove unwanted components at run time # Load main components # buses_df = load_buses_from_eg(countries, voltages) buses_df = load_buses_from_eg() # buses_df, links_df = load_links_from_eg(buses_df, config['links'], countries) buses_df, links_df = load_links_from_eg(buses_df) # converters_df = load_converters_from_eg(buses_df.index, config["links"]) converters_df = load_converters_from_eg(buses_df.index) # lines_df = load_lines_from_eg(buses_df.index, config["lines"], voltages, net.line_types) lines_df = load_lines_from_eg(buses_df.index) # transformers_df = load_transformers_from_eg(buses_df.index, config["transformers"]) transformers_df = load_transformers_from_eg(buses_df.index) # Add everything to the network net = Network() net.import_components_from_dataframe(buses_df, "Bus") net.import_components_from_dataframe(lines_df, "Line") net.import_components_from_dataframe(transformers_df, "Transformer") net.import_components_from_dataframe(links_df, "Link") net.import_components_from_dataframe(converters_df, "Link") # Update a bunch of parameters for given components according to parameters_correction.yaml apply_parameter_corrections(net) # Remove subnetworks with less than a given number of components net = remove_unconnected_components(net) # Determine to which country each bus (onshore or offshore) belong and do some stuff with substations # set_countries_and_substations(net, countries) set_countries_and_substations(net) # Set what portion of the link is under water # set_links_underwater_fraction(net, countries) set_links_underwater_fraction(net) # I have no clue what this is for... replace_b2b_converter_at_country_border_by_link(net) # Save base network net.export_to_csv_folder( f"{data_path}topologies/pypsa_entsoe_gridkit/generated/base_network/") if plot: import matplotlib.pyplot as plt net.plot(bus_sizes=0.001) plt.show() return net
def load_topology(nuts_codes, config, voltages: List[float] = None, plot: bool = False): net = Network() net.import_from_csv_folder( f"{data_path}topologies/pypsa_entsoe_gridkit/generated/base_network/") if 1: import matplotlib.pyplot as plt net.plot(bus_sizes=0.001) plt.show() exit() # Remove all buses outside desired regions region_shapes_ds = get_shapes(nuts_codes, save=True)["geometry"] buses_in_nuts_regions = \ net.buses[['x', 'y']].apply(lambda p: any([shape.contains(Point(p)) for shape in region_shapes_ds]), axis=1) net.buses = net.buses[buses_in_nuts_regions] if 0: plt.figure() net.plot(bus_sizes=0.001) # Remove all buses which are not at the desired voltage buses_with_v_nom_to_keep_b = net.buses.v_nom.isnull() if voltages is not None: buses_with_v_nom_to_keep_b |= net.buses.v_nom.isin(voltages) logger.info( "Removing buses with voltages {}" "".format( pd.Index( net.buses.v_nom.unique()).dropna().difference(voltages))) net.buses = net.buses[buses_with_v_nom_to_keep_b] if 1: plt.figure() net.plot(bus_sizes=0.001) plt.show() # Remove dangling branches net.lines = remove_dangling_branches(net.lines, net.buses.index) net.links = remove_dangling_branches(net.links, net.buses.index) net.transformers = remove_dangling_branches(net.transformers, net.buses.index) # Set electrical parameters set_electrical_parameters_lines(net, config['lines'], net.buses.v_nom.dropna().unique().tolist()) set_electrical_parameters_links(net, config['links']) set_electrical_parameters_transformers(net, config['transformers']) # Allows to set under construction links and lines to 0 or remove them completely, # and remove some unconnected components that might appear as a result net = adjust_capacities_of_under_construction_branches( net, config['lines'], config['links']) # Remove unconnected components # TODO: allow to simplify the network (i.e. convert everything to 380) or not ? net = cluster_network(net, nuts_codes) if plot: from epippy.topologies.core.plot import plot_topology all_lines = pd.concat( (net.links[['bus0', 'bus1']], net.lines[['bus0', 'bus1']])) plot_topology(net.buses, all_lines) plt.show() return net
def mv_grid_topology(pypsa_network, configs, timestep=None, line_color=None, node_color=None, line_load=None, grid_expansion_costs=None, filename=None, arrows=False, grid_district_geom=True, background_map=True, voltage=None, limits_cb_lines=None, limits_cb_nodes=None, xlim=None, ylim=None, lines_cmap='inferno_r', title='', scaling_factor_line_width=None): """ Plot line loading as color on lines. Displays line loading relative to nominal capacity. Parameters ---------- pypsa_network : :pypsa:`pypsa.Network<network>` configs : :obj:`dict` Dictionary with used configurations from config files. See :class:`~.grid.network.Config` for more information. timestep : :pandas:`pandas.Timestamp<timestamp>` Time step to plot analysis results for. If `timestep` is None maximum line load and if given, maximum voltage deviation, is used. In that case arrows cannot be drawn. Default: None. line_color : :obj:`str` or None Defines whereby to choose line colors (and implicitly size). Possible options are: * 'loading' Line color is set according to loading of the line. Loading of MV lines must be provided by parameter `line_load`. * 'expansion_costs' Line color is set according to investment costs of the line. This option also effects node colors and sizes by plotting investment in stations and setting `node_color` to 'storage_integration' in order to plot storage size of integrated storages. Grid expansion costs must be provided by parameter `grid_expansion_costs`. * None (default) Lines are plotted in black. Is also the fallback option in case of wrong input. node_color : :obj:`str` or None Defines whereby to choose node colors (and implicitly size). Possible options are: * 'technology' Node color as well as size is set according to type of node (generator, MV station, etc.). * 'voltage' Node color is set according to voltage deviation from 1 p.u.. Voltages of nodes in MV grid must be provided by parameter `voltage`. * 'storage_integration' Only storages are plotted. Size of node corresponds to size of storage. * None (default) Nodes are not plotted. Is also the fallback option in case of wrong input. line_load : :pandas:`pandas.DataFrame<dataframe>` or None Dataframe with current results from power flow analysis in A. Index of the dataframe is a :pandas:`pandas.DatetimeIndex<datetimeindex>`, columns are the line representatives. Only needs to be provided when parameter `line_color` is set to 'loading'. Default: None. grid_expansion_costs : :pandas:`pandas.DataFrame<dataframe>` or None Dataframe with grid expansion costs in kEUR. See `grid_expansion_costs` in :class:`~.grid.network.Results` for more information. Only needs to be provided when parameter `line_color` is set to 'expansion_costs'. Default: None. filename : :obj:`str` Filename to save plot under. If not provided, figure is shown directly. Default: None. arrows : :obj:`Boolean` If True draws arrows on lines in the direction of the power flow. Does only work when `line_color` option 'loading' is used and a time step is given. Default: False. grid_district_geom : :obj:`Boolean` If True grid district polygon is plotted in the background. This also requires the geopandas package to be installed. Default: True. background_map : :obj:`Boolean` If True map is drawn in the background. This also requires the contextily package to be installed. Default: True. voltage : :pandas:`pandas.DataFrame<dataframe>` Dataframe with voltage results from power flow analysis in p.u.. Index of the dataframe is a :pandas:`pandas.DatetimeIndex<datetimeindex>`, columns are the bus representatives. Only needs to be provided when parameter `node_color` is set to 'voltage'. Default: None. limits_cb_lines : :obj:`tuple` Tuple with limits for colorbar of line color. First entry is the minimum and second entry the maximum value. Only needs to be provided when parameter `line_color` is not None. Default: None. limits_cb_nodes : :obj:`tuple` Tuple with limits for colorbar of nodes. First entry is the minimum and second entry the maximum value. Only needs to be provided when parameter `node_color` is not None. Default: None. xlim : :obj:`tuple` Limits of x-axis. Default: None. ylim : :obj:`tuple` Limits of y-axis. Default: None. lines_cmap : :obj:`str` Colormap to use for lines in case `line_color` is 'loading' or 'expansion_costs'. Default: 'inferno_r'. title : :obj:`str` Title of the plot. Default: ''. scaling_factor_line_width : :obj:`float` or None If provided line width is set according to the nominal apparent power of the lines. If line width is None a default line width of 2 is used for each line. Default: None. """ def get_color_and_size(name, colors_dict, sizes_dict): if 'BranchTee' in name: return colors_dict['BranchTee'], sizes_dict['BranchTee'] elif 'LVStation' in name: return colors_dict['LVStation'], sizes_dict['LVStation'] elif 'GeneratorFluctuating' in name: return (colors_dict['GeneratorFluctuating'], sizes_dict['GeneratorFluctuating']) elif 'Generator' in name: return colors_dict['Generator'], sizes_dict['Generator'] elif 'DisconnectingPoint' in name: return (colors_dict['DisconnectingPoint'], sizes_dict['DisconnectingPoint']) elif 'MVStation' in name: return colors_dict['MVStation'], sizes_dict['MVStation'] elif 'Storage' in name: return colors_dict['Storage'], sizes_dict['Storage'] else: return colors_dict['else'], sizes_dict['else'] def nodes_by_technology(buses): bus_sizes = {} bus_colors = {} colors_dict = { 'BranchTee': 'b', 'GeneratorFluctuating': 'g', 'Generator': 'k', 'LVStation': 'c', 'MVStation': 'r', 'Storage': 'y', 'DisconnectingPoint': '0.75', 'else': 'orange' } sizes_dict = { 'BranchTee': 10, 'GeneratorFluctuating': 100, 'Generator': 100, 'LVStation': 50, 'MVStation': 120, 'Storage': 100, 'DisconnectingPoint': 50, 'else': 200 } for bus in buses: bus_colors[bus], bus_sizes[bus] = get_color_and_size( bus, colors_dict, sizes_dict) return bus_sizes, bus_colors def nodes_by_voltage(buses, voltage): bus_colors = {} bus_sizes = {} for bus in buses: if 'primary' in bus: bus_tmp = bus[12:] else: bus_tmp = bus[4:] if timestep is not None: bus_colors[bus] = 100 * abs(1 - voltage.loc[timestep, ('mv', bus_tmp)]) else: bus_colors[bus] = 100 * max( abs(1 - voltage.loc[:, ('mv', bus_tmp)])) bus_sizes[bus] = 50 return bus_sizes, bus_colors def nodes_storage_integration(buses): bus_sizes = {} for bus in buses: if not 'storage' in bus: bus_sizes[bus] = 0 else: tmp = bus.split('_') storage_repr = '_'.join(tmp[1:]) bus_sizes[bus] = pypsa_network.storage_units.loc[ storage_repr, 'p_nom'] * 1000 / 3 return bus_sizes def nodes_by_costs(buses, grid_expansion_costs): # sum costs for each station costs_lv_stations = grid_expansion_costs[ grid_expansion_costs.index.str.contains("LVStation")] costs_lv_stations['station'] = \ costs_lv_stations.reset_index()['index'].apply( lambda _: '_'.join(_.split('_')[0:2])).values costs_lv_stations = costs_lv_stations.groupby('station').sum() costs_mv_station = grid_expansion_costs[ grid_expansion_costs.index.str.contains("MVStation")] costs_mv_station['station'] = \ costs_mv_station.reset_index()['index'].apply( lambda _: '_'.join(_.split('_')[0:2])).values costs_mv_station = costs_mv_station.groupby('station').sum() bus_sizes = {} bus_colors = {} for bus in buses: if 'LVStation' in bus: try: tmp = bus.split('_') lv_st = '_'.join(tmp[2:]) bus_colors[bus] = costs_lv_stations.loc[lv_st, 'total_costs'] bus_sizes[bus] = 100 except: bus_colors[bus] = 0 bus_sizes[bus] = 0 elif 'MVStation' in bus: try: tmp = bus.split('_') mv_st = '_'.join(tmp[2:]) bus_colors[bus] = costs_mv_station.loc[mv_st, 'total_costs'] bus_sizes[bus] = 100 except: bus_colors[bus] = 0 bus_sizes[bus] = 0 else: bus_colors[bus] = 0 bus_sizes[bus] = 0 return bus_sizes, bus_colors # set font and font size font = {'family': 'serif', 'size': 15} matplotlib.rc('font', **font) # create pypsa network only containing MV buses and lines pypsa_plot = PyPSANetwork() pypsa_plot.buses = pypsa_network.buses.loc[pypsa_network.buses.v_nom > 1] # filter buses of aggregated loads and generators pypsa_plot.buses = pypsa_plot.buses[~pypsa_plot.buses.index.str. contains("agg")] pypsa_plot.lines = pypsa_network.lines[pypsa_network.lines.bus0.isin( pypsa_plot.buses.index)][pypsa_network.lines.bus1.isin( pypsa_plot.buses.index)] # line colors if line_color == 'loading': line_colors = tools.calculate_relative_line_load( pypsa_network, configs, line_load, pypsa_network.lines.v_nom, pypsa_plot.lines.index, timestep).max() elif line_color == 'expansion_costs': node_color = 'expansion_costs' line_costs = pypsa_plot.lines.join(grid_expansion_costs, rsuffix='costs', how='left') line_colors = line_costs.total_costs.fillna(0) else: line_colors = pd.Series('black', index=pypsa_plot.lines.index) # bus colors and sizes if node_color == 'technology': bus_sizes, bus_colors = nodes_by_technology(pypsa_plot.buses.index) bus_cmap = None elif node_color == 'voltage': bus_sizes, bus_colors = nodes_by_voltage(pypsa_plot.buses.index, voltage) bus_cmap = plt.cm.Blues elif node_color == 'storage_integration': bus_sizes = nodes_storage_integration(pypsa_plot.buses.index) bus_colors = 'orangered' bus_cmap = None elif node_color == 'expansion_costs': bus_sizes, bus_colors = nodes_by_costs(pypsa_plot.buses.index, grid_expansion_costs) bus_cmap = None elif node_color is None: bus_sizes = 0 bus_colors = 'r' bus_cmap = None else: logging.warning('Choice for `node_color` is not valid. Default is ' 'used instead.') bus_sizes = 0 bus_colors = 'r' bus_cmap = None # convert bus coordinates to Mercator if contextily and background_map: inProj = Proj(init='epsg:4326') outProj = Proj(init='epsg:3857') x2, y2 = transform(inProj, outProj, list(pypsa_plot.buses.loc[:, 'x']), list(pypsa_plot.buses.loc[:, 'y'])) pypsa_plot.buses.loc[:, 'x'] = x2 pypsa_plot.buses.loc[:, 'y'] = y2 # plot plt.figure(figsize=(12, 8)) ax = plt.gca() # plot grid district if grid_district_geom and geopandas: try: subst = pypsa_network.buses[pypsa_network.buses.index.str.contains( "MVStation")].index[0] subst_id = subst.split('_')[-1] projection = 3857 if contextily and background_map else 4326 region = get_grid_district_polygon(configs, subst_id=subst_id, projection=projection) region.plot(ax=ax, color='white', alpha=0.2, edgecolor='red', linewidth=2) except Exception as e: logging.warning("Grid district geometry could not be plotted due " "to the following error: {}".format(e)) # if scaling factor is given s_nom is plotted as line width if scaling_factor_line_width is not None: line_width = pypsa_plot.lines.s_nom * scaling_factor_line_width else: line_width = 2 cmap = plt.cm.get_cmap(lines_cmap) ll = pypsa_plot.plot(line_colors=line_colors, line_cmap=cmap, ax=ax, title=title, line_widths=line_width, branch_components=['Line'], basemap=True, bus_sizes=bus_sizes, bus_colors=bus_colors, bus_cmap=bus_cmap) # color bar line loading if line_color == 'loading': if limits_cb_lines is None: limits_cb_lines = (min(line_colors), max(line_colors)) v = np.linspace(limits_cb_lines[0], limits_cb_lines[1], 101) cb = plt.colorbar(ll[1], boundaries=v, ticks=v[0:101:10]) cb.set_clim(vmin=limits_cb_lines[0], vmax=limits_cb_lines[1]) cb.set_label('Line loading in p.u.') # color bar grid expansion costs elif line_color == 'expansion_costs': if limits_cb_lines is None: limits_cb_lines = (min(min(line_colors), min(bus_colors.values())), max(max(line_colors), max(bus_colors.values()))) v = np.linspace(limits_cb_lines[0], limits_cb_lines[1], 101) cb = plt.colorbar(ll[1], boundaries=v, ticks=v[0:101:10]) cb.set_clim(vmin=limits_cb_lines[0], vmax=limits_cb_lines[1]) cb.set_label('Grid expansion costs in kEUR') # color bar voltage if node_color == 'voltage': if limits_cb_nodes is None: limits_cb_nodes = (min(bus_colors.values()), max(bus_colors.values())) v_voltage = np.linspace(limits_cb_nodes[0], limits_cb_nodes[1], 101) cb_voltage = plt.colorbar(ll[0], boundaries=v_voltage, ticks=v_voltage[0:101:10]) cb_voltage.set_clim(vmin=limits_cb_nodes[0], vmax=limits_cb_nodes[1]) cb_voltage.set_label('Voltage deviation in %') # storages if node_color == 'expansion_costs': ax.scatter(pypsa_plot.buses.loc[pypsa_network.storage_units.loc[:, 'bus'], 'x'], pypsa_plot.buses.loc[pypsa_network.storage_units.loc[:, 'bus'], 'y'], c='orangered', s=pypsa_network.storage_units.loc[:, 'p_nom'] * 1000 / 3) # add legend for storage size and line capacity if (node_color == 'storage_integration' or node_color == 'expansion_costs') and \ pypsa_network.storage_units.loc[:, 'p_nom'].any() > 0: scatter_handle = plt.scatter([], [], c='orangered', s=100, label='= 300 kW battery storage') else: scatter_handle = None if scaling_factor_line_width is not None: line_handle = plt.plot([], [], c='black', linewidth=scaling_factor_line_width * 10, label='= 10 MVA') else: line_handle = None if scatter_handle and line_handle: plt.legend(handles=[scatter_handle, line_handle[0]], labelspacing=1, title='Storage size and line capacity', borderpad=0.5, loc=2, framealpha=0.5, fontsize='medium') elif scatter_handle: plt.legend(handles=[scatter_handle], labelspacing=1, title='Storage size', borderpad=0.5, loc=2, framealpha=0.5, fontsize='medium') elif line_handle: plt.legend(handles=[line_handle[0]], labelspacing=1, title='Line capacity', borderpad=0.5, loc=2, framealpha=0.5, fontsize='medium') # axes limits if xlim is not None: ax.set_xlim(xlim[0], xlim[1]) if ylim is not None: ax.set_ylim(ylim[0], ylim[1]) # hide axes labels ax.get_xaxis().set_visible(False) ax.get_yaxis().set_visible(False) # draw arrows on lines if arrows and timestep and line_color == 'loading': path = ll[1].get_segments() colors = cmap(ll[1].get_array() / 100) for i in range(len(path)): if pypsa_network.lines_t.p0.loc[timestep, line_colors.index[i]] > 0: arrowprops = dict(arrowstyle="->", color='b') #colors[i]) else: arrowprops = dict(arrowstyle="<-", color='b') #colors[i]) ax.annotate("", xy=abs((path[i][0] - path[i][1]) * 0.51 - path[i][0]), xytext=abs((path[i][0] - path[i][1]) * 0.49 - path[i][0]), arrowprops=arrowprops, size=10) # plot map data in background if contextily and background_map: try: add_basemap(ax, zoom=12) except Exception as e: logging.warning("Background map could not be plotted due to the " "following error: {}".format(e)) if filename is None: plt.show() else: plt.savefig(filename) plt.close()