예제 #1
0
    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
예제 #2
0
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
예제 #3
0
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()