def reinforce_grid(edisgo, timesteps_pfa=None, copy_graph=False,
                   max_while_iterations=10, combined_analysis=False):
    """
    Evaluates grid reinforcement needs and performs measures.

    This function is the parent function for all grid reinforcements.

    Parameters
    ----------
    edisgo : :class:`~.grid.network.EDisGo`
        The eDisGo API object
    timesteps_pfa : :obj:`str` or :pandas:`pandas.DatetimeIndex<datetimeindex>` or :pandas:`pandas.Timestamp<timestamp>`
        timesteps_pfa specifies for which time steps power flow analysis is
        conducted and therefore which time steps to consider when checking
        for over-loading and over-voltage issues.
        It defaults to None in which case all timesteps in
        timeseries.timeindex (see :class:`~.grid.network.TimeSeries`) are used.
        Possible options are:

        * None
          Time steps in timeseries.timeindex (see
          :class:`~.grid.network.TimeSeries`) are used.
        * 'snapshot_analysis'
          Reinforcement is conducted for two worst-case snapshots. See
          :meth:`edisgo.tools.tools.select_worstcase_snapshots()` for further
          explanation on how worst-case snapshots are chosen.
          Note: If you have large time series choosing this option will save
          calculation time since power flow analysis is only conducted for two
          time steps. If your time series already represents the worst-case
          keep the default value of None because finding the worst-case
          snapshots takes some time.
        * :pandas:`pandas.DatetimeIndex<datetimeindex>` or :pandas:`pandas.Timestamp<timestamp>`
          Use this option to explicitly choose which time steps to consider.

    copy_graph : :obj:`Boolean`
        If True reinforcement is conducted on a copied graph and discarded.
        Default: False.
    max_while_iterations : :obj:`int`
        Maximum number of times each while loop is conducted.
    combined_analysis : :obj:`Boolean`
        If True allowed voltage deviations for combined analysis of MV and LV
        grid are used. If False different allowed voltage deviations for MV
        and LV are used. See also config section
        `grid_expansion_allowed_voltage_deviations`. Default: False.

    Returns
    -------
    :class:`~.grid.network.Results`
        Returns the Results object holding grid expansion costs, equipment
        changes, etc.

    Notes
    -----
    See :ref:`features-in-detail` for more information on how grid
    reinforcement is conducted.

    """

    def _add_lines_changes_to_equipment_changes():
        equipment, index, quantity = [], [], []
        for line, number_of_lines in lines_changes.items():
            equipment.append(line.type.name)
            index.append(line)
            quantity.append(number_of_lines)
        edisgo_reinforce.network.results.equipment_changes = \
            edisgo_reinforce.network.results.equipment_changes.append(
                pd.DataFrame(
                    {'iteration_step': [iteration_step] * len(
                        lines_changes),
                     'change': ['changed'] * len(lines_changes),
                     'equipment': equipment,
                     'quantity': quantity},
                    index=index))

    def _add_transformer_changes_to_equipment_changes(mode):
        for station, transformer_list in transformer_changes[mode].items():
            edisgo_reinforce.network.results.equipment_changes = \
                edisgo_reinforce.network.results.equipment_changes.append(
                    pd.DataFrame(
                        {'iteration_step': [iteration_step] * len(
                            transformer_list),
                         'change': [mode] * len(transformer_list),
                         'equipment': transformer_list,
                         'quantity': [1] * len(transformer_list)},
                        index=[station] * len(transformer_list)))

    # assign MV feeder to every generator, LV station, load, and branch tee
    # to assign grid expansion costs to an MV feeder
    assign_mv_feeder_to_nodes(edisgo.network.mv_grid)

    # analyze for all time steps (advantage is that load and feed-in case can
    # be obtained more performant in case `timesteps_pfa` = 'snapshot_analysis'
    # plus edisgo and edisgo_reinforce will have pypsa representation in case
    # reinforcement needs to be conducted on a copied graph)
    edisgo.analyze()

    # in case reinforcement needs to be conducted on a copied graph the
    # edisgo object is deep copied
    if copy_graph is True:
        edisgo_reinforce = copy.deepcopy(edisgo)
    else:
        edisgo_reinforce = edisgo

    if timesteps_pfa is not None:
        # if timesteps_pfa = 'snapshot_analysis' get snapshots
        if (isinstance(timesteps_pfa, str) and
                    timesteps_pfa == 'snapshot_analysis'):
            snapshots = tools.select_worstcase_snapshots(
                edisgo_reinforce.network)
            # drop None values in case any of the two snapshots does not exist
            timesteps_pfa = pd.DatetimeIndex(data=[
                snapshots['load_case'], snapshots['feedin_case']]).dropna()
        # if timesteps_pfa is not of type datetime or does not contain
        # datetimes throw an error
        elif not isinstance(timesteps_pfa, datetime.datetime):
            if hasattr(timesteps_pfa, '__iter__'):
                if not all(isinstance(_, datetime.datetime)
                           for _ in timesteps_pfa):
                    raise ValueError(
                        'Input {} for timesteps_pfa is not valid.'.format(
                        timesteps_pfa))
            else:
                raise ValueError(
                    'Input {} for timesteps_pfa is not valid.'.format(
                        timesteps_pfa))

    iteration_step = 1
    edisgo_reinforce.analyze(timesteps=timesteps_pfa)

    # REINFORCE OVERLOADED TRANSFORMERS AND LINES

    logger.debug('==> Check station load.')
    overloaded_mv_station = checks.hv_mv_station_load(edisgo_reinforce.network)
    overloaded_lv_stations = checks.mv_lv_station_load(
        edisgo_reinforce.network)
    logger.debug('==> Check line load.')
    crit_lines_lv = checks.lv_line_load(edisgo_reinforce.network)
    crit_lines_mv = checks.mv_line_load(edisgo_reinforce.network)
    crit_lines = crit_lines_lv.append(crit_lines_mv)

    while_counter = 0
    while ((not overloaded_mv_station.empty or not overloaded_lv_stations.empty
                or not crit_lines.empty) and
            while_counter < max_while_iterations):

        if not overloaded_mv_station.empty:
            # reinforce substations
            transformer_changes = \
                reinforce_measures.extend_substation_overloading(
                    edisgo_reinforce.network, overloaded_mv_station)
            # write added and removed transformers to results.equipment_changes
            _add_transformer_changes_to_equipment_changes('added')
            _add_transformer_changes_to_equipment_changes('removed')

        if not overloaded_lv_stations.empty:
            # reinforce distribution substations
            transformer_changes = \
                reinforce_measures.extend_distribution_substation_overloading(
                    edisgo_reinforce.network, overloaded_lv_stations)
            # write added and removed transformers to results.equipment_changes
            _add_transformer_changes_to_equipment_changes('added')
            _add_transformer_changes_to_equipment_changes('removed')

        if not crit_lines.empty:
            # reinforce lines
            lines_changes = reinforce_measures.reinforce_branches_overloading(
                edisgo_reinforce.network, crit_lines)
            # write changed lines to results.equipment_changes
            _add_lines_changes_to_equipment_changes()

        # run power flow analysis again (after updating pypsa object) and check
        # if all over-loading problems were solved
        logger.debug('==> Run power flow analysis.')
        pypsa_io.update_pypsa_grid_reinforcement(
            edisgo_reinforce.network,
            edisgo_reinforce.network.results.equipment_changes[
                edisgo_reinforce.network.results.equipment_changes.
                    iteration_step==iteration_step])
        edisgo_reinforce.analyze(timesteps=timesteps_pfa)
        logger.debug('==> Recheck station load.')
        overloaded_mv_station = checks.hv_mv_station_load(
            edisgo_reinforce.network)
        overloaded_lv_stations = checks.mv_lv_station_load(
            edisgo_reinforce.network)
        logger.debug('==> Recheck line load.')
        crit_lines_lv = checks.lv_line_load(edisgo_reinforce.network)
        crit_lines_mv = checks.mv_line_load(edisgo_reinforce.network)
        crit_lines = crit_lines_lv.append(crit_lines_mv)

        iteration_step += 1
        while_counter += 1

    # check if all load problems were solved after maximum number of
    # iterations allowed
    if (while_counter == max_while_iterations and
            (not crit_lines.empty or not overloaded_mv_station.empty or
                 not overloaded_lv_stations.empty)):
        edisgo_reinforce.network.results.unresolved_issues.update(crit_lines)
        edisgo_reinforce.network.results.unresolved_issues.update(
            overloaded_lv_stations)
        edisgo_reinforce.network.results.unresolved_issues.update(
            overloaded_mv_station)
        raise exceptions.MaximumIterationError(
            "Overloading issues for the following lines could not be solved:"
            "{}".format(crit_lines))
    else:
        logger.info('==> Load issues were solved in {} iteration '
                    'step(s).'.format(while_counter))

    # REINFORCE BRANCHES DUE TO VOLTAGE ISSUES
    iteration_step += 1

    # solve voltage problems in MV grid
    logger.debug('==> Check voltage in MV grid.')
    if combined_analysis:
        voltage_levels = 'mv_lv'
    else:
        voltage_levels = 'mv'
    crit_nodes = checks.mv_voltage_deviation(edisgo_reinforce.network,
                                             voltage_levels=voltage_levels)

    while_counter = 0
    while crit_nodes and while_counter < max_while_iterations:

        # reinforce lines
        lines_changes = reinforce_measures.reinforce_branches_overvoltage(
            edisgo_reinforce.network, edisgo_reinforce.network.mv_grid,
            crit_nodes[edisgo_reinforce.network.mv_grid])
        # write changed lines to results.equipment_changes
        _add_lines_changes_to_equipment_changes()

        # run power flow analysis again (after updating pypsa object) and check
        # if all over-voltage problems were solved
        logger.debug('==> Run power flow analysis.')
        pypsa_io.update_pypsa_grid_reinforcement(
            edisgo_reinforce.network,
            edisgo_reinforce.network.results.equipment_changes[
                edisgo_reinforce.network.results.equipment_changes.
                    iteration_step == iteration_step])
        edisgo_reinforce.analyze(timesteps=timesteps_pfa)
        logger.debug('==> Recheck voltage in MV grid.')
        crit_nodes = checks.mv_voltage_deviation(edisgo_reinforce.network,
                                                 voltage_levels=voltage_levels)

        iteration_step += 1
        while_counter += 1

    # check if all voltage problems were solved after maximum number of
    # iterations allowed
    if while_counter == max_while_iterations and crit_nodes:
        for k, v in crit_nodes.items():
            for node in v.index:
                edisgo_reinforce.network.results.unresolved_issues.update(
                    {repr(node): v.loc[node, 'v_mag_pu']})
        raise exceptions.MaximumIterationError(
            "Over-voltage issues for the following nodes in MV grid could "
            "not be solved: {}".format(crit_nodes))
    else:
        logger.info('==> Voltage issues in MV grid were solved in {} '
                    'iteration step(s).'.format(while_counter))

    # solve voltage problems at secondary side of LV stations
    logger.debug('==> Check voltage at secondary side of LV stations.')
    if combined_analysis:
        voltage_levels = 'mv_lv'
    else:
        voltage_levels = 'lv'
    crit_stations = checks.lv_voltage_deviation(edisgo_reinforce.network,
                                                mode='stations',
                                                voltage_levels=voltage_levels)

    while_counter = 0
    while crit_stations and while_counter < max_while_iterations:
        # reinforce distribution substations
        transformer_changes = \
            reinforce_measures.extend_distribution_substation_overvoltage(
                edisgo_reinforce.network, crit_stations)
        # write added transformers to results.equipment_changes
        _add_transformer_changes_to_equipment_changes('added')

        # run power flow analysis again (after updating pypsa object) and check
        # if all over-voltage problems were solved
        logger.debug('==> Run power flow analysis.')
        pypsa_io.update_pypsa_grid_reinforcement(
            edisgo_reinforce.network,
            edisgo_reinforce.network.results.equipment_changes[
                edisgo_reinforce.network.results.equipment_changes.
                    iteration_step == iteration_step])
        edisgo_reinforce.analyze(timesteps=timesteps_pfa)
        logger.debug('==> Recheck voltage at secondary side of LV stations.')
        crit_stations = checks.lv_voltage_deviation(
            edisgo_reinforce.network, mode='stations',
            voltage_levels=voltage_levels)

        iteration_step += 1
        while_counter += 1

    # check if all voltage problems were solved after maximum number of
    # iterations allowed
    if while_counter == max_while_iterations and crit_stations:
        for k, v in crit_stations.items():
            for node in v.index:
                edisgo_reinforce.network.results.unresolved_issues.update(
                    {repr(node): v.loc[node, 'v_mag_pu']})
        raise exceptions.MaximumIterationError(
            "Over-voltage issues at busbar could not be solved for the "
            "following LV grids: {}".format(crit_stations))
    else:
        logger.info('==> Voltage issues at busbars in LV grids were solved '
                    'in {} iteration step(s).'.format(while_counter))

    # solve voltage problems in LV grids
    logger.debug('==> Check voltage in LV grids.')
    crit_nodes = checks.lv_voltage_deviation(edisgo_reinforce.network,
                                             voltage_levels=voltage_levels)

    while_counter = 0
    while crit_nodes and while_counter < max_while_iterations:
        # for every grid in crit_nodes do reinforcement
        for grid in crit_nodes:
            # reinforce lines
            lines_changes = reinforce_measures.reinforce_branches_overvoltage(
                edisgo_reinforce.network, grid, crit_nodes[grid])
            # write changed lines to results.equipment_changes
            _add_lines_changes_to_equipment_changes()

        # run power flow analysis again (after updating pypsa object) and check
        # if all over-voltage problems were solved
        logger.debug('==> Run power flow analysis.')
        pypsa_io.update_pypsa_grid_reinforcement(
            edisgo_reinforce.network,
            edisgo_reinforce.network.results.equipment_changes[
                edisgo_reinforce.network.results.equipment_changes.
                    iteration_step == iteration_step])
        edisgo_reinforce.analyze(timesteps=timesteps_pfa)
        logger.debug('==> Recheck voltage in LV grids.')
        crit_nodes = checks.lv_voltage_deviation(edisgo_reinforce.network,
                                                 voltage_levels=voltage_levels)

        iteration_step += 1
        while_counter += 1

    # check if all voltage problems were solved after maximum number of
    # iterations allowed
    if while_counter == max_while_iterations and crit_nodes:
        for k, v in crit_nodes.items():
            for node in v.index:
                edisgo_reinforce.network.results.unresolved_issues.update(
                    {repr(node): v.loc[node, 'v_mag_pu']})
        raise exceptions.MaximumIterationError(
            "Over-voltage issues for the following nodes in LV grids could "
            "not be solved: {}".format(crit_nodes))
    else:
        logger.info(
            '==> Voltage issues in LV grids were solved '
            'in {} iteration step(s).'.format(while_counter))

    # RECHECK FOR OVERLOADED TRANSFORMERS AND LINES
    logger.debug('==> Recheck station load.')
    overloaded_mv_station = checks.hv_mv_station_load(edisgo_reinforce.network)
    overloaded_lv_stations = checks.mv_lv_station_load(
        edisgo_reinforce.network)
    logger.debug('==> Recheck line load.')
    crit_lines_lv = checks.lv_line_load(edisgo_reinforce.network)
    crit_lines_mv = checks.mv_line_load(edisgo_reinforce.network)
    crit_lines = crit_lines_lv.append(crit_lines_mv)

    while_counter = 0
    while ((not overloaded_mv_station.empty or not overloaded_lv_stations.empty
            or not crit_lines.empty) and while_counter < max_while_iterations):

        if not overloaded_mv_station.empty:
            # reinforce substations
            transformer_changes = \
                reinforce_measures.extend_substation_overloading(
                    edisgo_reinforce.network, overloaded_mv_station)
            # write added and removed transformers to results.equipment_changes
            _add_transformer_changes_to_equipment_changes('added')
            _add_transformer_changes_to_equipment_changes('removed')

        if not overloaded_lv_stations.empty:
            # reinforce substations
            transformer_changes = \
                reinforce_measures.extend_distribution_substation_overloading(
                    edisgo_reinforce.network, overloaded_lv_stations)
            # write added and removed transformers to results.equipment_changes
            _add_transformer_changes_to_equipment_changes('added')
            _add_transformer_changes_to_equipment_changes('removed')

        if not crit_lines.empty:
            # reinforce lines
            lines_changes = reinforce_measures.reinforce_branches_overloading(
                edisgo_reinforce.network, crit_lines)
            # write changed lines to results.equipment_changes
            _add_lines_changes_to_equipment_changes()

        # run power flow analysis again (after updating pypsa object) and check
        # if all over-loading problems were solved
        logger.debug('==> Run power flow analysis.')
        pypsa_io.update_pypsa_grid_reinforcement(
            edisgo_reinforce.network,
            edisgo_reinforce.network.results.equipment_changes[
                edisgo_reinforce.network.results.equipment_changes.
                    iteration_step == iteration_step])
        edisgo_reinforce.analyze(timesteps=timesteps_pfa)
        logger.debug('==> Recheck station load.')
        overloaded_mv_station = checks.hv_mv_station_load(
            edisgo_reinforce.network)
        overloaded_lv_stations = checks.mv_lv_station_load(
            edisgo_reinforce.network)
        logger.debug('==> Recheck line load.')
        crit_lines_lv = checks.lv_line_load(edisgo_reinforce.network)
        crit_lines_mv = checks.mv_line_load(edisgo_reinforce.network)
        crit_lines = crit_lines_lv.append(crit_lines_mv)

        iteration_step += 1
        while_counter += 1

    # check if all load problems were solved after maximum number of
    # iterations allowed
    if (while_counter == max_while_iterations and
            (not crit_lines.empty or not overloaded_mv_station.empty or
                 not overloaded_lv_stations.empty)):
        edisgo_reinforce.network.results.unresolved_issues.update(crit_lines)
        edisgo_reinforce.network.results.unresolved_issues.update(
            overloaded_lv_stations)
        edisgo_reinforce.network.results.unresolved_issues.update(
            overloaded_mv_station)
        raise exceptions.MaximumIterationError(
            "Overloading issues (after solving over-voltage issues) for the"
            "following lines could not be solved: {}".format(crit_lines))
    else:
        logger.info(
            '==> Load issues were rechecked and solved '
            'in {} iteration step(s).'.format(while_counter))

    # final check 10% criteria
    checks.check_ten_percent_voltage_deviation(edisgo_reinforce.network)

    # calculate grid expansion costs
    edisgo_reinforce.network.results.grid_expansion_costs = \
        grid_expansion_costs(edisgo_reinforce.network)

    return edisgo_reinforce.network.results
