docs: def voltage_based(feedin, generators, curtailment_timeseries, edisgo,
                    curtailment_key, **kwargs):
      """
      Implements curtailment methodology 'voltage-based'.
  
      The curtailment that has to be met in each time step is allocated depending
      on the exceedance of the allowed voltage deviation at the nodes of the
      generators. The higher the exceedance, the higher the curtailment.
  
      The optional parameter `voltage_threshold` specifies the threshold for the
      exceedance of the allowed voltage deviation above which a generator is
      curtailed. By default it is set to zero, meaning that all generators at
      nodes with voltage deviations that exceed the allowed voltage deviation are
      curtailed. Generators at nodes where the allowed voltage deviation is not
      exceeded are not curtailed. In the case that the required curtailment
      exceeds the weather-dependent availability of all generators with voltage
      deviations above the specified threshold, the voltage threshold is lowered
      in steps of 0.01 p.u. until the curtailment target can be met.
  
      Above the threshold, the curtailment is proportional to the exceedance of
      the allowed voltage deviation. In order to find the linear relation between
      the curtailment and the voltage difference a linear problem is formulated
      and solved using the python package pyomo. See documentation for further
      information.
  
      Parameters
      ----------
      feedin : :pandas:`pandas.DataFrame<dataframe>`
          Dataframe holding the feed-in of each generator in kW for the
          technology (and weather cell) specified in `curtailment_key` parameter.
          Index of the dataframe is a
          :pandas:`pandas.DatetimeIndex<datetimeindex>`. Columns are the
          representatives of the fluctuating generators.
      generators : :pandas:`pandas.DataFrame<dataframe>`
          Dataframe with all generators of the type (and in weather cell)
          specified in `curtailment_key` parameter. See return value of
          :func:`edisgo.grid.tools.get_gen_info` for more information.
      curtailment_timeseries : :pandas:`pandas.Series<series>`
          The curtailment in kW to be distributed amongst the generators in
          `generators` parameter. Index of the series is a
          :pandas:`pandas.DatetimeIndex<datetimeindex>`.
      edisgo : :class:`edisgo.grid.network.EDisGo`
      curtailment_key : :obj:`str` or :obj:`tuple` with :obj:`str`
          The technology and weather cell ID if :obj:`tuple` or only
          the technology if :obj:`str` the curtailment is specified for.
      voltage_threshold: :obj:`float`
          The node voltage below which no curtailment is assigned to the
          respective generator if not necessary. Default: 0.0.
      solver: :obj:`str`
          The solver used to optimize the curtailment assigned to the generator.
          Possible options are:
  
          * 'cbc'
            coin-or branch and cut solver
          * 'glpk'
            gnu linear programming kit solver
          * any other available compatible with 'pyomo' like 'gurobi'
            or 'cplex'
  
          Default: 'cbc'
  
      """
  
      voltage_threshold = pd.Series(kwargs.get('voltage_threshold', 0.0),
                                    index=curtailment_timeseries.index)
      solver = kwargs.get('solver', 'cbc')
      combined_analysis = kwargs.get('combined_analysis', False)
  
      # get the voltages at the generators
      voltages_lv_gens = edisgo.network.results.v_res(
          nodes=generators.loc[(generators.voltage_level == 'lv')].index,
          level='lv')
      voltages_mv_gens = edisgo.network.results.v_res(
          nodes=generators.loc[(generators.voltage_level == 'mv')].index,
          level='mv')
      voltages_gens = voltages_lv_gens.join(voltages_mv_gens)
  
      # get voltages at stations
      grids = list(set(generators.grid))
      lv_stations = [_.station for _ in grids if 'LVStation' in repr(_.station)]
      voltage_lv_stations = edisgo.network.results.v_res(
          nodes=lv_stations, level='lv')
      voltages_mv_station = edisgo.network.results.v_res(
          nodes=[edisgo.network.mv_grid.station], level='mv')
      voltages_stations = voltage_lv_stations.join(voltages_mv_station)
  
      # get allowed voltage deviations
      if not combined_analysis:
          allowed_voltage_dev_mv = edisgo.network.config[
              'grid_expansion_allowed_voltage_deviations'][
              'mv_feedin_case_max_v_deviation']
          allowed_voltage_diff_lv = edisgo.network.config[
              'grid_expansion_allowed_voltage_deviations'][
              'lv_feedin_case_max_v_deviation']
      else:
          allowed_voltage_dev_mv = edisgo.network.config[
              'grid_expansion_allowed_voltage_deviations'][
              'mv_lv_feedin_case_max_v_deviation']
          allowed_voltage_diff_lv = edisgo.network.config[
              'grid_expansion_allowed_voltage_deviations'][
              'mv_lv_feedin_case_max_v_deviation']
      generators['allowed_voltage_dev'] = generators.voltage_level.apply(
          lambda _: allowed_voltage_diff_lv if _ == 'lv'
          else allowed_voltage_dev_mv)
  
      # calculate voltage difference from generator node to station
      voltage_gens_diff = pd.DataFrame()
      for gen in voltages_gens.columns:
          station = generators[generators.gen_repr==gen].grid.values[0].station
          voltage_gens_diff[gen] = voltages_gens.loc[:, gen] - \
                                   voltages_stations.loc[:, repr(station)] - \
                                   generators[generators.gen_repr ==
                                              gen].allowed_voltage_dev.iloc[0]
  
      # for every time step check if curtailment can be fulfilled, otherwise
      # reduce voltage threshold; set feed-in of generators below voltage
      # threshold to zero, so that they cannot be curtailed
      for ts in curtailment_timeseries.index:
          # get generators with voltage higher than threshold
          gen_pool = voltage_gens_diff.loc[
              ts, voltage_gens_diff.loc[ts, :] > voltage_threshold.loc[ts]].index
          # if curtailment cannot be fulfilled lower voltage threshold
          while sum(feedin.loc[ts, gen_pool]) < curtailment_timeseries.loc[ts]:
              voltage_threshold.loc[ts] = voltage_threshold.loc[ts] - 0.01
              gen_pool = voltage_gens_diff.loc[
                  ts, voltage_gens_diff.loc[ts, :] >
                  voltage_threshold.loc[ts]].index
          # set feed-in of generators below voltage threshold to zero, so that
          # they cannot be curtailed
          gen_pool_out = voltage_gens_diff.loc[
              ts, voltage_gens_diff.loc[ts, :] <=
              voltage_threshold.loc[ts]].index
          feedin.loc[ts, gen_pool_out] = 0
  
      # only optimize for time steps where curtailment is greater than zero
      timeindex = curtailment_timeseries[curtailment_timeseries > 0].index
      if not timeindex.empty:
          curtailment = _optimize_voltage_based_curtailment(
              feedin, voltage_gens_diff, curtailment_timeseries,
              voltage_threshold, timeindex, solver)
      else:
          curtailment = pd.DataFrame()
  
      # set curtailment for other time steps to zero
      curtailment = curtailment.append(pd.DataFrame(
          0, columns=feedin.columns, index=curtailment_timeseries[
              curtailment_timeseries <= 0].index))
  
      # check if curtailment target was met
      _check_curtailment_target(curtailment, curtailment_timeseries,
                                curtailment_key)
  
      # assign curtailment to individual generators
      _assign_curtailment(curtailment, edisgo, generators, curtailment_key)