def construct_partial_network(self, cluster, scenario): """ Compute the partial network that has been merged into a single cluster. The resulting network retains the external cluster buses that share some line with the cluster identified by `cluster`. These external buses will be prefixed by self.id_prefix in order to prevent name clashes with buses in the disaggregation :param cluster: Index of the cluster to disaggregate :return: Tuple of (partial_network, external_buses) where `partial_network` is the result of the partial decomposition and `external_buses` represent clusters adjacent to `cluster` that may be influenced by calculations done on the partial network. """ #Create an empty network partial_network = Network() # find all lines that have at least one bus inside the cluster busflags = (self.buses['cluster'] == cluster) def is_bus_in_cluster(conn): return busflags[conn] # Copy configurations to new network partial_network.snapshots = self.original_network.snapshots partial_network.snapshot_weightings = ( self.original_network.snapshot_weightings) partial_network.carriers = self.original_network.carriers # Collect all connectors that have some node inside the cluster external_buses = pd.DataFrame() line_types = ['lines', 'links', 'transformers'] for line_type in line_types: # Copy all lines that reside entirely inside the cluster ... setattr( partial_network, line_type, filter_internal_connector( getattr(self.original_network, line_type), is_bus_in_cluster)) # ... and their time series # TODO: These are all time series, not just the ones from lines # residing entirely in side the cluster. # Is this a problem? setattr(partial_network, line_type + '_t', getattr(self.original_network, line_type + '_t')) # Copy all lines whose `bus0` lies within the cluster left_external_connectors = filter_left_external_connector( getattr(self.original_network, line_type), is_bus_in_cluster) if not left_external_connectors.empty: f = lambda x: self.idx_prefix + self.clustering.busmap.loc[x] ca_option = pd.get_option('mode.chained_assignment') pd.set_option('mode.chained_assignment', None) left_external_connectors.loc[:, 'bus0'] = ( left_external_connectors.loc[:, 'bus0'].apply(f)) pd.set_option('mode.chained_assignment', ca_option) external_buses = pd.concat( (external_buses, left_external_connectors.bus0)) # Copy all lines whose `bus1` lies within the cluster right_external_connectors = filter_right_external_connector( getattr(self.original_network, line_type), is_bus_in_cluster) if not right_external_connectors.empty: f = lambda x: self.idx_prefix + self.clustering.busmap.loc[x] ca_option = pd.get_option('mode.chained_assignment') pd.set_option('mode.chained_assignment', None) right_external_connectors.loc[:, 'bus1'] = ( right_external_connectors.loc[:, 'bus1'].apply(f)) pd.set_option('mode.chained_assignment', ca_option) external_buses = pd.concat( (external_buses, right_external_connectors.bus1)) # Collect all buses that are contained in or somehow connected to the # cluster buses_in_lines = self.buses[busflags].index bus_types = [ 'loads', 'generators', 'stores', 'storage_units', 'shunt_impedances' ] # Copy all values that are part of the cluster partial_network.buses = self.original_network.buses[ self.original_network.buses.index.isin(buses_in_lines)] # Collect all buses that are external, but connected to the cluster ... externals_to_insert = self.clustered_network.buses[ self.clustered_network.buses.index.isin( map(lambda x: x[0][len(self.idx_prefix):], external_buses.values))] # ... prefix them to avoid name clashes with buses from the original # network ... self.reindex_with_prefix(externals_to_insert) # .. and insert them as well as their time series partial_network.buses = ( partial_network.buses.append(externals_to_insert)) partial_network.buses_t = self.original_network.buses_t # TODO: Rename `bustype` to on_bus_type for bustype in bus_types: # Copy loads, generators, ... from original network to network copy setattr( partial_network, bustype, filter_buses(getattr(self.original_network, bustype), buses_in_lines)) # Collect on-bus components from external, connected clusters buses_to_insert = filter_buses( getattr(self.clustered_network, bustype), map(lambda x: x[0][len(self.idx_prefix):], external_buses.values)) # Prefix their external bindings buses_to_insert.loc[:, 'bus'] = (self.idx_prefix + buses_to_insert.loc[:, 'bus']) setattr(partial_network, bustype, getattr(partial_network, bustype).append(buses_to_insert)) # Also copy their time series setattr(partial_network, bustype + '_t', getattr(self.original_network, bustype + '_t')) # Note: The code above copies more than necessary, because it # copies every time series for `bustype` from the original # network and not only the subset belonging to the partial # network. The commented code below tries to filter the time # series accordingly, but there must be bug somewhere because # using it, the time series in the clusters and sums of the # time series after disaggregation don't match up. """ series = getattr(self.original_network, bustype + '_t') partial_series = type(series)() for s in series: partial_series[s] = series[s].loc[ :, getattr(partial_network, bustype) .index.intersection(series[s].columns)] setattr(partial_network, bustype + '_t', partial_series) """ # Just a simple sanity check # TODO: Remove when sure that disaggregation will not go insane anymore for line_type in line_types: assert (getattr(partial_network, line_type).bus0.isin( partial_network.buses.index).all()) assert (getattr(partial_network, line_type).bus1.isin( partial_network.buses.index).all()) return partial_network, external_buses
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()