Repository: openego/eDisGo (language: python)

Issue:
[BUG] Reinforcement mode="lv" not working properly
**Describe the bug**
When edisgo.reinforce method mode="lv" is used, edisgo.analyze is called with mode="lv". But when analyze with mode="lv" is called without a lvgrid_id its not working properly. Or something else happens, the code is quite confusing.


Discussion / hints:

Fix patch:
diff --git a/doc/whatsnew/v0-3-0.rst b/doc/whatsnew/v0-3-0.rst
index d1a2dc27..e45a2e5d 100644
--- a/doc/whatsnew/v0-3-0.rst
+++ b/doc/whatsnew/v0-3-0.rst
@@ -17,3 +17,6 @@ Changes
 * Added integrity check for very short lines `#335 <https://github.com/openego/eDisGo/pull/335>`_
 * Refactoring of check_tech_constraints functions `#290 <https://github.com/openego/eDisGo/pull/290>`_
 * Add background map to plots `#346 <https://github.com/openego/eDisGo/pull/364>`_
+* Added method to scale timeseries `#353 <https://github.com/openego/eDisGo/pull/353>`_
+* Added method to iteratively reinforce a grid in case the power flow analysis does not always converge `#353 <https://github.com/openego/eDisGo/pull/353>`_
+* Added method to aggregate LV grid buses to station bus secondary side `#353 <https://github.com/openego/eDisGo/pull/353>`_
diff --git a/edisgo/edisgo.py b/edisgo/edisgo.py
index 6b8a8e47..e216c865 100755
--- a/edisgo/edisgo.py
+++ b/edisgo/edisgo.py
@@ -19,7 +19,10 @@
 from edisgo.flex_opt.heat_pump_operation import (
     operating_strategy as hp_operating_strategy,
 )
