def lv_voltage_deviation(network, mode=None, voltage_levels='mv_lv'):
    """
    Checks for voltage stability issues in LV grids.

    Parameters
    ----------
    network : :class:`~.grid.network.Network`
    mode : None or String
        If None voltage at all nodes in LV grid is checked. If mode is set to
        'stations' only voltage at busbar is checked.
    voltage_levels : :obj:`str`
        Specifies which allowed voltage deviations to use. Possible options
        are:

        * 'mv_lv'
          This is the default. The allowed voltage deviation for nodes in the
          MV grid is the same as for nodes in the LV grid. Further load and
          feed-in case are not distinguished.
        * 'lv'
          Use this to handle allowed voltage deviations in the MV and LV grid
          differently. Here, load and feed-in case are differentiated as well.

    Returns
    -------
    :obj:`dict`
        Dictionary with :class:`~.grid.grids.LVGrid` as key and a
        :pandas:`pandas.DataFrame<dataframe>` with its critical nodes, sorted
        descending by voltage deviation, as value.
        Index of the dataframe are all nodes (of type
        :class:`~.grid.components.Generator`, :class:`~.grid.components.Load`,
        etc.) with over-voltage issues. Columns are 'v_mag_pu' containing the
        maximum voltage deviation as float and 'time_index' containing the
        corresponding time step the over-voltage occured in as
        :pandas:`pandas.Timestamp<timestamp>`.

    Notes
    -----
    Over-voltage is determined based on allowed voltage deviations defined in
    the config file 'config_grid_expansion' in section
    'grid_expansion_allowed_voltage_deviations'.

    """

    crit_nodes = {}

    v_dev_allowed_per_case = {}
    if voltage_levels == 'mv_lv':
        offset = network.config[
            'grid_expansion_allowed_voltage_deviations']['hv_mv_trafo_offset']
        control_deviation = network.config[
            'grid_expansion_allowed_voltage_deviations'][
            'hv_mv_trafo_control_deviation']
        v_dev_allowed_per_case['feedin_case_upper'] = \
            1 + offset + control_deviation + network.config[
                'grid_expansion_allowed_voltage_deviations'][
                'mv_lv_feedin_case_max_v_deviation']
        v_dev_allowed_per_case['load_case_lower'] = \
            1 + offset - control_deviation - network.config[
                'grid_expansion_allowed_voltage_deviations'][
                'mv_lv_load_case_max_v_deviation']

        v_dev_allowed_per_case['feedin_case_lower'] = 0.9
        v_dev_allowed_per_case['load_case_upper'] = 1.1

        v_dev_allowed_upper = \
                network.timeseries.timesteps_load_feedin_case.case.apply(
                lambda _: v_dev_allowed_per_case['{}_upper'.format(_)])
        v_dev_allowed_lower = \
            network.timeseries.timesteps_load_feedin_case.case.apply(
                lambda _: v_dev_allowed_per_case['{}_lower'.format(_)])
    elif voltage_levels == 'lv':
        pass
    else:
        raise ValueError(
            'Specified mode {} is not a valid option.'.format(voltage_levels))

    for lv_grid in network.mv_grid.lv_grids:

        if mode:
            if mode == 'stations':
                nodes = [lv_grid.station]
            else:
                raise ValueError(
                    "{} is not a valid option for input variable 'mode' in "
                    "function lv_voltage_deviation. Try 'stations' or "
                    "None".format(mode))
        else:
            nodes = lv_grid.graph.nodes()

        if voltage_levels == 'lv':
            if mode == 'stations':
                # get voltage at primary side to calculate upper bound for
                # feed-in case and lower bound for load case
                v_lv_station_primary = network.results.v_res(
                    nodes=[lv_grid.station], level='mv').iloc[:, 0]
                timeindex = v_lv_station_primary.index
                v_dev_allowed_per_case['feedin_case_upper'] = \
                    v_lv_station_primary + network.config[
                        'grid_expansion_allowed_voltage_deviations'][
                        'mv_lv_station_feedin_case_max_v_deviation']
                v_dev_allowed_per_case['load_case_lower'] = \
                    v_lv_station_primary - network.config[
                        'grid_expansion_allowed_voltage_deviations'][
                        'mv_lv_station_load_case_max_v_deviation']
            else:
                # get voltage at secondary side to calculate upper bound for
                # feed-in case and lower bound for load case
                v_lv_station_secondary = network.results.v_res(
                    nodes=[lv_grid.station], level='lv').iloc[:, 0]
                timeindex = v_lv_station_secondary.index
                v_dev_allowed_per_case['feedin_case_upper'] = \
                    v_lv_station_secondary + network.config[
                        'grid_expansion_allowed_voltage_deviations'][
                        'lv_feedin_case_max_v_deviation']
                v_dev_allowed_per_case['load_case_lower'] = \
                    v_lv_station_secondary - network.config[
                        'grid_expansion_allowed_voltage_deviations'][
                        'lv_load_case_max_v_deviation']
            v_dev_allowed_per_case['feedin_case_lower'] = pd.Series(
                0.9, index=timeindex)
            v_dev_allowed_per_case['load_case_upper'] = pd.Series(
                1.1, index=timeindex)
            # maximum allowed voltage deviation in each time step
            v_dev_allowed_upper = []
            v_dev_allowed_lower = []
            for t in timeindex:
                case = \
                    network.timeseries.timesteps_load_feedin_case.loc[
                        t, 'case']
                v_dev_allowed_upper.append(
                    v_dev_allowed_per_case[
                        '{}_upper'.format(case)].loc[t])
                v_dev_allowed_lower.append(
                    v_dev_allowed_per_case[
                        '{}_lower'.format(case)].loc[t])
            v_dev_allowed_upper = pd.Series(v_dev_allowed_upper,
                                            index=timeindex)
            v_dev_allowed_lower = pd.Series(v_dev_allowed_lower,
                                            index=timeindex)

        crit_nodes_grid = _voltage_deviation(
            network, nodes, v_dev_allowed_upper, v_dev_allowed_lower,
            voltage_level='lv')

        if not crit_nodes_grid.empty:
            crit_nodes[lv_grid] = crit_nodes_grid.sort_values(
                by=['v_mag_pu'], ascending=False)

    if crit_nodes:
        if mode == 'stations':
            logger.debug(
                '==> {} LV station(s) has/have voltage issues.'.format(
                    len(crit_nodes)))
        else:
            logger.debug(
                '==> {} LV grid(s) has/have voltage issues.'.format(
                    len(crit_nodes)))
    else:
        if mode == 'stations':
            logger.debug('==> No voltage issues in LV stations.')
        else:
            logger.debug('==> No voltage issues in LV grids.')

    return crit_nodes
