From 4ed9487b86f2214ae540c6286a92843aa09ae6fc Mon Sep 17 00:00:00 2001 From: AnyaHe Date: Fri, 17 Mar 2023 14:36:37 +0100 Subject: [PATCH 01/12] add methods and test for reduced timesteps reinforcement --- edisgo/opf/timeseries_reduction.py | 108 +++++++++++++++++++++++++ tests/opf/test_timeseries_reduction.py | 57 +++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 tests/opf/test_timeseries_reduction.py diff --git a/edisgo/opf/timeseries_reduction.py b/edisgo/opf/timeseries_reduction.py index 4ba6adbb..3ee48502 100644 --- a/edisgo/opf/timeseries_reduction.py +++ b/edisgo/opf/timeseries_reduction.py @@ -30,6 +30,29 @@ def _scored_critical_loading(edisgo_obj): return crit_lines_score.sort_values(ascending=False) +def _scored_most_critical_loading(edisgo_obj): + """ + Method to get time steps where at least one component + """ + + # Get current relative to allowed current + relative_i_res = check_tech_constraints.components_relative_load(edisgo_obj) + + # Get lines that have violations + crit_lines_score = relative_i_res[relative_i_res > 1] + + # Get most critical timesteps per component + crit_lines_score = ( + (crit_lines_score[crit_lines_score == crit_lines_score.max()]) + .dropna(how="all") + .dropna(how="all", axis=1) + ) + + # Sort according to highest cumulated relative overloading + crit_lines_score = (crit_lines_score - 1).sum(axis=1) + return crit_lines_score.sort_values(ascending=False) + + def _scored_critical_overvoltage(edisgo_obj): voltage_dev = check_tech_constraints.voltage_deviation_from_allowed_voltage_limits( @@ -44,6 +67,91 @@ def _scored_critical_overvoltage(edisgo_obj): return voltage_dev_ov.sort_values(ascending=False) +def _scored_most_critical_voltage_issues(edisgo_obj): + voltage_diff = check_tech_constraints.voltage_deviation_from_allowed_voltage_limits( + edisgo_obj + ) + + # Get score for nodes that are over or under the allowed deviations + voltage_diff = voltage_diff.abs()[voltage_diff.abs() > 0] + # get only most critical events for component + # Todo: should there be different ones for over and undervoltage? + voltage_diff = ( + (voltage_diff[voltage_diff.abs() == voltage_diff.abs().max()]) + .dropna(how="all") + .dropna(how="all", axis=1) + ) + + voltage_diff = voltage_diff.sum(axis=1) + + return voltage_diff.sort_values(ascending=False) + + +def get_steps_reinforcement( + edisgo_obj, num_steps_loading=None, num_steps_voltage=None, percentage=1.0 +): + """ + Get the time steps with the most critical violations for curtailment + optimization. + Parameters + ----------- + edisgo_obj : :class:`~.EDisGo` + The eDisGo API object + num_steps_loading: int + The number of most critical overloading events to select, if None percentage + is used + num_steps_voltage: int + The number of most critical voltage issues to select, if None percentage is used + percentage : float + The percentage of most critical time steps to select + Returns + -------- + `pandas.DatetimeIndex` + the reduced time index for modeling curtailment + """ + # Run power flow if not available + if edisgo_obj.results.i_res is None or edisgo_obj.results.i_res.empty: + logger.debug("Running initial power flow") + edisgo_obj.analyze(raise_not_converged=False) # Todo: raise warning? + + # Select most critical steps based on current violations + loading_scores = _scored_most_critical_loading(edisgo_obj) + if num_steps_loading is None: + num_steps_loading = int(len(loading_scores) * percentage) + else: + if num_steps_loading > len(loading_scores): + logger.info( + f"The number of time steps with highest overloading " + f"({len(loading_scores)}) is lower than the defined number of " + f"loading time steps ({num_steps_loading}). Therefore, only " + f"{len(loading_scores)} time steps are exported." + ) + num_steps_loading = len(loading_scores) + steps = loading_scores[:num_steps_loading].index + + # Select most critical steps based on voltage violations + voltage_scores = _scored_most_critical_voltage_issues(edisgo_obj) + if num_steps_voltage is None: + num_steps_voltage = int(len(voltage_scores) * percentage) + else: + if num_steps_voltage > len(voltage_scores): + logger.info( + f"The number of time steps with highest voltage issues " + f"({len(voltage_scores)}) is lower than the defined number of " + f"voltage time steps ({num_steps_voltage}). Therefore, only " + f"{len(voltage_scores)} time steps are exported." + ) + num_steps_voltage = len(voltage_scores) + steps = steps.append( + voltage_scores[:num_steps_voltage].index + ) # Todo: Can this cause duplicated? + + if len(steps) == 0: + logger.warning("No critical steps detected. No network expansion required.") + + return pd.DatetimeIndex(steps.unique()) + + def get_steps_curtailment(edisgo_obj, percentage=0.5): """ Get the time steps with the most critical violations for curtailment diff --git a/tests/opf/test_timeseries_reduction.py b/tests/opf/test_timeseries_reduction.py new file mode 100644 index 00000000..cd6b42c6 --- /dev/null +++ b/tests/opf/test_timeseries_reduction.py @@ -0,0 +1,57 @@ +import numpy as np +import pytest + +from edisgo import EDisGo +from edisgo.opf.timeseries_reduction import ( + _scored_most_critical_loading, + _scored_most_critical_voltage_issues, + get_steps_reinforcement, +) + + +class TestTimeseriesReduction: + @classmethod + def setup_class(self): + self.edisgo = EDisGo(ding0_grid=pytest.ding0_test_network_path) + self.edisgo.set_time_series_worst_case_analysis() + self.timesteps = self.edisgo.timeseries.timeindex + + @pytest.fixture(autouse=True) + def run_power_flow(self): + """ + Fixture to run new power flow before each test. + + """ + self.edisgo.analyze() + + def test__scored_most_critical_loading(self): + + ts_crit = _scored_most_critical_loading(self.edisgo) + + assert len(ts_crit) == 3 + + assert (ts_crit.index == self.timesteps[[0, 1, 3]]).all() + + assert ( + np.isclose(ts_crit[self.timesteps[[0, 1, 3]]], [1.45613, 1.45613, 1.14647]) + ).all() + + def test__scored_most_critical_voltage_issues(self): + + ts_crit = _scored_most_critical_voltage_issues(self.edisgo) + + assert len(ts_crit) == 2 + + assert (ts_crit.index == self.timesteps[[0, 1]]).all() + + assert ( + np.isclose(ts_crit[self.timesteps[[0, 1]]], [0.01062258, 0.01062258]) + ).all() + + def test_get_steps_reinforcement(self): + + ts_crit = get_steps_reinforcement(self.edisgo) + + assert len(ts_crit) == 3 + + assert (ts_crit == self.timesteps[[0, 1, 3]]).all() From 2da2646a6caaee74d62351157ee0c9f4418c931c Mon Sep 17 00:00:00 2001 From: AnyaHe Date: Fri, 28 Apr 2023 14:49:26 +0200 Subject: [PATCH 02/12] add new option for timesteps_pfa to reinforce_grid and edisgo.reinforce() --- edisgo/edisgo.py | 4 ++++ edisgo/flex_opt/reinforce_grid.py | 3 +++ tests/flex_opt/test_reinforce_grid.py | 7 +++++++ 3 files changed, 14 insertions(+) diff --git a/edisgo/edisgo.py b/edisgo/edisgo.py index e216c865..807af58d 100755 --- a/edisgo/edisgo.py +++ b/edisgo/edisgo.py @@ -1102,6 +1102,10 @@ def reinforce( 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. + * 'reduced_analysis' + Reinforcement is conducted for all time steps at which at least one + branch shows its highest overloading or one bus shows its highest voltage + violation. * :pandas:`pandas.DatetimeIndex` or \ :pandas:`pandas.Timestamp` Use this option to explicitly choose which time steps to consider. diff --git a/edisgo/flex_opt/reinforce_grid.py b/edisgo/flex_opt/reinforce_grid.py index d0acf025..42fb7d92 100644 --- a/edisgo/flex_opt/reinforce_grid.py +++ b/edisgo/flex_opt/reinforce_grid.py @@ -11,6 +11,7 @@ from edisgo.flex_opt import check_tech_constraints as checks from edisgo.flex_opt import exceptions, reinforce_measures from edisgo.flex_opt.costs import grid_expansion_costs +from edisgo.opf.timeseries_reduction import get_steps_reinforcement from edisgo.tools import tools if TYPE_CHECKING: @@ -155,6 +156,8 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): snapshots["min_residual_load"], ] ).dropna() + elif isinstance(timesteps_pfa, str) and timesteps_pfa == "reduced_analysis": + timesteps_pfa = get_steps_reinforcement(edisgo) # if timesteps_pfa is not of type datetime or does not contain # datetimes throw an error elif not isinstance(timesteps_pfa, datetime.datetime): diff --git a/tests/flex_opt/test_reinforce_grid.py b/tests/flex_opt/test_reinforce_grid.py index 46085164..c40344df 100644 --- a/tests/flex_opt/test_reinforce_grid.py +++ b/tests/flex_opt/test_reinforce_grid.py @@ -54,3 +54,10 @@ def test_reinforce_grid(self): result.equipment_changes, comparison_result.equipment_changes, ) + # test new mode + res_reduced = reinforce_grid( + edisgo=copy.deepcopy(self.edisgo), timesteps_pfa="reduced_analysis" + ) + assert_frame_equal( + res_reduced.equipment_changes, results_dict[None].equipment_changes + ) From 653b90da730b2bf01453fc8279ae61e7c744f026 Mon Sep 17 00:00:00 2001 From: AnyaHe Date: Fri, 28 Apr 2023 15:20:30 +0200 Subject: [PATCH 03/12] add type hinting --- edisgo/opf/timeseries_reduction.py | 37 +++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/edisgo/opf/timeseries_reduction.py b/edisgo/opf/timeseries_reduction.py index 3ee48502..bcfe79a2 100644 --- a/edisgo/opf/timeseries_reduction.py +++ b/edisgo/opf/timeseries_reduction.py @@ -1,5 +1,9 @@ +from __future__ import annotations + import logging +from typing import TYPE_CHECKING + import numpy as np import pandas as pd @@ -8,6 +12,9 @@ from edisgo.flex_opt import check_tech_constraints +if TYPE_CHECKING: + from edisgo import EDisGo + logger = logging.getLogger(__name__) @@ -30,9 +37,9 @@ def _scored_critical_loading(edisgo_obj): return crit_lines_score.sort_values(ascending=False) -def _scored_most_critical_loading(edisgo_obj): +def _scored_most_critical_loading(edisgo_obj: EDisGo) -> pd.Series: """ - Method to get time steps where at least one component + Method to get time steps where at least one branch shows its highest overloading """ # Get current relative to allowed current @@ -67,7 +74,11 @@ def _scored_critical_overvoltage(edisgo_obj): return voltage_dev_ov.sort_values(ascending=False) -def _scored_most_critical_voltage_issues(edisgo_obj): +def _scored_most_critical_voltage_issues(edisgo_obj: EDisGo) -> pd.Series: + """ + Method to get time steps where at least one bus shows its highest deviation from + allowed voltage boundaries + """ voltage_diff = check_tech_constraints.voltage_deviation_from_allowed_voltage_limits( edisgo_obj ) @@ -88,20 +99,24 @@ def _scored_most_critical_voltage_issues(edisgo_obj): def get_steps_reinforcement( - edisgo_obj, num_steps_loading=None, num_steps_voltage=None, percentage=1.0 -): + edisgo_obj: EDisGo, + num_steps_loading: int = 0, + num_steps_voltage: int = 0, + percentage: float = 1.0, +) -> pd.DatetimeIndex: """ - Get the time steps with the most critical violations for curtailment - optimization. + Get the time steps with the most critical violations for reduced reinforcement. + Parameters ----------- edisgo_obj : :class:`~.EDisGo` The eDisGo API object num_steps_loading: int - The number of most critical overloading events to select, if None percentage + The number of most critical overloading events to select, if set to 0 percentage is used num_steps_voltage: int - The number of most critical voltage issues to select, if None percentage is used + The number of most critical voltage issues to select, if set to 0 percentage is + used percentage : float The percentage of most critical time steps to select Returns @@ -116,7 +131,7 @@ def get_steps_reinforcement( # Select most critical steps based on current violations loading_scores = _scored_most_critical_loading(edisgo_obj) - if num_steps_loading is None: + if num_steps_loading == 0: num_steps_loading = int(len(loading_scores) * percentage) else: if num_steps_loading > len(loading_scores): @@ -131,7 +146,7 @@ def get_steps_reinforcement( # Select most critical steps based on voltage violations voltage_scores = _scored_most_critical_voltage_issues(edisgo_obj) - if num_steps_voltage is None: + if num_steps_voltage == 0: num_steps_voltage = int(len(voltage_scores) * percentage) else: if num_steps_voltage > len(voltage_scores): From 832087c2fa999e45cddeb6b9fc2d5f98446869b0 Mon Sep 17 00:00:00 2001 From: AnyaHe Date: Fri, 28 Apr 2023 15:22:37 +0200 Subject: [PATCH 04/12] adapt whatsnew --- doc/whatsnew/v0-3-0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/whatsnew/v0-3-0.rst b/doc/whatsnew/v0-3-0.rst index e45a2e5d..c8462d0d 100644 --- a/doc/whatsnew/v0-3-0.rst +++ b/doc/whatsnew/v0-3-0.rst @@ -20,3 +20,4 @@ Changes * Added method to scale timeseries `#353 `_ * Added method to iteratively reinforce a grid in case the power flow analysis does not always converge `#353 `_ * Added method to aggregate LV grid buses to station bus secondary side `#353 `_ +* Added option to run reinforcement with reduced number of time steps `#379 `_ From b696cfead2ad9c96e1f60a1dce9e3804339a7c78 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 3 May 2023 17:26:45 +0200 Subject: [PATCH 05/12] Move troubleshooting mode to separate function --- edisgo/tools/temporal_complexity_reduction.py | 89 +++++++++++-------- 1 file changed, 50 insertions(+), 39 deletions(-) diff --git a/edisgo/tools/temporal_complexity_reduction.py b/edisgo/tools/temporal_complexity_reduction.py index 283bb4b9..517bae6f 100644 --- a/edisgo/tools/temporal_complexity_reduction.py +++ b/edisgo/tools/temporal_complexity_reduction.py @@ -317,6 +317,55 @@ def _scored_most_critical_voltage_issues_time_interval( return time_intervals_df +def _troubleshooting_mode(edisgo_obj): + """ + Handles non-convergence issues in power flow by iteratively reducing load and + feed-in until the power flow converges. + + Load and feed-in is reduced in steps of 10% down to 20% of the original load + and feed-in. The most critical time intervals / time steps can then be determined + based on the power flow results with the reduced load and feed-in. + """ + try: + logger.debug("Running initial power flow for temporal complexity reduction.") + edisgo_obj.analyze() + except ValueError or RuntimeError: + # if power flow did not converge for all time steps, run again with smaller + # loading - loading is decreased, until all time steps converge + logger.warning( + "When running power flow to determine most critical time intervals, " + "not all time steps converged. Power flow is run again with reduced " + "network load." + ) + for fraction in np.linspace(0.9, 0.2, 8): + try: + edisgo_obj.analyze( + troubleshooting_mode="iteration", + range_start=fraction, + range_num=1, + ) + logger.info( + f"Power flow fully converged for a reduction factor " + f"of {fraction}." + ) + break + except ValueError or RuntimeError: + if fraction == 0.2: + raise ValueError( + f"Power flow did not converge for smallest reduction " + f"factor of {fraction}. Most critical time intervals " + f"can therefore not be determined." + ) + else: + logger.info( + f"Power flow did not fully converge for a reduction factor " + f"of {fraction}." + ) + except Exception: + raise Exception + return edisgo_obj + + def get_most_critical_time_intervals( edisgo_obj, num_time_intervals=None, @@ -417,45 +466,7 @@ def get_most_critical_time_intervals( # Run power flow if use_troubleshooting_mode: - try: - logger.debug( - "Running initial power flow for temporal complexity reduction." - ) - edisgo_obj.analyze() - except ValueError: - # if power flow did not converge for all time steps, run again with smaller - # loading - loading is decreased, until all time steps converge - logger.warning( - "When running power flow to determine most critical time intervals, " - "not all time steps converged. Power flow is run again with reduced " - "network load." - ) - for fraction in np.linspace(0.9, 0.2, 8): - try: - edisgo_obj.analyze( - troubleshooting_mode="iteration", - range_start=fraction, - range_num=1, - ) - logger.info( - f"Power flow fully converged for a reduction factor " - f"of {fraction}." - ) - break - except ValueError: - if fraction == 0.2: - raise ValueError( - f"Power flow did not converge for smallest reduction " - f"factor of {fraction}. Most critical time intervals " - f"can therefore not be determined." - ) - else: - logger.info( - f"Power flow did not fully converge for a reduction factor " - f"of {fraction}." - ) - except Exception: - raise Exception + edisgo_obj = _troubleshooting_mode(edisgo_obj) else: logger.debug("Running initial power flow for temporal complexity reduction.") edisgo_obj.analyze() From ce88fd4001914f46e0be315fcde8275c177a3173 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 3 May 2023 17:27:19 +0200 Subject: [PATCH 06/12] Fix docstring --- edisgo/tools/temporal_complexity_reduction.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/edisgo/tools/temporal_complexity_reduction.py b/edisgo/tools/temporal_complexity_reduction.py index 517bae6f..550ad9df 100644 --- a/edisgo/tools/temporal_complexity_reduction.py +++ b/edisgo/tools/temporal_complexity_reduction.py @@ -196,7 +196,7 @@ def _scored_most_critical_voltage_issues_time_interval( the highest expected costs corresponds to index 0. The time steps in the respective time interval are given in column "time_steps" and the share of buses for which the maximum voltage deviation is reached during the time - interval is given in column "percentage_max_overloaded_components". Each bus + interval is given in column "percentage_buses_max_voltage_deviation". Each bus is only considered once. That means if its maximum voltage deviation was already considered in an earlier time interval, it is not considered again. @@ -397,7 +397,9 @@ def get_most_critical_time_intervals( The number of time intervals of most critical line loading and voltage issues to select. If None, `percentage` is used. Default: None. percentage : float - The percentage of most critical time intervals to select. Default: 1.0. + The percentage of most critical time intervals to select. The default is 1.0, in + which case all most critical time steps are selected. + Default: 1.0. time_steps_per_time_interval : int Amount of continuous time steps in an interval that violation is determined for. Currently, these can only be multiples of 24. @@ -452,7 +454,7 @@ def get_most_critical_time_intervals( For voltage issues, the time steps in the respective time interval are given in column "time_steps_voltage_issues" and the share of buses for which the maximum voltage deviation is reached during the time interval is given in column - "percentage_max_overloaded_components". + "percentage_buses_max_voltage_deviation". """ # check frequency of time series data From 1ec73cf0dca1e311d72a26f65ff3fa7b05500ad9 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 3 May 2023 17:32:27 +0200 Subject: [PATCH 07/12] Move functions to tools --- edisgo/flex_opt/reinforce_grid.py | 4 +- edisgo/opf/timeseries_reduction.py | 123 ------------- edisgo/tools/temporal_complexity_reduction.py | 161 ++++++++++++++++++ .../test_temporal_complexity_reduction.py | 28 ++- 4 files changed, 187 insertions(+), 129 deletions(-) diff --git a/edisgo/flex_opt/reinforce_grid.py b/edisgo/flex_opt/reinforce_grid.py index 42fb7d92..7d4712a4 100644 --- a/edisgo/flex_opt/reinforce_grid.py +++ b/edisgo/flex_opt/reinforce_grid.py @@ -11,8 +11,8 @@ from edisgo.flex_opt import check_tech_constraints as checks from edisgo.flex_opt import exceptions, reinforce_measures from edisgo.flex_opt.costs import grid_expansion_costs -from edisgo.opf.timeseries_reduction import get_steps_reinforcement from edisgo.tools import tools +from edisgo.tools.temporal_complexity_reduction import get_most_critical_time_steps if TYPE_CHECKING: from edisgo import EDisGo @@ -157,7 +157,7 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): ] ).dropna() elif isinstance(timesteps_pfa, str) and timesteps_pfa == "reduced_analysis": - timesteps_pfa = get_steps_reinforcement(edisgo) + timesteps_pfa = get_most_critical_time_steps(edisgo) # if timesteps_pfa is not of type datetime or does not contain # datetimes throw an error elif not isinstance(timesteps_pfa, datetime.datetime): diff --git a/edisgo/opf/timeseries_reduction.py b/edisgo/opf/timeseries_reduction.py index bcfe79a2..4ba6adbb 100644 --- a/edisgo/opf/timeseries_reduction.py +++ b/edisgo/opf/timeseries_reduction.py @@ -1,9 +1,5 @@ -from __future__ import annotations - import logging -from typing import TYPE_CHECKING - import numpy as np import pandas as pd @@ -12,9 +8,6 @@ from edisgo.flex_opt import check_tech_constraints -if TYPE_CHECKING: - from edisgo import EDisGo - logger = logging.getLogger(__name__) @@ -37,29 +30,6 @@ def _scored_critical_loading(edisgo_obj): return crit_lines_score.sort_values(ascending=False) -def _scored_most_critical_loading(edisgo_obj: EDisGo) -> pd.Series: - """ - Method to get time steps where at least one branch shows its highest overloading - """ - - # Get current relative to allowed current - relative_i_res = check_tech_constraints.components_relative_load(edisgo_obj) - - # Get lines that have violations - crit_lines_score = relative_i_res[relative_i_res > 1] - - # Get most critical timesteps per component - crit_lines_score = ( - (crit_lines_score[crit_lines_score == crit_lines_score.max()]) - .dropna(how="all") - .dropna(how="all", axis=1) - ) - - # Sort according to highest cumulated relative overloading - crit_lines_score = (crit_lines_score - 1).sum(axis=1) - return crit_lines_score.sort_values(ascending=False) - - def _scored_critical_overvoltage(edisgo_obj): voltage_dev = check_tech_constraints.voltage_deviation_from_allowed_voltage_limits( @@ -74,99 +44,6 @@ def _scored_critical_overvoltage(edisgo_obj): return voltage_dev_ov.sort_values(ascending=False) -def _scored_most_critical_voltage_issues(edisgo_obj: EDisGo) -> pd.Series: - """ - Method to get time steps where at least one bus shows its highest deviation from - allowed voltage boundaries - """ - voltage_diff = check_tech_constraints.voltage_deviation_from_allowed_voltage_limits( - edisgo_obj - ) - - # Get score for nodes that are over or under the allowed deviations - voltage_diff = voltage_diff.abs()[voltage_diff.abs() > 0] - # get only most critical events for component - # Todo: should there be different ones for over and undervoltage? - voltage_diff = ( - (voltage_diff[voltage_diff.abs() == voltage_diff.abs().max()]) - .dropna(how="all") - .dropna(how="all", axis=1) - ) - - voltage_diff = voltage_diff.sum(axis=1) - - return voltage_diff.sort_values(ascending=False) - - -def get_steps_reinforcement( - edisgo_obj: EDisGo, - num_steps_loading: int = 0, - num_steps_voltage: int = 0, - percentage: float = 1.0, -) -> pd.DatetimeIndex: - """ - Get the time steps with the most critical violations for reduced reinforcement. - - Parameters - ----------- - edisgo_obj : :class:`~.EDisGo` - The eDisGo API object - num_steps_loading: int - The number of most critical overloading events to select, if set to 0 percentage - is used - num_steps_voltage: int - The number of most critical voltage issues to select, if set to 0 percentage is - used - percentage : float - The percentage of most critical time steps to select - Returns - -------- - `pandas.DatetimeIndex` - the reduced time index for modeling curtailment - """ - # Run power flow if not available - if edisgo_obj.results.i_res is None or edisgo_obj.results.i_res.empty: - logger.debug("Running initial power flow") - edisgo_obj.analyze(raise_not_converged=False) # Todo: raise warning? - - # Select most critical steps based on current violations - loading_scores = _scored_most_critical_loading(edisgo_obj) - if num_steps_loading == 0: - num_steps_loading = int(len(loading_scores) * percentage) - else: - if num_steps_loading > len(loading_scores): - logger.info( - f"The number of time steps with highest overloading " - f"({len(loading_scores)}) is lower than the defined number of " - f"loading time steps ({num_steps_loading}). Therefore, only " - f"{len(loading_scores)} time steps are exported." - ) - num_steps_loading = len(loading_scores) - steps = loading_scores[:num_steps_loading].index - - # Select most critical steps based on voltage violations - voltage_scores = _scored_most_critical_voltage_issues(edisgo_obj) - if num_steps_voltage == 0: - num_steps_voltage = int(len(voltage_scores) * percentage) - else: - if num_steps_voltage > len(voltage_scores): - logger.info( - f"The number of time steps with highest voltage issues " - f"({len(voltage_scores)}) is lower than the defined number of " - f"voltage time steps ({num_steps_voltage}). Therefore, only " - f"{len(voltage_scores)} time steps are exported." - ) - num_steps_voltage = len(voltage_scores) - steps = steps.append( - voltage_scores[:num_steps_voltage].index - ) # Todo: Can this cause duplicated? - - if len(steps) == 0: - logger.warning("No critical steps detected. No network expansion required.") - - return pd.DatetimeIndex(steps.unique()) - - def get_steps_curtailment(edisgo_obj, percentage=0.5): """ Get the time steps with the most critical violations for curtailment diff --git a/edisgo/tools/temporal_complexity_reduction.py b/edisgo/tools/temporal_complexity_reduction.py index 550ad9df..eb8b801e 100644 --- a/edisgo/tools/temporal_complexity_reduction.py +++ b/edisgo/tools/temporal_complexity_reduction.py @@ -1,7 +1,10 @@ +from __future__ import annotations + import logging import os from copy import deepcopy +from typing import TYPE_CHECKING import numpy as np import pandas as pd @@ -10,9 +13,85 @@ from edisgo.flex_opt.costs import line_expansion_costs from edisgo.tools.tools import assign_feeder +if TYPE_CHECKING: + from edisgo import EDisGo + logger = logging.getLogger(__name__) +def _scored_most_critical_loading(edisgo_obj: EDisGo) -> pd.Series: + """ + Method to get time steps where at least one component shows its highest overloading. + + Parameters + ----------- + edisgo_obj : :class:`~.EDisGo` + + Returns + -------- + :pandas:`pandas.Series` + Series with time index and corresponding sum of maximum relative overloadings + of lines and transformers. The series only contains time steps, where at least + one component is maximally overloaded, and is sorted descending by the + sum of maximum relative overloadings. + + """ + + # Get current relative to allowed current + relative_i_res = check_tech_constraints.components_relative_load(edisgo_obj) + + # Get lines that have violations + crit_lines_score = relative_i_res[relative_i_res > 1] + + # Get most critical timesteps per component + crit_lines_score = ( + (crit_lines_score[crit_lines_score == crit_lines_score.max()]) + .dropna(how="all") + .dropna(how="all", axis=1) + ) + + # Sort according to highest cumulated relative overloading + crit_lines_score = (crit_lines_score - 1).sum(axis=1) + return crit_lines_score.sort_values(ascending=False) + + +def _scored_most_critical_voltage_issues(edisgo_obj: EDisGo) -> pd.Series: + """ + Method to get time steps where at least one bus shows its highest deviation from + allowed voltage boundaries. + + Parameters + ----------- + edisgo_obj : :class:`~.EDisGo` + + Returns + -------- + :pandas:`pandas.Series` + Series with time index and corresponding sum of maximum voltage deviations. + The series only contains time steps, where at least one bus has its highest + deviation from the allowed voltage limits, and is sorted descending by the + sum of maximum voltage deviations. + + """ + voltage_diff = check_tech_constraints.voltage_deviation_from_allowed_voltage_limits( + edisgo_obj + ) + + # Get score for nodes that are over or under the allowed deviations + voltage_diff = voltage_diff.abs()[voltage_diff.abs() > 0] + # get only most critical events for component + # Todo: should there be different ones for over and undervoltage? + voltage_diff = ( + (voltage_diff[voltage_diff.abs() == voltage_diff.abs().max()]) + .dropna(how="all") + .dropna(how="all", axis=1) + ) + + voltage_diff = voltage_diff.sum(axis=1) + + return voltage_diff.sort_values(ascending=False) + + def _scored_most_critical_loading_time_interval( edisgo_obj, time_steps_per_time_interval=168, @@ -532,3 +611,85 @@ def get_most_critical_time_intervals( ) ) return steps + + +def get_most_critical_time_steps( + edisgo_obj: EDisGo, + num_steps_loading=None, + num_steps_voltage=None, + percentage: float = 1.0, + use_troubleshooting_mode=True, +) -> pd.DatetimeIndex: + """ + Get the time steps with the most critical overloading and voltage issues. + + Parameters + ----------- + edisgo_obj : :class:`~.EDisGo` + The eDisGo API object + num_steps_loading : int + The number of most critical overloading events to select. If None, `percentage` + is used. Default: None. + num_steps_voltage : int + The number of most critical voltage issues to select. If None, `percentage` + is used. Default: None. + percentage : float + The percentage of most critical time steps to select. The default is 1.0, in + which case all most critical time steps are selected. + Default: 1.0. + use_troubleshooting_mode : bool + If set to True, non-convergence issues in power flow are tried to be handled + by reducing load and feed-in in steps of 10% down to 20% of the original load + and feed-in until the power flow converges. The most critical time intervals + are then determined based on the power flow results with the reduced load and + feed-in. If False, an error will be raised in case time steps do not converge. + Default: True. + + Returns + -------- + :pandas:`pandas.DatetimeIndex` + Time index with unique time steps where maximum overloading or maximum + voltage deviation is reached for at least one component respectively bus. + + """ + # Run power flow + if use_troubleshooting_mode: + edisgo_obj = _troubleshooting_mode(edisgo_obj) + else: + logger.debug("Running initial power flow for temporal complexity reduction.") + edisgo_obj.analyze() + + # Select most critical steps based on current violations + loading_scores = _scored_most_critical_loading(edisgo_obj) + if num_steps_loading is None: + num_steps_loading = int(len(loading_scores) * percentage) + else: + if num_steps_loading > len(loading_scores): + logger.info( + f"The number of time steps with highest overloading " + f"({len(loading_scores)}) is lower than the defined number of " + f"loading time steps ({num_steps_loading}). Therefore, only " + f"{len(loading_scores)} time steps are exported." + ) + num_steps_loading = len(loading_scores) + steps = loading_scores[:num_steps_loading].index + + # Select most critical steps based on voltage violations + voltage_scores = _scored_most_critical_voltage_issues(edisgo_obj) + if num_steps_voltage is None: + num_steps_voltage = int(len(voltage_scores) * percentage) + else: + if num_steps_voltage > len(voltage_scores): + logger.info( + f"The number of time steps with highest voltage issues " + f"({len(voltage_scores)}) is lower than the defined number of " + f"voltage time steps ({num_steps_voltage}). Therefore, only " + f"{len(voltage_scores)} time steps are exported." + ) + num_steps_voltage = len(voltage_scores) + steps = steps.append(voltage_scores[:num_steps_voltage].index) + + if len(steps) == 0: + logger.warning("No critical steps detected. No network expansion required.") + + return pd.DatetimeIndex(steps.unique()) diff --git a/tests/tools/test_temporal_complexity_reduction.py b/tests/tools/test_temporal_complexity_reduction.py index 98a8659f..05db1238 100644 --- a/tests/tools/test_temporal_complexity_reduction.py +++ b/tests/tools/test_temporal_complexity_reduction.py @@ -29,10 +29,32 @@ def setup_class(self): df, ) self.edisgo.timeseries.timeindex = self.timesteps + self.edisgo.analyze() - def test__scored_most_critical_loading_time_interval(self): + def test__scored_most_critical_loading(self): - self.edisgo.analyze() + ts_crit = temp_red._scored_most_critical_loading(self.edisgo) + + assert len(ts_crit) == 180 + assert np.isclose(ts_crit.iloc[0], 1.45613) + assert np.isclose(ts_crit.iloc[-1], 1.14647) + + def test__scored_most_critical_voltage_issues(self): + + ts_crit = temp_red._scored_most_critical_voltage_issues(self.edisgo) + + assert len(ts_crit) == 120 + assert np.isclose(ts_crit.iloc[0], 0.01062258) + assert np.isclose(ts_crit.iloc[-1], 0.01062258) + + def test_get_most_critical_time_steps(self): + + ts_crit = temp_red.get_most_critical_time_steps( + self.edisgo, num_steps_loading=2, num_steps_voltage=2 + ) + assert len(ts_crit) == 3 + + def test__scored_most_critical_loading_time_interval(self): # test with default values ts_crit = temp_red._scored_most_critical_loading_time_interval(self.edisgo, 24) @@ -61,8 +83,6 @@ def test__scored_most_critical_loading_time_interval(self): def test__scored_most_critical_voltage_issues_time_interval(self): - self.edisgo.analyze() - # test with default values ts_crit = temp_red._scored_most_critical_voltage_issues_time_interval( self.edisgo, 24 From b48847ce77e49610e8570b719b2b80a4205f31d4 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 3 May 2023 17:32:49 +0200 Subject: [PATCH 08/12] Fix bug in overlying grid --- edisgo/network/overlying_grid.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/edisgo/network/overlying_grid.py b/edisgo/network/overlying_grid.py index 4f3a1622..82718b6c 100644 --- a/edisgo/network/overlying_grid.py +++ b/edisgo/network/overlying_grid.py @@ -338,8 +338,7 @@ def distribute_overlying_grid_requirements(edisgo_obj): ).values, ) edisgo_copy.timeseries._loads_active_power.loc[:, hp_district.index] = ( - scaling_df - * edisgo_obj.overlying_grid.heat_pump_central_active_power.sum(axis=1)[0] + scaling_df * edisgo_obj.overlying_grid.heat_pump_central_active_power ).transpose() # decentral PtH - distribute dispatch time series from overlying grid From c97bbe1ec8798030d540a3ce442ca6e0ce44afeb Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 3 May 2023 17:35:54 +0200 Subject: [PATCH 09/12] Use more general Exception to catch non-convergence --- edisgo/tools/temporal_complexity_reduction.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/edisgo/tools/temporal_complexity_reduction.py b/edisgo/tools/temporal_complexity_reduction.py index eb8b801e..06cc54c4 100644 --- a/edisgo/tools/temporal_complexity_reduction.py +++ b/edisgo/tools/temporal_complexity_reduction.py @@ -408,7 +408,9 @@ def _troubleshooting_mode(edisgo_obj): try: logger.debug("Running initial power flow for temporal complexity reduction.") edisgo_obj.analyze() - except ValueError or RuntimeError: + # Exception is used, as non-convergence can also lead to RuntimeError, not only + # ValueError + except Exception: # if power flow did not converge for all time steps, run again with smaller # loading - loading is decreased, until all time steps converge logger.warning( @@ -428,7 +430,7 @@ def _troubleshooting_mode(edisgo_obj): f"of {fraction}." ) break - except ValueError or RuntimeError: + except Exception: if fraction == 0.2: raise ValueError( f"Power flow did not converge for smallest reduction " @@ -440,8 +442,6 @@ def _troubleshooting_mode(edisgo_obj): f"Power flow did not fully converge for a reduction factor " f"of {fraction}." ) - except Exception: - raise Exception return edisgo_obj From 2871276a639261badb82469f0f108587b81cc22d Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 3 May 2023 17:59:52 +0200 Subject: [PATCH 10/12] Allow parameterising of temporal complexity reduction --- edisgo/edisgo.py | 21 +++++++++++++++++++++ edisgo/flex_opt/reinforce_grid.py | 26 +++++++++++++++++++++++++- tests/flex_opt/test_reinforce_grid.py | 6 ++++-- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/edisgo/edisgo.py b/edisgo/edisgo.py index 807af58d..700153bc 100755 --- a/edisgo/edisgo.py +++ b/edisgo/edisgo.py @@ -1165,6 +1165,27 @@ def reinforce( This is used in case worst-case grid reinforcement is conducted in order to reinforce MV/LV stations for LV worst-cases. Default: False. + num_steps_loading : int + In case `timesteps_pfa` is set to 'reduced_analysis', this parameter can be + used to specify the number of most critical overloading events to consider. + If None, `percentage` is used. Default: None. + num_steps_voltage : int + In case `timesteps_pfa` is set to 'reduced_analysis', this parameter can be + used to specify the number of most critical voltage issues to select. If + None, `percentage` is used. Default: None. + percentage : float + In case `timesteps_pfa` is set to 'reduced_analysis', this parameter can be + used to specify the percentage of most critical time steps to select. The + default is 1.0, in which case all most critical time steps are selected. + Default: 1.0. + use_troubleshooting_mode : bool + In case `timesteps_pfa` is set to 'reduced_analysis', this parameter can be + used to specify how to handle non-convergence issues in the power flow + analysis. If set to True, non-convergence issues are tried to be + circumvented by reducing load and feed-in until the power flow converges. + The most critical time steps are then determined based on the power flow + results with the reduced load and feed-in. If False, an error will be + raised in case time steps do not converge. Default: True. Returns -------- diff --git a/edisgo/flex_opt/reinforce_grid.py b/edisgo/flex_opt/reinforce_grid.py index 7d4712a4..b3f06adb 100644 --- a/edisgo/flex_opt/reinforce_grid.py +++ b/edisgo/flex_opt/reinforce_grid.py @@ -82,6 +82,24 @@ def reinforce_grid( This is used in case worst-case grid reinforcement is conducted in order to reinforce MV/LV stations for LV worst-cases. Default: False. + num_steps_loading : int + In case `timesteps_pfa` is set to 'reduced_analysis', this parameter can be used + to specify the number of most critical overloading events to consider. + If None, `percentage` is used. Default: None. + num_steps_voltage : int + In case `timesteps_pfa` is set to 'reduced_analysis', this parameter can be used + to specify the number of most critical voltage issues to select. If None, + `percentage` is used. Default: None. + percentage : float + In case `timesteps_pfa` is set to 'reduced_analysis', this parameter can be used + to specify the percentage of most critical time steps to select. The default + is 1.0, in which case all most critical time steps are selected. + Default: 1.0. + use_troubleshooting_mode : bool + In case `timesteps_pfa` is set to 'reduced_analysis', this parameter can be used + to specify how to handle non-convergence issues in the power flow analysis. + See parameter `use_troubleshooting_mode` in function :attr:`~.EDisGo.reinforce` + for more information. Default: True. Returns ------- @@ -157,7 +175,13 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): ] ).dropna() elif isinstance(timesteps_pfa, str) and timesteps_pfa == "reduced_analysis": - timesteps_pfa = get_most_critical_time_steps(edisgo) + timesteps_pfa = get_most_critical_time_steps( + edisgo, + num_steps_loading=kwargs.get("num_steps_loading", None), + num_steps_voltage=kwargs.get("num_steps_voltage", None), + percentage=kwargs.get("percentage", 1.0), + use_troubleshooting_mode=kwargs.get("use_troubleshooting_mode", True), + ) # if timesteps_pfa is not of type datetime or does not contain # datetimes throw an error elif not isinstance(timesteps_pfa, datetime.datetime): diff --git a/tests/flex_opt/test_reinforce_grid.py b/tests/flex_opt/test_reinforce_grid.py index c40344df..26e96ad7 100644 --- a/tests/flex_opt/test_reinforce_grid.py +++ b/tests/flex_opt/test_reinforce_grid.py @@ -54,9 +54,11 @@ def test_reinforce_grid(self): result.equipment_changes, comparison_result.equipment_changes, ) - # test new mode + # test reduced analysis res_reduced = reinforce_grid( - edisgo=copy.deepcopy(self.edisgo), timesteps_pfa="reduced_analysis" + edisgo=copy.deepcopy(self.edisgo), + timesteps_pfa="reduced_analysis", + num_steps_loading=4, ) assert_frame_equal( res_reduced.equipment_changes, results_dict[None].equipment_changes From bbfe81a067123414be4c05536851c59323687287 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 3 May 2023 18:00:00 +0200 Subject: [PATCH 11/12] Minor doc change --- edisgo/tools/temporal_complexity_reduction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edisgo/tools/temporal_complexity_reduction.py b/edisgo/tools/temporal_complexity_reduction.py index 06cc54c4..7f0b038e 100644 --- a/edisgo/tools/temporal_complexity_reduction.py +++ b/edisgo/tools/temporal_complexity_reduction.py @@ -640,7 +640,7 @@ def get_most_critical_time_steps( use_troubleshooting_mode : bool If set to True, non-convergence issues in power flow are tried to be handled by reducing load and feed-in in steps of 10% down to 20% of the original load - and feed-in until the power flow converges. The most critical time intervals + and feed-in until the power flow converges. The most critical time steps are then determined based on the power flow results with the reduced load and feed-in. If False, an error will be raised in case time steps do not converge. Default: True. From aac7a505c65128d297712f072bb2da823ed5c058 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 3 May 2023 18:52:19 +0200 Subject: [PATCH 12/12] Limit urllib3 version due to error --- rtd_requirements.txt | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/rtd_requirements.txt b/rtd_requirements.txt index de875b29..6cc0e1f9 100644 --- a/rtd_requirements.txt +++ b/rtd_requirements.txt @@ -22,4 +22,5 @@ sphinx_rtd_theme >=0.5.2 sphinx-autodoc-typehints sphinx-autoapi sshtunnel +urllib3 < 2.0.0 workalendar diff --git a/setup.py b/setup.py index b04f9eb0..51935b88 100644 --- a/setup.py +++ b/setup.py @@ -59,6 +59,7 @@ def read(fname): "shapely >= 1.7.0", "sqlalchemy < 1.4.0", "sshtunnel", + "urllib3 < 2.0.0", "workalendar", ]