def one_storage_per_feeder(edisgo, storage_timeseries,
                           storage_nominal_power=None, **kwargs):
    """
    Allocates the given storage capacity to multiple smaller storages.

    For each feeder with load or voltage issues it is checked if integrating a
    storage will reduce peaks in the feeder, starting with the feeder with
    the highest theoretical grid expansion costs. A heuristic approach is used
    to estimate storage sizing and siting while storage operation is carried
    over from the given storage operation.

    Parameters
    -----------
    edisgo : :class:`~.grid.network.EDisGo`
    storage_timeseries : :pandas:`pandas.DataFrame<dataframe>`
        Total active and reactive power time series that will be allocated to
        the smaller storages in feeders with load or voltage issues. Columns of
        the dataframe are 'p' containing active power time series in kW and 'q'
        containing the reactive power time series in kvar. Index is a
        :pandas:`pandas.DatetimeIndex<datetimeindex>`.
    storage_nominal_power : :obj:`float` or None
        Nominal power in kW that will be allocated to the smaller storages in
        feeders with load or voltage issues. If no nominal power is provided
        the maximum active power given in `storage_timeseries` is used.
        Default: None.
    debug : :obj:`Boolean`, optional
        If dedug is True a dataframe with storage size and path to storage of
        all installed and possibly discarded storages is saved to a csv file
        and a plot with all storage positions is created and saved, both to the
        current working directory with filename `storage_results_{MVgrid_id}`.
        Default: False.
    check_costs_reduction : :obj:`Boolean` or :obj:`str`, optional
        This parameter specifies when and whether it should be checked if a
        storage reduced grid expansion costs or not. It can be used as a safety
        check but can be quite time consuming. Possible options are:

        * 'each_feeder'
          Costs reduction is checked for each feeder. If the storage did not
          reduce grid expansion costs it is discarded.
        * 'once'
          Costs reduction is checked after the total storage capacity is
          allocated to the feeders. If the storages did not reduce grid
          expansion costs they are all discarded.
        * False
          Costs reduction is never checked.

        Default: False.

    """

    def _feeder_ranking(grid_expansion_costs):
        """
        Get feeder ranking from grid expansion costs DataFrame.

        MV feeders are ranked descending by grid expansion costs that are
        attributed to that feeder.

        Parameters
        ----------
        grid_expansion_costs : :pandas:`pandas.DataFrame<dataframe>`
            grid_expansion_costs DataFrame from :class:`~.grid.network.Results`
            of the copied edisgo object.

        Returns
        -------
        :pandas:`pandas.Series<series>`
            Series with ranked MV feeders (in the copied graph) of type
            :class:`~.grid.components.Line`. Feeders are ranked by total grid
            expansion costs of all measures conducted in the feeder. The
            feeder with the highest costs is in the first row and the feeder
            with the lowest costs in the last row.

        """
        return grid_expansion_costs.groupby(
            ['mv_feeder'], sort=False).sum().reset_index().sort_values(
            by=['total_costs'], ascending=False)['mv_feeder']

    def _shortest_path(node):
        if isinstance(node, LVStation):
            return len(nx.shortest_path(
                node.mv_grid.graph, node.mv_grid.station, node))
        else:
            return len(nx.shortest_path(
                node.grid.graph, node.grid.station, node))

    def _find_battery_node(edisgo, critical_lines_feeder,
                           critical_nodes_feeder):
        """
        Evaluates where to install the storage.

        Parameters
        -----------
        edisgo : :class:`~.grid.network.EDisGo`
            The original edisgo object.
        critical_lines_feeder : :pandas:`pandas.DataFrame<dataframe>`
            Dataframe containing over-loaded lines in MV feeder, their maximum
            relative over-loading and the corresponding time step. See
            :func:`edisgo.flex_opt.check_tech_constraints.mv_line_load` for
            more information.
        critical_nodes_feeder : :obj:`list`
            List with all nodes in MV feeder with voltage issues.

        Returns
        -------
        :obj:`float`
            Node where storage is installed.

        """

        # if there are overloaded lines in the MV feeder the battery storage
        # will be installed at the node farthest away from the MV station
        if not critical_lines_feeder.empty:
            logger.debug("Storage positioning due to overload.")
            # dictionary with nodes and their corresponding path length to
            # MV station
            path_length_dict = {}
            for l in critical_lines_feeder.index:
                nodes = l.grid.graph.nodes_from_line(l)
                for node in nodes:
                    path_length_dict[node] = _shortest_path(node)
            # return node farthest away
            return [_ for _ in path_length_dict.keys()
                    if path_length_dict[_] == max(
                    path_length_dict.values())][0]

        # if there are voltage issues in the MV grid the battery storage will
        # be installed at the first node in path that exceeds 2/3 of the line
        # length from station to critical node with highest voltage deviation
        if critical_nodes_feeder:
            logger.debug("Storage positioning due to voltage issues.")
            node = critical_nodes_feeder[0]

            # get path length from station to critical node
            get_weight = lambda u, v, data: data['line'].length
            path_length = dijkstra_shortest_path_length(
                edisgo.network.mv_grid.graph,
                edisgo.network.mv_grid.station,
                get_weight, target=node)

            # find first node in path that exceeds 2/3 of the line length
            # from station to critical node farthest away from the station
            path = nx.shortest_path(edisgo.network.mv_grid.graph,
                                    edisgo.network.mv_grid.station,
                                    node)
            return next(j for j in path
                        if path_length[j] >= path_length[node] * 2 / 3)

        return None

    def _calc_storage_size(edisgo, feeder, max_storage_size):
        """
        Calculates storage size that reduces residual load.

        Parameters
        -----------
        edisgo : :class:`~.grid.network.EDisGo`
            The original edisgo object.
        feeder : :class:`~.grid.components.Line`
            MV feeder the storage will be connected to. The line object is an
            object from the copied graph.

        Returns
        -------
        :obj:`float`
            Storage size that reduced the residual load in the feeder.

        """
        step_size = 200
        sizes = [0] + list(np.arange(
            p_storage_min, max_storage_size + 0.5 * step_size, step_size))
        p_feeder = edisgo.network.results.pfa_p.loc[:, repr(feeder)]
        q_feeder = edisgo.network.results.pfa_q.loc[:, repr(feeder)]
        p_slack = edisgo.network.pypsa.generators_t.p.loc[
                  :, 'Generator_slack'] * 1e3

        # get sign of p and q
        l = edisgo.network.pypsa.lines.loc[repr(feeder), :]
        mv_station_bus = 'bus0' if l.loc['bus0'] == 'Bus_'.format(
            repr(edisgo.network.mv_grid.station)) else 'bus1'
        if mv_station_bus == 'bus0':
            diff = edisgo.network.pypsa.lines_t.p1.loc[:, repr(feeder)] - \
                   edisgo.network.pypsa.lines_t.p0.loc[:, repr(feeder)]
            diff_q = edisgo.network.pypsa.lines_t.q1.loc[:, repr(feeder)] - \
                     edisgo.network.pypsa.lines_t.q0.loc[:, repr(feeder)]
        else:
            diff = edisgo.network.pypsa.lines_t.p0.loc[:, repr(feeder)] - \
                   edisgo.network.pypsa.lines_t.p1.loc[:, repr(feeder)]
            diff_q = edisgo.network.pypsa.lines_t.q0.loc[:, repr(feeder)] - \
                     edisgo.network.pypsa.lines_t.q1.loc[:, repr(feeder)]
        p_sign = pd.Series([-1 if _ < 0 else 1 for _ in diff],
                           index=p_feeder.index)
        q_sign = pd.Series([-1 if _ < 0 else 1 for _ in diff_q],
                           index=p_feeder.index)

        # get allowed load factors per case
        lf = {'feedin_case': edisgo.network.config[
            'grid_expansion_load_factors']['mv_feedin_case_line'],
              'load_case': network.config[
            'grid_expansion_load_factors']['mv_load_case_line']}

        # calculate maximum apparent power for each storage size to find
        # storage size that minimizes apparent power in the feeder
        p_feeder = p_feeder.multiply(p_sign)
        q_feeder = q_feeder.multiply(q_sign)
        s_max = []
        for size in sizes:
            share = size / storage_nominal_power
            p_storage = storage_timeseries.p * share
            q_storage = storage_timeseries.q * share
            p_total = p_feeder + p_storage
            q_total = q_feeder + q_storage
            p_hv_mv_station = p_slack - p_storage
            lf_ts = p_hv_mv_station.apply(
                lambda _: lf['feedin_case'] if _ < 0 else lf['load_case'])
            s_max_ts = (p_total ** 2 + q_total ** 2).apply(
                sqrt).divide(lf_ts)
            s_max.append(max(s_max_ts))

        return sizes[pd.Series(s_max).idxmin()]

    def _critical_nodes_feeder(edisgo, feeder):
        """
        Returns all nodes in MV feeder with voltage issues.

        Parameters
        -----------
        edisgo : :class:`~.grid.network.EDisGo`
            The original edisgo object.
        feeder : :class:`~.grid.components.Line`
            MV feeder the storage will be connected to. The line object is an
            object from the copied graph.

        Returns
        -------
        :obj:`list`
            List with all nodes in MV feeder with voltage issues.

        """
        # get all nodes with voltage issues in MV grid
        critical_nodes = check_tech_constraints.mv_voltage_deviation(
            edisgo.network, voltage_levels='mv')
        if critical_nodes:
            critical_nodes = critical_nodes[edisgo.network.mv_grid]
        else:
            return []
        # filter nodes with voltage issues in feeder
        critical_nodes_feeder = []
        for n in critical_nodes.index:
            if repr(n.mv_feeder) == repr(feeder):
                critical_nodes_feeder.append(n)
        return critical_nodes_feeder

    def _critical_lines_feeder(edisgo, feeder):
        """
        Returns all lines in MV feeder with overload issues.

        Parameters
        -----------
        edisgo : :class:`~.grid.network.EDisGo`
            The original edisgo object.
        feeder : :class:`~.grid.components.Line`
            MV feeder the storage will be connected to. The line object is an
            object from the copied graph.

        Returns
        -------
        :pandas:`pandas.DataFrame<dataframe>`
            Dataframe containing over-loaded lines in MV feeder, their maximum
            relative over-loading and the corresponding time step. See
            :func:`edisgo.flex_opt.check_tech_constraints.mv_line_load` for
            more information.

        """
        # get all overloaded MV lines
        critical_lines = check_tech_constraints.mv_line_load(edisgo.network)
        # filter overloaded lines in feeder
        critical_lines_feeder = []
        for l in critical_lines.index:
            if repr(tools.get_mv_feeder_from_line(l)) == repr(feeder):
                critical_lines_feeder.append(l)
        return critical_lines.loc[critical_lines_feeder, :]

    def _estimate_new_number_of_lines(critical_lines_feeder):
        number_parallel_lines = 0
        for crit_line in critical_lines_feeder.index:
            number_parallel_lines += ceil(critical_lines_feeder.loc[
                crit_line, 'max_rel_overload'] * crit_line.quantity) - \
                                     crit_line.quantity
        return number_parallel_lines

    debug = kwargs.get('debug', False)
    check_costs_reduction = kwargs.get('check_costs_reduction', False)

    # global variables
    # minimum and maximum storage power to be connected to the MV grid
    p_storage_min = 300
    p_storage_max = 4500

    # remaining storage nominal power
    if storage_nominal_power is None:
        storage_nominal_power = max(abs(storage_timeseries.p))
    p_storage_remaining = storage_nominal_power

    if debug:
        feeder_repr = []
        storage_path = []
        storage_repr = []
        storage_size = []

    # rank MV feeders by grid expansion costs

    # conduct grid reinforcement on copied edisgo object on worst-case time
    # steps
    grid_expansion_results_init = edisgo.reinforce(
        copy_graph=True, timesteps_pfa='snapshot_analysis')

    # only analyse storage integration if there were any grid expansion needs
    if grid_expansion_results_init.equipment_changes.empty:
        logger.debug('No storage integration necessary since there are no '
                     'grid expansion needs.')
        return
    else:
        equipment_changes_reinforcement_init = \
            grid_expansion_results_init.equipment_changes.loc[
                grid_expansion_results_init.equipment_changes.iteration_step >
                0]
        total_grid_expansion_costs = \
            grid_expansion_results_init.grid_expansion_costs.total_costs.sum()
        if equipment_changes_reinforcement_init.empty:
            logger.debug('No storage integration necessary since there are no '
                         'grid expansion needs.')
            return
        else:
            network = equipment_changes_reinforcement_init.index[
                0].grid.network

    # calculate grid expansion costs without costs for new generators
    # to be used in feeder ranking
    grid_expansion_costs_feeder_ranking = costs.grid_expansion_costs(
        network, without_generator_import=True)

    ranked_feeders = _feeder_ranking(grid_expansion_costs_feeder_ranking)

    count = 1
    storage_obj_list = []
    total_grid_expansion_costs_new = 'not calculated'
    for feeder in ranked_feeders.values:
        logger.debug('Feeder: {}'.format(count))
        count += 1

        # first step: find node where storage will be installed

        critical_nodes_feeder = _critical_nodes_feeder(edisgo, feeder)
        critical_lines_feeder = _critical_lines_feeder(edisgo, feeder)

        # get node the storage will be connected to (in original graph)
        battery_node = _find_battery_node(edisgo, critical_lines_feeder,
                                          critical_nodes_feeder)

        if battery_node:

            # add to output lists
            if debug:
                feeder_repr.append(repr(feeder))
                storage_path.append(nx.shortest_path(
                    edisgo.network.mv_grid.graph,
                    edisgo.network.mv_grid.station,
                    battery_node))

            # second step: calculate storage size

            max_storage_size = min(p_storage_remaining, p_storage_max)
            p_storage = _calc_storage_size(edisgo, feeder, max_storage_size)

            # if p_storage is greater than or equal to the minimum storage
            # power required, do storage integration
            if p_storage >= p_storage_min:

                # third step: integrate storage

                share = p_storage / storage_nominal_power
                edisgo.integrate_storage(
                    timeseries=storage_timeseries.p * share,
                    position=battery_node,
                    voltage_level='mv',
                    timeseries_reactive_power=storage_timeseries.q * share)
                tools.assign_mv_feeder_to_nodes(edisgo.network.mv_grid)

                # get new storage object
                storage_obj = [_
                               for _ in
                               edisgo.network.mv_grid.graph.nodes_by_attribute(
                                   'storage') if _ in
                               edisgo.network.mv_grid.graph.neighbors(
                                   battery_node)][0]
                storage_obj_list.append(storage_obj)

                logger.debug(
                    'Storage with nominal power of {} kW connected to '
                    'node {} (path to HV/MV station {}).'.format(
                        p_storage, battery_node, nx.shortest_path(
                            battery_node.grid.graph, battery_node.grid.station,
                            battery_node)))

                # fourth step: check if storage integration reduced grid
                # reinforcement costs or number of issues

                if check_costs_reduction == 'each_feeder':

                    # calculate new grid expansion costs

                    grid_expansion_results_new = edisgo.reinforce(
                        copy_graph=True, timesteps_pfa='snapshot_analysis')

                    total_grid_expansion_costs_new = \
                        grid_expansion_results_new.grid_expansion_costs.\
                            total_costs.sum()

                    costs_diff = total_grid_expansion_costs - \
                                 total_grid_expansion_costs_new

                    if costs_diff > 0:
                        logger.debug(
                            'Storage integration in feeder {} reduced grid '
                            'expansion costs by {} kEuro.'.format(
                                feeder, costs_diff))

                        if debug:
                            storage_repr.append(repr(storage_obj))
                            storage_size.append(storage_obj.nominal_power)

                        total_grid_expansion_costs = \
                            total_grid_expansion_costs_new

                    else:
                        logger.debug(
                            'Storage integration in feeder {} did not reduce '
                            'grid expansion costs (costs increased by {} '
                            'kEuro).'.format(feeder, -costs_diff))

                        tools.disconnect_storage(edisgo.network, storage_obj)
                        p_storage = 0

                        if debug:
                            storage_repr.append(None)
                            storage_size.append(0)

                            edisgo.integrate_storage(
                                timeseries=storage_timeseries.p * 0,
                                position=battery_node,
                                voltage_level='mv',
                                timeseries_reactive_power=
                                storage_timeseries.q * 0)
                            tools.assign_mv_feeder_to_nodes(
                                edisgo.network.mv_grid)

                else:
                    number_parallel_lines_before = \
                        _estimate_new_number_of_lines(critical_lines_feeder)
                    edisgo.analyze()
                    critical_lines_feeder_new = _critical_lines_feeder(
                        edisgo, feeder)
                    critical_nodes_feeder_new = _critical_nodes_feeder(
                        edisgo, feeder)
                    number_parallel_lines = _estimate_new_number_of_lines(
                        critical_lines_feeder_new)

                    # if there are critical lines check if number of parallel
                    # lines was reduced
                    if not critical_lines_feeder.empty:
                        diff_lines = number_parallel_lines_before - \
                                     number_parallel_lines
                        # if it was not reduced check if there are critical
                        # nodes and if the number was reduced
                        if diff_lines <= 0:
                            # if there are no critical nodes remove storage
                            if not critical_nodes_feeder:
                                logger.debug(
                                    'Storage integration in feeder {} did not '
                                    'reduce number of critical lines (number '
                                    'increased by {}), storage '
                                    'is therefore removed.'.format(
                                        feeder, -diff_lines))

                                tools.disconnect_storage(edisgo.network,
                                                         storage_obj)
                                p_storage = 0

                                if debug:
                                    storage_repr.append(None)
                                    storage_size.append(0)

                                    edisgo.integrate_storage(
                                        timeseries=storage_timeseries.p * 0,
                                        position=battery_node,
                                        voltage_level='mv',
                                        timeseries_reactive_power=
                                        storage_timeseries.q * 0)
                                    tools.assign_mv_feeder_to_nodes(
                                        edisgo.network.mv_grid)
                            else:
                                logger.debug(
                                    'Critical nodes in feeder {} '
                                    'before and after storage integration: '
                                    '{} vs. {}'.format(
                                        feeder, critical_nodes_feeder,
                                        critical_nodes_feeder_new))
                                if debug:
                                    storage_repr.append(repr(storage_obj))
                                    storage_size.append(
                                        storage_obj.nominal_power)
                        else:
                            logger.debug(
                                'Storage integration in feeder {} reduced '
                                'number of critical lines.'.format(feeder))

                            if debug:
                                storage_repr.append(repr(storage_obj))
                                storage_size.append(storage_obj.nominal_power)

                    # if there are no critical lines
                    else:
                        logger.debug(
                            'Critical nodes in feeder {} '
                            'before and after storage integration: '
                            '{} vs. {}'.format(
                                feeder, critical_nodes_feeder,
                                critical_nodes_feeder_new))
                        if debug:
                            storage_repr.append(repr(storage_obj))
                            storage_size.append(storage_obj.nominal_power)

                # fifth step: if there is storage capacity left, rerun
                # the past steps for the next feeder in the ranking
                # list
                p_storage_remaining = p_storage_remaining - p_storage
                if not p_storage_remaining > p_storage_min:
                    break

            else:
                logger.debug('No storage integration in feeder {}.'.format(
                    feeder))

                if debug:
                    storage_repr.append(None)
                    storage_size.append(0)

                    edisgo.integrate_storage(
                        timeseries=storage_timeseries.p * 0,
                        position=battery_node,
                        voltage_level='mv',
                        timeseries_reactive_power=storage_timeseries.q * 0)
                    tools.assign_mv_feeder_to_nodes(edisgo.network.mv_grid)
        else:
            logger.debug('No storage integration in feeder {} because there '
                         'are neither overloading nor voltage issues.'.format(
                feeder))

            if debug:
                storage_repr.append(None)
                storage_size.append(0)
                feeder_repr.append(repr(feeder))
                storage_path.append([])

    if check_costs_reduction == 'once':
        # check costs reduction and discard all storages if costs were not
        # reduced
        grid_expansion_results_new = edisgo.reinforce(
            copy_graph=True, timesteps_pfa='snapshot_analysis')

        total_grid_expansion_costs_new = \
            grid_expansion_results_new.grid_expansion_costs. \
                total_costs.sum()

        costs_diff = total_grid_expansion_costs - \
                     total_grid_expansion_costs_new

        if costs_diff > 0:
            logger.info(
                'Storage integration in grid {} reduced grid '
                'expansion costs by {} kEuro.'.format(
                    edisgo.network.id, costs_diff))
        else:
            logger.info(
                'Storage integration in grid {} did not reduce '
                'grid expansion costs (costs increased by {} '
                'kEuro).'.format(edisgo.network.id, -costs_diff))

            for storage in storage_obj_list:
                tools.disconnect_storage(edisgo.network, storage)
    elif check_costs_reduction == 'each_feeder':
        # if costs redcution was checked after each storage only give out
        # total costs reduction
        if total_grid_expansion_costs_new == 'not calculated':
            costs_diff = 0
        else:
            total_grid_expansion_costs = grid_expansion_results_init.\
                grid_expansion_costs.total_costs.sum()
            costs_diff = total_grid_expansion_costs - \
                         total_grid_expansion_costs_new

        logger.info(
            'Storage integration in grid {} reduced grid '
            'expansion costs by {} kEuro.'.format(
                edisgo.network.id, costs_diff))

    if debug:
        plots.storage_size(edisgo.network.mv_grid, edisgo.network.pypsa,
                           filename='storage_results_{}.pdf'.format(
                               edisgo.network.id), lopf=False)
        storages_df = pd.DataFrame({'path': storage_path,
                                    'repr': storage_repr,
                                    'p_nom': storage_size},
                                   index=feeder_repr)
        storages_df.to_csv('storage_results_{}.csv'.format(edisgo.network.id))

    edisgo.network.results.storages_costs_reduction = pd.DataFrame(
        {'grid_expansion_costs_initial': total_grid_expansion_costs,
         'grid_expansion_costs_with_storages': total_grid_expansion_costs_new},
        index=[edisgo.network.id])