-from edisgo.flex_opt.reinforce_grid import reinforce_grid
+from edisgo.flex_opt.reinforce_grid import (
+    catch_convergence_reinforce_grid,
+    reinforce_grid,
+)
 from edisgo.io import dsm_import, generators_import, pypsa_io, timeseries_import
 from edisgo.io.ding0_import import import_ding0_grid
 from edisgo.io.electromobility_import import (
@@ -735,7 +738,7 @@ def to_pypsa(
             Check integrity of edisgo object before translating to pypsa. This option is
             meant to help the identification of possible sources of errors if the power
             flow calculations fail. See :attr:`~.edisgo.EDisGo.check_integrity` for
-            more information.
+            more information. Default: False.
 
         Other Parameters
         -------------------
@@ -748,7 +751,7 @@ def to_pypsa(
             Default: False.
         lv_grid_id : int or str
             ID (e.g. 1) or name (string representation, e.g. "LVGrid_1") of LV grid
-            to export in case mode is 'lv'.
+            to export in case mode is 'lv'. Default: None.
         aggregate_loads : str
             Mode for load aggregation in LV grids in case mode is 'mv' or 'mvlv'.
             Can be 'sectoral' aggregating the loads sector-wise, 'all' aggregating all
@@ -783,7 +786,7 @@ def to_graph(self):
 
         Returns
         -------
-        :networkx:`networkx.Graph<>`
+        :networkx:`networkx.Graph<networkx.Graph>`
             Graph representation of the grid as networkx Ordered Graph,
             where lines are represented by edges in the graph, and buses and
             transformers are represented by nodes.
@@ -860,8 +863,9 @@ def analyze(
         troubleshooting_mode: str | None = None,
         range_start: Number = 0.1,
         range_num: int = 10,
+        scale_timeseries: float | None = None,
         **kwargs,
-    ):
+    ) -> tuple[pd.DataFrame, pd.DataFrame]:
         """
         Conducts a static, non-linear power flow analysis.
 
@@ -922,7 +926,7 @@ def analyze(
             Default: True.
 
         troubleshooting_mode : str or None
-            Two optional troubleshooting methods in case of nonconvergence of nonlinear
+            Two optional troubleshooting methods in case of non-convergence of nonlinear
             power flow (cf. [1])
 
             * None (default)
@@ -934,16 +938,26 @@ def analyze(
                 Power flow analysis is conducted by reducing all power values of
                 generators and loads to a fraction, e.g. 10%, solving the load flow and
                 using it as a seed for the power at 20%, iteratively up to 100%.
+                Using parameters `range_start` and `range_num` you can define at what
+                scaling factor the iteration should start and how many iterations
+                should be conducted.
 
         range_start : float, optional
             Specifies the minimum fraction that power values are set to when using
-            troubleshooting_mode 'iteration'. Must be between 0 and 1.
+            `troubleshooting_mode` 'iteration'. Must be between 0 and 1.
             Default: 0.1.
-
         range_num : int, optional
             Specifies the number of fraction samples to generate when using
-            troubleshooting_mode 'iteration'. Must be non-negative.
+            `troubleshooting_mode` 'iteration'. Must be non-negative.
             Default: 10.
+        scale_timeseries : float or None, optional
+            If a value is given, the timeseries in the pypsa object are scaled with
+            this factor (values between 0 and 1 will scale down the time series and
+            values above 1 will scale the timeseries up). Downscaling of time series
+            can be used to check if power flow converges for smaller
+            grid loads. If None, timeseries are not scaled. In case of
+            `troubleshooting_mode` 'iteration' this parameter is ignored.
+            Default: None.
 
         Other Parameters
         -----------------
@@ -953,7 +967,8 @@ def analyze(
 
         Returns
         --------
-        :pandas:`pandas.DatetimeIndex<DatetimeIndex>`
+        tuple(:pandas:`pandas.DatetimeIndex<DatetimeIndex>`,\
+            :pandas:`pandas.DatetimeIndex<DatetimeIndex>`)
             Returns the time steps for which power flow analysis did not converge.
 
         References
@@ -987,6 +1002,20 @@ def _check_convergence():
                 )
             return timesteps_converged, timesteps_not_converged
 
+        def _scale_timeseries(pypsa_network_copy, fraction):
+            # Scales the timeseries in the pypsa object, the pypsa_network_copy is
+            # the network with the original time series
+            # Reduce power values of generators, loads and storages to given fraction
+            for obj1, obj2 in [
+                (pypsa_network.generators_t, pypsa_network_copy.generators_t),
+                (pypsa_network.loads_t, pypsa_network_copy.loads_t),
+                (pypsa_network.storage_units_t, pypsa_network_copy.storage_units_t),
+            ]:
+                for attr in ["p_set", "q_set"]:
+                    setattr(obj1, attr, getattr(obj2, attr) * fraction)
+
+            return pypsa_network
+
         if timesteps is None:
             timesteps = self.timeseries.timeindex
         # check if timesteps is array-like, otherwise convert to list
@@ -995,6 +1024,9 @@ def _check_convergence():
 
         pypsa_network = self.to_pypsa(mode=mode, timesteps=timesteps, **kwargs)
 
+        if scale_timeseries is not None and troubleshooting_mode != "iteration":
+            pypsa_network = _scale_timeseries(pypsa_network, scale_timeseries)
+
         if troubleshooting_mode == "lpf":
             # run linear power flow analysis
             pypsa_network.lpf()
@@ -1005,18 +1037,10 @@ def _check_convergence():
         elif troubleshooting_mode == "iteration":
             pypsa_network_copy = pypsa_network.copy()
             for fraction in np.linspace(range_start, 1, range_num):
-                # Reduce power values of generators, loads and storages to fraction of
-                # original value
-                for obj1, obj2 in [
-                    (pypsa_network.generators_t, pypsa_network_copy.generators_t),
-                    (pypsa_network.loads_t, pypsa_network_copy.loads_t),
-                    (pypsa_network.storage_units_t, pypsa_network_copy.storage_units_t),
-                ]:
-                    for attr in ["p_set", "q_set"]:
-                        setattr(obj1, attr, getattr(obj2, attr) * fraction)
+                pypsa_network = _scale_timeseries(pypsa_network_copy, fraction)
                 # run power flow analysis
                 pf_results = pypsa_network.pf(timesteps, use_seed=True)
-                logging.warning(
+                logger.info(
                     "Current fraction in iterative process: {}.".format(fraction)
                 )
                 # get converged and not converged time steps
@@ -1032,7 +1056,7 @@ def _check_convergence():
         # handle converged time steps
         pypsa_io.process_pfa_results(self, pypsa_network, timesteps_converged)
 
-        return timesteps_not_converged
+        return timesteps_converged, timesteps_not_converged
 
     def reinforce(
         self,
@@ -1043,97 +1067,206 @@ def reinforce(
         mode: str | None = None,
         without_generator_import: bool = False,
         n_minus_one: bool = False,
+        catch_convergence_problems: bool = False,
         **kwargs,
     ) -> Results:
         """
         Reinforces the network and calculates network expansion costs.
 
         If the :attr:`edisgo.network.timeseries.TimeSeries.is_worst_case` is
-        True input for `timesteps_pfa` and `mode` are overwritten and therefore
-        ignored.
+        True input for `timesteps_pfa` is overwritten and therefore ignored.
+
+        See :ref:`features-in-detail` for more information on how network
+        reinforcement is conducted.
+
+        Parameters
+        -----------
+        timesteps_pfa : 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
+            :attr:`~.network.timeseries.TimeSeries.timeindex` are used.
+            Possible options are:
 
-        See :func:`edisgo.flex_opt.reinforce_grid.reinforce_grid` for more
-        information on input parameters and methodology.
+            * None
+              Time steps in :attr:`~.network.timeseries.TimeSeries.timeindex` 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_grid : bool
+            If True, reinforcement is conducted on a copied grid and discarded.
+            Default: False.
+        max_while_iterations : int
+            Maximum number of times each while loop is conducted. Default: 20.
+        split_voltage_band : bool
+            If True the allowed voltage band of +/-10 percent is allocated to the
+            different voltage levels MV, MV/LV and LV according to config values set
+            in section `grid_expansion_allowed_voltage_deviations`. If False, the same
+            voltage limits are used for all voltage levels. Be aware that this does
+            currently not work correctly. Default: True.
+        mode : str
+            Determines network levels reinforcement is conducted for. Specify
+
+            * None to reinforce MV and LV network levels. None is the default.
+            * 'mv' to reinforce MV level only, neglecting MV/LV stations,
+              and LV network topology. LV load and generation is aggregated per
+              LV network and directly connected to the primary side of the
+              respective MV/LV station.
+            * 'mvlv' to reinforce MV network level only, including MV/LV stations,
+              and neglecting LV network topology. LV load and generation is
+              aggregated per LV network and directly connected to the secondary
+              side of the respective MV/LV station.
+            * 'lv' to reinforce LV networks. In case an LV grid is specified through
+              parameter `lv_grid_id`, the grid's MV/LV station is not included. In case
+              no LV grid ID is given, all MV/LV stations are included.
+        without_generator_import : bool
+            If True, excludes lines that were added in the generator import to connect
+            new generators from calculation of network expansion costs. Default: False.
+        n_minus_one : bool
+            Determines whether n-1 security should be checked. Currently, n-1 security
+            cannot be handled correctly, wherefore the case where this parameter is set
+            to True will lead to an error being raised. Default: False.
+        catch_convergence_problems : bool
+            Uses reinforcement strategy to reinforce not converging grid.
+            Reinforces first with only converging timesteps. Reinforce again with at
+            start not converging timesteps. If still not converging, scale timeseries.
+            Default: False
 
         Other Parameters
         -----------------
         is_worst_case : bool
             Is used to overwrite the return value from
-            :attr:`edisgo.network.timeseries.TimeSeries.is_worst_case`. If True
+            :attr:`edisgo.network.timeseries.TimeSeries.is_worst_case`. If True,
             reinforcement is calculated for worst-case MV and LV cases separately.
+        lv_grid_id : str or int or None
+            LV grid id to specify the grid to check, if mode is "lv". If no grid is
+            specified, all LV grids are checked. In that case, the power flow analysis
+            is conducted including the MV grid, in order to check loading and voltage
+            drop/rise of MV/LV stations.
+        skip_mv_reinforcement : bool
+            If True, MV is not reinforced, even if `mode` is "mv", "mvlv" or None.
+            This is used in case worst-case grid reinforcement is conducted in order to
+            reinforce MV/LV stations for LV worst-cases.
+            Default: False.
+
+        Returns
+        --------
+        :class:`~.network.results.Results`
+            Returns the Results object holding network expansion costs, equipment
+            changes, etc.
 
         """
-        if kwargs.get("is_worst_case", self.timeseries.is_worst_case):
+        if copy_grid:
+            edisgo_obj = copy.deepcopy(self)
+        else:
+            edisgo_obj = self
 
+        # Build reinforce run settings
+        if kwargs.get("is_worst_case", self.timeseries.is_worst_case):
             logger.debug(
                 "Running reinforcement in worst-case mode by differentiating between "
                 "MV and LV load and feed-in cases."
             )
-
-            if copy_grid:
-                edisgo_obj = copy.deepcopy(self)
-            else:
-                edisgo_obj = self
-
             timeindex_worst_cases = self.timeseries.timeindex_worst_cases
-
-            if mode != "lv":
-
-                timesteps_pfa = pd.DatetimeIndex(
-                    timeindex_worst_cases.loc[
-                        timeindex_worst_cases.index.str.contains("mv")
-                    ]
-                )
-                reinforce_grid(
-                    edisgo_obj,
-                    max_while_iterations=max_while_iterations,
-                    copy_grid=False,
-                    timesteps_pfa=timesteps_pfa,
-                    split_voltage_band=split_voltage_band,
-                    mode="mv",
-                    without_generator_import=without_generator_import,
-                    n_minus_one=n_minus_one,
-                )
-
-            if mode != "mv":
-                timesteps_pfa = pd.DatetimeIndex(
-                    timeindex_worst_cases.loc[
-                        timeindex_worst_cases.index.str.contains("lv")
-                    ]
+            timesteps_mv = pd.DatetimeIndex(
+                timeindex_worst_cases.loc[
+                    timeindex_worst_cases.index.str.contains("mv")
+                ]
+            )
+            timesteps_lv = pd.DatetimeIndex(
+                timeindex_worst_cases.loc[
+                    timeindex_worst_cases.index.str.contains("lv")
+                ]
+            )
+            # Run the analyze-method at the end, to get a power flow for all
+            # timesteps for reinforced components
+            run_analyze_at_the_end = True
+            if mode is None:
+                kwargs_mv = kwargs.copy()
+                kwargs_mv.update({"mode": "mv", "timesteps_pfa": timesteps_mv})
+                kwargs_mvlv = kwargs.copy()
+                kwargs_mvlv.update(
+                    {
+                        "mode": "mvlv",
+                        "timesteps_pfa": timesteps_lv,
+                        "skip_mv_reinforcement": True,
+                    }
                 )
-                reinforce_mode = mode if mode == "mvlv" else "lv"
-                reinforce_grid(
-                    edisgo_obj,
-                    max_while_iterations=max_while_iterations,
-                    copy_grid=False,
-                    timesteps_pfa=timesteps_pfa,
-                    split_voltage_band=split_voltage_band,
-                    mode=reinforce_mode,
-                    without_generator_import=without_generator_import,
-                    n_minus_one=n_minus_one,
+                kwargs_lv = kwargs.copy()
+                kwargs_lv.update({"mode": "lv", "timesteps_pfa": timesteps_lv})
+                kwargs.update({"mode": "mv", "timesteps_pfa": timesteps_mv})
+                setting_list = [
+                    kwargs_mv,
+                    kwargs_mvlv,
+                    kwargs_lv,
+                ]
+            elif mode == "mv":
+                kwargs.update({"mode": "mv", "timesteps_pfa": timesteps_mv})
+                setting_list = [kwargs]
+            elif mode == "mvlv":
+                kwargs.update(
+                    {
+                        "mode": "mvlv",
+                        "timesteps_pfa": timesteps_lv,
+                        "skip_mv_reinforcement": True,
+                    }
                 )
-
-            if mode not in ["mv", "lv"]:
-                edisgo_obj.analyze(mode=mode)
-            results = edisgo_obj.results
-
+                setting_list = [kwargs]
+            elif mode == "lv":
+                kwargs.update({"mode": "lv", "timesteps_pfa": timesteps_lv})
+                setting_list = [kwargs]
+            else:
+                raise ValueError(f"Mode {mode} does not exist.")
         else:
-            results = reinforce_grid(
-                self,
+            kwargs.update({"mode": mode, "timesteps_pfa": timesteps_pfa})
+            setting_list = [kwargs]
+            run_analyze_at_the_end = False
+
+        logger.info(f"Run the following reinforcements: {setting_list=}")
+        for setting in setting_list:
+            logger.info(f"Run the following reinforcement: {setting=}")
+            if not catch_convergence_problems:
+                func = reinforce_grid
+            else:
+                func = catch_convergence_reinforce_grid
+            func(
+                edisgo_obj,
                 max_while_iterations=max_while_iterations,
-                copy_grid=copy_grid,
-                timesteps_pfa=timesteps_pfa,
                 split_voltage_band=split_voltage_band,
-                mode=mode,
                 without_generator_import=without_generator_import,
                 n_minus_one=n_minus_one,
+                **setting,
+            )
+        if run_analyze_at_the_end:
+            lv_grid_id = kwargs.get("lv_grid_id", None)
+            if mode == "lv" and lv_grid_id:
+                analyze_mode = "lv"
+            elif mode == "lv":
+                analyze_mode = None
+            else:
+                analyze_mode = mode
+            edisgo_obj.analyze(
+                mode=analyze_mode, lv_grid_id=lv_grid_id, timesteps=timesteps_pfa
             )
 
         # add measure to Results object
         if not copy_grid:
             self.results.measures = "grid_expansion"
 
-        return results
+        return edisgo_obj.results
 
     def perform_mp_opf(self, timesteps, storage_series=None, **kwargs):
         """
@@ -1262,7 +1395,7 @@ def _set_timeseries():
                     )
                 elif ts_reactive_power == "default":
                     if ts_active_power is None:
-                        logging.warning(
+                        logger.warning(
                             f"Default reactive power time series of {comp_name} cannot "
                             "be set as active power time series was not provided."
                         )
@@ -2123,13 +2256,13 @@ def plot_mv_voltages(self, **kwargs):
         """
         try:
             if self.results.v_res is None:
-                logging.warning(
+                logger.warning(
                     "Voltages from power flow "
                     "analysis must be available to plot them."
                 )
                 return
         except AttributeError:
-            logging.warning(
+            logger.warning(
                 "Results must be available to plot voltages. "
                 "Please analyze grid first."
             )
@@ -2160,13 +2293,13 @@ def plot_mv_line_loading(self, **kwargs):
         """
         try:
             if self.results.i_res is None:
-                logging.warning(
+                logger.warning(
                     "Currents `i_res` from power flow analysis "
                     "must be available to plot line loading."
                 )
                 return
         except AttributeError:
-            logging.warning(
+            logger.warning(
                 "Results must be available to plot line loading. "
                 "Please analyze grid first."
             )
@@ -2201,13 +2334,13 @@ def plot_mv_grid_expansion_costs(self, **kwargs):
         """
         try:
             if self.results.grid_expansion_costs is None:
-                logging.warning(
+                logger.warning(
                     "Grid expansion cost results needed to plot "
                     "them. Please do grid reinforcement."
                 )
                 return
         except AttributeError:
-            logging.warning(
+            logger.warning(
                 "Results of MV topology needed to  plot topology "
                 "expansion costs. Please reinforce first."
             )
@@ -2866,7 +2999,7 @@ def _check_timeindex(check_df, param_name):
                     f"DSM.{param_name}",
                 )
 
-        logging.info("Integrity check finished. Please pay attention to warnings.")
+        logger.info("Integrity check finished. Please pay attention to warnings.")
 
     def resample_timeseries(
         self, method: str = "ffill", freq: str | pd.Timedelta = "15min"
@@ -3086,7 +3219,7 @@ def import_edisgo_from_files(
 
     if not from_zip_archive and str(edisgo_path).endswith(".zip"):
         from_zip_archive = True
-        logging.info("Given path is a zip archive. Setting 'from_zip_archive' to True.")
+        logger.info("Given path is a zip archive. Setting 'from_zip_archive' to True.")
 
     edisgo_obj = EDisGo()
     try:
@@ -3096,7 +3229,7 @@ def import_edisgo_from_files(
             "from_zip_archive": from_zip_archive,
         }
     except FileNotFoundError:
-        logging.info(
+        logger.info(
             "Configuration data could not be loaded from json wherefore "
             "the default configuration data is loaded."
         )
@@ -3115,7 +3248,7 @@ def import_edisgo_from_files(
         if os.path.exists(directory):
             edisgo_obj.topology.from_csv(directory, edisgo_obj, from_zip_archive)
         else:
-            logging.warning("No topology data found. Topology not imported.")
+            logger.warning("No topology data found. Topology not imported.")
 
     if import_timeseries:
         dtype = kwargs.get("dtype", None)
@@ -3130,7 +3263,7 @@ def import_edisgo_from_files(
                 directory, dtype=dtype, from_zip_archive=from_zip_archive
             )
         else:
-            logging.warning("No time series data found. Timeseries not imported.")
+            logger.warning("No time series data found. Timeseries not imported.")
 
     if import_results:
         parameters = kwargs.get("parameters", None)
@@ -3146,7 +3279,7 @@ def import_edisgo_from_files(
                 directory, parameters, dtype=dtype, from_zip_archive=from_zip_archive
             )
         else:
-            logging.warning("No results data found. Results not imported.")
+            logger.warning("No results data found. Results not imported.")
 
     if import_electromobility:
         if not from_zip_archive:
@@ -3160,7 +3293,7 @@ def import_edisgo_from_files(
                 directory, edisgo_obj, from_zip_archive=from_zip_archive
             )
         else:
-            logging.warning(
+            logger.warning(
                 "No electromobility data found. Electromobility not imported."
             )
 
@@ -3174,7 +3307,7 @@ def import_edisgo_from_files(
         if os.path.exists(directory):
             edisgo_obj.heat_pump.from_csv(directory, from_zip_archive=from_zip_archive)
         else:
-            logging.warning("No heat pump data found. Heat pump data not imported.")
+            logger.warning("No heat pump data found. Heat pump data not imported.")
 
     if import_dsm:
         if not from_zip_archive:
@@ -3186,7 +3319,7 @@ def import_edisgo_from_files(
         if os.path.exists(directory):
             edisgo_obj.dsm.from_csv(directory, from_zip_archive=from_zip_archive)
         else:
-            logging.warning("No DSM data found. DSM data not imported.")
+            logger.warning("No DSM data found. DSM data not imported.")
 
     if import_overlying_grid:
         if not from_zip_archive:
@@ -3200,7 +3333,7 @@ def import_edisgo_from_files(
                 directory, from_zip_archive=from_zip_archive
             )
         else:
-            logging.warning(
+            logger.warning(
                 "No overlying grid data found. Overlying grid data not imported."
             )
 
diff --git a/edisgo/flex_opt/charging_strategies.py b/edisgo/flex_opt/charging_strategies.py
index 275e9a03..393b26a0 100644
--- a/edisgo/flex_opt/charging_strategies.py
+++ b/edisgo/flex_opt/charging_strategies.py
@@ -324,7 +324,7 @@ def charging_strategy(
     )
     # fmt: on
 
-    logging.info(f"Charging strategy {strategy} completed.")
+    logger.info(f"Charging strategy {strategy} completed.")
 
 
 def harmonize_charging_processes_df(
diff --git a/edisgo/flex_opt/check_tech_constraints.py b/edisgo/flex_opt/check_tech_constraints.py
index b34ca2ff..6fe9f752 100644
--- a/edisgo/flex_opt/check_tech_constraints.py
+++ b/edisgo/flex_opt/check_tech_constraints.py
@@ -58,7 +58,7 @@ def mv_line_max_relative_overload(edisgo_obj, n_minus_one=False):
     return crit_lines
 
 
-def lv_line_max_relative_overload(edisgo_obj, n_minus_one=False):
+def lv_line_max_relative_overload(edisgo_obj, n_minus_one=False, lv_grid_id=None):
     """
     Returns time step and value of most severe overloading of lines in LV networks.
 
@@ -70,6 +70,9 @@ def lv_line_max_relative_overload(edisgo_obj, n_minus_one=False):
         for more information). Currently, n-1 security cannot be handled correctly,
         wherefore the case where this parameter is set to True will lead to an error
         being raised.
+    lv_grid_id : str or int or None
+        If None, checks overloading for all LV lines. Otherwise, only lines in given
+        LV grid are checked. Default: None.
 
     Returns
     -------
@@ -93,7 +96,7 @@ def lv_line_max_relative_overload(edisgo_obj, n_minus_one=False):
     """
 
     crit_lines = _line_max_relative_overload(
-        edisgo_obj, voltage_level="lv", n_minus_one=n_minus_one
+        edisgo_obj, voltage_level="lv", n_minus_one=n_minus_one, lv_grid_id=lv_grid_id
     )
 
     if not crit_lines.empty:
@@ -108,7 +111,9 @@ def lv_line_max_relative_overload(edisgo_obj, n_minus_one=False):
     return crit_lines
 
 
-def _line_max_relative_overload(edisgo_obj, voltage_level, n_minus_one=False):
+def _line_max_relative_overload(
+    edisgo_obj, voltage_level, n_minus_one=False, lv_grid_id=None
+):
     """
     Returns time step and value of most severe overloading of lines.
 
@@ -116,11 +121,14 @@ def _line_max_relative_overload(edisgo_obj, voltage_level, n_minus_one=False):
     ----------
     edisgo_obj : :class:`~.EDisGo`
     voltage_level : str
-        Voltage level, over-loading is checked for. Possible options are
-        'mv' or 'lv'.
+        Voltage level, over-loading is checked for. Possible options are 'mv' or 'lv'.
     n_minus_one : bool
         Determines which allowed load factors to use. See :py:attr:`~lines_allowed_load`
         for more information.
+    lv_grid_id : str or int or None
+        This parameter is only used in case `voltage_level` is "lv".
+        If None, checks overloading for all LV lines. Otherwise, only lines in given
+        LV grid are checked. Default: None.
 
     Returns
     -------
@@ -145,9 +153,13 @@ def _line_max_relative_overload(edisgo_obj, voltage_level, n_minus_one=False):
     # get lines in voltage level
     mv_grid = edisgo_obj.topology.mv_grid
     if voltage_level == "lv":
-        lines = edisgo_obj.topology.lines_df[
-            ~edisgo_obj.topology.lines_df.index.isin(mv_grid.lines_df.index)
-        ].index
+        if lv_grid_id is None:
+            lines = edisgo_obj.topology.lines_df[
+                ~edisgo_obj.topology.lines_df.index.isin(mv_grid.lines_df.index)
+            ].index
+        else:
+            lv_grid = edisgo_obj.topology.get_lv_grid(lv_grid_id)
+            lines = lv_grid.lines_df.index
     elif voltage_level == "mv":
         lines = mv_grid.lines_df.index
     else:
@@ -401,13 +413,16 @@ def hv_mv_station_max_overload(edisgo_obj):
     return crit_stations
 
 
-def mv_lv_station_max_overload(edisgo_obj):
+def mv_lv_station_max_overload(edisgo_obj, lv_grid_id=None):
     """
     Checks for over-loading of MV/LV stations.
 
     Parameters
     ----------
     edisgo_obj : :class:`~.EDisGo`
+    lv_grid_id : str or int or None
+        If None, checks overloading for all MV/LV stations. Otherwise, only station
+        in given LV grid is checked. Default: None.
 
     Returns
     -------
@@ -427,9 +442,13 @@ def mv_lv_station_max_overload(edisgo_obj):
     section 'grid_expansion_load_factors'.
 
     """
-
     crit_stations = pd.DataFrame(dtype=float)
-    for lv_grid in edisgo_obj.topology.lv_grids:
+
+    if lv_grid_id is not None:
+        lv_grids = [edisgo_obj.topology.get_lv_grid(lv_grid_id)]
+    else:
+        lv_grids = list(edisgo_obj.topology.lv_grids)
+    for lv_grid in lv_grids:
         crit_stations = pd.concat(
             [
                 crit_stations,
@@ -729,7 +748,7 @@ def components_relative_load(edisgo_obj, n_minus_one=False):
     return pd.concat([lines_rel_load, stations_rel_load], axis=1)
 
 
-def voltage_issues(edisgo_obj, voltage_level, split_voltage_band=True):
+def voltage_issues(edisgo_obj, voltage_level, split_voltage_band=True, lv_grid_id=None):
     """
     Gives buses with voltage issues and their maximum voltage deviation in p.u..
 
@@ -746,6 +765,10 @@ def voltage_issues(edisgo_obj, voltage_level, split_voltage_band=True):
         voltage levels MV, MV/LV and LV according to config values set in section
         `grid_expansion_allowed_voltage_deviations`. If False, the same voltage limits
         are used for all voltage levels. Default: True.
+    lv_grid_id : str or int or None
+        This parameter is only used in case `voltage_level` is "mv_lv" or "lv".
+        If None, checks voltage issues for all LV buses. Otherwise, only buses
+        in given LV grid are checked. Default: None.
 
     Returns
     -------
@@ -768,18 +791,30 @@ def voltage_issues(edisgo_obj, voltage_level, split_voltage_band=True):
     'grid_expansion_allowed_voltage_deviations'.
 
     """
-    station_buses = edisgo_obj.topology.transformers_df.bus1.unique()
+
     if voltage_level:
         if voltage_level == "mv_lv":
-            buses = station_buses
+            if lv_grid_id is None:
+                buses = edisgo_obj.topology.transformers_df.bus1.unique()
+            else:
+                lv_grid = edisgo_obj.topology.get_lv_grid(lv_grid_id)
+                buses = lv_grid.transformers_df.bus1.unique()
         elif voltage_level == "lv":
-            buses = edisgo_obj.topology.buses_df.index
-            # drop MV buses and buses of stations secondary sides
-            buses = buses.drop(
-                edisgo_obj.topology.mv_grid.buses_df.index.append(
-                    pd.Index(station_buses)
+            if lv_grid_id is None:
+                buses = edisgo_obj.topology.buses_df.index
+                # drop MV buses and buses of stations secondary sides
+                station_buses = edisgo_obj.topology.transformers_df.bus1.unique()
+                buses = buses.drop(
+                    edisgo_obj.topology.mv_grid.buses_df.index.append(
+                        pd.Index(station_buses)
+                    )
                 )
-            )
+            else:
+                lv_grid = edisgo_obj.topology.get_lv_grid(lv_grid_id)
+                buses = lv_grid.buses_df.index
+                # drop buses of station's secondary side
+                station_buses = lv_grid.transformers_df.bus1.unique()
+                buses = buses.drop(station_buses)
         elif voltage_level == "mv":
             buses = edisgo_obj.topology.mv_grid.buses_df.index
         else:
@@ -886,6 +921,7 @@ def _voltage_issues_helper(edisgo_obj, buses, split_voltage_band):
 
 def allowed_voltage_limits(edisgo_obj, buses=None, split_voltage_band=True):
     """
+    Calculates allowed upper and lower voltage limits.
 
     Parameters
     ----------
diff --git a/edisgo/flex_opt/heat_pump_operation.py b/edisgo/flex_opt/heat_pump_operation.py
index 140e5719..88cb413e 100644
--- a/edisgo/flex_opt/heat_pump_operation.py
+++ b/edisgo/flex_opt/heat_pump_operation.py
@@ -56,4 +56,4 @@ def operating_strategy(
         ),
     )
 
-    logging.debug(f"Heat pump operating strategy {strategy} completed.")
+    logger.debug(f"Heat pump operating strategy {strategy} completed.")
diff --git a/edisgo/flex_opt/reinforce_grid.py b/edisgo/flex_opt/reinforce_grid.py
index ee7aed75..45826225 100644
--- a/edisgo/flex_opt/reinforce_grid.py
+++ b/edisgo/flex_opt/reinforce_grid.py
@@ -23,12 +23,12 @@
 def reinforce_grid(
     edisgo: EDisGo,
     timesteps_pfa: str | pd.DatetimeIndex | pd.Timestamp | None = None,
-    copy_grid: bool = False,
     max_while_iterations: int = 20,
     split_voltage_band: bool = True,
     mode: str | None = None,
     without_generator_import: bool = False,
     n_minus_one: bool = False,
+    **kwargs,
 ) -> Results:
     """
     Evaluates network reinforcement needs and performs measures.
@@ -38,39 +38,15 @@ def reinforce_grid(
     Parameters
     ----------
     edisgo : :class:`~.EDisGo`
-        The eDisGo API object
+        The eDisGo object grid reinforcement is conducted on.
     timesteps_pfa : 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:`~.network.timeseries.TimeSeries`) are
-        used.
-        Possible options are:
-
-        * None
-          Time steps in timeseries.timeindex (see
-          :class:`~.network.timeseries.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_grid : bool
-        If True reinforcement is conducted on a copied grid and discarded.
-        Default: False.
+        conducted. See parameter `timesteps_pfa` in function :attr:`~.EDisGo.reinforce`
+        for more information.
     max_while_iterations : int
-        Maximum number of times each while loop is conducted.
+        Maximum number of times each while loop is conducted. Default: 20.
     split_voltage_band : bool
         If True the allowed voltage band of +/-10 percent is allocated to the different
         voltage levels MV, MV/LV and LV according to config values set in section
@@ -79,26 +55,32 @@ def reinforce_grid(
         correctly.
         Default: True.
     mode : str
-        Determines network levels reinforcement is conducted for. Specify
-
-        * None to reinforce MV and LV network levels. None is the default.
-        * 'mv' to reinforce MV network level only, neglecting MV/LV stations,
-          and LV network topology. LV load and generation is aggregated per
-          LV network and directly connected to the primary side of the
-          respective MV/LV station.
-        * 'mvlv' to reinforce MV network level only, including MV/LV stations,
-          and neglecting LV network topology. LV load and generation is
-          aggregated per LV network and directly connected to the secondary
-          side of the respective MV/LV station.
-        * 'lv' to reinforce LV networks including MV/LV stations.
+        Determines network levels reinforcement is conducted for. See parameter
+        `mode` in function :attr:`~.EDisGo.reinforce` for more information.
     without_generator_import : bool
-        If True excludes lines that were added in the generator import to
-        connect new generators to the topology from calculation of topology expansion
-        costs. Default: False.
+        If True, excludes lines that were added in the generator import to connect
+        new generators from calculation of network expansion costs. Default: False.
     n_minus_one : bool
         Determines whether n-1 security should be checked. Currently, n-1 security
         cannot be handled correctly, wherefore the case where this parameter is set to
-        True will lead to an error being raised.
+        True will lead to an error being raised. Default: False.
+
+    Other Parameters
+    -----------------
+    lv_grid_id : str or int or None
+        LV grid id to specify the grid to check, if mode is "lv". See parameter
+        `lv_grid_id` in function :attr:`~.EDisGo.reinforce` for more information.
+    scale_timeseries : float or None
+        If a value is given, the timeseries used in the power flow analysis are scaled
+        with this factor (values between 0 and 1 will scale down the time series and
+        values above 1 will scale the timeseries up). Downscaling of time series
+        can be used to gradually reinforce the grid. If None, timeseries are not scaled.
+        Default: None.
+    skip_mv_reinforcement : bool
+        If True, MV is not reinforced, even if `mode` is "mv", "mvlv" or None.
+        This is used in case worst-case grid reinforcement is conducted in order to
+        reinforce MV/LV stations for LV worst-cases.
+        Default: False.
 
     Returns
     -------
@@ -114,14 +96,14 @@ def reinforce_grid(
     """
 
     def _add_lines_changes_to_equipment_changes():
-        edisgo_reinforce.results.equipment_changes = pd.concat(
+        edisgo.results.equipment_changes = pd.concat(
             [
-                edisgo_reinforce.results.equipment_changes,
+                edisgo.results.equipment_changes,
                 pd.DataFrame(
                     {
                         "iteration_step": [iteration_step] * len(lines_changes),
                         "change": ["changed"] * len(lines_changes),
-                        "equipment": edisgo_reinforce.topology.lines_df.loc[
+                        "equipment": edisgo.topology.lines_df.loc[
                             lines_changes.keys(), "type_info"
                         ].values,
                         "quantity": [_ for _ in lines_changes.values()],
@@ -132,7 +114,7 @@ def _add_lines_changes_to_equipment_changes():
         )
 
     def _add_transformer_changes_to_equipment_changes(mode: str | None):
-        df_list = [edisgo_reinforce.results.equipment_changes]
+        df_list = [edisgo.results.equipment_changes]
         df_list.extend(
             pd.DataFrame(
                 {
@@ -146,7 +128,7 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
             for station, transformer_list in transformer_changes[mode].items()
         )
 
-        edisgo_reinforce.results.equipment_changes = pd.concat(df_list)
+        edisgo.results.equipment_changes = pd.concat(df_list)
 
     if n_minus_one is True:
         raise NotImplementedError("n-1 security can currently not be checked.")
@@ -157,22 +139,15 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
     # give warning in case split_voltage_band is set to False
     if split_voltage_band is False:
         logger.warning(
-            "You called the 'reinforce_grid' grid function with option "
+            "You called the 'reinforce_grid' function with option "
             "'split_voltage_band' = False. Be aware that this does "
             "currently not work correctly and might lead to infeasible "
             "grid reinforcement."
         )
 
-    # in case reinforcement needs to be conducted on a copied graph the
-    # edisgo object is deep copied
-    if copy_grid is True:
-        edisgo_reinforce = copy.deepcopy(edisgo)
-    else:
-        edisgo_reinforce = edisgo
-
     if timesteps_pfa is not None:
         if isinstance(timesteps_pfa, str) and timesteps_pfa == "snapshot_analysis":
-            snapshots = tools.select_worstcase_snapshots(edisgo_reinforce)
+            snapshots = tools.select_worstcase_snapshots(edisgo)
             # drop None values in case any of the two snapshots does not exist
             timesteps_pfa = pd.DatetimeIndex(
                 data=[
@@ -194,37 +169,45 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
                 )
 
     iteration_step = 1
-    analyze_mode = None if mode == "lv" else mode
+    lv_grid_id = kwargs.get("lv_grid_id", None)
+    scale_timeseries = kwargs.get("scale_timeseries", None)
+    if mode == "lv" and lv_grid_id:
+        analyze_mode = "lv"
+    elif mode == "lv":
+        analyze_mode = None
+    else:
+        analyze_mode = mode
 
-    edisgo_reinforce.analyze(mode=analyze_mode, timesteps=timesteps_pfa)
+    edisgo.analyze(
+        mode=analyze_mode,
+        timesteps=timesteps_pfa,
+        lv_grid_id=lv_grid_id,
+        scale_timeseries=scale_timeseries,
+    )
 
     # REINFORCE OVERLOADED TRANSFORMERS AND LINES
     logger.debug("==> Check station load.")
-
     overloaded_mv_station = (
         pd.DataFrame(dtype=float)
-        if mode == "lv"
-        else checks.hv_mv_station_max_overload(edisgo_reinforce)
+        if mode == "lv" or kwargs.get("skip_mv_reinforcement", False)
+        else checks.hv_mv_station_max_overload(edisgo)
     )
+    if lv_grid_id or (mode == "mv"):
+        overloaded_lv_stations = pd.DataFrame(dtype=float)
+    else:
+        overloaded_lv_stations = checks.mv_lv_station_max_overload(edisgo)
 
-    overloaded_lv_stations = (
-        pd.DataFrame(dtype=float)
-        if mode == "mv"
-        else checks.mv_lv_station_max_overload(edisgo_reinforce)
-    )
     logger.debug("==> Check line load.")
-
     crit_lines = (
         pd.DataFrame(dtype=float)
-        if mode == "lv"
-        else checks.mv_line_max_relative_overload(edisgo_reinforce)
+        if mode == "lv" or kwargs.get("skip_mv_reinforcement", False)
+        else checks.mv_line_max_relative_overload(edisgo)
     )
-
     if not mode or mode == "lv":
         crit_lines = pd.concat(
             [
                 crit_lines,
-                checks.lv_line_max_relative_overload(edisgo_reinforce),
+                checks.lv_line_max_relative_overload(edisgo, lv_grid_id=lv_grid_id),
             ]
         )
 
@@ -239,7 +222,7 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
             # reinforce substations
             transformer_changes = (
                 reinforce_measures.reinforce_hv_mv_station_overloading(
-                    edisgo_reinforce, overloaded_mv_station
+                    edisgo, overloaded_mv_station
                 )
             )
             # write added and removed transformers to results.equipment_changes
@@ -250,7 +233,7 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
             # reinforce distribution substations
             transformer_changes = (
                 reinforce_measures.reinforce_mv_lv_station_overloading(
-                    edisgo_reinforce, overloaded_lv_stations
+                    edisgo, overloaded_lv_stations
                 )
             )
             # write added and removed transformers to results.equipment_changes
@@ -260,7 +243,7 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
         if not crit_lines.empty:
             # reinforce lines
             lines_changes = reinforce_measures.reinforce_lines_overloading(
-                edisgo_reinforce, crit_lines
+                edisgo, crit_lines
             )
             # write changed lines to results.equipment_changes
             _add_lines_changes_to_equipment_changes()
@@ -268,31 +251,33 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
         # run power flow analysis again (after updating pypsa object) and check
         # if all over-loading problems were solved
         logger.debug("==> Run power flow analysis.")
-        edisgo_reinforce.analyze(mode=analyze_mode, timesteps=timesteps_pfa)
+        edisgo.analyze(
+            mode=analyze_mode,
+            timesteps=timesteps_pfa,
+            lv_grid_id=lv_grid_id,
+            scale_timeseries=scale_timeseries,
+        )
 
         logger.debug("==> Recheck station load.")
         overloaded_mv_station = (
             pd.DataFrame(dtype=float)
-            if mode == "lv"
-            else checks.hv_mv_station_max_overload(edisgo_reinforce)
+            if mode == "lv" or kwargs.get("skip_mv_reinforcement", False)
+            else checks.hv_mv_station_max_overload(edisgo)
         )
-
-        if mode != "mv":
-            overloaded_lv_stations = checks.mv_lv_station_max_overload(edisgo_reinforce)
+        if mode != "mv" and (not lv_grid_id):
+            overloaded_lv_stations = checks.mv_lv_station_max_overload(edisgo)
 
         logger.debug("==> Recheck line load.")
-
         crit_lines = (
             pd.DataFrame(dtype=float)
-            if mode == "lv"
-            else checks.mv_line_max_relative_overload(edisgo_reinforce)
+            if mode == "lv" or kwargs.get("skip_mv_reinforcement", False)
+            else checks.mv_line_max_relative_overload(edisgo)
         )
-
         if not mode or mode == "lv":
             crit_lines = pd.concat(
                 [
                     crit_lines,
-                    checks.lv_line_max_relative_overload(edisgo_reinforce),
+                    checks.lv_line_max_relative_overload(edisgo, lv_grid_id=lv_grid_id),
                 ]
             )
 
@@ -306,9 +291,9 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
         or not overloaded_mv_station.empty
         or not overloaded_lv_stations.empty
     ):
-        edisgo_reinforce.results.unresolved_issues = pd.concat(
+        edisgo.results.unresolved_issues = pd.concat(
             [
-                edisgo_reinforce.results.unresolved_issues,
+                edisgo.results.unresolved_issues,
                 crit_lines,
                 overloaded_lv_stations,
                 overloaded_mv_station,
@@ -331,9 +316,9 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
 
     crit_nodes = (
         pd.DataFrame()
-        if mode == "lv"
+        if mode == "lv" or kwargs.get("skip_mv_reinforcement", False)
         else checks.voltage_issues(
-            edisgo_reinforce, voltage_level="mv", split_voltage_band=split_voltage_band
+            edisgo, voltage_level="mv", split_voltage_band=split_voltage_band
         )
     )
 
@@ -342,8 +327,8 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
 
         # reinforce lines
         lines_changes = reinforce_measures.reinforce_lines_voltage_issues(
-            edisgo_reinforce,
-            edisgo_reinforce.topology.mv_grid,
+            edisgo,
+            edisgo.topology.mv_grid,
             crit_nodes,
         )
         # write changed lines to results.equipment_changes
@@ -352,11 +337,16 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
         # run power flow analysis again (after updating pypsa object) and check
         # if all over-voltage problems were solved
         logger.debug("==> Run power flow analysis.")
-        edisgo_reinforce.analyze(mode=analyze_mode, timesteps=timesteps_pfa)
+        edisgo.analyze(
+            mode=analyze_mode,
+            timesteps=timesteps_pfa,
+            lv_grid_id=lv_grid_id,
+            scale_timeseries=scale_timeseries,
+        )
 
         logger.debug("==> Recheck voltage in MV topology.")
         crit_nodes = checks.voltage_issues(
-            edisgo_reinforce, voltage_level="mv", split_voltage_band=split_voltage_band
+            edisgo, voltage_level="mv", split_voltage_band=split_voltage_band
         )
 
         iteration_step += 1
@@ -365,9 +355,9 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
     # check if all voltage problems were solved after maximum number of
     # iterations allowed
     if while_counter == max_while_iterations and crit_nodes.empty:
-        edisgo_reinforce.results.unresolved_issues = pd.concat(
+        edisgo.results.unresolved_issues = pd.concat(
             [
-                edisgo_reinforce.results.unresolved_issues,
+                edisgo.results.unresolved_issues,
                 pd.concat([_ for _ in crit_nodes.values()]),
             ]
         )
@@ -385,18 +375,21 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
     if mode != "mv":
         logger.debug("==> Check voltage at secondary side of LV stations.")
 
-        crit_stations = checks.voltage_issues(
-            edisgo_reinforce,
-            voltage_level="mv_lv",
-            split_voltage_band=split_voltage_band,
-        )
+        if lv_grid_id:
+            crit_stations = pd.DataFrame()
+        else:
+            crit_stations = checks.voltage_issues(
+                edisgo,
+                voltage_level="mv_lv",
+                split_voltage_band=split_voltage_band,
+            )
 
         while_counter = 0
         while not crit_stations.empty and while_counter < max_while_iterations:
             # reinforce distribution substations
             transformer_changes = (
                 reinforce_measures.reinforce_mv_lv_station_voltage_issues(
-                    edisgo_reinforce, crit_stations
+                    edisgo, crit_stations
                 )
             )
             # write added transformers to results.equipment_changes
@@ -405,11 +398,16 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
             # run power flow analysis again (after updating pypsa object) and
             # check if all over-voltage problems were solved
             logger.debug("==> Run power flow analysis.")
-            edisgo_reinforce.analyze(mode=analyze_mode, timesteps=timesteps_pfa)
+            edisgo.analyze(
+                mode=analyze_mode,
+                timesteps=timesteps_pfa,
+                lv_grid_id=lv_grid_id,
+                scale_timeseries=scale_timeseries,
+            )
 
             logger.debug("==> Recheck voltage at secondary side of LV stations.")
             crit_stations = checks.voltage_issues(
-                edisgo_reinforce,
+                edisgo,
                 voltage_level="mv_lv",
                 split_voltage_band=split_voltage_band,
             )
@@ -420,9 +418,9 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
         # check if all voltage problems were solved after maximum number of
         # iterations allowed
         if while_counter == max_while_iterations and crit_stations.empty:
-            edisgo_reinforce.results.unresolved_issues = pd.concat(
+            edisgo.results.unresolved_issues = pd.concat(
                 [
-                    edisgo_reinforce.results.unresolved_issues,
+                    edisgo.results.unresolved_issues,
                     pd.concat([_ for _ in crit_stations.values()]),
                 ]
             )
@@ -440,7 +438,10 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
     if not mode or mode == "lv":
         logger.debug("==> Check voltage in LV grids.")
         crit_nodes = checks.voltage_issues(
-            edisgo_reinforce, voltage_level="lv", split_voltage_band=split_voltage_band
+            edisgo,
+            voltage_level="lv",
+            split_voltage_band=split_voltage_band,
+            lv_grid_id=lv_grid_id,
         )
 
         while_counter = 0
@@ -449,8 +450,8 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
             for grid_id in crit_nodes.lv_grid_id.unique():
                 # reinforce lines
                 lines_changes = reinforce_measures.reinforce_lines_voltage_issues(
-                    edisgo_reinforce,
-                    edisgo_reinforce.topology.get_lv_grid(int(grid_id)),
+                    edisgo,
+                    edisgo.topology.get_lv_grid(int(grid_id)),
                     crit_nodes[crit_nodes.lv_grid_id == grid_id],
                 )
                 # write changed lines to results.equipment_changes
@@ -459,13 +460,19 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
             # run power flow analysis again (after updating pypsa object)
             # and check if all over-voltage problems were solved
             logger.debug("==> Run power flow analysis.")
-            edisgo_reinforce.analyze(mode=analyze_mode, timesteps=timesteps_pfa)
+            edisgo.analyze(
+                mode=analyze_mode,
+                timesteps=timesteps_pfa,
+                lv_grid_id=lv_grid_id,
+                scale_timeseries=scale_timeseries,
+            )
 
             logger.debug("==> Recheck voltage in LV grids.")
             crit_nodes = checks.voltage_issues(
-                edisgo_reinforce,
+                edisgo,
                 voltage_level="lv",
                 split_voltage_band=split_voltage_band,
+                lv_grid_id=lv_grid_id,
             )
 
             iteration_step += 1
@@ -474,9 +481,9 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
         # check if all voltage problems were solved after maximum number of
         # iterations allowed
         if while_counter == max_while_iterations and crit_nodes.empty:
-            edisgo_reinforce.results.unresolved_issues = pd.concat(
+            edisgo.results.unresolved_issues = pd.concat(
                 [
-                    edisgo_reinforce.results.unresolved_issues,
+                    edisgo.results.unresolved_issues,
                     pd.concat([_ for _ in crit_nodes.values()]),
                 ]
             )
@@ -492,29 +499,27 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
 
     # RECHECK FOR OVERLOADED TRANSFORMERS AND LINES
     logger.debug("==> Recheck station load.")
-
     overloaded_mv_station = (
         pd.DataFrame(dtype=float)
-        if mode == "lv"
-        else checks.hv_mv_station_max_overload(edisgo_reinforce)
+        if mode == "lv" or kwargs.get("skip_mv_reinforcement", False)
+        else checks.hv_mv_station_max_overload(edisgo)
     )
-
-    if mode != "mv":
-        overloaded_lv_stations = checks.mv_lv_station_max_overload(edisgo_reinforce)
+    if (lv_grid_id) or (mode == "mv"):
+        overloaded_lv_stations = pd.DataFrame(dtype=float)
+    else:
+        overloaded_lv_stations = checks.mv_lv_station_max_overload(edisgo)
 
     logger.debug("==> Recheck line load.")
-
     crit_lines = (
         pd.DataFrame(dtype=float)
-        if mode == "lv"
-        else checks.mv_line_max_relative_overload(edisgo_reinforce)
+        if mode == "lv" or kwargs.get("skip_mv_reinforcement", False)
+        else checks.mv_line_max_relative_overload(edisgo)
     )
-
     if not mode or mode == "lv":
         crit_lines = pd.concat(
             [
                 crit_lines,
-                checks.lv_line_max_relative_overload(edisgo_reinforce),
+                checks.lv_line_max_relative_overload(edisgo, lv_grid_id=lv_grid_id),
             ]
         )
 
@@ -529,7 +534,7 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
             # reinforce substations
             transformer_changes = (
                 reinforce_measures.reinforce_hv_mv_station_overloading(
-                    edisgo_reinforce, overloaded_mv_station
+                    edisgo, overloaded_mv_station
                 )
             )
             # write added and removed transformers to results.equipment_changes
@@ -540,7 +545,7 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
             # reinforce substations
             transformer_changes = (
                 reinforce_measures.reinforce_mv_lv_station_overloading(
-                    edisgo_reinforce, overloaded_lv_stations
+                    edisgo, overloaded_lv_stations
                 )
             )
             # write added and removed transformers to results.equipment_changes
@@ -550,7 +555,7 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
         if not crit_lines.empty:
             # reinforce lines
             lines_changes = reinforce_measures.reinforce_lines_overloading(
-                edisgo_reinforce, crit_lines
+                edisgo, crit_lines
             )
             # write changed lines to results.equipment_changes
             _add_lines_changes_to_equipment_changes()
@@ -558,31 +563,33 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
         # run power flow analysis again (after updating pypsa object) and check
         # if all over-loading problems were solved
         logger.debug("==> Run power flow analysis.")
-        edisgo_reinforce.analyze(mode=analyze_mode, timesteps=timesteps_pfa)
+        edisgo.analyze(
+            mode=analyze_mode,
+            timesteps=timesteps_pfa,
+            lv_grid_id=lv_grid_id,
+            scale_timeseries=scale_timeseries,
+        )
 
         logger.debug("==> Recheck station load.")
         overloaded_mv_station = (
             pd.DataFrame(dtype=float)
-            if mode == "lv"
-            else checks.hv_mv_station_max_overload(edisgo_reinforce)
+            if mode == "lv" or kwargs.get("skip_mv_reinforcement", False)
+            else checks.hv_mv_station_max_overload(edisgo)
         )
-
-        if mode != "mv":
-            overloaded_lv_stations = checks.mv_lv_station_max_overload(edisgo_reinforce)
+        if mode != "mv" and (not lv_grid_id):
+            overloaded_lv_stations = checks.mv_lv_station_max_overload(edisgo)
 
         logger.debug("==> Recheck line load.")
-
         crit_lines = (
             pd.DataFrame(dtype=float)
-            if mode == "lv"
-            else checks.mv_line_max_relative_overload(edisgo_reinforce)
+            if mode == "lv" or kwargs.get("skip_mv_reinforcement", False)
+            else checks.mv_line_max_relative_overload(edisgo)
         )
-
         if not mode or mode == "lv":
             crit_lines = pd.concat(
                 [
                     crit_lines,
-                    checks.lv_line_max_relative_overload(edisgo_reinforce),
+                    checks.lv_line_max_relative_overload(edisgo, lv_grid_id=lv_grid_id),
                 ]
             )
 
@@ -596,9 +603,9 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
         or not overloaded_mv_station.empty
         or not overloaded_lv_stations.empty
     ):
-        edisgo_reinforce.results.unresolved_issues = pd.concat(
+        edisgo.results.unresolved_issues = pd.concat(
             [
-                edisgo_reinforce.results.unresolved_issues,
+                edisgo.results.unresolved_issues,
                 crit_lines,
                 overloaded_lv_stations,
                 overloaded_mv_station,
@@ -616,7 +623,7 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
 
     # final check 10% criteria
     voltage_dev = checks.voltage_deviation_from_allowed_voltage_limits(
-        edisgo_reinforce, split_voltage_band=False
+        edisgo, split_voltage_band=False
     )
     voltage_dev = voltage_dev[voltage_dev != 0.0].dropna(how="all").dropna(how="all")
     if not voltage_dev.empty:
@@ -624,8 +631,284 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None):
         raise ValueError(message)
 
     # calculate topology expansion costs
-    edisgo_reinforce.results.grid_expansion_costs = grid_expansion_costs(
-        edisgo_reinforce, without_generator_import=without_generator_import
+    edisgo.results.grid_expansion_costs = grid_expansion_costs(
+        edisgo, without_generator_import=without_generator_import
+    )
+
+    return edisgo.results
+
+
+def catch_convergence_reinforce_grid(
+    edisgo: EDisGo,
+    **kwargs,
+) -> Results:
+    """
+    Reinforcement strategy to reinforce grids with non-converging time steps.
+
+    First, conducts a grid reinforcement with only converging time steps.
+    Afterwards, tries to run reinforcement with all time steps that did not converge
+    in the beginning. At last, if there are still time steps that do not converge,
+    the feed-in and load time series are iteratively scaled and the grid reinforced,
+    starting with a low grid load and scaling-up the time series until the original
+    values are reached.
+
+    Parameters
+    ----------
+    edisgo : :class:`~.EDisGo`
+    kwargs : dict
+        See parameters of function
+        :func:`edisgo.flex_opt.reinforce_grid.reinforce_grid`.
+
+    Returns
+    -------
+    :class:`~.network.results.Results`
+        Returns the Results object holding network expansion costs, equipment
+        changes, etc.
+
+    """
+
+    def reinforce():
+        try:
+            reinforce_grid(
+                edisgo,
+                timesteps_pfa=selected_timesteps,
+                scale_timeseries=set_scaling_factor,
+                **kwargs,
+            )
+            converged = True
+        except ValueError as e:
+            if "Power flow analysis did not converge for the" in str(e):
+                converged = False
+            else:
+                raise
+        return converged
+
+    logger.debug("Start 'catch-convergence' reinforcement.")
+
+    # Get the timesteps from kwargs and then remove it to set it later manually
+    timesteps_pfa = kwargs.get("timesteps_pfa")
+    kwargs.pop("timesteps_pfa")
+    selected_timesteps = timesteps_pfa
+
+    # Initial try
+    logger.info("Run initial reinforcement.")
+    set_scaling_factor = 1.0
+    iteration = 0
+    converged = reinforce()
+    if converged is False:
+        logger.info("Initial reinforcement did not succeed.")
+    else:
+        logger.info("Initial reinforcement succeeded.")
+        return edisgo.results
+
+    # Find non-converging time steps
+    if kwargs.get("mode", None) == "lv" and kwargs.get("lv_grid_id", None):
+        analyze_mode = "lv"
+    elif kwargs.get("mode", None) == "lv":
+        analyze_mode = None
+    else:
+        analyze_mode = kwargs.get("mode", None)
+    kwargs_analyze = kwargs
+    kwargs_analyze["mode"] = analyze_mode
+    converging_timesteps, non_converging_timesteps = edisgo.analyze(
+        timesteps=timesteps_pfa, raise_not_converged=False, **kwargs_analyze
+    )
+    logger.info(f"The following time steps converged: {converging_timesteps}.")
+    logger.info(
+        f"The following time steps did not converge: {non_converging_timesteps}."
     )
 
-    return edisgo_reinforce.results
+    # Run reinforcement for time steps that converged after initial reinforcement
+    if not converging_timesteps.empty:
+        logger.info(
+            "Run reinforcement for time steps that converged after initial "
+            "reinforcement."
+        )
+        selected_timesteps = converging_timesteps
+        reinforce()
+
+    # Run reinforcement for time steps that did not converge after initial reinforcement
+    if not non_converging_timesteps.empty:
+        logger.info(
+            "Run reinforcement for time steps that did not converge after initial "
+            "reinforcement."
+        )
+        selected_timesteps = non_converging_timesteps
+        converged = reinforce()
+
+    if converged:
+        return edisgo.results
+
+    # Run iterative grid reinforcement
+    else:
+        max_iterations = 10
+        highest_converged_scaling_factor = 0
+        minimal_scaling_factor = 0.05
+        while iteration < max_iterations:
+            iteration += 1
+            if converged:
+                if set_scaling_factor == 1:
+                    # reinforcement for scaling factor of 1 worked - finished
+                    break
+                else:
+                    # if converged, try again with scaling factor of 1
+                    highest_converged_scaling_factor = set_scaling_factor
+                    set_scaling_factor = 1
+            else:
+                if set_scaling_factor == minimal_scaling_factor:
+                    raise ValueError(
+                        f"Not reinforceable with {minimal_scaling_factor=}!"
+                    )
+                elif iteration == 1:
+                    set_scaling_factor = minimal_scaling_factor
+                else:
+                    set_scaling_factor = (
+                        (set_scaling_factor - highest_converged_scaling_factor) * 0.25
+                    ) + highest_converged_scaling_factor
+
+            logger.info(f"Try reinforcement with {set_scaling_factor=} at {iteration=}")
+            converged = reinforce()
+            if converged:
+                logger.info(
+                    f"Reinforcement succeeded for {set_scaling_factor=} "
+                    f"at {iteration=}."
+                )
+            else:
+                logger.info(
+                    f"Reinforcement failed for {set_scaling_factor=} at {iteration=}."
+                )
+
+        if converged is False:
+            raise ValueError(
+                f"Not reinforceable, max iterations ({max_iterations}) reached!"
+            )
+
+    # Final reinforcement
+    if set_scaling_factor != 1:
+        logger.info("Run final reinforcement.")
+        selected_timesteps = timesteps_pfa
+        reinforce()
+
+    return edisgo.results
+
+
+def enhanced_reinforce_grid(
+    edisgo_object: EDisGo, activate_cost_results_disturbing_mode: bool = False, **kwargs
+) -> EDisGo:
+    """
+    Reinforcement strategy to reinforce grids voltage level by voltage level in case
+    grid reinforcement method
+    :func:`edisgo.flex_opt.reinforce_grid.catch_convergence_reinforce_grid` is not
+    sufficient.
+
+    After first grid reinforcement for all voltage levels at once fails, reinforcement
+    is first conducted for the MV level only, afterwards for the MV level including
+    MV/LV stations and at last each LV grid separately.
+
+    Parameters
+    ----------
+    edisgo_object : :class:`~.EDisGo`
+    activate_cost_results_disturbing_mode : bool
+        If True, LV grids where normal grid reinforcement does not solve all issues,
+        two additional approaches are used to obtain a grid where power flow can be
+        conducted without non-convergence. These two approaches are currently not
+        included in the calculation of grid reinforcement costs, wherefore grid
+        reinforcement costs will be underestimated.
+        In the first approach, all lines in the LV grid are replaced by the
+        standard line type. Should this not be sufficient to solve non-convergence
+        issues, all components in the LV grid are aggregated to the MV/LV station.
+        Default: False.
+    kwargs : dict
+        Keyword arguments can be all parameters of function
+        :func:`edisgo.flex_opt.reinforce_grid.reinforce_grid`, except
+        `catch_convergence_problems` which will always be set to True, `mode` which
+        is set to None, and `skip_mv_reinforcement` which will be ignored.
+
+    Returns
+    -------
+    :class:`~.EDisGo`
+        The reinforced eDisGo object.
+
+    """
+    if kwargs.get("copy_grid", True):
+        edisgo_obj = copy.deepcopy(edisgo_object)
+    else:
+        edisgo_obj = edisgo_object
+    kwargs["copy_grid"] = False
+    kwargs.pop("skip_mv_reinforcement", False)
+
+    try:
+        logger.info("Try initial enhanced reinforcement.")
+        edisgo_obj.reinforce(mode=None, catch_convergence_problems=True, **kwargs)
+        logger.info("Initial enhanced reinforcement succeeded.")
+    except:  # noqa: E722
+        logger.info("Initial enhanced reinforcement failed.")
+        logger.info("Try mode 'mv' reinforcement.")
+        try:
+            edisgo_obj.reinforce(mode="mv", catch_convergence_problems=True, **kwargs)
+            logger.info("Mode 'mv' reinforcement succeeded.")
+        except:  # noqa: E722
+            logger.info("Mode 'mv' reinforcement failed.")
+
+        logger.info("Try mode 'mvlv' reinforcement.")
+        try:
+            edisgo_obj.reinforce(mode="mvlv", catch_convergence_problems=True, **kwargs)
+            logger.info("Mode 'mvlv' reinforcement succeeded.")
+        except:  # noqa: E722
+            logger.info("Mode 'mvlv' reinforcement failed.")
+
+        for lv_grid in list(edisgo_obj.topology.mv_grid.lv_grids):
+            try:
+                logger.info(f"Try mode 'lv' reinforcement for {lv_grid=}.")
+                edisgo_obj.reinforce(
+                    mode="lv",
+                    lv_grid_id=lv_grid.id,
+                    catch_convergence_problems=True,
+                    **kwargs,
+                )
+                logger.info(f"Mode 'lv' reinforcement for {lv_grid} successful.")
+            except:  # noqa: E722
+                logger.info(f"Mode 'lv' reinforcement for {lv_grid} failed.")
+                if activate_cost_results_disturbing_mode:
+                    try:
+                        logger.warning(
+                            f"Change all lines to standard type in {lv_grid=}."
+                        )
+                        lv_standard_line_type = edisgo_obj.config[
+                            "grid_expansion_standard_equipment"
+                        ]["lv_line"]
+                        edisgo_obj.topology.change_line_type(
+                            lv_grid.lines_df.index.to_list(), lv_standard_line_type
+                        )
+                        edisgo_obj.reinforce(
+                            mode="lv",
+                            lv_grid_id=lv_grid.id,
+                            catch_convergence_problems=True,
+                            **kwargs,
+                        )
+                        logger.info(
+                            f"Changed lines mode 'lv' for {lv_grid} successful."
+                        )
+                    except:  # noqa: E722
+                        logger.info(f"Changed lines mode 'lv' for {lv_grid} failed.")
+                        logger.warning(
+                            f"Aggregate all nodes to station bus in {lv_grid=}."
+                        )
+                        try:
+                            edisgo_obj.topology.aggregate_lv_grid_at_station(
+                                lv_grid_id=lv_grid.id
+                            )
+                            logger.info(
+                                f"Aggregate to station for {lv_grid} successful."
+                            )
+                        except:  # noqa: E722
+                            logger.info(f"Aggregate to station for {lv_grid} failed.")
+
+        try:
+            edisgo_obj.reinforce(mode=None, catch_convergence_problems=True, **kwargs)
+            logger.info("Enhanced reinforcement succeeded.")
+        except Exception as e:  # noqa: E722
+            logger.info("Enhanced reinforcement failed.")
+            raise e
+
+    return edisgo_obj
diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py
index 122e6677..56cb9d8a 100644
--- a/edisgo/flex_opt/reinforce_measures.py
+++ b/edisgo/flex_opt/reinforce_measures.py
@@ -399,7 +399,7 @@ def reinforce_lines_voltage_issues(edisgo_obj, grid, crit_nodes):
         # because voltage issues should have been solved during extension of
         # distribution substations due to overvoltage issues.
         if len(path) == 1:
-            logging.error(
+            logger.error(
                 "Voltage issues at busbar in LV network {} should have "
                 "been solved in previous steps.".format(grid)
             )
diff --git a/edisgo/io/electromobility_import.py b/edisgo/io/electromobility_import.py
index f3cb2ed2..cfc87462 100644
--- a/edisgo/io/electromobility_import.py
+++ b/edisgo/io/electromobility_import.py
@@ -284,7 +284,7 @@ def read_simbev_config_df(
             return df.assign(days=(df.end_date - df.start_date).iat[0].days + 1)
 
     except Exception:
-        logging.warning(
+        logger.warning(
             "SimBEV config file could not be imported. Charging point "
             "efficiency is set to 100%, the stepsize is set to 15 minutes "
             "and the simulated days are estimated from the charging "
diff --git a/edisgo/network/electromobility.py b/edisgo/network/electromobility.py
index 82b3f163..76babfc1 100644
--- a/edisgo/network/electromobility.py
+++ b/edisgo/network/electromobility.py
@@ -918,7 +918,7 @@ def from_csv(self, data_path, edisgo_obj, from_zip_archive=False):
                     )
 
                 except Exception:
-                    logging.warning(
+                    logger.warning(
                         f"Potential charging parks could not be loaded with "
                         f"EPSG {epsg}. Trying with EPSG 4326 as fallback."
                     )
diff --git a/edisgo/network/timeseries.py b/edisgo/network/timeseries.py
index be57bfc5..de5de9dc 100644
--- a/edisgo/network/timeseries.py
+++ b/edisgo/network/timeseries.py
@@ -716,7 +716,7 @@ def _overwrite_time_series(p, q, comp_type):
                 set(df.index) - set(self.loads_active_power.columns)
             )
             if loads_without_ts:
-                logging.warning(
+                logger.warning(
                     "There are loads where information on type of load is missing. "
                     "Handled types are 'conventional_load', 'charging_point', and "
                     "'heat_pump'. Loads with missing type information are handled as "
@@ -2148,7 +2148,7 @@ def _check_if_components_exist(
         comps_not_in_network = list(set(component_names) - set(comps_in_network))
 
         if comps_not_in_network:
-            logging.warning(
+            logger.warning(
                 f"Some of the provided {component_type} are not in the network. This "
                 f"concerns the following components: {comps_not_in_network}."
             )
@@ -2200,6 +2200,36 @@ def resample(self, method: str = "ffill", freq: str | pd.Timedelta = "15min"):
         # set new timeindex
         self._timeindex = index
 
+    def scale_timeseries(
+        self, p_scaling_factor: float = 1.0, q_scaling_factor: float = 1.0
+    ):
+        """
+        Scales component time series by given factors.
+
+        The changes are directly applied to the TimeSeries object.
+
+        Parameters
+        -----------
+        p_scaling_factor : float
+            Scaling factor to use for active power time series. Values between 0 and 1
+            will scale down the time series and values above 1 will scale the
+            timeseries up. Default: 1.
+        q_scaling_factor : float
+            Scaling factor to use for reactive power time series. Values between 0 and 1
+            will scale down the time series and values above 1 will scale the
+            timeseries up. Default: 1.
+
+        """
+        attributes_type = ["generators", "loads", "storage_units"]
+        power_types = {
+            "active_power": p_scaling_factor,
+            "reactive_power": q_scaling_factor,
+        }
+        for suffix, scaling_factor in power_types.items():
+            for type in attributes_type:
+                attribute = f"{type}_{suffix}"
+                setattr(self, attribute, getattr(self, attribute) * scaling_factor)
+
 
 class TimeSeriesRaw:
     """
diff --git a/edisgo/network/topology.py b/edisgo/network/topology.py
index aada8b1c..d8db32e9 100755
--- a/edisgo/network/topology.py
+++ b/edisgo/network/topology.py
@@ -745,7 +745,7 @@ def get_lv_grid(self, name):
         elif isinstance(name, str):
             return LVGrid(id=int(name.split("_")[-1]), edisgo_obj=edisgo_obj)
         else:
-            logging.warning("`name` must be integer or string.")
+            logger.warning("`name` must be integer or string.")
 
     @property
     def grid_district(self):
@@ -1379,7 +1379,7 @@ def _get_line_data():
             (self.lines_df.bus1 == bus0) & (self.lines_df.bus0 == bus1)
         ]
         if not bus0_bus1.empty and bus1_bus0.empty:
-            logging.debug("Line between bus0 {} and bus1 {} already exists.")
+            logger.debug("Line between bus0 {} and bus1 {} already exists.")
             return pd.concat(
                 [
                     bus1_bus0,
@@ -1732,7 +1732,7 @@ def change_line_type(self, lines, new_line_type):
                     self.lines_df.at[lines[0], "bus0"], "v_nom"
                 ]
                 if grid_voltage != data_new_line.U_n:
-                    logging.debug(
+                    logger.debug(
                         f"The line type of lines {lines} is changed to a type with a "
                         f"different nominal voltage (nominal voltage of new line type "
                         f"is {data_new_line.U_n} kV while nominal voltage of the medium"
@@ -3077,5 +3077,37 @@ def check_integrity(self):
                 f"optimisation."
             )
 
+    def aggregate_lv_grid_at_station(self, lv_grid_id: int | str) -> None:
+        """
+        Aggregates all LV grid components to secondary side of the grid's station.
+
+        All lines of the LV grid are dropped, as well as all buses except the station's
+        secondary side bus. Buses, the loads, generators and storage units are connected
+        to are changed to the station's secondary side bus. The changes are directly
+        applied to the Topology object.
+
+        Parameters
+        ----------
+        lv_grid_id : int or str
+            ID of the LV grid to aggregate.
+
+        """
+        lv_grid = self.get_lv_grid(name=lv_grid_id)
+        lines_to_drop = lv_grid.lines_df.index.to_list()
+        station_bus = lv_grid.station.index[0]
+        buses_to_drop = lv_grid.buses_df.loc[
+            lv_grid.buses_df.index != station_bus
+        ].index.to_list()
+
+        self.buses_df = self.buses_df[~self.buses_df.index.isin(buses_to_drop)]
+        self.lines_df = self.lines_df[~self.lines_df.index.isin(lines_to_drop)]
+        self.loads_df.loc[self.loads_df.bus.isin(buses_to_drop), "bus"] = station_bus
+        self.generators_df.loc[
+            self.generators_df.bus.isin(buses_to_drop), "bus"
+        ] = station_bus
+        self.storage_units_df.loc[
+            self.storage_units_df.bus.isin(buses_to_drop), "bus"
+        ] = station_bus
+
     def __repr__(self):
         return f"Network topology {self.id}"
diff --git a/edisgo/tools/plots.py b/edisgo/tools/plots.py
index 69141364..4170eaed 100644
--- a/edisgo/tools/plots.py
+++ b/edisgo/tools/plots.py
@@ -622,7 +622,7 @@ def nodes_by_costs(buses, grid_expansion_costs, edisgo_obj):
         if kwargs.get("bus_colors", None):
             bus_colors = pd.Series(kwargs.get("bus_colors")).loc[pypsa_plot.buses]
         else:
-            logging.warning(
+            logger.warning(
                 "Choice for `node_color` is not valid. Default bus colors are "
                 "used instead."
             )
@@ -630,7 +630,7 @@ def nodes_by_costs(buses, grid_expansion_costs, edisgo_obj):
         if kwargs.get("bus_sizes", None):
             bus_sizes = pd.Series(kwargs.get("bus_sizes")).loc[pypsa_plot.buses]
         else:
-            logging.warning(
+            logger.warning(
                 "Choice for `node_color` is not valid. Default bus sizes are "
                 "used instead."
             )
@@ -638,7 +638,7 @@ def nodes_by_costs(buses, grid_expansion_costs, edisgo_obj):
         if kwargs.get("bus_cmap", None):
             bus_cmap = kwargs.get("bus_cmap", None)
         else:
-            logging.warning(
+            logger.warning(
                 "Choice for `node_color` is not valid. Default bus colormap "
                 "is used instead."
             )
@@ -673,7 +673,7 @@ def nodes_by_costs(buses, grid_expansion_costs, edisgo_obj):
                 region = region.to_crs(epsg=projection)
             region.plot(ax=ax, color="white", alpha=0.2, edgecolor="red", linewidth=2)
         except Exception as e:
-            logging.warning(
+            logger.warning(
                 "Grid district geometry could not be plotted due "
                 "to the following error: {}".format(e)
             )
@@ -850,7 +850,7 @@ def nodes_by_costs(buses, grid_expansion_costs, edisgo_obj):
         try:
             add_basemap(ax, zoom=12)
         except Exception as e:
-            logging.warning(
+            logger.warning(
                 "Background map could not be plotted due to the "
                 "following error: {}".format(e)
             )


Test patch:
diff --git a/tests/flex_opt/test_check_tech_constraints.py b/tests/flex_opt/test_check_tech_constraints.py
index 93d98f0d..c3ec7ea4 100644
--- a/tests/flex_opt/test_check_tech_constraints.py
+++ b/tests/flex_opt/test_check_tech_constraints.py
@@ -51,6 +51,22 @@ def test_lv_line_max_relative_overload(self):
         )
         assert df.at["Line_50000002", "time_index"] == self.timesteps[0]
 
+        # test for single LV grid
+        lv_grid_id = 5
+        self.edisgo.analyze(mode="lv", lv_grid_id=lv_grid_id)
+        df = check_tech_constraints.lv_line_max_relative_overload(
+            self.edisgo, lv_grid_id=lv_grid_id
+        )
+        # check shape of dataframe
+        assert (1, 3) == df.shape
+        # check relative overload of line
+        assert np.isclose(
+            df.at["Line_50000002", "max_rel_overload"],
+            self.edisgo.results.s_res.at[self.timesteps[0], "Line_50000002"]
+            / 0.08521689973238901,
+        )
+        assert df.at["Line_50000002", "time_index"] == self.timesteps[0]
+
     def test_lines_allowed_load(self):
 
         # check with default value (all lines)
@@ -177,6 +193,14 @@ def test_mv_lv_station_max_overload(self):
         )
         assert df.at["LVGrid_1_station", "time_index"] == self.timesteps[0]
 
+        # test for single LV grid
+        lv_grid_id = 1
+        df = check_tech_constraints.mv_lv_station_max_overload(
+            self.edisgo, lv_grid_id=lv_grid_id
+        )
+        assert (1, 3) == df.shape
+        assert df.at["LVGrid_1_station", "time_index"] == self.timesteps[0]
+
     def test__station_load(self):
         # check LV grid
         grid = self.edisgo.topology.get_lv_grid(4)
@@ -466,8 +490,18 @@ def test_voltage_issues(self):
             0.08,
         )
 
-        # ########################## check mv_lv mode ###############################
+        # ################ check with single LV grid
+        lv_grid_id = 1
+        # uses same voltage issues as created above
+        voltage_issues = check_tech_constraints.voltage_issues(
+            self.edisgo,
+            voltage_level="lv",
+            split_voltage_band=False,
+            lv_grid_id=lv_grid_id,
+        )
+        assert len(voltage_issues) == 0
 
+        # ########################## check mv_lv mode ###############################
         # ############## check that voltage issue in station of LVGrid_6 is detected
         # uses same voltage issues as created above
         voltage_issues = check_tech_constraints.voltage_issues(
@@ -490,7 +524,18 @@ def test_voltage_issues(self):
             0.04,
         )
 
-        # ########################## check mv_lv mode ###############################
+        # ################ check with single LV grid
+        lv_grid_id = 1
+        # uses same voltage issues as created above
+        voltage_issues = check_tech_constraints.voltage_issues(
+            self.edisgo,
+            voltage_level="mv_lv",
+            split_voltage_band=False,
+            lv_grid_id=lv_grid_id,
+        )
+        assert len(voltage_issues) == 0
+
+        # ########################## check mode None ###############################
 
         # ################ check with voltage issues and default values
         voltage_issues = check_tech_constraints.voltage_issues(
diff --git a/tests/flex_opt/test_reinforce_grid.py b/tests/flex_opt/test_reinforce_grid.py
index 0f6c79c7..46085164 100644
--- a/tests/flex_opt/test_reinforce_grid.py
+++ b/tests/flex_opt/test_reinforce_grid.py
@@ -1,3 +1,5 @@
+import copy
+
 import numpy as np
 import pytest
 
@@ -9,6 +11,11 @@
 
 
 class TestReinforceGrid:
+    """
+    Here, currently only reinforce_grid function is tested.
+    Other functions in reinforce_grid module are currently tested in test_edisgo module.
+    """
+
     @classmethod
     def setup_class(cls):
         cls.edisgo = EDisGo(ding0_grid=pytest.ding0_test_network_path)
@@ -19,7 +26,7 @@ def test_reinforce_grid(self):
         modes = [None, "mv", "mvlv", "lv"]
 
         results_dict = {
-            mode: reinforce_grid(edisgo=self.edisgo, copy_grid=True, mode=mode)
+            mode: reinforce_grid(edisgo=copy.deepcopy(self.edisgo), mode=mode)
             for mode in modes
         }
 
diff --git a/tests/network/test_timeseries.py b/tests/network/test_timeseries.py
index 102751bd..96979423 100644
--- a/tests/network/test_timeseries.py
+++ b/tests/network/test_timeseries.py
@@ -1,3 +1,4 @@
+import copy
 import logging
 import os
 import shutil
@@ -2470,6 +2471,38 @@ def test_resample(self):
             atol=1e-5,
         )
 
+    def test_scale_timeseries(self):
+        self.edisgo.set_time_series_worst_case_analysis()
+        edisgo_scaled = copy.deepcopy(self.edisgo)
+        edisgo_scaled.timeseries.scale_timeseries(
+            p_scaling_factor=0.5, q_scaling_factor=0.4
+        )
+
+        assert_frame_equal(
+            edisgo_scaled.timeseries.generators_active_power,
+            self.edisgo.timeseries.generators_active_power * 0.5,
+        )
+        assert_frame_equal(
+            edisgo_scaled.timeseries.generators_reactive_power,
+            self.edisgo.timeseries.generators_reactive_power * 0.4,
+        )
+        assert_frame_equal(
+            edisgo_scaled.timeseries.loads_active_power,
+            self.edisgo.timeseries.loads_active_power * 0.5,
+        )
+        assert_frame_equal(
+            edisgo_scaled.timeseries.loads_reactive_power,
+            self.edisgo.timeseries.loads_reactive_power * 0.4,
+        )
+        assert_frame_equal(
+            edisgo_scaled.timeseries.storage_units_active_power,
+            self.edisgo.timeseries.storage_units_active_power * 0.5,
+        )
+        assert_frame_equal(
+            edisgo_scaled.timeseries.storage_units_reactive_power,
+            self.edisgo.timeseries.storage_units_reactive_power * 0.4,
+        )
+
 
 class TestTimeSeriesRaw:
     @pytest.fixture(autouse=True)
diff --git a/tests/network/test_topology.py b/tests/network/test_topology.py
index 1851e572..2eed4c1c 100644
--- a/tests/network/test_topology.py
+++ b/tests/network/test_topology.py
@@ -1,3 +1,4 @@
+import copy
 import logging
 import os
 import shutil
@@ -869,6 +870,24 @@ def test_to_csv(self):
 
         shutil.rmtree(dir)
 
+    def test_aggregate_lv_grid_at_station(self, caplog):
+        """Test method aggregate_lv_grid_at_station"""
+
+        lv_grid_id = 1
+        topology_obj = copy.deepcopy(self.topology)
+        lv_grid_orig = self.topology.get_lv_grid(lv_grid_id)
+        topology_obj.aggregate_lv_grid_at_station(lv_grid_id=lv_grid_id)
+        lv_grid = topology_obj.get_lv_grid(lv_grid_id)
+
+        assert lv_grid_orig.buses_df.shape[0] == 15
+        assert lv_grid.buses_df.shape[0] == 1
+
+        with caplog.at_level(logging.WARNING):
+            topology_obj.check_integrity()
+        assert "which are not defined" not in caplog.text
+        assert "The following buses are isolated" not in caplog.text
+        assert "The network has isolated nodes or edges." not in caplog.text
+
 
 class TestTopologyWithEdisgoObject:
     """
diff --git a/tests/test_edisgo.py b/tests/test_edisgo.py
index 3af24072..39ed3523 100755
--- a/tests/test_edisgo.py
+++ b/tests/test_edisgo.py
@@ -16,6 +16,8 @@
 
 from edisgo import EDisGo
 from edisgo.edisgo import import_edisgo_from_files
+from edisgo.flex_opt.reinforce_grid import enhanced_reinforce_grid
+from edisgo.network.results import Results
 
 
 class TestEDisGo:
@@ -420,12 +422,13 @@ def test_analyze(self, caplog):
             self.edisgo.analyze(troubleshooting_mode="iteration", range_start=5)
 
         caplog.clear()
-        self.edisgo.analyze(
-            troubleshooting_mode="iteration",
-            range_start=5,
-            range_num=2,
-            raise_not_converged=False,
-        )
+        with caplog.at_level("INFO"):
+            self.edisgo.analyze(
+                troubleshooting_mode="iteration",
+                range_start=5,
+                range_num=2,
+                raise_not_converged=False,
+            )
         assert "Current fraction in iterative process: 5.0." in caplog.text
         assert "Current fraction in iterative process: 1.0." in caplog.text
 
@@ -440,25 +443,110 @@ def test_reinforce(self):
         assert results.v_res.shape == (4, 142)
         assert self.edisgo.results.v_res.shape == (4, 142)
 
-        # ###################### test mode lv and copy grid ##########################
+        # ###################### test without worst case settings ####################
+        self.setup_worst_case_time_series()
+        results = self.edisgo.reinforce(is_worst_case=False)
+        assert results.unresolved_issues.empty
+        assert len(results.grid_expansion_costs) == 10
+        assert len(results.equipment_changes) == 10
+        assert results.v_res.shape == (4, 142)
+        assert self.edisgo.results.v_res.shape == (4, 142)
+
+        # ###################### test mode mv and copy grid ##########################
         self.setup_edisgo_object()
         self.setup_worst_case_time_series()
+        results = self.edisgo.reinforce(mode="mv", copy_grid=True)
+        assert results.unresolved_issues.empty
+        assert len(results.grid_expansion_costs) == 4
+        assert len(results.equipment_changes) == 4
+        assert results.v_res.shape == (4, 31)
+        assert self.edisgo.results.v_res.empty
+
+        # ###################### test mode lv and copy grid ##########################
         results = self.edisgo.reinforce(mode="lv", copy_grid=True)
         assert results.unresolved_issues.empty
         assert len(results.grid_expansion_costs) == 6
         assert len(results.equipment_changes) == 6
-        assert results.v_res.shape == (2, 142)
+        assert results.v_res.shape == (4, 142)
         assert self.edisgo.results.v_res.empty
 
         # ################# test mode mvlv and combined analysis ####################
-        # self.setup_edisgo_object()
-        # self.setup_worst_case_time_series()
-        results = self.edisgo.reinforce(mode="mvlv", combined_analysis=False)
+        results = self.edisgo.reinforce(
+            mode="mvlv", combined_analysis=False, is_worst_case=True
+        )
         assert results.unresolved_issues.empty
-        assert len(results.grid_expansion_costs) == 8
-        assert len(results.equipment_changes) == 8
+        assert len(results.grid_expansion_costs) == 4
+        assert len(results.equipment_changes) == 4
         assert results.v_res.shape == (4, 41)
 
+        # ###################### test with only one lv grid ##########################
+        # test grid without issues
+        self.edisgo.results = Results(self.edisgo)
+        lv_grid_id = 1
+        results = self.edisgo.reinforce(mode="lv", lv_grid_id=lv_grid_id)
+        assert results.unresolved_issues.empty
+        assert results.equipment_changes.empty
+        assert results.v_res.shape == (4, 15)
+        # test grid with issues
+        lv_grid_id = 5
+        results = self.edisgo.reinforce(mode="lv", lv_grid_id=lv_grid_id)
+        assert len(results.grid_expansion_costs) == 1
+        assert len(results.equipment_changes) == 1
+        assert results.v_res.shape == (4, 9)
+
+    def test_reinforce_catch_convergence(self):
+
+        # ###################### test that wrong mode is raised ######################
+        msg = "Provided mode mvl is not a valid mode."
+        with pytest.raises(ValueError, match=msg):
+            self.edisgo.reinforce(
+                catch_convergence_problems=True, is_worst_case=False, mode="mvl"
+            )
+
+        # ###################### test with catch convergence ##########################
+        self.setup_worst_case_time_series()
+        self.edisgo.timeseries.scale_timeseries(
+            p_scaling_factor=10, q_scaling_factor=10
+        )
+        results = self.edisgo.reinforce(
+            catch_convergence_problems=True,
+            is_worst_case=False,
+            copy_grid=True,
+        )
+        assert results.unresolved_issues.empty
+        assert len(results.grid_expansion_costs) == 134
+        assert len(results.equipment_changes) == 230
+        assert results.v_res.shape == (4, 142)
+
+        # ############### test with catch convergence worst case true ################
+        self.setup_worst_case_time_series()
+        self.edisgo.timeseries.scale_timeseries(
+            p_scaling_factor=10, q_scaling_factor=10
+        )
+        results = self.edisgo.reinforce(catch_convergence_problems=True)
+        assert results.unresolved_issues.empty
+        assert len(results.grid_expansion_costs) == 132
+        assert len(results.equipment_changes) == 218
+        assert results.v_res.shape == (4, 142)
+
+    @pytest.mark.slow
+    def test_enhanced_reinforce_grid(self):
+        self.setup_edisgo_object()
+        self.setup_worst_case_time_series()
+        self.edisgo.timeseries.scale_timeseries(
+            p_scaling_factor=100, q_scaling_factor=100
+        )
+        edisgo_obj = copy.deepcopy(self.edisgo)
+        edisgo_obj = enhanced_reinforce_grid(
+            edisgo_obj, activate_cost_results_disturbing_mode=True
+        )
+
+        results = edisgo_obj.results
+
+        assert len(results.grid_expansion_costs) == 835
+        assert len(results.equipment_changes) == 1764
+        assert results.v_res.shape == (4, 142)
+
     def test_add_component(self, caplog):
         self.setup_worst_case_time_series()
         index = self.edisgo.timeseries.timeindex