From 34c30b422c621636f9804db83521a554e206f5e5 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 26 Oct 2023 20:17:51 +0200 Subject: [PATCH 01/46] 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 b93e0f274..6a054a386 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 5ea720064..63e1a0bd4 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/46] 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 6a054a386..eb9ba090e 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/46] 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 eb9ba090e..d611ea5e8 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/46] 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 978f5241b..1100bf0b3 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/46] 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 d611ea5e8..6de6529fc 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/46] 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 0f7f171a5..ee70c69fe 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 6de6529fc..aa8cac046 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/46] 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 ee70c69fe..05db76eff 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/46] 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 8bd63d9b7..b3b711f35 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/46] 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 aa8cac046..4be4f44df 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/46] 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 b3b711f35..f6b98a624 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/46] 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 f6b98a624..d1aa910d0 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/46] 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 d1aa910d0..2079eb777 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/46] 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 05db76eff..0a6b222fc 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/46] 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 16449bd17..b0fb22781 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/46] 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 5b1a62a55..033009119 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/46] 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 033009119..5a4cbcbb8 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/46] 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 93d461b82..83e4afc65 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 d56f33505..93825e9ac 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/46] 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 28cefb687..31a54dbac 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, :], ) From c31aa3881d3a04f35a59161e7465c7f4375cde38 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 1 Feb 2024 13:00:25 -0800 Subject: [PATCH 19/46] Revert changes to reinforcement --- edisgo/flex_opt/reinforce_grid.py | 27 +- edisgo/flex_opt/reinforce_measures.py | 464 +++++++++------------- tests/flex_opt/test_reinforce_measures.py | 65 ++- 3 files changed, 224 insertions(+), 332 deletions(-) diff --git a/edisgo/flex_opt/reinforce_grid.py b/edisgo/flex_opt/reinforce_grid.py index 54bddfce4..a8be78421 100644 --- a/edisgo/flex_opt/reinforce_grid.py +++ b/edisgo/flex_opt/reinforce_grid.py @@ -842,7 +842,6 @@ 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: """ @@ -883,12 +882,7 @@ 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. 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. + the grid is separated. kwargs : dict Keyword arguments can be all parameters of function :func:`edisgo.flex_opt.reinforce_grid.reinforce_grid`, except @@ -902,7 +896,6 @@ 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 @@ -912,11 +905,7 @@ 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, - use_standard_line_type=use_standard_line_type, - ) + run_separate_lv_grids(edisgo_object, threshold=separation_threshold) logger.info("Run initial grid reinforcement for single LV grids.") for lv_grid in list(edisgo_object.topology.mv_grid.lv_grids): @@ -1093,11 +1082,7 @@ def enhanced_reinforce_grid( return edisgo_object -def run_separate_lv_grids( - edisgo_obj: EDisGo, - threshold: int | float = 2, - use_standard_line_type: bool = True, -) -> None: +def run_separate_lv_grids(edisgo_obj: EDisGo, threshold: int | float = 2) -> None: """ Separate all highly overloaded LV grids within the MV grid. @@ -1115,10 +1100,6 @@ def run_separate_lv_grids( 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 ------- @@ -1193,7 +1174,7 @@ def run_separate_lv_grids( 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, use_standard_line_type + edisgo_obj, lv_grid ) if len(lines_changes) > 0: _add_lines_changes_to_equipment_changes( diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index 4be4f44df..6a054a386 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -342,92 +342,70 @@ 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): +def reinforce_lines_voltage_issues(edisgo_obj, grid, crit_nodes): """ - Get standard line type for given voltage level from config. + Reinforce lines in MV and LV topology due to voltage issues. 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. + crit_nodes : :pandas:`pandas.DataFrame` + Dataframe with maximum deviations from allowed lower or upper voltage limits + in p.u. for all buses in specified grid. For more information on dataframe see + :attr:`~.flex_opt.check_tech_constraints.voltage_issues`. 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 - + ------- + dict + Dictionary with name of lines as keys and the corresponding number of + lines added as values. -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. + Notes + ----- + Reinforce measures: - 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. + 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 - 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 + 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 feeder can only be split at LV stations because they + + 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 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. + 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. """ - standard_line = get_standard_line(edisgo_obj, grid=grid) - lines_changes = {} + + # 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.") # find path to each node in order to find node with voltage issues farthest - # away from station + # away from station in each feeder + station_node = grid.transformers_df.bus1.iloc[0] graph = grid.graph - station_node = grid.station.index[0] paths = {} - for node in crit_nodes_in_feeder: + 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 @@ -438,167 +416,18 @@ def split_feeder_at_given_length( "Voltage issues at busbar in LV network {} should have " "been solved in previous steps.".format(grid) ) + nodes_feeder.setdefault(path[1], []).append(node) - # 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. - - Parameters - ---------- - edisgo_obj : :class:`~.EDisGo` - grid : :class:`~.network.grids.MVGrid` or :class:`~.network.grids.LVGrid` - crit_nodes : :pandas:`pandas.DataFrame` - Dataframe with maximum deviations from allowed lower or upper voltage limits - in p.u. for all buses in specified grid. For more information on dataframe see - :attr:`~.flex_opt.check_tech_constraints.voltage_issues`. - - Returns - ------- - dict - Dictionary with name of lines as keys and the corresponding number of - lines added as values. - - Notes - ----- - Reinforce measures: - - 1. For LV only, exchange all cables in feeder by standard cable if smaller cable is - currently used. - 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) - - # 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 - - lines_changes = {} - for repr_node in crit_feeders: - if isinstance(grid, LVGrid): + 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(): + 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"] @@ -653,21 +482,122 @@ def reinforce_lines_voltage_issues(edisgo_obj, grid, crit_nodes): disconnect_2_3 = False if disconnect_2_3 is True: - 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, - ) - 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]})." + # 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.update(lines_changes_tmp) + + # 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})." + ) if not lines_changes: logger.debug( @@ -849,9 +779,14 @@ 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" ] - standard_line_type = get_standard_line( - edisgo_obj, nominal_voltage=nominal_voltage - ) + 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" + ] # handling of standard lines lines_standard = relevant_lines.loc[ @@ -886,9 +821,7 @@ def _replace_by_parallel_standard_lines(lines): def separate_lv_grid( - edisgo_obj: EDisGo, - grid: LVGrid, - use_standard_line_type: bool = True, + edisgo_obj: EDisGo, grid: LVGrid ) -> tuple[dict[Any, Any], dict[str, int]]: """ Separate LV grid by adding a new substation and connect half of each feeder. @@ -918,10 +851,6 @@ def separate_lv_grid( ---------- edisgo_obj : :class:`~.EDisGo` grid : :class:`~.network.grids.LVGrid` - 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. Returns ------- @@ -1013,12 +942,13 @@ 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[standard_transformer_name] + ].loc[ + edisgo_obj.config["grid_expansion_standard_equipment"][ + "mv_lv_transformer" + ] + ] except KeyError: raise KeyError("Standard MV/LV transformer is not in the equipment list.") @@ -1030,7 +960,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 = standard_transformer_name + new_transformer_df.type_info = None 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])] @@ -1252,7 +1182,9 @@ def add_standard_transformer( logger.info(f"New LV grid {lv_grid_id_new} added to topology.") - lv_standard_line = get_standard_line(edisgo_obj, nominal_voltage=0.4) + lv_standard_line = edisgo_obj.config["grid_expansion_standard_equipment"][ + "lv_line" + ] # changes on relocated lines to the new LV grid # grid_ids @@ -1271,29 +1203,21 @@ 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_type 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=line_type, - num_parallel=num_parallel, + type_info=lv_standard_line, ) - lines_changes[line_added_lv] = 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"] edisgo_obj.remove_component( comp_type="line", diff --git a/tests/flex_opt/test_reinforce_measures.py b/tests/flex_opt/test_reinforce_measures.py index 63e1a0bd4..5ea720064 100644 --- a/tests/flex_opt/test_reinforce_measures.py +++ b/tests/flex_opt/test_reinforce_measures.py @@ -304,22 +304,15 @@ 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, 0.07], - "time_index": [self.timesteps[0], self.timesteps[0], self.timesteps[0]], + "abs_max_voltage_dev": [0.08, 0.05], + "time_index": [self.timesteps[0], self.timesteps[0]], }, - index=[ - "Bus_BranchTee_LVGrid_5_2", - "Bus_BranchTee_LVGrid_5_3", - "Bus_BranchTee_LVGrid_5_5", - ], + index=["Bus_BranchTee_LVGrid_5_2", "Bus_BranchTee_LVGrid_5_5"], ) grid = self.edisgo.topology.get_lv_grid("LVGrid_5") @@ -328,12 +321,10 @@ def test_reinforce_lines_voltage_issues(self): ) reinforced_lines = lines_changes.keys() - assert len(lines_changes) == 3 + assert len(lines_changes) == 2 assert "Line_50000003" in reinforced_lines - 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 "Line_50000009" in reinforced_lines + # check that LV station is one of the buses assert ( "BusBar_MVGrid_1_LVGrid_5_LV" in self.edisgo.topology.lines_df.loc[ @@ -343,14 +334,14 @@ def test_reinforce_lines_voltage_issues(self): assert ( "BusBar_MVGrid_1_LVGrid_5_LV" in self.edisgo.topology.lines_df.loc[ - "Line_50000006", ["bus0", "bus1"] + "Line_50000009", ["bus0", "bus1"] ].values ) # check other bus assert ( - "Bus_BranchTee_LVGrid_5_3" + "Bus_BranchTee_LVGrid_5_5" in self.edisgo.topology.lines_df.loc[ - "Line_50000006", ["bus0", "bus1"] + "Line_50000009", ["bus0", "bus1"] ].values ) assert ( @@ -359,30 +350,26 @@ def test_reinforce_lines_voltage_issues(self): "Line_50000003", ["bus0", "bus1"] ].values ) - # 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 + # check line parameters std_line = self.edisgo.topology.equipment_data["lv_cables"].loc[ self.edisgo.config["grid_expansion_standard_equipment"]["lv_line"] ] - 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 + 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) def test_reinforce_lines_overloading(self): # * check for needed parallel standard lines (MV and LV) => problems at From 9e2f7f57bec4996c9865cd9db1e57ff8383deddd Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 1 Feb 2024 13:01:38 -0800 Subject: [PATCH 20/46] Revert changes in grid reinforcement --- edisgo/flex_opt/reinforce_measures.py | 270 +++++++++----------------- 1 file changed, 92 insertions(+), 178 deletions(-) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index 6a054a386..b93e0f274 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -365,11 +365,9 @@ def reinforce_lines_voltage_issues(edisgo_obj, grid, crit_nodes): ----- Reinforce measures: - 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 + 1. 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 + 2. 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. @@ -384,18 +382,15 @@ def reinforce_lines_voltage_issues(edisgo_obj, grid, crit_nodes): """ - # load standard line data and set reinforce measure to exchange small cables by - # standard cables to True in case of LV grids + # load standard line data 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.") @@ -419,185 +414,104 @@ 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(): - 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 + # 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 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 + # 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 - 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 node_2_3 is not a representative, disconnect line + # if critical line is not yet a standard line replace old + # line by a standard 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] + # 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 - # 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 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 if not lines_changes: logger.debug( From 6a6feb00e751434df37f594fb5832db208f6ccd2 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 1 Feb 2024 14:39:43 -0800 Subject: [PATCH 21/46] Fix voltage level for HV/MV transformers --- edisgo/flex_opt/costs.py | 14 ++++++++------ tests/flex_opt/test_costs.py | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/edisgo/flex_opt/costs.py b/edisgo/flex_opt/costs.py index 2079eb777..4f512905b 100644 --- a/edisgo/flex_opt/costs.py +++ b/edisgo/flex_opt/costs.py @@ -67,7 +67,8 @@ def _get_transformer_costs(trafos): costs_trafos = pd.DataFrame( { "costs_transformers": len(hvmv_trafos) - * [float(edisgo_obj.config["costs_transformers"]["mv"])] + * [float(edisgo_obj.config["costs_transformers"]["mv"])], + "voltage_level": len(hvmv_trafos) * ["hv/mv"], }, index=hvmv_trafos, ) @@ -77,13 +78,14 @@ def _get_transformer_costs(trafos): pd.DataFrame( { "costs_transformers": len(mvlv_trafos) - * [float(edisgo_obj.config["costs_transformers"]["lv"])] + * [float(edisgo_obj.config["costs_transformers"]["lv"])], + "voltage_level": len(hvmv_trafos) * ["mv/lv"], }, index=mvlv_trafos, ), ] ) - return costs_trafos.loc[trafos.index, "costs_transformers"].values + return costs_trafos.loc[trafos.index, :] def _get_line_costs(lines_added): costs_lines = line_expansion_costs(edisgo_obj, lines_added.index) @@ -128,16 +130,16 @@ 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 + transformer_costs = _get_transformer_costs(trafos) costs = pd.concat( [ costs, pd.DataFrame( { "type": trafos.type_info.values, - "total_costs": _get_transformer_costs(trafos), + "total_costs": transformer_costs.costs_transformers, "quantity": len(trafos) * [1], - "voltage_level": len(trafos) * ["mv/lv"], + "voltage_level": transformer_costs.voltage_level, }, index=trafos.index, ), diff --git a/tests/flex_opt/test_costs.py b/tests/flex_opt/test_costs.py index 813714aa8..ed609326e 100644 --- a/tests/flex_opt/test_costs.py +++ b/tests/flex_opt/test_costs.py @@ -81,7 +81,7 @@ def test_costs(self): assert len(costs) == 4 assert ( costs.loc["MVStation_1_transformer_reinforced_2", "voltage_level"] - == "mv/lv" + == "hv/mv" ) assert costs.loc["MVStation_1_transformer_reinforced_2", "quantity"] == 1 assert costs.loc["MVStation_1_transformer_reinforced_2", "total_costs"] == 1000 From 39565ce83cf985a841567d82830be51720ef4138 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 1 Feb 2024 14:40:01 -0800 Subject: [PATCH 22/46] Fix test as costs calculation changed --- tests/flex_opt/test_costs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/flex_opt/test_costs.py b/tests/flex_opt/test_costs.py index ed609326e..5f3077695 100644 --- a/tests/flex_opt/test_costs.py +++ b/tests/flex_opt/test_costs.py @@ -97,8 +97,8 @@ def test_costs(self): assert costs.loc["Line_10019", "type"] == "48-AL1/8-ST1A" assert costs.loc["Line_10019", "voltage_level"] == "mv" assert np.isclose(costs.loc["Line_50000002", "total_costs"], 2.34) - assert np.isclose(costs.loc["Line_50000002", "length"], 0.09) - assert costs.loc["Line_50000002", "quantity"] == 3 + assert np.isclose(costs.loc["Line_50000002", "length"], 0.03) + assert costs.loc["Line_50000002", "quantity"] == 1 assert costs.loc["Line_50000002", "type"] == "NAYY 4x1x35" assert costs.loc["Line_50000002", "voltage_level"] == "lv" From 65b82a546e0b227a29e330191b247c9746b503b4 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 1 Feb 2024 14:52:28 -0800 Subject: [PATCH 23/46] Add test for transformer_expansion_costs --- tests/flex_opt/test_costs.py | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/tests/flex_opt/test_costs.py b/tests/flex_opt/test_costs.py index 5f3077695..308f9e7e6 100644 --- a/tests/flex_opt/test_costs.py +++ b/tests/flex_opt/test_costs.py @@ -3,7 +3,7 @@ import pytest from edisgo import EDisGo -from edisgo.flex_opt.costs import grid_expansion_costs, line_expansion_costs +from edisgo.flex_opt import costs as costs_mod class TestCosts: @@ -76,7 +76,7 @@ def test_costs(self): ], ) - costs = grid_expansion_costs(self.edisgo) + costs = costs_mod.grid_expansion_costs(self.edisgo) assert len(costs) == 4 assert ( @@ -103,7 +103,7 @@ def test_costs(self): assert costs.loc["Line_50000002", "voltage_level"] == "lv" def test_line_expansion_costs(self): - costs = line_expansion_costs(self.edisgo) + costs = costs_mod.line_expansion_costs(self.edisgo) assert len(costs) == len(self.edisgo.topology.lines_df) assert (costs.index == self.edisgo.topology.lines_df.index).all() assert len(costs[costs.voltage_level == "mv"]) == len( @@ -116,7 +116,9 @@ def test_line_expansion_costs(self): assert np.isclose(costs.at["Line_10000015", "costs_cable"], 0.27) assert costs.at["Line_10000015", "voltage_level"] == "lv" - costs = line_expansion_costs(self.edisgo, ["Line_10003", "Line_10000015"]) + costs = costs_mod.line_expansion_costs( + self.edisgo, ["Line_10003", "Line_10000015"] + ) assert len(costs) == 2 assert (costs.index.values == ["Line_10003", "Line_10000015"]).all() assert np.isclose(costs.at["Line_10003", "costs_earthworks"], 0.083904 * 60) @@ -125,3 +127,28 @@ def test_line_expansion_costs(self): assert np.isclose(costs.at["Line_10000015", "costs_earthworks"], 1.53) assert np.isclose(costs.at["Line_10000015", "costs_cable"], 0.27) assert costs.at["Line_10000015", "voltage_level"] == "lv" + + def test_transformer_expansion_costs(self): + costs = costs_mod.transformer_expansion_costs(self.edisgo) + transformers_df = pd.concat( + [ + self.edisgo.topology.transformers_df, + self.edisgo.topology.transformers_hvmv_df, + ] + ) + assert len(costs) == len(transformers_df) + assert sorted(costs.index) == sorted(transformers_df.index) + assert len(costs[costs.voltage_level == "hv/mv"]) == len( + self.edisgo.topology.transformers_hvmv_df + ) + assert np.isclose(costs.at["MVStation_1_transformer_1", "costs"], 1000) + assert costs.at["MVStation_1_transformer_1", "voltage_level"] == "hv/mv" + assert np.isclose(costs.at["LVStation_4_transformer_2", "costs"], 10) + assert costs.at["LVStation_4_transformer_2", "voltage_level"] == "mv/lv" + + costs = costs_mod.transformer_expansion_costs( + self.edisgo, ["LVStation_5_transformer_1"] + ) + assert len(costs) == 1 + assert np.isclose(costs.at["LVStation_5_transformer_1", "costs"], 10) + assert costs.at["LVStation_5_transformer_1", "voltage_level"] == "mv/lv" From b6745734849aa28b46fe8f7b920a60c986573916 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 1 Feb 2024 15:20:36 -0800 Subject: [PATCH 24/46] Remove adding split lines to equipment changes as there are no additional costs for the split lines --- edisgo/network/topology.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/edisgo/network/topology.py b/edisgo/network/topology.py index 49577df92..f7ac0bf99 100755 --- a/edisgo/network/topology.py +++ b/edisgo/network/topology.py @@ -2555,10 +2555,6 @@ def _connect_mv_bus_to_target_object( # switch data if switch_bus and switch_bus == line_data.bus0: self.switches_df.loc[switch_data.name, "branch"] = line_name_bus0 - # add line to equipment changes - edisgo_object.results._add_line_to_equipment_changes( - line=self.lines_df.loc[line_name_bus0, :], - ) # add new line between newly created branch tee and line's bus0 line_length = geo.calc_geo_dist_vincenty( @@ -2584,10 +2580,6 @@ def _connect_mv_bus_to_target_object( # switch data if switch_bus and switch_bus == line_data.bus1: self.switches_df.loc[switch_data.name, "branch"] = line_name_bus1 - # add line to equipment changes - edisgo_object.results._add_line_to_equipment_changes( - line=self.lines_df.loc[line_name_bus1, :], - ) # add new line for new bus line_length = geo.calc_geo_dist_vincenty( From fb4e655cba6d864e235868e3b9709e21a5e8782e Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 1 Feb 2024 15:21:08 -0800 Subject: [PATCH 25/46] Use number of parallel lines from line information in lines_df --- edisgo/network/results.py | 2 +- edisgo/network/topology.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/edisgo/network/results.py b/edisgo/network/results.py index 8f6e8b044..151fbd480 100755 --- a/edisgo/network/results.py +++ b/edisgo/network/results.py @@ -628,7 +628,7 @@ def _add_line_to_equipment_changes(self, line): "iteration_step": [0], "change": ["added"], "equipment": [line.type_info], - "quantity": [1], + "quantity": [line.num_parallel], }, index=[line.name], ), diff --git a/edisgo/network/topology.py b/edisgo/network/topology.py index f7ac0bf99..3cd9c6706 100755 --- a/edisgo/network/topology.py +++ b/edisgo/network/topology.py @@ -1973,7 +1973,7 @@ def connect_to_mv(self, edisgo_object, comp_data, comp_type="generator"): # object) comp_connected = False for dist_min_obj in conn_objects_min_stack: - # do not allow connection to virtual busses + # do not allow connection to virtual buses if "virtual" not in dist_min_obj["repr"]: line_type, num_parallel = select_cable(edisgo_object, "mv", power) target_obj_result = self._connect_mv_bus_to_target_object( @@ -2602,8 +2602,6 @@ 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, :], ) From 5d1c5bddc18a00e74adddbbba60c02b7cf16f6b0 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 1 Feb 2024 15:44:32 -0800 Subject: [PATCH 26/46] Bug fix use mvlv_trafos dataframe --- edisgo/flex_opt/costs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edisgo/flex_opt/costs.py b/edisgo/flex_opt/costs.py index 4f512905b..bb302daa8 100644 --- a/edisgo/flex_opt/costs.py +++ b/edisgo/flex_opt/costs.py @@ -79,7 +79,7 @@ def _get_transformer_costs(trafos): { "costs_transformers": len(mvlv_trafos) * [float(edisgo_obj.config["costs_transformers"]["lv"])], - "voltage_level": len(hvmv_trafos) * ["mv/lv"], + "voltage_level": len(mvlv_trafos) * ["mv/lv"], }, index=mvlv_trafos, ), From 2fde1629282aec84bfc2c63c1cba9094cdf4f608 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 1 Feb 2024 16:22:24 -0800 Subject: [PATCH 27/46] Adapt setup_logger tests --- edisgo/tools/logger.py | 2 +- tests/tools/test_logger.py | 49 ++++++++++++++++++++------------------ 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/edisgo/tools/logger.py b/edisgo/tools/logger.py index 5a4cbcbb8..20514f947 100644 --- a/edisgo/tools/logger.py +++ b/edisgo/tools/logger.py @@ -140,9 +140,9 @@ def create_home_dir(): log_dir = os.path.join( get_default_root_dir(), cfg_edisgo.get("user_dirs", "log_dir") ) - create_dir(log_dir) if log_dir is not None: + create_dir(log_dir) file_name = os.path.join(log_dir, file_name) if reset_loggers: diff --git a/tests/tools/test_logger.py b/tests/tools/test_logger.py index 80e372eef..947acb763 100644 --- a/tests/tools/test_logger.py +++ b/tests/tools/test_logger.py @@ -6,60 +6,63 @@ class TestClass: def test_setup_logger(self): - def check_file_output(output): - with open("edisgo.log") as file: + def check_file_output(filename, output): + with open(filename) as file: last_line = file.readlines()[-1].split(" ")[3:] last_line = " ".join(last_line) assert last_line == output def reset_loggers(): logger = logging.getLogger("edisgo") - logger.propagate = True - logger.handlers.clear() - logger = logging.getLogger() logger.handlers.clear() + logger.propagate = True - if os.path.exists("edisgo.log"): - os.remove("edisgo.log") + filename = os.path.join( + os.path.expanduser("~"), ".edisgo", "log", "test_log.log" + ) + if os.path.exists(filename): + os.remove(filename) setup_logger( loggers=[ {"name": "root", "file_level": "debug", "stream_level": "debug"}, {"name": "edisgo", "file_level": "debug", "stream_level": "debug"}, ], - file_name="edisgo.log", + file_name="test_log.log", + log_dir="default", ) logger = logging.getLogger("edisgo") # Test that edisgo logger writes to file. logger.debug("root") - check_file_output("edisgo - DEBUG: root\n") + check_file_output(filename, "edisgo - DEBUG: root\n") # Test that root logger writes to file. logging.debug("root") - check_file_output("root - DEBUG: root\n") + check_file_output(filename, "root - DEBUG: root\n") - # reset_loggers() + reset_loggers() + os.remove(filename) setup_logger( loggers=[ {"name": "edisgo", "file_level": "debug", "stream_level": "debug"}, ], - file_name="edisgo.log", reset_loggers=True, debug_message=True, ) logger = logging.getLogger("edisgo") + + filename = [_ for _ in os.listdir(os.getcwd()) if ".log" in _] + # if not 1 there are several log files, which shouldn't be the case and could + # mess up the following tests + assert len(filename) == 1 + filename = filename[0] # Test that edisgo logger writes to file. logger.debug("edisgo") - check_file_output("edisgo - DEBUG: edisgo\n") - # Test that root logger doesn't writes to file. - logging.debug("edisgo") - check_file_output("edisgo - DEBUG: edisgo\n") - - @classmethod - def teardown_class(cls): - logger = logging.getLogger("edisgo") - logger.handlers.clear() - logger.propagate = True + check_file_output(filename, "edisgo - DEBUG: edisgo\n") + # Test that root logger doesn't write to file. + logging.debug("root") + check_file_output(filename, "edisgo - DEBUG: edisgo\n") - os.remove("edisgo.log") + reset_loggers() + os.remove(filename) From 120da3388a1ee30159c98afd71702bb4f920887a Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 1 Feb 2024 16:32:04 -0800 Subject: [PATCH 28/46] Fix test - transformer costs were previously not included as transformer names were written with lower key t --- tests/flex_opt/test_reinforce_grid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/flex_opt/test_reinforce_grid.py b/tests/flex_opt/test_reinforce_grid.py index cb6074310..e073b2326 100644 --- a/tests/flex_opt/test_reinforce_grid.py +++ b/tests/flex_opt/test_reinforce_grid.py @@ -81,7 +81,7 @@ def test_run_separate_lv_grids(self): assert len(g.buses_df) > 1 assert len(lv_grids_new) == 26 - assert np.isclose(edisgo.results.grid_expansion_costs.total_costs.sum(), 280.06) + assert np.isclose(edisgo.results.grid_expansion_costs.total_costs.sum(), 440.06) # check if all generators are still present assert np.isclose( From d5d40001c1734ba82e0238a962a211f655d4fc47 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 1 Feb 2024 16:50:49 -0800 Subject: [PATCH 29/46] Fix tests due to changes costs calculation --- tests/test_edisgo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_edisgo.py b/tests/test_edisgo.py index bac0789cb..8e9b3f985 100755 --- a/tests/test_edisgo.py +++ b/tests/test_edisgo.py @@ -520,7 +520,7 @@ def test_reinforce_catch_convergence(self): ) results = self.edisgo.reinforce(catch_convergence_problems=True) assert results.unresolved_issues.empty - assert len(results.grid_expansion_costs) == 132 + assert len(results.grid_expansion_costs) == 134 assert len(results.equipment_changes) == 218 assert results.v_res.shape == (4, 142) @@ -542,7 +542,7 @@ def test_enhanced_reinforce_grid(self): results = edisgo_obj.results - assert len(results.grid_expansion_costs) == 445 + assert len(results.grid_expansion_costs) == 454 assert len(results.equipment_changes) == 892 assert results.v_res.shape == (4, 148) From d361a18fa1abfaba61e7097534d2415186b882a4 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 2 Feb 2024 16:10:30 -0800 Subject: [PATCH 30/46] Make different runs for linux and windows tests to include runonlinux tests when run on linux --- .github/workflows/tests-coverage.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests-coverage.yml b/.github/workflows/tests-coverage.yml index e0a72b1cd..f288bfe5c 100644 --- a/.github/workflows/tests-coverage.yml +++ b/.github/workflows/tests-coverage.yml @@ -58,8 +58,14 @@ jobs: environment-file: eDisGo_env_dev.yml python-version: ${{ matrix.python-version }} - - name: Run tests - if: ${{ !(runner.os == 'Linux' && matrix.python-version == 3.8 && matrix.name-suffix == 'coverage') }} + - name: Run tests Linux + if: runner.os == 'Linux' && matrix.name-suffix != 'coverage' + run: | + python -m pip install pytest pytest-notebook + python -m pytest --runslow --runonlinux --disable-warnings --color=yes -v + + - name: Run tests Windows + if: runner.os == 'Windows' run: | python -m pip install pytest pytest-notebook python -m pytest --runslow --disable-warnings --color=yes -v From 2b2f143f280bae14ff4850cacca597acef4e1364 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 2 Feb 2024 16:11:41 -0800 Subject: [PATCH 31/46] Make separate test for when logging file is written to user dir as this leads to problems on github with os Windows --- tests/tools/test_logger.py | 45 ++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/tests/tools/test_logger.py b/tests/tools/test_logger.py index 947acb763..2da119846 100644 --- a/tests/tools/test_logger.py +++ b/tests/tools/test_logger.py @@ -1,22 +1,26 @@ import logging import os +import pytest + from edisgo.tools.logger import setup_logger -class TestClass: - def test_setup_logger(self): - def check_file_output(filename, output): - with open(filename) as file: - last_line = file.readlines()[-1].split(" ")[3:] - last_line = " ".join(last_line) - assert last_line == output +def check_file_output(filename, output): + with open(filename) as file: + last_line = file.readlines()[-1].split(" ")[3:] + last_line = " ".join(last_line) + assert last_line == output + + +def reset_loggers(): + logger = logging.getLogger("edisgo") + logger.handlers.clear() + logger.propagate = True - def reset_loggers(): - logger = logging.getLogger("edisgo") - logger.handlers.clear() - logger.propagate = True +class TestClass: + def test_setup_logger(self): filename = os.path.join( os.path.expanduser("~"), ".edisgo", "log", "test_log.log" ) @@ -43,6 +47,19 @@ def reset_loggers(): reset_loggers() os.remove(filename) + @pytest.mark.runonlinux + def test_setup_logger_2(self): + """ + This test is only run on linux, as the log file is written to the user + home directory, which is not allowed when tests are run on github. + + """ + + # delete any existing log files + log_files = [_ for _ in os.listdir(os.getcwd()) if ".log" in _] + for log_file in log_files: + os.remove(log_file) + setup_logger( loggers=[ {"name": "edisgo", "file_level": "debug", "stream_level": "debug"}, @@ -52,11 +69,7 @@ def reset_loggers(): ) logger = logging.getLogger("edisgo") - filename = [_ for _ in os.listdir(os.getcwd()) if ".log" in _] - # if not 1 there are several log files, which shouldn't be the case and could - # mess up the following tests - assert len(filename) == 1 - filename = filename[0] + filename = [_ for _ in os.listdir(os.getcwd()) if ".log" in _][0] # Test that edisgo logger writes to file. logger.debug("edisgo") check_file_output(filename, "edisgo - DEBUG: edisgo\n") From dd2099d4d14d84d134befbb9e8d68bdf38a60817 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 2 Feb 2024 16:33:04 -0800 Subject: [PATCH 32/46] Add abstract Topology class to allow for a new Topology class containing only original Topology data --- edisgo/network/topology.py | 252 +++++++++++++++++++------------------ 1 file changed, 133 insertions(+), 119 deletions(-) diff --git a/edisgo/network/topology.py b/edisgo/network/topology.py index 3cd9c6706..f2a653b89 100755 --- a/edisgo/network/topology.py +++ b/edisgo/network/topology.py @@ -5,6 +5,7 @@ import random import warnings +from abc import ABC from zipfile import ZipFile import networkx as nx @@ -77,132 +78,16 @@ } -class Topology: +class TopologyBase(ABC): """ - Container for all grid topology data of a single MV grid. + Base class for container for all grid topology data of a single MV grid. Data may as well include grid topology data of underlying LV grids. - Other Parameters - ----------------- - config : None or :class:`~.tools.config.Config` - Provide your configurations if you want to load self-provided equipment - data. Path to csv files containing the technical data is set in - `config_system.cfg` in sections `system_dirs` and `equipment`. - The default is None in which case the equipment data provided by - eDisGo is used. - """ def __init__(self, **kwargs): - # load technical data of equipment - self._equipment_data = self._load_equipment_data(kwargs.get("config", None)) - - @staticmethod - def _load_equipment_data(config=None): - """ - Load equipment data for transformers, cables etc. - - Parameters - ----------- - config : :class:`~.tools.config.Config` - Config object with configuration data from config files. - - Returns - ------- - dict - Dictionary with :pandas:`pandas.DataFrame` containing - equipment data. Keys of the dictionary are 'mv_transformers', - 'mv_overhead_lines', 'mv_cables', 'lv_transformers', and - 'lv_cables'. - - Notes - ------ - This function calculates electrical values of transformers from - standard values (so far only for MV/LV transformers, not necessary for - HV/MV transformers as MV impedances are not used). - - $z_{pu}$ is calculated as follows: - - .. math:: z_{pu} = \frac{u_{kr}}{100} - - using the following simplification: - - .. math:: z_{pu} = \frac{Z}{Z_{nom}} - - with - - .. math:: Z = \frac{u_{kr}}{100} \\cdot \frac{U_n^2}{S_{nom}} - - and - - .. math:: Z_{nom} = \frac{U_n^2}{S_{nom}} - - $r_{pu}$ is calculated as follows: - - .. math:: r_{pu} = \frac{P_k}{S_{nom}} - - using the simplification of - - .. math:: r_{pu} = \frac{R}{Z_{nom}} - - with - - .. math:: R = \frac{P_k}{3 I_{nom}^2} = P_k \\cdot \frac{U_{nom}^2}{S_{nom}^2} - - $x_{pu}$ is calculated as follows: - - .. math:: x_{pu} = \\sqrt(z_{pu}^2-r_{pu}^2) - - """ - - equipment = { - "mv": ["transformers", "overhead_lines", "cables"], - "lv": ["transformers", "cables"], - } - - # if config is not provided set default path and filenames - if config is None: - equipment_dir = "equipment" - config = {} - for voltage_level, eq_list in equipment.items(): - for i in eq_list: - config[ - "equipment_{}_parameters_{}".format(voltage_level, i) - ] = "equipment-parameters_{}_{}.csv".format( - voltage_level.upper(), i - ) - else: - equipment_dir = config["system_dirs"]["equipment_dir"] - config = config["equipment"] - - package_path = edisgo.__path__[0] - data = {} - - for voltage_level, eq_list in equipment.items(): - for i in eq_list: - equipment_parameters = config[ - "equipment_{}_parameters_{}".format(voltage_level, i) - ] - data["{}_{}".format(voltage_level, i)] = pd.read_csv( - os.path.join(package_path, equipment_dir, equipment_parameters), - comment="#", - index_col="name", - delimiter=",", - decimal=".", - ) - # calculate electrical values of transformer from standard - # values (so far only for LV transformers, not necessary for - # MV as MV impedances are not used) - if voltage_level == "lv" and i == "transformers": - name = f"{voltage_level}_{i}" - - data[name]["r_pu"] = data[name]["P_k"] / data[name]["S_nom"] - - data[name]["x_pu"] = np.sqrt( - (data[name]["u_kr"] / 100) ** 2 - data[name]["r_pu"] ** 2 - ) - return data + pass @property def loads_df(self): @@ -631,6 +516,135 @@ def switches_df(self): def switches_df(self, df): self._switches_df = df + +class Topology(TopologyBase): + """ + Container for all grid topology data of a single MV grid. + + Data may as well include grid topology data of underlying LV grids. + + Other Parameters + ----------------- + config : None or :class:`~.tools.config.Config` + Provide your configurations if you want to load self-provided equipment + data. Path to csv files containing the technical data is set in + `config_system.cfg` in sections `system_dirs` and `equipment`. + The default is None in which case the equipment data provided by + eDisGo is used. + + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + # load technical data of equipment + self._equipment_data = self._load_equipment_data(kwargs.get("config", None)) + + @staticmethod + def _load_equipment_data(config=None): + """ + Load equipment data for transformers, cables etc. + + Parameters + ----------- + config : :class:`~.tools.config.Config` + Config object with configuration data from config files. + + Returns + ------- + dict + Dictionary with :pandas:`pandas.DataFrame` containing + equipment data. Keys of the dictionary are 'mv_transformers', + 'mv_overhead_lines', 'mv_cables', 'lv_transformers', and + 'lv_cables'. + + Notes + ------ + This function calculates electrical values of transformers from + standard values (so far only for MV/LV transformers, not necessary for + HV/MV transformers as MV impedances are not used). + + $z_{pu}$ is calculated as follows: + + .. math:: z_{pu} = \frac{u_{kr}}{100} + + using the following simplification: + + .. math:: z_{pu} = \frac{Z}{Z_{nom}} + + with + + .. math:: Z = \frac{u_{kr}}{100} \\cdot \frac{U_n^2}{S_{nom}} + + and + + .. math:: Z_{nom} = \frac{U_n^2}{S_{nom}} + + $r_{pu}$ is calculated as follows: + + .. math:: r_{pu} = \frac{P_k}{S_{nom}} + + using the simplification of + + .. math:: r_{pu} = \frac{R}{Z_{nom}} + + with + + .. math:: R = \frac{P_k}{3 I_{nom}^2} = P_k \\cdot \frac{U_{nom}^2}{S_{nom}^2} + + $x_{pu}$ is calculated as follows: + + .. math:: x_{pu} = \\sqrt(z_{pu}^2-r_{pu}^2) + + """ + + equipment = { + "mv": ["transformers", "overhead_lines", "cables"], + "lv": ["transformers", "cables"], + } + + # if config is not provided set default path and filenames + if config is None: + equipment_dir = "equipment" + config = {} + for voltage_level, eq_list in equipment.items(): + for i in eq_list: + config[ + "equipment_{}_parameters_{}".format(voltage_level, i) + ] = "equipment-parameters_{}_{}.csv".format( + voltage_level.upper(), i + ) + else: + equipment_dir = config["system_dirs"]["equipment_dir"] + config = config["equipment"] + + package_path = edisgo.__path__[0] + data = {} + + for voltage_level, eq_list in equipment.items(): + for i in eq_list: + equipment_parameters = config[ + "equipment_{}_parameters_{}".format(voltage_level, i) + ] + data["{}_{}".format(voltage_level, i)] = pd.read_csv( + os.path.join(package_path, equipment_dir, equipment_parameters), + comment="#", + index_col="name", + delimiter=",", + decimal=".", + ) + # calculate electrical values of transformer from standard + # values (so far only for LV transformers, not necessary for + # MV as MV impedances are not used) + if voltage_level == "lv" and i == "transformers": + name = f"{voltage_level}_{i}" + + data[name]["r_pu"] = data[name]["P_k"] / data[name]["S_nom"] + + data[name]["x_pu"] = np.sqrt( + (data[name]["u_kr"] / 100) ** 2 - data[name]["r_pu"] ** 2 + ) + return data + @property def charging_points_df(self): """ From a3d75e4b4ffe90601677701f3c67896edb7d02f3 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 2 Feb 2024 16:51:53 -0800 Subject: [PATCH 33/46] Only remove logging file if permission allows it --- tests/tools/test_logger.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/tools/test_logger.py b/tests/tools/test_logger.py index 2da119846..d9eea4107 100644 --- a/tests/tools/test_logger.py +++ b/tests/tools/test_logger.py @@ -1,8 +1,6 @@ import logging import os -import pytest - from edisgo.tools.logger import setup_logger @@ -13,10 +11,16 @@ def check_file_output(filename, output): assert last_line == output -def reset_loggers(): +def reset_loggers(filename): logger = logging.getLogger("edisgo") logger.handlers.clear() logger.propagate = True + # try removing file - when run on github for Windows removing the file leads + # to a PermissionError + try: + os.remove(filename) + except PermissionError: + pass class TestClass: @@ -44,10 +48,9 @@ def test_setup_logger(self): logging.debug("root") check_file_output(filename, "root - DEBUG: root\n") - reset_loggers() - os.remove(filename) + reset_loggers(filename) - @pytest.mark.runonlinux + # @pytest.mark.runonlinux def test_setup_logger_2(self): """ This test is only run on linux, as the log file is written to the user @@ -77,5 +80,4 @@ def test_setup_logger_2(self): logging.debug("root") check_file_output(filename, "edisgo - DEBUG: edisgo\n") - reset_loggers() - os.remove(filename) + reset_loggers(filename) From 56c201f25dd106545d7342ad67f7810fd5f40829 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 2 Feb 2024 16:52:34 -0800 Subject: [PATCH 34/46] Bug fix - set up julia whenever linux tests are run --- .github/workflows/tests-coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-coverage.yml b/.github/workflows/tests-coverage.yml index f288bfe5c..951972d4d 100644 --- a/.github/workflows/tests-coverage.yml +++ b/.github/workflows/tests-coverage.yml @@ -38,7 +38,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Set up julia - if: runner.os == 'Linux' && matrix.python-version == 3.8 && matrix.name-suffix == 'coverage' + if: runner.os == 'Linux' uses: julia-actions/setup-julia@v1 with: version: "1.6" From 82019fd76569be0a9aec2c83f67c1dd8bdab3580 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 2 Feb 2024 17:20:12 -0800 Subject: [PATCH 35/46] Implement to_csv and from_csv for TopologyBase class --- edisgo/network/topology.py | 334 ++++++++++++++++++++++--------------- 1 file changed, 199 insertions(+), 135 deletions(-) diff --git a/edisgo/network/topology.py b/edisgo/network/topology.py index f2a653b89..970d0e058 100755 --- a/edisgo/network/topology.py +++ b/edisgo/network/topology.py @@ -516,6 +516,165 @@ def switches_df(self): def switches_df(self, df): self._switches_df = df + def to_csv(self, directory): + """ + Exports topology to csv files. + + The following attributes are exported: + + * 'loads_df' : Attribute :py:attr:`~loads_df` is saved to + `loads.csv`. + * 'generators_df' : Attribute :py:attr:`~generators_df` is saved to + `generators.csv`. + * 'storage_units_df' : Attribute :py:attr:`~storage_units_df` is + saved to `storage_units.csv`. + * 'transformers_df' : Attribute :py:attr:`~transformers_df` is saved to + `transformers.csv`. + * 'transformers_hvmv_df' : Attribute :py:attr:`~transformers_df` is + saved to `transformers.csv`. + * 'lines_df' : Attribute :py:attr:`~lines_df` is saved to + `lines.csv`. + * 'buses_df' : Attribute :py:attr:`~buses_df` is saved to + `buses.csv`. + * 'switches_df' : Attribute :py:attr:`~switches_df` is saved to + `switches.csv`. + + Attributes are exported in a way that they can be directly imported to + pypsa. + + Parameters + ---------- + directory : str + Path to save topology to. + + """ + os.makedirs(directory, exist_ok=True) + if not self.loads_df.empty: + self.loads_df.to_csv(os.path.join(directory, "loads.csv")) + if not self.generators_df.empty: + self.generators_df.to_csv(os.path.join(directory, "generators.csv")) + if not self.storage_units_df.empty: + self.storage_units_df.to_csv(os.path.join(directory, "storage_units.csv")) + if not self.transformers_df.empty: + self.transformers_df.rename({"x_pu": "x", "r_pu": "r"}, axis=1).to_csv( + os.path.join(directory, "transformers.csv") + ) + if not self.transformers_hvmv_df.empty: + self.transformers_hvmv_df.rename({"x_pu": "x", "r_pu": "r"}, axis=1).to_csv( + os.path.join(directory, "transformers_hvmv.csv") + ) + self.lines_df.to_csv(os.path.join(directory, "lines.csv")) + self.buses_df.to_csv(os.path.join(directory, "buses.csv")) + if not self.switches_df.empty: + self.switches_df.to_csv(os.path.join(directory, "switches.csv")) + + def _get_matching_dict_of_attributes_and_file_names(self): + """ + Helper function that matches attribute names to file names. + + Is used in function :attr:`~.network.topology.TopologyBase.from_csv` to set + which attribute of :class:`~.network.topology.TopologyBase` is saved under + which file name. + + Returns + ------- + dict + Dictionary matching attribute names and file names with attribute + names as keys and corresponding file names as values. + + """ + return { + "buses_df": "buses.csv", + "lines_df": "lines.csv", + "loads_df": "loads.csv", + "generators_df": "generators.csv", + "storage_units_df": "storage_units.csv", + "transformers_df": "transformers.csv", + "transformers_hvmv_df": "transformers_hvmv.csv", + "switches_df": "switches.csv", + } + + def from_csv(self, data_path, edisgo_obj, from_zip_archive=False): + """ + Restores topology from csv files. + + Parameters + ---------- + data_path : str + Path to topology csv files or zip archive. + edisgo_obj : :class:`~.EDisGo` + from_zip_archive : bool + Set to True if data is archived in a zip archive. Default: False. + + """ + # get all attributes and corresponding file names + attrs = self._get_matching_dict_of_attributes_and_file_names() + + if from_zip_archive: + # read from zip archive + # setup ZipFile Class + zip = ZipFile(data_path) + + # get all directories and files within zip archive + files = zip.namelist() + + # add directory to attributes to match zip archive + attrs = {k: f"topology/{v}" for k, v in attrs.items()} + + else: + # read from directory + # check files within the directory + files = os.listdir(data_path) + + attrs_to_read = {k: v for k, v in attrs.items() if v in files} + + for attr, file in attrs_to_read.items(): + if from_zip_archive: + # open zip file to make it readable for pandas + with zip.open(file) as f: + df = pd.read_csv(f, index_col=0) + else: + path = os.path.join(data_path, file) + df = pd.read_csv(path, index_col=0) + + if attr == "generators_df": + # delete slack if it was included + df = df.loc[df.control != "Slack"] + elif "transformers" in attr: + # rename columns to match convention + df = df.rename(columns={"x": "x_pu", "r": "r_pu"}) + elif attr == "network": + # rename columns to match convention + df = df.rename( + columns={ + "mv_grid_district_geom": "geom", + "mv_grid_district_population": "population", + } + ) + + # set grid district information + setattr( + self, + "grid_district", + { + "population": df.population.iat[0], + "geom": wkt_loads(df.geom.iat[0]), + "srid": df.srid.iat[0], + }, + ) + + # set up medium voltage grid + setattr(self, "mv_grid", MVGrid(edisgo_obj=edisgo_obj, id=df.index[0])) + + continue + + # set attribute + setattr(self, attr, df) + + if from_zip_archive: + # make sure to destroy ZipFile Class to close any open connections + zip.close() + class Topology(TopologyBase): """ @@ -2796,55 +2955,21 @@ def to_csv(self, directory): """ Exports topology to csv files. - The following attributes are exported: + Extends function :attr:`~.network.topology.TopologyBase.to_csv`. + + Exports all attributes listed in :attr:`~.network.topology.TopologyBase.to_csv` + plus: - * 'loads_df' : Attribute :py:attr:`~loads_df` is saved to - `loads.csv`. - * 'generators_df' : Attribute :py:attr:`~generators_df` is saved to - `generators.csv`. - * 'storage_units_df' : Attribute :py:attr:`~storage_units_df` is - saved to `storage_units.csv`. - * 'transformers_df' : Attribute :py:attr:`~transformers_df` is saved to - `transformers.csv`. - * 'transformers_hvmv_df' : Attribute :py:attr:`~transformers_df` is - saved to `transformers.csv`. - * 'lines_df' : Attribute :py:attr:`~lines_df` is saved to - `lines.csv`. - * 'buses_df' : Attribute :py:attr:`~buses_df` is saved to - `buses.csv`. - * 'switches_df' : Attribute :py:attr:`~switches_df` is saved to - `switches.csv`. * 'grid_district' : Attribute :py:attr:`~grid_district` is saved to `network.csv`. - Attributes are exported in a way that they can be directly imported to - pypsa. - Parameters ---------- directory : str Path to save topology to. """ - os.makedirs(directory, exist_ok=True) - if not self.loads_df.empty: - self.loads_df.to_csv(os.path.join(directory, "loads.csv")) - if not self.generators_df.empty: - self.generators_df.to_csv(os.path.join(directory, "generators.csv")) - if not self.storage_units_df.empty: - self.storage_units_df.to_csv(os.path.join(directory, "storage_units.csv")) - if not self.transformers_df.empty: - self.transformers_df.rename({"x_pu": "x", "r_pu": "r"}, axis=1).to_csv( - os.path.join(directory, "transformers.csv") - ) - if not self.transformers_hvmv_df.empty: - self.transformers_hvmv_df.rename({"x_pu": "x", "r_pu": "r"}, axis=1).to_csv( - os.path.join(directory, "transformers_hvmv.csv") - ) - self.lines_df.to_csv(os.path.join(directory, "lines.csv")) - self.buses_df.to_csv(os.path.join(directory, "buses.csv")) - if not self.switches_df.empty: - self.switches_df.to_csv(os.path.join(directory, "switches.csv")) + super().to_csv(directory=directory) network = {"name": self.mv_grid.id} network.update(self._grid_district) @@ -2856,10 +2981,40 @@ def to_csv(self, directory): axis=1, ).to_csv(os.path.join(directory, "network.csv")) + def _get_matching_dict_of_attributes_and_file_names(self): + """ + Helper function that matches attribute names to file names. + + Is used in function :attr:`~.network.topology.Topology.from_csv` to set + which attribute of :class:`~.network.topology.Topology` is saved under + which file name. + + Function :attr:`~.network.topology.TopologyBase.\ + _get_matching_dict_of_attributes_and_file_names` is extended by this function. + + Returns + ------- + dict + Dictionary matching attribute names and file names with attribute + names as keys and corresponding file names as values. + + """ + helper_dict = super()._get_matching_dict_of_attributes_and_file_names() + helper_dict.update( + { + "network": "network.csv", + } + ) + return helper_dict + def from_csv(self, data_path, edisgo_obj, from_zip_archive=False): """ Restores topology from csv files. + Extends function :attr:`~.network.topology.TopologyBase.from_csv` by also + importing network.csv and original grid data. + Also performs an integrity check of the imported grid topology data. + Parameters ---------- data_path : str @@ -2869,102 +3024,11 @@ def from_csv(self, data_path, edisgo_obj, from_zip_archive=False): Set to True if data is archived in a zip archive. Default: False. """ - - def _get_matching_dict_of_attributes_and_file_names(): - """ - Helper function that matches attribute names to file names. - - Is used in function :attr:`~.network.topology.Topology.from_csv` to set - which attribute of :class:`~.network.topology.Topology` is saved under - which file name. - - Returns - ------- - dict - Dictionary matching attribute names and file names with attribute - names as keys and corresponding file names as values. - - """ - return { - "buses_df": "buses.csv", - "lines_df": "lines.csv", - "loads_df": "loads.csv", - "generators_df": "generators.csv", - "charging_points_df": "charging_points.csv", - "storage_units_df": "storage_units.csv", - "transformers_df": "transformers.csv", - "transformers_hvmv_df": "transformers_hvmv.csv", - "switches_df": "switches.csv", - "network": "network.csv", - } - - # get all attributes and corresponding file names - attrs = _get_matching_dict_of_attributes_and_file_names() - - if from_zip_archive: - # read from zip archive - # setup ZipFile Class - zip = ZipFile(data_path) - - # get all directories and files within zip archive - files = zip.namelist() - - # add directory to attributes to match zip archive - attrs = {k: f"topology/{v}" for k, v in attrs.items()} - - else: - # read from directory - # check files within the directory - files = os.listdir(data_path) - - attrs_to_read = {k: v for k, v in attrs.items() if v in files} - - for attr, file in attrs_to_read.items(): - if from_zip_archive: - # open zip file to make it readable for pandas - with zip.open(file) as f: - df = pd.read_csv(f, index_col=0) - else: - path = os.path.join(data_path, file) - df = pd.read_csv(path, index_col=0) - - if attr == "generators_df": - # delete slack if it was included - df = df.loc[df.control != "Slack"] - elif "transformers" in attr: - # rename columns to match convention - df = df.rename(columns={"x": "x_pu", "r": "r_pu"}) - elif attr == "network": - # rename columns to match convention - df = df.rename( - columns={ - "mv_grid_district_geom": "geom", - "mv_grid_district_population": "population", - } - ) - - # set grid district information - setattr( - self, - "grid_district", - { - "population": df.population.iat[0], - "geom": wkt_loads(df.geom.iat[0]), - "srid": df.srid.iat[0], - }, - ) - - # set up medium voltage grid - setattr(self, "mv_grid", MVGrid(edisgo_obj=edisgo_obj, id=df.index[0])) - - continue - - # set attribute - setattr(self, attr, df) - - if from_zip_archive: - # make sure to destroy ZipFile Class to close any open connections - zip.close() + super().from_csv( + data_path=data_path, + edisgo_obj=edisgo_obj, + from_zip_archive=from_zip_archive, + ) # Check data integrity self.check_integrity() From c7f6d1ca0ffc867bb24548d9c50767d4e7f20398 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 2 Feb 2024 17:24:47 -0800 Subject: [PATCH 36/46] Add runonlinux again as test fails under Windows on github --- tests/tools/test_logger.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/tools/test_logger.py b/tests/tools/test_logger.py index d9eea4107..eee071df6 100644 --- a/tests/tools/test_logger.py +++ b/tests/tools/test_logger.py @@ -1,6 +1,8 @@ import logging import os +import pytest + from edisgo.tools.logger import setup_logger @@ -50,7 +52,7 @@ def test_setup_logger(self): reset_loggers(filename) - # @pytest.mark.runonlinux + @pytest.mark.runonlinux def test_setup_logger_2(self): """ This test is only run on linux, as the log file is written to the user From 2be4d1e0820488a3047869afa0c5e0afdd494cc2 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 7 Feb 2024 11:45:01 -0800 Subject: [PATCH 37/46] Try running OPF test on Windows --- .github/workflows/tests-coverage.yml | 1 - tests/opf/test_powermodels_opf.py | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/tests-coverage.yml b/.github/workflows/tests-coverage.yml index 951972d4d..d20f6ecd6 100644 --- a/.github/workflows/tests-coverage.yml +++ b/.github/workflows/tests-coverage.yml @@ -38,7 +38,6 @@ jobs: python-version: ${{ matrix.python-version }} - name: Set up julia - if: runner.os == 'Linux' uses: julia-actions/setup-julia@v1 with: version: "1.6" diff --git a/tests/opf/test_powermodels_opf.py b/tests/opf/test_powermodels_opf.py index 4f6482f97..7546d92a4 100644 --- a/tests/opf/test_powermodels_opf.py +++ b/tests/opf/test_powermodels_opf.py @@ -217,7 +217,6 @@ def setup_class(self): df, ) - @pytest.mark.runonlinux def test_pm_optimize(self): # OPF with all flexibilities but without overlying grid constraints pm_optimize( From 9edf382b6f78579c22396e2fd8807b33623fb1f6 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 7 Feb 2024 12:48:16 -0800 Subject: [PATCH 38/46] Revert "Try running OPF test on Windows" This reverts commit 2be4d1e0820488a3047869afa0c5e0afdd494cc2. --- .github/workflows/tests-coverage.yml | 1 + tests/opf/test_powermodels_opf.py | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/tests-coverage.yml b/.github/workflows/tests-coverage.yml index d20f6ecd6..951972d4d 100644 --- a/.github/workflows/tests-coverage.yml +++ b/.github/workflows/tests-coverage.yml @@ -38,6 +38,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Set up julia + if: runner.os == 'Linux' uses: julia-actions/setup-julia@v1 with: version: "1.6" diff --git a/tests/opf/test_powermodels_opf.py b/tests/opf/test_powermodels_opf.py index 7546d92a4..4f6482f97 100644 --- a/tests/opf/test_powermodels_opf.py +++ b/tests/opf/test_powermodels_opf.py @@ -217,6 +217,7 @@ def setup_class(self): df, ) + @pytest.mark.runonlinux def test_pm_optimize(self): # OPF with all flexibilities but without overlying grid constraints pm_optimize( From b38a753d97e7b9b157c61254a3e8745e93690b03 Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 10 Jun 2024 17:34:22 +0200 Subject: [PATCH 39/46] Revert changes in topology --- edisgo/network/topology.py | 750 +++++++++++++++++-------------------- 1 file changed, 347 insertions(+), 403 deletions(-) diff --git a/edisgo/network/topology.py b/edisgo/network/topology.py index 970d0e058..81d4442fa 100755 --- a/edisgo/network/topology.py +++ b/edisgo/network/topology.py @@ -1,11 +1,11 @@ from __future__ import annotations +import copy import logging import os import random import warnings -from abc import ABC from zipfile import ZipFile import networkx as nx @@ -78,16 +78,133 @@ } -class TopologyBase(ABC): +class Topology: """ - Base class for container for all grid topology data of a single MV grid. + Container for all grid topology data of a single MV grid. Data may as well include grid topology data of underlying LV grids. + Other Parameters + ----------------- + config : None or :class:`~.tools.config.Config` + Provide your configurations if you want to load self-provided equipment + data. Path to csv files containing the technical data is set in + `config_system.cfg` in sections `system_dirs` and `equipment`. + The default is None in which case the equipment data provided by + eDisGo is used. + """ def __init__(self, **kwargs): - pass + # load technical data of equipment + self._equipment_data = self._load_equipment_data(kwargs.get("config", None)) + self._original_grid_topology = None + + @staticmethod + def _load_equipment_data(config=None): + """ + Load equipment data for transformers, cables etc. + + Parameters + ----------- + config : :class:`~.tools.config.Config` + Config object with configuration data from config files. + + Returns + ------- + dict + Dictionary with :pandas:`pandas.DataFrame` containing + equipment data. Keys of the dictionary are 'mv_transformers', + 'mv_overhead_lines', 'mv_cables', 'lv_transformers', and + 'lv_cables'. + + Notes + ------ + This function calculates electrical values of transformers from + standard values (so far only for MV/LV transformers, not necessary for + HV/MV transformers as MV impedances are not used). + + $z_{pu}$ is calculated as follows: + + .. math:: z_{pu} = \frac{u_{kr}}{100} + + using the following simplification: + + .. math:: z_{pu} = \frac{Z}{Z_{nom}} + + with + + .. math:: Z = \frac{u_{kr}}{100} \\cdot \frac{U_n^2}{S_{nom}} + + and + + .. math:: Z_{nom} = \frac{U_n^2}{S_{nom}} + + $r_{pu}$ is calculated as follows: + + .. math:: r_{pu} = \frac{P_k}{S_{nom}} + + using the simplification of + + .. math:: r_{pu} = \frac{R}{Z_{nom}} + + with + + .. math:: R = \frac{P_k}{3 I_{nom}^2} = P_k \\cdot \frac{U_{nom}^2}{S_{nom}^2} + + $x_{pu}$ is calculated as follows: + + .. math:: x_{pu} = \\sqrt(z_{pu}^2-r_{pu}^2) + + """ + + equipment = { + "mv": ["transformers", "overhead_lines", "cables"], + "lv": ["transformers", "cables"], + } + + # if config is not provided set default path and filenames + if config is None: + equipment_dir = "equipment" + config = {} + for voltage_level, eq_list in equipment.items(): + for i in eq_list: + config[ + "equipment_{}_parameters_{}".format(voltage_level, i) + ] = "equipment-parameters_{}_{}.csv".format( + voltage_level.upper(), i + ) + else: + equipment_dir = config["system_dirs"]["equipment_dir"] + config = config["equipment"] + + package_path = edisgo.__path__[0] + data = {} + + for voltage_level, eq_list in equipment.items(): + for i in eq_list: + equipment_parameters = config[ + "equipment_{}_parameters_{}".format(voltage_level, i) + ] + data["{}_{}".format(voltage_level, i)] = pd.read_csv( + os.path.join(package_path, equipment_dir, equipment_parameters), + comment="#", + index_col="name", + delimiter=",", + decimal=".", + ) + # calculate electrical values of transformer from standard + # values (so far only for LV transformers, not necessary for + # MV as MV impedances are not used) + if voltage_level == "lv" and i == "transformers": + name = f"{voltage_level}_{i}" + + data[name]["r_pu"] = data[name]["P_k"] / data[name]["S_nom"] + + data[name]["x_pu"] = np.sqrt( + (data[name]["u_kr"] / 100) ** 2 - data[name]["r_pu"] ** 2 + ) + return data @property def loads_df(self): @@ -425,405 +542,96 @@ def buses_df(self): ---------- df : :pandas:`pandas.DataFrame` Dataframe with all buses in MV network and underlying LV grids. - Index of the dataframe are bus names as strings. Columns of the - dataframe are: - - v_nom : float - Nominal voltage in kV. - - x : float - x-coordinate (longitude) of geolocation. - - y : float - y-coordinate (latitude) of geolocation. - - mv_grid_id : int - ID of MV grid the bus is in. - - lv_grid_id : int - ID of LV grid the bus is in. In case of MV buses this is NaN. - - in_building : bool - Signifies whether a bus is inside a building, in which case - only components belonging to this house connection can be - connected to it. - - Returns - -------- - :pandas:`pandas.DataFrame` - Dataframe with all buses in MV network and underlying LV grids. - - """ - try: - return self._buses_df - except Exception: - return pd.DataFrame(columns=COLUMNS["buses_df"]) - - @buses_df.setter - def buses_df(self, df): - # make sure in_building takes on only True or False (not numpy bools) - # needs to be tested using `== True`, not `is True` - buses_in_building = df[df.in_building == True].index # noqa: E712 - df.loc[buses_in_building, "in_building"] = True - df.loc[~df.index.isin(buses_in_building), "in_building"] = False - self._buses_df = df - - @property - def switches_df(self): - """ - Dataframe with all switches in MV network and underlying LV grids. - - Switches are implemented as branches that, when they are closed, are - connected to a bus (`bus_closed`) such that there is a closed ring, - and when they are open, connected to a virtual bus (`bus_open`), such - that there is no closed ring. Once the ring is closed, the virtual - is a single bus that is not connected to the rest of the grid. - - Parameters - ---------- - df : :pandas:`pandas.DataFrame` - Dataframe with all switches in MV network and underlying LV grids. - Index of the dataframe are switch names as string. Columns of the - dataframe are: - - bus_open : str - Identifier of bus the switch branch is connected to when the - switch is open. - - bus_closed : str - Identifier of bus the switch branch is connected to when the - switch is closed. - - branch : str - Identifier of branch that represents the switch. - - type : str - Type of switch, e.g. switch disconnector. - - Returns - -------- - :pandas:`pandas.DataFrame` - Dataframe with all switches in MV network and underlying LV grids. - For more information on the dataframe see input parameter `df`. - - """ - try: - return self._switches_df - except Exception: - return pd.DataFrame(columns=COLUMNS["switches_df"]) - - @switches_df.setter - def switches_df(self, df): - self._switches_df = df - - def to_csv(self, directory): - """ - Exports topology to csv files. - - The following attributes are exported: - - * 'loads_df' : Attribute :py:attr:`~loads_df` is saved to - `loads.csv`. - * 'generators_df' : Attribute :py:attr:`~generators_df` is saved to - `generators.csv`. - * 'storage_units_df' : Attribute :py:attr:`~storage_units_df` is - saved to `storage_units.csv`. - * 'transformers_df' : Attribute :py:attr:`~transformers_df` is saved to - `transformers.csv`. - * 'transformers_hvmv_df' : Attribute :py:attr:`~transformers_df` is - saved to `transformers.csv`. - * 'lines_df' : Attribute :py:attr:`~lines_df` is saved to - `lines.csv`. - * 'buses_df' : Attribute :py:attr:`~buses_df` is saved to - `buses.csv`. - * 'switches_df' : Attribute :py:attr:`~switches_df` is saved to - `switches.csv`. - - Attributes are exported in a way that they can be directly imported to - pypsa. - - Parameters - ---------- - directory : str - Path to save topology to. - - """ - os.makedirs(directory, exist_ok=True) - if not self.loads_df.empty: - self.loads_df.to_csv(os.path.join(directory, "loads.csv")) - if not self.generators_df.empty: - self.generators_df.to_csv(os.path.join(directory, "generators.csv")) - if not self.storage_units_df.empty: - self.storage_units_df.to_csv(os.path.join(directory, "storage_units.csv")) - if not self.transformers_df.empty: - self.transformers_df.rename({"x_pu": "x", "r_pu": "r"}, axis=1).to_csv( - os.path.join(directory, "transformers.csv") - ) - if not self.transformers_hvmv_df.empty: - self.transformers_hvmv_df.rename({"x_pu": "x", "r_pu": "r"}, axis=1).to_csv( - os.path.join(directory, "transformers_hvmv.csv") - ) - self.lines_df.to_csv(os.path.join(directory, "lines.csv")) - self.buses_df.to_csv(os.path.join(directory, "buses.csv")) - if not self.switches_df.empty: - self.switches_df.to_csv(os.path.join(directory, "switches.csv")) - - def _get_matching_dict_of_attributes_and_file_names(self): - """ - Helper function that matches attribute names to file names. - - Is used in function :attr:`~.network.topology.TopologyBase.from_csv` to set - which attribute of :class:`~.network.topology.TopologyBase` is saved under - which file name. - - Returns - ------- - dict - Dictionary matching attribute names and file names with attribute - names as keys and corresponding file names as values. - - """ - return { - "buses_df": "buses.csv", - "lines_df": "lines.csv", - "loads_df": "loads.csv", - "generators_df": "generators.csv", - "storage_units_df": "storage_units.csv", - "transformers_df": "transformers.csv", - "transformers_hvmv_df": "transformers_hvmv.csv", - "switches_df": "switches.csv", - } - - def from_csv(self, data_path, edisgo_obj, from_zip_archive=False): - """ - Restores topology from csv files. - - Parameters - ---------- - data_path : str - Path to topology csv files or zip archive. - edisgo_obj : :class:`~.EDisGo` - from_zip_archive : bool - Set to True if data is archived in a zip archive. Default: False. - - """ - # get all attributes and corresponding file names - attrs = self._get_matching_dict_of_attributes_and_file_names() - - if from_zip_archive: - # read from zip archive - # setup ZipFile Class - zip = ZipFile(data_path) - - # get all directories and files within zip archive - files = zip.namelist() - - # add directory to attributes to match zip archive - attrs = {k: f"topology/{v}" for k, v in attrs.items()} - - else: - # read from directory - # check files within the directory - files = os.listdir(data_path) - - attrs_to_read = {k: v for k, v in attrs.items() if v in files} - - for attr, file in attrs_to_read.items(): - if from_zip_archive: - # open zip file to make it readable for pandas - with zip.open(file) as f: - df = pd.read_csv(f, index_col=0) - else: - path = os.path.join(data_path, file) - df = pd.read_csv(path, index_col=0) - - if attr == "generators_df": - # delete slack if it was included - df = df.loc[df.control != "Slack"] - elif "transformers" in attr: - # rename columns to match convention - df = df.rename(columns={"x": "x_pu", "r": "r_pu"}) - elif attr == "network": - # rename columns to match convention - df = df.rename( - columns={ - "mv_grid_district_geom": "geom", - "mv_grid_district_population": "population", - } - ) - - # set grid district information - setattr( - self, - "grid_district", - { - "population": df.population.iat[0], - "geom": wkt_loads(df.geom.iat[0]), - "srid": df.srid.iat[0], - }, - ) - - # set up medium voltage grid - setattr(self, "mv_grid", MVGrid(edisgo_obj=edisgo_obj, id=df.index[0])) - - continue - - # set attribute - setattr(self, attr, df) - - if from_zip_archive: - # make sure to destroy ZipFile Class to close any open connections - zip.close() - - -class Topology(TopologyBase): - """ - Container for all grid topology data of a single MV grid. - - Data may as well include grid topology data of underlying LV grids. - - Other Parameters - ----------------- - config : None or :class:`~.tools.config.Config` - Provide your configurations if you want to load self-provided equipment - data. Path to csv files containing the technical data is set in - `config_system.cfg` in sections `system_dirs` and `equipment`. - The default is None in which case the equipment data provided by - eDisGo is used. - - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - # load technical data of equipment - self._equipment_data = self._load_equipment_data(kwargs.get("config", None)) - - @staticmethod - def _load_equipment_data(config=None): - """ - Load equipment data for transformers, cables etc. - - Parameters - ----------- - config : :class:`~.tools.config.Config` - Config object with configuration data from config files. - - Returns - ------- - dict - Dictionary with :pandas:`pandas.DataFrame` containing - equipment data. Keys of the dictionary are 'mv_transformers', - 'mv_overhead_lines', 'mv_cables', 'lv_transformers', and - 'lv_cables'. - - Notes - ------ - This function calculates electrical values of transformers from - standard values (so far only for MV/LV transformers, not necessary for - HV/MV transformers as MV impedances are not used). - - $z_{pu}$ is calculated as follows: - - .. math:: z_{pu} = \frac{u_{kr}}{100} - - using the following simplification: - - .. math:: z_{pu} = \frac{Z}{Z_{nom}} - - with - - .. math:: Z = \frac{u_{kr}}{100} \\cdot \frac{U_n^2}{S_{nom}} - - and - - .. math:: Z_{nom} = \frac{U_n^2}{S_{nom}} - - $r_{pu}$ is calculated as follows: - - .. math:: r_{pu} = \frac{P_k}{S_{nom}} - - using the simplification of - - .. math:: r_{pu} = \frac{R}{Z_{nom}} - - with - - .. math:: R = \frac{P_k}{3 I_{nom}^2} = P_k \\cdot \frac{U_{nom}^2}{S_{nom}^2} + Index of the dataframe are bus names as strings. Columns of the + dataframe are: - $x_{pu}$ is calculated as follows: + v_nom : float + Nominal voltage in kV. - .. math:: x_{pu} = \\sqrt(z_{pu}^2-r_{pu}^2) + x : float + x-coordinate (longitude) of geolocation. - """ + y : float + y-coordinate (latitude) of geolocation. - equipment = { - "mv": ["transformers", "overhead_lines", "cables"], - "lv": ["transformers", "cables"], - } + mv_grid_id : int + ID of MV grid the bus is in. - # if config is not provided set default path and filenames - if config is None: - equipment_dir = "equipment" - config = {} - for voltage_level, eq_list in equipment.items(): - for i in eq_list: - config[ - "equipment_{}_parameters_{}".format(voltage_level, i) - ] = "equipment-parameters_{}_{}.csv".format( - voltage_level.upper(), i - ) - else: - equipment_dir = config["system_dirs"]["equipment_dir"] - config = config["equipment"] + lv_grid_id : int + ID of LV grid the bus is in. In case of MV buses this is NaN. - package_path = edisgo.__path__[0] - data = {} + in_building : bool + Signifies whether a bus is inside a building, in which case + only components belonging to this house connection can be + connected to it. - for voltage_level, eq_list in equipment.items(): - for i in eq_list: - equipment_parameters = config[ - "equipment_{}_parameters_{}".format(voltage_level, i) - ] - data["{}_{}".format(voltage_level, i)] = pd.read_csv( - os.path.join(package_path, equipment_dir, equipment_parameters), - comment="#", - index_col="name", - delimiter=",", - decimal=".", - ) - # calculate electrical values of transformer from standard - # values (so far only for LV transformers, not necessary for - # MV as MV impedances are not used) - if voltage_level == "lv" and i == "transformers": - name = f"{voltage_level}_{i}" + Returns + -------- + :pandas:`pandas.DataFrame` + Dataframe with all buses in MV network and underlying LV grids. - data[name]["r_pu"] = data[name]["P_k"] / data[name]["S_nom"] + """ + try: + return self._buses_df + except Exception: + return pd.DataFrame(columns=COLUMNS["buses_df"]) - data[name]["x_pu"] = np.sqrt( - (data[name]["u_kr"] / 100) ** 2 - data[name]["r_pu"] ** 2 - ) - return data + @buses_df.setter + def buses_df(self, df): + # make sure in_building takes on only True or False (not numpy bools) + # needs to be tested using `== True`, not `is True` + buses_in_building = df[df.in_building == True].index # noqa: E712 + df.loc[buses_in_building, "in_building"] = True + df.loc[~df.index.isin(buses_in_building), "in_building"] = False + self._buses_df = df @property - def charging_points_df(self): + def switches_df(self): """ - Returns a subset of :py:attr:`~loads_df` containing only charging points. + Dataframe with all switches in MV network and underlying LV grids. + + Switches are implemented as branches that, when they are closed, are + connected to a bus (`bus_closed`) such that there is a closed ring, + and when they are open, connected to a virtual bus (`bus_open`), such + that there is no closed ring. Once the ring is closed, the virtual + is a single bus that is not connected to the rest of the grid. Parameters ---------- - type : str - Load type. Default: "charging_point" + df : :pandas:`pandas.DataFrame` + Dataframe with all switches in MV network and underlying LV grids. + Index of the dataframe are switch names as string. Columns of the + dataframe are: + + bus_open : str + Identifier of bus the switch branch is connected to when the + switch is open. + + bus_closed : str + Identifier of bus the switch branch is connected to when the + switch is closed. + + branch : str + Identifier of branch that represents the switch. + + type : str + Type of switch, e.g. switch disconnector. Returns - ------- + -------- :pandas:`pandas.DataFrame` - Pandas DataFrame with all loads of the given type. + Dataframe with all switches in MV network and underlying LV grids. + For more information on the dataframe see input parameter `df`. """ - if "charging_point" in self.loads_df.type.unique(): - return self.loads_df.loc[self.loads_df.type == "charging_point"] - else: - return pd.DataFrame(columns=COLUMNS["loads_df"]) + try: + return self._switches_df + except Exception: + return pd.DataFrame(columns=COLUMNS["switches_df"]) + + @switches_df.setter + def switches_df(self, df): + self._switches_df = df @property def id(self): @@ -1025,6 +833,36 @@ def equipment_data(self): """ return self._equipment_data + @property + def original_grid_topology(self): + """ + Network topology before components are added or removed and grid is reinforced. + + This is set up when the ding0 grid is loaded. + + Parameters + ---------- + :py:class:`~.network.topology.TopologyBase` + Topology class with original grid topology data. + + Returns + -------- + :py:class:`~.network.topology.TopologyBase` + + """ + return self._original_grid_topology + + @original_grid_topology.setter + def original_grid_topology(self, topo): + # deepcopy is used so that in case topology object is changed the original + # topology is not changed + topo = copy.deepcopy(topo) + # make sure the original topology is set to None, to avoid recursive behavior + # when topology object is written to csv + if topo._original_grid_topology is not None: + topo._original_grid_topology = None + self._original_grid_topology = topo + def get_connected_lines_from_bus(self, bus_name): """ Returns all lines connected to specified bus. @@ -2957,19 +2795,55 @@ def to_csv(self, directory): Extends function :attr:`~.network.topology.TopologyBase.to_csv`. - Exports all attributes listed in :attr:`~.network.topology.TopologyBase.to_csv` - plus: + The following attributes are exported: + * 'loads_df' : Attribute :py:attr:`~loads_df` is saved to + `loads.csv`. + * 'generators_df' : Attribute :py:attr:`~generators_df` is saved to + `generators.csv`. + * 'storage_units_df' : Attribute :py:attr:`~storage_units_df` is + saved to `storage_units.csv`. + * 'transformers_df' : Attribute :py:attr:`~transformers_df` is saved to + `transformers.csv`. + * 'transformers_hvmv_df' : Attribute :py:attr:`~transformers_df` is + saved to `transformers.csv`. + * 'lines_df' : Attribute :py:attr:`~lines_df` is saved to + `lines.csv`. + * 'buses_df' : Attribute :py:attr:`~buses_df` is saved to + `buses.csv`. + * 'switches_df' : Attribute :py:attr:`~switches_df` is saved to + `switches.csv`. * 'grid_district' : Attribute :py:attr:`~grid_district` is saved to `network.csv`. + Attributes are exported in a way that they can be directly imported to + pypsa. + Parameters ---------- directory : str Path to save topology to. """ - super().to_csv(directory=directory) + os.makedirs(directory, exist_ok=True) + if not self.loads_df.empty: + self.loads_df.to_csv(os.path.join(directory, "loads.csv")) + if not self.generators_df.empty: + self.generators_df.to_csv(os.path.join(directory, "generators.csv")) + if not self.storage_units_df.empty: + self.storage_units_df.to_csv(os.path.join(directory, "storage_units.csv")) + if not self.transformers_df.empty: + self.transformers_df.rename({"x_pu": "x", "r_pu": "r"}, axis=1).to_csv( + os.path.join(directory, "transformers.csv") + ) + if not self.transformers_hvmv_df.empty: + self.transformers_hvmv_df.rename({"x_pu": "x", "r_pu": "r"}, axis=1).to_csv( + os.path.join(directory, "transformers_hvmv.csv") + ) + self.lines_df.to_csv(os.path.join(directory, "lines.csv")) + self.buses_df.to_csv(os.path.join(directory, "buses.csv")) + if not self.switches_df.empty: + self.switches_df.to_csv(os.path.join(directory, "switches.csv")) network = {"name": self.mv_grid.id} network.update(self._grid_district) @@ -2981,17 +2855,20 @@ def to_csv(self, directory): axis=1, ).to_csv(os.path.join(directory, "network.csv")) + # original network + if self.original_grid_topology is not None: + self.original_grid_topology.to_csv( + os.path.join(directory, "original_grid_topology") + ) + def _get_matching_dict_of_attributes_and_file_names(self): """ Helper function that matches attribute names to file names. - Is used in function :attr:`~.network.topology.Topology.from_csv` to set - which attribute of :class:`~.network.topology.Topology` is saved under + Is used in function :attr:`~.network.topology.TopologyBase.from_csv` to set + which attribute of :class:`~.network.topology.TopologyBase` is saved under which file name. - Function :attr:`~.network.topology.TopologyBase.\ - _get_matching_dict_of_attributes_and_file_names` is extended by this function. - Returns ------- dict @@ -2999,13 +2876,17 @@ def _get_matching_dict_of_attributes_and_file_names(self): names as keys and corresponding file names as values. """ - helper_dict = super()._get_matching_dict_of_attributes_and_file_names() - helper_dict.update( - { - "network": "network.csv", - } - ) - return helper_dict + return { + "buses_df": "buses.csv", + "lines_df": "lines.csv", + "loads_df": "loads.csv", + "generators_df": "generators.csv", + "storage_units_df": "storage_units.csv", + "transformers_df": "transformers.csv", + "transformers_hvmv_df": "transformers_hvmv.csv", + "switches_df": "switches.csv", + "network": "network.csv", + } def from_csv(self, data_path, edisgo_obj, from_zip_archive=False): """ @@ -3024,11 +2905,74 @@ def from_csv(self, data_path, edisgo_obj, from_zip_archive=False): Set to True if data is archived in a zip archive. Default: False. """ - super().from_csv( - data_path=data_path, - edisgo_obj=edisgo_obj, - from_zip_archive=from_zip_archive, - ) + # get all attributes and corresponding file names + attrs = self._get_matching_dict_of_attributes_and_file_names() + + if from_zip_archive: + # read from zip archive + # setup ZipFile Class + zip = ZipFile(data_path) + + # get all directories and files within zip archive + files = zip.namelist() + + # add directory to attributes to match zip archive + attrs = {k: f"topology/{v}" for k, v in attrs.items()} + + else: + # read from directory + # check files within the directory + files = os.listdir(data_path) + + attrs_to_read = {k: v for k, v in attrs.items() if v in files} + + for attr, file in attrs_to_read.items(): + if from_zip_archive: + # open zip file to make it readable for pandas + with zip.open(file) as f: + df = pd.read_csv(f, index_col=0) + else: + path = os.path.join(data_path, file) + df = pd.read_csv(path, index_col=0) + + if attr == "generators_df": + # delete slack if it was included + df = df.loc[df.control != "Slack"] + elif "transformers" in attr: + # rename columns to match convention + df = df.rename(columns={"x": "x_pu", "r": "r_pu"}) + elif attr == "network": + # rename columns to match convention + df = df.rename( + columns={ + "mv_grid_district_geom": "geom", + "mv_grid_district_population": "population", + } + ) + + # set grid district information + setattr( + self, + "grid_district", + { + "population": df.population.iat[0], + "geom": wkt_loads(df.geom.iat[0]), + "srid": df.srid.iat[0], + }, + ) + + # set up medium voltage grid + setattr(self, "mv_grid", MVGrid(edisgo_obj=edisgo_obj, id=df.index[0])) + + continue + + # set attribute + setattr(self, attr, df) + + # ToDo original grid topology + if from_zip_archive: + # make sure to destroy ZipFile Class to close any open connections + zip.close() # Check data integrity self.check_integrity() From d0cac72352c451abb84186dec1a4116359dd71cd Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 10 Jun 2024 17:35:05 +0200 Subject: [PATCH 40/46] Add to ding0 import and to_csv --- edisgo/io/ding0_import.py | 3 +++ tests/io/test_ding0_import.py | 7 +++++++ tests/network/test_topology.py | 9 ++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/edisgo/io/ding0_import.py b/edisgo/io/ding0_import.py index 326553111..8f1b9e08e 100644 --- a/edisgo/io/ding0_import.py +++ b/edisgo/io/ding0_import.py @@ -132,3 +132,6 @@ def sort_hvmv_transformer_buses(transformers_df): # check data integrity edisgo_obj.topology.check_integrity() + + # write topology to Topology.original_grid_topology to save original grid topology + edisgo_obj.topology.original_grid_topology = edisgo_obj.topology diff --git a/tests/io/test_ding0_import.py b/tests/io/test_ding0_import.py index 11c77436c..7a1e04bc4 100644 --- a/tests/io/test_ding0_import.py +++ b/tests/io/test_ding0_import.py @@ -36,6 +36,13 @@ def test_import_ding0_grid(self): lv_grid = self.topology.get_lv_grid(3) assert len(lv_grid.buses_df) == 9 + # original topology + assert self.topology.original_grid_topology.buses_df.shape[0] == 142 + # check that original topology does not change when Topology object is changed + self.topology.add_generator("Bus_GeneratorFluctuating_2", 3.0, "solar") + assert self.topology.original_grid_topology.generators_df.shape[0] == 28 + assert self.topology.original_grid_topology.original_grid_topology is None + def test_path_error(self): """Test catching error when path to network does not exist.""" msg = "Directory wrong_directory does not exist." diff --git a/tests/network/test_topology.py b/tests/network/test_topology.py index 0baf02f34..a52139cc5 100644 --- a/tests/network/test_topology.py +++ b/tests/network/test_topology.py @@ -867,8 +867,15 @@ def test_to_csv(self): self.topology.to_csv(dir) saved_files = os.listdir(dir) - assert len(saved_files) == 9 + assert len(saved_files) == 10 assert "generators.csv" in saved_files + assert "network.csv" in saved_files + + # check original network + saved_files = os.listdir(os.path.join(dir, "original_grid_topology")) + assert len(saved_files) == 9 + assert "buses.csv" in saved_files + assert "lines.csv" in saved_files shutil.rmtree(dir) From c17015babef76d1b59c00b0bfcaf78549c415161 Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 10 Jun 2024 17:42:45 +0200 Subject: [PATCH 41/46] Minor changes --- edisgo/network/topology.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/edisgo/network/topology.py b/edisgo/network/topology.py index 81d4442fa..08336336f 100755 --- a/edisgo/network/topology.py +++ b/edisgo/network/topology.py @@ -842,12 +842,12 @@ def original_grid_topology(self): Parameters ---------- - :py:class:`~.network.topology.TopologyBase` + :py:class:`~.network.topology.Topology` Topology class with original grid topology data. Returns -------- - :py:class:`~.network.topology.TopologyBase` + :py:class:`~.network.topology.Topology` """ return self._original_grid_topology @@ -2793,8 +2793,6 @@ def to_csv(self, directory): """ Exports topology to csv files. - Extends function :attr:`~.network.topology.TopologyBase.to_csv`. - The following attributes are exported: * 'loads_df' : Attribute :py:attr:`~loads_df` is saved to @@ -2892,10 +2890,6 @@ def from_csv(self, data_path, edisgo_obj, from_zip_archive=False): """ Restores topology from csv files. - Extends function :attr:`~.network.topology.TopologyBase.from_csv` by also - importing network.csv and original grid data. - Also performs an integrity check of the imported grid topology data. - Parameters ---------- data_path : str From cc2588891c88df785f626629354ded9aa2314ee8 Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 11 Jun 2024 14:27:12 +0200 Subject: [PATCH 42/46] Add charging points getter again --- edisgo/network/topology.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/edisgo/network/topology.py b/edisgo/network/topology.py index 08336336f..99209287e 100755 --- a/edisgo/network/topology.py +++ b/edisgo/network/topology.py @@ -633,6 +633,22 @@ def switches_df(self): def switches_df(self, df): self._switches_df = df + @property + def charging_points_df(self): + """ + Returns a subset of :py:attr:`~loads_df` containing only charging points. + + Returns + ------- + :pandas:`pandas.DataFrame` + DataFrame with all chargings points in the grid. + + """ + if "charging_point" in self.loads_df.type.unique(): + return self.loads_df.loc[self.loads_df.type == "charging_point"] + else: + return pd.DataFrame(columns=COLUMNS["loads_df"]) + @property def id(self): """ From 4f47fbd9cbd34b067ba5004e4caaa8a92e575aae Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 12 Jun 2024 11:26:38 +0200 Subject: [PATCH 43/46] Bug fix in case original grid topology is set to None --- edisgo/network/topology.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/edisgo/network/topology.py b/edisgo/network/topology.py index 99209287e..b2b2a9ea1 100755 --- a/edisgo/network/topology.py +++ b/edisgo/network/topology.py @@ -870,13 +870,14 @@ def original_grid_topology(self): @original_grid_topology.setter def original_grid_topology(self, topo): - # deepcopy is used so that in case topology object is changed the original - # topology is not changed - topo = copy.deepcopy(topo) - # make sure the original topology is set to None, to avoid recursive behavior - # when topology object is written to csv - if topo._original_grid_topology is not None: - topo._original_grid_topology = None + if topo is not None: + # deepcopy is used so that in case topology object is changed the original + # topology is not changed + topo = copy.deepcopy(topo) + # make sure the original topology is set to None, to avoid recursive + # behavior when topology object is written to csv + if topo._original_grid_topology is not None: + topo._original_grid_topology = None self._original_grid_topology = topo def get_connected_lines_from_bus(self, bus_name): From 0baf18ef09291032d9b60cfada7da8c7d56af34a Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 12 Jun 2024 11:28:13 +0200 Subject: [PATCH 44/46] Implement getting original grid topology from csv or zip --- edisgo/network/topology.py | 125 ++++++++++++++++++++++++------------- tests/test_edisgo.py | 40 +++++++++++- 2 files changed, 122 insertions(+), 43 deletions(-) diff --git a/edisgo/network/topology.py b/edisgo/network/topology.py index b2b2a9ea1..b8cb70fb2 100755 --- a/edisgo/network/topology.py +++ b/edisgo/network/topology.py @@ -2916,6 +2916,68 @@ def from_csv(self, data_path, edisgo_obj, from_zip_archive=False): Set to True if data is archived in a zip archive. Default: False. """ + + def _set_data(attrs_to_set, set_obj): + """ + Sets topology attributes from csv files. + + Parameters + ---------- + attrs_to_set : dict + Dictionary with attributes to set in the form as returned by + _get_matching_dict_of_attributes_and_file_names(). + set_obj : Topology + Topology object on which to set the data, as data can be set to the + Topology object in Topology.original_grid_data as well. + + """ + for attr, file in attrs_to_set.items(): + if from_zip_archive: + # open zip file to make it readable for pandas + with zip.open(file) as f: + df = pd.read_csv(f, index_col=0) + else: + path = os.path.join(data_path, file) + df = pd.read_csv(path, index_col=0) + + if attr == "generators_df": + # delete slack if it was included + df = df.loc[df.control != "Slack"] + elif "transformers" in attr: + # rename columns to match convention + df = df.rename(columns={"x": "x_pu", "r": "r_pu"}) + elif attr == "network": + # rename columns to match convention + df = df.rename( + columns={ + "mv_grid_district_geom": "geom", + "mv_grid_district_population": "population", + } + ) + + # set grid district information + setattr( + set_obj, + "grid_district", + { + "population": df.population.iat[0], + "geom": wkt_loads(df.geom.iat[0]), + "srid": df.srid.iat[0], + }, + ) + + # set up medium voltage grid + setattr( + set_obj, + "mv_grid", + MVGrid(edisgo_obj=edisgo_obj, id=df.index[0]), + ) + + continue + + # set attribute + setattr(set_obj, attr, df) + # get all attributes and corresponding file names attrs = self._get_matching_dict_of_attributes_and_file_names() @@ -2936,51 +2998,30 @@ def from_csv(self, data_path, edisgo_obj, from_zip_archive=False): files = os.listdir(data_path) attrs_to_read = {k: v for k, v in attrs.items() if v in files} + _set_data(attrs_to_read, self) - for attr, file in attrs_to_read.items(): - if from_zip_archive: - # open zip file to make it readable for pandas - with zip.open(file) as f: - df = pd.read_csv(f, index_col=0) + # read original grid topology data + attrs = self._get_matching_dict_of_attributes_and_file_names() + if from_zip_archive: + # add directory to attributes to match zip archive + attrs = { + k: f"topology/original_grid_topology/{v}" for k, v in attrs.items() + } + attrs_to_read = {k: v for k, v in attrs.items() if v in files} + else: + if "original_grid_topology" in files: + files = os.listdir(os.path.join(data_path, "original_grid_topology")) + attrs_to_read = { + k: f"original_grid_topology/{v}" + for k, v in attrs.items() + if v in files + } else: - path = os.path.join(data_path, file) - df = pd.read_csv(path, index_col=0) - - if attr == "generators_df": - # delete slack if it was included - df = df.loc[df.control != "Slack"] - elif "transformers" in attr: - # rename columns to match convention - df = df.rename(columns={"x": "x_pu", "r": "r_pu"}) - elif attr == "network": - # rename columns to match convention - df = df.rename( - columns={ - "mv_grid_district_geom": "geom", - "mv_grid_district_population": "population", - } - ) - - # set grid district information - setattr( - self, - "grid_district", - { - "population": df.population.iat[0], - "geom": wkt_loads(df.geom.iat[0]), - "srid": df.srid.iat[0], - }, - ) - - # set up medium voltage grid - setattr(self, "mv_grid", MVGrid(edisgo_obj=edisgo_obj, id=df.index[0])) - - continue - - # set attribute - setattr(self, attr, df) + attrs_to_read = {} + if attrs_to_read: + self.original_grid_topology = Topology() + _set_data(attrs_to_read, self.original_grid_topology) - # ToDo original grid topology if from_zip_archive: # make sure to destroy ZipFile Class to close any open connections zip.close() diff --git a/tests/test_edisgo.py b/tests/test_edisgo.py index e0379bd59..fe4f1766e 100755 --- a/tests/test_edisgo.py +++ b/tests/test_edisgo.py @@ -1964,6 +1964,11 @@ def test_import_edisgo_from_files(self): data={"load_1": [5.0, 6.0], "load_2": [7.0, 8.0]}, index=edisgo_obj.timeseries.timeindex[0:2], ) + # manipulate original grid topology, to check whether it is retrieved correctly + # or if the actual topology is used + edisgo_obj.topology.original_grid_topology.remove_load( + "Load_agricultural_LVGrid_3_1" + ) # ################ test with non-existing path ###################### @@ -1989,6 +1994,9 @@ def test_import_edisgo_from_files(self): edisgo_obj.topology.loads_df, check_dtype=False, ) + # check original grid data + assert len(edisgo_obj_loaded.topology.original_grid_topology.buses_df) == 141 + assert len(edisgo_obj_loaded.topology.original_grid_topology.loads_df) == 49 # check time series assert edisgo_obj_loaded.timeseries.timeindex.empty # check configs @@ -1997,7 +2005,17 @@ def test_import_edisgo_from_files(self): assert edisgo_obj_loaded.results.i_res.empty # ############ test with loading other data ########### - + # delete directory + shutil.rmtree(save_dir) + edisgo_obj.topology.original_grid_topology = None + edisgo_obj.save( + save_dir, + save_results=False, + save_electromobility=True, + save_heatpump=True, + save_overlying_grid=True, + save_dsm=True, + ) edisgo_obj_loaded = import_edisgo_from_files( save_dir, import_electromobility=True, @@ -2098,3 +2116,23 @@ def test_import_edisgo_from_files(self): # delete zip file os.remove(zip_file) + + # check zipping original grid data + edisgo_obj = EDisGo(ding0_grid=pytest.ding0_test_network_path) + # manipulate original grid topology, to check whether it is retrieved correctly + # or if the actual topology is used + edisgo_obj.topology.original_grid_topology.remove_load( + "Load_agricultural_LVGrid_3_1" + ) + edisgo_obj.save( + save_dir, + archive=True, + ) + zip_file = f"{save_dir}.zip" + edisgo_obj_loaded = import_edisgo_from_files( + zip_file, + from_zip_archive=True, + ) + # check original grid data + assert len(edisgo_obj_loaded.topology.original_grid_topology.buses_df) == 141 + assert len(edisgo_obj_loaded.topology.original_grid_topology.loads_df) == 49 From c72bc9118db7d2906b51b4c74a73e18b0cedd315 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 12 Jun 2024 11:28:37 +0200 Subject: [PATCH 45/46] Adapt to new matplotlib version --- edisgo/tools/plots.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/edisgo/tools/plots.py b/edisgo/tools/plots.py index 381c73754..771cdb781 100644 --- a/edisgo/tools/plots.py +++ b/edisgo/tools/plots.py @@ -6,7 +6,6 @@ from typing import TYPE_CHECKING import matplotlib -import matplotlib.cm as cm import numpy as np import pandas as pd import plotly.graph_objects as go @@ -888,7 +887,7 @@ def color_map_color( """ norm = matplotlib.colors.Normalize(vmin=vmin, vmax=vmax) if isinstance(cmap_name, str): - cmap = cm.get_cmap(cmap_name) + cmap = matplotlib.pyplot.colormaps.get_cmap(cmap_name) else: cmap = matplotlib.colors.LinearSegmentedColormap.from_list("mycmap", cmap_name) rgb = cmap(norm(abs(value)))[:3] From 19b1de4ab936e178a1086b98e4313a5b7efd0765 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 18 Sep 2024 13:42:12 +0200 Subject: [PATCH 46/46] Add costs function --- edisgo/flex_opt/costs.py | 200 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) diff --git a/edisgo/flex_opt/costs.py b/edisgo/flex_opt/costs.py index 7cbe359b1..1f582354a 100644 --- a/edisgo/flex_opt/costs.py +++ b/edisgo/flex_opt/costs.py @@ -372,3 +372,203 @@ def transformer_expansion_costs(edisgo_obj, transformer_names=None): ] ) return costs_df + + +def grid_expansion_costs_from_diff(edisgo_obj): + """ + Returns grid expansion costs based on a comparison of the original grid topology + and the current grid topology. + + The original grid topology is stored in :class:`~.EDisGo` + + Parameters + ---------- + + Returns + -------- + `pandas.DataFrame` + + """ + edisgo_grid_orig = edisgo_obj.topology.original_grid_topology + + buses_orig = edisgo_grid_orig.topology.buses_df.copy() + lines_orig = edisgo_grid_orig.topology.lines_df.copy() + + buses = edisgo_obj.topology.buses_df.copy() + lines = edisgo_obj.topology.lines_df.copy() + + # get lines that are in grid now but not in original grid - these were either + # added or changed + new_or_changed_or_split_lines = [_ for _ in lines.index if + _ not in lines_orig.index] + # get lines that were in original grid but not in grid now - these were split or + # connected to other bus (when splitting LV grid lines where feeder is split are + # renamed) or removed when component was deleted, e.g. generator decommissioned + removed_lines = [_ for _ in lines_orig.index if _ not in lines.index] + # get lines that are in both grids to check, whether they changed + lines_in_both_grids = [_ for _ in lines_orig.index if _ in lines.index] + + # get new buses + new_buses = [_ for _ in buses.index if _ not in buses_orig.index] + # get removed buses - exist when generators were decommisioned or lines aggregated + removed_buses = [_ for _ in buses_orig.index if _ not in buses.index] + + # track lines changes + lines_changed = pd.DataFrame() + + # check lines in both grids whether they changed - check line type and length and + # add to lines_changed if either changed + lines_tmp = lines.loc[lines_in_both_grids, :] + lines_changed_length = lines_tmp[ + lines_orig.loc[lines_in_both_grids, "length"] != lines.loc[ + lines_in_both_grids, "length"]] + if not lines_changed_length.empty: + lines_changed = pd.concat([lines_changed, lines_changed_length], axis=0) + lines_changed_type = lines_tmp[ + lines_orig.loc[lines_in_both_grids, "type_info"] != lines.loc[ + lines_in_both_grids, "type_info"]] + if not lines_changed_type.empty: + lines_changed = pd.concat([lines_changed, lines_changed_type], axis=0) + + # check removed lines + for removed_line in removed_lines.copy(): + # check whether any of its buses were also removed + if (lines_orig.at[removed_line, "bus0"] in removed_buses) or ( + lines_orig.at[removed_line, "bus1"] in removed_buses): + # drop from removed lines + removed_lines.remove(removed_line) + + # remaining lines in removed_lines list should be lines that were split + # match each line in removed_lines with lines in new_or_changed_or_split_lines - + # important because these may not lead to additional costs + for removed_line in removed_lines: + # find path between original buses in new grid - all lines on that path + line_bus0 = lines_orig.at[removed_line, "bus0"] + line_bus1 = lines_orig.at[removed_line, "bus1"] + if buses.at[line_bus0, "v_nom"] > 1.0: + graph = edisgo_obj.topology.mv_grid.graph + else: + # check if buses are in same LV grid or not (could happen when grid is + # split) + if int(buses.at[line_bus0, "lv_grid_id"]) == int( + buses.at[line_bus1, "lv_grid_id"]): + graph = edisgo_obj.topology.get_lv_grid( + int(buses.at[line_bus0, "lv_grid_id"])).graph + else: + graph = edisgo_obj.topology.to_graph() + path = nx.shortest_path(graph, line_bus0, line_bus1) + # get lines in path + lines_in_path = lines[lines.bus0.isin(path) & lines.bus1.isin(path)] + # drop these lines from new_or_changed_or_split_lines + for l in lines_in_path.index: + try: + new_or_changed_or_split_lines.remove(l) + except: + logger.debug(f"Line {l} is in path but could not be removed.") + # check whether line type changed or number of parallel lines and add + # to lines_changed + lines_changed_type = lines_in_path[ + lines_in_path.type_info != lines_orig.at[removed_line, "type_info"]] + if not lines_changed_type.empty: + # add to lines_changed dataframe + lines_changed = pd.concat([lines_changed, lines_changed_type], axis=0) + # drop from lines_in_path + lines_in_path.drop(index=lines_changed_type.index, inplace=True) + # for num parallel changes only consider additional line in costs + lines_changed_num_parallel = lines_in_path[ + lines_in_path.num_parallel != lines_orig.at[removed_line, "num_parallel"]] + if not lines_changed_num_parallel.empty: + # reduce num_parallel by number of parallel lines in original grid + lines_changed_num_parallel["num_parallel"] = lines_changed_num_parallel[ + "num_parallel"] - lines_orig.at[ + removed_line, "num_parallel"] + lines_changed = pd.concat( + [lines_changed, lines_changed_num_parallel], axis=0 + ) + + # get new buses where new component is directly connected - these are most likely + # not new branch tees where line was split + buses_components_orig = pd.concat( + [edisgo_grid_orig.topology.loads_df.loc[:, ["bus"]], + edisgo_grid_orig.topology.generators_df.loc[:, ["bus"]], + edisgo_grid_orig.topology.storage_units_df.loc[:, ["bus"]]] + ) + buses_components = pd.concat( + [edisgo_obj.topology.loads_df.loc[:, ["bus"]], + edisgo_obj.topology.generators_df.loc[:, ["bus"]], + edisgo_obj.topology.storage_units_df.loc[:, ["bus"]]] + ) + buses_components_new = list(set([_ for _ in buses_components.bus if + _ not in buses_components_orig.bus and _ in + new_buses])) + new_or_changed_or_split_lines_df = lines.loc[new_or_changed_or_split_lines, :] + added_lines = new_or_changed_or_split_lines_df[ + (new_or_changed_or_split_lines_df.bus0.isin(buses_components_new)) | ( + new_or_changed_or_split_lines_df.bus1.isin(buses_components_new))] + lines_changed = pd.concat([lines_changed, added_lines], axis=0) + + # remove from new_or_changed_or_split_lines + for l in new_or_changed_or_split_lines_df.index: + new_or_changed_or_split_lines.remove(l) + + if not len(new_or_changed_or_split_lines) == 0: + logger.warning( + f"new_or_changed_or_split_lines is not empty: " + f"{new_or_changed_or_split_lines}" + ) + + # determine line costs + lines_changed.drop_duplicates(keep="last", inplace=True, subset=["bus0", "bus1"]) + line_costs = line_expansion_costs(edisgo_obj, lines_names=lines_changed.index) + costs_df = pd.DataFrame( + { + "type": lines_changed.type_info, + "total_costs": (line_costs.costs_earthworks + + line_costs.costs_cable * lines_changed.num_parallel), + "length": lines_changed.num_parallel * lines_changed.length, + "quantity": lines_changed.num_parallel, + "voltage_level": line_costs.voltage_level, + }, + index=lines_changed.index + ) + + # add costs for transformers + transformers_orig = pd.concat( + [edisgo_grid_orig.topology.transformers_df.copy(), + edisgo_grid_orig.topology.transformers_hvmv_df.copy()] + ) + transformers = pd.concat( + [edisgo_obj.topology.transformers_df.copy(), + edisgo_obj.topology.transformers_hvmv_df.copy()] + ) + new_transformers = [_ for _ in transformers.index if + _ not in transformers_orig.index] + transformers_in_both_grids = [_ for _ in transformers_orig.index if + _ in transformers.index] + transformers_changed = transformers.loc[new_transformers, :] + # check transformers in both grids whether they changed - check type_info + # and add to transformers_changed if type changed + transformers_tmp = transformers.loc[transformers_in_both_grids, :] + transformers_changed_type = transformers_tmp[ + transformers_orig.loc[transformers_in_both_grids, "type_info"] != + transformers.loc[ + transformers_in_both_grids, "type_info"]] + if not transformers_changed_type.empty: + transformers_changed = pd.concat( + [transformers_changed, transformers_changed_type], axis=0) + transformer_costs = transformer_expansion_costs( + edisgo_obj, transformers_changed.index + ) + transformer_costs_df = pd.DataFrame( + { + "type": transformers_changed.type_info, + "total_costs": transformer_costs.costs, + "length": 0.0, + "quantity": 1, + "voltage_level": transformer_costs.voltage_level, + }, + index=transformers_changed.index + ) + costs_df = pd.concat([costs_df, transformer_costs_df]) + + return lines_changed, transformers_changed, costs_df