Allocates the given storage capacity to multiple smaller storages.

    For each feeder with load or voltage issues it is checked if integrating a
    storage will reduce peaks in the feeder, starting with the feeder with
    the highest theoretical grid expansion costs. A heuristic approach is used
    to estimate storage sizing and siting while storage operation is carried
    over from the given storage operation.

    Parameters
    -----------
    edisgo : :class:`~.grid.network.EDisGo`
    storage_timeseries : :pandas:`pandas.DataFrame<dataframe>`
        Total active and reactive power time series that will be allocated to
        the smaller storages in feeders with load or voltage issues. Columns of
        the dataframe are 'p' containing active power time series in kW and 'q'
        containing the reactive power time series in kvar. Index is a
        :pandas:`pandas.DatetimeIndex<datetimeindex>`.
    storage_nominal_power : :obj:`float` or None
        Nominal power in kW that will be allocated to the smaller storages in
        feeders with load or voltage issues. If no nominal power is provided
        the maximum active power given in `storage_timeseries` is used.
        Default: None.
    debug : :obj:`Boolean`, optional
        If dedug is True a dataframe with storage size and path to storage of
        all installed and possibly discarded storages is saved to a csv file
        and a plot with all storage positions is created and saved, both to the
        current working directory with filename `storage_results_{MVgrid_id}`.
        Default: False.
    check_costs_reduction : :obj:`Boolean` or :obj:`str`, optional
        This parameter specifies when and whether it should be checked if a
        storage reduced grid expansion costs or not. It can be used as a safety
        check but can be quite time consuming. Possible options are:

        * 'each_feeder'
          Costs reduction is checked for each feeder. If the storage did not
          reduce grid expansion costs it is discarded.
        * 'once'
          Costs reduction is checked after the total storage capacity is
          allocated to the feeders. If the storages did not reduce grid
          expansion costs they are all discarded.
        * False
          Costs reduction is never checked.

        Default: False.