From 34c30b422c621636f9804db83521a554e206f5e5 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 26 Oct 2023 20:17:51 +0200 Subject: [PATCH 01/18] Add new reinforce measure in case of voltage issues --- edisgo/flex_opt/reinforce_measures.py | 270 ++++++++++++++-------- tests/flex_opt/test_reinforce_measures.py | 65 +++--- 2 files changed, 217 insertions(+), 118 deletions(-) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index b93e0f27..6a054a38 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -365,9 +365,11 @@ def reinforce_lines_voltage_issues(edisgo_obj, grid, crit_nodes): ----- Reinforce measures: - 1. Disconnect line at 2/3 of the length between station and critical node + 1. For LV only, exchange all cables in feeder by standard cable if smaller cable is + currently used. + 2. Disconnect line at 2/3 of the length between station and critical node farthest away from the station and install new standard line - 2. Install parallel standard line + 3. Install parallel standard line In LV grids only lines outside buildings are reinforced; loads and generators in buildings cannot be directly connected to the MV/LV station. @@ -382,15 +384,18 @@ def reinforce_lines_voltage_issues(edisgo_obj, grid, crit_nodes): """ - # load standard line data + # load standard line data and set reinforce measure to exchange small cables by + # standard cables to True in case of LV grids if isinstance(grid, LVGrid): standard_line = edisgo_obj.config["grid_expansion_standard_equipment"][ "lv_line" ] + check_standard_cable = True elif isinstance(grid, MVGrid): standard_line = edisgo_obj.config["grid_expansion_standard_equipment"][ f"mv_line_{int(grid.nominal_voltage)}kv" ] + check_standard_cable = False else: raise ValueError("Inserted grid is invalid.") @@ -414,104 +419,185 @@ def reinforce_lines_voltage_issues(edisgo_obj, grid, crit_nodes): nodes_feeder.setdefault(path[1], []).append(node) lines_changes = {} + # per default, measure to disconnect at two-thirds is set to True and only if cables + # in grid are exchanged by standard lines it is set to False, to recheck voltage + disconnect_2_3 = True + if check_standard_cable is True: + # get all cables in feeder (moved here to only run it once, not for every + # feeder) + grid.assign_grid_feeder() for repr_node in nodes_feeder.keys(): - # find node farthest away - get_weight = lambda u, v, data: data["length"] # noqa: E731 - path_length = 0 - for n in nodes_feeder[repr_node]: - path_length_dict_tmp = dijkstra_shortest_path_length( - graph, station_node, get_weight, target=n + if check_standard_cable is True: + lines_in_feeder = grid.lines_df[grid.lines_df.grid_feeder == repr_node] + # check if line type is any of the following + small_cables = ["NAYY 4x1x120", "NAYY 4x1x95", "NAYY 4x1x50", "NAYY 4x1x35"] + small_lines_in_feeder = lines_in_feeder[ + lines_in_feeder.type_info.isin(small_cables) + ] + # filter cables connecting houses (their type is kept) + # ToDo Currently new components can be connected to house connection via + # a new cable, wherefore it is checked, whether the house connecting cable + # is an end cable. Needs to be changed once grid connection is changed. + for line in small_lines_in_feeder.index: + lines_bus0 = edisgo_obj.topology.get_connected_lines_from_bus( + small_lines_in_feeder.at[line, "bus0"] + ) + lines_bus1 = edisgo_obj.topology.get_connected_lines_from_bus( + small_lines_in_feeder.at[line, "bus1"] + ) + if len(lines_bus0) == 1 or len(lines_bus1) == 1: + small_lines_in_feeder.drop(index=line, inplace=True) + # if there are small lines, exchange them + if len(small_lines_in_feeder) > 0: + edisgo_obj.topology.change_line_type( + small_lines_in_feeder.index, standard_line + ) + # check if s_nom before is larger than when using standard cable + # and if so, install parallel cable + lines_lower_snom = small_lines_in_feeder[ + small_lines_in_feeder.s_nom + > grid.lines_df.loc[small_lines_in_feeder.index, "s_nom"] + ] + if len(lines_lower_snom) > 0: + number_parallel_lines = np.ceil( + lines_lower_snom.s_nom + / grid.lines_df.loc[lines_lower_snom.index, "s_nom"] + ) + # update number of parallel lines + edisgo_obj.topology.update_number_of_parallel_lines( + number_parallel_lines + ) + # add to lines changes + update_dict = { + _: grid.lines_df.at[_, "num_parallel"] + for _ in small_lines_in_feeder.index + } + lines_changes.update(update_dict) + logger.debug( + f"When solving voltage issues in LV grid {grid.id} in feeder " + f"{repr_node}, {len(small_lines_in_feeder)} were exchanged by " + f"standard lines." + ) + # if any cable was changed, set disconnect_2_3 to False + disconnect_2_3 = False + + if disconnect_2_3 is True: + # find node farthest away + get_weight = lambda u, v, data: data["length"] # noqa: E731 + path_length = 0 + for n in nodes_feeder[repr_node]: + path_length_dict_tmp = dijkstra_shortest_path_length( + graph, station_node, get_weight, target=n + ) + if path_length_dict_tmp[n] > path_length: + node = n + path_length = path_length_dict_tmp[n] + path_length_dict = path_length_dict_tmp + path = paths[node] + + # find first node in path that exceeds 2/3 of the line length + # from station to critical node farthest away from the station + node_2_3 = next( + j for j in path if path_length_dict[j] >= path_length_dict[node] * 2 / 3 ) - if path_length_dict_tmp[n] > path_length: - node = n - path_length = path_length_dict_tmp[n] - path_length_dict = path_length_dict_tmp - path = paths[node] - - # find first node in path that exceeds 2/3 of the line length - # from station to critical node farthest away from the station - node_2_3 = next( - j for j in path if path_length_dict[j] >= path_length_dict[node] * 2 / 3 - ) - # if LVGrid: check if node_2_3 is outside of a house - # and if not find next BranchTee outside the house - if isinstance(grid, LVGrid): - while ( - ~np.isnan(grid.buses_df.loc[node_2_3].in_building) - and grid.buses_df.loc[node_2_3].in_building - ): - node_2_3 = path[path.index(node_2_3) - 1] - # break if node is station - if node_2_3 is path[0]: - logger.error("Could not reinforce voltage issue.") - break - - # if MVGrid: check if node_2_3 is LV station and if not find - # next LV station - else: - while node_2_3 not in edisgo_obj.topology.transformers_df.bus0.values: - try: - # try to find LVStation behind node_2_3 - node_2_3 = path[path.index(node_2_3) + 1] - except IndexError: - # if no LVStation between node_2_3 and node with - # voltage problem, connect node directly to - # MVStation - node_2_3 = node - break - - # if node_2_3 is a representative (meaning it is already - # directly connected to the station), line cannot be - # disconnected and must therefore be reinforced - if node_2_3 in nodes_feeder.keys(): - crit_line_name = graph.get_edge_data(station_node, node_2_3)["branch_name"] - crit_line = grid.lines_df.loc[crit_line_name] - - # if critical line is already a standard line install one - # more parallel line - if crit_line.type_info == standard_line: - edisgo_obj.topology.update_number_of_parallel_lines( - pd.Series( - index=[crit_line_name], - data=[ - edisgo_obj.topology._lines_df.at[ - crit_line_name, "num_parallel" - ] - + 1 - ], + # if LVGrid: check if node_2_3 is outside of a house + # and if not find next BranchTee outside the house + if isinstance(grid, LVGrid): + while ( + ~np.isnan(grid.buses_df.loc[node_2_3].in_building) + and grid.buses_df.loc[node_2_3].in_building + ): + node_2_3 = path[path.index(node_2_3) - 1] + # break if node is station + if node_2_3 is path[0]: + logger.error("Could not reinforce voltage issue.") + break + + # if MVGrid: check if node_2_3 is LV station and if not find + # next LV station + else: + while node_2_3 not in edisgo_obj.topology.transformers_df.bus0.values: + try: + # try to find LVStation behind node_2_3 + node_2_3 = path[path.index(node_2_3) + 1] + except IndexError: + # if no LVStation between node_2_3 and node with + # voltage problem, connect node directly to + # MVStation + node_2_3 = node + break + + # if node_2_3 is a representative (meaning it is already + # directly connected to the station), line cannot be + # disconnected and must therefore be reinforced + if node_2_3 in nodes_feeder.keys(): + crit_line_name = graph.get_edge_data(station_node, node_2_3)[ + "branch_name" + ] + crit_line = grid.lines_df.loc[crit_line_name] + + # if critical line is already a standard line install one + # more parallel line + if crit_line.type_info == standard_line: + edisgo_obj.topology.update_number_of_parallel_lines( + pd.Series( + index=[crit_line_name], + data=[ + edisgo_obj.topology._lines_df.at[ + crit_line_name, "num_parallel" + ] + + 1 + ], + ) + ) + lines_changes[crit_line_name] = 1 + + # if critical line is not yet a standard line replace old + # line by a standard line + else: + # number of parallel standard lines could be calculated + # following [2] p.103; for now number of parallel + # standard lines is iterated + edisgo_obj.topology.change_line_type( + [crit_line_name], standard_line ) + lines_changes[crit_line_name] = 1 + logger.debug( + f"When solving voltage issues in grid {grid.id} in feeder " + f"{repr_node}, disconnection at 2/3 was tried but bus is already " + f"connected to the station, wherefore line {crit_line_name} was " + f"reinforced." ) - lines_changes[crit_line_name] = 1 - # if critical line is not yet a standard line replace old - # line by a standard line + # if node_2_3 is not a representative, disconnect line else: - # number of parallel standard lines could be calculated - # following [2] p.103; for now number of parallel - # standard lines is iterated + # get line between node_2_3 and predecessor node (that is + # closer to the station) + pred_node = path[path.index(node_2_3) - 1] + crit_line_name = graph.get_edge_data(node_2_3, pred_node)["branch_name"] + if grid.lines_df.at[crit_line_name, "bus0"] == pred_node: + edisgo_obj.topology._lines_df.at[ + crit_line_name, "bus0" + ] = station_node + elif grid.lines_df.at[crit_line_name, "bus1"] == pred_node: + edisgo_obj.topology._lines_df.at[ + crit_line_name, "bus1" + ] = station_node + else: + raise ValueError("Bus not in line buses. Please check.") + # change line length and type + edisgo_obj.topology._lines_df.at[ + crit_line_name, "length" + ] = path_length_dict[node_2_3] edisgo_obj.topology.change_line_type([crit_line_name], standard_line) lines_changes[crit_line_name] = 1 - - # if node_2_3 is not a representative, disconnect line - else: - # get line between node_2_3 and predecessor node (that is - # closer to the station) - pred_node = path[path.index(node_2_3) - 1] - crit_line_name = graph.get_edge_data(node_2_3, pred_node)["branch_name"] - if grid.lines_df.at[crit_line_name, "bus0"] == pred_node: - edisgo_obj.topology._lines_df.at[crit_line_name, "bus0"] = station_node - elif grid.lines_df.at[crit_line_name, "bus1"] == pred_node: - edisgo_obj.topology._lines_df.at[crit_line_name, "bus1"] = station_node - else: - raise ValueError("Bus not in line buses. Please check.") - # change line length and type - edisgo_obj.topology._lines_df.at[ - crit_line_name, "length" - ] = path_length_dict[node_2_3] - edisgo_obj.topology.change_line_type([crit_line_name], standard_line) - lines_changes[crit_line_name] = 1 - # TODO: Include switch disconnector + # TODO: Include switch disconnector + logger.debug( + f"When solving voltage issues in grid {grid.id} in feeder " + f"{repr_node}, disconnection at 2/3 was conducted " + f"(line {crit_line_name})." + ) if not lines_changes: logger.debug( diff --git a/tests/flex_opt/test_reinforce_measures.py b/tests/flex_opt/test_reinforce_measures.py index 5ea72006..63e1a0bd 100644 --- a/tests/flex_opt/test_reinforce_measures.py +++ b/tests/flex_opt/test_reinforce_measures.py @@ -304,15 +304,22 @@ def test_reinforce_lines_voltage_issues(self): # Line_50000003 (which is first line in feeder and not a # standard line) # * check where node_2_3 is not in_building => problem at + # Bus_BranchTee_LVGrid_5_3, leads to reinforcement of line + # Line_50000006 (which is first line in feeder and a standard line) + # * check where node_2_3 is not in_building => problem at # Bus_BranchTee_LVGrid_5_5, leads to reinforcement of line # Line_50000009 (which is first line in feeder and a standard line) crit_nodes = pd.DataFrame( { - "abs_max_voltage_dev": [0.08, 0.05], - "time_index": [self.timesteps[0], self.timesteps[0]], + "abs_max_voltage_dev": [0.08, 0.05, 0.07], + "time_index": [self.timesteps[0], self.timesteps[0], self.timesteps[0]], }, - index=["Bus_BranchTee_LVGrid_5_2", "Bus_BranchTee_LVGrid_5_5"], + index=[ + "Bus_BranchTee_LVGrid_5_2", + "Bus_BranchTee_LVGrid_5_3", + "Bus_BranchTee_LVGrid_5_5", + ], ) grid = self.edisgo.topology.get_lv_grid("LVGrid_5") @@ -321,10 +328,12 @@ def test_reinforce_lines_voltage_issues(self): ) reinforced_lines = lines_changes.keys() - assert len(lines_changes) == 2 + assert len(lines_changes) == 3 assert "Line_50000003" in reinforced_lines - assert "Line_50000009" in reinforced_lines - # check that LV station is one of the buses + assert "Line_50000006" in reinforced_lines + assert "Line_50000008" in reinforced_lines + # check that LV station is one of the buses for first two issues where + # disconnection at 2/3 was done assert ( "BusBar_MVGrid_1_LVGrid_5_LV" in self.edisgo.topology.lines_df.loc[ @@ -334,14 +343,14 @@ def test_reinforce_lines_voltage_issues(self): assert ( "BusBar_MVGrid_1_LVGrid_5_LV" in self.edisgo.topology.lines_df.loc[ - "Line_50000009", ["bus0", "bus1"] + "Line_50000006", ["bus0", "bus1"] ].values ) # check other bus assert ( - "Bus_BranchTee_LVGrid_5_5" + "Bus_BranchTee_LVGrid_5_3" in self.edisgo.topology.lines_df.loc[ - "Line_50000009", ["bus0", "bus1"] + "Line_50000006", ["bus0", "bus1"] ].values ) assert ( @@ -350,26 +359,30 @@ def test_reinforce_lines_voltage_issues(self): "Line_50000003", ["bus0", "bus1"] ].values ) - # check line parameters + # check buses of line that was only exchanged by standard line + assert ( + self.edisgo.topology.lines_df.at["Line_50000008", "bus0"] + == "Bus_BranchTee_LVGrid_5_5" + ) + assert ( + self.edisgo.topology.lines_df.at["Line_50000008", "bus1"] + == "Bus_BranchTee_LVGrid_5_6" + ) + # check line parameters - all lines where exchanged by one standard line std_line = self.edisgo.topology.equipment_data["lv_cables"].loc[ self.edisgo.config["grid_expansion_standard_equipment"]["lv_line"] ] - line = self.edisgo.topology.lines_df.loc["Line_50000003"] - assert line.type_info == std_line.name - assert np.isclose(line.r, std_line.R_per_km * line.length) - assert np.isclose( - line.x, std_line.L_per_km * line.length * 2 * np.pi * 50 / 1e3 - ) - assert np.isclose( - line.s_nom, np.sqrt(3) * grid.nominal_voltage * std_line.I_max_th - ) - assert line.num_parallel == 1 - line = self.edisgo.topology.lines_df.loc["Line_50000009"] - assert line.type_info == std_line.name - assert line.num_parallel == 2 - assert np.isclose(line.r, 0.02781 / 2) - assert np.isclose(line.x, 0.010857344210806 / 2) - assert np.isclose(line.s_nom, 0.190525588832576 * 2) + for line_name in ["Line_50000003", "Line_50000006", "Line_50000008"]: + line = self.edisgo.topology.lines_df.loc[line_name] + assert line.type_info == std_line.name + assert np.isclose(line.r, std_line.R_per_km * line.length) + assert np.isclose( + line.x, std_line.L_per_km * line.length * 2 * np.pi * 50 / 1e3 + ) + assert np.isclose( + line.s_nom, np.sqrt(3) * grid.nominal_voltage * std_line.I_max_th + ) + assert line.num_parallel == 1 def test_reinforce_lines_overloading(self): # * check for needed parallel standard lines (MV and LV) => problems at From b9d27d8df9f4ae649ed152b5002886defcbb45f8 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 27 Oct 2023 14:04:18 +0200 Subject: [PATCH 02/18] Add function to get standard line from config --- edisgo/flex_opt/reinforce_measures.py | 72 ++++++++++++++++----------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index 6a054a38..eb9ba090 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -342,6 +342,42 @@ def reinforce_mv_lv_station_voltage_issues(edisgo_obj, critical_stations): return transformers_changes +def get_standard_line(edisgo_obj, grid=None, nominal_voltage=None): + """ + Get standard line type for given voltage level from config. + + Parameters + ----------- + edisgo_obj : :class:`~.EDisGo` + grid : :class:`~.network.grids.MVGrid` or :class:`~.network.grids.LVGrid` + nominal_voltage : float + Nominal voltage of grid level to obtain standard line type for. Can be + 0.4, 10 or 20 kV. + + Returns + --------- + str + Name of standard line, e.g. "NAYY 4x1x150". + + """ + if grid is not None: + if isinstance(grid, LVGrid): + nominal_voltage = 0.4 + elif isinstance(grid, MVGrid): + nominal_voltage = grid.buses_df.v_nom.values[0] + else: + raise ValueError("Inserted grid is invalid.") + if nominal_voltage == 0.4: + standard_line_type = edisgo_obj.config["grid_expansion_standard_equipment"][ + "lv_line" + ] + else: + standard_line_type = edisgo_obj.config["grid_expansion_standard_equipment"][ + f"mv_line_{int(nominal_voltage)}kv" + ] + return standard_line_type + + def reinforce_lines_voltage_issues(edisgo_obj, grid, crit_nodes): """ Reinforce lines in MV and LV topology due to voltage issues. @@ -383,21 +419,8 @@ def reinforce_lines_voltage_issues(edisgo_obj, grid, crit_nodes): don't need to be n-1 safe. """ - - # load standard line data and set reinforce measure to exchange small cables by - # standard cables to True in case of LV grids - if isinstance(grid, LVGrid): - standard_line = edisgo_obj.config["grid_expansion_standard_equipment"][ - "lv_line" - ] - check_standard_cable = True - elif isinstance(grid, MVGrid): - standard_line = edisgo_obj.config["grid_expansion_standard_equipment"][ - f"mv_line_{int(grid.nominal_voltage)}kv" - ] - check_standard_cable = False - else: - raise ValueError("Inserted grid is invalid.") + # load standard line data + standard_line = get_standard_line(edisgo_obj, grid=grid) # find path to each node in order to find node with voltage issues farthest # away from station in each feeder @@ -422,12 +445,12 @@ def reinforce_lines_voltage_issues(edisgo_obj, grid, crit_nodes): # per default, measure to disconnect at two-thirds is set to True and only if cables # in grid are exchanged by standard lines it is set to False, to recheck voltage disconnect_2_3 = True - if check_standard_cable is True: + if isinstance(grid, LVGrid): # get all cables in feeder (moved here to only run it once, not for every # feeder) grid.assign_grid_feeder() for repr_node in nodes_feeder.keys(): - if check_standard_cable is True: + if isinstance(grid, LVGrid): lines_in_feeder = grid.lines_df[grid.lines_df.grid_feeder == repr_node] # check if line type is any of the following small_cables = ["NAYY 4x1x120", "NAYY 4x1x95", "NAYY 4x1x50", "NAYY 4x1x35"] @@ -779,14 +802,9 @@ def _replace_by_parallel_standard_lines(lines): nominal_voltage = edisgo_obj.topology.buses_df.loc[ edisgo_obj.topology.lines_df.loc[relevant_lines.index[0], "bus0"], "v_nom" ] - if nominal_voltage == 0.4: - standard_line_type = edisgo_obj.config["grid_expansion_standard_equipment"][ - "lv_line" - ] - else: - standard_line_type = edisgo_obj.config["grid_expansion_standard_equipment"][ - f"mv_line_{int(nominal_voltage)}kv" - ] + standard_line_type = get_standard_line( + edisgo_obj, nominal_voltage=nominal_voltage + ) # handling of standard lines lines_standard = relevant_lines.loc[ @@ -1182,9 +1200,7 @@ def add_standard_transformer( logger.info(f"New LV grid {lv_grid_id_new} added to topology.") - lv_standard_line = edisgo_obj.config["grid_expansion_standard_equipment"][ - "lv_line" - ] + lv_standard_line = get_standard_line(edisgo_obj, nominal_voltage=0.4) # changes on relocated lines to the new LV grid # grid_ids From f82804b494d9386ac74a80417a1cb0c8f84ba0bd Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 27 Oct 2023 15:04:37 +0200 Subject: [PATCH 03/18] Move splitting of feeder to separate function --- edisgo/flex_opt/reinforce_measures.py | 353 +++++++++++++++----------- 1 file changed, 200 insertions(+), 153 deletions(-) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index eb9ba090..d611ea5e 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -378,6 +378,180 @@ def get_standard_line(edisgo_obj, grid=None, nominal_voltage=None): return standard_line_type +def split_feeder_at_given_length( + edisgo_obj, grid, feeder_name, crit_nodes_in_feeder, disconnect_length=2 / 3 +): + """ + Splits given feeder at specified length. + + This is a standard grid expansion measure in case of voltage issues. There, the + feeder is usually disconnected at 2/3 of the feeder length. + + The feeder is split at 2/3 of the length between the station and the critical node + farthest away from the station. A new standard line is installed, or if the line is + already connected to the grid's station exchanged by standard line or a + parallel standard line installed. + In LV grids, feeder can only be split outside of buildings, i.e. loads and + generators in buildings cannot be directly connected to the MV/LV station. + In MV grids feeder can only be split at LV stations because they + have switch disconnectors needed to operate the lines as half rings (loads + in MV would be suitable as well because they have a switch bay (Schaltfeld) + but this is currently not implemented). + + Parameters + ----------- + edisgo_obj : :class:`~.EDisGo` + grid : :class:`~.network.grids.MVGrid` or :class:`~.network.grids.LVGrid` + feeder_name : str + The feeder name corresponds to the name of the neighboring + node of the respective grid's station. + crit_nodes_in_feeder : list(str) + List with names of buses that have voltage issues or should be considered + when finding the point in the feeder where to split it. This is needed + in order to find the critical node farthest away from the station. + disconnect_length : float + Relative length at which the feeder should be split. Default: 2/3. + + Returns + ------- + dict{str: float} + Dictionary with name of lines at which feeder was split as keys and the + corresponding number of lines added as values. + + """ + standard_line = get_standard_line(edisgo_obj, grid=grid) + lines_changes = {} + + # find path to each node in order to find node with voltage issues farthest + # away from station + graph = grid.graph + station_node = grid.station.index[0] + paths = {} + for node in crit_nodes_in_feeder: + path = nx.shortest_path(graph, station_node, node) + paths[node] = path + # raise exception if voltage issue occurs at station's secondary side + # because voltage issues should have been solved during extension of + # distribution substations due to overvoltage issues. + if len(path) == 1: + logger.error( + "Voltage issues at busbar in LV network {} should have " + "been solved in previous steps.".format(grid) + ) + + # find node farthest away + get_weight = lambda u, v, data: data["length"] # noqa: E731 + path_length = 0 + for n in crit_nodes_in_feeder: + path_length_dict_tmp = dijkstra_shortest_path_length( + graph, grid.station.index[0], get_weight, target=n + ) + if path_length_dict_tmp[n] > path_length: + node = n + path_length = path_length_dict_tmp[n] + path_length_dict = path_length_dict_tmp + path = paths[node] + + # find first node in path that exceeds given length of the line length + # from station to critical node farthest away from the station where feeder should + # be separated + disconnect_node = next( + j + for j in path + if path_length_dict[j] >= path_length_dict[node] * disconnect_length + ) + + # if LVGrid: check if disconnect_node is outside of a house + # and if not find next BranchTee outside the house + if isinstance(grid, LVGrid): + while ( + ~np.isnan(grid.buses_df.loc[disconnect_node].in_building) + and grid.buses_df.loc[disconnect_node].in_building + ): + disconnect_node = path[path.index(disconnect_node) - 1] + # break if node is station + if disconnect_node is path[0]: + logger.error("Could not reinforce voltage issue.") + break + + # if MVGrid: check if disconnect_node is LV station and if not find + # next LV station + else: + while disconnect_node not in edisgo_obj.topology.transformers_df.bus0.values: + try: + # try to find LVStation behind disconnect_node + disconnect_node = path[path.index(disconnect_node) + 1] + except IndexError: + # if no LVStation between disconnect_node and node with + # voltage problem, connect node directly to + # MVStation + disconnect_node = node + break + + # if disconnect_node is a representative (meaning it is already + # directly connected to the station), line cannot be + # disconnected and must therefore be reinforced + if disconnect_node == feeder_name: + crit_line_name = graph.get_edge_data(station_node, disconnect_node)[ + "branch_name" + ] + crit_line = grid.lines_df.loc[crit_line_name] + + # if critical line is already a standard line install one + # more parallel line + if crit_line.type_info == standard_line: + edisgo_obj.topology.update_number_of_parallel_lines( + pd.Series( + index=[crit_line_name], + data=[ + edisgo_obj.topology._lines_df.at[crit_line_name, "num_parallel"] + + 1 + ], + ) + ) + lines_changes[crit_line_name] = 1 + + # if critical line is not yet a standard line replace old + # line by a standard line + else: + # number of parallel standard lines could be calculated + # following [2] p.103; for now number of parallel + # standard lines is iterated + edisgo_obj.topology.change_line_type([crit_line_name], standard_line) + lines_changes[crit_line_name] = 1 + logger.debug( + f"When solving voltage issues in grid {grid.id} in feeder " + f"{feeder_name}, disconnection at 2/3 was tried but bus is already " + f"connected to the station, wherefore line {crit_line_name} was " + f"reinforced." + ) + + # if disconnect_node is not a representative, disconnect line + else: + # get line between disconnect_node and predecessor node (that is + # closer to the station) + pred_node = path[path.index(disconnect_node) - 1] + crit_line_name = graph.get_edge_data(disconnect_node, pred_node)["branch_name"] + if grid.lines_df.at[crit_line_name, "bus0"] == pred_node: + edisgo_obj.topology._lines_df.at[crit_line_name, "bus0"] = station_node + elif grid.lines_df.at[crit_line_name, "bus1"] == pred_node: + edisgo_obj.topology._lines_df.at[crit_line_name, "bus1"] = station_node + else: + raise ValueError("Bus not in line buses. Please check.") + # change line length and type + edisgo_obj.topology._lines_df.at[crit_line_name, "length"] = path_length_dict[ + disconnect_node + ] + edisgo_obj.topology.change_line_type([crit_line_name], standard_line) + lines_changes[crit_line_name] = 1 + # TODO: Include switch disconnector + logger.debug( + f"Feeder {feeder_name} in grid {grid.id} was split at " + f"line {crit_line_name}." + ) + return lines_changes + + def reinforce_lines_voltage_issues(edisgo_obj, grid, crit_nodes): """ Reinforce lines in MV and LV topology due to voltage issues. @@ -403,53 +577,27 @@ def reinforce_lines_voltage_issues(edisgo_obj, grid, crit_nodes): 1. For LV only, exchange all cables in feeder by standard cable if smaller cable is currently used. - 2. Disconnect line at 2/3 of the length between station and critical node - farthest away from the station and install new standard line - 3. Install parallel standard line - - In LV grids only lines outside buildings are reinforced; loads and - generators in buildings cannot be directly connected to the MV/LV station. - - In MV grids lines can only be disconnected at LV stations because they - have switch disconnectors needed to operate the lines as half rings (loads - in MV would be suitable as well because they have a switch bay (Schaltfeld) - but loads in dingo are only connected to MV busbar). If there is no - suitable LV station the generator is directly connected to the MV busbar. - There is no need for a switch disconnector in that case because generators - don't need to be n-1 safe. + 2. Split feeder at 2/3 of the length between station and critical node + farthest away from the station and install new standard line, or if the line is + already connected to the grid's station exchange by standard line or install + parallel standard line. See function :attr:`split_feeder_at_given_length` for more + information. """ # load standard line data standard_line = get_standard_line(edisgo_obj, grid=grid) - # find path to each node in order to find node with voltage issues farthest - # away from station in each feeder - station_node = grid.transformers_df.bus1.iloc[0] - graph = grid.graph - paths = {} - nodes_feeder = {} - for node in crit_nodes.index: - path = nx.shortest_path(graph, station_node, node) - paths[node] = path - # raise exception if voltage issue occurs at station's secondary side - # because voltage issues should have been solved during extension of - # distribution substations due to overvoltage issues. - if len(path) == 1: - logger.error( - "Voltage issues at busbar in LV network {} should have " - "been solved in previous steps.".format(grid) - ) - nodes_feeder.setdefault(path[1], []).append(node) + # get feeders with voltage issues + grid.assign_grid_feeder() + crit_buses_df = grid.buses_df.loc[crit_nodes.index, :] + crit_feeders = crit_buses_df.grid_feeder.unique() - lines_changes = {} # per default, measure to disconnect at two-thirds is set to True and only if cables # in grid are exchanged by standard lines it is set to False, to recheck voltage disconnect_2_3 = True - if isinstance(grid, LVGrid): - # get all cables in feeder (moved here to only run it once, not for every - # feeder) - grid.assign_grid_feeder() - for repr_node in nodes_feeder.keys(): + + lines_changes = {} + for repr_node in crit_feeders: if isinstance(grid, LVGrid): lines_in_feeder = grid.lines_df[grid.lines_df.grid_feeder == repr_node] # check if line type is any of the following @@ -505,122 +653,21 @@ def reinforce_lines_voltage_issues(edisgo_obj, grid, crit_nodes): disconnect_2_3 = False if disconnect_2_3 is True: - # find node farthest away - get_weight = lambda u, v, data: data["length"] # noqa: E731 - path_length = 0 - for n in nodes_feeder[repr_node]: - path_length_dict_tmp = dijkstra_shortest_path_length( - graph, station_node, get_weight, target=n - ) - if path_length_dict_tmp[n] > path_length: - node = n - path_length = path_length_dict_tmp[n] - path_length_dict = path_length_dict_tmp - path = paths[node] - - # find first node in path that exceeds 2/3 of the line length - # from station to critical node farthest away from the station - node_2_3 = next( - j for j in path if path_length_dict[j] >= path_length_dict[node] * 2 / 3 + lines_changes_tmp = split_feeder_at_given_length( + edisgo_obj, + grid, + feeder_name=repr_node, + crit_nodes_in_feeder=crit_buses_df[ + crit_buses_df.grid_feeder == repr_node + ].index, + disconnect_length=2 / 3, ) - - # if LVGrid: check if node_2_3 is outside of a house - # and if not find next BranchTee outside the house - if isinstance(grid, LVGrid): - while ( - ~np.isnan(grid.buses_df.loc[node_2_3].in_building) - and grid.buses_df.loc[node_2_3].in_building - ): - node_2_3 = path[path.index(node_2_3) - 1] - # break if node is station - if node_2_3 is path[0]: - logger.error("Could not reinforce voltage issue.") - break - - # if MVGrid: check if node_2_3 is LV station and if not find - # next LV station - else: - while node_2_3 not in edisgo_obj.topology.transformers_df.bus0.values: - try: - # try to find LVStation behind node_2_3 - node_2_3 = path[path.index(node_2_3) + 1] - except IndexError: - # if no LVStation between node_2_3 and node with - # voltage problem, connect node directly to - # MVStation - node_2_3 = node - break - - # if node_2_3 is a representative (meaning it is already - # directly connected to the station), line cannot be - # disconnected and must therefore be reinforced - if node_2_3 in nodes_feeder.keys(): - crit_line_name = graph.get_edge_data(station_node, node_2_3)[ - "branch_name" - ] - crit_line = grid.lines_df.loc[crit_line_name] - - # if critical line is already a standard line install one - # more parallel line - if crit_line.type_info == standard_line: - edisgo_obj.topology.update_number_of_parallel_lines( - pd.Series( - index=[crit_line_name], - data=[ - edisgo_obj.topology._lines_df.at[ - crit_line_name, "num_parallel" - ] - + 1 - ], - ) - ) - lines_changes[crit_line_name] = 1 - - # if critical line is not yet a standard line replace old - # line by a standard line - else: - # number of parallel standard lines could be calculated - # following [2] p.103; for now number of parallel - # standard lines is iterated - edisgo_obj.topology.change_line_type( - [crit_line_name], standard_line - ) - lines_changes[crit_line_name] = 1 - logger.debug( - f"When solving voltage issues in grid {grid.id} in feeder " - f"{repr_node}, disconnection at 2/3 was tried but bus is already " - f"connected to the station, wherefore line {crit_line_name} was " - f"reinforced." - ) - - # if node_2_3 is not a representative, disconnect line - else: - # get line between node_2_3 and predecessor node (that is - # closer to the station) - pred_node = path[path.index(node_2_3) - 1] - crit_line_name = graph.get_edge_data(node_2_3, pred_node)["branch_name"] - if grid.lines_df.at[crit_line_name, "bus0"] == pred_node: - edisgo_obj.topology._lines_df.at[ - crit_line_name, "bus0" - ] = station_node - elif grid.lines_df.at[crit_line_name, "bus1"] == pred_node: - edisgo_obj.topology._lines_df.at[ - crit_line_name, "bus1" - ] = station_node - else: - raise ValueError("Bus not in line buses. Please check.") - # change line length and type - edisgo_obj.topology._lines_df.at[ - crit_line_name, "length" - ] = path_length_dict[node_2_3] - edisgo_obj.topology.change_line_type([crit_line_name], standard_line) - lines_changes[crit_line_name] = 1 - # TODO: Include switch disconnector - logger.debug( - f"When solving voltage issues in grid {grid.id} in feeder " - f"{repr_node}, disconnection at 2/3 was conducted " - f"(line {crit_line_name})." - ) + logger.debug( + f"When solving voltage issues in grid {grid.id} in feeder " + f"{repr_node}, disconnection at 2/3 was conducted " + f"(line {list(lines_changes_tmp.keys())[0]})." + ) + lines_changes.update(lines_changes_tmp) if not lines_changes: logger.debug( From 8bc8bea5a42db95ac157fe9ded17fcbffb5ea4df Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 27 Oct 2023 15:11:51 +0200 Subject: [PATCH 04/18] Minor doc change --- edisgo/network/grids.py | 1 + 1 file changed, 1 insertion(+) diff --git a/edisgo/network/grids.py b/edisgo/network/grids.py index 978f5241..1100bf0b 100644 --- a/edisgo/network/grids.py +++ b/edisgo/network/grids.py @@ -376,6 +376,7 @@ def assign_grid_feeder(self, mode: str = "grid_feeder"): HV/MV station. The grid feeder name corresponds to the name of the neighboring node of the respective grid's station. The feeder name of the source node, i.e. the station, is set to "station_node". + Parameters ---------- mode : str From 67d2cb3a957c946949a377a4b19c6fd5884c3216 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 27 Oct 2023 19:30:53 +0200 Subject: [PATCH 05/18] Add option to keep line_type and number of parallel lines when splitting LV grid --- edisgo/flex_opt/reinforce_measures.py | 32 +++++++++++++++++++-------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index d611ea5e..6de6529f 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -886,7 +886,9 @@ def _replace_by_parallel_standard_lines(lines): def separate_lv_grid( - edisgo_obj: EDisGo, grid: LVGrid + edisgo_obj: EDisGo, + grid: LVGrid, + use_standard_line: bool = True, ) -> tuple[dict[Any, Any], dict[str, int]]: """ Separate LV grid by adding a new substation and connect half of each feeder. @@ -916,6 +918,10 @@ def separate_lv_grid( ---------- edisgo_obj : :class:`~.EDisGo` grid : :class:`~.network.grids.LVGrid` + use_standard_line : bool + If True, standard line type is used to connect bus, where feeder is split, to + the station. If False, the same line type and number of parallel lines as + the original line is used. Default: True. Returns ------- @@ -1266,21 +1272,29 @@ def add_standard_transformer( G, station_node, get_weight, target=node_1_2 )[node_1_2] + # predecessor node of node_1_2 + pred_node = path[path.index(node_1_2) - 1] + # the line + line_removed = G.get_edge_data(node_1_2, pred_node)["branch_name"] + if use_standard_line is True: + line_type = lv_standard_line + num_parallel = 1 + else: + type_info = edisgo_obj.topology.lines_df.at[line_removed, "type_info"] + line_type = type_info if type_info is not None else lv_standard_line + num_parallel = edisgo_obj.topology.lines_df.at[ + line_removed, "num_parallel" + ] line_added_lv = edisgo_obj.add_component( comp_type="line", bus0=lv_bus_new, bus1=node_1_2, length=dist, - type_info=lv_standard_line, + type_info=line_type, + num_parallel=num_parallel, ) - lines_changes[line_added_lv] = 1 - - # predecessor node of node_1_2 - pred_node = path[path.index(node_1_2) - 1] - - # the line - line_removed = G.get_edge_data(node_1_2, pred_node)["branch_name"] + lines_changes[line_added_lv] = num_parallel edisgo_obj.remove_component( comp_type="line", From ebd81727fbb59eaefe29fc063dcc9c3242990a9f Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 27 Oct 2023 19:37:24 +0200 Subject: [PATCH 06/18] Change parameter name --- edisgo/flex_opt/reinforce_grid.py | 2 ++ edisgo/flex_opt/reinforce_measures.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/edisgo/flex_opt/reinforce_grid.py b/edisgo/flex_opt/reinforce_grid.py index 0f7f171a..ee70c69f 100644 --- a/edisgo/flex_opt/reinforce_grid.py +++ b/edisgo/flex_opt/reinforce_grid.py @@ -835,6 +835,7 @@ def enhanced_reinforce_grid( activate_cost_results_disturbing_mode: bool = False, separate_lv_grids: bool = True, separation_threshold: int | float = 2, + use_standard_line_type: bool = True, **kwargs, ) -> EDisGo: """ @@ -889,6 +890,7 @@ def enhanced_reinforce_grid( """ kwargs.pop("skip_mv_reinforcement", False) + # ToDo kwargs timesteps_pfa is currently ignored, should that be changed? num_lv_grids_standard_lines = 0 num_lv_grids_aggregated = 0 diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index 6de6529f..aa8cac04 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -888,7 +888,7 @@ def _replace_by_parallel_standard_lines(lines): def separate_lv_grid( edisgo_obj: EDisGo, grid: LVGrid, - use_standard_line: bool = True, + use_standard_line_type: bool = True, ) -> tuple[dict[Any, Any], dict[str, int]]: """ Separate LV grid by adding a new substation and connect half of each feeder. @@ -918,7 +918,7 @@ def separate_lv_grid( ---------- edisgo_obj : :class:`~.EDisGo` grid : :class:`~.network.grids.LVGrid` - use_standard_line : bool + use_standard_line_type : bool If True, standard line type is used to connect bus, where feeder is split, to the station. If False, the same line type and number of parallel lines as the original line is used. Default: True. @@ -1276,7 +1276,7 @@ def add_standard_transformer( pred_node = path[path.index(node_1_2) - 1] # the line line_removed = G.get_edge_data(node_1_2, pred_node)["branch_name"] - if use_standard_line is True: + if use_standard_line_type is True: line_type = lv_standard_line num_parallel = 1 else: From 56dddb931e13b06a960c776d508810817528af20 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 27 Oct 2023 19:39:53 +0200 Subject: [PATCH 07/18] Fix adding parameter to function --- edisgo/flex_opt/reinforce_grid.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/edisgo/flex_opt/reinforce_grid.py b/edisgo/flex_opt/reinforce_grid.py index ee70c69f..05db76ef 100644 --- a/edisgo/flex_opt/reinforce_grid.py +++ b/edisgo/flex_opt/reinforce_grid.py @@ -876,7 +876,12 @@ def enhanced_reinforce_grid( separation_threshold : int or float Overloading threshold for LV grid separation. If the overloading is higher than the threshold times the total nominal apparent power of the MV/LV transformer(s) - the grid is separated. + the grid is separated. Default: 2. + use_standard_line_type : bool + Only used when `separate_lv_grids` is set to True. If use_standard_line_type is + True, standard line type is used to connect bus, where feeder is split, to + the station. If False, the same line type and number of parallel lines as + the original line is used. Default: True. kwargs : dict Keyword arguments can be all parameters of function :func:`edisgo.flex_opt.reinforce_grid.reinforce_grid`, except @@ -900,7 +905,11 @@ def enhanced_reinforce_grid( "Separating lv grids. Set the parameter 'separate_lv_grids' to False if " "this is not desired." ) - run_separate_lv_grids(edisgo_object, threshold=separation_threshold) + run_separate_lv_grids( + edisgo_object, + threshold=separation_threshold, + use_standard_line_type=use_standard_line_type, + ) logger.info("Run initial grid reinforcement for single LV grids.") for lv_grid in list(edisgo_object.topology.mv_grid.lv_grids): From 8924e2d5c906951abea71d640dcb3ae2f2675d21 Mon Sep 17 00:00:00 2001 From: birgits Date: Sat, 28 Oct 2023 15:44:54 +0200 Subject: [PATCH 08/18] Add function to obtain transformer costs --- edisgo/flex_opt/costs.py | 66 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/edisgo/flex_opt/costs.py b/edisgo/flex_opt/costs.py index 8bd63d9b..b3b711f3 100644 --- a/edisgo/flex_opt/costs.py +++ b/edisgo/flex_opt/costs.py @@ -288,3 +288,69 @@ def line_expansion_costs(edisgo_obj, lines_names=None): ] ) return costs_lines.loc[lines_df.index] + + +def transformer_expansion_costs(edisgo_obj, transformer_names=None): + """ + Returns costs per transformer in kEUR as well as voltage level they are in. + + Parameters + ----------- + edisgo_obj : :class:`~.EDisGo` + eDisGo object + transformer_names: None or list(str) + List of names of transformers to return cost information for. If None, it is + returned for all transformers in + :attr:`~.network.topology.Topology.transformers_df` and + :attr:`~.network.topology.Topology.transformers_hvmv_df`. + + Returns + ------- + costs: :pandas:`pandas.DataFrame` + Dataframe with names of transformers in index and columns 'costs' with + costs per transformer in kEUR and 'voltage_level' with information on voltage + level the transformer is in. + + """ + transformers_df = pd.concat( + [ + edisgo_obj.topology.transformers_df.copy(), + edisgo_obj.topology.transformers_hvmv_df.copy(), + ] + ) + if transformer_names is not None: + transformers_df = transformers_df.loc[transformer_names, ["type_info"]] + + if len(transformers_df) == 0: + return pd.DataFrame(columns=["costs", "voltage_level"]) + + hvmv_transformers = transformers_df[ + transformers_df.index.isin(edisgo_obj.topology.transformers_hvmv_df.index) + ].index + mvlv_transformers = transformers_df[ + transformers_df.index.isin(edisgo_obj.topology.transformers_df.index) + ].index + + costs_hvmv = float(edisgo_obj.config["costs_transformers"]["mv"]) + costs_mvlv = float(edisgo_obj.config["costs_transformers"]["lv"]) + + costs_df = pd.DataFrame( + { + "costs": costs_hvmv, + "voltage_level": "hv/mv", + }, + index=hvmv_transformers, + ) + costs_df = pd.concat( + [ + costs_df, + pd.DataFrame( + { + "costs": costs_mvlv, + "voltage_level": "mv/lv", + }, + index=mvlv_transformers, + ), + ] + ) + return costs_df From 8f55efb8e9af168318048aac94c15ebb03d27808 Mon Sep 17 00:00:00 2001 From: birgits Date: Sat, 28 Oct 2023 17:26:09 +0200 Subject: [PATCH 09/18] Add type_info to new transformers when grid is split --- edisgo/flex_opt/reinforce_measures.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index aa8cac04..4be4f44d 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -1013,13 +1013,12 @@ def add_standard_transformer( ) try: + standard_transformer_name = edisgo_obj.config[ + "grid_expansion_standard_equipment" + ]["mv_lv_transformer"] standard_transformer = edisgo_obj.topology.equipment_data[ "lv_transformers" - ].loc[ - edisgo_obj.config["grid_expansion_standard_equipment"][ - "mv_lv_transformer" - ] - ] + ].loc[standard_transformer_name] except KeyError: raise KeyError("Standard MV/LV transformer is not in the equipment list.") @@ -1031,7 +1030,7 @@ def add_standard_transformer( new_transformer_name[grid_id_ind] = lv_grid_id_new new_transformer_df.s_nom = standard_transformer.S_nom - new_transformer_df.type_info = None + new_transformer_df.type_info = standard_transformer_name new_transformer_df.r_pu = standard_transformer.r_pu new_transformer_df.x_pu = standard_transformer.x_pu new_transformer_df.index = ["_".join([str(_) for _ in new_transformer_name])] From 6b6b75b6ce5207056d1075f26eff6db376bf46db Mon Sep 17 00:00:00 2001 From: birgits Date: Sat, 28 Oct 2023 17:27:58 +0200 Subject: [PATCH 10/18] Use minimum of quantity added and parallel lines because sometimes lines are removed which is not written to equipment changes --- edisgo/flex_opt/costs.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/edisgo/flex_opt/costs.py b/edisgo/flex_opt/costs.py index b3b711f3..f6b98a62 100644 --- a/edisgo/flex_opt/costs.py +++ b/edisgo/flex_opt/costs.py @@ -161,6 +161,19 @@ def _get_line_costs(lines_added): .sum() .loc[lines_added_unique, ["quantity"]] ) + # use the minimum of quantity and num_parallel, as sometimes lines are added + # and in a next reinforcement step removed again, e.g. when feeder is split + # at 2/3 and a new single standard line is added + lines_added = pd.merge( + lines_added, + edisgo_obj.topology.lines_df.loc[:, ["num_parallel"]], + how="left", + left_index=True, + right_index=True, + ) + lines_added["quantity_added"] = lines_added.loc[ + :, ["quantity", "num_parallel"] + ].min(axis=1) lines_added["length"] = edisgo_obj.topology.lines_df.loc[ lines_added.index, "length" ] @@ -176,9 +189,9 @@ def _get_line_costs(lines_added): ].values, "total_costs": line_costs.costs.values, "length": ( - lines_added.quantity * lines_added.length + lines_added.quantity_added * lines_added.length ).values, - "quantity": lines_added.quantity.values, + "quantity": lines_added.quantity_added.values, "voltage_level": line_costs.voltage_level.values, }, index=lines_added.index, From 1a2aed110120d85159bc5ca921f7c28e5e7568f9 Mon Sep 17 00:00:00 2001 From: birgits Date: Sat, 28 Oct 2023 17:52:30 +0200 Subject: [PATCH 11/18] Bug fix not all transformer changes have station in name --- edisgo/flex_opt/costs.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/edisgo/flex_opt/costs.py b/edisgo/flex_opt/costs.py index f6b98a62..d1aa910d 100644 --- a/edisgo/flex_opt/costs.py +++ b/edisgo/flex_opt/costs.py @@ -107,9 +107,7 @@ def _get_line_costs(lines_added): # costs for transformers if not equipment_changes.empty: transformers = equipment_changes[ - equipment_changes.index.isin( - [f"{_}_station" for _ in edisgo_obj.topology._grids_repr] - ) + equipment_changes.equipment.str.contains("Transformer") ] added_transformers = transformers[transformers["change"] == "added"] removed_transformers = transformers[transformers["change"] == "removed"] @@ -129,6 +127,7 @@ def _get_line_costs(lines_added): ) trafos = all_trafos.loc[added_transformers["equipment"]] # calculate costs for each transformer + # ToDo voltage level should be hv/mv for HV/MV transformers costs = pd.concat( [ costs, From aba6897949e120b7651bd2e0eab1ce209f01b2a4 Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 30 Oct 2023 15:39:59 +0100 Subject: [PATCH 12/18] Bug fix --- edisgo/flex_opt/costs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/edisgo/flex_opt/costs.py b/edisgo/flex_opt/costs.py index d1aa910d..2079eb77 100644 --- a/edisgo/flex_opt/costs.py +++ b/edisgo/flex_opt/costs.py @@ -108,6 +108,7 @@ def _get_line_costs(lines_added): if not equipment_changes.empty: transformers = equipment_changes[ equipment_changes.equipment.str.contains("Transformer") + | equipment_changes.equipment.str.contains("transformer") ] added_transformers = transformers[transformers["change"] == "added"] removed_transformers = transformers[transformers["change"] == "removed"] From 25c5df521c827db6d8c834f4b7fe69eab2a59cd5 Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 30 Oct 2023 21:19:52 +0100 Subject: [PATCH 13/18] Bug fix add missing parameter --- edisgo/flex_opt/reinforce_grid.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/edisgo/flex_opt/reinforce_grid.py b/edisgo/flex_opt/reinforce_grid.py index 05db76ef..0a6b222f 100644 --- a/edisgo/flex_opt/reinforce_grid.py +++ b/edisgo/flex_opt/reinforce_grid.py @@ -1127,7 +1127,11 @@ def enhanced_reinforce_grid( return edisgo_object -def run_separate_lv_grids(edisgo_obj: EDisGo, threshold: int | float = 2) -> None: +def run_separate_lv_grids( + edisgo_obj: EDisGo, + threshold: int | float = 2, + use_standard_line_type: bool = True, +) -> None: """ Separate all highly overloaded LV grids within the MV grid. @@ -1145,6 +1149,10 @@ def run_separate_lv_grids(edisgo_obj: EDisGo, threshold: int | float = 2) -> Non Overloading threshold. If the overloading is higher than the threshold times the total nominal apparent power of the MV/LV transformer(s), the grid is separated. + use_standard_line_type : bool + If use_standard_line_type is True, standard line type is used to connect bus + where feeder is split to the station. If False, the same line type and number + of parallel lines as the original line is used. Default: True. Returns ------- @@ -1219,7 +1227,7 @@ def run_separate_lv_grids(edisgo_obj: EDisGo, threshold: int | float = 2) -> Non if worst_case > threshold * transformers_s_nom: logger.info(f"Trying to separate {lv_grid}...") transformers_changes, lines_changes = separate_lv_grid( - edisgo_obj, lv_grid + edisgo_obj, lv_grid, use_standard_line_type ) if len(lines_changes) > 0: _add_lines_changes_to_equipment_changes( From 7ea7eb82073795962127e48e69906932ca73b2f8 Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 31 Oct 2023 20:07:59 +0100 Subject: [PATCH 14/18] Bug fix convert to float64 so that round() does not fail b. --- edisgo/io/powermodels_io.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/edisgo/io/powermodels_io.py b/edisgo/io/powermodels_io.py index 16449bd1..b0fb2278 100644 --- a/edisgo/io/powermodels_io.py +++ b/edisgo/io/powermodels_io.py @@ -957,10 +957,16 @@ def _build_load( ) pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "storage") p_d = -min( - [psa_net.storage_units_t.p_set[inflexible_storage_units[stor_i]][0], 0] + [ + psa_net.storage_units_t.p_set[inflexible_storage_units[stor_i]][0], + np.float64(0.0), + ] ) q_d = -max( - [psa_net.storage_units_t.q_set[inflexible_storage_units[stor_i]][0], 0] + [ + psa_net.storage_units_t.q_set[inflexible_storage_units[stor_i]][0], + np.float64(0.0), + ] ) pm["load"][str(stor_i + len(loads_df.index) + 1)] = { "pd": p_d.round(20) / s_base, From 097f9651803761981312677a4365ec247ba97a5d Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 15 Nov 2023 11:27:24 -0800 Subject: [PATCH 15/18] Bug fix create directory for logging --- edisgo/tools/logger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edisgo/tools/logger.py b/edisgo/tools/logger.py index 5b1a62a5..03300911 100644 --- a/edisgo/tools/logger.py +++ b/edisgo/tools/logger.py @@ -140,7 +140,7 @@ def create_home_dir(): log_dir = os.path.join( get_default_root_dir(), cfg_edisgo.get("user_dirs", "log_dir") ) - create_dir(log_dir) + create_dir(log_dir) if log_dir is not None: file_name = os.path.join(log_dir, file_name) From 4dfc1d0f69c076075f8d7c8718b499995b5c8f62 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 15 Nov 2023 11:40:15 -0800 Subject: [PATCH 16/18] Bug fix create directories recursively --- edisgo/tools/logger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edisgo/tools/logger.py b/edisgo/tools/logger.py index 03300911..5a4cbcbb 100644 --- a/edisgo/tools/logger.py +++ b/edisgo/tools/logger.py @@ -119,7 +119,7 @@ def setup_logger( def create_dir(dir_path): if not os.path.isdir(dir_path): - os.mkdir(dir_path) + os.makedirs(dir_path) def get_default_root_dir(): dir_path = str(cfg_edisgo.get("user_dirs", "root_dir")) From 2973cbaefb5dfb169c6b71af5e396f20448fcccd Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 17 Nov 2023 17:28:13 -0800 Subject: [PATCH 17/18] Enable getting electricity time series for status quo for households and industry --- edisgo/io/timeseries_import.py | 59 ++++++++++++++---------------- tests/io/test_timeseries_import.py | 17 ++++++++- 2 files changed, 43 insertions(+), 33 deletions(-) diff --git a/edisgo/io/timeseries_import.py b/edisgo/io/timeseries_import.py index 93d461b8..83e4afc6 100644 --- a/edisgo/io/timeseries_import.py +++ b/edisgo/io/timeseries_import.py @@ -1052,7 +1052,10 @@ def _get_demand_share(): db_table = egon_cts_heat_demand_building_share with session_scope_egon_data(engine) as session: - query = session.query(db_table.building_id, db_table.profile_share,).filter( + query = session.query( + db_table.building_id, + db_table.profile_share, + ).filter( db_table.scenario == scenario, db_table.bus_id == bus_id, ) @@ -1080,7 +1083,10 @@ def _get_substation_profile(): db_table = egon_etrago_heat_cts with session_scope_egon_data(engine) as session: - query = session.query(db_table.bus_id, db_table.p_set,).filter( + query = session.query( + db_table.bus_id, + db_table.p_set, + ).filter( db_table.scn_name == scenario, db_table.bus_id == bus_id, ) @@ -1126,7 +1132,6 @@ def _get_total_heat_demand_grid(): saio.register_schema("demand", engine) if sector == "electricity": - from saio.demand import ( egon_cts_electricity_demand_building_share, egon_etrago_electricity_cts, @@ -1138,7 +1143,6 @@ def _get_total_heat_demand_grid(): df_demand_share = _get_demand_share() elif sector == "heat": - from saio.demand import ( egon_cts_heat_demand_building_share, egon_etrago_heat_cts, @@ -1187,7 +1191,7 @@ def get_residential_electricity_profiles_per_building(building_ids, scenario, en List of building IDs to retrieve electricity demand profiles for. scenario : str Scenario for which to retrieve demand data. Possible options - are 'eGon2035' and 'eGon100RE'. + are 'eGon2021', 'eGon2035' and 'eGon100RE'. engine : :sqlalchemy:`sqlalchemy.Engine` Database engine. @@ -1216,30 +1220,21 @@ def _get_scaling_factors_of_zensus_cells(zensus_ids): column factor. """ - with session_scope_egon_data(engine) as session: - if scenario == "eGon2035": - query = session.query( - egon_household_electricity_profile_in_census_cell.cell_id, - egon_household_electricity_profile_in_census_cell.factor_2035.label( - "factor" - ), - ).filter( - egon_household_electricity_profile_in_census_cell.cell_id.in_( - zensus_ids - ) - ) - else: - query = session.query( - egon_household_electricity_profile_in_census_cell.cell_id, - egon_household_electricity_profile_in_census_cell.factor_2050.label( - "factor" - ), - ).filter( - egon_household_electricity_profile_in_census_cell.cell_id.in_( - zensus_ids - ) - ) - return pd.read_sql(query.statement, engine, index_col="cell_id") + if scenario == "eGon2021": + return pd.DataFrame(index=zensus_ids, data={"factor": 1.0}) + else: + with session_scope_egon_data(engine) as session: + if scenario == "eGon2035": + query = session.query( + hh_profile.cell_id, + hh_profile.factor_2035.label("factor"), + ).filter(hh_profile.cell_id.in_(zensus_ids)) + else: + query = session.query( + hh_profile.cell_id, + hh_profile.factor_2050.label("factor"), + ).filter(hh_profile.cell_id.in_(zensus_ids)) + return pd.read_sql(query.statement, engine, index_col="cell_id") def _get_profile_ids_of_buildings(building_ids): """ @@ -1298,7 +1293,9 @@ def _get_profiles(profile_ids): saio.register_schema("demand", engine) from saio.demand import ( - egon_household_electricity_profile_in_census_cell, + egon_household_electricity_profile_in_census_cell as hh_profile, + ) + from saio.demand import ( egon_household_electricity_profile_of_buildings, iee_household_load_profiles, ) @@ -1346,7 +1343,7 @@ def get_industrial_electricity_profiles_per_site(site_ids, scenario, engine): List of industrial site and OSM IDs to retrieve electricity demand profiles for. scenario : str Scenario for which to retrieve demand data. Possible options - are 'eGon2035' and 'eGon100RE'. + are 'eGon2021', 'eGon2035' and 'eGon100RE'. engine : :sqlalchemy:`sqlalchemy.Engine` Database engine. diff --git a/tests/io/test_timeseries_import.py b/tests/io/test_timeseries_import.py index d56f3350..93825e9a 100644 --- a/tests/io/test_timeseries_import.py +++ b/tests/io/test_timeseries_import.py @@ -229,7 +229,6 @@ def test_get_district_heating_heat_demand_profiles(self): @pytest.mark.local def test_get_cts_profiles_per_building(self): - edisgo_object = EDisGo( ding0_grid=pytest.ding0_test_network_3_path, legacy_ding0_grids=False ) @@ -251,7 +250,6 @@ def test_get_cts_profiles_per_building(self): @pytest.mark.local def test_get_cts_profiles_per_grid(self): - df = timeseries_import.get_cts_profiles_per_grid( 33535, "eGon2035", "heat", pytest.engine ) @@ -270,6 +268,13 @@ def test_get_residential_electricity_profiles_per_building(self): assert df.shape == (8760, 1) assert np.isclose(df.loc[:, 442081].sum(), 3.20688, atol=1e-3) + # test with status quo + df = timeseries_import.get_residential_electricity_profiles_per_building( + [-1, 442081], "eGon2021", pytest.engine + ) + assert df.shape == (8760, 1) + assert np.isclose(df.loc[:, 442081].sum(), 4.288845, atol=1e-3) + @pytest.mark.local def test_get_industrial_electricity_profiles_per_site(self): # test with one site and one OSM area @@ -285,3 +290,11 @@ def test_get_industrial_electricity_profiles_per_site(self): [541658], "eGon2035", pytest.engine ) assert df.shape == (8760, 1) + + # test with status quo + df = timeseries_import.get_industrial_electricity_profiles_per_site( + [1, 541658], "eGon2021", pytest.engine + ) + assert df.shape == (8760, 2) + assert np.isclose(df.loc[:, 1].sum(), 31655.640, atol=1e-3) + assert np.isclose(df.loc[:, 541658].sum(), 2910.816, atol=1e-3) From f3eab586d3681fd856948ac3e12e1073ec88079f Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 6 Dec 2023 15:54:26 -0800 Subject: [PATCH 18/18] Add todo --- edisgo/network/topology.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/edisgo/network/topology.py b/edisgo/network/topology.py index 28cefb68..31a54dba 100755 --- a/edisgo/network/topology.py +++ b/edisgo/network/topology.py @@ -2586,6 +2586,8 @@ def _connect_mv_bus_to_target_object( num_parallel=number_parallel_lines, ) # add line to equipment changes + # ToDo number_parallel_lines should be given to + # _add_line_to_equipment_changes edisgo_object.results._add_line_to_equipment_changes( line=self.lines_df.loc[new_line_name, :], )