From 72dcf077433ee3a0c855189fb70d026e3b05ce22 Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 12 Feb 2024 20:37:52 -0800 Subject: [PATCH 01/11] Make residual_load function --- edisgo/flex_opt/charging_strategies.py | 2 +- edisgo/network/timeseries.py | 3 +-- edisgo/tools/tools.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/edisgo/flex_opt/charging_strategies.py b/edisgo/flex_opt/charging_strategies.py index 393b26a05..e4def98a7 100644 --- a/edisgo/flex_opt/charging_strategies.py +++ b/edisgo/flex_opt/charging_strategies.py @@ -211,7 +211,7 @@ def charging_strategy( ) # get residual load - init_residual_load = edisgo_obj.timeseries.residual_load + init_residual_load = edisgo_obj.timeseries.residual_load() len_residual_load = int(charging_processes_df.park_end_timesteps.max()) diff --git a/edisgo/network/timeseries.py b/edisgo/network/timeseries.py index 8da353ff2..4d56bcbe1 100644 --- a/edisgo/network/timeseries.py +++ b/edisgo/network/timeseries.py @@ -1785,8 +1785,7 @@ def _get_q_sign_and_power_factor_per_component( "storage_units_reactive_power", reactive_power ) - @property - def residual_load(self): + def residual_load(self, feeder=None, edisgo_obj=None): """ Returns residual load in network. diff --git a/edisgo/tools/tools.py b/edisgo/tools/tools.py index ead8e08d2..48da04554 100644 --- a/edisgo/tools/tools.py +++ b/edisgo/tools/tools.py @@ -53,7 +53,7 @@ def select_worstcase_snapshots(edisgo_obj): :pandas:`pandas.Timestamp`. """ - residual_load = edisgo_obj.timeseries.residual_load + residual_load = edisgo_obj.timeseries.residual_load() timestamp = { "min_residual_load": residual_load.idxmin(), From e6994531e0cc78a07c125287a85efab3f6ab7238 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 14 Feb 2024 14:58:27 -0800 Subject: [PATCH 02/11] Implement n-1 for lines in cycles --- edisgo/flex_opt/check_tech_constraints.py | 74 +++++++++---------- tests/flex_opt/test_check_tech_constraints.py | 20 +++++ 2 files changed, 54 insertions(+), 40 deletions(-) diff --git a/edisgo/flex_opt/check_tech_constraints.py b/edisgo/flex_opt/check_tech_constraints.py index 3abf0b671..38beb356e 100644 --- a/edisgo/flex_opt/check_tech_constraints.py +++ b/edisgo/flex_opt/check_tech_constraints.py @@ -1,3 +1,4 @@ +import itertools import logging import numpy as np @@ -250,7 +251,8 @@ def _lines_allowed_load_voltage_level(edisgo_obj, voltage_level, n_minus_one=Fal section 'grid_expansion_load_factors' are used. This is the default. In case it is set to True, allowed load factors defined in the config file 'config_grid_expansion' in section 'grid_expansion_load_factors_n_minus_one' - are used. This case is currently not implemented. + are used for lines in rings. For all other lines, 'grid_expansion_load_factors' + are used. Returns ------- @@ -282,45 +284,37 @@ def _lines_allowed_load_voltage_level(edisgo_obj, voltage_level, n_minus_one=Fal # get allowed loads per case if n_minus_one is True: - raise NotImplementedError("n-1 security can currently not be checked.") - # # handle lines in cycles differently from lines in stubs - # for case in ["feed-in_case", "load_case"]: - # if ( - # edisgo_obj.config["grid_expansion_load_factors_n_minus_one"][ - # f"{voltage_level}_{case}_line" - # ] - # != 1.0 - # ): - # - # buses_in_cycles = list( - # set(itertools.chain.from_iterable(edisgo_obj.topology.rings)) - # ) - # - # # Find lines in cycles - # lines_in_cycles = list( - # lines_df.loc[ - # lines_df[["bus0", "bus1"]].isin(buses_in_cycles).all(axis=1) - # ].index.values - # ) - # lines_radial_feeders = list( - # lines_df.loc[~lines_df.index.isin(lines_in_cycles)].index.values - # ) - # - # # lines in cycles have to be n-1 secure - # allowed_load_per_case[case] = ( - # lines_df.loc[lines_in_cycles].s_nom - # * edisgo_obj.config["grid_expansion_load_factors_n_minus_one"][ - # f"{voltage_level}_{case}_line" - # ] - # ) - # - # # lines in radial feeders are not n-1 secure anyway - # allowed_load_per_case[case] = pd.concat( - # [ - # allowed_load_per_case[case], - # lines_df.loc[lines_radial_feeders].s_nom, - # ] - # ) + # handle lines in cycles differently from lines in stubs + buses_in_cycles = list( + set(itertools.chain.from_iterable(edisgo_obj.topology.rings)) + ) + # find lines in cycles + lines_in_cycles = list( + lines_df.loc[ + lines_df[["bus0", "bus1"]].isin(buses_in_cycles).all(axis=1) + ].index.values + ) + lines_radial_feeders = list( + lines_df.loc[~lines_df.index.isin(lines_in_cycles)].index.values + ) + for case in ["feed-in_case", "load_case"]: + # lines in cycles have to be n-1 secure + allowed_load_per_case[case] = ( + lines_df.loc[lines_in_cycles].s_nom + * edisgo_obj.config["grid_expansion_load_factors_n_minus_one"][ + f"{voltage_level}_{case}_line" + ] + ) + # lines in radial feeders are not n-1 secure anyway + allowed_load_per_case[case] = pd.concat( + [ + allowed_load_per_case[case], + lines_df.loc[lines_radial_feeders].s_nom + * edisgo_obj.config["grid_expansion_load_factors"][ + f"{voltage_level}_{case}_line" + ], + ] + ) else: for case in ["feed-in_case", "load_case"]: allowed_load_per_case[case] = ( diff --git a/tests/flex_opt/test_check_tech_constraints.py b/tests/flex_opt/test_check_tech_constraints.py index 60627743b..6f2e20da2 100644 --- a/tests/flex_opt/test_check_tech_constraints.py +++ b/tests/flex_opt/test_check_tech_constraints.py @@ -117,6 +117,26 @@ def test__lines_allowed_load_voltage_level(self): df.at[self.timesteps[0], "Line_10024"], 7.27461339178928, ) + # check n-1 + df = check_tech_constraints._lines_allowed_load_voltage_level( + self.edisgo, "mv", n_minus_one=True + ) + # check shape of dataframe + assert (4, 30) == df.shape + # check in feed-in case + assert np.isclose( + df.at[self.timesteps[2], "Line_10005"], + 7.27461339178928, + ) + # check in load case + assert np.isclose( + df.at[self.timesteps[0], "Line_10005"], + 7.274613391789284 / 2, + ) + assert np.isclose( + df.at[self.timesteps[0], "Line_10024"], + 7.27461339178928, + ) # check for LV df = check_tech_constraints._lines_allowed_load_voltage_level(self.edisgo, "lv") From e7d9d74e733174618607931cc468b0bbb3760fa2 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 14 Feb 2024 15:41:07 -0800 Subject: [PATCH 03/11] Adapt docstrings --- edisgo/flex_opt/check_tech_constraints.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/edisgo/flex_opt/check_tech_constraints.py b/edisgo/flex_opt/check_tech_constraints.py index 38beb356e..447543572 100644 --- a/edisgo/flex_opt/check_tech_constraints.py +++ b/edisgo/flex_opt/check_tech_constraints.py @@ -18,9 +18,7 @@ def mv_line_max_relative_overload(edisgo_obj, n_minus_one=False): edisgo_obj : :class:`~.EDisGo` n_minus_one : bool Determines which allowed load factors to use (see :py:attr:`~lines_allowed_load` - 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. + for more information). Returns ------- @@ -39,7 +37,8 @@ def mv_line_max_relative_overload(edisgo_obj, n_minus_one=False): ----- Line over-load is determined based on allowed load factors for feed-in and load cases that are defined in the config file 'config_grid_expansion' in - section 'grid_expansion_load_factors'. + section 'grid_expansion_load_factors' or in case of n-1 security + 'grid_expansion_load_factors_n_minus_one'. """ @@ -68,9 +67,7 @@ def lv_line_max_relative_overload(edisgo_obj, n_minus_one=False, lv_grid_id=None edisgo_obj : :class:`~.EDisGo` n_minus_one : bool Determines which allowed load factors to use (see :py:attr:`~lines_allowed_load` - 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. + for more information). 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. @@ -92,7 +89,8 @@ def lv_line_max_relative_overload(edisgo_obj, n_minus_one=False, lv_grid_id=None ----- Line over-load is determined based on allowed load factors for feed-in and load cases that are defined in the config file 'config_grid_expansion' in - section 'grid_expansion_load_factors'. + section 'grid_expansion_load_factors' or in case of n-1 security + 'grid_expansion_load_factors_n_minus_one'. """ From 6aabfce33e82f3caa8cf455ddc5034f591ec73d7 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 21 Feb 2024 14:35:37 -0800 Subject: [PATCH 04/11] Allow only scaling certain time series --- edisgo/network/timeseries.py | 11 ++++++++--- tests/network/test_timeseries.py | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/edisgo/network/timeseries.py b/edisgo/network/timeseries.py index 4d56bcbe1..7fb04c48e 100644 --- a/edisgo/network/timeseries.py +++ b/edisgo/network/timeseries.py @@ -2204,7 +2204,8 @@ def resample(self, method: str = "ffill", freq: str | pd.Timedelta = "15min"): self._timeindex = index def scale_timeseries( - self, p_scaling_factor: float = 1.0, q_scaling_factor: float = 1.0 + self, p_scaling_factor: float = 1.0, q_scaling_factor: float = 1.0, + components: list | None = None ): """ Scales component time series by given factors. @@ -2221,15 +2222,19 @@ def scale_timeseries( 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. + components : list(str) + Components to scale. Possible options are "generators", "loads", and + "storage_units". Per default (if components is None), all are scaled. """ - attributes_type = ["generators", "loads", "storage_units"] + if components is None: + components = ["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: + for type in components: attribute = f"{type}_{suffix}" setattr(self, attribute, getattr(self, attribute) * scaling_factor) diff --git a/tests/network/test_timeseries.py b/tests/network/test_timeseries.py index 2c05eda49..9b12ec6a7 100644 --- a/tests/network/test_timeseries.py +++ b/tests/network/test_timeseries.py @@ -2458,6 +2458,8 @@ def test_resample(self): def test_scale_timeseries(self): self.edisgo.set_time_series_worst_case_analysis() + + # test scaling all time series edisgo_scaled = copy.deepcopy(self.edisgo) edisgo_scaled.timeseries.scale_timeseries( p_scaling_factor=0.5, q_scaling_factor=0.4 @@ -2488,6 +2490,37 @@ def test_scale_timeseries(self): self.edisgo.timeseries.storage_units_reactive_power * 0.4, ) + # test only scaling load time series + edisgo_scaled = copy.deepcopy(self.edisgo) + edisgo_scaled.timeseries.scale_timeseries( + p_scaling_factor=0.5, components=["loads"] + ) + + assert_frame_equal( + edisgo_scaled.timeseries.generators_active_power, + self.edisgo.timeseries.generators_active_power, + ) + assert_frame_equal( + edisgo_scaled.timeseries.generators_reactive_power, + self.edisgo.timeseries.generators_reactive_power, + ) + 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, + ) + assert_frame_equal( + edisgo_scaled.timeseries.storage_units_active_power, + self.edisgo.timeseries.storage_units_active_power, + ) + assert_frame_equal( + edisgo_scaled.timeseries.storage_units_reactive_power, + self.edisgo.timeseries.storage_units_reactive_power, + ) + class TestTimeSeriesRaw: @pytest.fixture(autouse=True) From 6a4b413491961414f5661588b4d9efa4f118ee81 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 21 Feb 2024 14:55:23 -0800 Subject: [PATCH 05/11] Make residual_load a function and enable returning it per feeder --- edisgo/network/timeseries.py | 84 ++++++++++++++++++++++---- examples/electromobility_example.ipynb | 4 +- tests/network/test_timeseries.py | 32 +++++++++- 3 files changed, 105 insertions(+), 15 deletions(-) diff --git a/edisgo/network/timeseries.py b/edisgo/network/timeseries.py index 7fb04c48e..731b24838 100644 --- a/edisgo/network/timeseries.py +++ b/edisgo/network/timeseries.py @@ -1787,7 +1787,7 @@ def _get_q_sign_and_power_factor_per_component( def residual_load(self, feeder=None, edisgo_obj=None): """ - Returns residual load in network. + Returns residual load in network or per feeder. Residual load for each time step is calculated from total load minus total generation minus storage active power (discharge is @@ -1796,17 +1796,79 @@ def residual_load(self, feeder=None, edisgo_obj=None): residual load here represents a feed-in case. Grid losses are not considered. + Parameters + ----------- + feeder : str + Specifies whether to determine the residual load for the entire grid + or per feeder. Feeder can be either the MV or grid feeder. + If set to None, which is the default, the residual load for the entire grid + is returned. + If set to "mv_feeder", the MV feeders the buses and lines are in are + determined. If mode is "grid_feeder", LV buses and lines are assigned the + LV feeder they are in and MV buses and lines are assigned the MV feeder + they are in. The residual load is in both cases returned per feeder. + Default: None. + Returns ------- - :pandas:`pandas.Series` - Series with residual load in MW. - - """ - return ( - self.loads_active_power.sum(axis=1) - - self.generators_active_power.sum(axis=1) - - self.storage_units_active_power.sum(axis=1) - ) + :pandas:`pandas.Series` or :pandas:`pandas.DataFrame` + Returns residual load per time step in MW. Index is a time index. + If `feeder` is None, a series with the residual load in the entire grid + is returned. If `feeder` is "mv_feeder" or "grid_feeder" + a dataframe is returned where the column names correspond to the feeder + name. As the station is not in any feeder, it is assigned the name + "station_node" and only components directly connected to the station are + considered in the calculation of its residual load. In case `feeder` is set + to "mv_grid" the "station_node" just includes the HV-MV station, but in case + `feeder` is set to "grid_feeder" all MV-LV stations as well as the HV-MV + station are included in the "station_node" (this could be changed at some + point). + + """ + if feeder is None: + return ( + self.loads_active_power.sum(axis=1) + - self.generators_active_power.sum(axis=1) + - self.storage_units_active_power.sum(axis=1) + ) + else: + # check if feeder was already assigned and if not, assign it + if not feeder in edisgo_obj.topology.buses_df.columns: + edisgo_obj.topology.assign_feeders(mode=feeder) + # iterate over components and add/subtract to/from residual load + residual_load = pd.DataFrame( + data=0.0, + columns=edisgo_obj.topology.buses_df.loc[:, feeder].unique(), + index=self.timeindex, + ) + sign_dict = { + "loads": 1.0, + "generators": -1.0, + "storage_units": -1.0, + } + for comp_type in sign_dict.keys(): + # groupby feeder + groupby_bus = pd.merge( + getattr(edisgo_obj.topology, f"{comp_type}_df"), + edisgo_obj.topology.buses_df, + how="left", left_on="bus", right_index=True, + ).groupby(feeder) + residual_load = residual_load.add( + sign_dict[comp_type] * pd.concat( + [ + pd.DataFrame( + { + k: getattr(self, f"{comp_type}_active_power").loc[ + :, v].sum(axis=1) + } + ) + for k, v in groupby_bus.groups.items() + ], + axis=1, + ), + fill_value=0.0, + ) + return residual_load @property def timesteps_load_feedin_case(self): @@ -1834,7 +1896,7 @@ def timesteps_load_feedin_case(self): """ - return self.residual_load.apply( + return self.residual_load().apply( lambda _: "feed-in_case" if _ < 0.0 else "load_case" ) diff --git a/examples/electromobility_example.ipynb b/examples/electromobility_example.ipynb index 5bc23ea1d..2581f1997 100644 --- a/examples/electromobility_example.ipynb +++ b/examples/electromobility_example.ipynb @@ -201,7 +201,7 @@ "\n", "edisgo.timeseries.generators_active_power.sum(axis=1).plot.line(ax=ax)\n", "edisgo.timeseries.loads_active_power.sum(axis=1).plot.line(ax=ax)\n", - "edisgo.timeseries.residual_load.plot.line(ax=ax)\n", + "edisgo.timeseries.residual_load().plot.line(ax=ax)\n", "\n", "ax.legend([\"Feed-in\", \"Demand\", \"Residual load\"])\n", "ax.set_ylabel(\"Power in MW\")\n", @@ -686,7 +686,7 @@ "# conduct grid analysis\n", "# to keep the calculation time low in this example, only time steps with maximum and \n", "# minimum residual load are analysed\n", - "residual_load = edisgo.timeseries.residual_load\n", + "residual_load = edisgo.timeseries.residual_load()\n", "worst_case_time_steps = pd.DatetimeIndex(\n", " [residual_load.idxmin(), residual_load.idxmax()]\n", ")\n", diff --git a/tests/network/test_timeseries.py b/tests/network/test_timeseries.py index 9b12ec6a7..4fbc949b0 100644 --- a/tests/network/test_timeseries.py +++ b/tests/network/test_timeseries.py @@ -2025,6 +2025,9 @@ def test_fixed_cosphi(self): def test_residual_load(self): self.edisgo.set_time_series_worst_case_analysis() + + # test residual load of whole grid + residual_load = self.edisgo.timeseries.residual_load() time_steps_load_case = self.edisgo.timeseries.timeindex_worst_cases[ self.edisgo.timeseries.timeindex_worst_cases.index.str.contains("load") ].values @@ -2033,14 +2036,39 @@ def test_residual_load(self): + self.edisgo.topology.storage_units_df.p_nom.sum() ) assert np.allclose( - self.edisgo.timeseries.residual_load.loc[time_steps_load_case], peak_load + residual_load.loc[time_steps_load_case], peak_load ) time_steps_feedin_case = self.edisgo.timeseries.timeindex_worst_cases[ self.edisgo.timeseries.timeindex_worst_cases.index.str.contains("feed") ].values assert ( - self.edisgo.timeseries.residual_load.loc[time_steps_feedin_case] < 0 + residual_load.loc[time_steps_feedin_case] < 0 ).all() + # test residual load per MV grid feeder + residual_load_feeder = self.edisgo.timeseries.residual_load( + feeder="mv_feeder", edisgo_obj=self.edisgo + ) + assert np.allclose( + residual_load, residual_load_feeder.sum(axis=1) + ) + assert residual_load_feeder.shape == (4, 6) + assert np.allclose( + residual_load_feeder["BusBar_lac_1"], [0.31, 0.31, -3.00350, -3.01900] + ) + assert np.allclose( + residual_load_feeder["station_node"], [0.4, 0.4, -0.4, -0.4] + ) + # test residual load per MV and LV feeder + residual_load_feeder = self.edisgo.timeseries.residual_load( + feeder="grid_feeder", edisgo_obj=self.edisgo + ) + assert np.allclose( + self.edisgo.timeseries.residual_load(), residual_load_feeder.sum(axis=1) + ) + assert residual_load_feeder.shape == (4, 28) + assert np.allclose( + residual_load_feeder["Bus_BranchTee_LVGrid_1_11"], [0.002794, 0.002794, -0.0378309, -0.0379706] + ) def test_timesteps_load_feedin_case(self): self.edisgo.set_time_series_worst_case_analysis() From 58bdc073034c632219912c177b30a5710208c3a6 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 21 Feb 2024 15:14:37 -0800 Subject: [PATCH 06/11] Add n-1 for line checks --- edisgo/flex_opt/check_tech_constraints.py | 30 ++++++++++++------- tests/flex_opt/test_check_tech_constraints.py | 19 ++++++++++-- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/edisgo/flex_opt/check_tech_constraints.py b/edisgo/flex_opt/check_tech_constraints.py index 447543572..d0f1de56a 100644 --- a/edisgo/flex_opt/check_tech_constraints.py +++ b/edisgo/flex_opt/check_tech_constraints.py @@ -194,7 +194,21 @@ def lines_allowed_load(edisgo_obj, lines=None, n_minus_one=False): Allowed loading is determined based on allowed load factors for feed-in and load cases that are defined in the config file 'config_grid_expansion' in - section 'grid_expansion_load_factors'. + section 'grid_expansion_load_factors' or in case of n-1 security + 'grid_expansion_load_factors_n_minus_one'. + + n-1 security load factors only apply to lines in rings, wherefore + 'grid_expansion_load_factors_n_minus_one' are only used for those lines. + For all other lines, 'grid_expansion_load_factors' are always used. + + Whether load factors for the load or feed-in case apply is determined using the + residual load in the grid. In case different load factors are used in the different + cases this may not be the best way, as it could be the case that in one feeder + the feed-in is higher than the load and in another feeder the load is higher than + the feed-in. + If you want to check for n-1 and have different allowed load factors for the load + and feed-in case it is suggested to use the reinforcement function + :func:`~.flex_opt.reinforce_grid.reinforce_for_n_minus_one`. Parameters ---------- @@ -208,7 +222,8 @@ def lines_allowed_load(edisgo_obj, lines=None, n_minus_one=False): section 'grid_expansion_load_factors' are used. This is the default. In case it is set to True, allowed load factors defined in the config file 'config_grid_expansion' in section 'grid_expansion_load_factors_n_minus_one' - are used. This case is currently not implemented. + are used for lines in rings. For all other lines, 'grid_expansion_load_factors' + are used. Returns ------- @@ -221,7 +236,7 @@ def lines_allowed_load(edisgo_obj, lines=None, n_minus_one=False): """ allowed_load_lv = _lines_allowed_load_voltage_level( - edisgo_obj, voltage_level="lv", n_minus_one=n_minus_one + edisgo_obj, voltage_level="lv", n_minus_one=False ) allowed_load_mv = _lines_allowed_load_voltage_level( edisgo_obj, voltage_level="mv", n_minus_one=n_minus_one @@ -244,13 +259,8 @@ def _lines_allowed_load_voltage_level(edisgo_obj, voltage_level, n_minus_one=Fal Grid level, allowed line load is returned for. Possible options are "mv" or "lv". n_minus_one : bool - Determines which allowed load factors to use. In case it is set to False, - allowed load factors defined in the config file 'config_grid_expansion' in - section 'grid_expansion_load_factors' are used. This is the default. - In case it is set to True, allowed load factors defined in the config file - 'config_grid_expansion' in section 'grid_expansion_load_factors_n_minus_one' - are used for lines in rings. For all other lines, 'grid_expansion_load_factors' - are used. + Determines which allowed load factors to use. See :py:attr:`~lines_allowed_load` + for more information. Returns ------- diff --git a/tests/flex_opt/test_check_tech_constraints.py b/tests/flex_opt/test_check_tech_constraints.py index 6f2e20da2..ec9967670 100644 --- a/tests/flex_opt/test_check_tech_constraints.py +++ b/tests/flex_opt/test_check_tech_constraints.py @@ -25,6 +25,9 @@ def run_power_flow(self): def test_mv_line_max_relative_overload(self): # implicitly checks function _line_overload + # Note: n-1 cannot be checked easily, because load case is never more severe + # than feed-in case, even when load time series are scaled up. It just leads + # to a non-convergence at some point. df = check_tech_constraints.mv_line_max_relative_overload(self.edisgo) # check shape of dataframe @@ -170,12 +173,22 @@ def test_lines_relative_load(self): df.at[self.timesteps[0], "Line_10005"], 0.00142 / 7.27461, atol=1e-5 ) - # check with specifying lines + # check with specifying lines and n-1 is True df = check_tech_constraints.lines_relative_load( - self.edisgo, lines=["Line_10005", "Line_50000002"] + self.edisgo, lines=["Line_10005", "Line_10024", "Line_50000002"], + n_minus_one=True ) # check shape of dataframe - assert (4, 2) == df.shape + assert (4, 3) == df.shape + # check in load case + assert np.isclose( + df.at[self.timesteps[0], "Line_10005"], + 0.00142 / (7.27461 / 2), atol=1e-5 + ) + assert np.isclose( + df.at[self.timesteps[0], "Line_10024"], + 0.06931 / 7.27461339178928, atol=1e-5 + ) def test_hv_mv_station_max_overload(self): # implicitly checks function _station_overload From 3a179cc0e0eaf47b49203c0192ec2f13cc12d3e0 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 21 Feb 2024 15:43:30 -0800 Subject: [PATCH 07/11] Allow checking for n-1 of stations --- edisgo/flex_opt/check_tech_constraints.py | 80 ++++++++++++++----- tests/flex_opt/test_check_tech_constraints.py | 21 +++++ 2 files changed, 81 insertions(+), 20 deletions(-) diff --git a/edisgo/flex_opt/check_tech_constraints.py b/edisgo/flex_opt/check_tech_constraints.py index d0f1de56a..ef790563a 100644 --- a/edisgo/flex_opt/check_tech_constraints.py +++ b/edisgo/flex_opt/check_tech_constraints.py @@ -19,6 +19,7 @@ def mv_line_max_relative_overload(edisgo_obj, n_minus_one=False): n_minus_one : bool Determines which allowed load factors to use (see :py:attr:`~lines_allowed_load` for more information). + Default: False. Returns ------- @@ -71,6 +72,7 @@ def lv_line_max_relative_overload(edisgo_obj, n_minus_one=False, lv_grid_id=None 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. + Default: False. Returns ------- @@ -224,6 +226,7 @@ def lines_allowed_load(edisgo_obj, lines=None, n_minus_one=False): 'config_grid_expansion' in section 'grid_expansion_load_factors_n_minus_one' are used for lines in rings. For all other lines, 'grid_expansion_load_factors' are used. + Default: False. Returns ------- @@ -380,13 +383,17 @@ def lines_relative_load(edisgo_obj, lines=None, n_minus_one=False): return loading / allowed_loading -def hv_mv_station_max_overload(edisgo_obj): +def hv_mv_station_max_overload(edisgo_obj, n_minus_one=False): """ Checks for over-loading of HV/MV station. Parameters ---------- edisgo_obj : :class:`~.EDisGo` + n_minus_one : bool + Determines which allowed load factors to use. See + :py:attr:`~stations_allowed_load` for more information. + Default: False. Returns ------- @@ -403,10 +410,11 @@ def hv_mv_station_max_overload(edisgo_obj): ----- Over-load is determined based on allowed load factors for feed-in and load cases that are defined in the config file 'config_grid_expansion' in - section 'grid_expansion_load_factors'. + section 'grid_expansion_load_factors' or in case of n-1 security + 'grid_expansion_load_factors_n_minus_one'. """ - crit_stations = _station_max_overload(edisgo_obj, edisgo_obj.topology.mv_grid) + crit_stations = _station_max_overload(edisgo_obj, edisgo_obj.topology.mv_grid, n_minus_one=n_minus_one) if not crit_stations.empty: logger.debug("==> HV/MV station has load issues.") else: @@ -415,7 +423,7 @@ def hv_mv_station_max_overload(edisgo_obj): return crit_stations -def mv_lv_station_max_overload(edisgo_obj, lv_grid_id=None): +def mv_lv_station_max_overload(edisgo_obj, lv_grid_id=None, n_minus_one=False): """ Checks for over-loading of MV/LV stations. @@ -425,6 +433,10 @@ def mv_lv_station_max_overload(edisgo_obj, lv_grid_id=None): 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. + n_minus_one : bool + Determines which allowed load factors to use. See + :py:attr:`~stations_allowed_load` for more information. + Default: False. Returns ------- @@ -441,7 +453,8 @@ def mv_lv_station_max_overload(edisgo_obj, lv_grid_id=None): ----- Over-load is determined based on allowed load factors for feed-in and load cases that are defined in the config file 'config_grid_expansion' in - section 'grid_expansion_load_factors'. + section 'grid_expansion_load_factors' or in case of n-1 security + 'grid_expansion_load_factors_n_minus_one'. """ crit_stations = pd.DataFrame(dtype=float) @@ -454,7 +467,7 @@ def mv_lv_station_max_overload(edisgo_obj, lv_grid_id=None): crit_stations = pd.concat( [ crit_stations, - _station_max_overload(edisgo_obj, lv_grid), + _station_max_overload(edisgo_obj, lv_grid, n_minus_one=n_minus_one), ] ) if not crit_stations.empty: @@ -469,7 +482,7 @@ def mv_lv_station_max_overload(edisgo_obj, lv_grid_id=None): return crit_stations -def _station_max_overload(edisgo_obj, grid): +def _station_max_overload(edisgo_obj, grid, n_minus_one=False): """ Checks for over-loading of stations. @@ -477,6 +490,10 @@ def _station_max_overload(edisgo_obj, grid): ---------- edisgo_obj : :class:`~.EDisGo` grid : :class:`~.network.grids.LVGrid` or :class:`~.network.grids.MVGrid` + n_minus_one : bool + Determines which allowed load factors to use. See + :py:attr:`~stations_allowed_load` for more information. + Default: False. Returns ------- @@ -501,7 +518,7 @@ def _station_max_overload(edisgo_obj, grid): s_station_pfa = _station_load(edisgo_obj, grid) # get maximum allowed apparent power of station in each time step - s_station_allowed = _station_allowed_load(edisgo_obj, grid) + s_station_allowed = _station_allowed_load(edisgo_obj, grid, n_minus_one=n_minus_one) # calculate residual apparent power (if negative, station is over-loaded) s_res = s_station_allowed - s_station_pfa @@ -583,10 +600,9 @@ def _station_load(edisgo_obj, grid): raise ValueError("Inserted grid is invalid.") -def _station_allowed_load(edisgo_obj, grid): +def _station_allowed_load(edisgo_obj, grid, n_minus_one=False): """ - Returns allowed loading of grid's station to the overlying voltage level per time - step in MVA. + Returns allowed loading of grid's station per time step in MVA. Allowed loading considers allowed load factors in heavy load flow case ('load case') and reverse power flow case ('feed-in case') that are defined in the config file @@ -597,6 +613,9 @@ def _station_allowed_load(edisgo_obj, grid): edisgo_obj : :class:`~.EDisGo` grid : :class:`~.network.grids.LVGrid` or :class:`~.network.grids.MVGrid` Grid to get allowed station loading for. + n_minus_one : bool + Determines which allowed load factors to use. See + :py:attr:`~stations_allowed_load` for more information. Returns ------- @@ -620,10 +639,12 @@ def _station_allowed_load(edisgo_obj, grid): # get maximum allowed apparent power of station in each time step s_station = sum(transformers_df.s_nom) + if n_minus_one is True: + which = "grid_expansion_load_factors_n_minus_one" + else: + which = "grid_expansion_load_factors" load_factor = edisgo_obj.timeseries.timesteps_load_feedin_case.apply( - lambda _: edisgo_obj.config["grid_expansion_load_factors"][ - f"{voltage_level}_{_}_transformer" - ] + lambda _: edisgo_obj.config[which][f"{voltage_level}_{_}_transformer"] ) return pd.DataFrame( @@ -631,14 +652,24 @@ def _station_allowed_load(edisgo_obj, grid): ) -def stations_allowed_load(edisgo_obj, grids=None): +def stations_allowed_load(edisgo_obj, grids=None, n_minus_one=False): """ - Returns allowed loading of specified grids stations to the overlying voltage level + Returns allowed loading of specified grid's stations to the overlying voltage level per time step in MVA. Allowed loading considers allowed load factors in heavy load flow case ('load case') and reverse power flow case ('feed-in case') that are defined in the config file - 'config_grid_expansion' in section 'grid_expansion_load_factors'. + 'config_grid_expansion' in section 'grid_expansion_load_factors' or in case of n-1 + security 'grid_expansion_load_factors_n_minus_one'. + + Whether load factors for the load or feed-in case apply is determined using the + residual load in the grid. In case different load factors are used in the different + cases for MV-LV stations this may not be the best way, as it could be the case that + the residual load in the entire grid is positive while it is negative in some LV + grids. + If you want to check for n-1 and have different allowed load factors for the load + and feed-in case it is suggested to use the reinforcement function + :func:`~.flex_opt.reinforce_grid.reinforce_for_n_minus_one`. Parameters ---------- @@ -646,6 +677,14 @@ def stations_allowed_load(edisgo_obj, grids=None): grids : list(:class:`~.network.grids.Grid`) List of MV and LV grids to get allowed station loading for. Per default allowed loading is returned for all stations in the network. Default: None. + n_minus_one : bool + Determines which allowed load factors to use. In case it is set to False, + allowed load factors defined in the config file 'config_grid_expansion' in + section 'grid_expansion_load_factors' are used. This is the default. + In case it is set to True, allowed load factors defined in the config file + 'config_grid_expansion' in section 'grid_expansion_load_factors_n_minus_one' + are used. + Default: False. Returns ------- @@ -663,12 +702,12 @@ def stations_allowed_load(edisgo_obj, grids=None): allowed_loading = pd.DataFrame() for grid in grids: allowed_loading = pd.concat( - [allowed_loading, _station_allowed_load(edisgo_obj, grid)], axis=1 + [allowed_loading, _station_allowed_load(edisgo_obj, grid, n_minus_one=n_minus_one)], axis=1 ) return allowed_loading -def stations_relative_load(edisgo_obj, grids=None): +def stations_relative_load(edisgo_obj, grids=None, n_minus_one=False): """ Returns relative loading of specified grids stations to the overlying voltage level per time step in p.u.. @@ -729,6 +768,7 @@ def components_relative_load(edisgo_obj, n_minus_one=False): n_minus_one : bool Determines which allowed load factors to use. See :py:attr:`~lines_allowed_load` for more information. + ToDo stations Returns ------- @@ -743,7 +783,7 @@ def components_relative_load(edisgo_obj, n_minus_one=False): :attr:`~.network.grids.Grid.station_name`). """ - stations_rel_load = stations_relative_load(edisgo_obj) + stations_rel_load = stations_relative_load(edisgo_obj, n_minus_one=n_minus_one) lines_rel_load = lines_relative_load( edisgo_obj, lines=None, n_minus_one=n_minus_one ) diff --git a/tests/flex_opt/test_check_tech_constraints.py b/tests/flex_opt/test_check_tech_constraints.py index ec9967670..a7c30ef0d 100644 --- a/tests/flex_opt/test_check_tech_constraints.py +++ b/tests/flex_opt/test_check_tech_constraints.py @@ -209,6 +209,17 @@ def test_hv_mv_station_max_overload(self): ) assert df.at["MVGrid_1_station", "time_index"] == self.timesteps[0] + # check n-1 + df = check_tech_constraints.hv_mv_station_max_overload(self.edisgo, n_minus_one=True) + # check shape of dataframe + assert (1, 3) == df.shape + # check missing transformer capacity + assert np.isclose( + df.at["MVGrid_1_station", "s_missing"], + (np.hypot(30, 30) - 20), + ) + assert df.at["MVGrid_1_station", "time_index"] == self.timesteps[0] + def test_mv_lv_station_max_overload(self): # implicitly checks function _station_overload @@ -291,6 +302,16 @@ def test__station_allowed_load(self): ] assert np.isclose(40.0, df.loc[feed_in_cases.values].values).all() + # check MV grid n-1 + df = check_tech_constraints._station_allowed_load( + self.edisgo, grid, n_minus_one=True + ) + # check shape of dataframe + assert (4, 1) == df.shape + # check values + assert np.isclose(20.0, df.loc[load_cases.values].values).all() + assert np.isclose(40.0, df.loc[feed_in_cases.values].values).all() + def test_stations_allowed_load(self): # check without specifying a grid df = check_tech_constraints.stations_allowed_load(self.edisgo) From 1958629d50662d26508ff0a36b1dbc28056928b8 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 21 Feb 2024 15:50:44 -0800 Subject: [PATCH 08/11] Run pre-commit hooks --- edisgo/flex_opt/check_tech_constraints.py | 10 ++++++++-- edisgo/network/timeseries.py | 20 ++++++++++++------- tests/flex_opt/test_check_tech_constraints.py | 15 ++++++++------ tests/network/test_timeseries.py | 19 ++++++------------ 4 files changed, 36 insertions(+), 28 deletions(-) diff --git a/edisgo/flex_opt/check_tech_constraints.py b/edisgo/flex_opt/check_tech_constraints.py index ef790563a..0f372005b 100644 --- a/edisgo/flex_opt/check_tech_constraints.py +++ b/edisgo/flex_opt/check_tech_constraints.py @@ -414,7 +414,9 @@ def hv_mv_station_max_overload(edisgo_obj, n_minus_one=False): 'grid_expansion_load_factors_n_minus_one'. """ - crit_stations = _station_max_overload(edisgo_obj, edisgo_obj.topology.mv_grid, n_minus_one=n_minus_one) + crit_stations = _station_max_overload( + edisgo_obj, edisgo_obj.topology.mv_grid, n_minus_one=n_minus_one + ) if not crit_stations.empty: logger.debug("==> HV/MV station has load issues.") else: @@ -702,7 +704,11 @@ def stations_allowed_load(edisgo_obj, grids=None, n_minus_one=False): allowed_loading = pd.DataFrame() for grid in grids: allowed_loading = pd.concat( - [allowed_loading, _station_allowed_load(edisgo_obj, grid, n_minus_one=n_minus_one)], axis=1 + [ + allowed_loading, + _station_allowed_load(edisgo_obj, grid, n_minus_one=n_minus_one), + ], + axis=1, ) return allowed_loading diff --git a/edisgo/network/timeseries.py b/edisgo/network/timeseries.py index 731b24838..b53856f13 100644 --- a/edisgo/network/timeseries.py +++ b/edisgo/network/timeseries.py @@ -1833,7 +1833,7 @@ def residual_load(self, feeder=None, edisgo_obj=None): ) else: # check if feeder was already assigned and if not, assign it - if not feeder in edisgo_obj.topology.buses_df.columns: + if feeder not in edisgo_obj.topology.buses_df.columns: edisgo_obj.topology.assign_feeders(mode=feeder) # iterate over components and add/subtract to/from residual load residual_load = pd.DataFrame( @@ -1851,15 +1851,19 @@ def residual_load(self, feeder=None, edisgo_obj=None): groupby_bus = pd.merge( getattr(edisgo_obj.topology, f"{comp_type}_df"), edisgo_obj.topology.buses_df, - how="left", left_on="bus", right_index=True, + how="left", + left_on="bus", + right_index=True, ).groupby(feeder) residual_load = residual_load.add( - sign_dict[comp_type] * pd.concat( + sign_dict[comp_type] + * pd.concat( [ pd.DataFrame( { - k: getattr(self, f"{comp_type}_active_power").loc[ - :, v].sum(axis=1) + k: getattr(self, f"{comp_type}_active_power") + .loc[:, v] + .sum(axis=1) } ) for k, v in groupby_bus.groups.items() @@ -2266,8 +2270,10 @@ def resample(self, method: str = "ffill", freq: str | pd.Timedelta = "15min"): self._timeindex = index def scale_timeseries( - self, p_scaling_factor: float = 1.0, q_scaling_factor: float = 1.0, - components: list | None = None + self, + p_scaling_factor: float = 1.0, + q_scaling_factor: float = 1.0, + components: list | None = None, ): """ Scales component time series by given factors. diff --git a/tests/flex_opt/test_check_tech_constraints.py b/tests/flex_opt/test_check_tech_constraints.py index a7c30ef0d..ab177bda4 100644 --- a/tests/flex_opt/test_check_tech_constraints.py +++ b/tests/flex_opt/test_check_tech_constraints.py @@ -175,19 +175,20 @@ def test_lines_relative_load(self): # check with specifying lines and n-1 is True df = check_tech_constraints.lines_relative_load( - self.edisgo, lines=["Line_10005", "Line_10024", "Line_50000002"], - n_minus_one=True + self.edisgo, + lines=["Line_10005", "Line_10024", "Line_50000002"], + n_minus_one=True, ) # check shape of dataframe assert (4, 3) == df.shape # check in load case assert np.isclose( - df.at[self.timesteps[0], "Line_10005"], - 0.00142 / (7.27461 / 2), atol=1e-5 + df.at[self.timesteps[0], "Line_10005"], 0.00142 / (7.27461 / 2), atol=1e-5 ) assert np.isclose( df.at[self.timesteps[0], "Line_10024"], - 0.06931 / 7.27461339178928, atol=1e-5 + 0.06931 / 7.27461339178928, + atol=1e-5, ) def test_hv_mv_station_max_overload(self): @@ -210,7 +211,9 @@ def test_hv_mv_station_max_overload(self): assert df.at["MVGrid_1_station", "time_index"] == self.timesteps[0] # check n-1 - df = check_tech_constraints.hv_mv_station_max_overload(self.edisgo, n_minus_one=True) + df = check_tech_constraints.hv_mv_station_max_overload( + self.edisgo, n_minus_one=True + ) # check shape of dataframe assert (1, 3) == df.shape # check missing transformer capacity diff --git a/tests/network/test_timeseries.py b/tests/network/test_timeseries.py index 4fbc949b0..5fc15ea48 100644 --- a/tests/network/test_timeseries.py +++ b/tests/network/test_timeseries.py @@ -2035,29 +2035,21 @@ def test_residual_load(self): self.edisgo.topology.loads_df.p_set.sum() + self.edisgo.topology.storage_units_df.p_nom.sum() ) - assert np.allclose( - residual_load.loc[time_steps_load_case], peak_load - ) + assert np.allclose(residual_load.loc[time_steps_load_case], peak_load) time_steps_feedin_case = self.edisgo.timeseries.timeindex_worst_cases[ self.edisgo.timeseries.timeindex_worst_cases.index.str.contains("feed") ].values - assert ( - residual_load.loc[time_steps_feedin_case] < 0 - ).all() + assert (residual_load.loc[time_steps_feedin_case] < 0).all() # test residual load per MV grid feeder residual_load_feeder = self.edisgo.timeseries.residual_load( feeder="mv_feeder", edisgo_obj=self.edisgo ) - assert np.allclose( - residual_load, residual_load_feeder.sum(axis=1) - ) + assert np.allclose(residual_load, residual_load_feeder.sum(axis=1)) assert residual_load_feeder.shape == (4, 6) assert np.allclose( residual_load_feeder["BusBar_lac_1"], [0.31, 0.31, -3.00350, -3.01900] ) - assert np.allclose( - residual_load_feeder["station_node"], [0.4, 0.4, -0.4, -0.4] - ) + assert np.allclose(residual_load_feeder["station_node"], [0.4, 0.4, -0.4, -0.4]) # test residual load per MV and LV feeder residual_load_feeder = self.edisgo.timeseries.residual_load( feeder="grid_feeder", edisgo_obj=self.edisgo @@ -2067,7 +2059,8 @@ def test_residual_load(self): ) assert residual_load_feeder.shape == (4, 28) assert np.allclose( - residual_load_feeder["Bus_BranchTee_LVGrid_1_11"], [0.002794, 0.002794, -0.0378309, -0.0379706] + residual_load_feeder["Bus_BranchTee_LVGrid_1_11"], + [0.002794, 0.002794, -0.0378309, -0.0379706], ) def test_timesteps_load_feedin_case(self): From b15dc001266eb4ddb46318dd0e943c5fc9e69673 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 21 Feb 2024 18:57:16 -0800 Subject: [PATCH 09/11] Add n-1 when calculating relative load of all components --- edisgo/flex_opt/check_tech_constraints.py | 11 ++++++---- tests/flex_opt/test_check_tech_constraints.py | 20 +++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/edisgo/flex_opt/check_tech_constraints.py b/edisgo/flex_opt/check_tech_constraints.py index 0f372005b..d3de22215 100644 --- a/edisgo/flex_opt/check_tech_constraints.py +++ b/edisgo/flex_opt/check_tech_constraints.py @@ -730,6 +730,10 @@ def stations_relative_load(edisgo_obj, grids=None, n_minus_one=False): List of MV and LV grids to get relative station loading for. Per default relative loading is returned for all stations in the network that were included in the power flow analysis. Default: None. + n_minus_one : bool + Determines which allowed load factors to use. See + :py:attr:`~stations_allowed_load` for more information. + Default: False. Returns ------- @@ -745,7 +749,7 @@ def stations_relative_load(edisgo_obj, grids=None, n_minus_one=False): grids = edisgo_obj.topology.grids # get allowed loading - allowed_loading = stations_allowed_load(edisgo_obj, grids) + allowed_loading = stations_allowed_load(edisgo_obj, grids, n_minus_one=n_minus_one) # get loading from power flow results loading = pd.DataFrame() @@ -763,7 +767,7 @@ def components_relative_load(edisgo_obj, n_minus_one=False): """ Returns relative loading of all lines and stations included in power flow analysis. - The component's relative loading is determined by dividing the stations loading + The component's relative loading is determined by dividing the loading (from power flow analysis) by the allowed loading (considering allowed load factors in heavy load flow case ('load case') and reverse power flow case ('feed-in case') from config files). @@ -773,8 +777,7 @@ def components_relative_load(edisgo_obj, n_minus_one=False): edisgo_obj : :class:`~.EDisGo` n_minus_one : bool Determines which allowed load factors to use. See :py:attr:`~lines_allowed_load` - for more information. - ToDo stations + and :py:attr:`~stations_allowed_load` for more information. Returns ------- diff --git a/tests/flex_opt/test_check_tech_constraints.py b/tests/flex_opt/test_check_tech_constraints.py index ab177bda4..6987ef8b2 100644 --- a/tests/flex_opt/test_check_tech_constraints.py +++ b/tests/flex_opt/test_check_tech_constraints.py @@ -406,6 +406,26 @@ def test_components_relative_load(self): df.at[self.timesteps[0], "Line_10005"], 0.00142 / 7.27461, atol=1e-5 ) + # check with power flow results available for all components and n-1 + df = check_tech_constraints.components_relative_load( + self.edisgo, n_minus_one=True + ) + # check shape of dataframe + assert (4, 142) == df.shape + # check values + load_cases = self.edisgo.timeseries.timeindex_worst_cases[ + self.edisgo.timeseries.timeindex_worst_cases.index.str.contains("load") + ] + assert np.isclose( + 0.02853, df.loc[load_cases.values, "LVGrid_4_station"].values, atol=1e-5 + ).all() + assert np.isclose( + df.at[self.timesteps[0], "Line_10005"], 0.00142 / 7.27461 * 2, atol=1e-5 + ) + assert np.isclose( + 0.03427 * 2, df.loc[load_cases.values, "MVGrid_1_station"].values, atol=1e-5 + ).all() + # check with power flow results not available for all components self.edisgo.analyze(mode="mvlv") df = check_tech_constraints.components_relative_load(self.edisgo) From 56e8907cb9759c0adef29acf3423b6c0d955d156 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 21 Feb 2024 19:08:46 -0800 Subject: [PATCH 10/11] Minor changes for n-1 --- edisgo/flex_opt/check_tech_constraints.py | 7 ++++++- tests/flex_opt/test_check_tech_constraints.py | 9 +++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/edisgo/flex_opt/check_tech_constraints.py b/edisgo/flex_opt/check_tech_constraints.py index d3de22215..37b5c2baf 100644 --- a/edisgo/flex_opt/check_tech_constraints.py +++ b/edisgo/flex_opt/check_tech_constraints.py @@ -69,10 +69,10 @@ def lv_line_max_relative_overload(edisgo_obj, n_minus_one=False, lv_grid_id=None n_minus_one : bool Determines which allowed load factors to use (see :py:attr:`~lines_allowed_load` for more information). + Default: False. 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. - Default: False. Returns ------- @@ -126,6 +126,7 @@ def _line_max_relative_overload( n_minus_one : bool Determines which allowed load factors to use. See :py:attr:`~lines_allowed_load` for more information. + Default: False. 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 @@ -264,6 +265,7 @@ def _lines_allowed_load_voltage_level(edisgo_obj, voltage_level, n_minus_one=Fal n_minus_one : bool Determines which allowed load factors to use. See :py:attr:`~lines_allowed_load` for more information. + Default: False. Returns ------- @@ -358,6 +360,7 @@ def lines_relative_load(edisgo_obj, lines=None, n_minus_one=False): n_minus_one : bool Determines which allowed load factors to use. See :py:attr:`~lines_allowed_load` for more information. + Default: False. Returns -------- @@ -618,6 +621,7 @@ def _station_allowed_load(edisgo_obj, grid, n_minus_one=False): n_minus_one : bool Determines which allowed load factors to use. See :py:attr:`~stations_allowed_load` for more information. + Default: False. Returns ------- @@ -778,6 +782,7 @@ def components_relative_load(edisgo_obj, n_minus_one=False): n_minus_one : bool Determines which allowed load factors to use. See :py:attr:`~lines_allowed_load` and :py:attr:`~stations_allowed_load` for more information. + Default: False. Returns ------- diff --git a/tests/flex_opt/test_check_tech_constraints.py b/tests/flex_opt/test_check_tech_constraints.py index 6987ef8b2..2bed52c36 100644 --- a/tests/flex_opt/test_check_tech_constraints.py +++ b/tests/flex_opt/test_check_tech_constraints.py @@ -94,12 +94,17 @@ def test_lines_allowed_load(self): 0.08521689973238901, ) - # check with specifying lines + # check with specifying lines and n-1 df = check_tech_constraints.lines_allowed_load( - self.edisgo, lines=["Line_10005", "Line_50000002"] + self.edisgo, lines=["Line_10005", "Line_50000002"], n_minus_one=True ) # check shape of dataframe assert (4, 2) == df.shape + # check value of line in ring + assert np.isclose( + df.at[self.timesteps[0], "Line_10005"], + 7.274613391789284 / 2, + ) def test__lines_allowed_load_voltage_level(self): # check for MV From 0b3f1f90e31ffcad1a1e0492230ce19adfda9a48 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 13 Sep 2024 08:09:47 +0200 Subject: [PATCH 11/11] Start implementing n-1 security --- edisgo/edisgo.py | 1 + edisgo/flex_opt/reinforce_grid.py | 119 +++++++++++++++++++++++++----- 2 files changed, 101 insertions(+), 19 deletions(-) diff --git a/edisgo/edisgo.py b/edisgo/edisgo.py index f0edd32b6..a8e3d2cf3 100755 --- a/edisgo/edisgo.py +++ b/edisgo/edisgo.py @@ -1329,6 +1329,7 @@ def reinforce( changes, etc. """ + # ToDo n-1 hier einbauen? Ansonsten Info dass es in extra Funktion gemacht wird if copy_grid: edisgo_obj = copy.deepcopy(self) else: diff --git a/edisgo/flex_opt/reinforce_grid.py b/edisgo/flex_opt/reinforce_grid.py index 89a0a6515..9632d0979 100644 --- a/edisgo/flex_opt/reinforce_grid.py +++ b/edisgo/flex_opt/reinforce_grid.py @@ -1,5 +1,6 @@ from __future__ import annotations +import copy import datetime import logging @@ -12,6 +13,7 @@ from edisgo.flex_opt import exceptions, reinforce_measures from edisgo.flex_opt.costs import grid_expansion_costs from edisgo.flex_opt.reinforce_measures import separate_lv_grid +from edisgo.network.timeseries import TimeSeriesRaw from edisgo.tools import tools from edisgo.tools.temporal_complexity_reduction import get_most_critical_time_steps @@ -63,6 +65,7 @@ def reinforce_grid( 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 + ToDo adapt 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. @@ -114,9 +117,6 @@ def reinforce_grid( reinforcement is conducted. """ - if n_minus_one is True: - raise NotImplementedError("n-1 security can currently not be checked.") - # check if provided mode is valid if mode and mode not in ["mv", "mvlv", "lv"]: raise ValueError(f"Provided mode {mode} is not a valid mode.") @@ -182,24 +182,28 @@ def reinforce_grid( overloaded_mv_station = ( pd.DataFrame(dtype=float) if mode == "lv" or kwargs.get("skip_mv_reinforcement", False) - else checks.hv_mv_station_max_overload(edisgo) + else checks.hv_mv_station_max_overload(edisgo, n_minus_one=n_minus_one) ) 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 = checks.mv_lv_station_max_overload( + edisgo, n_minus_one=n_minus_one + ) logger.debug("==> Check line load.") crit_lines = ( pd.DataFrame(dtype=float) if mode == "lv" or kwargs.get("skip_mv_reinforcement", False) - else checks.mv_line_max_relative_overload(edisgo) + else checks.mv_line_max_relative_overload(edisgo, n_minus_one=n_minus_one) ) if not mode or mode == "lv": crit_lines = pd.concat( [ crit_lines, - checks.lv_line_max_relative_overload(edisgo, lv_grid_id=lv_grid_id), + checks.lv_line_max_relative_overload( + edisgo, lv_grid_id=lv_grid_id, n_minus_one=n_minus_one + ), ] ) @@ -263,22 +267,26 @@ def reinforce_grid( overloaded_mv_station = ( pd.DataFrame(dtype=float) if mode == "lv" or kwargs.get("skip_mv_reinforcement", False) - else checks.hv_mv_station_max_overload(edisgo) + else checks.hv_mv_station_max_overload(edisgo, n_minus_one=n_minus_one) ) if mode != "mv" and (not lv_grid_id): - overloaded_lv_stations = checks.mv_lv_station_max_overload(edisgo) + overloaded_lv_stations = checks.mv_lv_station_max_overload( + edisgo, n_minus_one=n_minus_one + ) logger.debug("==> Recheck line load.") crit_lines = ( pd.DataFrame(dtype=float) if mode == "lv" or kwargs.get("skip_mv_reinforcement", False) - else checks.mv_line_max_relative_overload(edisgo) + else checks.mv_line_max_relative_overload(edisgo, n_minus_one=n_minus_one) ) if not mode or mode == "lv": crit_lines = pd.concat( [ crit_lines, - checks.lv_line_max_relative_overload(edisgo, lv_grid_id=lv_grid_id), + checks.lv_line_max_relative_overload( + edisgo, lv_grid_id=lv_grid_id, n_minus_one=n_minus_one + ), ] ) @@ -506,24 +514,28 @@ def reinforce_grid( overloaded_mv_station = ( pd.DataFrame(dtype=float) if mode == "lv" or kwargs.get("skip_mv_reinforcement", False) - else checks.hv_mv_station_max_overload(edisgo) + else checks.hv_mv_station_max_overload(edisgo, n_minus_one=n_minus_one) ) 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 = checks.mv_lv_station_max_overload( + edisgo, n_minus_one=n_minus_one + ) logger.debug("==> Recheck line load.") crit_lines = ( pd.DataFrame(dtype=float) if mode == "lv" or kwargs.get("skip_mv_reinforcement", False) - else checks.mv_line_max_relative_overload(edisgo) + else checks.mv_line_max_relative_overload(edisgo, n_minus_one=n_minus_one) ) if not mode or mode == "lv": crit_lines = pd.concat( [ crit_lines, - checks.lv_line_max_relative_overload(edisgo, lv_grid_id=lv_grid_id), + checks.lv_line_max_relative_overload( + edisgo, lv_grid_id=lv_grid_id, n_minus_one=n_minus_one + ), ] ) @@ -587,22 +599,26 @@ def reinforce_grid( overloaded_mv_station = ( pd.DataFrame(dtype=float) if mode == "lv" or kwargs.get("skip_mv_reinforcement", False) - else checks.hv_mv_station_max_overload(edisgo) + else checks.hv_mv_station_max_overload(edisgo, n_minus_one=n_minus_one) ) if mode != "mv" and (not lv_grid_id): - overloaded_lv_stations = checks.mv_lv_station_max_overload(edisgo) + overloaded_lv_stations = checks.mv_lv_station_max_overload( + edisgo, n_minus_one=n_minus_one + ) logger.debug("==> Recheck line load.") crit_lines = ( pd.DataFrame(dtype=float) if mode == "lv" or kwargs.get("skip_mv_reinforcement", False) - else checks.mv_line_max_relative_overload(edisgo) + else checks.mv_line_max_relative_overload(edisgo, n_minus_one=n_minus_one) ) if not mode or mode == "lv": crit_lines = pd.concat( [ crit_lines, - checks.lv_line_max_relative_overload(edisgo, lv_grid_id=lv_grid_id), + checks.lv_line_max_relative_overload( + edisgo, lv_grid_id=lv_grid_id, n_minus_one=n_minus_one + ), ] ) @@ -651,6 +667,71 @@ def reinforce_grid( return edisgo.results +def reinforce_for_n_minus_one( + edisgo_obj: EDisGo, + **kwargs, +): + """ + Grid reinforcment to comply with n-1 security + + Only means reduced allowed line loading. Voltage limits are not changed. + Fault conditions are not used. But load case is checked per MV feeder. + + Per default LV is included. To change that set mode to "mv". + + Parameters + ---------- + edisgo : :class:`~.EDisGo` + kwargs : dict + See parameters of function + :func:`edisgo.flex_opt.reinforce_grid.reinforce_grid`. + + Returns + -------- + + """ + # ToDo HV/MV transformer needs to be checked separately + + comp_types = ["loads", "generators", "storage_units"] + # get residual load in each MV feeder + edisgo_obj.topology.assign_feeders("mv_feeder") + residual_load_feeders = edisgo_obj.timeseries.residual_load( + feeder="mv_feeder", edisgo_obj=edisgo_obj + ) + # save original time series + orig_ts = copy.deepcopy(edisgo_obj.timeseries) + orig_ts.time_series_raw = TimeSeriesRaw() + # per feeder, set all time series to zero, in case residual load in feeder is + # negative (feed-in case) + for feeder in residual_load_feeders.columns: + if feeder != "station_node": + # get time steps where residual load is negative + residual_load_feeder = residual_load_feeders[feeder] + ts_neg = residual_load_feeder[residual_load_feeder < 0].dropna() + # ToDo check if it works correctly if dataframe or ts_net is empty + for comp_type in comp_types: + # get all components of that type in feeder + groupby_bus = pd.merge( + getattr(edisgo_obj.topology, f"{comp_type}_df"), + edisgo_obj.topology.buses_df, + how="left", + left_on="bus", + right_index=True, + ).groupby(feeder) + comps_feeder = groupby_bus.groups[feeder] + for ts_type in ["active_power", "reactive_power"]: + # set time series data to zero + getattr(edisgo_obj.topology, f"{comp_type}_{ts_type}").loc[ + ts_neg, comps_feeder + ] = 0.0 + + # reinforce with n-1 values + # n_minus_one = kwargs.pop("") + edisgo_obj.reinforce(**kwargs) + + # reset time series + + def catch_convergence_reinforce_grid( edisgo: EDisGo, **kwargs,