Evaluates grid reinforcement needs and performs measures.

    This function is the parent function for all grid reinforcements.

    Parameters
    ----------
    edisgo : :class:`~.grid.network.EDisGo`
        The eDisGo API object
    timesteps_pfa : :obj:`str` or :pandas:`pandas.DatetimeIndex<datetimeindex>` or :pandas:`pandas.Timestamp<timestamp>`
        timesteps_pfa specifies for which time steps power flow analysis is
        conducted and therefore which time steps to consider when checking
        for over-loading and over-voltage issues.
        It defaults to None in which case all timesteps in
        timeseries.timeindex (see :class:`~.grid.network.TimeSeries`) are used.
        Possible options are:

        * None
          Time steps in timeseries.timeindex (see
          :class:`~.grid.network.TimeSeries`) are used.
        * 'snapshot_analysis'
          Reinforcement is conducted for two worst-case snapshots. See
          :meth:`edisgo.tools.tools.select_worstcase_snapshots()` for further
          explanation on how worst-case snapshots are chosen.
          Note: If you have large time series choosing this option will save
          calculation time since power flow analysis is only conducted for two
          time steps. If your time series already represents the worst-case
          keep the default value of None because finding the worst-case
          snapshots takes some time.
        * :pandas:`pandas.DatetimeIndex<datetimeindex>` or :pandas:`pandas.Timestamp<timestamp>`
          Use this option to explicitly choose which time steps to consider.

    copy_graph : :obj:`Boolean`
        If True reinforcement is conducted on a copied graph and discarded.
        Default: False.
    max_while_iterations : :obj:`int`
        Maximum number of times each while loop is conducted.
    combined_analysis : :obj:`Boolean`
        If True allowed voltage deviations for combined analysis of MV and LV
        grid are used. If False different allowed voltage deviations for MV
        and LV are used. See also config section
        `grid_expansion_allowed_voltage_deviations`. Default: False.

    Returns
    -------
    :class:`~.grid.network.Results`
        Returns the Results object holding grid expansion costs, equipment
        changes, etc.

    Notes
    -----
    See :ref:`features-in-detail` for more information on how grid
    reinforcement is conducted.