docs: 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