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