Checks for voltage stability issues in LV grids.

    Parameters
    ----------
    network : :class:`~.grid.network.Network`
    mode : None or String
        If None voltage at all nodes in LV grid is checked. If mode is set to
        'stations' only voltage at busbar is checked.
    voltage_levels : :obj:`str`
        Specifies which allowed voltage deviations to use. Possible options
        are:

        * 'mv_lv'
          This is the default. The allowed voltage deviation for nodes in the
          MV grid is the same as for nodes in the LV grid. Further load and
          feed-in case are not distinguished.
        * 'lv'
          Use this to handle allowed voltage deviations in the MV and LV grid
          differently. Here, load and feed-in case are differentiated as well.

    Returns
    -------
    :obj:`dict`
        Dictionary with :class:`~.grid.grids.LVGrid` as key and a
        :pandas:`pandas.DataFrame<dataframe>` with its critical nodes, sorted
        descending by voltage deviation, as value.
        Index of the dataframe are all nodes (of type
        :class:`~.grid.components.Generator`, :class:`~.grid.components.Load`,
        etc.) with over-voltage issues. Columns are 'v_mag_pu' containing the
        maximum voltage deviation as float and 'time_index' containing the
        corresponding time step the over-voltage occured in as
        :pandas:`pandas.Timestamp<timestamp>`.

    Notes
    -----
    Over-voltage is determined based on allowed voltage deviations defined in
    the config file 'config_grid_expansion' in section
    'grid_expansion_allowed_voltage_deviations'.