From 287693608d3f334e7cb554f0de08b2b05b1a7d12 Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Sat, 30 Apr 2022 14:20:43 +0200 Subject: [PATCH 01/43] A critical string load is remedied by a parallel line over half the length of the string for MV Grid --- edisgo/flex_opt/reinforce_measures.py | 88 +++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index 938c4e8de..d455610c3 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -701,5 +701,93 @@ def _replace_by_parallel_standard_lines(lines): relevant_lines = relevant_lines.loc[~relevant_lines.index.isin(lines_single.index)] if not relevant_lines.empty: _replace_by_parallel_standard_lines(relevant_lines.index) + return lines_changes + + +def add_parallel_line_over_half_length_of_string(edisgo_obj, grid, crit_lines): + standard_line = edisgo_obj.config["grid_expansion_standard_equipment"]["mv_line"] + + station_node = grid.transformers_df.bus1.iloc[0] + + voltage_level = "mv" + + relevant_lines = edisgo_obj.topology.lines_df.loc[ + crit_lines[crit_lines.voltage_level == voltage_level].index + ] + + # find the most critical lines connected to different MV feeder in HV/MV station + crit_nodes_feeder = relevant_lines[relevant_lines["bus0"] == station_node] + + # find the closed and open sides of switches + switch_df = edisgo_obj.topology.switches_df.loc[:, "bus_closed":"bus_open"].values + switches = [node for nodes in switch_df for node in nodes] + + graph = grid.graph + paths = {} + nodes_feeder = {} + + for node in switches: + # paths for the open and closed sides of CBs + path = nx.shortest_path(graph, station_node, node) + for crit_node in crit_nodes_feeder.bus1.values: + if crit_node in path: + paths[node] = path + nodes_feeder.setdefault(path[1], []).append(node) + + lines_changes = {} + for farthest_node in nodes_feeder.values(): + + def get_weight(u, v, data): + return data["length"] + + path_length_dict_tmp = dijkstra_shortest_path_length( + graph, station_node, get_weight, target=farthest_node + ) + path = paths[farthest_node[0]] + node_1_2 = next( + j + for j in path + if path_length_dict_tmp[j] >= path_length_dict_tmp[farthest_node[0]] * 1 / 2 + ) + + # if MVGrid: check if node_1_2 is LV station and if not find + # next LV station + while node_1_2 not in edisgo_obj.topology.transformers_df.bus0.values: + try: + # try to find LVStation behind node_1_2 + node_1_2 = path[path.index(node_1_2) + 1] + except IndexError: + # if no LVStation between node_1_2 and node with + # voltage problem, connect node directly to + # MVStation + node_1_2 = farthest_node[0] + break + + # get line between node_1_2 and predecessor node (that is + # closer to the station) + pred_node = path[path.index(node_1_2) - 1] + crit_line_name = graph.get_edge_data(node_1_2, 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_tmp[node_1_2] + edisgo_obj.topology.change_line_type([crit_line_name], standard_line) + lines_changes[crit_line_name] = 1 + + if not lines_changes: + logger.debug( + "==> {} line(s) was/were reinforced due to loading " + "issues.".format(len(lines_changes)) + ) return lines_changes From 227c0981dca1f6027e042ac02f0c20e3042e45df Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Mon, 2 May 2022 14:39:17 +0200 Subject: [PATCH 02/43] test again --- edisgo/flex_opt/reinforce_measures.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index d455610c3..378b9a0c5 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -787,7 +787,7 @@ def get_weight(u, v, data): if not lines_changes: logger.debug( - "==> {} line(s) was/were reinforced due to loading " - "issues.".format(len(lines_changes)) + f"==> {len(lines_changes)} line(s) was/were reinforced due to loading " + "issues." ) return lines_changes From e76bd67c8107a243dd852320979e700636983faf Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Fri, 22 Jul 2022 14:47:38 +0200 Subject: [PATCH 03/43] Split the feeder into 2 and Add new MV/LV substation at the half-length of the feeder --- edisgo/flex_opt/reinforce_measures.py | 522 +++++++++++++++++-- edisgo/tools/coordination.py | 719 ++++++++++++++++++++++++++ 2 files changed, 1184 insertions(+), 57 deletions(-) create mode 100644 edisgo/tools/coordination.py diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index 378b9a0c5..382cc6ce1 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -10,6 +10,7 @@ ) from edisgo.network.grids import LVGrid, MVGrid +from edisgo.tools import geo logger = logging.getLogger(__name__) @@ -704,90 +705,497 @@ def _replace_by_parallel_standard_lines(lines): return lines_changes -def add_parallel_line_over_half_length_of_string(edisgo_obj, grid, crit_lines): - standard_line = edisgo_obj.config["grid_expansion_standard_equipment"]["mv_line"] +def split_feeder_at_half_length(edisgo_obj, grid, crit_lines): + # ToDo: Type hinting - station_node = grid.transformers_df.bus1.iloc[0] + """ - voltage_level = "mv" + The critical string load is remedied by the following methods: + 1-Find the point at the half-length of the feeder + 2-If the half-length of the feeder is the first node that comes from the main + station,reinforce the lines by adding parallel lines since the first node + directly is connected to the main station. + 3- Otherwise, find the next LV station comes after the mid-point of the + feeder and split the line from this point so that it is to be connected to + the main station. + 4-Find the preceding LV station of the newly disconnected LV station and + remove the linebetween these two LV stations to create 2 independent feeders. + 5- If grid: LV, do not count in the nodes in the building - relevant_lines = edisgo_obj.topology.lines_df.loc[ - crit_lines[crit_lines.voltage_level == voltage_level].index - ] + Parameters + ---------- + edisgo_obj:class:`~.EDisGo` + grid: class:`~.network.grids.MVGrid` or :class:`~.network.grids.LVGrid` + crit_lines: Dataframe containing over-loaded lines, their maximum relative + over-loading (maximum calculated current over allowed current) and the + corresponding time step. + Index of the data frame is the names of the over-loaded lines. + Columns are 'max_rel_overload' containing the maximum relative + over-loading as float, 'time_index' containing the corresponding + time-step the over-loading occurred in as + :pandas:`pandas.Timestamp`, and 'voltage_level' specifying + the voltage level the line is in (either 'mv' or 'lv'). - # find the most critical lines connected to different MV feeder in HV/MV station - crit_nodes_feeder = relevant_lines[relevant_lines["bus0"] == station_node] - # find the closed and open sides of switches - switch_df = edisgo_obj.topology.switches_df.loc[:, "bus_closed":"bus_open"].values - switches = [node for nodes in switch_df for node in nodes] + Returns + ------- + dict - graph = grid.graph - paths = {} - nodes_feeder = {} + Dictionary with the name of lines as keys and the corresponding number of + lines added as values. - for node in switches: + Notes + ----- + In this method, the division is done according to the longest route (not the feeder + has more load) + + """ + + # TODO: to be integrated in the future outside of functions + def get_weight(u, v, data): + return data["length"] + if isinstance(grid, LVGrid): + + standard_line = edisgo_obj.config["grid_expansion_standard_equipment"][ + "lv_line" + ] + voltage_level = "lv" + G = grid.graph + station_node = list(G.nodes)[0] # main station + # ToDo:implement the method in crit_lines_feeder to relevant lines + # find all the lv lines that have overloading issues in lines_df + relevant_lines = edisgo_obj.topology.lines_df.loc[ + crit_lines[crit_lines.voltage_level == voltage_level].index + ] + + # find the most critical lines connected to different LV feeder in MV/LV station + crit_lines_feeder = relevant_lines[ + relevant_lines["bus0"].str.contains("LV") + & relevant_lines["bus0"].str.contains(repr(grid).split("_")[1]) + ] + + elif isinstance(grid, MVGrid): + + standard_line = edisgo_obj.config["grid_expansion_standard_equipment"][ + "mv_line" + ] + voltage_level = "mv" + G = grid.graph + # Todo: the overlading can occur not only between the main node and + # its next node + station_node = grid.transformers_df.bus1.iat[0] + + # find all the mv lines that have overloading issues in lines_df + relevant_lines = edisgo_obj.topology.lines_df.loc[ + crit_lines[crit_lines.voltage_level == voltage_level].index + ] + + # find the most critical lines connected to different LV feeder in MV/LV station + crit_lines_feeder = relevant_lines[relevant_lines["bus0"] == station_node] + + # find the closed and open sides of switches + switch_df = edisgo_obj.topology.switches_df.loc[ + :, "bus_closed":"bus_open" + ].values + switches = [node for nodes in switch_df for node in nodes] + + else: + raise ValueError(f"Grid Type {type(grid)} is not supported.") + + if isinstance(grid, LVGrid): + nodes = G + else: + nodes = switches + + paths = {} + nodes_feeder = {} + for node in nodes: # paths for the open and closed sides of CBs - path = nx.shortest_path(graph, station_node, node) - for crit_node in crit_nodes_feeder.bus1.values: - if crit_node in path: + path = nx.shortest_path(G, station_node, node) + for first_node in crit_lines_feeder.bus1.values: + if first_node in path: paths[node] = path - nodes_feeder.setdefault(path[1], []).append(node) + nodes_feeder.setdefault(path[1], []).append( + node + ) # key:first_node values:nodes in the critical feeder lines_changes = {} - for farthest_node in nodes_feeder.values(): - def get_weight(u, v, data): - return data["length"] + for node_list in nodes_feeder.values(): + + farthest_node = node_list[-1] path_length_dict_tmp = dijkstra_shortest_path_length( - graph, station_node, get_weight, target=farthest_node + G, station_node, get_weight, target=farthest_node ) - path = paths[farthest_node[0]] + path = paths[farthest_node] + node_1_2 = next( j for j in path - if path_length_dict_tmp[j] >= path_length_dict_tmp[farthest_node[0]] * 1 / 2 + if path_length_dict_tmp[j] >= path_length_dict_tmp[farthest_node] * 1 / 2 ) + # if LVGrid: check if node_1_2 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_1_2].in_building) + and grid.buses_df.loc[node_1_2].in_building + ): + node_1_2 = path[path.index(node_1_2) - 1] + # break if node is station + if node_1_2 is path[0]: + logger.error("Could not reinforce overloading issue.") + break + # if MVGrid: check if node_1_2 is LV station and if not find # next LV station - while node_1_2 not in edisgo_obj.topology.transformers_df.bus0.values: - try: - # try to find LVStation behind node_1_2 - node_1_2 = path[path.index(node_1_2) + 1] - except IndexError: - # if no LVStation between node_1_2 and node with - # voltage problem, connect node directly to - # MVStation - node_1_2 = farthest_node[0] - break - - # get line between node_1_2 and predecessor node (that is - # closer to the station) - pred_node = path[path.index(node_1_2) - 1] - crit_line_name = graph.get_edge_data(node_1_2, 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: + while node_1_2 not in edisgo_obj.topology.transformers_df.bus0.values: + try: + # try to find LVStation behind node_1_2 + node_1_2 = path[path.index(node_1_2) + 1] + except IndexError: + # if no LVStation between node_1_2 and node with + # voltage problem, connect node + # directly toMVStation + node_1_2 = farthest_node + break + + # if node_1_2 is a representative (meaning it is already + # directly connected to the station), line cannot be + # disconnected and must therefore be reinforced + # todo:add paralell line to all other lines in case + if node_1_2 in nodes_feeder.keys(): + crit_line_name = G.get_edge_data(station_node, node_1_2)["branch_name"] + crit_line = edisgo_obj.topology.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 + + # if node_1_2 is not a representative, disconnect line else: + # get line between node_1_2 and predecessor node (that is + # closer to the station) + pred_node = path[path.index(node_1_2) - 1] + crit_line_name = G.get_edge_data(node_1_2, 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 - raise ValueError("Bus not in line buses. " "Please check.") + else: - # change line length and type + raise ValueError("Bus not in line buses. " "Please check.") - edisgo_obj.topology._lines_df.at[ - crit_line_name, "length" - ] = path_length_dict_tmp[node_1_2] - edisgo_obj.topology.change_line_type([crit_line_name], standard_line) - lines_changes[crit_line_name] = 1 + # change line length and type - if not lines_changes: - logger.debug( - f"==> {len(lines_changes)} line(s) was/were reinforced due to loading " - "issues." - ) + edisgo_obj.topology._lines_df.at[ + crit_line_name, "length" + ] = path_length_dict_tmp[node_1_2] + edisgo_obj.topology.change_line_type([crit_line_name], standard_line) + lines_changes[crit_line_name] = 1 + + if not lines_changes: + logger.debug( + f"==> {len(lines_changes)} line(s) was/were reinforced due to loading " + "issues." + ) return lines_changes + + +def add_substation_at_half_length(edisgo_obj, grid, crit_lines): + """ + + *This method can be implemented only to LV grids + + The critical string load in LV grid is remedied by the splitting the feeder + at the half-length and adding a new MV/LV station + + 1-Find the points at the half-length of the feeders + 2-Add new MV/LV station with standard transformer into the MV grid at this point + 3-The distance between the existing station (eg. Busbar_mvgd_460_lvgd_131525_MV) + and newly added MV/LV station (eg. Busbar_mvgd_460_lvgd_131525_sep1_MV),is equal + to the length between the mid point of the feeder in the LV grid + nd preceding node of this point (eg. BranchTee_mvgd_460_lvgd_131525_5) + + + Parameters + ---------- + edisgo_obj:class:`~.EDisGo` + grid: class:`~.network.grids.MVGrid` or :class:`~.network.grids.LVGrid` + crit_lines: Dataframe containing over-loaded lines, their maximum relative + over-loading (maximum calculated current over allowed current) and the + corresponding time step. + Index of the data frame is the names of the over-loaded lines. + Columns are 'max_rel_overload' containing the maximum relative + over-loading as float, 'time_index' containing the corresponding + time-step the over-loading occurred in as + :pandas:`pandas.Timestamp`, and 'voltage_level' specifying + the voltage level the line is in (either 'mv' or 'lv'). + + + Returns + ------- + #todo: changes + + Notes + ----- + In this method, the seperation is done according to the longest route + (not the feeder has more load) + !! The name of the nodes moved to the new LV grid and the names of the + lines, buses, etc.connected to these nodes can remain the same. + """ + + # todo:Typehinting + # todo:Test + # todo:Logging + # todo:If the half-lengths of the feeders is the first node that comes from the + # main station + # todo: if there are still overloaded lines in the grid, reinforce... + # todo: if the mid point is a node + + def create_busbar_name(lv_station_busbar_name, lv_grid_id): + """ + create a LV and MV busbar name with same grid_id but added sep1 that implies + the seperation + + Parameters + ---------- + lv_station_busbar_name :eg 'BusBar_mvgd_460_lvgd_131573_LV' + lv_grid_id : eg. 131573 + + Returns + New lv_busbar and mv_busbar name + """ + lv_station_busbar_name = lv_station_busbar_name.split("_") + grid_id_ind = lv_station_busbar_name.index(str(lv_grid_id)) + 1 + lv_station_busbar_name.insert(grid_id_ind, "sep1") + lv_busbar = lv_station_busbar_name + lv_busbar = "_".join([str(_) for _ in lv_busbar]) + mv_busbar = lv_station_busbar_name + mv_busbar[-1] = "MV" + mv_busbar = "_".join([str(_) for _ in mv_busbar]) + + return lv_busbar, mv_busbar + + def add_standard_transformer( + grid, new_station_name_lv, new_station_name_mv, lv_grid_id, edisgo_obj + ): + + """ + + Parameters + ---------- + new_station_name_lv : the lv busbar name of the created MV/LV station + eg.BusBar_mvgd_460_lvgd_131525_sep1_LV + new_station_name_mv : the mv busbar name of the created MV/LV station + eg.BusBar_mvgd_460_lvgd_131525_sep1_MV + lv_grid_id:131525 + + Returns + New tranformer dataframe + + """ + + standard_transformer = edisgo_obj.topology.equipment_data[ + "lv_transformers" + ].loc[ + edisgo_obj.config["grid_expansion_standard_equipment"]["mv_lv_transformer"] + ] + new_transformer = grid.transformers_df.iloc[0] + new_transformer_name = new_transformer.name.split("_") + grid_id_ind = new_transformer_name.index(str(lv_grid_id)) + 1 + new_transformer_name.insert(grid_id_ind, "sep_1") + + new_transformer.s_nom = standard_transformer.S_nom + new_transformer.type_info = standard_transformer.name + new_transformer.r_pu = standard_transformer.r_pu + new_transformer.x_pu = standard_transformer.x_pu + new_transformer.name = "_".join([str(_) for _ in new_transformer_name]) + new_transformer.bus0 = new_station_name_mv + new_transformer.bus1 = new_station_name_lv + + new_transformer_df = new_transformer.to_frame().T + + # toDo:drop duplicates + edisgo_obj.topology.transformers_df = pd.concat( + [edisgo_obj.topology.transformers_df, new_transformer_df] + ) + return new_transformer_df + + top_edisgo = edisgo_obj.topology + G = grid.graph + station_node = grid.transformers_df.bus1.iat[0] + lv_grid_id = repr(grid).split("_")[1] + relevant_lines = top_edisgo.lines_df.loc[ + crit_lines[crit_lines.voltage_level == "lv"].index + ] + + relevant_lines = relevant_lines[relevant_lines["bus0"].str.contains(lv_grid_id)] + + crit_lines_feeder = relevant_lines[relevant_lines["bus0"] == station_node] + + paths = {} + nodes_feeder = {} + + for node in G: + + path = nx.shortest_path(G, station_node, node) + + for first_node in crit_lines_feeder.bus1.values: + if first_node in path: + paths[node] = path + nodes_feeder.setdefault(path[1], []).append( + node + ) # key:first_node values:nodes in the critical feeder + + nodes_moved = [] + node_halflength = [] + + for node_list in nodes_feeder.values(): + + farthest_node = node_list[-1] + + def get_weight(u, v, data): + return data["length"] + + path_length_dict_tmp = dijkstra_shortest_path_length( + G, station_node, get_weight, target=farthest_node + ) + path = paths[farthest_node] + + node_1_2 = next( + j + for j in path + if path_length_dict_tmp[j] >= path_length_dict_tmp[farthest_node] * 1 / 2 + ) + node_halflength.append(node_1_2) + + # store all the following nodes of node_1_2 that will be connected + # to the new station + + # Todo: if there is no following node of node1_2 + # find the nodes to be removed. keys: node_1_2 values: nodes to be + # moved to the new station + nodes_to_be_moved = path[path.index(node_1_2) + 1 :] + + for node in nodes_to_be_moved: + nodes_moved.append(node) + + if not crit_lines_feeder.empty: + # Create the busbar name of primary and secondary side of new MV/LV station + new_lv_busbar = create_busbar_name(station_node, lv_grid_id)[0] + new_mv_busbar = create_busbar_name(station_node, lv_grid_id)[1] + + # Create a New MV/LV station in the topology + # ADD MV and LV bus + # For the time being, the new station is located at the same + # coordinates as the existing station + + v_nom_lv = top_edisgo.buses_df[ + top_edisgo.buses_df.index.str.contains("LV") + ].v_nom[0] + v_nom_mv = top_edisgo.buses_df[ + top_edisgo.buses_df.index.str.contains("MV") + ].v_nom[0] + x_bus = top_edisgo.buses_df.loc[station_node, "x"] + y_bus = top_edisgo.buses_df.loc[station_node, "y"] + new_lv_grid_id = lv_grid_id + "_" + "sep1" + building_bus = top_edisgo.buses_df.loc[station_node, "in_building"] + + # addd lv busbar + top_edisgo.add_bus( + new_lv_busbar, + v_nom_lv, + x=x_bus, + y=y_bus, + lv_grid_id=new_lv_grid_id, + in_building=building_bus, + ) + # add mv busbar + top_edisgo.add_bus( + new_mv_busbar, v_nom_mv, x=x_bus, y=y_bus, in_building=building_bus + ) + + # ADD a LINE between existing and new station + + # find the MV side of the station_node to connect the MV side of + # the new station to MV side of current station + + existing_node_mv_busbar = top_edisgo.transformers_df[ + top_edisgo.transformers_df.bus1 == station_node + ].bus0[0] + + standard_line = edisgo_obj.config["grid_expansion_standard_equipment"][ + "mv_line" + ] # the new line type is standard Mv line + + # Change the coordinates based on the length + # todo:Length is specified. Random coordinates are to be found according to + # length + max_length = 0 + for node in node_halflength: + length = geo.calc_geo_dist_vincenty( + top_edisgo, station_node, node, branch_detour_factor=1.3 + ) + if length >= max_length: + max_length = length + + top_edisgo.add_line( + bus0=existing_node_mv_busbar, + bus1=new_mv_busbar, + length=max_length, + type_info=standard_line, + ) + + # ADD TRANSFORMER + add_standard_transformer( + grid, new_lv_busbar, new_mv_busbar, lv_grid_id, edisgo_obj + ) + + # Create new LV grid in the topology + lv_grid = LVGrid(id=new_lv_grid_id, edisgo_obj=edisgo_obj) + top_edisgo.mv_grid._lv_grids.append(lv_grid) + top_edisgo._grids[str(lv_grid)] = lv_grid + + # Change the grid ids of the nodes that are to be moved to the new LV grid + for node in nodes_moved: + top_edisgo.buses_df.loc[node, "lv_grid_id"] = new_lv_grid_id + + # todo: logger + # relocate the nodes come from the half-length point of the feeder from the + # existing grid to newly created grid + for node in nodes_moved: + if top_edisgo.lines_df.bus1.isin([node]).any(): + line_series = top_edisgo.lines_df[top_edisgo.lines_df.bus1 == node] + + if line_series.bus0[0] in node_halflength: + bus0 = line_series.bus0[0] + top_edisgo.lines_df.loc[ + top_edisgo.lines_df.bus0 == bus0, "bus0" + ] = new_lv_busbar diff --git a/edisgo/tools/coordination.py b/edisgo/tools/coordination.py new file mode 100644 index 000000000..faae88bc4 --- /dev/null +++ b/edisgo/tools/coordination.py @@ -0,0 +1,719 @@ +import copy +import logging +import math + +from time import time + +import networkx as nx +import plotly.graph_objects as go + +from dash import dcc, html +from dash.dependencies import Input, Output +from pyproj import Transformer + + +def draw_plotly( + edisgo_obj, + G, + mode_lines=False, + mode_nodes="adjecencies", + grid=False, + busmap_df=None, +): + """ + Plot the graph and shows information of the grid + + Parameters + ---------- + edisgo_obj : :class:`~edisgo.EDisGo` + EDisGo object which contains data of the grid + + G : :networkx:`Graph` + Transfer the graph of the grid to plot, the graph must contain the positions + + mode_lines : :obj:`str` + Defines the color of the lines + + * 'relative_loading' + - shows the line loading relative to the s_nom of the line + * 'loading' + - shows the loading + * 'reinforce' + - shows the reinforced lines in green + + mode_nodes : :obj:`str` + + * 'voltage_deviation' + - shows the deviation of the node voltage relative to 1 p.u. + * 'adjecencies' + - shows the the number of connections of the graph + + grid : :class:`~.network.grids.Grid` or :obj:`False` + + * :class:`~.network.grids.Grid` + - transfer the grid of the graph, to set the coordinate + origin to the first bus of the grid + * :obj:`False` + - the coordinates are not modified + + """ + + # initialization + transformer_4326_to_3035 = Transformer.from_crs( + "EPSG:4326", "EPSG:3035", always_xy=True + ) + data = [] + if not grid: + x_root = 0 + y_root = 0 + elif grid is None: + node_root = edisgo_obj.topology.transformers_hvmv_df.bus1[0] + x_root, y_root = G.nodes[node_root]["pos"] + else: + node_root = grid.transformers_df.bus1[0] + x_root, y_root = G.nodes[node_root]["pos"] + + x_root, y_root = transformer_4326_to_3035.transform(x_root, y_root) + + # line text + middle_node_x = [] + middle_node_y = [] + middle_node_text = [] + for edge in G.edges(data=True): + x0, y0 = G.nodes[edge[0]]["pos"] + x1, y1 = G.nodes[edge[1]]["pos"] + x0, y0 = transformer_4326_to_3035.transform(x0, y0) + x1, y1 = transformer_4326_to_3035.transform(x1, y1) + middle_node_x.append((x0 - x_root + x1 - x_root) / 2) + middle_node_y.append((y0 - y_root + y1 - y_root) / 2) + + text = str(edge[2]["branch_name"]) + try: + loading = edisgo_obj.results.s_res.T.loc[ + edge[2]["branch_name"] + ].max() # * 1000 + text = text + "
" + "Loading = " + str(loading) + except KeyError: + text = text + + try: + text = text + "
" + "GRAPH_LOAD = " + str(edge[2]["load"]) + except KeyError: + text = text + + try: + line_parameters = edisgo_obj.topology.lines_df.loc[ + edge[2]["branch_name"], : + ] + for index, value in line_parameters.iteritems(): + text = text + "
" + str(index) + " = " + str(value) + except KeyError: + text = text + + try: + r = edisgo_obj.topology.lines_df.r.loc[edge[2]["branch_name"]] + x = edisgo_obj.topology.lines_df.x.loc[edge[2]["branch_name"]] + s_nom = edisgo_obj.topology.lines_df.s_nom.loc[edge[2]["branch_name"]] + length = edisgo_obj.topology.lines_df.length.loc[edge[2]["branch_name"]] + bus_0 = edisgo_obj.topology.lines_df.bus0.loc[edge[2]["branch_name"]] + v_nom = edisgo_obj.topology.buses_df.loc[bus_0, "v_nom"] + import math + + text = text + "
" + "r/length = " + str(r / length) + text = ( + text + + "
" + + "x/length = " + + str(x / length / 2 / math.pi / 50 * 1000) + ) + text = text + "
" + "i_max_th = " + str(s_nom / math.sqrt(3) / v_nom) + except KeyError: + text = text + + middle_node_text.append(text) + + middle_node_trace = go.Scatter( + x=middle_node_x, + y=middle_node_y, + text=middle_node_text, + mode="markers", + hoverinfo="text", + marker=dict(opacity=0.0, size=10, color="white"), + ) + data.append(middle_node_trace) + + # line plot + import matplotlib as matplotlib + import matplotlib.cm as cm + + if mode_lines == "loading": + s_res_view = edisgo_obj.results.s_res.T.index.isin( + [edge[2]["branch_name"] for edge in G.edges.data()] + ) + color_min = edisgo_obj.results.s_res.T.loc[s_res_view].T.min().max() + color_max = edisgo_obj.results.s_res.T.loc[s_res_view].T.max().max() + elif mode_lines == "relative_loading": + color_min = 0 + color_max = 1 + + if (mode_lines != "reinforce") and not mode_lines: + + def color_map_color( + value, cmap_name="coolwarm", vmin=color_min, vmax=color_max + ): + norm = matplotlib.colors.Normalize(vmin=vmin, vmax=vmax) + cmap = cm.get_cmap(cmap_name) + rgb = cmap(norm(abs(value)))[:3] + color = matplotlib.colors.rgb2hex(rgb) + return color + + for edge in G.edges(data=True): + edge_x = [] + edge_y = [] + + x0, y0 = G.nodes[edge[0]]["pos"] + x1, y1 = G.nodes[edge[1]]["pos"] + x0, y0 = transformer_4326_to_3035.transform(x0, y0) + x1, y1 = transformer_4326_to_3035.transform(x1, y1) + edge_x.append(x0 - x_root) + edge_x.append(x1 - x_root) + edge_x.append(None) + edge_y.append(y0 - y_root) + edge_y.append(y1 - y_root) + edge_y.append(None) + + if mode_lines == "reinforce": + if edisgo_obj.results.grid_expansion_costs.index.isin( + [edge[2]["branch_name"]] + ).any(): + color = "lightgreen" + else: + color = "black" + elif mode_lines == "loading": + loading = edisgo_obj.results.s_res.T.loc[edge[2]["branch_name"]].max() + color = color_map_color(loading) + elif mode_lines == "relative_loading": + loading = edisgo_obj.results.s_res.T.loc[edge[2]["branch_name"]].max() + s_nom = edisgo_obj.topology.lines_df.s_nom.loc[edge[2]["branch_name"]] + color = color_map_color(loading / s_nom) + if loading > s_nom: + color = "green" + else: + color = "black" + + edge_trace = go.Scatter( + x=edge_x, + y=edge_y, + hoverinfo="none", + opacity=0.4, + mode="lines", + line=dict(width=2, color=color), + ) + data.append(edge_trace) + + # node plot + node_x = [] + node_y = [] + + for node in G.nodes(): + x, y = G.nodes[node]["pos"] + x, y = transformer_4326_to_3035.transform(x, y) + node_x.append(x - x_root) + node_y.append(y - y_root) + + colors = [] + if mode_nodes == "adjecencies": + for node, adjacencies in enumerate(G.adjacency()): + colors.append(len(adjacencies[1])) + colorscale = "YlGnBu" + cmid = None + colorbar = dict( + thickness=15, title="Node Connections", xanchor="left", titleside="right" + ) + elif mode_nodes == "voltage_deviation": + for node in G.nodes(): + v_min = edisgo_obj.results.v_res.T.loc[node].min() + v_max = edisgo_obj.results.v_res.T.loc[node].max() + if abs(v_min - 1) > abs(v_max - 1): + color = v_min - 1 + else: + color = v_max - 1 + colors.append(color) + colorscale = "RdBu" + cmid = 0 + colorbar = dict( + thickness=15, + title="Node Voltage Deviation", + xanchor="left", + titleside="right", + ) + + node_text = [] + for node in G.nodes(): + text = str(node) + try: + peak_load = edisgo_obj.topology.loads_df.loc[ + edisgo_obj.topology.loads_df.bus == node + ].peak_load.sum() + text = text + "
" + "peak_load = " + str(peak_load) + p_nom = edisgo_obj.topology.generators_df.loc[ + edisgo_obj.topology.generators_df.bus == node + ].p_nom.sum() + text = text + "
" + "p_nom_gen = " + str(p_nom) + p_charge = edisgo_obj.topology.charging_points_df.loc[ + edisgo_obj.topology.charging_points_df.bus == node + ].p_nom.sum() + text = text + "
" + "p_nom_charge = " + str(p_charge) + except ValueError: + text = text + + try: + s_tran_1 = edisgo_obj.topology.transformers_df.loc[ + edisgo_obj.topology.transformers_df.bus0 == node, "s_nom" + ].sum() + s_tran_2 = edisgo_obj.topology.transformers_df.loc[ + edisgo_obj.topology.transformers_df.bus1 == node, "s_nom" + ].sum() + s_tran = s_tran_1 + s_tran_2 + text = text + "
" + "s_transformer = {:.2f}kVA".format(s_tran * 1000) + except KeyError: + text = text + + try: + v_min = edisgo_obj.results.v_res.T.loc[node].min() + v_max = edisgo_obj.results.v_res.T.loc[node].max() + if abs(v_min - 1) > abs(v_max - 1): + text = text + "
" + "v = " + str(v_min) + else: + text = text + "
" + "v = " + str(v_max) + except KeyError: + text = text + + try: + text = text + "
" + "Neighbors = " + str(G.degree(node)) + except KeyError: + text = text + + try: + node_parameters = edisgo_obj.topology.buses_df.loc[node] + for index, value in node_parameters.iteritems(): + text = text + "
" + str(index) + " = " + str(value) + except KeyError: + text = text + + if busmap_df is not None: + text = text + "
" + "new_bus_name = " + busmap_df.loc[node, "new_bus"] + + node_text.append(text) + + node_trace = go.Scatter( + x=node_x, + y=node_y, + mode="markers", + hoverinfo="text", + text=node_text, + marker=dict( + showscale=True, + colorscale=colorscale, + reversescale=True, + color=colors, + size=8, + cmid=cmid, + line_width=2, + colorbar=colorbar, + ), + ) + + data.append(node_trace) + + fig = go.Figure( + data=data, + layout=go.Layout( + height=500, + titlefont_size=16, + showlegend=False, + hovermode="closest", + margin=dict(b=20, l=5, r=5, t=40), + xaxis=dict(showgrid=True, zeroline=True, showticklabels=True), + yaxis=dict(showgrid=True, zeroline=True, showticklabels=True), + ), + ) + + fig.update_yaxes(scaleanchor="x", scaleratio=1) + # fig.update_yaxes(tick0=0, dtick=1000) + # fig.update_xaxes(tick0=0, dtick=1000) + return fig + + +def dash_plot(**kwargs): + """ + Uses the :func:`draw_plotly` for interactive plotting. + + Shows different behavior for different number of parameters. + One edisgo object creates one large plot. + Two or more edisgo objects create two adjacent plots, + the objects to be plotted are selected in the dropdown menu. + + **Example run:** + + | app = dash_plot(edisgo_obj_1=edisgo_obj_1,edisgo_obj_2=edisgo_obj_2,...) + | app.run_server(mode="inline",debug=True) + + """ + + from jupyter_dash import JupyterDash + + def chosen_graph(edisgo_obj, selected_grid): + lv_grids = list(edisgo_obj.topology.mv_grid.lv_grids) + lv_grid_name_list = list(map(str, lv_grids)) + # selected_grid = "LVGrid_452669" + # selected_grid = lv_grid_name_list[0] + try: + lv_grid_id = lv_grid_name_list.index(selected_grid) + except ValueError: + lv_grid_id = False + + mv_grid = edisgo_obj.topology.mv_grid + lv_grid = lv_grids[lv_grid_id] + + if selected_grid == "Grid": + G = edisgo_obj.to_graph() + grid = None + elif selected_grid == str(mv_grid): + G = mv_grid.graph + grid = mv_grid + elif selected_grid.split("_")[0] == "LVGrid": + G = lv_grid.graph + grid = lv_grid + else: + raise ValueError("False Grid") + + return G, grid + + edisgo_obj = list(kwargs.values())[0] + mv_grid = edisgo_obj.topology.mv_grid + lv_grids = list(edisgo_obj.topology.mv_grid.lv_grids) + + edisgo_name_list = list(kwargs.keys()) + + lv_grid_name_list = list(map(str, lv_grids)) + + grid_name_list = ["Grid", str(mv_grid)] + lv_grid_name_list + + line_plot_modes = ["reinforce", "loading", "relative_loading"] + node_plot_modes = ["adjecencies", "voltage_deviation"] + + app = JupyterDash(__name__) + if len(kwargs) > 1: + app.layout = html.Div( + [ + html.Div( + [ + dcc.Dropdown( + id="dropdown_edisgo_object_1", + options=[ + {"label": i, "value": i} for i in edisgo_name_list + ], + value=edisgo_name_list[0], + ), + dcc.Dropdown( + id="dropdown_edisgo_object_2", + options=[ + {"label": i, "value": i} for i in edisgo_name_list + ], + value=edisgo_name_list[1], + ), + dcc.Dropdown( + id="dropdown_grid", + options=[{"label": i, "value": i} for i in grid_name_list], + value=grid_name_list[1], + ), + dcc.Dropdown( + id="dropdown_line_plot_mode", + options=[{"label": i, "value": i} for i in line_plot_modes], + value=line_plot_modes[0], + ), + dcc.Dropdown( + id="dropdown_node_plot_mode", + options=[{"label": i, "value": i} for i in node_plot_modes], + value=node_plot_modes[0], + ), + ] + ), + html.Div( + [ + html.Div([dcc.Graph(id="fig_1")], style={"flex": "auto"}), + html.Div([dcc.Graph(id="fig_2")], style={"flex": "auto"}), + ], + style={"display": "flex", "flex-direction": "row"}, + ), + ], + style={"display": "flex", "flex-direction": "column"}, + ) + + @app.callback( + Output("fig_1", "figure"), + Output("fig_2", "figure"), + Input("dropdown_grid", "value"), + Input("dropdown_edisgo_object_1", "value"), + Input("dropdown_edisgo_object_2", "value"), + Input("dropdown_line_plot_mode", "value"), + Input("dropdown_node_plot_mode", "value"), + ) + def update_figure( + selected_grid, + selected_edisgo_object_1, + selected_edisgo_object_2, + selected_line_plot_mode, + selected_node_plot_mode, + ): + + edisgo_obj = kwargs[selected_edisgo_object_1] + (G, grid) = chosen_graph(edisgo_obj, selected_grid) + fig_1 = draw_plotly( + edisgo_obj, + G, + selected_line_plot_mode, + selected_node_plot_mode, + grid=grid, + ) + + edisgo_obj = kwargs[selected_edisgo_object_2] + (G, grid) = chosen_graph(edisgo_obj, selected_grid) + fig_2 = draw_plotly( + edisgo_obj, + G, + selected_line_plot_mode, + selected_node_plot_mode, + grid=grid, + ) + + return fig_1, fig_2 + + else: + app.layout = html.Div( + [ + html.Div( + [ + dcc.Dropdown( + id="dropdown_grid", + options=[{"label": i, "value": i} for i in grid_name_list], + value=grid_name_list[1], + ), + dcc.Dropdown( + id="dropdown_line_plot_mode", + options=[{"label": i, "value": i} for i in line_plot_modes], + value=line_plot_modes[0], + ), + dcc.Dropdown( + id="dropdown_node_plot_mode", + options=[{"label": i, "value": i} for i in node_plot_modes], + value=node_plot_modes[0], + ), + ] + ), + html.Div( + [html.Div([dcc.Graph(id="fig")], style={"flex": "auto"})], + style={"display": "flex", "flex-direction": "row"}, + ), + ], + style={"display": "flex", "flex-direction": "column"}, + ) + + @app.callback( + Output("fig", "figure"), + Input("dropdown_grid", "value"), + Input("dropdown_line_plot_mode", "value"), + Input("dropdown_node_plot_mode", "value"), + ) + def update_figure( + selected_grid, selected_line_plot_mode, selected_node_plot_mode + ): + + edisgo_obj = list(kwargs.values())[0] + (G, grid) = chosen_graph(edisgo_obj, selected_grid) + fig = draw_plotly( + edisgo_obj, + G, + selected_line_plot_mode, + selected_node_plot_mode, + grid=grid, + ) + return fig + + return app + + +# Functions for other functions +coor_transform = Transformer.from_crs("EPSG:4326", "EPSG:3035", always_xy=True) +coor_transform_back = Transformer.from_crs("EPSG:3035", "EPSG:4326", always_xy=True) + + +# Pseudo coordinates +def make_pseudo_coordinates(edisgo_root): + def make_coordinates(graph_root): + def coordinate_source(pos_start, length, node_numerator, node_total_numerator): + length = length / 1.3 + angle = node_numerator * 360 / node_total_numerator + x0, y0 = pos_start + x1 = x0 + 1000 * length * math.cos(math.radians(angle)) + y1 = y0 + 1000 * length * math.sin(math.radians(angle)) + pos_end = (x1, y1) + origin_angle = math.degrees(math.atan2(y1 - y0, x1 - x0)) + return pos_end, origin_angle + + def coordinate_branch( + pos_start, angle_offset, length, node_numerator, node_total_numerator + ): + length = length / 1.3 + angle = ( + node_numerator * 180 / (node_total_numerator + 1) + angle_offset - 90 + ) + x0, y0 = pos_start + x1 = x0 + 1000 * length * math.cos(math.radians(angle)) + y1 = y0 + 1000 * length * math.sin(math.radians(angle)) + origin_angle = math.degrees(math.atan2(y1 - y0, x1 - x0)) + pos_end = (x1, y1) + return pos_end, origin_angle + + def coordinate_longest_path(pos_start, angle_offset, length): + length = length / 1.3 + angle = angle_offset + x0, y0 = pos_start + x1 = x0 + 1000 * length * math.cos(math.radians(angle)) + y1 = y0 + 1000 * length * math.sin(math.radians(angle)) + origin_angle = math.degrees(math.atan2(y1 - y0, x1 - x0)) + pos_end = (x1, y1) + return pos_end, origin_angle + + def coordinate_longest_path_neighbor( + pos_start, angle_offset, length, direction + ): + length = length / 1.3 + if direction: + angle_random_offset = 90 + else: + angle_random_offset = -90 + angle = angle_offset + angle_random_offset + x0, y0 = pos_start + x1 = x0 + 1000 * length * math.cos(math.radians(angle)) + y1 = y0 + 1000 * length * math.sin(math.radians(angle)) + origin_angle = math.degrees(math.atan2(y1 - y0, x1 - x0)) + pos_end = (x1, y1) + + return pos_end, origin_angle + + start_node = list(nx.nodes(graph_root))[0] + graph_root.nodes[start_node]["pos"] = (0, 0) + graph_copy = graph_root.copy() + + long_paths = [] + next_nodes = [] + + for i in range(1, 30): + path_length_to_transformer = [] + for node in graph_copy.nodes(): + try: + paths = list(nx.shortest_simple_paths(graph_copy, start_node, node)) + except ValueError: + paths = [[]] + path_length_to_transformer.append(len(paths[0])) + index = path_length_to_transformer.index(max(path_length_to_transformer)) + path_to_max_distance_node = list( + nx.shortest_simple_paths( + graph_copy, start_node, list(nx.nodes(graph_copy))[index] + ) + )[0] + path_to_max_distance_node.remove(start_node) + graph_copy.remove_nodes_from(path_to_max_distance_node) + for node in path_to_max_distance_node: + long_paths.append(node) + + path_to_max_distance_node = long_paths + n = 0 + + for node in list(nx.neighbors(graph_root, start_node)): + n = n + 1 + pos, origin_angle = coordinate_source( + graph_root.nodes[start_node]["pos"], + graph_root.edges[start_node, node]["length"], + n, + len(list(nx.neighbors(graph_root, start_node))), + ) + graph_root.nodes[node]["pos"] = pos + graph_root.nodes[node]["origin_angle"] = origin_angle + next_nodes.append(node) + + graph_copy = graph_root.copy() + graph_copy.remove_node(start_node) + while graph_copy.number_of_nodes() > 0: + next_node = next_nodes[0] + n = 0 + for node in list(nx.neighbors(graph_copy, next_node)): + n = n + 1 + if node in path_to_max_distance_node: + pos, origin_angle = coordinate_longest_path( + graph_root.nodes[next_node]["pos"], + graph_root.nodes[next_node]["origin_angle"], + graph_root.edges[next_node, node]["length"], + ) + elif next_node in path_to_max_distance_node: + direction = math.fmod( + len( + list( + nx.shortest_simple_paths( + graph_root, start_node, next_node + ) + )[0] + ), + 2, + ) + pos, origin_angle = coordinate_longest_path_neighbor( + graph_root.nodes[next_node]["pos"], + graph_root.nodes[next_node]["origin_angle"], + graph_root.edges[next_node, node]["length"], + direction, + ) + else: + pos, origin_angle = coordinate_branch( + graph_root.nodes[next_node]["pos"], + graph_root.nodes[next_node]["origin_angle"], + graph_root.edges[next_node, node]["length"], + n, + len(list(nx.neighbors(graph_copy, next_node))), + ) + + graph_root.nodes[node]["pos"] = pos + graph_root.nodes[node]["origin_angle"] = origin_angle + next_nodes.append(node) + + graph_copy.remove_node(next_node) + next_nodes.remove(next_node) + + return graph_root + + logger = logging.getLogger("edisgo.cr_make_pseudo_coor") + start_time = time() + logger.info( + "Start - Making pseudo coordinates for grid: {}".format( + str(edisgo_root.topology.mv_grid) + ) + ) + + edisgo_obj = copy.deepcopy(edisgo_root) + lv_grids = list(edisgo_obj.topology.mv_grid.lv_grids) + + for lv_grid in lv_grids: + logger.debug("Make pseudo coordinates for: {}".format(lv_grid)) + G = lv_grid.graph + x0, y0 = G.nodes[list(nx.nodes(G))[0]]["pos"] + G = make_coordinates(G) + x0, y0 = coor_transform.transform(x0, y0) + for node in G.nodes(): + x, y = G.nodes[node]["pos"] + x, y = coor_transform_back.transform(x + x0, y + y0) + edisgo_obj.topology.buses_df.loc[node, "x"] = x + edisgo_obj.topology.buses_df.loc[node, "y"] = y + + logger.info("Finished in {}s".format(time() - start_time)) + return edisgo_obj From 79501afd9bdbf55fe69b92463ff4b75721add96a Mon Sep 17 00:00:00 2001 From: Malte Jahn Date: Wed, 27 Jul 2022 11:37:48 +0200 Subject: [PATCH 04/43] Added pseudo coordinates --- edisgo/tools/pseudo_coordinates.py | 213 +++++++++++++++++++++++++ tests/tools/test_pseudo_coordinates.py | 30 ++++ 2 files changed, 243 insertions(+) create mode 100644 edisgo/tools/pseudo_coordinates.py create mode 100644 tests/tools/test_pseudo_coordinates.py diff --git a/edisgo/tools/pseudo_coordinates.py b/edisgo/tools/pseudo_coordinates.py new file mode 100644 index 000000000..f0fda262e --- /dev/null +++ b/edisgo/tools/pseudo_coordinates.py @@ -0,0 +1,213 @@ +from __future__ import annotations + +import copy +import logging +import math + +from time import time +from typing import TYPE_CHECKING + +import networkx as nx + +from pyproj import Transformer + +if TYPE_CHECKING: + from edisgo import EDisGo + +logger = logging.getLogger(__name__) + +# Transform coordinates to equidistant and back +coor_transform = Transformer.from_crs("EPSG:4326", "EPSG:3035", always_xy=True) +coor_transform_back = Transformer.from_crs("EPSG:3035", "EPSG:4326", always_xy=True) + + +# Pseudo coordinates +def make_pseudo_coordinates( + edisgo_root: EDisGo, mv_coordinates: bool = False +) -> EDisGo: + """ + Generates pseudo coordinates for grids. + + Parameters + ---------- + edisgo_root : :class:`~.EDisGo` + eDisGo Object + mv_coordinates : bool, optional + If True pseudo coordinates are also generated for mv_grid. + Default: False + Returns + ------- + edisgo_object : :class:`~.EDisGo` + eDisGo object with coordinates for all nodes + + """ + branch_detour_factor = edisgo_root.config["grid_connection"]["branch_detour_factor"] + + def make_coordinates(graph_root): + def coordinate_source(pos_start, length, node_numerator, node_total_numerator): + length = length / branch_detour_factor + angle = node_numerator * 360 / node_total_numerator + x0, y0 = pos_start + x1 = x0 + 1000 * length * math.cos(math.radians(angle)) + y1 = y0 + 1000 * length * math.sin(math.radians(angle)) + pos_end = (x1, y1) + origin_angle = math.degrees(math.atan2(y1 - y0, x1 - x0)) + return pos_end, origin_angle + + def coordinate_branch( + pos_start, angle_offset, length, node_numerator, node_total_numerator + ): + length = length / branch_detour_factor + angle = ( + node_numerator * 180 / (node_total_numerator + 1) + angle_offset - 90 + ) + x0, y0 = pos_start + x1 = x0 + 1000 * length * math.cos(math.radians(angle)) + y1 = y0 + 1000 * length * math.sin(math.radians(angle)) + origin_angle = math.degrees(math.atan2(y1 - y0, x1 - x0)) + pos_end = (x1, y1) + return pos_end, origin_angle + + def coordinate_longest_path(pos_start, angle_offset, length): + length = length / branch_detour_factor + angle = angle_offset + x0, y0 = pos_start + x1 = x0 + 1000 * length * math.cos(math.radians(angle)) + y1 = y0 + 1000 * length * math.sin(math.radians(angle)) + origin_angle = math.degrees(math.atan2(y1 - y0, x1 - x0)) + pos_end = (x1, y1) + return pos_end, origin_angle + + def coordinate_longest_path_neighbor( + pos_start, angle_offset, length, direction + ): + length = length / branch_detour_factor + if direction: + angle_random_offset = 90 + else: + angle_random_offset = -90 + angle = angle_offset + angle_random_offset + x0, y0 = pos_start + x1 = x0 + 1000 * length * math.cos(math.radians(angle)) + y1 = y0 + 1000 * length * math.sin(math.radians(angle)) + origin_angle = math.degrees(math.atan2(y1 - y0, x1 - x0)) + pos_end = (x1, y1) + + return pos_end, origin_angle + + start_node = list(nx.nodes(graph_root))[0] + graph_root.nodes[start_node]["pos"] = (0, 0) + graph_copy = graph_root.copy() + + long_paths = [] + next_nodes = [] + + for i in range(0, len(list(nx.neighbors(graph_root, start_node)))): + path_length_to_transformer = [] + for node in graph_copy.nodes(): + try: + paths = list(nx.shortest_simple_paths(graph_copy, start_node, node)) + except nx.NetworkXNoPath: + paths = [[]] + path_length_to_transformer.append(len(paths[0])) + index = path_length_to_transformer.index(max(path_length_to_transformer)) + path_to_max_distance_node = list( + nx.shortest_simple_paths( + graph_copy, start_node, list(nx.nodes(graph_copy))[index] + ) + )[0] + path_to_max_distance_node.remove(start_node) + graph_copy.remove_nodes_from(path_to_max_distance_node) + for node in path_to_max_distance_node: + long_paths.append(node) + + path_to_max_distance_node = long_paths + n = 0 + + for node in list(nx.neighbors(graph_root, start_node)): + n = n + 1 + pos, origin_angle = coordinate_source( + graph_root.nodes[start_node]["pos"], + graph_root.edges[start_node, node]["length"], + n, + len(list(nx.neighbors(graph_root, start_node))), + ) + graph_root.nodes[node]["pos"] = pos + graph_root.nodes[node]["origin_angle"] = origin_angle + next_nodes.append(node) + + graph_copy = graph_root.copy() + graph_copy.remove_node(start_node) + while graph_copy.number_of_nodes() > 0: + next_node = next_nodes[0] + n = 0 + for node in list(nx.neighbors(graph_copy, next_node)): + n = n + 1 + if node in path_to_max_distance_node: + pos, origin_angle = coordinate_longest_path( + graph_root.nodes[next_node]["pos"], + graph_root.nodes[next_node]["origin_angle"], + graph_root.edges[next_node, node]["length"], + ) + elif next_node in path_to_max_distance_node: + direction = math.fmod( + len( + list( + nx.shortest_simple_paths( + graph_root, start_node, next_node + ) + )[0] + ), + 2, + ) + pos, origin_angle = coordinate_longest_path_neighbor( + graph_root.nodes[next_node]["pos"], + graph_root.nodes[next_node]["origin_angle"], + graph_root.edges[next_node, node]["length"], + direction, + ) + else: + pos, origin_angle = coordinate_branch( + graph_root.nodes[next_node]["pos"], + graph_root.nodes[next_node]["origin_angle"], + graph_root.edges[next_node, node]["length"], + n, + len(list(nx.neighbors(graph_copy, next_node))), + ) + + graph_root.nodes[node]["pos"] = pos + graph_root.nodes[node]["origin_angle"] = origin_angle + next_nodes.append(node) + + graph_copy.remove_node(next_node) + next_nodes.remove(next_node) + + return graph_root + + start_time = time() + logger.info( + "Start - Making pseudo coordinates for grid: {}".format( + str(edisgo_root.topology.mv_grid) + ) + ) + + edisgo_obj = copy.deepcopy(edisgo_root) + + grids = list(edisgo_obj.topology.mv_grid.lv_grids) + if mv_coordinates: + grids = [edisgo_obj.topology.mv_grid] + grids + + for grid in grids: + logger.debug("Make pseudo coordinates for: {}".format(grid)) + G = grid.graph + x0, y0 = G.nodes[list(nx.nodes(G))[0]]["pos"] + G = make_coordinates(G) + x0, y0 = coor_transform.transform(x0, y0) + for node in G.nodes(): + x, y = G.nodes[node]["pos"] + x, y = coor_transform_back.transform(x + x0, y + y0) + edisgo_obj.topology.buses_df.loc[node, "x"] = x + edisgo_obj.topology.buses_df.loc[node, "y"] = y + + logger.info("Finished in {}s".format(time() - start_time)) + return edisgo_obj diff --git a/tests/tools/test_pseudo_coordinates.py b/tests/tools/test_pseudo_coordinates.py new file mode 100644 index 000000000..b3751769d --- /dev/null +++ b/tests/tools/test_pseudo_coordinates.py @@ -0,0 +1,30 @@ +import pytest + +from edisgo import EDisGo +from edisgo.tools.pseudo_coordinates import make_pseudo_coordinates + + +class TestPseudoCoordinates: + @classmethod + def setup_class(cls): + cls.edisgo_root = EDisGo(ding0_grid=pytest.ding0_test_network_path) + + def test_make_pseudo_coordinates(self): + # make pseudo coordinates + edisgo_pseudo_coordinates = make_pseudo_coordinates( + edisgo_root=self.edisgo_root, mv_coordinates=True + ) + + # test that coordinates change for one node + coordinates = self.edisgo_root.topology.buses_df.loc[ + "Bus_BranchTee_LVGrid_1_9", ["x", "y"] + ] + assert round(coordinates[0], 5) != round(7.943307, 5) + assert round(coordinates[1], 5) != round(48.080396, 5) + + # test if the right coordinates are set for one node + coordinates = edisgo_pseudo_coordinates.topology.buses_df.loc[ + "Bus_BranchTee_LVGrid_1_9", ["x", "y"] + ] + assert round(coordinates[0], 5) == round(7.943307, 5) + assert round(coordinates[1], 5) == round(48.080396, 5) From 763ddc15c9b5f752ea740b51e056510ebea4cd02 Mon Sep 17 00:00:00 2001 From: Malte Jahn Date: Thu, 28 Jul 2022 13:08:59 +0200 Subject: [PATCH 05/43] Update version for plotting packages --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index dd0fd7221..40052998d 100644 --- a/setup.py +++ b/setup.py @@ -56,16 +56,16 @@ def read(fname): "contextily", "descartes", "plotly", - "dash==2.0.0", - "werkzeug==2.0.3", + "dash==2.6.0", + "werkzeug==2.2.0", ] examples_requirements = [ "jupyter", "jupyterlab", "plotly", - "dash==2.0.0", + "dash==2.6.0", "jupyter_dash", - "werkzeug==2.0.3", + "werkzeug==2.2.0", ] dev_requirements = [ "pytest", From 0e4730454bf33dd467f483fcb3698a8c54be77f6 Mon Sep 17 00:00:00 2001 From: Malte Jahn Date: Thu, 28 Jul 2022 13:12:51 +0200 Subject: [PATCH 06/43] Improve plotting and pseudo_coordinates - add pseudo coordinates for one graph - add pseudo corrdinates for dash_plot --- edisgo/tools/plots.py | 380 +++++++++++++++++++++++------ edisgo/tools/pseudo_coordinates.py | 311 ++++++++++++----------- 2 files changed, 472 insertions(+), 219 deletions(-) diff --git a/edisgo/tools/plots.py b/edisgo/tools/plots.py index 7fcb0fce4..4b303d484 100644 --- a/edisgo/tools/plots.py +++ b/edisgo/tools/plots.py @@ -20,6 +20,7 @@ from pypsa import Network as PyPSANetwork from edisgo.tools import session_scope, tools +from edisgo.tools.pseudo_coordinates import make_pseudo_coordinates_graph if TYPE_CHECKING: from numbers import Number @@ -900,6 +901,7 @@ def draw_plotly( G: Graph | None = None, line_color: str = "relative_loading", node_color: str = "voltage_deviation", + timestep: str = "min", grid: bool | Grid = False, ) -> BaseFigure: """ @@ -932,6 +934,16 @@ def draw_plotly( * 'voltage_deviation' (default) Node color is set according to voltage deviation from 1 p.u.. + timestep : str or None + Defines whereby to choose node colors (and implicitly size). Possible + options are: + + * 'max_abs' (default) + Node color as well as size is set according to the number of direct neighbors. + * 'min' + * 'max' + * 'Timestep' + grid : :class:`~.network.grids.Grid` or bool Grid to use as root node. If a grid is given the transforer station is used as root. If False the root is set to the coordinates x=0 and y=0. Else the @@ -956,17 +968,29 @@ def draw_plotly( if hasattr(grid, "transformers_df"): node_root = grid.transformers_df.bus1.iat[0] x_root, y_root = G.nodes[node_root]["pos"] - elif not grid: x_root = 0 y_root = 0 - else: node_root = edisgo_obj.topology.transformers_hvmv_df.bus1.iat[0] x_root, y_root = G.nodes[node_root]["pos"] x_root, y_root = transformer_4326_to_3035.transform(x_root, y_root) + s_res_view = edisgo_obj.results.s_res.T.index.isin( + [edge[2]["branch_name"] for edge in G.edges.data()] + ) + v_res_view = edisgo_obj.results.v_res.T.index.isin([node for node in G.nodes]) + if timestep == "min": + s_res = edisgo_obj.results.s_res.T.loc[s_res_view].T.min() + v_res = edisgo_obj.results.v_res.T.loc[v_res_view].T.min() + elif timestep == "max": + s_res = edisgo_obj.results.s_res.T.loc[s_res_view].T.max() + v_res = edisgo_obj.results.v_res.T.loc[v_res_view].T.max() + else: + s_res = edisgo_obj.results.s_res.T.loc[s_res_view, timestep] + v_res = edisgo_obj.results.v_res.T.loc[v_res_view, timestep] + # line text middle_node_x = [] middle_node_y = [] @@ -984,8 +1008,7 @@ def draw_plotly( text = str(branch_name) try: - loading = edisgo_obj.results.s_res.T.loc[branch_name].max() - text += "
" + "Loading = " + str(loading) + text += "
" + "Loading = " + str(s_res.loc[branch_name]) except KeyError: logger.debug( f"Could not find loading for branch {branch_name}", exc_info=True @@ -1018,11 +1041,8 @@ def draw_plotly( # line plot if line_color == "loading": - s_res_view = edisgo_obj.results.s_res.T.index.isin( - [edge[2]["branch_name"] for edge in G.edges.data()] - ) - color_min = edisgo_obj.results.s_res.T.loc[s_res_view].T.min().max() - color_max = edisgo_obj.results.s_res.T.loc[s_res_view].T.max().max() + color_min = s_res.T.min() + color_max = s_res.T.max() elif line_color == "relative_loading": color_min = 0 @@ -1037,16 +1057,16 @@ def draw_plotly( edge_x = [x0 - x_root, x1 - x_root, None] edge_y = [y0 - y_root, y1 - y_root, None] + branch_name = edge[2]["branch_name"] + if line_color == "reinforce": - if edisgo_obj.results.grid_expansion_costs.index.isin( - [edge[2]["branch_name"]] - ).any(): + if edisgo_obj.results.grid_expansion_costs.index.isin([branch_name]).any(): color = "lightgreen" else: color = "black" elif line_color == "loading": - loading = edisgo_obj.results.s_res.T.loc[edge[2]["branch_name"]].max() + loading = s_res.loc[branch_name] color = color_map_color( loading, vmin=color_min, @@ -1054,8 +1074,8 @@ def draw_plotly( ) elif line_color == "relative_loading": - loading = edisgo_obj.results.s_res.T.loc[edge[2]["branch_name"]].max() - s_nom = edisgo_obj.topology.lines_df.s_nom.loc[edge[2]["branch_name"]] + loading = s_res.loc[branch_name] + s_nom = edisgo_obj.topology.lines_df.s_nom.loc[branch_name] color = color_map_color( loading / s_nom, vmin=color_min, @@ -1090,14 +1110,7 @@ def draw_plotly( colors = [] for node in G.nodes(): - v_res = edisgo_obj.results.v_res.T.loc[node] - v_min = v_res.min() - v_max = v_res.max() - - if abs(v_min - 1) > abs(v_max - 1): - color = v_min - 1 - else: - color = v_max - 1 + color = v_res.loc[node] - 1 colors.append(color) @@ -1127,16 +1140,15 @@ def draw_plotly( edisgo_obj.topology.loads_df.bus == node ].p_set.sum() text += "
" + "peak_load = " + str(peak_load) + p_nom = edisgo_obj.topology.generators_df.loc[ edisgo_obj.topology.generators_df.bus == node ].p_nom.sum() text += "
" + "p_nom_gen = " + str(p_nom) - v_min = edisgo_obj.results.v_res.T.loc[node].min() - v_max = edisgo_obj.results.v_res.T.loc[node].max() - if abs(v_min - 1) > abs(v_max - 1): - text += "
" + "v = " + str(v_min) - else: - text += "
" + "v = " + str(v_max) + + v = v_res.loc[node] + text += "
" + "v = " + str(v) + except KeyError: logger.debug(f"Failed to add text for node {node}.", exc_info=True) text = text @@ -1291,6 +1303,26 @@ def dash_plot( if node_plot_modes is None: node_plot_modes = ["adjacencies", "voltage_deviation"] + if edisgo_obj_1.timeseries.is_worst_case: + timestep_labels = [ + "min", + "max", + ] + edisgo_obj_1.timeseries.timeindex_worst_cases.index.to_list() + else: + timestep_labels = [ + "min", + "max", + ] + edisgo_obj_1.timeseries.timeindex.values.to_list() + + timestep_values = ["min", "max"] + edisgo_obj_1.timeseries.timeindex.to_list() + timestep_option = list() + for i in range(0, len(timestep_labels)): + timestep_option.append( + {"label": timestep_labels[i], "value": timestep_values[i]} + ) + + padding = 0 + app = JupyterDash(__name__) if isinstance(edisgo_objects, dict) and len(edisgo_objects) > 1: @@ -1298,36 +1330,145 @@ def dash_plot( [ html.Div( [ - dcc.Dropdown( - id="dropdown_edisgo_object_1", - options=[ - {"label": i, "value": i} for i in edisgo_name_list + html.Div( + [ + html.Label("Edisgo objects"), ], - value=edisgo_name_list[0], + style={"padding": padding, "flex": 1}, ), - dcc.Dropdown( - id="dropdown_edisgo_object_2", - options=[ - {"label": i, "value": i} for i in edisgo_name_list + html.Div( + [], + style={"padding": padding, "flex": 1}, + ), + ], + style={ + "display": "flex", + "flex-direction": "row", + "padding": 0, + "flex": 1, + }, + ), + html.Div( + [ + html.Div( + [ + dcc.Dropdown( + id="dropdown_edisgo_object_1", + options=[ + {"label": i, "value": i} + for i in edisgo_name_list + ], + value=edisgo_name_list[0], + ), ], - value=edisgo_name_list[1], + style={"padding": padding, "flex": 1}, ), - dcc.Dropdown( - id="dropdown_grid", - options=[{"label": i, "value": i} for i in grid_name_list], - value=grid_name_list[1], + html.Div( + [ + dcc.Dropdown( + id="dropdown_edisgo_object_2", + options=[ + {"label": i, "value": i} + for i in edisgo_name_list + ], + value=edisgo_name_list[1], + ), + ], + style={"padding": padding, "flex": 1}, ), - dcc.Dropdown( - id="dropdown_line_plot_mode", - options=[{"label": i, "value": i} for i in line_plot_modes], - value=line_plot_modes[0], + ], + style={ + "display": "flex", + "flex-direction": "row", + "padding": 0, + "flex": 1, + }, + ), + html.Div( + [ + html.Div( + [ + html.Label("Grid"), + dcc.Dropdown( + id="dropdown_grid", + options=[ + {"label": i, "value": i} for i in grid_name_list + ], + value=grid_name_list[1], + ), + ], + style={"padding": padding, "flex": 1}, + ), + html.Div( + [ + html.Label("Pseudo coordinates"), + dcc.RadioItems( + id="radioitems_pseudo_coordinates", + options=[ + {"label": "False", "value": False}, + {"label": "True", "value": True}, + ], + value=False, + ), + ], + style={"padding": padding, "flex": 1}, + ), + ], + style={ + "display": "flex", + "flex-direction": "row", + "padding": 0, + "flex": 1, + }, + ), + html.Div( + [ + html.Div( + [ + html.Label("Line plot mode"), + dcc.Dropdown( + id="dropdown_line_plot_mode", + options=[ + {"label": i, "value": i} + for i in line_plot_modes + ], + value=line_plot_modes[0], + ), + ], + style={"padding": padding, "flex": 1}, ), + html.Div( + [ + html.Label("Node plot mode"), + dcc.Dropdown( + id="dropdown_node_plot_mode", + options=[ + {"label": i, "value": i} + for i in node_plot_modes + ], + value=node_plot_modes[0], + ), + ], + style={"padding": padding, "flex": 1}, + ), + ], + style={ + "display": "flex", + "flex-direction": "row", + "padding": 0, + "flex": 1, + }, + ), + html.Div( + [ + html.Label("Timestep"), dcc.Dropdown( - id="dropdown_node_plot_mode", - options=[{"label": i, "value": i} for i in node_plot_modes], - value=node_plot_modes[0], + id="timestep", + options=timestep_option, + value=timestep_option[0]["value"], ), - ] + ], + style={"padding": padding, "flex": 1}, ), html.Div( [ @@ -1348,6 +1489,8 @@ def dash_plot( Input("dropdown_edisgo_object_2", "value"), Input("dropdown_line_plot_mode", "value"), Input("dropdown_node_plot_mode", "value"), + Input("radioitems_pseudo_coordinates", "value"), + Input("timestep", "value"), ) def update_figure( selected_grid, @@ -1355,24 +1498,32 @@ def update_figure( selected_edisgo_object_2, selected_line_plot_mode, selected_node_plot_mode, + pseudo_coordinates, + selected_timestep, ): edisgo_obj = edisgo_objects[selected_edisgo_object_1] (G, grid) = chosen_graph(edisgo_obj, selected_grid) + if pseudo_coordinates: + G = make_pseudo_coordinates_graph(G) fig_1 = draw_plotly( - edisgo_obj, - G, - selected_line_plot_mode, - selected_node_plot_mode, + edisgo_obj=edisgo_obj, + G=G, + line_color=selected_line_plot_mode, + node_color=selected_node_plot_mode, + timestep=selected_timestep, grid=grid, ) edisgo_obj = edisgo_objects[selected_edisgo_object_2] (G, grid) = chosen_graph(edisgo_obj, selected_grid) + if pseudo_coordinates: + G = make_pseudo_coordinates_graph(G) fig_2 = draw_plotly( - edisgo_obj, - G, - selected_line_plot_mode, - selected_node_plot_mode, + edisgo_obj=edisgo_obj, + G=G, + line_color=selected_line_plot_mode, + node_color=selected_node_plot_mode, + timestep=selected_timestep, grid=grid, ) @@ -1383,22 +1534,89 @@ def update_figure( [ html.Div( [ - dcc.Dropdown( - id="dropdown_grid", - options=[{"label": i, "value": i} for i in grid_name_list], - value=grid_name_list[1], + html.Div( + [ + html.Label("Grid"), + dcc.Dropdown( + id="dropdown_grid", + options=[ + {"label": i, "value": i} for i in grid_name_list + ], + value=grid_name_list[1], + ), + ], + style={"padding": padding, "flex": 1}, ), - dcc.Dropdown( - id="dropdown_line_plot_mode", - options=[{"label": i, "value": i} for i in line_plot_modes], - value=line_plot_modes[0], + html.Div( + [ + html.Label("Pseudo coordinates"), + dcc.RadioItems( + id="radioitems_pseudo_coordinates", + options=[ + {"label": "False", "value": False}, + {"label": "True", "value": True}, + ], + value=False, + ), + ], + style={"padding": padding, "flex": 1}, + ), + ], + style={ + "display": "flex", + "flex-direction": "row", + "padding": 0, + "flex": 1, + }, + ), + html.Div( + [ + html.Div( + [ + html.Label("Line plot mode"), + dcc.Dropdown( + id="dropdown_line_plot_mode", + options=[ + {"label": i, "value": i} + for i in line_plot_modes + ], + value=line_plot_modes[0], + ), + ], + style={"padding": padding, "flex": 1}, + ), + html.Div( + [ + html.Label("Node plot mode"), + dcc.Dropdown( + id="dropdown_node_plot_mode", + options=[ + {"label": i, "value": i} + for i in node_plot_modes + ], + value=node_plot_modes[0], + ), + ], + style={"padding": padding, "flex": 1}, ), + ], + style={ + "display": "flex", + "flex-direction": "row", + "padding": 0, + "flex": 1, + }, + ), + html.Div( + [ + html.Label("Timestep"), dcc.Dropdown( - id="dropdown_node_plot_mode", - options=[{"label": i, "value": i} for i in node_plot_modes], - value=node_plot_modes[0], + id="timestep", + options=timestep_option, + value=timestep_option[0]["value"], ), - ] + ], + style={"padding": padding, "flex": 1}, ), html.Div( [html.Div([dcc.Graph(id="fig")], style={"flex": "auto"})], @@ -1413,18 +1631,28 @@ def update_figure( Input("dropdown_grid", "value"), Input("dropdown_line_plot_mode", "value"), Input("dropdown_node_plot_mode", "value"), + Input("radioitems_pseudo_coordinates", "value"), + Input("timestep", "value"), ) def update_figure( - selected_grid, selected_line_plot_mode, selected_node_plot_mode + selected_grid, + selected_line_plot_mode, + selected_node_plot_mode, + pseudo_coordinates, + selected_timestep, ): (G, grid) = chosen_graph(edisgo_obj_1, selected_grid) + if pseudo_coordinates: + G = make_pseudo_coordinates_graph(G) fig = draw_plotly( - edisgo_obj_1, - G, - selected_line_plot_mode, - selected_node_plot_mode, + edisgo_obj=edisgo_obj_1, + G=G, + line_color=selected_line_plot_mode, + node_color=selected_node_plot_mode, + timestep=selected_timestep, grid=grid, ) + return fig return app diff --git a/edisgo/tools/pseudo_coordinates.py b/edisgo/tools/pseudo_coordinates.py index f0fda262e..60e446c0a 100644 --- a/edisgo/tools/pseudo_coordinates.py +++ b/edisgo/tools/pseudo_coordinates.py @@ -22,6 +22,143 @@ # Pseudo coordinates +def make_coordinates(graph_root, branch_detour_factor=1.3): + # EDisGo().config["grid_connection"]["branch_detour_factor"]): + def coordinate_source(pos_start, length, node_numerator, node_total_numerator): + length = length / branch_detour_factor + angle = node_numerator * 360 / node_total_numerator + x0, y0 = pos_start + x1 = x0 + 1000 * length * math.cos(math.radians(angle)) + y1 = y0 + 1000 * length * math.sin(math.radians(angle)) + pos_end = (x1, y1) + origin_angle = math.degrees(math.atan2(y1 - y0, x1 - x0)) + return pos_end, origin_angle + + def coordinate_branch( + pos_start, angle_offset, length, node_numerator, node_total_numerator + ): + length = length / branch_detour_factor + angle = node_numerator * 180 / (node_total_numerator + 1) + angle_offset - 90 + x0, y0 = pos_start + x1 = x0 + 1000 * length * math.cos(math.radians(angle)) + y1 = y0 + 1000 * length * math.sin(math.radians(angle)) + origin_angle = math.degrees(math.atan2(y1 - y0, x1 - x0)) + pos_end = (x1, y1) + return pos_end, origin_angle + + def coordinate_longest_path(pos_start, angle_offset, length): + length = length / branch_detour_factor + angle = angle_offset + x0, y0 = pos_start + x1 = x0 + 1000 * length * math.cos(math.radians(angle)) + y1 = y0 + 1000 * length * math.sin(math.radians(angle)) + origin_angle = math.degrees(math.atan2(y1 - y0, x1 - x0)) + pos_end = (x1, y1) + return pos_end, origin_angle + + def coordinate_longest_path_neighbor(pos_start, angle_offset, length, direction): + length = length / branch_detour_factor + if direction: + angle_random_offset = 90 + else: + angle_random_offset = -90 + angle = angle_offset + angle_random_offset + x0, y0 = pos_start + x1 = x0 + 1000 * length * math.cos(math.radians(angle)) + y1 = y0 + 1000 * length * math.sin(math.radians(angle)) + origin_angle = math.degrees(math.atan2(y1 - y0, x1 - x0)) + pos_end = (x1, y1) + + return pos_end, origin_angle + + start_node = list(nx.nodes(graph_root))[0] + graph_root.nodes[start_node]["pos"] = (0, 0) + graph_copy = graph_root.copy() + + long_paths = [] + next_nodes = [] + + for i in range(0, len(list(nx.neighbors(graph_root, start_node)))): + path_length_to_transformer = [] + for node in graph_copy.nodes(): + try: + paths = list(nx.shortest_simple_paths(graph_copy, start_node, node)) + except nx.NetworkXNoPath: + paths = [[]] + path_length_to_transformer.append(len(paths[0])) + index = path_length_to_transformer.index(max(path_length_to_transformer)) + path_to_max_distance_node = list( + nx.shortest_simple_paths( + graph_copy, start_node, list(nx.nodes(graph_copy))[index] + ) + )[0] + path_to_max_distance_node.remove(start_node) + graph_copy.remove_nodes_from(path_to_max_distance_node) + for node in path_to_max_distance_node: + long_paths.append(node) + + path_to_max_distance_node = long_paths + n = 0 + + for node in list(nx.neighbors(graph_root, start_node)): + n = n + 1 + pos, origin_angle = coordinate_source( + graph_root.nodes[start_node]["pos"], + graph_root.edges[start_node, node]["length"], + n, + len(list(nx.neighbors(graph_root, start_node))), + ) + graph_root.nodes[node]["pos"] = pos + graph_root.nodes[node]["origin_angle"] = origin_angle + next_nodes.append(node) + + graph_copy = graph_root.copy() + graph_copy.remove_node(start_node) + while graph_copy.number_of_nodes() > 0: + next_node = next_nodes[0] + n = 0 + for node in list(nx.neighbors(graph_copy, next_node)): + n = n + 1 + if node in path_to_max_distance_node: + pos, origin_angle = coordinate_longest_path( + graph_root.nodes[next_node]["pos"], + graph_root.nodes[next_node]["origin_angle"], + graph_root.edges[next_node, node]["length"], + ) + elif next_node in path_to_max_distance_node: + direction = math.fmod( + len( + list( + nx.shortest_simple_paths(graph_root, start_node, next_node) + )[0] + ), + 2, + ) + pos, origin_angle = coordinate_longest_path_neighbor( + graph_root.nodes[next_node]["pos"], + graph_root.nodes[next_node]["origin_angle"], + graph_root.edges[next_node, node]["length"], + direction, + ) + else: + pos, origin_angle = coordinate_branch( + graph_root.nodes[next_node]["pos"], + graph_root.nodes[next_node]["origin_angle"], + graph_root.edges[next_node, node]["length"], + n, + len(list(nx.neighbors(graph_copy, next_node))), + ) + + graph_root.nodes[node]["pos"] = pos + graph_root.nodes[node]["origin_angle"] = origin_angle + next_nodes.append(node) + + graph_copy.remove_node(next_node) + next_nodes.remove(next_node) + + return graph_root + + def make_pseudo_coordinates( edisgo_root: EDisGo, mv_coordinates: bool = False ) -> EDisGo: @@ -41,149 +178,6 @@ def make_pseudo_coordinates( eDisGo object with coordinates for all nodes """ - branch_detour_factor = edisgo_root.config["grid_connection"]["branch_detour_factor"] - - def make_coordinates(graph_root): - def coordinate_source(pos_start, length, node_numerator, node_total_numerator): - length = length / branch_detour_factor - angle = node_numerator * 360 / node_total_numerator - x0, y0 = pos_start - x1 = x0 + 1000 * length * math.cos(math.radians(angle)) - y1 = y0 + 1000 * length * math.sin(math.radians(angle)) - pos_end = (x1, y1) - origin_angle = math.degrees(math.atan2(y1 - y0, x1 - x0)) - return pos_end, origin_angle - - def coordinate_branch( - pos_start, angle_offset, length, node_numerator, node_total_numerator - ): - length = length / branch_detour_factor - angle = ( - node_numerator * 180 / (node_total_numerator + 1) + angle_offset - 90 - ) - x0, y0 = pos_start - x1 = x0 + 1000 * length * math.cos(math.radians(angle)) - y1 = y0 + 1000 * length * math.sin(math.radians(angle)) - origin_angle = math.degrees(math.atan2(y1 - y0, x1 - x0)) - pos_end = (x1, y1) - return pos_end, origin_angle - - def coordinate_longest_path(pos_start, angle_offset, length): - length = length / branch_detour_factor - angle = angle_offset - x0, y0 = pos_start - x1 = x0 + 1000 * length * math.cos(math.radians(angle)) - y1 = y0 + 1000 * length * math.sin(math.radians(angle)) - origin_angle = math.degrees(math.atan2(y1 - y0, x1 - x0)) - pos_end = (x1, y1) - return pos_end, origin_angle - - def coordinate_longest_path_neighbor( - pos_start, angle_offset, length, direction - ): - length = length / branch_detour_factor - if direction: - angle_random_offset = 90 - else: - angle_random_offset = -90 - angle = angle_offset + angle_random_offset - x0, y0 = pos_start - x1 = x0 + 1000 * length * math.cos(math.radians(angle)) - y1 = y0 + 1000 * length * math.sin(math.radians(angle)) - origin_angle = math.degrees(math.atan2(y1 - y0, x1 - x0)) - pos_end = (x1, y1) - - return pos_end, origin_angle - - start_node = list(nx.nodes(graph_root))[0] - graph_root.nodes[start_node]["pos"] = (0, 0) - graph_copy = graph_root.copy() - - long_paths = [] - next_nodes = [] - - for i in range(0, len(list(nx.neighbors(graph_root, start_node)))): - path_length_to_transformer = [] - for node in graph_copy.nodes(): - try: - paths = list(nx.shortest_simple_paths(graph_copy, start_node, node)) - except nx.NetworkXNoPath: - paths = [[]] - path_length_to_transformer.append(len(paths[0])) - index = path_length_to_transformer.index(max(path_length_to_transformer)) - path_to_max_distance_node = list( - nx.shortest_simple_paths( - graph_copy, start_node, list(nx.nodes(graph_copy))[index] - ) - )[0] - path_to_max_distance_node.remove(start_node) - graph_copy.remove_nodes_from(path_to_max_distance_node) - for node in path_to_max_distance_node: - long_paths.append(node) - - path_to_max_distance_node = long_paths - n = 0 - - for node in list(nx.neighbors(graph_root, start_node)): - n = n + 1 - pos, origin_angle = coordinate_source( - graph_root.nodes[start_node]["pos"], - graph_root.edges[start_node, node]["length"], - n, - len(list(nx.neighbors(graph_root, start_node))), - ) - graph_root.nodes[node]["pos"] = pos - graph_root.nodes[node]["origin_angle"] = origin_angle - next_nodes.append(node) - - graph_copy = graph_root.copy() - graph_copy.remove_node(start_node) - while graph_copy.number_of_nodes() > 0: - next_node = next_nodes[0] - n = 0 - for node in list(nx.neighbors(graph_copy, next_node)): - n = n + 1 - if node in path_to_max_distance_node: - pos, origin_angle = coordinate_longest_path( - graph_root.nodes[next_node]["pos"], - graph_root.nodes[next_node]["origin_angle"], - graph_root.edges[next_node, node]["length"], - ) - elif next_node in path_to_max_distance_node: - direction = math.fmod( - len( - list( - nx.shortest_simple_paths( - graph_root, start_node, next_node - ) - )[0] - ), - 2, - ) - pos, origin_angle = coordinate_longest_path_neighbor( - graph_root.nodes[next_node]["pos"], - graph_root.nodes[next_node]["origin_angle"], - graph_root.edges[next_node, node]["length"], - direction, - ) - else: - pos, origin_angle = coordinate_branch( - graph_root.nodes[next_node]["pos"], - graph_root.nodes[next_node]["origin_angle"], - graph_root.edges[next_node, node]["length"], - n, - len(list(nx.neighbors(graph_copy, next_node))), - ) - - graph_root.nodes[node]["pos"] = pos - graph_root.nodes[node]["origin_angle"] = origin_angle - next_nodes.append(node) - - graph_copy.remove_node(next_node) - next_nodes.remove(next_node) - - return graph_root - start_time = time() logger.info( "Start - Making pseudo coordinates for grid: {}".format( @@ -211,3 +205,34 @@ def coordinate_longest_path_neighbor( logger.info("Finished in {}s".format(time() - start_time)) return edisgo_obj + + +def make_pseudo_coordinates_graph(G): + """ + Generates pseudo coordinates for one graph. + + Parameters + ---------- + edisgo_root : :class:`~.EDisGo` + eDisGo Object + mv_coordinates : bool, optional + If True pseudo coordinates are also generated for mv_grid. + Default: False + Returns + ------- + edisgo_object : :class:`~.EDisGo` + eDisGo object with coordinates for all nodes + + """ + start_time = time() + logger.info("Start - Making pseudo coordinates for graph") + + x0, y0 = G.nodes[list(nx.nodes(G))[0]]["pos"] + G = make_coordinates(G) + x0, y0 = coor_transform.transform(x0, y0) + for node in G.nodes(): + x, y = G.nodes[node]["pos"] + G.nodes[node]["pos"] = coor_transform_back.transform(x + x0, y + y0) + + logger.info("Finished in {}s".format(time() - start_time)) + return G From 0e4e7ae7aad387be5aa82cd7626cdb42f6195c0f Mon Sep 17 00:00:00 2001 From: Malte Jahn Date: Fri, 29 Jul 2022 08:32:52 +0200 Subject: [PATCH 07/43] Improve plotting with plotly and dash - added colorbar in draw_plotly by nailend - added function show_dash_plot to show Jupyter dash app --- edisgo/tools/plots.py | 238 ++++++++++++++++++++++++++++-------------- 1 file changed, 159 insertions(+), 79 deletions(-) diff --git a/edisgo/tools/plots.py b/edisgo/tools/plots.py index 4b303d484..f0ba49603 100644 --- a/edisgo/tools/plots.py +++ b/edisgo/tools/plots.py @@ -867,7 +867,7 @@ def color_map_color( vmin: Number, vmax: Number, cmap_name: str = "coolwarm", -): +) -> str: """ Get matching color for a value on a matplotlib color map. @@ -910,10 +910,12 @@ def draw_plotly( Parameters ---------- edisgo_obj : :class:`~.EDisGo` + G : :networkx:`networkx.Graph`, optional Graph representation of the grid as networkx Ordered Graph, where lines are represented by edges in the graph, and buses and transformers are represented by nodes. If no graph is given the mv grid graph of the edisgo object is used. + line_color : str Defines whereby to choose line colors (and implicitly size). Possible options are: @@ -934,18 +936,19 @@ def draw_plotly( * 'voltage_deviation' (default) Node color is set according to voltage deviation from 1 p.u.. - timestep : str or None - Defines whereby to choose node colors (and implicitly size). Possible - options are: + timestep : str or :pandas:`pandas.Timestamp` + Defines which values are shown for the load of the lines and the voltage of the + nodes: - * 'max_abs' (default) - Node color as well as size is set according to the number of direct neighbors. - * 'min' + * 'min' (default) + Minimal line load and minimal node voltage of all time steps. * 'max' - * 'Timestep' + Maximal line load and minimal node voltage of all time steps. + * 'timestep' + Line load and node voltage for the selected time step. grid : :class:`~.network.grids.Grid` or bool - Grid to use as root node. If a grid is given the transforer station is used + Grid to use as root node. If a grid is given the transformer station is used as root. If False the root is set to the coordinates x=0 and y=0. Else the coordinates from the hv-mv-station of the mv grid are used. Default: False @@ -955,16 +958,49 @@ def draw_plotly( Plotly figure with branches and nodes. """ - # initialization + # initialization coordinate transformation transformer_4326_to_3035 = Transformer.from_crs( "EPSG:4326", "EPSG:3035", always_xy=True, ) + def get_coordinates_for_edge(edge): + x0, y0 = G.nodes[edge[0]]["pos"] + x1, y1 = G.nodes[edge[1]]["pos"] + x0, y0 = transformer_4326_to_3035.transform(x0, y0) + x1, y1 = transformer_4326_to_3035.transform(x1, y1) + return x0, y0, x1, y1 + + line_color_options = ["loading", "relative_loading", "reinforce"] + if line_color not in line_color_options: + raise KeyError(f"Line colors need to be one of {line_color_options}") + + fig = go.Figure( + layout=go.Layout( + height=500, + showlegend=False, + hovermode="closest", + margin=dict(b=20, l=5, r=5, t=40), + xaxis=dict( + showgrid=True, + zeroline=True, + showticklabels=True, + ), + yaxis=dict( + showgrid=True, + zeroline=True, + showticklabels=True, + scaleanchor="x", + scaleratio=1, + ), + ), + ) + if G is None: G = edisgo_obj.topology.mv_grid.graph + # Center transformer coordinates on (0,0). if hasattr(grid, "transformers_df"): node_root = grid.transformers_df.bus1.iat[0] x_root, y_root = G.nodes[node_root]["pos"] @@ -977,6 +1013,7 @@ def draw_plotly( x_root, y_root = transformer_4326_to_3035.transform(x_root, y_root) + # Select the values for loads and nodes. s_res_view = edisgo_obj.results.s_res.T.index.isin( [edge[2]["branch_name"] for edge in G.edges.data()] ) @@ -997,10 +1034,7 @@ def draw_plotly( middle_node_text = [] for edge in G.edges(data=True): - x0, y0 = G.nodes[edge[0]]["pos"] - x1, y1 = G.nodes[edge[1]]["pos"] - x0, y0 = transformer_4326_to_3035.transform(x0, y0) - x1, y1 = transformer_4326_to_3035.transform(x1, y1) + x0, y0, x1, y1 = get_coordinates_for_edge(edge) middle_node_x.append((x0 - x_root + x1 - x_root) / 2) middle_node_y.append((y0 - y_root + y1 - y_root) / 2) @@ -1028,41 +1062,69 @@ def draw_plotly( middle_node_text.append(text) - middle_node_trace = go.Scatter( + middle_node_scatter = go.Scattergl( x=middle_node_x, y=middle_node_y, text=middle_node_text, mode="markers", hoverinfo="text", - marker=dict(opacity=0.0, size=10, color="white"), + marker=dict( + opacity=0.0, + size=10, + color="white", + ), + showlegend=False, ) - - data = [middle_node_trace] + fig.add_trace(middle_node_scatter) # line plot + showscale = True if line_color == "loading": color_min = s_res.T.min() color_max = s_res.T.max() - + colorscale = "YlOrRd" elif line_color == "relative_loading": color_min = 0 color_max = 1 + colorscale = "YlOrRd" + elif line_color == "reinforce": + color_min = 0 + color_max = 1 + colorscale = [[0, "green"], [1, "red"]] + else: + showscale = False for edge in G.edges(data=True): - x0, y0 = G.nodes[edge[0]]["pos"] - x1, y1 = G.nodes[edge[1]]["pos"] - x0, y0 = transformer_4326_to_3035.transform(x0, y0) - x1, y1 = transformer_4326_to_3035.transform(x1, y1) - + x0, y0, x1, y1 = get_coordinates_for_edge(edge) edge_x = [x0 - x_root, x1 - x_root, None] edge_y = [y0 - y_root, y1 - y_root, None] branch_name = edge[2]["branch_name"] if line_color == "reinforce": - if edisgo_obj.results.grid_expansion_costs.index.isin([branch_name]).any(): - color = "lightgreen" - else: + try: + # Possible distinction between added parallel lines and changed lines + if ( + edisgo_obj.results.equipment_changes.index[ + edisgo_obj.results.equipment_changes["change"] == "added" + ] + .isin([branch_name]) + .any() + ): + color = "green" + # Changed lines + elif ( + edisgo_obj.results.equipment_changes.index[ + edisgo_obj.results.equipment_changes["change"] == "changed" + ] + .isin([branch_name]) + .any() + ): + + color = "red" + else: + color = "black" + except Exception: color = "black" elif line_color == "loading": @@ -1071,6 +1133,7 @@ def draw_plotly( loading, vmin=color_min, vmax=color_max, + cmap_name=colorscale, ) elif line_color == "relative_loading": @@ -1080,21 +1143,48 @@ def draw_plotly( loading / s_nom, vmin=color_min, vmax=color_max, + cmap_name=colorscale, ) if loading > s_nom: color = "green" else: color = "black" - edge_trace = go.Scatter( + edge_scatter = go.Scattergl( + mode="lines", x=edge_x, y=edge_y, hoverinfo="none", - opacity=0.4, - mode="lines", - line=dict(width=2, color=color), + opacity=0.5, + showlegend=False, + line=dict( + width=2, + color=color, + ), ) - data.append(edge_trace) + fig.add_trace(edge_scatter) + + colorbar_edge_scatter = go.Scattergl( + mode="markers", + x=[None], + y=[None], + marker=dict( + colorbar=dict( + title="Lines", xanchor="left", titleside="right", x=1.17, thickness=15 + ), + colorscale=colorscale, + cmax=color_max, + cmin=color_min, + showscale=showscale, + ), + ) + + if line_color == "reinforce": + colorbar_edge_scatter.marker.colorbar.tickmode = "array" + colorbar_edge_scatter.marker.colorbar.ticktext = ["added", "changed"] + colorbar_edge_scatter.marker.colorbar.tickvals = [0, 1] + + fig.add_trace(colorbar_edge_scatter) # node plot node_x = [] @@ -1107,12 +1197,10 @@ def draw_plotly( node_y.append(y - y_root) if node_color == "voltage_deviation": - colors = [] - + node_colors = [] for node in G.nodes(): color = v_res.loc[node] - 1 - - colors.append(color) + node_colors.append(color) colorbar = dict( thickness=15, @@ -1124,7 +1212,7 @@ def draw_plotly( cmid = 0 else: - colors = [len(adjacencies[1]) for adjacencies in G.adjacency()] + node_colors = [len(adjacencies[1]) for adjacencies in G.adjacency()] colorscale = "YlGnBu" cmid = None @@ -1173,7 +1261,7 @@ def draw_plotly( node_text.append(text) - node_trace = go.Scatter( + node_scatter = go.Scattergl( x=node_x, y=node_y, mode="markers", @@ -1182,31 +1270,14 @@ def draw_plotly( marker=dict( showscale=True, colorscale=colorscale, - reversescale=True, - color=colors, + color=node_colors, size=8, cmid=cmid, line_width=2, colorbar=colorbar, ), ) - - data.append(node_trace) - - fig = go.Figure( - data=data, - layout=go.Layout( - height=500, - titlefont_size=16, - showlegend=False, - hovermode="closest", - margin=dict(b=20, l=5, r=5, t=40), - xaxis=dict(showgrid=True, zeroline=True, showticklabels=True), - yaxis=dict(showgrid=True, zeroline=True, showticklabels=True), - ), - ) - - fig.update_yaxes(scaleanchor="x", scaleratio=1) + fig.add_trace(node_scatter) return fig @@ -1225,7 +1296,6 @@ def chosen_graph( Grid name. Can be either 'Grid' to select the MV grid with all LV grids or the name of the MV grid to select only the MV grid or the name of one of the LV grids of the eDisGo object to select a specific LV grid. - Returns ------- (:networkx:`networkx.Graph`, :class:`~.network.grids.Grid` or bool) @@ -1235,6 +1305,7 @@ def chosen_graph( """ mv_grid = edisgo_obj.topology.mv_grid + lv_grid_name_list = list(map(str, edisgo_obj.topology.mv_grid.lv_grids)) if selected_grid == "Grid": G = edisgo_obj.to_graph() @@ -1256,32 +1327,16 @@ def chosen_graph( return G, grid -def dash_plot( - edisgo_objects: EDisGo | dict[str, EDisGo], - line_plot_modes: list[str] | None = None, - node_plot_modes: list[str] | None = None, -) -> JupyterDash: +def dash_plot(edisgo_objects: EDisGo | dict[str, EDisGo]) -> JupyterDash: """ Generates a jupyter dash app from given eDisGo object(s). - TODO: The app doesn't display two seperate colorbars for line and bus values atm - Parameters ---------- edisgo_objects : :class:`~.EDisGo` or dict[str, :class:`~.EDisGo`] eDisGo objects to show in plotly dash app. In the case of multiple edisgo objects pass a dictionary with the eDisGo objects as values and the respective eDisGo object names as keys. - line_plot_modes : list(str), optional - List of line plot modes to display in plotly dash app. See - :py:func:`~edisgo.tools.plots.draw_plotly` for more information. If None is - passed the modes 'reinforce', 'loading' and 'relative_loading' will be used. - Default: None - node_plot_modes : list(str), optional - List of line plot modes to display in plotly dash app. See - :py:func:`~edisgo.tools.plots.draw_plotly` for more information. If None is - passed the modes 'adjacencies' and 'voltage_deviation' will be used. - Default: None Returns ------- @@ -1292,16 +1347,20 @@ def dash_plot( if isinstance(edisgo_objects, dict): edisgo_name_list = list(edisgo_objects.keys()) edisgo_obj_1 = list(edisgo_objects.values())[0] + + edisgo_obj_1_mv_grid_name = str(edisgo_obj_1.topology.mv_grid) + for edisgo_obj in edisgo_objects.values(): + if edisgo_obj_1_mv_grid_name != str(edisgo_obj.topology.mv_grid): + raise ValueError("edisgo_objects are not matching") + else: edisgo_name_list = ["edisgo_obj"] edisgo_obj_1 = edisgo_objects grid_name_list = ["Grid"] + edisgo_obj_1.topology._grids_repr - if line_plot_modes is None: - line_plot_modes = ["reinforce", "loading", "relative_loading"] - if node_plot_modes is None: - node_plot_modes = ["adjacencies", "voltage_deviation"] + line_plot_modes = ["reinforce", "loading", "relative_loading"] + node_plot_modes = ["adjacencies", "voltage_deviation"] if edisgo_obj_1.timeseries.is_worst_case: timestep_labels = [ @@ -1656,3 +1715,24 @@ def update_figure( return fig return app + + +def show_dash_plot( + edisgo_objects: EDisGo | dict[str, EDisGo], + debug: bool = False, +): + """ + Shows the generated jupyter dash app from given eDisGo object(s). + + Parameters + ---------- + edisgo_objects : :class:`~.EDisGo` or dict[str, :class:`~.EDisGo`] + eDisGo objects to show in plotly dash app. In the case of multiple edisgo + objects pass a dictionary with the eDisGo objects as values and the respective + eDisGo object names as keys. + + debug: bool + Enables debugging of the jupyter dash app. + """ + app = dash_plot(edisgo_objects) + app.run_server(mode="inline", debug=debug, height=700) From 6b9142c7dbe1825801b47ac8f8f5c2f7739448b6 Mon Sep 17 00:00:00 2001 From: Malte Jahn Date: Fri, 29 Jul 2022 10:59:30 +0200 Subject: [PATCH 08/43] Fix bugs and add tests - Raise error when no results are passed to plot results - Add tests for pseudo coordinates and plotting --- edisgo/tools/plots.py | 6 +++ examples/plot_example.ipynb | 28 ++++++----- tests/tools/test_plots.py | 70 +++++++++++++++++++++++--- tests/tools/test_pseudo_coordinates.py | 4 ++ 4 files changed, 89 insertions(+), 19 deletions(-) diff --git a/edisgo/tools/plots.py b/edisgo/tools/plots.py index f0ba49603..c70bafcc2 100644 --- a/edisgo/tools/plots.py +++ b/edisgo/tools/plots.py @@ -976,6 +976,12 @@ def get_coordinates_for_edge(edge): if line_color not in line_color_options: raise KeyError(f"Line colors need to be one of {line_color_options}") + if edisgo_obj.results.s_res.empty and edisgo_obj.results.v_res.empty: + if line_color in ["loading", "relative_loading"]: + raise ValueError("No results to show. -> Run power flow.") + if node_color in ["voltage_deviation"]: + raise ValueError("No results to show. -> Run power flow.") + fig = go.Figure( layout=go.Layout( height=500, diff --git a/examples/plot_example.ipynb b/examples/plot_example.ipynb index 198302ec6..fad8769c0 100755 --- a/examples/plot_example.ipynb +++ b/examples/plot_example.ipynb @@ -49,7 +49,7 @@ "\n", "from edisgo import EDisGo\n", "from edisgo.tools.plots import draw_plotly\n", - "from edisgo.tools.plots import dash_plot" + "from edisgo.tools.plots import show_dash_plot" ] }, { @@ -145,6 +145,15 @@ "edisgo_reinforced.reinforce()" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "edisgo_reinforced.results.equipment_changes.loc[\"Line_10006\",\"change\"]=\"added\"" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -276,8 +285,7 @@ "metadata": {}, "outputs": [], "source": [ - "app = dash_plot(edisgo_objects=edisgo_root)\n", - "app.run_server(mode=\"inline\")" + "show_dash_plot(edisgo_objects=edisgo_root)" ] }, { @@ -293,10 +301,7 @@ "metadata": {}, "outputs": [], "source": [ - "app = dash_plot(\n", - " edisgo_objects={\"edisgo_obj_1\": edisgo_root, \"edisgo_obj_2\": edisgo_reinforced}\n", - ")\n", - "app.run_server(mode=\"inline\")" + "show_dash_plot(edisgo_objects={\"edisgo_obj_1\": edisgo_root, \"edisgo_obj_2\": edisgo_reinforced})" ] }, { @@ -314,14 +319,13 @@ "metadata": {}, "outputs": [], "source": [ - "app = dash_plot(\n", + "show_dash_plot(\n", " edisgo_objects={\n", " \"edisgo_obj_1\": edisgo_root,\n", " \"edisgo_obj_2\": edisgo_reinforced,\n", - " \"edisgo_obj_3\": edisgo_copy,\n", + " \"edisgo_obj_3\": edisgo_copy\n", " }\n", - ")\n", - "app.run_server(mode=\"inline\")" + ")" ] }, { @@ -348,7 +352,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.10" + "version": "3.8.12" }, "toc": { "base_numbering": 1, diff --git a/tests/tools/test_plots.py b/tests/tools/test_plots.py index 0e385d2e0..8db6613ce 100644 --- a/tests/tools/test_plots.py +++ b/tests/tools/test_plots.py @@ -1,27 +1,83 @@ +import copy + import pytest from edisgo import EDisGo -from edisgo.tools.plots import dash_plot +from edisgo.tools.plots import chosen_graph, dash_plot, draw_plotly class TestPlots: @classmethod def setup_class(cls): - cls.edisgo = EDisGo(ding0_grid=pytest.ding0_test_network_path) - cls.edisgo.set_time_series_worst_case_analysis() - cls.edisgo.reinforce() + cls.edisgo_root = EDisGo(ding0_grid=pytest.ding0_test_network_path) + cls.edisgo_root.set_time_series_worst_case_analysis() + cls.edisgo_analyzed = copy.deepcopy(cls.edisgo_root) + cls.edisgo_reinforced = copy.deepcopy(cls.edisgo_root) + cls.edisgo_analyzed.analyze() + cls.edisgo_reinforced.reinforce() + + def test_draw_plotly(self): + # test + edisgo_obj = self.edisgo_root + grid = edisgo_obj.topology.mv_grid + G = grid.graph + + mode_lines = "reinforce" + mode_nodes = "adjacencies" + fig = draw_plotly( + edisgo_obj, G, line_color=mode_lines, node_color=mode_nodes, grid=grid + ) + fig.show() + + edisgo_obj = self.edisgo_reinforced + grid = edisgo_obj.topology.mv_grid + G = grid.graph + + mode_lines = "relative_loading" + mode_nodes = "voltage_deviation" + fig = draw_plotly( + edisgo_obj, G, line_color=mode_lines, node_color=mode_nodes, grid=grid + ) + fig.show() + + # plotting loading and voltage deviation, with unchanged coordinates + mode_lines = "loading" + mode_nodes = "voltage_deviation" + fig = draw_plotly( + edisgo_obj, G, line_color=mode_lines, node_color=mode_nodes, grid=False + ) + fig.show() + + # plotting reinforced lines and node adjacencies + edisgo_obj = self.edisgo_reinforced + edisgo_obj.results.equipment_changes.loc["Line_10006", "change"] = "added" + G = edisgo_obj.topology.mv_grid.graph + + mode_lines = "reinforce" + mode_nodes = "adjacencies" + fig = draw_plotly( + edisgo_obj, G, line_color=mode_lines, node_color=mode_nodes, grid=False + ) + fig.show() + + def test_chosen_graph(self): + chosen_graph(edisgo_obj=self.edisgo_root, selected_grid="Grid") + grid = str(self.edisgo_root.topology.mv_grid) + chosen_graph(edisgo_obj=self.edisgo_root, selected_grid=grid) + grid = list(map(str, self.edisgo_root.topology.mv_grid.lv_grids))[0] + chosen_graph(edisgo_obj=self.edisgo_root, selected_grid=grid) def test_dash_plot(self): # TODO: at the moment this doesn't really test anything. Add meaningful tests. # test if any errors occur when only passing one edisgo object app = dash_plot( - edisgo_objects=self.edisgo, + edisgo_objects=self.edisgo_root, ) # test if any errors occur when passing multiple edisgo objects app = dash_plot( # noqa: F841 edisgo_objects={ - "edisgo_1": self.edisgo, - "edisgo_2": self.edisgo, + "edisgo_1": self.edisgo_root, + "edisgo_2": self.edisgo_reinforced, } ) diff --git a/tests/tools/test_pseudo_coordinates.py b/tests/tools/test_pseudo_coordinates.py index b3751769d..4ada42f05 100644 --- a/tests/tools/test_pseudo_coordinates.py +++ b/tests/tools/test_pseudo_coordinates.py @@ -1,3 +1,4 @@ +import numpy as np import pytest from edisgo import EDisGo @@ -28,3 +29,6 @@ def test_make_pseudo_coordinates(self): ] assert round(coordinates[0], 5) == round(7.943307, 5) assert round(coordinates[1], 5) == round(48.080396, 5) + + assert self.edisgo_root.topology.buses_df.x.isin([np.NaN]).any() + assert not edisgo_pseudo_coordinates.topology.buses_df.x.isin([np.NaN]).any() From 97f5bea69c5566a5e13e2608ca3562e91fd7d331 Mon Sep 17 00:00:00 2001 From: Malte Jahn Date: Fri, 29 Jul 2022 13:38:25 +0200 Subject: [PATCH 09/43] Bug fix missing hover label nodes --- edisgo/tools/plots.py | 59 ++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/edisgo/tools/plots.py b/edisgo/tools/plots.py index c70bafcc2..6138ceea1 100644 --- a/edisgo/tools/plots.py +++ b/edisgo/tools/plots.py @@ -982,27 +982,6 @@ def get_coordinates_for_edge(edge): if node_color in ["voltage_deviation"]: raise ValueError("No results to show. -> Run power flow.") - fig = go.Figure( - layout=go.Layout( - height=500, - showlegend=False, - hovermode="closest", - margin=dict(b=20, l=5, r=5, t=40), - xaxis=dict( - showgrid=True, - zeroline=True, - showticklabels=True, - ), - yaxis=dict( - showgrid=True, - zeroline=True, - showticklabels=True, - scaleanchor="x", - scaleratio=1, - ), - ), - ) - if G is None: G = edisgo_obj.topology.mv_grid.graph @@ -1068,7 +1047,7 @@ def get_coordinates_for_edge(edge): middle_node_text.append(text) - middle_node_scatter = go.Scattergl( + middle_node_scatter = go.Scatter( x=middle_node_x, y=middle_node_y, text=middle_node_text, @@ -1081,7 +1060,7 @@ def get_coordinates_for_edge(edge): ), showlegend=False, ) - fig.add_trace(middle_node_scatter) + data = [middle_node_scatter] # line plot showscale = True @@ -1156,7 +1135,7 @@ def get_coordinates_for_edge(edge): else: color = "black" - edge_scatter = go.Scattergl( + edge_scatter = go.Scatter( mode="lines", x=edge_x, y=edge_y, @@ -1168,9 +1147,9 @@ def get_coordinates_for_edge(edge): color=color, ), ) - fig.add_trace(edge_scatter) + data.append(edge_scatter) - colorbar_edge_scatter = go.Scattergl( + colorbar_edge_scatter = go.Scatter( mode="markers", x=[None], y=[None], @@ -1190,7 +1169,7 @@ def get_coordinates_for_edge(edge): colorbar_edge_scatter.marker.colorbar.ticktext = ["added", "changed"] colorbar_edge_scatter.marker.colorbar.tickvals = [0, 1] - fig.add_trace(colorbar_edge_scatter) + data.append(colorbar_edge_scatter) # node plot node_x = [] @@ -1267,7 +1246,7 @@ def get_coordinates_for_edge(edge): node_text.append(text) - node_scatter = go.Scattergl( + node_scatter = go.Scatter( x=node_x, y=node_y, mode="markers", @@ -1283,7 +1262,29 @@ def get_coordinates_for_edge(edge): colorbar=colorbar, ), ) - fig.add_trace(node_scatter) + data.append(node_scatter) + + fig = go.Figure( + data=data, + layout=go.Layout( + height=500, + showlegend=False, + hovermode="closest", + margin=dict(b=20, l=5, r=5, t=40), + xaxis=dict( + showgrid=True, + zeroline=True, + showticklabels=True, + ), + yaxis=dict( + showgrid=True, + zeroline=True, + showticklabels=True, + scaleanchor="x", + scaleratio=1, + ), + ), + ) return fig From f1f8e97cd8250268882756247e99d7cd39d079e9 Mon Sep 17 00:00:00 2001 From: Malte Jahn Date: Fri, 29 Jul 2022 14:13:01 +0200 Subject: [PATCH 10/43] Bug fix missing MV_Grid in dash_plot --- edisgo/tools/plots.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/edisgo/tools/plots.py b/edisgo/tools/plots.py index 6138ceea1..598acbc25 100644 --- a/edisgo/tools/plots.py +++ b/edisgo/tools/plots.py @@ -1312,7 +1312,6 @@ def chosen_graph( """ mv_grid = edisgo_obj.topology.mv_grid - lv_grid_name_list = list(map(str, edisgo_obj.topology.mv_grid.lv_grids)) if selected_grid == "Grid": G = edisgo_obj.to_graph() @@ -1364,7 +1363,11 @@ def dash_plot(edisgo_objects: EDisGo | dict[str, EDisGo]) -> JupyterDash: edisgo_name_list = ["edisgo_obj"] edisgo_obj_1 = edisgo_objects - grid_name_list = ["Grid"] + edisgo_obj_1.topology._grids_repr + mv_grid = edisgo_obj_1.topology.mv_grid + + lv_grid_name_list = list(map(str, mv_grid.lv_grids)) + + grid_name_list = ["Grid", str(mv_grid)] + lv_grid_name_list line_plot_modes = ["reinforce", "loading", "relative_loading"] node_plot_modes = ["adjacencies", "voltage_deviation"] From b9036237bbf8384b1ee531d090d9cfcf1b176920 Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Tue, 30 Aug 2022 15:18:16 +0200 Subject: [PATCH 11/43] Optimal Relocation of CBs --- edisgo/flex_opt/reinforce_measures.py | 347 ++++++++++++++++++++++++++ 1 file changed, 347 insertions(+) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index 7a35d419d..1ddd8f372 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -9,6 +9,7 @@ _dijkstra as dijkstra_shortest_path_length, ) +from edisgo.network.components import Switch from edisgo.network.grids import LVGrid, MVGrid from edisgo.tools import geo @@ -1226,3 +1227,349 @@ def get_weight(u, v, data): top_edisgo.lines_df.loc[ top_edisgo.lines_df.bus0 == bus0, "bus0" ] = new_lv_busbar + + +def optimize_cb_location(edisgo_obj, mv_grid, mode="loadgen"): + """ + Locates the circuit breakers at the optimal position in the rings to + reduce the difference in loading of feeders + + Parameters + ---------- + edisgo_obj: + class:`~.EDisGo` + mv_grid : + class:`~.network.grids.MVGrid` + mode :obj:`str` + Type of loading. + 1-'load' + 2-'loadgen' + 3-'gen' + Default: 'loadgen'. + + + Notes:According to planning principles of MV grids, a MV ring is run as two strings + (half-rings) separated by a circuit breaker which is open at normal operation. + Assuming a ring (route which is connected to the root node at either sides), + the optimal position of a circuit breaker is defined as the position + (virtual cable) between two nodes where the conveyed current is minimal on the + route.Instead of the peak current,the peak load is used here (assuming a constant + voltage. + + The circuit breaker will be installed to a node in the main route of the ring + + If a ring is dominated by loads (peak load > peak capacity of generators), + only loads are used for determining the location of circuit breaker. + If generators are prevailing (peak load < peak capacity of generators), + only generator capacities are considered for relocation. + + Returns + ------- + obj:`str` + the node where the cb is located + + """ + logging.basicConfig(format=10) + # power factor of loads and generators + cos_phi_load = edisgo_obj.config["reactive_power_factor"]["mv_load"] + cos_phi_feedin = edisgo_obj.config["reactive_power_factor"]["mv_gen"] + + buses_df = edisgo_obj.topology.buses_df + lines_df = edisgo_obj.topology.lines_df + loads_df = edisgo_obj.topology.loads_df + generators_df = edisgo_obj.topology.generators_df + switches_df = edisgo_obj.topology.switches_df + transformers_df = edisgo_obj.topology.transformers_df + + station = mv_grid.station.index[0] + graph = mv_grid.graph + + def id_mv_node(mv_node): + """ + Returns id of mv node + Parameters + ---------- + mv_node:'str' + name of node. E.g. 'BusBar_mvgd_2534_lvgd_450268_MV' + + Returns + ------- + obj:`str` + the id of the node. E.g '450268' + """ + lv_bus_tranformer = transformers_df[transformers_df.bus0 == mv_node].bus1[0] + lv_id = buses_df[buses_df.index == lv_bus_tranformer].lv_grid_id[0] + return int(lv_id) + + def _sort_rings(remove_mv_station=True): + """ + Sorts the nodes beginning from HV/MV station in the ring. + + Parameters + ---------- + remove_mv_station : + obj:`boolean` + If True reinforcement HV/MV station is not included + Default: True. + + Returns + ------- + obj:'dict` + Dictionary with name of sorted nodes in the ring + """ + # close switches + switches = [ + Switch(id=_, topology=edisgo_obj.topology) + for _ in edisgo_obj.topology.switches_df.index + ] + switch_status = {} + for switch in switches: + switch_status[switch] = switch.state + switch.close() + # find rings in topology + graph = edisgo_obj.topology.to_graph() + rings = nx.cycle_basis(graph, root=station) + if remove_mv_station: + + for r in rings: + r.remove(station) + + # reopen switches + for switch in switches: + if switch_status[switch] == "open": + switch.open() + return rings + + def get_subtree_of_nodes(ring, graph): + """ + Finds all nodes of a tree that is connected to main nodes in the ring and are + (except main nodes) not part of the ring of main nodes (traversal of graph + from main nodes excluding nodes along ring). + Parameters + ---------- + edisgo_obj: + class:`~.EDisGo` + ring: + obj:'dict` + Dictionary with name of sorted nodes in the ring + graph + networkx:`networkx.Graph` + + Returns + ------- + obj:'dict` + index:main node + columns: nodes of main node's tree + """ + node_ring_d = {} + for node in ring: + + if node == station: + continue + + nodes_subtree = set() + for path in nx.shortest_path(graph, node).values(): + if len(path) > 1: + if (path[1] not in ring) and (path[1] != station): + nodes_subtree.update(path[1 : len(path)]) + + if len(nodes_subtree) == 0: + node_ring_d.setdefault(node, []).append(None) + else: + for node_subtree in nodes_subtree: + node_ring_d.setdefault(node, []).append(node_subtree) + + return node_ring_d + + def _calculate_peak_load_gen(bus_node): + """ + Cumulative peak load/generation of loads/generators connected to underlying + MV or LV grid + Parameters + ---------- + bus_node: + obj: bus_name of the node. + + Returns + ------- + obj:'list' + list of total generation and load of MV node + """ + if ( + bus_node + in buses_df[ + buses_df.index.str.contains("BusBar") + & (~buses_df.index.str.contains("virtual")) + & (buses_df.v_nom >= 10) + ].index.values + ): + id_node = id_mv_node(bus_node) + p_load = ( + loads_df[loads_df.index.str.contains(str(id_node))].p_set.sum() + / cos_phi_load + ) + p_gen = ( + generators_df[ + generators_df.index.str.contains(str(id_node)) + ].p_nom.sum() + / cos_phi_feedin + ) + + elif bus_node in buses_df[buses_df.index.str.contains("gen")].index.values: + p_gen = ( + generators_df[generators_df.bus == bus_node].p_nom.sum() + / cos_phi_feedin + ) + p_load = loads_df[loads_df.bus == bus_node].p_set.sum() / cos_phi_feedin + + else: + p_gen = 0 + p_load = 0 + + return [p_gen, p_load] + + def _circuit_breaker(ring): + """ + finds the circuit of the related ring + Parameters + ---------- + ring: + obj:'dict` + Dictionary with name of sorted nodes in the ring + Returns + ------- + obj: str + the name of circuit breaker + """ + circuit_breaker = [] + for node in ring: + + for switch in switches_df.bus_closed.values: + if switch in node: + circuit_b = switches_df.loc[ + switches_df.bus_closed == node, "bus_closed" + ].index[0] + circuit_breaker.append(circuit_b) + else: + continue + return circuit_breaker[0] + + def _change_dataframe(node_cb, ring): + + circuit_breaker = _circuit_breaker(ring) + + if node_cb != switches_df.loc[circuit_breaker, "bus_closed"]: + + node_existing = switches_df.loc[circuit_breaker, "bus_closed"] + new_virtual_bus = f"virtual_{node_cb}" + # if the adjacent node is previous circuit breaker + if f"virtual_{node2}" in mv_grid.graph.adj[node_cb]: + branch = mv_grid.graph.adj[node_cb][f"virtual_{node2}"]["branch_name"] + else: + branch = mv_grid.graph.adj[node_cb][node2]["branch_name"] + # Switch + # change bus0 + switches_df.loc[circuit_breaker, "bus_closed"] = node_cb + # change bus1 + switches_df.loc[circuit_breaker, "bus_open"] = new_virtual_bus + # change branch + switches_df.loc[circuit_breaker, "branch"] = branch + + # Bus + x_coord = buses_df.loc[node_cb, "x"] + y_coord = buses_df.loc[node_cb, "y"] + buses_df.rename(index={node_existing: new_virtual_bus}, inplace=True) + buses_df.loc[new_virtual_bus, "x"] = x_coord + buses_df.loc[new_virtual_bus, "y"] = y_coord + + buses_df.rename( + index={f"virtual_{node_existing}": node_existing}, inplace=True + ) + + # Line + lines_df.loc[ + lines_df.bus0 == f"virtual_{node_existing}", "bus0" + ] = node_existing + if lines_df.loc[branch, "bus0"] == node_cb: + lines_df.loc[branch, "bus0"] = new_virtual_bus + else: + lines_df.loc[branch, "bus1"] = new_virtual_bus + else: + logging.info("The location of switch disconnector has not changed") + + rings = _sort_rings(remove_mv_station=True) + for ring in rings: + node_ring_dictionary = get_subtree_of_nodes(ring, graph) + node_ring_df = pd.DataFrame.from_dict(node_ring_dictionary, orient="index") + + node_peak_d = {} + for index, value in node_ring_df.iterrows(): + total_peak_gen = 0 + total_peak_load = 0 + if value[0] is not None: + for v in value: + if v is None: + continue + # sum the load and generation of all subtree nodes + total_peak_gen += _calculate_peak_load_gen(v)[0] + total_peak_load += _calculate_peak_load_gen(v)[1] + # sum the load and generation of nodes of subtree and tree itself + total_peak_gen = total_peak_gen + _calculate_peak_load_gen(index)[0] + total_peak_load = total_peak_load + _calculate_peak_load_gen(index)[1] + else: + total_peak_gen += _calculate_peak_load_gen(index)[0] + total_peak_load += _calculate_peak_load_gen(index)[1] + node_peak_d.setdefault(index, []).append(total_peak_gen) + node_peak_d.setdefault(index, []).append(total_peak_load) + node_peak_df = pd.DataFrame.from_dict(node_peak_d, orient="index") + node_peak_df.rename( + columns={0: "total_peak_gen", 1: "total_peak_load"}, inplace=True + ) + + diff_min = 10e9 + if mode == "load": + node_peak_data = node_peak_df.total_peak_load + elif mode == "generation": + node_peak_data = node_peak_df.total_peak_gen + elif mode == "loadgen": + # is ring dominated by load or generation? + # (check if there's more load than generation in ring or vice versa) + if sum(node_peak_df.total_peak_load) > sum(node_peak_df.total_peak_gen): + node_peak_data = node_peak_df.total_peak_load + else: + node_peak_data = node_peak_df.total_peak_gen + else: + raise ValueError("parameter 'mode' is invalid!") + + for ctr in range(len(node_peak_df.index)): + + # split route and calc demand difference + route_data_part1 = sum(node_peak_data[0:ctr]) + route_data_part2 = sum(node_peak_data[ctr : len(node_peak_df.index)]) + + diff = abs(route_data_part1 - route_data_part2) + if diff <= diff_min: + diff_min = diff + position = ctr + else: + break + + # new cb location + node_cb = node_peak_df.index[position] + + # check if node is last node of ring + if position < len(node_peak_df.index): + # check which branch to disconnect by determining load difference + # of neighboring nodes + diff2 = abs( + sum(node_peak_data[0 : position + 1]) + - sum(node_peak_data[position + 1 : len(node_peak_data)]) + ) + + if diff2 < diff_min: + + node2 = node_peak_df.index[position + 1] + else: + node2 = node_peak_df.index[position - 1] + _change_dataframe(node_cb, ring) + return node_cb From fcd443e3bd4da13835234421888369b1d5947685 Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Tue, 15 Nov 2022 15:08:14 +0100 Subject: [PATCH 12/43] config change --- edisgo/config/config_grid_expansion_default.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/edisgo/config/config_grid_expansion_default.cfg b/edisgo/config/config_grid_expansion_default.cfg index a82e027c9..3dfb27c67 100644 --- a/edisgo/config/config_grid_expansion_default.cfg +++ b/edisgo/config/config_grid_expansion_default.cfg @@ -80,9 +80,9 @@ mv_feed-in_case_transformer = 1.0 mv_feed-in_case_line = 1.0 lv_load_case_transformer = 1.0 -lv_load_case_line = 1.0 +lv_load_case_line = 0.1 lv_feed-in_case_transformer = 1.0 -lv_feed-in_case_line = 1.0 +lv_feed-in_case_line = 0.1 # costs # ============ From 7d6c905a45ebe07b21dcb9143926969acb099656 Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Sun, 20 Nov 2022 20:58:21 +0100 Subject: [PATCH 13/43] integration of new reinforcement methods --- edisgo/flex_opt/costs.py | 33 ++- edisgo/flex_opt/reinforce_grid_alternative.py | 251 ++++++++++++++++ edisgo/flex_opt/reinforce_measures.py | 277 ++++++++++-------- 3 files changed, 445 insertions(+), 116 deletions(-) create mode 100644 edisgo/flex_opt/reinforce_grid_alternative.py diff --git a/edisgo/flex_opt/costs.py b/edisgo/flex_opt/costs.py index e7a428678..4e5a62f65 100644 --- a/edisgo/flex_opt/costs.py +++ b/edisgo/flex_opt/costs.py @@ -173,7 +173,7 @@ def _get_line_costs(lines_added): lines_added.index, "type_info" ].values, "total_costs": line_costs.costs.values, - "length": ( + "total cable length": ( lines_added.quantity * lines_added.length ).values, "quantity": lines_added.quantity.values, @@ -281,3 +281,34 @@ def line_expansion_costs(edisgo_obj, lines_names): ] ) return costs_lines.loc[lines_df.index] + + +def cost_breakdown(edisgo_obj, lines): + # costs for lines + # get changed lines + + lines_added = lines.iloc[ + ( + lines.equipment + == edisgo_obj.topology.lines_df.loc[lines.index, "type_info"] + ).values + ]["quantity"].to_frame() + lines_added_unique = lines_added.index.unique() + lines_added = ( + lines_added.groupby(axis=0, level=0).sum().loc[lines_added_unique, ["quantity"]] + ) + lines_added["length"] = edisgo_obj.topology.lines_df.loc[ + lines_added.index, "length" + ] + if not lines_added.empty: + costs_lines = line_expansion_costs(edisgo_obj, lines_added.index) + costs_lines["costs"] = costs_lines.apply( + lambda x: x.costs_earthworks + + x.costs_cable * lines_added.loc[x.name, "quantity"], + axis=1, + ) + costs_lines["costs_cable"] = costs_lines.apply( + lambda x: x.costs_cable * lines_added.loc[x.name, "quantity"], + axis=1, + ) + return costs_lines diff --git a/edisgo/flex_opt/reinforce_grid_alternative.py b/edisgo/flex_opt/reinforce_grid_alternative.py new file mode 100644 index 000000000..628d7ad15 --- /dev/null +++ b/edisgo/flex_opt/reinforce_grid_alternative.py @@ -0,0 +1,251 @@ +from __future__ import annotations + +import copy +import datetime +import logging + +import pandas as pd + +from edisgo.flex_opt import check_tech_constraints as checks +from edisgo.flex_opt import exceptions, reinforce_measures +from edisgo.flex_opt.costs import grid_expansion_costs +from edisgo.tools import tools + +logger = logging.getLogger(__name__) + + +def reinforce_line_overloading_alternative( + edisgo, + timesteps_pfa=None, + copy_grid=False, + mode=None, + max_while_iterations=20, + without_generator_import=False, +): + """ + Evaluates network reinforcement needs and performs measures. + + This function is the parent function for all network reinforcements. + + Parameters + ---------- + edisgo: class:`~.EDisGo` + The eDisGo API object + timesteps_pfa: str or \ + :pandas:`pandas.DatetimeIndex` or \ + :pandas:`pandas.Timestamp` + timesteps_pfa specifies for which time steps power flow analysis is + conducted and therefore which time steps to consider when checking + for over-loading and over-voltage issues. + It defaults to None in which case all timesteps in + timeseries.timeindex (see :class:`~.network.network.TimeSeries`) are + used. + Possible options are: + + * None + Time steps in timeseries.timeindex (see + :class:`~.network.network.TimeSeries`) are used. + * 'snapshot_analysis' + Reinforcement is conducted for two worst-case snapshots. See + :meth:`edisgo.tools.tools.select_worstcase_snapshots()` for further + explanation on how worst-case snapshots are chosen. + Note: If you have large time series choosing this option will save + calculation time since power flow analysis is only conducted for two + time steps. If your time series already represents the worst-case + keep the default value of None because finding the worst-case + snapshots takes some time. + * :pandas:`pandas.DatetimeIndex` or \ + :pandas:`pandas.Timestamp` + Use this option to explicitly choose which time steps to consider. + copy_grid:If True reinforcement is conducted on a copied grid and discarded. + Default: False. + mode : str + Determines network levels reinforcement is conducted for. Specify + * None to reinforce MV and LV network levels. None is the default. + * 'mv' to reinforce MV network level only, neglecting MV/LV stations, + and LV network topology. LV load and generation is aggregated per + LV network and directly connected to the primary side of the + respective MV/LV station. + * 'lv' to reinforce LV networks including MV/LV stations. + max_while_iterations : int + Maximum number of times each while loop is conducted. + without_generator_import: bool + If True excludes lines that were added in the generator import to + connect new generators to the topology from calculation of topology expansion + costs. Default: False. + + Returns + ------- + :class:`~.network.network.Results` + Returns the Results object holding network expansion costs, equipment + changes, etc. + + Assumptions + ------ + 1-The removing cost of cables are not incorporated. + 2-One type of line cost is used for mv and lv + 3-Line Reinforcements are done with the same type of lines as lines reinforced + + """ + + def _add_lines_changes_to_equipment_changes(): + edisgo_reinforce.results.equipment_changes = pd.concat( + [ + edisgo_reinforce.results.equipment_changes, + pd.DataFrame( + { + "iteration_step": [iteration_step] * len(lines_changes), + "change": ["changed"] * len(lines_changes), + "equipment": edisgo_reinforce.topology.lines_df.loc[ + lines_changes.keys(), "type_info" + ].values, + "quantity": [_ for _ in lines_changes.values()], + }, + index=lines_changes.keys(), + ), + ], + ) + + # check if provided mode is valid + if mode and mode not in ["mv", "lv"]: + raise ValueError(f"Provided mode {mode} is not a valid mode.") + # in case reinforcement needs to be conducted on a copied graph the + # edisgo object is deep copied + if copy_grid is True: + edisgo_reinforce = copy.deepcopy(edisgo) + else: + edisgo_reinforce = edisgo + + if timesteps_pfa is not None: + if isinstance(timesteps_pfa, str) and timesteps_pfa == "snapshot_analysis": + snapshots = tools.select_worstcase_snapshots(edisgo_reinforce) + # drop None values in case any of the two snapshots does not exist + timesteps_pfa = pd.DatetimeIndex( + data=[ + snapshots["max_residual_load"], + snapshots["min_residual_load"], + ] + ).dropna() + # if timesteps_pfa is not of type datetime or does not contain + # datetimes throw an error + elif not isinstance(timesteps_pfa, datetime.datetime): + if hasattr(timesteps_pfa, "__iter__"): + if not all(isinstance(_, datetime.datetime) for _ in timesteps_pfa): + raise ValueError( + f"Input {timesteps_pfa} for timesteps_pfa is not valid." + ) + else: + raise ValueError( + f"Input {timesteps_pfa} for timesteps_pfa is not valid." + ) + + iteration_step = 1 + analyze_mode = None if mode == "lv" else mode + + edisgo_reinforce.analyze(mode=analyze_mode, timesteps=timesteps_pfa) + + # 1-REINFORCE OVERLOADED LINES + logger.debug("==> Check line loadings.") + crit_lines_mv = checks.mv_line_load(edisgo_reinforce) + crit_lines_lv = checks.lv_line_load(edisgo_reinforce) + + # 1.1 Method: Split the feeder at the half-length of feeder (applied only once to + # secure n-1). + # 1.1.1- MV grid + if (not mode or mode == "mv") and not crit_lines_mv.empty: + # TODO: add method resiting of CB + logger.debug( + f"==>feeder splitting method is running for MV grid " + f"{edisgo_reinforce.topology.mv_grid}: " + ) + lines_changes = reinforce_measures.split_feeder_at_half_length( + edisgo_reinforce, edisgo_reinforce.topology.mv_grid, crit_lines_mv + ) + _add_lines_changes_to_equipment_changes() + # if not lines_changes: + + logger.debug("==> Run power flow analysis.") + edisgo_reinforce.analyze(mode=analyze_mode, timesteps=timesteps_pfa) + + # 1.1.2- LV grid + if (not mode or mode == "lv") and not crit_lines_lv.empty: + # TODO: add method split the feeder + add substation + for lv_grid in list(edisgo_reinforce.topology.mv_grid.lv_grids): + + logger.debug( + f"==>feeder splitting method is running for LV grid {lv_grid}: " + ) + lines_changes = reinforce_measures.split_feeder_at_half_length( + edisgo_reinforce, lv_grid, crit_lines_lv + ) + _add_lines_changes_to_equipment_changes() + + logger.debug("==> Run power flow analysis.") + edisgo_reinforce.analyze(mode=analyze_mode, timesteps=timesteps_pfa) + + logger.debug("==> Recheck line load.") + crit_lines = ( + pd.DataFrame(dtype=float) + if mode == "lv" + else checks.mv_line_load(edisgo_reinforce) + ) + + if not mode or mode == "lv": + crit_lines = pd.concat( + [ + crit_lines, + checks.lv_line_load(edisgo_reinforce), + ] + ) + + # 1.2 Method: Add same type of parallel line -MV and LV grid + while_counter = 0 + while not crit_lines.empty and while_counter < max_while_iterations: + + logger.debug(f"==>add parallel line method is running_Step{iteration_step}") + lines_changes = reinforce_measures.add_same_type_of_parallel_line( + edisgo_reinforce, crit_lines + ) + + _add_lines_changes_to_equipment_changes() + + logger.debug("==> Run power flow analysis.") + edisgo_reinforce.analyze(mode=analyze_mode, timesteps=timesteps_pfa) + + logger.debug("==> Recheck line load.") + crit_lines = ( + pd.DataFrame(dtype=float) + if mode == "lv" + else checks.mv_line_load(edisgo_reinforce) + ) + + if not mode or mode == "lv": + crit_lines = pd.concat( + [ + crit_lines, + checks.lv_line_load(edisgo_reinforce), + ] + ) + while_counter += 1 + iteration_step += +1 + # check if all load problems were solved after maximum number of + # iterations allowed + if while_counter == max_while_iterations and (not crit_lines.empty): + edisgo_reinforce.results.unresolved_issues = pd.concat( + [ + edisgo_reinforce.results.unresolved_issues, + crit_lines, + ] + ) + raise exceptions.MaximumIterationError( + "Overloading issues could not be solved after maximum allowed " + "iterations." + ) + else: + logger.info( + f"==> Load issues were solved in {while_counter} iteration step(s)." + ) + edisgo_reinforce.results.grid_expansion_costs = grid_expansion_costs( + edisgo_reinforce, without_generator_import=without_generator_import + ) + return edisgo_reinforce.results diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index d2943fa91..7c676cecb 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -746,26 +746,77 @@ def _replace_by_parallel_standard_lines(lines): return lines_changes -def split_feeder_at_half_length(edisgo_obj, grid, crit_lines): - # ToDo: Type hinting +def add_same_type_of_parallel_line(edisgo_obj, crit_lines): + """ + Adds one parallel line of same type. + Adds number of added lines to `lines_changes` dictionary. + + Parameters + ---------- + crit_lines: pandas:`pandas.DataFrame` + Dataframe containing over-loaded lines, their maximum relative + over-loading (maximum calculated current over allowed current) and the + corresponding time step. + Index of the dataframe are the names of the over-loaded lines. + Columns are 'max_rel_overload' containing the maximum relative + over-loading as float, 'time_index' containing the corresponding + time step the over-loading occured in as + :pandas:`pandas.Timestamp`, and 'voltage_level' specifying + the voltage level the line is in (either 'mv' or 'lv'). + edisgo_obj: class:`~.EDisGo` + + Returns + ------- + dict + Dictionary with name of lines as keys and the corresponding number of + lines added as values. + + Notes + ------ """ - The critical string load is remedied by the following methods: - 1-Find the point at the half-length of the feeder - 2-If the half-length of the feeder is the first node that comes from the main - station,reinforce the lines by adding parallel lines since the first node - directly is connected to the main station. - 3- Otherwise, find the next LV station comes after the mid-point of the - feeder and split the line from this point so that it is to be connected to - the main station. - 4-Find the preceding LV station of the newly disconnected LV station and - remove the linebetween these two LV stations to create 2 independent feeders. - 5- If grid: LV, do not count in the nodes in the building + lines_changes = {} + # add number of added lines to lines_changes + lines_changes.update( + pd.Series(index=crit_lines.index, data=[1] * len(crit_lines.index)).to_dict() + ) + + # update number of lines and accordingly line attributes + + edisgo_obj.topology.update_number_of_parallel_lines( + pd.Series( + index=crit_lines.index, + data=( + edisgo_obj.topology.lines_df[ + edisgo_obj.topology.lines_df.index.isin(crit_lines.index) + ].num_parallel + + 1 + ), + ) + ) + + return lines_changes + + +def split_feeder_at_half_length(edisgo_obj, grid, crit_lines): + """ + Reinforce lines in MV or LV topology due to overloading. + + 1-The point at half the length of the feeder is found. + 2-The first node following this point is chosen as the point where the new + connection will be made. This node can only be a station. + 3-This node is disconnected from the previous node and connected to the main station + + Notes: + In LV grids, the node inside the building is not considered. + If the node is the first node after the main station, the method is + not applied. + Parameters ---------- - edisgo_obj:class:`~.EDisGo` + edisgo_obj: class:`~.EDisGo` grid: class:`~.network.grids.MVGrid` or :class:`~.network.grids.LVGrid` crit_lines: Dataframe containing over-loaded lines, their maximum relative over-loading (maximum calculated current over allowed current) and the @@ -787,67 +838,70 @@ def split_feeder_at_half_length(edisgo_obj, grid, crit_lines): Notes ----- - In this method, the division is done according to the longest route (not the feeder - has more load) - + In this method, the separation is done according to the longest route + (not the feeder has more load) """ - # TODO: to be integrated in the future outside of functions def get_weight(u, v, data): return data["length"] if isinstance(grid, LVGrid): - standard_line = edisgo_obj.config["grid_expansion_standard_equipment"][ - "lv_line" - ] voltage_level = "lv" - G = grid.graph - station_node = list(G.nodes)[0] # main station - # ToDo:implement the method in crit_lines_feeder to relevant lines - # find all the lv lines that have overloading issues in lines_df relevant_lines = edisgo_obj.topology.lines_df.loc[ crit_lines[crit_lines.voltage_level == voltage_level].index ] - - # find the most critical lines connected to different LV feeder in MV/LV station - crit_lines_feeder = relevant_lines[ - relevant_lines["bus0"].str.contains("LV") - & relevant_lines["bus0"].str.contains(repr(grid).split("_")[1]) - ] - + """ + # TODO:to be deleted after decision + if not relevant_lines.empty: + nominal_voltage = edisgo_obj.topology.buses_df.loc[ + edisgo_obj.topology.lines_df.loc[relevant_lines.index[0], "bus0"], + "v_nom", + ] + standard_line_type = edisgo_obj.config["grid_expansion_standard_equipment"][ + "lv_line" + ] + """ elif isinstance(grid, MVGrid): - standard_line = edisgo_obj.config["grid_expansion_standard_equipment"][ - "mv_line" - ] voltage_level = "mv" - G = grid.graph - # Todo: the overlading can occur not only between the main node and - # its next node - station_node = grid.transformers_df.bus1.iat[0] - # find all the mv lines that have overloading issues in lines_df relevant_lines = edisgo_obj.topology.lines_df.loc[ crit_lines[crit_lines.voltage_level == voltage_level].index ] - - # find the most critical lines connected to different LV feeder in MV/LV station - crit_lines_feeder = relevant_lines[relevant_lines["bus0"] == station_node] - - # find the closed and open sides of switches - switch_df = edisgo_obj.topology.switches_df.loc[ - :, "bus_closed":"bus_open" - ].values - switches = [node for nodes in switch_df for node in nodes] + # TODO:to be deleted after decision + """ + if not relevant_lines.empty: + nominal_voltage = edisgo_obj.topology.buses_df.loc[ + edisgo_obj.topology.lines_df.loc[relevant_lines.index[0], "bus0"], + "v_nom", + ] + standard_line_type = edisgo_obj.config["grid_expansion_standard_equipment"][ + "lv_line" + ] + """ else: raise ValueError(f"Grid Type {type(grid)} is not supported.") + G = grid.graph + station_node = list(G.nodes)[0] # main station + + # The most overloaded lines, generally first lines connected to the main station + crit_lines_feeder = relevant_lines[relevant_lines["bus0"] == station_node] + + # the last node of each feeder of the ring networks (switches are open) + switch_df = edisgo_obj.topology.switches_df.loc[:, "bus_closed":"bus_open"].values + switches = [node for last_nodes in switch_df for node in last_nodes] + if isinstance(grid, LVGrid): nodes = G else: nodes = switches + # for the radial feeders in MV grid + for node in G.nodes: + if node in crit_lines.index.values: + nodes.append(node) paths = {} nodes_feeder = {} @@ -863,8 +917,10 @@ def get_weight(u, v, data): lines_changes = {} - for node_list in nodes_feeder.values(): - + for node_feeder, node_list in nodes_feeder.items(): + feeder_first_line = crit_lines_feeder[ + crit_lines_feeder.bus1 == node_feeder + ].index[0] farthest_node = node_list[-1] path_length_dict_tmp = dijkstra_shortest_path_length( @@ -878,7 +934,7 @@ def get_weight(u, v, data): if path_length_dict_tmp[j] >= path_length_dict_tmp[farthest_node] * 1 / 2 ) - # if LVGrid: check if node_1_2 is outside of a house + # if LVGrid: check if node_1_2 is outside a house # and if not find next BranchTee outside the house if isinstance(grid, LVGrid): while ( @@ -888,84 +944,75 @@ def get_weight(u, v, data): node_1_2 = path[path.index(node_1_2) - 1] # break if node is station if node_1_2 is path[0]: - logger.error("Could not reinforce overloading issue.") + logger.error( + f" {feeder_first_line} and following lines could not " + f"be reinforced due to insufficient number of node . " + ) break # if MVGrid: check if node_1_2 is LV station and if not find - # next LV station + # next or preceding LV station else: while node_1_2 not in edisgo_obj.topology.transformers_df.bus0.values: try: - # try to find LVStation behind node_1_2 node_1_2 = path[path.index(node_1_2) + 1] except IndexError: - # if no LVStation between node_1_2 and node with - # voltage problem, connect node - # directly toMVStation - node_1_2 = farthest_node - break - - # if node_1_2 is a representative (meaning it is already - # directly connected to the station), line cannot be - # disconnected and must therefore be reinforced - # todo:add paralell line to all other lines in case - if node_1_2 in nodes_feeder.keys(): - crit_line_name = G.get_edge_data(station_node, node_1_2)["branch_name"] - crit_line = edisgo_obj.topology.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 - - # if node_1_2 is not a representative, disconnect line - else: - # get line between node_1_2 and predecessor node (that is - # closer to the station) + while ( + node_1_2 not in edisgo_obj.topology.transformers_df.bus0.values + ): + if path.index(node_1_2) > 1: + node_1_2 = path[path.index(node_1_2) - 1] + else: + logger.error( + f" {feeder_first_line} and following lines could not " + f"be reinforced due to the lack of LV station . " + ) + break + + # if node_1_2 is a representative (meaning it is already directly connected + # to the station), line cannot be disconnected and reinforced + if node_1_2 not in nodes_feeder.keys(): + # get line between node_1_2 and predecessor node pred_node = path[path.index(node_1_2) - 1] - crit_line_name = G.get_edge_data(node_1_2, 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 + line_removed = G.get_edge_data(node_1_2, pred_node)["branch_name"] + + # note:line between node_1_2 and pred_node is not removed and the connection + # points of line ,changed from the node to main station, is changed. + # Therefore, the line connected to the main station has the same name + # with the line to be removed. + # todo: the name of added line should be + # created and name of removed line should be deleted from the lines_df + + # change the connection of the node_1_2 from pred node to main station + if grid.lines_df.at[line_removed, "bus0"] == pred_node: + + edisgo_obj.topology._lines_df.at[line_removed, "bus0"] = station_node + logger.info( + f"==> {grid}--> the line {line_removed} disconnected from " + f"{pred_node} and connected to the main station {station_node} " + ) + elif grid.lines_df.at[line_removed, "bus1"] == pred_node: + edisgo_obj.topology._lines_df.at[line_removed, "bus1"] = station_node + logger.info( + f"==> {grid}-->the line {line_removed} disconnected from " + f"{pred_node} and connected to the main station {station_node} " + ) else: - raise ValueError("Bus not in line buses. " "Please check.") - - # change line length and type - + # change the line length + # the properties of the added line are the same as the removed line edisgo_obj.topology._lines_df.at[ - crit_line_name, "length" + line_removed, "length" ] = path_length_dict_tmp[node_1_2] - edisgo_obj.topology.change_line_type([crit_line_name], standard_line) - lines_changes[crit_line_name] = 1 - - if not lines_changes: - logger.debug( - f"==> {len(lines_changes)} line(s) was/were reinforced due to loading " - "issues." + line_added = line_removed + lines_changes[line_added] = 1 + if lines_changes: + logger.info( + f"{len(lines_changes)} line/s are reinforced by split feeder " + f"method in {grid}" ) + return lines_changes From fc31391f6babf9ae1016f62cf87b90ed1eb71734 Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Sun, 20 Nov 2022 21:41:56 +0100 Subject: [PATCH 14/43] config_revert --- edisgo/config/config_grid_expansion_default.cfg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/edisgo/config/config_grid_expansion_default.cfg b/edisgo/config/config_grid_expansion_default.cfg index 3dfb27c67..c67c38bb2 100644 --- a/edisgo/config/config_grid_expansion_default.cfg +++ b/edisgo/config/config_grid_expansion_default.cfg @@ -75,14 +75,14 @@ mv_lv_station_feed-in_case_max_v_deviation = 0.015 # ============ # Source: Rehtanz et. al.: "Verteilnetzstudie für das Land Baden-Württemberg", 2017. mv_load_case_transformer = 0.5 -mv_load_case_line = 0.5 +mv_load_case_line = 1.0 mv_feed-in_case_transformer = 1.0 mv_feed-in_case_line = 1.0 lv_load_case_transformer = 1.0 -lv_load_case_line = 0.1 +lv_load_case_line = 1.0 lv_feed-in_case_transformer = 1.0 -lv_feed-in_case_line = 0.1 +lv_feed-in_case_line = 1.0 # costs # ============ From 3aecfb0dde598022ab92ffa4b4d5e0c22f6796e5 Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Sun, 27 Nov 2022 23:55:58 +0100 Subject: [PATCH 15/43] integration_split+station --- edisgo/flex_opt/reinforce_measures.py | 477 +++++++++-------- edisgo/tools/coordination.py | 719 -------------------------- 2 files changed, 273 insertions(+), 923 deletions(-) delete mode 100644 edisgo/tools/coordination.py diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index 7c676cecb..660051e84 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -11,7 +11,6 @@ from edisgo.network.components import Switch from edisgo.network.grids import LVGrid, MVGrid -from edisgo.tools import geo logger = logging.getLogger(__name__) @@ -801,17 +800,17 @@ def add_same_type_of_parallel_line(edisgo_obj, crit_lines): def split_feeder_at_half_length(edisgo_obj, grid, crit_lines): """ - Reinforce lines in MV or LV topology due to overloading. + The critical string load in MV and LV grid is remedied by splitting the feeder + at the half-length - 1-The point at half the length of the feeder is found. + 1-The point at half the length of the feeders is found. 2-The first node following this point is chosen as the point where the new connection will be made. This node can only be a station. 3-This node is disconnected from the previous node and connected to the main station Notes: In LV grids, the node inside the building is not considered. - If the node is the first node after the main station, the method is - not applied. + The method is not applied if the node is the first node after the main station. Parameters @@ -1016,27 +1015,35 @@ def get_weight(u, v, data): return lines_changes -def add_substation_at_half_length(edisgo_obj, grid, crit_lines): +def add_station_at_half_length(edisgo_obj, grid, crit_lines): """ + If the number of overloaded feeders in the LV grid is more than 2, the feeders are + split at their half-length, and the disconnected points are connected to the + new MV/LV station. - *This method can be implemented only to LV grids - - The critical string load in LV grid is remedied by the splitting the feeder - at the half-length and adding a new MV/LV station - 1-Find the points at the half-length of the feeders - 2-Add new MV/LV station with standard transformer into the MV grid at this point - 3-The distance between the existing station (eg. Busbar_mvgd_460_lvgd_131525_MV) - and newly added MV/LV station (eg. Busbar_mvgd_460_lvgd_131525_sep1_MV),is equal - to the length between the mid point of the feeder in the LV grid - nd preceding node of this point (eg. BranchTee_mvgd_460_lvgd_131525_5) + 1-The point at half the length of the feeders is found. + 2-The first node following this point is chosen as the point where the new + connection will be made. This node can only be a station. + 3-This node is disconnected from the previous node and connected to a new station. + 4-New MV/LV is connected to the existing MV/LV station with a line of which length + equals the line length between the node at the half-length (node_1_2) and its + preceding node. + Notes: + -If the number of overloaded lines in the LV grid is less than 3 and the node_1_2 + is the first node after the main station, the method is not applied. + -The name of the new grid will be the existing grid code + (e.g. 40000) + 1001 = 400001001 + -The name of the lines in the new LV grid is the same as the grid where the nodes + are removed + -Except line names, all the data frames are named based on the new grid name Parameters ---------- - edisgo_obj:class:`~.EDisGo` - grid: class:`~.network.grids.MVGrid` or :class:`~.network.grids.LVGrid` - crit_lines: Dataframe containing over-loaded lines, their maximum relative + edisgo_obj: class:`~.EDisGo` + grid: class:`~.network.grids.LVGrid` + crit_lines: Dataframe containing over-loaded lines, their maximum relative over-loading (maximum calculated current over allowed current) and the corresponding time step. Index of the data frame is the names of the over-loaded lines. @@ -1046,247 +1053,309 @@ def add_substation_at_half_length(edisgo_obj, grid, crit_lines): :pandas:`pandas.Timestamp`, and 'voltage_level' specifying the voltage level the line is in (either 'mv' or 'lv'). - Returns ------- - #todo: changes + line_changes= dict + Dictionary with name of lines as keys and the corresponding number of + lines added as values. + transformer_changes= dict + Dictionary with added and removed transformers in the form:: - Notes - ----- - In this method, the seperation is done according to the longest route - (not the feeder has more load) - !! The name of the nodes moved to the new LV grid and the names of the - lines, buses, etc.connected to these nodes can remain the same. + {'added': {'Grid_1': ['transformer_reinforced_1', + ..., + 'transformer_reinforced_x'], + 'Grid_10': ['transformer_reinforced_10'] + } + } """ - # todo:Typehinting - # todo:Test - # todo:Logging - # todo:If the half-lengths of the feeders is the first node that comes from the - # main station - # todo: if there are still overloaded lines in the grid, reinforce... - # todo: if the mid point is a node + def get_weight(u, v, data): + return data["length"] + + def create_bus_name(bus, voltage_level): - def create_busbar_name(lv_station_busbar_name, lv_grid_id): """ - create a LV and MV busbar name with same grid_id but added sep1 that implies - the seperation + Create an LV and MV bus-bar name with the same grid_id but added "1001" that + implies the separation Parameters ---------- - lv_station_busbar_name :eg 'BusBar_mvgd_460_lvgd_131573_LV' - lv_grid_id : eg. 131573 + bus :eg 'BusBar_mvgd_460_lvgd_131573_LV' + voltage_level : "mv" or "lv" Returns - New lv_busbar and mv_busbar name + ---------- + bus: str New bus-bar name """ - lv_station_busbar_name = lv_station_busbar_name.split("_") - grid_id_ind = lv_station_busbar_name.index(str(lv_grid_id)) + 1 - lv_station_busbar_name.insert(grid_id_ind, "sep1") - lv_busbar = lv_station_busbar_name - lv_busbar = "_".join([str(_) for _ in lv_busbar]) - mv_busbar = lv_station_busbar_name - mv_busbar[-1] = "MV" - mv_busbar = "_".join([str(_) for _ in mv_busbar]) - - return lv_busbar, mv_busbar + if bus in edisgo_obj.topology.buses_df.index: + bus = bus.split("_") + grid_id_ind = bus.index(str(grid.id)) + bus[grid_id_ind] = str(grid.id) + "1001" + if voltage_level == "lv": + bus = "_".join([str(_) for _ in bus]) + elif voltage_level == "mv": + bus[-1] = "MV" + bus = "_".join([str(_) for _ in bus]) + else: + logger.error("voltage level can only be " "mv" " or " "lv" "") + else: + raise IndexError("The bus is not in the dataframe") - def add_standard_transformer( - grid, new_station_name_lv, new_station_name_mv, lv_grid_id, edisgo_obj - ): + return bus + def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): """ + Adds standard transformer to topology. Parameters ---------- - new_station_name_lv : the lv busbar name of the created MV/LV station - eg.BusBar_mvgd_460_lvgd_131525_sep1_LV - new_station_name_mv : the mv busbar name of the created MV/LV station - eg.BusBar_mvgd_460_lvgd_131525_sep1_MV - lv_grid_id:131525 + edisgo_obj: class:`~.EDisGo` + grid: `~.network.grids.LVGrid` + bus_lv: Identifier of lv bus + bus_mv: Identifier of mv bus Returns - New tranformer dataframe - + ---------- + transformer_changes= dict """ + if bus_lv not in edisgo_obj.topology.buses_df.index: + raise ValueError( + f"Specified bus {bus_lv} is not valid as it is not defined in " + "buses_df." + ) + if bus_mv not in edisgo_obj.topology.buses_df.index: + raise ValueError( + f"Specified bus {bus_mv} is not valid as it is not defined in " + "buses_df." + ) + + try: + standard_transformer = edisgo_obj.topology.equipment_data[ + "lv_transformers" + ].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.") + + transformers_changes = {"added": {}} + + transformer_s = grid.transformers_df.iloc[0] + new_transformer_name = transformer_s.name.split("_") + grid_id_ind = new_transformer_name.index(str(grid.id)) + new_transformer_name[grid_id_ind] = str(grid.id) + "1001" + + transformer_s.s_nom = standard_transformer.S_nom + transformer_s.type_info = standard_transformer.name + transformer_s.r_pu = standard_transformer.r_pu + transformer_s.x_pu = standard_transformer.x_pu + transformer_s.name = "_".join([str(_) for _ in new_transformer_name]) + transformer_s.bus0 = bus_mv + transformer_s.bus1 = bus_lv + + new_transformer_df = transformer_s.to_frame().T - standard_transformer = edisgo_obj.topology.equipment_data[ - "lv_transformers" - ].loc[ - edisgo_obj.config["grid_expansion_standard_equipment"]["mv_lv_transformer"] - ] - new_transformer = grid.transformers_df.iloc[0] - new_transformer_name = new_transformer.name.split("_") - grid_id_ind = new_transformer_name.index(str(lv_grid_id)) + 1 - new_transformer_name.insert(grid_id_ind, "sep_1") - - new_transformer.s_nom = standard_transformer.S_nom - new_transformer.type_info = standard_transformer.name - new_transformer.r_pu = standard_transformer.r_pu - new_transformer.x_pu = standard_transformer.x_pu - new_transformer.name = "_".join([str(_) for _ in new_transformer_name]) - new_transformer.bus0 = new_station_name_mv - new_transformer.bus1 = new_station_name_lv - - new_transformer_df = new_transformer.to_frame().T - - # toDo:drop duplicates edisgo_obj.topology.transformers_df = pd.concat( [edisgo_obj.topology.transformers_df, new_transformer_df] ) - return new_transformer_df + transformers_changes["added"][ + f"LVGrid_{str(grid.id)}1001" + ] = new_transformer_df.index.tolist() + return transformers_changes - top_edisgo = edisgo_obj.topology G = grid.graph - station_node = grid.transformers_df.bus1.iat[0] - lv_grid_id = repr(grid).split("_")[1] - relevant_lines = top_edisgo.lines_df.loc[ + station_node = list(G.nodes)[0] # main station + + relevant_lines = edisgo_obj.topology.lines_df.loc[ crit_lines[crit_lines.voltage_level == "lv"].index ] - - relevant_lines = relevant_lines[relevant_lines["bus0"].str.contains(lv_grid_id)] - crit_lines_feeder = relevant_lines[relevant_lines["bus0"] == station_node] paths = {} - nodes_feeder = {} + first_nodes_feeders = {} for node in G: - path = nx.shortest_path(G, station_node, node) for first_node in crit_lines_feeder.bus1.values: if first_node in path: paths[node] = path - nodes_feeder.setdefault(path[1], []).append( - node - ) # key:first_node values:nodes in the critical feeder - - nodes_moved = [] - node_halflength = [] - - for node_list in nodes_feeder.values(): + first_nodes_feeders.setdefault(path[1], []).append( + node # first nodes and paths + ) - farthest_node = node_list[-1] + lines_changes = {} + transformers_changes = {} + nodes_tb_relocated = {} # nodes to be moved into the new grid + + # note: The number of critical lines in the Lv grid can be more than 2. However, + # if the node_1_2 of the first feeder in the for loop is not the first node of the + # feeder, it will add data frames even though the following feeders only 1 node + # (node_1_2=first node of feeder). In this type of case,the number of critical lines + # should be evaluated for the feeders whose node_1_2 s are not the first node of the + # feeder. The first check should be done on the feeders that have fewer nodes. + + first_nodes_feeders = sorted( + first_nodes_feeders.items(), key=lambda item: len(item[1]), reverse=False + ) + first_nodes_feeders = dict(first_nodes_feeders) - def get_weight(u, v, data): - return data["length"] + loop_counter = len(first_nodes_feeders) + for first_node, nodes_feeder in first_nodes_feeders.items(): + first_line = crit_lines_feeder[crit_lines_feeder.bus1 == first_node].index[ + 0 + ] # first line of the feeder + last_node = nodes_feeder[-1] # the last node of the feeder path_length_dict_tmp = dijkstra_shortest_path_length( - G, station_node, get_weight, target=farthest_node - ) - path = paths[farthest_node] + G, station_node, get_weight, target=last_node + ) # the length of each line (the shortest path) + path = paths[ + last_node + ] # path does not include the nodes branching from the node on the main path node_1_2 = next( j for j in path - if path_length_dict_tmp[j] >= path_length_dict_tmp[farthest_node] * 1 / 2 - ) - node_halflength.append(node_1_2) - - # store all the following nodes of node_1_2 that will be connected - # to the new station - - # Todo: if there is no following node of node1_2 - # find the nodes to be removed. keys: node_1_2 values: nodes to be - # moved to the new station - nodes_to_be_moved = path[path.index(node_1_2) + 1 :] - - for node in nodes_to_be_moved: - nodes_moved.append(node) - - if not crit_lines_feeder.empty: - # Create the busbar name of primary and secondary side of new MV/LV station - new_lv_busbar = create_busbar_name(station_node, lv_grid_id)[0] - new_mv_busbar = create_busbar_name(station_node, lv_grid_id)[1] - - # Create a New MV/LV station in the topology - # ADD MV and LV bus - # For the time being, the new station is located at the same - # coordinates as the existing station - - v_nom_lv = top_edisgo.buses_df[ - top_edisgo.buses_df.index.str.contains("LV") - ].v_nom[0] - v_nom_mv = top_edisgo.buses_df[ - top_edisgo.buses_df.index.str.contains("MV") - ].v_nom[0] - x_bus = top_edisgo.buses_df.loc[station_node, "x"] - y_bus = top_edisgo.buses_df.loc[station_node, "y"] - new_lv_grid_id = lv_grid_id + "_" + "sep1" - building_bus = top_edisgo.buses_df.loc[station_node, "in_building"] - - # addd lv busbar - top_edisgo.add_bus( - new_lv_busbar, - v_nom_lv, - x=x_bus, - y=y_bus, - lv_grid_id=new_lv_grid_id, - in_building=building_bus, - ) - # add mv busbar - top_edisgo.add_bus( - new_mv_busbar, v_nom_mv, x=x_bus, y=y_bus, in_building=building_bus + if path_length_dict_tmp[j] >= path_length_dict_tmp[last_node] * 1 / 2 ) + # if LVGrid: check if node_1_2 is outside a house + # and if not find next BranchTee outside the house + if isinstance(grid, LVGrid): + while ( + ~np.isnan(grid.buses_df.loc[node_1_2].in_building) + and grid.buses_df.loc[node_1_2].in_building + ): + node_1_2 = path[path.index(node_1_2) - 1] + # break if node is station + if node_1_2 is path[0]: + grid.error( + f" {first_line} and following lines could not be reinforced " + f"due to insufficient number of node in the feeder . " + ) + break + loop_counter -= 1 + # if node_1_2 is a representative (meaning it is already directly connected + # to the station), line cannot be disconnected and reinforced + if node_1_2 not in first_nodes_feeders.keys(): + nodes_tb_relocated[node_1_2] = nodes_feeder[nodes_feeder.index(node_1_2) :] + pred_node = path[path.index(node_1_2) - 1] # predecessor node of node_1_2 + line_removed = G.get_edge_data(node_1_2, pred_node)[ + "branch_name" + ] # the line + line_added_lv = line_removed + lines_changes[line_added_lv] = 1 + # removed from exiting LV grid and converted to an MV line between new + # and existing MV/LV station + if len(nodes_tb_relocated) > 2 and loop_counter == 0: + # Create the bus-bar name of primary and secondary side of new MV/LV station + lv_bus_new = create_bus_name(station_node, "lv") + mv_bus_new = create_bus_name(station_node, "mv") + + # ADD MV and LV bus + v_nom_lv = edisgo_obj.topology.buses_df.loc[ + grid.transformers_df.bus1[0], + "v_nom", + ] + v_nom_mv = edisgo_obj.topology.buses_df.loc[ + grid.transformers_df.bus0[0], + "v_nom", + ] - # ADD a LINE between existing and new station + x_bus = grid.buses_df.loc[station_node, "x"] + y_bus = grid.buses_df.loc[station_node, "y"] - # find the MV side of the station_node to connect the MV side of - # the new station to MV side of current station + # the new lv line id: e.g. 496021001 + lv_grid_id_new = int(str(grid.id) + "1001") + building_bus = grid.buses_df.loc[station_node, "in_building"] - existing_node_mv_busbar = top_edisgo.transformers_df[ - top_edisgo.transformers_df.bus1 == station_node - ].bus0[0] + # the distance between new and existing MV station in MV grid will be the + # same with the distance between pred. node of node_1_2 of one of first + # feeders to be split in LV grid - standard_line = edisgo_obj.config["grid_expansion_standard_equipment"][ - "mv_line" - ] # the new line type is standard Mv line - - # Change the coordinates based on the length - # todo:Length is specified. Random coordinates are to be found according to - # length - max_length = 0 - for node in node_halflength: - length = geo.calc_geo_dist_vincenty( - top_edisgo, station_node, node, branch_detour_factor=1.3 + length = ( + path_length_dict_tmp[node_1_2] + - path_length_dict_tmp[path[path.index(node_1_2) - 1]] ) - if length >= max_length: - max_length = length - - top_edisgo.add_line( - bus0=existing_node_mv_busbar, - bus1=new_mv_busbar, - length=max_length, - type_info=standard_line, - ) - # ADD TRANSFORMER - add_standard_transformer( - grid, new_lv_busbar, new_mv_busbar, lv_grid_id, edisgo_obj - ) + # if the transformer already added, do not add bus and transformer once more + if not transformers_changes: + # the coordinates of new MV station (x2,y2) + # the coordinates of existing LV station (x1,y1) + # y1=y2, x2=x1+length/1000 + + # add lv busbar + edisgo_obj.topology.add_bus( + lv_bus_new, + v_nom_lv, + x=x_bus + length / 1000, + y=y_bus, + lv_grid_id=lv_grid_id_new, + in_building=building_bus, + ) + # add mv busbar + edisgo_obj.topology.add_bus( + mv_bus_new, + v_nom_mv, + x=x_bus + length / 1000, + y=y_bus, + in_building=building_bus, + ) + + # ADD TRANSFORMER + transformer_changes = add_standard_transformer( + edisgo_obj, grid, lv_bus_new, mv_bus_new + ) + transformers_changes.update(transformer_changes) + + logger.debug(f"A new grid {lv_grid_id_new} added into topology") + + # ADD the MV LINE between existing and new MV station + + standard_line = edisgo_obj.config["grid_expansion_standard_equipment"][ + f"mv_line_{int(edisgo_obj.topology.mv_grid.nominal_voltage)}kv" + ] + + line_added_mv = edisgo_obj.topology.add_line( + bus0=grid.transformers_df.bus0[0], + bus1=mv_bus_new, + length=length, + type_info=standard_line, + kind="cable", + ) + lines_changes[line_added_mv] = 1 + + # changes on relocated lines to the new LV grid + # grid_ids + for node_1_2, nodes in nodes_tb_relocated.items(): + edisgo_obj.topology.buses_df.loc[ + node_1_2, "lv_grid_id" + ] = lv_grid_id_new + edisgo_obj.topology.buses_df.loc[ + nodes, "lv_grid_id" + ] = lv_grid_id_new + # line connection of node_1_2 from the predecessor node in the + # existing grid to the lv side of new station + if edisgo_obj.topology.lines_df.bus1.isin([node_1_2]).any(): + edisgo_obj.topology.lines_df.loc[ + edisgo_obj.topology.lines_df.bus1 == node_1_2, "bus0" + ] = lv_bus_new + else: + raise LookupError(f"{node_1_2} is not in the lines dataframe") + logger.debug( + f"the node {node_1_2} is split from the line and connected to " + f"{lv_grid_id_new} " + ) + + logger.info( + f"{len(nodes_tb_relocated.keys())} feeders are removed from the grid " + f"{grid} and located in new grid{repr(grid)+str(1001)} by split feeder+" + f"add transformer method" + ) - # Create new LV grid in the topology - lv_grid = LVGrid(id=new_lv_grid_id, edisgo_obj=edisgo_obj) - top_edisgo.mv_grid._lv_grids.append(lv_grid) - top_edisgo._grids[str(lv_grid)] = lv_grid - - # Change the grid ids of the nodes that are to be moved to the new LV grid - for node in nodes_moved: - top_edisgo.buses_df.loc[node, "lv_grid_id"] = new_lv_grid_id - - # todo: logger - # relocate the nodes come from the half-length point of the feeder from the - # existing grid to newly created grid - for node in nodes_moved: - if top_edisgo.lines_df.bus1.isin([node]).any(): - line_series = top_edisgo.lines_df[top_edisgo.lines_df.bus1 == node] - - if line_series.bus0[0] in node_halflength: - bus0 = line_series.bus0[0] - top_edisgo.lines_df.loc[ - top_edisgo.lines_df.bus0 == bus0, "bus0" - ] = new_lv_busbar + return transformers_changes, lines_changes def optimize_cb_location(edisgo_obj, mv_grid, mode="loadgen"): diff --git a/edisgo/tools/coordination.py b/edisgo/tools/coordination.py deleted file mode 100644 index faae88bc4..000000000 --- a/edisgo/tools/coordination.py +++ /dev/null @@ -1,719 +0,0 @@ -import copy -import logging -import math - -from time import time - -import networkx as nx -import plotly.graph_objects as go - -from dash import dcc, html -from dash.dependencies import Input, Output -from pyproj import Transformer - - -def draw_plotly( - edisgo_obj, - G, - mode_lines=False, - mode_nodes="adjecencies", - grid=False, - busmap_df=None, -): - """ - Plot the graph and shows information of the grid - - Parameters - ---------- - edisgo_obj : :class:`~edisgo.EDisGo` - EDisGo object which contains data of the grid - - G : :networkx:`Graph` - Transfer the graph of the grid to plot, the graph must contain the positions - - mode_lines : :obj:`str` - Defines the color of the lines - - * 'relative_loading' - - shows the line loading relative to the s_nom of the line - * 'loading' - - shows the loading - * 'reinforce' - - shows the reinforced lines in green - - mode_nodes : :obj:`str` - - * 'voltage_deviation' - - shows the deviation of the node voltage relative to 1 p.u. - * 'adjecencies' - - shows the the number of connections of the graph - - grid : :class:`~.network.grids.Grid` or :obj:`False` - - * :class:`~.network.grids.Grid` - - transfer the grid of the graph, to set the coordinate - origin to the first bus of the grid - * :obj:`False` - - the coordinates are not modified - - """ - - # initialization - transformer_4326_to_3035 = Transformer.from_crs( - "EPSG:4326", "EPSG:3035", always_xy=True - ) - data = [] - if not grid: - x_root = 0 - y_root = 0 - elif grid is None: - node_root = edisgo_obj.topology.transformers_hvmv_df.bus1[0] - x_root, y_root = G.nodes[node_root]["pos"] - else: - node_root = grid.transformers_df.bus1[0] - x_root, y_root = G.nodes[node_root]["pos"] - - x_root, y_root = transformer_4326_to_3035.transform(x_root, y_root) - - # line text - middle_node_x = [] - middle_node_y = [] - middle_node_text = [] - for edge in G.edges(data=True): - x0, y0 = G.nodes[edge[0]]["pos"] - x1, y1 = G.nodes[edge[1]]["pos"] - x0, y0 = transformer_4326_to_3035.transform(x0, y0) - x1, y1 = transformer_4326_to_3035.transform(x1, y1) - middle_node_x.append((x0 - x_root + x1 - x_root) / 2) - middle_node_y.append((y0 - y_root + y1 - y_root) / 2) - - text = str(edge[2]["branch_name"]) - try: - loading = edisgo_obj.results.s_res.T.loc[ - edge[2]["branch_name"] - ].max() # * 1000 - text = text + "
" + "Loading = " + str(loading) - except KeyError: - text = text - - try: - text = text + "
" + "GRAPH_LOAD = " + str(edge[2]["load"]) - except KeyError: - text = text - - try: - line_parameters = edisgo_obj.topology.lines_df.loc[ - edge[2]["branch_name"], : - ] - for index, value in line_parameters.iteritems(): - text = text + "
" + str(index) + " = " + str(value) - except KeyError: - text = text - - try: - r = edisgo_obj.topology.lines_df.r.loc[edge[2]["branch_name"]] - x = edisgo_obj.topology.lines_df.x.loc[edge[2]["branch_name"]] - s_nom = edisgo_obj.topology.lines_df.s_nom.loc[edge[2]["branch_name"]] - length = edisgo_obj.topology.lines_df.length.loc[edge[2]["branch_name"]] - bus_0 = edisgo_obj.topology.lines_df.bus0.loc[edge[2]["branch_name"]] - v_nom = edisgo_obj.topology.buses_df.loc[bus_0, "v_nom"] - import math - - text = text + "
" + "r/length = " + str(r / length) - text = ( - text - + "
" - + "x/length = " - + str(x / length / 2 / math.pi / 50 * 1000) - ) - text = text + "
" + "i_max_th = " + str(s_nom / math.sqrt(3) / v_nom) - except KeyError: - text = text - - middle_node_text.append(text) - - middle_node_trace = go.Scatter( - x=middle_node_x, - y=middle_node_y, - text=middle_node_text, - mode="markers", - hoverinfo="text", - marker=dict(opacity=0.0, size=10, color="white"), - ) - data.append(middle_node_trace) - - # line plot - import matplotlib as matplotlib - import matplotlib.cm as cm - - if mode_lines == "loading": - s_res_view = edisgo_obj.results.s_res.T.index.isin( - [edge[2]["branch_name"] for edge in G.edges.data()] - ) - color_min = edisgo_obj.results.s_res.T.loc[s_res_view].T.min().max() - color_max = edisgo_obj.results.s_res.T.loc[s_res_view].T.max().max() - elif mode_lines == "relative_loading": - color_min = 0 - color_max = 1 - - if (mode_lines != "reinforce") and not mode_lines: - - def color_map_color( - value, cmap_name="coolwarm", vmin=color_min, vmax=color_max - ): - norm = matplotlib.colors.Normalize(vmin=vmin, vmax=vmax) - cmap = cm.get_cmap(cmap_name) - rgb = cmap(norm(abs(value)))[:3] - color = matplotlib.colors.rgb2hex(rgb) - return color - - for edge in G.edges(data=True): - edge_x = [] - edge_y = [] - - x0, y0 = G.nodes[edge[0]]["pos"] - x1, y1 = G.nodes[edge[1]]["pos"] - x0, y0 = transformer_4326_to_3035.transform(x0, y0) - x1, y1 = transformer_4326_to_3035.transform(x1, y1) - edge_x.append(x0 - x_root) - edge_x.append(x1 - x_root) - edge_x.append(None) - edge_y.append(y0 - y_root) - edge_y.append(y1 - y_root) - edge_y.append(None) - - if mode_lines == "reinforce": - if edisgo_obj.results.grid_expansion_costs.index.isin( - [edge[2]["branch_name"]] - ).any(): - color = "lightgreen" - else: - color = "black" - elif mode_lines == "loading": - loading = edisgo_obj.results.s_res.T.loc[edge[2]["branch_name"]].max() - color = color_map_color(loading) - elif mode_lines == "relative_loading": - loading = edisgo_obj.results.s_res.T.loc[edge[2]["branch_name"]].max() - s_nom = edisgo_obj.topology.lines_df.s_nom.loc[edge[2]["branch_name"]] - color = color_map_color(loading / s_nom) - if loading > s_nom: - color = "green" - else: - color = "black" - - edge_trace = go.Scatter( - x=edge_x, - y=edge_y, - hoverinfo="none", - opacity=0.4, - mode="lines", - line=dict(width=2, color=color), - ) - data.append(edge_trace) - - # node plot - node_x = [] - node_y = [] - - for node in G.nodes(): - x, y = G.nodes[node]["pos"] - x, y = transformer_4326_to_3035.transform(x, y) - node_x.append(x - x_root) - node_y.append(y - y_root) - - colors = [] - if mode_nodes == "adjecencies": - for node, adjacencies in enumerate(G.adjacency()): - colors.append(len(adjacencies[1])) - colorscale = "YlGnBu" - cmid = None - colorbar = dict( - thickness=15, title="Node Connections", xanchor="left", titleside="right" - ) - elif mode_nodes == "voltage_deviation": - for node in G.nodes(): - v_min = edisgo_obj.results.v_res.T.loc[node].min() - v_max = edisgo_obj.results.v_res.T.loc[node].max() - if abs(v_min - 1) > abs(v_max - 1): - color = v_min - 1 - else: - color = v_max - 1 - colors.append(color) - colorscale = "RdBu" - cmid = 0 - colorbar = dict( - thickness=15, - title="Node Voltage Deviation", - xanchor="left", - titleside="right", - ) - - node_text = [] - for node in G.nodes(): - text = str(node) - try: - peak_load = edisgo_obj.topology.loads_df.loc[ - edisgo_obj.topology.loads_df.bus == node - ].peak_load.sum() - text = text + "
" + "peak_load = " + str(peak_load) - p_nom = edisgo_obj.topology.generators_df.loc[ - edisgo_obj.topology.generators_df.bus == node - ].p_nom.sum() - text = text + "
" + "p_nom_gen = " + str(p_nom) - p_charge = edisgo_obj.topology.charging_points_df.loc[ - edisgo_obj.topology.charging_points_df.bus == node - ].p_nom.sum() - text = text + "
" + "p_nom_charge = " + str(p_charge) - except ValueError: - text = text - - try: - s_tran_1 = edisgo_obj.topology.transformers_df.loc[ - edisgo_obj.topology.transformers_df.bus0 == node, "s_nom" - ].sum() - s_tran_2 = edisgo_obj.topology.transformers_df.loc[ - edisgo_obj.topology.transformers_df.bus1 == node, "s_nom" - ].sum() - s_tran = s_tran_1 + s_tran_2 - text = text + "
" + "s_transformer = {:.2f}kVA".format(s_tran * 1000) - except KeyError: - text = text - - try: - v_min = edisgo_obj.results.v_res.T.loc[node].min() - v_max = edisgo_obj.results.v_res.T.loc[node].max() - if abs(v_min - 1) > abs(v_max - 1): - text = text + "
" + "v = " + str(v_min) - else: - text = text + "
" + "v = " + str(v_max) - except KeyError: - text = text - - try: - text = text + "
" + "Neighbors = " + str(G.degree(node)) - except KeyError: - text = text - - try: - node_parameters = edisgo_obj.topology.buses_df.loc[node] - for index, value in node_parameters.iteritems(): - text = text + "
" + str(index) + " = " + str(value) - except KeyError: - text = text - - if busmap_df is not None: - text = text + "
" + "new_bus_name = " + busmap_df.loc[node, "new_bus"] - - node_text.append(text) - - node_trace = go.Scatter( - x=node_x, - y=node_y, - mode="markers", - hoverinfo="text", - text=node_text, - marker=dict( - showscale=True, - colorscale=colorscale, - reversescale=True, - color=colors, - size=8, - cmid=cmid, - line_width=2, - colorbar=colorbar, - ), - ) - - data.append(node_trace) - - fig = go.Figure( - data=data, - layout=go.Layout( - height=500, - titlefont_size=16, - showlegend=False, - hovermode="closest", - margin=dict(b=20, l=5, r=5, t=40), - xaxis=dict(showgrid=True, zeroline=True, showticklabels=True), - yaxis=dict(showgrid=True, zeroline=True, showticklabels=True), - ), - ) - - fig.update_yaxes(scaleanchor="x", scaleratio=1) - # fig.update_yaxes(tick0=0, dtick=1000) - # fig.update_xaxes(tick0=0, dtick=1000) - return fig - - -def dash_plot(**kwargs): - """ - Uses the :func:`draw_plotly` for interactive plotting. - - Shows different behavior for different number of parameters. - One edisgo object creates one large plot. - Two or more edisgo objects create two adjacent plots, - the objects to be plotted are selected in the dropdown menu. - - **Example run:** - - | app = dash_plot(edisgo_obj_1=edisgo_obj_1,edisgo_obj_2=edisgo_obj_2,...) - | app.run_server(mode="inline",debug=True) - - """ - - from jupyter_dash import JupyterDash - - def chosen_graph(edisgo_obj, selected_grid): - lv_grids = list(edisgo_obj.topology.mv_grid.lv_grids) - lv_grid_name_list = list(map(str, lv_grids)) - # selected_grid = "LVGrid_452669" - # selected_grid = lv_grid_name_list[0] - try: - lv_grid_id = lv_grid_name_list.index(selected_grid) - except ValueError: - lv_grid_id = False - - mv_grid = edisgo_obj.topology.mv_grid - lv_grid = lv_grids[lv_grid_id] - - if selected_grid == "Grid": - G = edisgo_obj.to_graph() - grid = None - elif selected_grid == str(mv_grid): - G = mv_grid.graph - grid = mv_grid - elif selected_grid.split("_")[0] == "LVGrid": - G = lv_grid.graph - grid = lv_grid - else: - raise ValueError("False Grid") - - return G, grid - - edisgo_obj = list(kwargs.values())[0] - mv_grid = edisgo_obj.topology.mv_grid - lv_grids = list(edisgo_obj.topology.mv_grid.lv_grids) - - edisgo_name_list = list(kwargs.keys()) - - lv_grid_name_list = list(map(str, lv_grids)) - - grid_name_list = ["Grid", str(mv_grid)] + lv_grid_name_list - - line_plot_modes = ["reinforce", "loading", "relative_loading"] - node_plot_modes = ["adjecencies", "voltage_deviation"] - - app = JupyterDash(__name__) - if len(kwargs) > 1: - app.layout = html.Div( - [ - html.Div( - [ - dcc.Dropdown( - id="dropdown_edisgo_object_1", - options=[ - {"label": i, "value": i} for i in edisgo_name_list - ], - value=edisgo_name_list[0], - ), - dcc.Dropdown( - id="dropdown_edisgo_object_2", - options=[ - {"label": i, "value": i} for i in edisgo_name_list - ], - value=edisgo_name_list[1], - ), - dcc.Dropdown( - id="dropdown_grid", - options=[{"label": i, "value": i} for i in grid_name_list], - value=grid_name_list[1], - ), - dcc.Dropdown( - id="dropdown_line_plot_mode", - options=[{"label": i, "value": i} for i in line_plot_modes], - value=line_plot_modes[0], - ), - dcc.Dropdown( - id="dropdown_node_plot_mode", - options=[{"label": i, "value": i} for i in node_plot_modes], - value=node_plot_modes[0], - ), - ] - ), - html.Div( - [ - html.Div([dcc.Graph(id="fig_1")], style={"flex": "auto"}), - html.Div([dcc.Graph(id="fig_2")], style={"flex": "auto"}), - ], - style={"display": "flex", "flex-direction": "row"}, - ), - ], - style={"display": "flex", "flex-direction": "column"}, - ) - - @app.callback( - Output("fig_1", "figure"), - Output("fig_2", "figure"), - Input("dropdown_grid", "value"), - Input("dropdown_edisgo_object_1", "value"), - Input("dropdown_edisgo_object_2", "value"), - Input("dropdown_line_plot_mode", "value"), - Input("dropdown_node_plot_mode", "value"), - ) - def update_figure( - selected_grid, - selected_edisgo_object_1, - selected_edisgo_object_2, - selected_line_plot_mode, - selected_node_plot_mode, - ): - - edisgo_obj = kwargs[selected_edisgo_object_1] - (G, grid) = chosen_graph(edisgo_obj, selected_grid) - fig_1 = draw_plotly( - edisgo_obj, - G, - selected_line_plot_mode, - selected_node_plot_mode, - grid=grid, - ) - - edisgo_obj = kwargs[selected_edisgo_object_2] - (G, grid) = chosen_graph(edisgo_obj, selected_grid) - fig_2 = draw_plotly( - edisgo_obj, - G, - selected_line_plot_mode, - selected_node_plot_mode, - grid=grid, - ) - - return fig_1, fig_2 - - else: - app.layout = html.Div( - [ - html.Div( - [ - dcc.Dropdown( - id="dropdown_grid", - options=[{"label": i, "value": i} for i in grid_name_list], - value=grid_name_list[1], - ), - dcc.Dropdown( - id="dropdown_line_plot_mode", - options=[{"label": i, "value": i} for i in line_plot_modes], - value=line_plot_modes[0], - ), - dcc.Dropdown( - id="dropdown_node_plot_mode", - options=[{"label": i, "value": i} for i in node_plot_modes], - value=node_plot_modes[0], - ), - ] - ), - html.Div( - [html.Div([dcc.Graph(id="fig")], style={"flex": "auto"})], - style={"display": "flex", "flex-direction": "row"}, - ), - ], - style={"display": "flex", "flex-direction": "column"}, - ) - - @app.callback( - Output("fig", "figure"), - Input("dropdown_grid", "value"), - Input("dropdown_line_plot_mode", "value"), - Input("dropdown_node_plot_mode", "value"), - ) - def update_figure( - selected_grid, selected_line_plot_mode, selected_node_plot_mode - ): - - edisgo_obj = list(kwargs.values())[0] - (G, grid) = chosen_graph(edisgo_obj, selected_grid) - fig = draw_plotly( - edisgo_obj, - G, - selected_line_plot_mode, - selected_node_plot_mode, - grid=grid, - ) - return fig - - return app - - -# Functions for other functions -coor_transform = Transformer.from_crs("EPSG:4326", "EPSG:3035", always_xy=True) -coor_transform_back = Transformer.from_crs("EPSG:3035", "EPSG:4326", always_xy=True) - - -# Pseudo coordinates -def make_pseudo_coordinates(edisgo_root): - def make_coordinates(graph_root): - def coordinate_source(pos_start, length, node_numerator, node_total_numerator): - length = length / 1.3 - angle = node_numerator * 360 / node_total_numerator - x0, y0 = pos_start - x1 = x0 + 1000 * length * math.cos(math.radians(angle)) - y1 = y0 + 1000 * length * math.sin(math.radians(angle)) - pos_end = (x1, y1) - origin_angle = math.degrees(math.atan2(y1 - y0, x1 - x0)) - return pos_end, origin_angle - - def coordinate_branch( - pos_start, angle_offset, length, node_numerator, node_total_numerator - ): - length = length / 1.3 - angle = ( - node_numerator * 180 / (node_total_numerator + 1) + angle_offset - 90 - ) - x0, y0 = pos_start - x1 = x0 + 1000 * length * math.cos(math.radians(angle)) - y1 = y0 + 1000 * length * math.sin(math.radians(angle)) - origin_angle = math.degrees(math.atan2(y1 - y0, x1 - x0)) - pos_end = (x1, y1) - return pos_end, origin_angle - - def coordinate_longest_path(pos_start, angle_offset, length): - length = length / 1.3 - angle = angle_offset - x0, y0 = pos_start - x1 = x0 + 1000 * length * math.cos(math.radians(angle)) - y1 = y0 + 1000 * length * math.sin(math.radians(angle)) - origin_angle = math.degrees(math.atan2(y1 - y0, x1 - x0)) - pos_end = (x1, y1) - return pos_end, origin_angle - - def coordinate_longest_path_neighbor( - pos_start, angle_offset, length, direction - ): - length = length / 1.3 - if direction: - angle_random_offset = 90 - else: - angle_random_offset = -90 - angle = angle_offset + angle_random_offset - x0, y0 = pos_start - x1 = x0 + 1000 * length * math.cos(math.radians(angle)) - y1 = y0 + 1000 * length * math.sin(math.radians(angle)) - origin_angle = math.degrees(math.atan2(y1 - y0, x1 - x0)) - pos_end = (x1, y1) - - return pos_end, origin_angle - - start_node = list(nx.nodes(graph_root))[0] - graph_root.nodes[start_node]["pos"] = (0, 0) - graph_copy = graph_root.copy() - - long_paths = [] - next_nodes = [] - - for i in range(1, 30): - path_length_to_transformer = [] - for node in graph_copy.nodes(): - try: - paths = list(nx.shortest_simple_paths(graph_copy, start_node, node)) - except ValueError: - paths = [[]] - path_length_to_transformer.append(len(paths[0])) - index = path_length_to_transformer.index(max(path_length_to_transformer)) - path_to_max_distance_node = list( - nx.shortest_simple_paths( - graph_copy, start_node, list(nx.nodes(graph_copy))[index] - ) - )[0] - path_to_max_distance_node.remove(start_node) - graph_copy.remove_nodes_from(path_to_max_distance_node) - for node in path_to_max_distance_node: - long_paths.append(node) - - path_to_max_distance_node = long_paths - n = 0 - - for node in list(nx.neighbors(graph_root, start_node)): - n = n + 1 - pos, origin_angle = coordinate_source( - graph_root.nodes[start_node]["pos"], - graph_root.edges[start_node, node]["length"], - n, - len(list(nx.neighbors(graph_root, start_node))), - ) - graph_root.nodes[node]["pos"] = pos - graph_root.nodes[node]["origin_angle"] = origin_angle - next_nodes.append(node) - - graph_copy = graph_root.copy() - graph_copy.remove_node(start_node) - while graph_copy.number_of_nodes() > 0: - next_node = next_nodes[0] - n = 0 - for node in list(nx.neighbors(graph_copy, next_node)): - n = n + 1 - if node in path_to_max_distance_node: - pos, origin_angle = coordinate_longest_path( - graph_root.nodes[next_node]["pos"], - graph_root.nodes[next_node]["origin_angle"], - graph_root.edges[next_node, node]["length"], - ) - elif next_node in path_to_max_distance_node: - direction = math.fmod( - len( - list( - nx.shortest_simple_paths( - graph_root, start_node, next_node - ) - )[0] - ), - 2, - ) - pos, origin_angle = coordinate_longest_path_neighbor( - graph_root.nodes[next_node]["pos"], - graph_root.nodes[next_node]["origin_angle"], - graph_root.edges[next_node, node]["length"], - direction, - ) - else: - pos, origin_angle = coordinate_branch( - graph_root.nodes[next_node]["pos"], - graph_root.nodes[next_node]["origin_angle"], - graph_root.edges[next_node, node]["length"], - n, - len(list(nx.neighbors(graph_copy, next_node))), - ) - - graph_root.nodes[node]["pos"] = pos - graph_root.nodes[node]["origin_angle"] = origin_angle - next_nodes.append(node) - - graph_copy.remove_node(next_node) - next_nodes.remove(next_node) - - return graph_root - - logger = logging.getLogger("edisgo.cr_make_pseudo_coor") - start_time = time() - logger.info( - "Start - Making pseudo coordinates for grid: {}".format( - str(edisgo_root.topology.mv_grid) - ) - ) - - edisgo_obj = copy.deepcopy(edisgo_root) - lv_grids = list(edisgo_obj.topology.mv_grid.lv_grids) - - for lv_grid in lv_grids: - logger.debug("Make pseudo coordinates for: {}".format(lv_grid)) - G = lv_grid.graph - x0, y0 = G.nodes[list(nx.nodes(G))[0]]["pos"] - G = make_coordinates(G) - x0, y0 = coor_transform.transform(x0, y0) - for node in G.nodes(): - x, y = G.nodes[node]["pos"] - x, y = coor_transform_back.transform(x + x0, y + y0) - edisgo_obj.topology.buses_df.loc[node, "x"] = x - edisgo_obj.topology.buses_df.loc[node, "y"] = y - - logger.info("Finished in {}s".format(time() - start_time)) - return edisgo_obj From d811c071977defda48c1e07556463466735e6602 Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Mon, 28 Nov 2022 22:24:21 +0100 Subject: [PATCH 16/43] add split+station method to reinforce_grid_alternative --- edisgo/flex_opt/reinforce_grid_alternative.py | 65 ++++++++++++++++--- edisgo/flex_opt/reinforce_measures.py | 2 + 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/edisgo/flex_opt/reinforce_grid_alternative.py b/edisgo/flex_opt/reinforce_grid_alternative.py index 628d7ad15..3de5bc334 100644 --- a/edisgo/flex_opt/reinforce_grid_alternative.py +++ b/edisgo/flex_opt/reinforce_grid_alternative.py @@ -26,7 +26,19 @@ def reinforce_line_overloading_alternative( Evaluates network reinforcement needs and performs measures. This function is the parent function for all network reinforcements. + MV Grid Reinforcement: + 1- + 2- + LV Grid Reinforcement: + 1- Split+add station method is implemented into all the lv grids if there are more + than 3 overloaded lines in the grid. + 2- Split method is implemented into the grids which are not reinforced by split+add + station method + + MV_LV Grid Reinforcement + 1- The remaining overloaded lines are reinforced by add same type of parallel line + method Parameters ---------- edisgo: class:`~.EDisGo` @@ -86,6 +98,7 @@ def reinforce_line_overloading_alternative( 2-One type of line cost is used for mv and lv 3-Line Reinforcements are done with the same type of lines as lines reinforced + """ def _add_lines_changes_to_equipment_changes(): @@ -106,9 +119,26 @@ def _add_lines_changes_to_equipment_changes(): ], ) + def _add_transformer_changes_to_equipment_changes(mode: str | None): + df_list = [edisgo_reinforce.results.equipment_changes] + df_list.extend( + pd.DataFrame( + { + "iteration_step": [iteration_step] * len(transformer_list), + "change": [mode] * len(transformer_list), + "equipment": transformer_list, + "quantity": [1] * len(transformer_list), + }, + index=[station] * len(transformer_list), + ) + for station, transformer_list in transformer_changes[mode].items() + ) + + edisgo_reinforce.results.equipment_changes = pd.concat(df_list) + # check if provided mode is valid if mode and mode not in ["mv", "lv"]: - raise ValueError(f"Provided mode {mode} is not a valid mode.") + raise ValueError(f"Provided mode {mode} is not valid.") # in case reinforcement needs to be conducted on a copied graph the # edisgo object is deep copied if copy_grid is True: @@ -149,9 +179,9 @@ def _add_lines_changes_to_equipment_changes(): crit_lines_mv = checks.mv_line_load(edisgo_reinforce) crit_lines_lv = checks.lv_line_load(edisgo_reinforce) - # 1.1 Method: Split the feeder at the half-length of feeder (applied only once to + # 1.1 Voltage level= MV + # 1.1.1 Method:Split the feeder at the half-length of feeder (applied only once to # secure n-1). - # 1.1.1- MV grid if (not mode or mode == "mv") and not crit_lines_mv.empty: # TODO: add method resiting of CB logger.debug( @@ -162,23 +192,38 @@ def _add_lines_changes_to_equipment_changes(): edisgo_reinforce, edisgo_reinforce.topology.mv_grid, crit_lines_mv ) _add_lines_changes_to_equipment_changes() - # if not lines_changes: logger.debug("==> Run power flow analysis.") edisgo_reinforce.analyze(mode=analyze_mode, timesteps=timesteps_pfa) - # 1.1.2- LV grid + # 1.2- Voltage level= LV if (not mode or mode == "lv") and not crit_lines_lv.empty: - # TODO: add method split the feeder + add substation for lv_grid in list(edisgo_reinforce.topology.mv_grid.lv_grids): - + # 1.2.1 Method: Split the feeder at the half-length of feeder and add + # new station( applied only once ) + # if the number of overloaded lines is more than 2 logger.debug( - f"==>feeder splitting method is running for LV grid {lv_grid}: " + f"==>split+add substation method is running for LV grid {lv_grid}: " ) - lines_changes = reinforce_measures.split_feeder_at_half_length( + ( + transformer_changes, + lines_changes, + ) = reinforce_measures.add_station_at_half_length( edisgo_reinforce, lv_grid, crit_lines_lv ) - _add_lines_changes_to_equipment_changes() + if transformer_changes and lines_changes: + _add_transformer_changes_to_equipment_changes("added") + _add_lines_changes_to_equipment_changes() + else: + # 1.2.2 Method:Split the feeder at the half-length of feeder (applied + # only once) + logger.debug( + f"==>feeder splitting method is running for LV grid {lv_grid}: " + ) + lines_changes = reinforce_measures.split_feeder_at_half_length( + edisgo_reinforce, lv_grid, crit_lines_lv + ) + _add_lines_changes_to_equipment_changes() logger.debug("==> Run power flow analysis.") edisgo_reinforce.analyze(mode=analyze_mode, timesteps=timesteps_pfa) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index 660051e84..5dbc3ec19 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -1354,6 +1354,8 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): f"{grid} and located in new grid{repr(grid)+str(1001)} by split feeder+" f"add transformer method" ) + if len(lines_changes) < 3: + lines_changes = {} return transformers_changes, lines_changes From 576f38e8903518cb462e034d364a8273311614c3 Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Sun, 18 Dec 2022 23:34:53 +0100 Subject: [PATCH 17/43] Debugging in the method "relocate circuit breaker" --- edisgo/flex_opt/reinforce_measures.py | 455 ++++++++++++++------------ 1 file changed, 248 insertions(+), 207 deletions(-) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index 5dbc3ec19..e4b888a5e 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -1351,8 +1351,8 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): logger.info( f"{len(nodes_tb_relocated.keys())} feeders are removed from the grid " - f"{grid} and located in new grid{repr(grid)+str(1001)} by split feeder+" - f"add transformer method" + f"{grid} and located in new grid{repr(grid) + str(1001)} by split " + f"feeder+add transformer method" ) if len(lines_changes) < 3: lines_changes = {} @@ -1360,7 +1360,7 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): return transformers_changes, lines_changes -def optimize_cb_location(edisgo_obj, mv_grid, mode="loadgen"): +def relocate_circuit_breaker(edisgo_obj, mode="loadgen"): """ Locates the circuit breakers at the optimal position in the rings to reduce the difference in loading of feeders @@ -1379,18 +1379,18 @@ def optimize_cb_location(edisgo_obj, mv_grid, mode="loadgen"): Default: 'loadgen'. - Notes:According to planning principles of MV grids, a MV ring is run as two strings - (half-rings) separated by a circuit breaker which is open at normal operation. - Assuming a ring (route which is connected to the root node at either sides), - the optimal position of a circuit breaker is defined as the position + Notes: According to planning principles of MV grids, an MV ring is run as two + strings (half-rings) separated by a circuit breaker which is open at normal + operation. Assuming a ring (a route which is connected to the root node on either + side),the optimal position of a circuit breaker is defined as the position (virtual cable) between two nodes where the conveyed current is minimal on the - route.Instead of the peak current,the peak load is used here (assuming a constant + route. Instead of the peak current, the peak load is used here (assuming a constant voltage. - The circuit breaker will be installed to a node in the main route of the ring + The circuit breaker will be installed on a node in the main route of the ring. If a ring is dominated by loads (peak load > peak capacity of generators), - only loads are used for determining the location of circuit breaker. + only loads are used for determining the location of the circuit breaker. If generators are prevailing (peak load < peak capacity of generators), only generator capacities are considered for relocation. @@ -1400,39 +1400,8 @@ def optimize_cb_location(edisgo_obj, mv_grid, mode="loadgen"): the node where the cb is located """ - logging.basicConfig(format=10) - # power factor of loads and generators - cos_phi_load = edisgo_obj.config["reactive_power_factor"]["mv_load"] - cos_phi_feedin = edisgo_obj.config["reactive_power_factor"]["mv_gen"] - - buses_df = edisgo_obj.topology.buses_df - lines_df = edisgo_obj.topology.lines_df - loads_df = edisgo_obj.topology.loads_df - generators_df = edisgo_obj.topology.generators_df - switches_df = edisgo_obj.topology.switches_df - transformers_df = edisgo_obj.topology.transformers_df - - station = mv_grid.station.index[0] - graph = mv_grid.graph - - def id_mv_node(mv_node): - """ - Returns id of mv node - Parameters - ---------- - mv_node:'str' - name of node. E.g. 'BusBar_mvgd_2534_lvgd_450268_MV' - - Returns - ------- - obj:`str` - the id of the node. E.g '450268' - """ - lv_bus_tranformer = transformers_df[transformers_df.bus0 == mv_node].bus1[0] - lv_id = buses_df[buses_df.index == lv_bus_tranformer].lv_grid_id[0] - return int(lv_id) - def _sort_rings(remove_mv_station=True): + def _sort_nodes(remove_mv_station=True): """ Sorts the nodes beginning from HV/MV station in the ring. @@ -1440,7 +1409,7 @@ def _sort_rings(remove_mv_station=True): ---------- remove_mv_station : obj:`boolean` - If True reinforcement HV/MV station is not included + If True, reinforcement HV/MV station is not included Default: True. Returns @@ -1461,9 +1430,8 @@ def _sort_rings(remove_mv_station=True): graph = edisgo_obj.topology.to_graph() rings = nx.cycle_basis(graph, root=station) if remove_mv_station: - - for r in rings: - r.remove(station) + for ring in rings: + ring.remove(station) # reopen switches for switch in switches: @@ -1471,11 +1439,11 @@ def _sort_rings(remove_mv_station=True): switch.open() return rings - def get_subtree_of_nodes(ring, graph): + def _get_subtree_of_nodes(ring, graph): """ - Finds all nodes of a tree that is connected to main nodes in the ring and are - (except main nodes) not part of the ring of main nodes (traversal of graph - from main nodes excluding nodes along ring). + Finds all nodes of a subtree connected to main nodes in the ring + (except main nodes) + Parameters ---------- edisgo_obj: @@ -1490,78 +1458,37 @@ def get_subtree_of_nodes(ring, graph): ------- obj:'dict` index:main node - columns: nodes of main node's tree + columns: nodes of subtree """ - node_ring_d = {} + subtree_dict = {} for node in ring: + # exclude main node + if node != station: + nodes_subtree = set() + for path in nx.shortest_path(graph, node).values(): + if len(path) > 1: + # Virtul_Busbars should not be included as it has the same + # characteristics as its main node. e.g. virtual_BusBar_ + # mvgd_1056_lvgd_97722_MV =BusBar_mvgd_1056_lvgd_97722_MV + if ( + (path[1] not in ring) + and (path[1] != station) + and ("virtual" not in path[1]) + ): + nodes_subtree.update(path[1 : len(path)]) + + if len(nodes_subtree) == 0: + subtree_dict.setdefault(node, []).append(None) + else: + for node_subtree in nodes_subtree: + subtree_dict.setdefault(node, []).append(node_subtree) - if node == station: - continue - - nodes_subtree = set() - for path in nx.shortest_path(graph, node).values(): - if len(path) > 1: - if (path[1] not in ring) and (path[1] != station): - nodes_subtree.update(path[1 : len(path)]) - - if len(nodes_subtree) == 0: - node_ring_d.setdefault(node, []).append(None) - else: - for node_subtree in nodes_subtree: - node_ring_d.setdefault(node, []).append(node_subtree) - - return node_ring_d - - def _calculate_peak_load_gen(bus_node): - """ - Cumulative peak load/generation of loads/generators connected to underlying - MV or LV grid - Parameters - ---------- - bus_node: - obj: bus_name of the node. + return subtree_dict - Returns - ------- - obj:'list' - list of total generation and load of MV node + def _get_circuit_breaker_df(ring): """ - if ( - bus_node - in buses_df[ - buses_df.index.str.contains("BusBar") - & (~buses_df.index.str.contains("virtual")) - & (buses_df.v_nom >= 10) - ].index.values - ): - id_node = id_mv_node(bus_node) - p_load = ( - loads_df[loads_df.index.str.contains(str(id_node))].p_set.sum() - / cos_phi_load - ) - p_gen = ( - generators_df[ - generators_df.index.str.contains(str(id_node)) - ].p_nom.sum() - / cos_phi_feedin - ) - - elif bus_node in buses_df[buses_df.index.str.contains("gen")].index.values: - p_gen = ( - generators_df[generators_df.bus == bus_node].p_nom.sum() - / cos_phi_feedin - ) - p_load = loads_df[loads_df.bus == bus_node].p_set.sum() / cos_phi_feedin - - else: - p_gen = 0 - p_load = 0 + Returns the circuit breaker df of the related ring - return [p_gen, p_load] - - def _circuit_breaker(ring): - """ - finds the circuit of the related ring Parameters ---------- ring: @@ -1569,138 +1496,252 @@ def _circuit_breaker(ring): Dictionary with name of sorted nodes in the ring Returns ------- - obj: str - the name of circuit breaker + obj: dict + circuit breaker df """ - circuit_breaker = [] for node in ring: + for cb in edisgo_obj.topology.switches_df.bus_closed.values: + if cb in node: + circuit_breaker_df = edisgo_obj.topology.switches_df[ + edisgo_obj.topology.switches_df.bus_closed == cb + ] - for switch in switches_df.bus_closed.values: - if switch in node: - circuit_b = switches_df.loc[ - switches_df.bus_closed == node, "bus_closed" - ].index[0] - circuit_breaker.append(circuit_b) - else: - continue - return circuit_breaker[0] + return circuit_breaker_df - def _change_dataframe(node_cb, ring): + def _change_dataframe(cb_new_closed, cb_old_df): - circuit_breaker = _circuit_breaker(ring) - - if node_cb != switches_df.loc[circuit_breaker, "bus_closed"]: - - node_existing = switches_df.loc[circuit_breaker, "bus_closed"] - new_virtual_bus = f"virtual_{node_cb}" + # if the new cb location is not same as before + if ( + cb_new_closed + != edisgo_obj.topology.switches_df.loc[cb_old_df.index[0], "bus_closed"] + ): + # closed: the closed side of cb e.g. BusBar_mvgd_1056_lvgd_97722_MV + # open: the open side of cb e.g. virtual_BusBar_mvgd_1056_lvgd_97722_MV + cb_old_closed = cb_old_df.bus_closed[0] + cb_old_open = f"virtual_{cb_old_closed}" + # open side of new cb + cb_new_open = f"virtual_{cb_new_closed}" + + # create the branch # if the adjacent node is previous circuit breaker - if f"virtual_{node2}" in mv_grid.graph.adj[node_cb]: - branch = mv_grid.graph.adj[node_cb][f"virtual_{node2}"]["branch_name"] + if f"virtual_{node2}" in G.adj[cb_new_closed]: + branch = G.adj[cb_new_closed][f"virtual_{node2}"]["branch_name"] else: - branch = mv_grid.graph.adj[node_cb][node2]["branch_name"] - # Switch + branch = G.adj[cb_new_closed][node2]["branch_name"] + + # Update switches_df # change bus0 - switches_df.loc[circuit_breaker, "bus_closed"] = node_cb + edisgo_obj.topology.switches_df.loc[ + cb_old_df.index[0], "bus_closed" + ] = cb_new_closed # change bus1 - switches_df.loc[circuit_breaker, "bus_open"] = new_virtual_bus + edisgo_obj.topology.switches_df.loc[ + cb_old_df.index[0], "bus_open" + ] = cb_new_open # change branch - switches_df.loc[circuit_breaker, "branch"] = branch + edisgo_obj.topology.switches_df.loc[cb_old_df.index[0], "branch"] = branch - # Bus - x_coord = buses_df.loc[node_cb, "x"] - y_coord = buses_df.loc[node_cb, "y"] - buses_df.rename(index={node_existing: new_virtual_bus}, inplace=True) - buses_df.loc[new_virtual_bus, "x"] = x_coord - buses_df.loc[new_virtual_bus, "y"] = y_coord + # Update Buses_df + x_coord = grid.buses_df.loc[cb_new_closed, "x"] + y_coord = grid.buses_df.loc[cb_new_closed, "y"] + edisgo_obj.topology.buses_df.rename( + index={cb_old_closed: cb_new_open}, inplace=True + ) + edisgo_obj.topology.buses_df.loc[cb_new_open, "x"] = x_coord + edisgo_obj.topology.buses_df.loc[cb_new_open, "y"] = y_coord + edisgo_obj.topology.buses_df.rename( + index={cb_old_open: cb_old_closed}, inplace=True + ) - buses_df.rename( - index={f"virtual_{node_existing}": node_existing}, inplace=True + # Update lines_df + # convert old virtual busbar to real busbars + if not edisgo_obj.topology.lines_df.loc[ + edisgo_obj.topology.lines_df.bus0 == cb_old_open, "bus0" + ].empty: + edisgo_obj.topology.lines_df.loc[ + edisgo_obj.topology.lines_df.bus0 == cb_old_open, + "bus0", + ] = cb_old_closed + else: + edisgo_obj.topology.lines_df.loc[ + edisgo_obj.topology.lines_df.bus1 == cb_old_open, + "bus1", + ] = cb_old_closed + # convert the node where cb will be located from real bus-bar to virtual + if edisgo_obj.topology.lines_df.loc[branch, "bus0"] == cb_new_closed: + edisgo_obj.topology.lines_df.loc[branch, "bus0"] = cb_new_open + else: + edisgo_obj.topology.lines_df.loc[branch, "bus1"] = cb_new_open + logging.info(f"The new location of circuit breaker is {cb_new_closed}") + else: + logging.info( + f"The location of circuit breaker {cb_old_df.bus_closed[0]} " + f"has not changed" ) - # Line - lines_df.loc[ - lines_df.bus0 == f"virtual_{node_existing}", "bus0" - ] = node_existing - if lines_df.loc[branch, "bus0"] == node_cb: - lines_df.loc[branch, "bus0"] = new_virtual_bus + cos_phi_load = edisgo_obj.config["reactive_power_factor"]["mv_load"] + cos_phi_feedin = edisgo_obj.config["reactive_power_factor"]["mv_gen"] + + grid = edisgo_obj.topology.mv_grid + G = grid.graph + station = list(G.nodes)[0] + + circuit_breaker_changes = {} + node_peak_gen_dict = {} # dictionary of peak generations of all nodes in the graph + node_peak_load_dict = {} # dictionary of peak loads of all nodes in the graph + # add all the loads and gens to the dicts + for node in list(G.nodes): + # for Bus-bars + if "BusBar" in node: + # the lv_side of node + if "virtual" in node: + bus_node_lv = edisgo_obj.topology.transformers_df[ + edisgo_obj.topology.transformers_df.bus0 + == node.replace("virtual_", "") + ].bus1[0] else: - lines_df.loc[branch, "bus1"] = new_virtual_bus + bus_node_lv = edisgo_obj.topology.transformers_df[ + edisgo_obj.topology.transformers_df.bus0 == node + ].bus1[0] + # grid_id + grid_id = edisgo_obj.topology.buses_df[ + edisgo_obj.topology.buses_df.index.values == bus_node_lv + ].lv_grid_id[0] + # get lv_grid + count = 0 + for lv_grd in list(edisgo_obj.topology.mv_grid.lv_grids): + if str(int(grid_id)) in repr(lv_grd): + break + count += 1 + lv_grid = list(edisgo_obj.topology.mv_grid.lv_grids)[count] + + # todo:power adjustment + node_peak_gen_dict[node] = ( + lv_grid.generators_df.p_nom.sum() / cos_phi_feedin + ) + node_peak_load_dict[node] = lv_grid.loads_df.p_set.sum() / cos_phi_load + + # Generators + elif "gen" in node: + node_peak_gen_dict[node] = ( + edisgo_obj.topology.mv_grid.generators_df[ + edisgo_obj.topology.mv_grid.generators_df.bus == node + ].p_nom.sum() + / cos_phi_feedin + ) + node_peak_load_dict[node] = 0 + + # branchTees do not have any load and generation else: - logging.info("The location of switch disconnector has not changed") + node_peak_gen_dict[node] = 0 + node_peak_load_dict[node] = 0 - rings = _sort_rings(remove_mv_station=True) + rings = _sort_nodes(remove_mv_station=True) for ring in rings: - node_ring_dictionary = get_subtree_of_nodes(ring, graph) - node_ring_df = pd.DataFrame.from_dict(node_ring_dictionary, orient="index") - - node_peak_d = {} - for index, value in node_ring_df.iterrows(): + # nodes and subtree of these nodes + subtree_dict = _get_subtree_of_nodes(ring, G) + # find the peak generations and loads of nodes in the specified ring + for node, subtree_list in subtree_dict.items(): total_peak_gen = 0 total_peak_load = 0 - if value[0] is not None: - for v in value: - if v is None: - continue - # sum the load and generation of all subtree nodes - total_peak_gen += _calculate_peak_load_gen(v)[0] - total_peak_load += _calculate_peak_load_gen(v)[1] - # sum the load and generation of nodes of subtree and tree itself - total_peak_gen = total_peak_gen + _calculate_peak_load_gen(index)[0] - total_peak_load = total_peak_load + _calculate_peak_load_gen(index)[1] - else: - total_peak_gen += _calculate_peak_load_gen(index)[0] - total_peak_load += _calculate_peak_load_gen(index)[1] - node_peak_d.setdefault(index, []).append(total_peak_gen) - node_peak_d.setdefault(index, []).append(total_peak_load) - node_peak_df = pd.DataFrame.from_dict(node_peak_d, orient="index") - node_peak_df.rename( - columns={0: "total_peak_gen", 1: "total_peak_load"}, inplace=True - ) + for subtree_node in subtree_list: + if subtree_node is not None: + total_peak_gen = total_peak_gen + node_peak_gen_dict[subtree_node] + total_peak_load = ( + total_peak_load + node_peak_load_dict[subtree_node] + ) + + node_peak_gen_dict[node] = total_peak_gen + node_peak_gen_dict[node] + node_peak_load_dict[node] = total_peak_load + node_peak_load_dict[node] + + nodes_peak_load = [] + nodes_peak_generation = [] + + for node in ring: + nodes_peak_load.append(node_peak_load_dict[node]) + nodes_peak_generation.append(node_peak_gen_dict[node]) - diff_min = 10e9 if mode == "load": - node_peak_data = node_peak_df.total_peak_load + node_peak_data = nodes_peak_load elif mode == "generation": - node_peak_data = node_peak_df.total_peak_gen + node_peak_data = nodes_peak_generation elif mode == "loadgen": # is ring dominated by load or generation? # (check if there's more load than generation in ring or vice versa) - if sum(node_peak_df.total_peak_load) > sum(node_peak_df.total_peak_gen): - node_peak_data = node_peak_df.total_peak_load + if sum(nodes_peak_load) > sum(nodes_peak_generation): + node_peak_data = nodes_peak_load else: - node_peak_data = node_peak_df.total_peak_gen + node_peak_data = nodes_peak_generation else: raise ValueError("parameter 'mode' is invalid!") - for ctr in range(len(node_peak_df.index)): - - # split route and calc demand difference - route_data_part1 = sum(node_peak_data[0:ctr]) - route_data_part2 = sum(node_peak_data[ctr : len(node_peak_df.index)]) + # if none of the nodes is of the type LVStation, a switch + # disconnecter will be installed anyways. + if any([node for node in ring if "BusBar" in node]): + has_lv_station = True + else: + has_lv_station = False + logging.debug( + f"Ring {ring} does not have a LV station." + f"Switch disconnecter is installed at arbitrary " + "node." + ) - diff = abs(route_data_part1 - route_data_part2) - if diff <= diff_min: - diff_min = diff - position = ctr - else: - break + # calc optimal circuit breaker position + # Set start value for difference in ring halfs + diff_min = 10e9 + position = 0 + for ctr in range(len(node_peak_data)): + # check if node that owns the switch disconnector is of type + # LVStation + + if "BusBar" in ring[ctr] or not has_lv_station: + # split route and calc demand difference + route_data_part1 = sum(node_peak_data[0:ctr]) + route_data_part2 = sum(node_peak_data[ctr : len(node_peak_data)]) + + # equality has to be respected, otherwise comparison stops when + # demand/generation=0 + diff = abs(route_data_part1 - route_data_part2) + if diff <= diff_min: + diff_min = diff + position = ctr + else: + break # new cb location - node_cb = node_peak_df.index[position] + cb_new_closed = ring[position] # check if node is last node of ring - if position < len(node_peak_df.index): + if position < len(node_peak_data): # check which branch to disconnect by determining load difference # of neighboring nodes + diff2 = abs( sum(node_peak_data[0 : position + 1]) - sum(node_peak_data[position + 1 : len(node_peak_data)]) ) if diff2 < diff_min: - - node2 = node_peak_df.index[position + 1] + node2 = ring[position + 1] else: - node2 = node_peak_df.index[position - 1] - _change_dataframe(node_cb, ring) - return node_cb + node2 = ring[position - 1] + else: + node2 = ring[position - 1] + + cb_df_old = _get_circuit_breaker_df(ring) # old circuit breaker df + + # update buses_df, lines_df and switches_df + _change_dataframe(cb_new_closed, cb_df_old) + + # add number of changed circuit breakers to circuit_breaker_changes + if cb_new_closed != cb_df_old.bus_closed[0]: + circuit_breaker_changes[cb_df_old.index[0]] = 1 + + if len(circuit_breaker_changes): + logger.info( + f"{len(circuit_breaker_changes)} circuit breakers are relocated in {grid}" + ) + else: + logger.info(f"no circuit breaker is relocated in {grid}") + return circuit_breaker_changes From 4d4a544cc8e249cc996a4ca23dff94752761efd7 Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Sun, 18 Dec 2022 23:38:25 +0100 Subject: [PATCH 18/43] Integration of the method "relocate circuit breaker" --- edisgo/flex_opt/reinforce_grid_alternative.py | 216 ++++++++++++------ 1 file changed, 142 insertions(+), 74 deletions(-) diff --git a/edisgo/flex_opt/reinforce_grid_alternative.py b/edisgo/flex_opt/reinforce_grid_alternative.py index 3de5bc334..e8d660737 100644 --- a/edisgo/flex_opt/reinforce_grid_alternative.py +++ b/edisgo/flex_opt/reinforce_grid_alternative.py @@ -16,6 +16,7 @@ def reinforce_line_overloading_alternative( edisgo, + add_method=None, timesteps_pfa=None, copy_grid=False, mode=None, @@ -136,6 +137,25 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): edisgo_reinforce.results.equipment_changes = pd.concat(df_list) + def _add_circuit_breaker_changes_to_equipment_changes(): + edisgo_reinforce.results.equipment_changes = pd.concat( + [ + edisgo_reinforce.results.equipment_changes, + pd.DataFrame( + { + "iteration_step": [iteration_step] + * len(circuit_breaker_changes), + "change": ["changed"] * len(circuit_breaker_changes), + "equipment": edisgo_reinforce.topology.switches_df.loc[ + circuit_breaker_changes.keys(), "type_info" + ].values, + "quantity": [_ for _ in circuit_breaker_changes.values()], + }, + index=circuit_breaker_changes.keys(), + ), + ], + ) + # check if provided mode is valid if mode and mode not in ["mv", "lv"]: raise ValueError(f"Provided mode {mode} is not valid.") @@ -169,10 +189,27 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): f"Input {timesteps_pfa} for timesteps_pfa is not valid." ) + methods = [ + "relocate_circuit_breaker", + "add_station_at_half_length", + "split_feeder_at_half_length", + "add_same_type_of_parallel_line", + ] + + if add_method is None: + add_method = methods + + if isinstance(add_method, str): + add_method = [add_method] + + if add_method and not any(method in methods for method in add_method): + # check if provided method is valid + raise ValueError(f"Provided method {add_method} is not valid.") + iteration_step = 1 - analyze_mode = None if mode == "lv" else mode + # analyze_mode = None if mode == "lv" else mode - edisgo_reinforce.analyze(mode=analyze_mode, timesteps=timesteps_pfa) + edisgo_reinforce.analyze(timesteps=timesteps_pfa) # 1-REINFORCE OVERLOADED LINES logger.debug("==> Check line loadings.") @@ -183,50 +220,80 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): # 1.1.1 Method:Split the feeder at the half-length of feeder (applied only once to # secure n-1). if (not mode or mode == "mv") and not crit_lines_mv.empty: - # TODO: add method resiting of CB - logger.debug( - f"==>feeder splitting method is running for MV grid " - f"{edisgo_reinforce.topology.mv_grid}: " - ) - lines_changes = reinforce_measures.split_feeder_at_half_length( - edisgo_reinforce, edisgo_reinforce.topology.mv_grid, crit_lines_mv - ) - _add_lines_changes_to_equipment_changes() + if "add_station_at_half_length" in add_method: + logger.warning( + "method:add_station_at_half_length is only applicable for LV grids" + ) - logger.debug("==> Run power flow analysis.") - edisgo_reinforce.analyze(mode=analyze_mode, timesteps=timesteps_pfa) + if "relocate_circuit_breaker" in add_method or add_method is None: + # method-1: relocate_circuit_breaker + logger.info( + "==> the method relocate circuit breaker location" + " " + "is running for MV grid {edisgo_reinforce.topology.mv_grid}: " + ) + circuit_breaker_changes = reinforce_measures.relocate_circuit_breaker( + edisgo_reinforce, mode="loadgen" + ) + _add_circuit_breaker_changes_to_equipment_changes() + logger.debug("==> Run power flow analysis.") + edisgo_reinforce.analyze(timesteps=timesteps_pfa) + + if "split_feeder_at_half_length" in add_method or add_method is None: + # method-2: split_feeder_at_half_length + logger.info( + f"==>feeder splitting method is running for MV grid " + f"{edisgo_reinforce.topology.mv_grid}: " + ) + lines_changes = reinforce_measures.split_feeder_at_half_length( + edisgo_reinforce, edisgo_reinforce.topology.mv_grid, crit_lines_mv + ) + _add_lines_changes_to_equipment_changes() + + logger.debug("==> Run power flow analysis.") + edisgo_reinforce.analyze(timesteps=timesteps_pfa) # 1.2- Voltage level= LV if (not mode or mode == "lv") and not crit_lines_lv.empty: - for lv_grid in list(edisgo_reinforce.topology.mv_grid.lv_grids): - # 1.2.1 Method: Split the feeder at the half-length of feeder and add - # new station( applied only once ) - # if the number of overloaded lines is more than 2 - logger.debug( - f"==>split+add substation method is running for LV grid {lv_grid}: " - ) - ( - transformer_changes, - lines_changes, - ) = reinforce_measures.add_station_at_half_length( - edisgo_reinforce, lv_grid, crit_lines_lv + + if "relocate_circuit_breaker" in add_method: + logger.warning( + "method:relocate_circuit_breaker is only applicable for Mv grids" ) - if transformer_changes and lines_changes: - _add_transformer_changes_to_equipment_changes("added") - _add_lines_changes_to_equipment_changes() - else: - # 1.2.2 Method:Split the feeder at the half-length of feeder (applied - # only once) + # reset changes from MV grid + transformer_changes = {} + lines_changes = {} + for lv_grid in list(edisgo_reinforce.topology.mv_grid.lv_grids): + if "add_station_at_half_length" in add_method or add_method is None: + # 1.2.1 Method: Split the feeder at the half-length of feeder and add + # new station( applied only once ) + # if the number of overloaded lines is more than 2 logger.debug( - f"==>feeder splitting method is running for LV grid {lv_grid}: " + f"==>split+add substation method is running for LV grid {lv_grid}: " ) - lines_changes = reinforce_measures.split_feeder_at_half_length( + ( + transformer_changes, + lines_changes, + ) = reinforce_measures.add_station_at_half_length( edisgo_reinforce, lv_grid, crit_lines_lv ) + if transformer_changes and lines_changes: + _add_transformer_changes_to_equipment_changes("added") _add_lines_changes_to_equipment_changes() + else: + if "split_feeder_at_half_length" in add_method or add_method is None: + # 1.2.2 Method:Split the feeder at the half-length of feeder + # (applied only once) + logger.debug( + f"==>feeder splitting method is running for LV grid {lv_grid}: " + ) + lines_changes = reinforce_measures.split_feeder_at_half_length( + edisgo_reinforce, lv_grid, crit_lines_lv + ) + _add_lines_changes_to_equipment_changes() logger.debug("==> Run power flow analysis.") - edisgo_reinforce.analyze(mode=analyze_mode, timesteps=timesteps_pfa) + edisgo_reinforce.analyze(timesteps=timesteps_pfa) logger.debug("==> Recheck line load.") crit_lines = ( @@ -242,54 +309,55 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): checks.lv_line_load(edisgo_reinforce), ] ) + if "add_same_type_of_parallel_line" in add_method or add_method is None: + # 2- Remanining crit_lines- Voltage level MV and LV + # Method: Add same type of parallel line + while_counter = 0 + while not crit_lines.empty and while_counter < max_while_iterations: + + logger.info(f"==>add parallel line method is running_Step{iteration_step}") + lines_changes = reinforce_measures.add_same_type_of_parallel_line( + edisgo_reinforce, crit_lines + ) - # 1.2 Method: Add same type of parallel line -MV and LV grid - while_counter = 0 - while not crit_lines.empty and while_counter < max_while_iterations: - - logger.debug(f"==>add parallel line method is running_Step{iteration_step}") - lines_changes = reinforce_measures.add_same_type_of_parallel_line( - edisgo_reinforce, crit_lines - ) + _add_lines_changes_to_equipment_changes() - _add_lines_changes_to_equipment_changes() + logger.debug("==> Run power flow analysis.") + edisgo_reinforce.analyze(timesteps=timesteps_pfa) - logger.debug("==> Run power flow analysis.") - edisgo_reinforce.analyze(mode=analyze_mode, timesteps=timesteps_pfa) - - logger.debug("==> Recheck line load.") - crit_lines = ( - pd.DataFrame(dtype=float) - if mode == "lv" - else checks.mv_line_load(edisgo_reinforce) - ) + logger.debug("==> Recheck line load.") + crit_lines = ( + pd.DataFrame(dtype=float) + if mode == "lv" + else checks.mv_line_load(edisgo_reinforce) + ) - if not mode or mode == "lv": - crit_lines = pd.concat( + if not mode or mode == "lv": + crit_lines = pd.concat( + [ + crit_lines, + checks.lv_line_load(edisgo_reinforce), + ] + ) + while_counter += 1 + iteration_step += +1 + # check if all load problems were solved after maximum number of + # iterations allowed + if while_counter == max_while_iterations and (not crit_lines.empty): + edisgo_reinforce.results.unresolved_issues = pd.concat( [ + edisgo_reinforce.results.unresolved_issues, crit_lines, - checks.lv_line_load(edisgo_reinforce), ] ) - while_counter += 1 - iteration_step += +1 - # check if all load problems were solved after maximum number of - # iterations allowed - if while_counter == max_while_iterations and (not crit_lines.empty): - edisgo_reinforce.results.unresolved_issues = pd.concat( - [ - edisgo_reinforce.results.unresolved_issues, - crit_lines, - ] - ) - raise exceptions.MaximumIterationError( - "Overloading issues could not be solved after maximum allowed " - "iterations." - ) - else: - logger.info( - f"==> Load issues were solved in {while_counter} iteration step(s)." - ) + raise exceptions.MaximumIterationError( + "Overloading issues could not be solved after maximum allowed " + "iterations." + ) + else: + logger.info( + f"==> Load issues were solved in {while_counter} iteration step(s)." + ) edisgo_reinforce.results.grid_expansion_costs = grid_expansion_costs( edisgo_reinforce, without_generator_import=without_generator_import ) From f6d6b2b9d1fd5593b86e6cde5723355dc964f2e4 Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Sun, 18 Dec 2022 23:39:57 +0100 Subject: [PATCH 19/43] add costs for cbs --- edisgo/flex_opt/costs.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/edisgo/flex_opt/costs.py b/edisgo/flex_opt/costs.py index 4e5a62f65..6eac92eea 100644 --- a/edisgo/flex_opt/costs.py +++ b/edisgo/flex_opt/costs.py @@ -183,6 +183,43 @@ def _get_line_costs(lines_added): ), ] ) + # costs for circuit breakers + # get changed cbs + circuit_breakers = equipment_changes.loc[ + equipment_changes.index.isin(edisgo_obj.topology.switches_df.index) + ] + + cb_changed = circuit_breakers.iloc[ + ( + circuit_breakers.equipment + == edisgo_obj.topology.switches_df.loc[ + circuit_breakers.index, "type_info" + ] + ).values + ]["quantity"].to_frame() + + if not cb_changed.empty: + cb_costs = float( + edisgo_obj.config["costs_circuit_breakers"][ + "circuit_breaker_installation_work" + ] + ) + costs = pd.concat( + [ + costs, + pd.DataFrame( + { + "type": edisgo_obj.topology.switches_df.loc[ + cb_changed.index, "type_info" + ].values, + "total_costs": cb_costs, + "quantity": cb_changed.quantity.values, + "voltage_level": "mv", + }, + index=cb_changed.index, + ), + ] + ) # if no costs incurred write zero costs to DataFrame if costs.empty: From c71263a0830b2d265fe80651456e99662e98be09 Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Sun, 18 Dec 2022 23:40:32 +0100 Subject: [PATCH 20/43] add cost of cb --- edisgo/config/config_grid_expansion_default.cfg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/edisgo/config/config_grid_expansion_default.cfg b/edisgo/config/config_grid_expansion_default.cfg index c67c38bb2..5f7fae44f 100644 --- a/edisgo/config/config_grid_expansion_default.cfg +++ b/edisgo/config/config_grid_expansion_default.cfg @@ -75,14 +75,14 @@ mv_lv_station_feed-in_case_max_v_deviation = 0.015 # ============ # Source: Rehtanz et. al.: "Verteilnetzstudie für das Land Baden-Württemberg", 2017. mv_load_case_transformer = 0.5 -mv_load_case_line = 1.0 +mv_load_case_line = 0.1 mv_feed-in_case_transformer = 1.0 -mv_feed-in_case_line = 1.0 +mv_feed-in_case_line = 0.1 lv_load_case_transformer = 1.0 -lv_load_case_line = 1.0 +lv_load_case_line = 0.01 lv_feed-in_case_transformer = 1.0 -lv_feed-in_case_line = 1.0 +lv_feed-in_case_line = 0.01 # costs # ============ From efbd997ab8f01d592dfce15223757febd8caa15b Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Sun, 22 Jan 2023 23:36:59 +0100 Subject: [PATCH 21/43] add alternative voltage issue reinforcement methods --- edisgo/flex_opt/reinforce_measures.py | 533 +++++++++++++++++++++++++- 1 file changed, 529 insertions(+), 4 deletions(-) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index e4b888a5e..c0d59d366 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -841,9 +841,6 @@ def split_feeder_at_half_length(edisgo_obj, grid, crit_lines): (not the feeder has more load) """ - def get_weight(u, v, data): - return data["length"] - if isinstance(grid, LVGrid): voltage_level = "lv" @@ -917,6 +914,7 @@ def get_weight(u, v, data): lines_changes = {} for node_feeder, node_list in nodes_feeder.items(): + get_weight = lambda u, v, data: data["length"] # noqa: E731 feeder_first_line = crit_lines_feeder[ crit_lines_feeder.bus1 == node_feeder ].index[0] @@ -1008,7 +1006,7 @@ def get_weight(u, v, data): lines_changes[line_added] = 1 if lines_changes: logger.info( - f"{len(lines_changes)} line/s are reinforced by split feeder " + f"{len(lines_changes)} line/s are reinforced by split feeder at half-length" f"method in {grid}" ) @@ -1745,3 +1743,530 @@ def _change_dataframe(cb_new_closed, cb_old_df): else: logger.info(f"no circuit breaker is relocated in {grid}") return circuit_breaker_changes + + +def split_feeder_at_2_3_length(edisgo_obj, grid, crit_nodes): + """ + The voltage issue of the lines in MV and LV grid is remedied by splitting the feeder + at the 2/3-length + + 1-The point at 2/3-length of the feeders is found. + 2-The first node following this point is chosen as the point where the new + connection will be made. This node can only be a station. + 3-This node is disconnected from the previous node and connected to the main station + + Notes: + In LV grids, the node inside the building is not considered. + The method is not applied if the node is the first node after the main station. + + Parameters + ---------- + edisgo_obj:class:`~.EDisGo` + grid:class:`~.network.grids.MVGrid` or :class:`~.network.grids.LVGrid` + crit_nodes:pandas:`pandas.DataFrame` + Dataframe with all nodes with voltage issues in the grid and + their maximal deviations from allowed lower or upper voltage limits + sorted descending from highest to lowest voltage deviation + (it is not distinguished between over- or undervoltage). + Columns of the dataframe are 'v_diff_max' containing the maximum + absolute voltage deviation as float and 'time_index' containing the + corresponding time step the voltage issue occured in as + :pandas:`pandas.Timestamp`. Index of the dataframe are the + names of all buses with voltage issues. + + Returns + ------- + dict + + Dictionary with the name of lines as keys and the corresponding number of + lines added as values. + + Notes + ----- + In this method, the separation is done according to the farthest node of feeder + + """ + + G = grid.graph + station_node = list(G.nodes)[0] # main station + + paths = {} + nodes_feeder = {} + for node in crit_nodes.index: + path = nx.shortest_path(G, 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: + logging.error( + f"Voltage issues at busbar in LV network {grid} " + f"should have been solved in previous steps." + ) + nodes_feeder.setdefault(path[1], []).append(node) + + lines_changes = {} + 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( + G, 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 the 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 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( + f" line of {node_2_3} could not be reinforced due to " + f"insufficient number of node . " + ) + break + + # if MVGrid: check if node_2_3 is LV station and if not find + # next or preceding 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: + while ( + node_2_3 not in edisgo_obj.topology.transformers_df.bus0.values + ): + if path.index(node_2_3) > 1: + node_2_3 = path[path.index(node_2_3) - 1] + else: + logger.error( + f" line of {node_2_3} could not be reinforced due to " + f"the lack of LV station . " + ) + 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 = G.get_edge_data(station_node, node_2_3)["branch_name"] + crit_line = grid.lines_df.loc[crit_line_name] + + lines_changes = add_same_type_of_parallel_line(edisgo_obj, crit_line) + + else: + # get line between node_2_3 and predecessor node + pred_node = path[path.index(node_2_3) - 1] + line_removed = G.get_edge_data(node_2_3, pred_node)["branch_name"] + + # note:line between node_2_3 and pred_node is not removed and the connection + # points of line ,changed from the node to main station, is changed. + # Therefore, the line connected to the main station has the same name + # with the line to be removed. + # todo: the name of added line should be + # created and name of removed line should be deleted from the lines_df + + # change the connection of the node_2_3 from pred node to main station + if grid.lines_df.at[line_removed, "bus0"] == pred_node: + + edisgo_obj.topology._lines_df.at[line_removed, "bus0"] = station_node + logger.info( + f"==> {grid}--> the line {line_removed} disconnected from " + f"{pred_node} and connected to the main station {station_node} " + ) + elif grid.lines_df.at[line_removed, "bus1"] == pred_node: + + edisgo_obj.topology._lines_df.at[line_removed, "bus1"] = station_node + logger.info( + f"==> {grid}-->the line {line_removed} disconnected from " + f"{pred_node} and connected to the main station {station_node} " + ) + else: + raise ValueError("Bus not in line buses. " "Please check.") + # change the line length + # the properties of the added line are the same as the removed line + edisgo_obj.topology._lines_df.at[line_removed, "length"] = path_length_dict[ + node_2_3 + ] + line_added = line_removed + lines_changes[line_added] = 1 + print("done") + if lines_changes: + logger.info( + f"{len(lines_changes)} line/s are reinforced by split feeder at 2/3-length " + f"method in {grid}" + ) + return lines_changes + + +def add_substation_at_2_3_length(edisgo_obj, grid, crit_nodes): + """ + If the number of overloaded feeders in the LV grid is more than 2, the feeders are + split at their 2/3-length, and the disconnected points are connected to the + new MV/LV station. + + + 1-The point at 2/3 the length of the feeders is found. + 2-The first node following this point is chosen as the point where the new + connection will be made. This node can only be a station. + 3-This node is disconnected from the previous node and connected to a new station. + 4-New MV/LV is connected to the existing MV/LV station with a line of which length + equals the line length between the node at the half-length (node_2_3) and its + preceding node. + + Notes: + -If the number of overloaded lines in the LV grid is less than 3 and the node_2_3 + is the first node after the main station, the method is not applied. + -The name of the new grid will be the existing grid code + (e.g. 40000) + 1001 = 400001001 + -The name of the lines in the new LV grid is the same as the grid where the nodes + are removed + -Except line names, all the data frames are named based on the new grid name + + Parameters + ---------- + edisgo_obj: class:`~.EDisGo` + grid: class:`~.network.grids.LVGrid` + crit_lines: Dataframe containing over-loaded lines, their maximum relative + over-loading (maximum calculated current over allowed current) and the + corresponding time step. + Index of the data frame is the names of the over-loaded lines. + Columns are 'max_rel_overload' containing the maximum relative + over-loading as float, 'time_index' containing the corresponding + time-step the over-loading occurred in as + :pandas:`pandas.Timestamp`, and 'voltage_level' specifying + the voltage level the line is in (either 'mv' or 'lv'). + + Returns + ------- + line_changes= dict + Dictionary with name of lines as keys and the corresponding number of + lines added as values. + transformer_changes= dict + Dictionary with added and removed transformers in the form:: + + {'added': {'Grid_1': ['transformer_reinforced_1', + ..., + 'transformer_reinforced_x'], + 'Grid_10': ['transformer_reinforced_10'] + } + } + """ + + def _get_subtree_of_node(node, main_path): + + if node != station_node: + nodes_subtree = set() + for path in nx.shortest_path(G, node).values(): + if len(path) > 1: + if (path[1] not in main_path) and (path[1] != station_node): + nodes_subtree.update(path[1 : len(path)]) + + return nodes_subtree + + def create_bus_name(bus, voltage_level): + + """ + Create an LV and MV bus-bar name with the same grid_id but added "1001" that + implies the separation + + Parameters + ---------- + bus :eg 'BusBar_mvgd_460_lvgd_131573_LV' + voltage_level : "mv" or "lv" + + Returns + ---------- + bus: str New bus-bar name + """ + if bus in edisgo_obj.topology.buses_df.index: + bus = bus.split("_") + grid_id_ind = bus.index(str(grid.id)) + bus[grid_id_ind] = str(grid.id) + "1001" + if voltage_level == "lv": + bus = "_".join([str(_) for _ in bus]) + elif voltage_level == "mv": + bus[-1] = "MV" + bus = "_".join([str(_) for _ in bus]) + else: + logger.error("voltage level can only be " "mv" " or " "lv" "") + else: + raise IndexError("The bus is not in the dataframe") + + return bus + + def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): + """ + Adds standard transformer to topology. + + Parameters + ---------- + edisgo_obj: class:`~.EDisGo` + grid: `~.network.grids.LVGrid` + bus_lv: Identifier of lv bus + bus_mv: Identifier of mv bus + + Returns + ---------- + transformer_changes= dict + """ + if bus_lv not in edisgo_obj.topology.buses_df.index: + raise ValueError( + f"Specified bus {bus_lv} is not valid as it is not defined in " + "buses_df." + ) + if bus_mv not in edisgo_obj.topology.buses_df.index: + raise ValueError( + f"Specified bus {bus_mv} is not valid as it is not defined in " + "buses_df." + ) + + try: + standard_transformer = edisgo_obj.topology.equipment_data[ + "lv_transformers" + ].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.") + + transformers_changes = {"added": {}} + + transformer_s = grid.transformers_df.iloc[0] + new_transformer_name = transformer_s.name.split("_") + grid_id_ind = new_transformer_name.index(str(grid.id)) + new_transformer_name[grid_id_ind] = str(grid.id) + "1001" + + transformer_s.s_nom = standard_transformer.S_nom + transformer_s.type_info = standard_transformer.name + transformer_s.r_pu = standard_transformer.r_pu + transformer_s.x_pu = standard_transformer.x_pu + transformer_s.name = "_".join([str(_) for _ in new_transformer_name]) + transformer_s.bus0 = bus_mv + transformer_s.bus1 = bus_lv + + new_transformer_df = transformer_s.to_frame().T + + edisgo_obj.topology.transformers_df = pd.concat( + [edisgo_obj.topology.transformers_df, new_transformer_df] + ) + transformers_changes["added"][ + f"LVGrid_{str(grid.id)}1001" + ] = new_transformer_df.index.tolist() + return transformers_changes + + G = grid.graph + station_node = list(G.nodes)[0] # main station + + paths = {} + first_nodes_feeders = {} + for node in crit_nodes.index: + path = nx.shortest_path(G, 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: + logging.error( + f"Voltage issues at busbar in LV network {grid} should have " + "been solved in previous steps." + ) + first_nodes_feeders.setdefault(path[1], []).append(node) + lines_changes = {} + transformers_changes = {} + nodes_tb_relocated = {} # nodes to be moved into the new grid + + first_nodes_feeders = sorted( + first_nodes_feeders.items(), key=lambda item: len(item[1]), reverse=False + ) + first_nodes_feeders = dict(first_nodes_feeders) + + loop_counter = len(first_nodes_feeders) + + for first_node, nodes_feeder in first_nodes_feeders.items(): + + # find the farthest node in the feeder + get_weight = lambda u, v, data: data["length"] # noqa: E731 + + path_length = 0 + for n in first_nodes_feeders[first_node]: + path_length_dict_tmp = dijkstra_shortest_path_length( + G, 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] + + 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 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]: + grid.error( + f" line of {node_2_3} could not be reinforced " + f"due to insufficient number of node in the feeder . " + ) + break + + loop_counter -= 1 + # if node_2_3 is a representative (meaning it is already directly connected + # to the station), line cannot be disconnected and reinforced + + if node_2_3 not in first_nodes_feeders.keys(): + if node_2_3 not in first_nodes_feeders.keys(): + nodes_path = path.copy() + for main_node in nodes_path: + sub_nodes = _get_subtree_of_node(main_node, main_path=nodes_path) + if sub_nodes is not None: + nodes_path[ + nodes_path.index(main_node) + + 1 : nodes_path.index(main_node) + + 1 + ] = [n for n in sub_nodes] + nodes_tb_relocated[node_2_3] = nodes_path[nodes_path.index(node_2_3) :] + pred_node = path[path.index(node_2_3) - 1] # predecessor node of node_2_3 + + line_removed = G.get_edge_data(node_2_3, pred_node)[ + "branch_name" + ] # the line + line_added_lv = line_removed + lines_changes[line_added_lv] = 1 + # removed from exiting LV grid and converted to an MV line between new + # and existing MV/LV station + if len(nodes_tb_relocated) > 2 and loop_counter == 0: + # Create the bus-bar name of primary and secondary side of new MV/LV station + lv_bus_new = create_bus_name(station_node, "lv") + mv_bus_new = create_bus_name(station_node, "mv") + + # ADD MV and LV bus + v_nom_lv = edisgo_obj.topology.buses_df.loc[ + grid.transformers_df.bus1[0], + "v_nom", + ] + v_nom_mv = edisgo_obj.topology.buses_df.loc[ + grid.transformers_df.bus0[0], + "v_nom", + ] + + x_bus = grid.buses_df.loc[station_node, "x"] + y_bus = grid.buses_df.loc[station_node, "y"] + + # the new lv line id: e.g. 496021001 + lv_grid_id_new = int(str(grid.id) + "1001") + building_bus = grid.buses_df.loc[station_node, "in_building"] + + # the distance between new and existing MV station in MV grid will be the + # same with the distance between pred. node of node_2_3 of one of first + # feeders to be split in LV grid + + length = ( + path_length_dict[node_2_3] + - path_length_dict[path[path.index(node_2_3) - 1]] + ) + # if the transformer already added, do not add bus and transformer once more + if not transformers_changes: + # the coordinates of new MV station (x2,y2) + # the coordinates of existing LV station (x1,y1) + # y1=y2, x2=x1+length/1000 + + # add lv busbar + edisgo_obj.topology.add_bus( + lv_bus_new, + v_nom_lv, + x=x_bus + length / 1000, + y=y_bus, + lv_grid_id=lv_grid_id_new, + in_building=building_bus, + ) + # add mv busbar + edisgo_obj.topology.add_bus( + mv_bus_new, + v_nom_mv, + x=x_bus + length / 1000, + y=y_bus, + in_building=building_bus, + ) + + # ADD TRANSFORMER + transformer_changes = add_standard_transformer( + edisgo_obj, grid, lv_bus_new, mv_bus_new + ) + transformers_changes.update(transformer_changes) + + logger.debug(f"A new grid {lv_grid_id_new} added into topology") + + # ADD the MV LINE between existing and new MV station + + standard_line = edisgo_obj.config["grid_expansion_standard_equipment"][ + f"mv_line_{int(edisgo_obj.topology.mv_grid.nominal_voltage)}kv" + ] + + line_added_mv = edisgo_obj.topology.add_line( + bus0=grid.transformers_df.bus0[0], + bus1=mv_bus_new, + length=length, + type_info=standard_line, + kind="cable", + ) + lines_changes[line_added_mv] = 1 + # changes on relocated lines to the new LV grid + # grid_ids + for node_2_3, nodes in nodes_tb_relocated.items(): + edisgo_obj.topology.buses_df.loc[ + node_2_3, "lv_grid_id" + ] = lv_grid_id_new + edisgo_obj.topology.buses_df.loc[ + nodes, "lv_grid_id" + ] = lv_grid_id_new + # line connection of node_2_3 from the predecessor node in the + # existing grid to the lv side of new station + if edisgo_obj.topology.lines_df.bus1.isin([node_2_3]).any(): + edisgo_obj.topology.lines_df.loc[ + edisgo_obj.topology.lines_df.bus1 == node_2_3, "bus0" + ] = lv_bus_new + else: + raise LookupError(f"{node_2_3} is not in the lines dataframe") + logger.debug( + f"the node {node_2_3} is split from the line and connected to " + f"{lv_grid_id_new} " + ) + logger.info( + f"{len(nodes_tb_relocated.keys())} feeders are removed from the grid " + f"{grid} and located in new grid{repr(grid) + str(1001)} by split " + f"feeder+add transformer method" + ) + if len(lines_changes) < 3: + lines_changes = {} + + return transformers_changes, lines_changes From 91a7ca26e8e1a268a8add4904d9fc36e81207d7c Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Tue, 24 Jan 2023 23:53:28 +0100 Subject: [PATCH 22/43] add cost of each type of cables --- .../equipment-parameters_LV_cables.csv | 20 +++---- .../equipment-parameters_MV_cables.csv | 24 ++++----- ...equipment-parameters_MV_overhead_lines.csv | 14 ++--- edisgo/flex_opt/costs.py | 53 ++++++++++++++++--- 4 files changed, 76 insertions(+), 35 deletions(-) diff --git a/edisgo/equipment/equipment-parameters_LV_cables.csv b/edisgo/equipment/equipment-parameters_LV_cables.csv index ac72e50fc..02fdf58f2 100644 --- a/edisgo/equipment/equipment-parameters_LV_cables.csv +++ b/edisgo/equipment/equipment-parameters_LV_cables.csv @@ -1,10 +1,10 @@ -name,U_n,I_max_th,R_per_km,L_per_km,C_per_km -#-,kV,kA,ohm/km,mH/km,uF/km -NAYY 4x1x300,0.4,0.419,0.1,0.279,0 -NAYY 4x1x240,0.4,0.364,0.125,0.254,0 -NAYY 4x1x185,0.4,0.313,0.164,0.256,0 -NAYY 4x1x150,0.4,0.275,0.206,0.256,0 -NAYY 4x1x120,0.4,0.245,0.253,0.256,0 -NAYY 4x1x95,0.4,0.215,0.320,0.261,0 -NAYY 4x1x50,0.4,0.144,0.449,0.270,0 -NAYY 4x1x35,0.4,0.123,0.868,0.271,0 +name,U_n,I_max_th,R_per_km,L_per_km,C_per_km,cost +#-,kV,kA,ohm/km,mH/km,uF/km,kEuro/km +NAYY 4x1x300,0.4,0.419,0.1,0.279,0,20 +NAYY 4x1x240,0.4,0.364,0.125,0.254,0,12.8 +NAYY 4x1x185,0.4,0.313,0.164,0.256,0,10 +NAYY 4x1x150,0.4,0.275,0.206,0.256,0,8.4 +NAYY 4x1x120,0.4,0.245,0.253,0.256,0,7.7 +NAYY 4x1x95,0.4,0.215,0.320,0.261,0,6.5 +NAYY 4x1x50,0.4,0.144,0.449,0.270,0,6 +NAYY 4x1x35,0.4,0.123,0.868,0.271,0,5 diff --git a/edisgo/equipment/equipment-parameters_MV_cables.csv b/edisgo/equipment/equipment-parameters_MV_cables.csv index 86da0d102..cea708215 100644 --- a/edisgo/equipment/equipment-parameters_MV_cables.csv +++ b/edisgo/equipment/equipment-parameters_MV_cables.csv @@ -1,12 +1,12 @@ -name,U_n,I_max_th,R_per_km,L_per_km,C_per_km -#-,kV,kA,ohm/km,mH/km,uF/km -NA2XS2Y 3x1x185 RM/25,10,0.357,0.164,0.38,0.41 -NA2XS2Y 3x1x240 RM/25,10,0.417,0.125,0.36,0.47 -NA2XS2Y 3x1x300 RM/25,10,0.466,0.1,0.35,0.495 -NA2XS2Y 3x1x400 RM/35,10,0.535,0.078,0.34,0.57 -NA2XS2Y 3x1x500 RM/35,10,0.609,0.061,0.32,0.63 -NA2XS2Y 3x1x150 RE/25,20,0.319,0.206,0.4011,0.24 -NA2XS2Y 3x1x240,20,0.417,0.13,0.3597,0.304 -NA2XS(FL)2Y 3x1x300 RM/25,20,0.476,0.1,0.37,0.25 -NA2XS(FL)2Y 3x1x400 RM/35,20,0.525,0.078,0.36,0.27 -NA2XS(FL)2Y 3x1x500 RM/35,20,0.598,0.06,0.34,0.3 +name,U_n,I_max_th,R_per_km,L_per_km,C_per_km,cost +#-,kV,kA,ohm/km,mH/km,uF/km,kEuro/km +NA2XS2Y 3x1x185 RM/25,10,0.357,0.164,0.38,0.41,24 +NA2XS2Y 3x1x240 RM/25,10,0.417,0.125,0.36,0.47,27 +NA2XS2Y 3x1x300 RM/25,10,0.466,0.1,0.35,0.495,39 +NA2XS2Y 3x1x400 RM/35,10,0.535,0.078,0.34,0.57,50 +NA2XS2Y 3x1x500 RM/35,10,0.609,0.061,0.32,0.63,60 +NA2XS2Y 3x1x150 RE/25,20,0.319,0.206,0.4011,0.24,39 +NA2XS2Y 3x1x240,20,0.417,0.13,0.3597,0.304,50 +NA2XS(FL)2Y 3x1x300 RM/25,20,0.476,0.1,0.37,0.25,60 +NA2XS(FL)2Y 3x1x400 RM/35,20,0.525,0.078,0.36,0.27,90 +NA2XS(FL)2Y 3x1x500 RM/35,20,0.598,0.06,0.34,0.3,120 diff --git a/edisgo/equipment/equipment-parameters_MV_overhead_lines.csv b/edisgo/equipment/equipment-parameters_MV_overhead_lines.csv index cebce1a7b..b4ccb9e4f 100644 --- a/edisgo/equipment/equipment-parameters_MV_overhead_lines.csv +++ b/edisgo/equipment/equipment-parameters_MV_overhead_lines.csv @@ -1,8 +1,8 @@ -name,U_n,I_max_th,R_per_km,L_per_km,C_per_km +name,U_n,I_max_th,R_per_km,L_per_km,C_per_km,cost #-,kV,kA,ohm/km,mH/km,uF/km -48-AL1/8-ST1A,10,0.21,0.35,1.11,0.0104 -94-AL1/15-ST1A,10,0.35,0.33,1.05,0.0112 -122-AL1/20-ST1A,10,0.41,0.31,0.99,0.0115 -48-AL1/8-ST1A,20,0.21,0.37,1.18,0.0098 -94-AL1/15-ST1A,20,0.35,0.35,1.11,0.0104 -122-AL1/20-ST1A,20,0.41,0.34,1.08,0.0106 +48-AL1/8-ST1A,10,0.21,0.35,1.11,0.0104,130 +94-AL1/15-ST1A,10,0.35,0.33,1.05,0.0112,160 +122-AL1/20-ST1A,10,0.41,0.31,0.99,0.0115,190 +48-AL1/8-ST1A,20,0.21,0.37,1.18,0.0098,220 +94-AL1/15-ST1A,20,0.35,0.35,1.11,0.0104,250 +122-AL1/20-ST1A,20,0.41,0.34,1.08,0.0106,300 diff --git a/edisgo/flex_opt/costs.py b/edisgo/flex_opt/costs.py index 6eac92eea..0c546592b 100644 --- a/edisgo/flex_opt/costs.py +++ b/edisgo/flex_opt/costs.py @@ -1,5 +1,6 @@ import os +import numpy as np import pandas as pd if "READTHEDOCS" not in os.environ: @@ -262,6 +263,39 @@ def line_expansion_costs(edisgo_obj, lines_names): 'costs_earthworks', 'costs_cable', 'voltage_level' for each line """ + + def cost_cable_types(mode): + # TODO: rewrite it with pd.merge or pd.concat + costs_cable = [] + for equip_name1 in equipment_df.equipment: + if mode == "mv": + mv_cable_df = ( + edisgo_obj.topology.equipment_data[f"{mode}_cables"] + .loc[ + edisgo_obj.topology.equipment_data[f"{mode}_cables"].U_n == 10.0 + ] + .loc[:, ["cost"]] + ) + mv_overhead_lines = ( + edisgo_obj.topology.equipment_data[f"{mode}_overhead_lines"] + .loc[ + edisgo_obj.topology.equipment_data[f"{mode}_overhead_lines"].U_n + == 10.0 + ] + .loc[:, ["cost"]] + ) + cost_df = pd.concat([mv_cable_df, mv_overhead_lines]) + else: + cost_df = edisgo_obj.topology.equipment_data[f"{mode}_cables"].loc[ + :, ["cost"] + ] + + for equip_name2 in cost_df.index: + if equip_name1 == equip_name2: + cost = cost_df.loc[cost_df.index.isin([equip_name1])].cost[0] + costs_cable.append(cost) + return costs_cable + lines_df = edisgo_obj.topology.lines_df.loc[lines_names, ["length"]] mv_lines = lines_df[ lines_df.index.isin(edisgo_obj.topology.mv_grid.lines_df.index) @@ -280,22 +314,26 @@ def line_expansion_costs(edisgo_obj, lines_names): else: population_density = "urban" - costs_cable_mv = float(edisgo_obj.config["costs_cables"]["mv_cable"]) - costs_cable_lv = float(edisgo_obj.config["costs_cables"]["lv_cable"]) + equipment_df = edisgo_obj.results.equipment_changes + + costs_cable_mv = np.array(cost_cable_types("mv")) + costs_cable_lv = np.array(cost_cable_types("lv")) costs_cable_earthwork_mv = float( edisgo_obj.config["costs_cables"][ - "mv_cable_incl_earthwork_{}".format(population_density) + f"mv_cable_incl_earthwork_{population_density}" ] ) costs_cable_earthwork_lv = float( edisgo_obj.config["costs_cables"][ - "lv_cable_incl_earthwork_{}".format(population_density) + f"lv_cable_incl_earthwork_{population_density}" ] ) costs_lines = pd.DataFrame( { - "costs_earthworks": (costs_cable_earthwork_mv - costs_cable_mv) + "costs_earthworks": ( + [costs_cable_earthwork_mv] * len(costs_cable_mv) - costs_cable_mv + ) * lines_df.loc[mv_lines].length, "costs_cable": costs_cable_mv * lines_df.loc[mv_lines].length, "voltage_level": ["mv"] * len(mv_lines), @@ -308,7 +346,10 @@ def line_expansion_costs(edisgo_obj, lines_names): costs_lines, pd.DataFrame( { - "costs_earthworks": (costs_cable_earthwork_lv - costs_cable_lv) + "costs_earthworks": ( + [costs_cable_earthwork_lv] * len(costs_cable_lv) + - costs_cable_lv + ) * lines_df.loc[lv_lines].length, "costs_cable": costs_cable_lv * lines_df.loc[lv_lines].length, "voltage_level": ["lv"] * len(lv_lines), From dcb68fb3078aca237ed537f2179ca563fc8b7faf Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Tue, 24 Jan 2023 23:54:37 +0100 Subject: [PATCH 23/43] add a cost for CB installation work --- edisgo/config/config_grid_expansion_default.cfg | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/edisgo/config/config_grid_expansion_default.cfg b/edisgo/config/config_grid_expansion_default.cfg index 5f7fae44f..5fb3b23cf 100644 --- a/edisgo/config/config_grid_expansion_default.cfg +++ b/edisgo/config/config_grid_expansion_default.cfg @@ -110,3 +110,7 @@ mv_cable_incl_earthwork_urban = 140 # costs in kEUR, source: DENA Verteilnetzstudie lv = 10 mv = 1000 + +[costs_circuit_breakers] + +circuit_breaker_installation_work=10 From 337218416bd94888474e859f5b27feff158901f3 Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Tue, 24 Jan 2023 23:55:35 +0100 Subject: [PATCH 24/43] add type_info for transformer data --- .../equipment-parameters_LV_transformers.csv | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/edisgo/equipment/equipment-parameters_LV_transformers.csv b/edisgo/equipment/equipment-parameters_LV_transformers.csv index e570ac931..4219d8b30 100644 --- a/edisgo/equipment/equipment-parameters_LV_transformers.csv +++ b/edisgo/equipment/equipment-parameters_LV_transformers.csv @@ -1,9 +1,9 @@ -name,S_nom,u_kr,P_k +name,S_nom,u_kr,P_k,type_info, #,MVA,%,MW -100 kVA,0.1,4,0.00175 -160 kVA,0.16,4,0.00235 -250 kVA,0.25,4,0.00325 -400 kVA,0.4,4,0.0046 -630 kVA,0.63,4,0.0065 -800 kVA,0.8,6,0.0084 -1000 kVA,1.0,6,0.00105 +100 kVA,0.1,,4,0.00175,100 kVA 10/0.4 kV +160 kVA,0.16,4,0.00235,160 kVA 10/0.4 kV +250 kVA,0.25,4,0.00325,250 kVA 10/0.4 kV +400 kVA,0.4,4,0.0046,400 kVA 10/0.4 kV +630 kVA,0.63,4,0.0065,630 kVA 10/0.4 kV +800 kVA,0.8,6,0.0084,800 kVA 10/0.4 kV +1000 kVA,1.0,6,0.00105,1000 kVA 10/0.4 kV From 3103d63b46bcb7c6e11131901ca1aeefa3922b5f Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Wed, 25 Jan 2023 22:11:45 +0100 Subject: [PATCH 25/43] minor change in costs.py --- edisgo/flex_opt/costs.py | 49 +++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/edisgo/flex_opt/costs.py b/edisgo/flex_opt/costs.py index 0c546592b..c7c25f428 100644 --- a/edisgo/flex_opt/costs.py +++ b/edisgo/flex_opt/costs.py @@ -266,30 +266,35 @@ def line_expansion_costs(edisgo_obj, lines_names): def cost_cable_types(mode): # TODO: rewrite it with pd.merge or pd.concat + equipment_df = edisgo_obj.topology.lines_df[ + edisgo_obj.topology.lines_df.index.isin(lines_names) + ] costs_cable = [] - for equip_name1 in equipment_df.equipment: - if mode == "mv": - mv_cable_df = ( - edisgo_obj.topology.equipment_data[f"{mode}_cables"] - .loc[ - edisgo_obj.topology.equipment_data[f"{mode}_cables"].U_n == 10.0 - ] - .loc[:, ["cost"]] - ) - mv_overhead_lines = ( - edisgo_obj.topology.equipment_data[f"{mode}_overhead_lines"] - .loc[ - edisgo_obj.topology.equipment_data[f"{mode}_overhead_lines"].U_n - == 10.0 - ] - .loc[:, ["cost"]] - ) - cost_df = pd.concat([mv_cable_df, mv_overhead_lines]) - else: - cost_df = edisgo_obj.topology.equipment_data[f"{mode}_cables"].loc[ - :, ["cost"] + if mode == "mv": + voltage_mv_grid = edisgo_obj.topology.mv_grid.buses_df.v_nom[0] + mv_cable_df = ( + edisgo_obj.topology.equipment_data[f"{mode}_cables"] + .loc[ + edisgo_obj.topology.equipment_data[f"{mode}_cables"].U_n + == voltage_mv_grid + ] + .loc[:, ["cost"]] + ) + mv_overhead_lines = ( + edisgo_obj.topology.equipment_data[f"{mode}_overhead_lines"] + .loc[ + edisgo_obj.topology.equipment_data[f"{mode}_overhead_lines"].U_n + == voltage_mv_grid ] + .loc[:, ["cost"]] + ) + cost_df = pd.concat([mv_cable_df, mv_overhead_lines]) + else: + cost_df = edisgo_obj.topology.equipment_data[f"{mode}_cables"].loc[ + :, ["cost"] + ] + for equip_name1 in equipment_df.type_info: for equip_name2 in cost_df.index: if equip_name1 == equip_name2: cost = cost_df.loc[cost_df.index.isin([equip_name1])].cost[0] @@ -314,8 +319,6 @@ def cost_cable_types(mode): else: population_density = "urban" - equipment_df = edisgo_obj.results.equipment_changes - costs_cable_mv = np.array(cost_cable_types("mv")) costs_cable_lv = np.array(cost_cable_types("lv")) costs_cable_earthwork_mv = float( From df09453fc550caf2683e4de4cf7f6f1df67b8565 Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Wed, 25 Jan 2023 22:13:34 +0100 Subject: [PATCH 26/43] add reinforcement voltage issues methods --- edisgo/flex_opt/reinforce_measures.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index c0d59d366..a534af38b 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -1146,7 +1146,7 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): new_transformer_name[grid_id_ind] = str(grid.id) + "1001" transformer_s.s_nom = standard_transformer.S_nom - transformer_s.type_info = standard_transformer.name + transformer_s.type_info = standard_transformer.type_info transformer_s.r_pu = standard_transformer.r_pu transformer_s.x_pu = standard_transformer.x_pu transformer_s.name = "_".join([str(_) for _ in new_transformer_name]) @@ -1349,8 +1349,8 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): logger.info( f"{len(nodes_tb_relocated.keys())} feeders are removed from the grid " - f"{grid} and located in new grid{repr(grid) + str(1001)} by split " - f"feeder+add transformer method" + f"{grid} and located in new grid{repr(grid) + str(1001)} by method: " + f"add_station_at_half_length " ) if len(lines_changes) < 3: lines_changes = {} From eadccc7db062ac06162bab2d3a887a97db6a9aef Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Wed, 25 Jan 2023 22:18:20 +0100 Subject: [PATCH 27/43] Integration of voltage issue reinforcement methods All structure of reinforce_grid_alternative.py and methods are to be updated --- edisgo/flex_opt/reinforce_grid_alternative.py | 306 +++++++++++++++++- 1 file changed, 291 insertions(+), 15 deletions(-) diff --git a/edisgo/flex_opt/reinforce_grid_alternative.py b/edisgo/flex_opt/reinforce_grid_alternative.py index e8d660737..ee35a60dd 100644 --- a/edisgo/flex_opt/reinforce_grid_alternative.py +++ b/edisgo/flex_opt/reinforce_grid_alternative.py @@ -220,17 +220,16 @@ def _add_circuit_breaker_changes_to_equipment_changes(): # 1.1.1 Method:Split the feeder at the half-length of feeder (applied only once to # secure n-1). if (not mode or mode == "mv") and not crit_lines_mv.empty: - if "add_station_at_half_length" in add_method: - logger.warning( - "method:add_station_at_half_length is only applicable for LV grids" + if add_method == ["add_station_at_half_length"]: + logger.error( + "==>method:add_station_at_half_length is only applicable for LV grids" ) if "relocate_circuit_breaker" in add_method or add_method is None: # method-1: relocate_circuit_breaker logger.info( - "==> the method relocate circuit breaker location" - " " - "is running for MV grid {edisgo_reinforce.topology.mv_grid}: " + f"==> method:relocate circuit breaker location is running " + f"for MV grid {edisgo_reinforce.topology.mv_grid}: " ) circuit_breaker_changes = reinforce_measures.relocate_circuit_breaker( edisgo_reinforce, mode="loadgen" @@ -239,10 +238,10 @@ def _add_circuit_breaker_changes_to_equipment_changes(): logger.debug("==> Run power flow analysis.") edisgo_reinforce.analyze(timesteps=timesteps_pfa) - if "split_feeder_at_half_length" in add_method or add_method is None: + elif "split_feeder_at_half_length" in add_method or add_method is None: # method-2: split_feeder_at_half_length logger.info( - f"==>feeder splitting method is running for MV grid " + f"==>method:split_feeder_at_half_length is running for MV grid " f"{edisgo_reinforce.topology.mv_grid}: " ) lines_changes = reinforce_measures.split_feeder_at_half_length( @@ -255,21 +254,22 @@ def _add_circuit_breaker_changes_to_equipment_changes(): # 1.2- Voltage level= LV if (not mode or mode == "lv") and not crit_lines_lv.empty: - - if "relocate_circuit_breaker" in add_method: - logger.warning( - "method:relocate_circuit_breaker is only applicable for Mv grids" + while_counter = 1 + if add_method == ["relocate_circuit_breaker"]: + logger.error( + "==>method:relocate_circuit_breaker is only applicable for MV grids" ) # reset changes from MV grid transformer_changes = {} lines_changes = {} for lv_grid in list(edisgo_reinforce.topology.mv_grid.lv_grids): + if "add_station_at_half_length" in add_method or add_method is None: # 1.2.1 Method: Split the feeder at the half-length of feeder and add # new station( applied only once ) # if the number of overloaded lines is more than 2 logger.debug( - f"==>split+add substation method is running for LV grid {lv_grid}: " + f"==>method:add_station_at_half_length is running for {lv_grid}: " ) ( transformer_changes, @@ -285,7 +285,8 @@ def _add_circuit_breaker_changes_to_equipment_changes(): # 1.2.2 Method:Split the feeder at the half-length of feeder # (applied only once) logger.debug( - f"==>feeder splitting method is running for LV grid {lv_grid}: " + f"==>method:split_feeder_at_half_length is running for " + f"{lv_grid}: " ) lines_changes = reinforce_measures.split_feeder_at_half_length( edisgo_reinforce, lv_grid, crit_lines_lv @@ -315,7 +316,10 @@ def _add_circuit_breaker_changes_to_equipment_changes(): while_counter = 0 while not crit_lines.empty and while_counter < max_while_iterations: - logger.info(f"==>add parallel line method is running_Step{iteration_step}") + logger.info( + f"==>method:add_same_type_of_parallel_line is " + f"running_Step{iteration_step}" + ) lines_changes = reinforce_measures.add_same_type_of_parallel_line( edisgo_reinforce, crit_lines ) @@ -358,7 +362,279 @@ def _add_circuit_breaker_changes_to_equipment_changes(): logger.info( f"==> Load issues were solved in {while_counter} iteration step(s)." ) + + if not crit_lines.empty: + logger.warning("==> Not all of the overloading issues could be solved.") + edisgo_reinforce.results.grid_expansion_costs = grid_expansion_costs( edisgo_reinforce, without_generator_import=without_generator_import ) + + return edisgo_reinforce.results + + +def reinforce_lines_voltage_issues_alternative( + edisgo, + add_method=None, + timesteps_pfa=None, + copy_grid=False, + mode=None, + max_while_iterations=20, + combined_analysis=False, + without_generator_import=False, +): + def _add_lines_changes_to_equipment_changes(): + edisgo_reinforce.results.equipment_changes = pd.concat( + [ + edisgo_reinforce.results.equipment_changes, + pd.DataFrame( + { + "iteration_step": [iteration_step] * len(lines_changes), + "change": ["changed"] * len(lines_changes), + "equipment": edisgo_reinforce.topology.lines_df.loc[ + lines_changes.keys(), "type_info" + ].values, + "quantity": [_ for _ in lines_changes.values()], + }, + index=lines_changes.keys(), + ), + ], + ) + + def _add_transformer_changes_to_equipment_changes(mode: str | None): + df_list = [edisgo_reinforce.results.equipment_changes] + df_list.extend( + pd.DataFrame( + { + "iteration_step": [iteration_step] * len(transformer_list), + "change": [mode] * len(transformer_list), + "equipment": transformer_list, + "quantity": [1] * len(transformer_list), + }, + index=[station] * len(transformer_list), + ) + for station, transformer_list in transformer_changes[mode].items() + ) + + edisgo_reinforce.results.equipment_changes = pd.concat(df_list) + + # check if provided mode is valid + if mode and mode not in ["mv", "lv"]: + raise ValueError(f"Provided mode {mode} is not valid.") + # in case reinforcement needs to be conducted on a copied graph the + # edisgo object is deep copied + if copy_grid is True: + edisgo_reinforce = copy.deepcopy(edisgo) + else: + edisgo_reinforce = edisgo + + if timesteps_pfa is not None: + if isinstance(timesteps_pfa, str) and timesteps_pfa == "snapshot_analysis": + snapshots = tools.select_worstcase_snapshots(edisgo_reinforce) + # drop None values in case any of the two snapshots does not exist + timesteps_pfa = pd.DatetimeIndex( + data=[ + snapshots["max_residual_load"], + snapshots["min_residual_load"], + ] + ).dropna() + # if timesteps_pfa is not of type datetime or does not contain + # datetimes throw an error + elif not isinstance(timesteps_pfa, datetime.datetime): + if hasattr(timesteps_pfa, "__iter__"): + if not all(isinstance(_, datetime.datetime) for _ in timesteps_pfa): + raise ValueError( + f"Input {timesteps_pfa} for timesteps_pfa is not valid." + ) + else: + raise ValueError( + f"Input {timesteps_pfa} for timesteps_pfa is not valid." + ) + methods = [ + "split_feeder_at_2_3_length", + "add_substation_at_2_3_length", + ] + + if add_method is None: + add_method = methods + + if isinstance(add_method, str): + add_method = [add_method] + + if add_method and not any(method in methods for method in add_method): + # check if provided method is valid + raise ValueError(f"Provided method {add_method} is not valid.") + + iteration_step = 1 + # analyze_mode = None if mode == "lv" else mode + + edisgo_reinforce.analyze(timesteps=timesteps_pfa) + + # REINFORCE BRANCHES DUE TO VOLTAGE ISSUES + + # 1.1 Voltage level= MV + logger.debug("==> Check voltage in MV topology.") + voltage_levels_mv = "mv_lv" if combined_analysis else "mv" + + crit_nodes_mv = checks.mv_voltage_deviation( + edisgo_reinforce, voltage_levels=voltage_levels_mv + ) + + if (not mode or mode == "mv") and any(crit_nodes_mv): + # 1.1.1 Method:Split the feeder at the half-length of feeder (applied only once + # to secure n-1). + while_counter = 0 + while any(crit_nodes_mv) and while_counter < max_while_iterations: + if "add_substation_at_2_3_length" in add_method: + logger.warning( + "method:add_substation_at_2_3_length is only applicable for " + "LV grids" + ) + + if "split_feeder_at_2_3_length" in add_method: + logger.info( + f"==>method:split_feeder_at_2_3_length is running for " + f"{edisgo_reinforce.topology.mv_grid}: " + ) + + lines_changes = reinforce_measures.reinforce_lines_voltage_issues( + edisgo_reinforce, + edisgo_reinforce.topology.mv_grid, + crit_nodes_mv[repr(edisgo_reinforce.topology.mv_grid)], + ) + _add_lines_changes_to_equipment_changes() + + logger.debug("==> Run power flow analysis.") + edisgo_reinforce.analyze(timesteps=timesteps_pfa) + + logger.debug("==> Recheck voltage in MV grid.") + crit_nodes_mv = checks.mv_voltage_deviation( + edisgo_reinforce, voltage_levels=voltage_levels_mv + ) + + iteration_step += 1 + while_counter += 1 + + # check if all voltage problems were solved after maximum number of + # iterations allowed + if while_counter == max_while_iterations and any(crit_nodes_mv): + edisgo_reinforce.results.unresolved_issues = pd.concat( + [ + edisgo_reinforce.results.unresolved_issues, + pd.concat([_ for _ in crit_nodes_mv.values()]), + ] + ) + raise exceptions.MaximumIterationError( + "Over-voltage issues for the following nodes in MV grids " + f"could not be solved: {crit_nodes_mv}" + ) + else: + logger.info( + "==> Voltage issues in MV grids were solved " + f"in {while_counter} iteration step(s)." + ) + + # 1.2 Voltage level= LV + voltage_levels_lv = "mv_lv" if combined_analysis else "lv" + logger.debug("==> Check voltage in LV grids.") + + crit_nodes_lv = checks.lv_voltage_deviation( + edisgo_reinforce, voltage_levels=voltage_levels_lv + ) + if (not mode or mode == "lv") and any(crit_nodes_lv): + + # 1.1.1 Method:Split the feeder at the half-length of feeder and add new station + # ( applied only once ) if the number of overloaded lines is more than 2 + # reset changes from MV grid + + while_counter = 0 + while any(crit_nodes_lv) and while_counter < max_while_iterations: + + transformer_changes = {} + lines_changes = {} + for lv_grid in crit_nodes_lv: + if ( + "add_substation_at_2_3_length" in add_method and while_counter == 0 + ) or (add_method is None and while_counter == 0): + # 1.2.1 Method: Split the feeder at the half-length of feeder and + # add new station( applied only once ) + # if the number of overloaded lines is more than 2 + + logger.debug( + f"==>method:add_substation_at_2_3_length method is running for " + f"{lv_grid}: " + ) + ( + transformer_changes, + lines_changes, + ) = reinforce_measures.add_substation_at_2_3_length( + edisgo_reinforce, + edisgo_reinforce.topology.get_lv_grid(lv_grid), + crit_nodes_lv[lv_grid], + ) + + if transformer_changes and lines_changes: + _add_transformer_changes_to_equipment_changes("added") + _add_lines_changes_to_equipment_changes() + + if ( + "split_feeder_at_2_3_length" in add_method + and not any(transformer_changes) + ) or (add_method is None and not any(transformer_changes)): + # 1.2.2 Method:split_feeder_at_2/3-length of feeder + # (applied only once) + logger.debug( + f"==>method:split_feeder_at_2_3_length is running for " + f"{lv_grid}: " + ) + + lines_changes = reinforce_measures.reinforce_lines_voltage_issues( + edisgo_reinforce, + edisgo_reinforce.topology.get_lv_grid(lv_grid), + crit_nodes_lv[lv_grid], + ) + # write changed lines to results.equipment_changes + _add_lines_changes_to_equipment_changes() + + # run power flow analysis again (after updating pypsa object) + # and check if all over-voltage problems were solved + logger.debug("==> Run power flow analysis.") + edisgo_reinforce.analyze(timesteps=timesteps_pfa) + + logger.debug("==> Recheck voltage in LV grids.") + crit_nodes_lv = checks.lv_voltage_deviation( + edisgo_reinforce, voltage_levels=voltage_levels_lv + ) + + iteration_step += 1 + while_counter += 1 + + # run power flow analysis again (after updating pypsa object) + # and check if all over-voltage problems were solved + logger.debug("==> Run power flow analysis.") + edisgo_reinforce.analyze(timesteps=timesteps_pfa) + + # check if all voltage problems were solved after maximum number of + # iterations allowed + if while_counter == max_while_iterations and any(crit_nodes_lv): + edisgo_reinforce.results.unresolved_issues = pd.concat( + [ + edisgo_reinforce.results.unresolved_issues, + pd.concat([_ for _ in crit_nodes_lv.values()]), + ] + ) + raise exceptions.MaximumIterationError( + "Over-voltage issues for the following nodes in LV grids " + f"could not be solved: {crit_nodes_lv}" + ) + else: + logger.info( + "==> Voltage issues in LV grids were solved " + f"in {while_counter} iteration step(s)." + ) + + logger.debug("==> Run power flow analysis.") + + edisgo_reinforce.analyze(timesteps=timesteps_pfa) + return edisgo_reinforce.results From fe58bf80941e4f51b4035ea99acf7316b1b1b372 Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Thu, 26 Jan 2023 00:06:00 +0100 Subject: [PATCH 28/43] Minor fix --- edisgo/flex_opt/reinforce_grid_alternative.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/edisgo/flex_opt/reinforce_grid_alternative.py b/edisgo/flex_opt/reinforce_grid_alternative.py index ee35a60dd..c06e7d90e 100644 --- a/edisgo/flex_opt/reinforce_grid_alternative.py +++ b/edisgo/flex_opt/reinforce_grid_alternative.py @@ -238,7 +238,7 @@ def _add_circuit_breaker_changes_to_equipment_changes(): logger.debug("==> Run power flow analysis.") edisgo_reinforce.analyze(timesteps=timesteps_pfa) - elif "split_feeder_at_half_length" in add_method or add_method is None: + if "split_feeder_at_half_length" in add_method or add_method is None: # method-2: split_feeder_at_half_length logger.info( f"==>method:split_feeder_at_half_length is running for MV grid " @@ -315,10 +315,14 @@ def _add_circuit_breaker_changes_to_equipment_changes(): # Method: Add same type of parallel line while_counter = 0 while not crit_lines.empty and while_counter < max_while_iterations: + if mode is None: + grid_level = "MV and LV " + else: + grid_level = mode logger.info( f"==>method:add_same_type_of_parallel_line is " - f"running_Step{iteration_step}" + f"running for {grid_level} grid/s_Step{iteration_step}" ) lines_changes = reinforce_measures.add_same_type_of_parallel_line( edisgo_reinforce, crit_lines From 785dda09d7c8cb9dc2397480ea7e0d535250fe7f Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Thu, 26 Jan 2023 09:37:35 +0100 Subject: [PATCH 29/43] Docstring fix --- edisgo/flex_opt/reinforce_grid_alternative.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/edisgo/flex_opt/reinforce_grid_alternative.py b/edisgo/flex_opt/reinforce_grid_alternative.py index c06e7d90e..152b9247b 100644 --- a/edisgo/flex_opt/reinforce_grid_alternative.py +++ b/edisgo/flex_opt/reinforce_grid_alternative.py @@ -27,9 +27,12 @@ def reinforce_line_overloading_alternative( Evaluates network reinforcement needs and performs measures. This function is the parent function for all network reinforcements. + todo: docstring is to be updated MV Grid Reinforcement: - 1- - 2- + 1- Circuit Breakers are relocated based on the least load/gen difference between + the feeders + 2- Split method is implemented into the grids which are not reinforced by split+add + station method LV Grid Reinforcement: 1- Split+add station method is implemented into all the lv grids if there are more @@ -44,6 +47,13 @@ def reinforce_line_overloading_alternative( ---------- edisgo: class:`~.EDisGo` The eDisGo API object + add_method: The following methods can be used: + [ + "relocate_circuit_breaker", + "add_station_at_half_length", + "split_feeder_at_half_length", + "add_same_type_of_parallel_line", + ] timesteps_pfa: str or \ :pandas:`pandas.DatetimeIndex` or \ :pandas:`pandas.Timestamp` @@ -79,7 +89,7 @@ def reinforce_line_overloading_alternative( and LV network topology. LV load and generation is aggregated per LV network and directly connected to the primary side of the respective MV/LV station. - * 'lv' to reinforce LV networks including MV/LV stations. + * 'lv' to reinforce LV networks. max_while_iterations : int Maximum number of times each while loop is conducted. without_generator_import: bool @@ -311,7 +321,7 @@ def _add_circuit_breaker_changes_to_equipment_changes(): ] ) if "add_same_type_of_parallel_line" in add_method or add_method is None: - # 2- Remanining crit_lines- Voltage level MV and LV + # 2- Remaining crit_lines- Voltage level MV and LV # Method: Add same type of parallel line while_counter = 0 while not crit_lines.empty and while_counter < max_while_iterations: From 81c591c69a16e08d62ab717e9d23663de70f140d Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Sat, 28 Jan 2023 15:58:45 +0100 Subject: [PATCH 30/43] minor fix for the make pseudo coordinates function --- edisgo/network/topology.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edisgo/network/topology.py b/edisgo/network/topology.py index 87455a174..df8720688 100755 --- a/edisgo/network/topology.py +++ b/edisgo/network/topology.py @@ -1474,8 +1474,8 @@ def add_bus(self, bus_name, v_nom, **kwargs): ) self.buses_df = pd.concat( [ - self.buses_df, new_bus_df, + self.buses_df, ] ) return bus_name From b8946fdc2c9829fba912507878e000012d3e9a5b Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Sat, 28 Jan 2023 22:54:45 +0100 Subject: [PATCH 31/43] add load factor to plotly Update: The graph shows the loading after the change of load factor --- edisgo/tools/plots.py | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/edisgo/tools/plots.py b/edisgo/tools/plots.py index 07c040422..8513dcbd0 100644 --- a/edisgo/tools/plots.py +++ b/edisgo/tools/plots.py @@ -1112,6 +1112,36 @@ def get_coordinates_for_edge(edge): x_root, y_root = transformer_4326_to_3035.transform(x_root, y_root) + def get_load_factor(s_res, branch_name): + + s_res_branch = edisgo_obj.results.s_res.loc[selected_timesteps, s_res_view].T[ + edisgo_obj.results.s_res.loc[selected_timesteps, s_res_view].T.index.isin( + [branch_name] + ) + ] + s_res_time_index = s_res_branch.apply( + lambda row: row[row == s_res_branch.max(axis=1)[0]].index, axis=1 + ).item()[0] + + if str(s_res_time_index) == "1970-01-01 00:00:00": + load_factor = edisgo_obj.config["grid_expansion_load_factors"][ + "mv_load_case_line" + ] + elif str(s_res_time_index) == "1970-01-01 01:00:00": + load_factor = edisgo_obj.config["grid_expansion_load_factors"][ + "lv_load_case_line" + ] + elif str(s_res_time_index) == "1970-01-01 02:00:00": + load_factor = edisgo_obj.config["grid_expansion_load_factors"][ + "mv_feed-in_case_line" + ] + elif str(s_res_time_index) == "1970-01-01 03:00:00": + load_factor = edisgo_obj.config["grid_expansion_load_factors"][ + "lv_feed-in_case_line" + ] + + return load_factor + def plot_line_text(): middle_node_x = [] middle_node_y = [] @@ -1126,7 +1156,11 @@ def plot_line_text(): text = str(branch_name) if power_flow_results: - text += "
" + "Loading = " + str(s_res.loc[branch_name]) + + load_factor = get_load_factor(s_res, branch_name) + text += ( + "
" + "Loading = " + str(s_res.loc[branch_name] / load_factor) + ) line_parameters = edisgo_obj.topology.lines_df.loc[branch_name, :] for index, value in line_parameters.iteritems(): @@ -1208,7 +1242,8 @@ def plot_lines(): color = "black" elif line_color == "loading": - loading = s_res.loc[branch_name] + load_factor = get_load_factor(s_res, branch_name) + loading = s_res.loc[branch_name] / load_factor color = color_map_color( loading, vmin=color_min, @@ -1217,7 +1252,8 @@ def plot_lines(): ) elif line_color == "relative_loading": - loading = s_res.loc[branch_name] + load_factor = get_load_factor(s_res, branch_name) + loading = s_res.loc[branch_name] / load_factor s_nom = edisgo_obj.topology.lines_df.s_nom.loc[branch_name] color = color_map_color( loading / s_nom * 0.9, From 3ded3d041562bca528be5a5b0ffcc46269a2c1f8 Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Sat, 28 Jan 2023 22:56:06 +0100 Subject: [PATCH 32/43] Docstring update --- edisgo/flex_opt/costs.py | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/edisgo/flex_opt/costs.py b/edisgo/flex_opt/costs.py index c7c25f428..274269ce8 100644 --- a/edisgo/flex_opt/costs.py +++ b/edisgo/flex_opt/costs.py @@ -265,6 +265,16 @@ def line_expansion_costs(edisgo_obj, lines_names): """ def cost_cable_types(mode): + """ + + Parameters + ---------- + mode: mv or lv + + Returns + ------- + The cost of each line type + """ # TODO: rewrite it with pd.merge or pd.concat equipment_df = edisgo_obj.topology.lines_df[ edisgo_obj.topology.lines_df.index.isin(lines_names) @@ -364,14 +374,32 @@ def cost_cable_types(mode): return costs_lines.loc[lines_df.index] -def cost_breakdown(edisgo_obj, lines): - # costs for lines +def cost_breakdown(edisgo_obj, lines_df): + """ + + Parameters + ---------- + edisgo_obj: class:`~.edisgo.EDisGo` + eDisGo object of which lines of lines_df are part + lines: pandas.core.frame.DataFrame + the changed lines + + Returns + ------- + `pandas.DataFrame` + + Example + costs_earthworks costs_cable voltage_level costs + Line name 12.3840 2.0160 lv 14.40 + + """ + # cost-breakdown of changed lines # get changed lines - lines_added = lines.iloc[ + lines_added = lines_df.iloc[ ( - lines.equipment - == edisgo_obj.topology.lines_df.loc[lines.index, "type_info"] + lines_df.equipment + == edisgo_obj.topology.lines_df.loc[lines_df.index, "type_info"] ).values ]["quantity"].to_frame() lines_added_unique = lines_added.index.unique() From 01fe2171d5297fca0e4ba8fe081e7c0e619e0c4c Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Sun, 29 Jan 2023 20:14:55 +0100 Subject: [PATCH 33/43] Minor Corrections_reinforce --- edisgo/flex_opt/reinforce_grid_alternative.py | 57 ++++++++++--------- edisgo/flex_opt/reinforce_measures.py | 29 ++++++---- 2 files changed, 48 insertions(+), 38 deletions(-) diff --git a/edisgo/flex_opt/reinforce_grid_alternative.py b/edisgo/flex_opt/reinforce_grid_alternative.py index 152b9247b..46108633f 100644 --- a/edisgo/flex_opt/reinforce_grid_alternative.py +++ b/edisgo/flex_opt/reinforce_grid_alternative.py @@ -24,25 +24,25 @@ def reinforce_line_overloading_alternative( without_generator_import=False, ): """ + todo: docstring is to be updated Evaluates network reinforcement needs and performs measures. This function is the parent function for all network reinforcements. - todo: docstring is to be updated + MV Grid Reinforcement: - 1- Circuit Breakers are relocated based on the least load/gen difference between - the feeders - 2- Split method is implemented into the grids which are not reinforced by split+add - station method + After circuit breakers are relocated based on the least load/gen difference + between the feeders, the feeder is split at the half-length and connected + to the new mv/lv station - LV Grid Reinforcement: - 1- Split+add station method is implemented into all the lv grids if there are more - than 3 overloaded lines in the grid. - 2- Split method is implemented into the grids which are not reinforced by split+add - station method + LV Grid Reinforcement + If the number of overloaded lines are more than 2 in the grid, the feeder is + split at the half-length and connected to the new mv/lv station. Otherwise, + the feeder is split at the half-length and connected to the HV/MV station. MV_LV Grid Reinforcement - 1- The remaining overloaded lines are reinforced by add same type of parallel line + The remaining overloaded lines are reinforced by add same type of parallel line method + Parameters ---------- edisgo: class:`~.EDisGo` @@ -106,8 +106,7 @@ def reinforce_line_overloading_alternative( Assumptions ------ 1-The removing cost of cables are not incorporated. - 2-One type of line cost is used for mv and lv - 3-Line Reinforcements are done with the same type of lines as lines reinforced + 2-Line Reinforcements are done with the same type of lines as lines reinforced """ @@ -175,6 +174,8 @@ def _add_circuit_breaker_changes_to_equipment_changes(): edisgo_reinforce = copy.deepcopy(edisgo) else: edisgo_reinforce = edisgo + # todo:activate + # edisgo_reinforce = remove_short_lines(remove_1m_end_lines(edisgo_reinforce)) if timesteps_pfa is not None: if isinstance(timesteps_pfa, str) and timesteps_pfa == "snapshot_analysis": @@ -230,7 +231,10 @@ def _add_circuit_breaker_changes_to_equipment_changes(): # 1.1.1 Method:Split the feeder at the half-length of feeder (applied only once to # secure n-1). if (not mode or mode == "mv") and not crit_lines_mv.empty: - if add_method == ["add_station_at_half_length"]: + if ( + add_method == ["add_station_at_half_length"] + or "add_station_at_half_length" in add_method + ): logger.error( "==>method:add_station_at_half_length is only applicable for LV grids" ) @@ -242,18 +246,16 @@ def _add_circuit_breaker_changes_to_equipment_changes(): f"for MV grid {edisgo_reinforce.topology.mv_grid}: " ) circuit_breaker_changes = reinforce_measures.relocate_circuit_breaker( - edisgo_reinforce, mode="loadgen" + edisgo_reinforce, mode="load" ) _add_circuit_breaker_changes_to_equipment_changes() logger.debug("==> Run power flow analysis.") edisgo_reinforce.analyze(timesteps=timesteps_pfa) + crit_lines_mv = checks.mv_line_load(edisgo_reinforce) if "split_feeder_at_half_length" in add_method or add_method is None: # method-2: split_feeder_at_half_length - logger.info( - f"==>method:split_feeder_at_half_length is running for MV grid " - f"{edisgo_reinforce.topology.mv_grid}: " - ) + lines_changes = reinforce_measures.split_feeder_at_half_length( edisgo_reinforce, edisgo_reinforce.topology.mv_grid, crit_lines_mv ) @@ -265,7 +267,10 @@ def _add_circuit_breaker_changes_to_equipment_changes(): # 1.2- Voltage level= LV if (not mode or mode == "lv") and not crit_lines_lv.empty: while_counter = 1 - if add_method == ["relocate_circuit_breaker"]: + if ( + add_method == ["relocate_circuit_breaker"] + or "relocate_circuit_breaker" in add_method + ): logger.error( "==>method:relocate_circuit_breaker is only applicable for MV grids" ) @@ -278,15 +283,14 @@ def _add_circuit_breaker_changes_to_equipment_changes(): # 1.2.1 Method: Split the feeder at the half-length of feeder and add # new station( applied only once ) # if the number of overloaded lines is more than 2 - logger.debug( - f"==>method:add_station_at_half_length is running for {lv_grid}: " - ) + ( transformer_changes, lines_changes, ) = reinforce_measures.add_station_at_half_length( edisgo_reinforce, lv_grid, crit_lines_lv ) + if transformer_changes and lines_changes: _add_transformer_changes_to_equipment_changes("added") _add_lines_changes_to_equipment_changes() @@ -294,10 +298,7 @@ def _add_circuit_breaker_changes_to_equipment_changes(): if "split_feeder_at_half_length" in add_method or add_method is None: # 1.2.2 Method:Split the feeder at the half-length of feeder # (applied only once) - logger.debug( - f"==>method:split_feeder_at_half_length is running for " - f"{lv_grid}: " - ) + lines_changes = reinforce_measures.split_feeder_at_half_length( edisgo_reinforce, lv_grid, crit_lines_lv ) @@ -378,7 +379,7 @@ def _add_circuit_breaker_changes_to_equipment_changes(): ) if not crit_lines.empty: - logger.warning("==> Not all of the overloading issues could be solved.") + logger.warning("==> Not all overloading issues could be solved.") edisgo_reinforce.results.grid_expansion_costs = grid_expansion_costs( edisgo_reinforce, without_generator_import=without_generator_import diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index a534af38b..b63d9d918 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -948,9 +948,13 @@ def split_feeder_at_half_length(edisgo_obj, grid, crit_lines): break # if MVGrid: check if node_1_2 is LV station and if not find - # next or preceding LV station + # next or preceding LV station. If there is no LV station, do not split the + # feeder else: - while node_1_2 not in edisgo_obj.topology.transformers_df.bus0.values: + while ( + node_1_2 not in nodes_feeder.keys() + and node_1_2 not in edisgo_obj.topology.transformers_df.bus0.values + ): try: node_1_2 = path[path.index(node_1_2) + 1] except IndexError: @@ -964,11 +968,15 @@ def split_feeder_at_half_length(edisgo_obj, grid, crit_lines): f" {feeder_first_line} and following lines could not " f"be reinforced due to the lack of LV station . " ) + node_1_2 = str() break # if node_1_2 is a representative (meaning it is already directly connected # to the station), line cannot be disconnected and reinforced - if node_1_2 not in nodes_feeder.keys(): + if node_1_2 not in nodes_feeder.keys() and not len(node_1_2) == 0: + logger.info( + f"==>method:split_feeder_at_half_length is running for " f"{grid}: " + ) # get line between node_1_2 and predecessor node pred_node = path[path.index(node_1_2) - 1] line_removed = G.get_edge_data(node_1_2, pred_node)["branch_name"] @@ -1006,8 +1014,8 @@ def split_feeder_at_half_length(edisgo_obj, grid, crit_lines): lines_changes[line_added] = 1 if lines_changes: logger.info( - f"{len(lines_changes)} line/s are reinforced by split feeder at half-length" - f"method in {grid}" + f"{len(lines_changes)} line/s are reinforced by method: split feeder " + f"at half-length method in {grid}" ) return lines_changes @@ -1248,6 +1256,7 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): # removed from exiting LV grid and converted to an MV line between new # and existing MV/LV station if len(nodes_tb_relocated) > 2 and loop_counter == 0: + logger.info(f"==>method:add_station_at_half_length is running for {grid}: ") # Create the bus-bar name of primary and secondary side of new MV/LV station lv_bus_new = create_bus_name(station_node, "lv") mv_bus_new = create_bus_name(station_node, "mv") @@ -1273,10 +1282,11 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): # same with the distance between pred. node of node_1_2 of one of first # feeders to be split in LV grid - length = ( + length_lv = ( path_length_dict_tmp[node_1_2] - path_length_dict_tmp[path[path.index(node_1_2) - 1]] ) + length_mv = path_length_dict_tmp[node_1_2] # if the transformer already added, do not add bus and transformer once more if not transformers_changes: @@ -1288,7 +1298,7 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): edisgo_obj.topology.add_bus( lv_bus_new, v_nom_lv, - x=x_bus + length / 1000, + x=x_bus + length_lv / 1000, y=y_bus, lv_grid_id=lv_grid_id_new, in_building=building_bus, @@ -1297,7 +1307,7 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): edisgo_obj.topology.add_bus( mv_bus_new, v_nom_mv, - x=x_bus + length / 1000, + x=x_bus + length_mv / 1000, y=y_bus, in_building=building_bus, ) @@ -1319,7 +1329,7 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): line_added_mv = edisgo_obj.topology.add_line( bus0=grid.transformers_df.bus0[0], bus1=mv_bus_new, - length=length, + length=length_mv, type_info=standard_line, kind="cable", ) @@ -1697,7 +1707,6 @@ def _change_dataframe(cb_new_closed, cb_old_df): # split route and calc demand difference route_data_part1 = sum(node_peak_data[0:ctr]) route_data_part2 = sum(node_peak_data[ctr : len(node_peak_data)]) - # equality has to be respected, otherwise comparison stops when # demand/generation=0 diff = abs(route_data_part1 - route_data_part2) From 493ec8cada33003a9c7e854cc7ba8dd52f6e6113 Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Wed, 8 Feb 2023 00:09:31 +0100 Subject: [PATCH 34/43] Bug fix and changes in voltage issue reinforcement methods --- edisgo/flex_opt/reinforce_grid_alternative.py | 87 +++--- edisgo/flex_opt/reinforce_measures.py | 249 ++++++++++-------- 2 files changed, 179 insertions(+), 157 deletions(-) diff --git a/edisgo/flex_opt/reinforce_grid_alternative.py b/edisgo/flex_opt/reinforce_grid_alternative.py index 46108633f..2479bd02d 100644 --- a/edisgo/flex_opt/reinforce_grid_alternative.py +++ b/edisgo/flex_opt/reinforce_grid_alternative.py @@ -19,7 +19,9 @@ def reinforce_line_overloading_alternative( add_method=None, timesteps_pfa=None, copy_grid=False, - mode=None, + grid_mode=None, + loading_mode="load", + split_mode="back", max_while_iterations=20, without_generator_import=False, ): @@ -45,6 +47,7 @@ def reinforce_line_overloading_alternative( Parameters ---------- + edisgo: class:`~.EDisGo` The eDisGo API object add_method: The following methods can be used: @@ -82,7 +85,7 @@ def reinforce_line_overloading_alternative( Use this option to explicitly choose which time steps to consider. copy_grid:If True reinforcement is conducted on a copied grid and discarded. Default: False. - mode : str + grid_mode : str Determines network levels reinforcement is conducted for. Specify * None to reinforce MV and LV network levels. None is the default. * 'mv' to reinforce MV network level only, neglecting MV/LV stations, @@ -96,7 +99,12 @@ def reinforce_line_overloading_alternative( If True excludes lines that were added in the generator import to connect new generators to the topology from calculation of topology expansion costs. Default: False. - + loading_mode: + Type of loading. + 1-'load' + 2-'loadgen' + 3-'gen' + Default: 'load'. Returns ------- :class:`~.network.network.Results` @@ -166,15 +174,15 @@ def _add_circuit_breaker_changes_to_equipment_changes(): ) # check if provided mode is valid - if mode and mode not in ["mv", "lv"]: - raise ValueError(f"Provided mode {mode} is not valid.") + if grid_mode and grid_mode not in ["mv", "lv"]: + raise ValueError(f"Provided mode {grid_mode} is not valid.") # in case reinforcement needs to be conducted on a copied graph the # edisgo object is deep copied if copy_grid is True: edisgo_reinforce = copy.deepcopy(edisgo) else: edisgo_reinforce = edisgo - # todo:activate + # edisgo_reinforce = remove_short_lines(remove_1m_end_lines(edisgo_reinforce)) if timesteps_pfa is not None: @@ -230,7 +238,7 @@ def _add_circuit_breaker_changes_to_equipment_changes(): # 1.1 Voltage level= MV # 1.1.1 Method:Split the feeder at the half-length of feeder (applied only once to # secure n-1). - if (not mode or mode == "mv") and not crit_lines_mv.empty: + if (not grid_mode or grid_mode == "mv") and not crit_lines_mv.empty: if ( add_method == ["add_station_at_half_length"] or "add_station_at_half_length" in add_method @@ -246,7 +254,7 @@ def _add_circuit_breaker_changes_to_equipment_changes(): f"for MV grid {edisgo_reinforce.topology.mv_grid}: " ) circuit_breaker_changes = reinforce_measures.relocate_circuit_breaker( - edisgo_reinforce, mode="load" + edisgo_reinforce, mode=loading_mode ) _add_circuit_breaker_changes_to_equipment_changes() logger.debug("==> Run power flow analysis.") @@ -257,7 +265,10 @@ def _add_circuit_breaker_changes_to_equipment_changes(): # method-2: split_feeder_at_half_length lines_changes = reinforce_measures.split_feeder_at_half_length( - edisgo_reinforce, edisgo_reinforce.topology.mv_grid, crit_lines_mv + edisgo_reinforce, + edisgo_reinforce.topology.mv_grid, + crit_lines_mv, + split_mode=split_mode, ) _add_lines_changes_to_equipment_changes() @@ -265,7 +276,7 @@ def _add_circuit_breaker_changes_to_equipment_changes(): edisgo_reinforce.analyze(timesteps=timesteps_pfa) # 1.2- Voltage level= LV - if (not mode or mode == "lv") and not crit_lines_lv.empty: + if (not grid_mode or grid_mode == "lv") and not crit_lines_lv.empty: while_counter = 1 if ( add_method == ["relocate_circuit_breaker"] @@ -310,11 +321,11 @@ def _add_circuit_breaker_changes_to_equipment_changes(): logger.debug("==> Recheck line load.") crit_lines = ( pd.DataFrame(dtype=float) - if mode == "lv" + if grid_mode == "lv" else checks.mv_line_load(edisgo_reinforce) ) - if not mode or mode == "lv": + if not grid_mode or grid_mode == "lv": crit_lines = pd.concat( [ crit_lines, @@ -326,10 +337,10 @@ def _add_circuit_breaker_changes_to_equipment_changes(): # Method: Add same type of parallel line while_counter = 0 while not crit_lines.empty and while_counter < max_while_iterations: - if mode is None: + if grid_mode is None: grid_level = "MV and LV " else: - grid_level = mode + grid_level = grid_mode logger.info( f"==>method:add_same_type_of_parallel_line is " @@ -347,11 +358,11 @@ def _add_circuit_breaker_changes_to_equipment_changes(): logger.debug("==> Recheck line load.") crit_lines = ( pd.DataFrame(dtype=float) - if mode == "lv" + if grid_mode == "lv" else checks.mv_line_load(edisgo_reinforce) ) - if not mode or mode == "lv": + if not grid_mode or grid_mode == "lv": crit_lines = pd.concat( [ crit_lines, @@ -392,8 +403,9 @@ def reinforce_lines_voltage_issues_alternative( edisgo, add_method=None, timesteps_pfa=None, + split_mode="forward", copy_grid=False, - mode=None, + grid_mode=None, max_while_iterations=20, combined_analysis=False, without_generator_import=False, @@ -434,8 +446,8 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): edisgo_reinforce.results.equipment_changes = pd.concat(df_list) # check if provided mode is valid - if mode and mode not in ["mv", "lv"]: - raise ValueError(f"Provided mode {mode} is not valid.") + if grid_mode and grid_mode not in ["mv", "lv"]: + raise ValueError(f"Provided mode {grid_mode} is not valid.") # in case reinforcement needs to be conducted on a copied graph the # edisgo object is deep copied if copy_grid is True: @@ -495,7 +507,7 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): edisgo_reinforce, voltage_levels=voltage_levels_mv ) - if (not mode or mode == "mv") and any(crit_nodes_mv): + if (not grid_mode or grid_mode == "mv") and any(crit_nodes_mv): # 1.1.1 Method:Split the feeder at the half-length of feeder (applied only once # to secure n-1). while_counter = 0 @@ -512,10 +524,11 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): f"{edisgo_reinforce.topology.mv_grid}: " ) - lines_changes = reinforce_measures.reinforce_lines_voltage_issues( + lines_changes = reinforce_measures.split_feeder_at_2_3_length( edisgo_reinforce, edisgo_reinforce.topology.mv_grid, crit_nodes_mv[repr(edisgo_reinforce.topology.mv_grid)], + split_mode=split_mode, ) _add_lines_changes_to_equipment_changes() @@ -556,7 +569,7 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): crit_nodes_lv = checks.lv_voltage_deviation( edisgo_reinforce, voltage_levels=voltage_levels_lv ) - if (not mode or mode == "lv") and any(crit_nodes_lv): + if (not grid_mode or grid_mode == "lv") and any(crit_nodes_lv): # 1.1.1 Method:Split the feeder at the half-length of feeder and add new station # ( applied only once ) if the number of overloaded lines is more than 2 @@ -569,8 +582,8 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): lines_changes = {} for lv_grid in crit_nodes_lv: if ( - "add_substation_at_2_3_length" in add_method and while_counter == 0 - ) or (add_method is None and while_counter == 0): + "add_substation_at_2_3_length" in add_method and while_counter < 1 + ) or (add_method is None and while_counter < 1): # 1.2.1 Method: Split the feeder at the half-length of feeder and # add new station( applied only once ) # if the number of overloaded lines is more than 2 @@ -624,32 +637,14 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): iteration_step += 1 while_counter += 1 - # run power flow analysis again (after updating pypsa object) - # and check if all over-voltage problems were solved logger.debug("==> Run power flow analysis.") edisgo_reinforce.analyze(timesteps=timesteps_pfa) - # check if all voltage problems were solved after maximum number of - # iterations allowed - if while_counter == max_while_iterations and any(crit_nodes_lv): - edisgo_reinforce.results.unresolved_issues = pd.concat( - [ - edisgo_reinforce.results.unresolved_issues, - pd.concat([_ for _ in crit_nodes_lv.values()]), - ] - ) - raise exceptions.MaximumIterationError( - "Over-voltage issues for the following nodes in LV grids " - f"could not be solved: {crit_nodes_lv}" - ) - else: + if any(crit_nodes_lv) and "add_substation_at_2_3_length" in add_method: logger.info( - "==> Voltage issues in LV grids were solved " - f"in {while_counter} iteration step(s)." + "==> Voltage issues in LV grids could not solve in {while_counter} " + "iteration step(s) since only method :add_substation_at_2_3_length is " + "applied " ) - logger.debug("==> Run power flow analysis.") - - edisgo_reinforce.analyze(timesteps=timesteps_pfa) - return edisgo_reinforce.results diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index b63d9d918..958ab87a4 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -477,7 +477,7 @@ def reinforce_lines_voltage_issues(edisgo_obj, grid, crit_nodes): # 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] + crit_line = grid.lines_df.loc[crit_line_name].to_frame().T # if critical line is already a standard line install one # more parallel line @@ -798,7 +798,7 @@ def add_same_type_of_parallel_line(edisgo_obj, crit_lines): return lines_changes -def split_feeder_at_half_length(edisgo_obj, grid, crit_lines): +def split_feeder_at_half_length(edisgo_obj, grid, crit_lines, split_mode="back"): """ The critical string load in MV and LV grid is remedied by splitting the feeder at the half-length @@ -826,7 +826,12 @@ def split_feeder_at_half_length(edisgo_obj, grid, crit_lines): time-step the over-loading occurred in as :pandas:`pandas.Timestamp`, and 'voltage_level' specifying the voltage level the line is in (either 'mv' or 'lv'). - + split_mode: it determines the pathway to be searched for MV/LV station when the + node_1_2 comes after the half-length of feeder is not a MV/LV station. + *None: search for MV/LV station in all the nodes in the path (first back then + forward) + *back: search for MV/LV station in preceding nodes of node_1_2 in the path + *forward: search for MV/LV station in latter nodes of node_1_2 in the path Returns ------- @@ -847,17 +852,7 @@ def split_feeder_at_half_length(edisgo_obj, grid, crit_lines): relevant_lines = edisgo_obj.topology.lines_df.loc[ crit_lines[crit_lines.voltage_level == voltage_level].index ] - """ - # TODO:to be deleted after decision - if not relevant_lines.empty: - nominal_voltage = edisgo_obj.topology.buses_df.loc[ - edisgo_obj.topology.lines_df.loc[relevant_lines.index[0], "bus0"], - "v_nom", - ] - standard_line_type = edisgo_obj.config["grid_expansion_standard_equipment"][ - "lv_line" - ] - """ + elif isinstance(grid, MVGrid): voltage_level = "mv" @@ -866,16 +861,6 @@ def split_feeder_at_half_length(edisgo_obj, grid, crit_lines): crit_lines[crit_lines.voltage_level == voltage_level].index ] # TODO:to be deleted after decision - """ - if not relevant_lines.empty: - nominal_voltage = edisgo_obj.topology.buses_df.loc[ - edisgo_obj.topology.lines_df.loc[relevant_lines.index[0], "bus0"], - "v_nom", - ] - standard_line_type = edisgo_obj.config["grid_expansion_standard_equipment"][ - "lv_line" - ] - """ else: raise ValueError(f"Grid Type {type(grid)} is not supported.") @@ -886,18 +871,17 @@ def split_feeder_at_half_length(edisgo_obj, grid, crit_lines): # The most overloaded lines, generally first lines connected to the main station crit_lines_feeder = relevant_lines[relevant_lines["bus0"] == station_node] - # the last node of each feeder of the ring networks (switches are open) - switch_df = edisgo_obj.topology.switches_df.loc[:, "bus_closed":"bus_open"].values - switches = [node for last_nodes in switch_df for node in last_nodes] - if isinstance(grid, LVGrid): - nodes = G + nodes = G.nodes else: + switches = np.concatenate( + ( + edisgo_obj.topology.switches_df.bus_open.values, + edisgo_obj.topology.switches_df.bus_closed.values, + ) + ) nodes = switches - # for the radial feeders in MV grid - for node in G.nodes: - if node in crit_lines.index.values: - nodes.append(node) + # todo:add radial feeders paths = {} nodes_feeder = {} @@ -951,25 +935,43 @@ def split_feeder_at_half_length(edisgo_obj, grid, crit_lines): # next or preceding LV station. If there is no LV station, do not split the # feeder else: + nodes_tb_selected = [ + path[path.index(node_1_2) - ctr] for ctr in range(len(path)) + ] + if split_mode is None: + # the nodes in the entire path will be evaluated for has_mv/lv_station + # first the nodes before node_1_2 + nodes_tb_selected.remove(station_node) + elif split_mode == "back": + # the preceding nodes of node_1_2 will be evaluated + nodes_tb_selected = nodes_tb_selected[ + : nodes_tb_selected.index(station_node) + ] + elif split_mode == "forward": + # the latter nodes of node_1_2 will be evaluated.(node_1_2-switch) + nodes_tb_selected = list( + reversed( + nodes_tb_selected[nodes_tb_selected.index(station_node) + 1 :] + ) + ) + nodes_tb_selected.insert(0, node_1_2) + else: + logger.error(f"{split_mode} is not a valid mode") + while ( node_1_2 not in nodes_feeder.keys() and node_1_2 not in edisgo_obj.topology.transformers_df.bus0.values + and not len(node_1_2) == 0 ): try: - node_1_2 = path[path.index(node_1_2) + 1] + node_1_2 = nodes_tb_selected[nodes_tb_selected.index(node_1_2) + 1] except IndexError: - while ( - node_1_2 not in edisgo_obj.topology.transformers_df.bus0.values - ): - if path.index(node_1_2) > 1: - node_1_2 = path[path.index(node_1_2) - 1] - else: - logger.error( - f" {feeder_first_line} and following lines could not " - f"be reinforced due to the lack of LV station . " - ) - node_1_2 = str() - break + logger.error( + f" {feeder_first_line} and following lines could not " + f"be reinforced due to the lack of LV station . " + ) + node_1_2 = str() + break # if node_1_2 is a representative (meaning it is already directly connected # to the station), line cannot be disconnected and reinforced @@ -1182,7 +1184,7 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): paths = {} first_nodes_feeders = {} - for node in G: + for node in G.nodes: path = nx.shortest_path(G, station_node, node) for first_node in crit_lines_feeder.bus1.values: @@ -1598,7 +1600,7 @@ def _change_dataframe(cb_new_closed, cb_old_df): node_peak_gen_dict = {} # dictionary of peak generations of all nodes in the graph node_peak_load_dict = {} # dictionary of peak loads of all nodes in the graph # add all the loads and gens to the dicts - for node in list(G.nodes): + for node in G.nodes: # for Bus-bars if "BusBar" in node: # the lv_side of node @@ -1616,14 +1618,8 @@ def _change_dataframe(cb_new_closed, cb_old_df): edisgo_obj.topology.buses_df.index.values == bus_node_lv ].lv_grid_id[0] # get lv_grid - count = 0 - for lv_grd in list(edisgo_obj.topology.mv_grid.lv_grids): - if str(int(grid_id)) in repr(lv_grd): - break - count += 1 - lv_grid = list(edisgo_obj.topology.mv_grid.lv_grids)[count] + lv_grid = edisgo_obj.topology.get_lv_grid(int(grid_id)) - # todo:power adjustment node_peak_gen_dict[node] = ( lv_grid.generators_df.p_nom.sum() / cos_phi_feedin ) @@ -1754,7 +1750,7 @@ def _change_dataframe(cb_new_closed, cb_old_df): return circuit_breaker_changes -def split_feeder_at_2_3_length(edisgo_obj, grid, crit_nodes): +def split_feeder_at_2_3_length(edisgo_obj, grid, crit_nodes, split_mode="forward"): """ The voltage issue of the lines in MV and LV grid is remedied by splitting the feeder at the 2/3-length @@ -1800,7 +1796,7 @@ def split_feeder_at_2_3_length(edisgo_obj, grid, crit_nodes): station_node = list(G.nodes)[0] # main station paths = {} - nodes_feeder = {} + crit_nodes_feeder = {} for node in crit_nodes.index: path = nx.shortest_path(G, station_node, node) paths[node] = path @@ -1812,21 +1808,21 @@ def split_feeder_at_2_3_length(edisgo_obj, grid, crit_nodes): f"Voltage issues at busbar in LV network {grid} " f"should have been solved in previous steps." ) - nodes_feeder.setdefault(path[1], []).append(node) + crit_nodes_feeder.setdefault(path[1], []).append(node) lines_changes = {} - for repr_node in nodes_feeder.keys(): + for repr_node in crit_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]: + for c_node in crit_nodes_feeder[repr_node]: path_length_dict_tmp = dijkstra_shortest_path_length( - G, station_node, get_weight, target=n + G, station_node, get_weight, target=c_node ) - if path_length_dict_tmp[n] > path_length: - node = n - path_length = path_length_dict_tmp[n] + if path_length_dict_tmp[c_node] > path_length: + node = c_node + path_length = path_length_dict_tmp[c_node] path_length_dict = path_length_dict_tmp path = paths[node] @@ -1835,7 +1831,8 @@ def split_feeder_at_2_3_length(edisgo_obj, grid, crit_nodes): node_2_3 = next( j for j in path if path_length_dict[j] >= path_length_dict[node] * 2 / 3 ) - + # store the first found node_2_3 + st_node_2_3 = node_2_3 # if LVGrid: check if node_2_3 is outside a house # and if not find next BranchTee outside the house if isinstance(grid, LVGrid): @@ -1855,31 +1852,63 @@ def split_feeder_at_2_3_length(edisgo_obj, grid, crit_nodes): # if MVGrid: check if node_2_3 is LV station and if not find # next or preceding LV station else: - while node_2_3 not in edisgo_obj.topology.transformers_df.bus0.values: + nodes_tb_selected = [ + path[path.index(node_2_3) - ctr] for ctr in range(len(path)) + ] + if split_mode is None: + # the nodes in the entire path will be evaluated for has_mv/lv_station + # first the latter nodes of node_2_3 + nodes_tb_selected = ( + list( + reversed( + nodes_tb_selected[ + nodes_tb_selected.index(station_node) + 1 : + ] + ) + ) + + nodes_tb_selected[: nodes_tb_selected.index(station_node)] + ) + elif split_mode == "back": + # the preceding nodes of node_2_3 will be evaluated + nodes_tb_selected = nodes_tb_selected[ + : nodes_tb_selected.index(station_node) + ] + elif split_mode == "forward": + # the latter nodes of node_2_3 will be evaluated.(node_2_3-switch) + nodes_tb_selected = list( + reversed( + nodes_tb_selected[nodes_tb_selected.index(station_node) + 1 :] + ) + ) + nodes_tb_selected.insert(0, node_2_3) + else: + logger.error(f"{split_mode} is not a valid mode") + + while ( + node_2_3 not in edisgo_obj.topology.transformers_df.bus0.values + and not len(node_2_3) == 0 + ): try: - # try to find LVStation behind node_2_3 - node_2_3 = path[path.index(node_2_3) + 1] + node_2_3 = nodes_tb_selected[nodes_tb_selected.index(node_2_3) + 1] except IndexError: - while ( - node_2_3 not in edisgo_obj.topology.transformers_df.bus0.values - ): - if path.index(node_2_3) > 1: - node_2_3 = path[path.index(node_2_3) - 1] - else: - logger.error( - f" line of {node_2_3} could not be reinforced due to " - f"the lack of LV station . " - ) - break + logger.error( + f" A lv station could not be found in the line of {node_2_3}. " + f"Therefore the node {st_node_2_3} will be separated from the " + f"feeder " + ) + # instead of connecting last nodes of the feeders and reducing n-1 + # security, install a disconnector in its current location + node_2_3 = st_node_2_3 + 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(): + if node_2_3 in crit_nodes_feeder.keys(): crit_line_name = G.get_edge_data(station_node, node_2_3)["branch_name"] - crit_line = grid.lines_df.loc[crit_line_name] - + crit_line = grid.lines_df.loc[crit_line_name:] + # add same type of parallel line lines_changes = add_same_type_of_parallel_line(edisgo_obj, crit_line) else: @@ -1918,7 +1947,7 @@ def split_feeder_at_2_3_length(edisgo_obj, grid, crit_nodes): ] line_added = line_removed lines_changes[line_added] = 1 - print("done") + if lines_changes: logger.info( f"{len(lines_changes)} line/s are reinforced by split feeder at 2/3-length " @@ -1929,6 +1958,7 @@ def split_feeder_at_2_3_length(edisgo_obj, grid, crit_nodes): def add_substation_at_2_3_length(edisgo_obj, grid, crit_nodes): """ + todo: docstring to be updated If the number of overloaded feeders in the LV grid is more than 2, the feeders are split at their 2/3-length, and the disconnected points are connected to the new MV/LV station. @@ -2089,7 +2119,7 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): station_node = list(G.nodes)[0] # main station paths = {} - first_nodes_feeders = {} + crit_nodes_feeder = {} for node in crit_nodes.index: path = nx.shortest_path(G, station_node, node) paths[node] = path @@ -2101,13 +2131,13 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): f"Voltage issues at busbar in LV network {grid} should have " "been solved in previous steps." ) - first_nodes_feeders.setdefault(path[1], []).append(node) + crit_nodes_feeder.setdefault(path[1], []).append(node) lines_changes = {} transformers_changes = {} nodes_tb_relocated = {} # nodes to be moved into the new grid first_nodes_feeders = sorted( - first_nodes_feeders.items(), key=lambda item: len(item[1]), reverse=False + crit_nodes_feeder.items(), key=lambda item: len(item[1]), reverse=False ) first_nodes_feeders = dict(first_nodes_feeders) @@ -2119,13 +2149,13 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): get_weight = lambda u, v, data: data["length"] # noqa: E731 path_length = 0 - for n in first_nodes_feeders[first_node]: + for c_node in first_nodes_feeders[first_node]: path_length_dict_tmp = dijkstra_shortest_path_length( - G, station_node, get_weight, target=n + G, station_node, get_weight, target=c_node ) - if path_length_dict_tmp[n] > path_length: - node = n - path_length = path_length_dict_tmp[n] + if path_length_dict_tmp[c_node] > path_length: + node = c_node + path_length = path_length_dict_tmp[c_node] path_length_dict = path_length_dict_tmp path = paths[node] @@ -2153,26 +2183,23 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): # to the station), line cannot be disconnected and reinforced if node_2_3 not in first_nodes_feeders.keys(): - if node_2_3 not in first_nodes_feeders.keys(): - nodes_path = path.copy() - for main_node in nodes_path: - sub_nodes = _get_subtree_of_node(main_node, main_path=nodes_path) - if sub_nodes is not None: - nodes_path[ - nodes_path.index(main_node) - + 1 : nodes_path.index(main_node) - + 1 - ] = [n for n in sub_nodes] - nodes_tb_relocated[node_2_3] = nodes_path[nodes_path.index(node_2_3) :] - pred_node = path[path.index(node_2_3) - 1] # predecessor node of node_2_3 - - line_removed = G.get_edge_data(node_2_3, pred_node)[ - "branch_name" - ] # the line - line_added_lv = line_removed - lines_changes[line_added_lv] = 1 - # removed from exiting LV grid and converted to an MV line between new - # and existing MV/LV station + nodes_path = path.copy() + for main_node in nodes_path: + sub_nodes = _get_subtree_of_node(main_node, main_path=nodes_path) + if sub_nodes is not None: + nodes_path[ + nodes_path.index(main_node) + + 1 : nodes_path.index(main_node) + + 1 + ] = [n for n in sub_nodes] + nodes_tb_relocated[node_2_3] = nodes_path[nodes_path.index(node_2_3) :] + pred_node = path[path.index(node_2_3) - 1] # predecessor node of node_2_3 + + line_removed = G.get_edge_data(node_2_3, pred_node)["branch_name"] # the line + line_added_lv = line_removed + lines_changes[line_added_lv] = 1 + # removed from exiting LV grid and converted to an MV line between new + # and existing MV/LV station if len(nodes_tb_relocated) > 2 and loop_counter == 0: # Create the bus-bar name of primary and secondary side of new MV/LV station lv_bus_new = create_bus_name(station_node, "lv") From 2e3fea45e0e1fa00e54abb2ec55d05143843f07b Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Sun, 12 Feb 2023 15:57:50 +0100 Subject: [PATCH 35/43] fix all the errors --- edisgo/flex_opt/reinforce_grid_alternative.py | 318 +++++++++++++----- edisgo/flex_opt/reinforce_measures.py | 93 ++--- 2 files changed, 293 insertions(+), 118 deletions(-) diff --git a/edisgo/flex_opt/reinforce_grid_alternative.py b/edisgo/flex_opt/reinforce_grid_alternative.py index 2479bd02d..6a658ebf1 100644 --- a/edisgo/flex_opt/reinforce_grid_alternative.py +++ b/edisgo/flex_opt/reinforce_grid_alternative.py @@ -105,6 +105,14 @@ def reinforce_line_overloading_alternative( 2-'loadgen' 3-'gen' Default: 'load'. + split_mode: it determines the pathway to be searched for MV/LV station when the + node_1_2 comes after the half-length of feeder is not a MV/LV station. + Default: back + *None: search for MV/LV station in all the nodes in the path (first back then + forward) + *back: search for MV/LV station in preceding nodes of node_1_2 in the path + *forward: search for MV/LV station in latter nodes of node_1_2 in the path + Returns ------- :class:`~.network.network.Results` @@ -234,7 +242,16 @@ def _add_circuit_breaker_changes_to_equipment_changes(): logger.debug("==> Check line loadings.") crit_lines_mv = checks.mv_line_load(edisgo_reinforce) crit_lines_lv = checks.lv_line_load(edisgo_reinforce) - + if not crit_lines_mv: + logger.info( + f"{edisgo_reinforce.topology.mv_grid}==>there is no critical line in MV " + f"grid " + ) + if not crit_lines_lv: + logger.info( + f"{edisgo_reinforce.topology.mv_grid}==>there is no critical line in lv " + f"grids " + ) # 1.1 Voltage level= MV # 1.1.1 Method:Split the feeder at the half-length of feeder (applied only once to # secure n-1). @@ -244,18 +261,20 @@ def _add_circuit_breaker_changes_to_equipment_changes(): or "add_station_at_half_length" in add_method ): logger.error( - "==>method:add_station_at_half_length is only applicable for LV grids" + f"{edisgo_reinforce.topology.mv_grid}==> method" + f":add_station_at_half_length is only applicable for LV grids " ) if "relocate_circuit_breaker" in add_method or add_method is None: # method-1: relocate_circuit_breaker logger.info( - f"==> method:relocate circuit breaker location is running " - f"for MV grid {edisgo_reinforce.topology.mv_grid}: " + f"{edisgo_reinforce.topology.mv_grid}==> method:relocate circuit " + f"breaker location is running " ) circuit_breaker_changes = reinforce_measures.relocate_circuit_breaker( edisgo_reinforce, mode=loading_mode ) + # write the installation cost of CBs to results.equipment_changes _add_circuit_breaker_changes_to_equipment_changes() logger.debug("==> Run power flow analysis.") edisgo_reinforce.analyze(timesteps=timesteps_pfa) @@ -270,6 +289,7 @@ def _add_circuit_breaker_changes_to_equipment_changes(): crit_lines_mv, split_mode=split_mode, ) + # write changed lines to results.equipment_changes _add_lines_changes_to_equipment_changes() logger.debug("==> Run power flow analysis.") @@ -277,19 +297,19 @@ def _add_circuit_breaker_changes_to_equipment_changes(): # 1.2- Voltage level= LV if (not grid_mode or grid_mode == "lv") and not crit_lines_lv.empty: - while_counter = 1 if ( add_method == ["relocate_circuit_breaker"] or "relocate_circuit_breaker" in add_method ): logger.error( - "==>method:relocate_circuit_breaker is only applicable for MV grids" + " method:relocate_circuit_breaker is only applicable for MV grids" ) - # reset changes from MV grid - transformer_changes = {} - lines_changes = {} + for lv_grid in list(edisgo_reinforce.topology.mv_grid.lv_grids): + transformer_changes = {} + lines_changes = {} + if "add_station_at_half_length" in add_method or add_method is None: # 1.2.1 Method: Split the feeder at the half-length of feeder and add # new station( applied only once ) @@ -303,6 +323,7 @@ def _add_circuit_breaker_changes_to_equipment_changes(): ) if transformer_changes and lines_changes: + # write changed lines and transformers to results.equipment_changes _add_transformer_changes_to_equipment_changes("added") _add_lines_changes_to_equipment_changes() else: @@ -313,8 +334,11 @@ def _add_circuit_breaker_changes_to_equipment_changes(): lines_changes = reinforce_measures.split_feeder_at_half_length( edisgo_reinforce, lv_grid, crit_lines_lv ) + # write changed lines to results.equipment_changes _add_lines_changes_to_equipment_changes() + # run power flow analysis again (after updating pypsa object) and check + # if all over-voltage problems were solved logger.debug("==> Run power flow analysis.") edisgo_reinforce.analyze(timesteps=timesteps_pfa) @@ -343,15 +367,17 @@ def _add_circuit_breaker_changes_to_equipment_changes(): grid_level = grid_mode logger.info( - f"==>method:add_same_type_of_parallel_line is " - f"running for {grid_level} grid/s_Step{iteration_step}" + f"{edisgo_reinforce.topology.mv_grid}==>method:add_same_type_of_" + f"parallel_line is running for {grid_level} grid/s_Step{iteration_step}" ) lines_changes = reinforce_measures.add_same_type_of_parallel_line( edisgo_reinforce, crit_lines ) - + # write changed lines to results.equipment_changes _add_lines_changes_to_equipment_changes() + # run power flow analysis again (after updating pypsa object) and check + # if all over-voltage problems were solved logger.debug("==> Run power flow analysis.") edisgo_reinforce.analyze(timesteps=timesteps_pfa) @@ -381,16 +407,21 @@ def _add_circuit_breaker_changes_to_equipment_changes(): ] ) raise exceptions.MaximumIterationError( - "Overloading issues could not be solved after maximum allowed " + f"{edisgo_reinforce.topology.mv_grid}==>Overloading issues could not " + f"be solved after maximum allowed " "iterations." ) else: logger.info( - f"==> Load issues were solved in {while_counter} iteration step(s)." + f"{edisgo_reinforce.topology.mv_grid}==> Load issues were solved in " + f"{while_counter} iteration step(s)." ) if not crit_lines.empty: - logger.warning("==> Not all overloading issues could be solved.") + logger.warning( + "{edisgo_reinforce.topology.mv_grid}==>Not all overloading issues could " + "be solved. " + ) edisgo_reinforce.results.grid_expansion_costs = grid_expansion_costs( edisgo_reinforce, without_generator_import=without_generator_import @@ -410,6 +441,79 @@ def reinforce_lines_voltage_issues_alternative( combined_analysis=False, without_generator_import=False, ): + """ + # Todo: To be updated + Parameters + ---------- + edisgo: class:`~.EDisGo` + The eDisGo API object + + add_method: The following methods can be used: + [ + "add_station_at_half_length", + "split_feeder_at_half_length", + ] + timesteps_pfa: str or \ + :pandas:`pandas.DatetimeIndex` or \ + :pandas:`pandas.Timestamp` + timesteps_pfa specifies for which time steps power flow analysis is + conducted and therefore which time steps to consider when checking + for over-loading and over-voltage issues. + It defaults to None in which case all timesteps in + timeseries.timeindex (see :class:`~.network.network.TimeSeries`) are + used. + Possible options are: + + * None + Time steps in timeseries.timeindex (see + :class:`~.network.network.TimeSeries`) are used. + * 'snapshot_analysis' + Reinforcement is conducted for two worst-case snapshots. See + :meth:`edisgo.tools.tools.select_worstcase_snapshots()` for further + explanation on how worst-case snapshots are chosen. + Note: If you have large time series choosing this option will save + calculation time since power flow analysis is only conducted for two + time steps. If your time series already represents the worst-case + keep the default value of None because finding the worst-case + snapshots takes some time. + * :pandas:`pandas.DatetimeIndex` or \ + :pandas:`pandas.Timestamp` + Use this option to explicitly choose which time steps to consider. + + split_mode: it determines the pathway to be searched for MV/LV station when the + node_2_3 comes after the half-length of feeder is not a MV/LV station. + Default: Forward. + *None: search for MV/LV station in all the nodes in the path (first back then + forward) + *back: search for MV/LV station in preceding nodes of node_2_3 in the path + *forward: search for MV/LV station in latter nodes of node_2_3 in the path + + copy_grid:If True reinforcement is conducted on a copied grid and discarded. + Default: False. + grid_mode: + Determines network levels reinforcement is conducted for. Specify + * None to reinforce MV and LV network levels. None is the default. + * 'mv' to reinforce MV network level only, neglecting MV/LV stations, + and LV network topology. LV load and generation is aggregated per + LV network and directly connected to the primary side of the + respective MV/LV station. + * 'lv' to reinforce LV networks. + max_while_iterations : int + Maximum number of times each while loop is conducted. + without_generator_import: bool + If True excludes lines that were added in the generator import to + connect new generators to the topology from calculation of topology expansion + costs. Default: False. + + + Returns + ------- + :class:`~.network.network.Results` + Returns the Results object holding network expansion costs, equipment + changes, etc. + + """ + def _add_lines_changes_to_equipment_changes(): edisgo_reinforce.results.equipment_changes = pd.concat( [ @@ -479,7 +583,7 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): ) methods = [ "split_feeder_at_2_3_length", - "add_substation_at_2_3_length", + "add_station_at_2_3_length", ] if add_method is None: @@ -493,35 +597,51 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): raise ValueError(f"Provided method {add_method} is not valid.") iteration_step = 1 - # analyze_mode = None if mode == "lv" else mode edisgo_reinforce.analyze(timesteps=timesteps_pfa) # REINFORCE BRANCHES DUE TO VOLTAGE ISSUES - # 1.1 Voltage level= MV - logger.debug("==> Check voltage in MV topology.") - voltage_levels_mv = "mv_lv" if combined_analysis else "mv" + # 1.Voltage level= MV + logger.debug(f"{edisgo_reinforce.topology.mv_grid}==>Check voltage in MV topology.") + voltage_level_mv = "mv" + # The nodes that have voltage issue crit_nodes_mv = checks.mv_voltage_deviation( - edisgo_reinforce, voltage_levels=voltage_levels_mv + edisgo_reinforce, voltage_levels=voltage_level_mv ) + if not crit_nodes_mv: + logger.info( + f"{edisgo_reinforce.topology.mv_grid}==>there is no critical line in MV " + f"grid" + ) - if (not grid_mode or grid_mode == "mv") and any(crit_nodes_mv): - # 1.1.1 Method:Split the feeder at the half-length of feeder (applied only once - # to secure n-1). + if (not grid_mode or grid_mode == "mv") and crit_nodes_mv: + # 1.1Method:Split the feeder at the 2_3-length of the feeder (applied several + # times till all the voltage issues are remedied while_counter = 0 - while any(crit_nodes_mv) and while_counter < max_while_iterations: - if "add_substation_at_2_3_length" in add_method: - logger.warning( - "method:add_substation_at_2_3_length is only applicable for " - "LV grids" + while crit_nodes_mv and while_counter < max_while_iterations: + if add_method == ["add_station_at_2_3_length"] and grid_mode is not None: + raise exceptions.Error( + f"{edisgo_reinforce.topology.mv_grid}==>method" + f":add_station_at_2_3_length is only applicable for LV " + "grids" + ) + elif add_method == ["add_station_at_2_3_length"] and grid_mode is None: + logger.error( + f"{edisgo_reinforce.topology.mv_grid}==>method" + f":add_station_at_2_3_length is only applicable for LV grids " + ) + while_counter = max_while_iterations + else: + logger.error( + f"{edisgo_reinforce.topology.mv_grid}==>method" + f":add_station_at_2_3_length is only applicable for LV grids " ) - if "split_feeder_at_2_3_length" in add_method: logger.info( - f"==>method:split_feeder_at_2_3_length is running for " - f"{edisgo_reinforce.topology.mv_grid}: " + f"{edisgo_reinforce.topology.mv_grid}==>method" + f":split_feeder_at_2_3_length is running " ) lines_changes = reinforce_measures.split_feeder_at_2_3_length( @@ -530,14 +650,21 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): crit_nodes_mv[repr(edisgo_reinforce.topology.mv_grid)], split_mode=split_mode, ) + # write changed lines to results.equipment_changes _add_lines_changes_to_equipment_changes() - logger.debug("==> Run power flow analysis.") - edisgo_reinforce.analyze(timesteps=timesteps_pfa) + # run power flow analysis again (after updating pypsa object) and check + # if all over-voltage problems were solved + logger.debug( + f"{edisgo_reinforce.topology.mv_grid}==>Run power flow analysis." + ) + edisgo_reinforce.analyze(timesteps=timesteps_pfa) - logger.debug("==> Recheck voltage in MV grid.") + logger.debug( + f"{edisgo_reinforce.topology.mv_grid}==> Recheck voltage in MV grid." + ) crit_nodes_mv = checks.mv_voltage_deviation( - edisgo_reinforce, voltage_levels=voltage_levels_mv + edisgo_reinforce, voltage_levels=voltage_level_mv ) iteration_step += 1 @@ -545,106 +672,137 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): # check if all voltage problems were solved after maximum number of # iterations allowed - if while_counter == max_while_iterations and any(crit_nodes_mv): + if while_counter == max_while_iterations and crit_nodes_mv: edisgo_reinforce.results.unresolved_issues = pd.concat( [ edisgo_reinforce.results.unresolved_issues, pd.concat([_ for _ in crit_nodes_mv.values()]), ] ) - raise exceptions.MaximumIterationError( - "Over-voltage issues for the following nodes in MV grids " - f"could not be solved: {crit_nodes_mv}" + + logger.info( + f"{edisgo_reinforce.topology.mv_grid}==>Voltage issues for the " + f"following nodes could not be solved in Mv grid since the the number " + f"of max. iteration is reached {crit_nodes_mv.keys()} " ) - else: + elif not crit_nodes_mv: logger.info( - "==> Voltage issues in MV grids were solved " - f"in {while_counter} iteration step(s)." + f"{edisgo_reinforce.topology.mv_grid}==>Voltage issues were solved in " + f"Mv grid in {while_counter} iteration step(s). " ) - # 1.2 Voltage level= LV - voltage_levels_lv = "mv_lv" if combined_analysis else "lv" + # 2 Voltage level= LV + + # todo: If new grid created by the method add + # station requires a voltage issue reinforcement, it will + # raise an error since the buses and lines name of the moved nodes to + # the new grid is not changed. + voltage_level_lv = "lv" logger.debug("==> Check voltage in LV grids.") crit_nodes_lv = checks.lv_voltage_deviation( - edisgo_reinforce, voltage_levels=voltage_levels_lv + edisgo_reinforce, voltage_levels=voltage_level_lv ) - if (not grid_mode or grid_mode == "lv") and any(crit_nodes_lv): + if not crit_nodes_lv: + logger.info( + f"{edisgo_reinforce.topology.mv_grid}==>there is no critical line in lv " + "grids " + ) + if (not grid_mode or grid_mode == "lv") and crit_nodes_lv: - # 1.1.1 Method:Split the feeder at the half-length of feeder and add new station - # ( applied only once ) if the number of overloaded lines is more than 2 - # reset changes from MV grid + # 2.1 add new station ( applied only once ) if the number of overloaded lines + # is more than 2 while_counter = 0 - while any(crit_nodes_lv) and while_counter < max_while_iterations: - - transformer_changes = {} - lines_changes = {} + while crit_nodes_lv and while_counter < max_while_iterations: for lv_grid in crit_nodes_lv: + + transformer_changes = {} + lines_changes = {} if ( - "add_substation_at_2_3_length" in add_method and while_counter < 1 + "add_station_at_2_3_length" in add_method and while_counter < 1 ) or (add_method is None and while_counter < 1): - # 1.2.1 Method: Split the feeder at the half-length of feeder and - # add new station( applied only once ) - # if the number of overloaded lines is more than 2 - logger.debug( - f"==>method:add_substation_at_2_3_length method is running for " - f"{lv_grid}: " + logger.info( + f"{lv_grid}:==>method:add_station_at_2_3_length method is " + f"running " ) ( transformer_changes, lines_changes, - ) = reinforce_measures.add_substation_at_2_3_length( + ) = reinforce_measures.add_station_at_2_3_length( edisgo_reinforce, edisgo_reinforce.topology.get_lv_grid(lv_grid), crit_nodes_lv[lv_grid], ) - if transformer_changes and lines_changes: + # write changed lines and transformers to + # results.equipment_changes _add_transformer_changes_to_equipment_changes("added") _add_lines_changes_to_equipment_changes() + # 2.2 Method:split_feeder_at_2/3-length of feeder if ( "split_feeder_at_2_3_length" in add_method and not any(transformer_changes) ) or (add_method is None and not any(transformer_changes)): - # 1.2.2 Method:split_feeder_at_2/3-length of feeder - # (applied only once) - logger.debug( - f"==>method:split_feeder_at_2_3_length is running for " - f"{lv_grid}: " + logger.info( + f"{lv_grid}:==>method:split_feeder_at_2_3_length is running" ) - - lines_changes = reinforce_measures.reinforce_lines_voltage_issues( + lines_changes = reinforce_measures.split_feeder_at_2_3_length( edisgo_reinforce, edisgo_reinforce.topology.get_lv_grid(lv_grid), crit_nodes_lv[lv_grid], + split_mode=split_mode, ) # write changed lines to results.equipment_changes _add_lines_changes_to_equipment_changes() # run power flow analysis again (after updating pypsa object) # and check if all over-voltage problems were solved - logger.debug("==> Run power flow analysis.") + logger.debug("==>Run power flow analysis.") edisgo_reinforce.analyze(timesteps=timesteps_pfa) - logger.debug("==> Recheck voltage in LV grids.") + logger.debug("==>Recheck voltage in LV grids.") crit_nodes_lv = checks.lv_voltage_deviation( - edisgo_reinforce, voltage_levels=voltage_levels_lv + edisgo_reinforce, voltage_levels=voltage_level_lv ) + if add_method == ["add_station_at_2_3_length"]: + while_counter = max_while_iterations - 1 + iteration_step += 1 while_counter += 1 - logger.debug("==> Run power flow analysis.") - edisgo_reinforce.analyze(timesteps=timesteps_pfa) - - if any(crit_nodes_lv) and "add_substation_at_2_3_length" in add_method: + # check if all load problems were solved after maximum number of + # iterations allowed + if while_counter == max_while_iterations and crit_nodes_lv: + edisgo_reinforce.results.unresolved_issues = pd.concat( + [ + edisgo_reinforce.results.unresolved_issues, + pd.concat([_ for _ in crit_nodes_lv.values()]), + ] + ) + if add_method == ["add_station_at_2_3_length"]: + logger.error( + f"{edisgo_reinforce.topology.mv_grid}==> Voltage issues in LV " + f"grids could not solve in {while_counter} iteration step(s) " + f"since only method :add_station_at_2_3_length is applied " + ) + else: + logger.info( + f"{edisgo_reinforce.topology.mv_grid}==>Voltage issues for the" + f"following nodes in LV grids could not be solved: " + f"{crit_nodes_lv.keys()} " + ) + else: logger.info( - "==> Voltage issues in LV grids could not solve in {while_counter} " - "iteration step(s) since only method :add_substation_at_2_3_length is " - "applied " + f"{edisgo_reinforce.topology.mv_grid}==>Voltage issues in LV grids " + f"were solved in {while_counter} iteration step(s)." ) + edisgo_reinforce.results.grid_expansion_costs = grid_expansion_costs( + edisgo_reinforce, without_generator_import=without_generator_import + ) + return edisgo_reinforce.results diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index 958ab87a4..ab45a60be 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -828,6 +828,7 @@ def split_feeder_at_half_length(edisgo_obj, grid, crit_lines, split_mode="back") the voltage level the line is in (either 'mv' or 'lv'). split_mode: it determines the pathway to be searched for MV/LV station when the node_1_2 comes after the half-length of feeder is not a MV/LV station. + Default: back *None: search for MV/LV station in all the nodes in the path (first back then forward) *back: search for MV/LV station in preceding nodes of node_1_2 in the path @@ -926,7 +927,7 @@ def split_feeder_at_half_length(edisgo_obj, grid, crit_lines, split_mode="back") # break if node is station if node_1_2 is path[0]: logger.error( - f" {feeder_first_line} and following lines could not " + f" {grid}==>{feeder_first_line} and following lines could not " f"be reinforced due to insufficient number of node . " ) break @@ -956,7 +957,7 @@ def split_feeder_at_half_length(edisgo_obj, grid, crit_lines, split_mode="back") ) nodes_tb_selected.insert(0, node_1_2) else: - logger.error(f"{split_mode} is not a valid mode") + logger.error(f"{grid}==>{split_mode} is not a valid mode") while ( node_1_2 not in nodes_feeder.keys() @@ -967,7 +968,7 @@ def split_feeder_at_half_length(edisgo_obj, grid, crit_lines, split_mode="back") node_1_2 = nodes_tb_selected[nodes_tb_selected.index(node_1_2) + 1] except IndexError: logger.error( - f" {feeder_first_line} and following lines could not " + f" {grid}==>{feeder_first_line} and following lines could not " f"be reinforced due to the lack of LV station . " ) node_1_2 = str() @@ -976,9 +977,7 @@ def split_feeder_at_half_length(edisgo_obj, grid, crit_lines, split_mode="back") # if node_1_2 is a representative (meaning it is already directly connected # to the station), line cannot be disconnected and reinforced if node_1_2 not in nodes_feeder.keys() and not len(node_1_2) == 0: - logger.info( - f"==>method:split_feeder_at_half_length is running for " f"{grid}: " - ) + logger.info(f"{grid}==>method:split_feeder_at_half_length is running") # get line between node_1_2 and predecessor node pred_node = path[path.index(node_1_2) - 1] line_removed = G.get_edge_data(node_1_2, pred_node)["branch_name"] @@ -995,14 +994,14 @@ def split_feeder_at_half_length(edisgo_obj, grid, crit_lines, split_mode="back") edisgo_obj.topology._lines_df.at[line_removed, "bus0"] = station_node logger.info( - f"==> {grid}--> the line {line_removed} disconnected from " + f"{grid}==> the line {line_removed} disconnected from " f"{pred_node} and connected to the main station {station_node} " ) elif grid.lines_df.at[line_removed, "bus1"] == pred_node: edisgo_obj.topology._lines_df.at[line_removed, "bus1"] = station_node logger.info( - f"==> {grid}-->the line {line_removed} disconnected from " + f"{grid}==>the line {line_removed} disconnected from " f"{pred_node} and connected to the main station {station_node} " ) else: @@ -1016,8 +1015,8 @@ def split_feeder_at_half_length(edisgo_obj, grid, crit_lines, split_mode="back") lines_changes[line_added] = 1 if lines_changes: logger.info( - f"{len(lines_changes)} line/s are reinforced by method: split feeder " - f"at half-length method in {grid}" + f"{grid}==>{len(lines_changes)} line/s are reinforced by method: " + f"split feeder at half-length" ) return lines_changes @@ -1240,8 +1239,8 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): # break if node is station if node_1_2 is path[0]: grid.error( - f" {first_line} and following lines could not be reinforced " - f"due to insufficient number of node in the feeder . " + f" {grid}==>{first_line} and following lines could not be " + f"reinforced due to insufficient number of node in the feeder. " ) break loop_counter -= 1 @@ -1258,7 +1257,7 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): # removed from exiting LV grid and converted to an MV line between new # and existing MV/LV station if len(nodes_tb_relocated) > 2 and loop_counter == 0: - logger.info(f"==>method:add_station_at_half_length is running for {grid}: ") + logger.info(f"{grid}==>method:add_station_at_half_length is running ") # Create the bus-bar name of primary and secondary side of new MV/LV station lv_bus_new = create_bus_name(station_node, "lv") mv_bus_new = create_bus_name(station_node, "mv") @@ -1320,7 +1319,10 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): ) transformers_changes.update(transformer_changes) - logger.debug(f"A new grid {lv_grid_id_new} added into topology") + logger.debug( + f"{edisgo_obj.topology.mv_grid}==>A new grid {lv_grid_id_new} " + f"added into topology" + ) # ADD the MV LINE between existing and new MV station @@ -1686,7 +1688,7 @@ def _change_dataframe(cb_new_closed, cb_old_df): else: has_lv_station = False logging.debug( - f"Ring {ring} does not have a LV station." + f"{grid}==>Ring {ring} does not have a LV station." f"Switch disconnecter is installed at arbitrary " "node." ) @@ -1743,10 +1745,11 @@ def _change_dataframe(cb_new_closed, cb_old_df): if len(circuit_breaker_changes): logger.info( - f"{len(circuit_breaker_changes)} circuit breakers are relocated in {grid}" + f"{grid}==>{len(circuit_breaker_changes)} circuit breakers are " + f"relocated in " ) else: - logger.info(f"no circuit breaker is relocated in {grid}") + logger.info(f"{grid}==>no circuit breaker is relocated") return circuit_breaker_changes @@ -1778,6 +1781,13 @@ def split_feeder_at_2_3_length(edisgo_obj, grid, crit_nodes, split_mode="forward corresponding time step the voltage issue occured in as :pandas:`pandas.Timestamp`. Index of the dataframe are the names of all buses with voltage issues. + split_mode: it determines the pathway to be searched for MV/LV station when the + node_1_2 comes after the half-length of feeder is not a MV/LV station. + Default: forward + *None: search for MV/LV station in all the nodes in the path (first back then + forward) + *back: search for MV/LV station in preceding nodes of node_1_2 in the path + *forward: search for MV/LV station in latter nodes of node_1_2 in the path Returns ------- @@ -1805,7 +1815,7 @@ def split_feeder_at_2_3_length(edisgo_obj, grid, crit_nodes, split_mode="forward # distribution substations due to overvoltage issues. if len(path) == 1: logging.error( - f"Voltage issues at busbar in LV network {grid} " + f"{grid}==>Voltage issues at busbar in LV network " f"should have been solved in previous steps." ) crit_nodes_feeder.setdefault(path[1], []).append(node) @@ -1844,7 +1854,7 @@ def split_feeder_at_2_3_length(edisgo_obj, grid, crit_nodes, split_mode="forward # break if node is station if node_2_3 is path[0]: logger.error( - f" line of {node_2_3} could not be reinforced due to " + f" {grid}==>line of {node_2_3} could not be reinforced due to " f"insufficient number of node . " ) break @@ -1891,10 +1901,9 @@ def split_feeder_at_2_3_length(edisgo_obj, grid, crit_nodes, split_mode="forward try: node_2_3 = nodes_tb_selected[nodes_tb_selected.index(node_2_3) + 1] except IndexError: - logger.error( - f" A lv station could not be found in the line of {node_2_3}. " - f"Therefore the node {st_node_2_3} will be separated from the " - f"feeder " + logger.warning( + f"{grid}==> A lv station could not be found in the line of " + f"{node_2_3}.Therefore the feeder is split from {st_node_2_3} " ) # instead of connecting last nodes of the feeders and reducing n-1 # security, install a disconnector in its current location @@ -1907,10 +1916,13 @@ def split_feeder_at_2_3_length(edisgo_obj, grid, crit_nodes, split_mode="forward if node_2_3 in crit_nodes_feeder.keys(): crit_line_name = G.get_edge_data(station_node, node_2_3)["branch_name"] - crit_line = grid.lines_df.loc[crit_line_name:] + crit_line = grid.lines_df[grid.lines_df.index.isin([crit_line_name])] # add same type of parallel line lines_changes = add_same_type_of_parallel_line(edisgo_obj, crit_line) - + logger.info( + f"{grid} ==> voltage issue of {crit_line_name} solved by " + f"adding same type of parallel line " + ) else: # get line between node_2_3 and predecessor node pred_node = path[path.index(node_2_3) - 1] @@ -1935,7 +1947,7 @@ def split_feeder_at_2_3_length(edisgo_obj, grid, crit_nodes, split_mode="forward edisgo_obj.topology._lines_df.at[line_removed, "bus1"] = station_node logger.info( - f"==> {grid}-->the line {line_removed} disconnected from " + f"==> {grid}==>the line {line_removed} disconnected from " f"{pred_node} and connected to the main station {station_node} " ) else: @@ -1950,13 +1962,13 @@ def split_feeder_at_2_3_length(edisgo_obj, grid, crit_nodes, split_mode="forward if lines_changes: logger.info( - f"{len(lines_changes)} line/s are reinforced by split feeder at 2/3-length " - f"method in {grid}" + f"{grid}==>{len(lines_changes)} line/s are reinforced by method: " + f"split feeder at 2_3-length" ) return lines_changes -def add_substation_at_2_3_length(edisgo_obj, grid, crit_nodes): +def add_station_at_2_3_length(edisgo_obj, grid, crit_nodes): """ todo: docstring to be updated If the number of overloaded feeders in the LV grid is more than 2, the feeders are @@ -2128,7 +2140,7 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): # distribution substations due to overvoltage issues. if len(path) == 1: logging.error( - f"Voltage issues at busbar in LV network {grid} should have " + f"{grid}==>Voltage issues at busbar in LV network should have " "been solved in previous steps." ) crit_nodes_feeder.setdefault(path[1], []).append(node) @@ -2173,7 +2185,7 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): # break if node is station if node_2_3 is path[0]: grid.error( - f" line of {node_2_3} could not be reinforced " + f" {grid}==>line of {node_2_3} could not be reinforced " f"due to insufficient number of node in the feeder . " ) break @@ -2226,10 +2238,12 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): # same with the distance between pred. node of node_2_3 of one of first # feeders to be split in LV grid - length = ( + length_lv = ( path_length_dict[node_2_3] - path_length_dict[path[path.index(node_2_3) - 1]] ) + length_mv = path_length_dict[node_2_3] + # if the transformer already added, do not add bus and transformer once more if not transformers_changes: # the coordinates of new MV station (x2,y2) @@ -2240,7 +2254,7 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): edisgo_obj.topology.add_bus( lv_bus_new, v_nom_lv, - x=x_bus + length / 1000, + x=x_bus + length_lv / 1000, y=y_bus, lv_grid_id=lv_grid_id_new, in_building=building_bus, @@ -2249,7 +2263,7 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): edisgo_obj.topology.add_bus( mv_bus_new, v_nom_mv, - x=x_bus + length / 1000, + x=x_bus + length_mv / 1000, y=y_bus, in_building=building_bus, ) @@ -2260,7 +2274,10 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): ) transformers_changes.update(transformer_changes) - logger.debug(f"A new grid {lv_grid_id_new} added into topology") + logger.debug( + f"{edisgo_obj.topology.mv_grid}==>A new grid {lv_grid_id_new} " + f"added into topology" + ) # ADD the MV LINE between existing and new MV station @@ -2271,7 +2288,7 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): line_added_mv = edisgo_obj.topology.add_line( bus0=grid.transformers_df.bus0[0], bus1=mv_bus_new, - length=length, + length=length_mv, type_info=standard_line, kind="cable", ) @@ -2299,8 +2316,8 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): ) logger.info( f"{len(nodes_tb_relocated.keys())} feeders are removed from the grid " - f"{grid} and located in new grid{repr(grid) + str(1001)} by split " - f"feeder+add transformer method" + f"{grid} and located in new grid{repr(grid) + str(1001)} by method: " + f"add_station_at_2_3_length " ) if len(lines_changes) < 3: lines_changes = {} From 9e85bf31a5ae7ff0c19199186da00014b6b06c19 Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Sun, 19 Feb 2023 17:40:59 +0100 Subject: [PATCH 36/43] minor fix --- edisgo/flex_opt/reinforce_grid_alternative.py | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/edisgo/flex_opt/reinforce_grid_alternative.py b/edisgo/flex_opt/reinforce_grid_alternative.py index 6a658ebf1..cf9a69144 100644 --- a/edisgo/flex_opt/reinforce_grid_alternative.py +++ b/edisgo/flex_opt/reinforce_grid_alternative.py @@ -19,7 +19,7 @@ def reinforce_line_overloading_alternative( add_method=None, timesteps_pfa=None, copy_grid=False, - grid_mode=None, + voltage_level=None, loading_mode="load", split_mode="back", max_while_iterations=20, @@ -85,7 +85,7 @@ def reinforce_line_overloading_alternative( Use this option to explicitly choose which time steps to consider. copy_grid:If True reinforcement is conducted on a copied grid and discarded. Default: False. - grid_mode : str + voltage_level : str Determines network levels reinforcement is conducted for. Specify * None to reinforce MV and LV network levels. None is the default. * 'mv' to reinforce MV network level only, neglecting MV/LV stations, @@ -182,8 +182,8 @@ def _add_circuit_breaker_changes_to_equipment_changes(): ) # check if provided mode is valid - if grid_mode and grid_mode not in ["mv", "lv"]: - raise ValueError(f"Provided mode {grid_mode} is not valid.") + if voltage_level and voltage_level not in ["mv", "lv"]: + raise ValueError(f"Provided mode {voltage_level} is not valid.") # in case reinforcement needs to be conducted on a copied graph the # edisgo object is deep copied if copy_grid is True: @@ -242,12 +242,12 @@ def _add_circuit_breaker_changes_to_equipment_changes(): logger.debug("==> Check line loadings.") crit_lines_mv = checks.mv_line_load(edisgo_reinforce) crit_lines_lv = checks.lv_line_load(edisgo_reinforce) - if not crit_lines_mv: + if not any(crit_lines_mv): logger.info( f"{edisgo_reinforce.topology.mv_grid}==>there is no critical line in MV " f"grid " ) - if not crit_lines_lv: + if not any(crit_lines_lv): logger.info( f"{edisgo_reinforce.topology.mv_grid}==>there is no critical line in lv " f"grids " @@ -255,7 +255,7 @@ def _add_circuit_breaker_changes_to_equipment_changes(): # 1.1 Voltage level= MV # 1.1.1 Method:Split the feeder at the half-length of feeder (applied only once to # secure n-1). - if (not grid_mode or grid_mode == "mv") and not crit_lines_mv.empty: + if (not voltage_level or voltage_level == "mv") and not crit_lines_mv.empty: if ( add_method == ["add_station_at_half_length"] or "add_station_at_half_length" in add_method @@ -296,7 +296,7 @@ def _add_circuit_breaker_changes_to_equipment_changes(): edisgo_reinforce.analyze(timesteps=timesteps_pfa) # 1.2- Voltage level= LV - if (not grid_mode or grid_mode == "lv") and not crit_lines_lv.empty: + if (not voltage_level or voltage_level == "lv") and not crit_lines_lv.empty: if ( add_method == ["relocate_circuit_breaker"] or "relocate_circuit_breaker" in add_method @@ -345,11 +345,11 @@ def _add_circuit_breaker_changes_to_equipment_changes(): logger.debug("==> Recheck line load.") crit_lines = ( pd.DataFrame(dtype=float) - if grid_mode == "lv" + if voltage_level == "lv" else checks.mv_line_load(edisgo_reinforce) ) - if not grid_mode or grid_mode == "lv": + if not voltage_level or voltage_level == "lv": crit_lines = pd.concat( [ crit_lines, @@ -361,10 +361,10 @@ def _add_circuit_breaker_changes_to_equipment_changes(): # Method: Add same type of parallel line while_counter = 0 while not crit_lines.empty and while_counter < max_while_iterations: - if grid_mode is None: + if voltage_level is None: grid_level = "MV and LV " else: - grid_level = grid_mode + grid_level = voltage_level logger.info( f"{edisgo_reinforce.topology.mv_grid}==>method:add_same_type_of_" @@ -384,11 +384,11 @@ def _add_circuit_breaker_changes_to_equipment_changes(): logger.debug("==> Recheck line load.") crit_lines = ( pd.DataFrame(dtype=float) - if grid_mode == "lv" + if voltage_level == "lv" else checks.mv_line_load(edisgo_reinforce) ) - if not grid_mode or grid_mode == "lv": + if not voltage_level or voltage_level == "lv": crit_lines = pd.concat( [ crit_lines, @@ -419,7 +419,7 @@ def _add_circuit_breaker_changes_to_equipment_changes(): if not crit_lines.empty: logger.warning( - "{edisgo_reinforce.topology.mv_grid}==>Not all overloading issues could " + f"{edisgo_reinforce.topology.mv_grid}==>Not all overloading issues could " "be solved. " ) @@ -436,7 +436,7 @@ def reinforce_lines_voltage_issues_alternative( timesteps_pfa=None, split_mode="forward", copy_grid=False, - grid_mode=None, + voltage_level=None, max_while_iterations=20, combined_analysis=False, without_generator_import=False, @@ -490,7 +490,7 @@ def reinforce_lines_voltage_issues_alternative( copy_grid:If True reinforcement is conducted on a copied grid and discarded. Default: False. - grid_mode: + voltage_level: Determines network levels reinforcement is conducted for. Specify * None to reinforce MV and LV network levels. None is the default. * 'mv' to reinforce MV network level only, neglecting MV/LV stations, @@ -550,8 +550,8 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): edisgo_reinforce.results.equipment_changes = pd.concat(df_list) # check if provided mode is valid - if grid_mode and grid_mode not in ["mv", "lv"]: - raise ValueError(f"Provided mode {grid_mode} is not valid.") + if voltage_level and voltage_level not in ["mv", "lv"]: + raise ValueError(f"Provided mode {voltage_level} is not valid.") # in case reinforcement needs to be conducted on a copied graph the # edisgo object is deep copied if copy_grid is True: @@ -616,18 +616,21 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): f"grid" ) - if (not grid_mode or grid_mode == "mv") and crit_nodes_mv: + if (not voltage_level or voltage_level == "mv") and crit_nodes_mv: # 1.1Method:Split the feeder at the 2_3-length of the feeder (applied several # times till all the voltage issues are remedied while_counter = 0 while crit_nodes_mv and while_counter < max_while_iterations: - if add_method == ["add_station_at_2_3_length"] and grid_mode is not None: + if ( + add_method == ["add_station_at_2_3_length"] + and voltage_level is not None + ): raise exceptions.Error( f"{edisgo_reinforce.topology.mv_grid}==>method" f":add_station_at_2_3_length is only applicable for LV " "grids" ) - elif add_method == ["add_station_at_2_3_length"] and grid_mode is None: + elif add_method == ["add_station_at_2_3_length"] and voltage_level is None: logger.error( f"{edisgo_reinforce.topology.mv_grid}==>method" f":add_station_at_2_3_length is only applicable for LV grids " @@ -708,7 +711,7 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): f"{edisgo_reinforce.topology.mv_grid}==>there is no critical line in lv " "grids " ) - if (not grid_mode or grid_mode == "lv") and crit_nodes_lv: + if (not voltage_level or voltage_level == "lv") and crit_nodes_lv: # 2.1 add new station ( applied only once ) if the number of overloaded lines # is more than 2 From b0540b13c8e0b01da624934a9061f855875b1187 Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Fri, 24 Feb 2023 13:41:54 +0100 Subject: [PATCH 37/43] changed the costs of cables --- .../config/config_grid_expansion_default.cfg | 11 +++++----- .../equipment-parameters_LV_cables.csv | 16 +++++++-------- .../equipment-parameters_MV_cables.csv | 20 +++++++++---------- ...equipment-parameters_MV_overhead_lines.csv | 12 +++++------ 4 files changed, 30 insertions(+), 29 deletions(-) diff --git a/edisgo/config/config_grid_expansion_default.cfg b/edisgo/config/config_grid_expansion_default.cfg index aa88e8529..aecd1b8fd 100644 --- a/edisgo/config/config_grid_expansion_default.cfg +++ b/edisgo/config/config_grid_expansion_default.cfg @@ -75,14 +75,15 @@ mv_lv_station_feed-in_case_max_v_deviation = 0.015 # ============ # Source: Rehtanz et. al.: "Verteilnetzstudie für das Land Baden-Württemberg", 2017. mv_load_case_transformer = 0.5 -mv_load_case_line = 0.1 +mv_load_case_line = 0.4 mv_feed-in_case_transformer = 1.0 -mv_feed-in_case_line = 0.1 +mv_feed-in_case_line = 0.8 lv_load_case_transformer = 1.0 -lv_load_case_line = 0.01 +lv_load_case_line =1 lv_feed-in_case_transformer = 1.0 -lv_feed-in_case_line = 0.01 +lv_feed-in_case_line = 1 + # costs # ============ @@ -101,7 +102,7 @@ lv_feed-in_case_line = 0.01 lv_cable = 9 lv_cable_incl_earthwork_rural = 60 lv_cable_incl_earthwork_urban = 100 -mv_cable = 20 +mv_cable = 50 mv_cable_incl_earthwork_rural = 80 mv_cable_incl_earthwork_urban = 140 diff --git a/edisgo/equipment/equipment-parameters_LV_cables.csv b/edisgo/equipment/equipment-parameters_LV_cables.csv index 02fdf58f2..58bccd228 100644 --- a/edisgo/equipment/equipment-parameters_LV_cables.csv +++ b/edisgo/equipment/equipment-parameters_LV_cables.csv @@ -1,10 +1,10 @@ name,U_n,I_max_th,R_per_km,L_per_km,C_per_km,cost #-,kV,kA,ohm/km,mH/km,uF/km,kEuro/km -NAYY 4x1x300,0.4,0.419,0.1,0.279,0,20 -NAYY 4x1x240,0.4,0.364,0.125,0.254,0,12.8 -NAYY 4x1x185,0.4,0.313,0.164,0.256,0,10 -NAYY 4x1x150,0.4,0.275,0.206,0.256,0,8.4 -NAYY 4x1x120,0.4,0.245,0.253,0.256,0,7.7 -NAYY 4x1x95,0.4,0.215,0.320,0.261,0,6.5 -NAYY 4x1x50,0.4,0.144,0.449,0.270,0,6 -NAYY 4x1x35,0.4,0.123,0.868,0.271,0,5 +NAYY 4x1x300,0.4,0.419,0.1,0.279,0,12.75 +NAYY 4x1x240,0.4,0.364,0.125,0.254,0,10.177 +NAYY 4x1x185,0.4,0.313,0.164,0.256,0,7.834 +NAYY 4x1x150,0.4,0.275,0.206,0.256,0,6.25 +NAYY 4x1x120,0.4,0.245,0.253,0.256,0,5.075 +NAYY 4x1x95,0.4,0.215,0.320,0.261,0,4.098 +NAYY 4x1x50,0.4,0.144,0.449,0.270,0,2.27 +NAYY 4x1x35,0.4,0.123,0.868,0.271,0,1.696 diff --git a/edisgo/equipment/equipment-parameters_MV_cables.csv b/edisgo/equipment/equipment-parameters_MV_cables.csv index cea708215..70e265d1d 100644 --- a/edisgo/equipment/equipment-parameters_MV_cables.csv +++ b/edisgo/equipment/equipment-parameters_MV_cables.csv @@ -1,12 +1,12 @@ name,U_n,I_max_th,R_per_km,L_per_km,C_per_km,cost #-,kV,kA,ohm/km,mH/km,uF/km,kEuro/km -NA2XS2Y 3x1x185 RM/25,10,0.357,0.164,0.38,0.41,24 -NA2XS2Y 3x1x240 RM/25,10,0.417,0.125,0.36,0.47,27 -NA2XS2Y 3x1x300 RM/25,10,0.466,0.1,0.35,0.495,39 -NA2XS2Y 3x1x400 RM/35,10,0.535,0.078,0.34,0.57,50 -NA2XS2Y 3x1x500 RM/35,10,0.609,0.061,0.32,0.63,60 -NA2XS2Y 3x1x150 RE/25,20,0.319,0.206,0.4011,0.24,39 -NA2XS2Y 3x1x240,20,0.417,0.13,0.3597,0.304,50 -NA2XS(FL)2Y 3x1x300 RM/25,20,0.476,0.1,0.37,0.25,60 -NA2XS(FL)2Y 3x1x400 RM/35,20,0.525,0.078,0.36,0.27,90 -NA2XS(FL)2Y 3x1x500 RM/35,20,0.598,0.06,0.34,0.3,120 +NA2XS2Y 3x1x185 RM/25,10,0.357,0.164,0.38,0.41,16.88 +NA2XS2Y 3x1x240 RM/25,10,0.417,0.125,0.36,0.47,19.01 +NA2XS2Y 3x1x300 RM/25,10,0.466,0.1,0.35,0.495,21.09 +NA2XS2Y 3x1x400 RM/35,10,0.535,0.078,0.34,0.57,26.53 +NA2XS2Y 3x1x500 RM/35,10,0.609,0.061,0.32,0.63,29.95 +NA2XS2Y 3x1x150 RE/25,20,0.319,0.206,0.4011,0.24,15.54 +NA2XS2Y 3x1x240,20,0.417,0.13,0.3597,0.304,19.01 +NA2XS(FL)2Y 3x1x300 RM/25,20,0.476,0.1,0.37,0.25,21.09 +NA2XS(FL)2Y 3x1x400 RM/35,20,0.525,0.078,0.36,0.27,26.53 +NA2XS(FL)2Y 3x1x500 RM/35,20,0.598,0.06,0.34,0.3,29.95 diff --git a/edisgo/equipment/equipment-parameters_MV_overhead_lines.csv b/edisgo/equipment/equipment-parameters_MV_overhead_lines.csv index b4ccb9e4f..d6ef0ecf4 100644 --- a/edisgo/equipment/equipment-parameters_MV_overhead_lines.csv +++ b/edisgo/equipment/equipment-parameters_MV_overhead_lines.csv @@ -1,8 +1,8 @@ name,U_n,I_max_th,R_per_km,L_per_km,C_per_km,cost #-,kV,kA,ohm/km,mH/km,uF/km -48-AL1/8-ST1A,10,0.21,0.35,1.11,0.0104,130 -94-AL1/15-ST1A,10,0.35,0.33,1.05,0.0112,160 -122-AL1/20-ST1A,10,0.41,0.31,0.99,0.0115,190 -48-AL1/8-ST1A,20,0.21,0.37,1.18,0.0098,220 -94-AL1/15-ST1A,20,0.35,0.35,1.11,0.0104,250 -122-AL1/20-ST1A,20,0.41,0.34,1.08,0.0106,300 +48-AL1/8-ST1A,10,0.21,0.35,1.11,0.0104,1.33 +94-AL1/15-ST1A,10,0.35,0.33,1.05,0.0112,2.60 +122-AL1/20-ST1A,10,0.41,0.31,0.99,0.0115,3.36 +48-AL1/8-ST1A,20,0.21,0.37,1.18,0.0098,1.33 +94-AL1/15-ST1A,20,0.35,0.35,1.11,0.0104,2.60 +122-AL1/20-ST1A,20,0.41,0.34,1.08,0.0106,3.36 From db294bcd140c7a9f4701f3740dc978b89d21e36d Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Fri, 24 Feb 2023 13:42:10 +0100 Subject: [PATCH 38/43] Revert "changed the costs of cables" This reverts commit b0540b13c8e0b01da624934a9061f855875b1187. --- .../config/config_grid_expansion_default.cfg | 11 +++++----- .../equipment-parameters_LV_cables.csv | 16 +++++++-------- .../equipment-parameters_MV_cables.csv | 20 +++++++++---------- ...equipment-parameters_MV_overhead_lines.csv | 12 +++++------ 4 files changed, 29 insertions(+), 30 deletions(-) diff --git a/edisgo/config/config_grid_expansion_default.cfg b/edisgo/config/config_grid_expansion_default.cfg index aecd1b8fd..aa88e8529 100644 --- a/edisgo/config/config_grid_expansion_default.cfg +++ b/edisgo/config/config_grid_expansion_default.cfg @@ -75,15 +75,14 @@ mv_lv_station_feed-in_case_max_v_deviation = 0.015 # ============ # Source: Rehtanz et. al.: "Verteilnetzstudie für das Land Baden-Württemberg", 2017. mv_load_case_transformer = 0.5 -mv_load_case_line = 0.4 +mv_load_case_line = 0.1 mv_feed-in_case_transformer = 1.0 -mv_feed-in_case_line = 0.8 +mv_feed-in_case_line = 0.1 lv_load_case_transformer = 1.0 -lv_load_case_line =1 +lv_load_case_line = 0.01 lv_feed-in_case_transformer = 1.0 -lv_feed-in_case_line = 1 - +lv_feed-in_case_line = 0.01 # costs # ============ @@ -102,7 +101,7 @@ lv_feed-in_case_line = 1 lv_cable = 9 lv_cable_incl_earthwork_rural = 60 lv_cable_incl_earthwork_urban = 100 -mv_cable = 50 +mv_cable = 20 mv_cable_incl_earthwork_rural = 80 mv_cable_incl_earthwork_urban = 140 diff --git a/edisgo/equipment/equipment-parameters_LV_cables.csv b/edisgo/equipment/equipment-parameters_LV_cables.csv index 58bccd228..02fdf58f2 100644 --- a/edisgo/equipment/equipment-parameters_LV_cables.csv +++ b/edisgo/equipment/equipment-parameters_LV_cables.csv @@ -1,10 +1,10 @@ name,U_n,I_max_th,R_per_km,L_per_km,C_per_km,cost #-,kV,kA,ohm/km,mH/km,uF/km,kEuro/km -NAYY 4x1x300,0.4,0.419,0.1,0.279,0,12.75 -NAYY 4x1x240,0.4,0.364,0.125,0.254,0,10.177 -NAYY 4x1x185,0.4,0.313,0.164,0.256,0,7.834 -NAYY 4x1x150,0.4,0.275,0.206,0.256,0,6.25 -NAYY 4x1x120,0.4,0.245,0.253,0.256,0,5.075 -NAYY 4x1x95,0.4,0.215,0.320,0.261,0,4.098 -NAYY 4x1x50,0.4,0.144,0.449,0.270,0,2.27 -NAYY 4x1x35,0.4,0.123,0.868,0.271,0,1.696 +NAYY 4x1x300,0.4,0.419,0.1,0.279,0,20 +NAYY 4x1x240,0.4,0.364,0.125,0.254,0,12.8 +NAYY 4x1x185,0.4,0.313,0.164,0.256,0,10 +NAYY 4x1x150,0.4,0.275,0.206,0.256,0,8.4 +NAYY 4x1x120,0.4,0.245,0.253,0.256,0,7.7 +NAYY 4x1x95,0.4,0.215,0.320,0.261,0,6.5 +NAYY 4x1x50,0.4,0.144,0.449,0.270,0,6 +NAYY 4x1x35,0.4,0.123,0.868,0.271,0,5 diff --git a/edisgo/equipment/equipment-parameters_MV_cables.csv b/edisgo/equipment/equipment-parameters_MV_cables.csv index 70e265d1d..cea708215 100644 --- a/edisgo/equipment/equipment-parameters_MV_cables.csv +++ b/edisgo/equipment/equipment-parameters_MV_cables.csv @@ -1,12 +1,12 @@ name,U_n,I_max_th,R_per_km,L_per_km,C_per_km,cost #-,kV,kA,ohm/km,mH/km,uF/km,kEuro/km -NA2XS2Y 3x1x185 RM/25,10,0.357,0.164,0.38,0.41,16.88 -NA2XS2Y 3x1x240 RM/25,10,0.417,0.125,0.36,0.47,19.01 -NA2XS2Y 3x1x300 RM/25,10,0.466,0.1,0.35,0.495,21.09 -NA2XS2Y 3x1x400 RM/35,10,0.535,0.078,0.34,0.57,26.53 -NA2XS2Y 3x1x500 RM/35,10,0.609,0.061,0.32,0.63,29.95 -NA2XS2Y 3x1x150 RE/25,20,0.319,0.206,0.4011,0.24,15.54 -NA2XS2Y 3x1x240,20,0.417,0.13,0.3597,0.304,19.01 -NA2XS(FL)2Y 3x1x300 RM/25,20,0.476,0.1,0.37,0.25,21.09 -NA2XS(FL)2Y 3x1x400 RM/35,20,0.525,0.078,0.36,0.27,26.53 -NA2XS(FL)2Y 3x1x500 RM/35,20,0.598,0.06,0.34,0.3,29.95 +NA2XS2Y 3x1x185 RM/25,10,0.357,0.164,0.38,0.41,24 +NA2XS2Y 3x1x240 RM/25,10,0.417,0.125,0.36,0.47,27 +NA2XS2Y 3x1x300 RM/25,10,0.466,0.1,0.35,0.495,39 +NA2XS2Y 3x1x400 RM/35,10,0.535,0.078,0.34,0.57,50 +NA2XS2Y 3x1x500 RM/35,10,0.609,0.061,0.32,0.63,60 +NA2XS2Y 3x1x150 RE/25,20,0.319,0.206,0.4011,0.24,39 +NA2XS2Y 3x1x240,20,0.417,0.13,0.3597,0.304,50 +NA2XS(FL)2Y 3x1x300 RM/25,20,0.476,0.1,0.37,0.25,60 +NA2XS(FL)2Y 3x1x400 RM/35,20,0.525,0.078,0.36,0.27,90 +NA2XS(FL)2Y 3x1x500 RM/35,20,0.598,0.06,0.34,0.3,120 diff --git a/edisgo/equipment/equipment-parameters_MV_overhead_lines.csv b/edisgo/equipment/equipment-parameters_MV_overhead_lines.csv index d6ef0ecf4..b4ccb9e4f 100644 --- a/edisgo/equipment/equipment-parameters_MV_overhead_lines.csv +++ b/edisgo/equipment/equipment-parameters_MV_overhead_lines.csv @@ -1,8 +1,8 @@ name,U_n,I_max_th,R_per_km,L_per_km,C_per_km,cost #-,kV,kA,ohm/km,mH/km,uF/km -48-AL1/8-ST1A,10,0.21,0.35,1.11,0.0104,1.33 -94-AL1/15-ST1A,10,0.35,0.33,1.05,0.0112,2.60 -122-AL1/20-ST1A,10,0.41,0.31,0.99,0.0115,3.36 -48-AL1/8-ST1A,20,0.21,0.37,1.18,0.0098,1.33 -94-AL1/15-ST1A,20,0.35,0.35,1.11,0.0104,2.60 -122-AL1/20-ST1A,20,0.41,0.34,1.08,0.0106,3.36 +48-AL1/8-ST1A,10,0.21,0.35,1.11,0.0104,130 +94-AL1/15-ST1A,10,0.35,0.33,1.05,0.0112,160 +122-AL1/20-ST1A,10,0.41,0.31,0.99,0.0115,190 +48-AL1/8-ST1A,20,0.21,0.37,1.18,0.0098,220 +94-AL1/15-ST1A,20,0.35,0.35,1.11,0.0104,250 +122-AL1/20-ST1A,20,0.41,0.34,1.08,0.0106,300 From 894d4d81bfb250c0039696b3670e95235d1e217e Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Fri, 24 Feb 2023 13:47:00 +0100 Subject: [PATCH 39/43] cost update and minor changes in reinforce measures --- .../equipment-parameters_LV_cables.csv | 16 ++-- .../equipment-parameters_MV_cables.csv | 20 ++--- ...equipment-parameters_MV_overhead_lines.csv | 12 +-- edisgo/flex_opt/reinforce_measures.py | 88 +++++++++++++++---- 4 files changed, 93 insertions(+), 43 deletions(-) diff --git a/edisgo/equipment/equipment-parameters_LV_cables.csv b/edisgo/equipment/equipment-parameters_LV_cables.csv index 02fdf58f2..58bccd228 100644 --- a/edisgo/equipment/equipment-parameters_LV_cables.csv +++ b/edisgo/equipment/equipment-parameters_LV_cables.csv @@ -1,10 +1,10 @@ name,U_n,I_max_th,R_per_km,L_per_km,C_per_km,cost #-,kV,kA,ohm/km,mH/km,uF/km,kEuro/km -NAYY 4x1x300,0.4,0.419,0.1,0.279,0,20 -NAYY 4x1x240,0.4,0.364,0.125,0.254,0,12.8 -NAYY 4x1x185,0.4,0.313,0.164,0.256,0,10 -NAYY 4x1x150,0.4,0.275,0.206,0.256,0,8.4 -NAYY 4x1x120,0.4,0.245,0.253,0.256,0,7.7 -NAYY 4x1x95,0.4,0.215,0.320,0.261,0,6.5 -NAYY 4x1x50,0.4,0.144,0.449,0.270,0,6 -NAYY 4x1x35,0.4,0.123,0.868,0.271,0,5 +NAYY 4x1x300,0.4,0.419,0.1,0.279,0,12.75 +NAYY 4x1x240,0.4,0.364,0.125,0.254,0,10.177 +NAYY 4x1x185,0.4,0.313,0.164,0.256,0,7.834 +NAYY 4x1x150,0.4,0.275,0.206,0.256,0,6.25 +NAYY 4x1x120,0.4,0.245,0.253,0.256,0,5.075 +NAYY 4x1x95,0.4,0.215,0.320,0.261,0,4.098 +NAYY 4x1x50,0.4,0.144,0.449,0.270,0,2.27 +NAYY 4x1x35,0.4,0.123,0.868,0.271,0,1.696 diff --git a/edisgo/equipment/equipment-parameters_MV_cables.csv b/edisgo/equipment/equipment-parameters_MV_cables.csv index cea708215..70e265d1d 100644 --- a/edisgo/equipment/equipment-parameters_MV_cables.csv +++ b/edisgo/equipment/equipment-parameters_MV_cables.csv @@ -1,12 +1,12 @@ name,U_n,I_max_th,R_per_km,L_per_km,C_per_km,cost #-,kV,kA,ohm/km,mH/km,uF/km,kEuro/km -NA2XS2Y 3x1x185 RM/25,10,0.357,0.164,0.38,0.41,24 -NA2XS2Y 3x1x240 RM/25,10,0.417,0.125,0.36,0.47,27 -NA2XS2Y 3x1x300 RM/25,10,0.466,0.1,0.35,0.495,39 -NA2XS2Y 3x1x400 RM/35,10,0.535,0.078,0.34,0.57,50 -NA2XS2Y 3x1x500 RM/35,10,0.609,0.061,0.32,0.63,60 -NA2XS2Y 3x1x150 RE/25,20,0.319,0.206,0.4011,0.24,39 -NA2XS2Y 3x1x240,20,0.417,0.13,0.3597,0.304,50 -NA2XS(FL)2Y 3x1x300 RM/25,20,0.476,0.1,0.37,0.25,60 -NA2XS(FL)2Y 3x1x400 RM/35,20,0.525,0.078,0.36,0.27,90 -NA2XS(FL)2Y 3x1x500 RM/35,20,0.598,0.06,0.34,0.3,120 +NA2XS2Y 3x1x185 RM/25,10,0.357,0.164,0.38,0.41,16.88 +NA2XS2Y 3x1x240 RM/25,10,0.417,0.125,0.36,0.47,19.01 +NA2XS2Y 3x1x300 RM/25,10,0.466,0.1,0.35,0.495,21.09 +NA2XS2Y 3x1x400 RM/35,10,0.535,0.078,0.34,0.57,26.53 +NA2XS2Y 3x1x500 RM/35,10,0.609,0.061,0.32,0.63,29.95 +NA2XS2Y 3x1x150 RE/25,20,0.319,0.206,0.4011,0.24,15.54 +NA2XS2Y 3x1x240,20,0.417,0.13,0.3597,0.304,19.01 +NA2XS(FL)2Y 3x1x300 RM/25,20,0.476,0.1,0.37,0.25,21.09 +NA2XS(FL)2Y 3x1x400 RM/35,20,0.525,0.078,0.36,0.27,26.53 +NA2XS(FL)2Y 3x1x500 RM/35,20,0.598,0.06,0.34,0.3,29.95 diff --git a/edisgo/equipment/equipment-parameters_MV_overhead_lines.csv b/edisgo/equipment/equipment-parameters_MV_overhead_lines.csv index b4ccb9e4f..d6ef0ecf4 100644 --- a/edisgo/equipment/equipment-parameters_MV_overhead_lines.csv +++ b/edisgo/equipment/equipment-parameters_MV_overhead_lines.csv @@ -1,8 +1,8 @@ name,U_n,I_max_th,R_per_km,L_per_km,C_per_km,cost #-,kV,kA,ohm/km,mH/km,uF/km -48-AL1/8-ST1A,10,0.21,0.35,1.11,0.0104,130 -94-AL1/15-ST1A,10,0.35,0.33,1.05,0.0112,160 -122-AL1/20-ST1A,10,0.41,0.31,0.99,0.0115,190 -48-AL1/8-ST1A,20,0.21,0.37,1.18,0.0098,220 -94-AL1/15-ST1A,20,0.35,0.35,1.11,0.0104,250 -122-AL1/20-ST1A,20,0.41,0.34,1.08,0.0106,300 +48-AL1/8-ST1A,10,0.21,0.35,1.11,0.0104,1.33 +94-AL1/15-ST1A,10,0.35,0.33,1.05,0.0112,2.60 +122-AL1/20-ST1A,10,0.41,0.31,0.99,0.0115,3.36 +48-AL1/8-ST1A,20,0.21,0.37,1.18,0.0098,1.33 +94-AL1/15-ST1A,20,0.35,0.35,1.11,0.0104,2.60 +122-AL1/20-ST1A,20,0.41,0.34,1.08,0.0106,3.36 diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index ab45a60be..c99e6cae0 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -1604,18 +1604,12 @@ def _change_dataframe(cb_new_closed, cb_old_df): # add all the loads and gens to the dicts for node in G.nodes: # for Bus-bars - if "BusBar" in node: + if node in edisgo_obj.topology.transformers_df.bus0.values: # the lv_side of node - if "virtual" in node: - bus_node_lv = edisgo_obj.topology.transformers_df[ - edisgo_obj.topology.transformers_df.bus0 - == node.replace("virtual_", "") - ].bus1[0] - else: - bus_node_lv = edisgo_obj.topology.transformers_df[ - edisgo_obj.topology.transformers_df.bus0 == node - ].bus1[0] - # grid_id + bus_node_lv = edisgo_obj.topology.transformers_df[ + edisgo_obj.topology.transformers_df.bus0 == node + ].bus1[0] + grid_id = edisgo_obj.topology.buses_df[ edisgo_obj.topology.buses_df.index.values == bus_node_lv ].lv_grid_id[0] @@ -1627,15 +1621,40 @@ def _change_dataframe(cb_new_closed, cb_old_df): ) node_peak_load_dict[node] = lv_grid.loads_df.p_set.sum() / cos_phi_load + elif node in edisgo_obj.topology.switches_df.bus_open.values: + + bus_open = edisgo_obj.topology.switches_df[ + edisgo_obj.topology.switches_df.bus_open == node + ].bus_closed[0] + bus_node_lv = edisgo_obj.topology.transformers_df[ + edisgo_obj.topology.transformers_df.bus0 == bus_open + ].bus1[0] + # grid_id + grid_id = edisgo_obj.topology.buses_df[ + edisgo_obj.topology.buses_df.index.values == bus_node_lv + ].lv_grid_id[0] + # get lv_grid + lv_grid = edisgo_obj.topology.get_lv_grid(int(grid_id)) + + node_peak_gen_dict[node] = 0 + node_peak_load_dict[node] = 0 # Generators - elif "gen" in node: + elif node in edisgo_obj.topology.generators_df.bus.values: node_peak_gen_dict[node] = ( - edisgo_obj.topology.mv_grid.generators_df[ - edisgo_obj.topology.mv_grid.generators_df.bus == node + edisgo_obj.topology.generators_df[ + edisgo_obj.topology.generators_df.bus == node ].p_nom.sum() / cos_phi_feedin ) node_peak_load_dict[node] = 0 + elif node in edisgo_obj.topology.loads_df.bus.values: + node_peak_load_dict[node] = ( + edisgo_obj.topology.loads_df[ + edisgo_obj.topology.loads_df.bus == node + ].p_set.sum() + / cos_phi_feedin + ) + node_peak_gen_dict[node] = 0 # branchTees do not have any load and generation else: @@ -1683,7 +1702,13 @@ def _change_dataframe(cb_new_closed, cb_old_df): # if none of the nodes is of the type LVStation, a switch # disconnecter will be installed anyways. - if any([node for node in ring if "BusBar" in node]): + if any( + [ + node + for node in ring + if node in edisgo_obj.topology.transformers_df.bus0.values + ] + ): has_lv_station = True else: has_lv_station = False @@ -1701,7 +1726,10 @@ def _change_dataframe(cb_new_closed, cb_old_df): # check if node that owns the switch disconnector is of type # LVStation - if "BusBar" in ring[ctr] or not has_lv_station: + if ( + ring[ctr] in edisgo_obj.topology.transformers_df.bus0.values + or not has_lv_station + ): # split route and calc demand difference route_data_part1 = sum(node_peak_data[0:ctr]) route_data_part2 = sum(node_peak_data[ctr : len(node_peak_data)]) @@ -1746,7 +1774,7 @@ def _change_dataframe(cb_new_closed, cb_old_df): if len(circuit_breaker_changes): logger.info( f"{grid}==>{len(circuit_breaker_changes)} circuit breakers are " - f"relocated in " + f"relocated " ) else: logger.info(f"{grid}==>no circuit breaker is relocated") @@ -1940,14 +1968,14 @@ def split_feeder_at_2_3_length(edisgo_obj, grid, crit_nodes, split_mode="forward edisgo_obj.topology._lines_df.at[line_removed, "bus0"] = station_node logger.info( - f"==> {grid}--> the line {line_removed} disconnected from " + f"{grid}--> the line {line_removed} disconnected from " f"{pred_node} and connected to the main station {station_node} " ) elif grid.lines_df.at[line_removed, "bus1"] == pred_node: edisgo_obj.topology._lines_df.at[line_removed, "bus1"] = station_node logger.info( - f"==> {grid}==>the line {line_removed} disconnected from " + f"{grid}==>the line {line_removed} disconnected from " f"{pred_node} and connected to the main station {station_node} " ) else: @@ -2323,3 +2351,25 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): lines_changes = {} return transformers_changes, lines_changes + + +def add_same_type_parallel_line_voltage_issue(edisgo_obj, grid, crit_nodes): + logger.info( + f"{grid}:==>method:add_same_type_parallel_line_voltage_issue is running" + ) + G = grid.graph + station_node = list(G.nodes)[0] + most_crit_node = crit_nodes[ + crit_nodes.v_diff_max == crit_nodes.v_diff_max.max() + ].index[0] + path = nx.shortest_path(G, station_node, most_crit_node) + + crit_lines = {} + for ctr in range(len(path) - 1): + lines = G.get_edge_data(path[ctr], path[ctr + 1])["branch_name"] + crit_lines[lines] = 1 + crit_lines = pd.Series(crit_lines) + + lines_changes = add_same_type_of_parallel_line(edisgo_obj, crit_lines) + + return lines_changes From 6b967a75db1b4f4e57722e4510d9dc6c38992193 Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Fri, 24 Feb 2023 13:50:12 +0100 Subject: [PATCH 40/43] Structure updated --- edisgo/flex_opt/reinforce_grid_alternative.py | 316 ++++++++++-------- 1 file changed, 178 insertions(+), 138 deletions(-) diff --git a/edisgo/flex_opt/reinforce_grid_alternative.py b/edisgo/flex_opt/reinforce_grid_alternative.py index cf9a69144..33f067d0d 100644 --- a/edisgo/flex_opt/reinforce_grid_alternative.py +++ b/edisgo/flex_opt/reinforce_grid_alternative.py @@ -584,6 +584,7 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): methods = [ "split_feeder_at_2_3_length", "add_station_at_2_3_length", + "add_same_type_parallel_line_voltage_issue", ] if add_method is None: @@ -604,7 +605,8 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): # 1.Voltage level= MV logger.debug(f"{edisgo_reinforce.topology.mv_grid}==>Check voltage in MV topology.") - voltage_level_mv = "mv" + + voltage_level_mv = "mv_lv" if combined_analysis else "mv" # The nodes that have voltage issue crit_nodes_mv = checks.mv_voltage_deviation( @@ -619,88 +621,113 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): if (not voltage_level or voltage_level == "mv") and crit_nodes_mv: # 1.1Method:Split the feeder at the 2_3-length of the feeder (applied several # times till all the voltage issues are remedied - while_counter = 0 - while crit_nodes_mv and while_counter < max_while_iterations: - if ( - add_method == ["add_station_at_2_3_length"] - and voltage_level is not None - ): - raise exceptions.Error( - f"{edisgo_reinforce.topology.mv_grid}==>method" - f":add_station_at_2_3_length is only applicable for LV " - "grids" - ) - elif add_method == ["add_station_at_2_3_length"] and voltage_level is None: - logger.error( - f"{edisgo_reinforce.topology.mv_grid}==>method" - f":add_station_at_2_3_length is only applicable for LV grids " - ) - while_counter = max_while_iterations - else: - logger.error( - f"{edisgo_reinforce.topology.mv_grid}==>method" - f":add_station_at_2_3_length is only applicable for LV grids " - ) - if "split_feeder_at_2_3_length" in add_method: - logger.info( - f"{edisgo_reinforce.topology.mv_grid}==>method" - f":split_feeder_at_2_3_length is running " - ) - lines_changes = reinforce_measures.split_feeder_at_2_3_length( - edisgo_reinforce, - edisgo_reinforce.topology.mv_grid, - crit_nodes_mv[repr(edisgo_reinforce.topology.mv_grid)], - split_mode=split_mode, - ) - # write changed lines to results.equipment_changes - _add_lines_changes_to_equipment_changes() - - # run power flow analysis again (after updating pypsa object) and check - # if all over-voltage problems were solved - logger.debug( - f"{edisgo_reinforce.topology.mv_grid}==>Run power flow analysis." + if add_method == ["add_station_at_2_3_length"] and voltage_level is not None: + raise exceptions.Error( + f"{edisgo_reinforce.topology.mv_grid}==>method" + f":add_station_at_2_3_length is only applicable for LV " + "grids" ) - edisgo_reinforce.analyze(timesteps=timesteps_pfa) - - logger.debug( - f"{edisgo_reinforce.topology.mv_grid}==> Recheck voltage in MV grid." + elif add_method == ["add_station_at_2_3_length"] and voltage_level is None: + logger.error( + f"{edisgo_reinforce.topology.mv_grid}==>method" + f":add_station_at_2_3_length is only applicable for LV grids " ) - crit_nodes_mv = checks.mv_voltage_deviation( - edisgo_reinforce, voltage_levels=voltage_level_mv + while_counter = max_while_iterations + elif "add_station_at_2_3_length" in add_method: + logger.error( + f"{edisgo_reinforce.topology.mv_grid}==>method" + f":add_station_at_2_3_length is only applicable for LV grids " ) - - iteration_step += 1 - while_counter += 1 - - # check if all voltage problems were solved after maximum number of - # iterations allowed - if while_counter == max_while_iterations and crit_nodes_mv: - edisgo_reinforce.results.unresolved_issues = pd.concat( - [ - edisgo_reinforce.results.unresolved_issues, - pd.concat([_ for _ in crit_nodes_mv.values()]), - ] + if "split_feeder_at_2_3_length" in add_method: + logger.info( + f"{edisgo_reinforce.topology.mv_grid}==>method" + f":split_feeder_at_2_3_length is running " ) - logger.info( - f"{edisgo_reinforce.topology.mv_grid}==>Voltage issues for the " - f"following nodes could not be solved in Mv grid since the the number " - f"of max. iteration is reached {crit_nodes_mv.keys()} " + lines_changes = reinforce_measures.split_feeder_at_2_3_length( + edisgo_reinforce, + edisgo_reinforce.topology.mv_grid, + crit_nodes_mv[repr(edisgo_reinforce.topology.mv_grid)], + split_mode=split_mode, ) - elif not crit_nodes_mv: + # write changed lines to results.equipment_changes + _add_lines_changes_to_equipment_changes() + # run power flow analysis again (after updating pypsa object) and check + # if all over-voltage problems were solved + logger.debug(f"{edisgo_reinforce.topology.mv_grid}==>Run power flow analysis.") + edisgo_reinforce.analyze(timesteps=timesteps_pfa) + + logger.debug( + f"{edisgo_reinforce.topology.mv_grid}==> Recheck voltage in MV grid." + ) + crit_nodes_mv = checks.mv_voltage_deviation( + edisgo_reinforce, voltage_levels=voltage_level_mv + ) + + if "add_same_type_parallel_line_voltage_issue" in add_method: logger.info( - f"{edisgo_reinforce.topology.mv_grid}==>Voltage issues were solved in " - f"Mv grid in {while_counter} iteration step(s). " + f"{edisgo_reinforce.topology.mv_grid}==>method" + f":add_same_type_parallel_line_voltage_issue is running " ) + while_counter = 0 + while crit_nodes_mv and while_counter < max_while_iterations: + lines_changes = ( + reinforce_measures.add_same_type_parallel_line_voltage_issue( + edisgo_reinforce, + edisgo_reinforce.topology.mv_grid, + crit_nodes_mv[repr(edisgo_reinforce.topology.mv_grid)], + ) + ) + # write changed lines to results.equipment_changes + _add_lines_changes_to_equipment_changes() + + # run power flow analysis again (after updating pypsa object) + # and check if all over-voltage problems were solved + logger.debug("==>Run power flow analysis.") + edisgo_reinforce.analyze(timesteps=timesteps_pfa) + + logger.debug("==>Recheck voltage in LV grids.") + crit_nodes_mv = checks.mv_voltage_deviation( + edisgo_reinforce, voltage_levels=voltage_level_mv + ) + iteration_step += 1 + while_counter += 1 + + # check if all voltage problems were solved after maximum number of + # iterations allowed + if while_counter == max_while_iterations and crit_nodes_mv: + edisgo_reinforce.results.unresolved_issues = pd.concat( + [ + edisgo_reinforce.results.unresolved_issues, + pd.concat([_ for _ in crit_nodes_mv.values()]), + ] + ) + + logger.info( + f"{edisgo_reinforce.topology.mv_grid}==>Voltage issues for the " + f"following nodes could not be solved in Mv grid since the the " + f"number of max. iteration is reached {crit_nodes_mv.keys()} " + ) + elif not crit_nodes_mv: + logger.info( + f"{edisgo_reinforce.topology.mv_grid}==>Voltage issues were solved " + f"in Mv grid in {iteration_step} iteration step(s). " + ) + if any(crit_nodes_mv): + logger.warning( + f"{edisgo_reinforce.topology.mv_grid}==>Not all overloading issues in" + f" MV grid could be solved. " + ) # 2 Voltage level= LV # todo: If new grid created by the method add # station requires a voltage issue reinforcement, it will # raise an error since the buses and lines name of the moved nodes to # the new grid is not changed. - voltage_level_lv = "lv" + + voltage_level_lv = "mv_lv" if combined_analysis else "lv" logger.debug("==> Check voltage in LV grids.") crit_nodes_lv = checks.lv_voltage_deviation( @@ -713,42 +740,33 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): ) if (not voltage_level or voltage_level == "lv") and crit_nodes_lv: - # 2.1 add new station ( applied only once ) if the number of overloaded lines - # is more than 2 - - while_counter = 0 - while crit_nodes_lv and while_counter < max_while_iterations: - for lv_grid in crit_nodes_lv: - - transformer_changes = {} - lines_changes = {} - if ( - "add_station_at_2_3_length" in add_method and while_counter < 1 - ) or (add_method is None and while_counter < 1): - - logger.info( - f"{lv_grid}:==>method:add_station_at_2_3_length method is " - f"running " - ) - ( - transformer_changes, - lines_changes, - ) = reinforce_measures.add_station_at_2_3_length( - edisgo_reinforce, - edisgo_reinforce.topology.get_lv_grid(lv_grid), - crit_nodes_lv[lv_grid], - ) - if transformer_changes and lines_changes: - # write changed lines and transformers to - # results.equipment_changes - _add_transformer_changes_to_equipment_changes("added") - _add_lines_changes_to_equipment_changes() + for lv_grid in crit_nodes_lv: + transformer_changes = {} + lines_changes = {} + # 2.1 add new station ( applied only once ) if the number of overloaded + # lines is more than 2 + if "add_station_at_2_3_length" in add_method: + logger.info( + f"{lv_grid}:==>method:add_station_at_2_3_length method is " + f"running " + ) + ( + transformer_changes, + lines_changes, + ) = reinforce_measures.add_station_at_2_3_length( + edisgo_reinforce, + edisgo_reinforce.topology.get_lv_grid(lv_grid), + crit_nodes_lv[lv_grid], + ) + if transformer_changes and lines_changes: + # write changed lines and transformers to + # results.equipment_changes + _add_transformer_changes_to_equipment_changes("added") + _add_lines_changes_to_equipment_changes() + else: # 2.2 Method:split_feeder_at_2/3-length of feeder - if ( - "split_feeder_at_2_3_length" in add_method - and not any(transformer_changes) - ) or (add_method is None and not any(transformer_changes)): + if "split_feeder_at_2_3_length" in add_method or add_method is None: logger.info( f"{lv_grid}:==>method:split_feeder_at_2_3_length is running" ) @@ -761,47 +779,69 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): # write changed lines to results.equipment_changes _add_lines_changes_to_equipment_changes() - # run power flow analysis again (after updating pypsa object) - # and check if all over-voltage problems were solved - logger.debug("==>Run power flow analysis.") - edisgo_reinforce.analyze(timesteps=timesteps_pfa) - - logger.debug("==>Recheck voltage in LV grids.") - crit_nodes_lv = checks.lv_voltage_deviation( - edisgo_reinforce, voltage_levels=voltage_level_lv - ) + # run power flow analysis again (after updating pypsa object) + # and check if all over-voltage problems were solved + logger.debug("==>Run power flow analysis.") + edisgo_reinforce.analyze(timesteps=timesteps_pfa) - if add_method == ["add_station_at_2_3_length"]: - while_counter = max_while_iterations - 1 + logger.debug("==>Recheck voltage in LV grids.") + crit_nodes_lv = checks.lv_voltage_deviation( + edisgo_reinforce, voltage_levels=voltage_level_lv + ) + if "add_same_type_parallel_line_voltage_issue" in add_method: + while_counter = 0 + while crit_nodes_lv and while_counter < max_while_iterations: + for lv_grid in crit_nodes_lv: + logger.info( + f"{edisgo_reinforce.topology.mv_grid}==>method:add_same_type_of" + f"_parallel_line is running for LV grid/s_Step{iteration_step}" + ) + lines_changes = ( + reinforce_measures.add_same_type_parallel_line_voltage_issue( + edisgo_reinforce, + edisgo_reinforce.topology.get_lv_grid(lv_grid), + crit_nodes_lv[lv_grid], + ) + ) + # write changed lines to results.equipment_changes + _add_lines_changes_to_equipment_changes() - iteration_step += 1 - while_counter += 1 + # run power flow analysis again (after updating pypsa object) + # and check if all over-voltage problems were solved + logger.debug("==>Run power flow analysis.") + edisgo_reinforce.analyze(timesteps=timesteps_pfa) - # check if all load problems were solved after maximum number of - # iterations allowed - if while_counter == max_while_iterations and crit_nodes_lv: - edisgo_reinforce.results.unresolved_issues = pd.concat( - [ - edisgo_reinforce.results.unresolved_issues, - pd.concat([_ for _ in crit_nodes_lv.values()]), - ] - ) - if add_method == ["add_station_at_2_3_length"]: - logger.error( - f"{edisgo_reinforce.topology.mv_grid}==> Voltage issues in LV " - f"grids could not solve in {while_counter} iteration step(s) " - f"since only method :add_station_at_2_3_length is applied " + logger.debug("==>Recheck voltage in LV grids.") + crit_nodes_lv = checks.lv_voltage_deviation( + edisgo_reinforce, voltage_levels=voltage_level_lv ) - else: - logger.info( - f"{edisgo_reinforce.topology.mv_grid}==>Voltage issues for the" - f"following nodes in LV grids could not be solved: " - f"{crit_nodes_lv.keys()} " - ) - else: - logger.info( - f"{edisgo_reinforce.topology.mv_grid}==>Voltage issues in LV grids " - f"were solved in {while_counter} iteration step(s)." + + iteration_step += 1 + while_counter += 1 + + # check if all load problems were solved after maximum number of + # iterations allowed + if while_counter == max_while_iterations and crit_nodes_lv: + edisgo_reinforce.results.unresolved_issues = pd.concat( + [ + edisgo_reinforce.results.unresolved_issues, + pd.concat([_ for _ in crit_nodes_lv.values()]), + ] + ) + raise exceptions.MaximumIterationError( + f"{edisgo_reinforce.topology.mv_grid}==>Overloading issues " + f"could not be solved after maximum allowed iterations." + ) + else: + logger.info( + f"{edisgo_reinforce.topology.mv_grid}==>Voltage issues in LV " + f"grids were solved in {while_counter} iteration step(s)." + ) + + if any(crit_nodes_lv): + logger.warning( + f"{edisgo_reinforce.topology.mv_grid}==>Not all overloading issues in " + f"LV could be solved. " ) edisgo_reinforce.results.grid_expansion_costs = grid_expansion_costs( From 43c8e907ee39653fdbca9ded1eb302cde84335b5 Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Fri, 21 Apr 2023 10:38:02 +0200 Subject: [PATCH 41/43] Update the cost --- edisgo/config/config_grid_expansion_default.cfg | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/edisgo/config/config_grid_expansion_default.cfg b/edisgo/config/config_grid_expansion_default.cfg index aa88e8529..f03a031c6 100644 --- a/edisgo/config/config_grid_expansion_default.cfg +++ b/edisgo/config/config_grid_expansion_default.cfg @@ -75,14 +75,14 @@ mv_lv_station_feed-in_case_max_v_deviation = 0.015 # ============ # Source: Rehtanz et. al.: "Verteilnetzstudie für das Land Baden-Württemberg", 2017. mv_load_case_transformer = 0.5 -mv_load_case_line = 0.1 +mv_load_case_line = 0.5 mv_feed-in_case_transformer = 1.0 -mv_feed-in_case_line = 0.1 +mv_feed-in_case_line = 1 lv_load_case_transformer = 1.0 -lv_load_case_line = 0.01 +lv_load_case_line = 1 lv_feed-in_case_transformer = 1.0 -lv_feed-in_case_line = 0.01 +lv_feed-in_case_line = 1 # costs # ============ @@ -108,7 +108,8 @@ mv_cable_incl_earthwork_urban = 140 [costs_transformers] # costs in kEUR, source: DENA Verteilnetzstudie -lv = 10 +#LS+installation +lv = 60 mv = 1000 [costs_circuit_breakers] From c8b955131f9db03aad00e2cdc6f1672e150c38c9 Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Fri, 21 Apr 2023 10:39:13 +0200 Subject: [PATCH 42/43] minor fix --- edisgo/flex_opt/costs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/edisgo/flex_opt/costs.py b/edisgo/flex_opt/costs.py index 274269ce8..ed7ea82d5 100644 --- a/edisgo/flex_opt/costs.py +++ b/edisgo/flex_opt/costs.py @@ -420,4 +420,6 @@ def cost_breakdown(edisgo_obj, lines_df): lambda x: x.costs_cable * lines_added.loc[x.name, "quantity"], axis=1, ) + else: + costs_lines = pd.DataFrame() return costs_lines From fcaacb7827f79de850f7dc92ae6aa3ef45aa1f7c Mon Sep 17 00:00:00 2001 From: Batuhan Sanli Date: Fri, 21 Apr 2023 10:55:49 +0200 Subject: [PATCH 43/43] bug fix --- edisgo/flex_opt/reinforce_grid_alternative.py | 23 ++-- edisgo/flex_opt/reinforce_measures.py | 120 ++++++++++++------ 2 files changed, 92 insertions(+), 51 deletions(-) diff --git a/edisgo/flex_opt/reinforce_grid_alternative.py b/edisgo/flex_opt/reinforce_grid_alternative.py index 33f067d0d..f4fd55164 100644 --- a/edisgo/flex_opt/reinforce_grid_alternative.py +++ b/edisgo/flex_opt/reinforce_grid_alternative.py @@ -282,15 +282,20 @@ def _add_circuit_breaker_changes_to_equipment_changes(): if "split_feeder_at_half_length" in add_method or add_method is None: # method-2: split_feeder_at_half_length - - lines_changes = reinforce_measures.split_feeder_at_half_length( - edisgo_reinforce, - edisgo_reinforce.topology.mv_grid, - crit_lines_mv, - split_mode=split_mode, - ) - # write changed lines to results.equipment_changes - _add_lines_changes_to_equipment_changes() + if not crit_lines_mv.empty: + lines_changes = reinforce_measures.split_feeder_at_half_length( + edisgo_reinforce, + edisgo_reinforce.topology.mv_grid, + crit_lines_mv, + split_mode=split_mode, + ) + # write changed lines to results.equipment_changes + _add_lines_changes_to_equipment_changes() + else: + logger.info( + f"{edisgo_reinforce.topology.mv_grid}==> no overloaded lines for " + f"the method:split_feeder_at_half_length " + ) logger.debug("==> Run power flow analysis.") edisgo_reinforce.analyze(timesteps=timesteps_pfa) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index c99e6cae0..4fbbe6eb1 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -1024,9 +1024,9 @@ def split_feeder_at_half_length(edisgo_obj, grid, crit_lines, split_mode="back") def add_station_at_half_length(edisgo_obj, grid, crit_lines): """ - If the number of overloaded feeders in the LV grid is more than 2, the feeders are - split at their half-length, and the disconnected points are connected to the - new MV/LV station. + If the number of overloaded feeders in the LV grid is more than 1(this can be + changed 2 or 3) , the feeders are split at their half-length, and the + disconnected points are connected to the new MV/LV station. 1-The point at half the length of the feeders is found. @@ -1038,7 +1038,8 @@ def add_station_at_half_length(edisgo_obj, grid, crit_lines): preceding node. Notes: - -If the number of overloaded lines in the LV grid is less than 3 and the node_1_2 + -If the number of overloaded lines in the LV grid is less than 2 (this can be + changed 2 or 3) and the node_1_2 is the first node after the main station, the method is not applied. -The name of the new grid will be the existing grid code (e.g. 40000) + 1001 = 400001001 @@ -1256,7 +1257,9 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): lines_changes[line_added_lv] = 1 # removed from exiting LV grid and converted to an MV line between new # and existing MV/LV station - if len(nodes_tb_relocated) > 2 and loop_counter == 0: + + # if the number of overloaded lines is more than 1 + if len(nodes_tb_relocated) > 1 and loop_counter == 0: logger.info(f"{grid}==>method:add_station_at_half_length is running ") # Create the bus-bar name of primary and secondary side of new MV/LV station lv_bus_new = create_bus_name(station_node, "lv") @@ -1366,7 +1369,8 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): f"{grid} and located in new grid{repr(grid) + str(1001)} by method: " f"add_station_at_half_length " ) - if len(lines_changes) < 3: + # if the number of overloaded lines is more than 1 + if len(lines_changes) < 2: lines_changes = {} return transformers_changes, lines_changes @@ -1591,8 +1595,12 @@ def _change_dataframe(cb_new_closed, cb_old_df): f"has not changed" ) - cos_phi_load = edisgo_obj.config["reactive_power_factor"]["mv_load"] - cos_phi_feedin = edisgo_obj.config["reactive_power_factor"]["mv_gen"] + cos_phi_mv_load = edisgo_obj.config["reactive_power_factor"]["mv_load"] + cos_phi_mv_gen = edisgo_obj.config["reactive_power_factor"]["mv_gen"] + cos_phi_mv_cp = edisgo_obj.config["reactive_power_factor"]["mv_cp"] + cos_phi_lv_gen = edisgo_obj.config["reactive_power_factor"]["lv_gen"] + cos_phi_lv_load = edisgo_obj.config["reactive_power_factor"]["lv_load"] + cos_phi_lv_cp = edisgo_obj.config["reactive_power_factor"]["lv_cp"] grid = edisgo_obj.topology.mv_grid G = grid.graph @@ -1605,21 +1613,36 @@ def _change_dataframe(cb_new_closed, cb_old_df): for node in G.nodes: # for Bus-bars if node in edisgo_obj.topology.transformers_df.bus0.values: - # the lv_side of node - bus_node_lv = edisgo_obj.topology.transformers_df[ - edisgo_obj.topology.transformers_df.bus0 == node - ].bus1[0] - - grid_id = edisgo_obj.topology.buses_df[ - edisgo_obj.topology.buses_df.index.values == bus_node_lv - ].lv_grid_id[0] + # for e.g. BranchTee_mvgd_1690_84 + if node in edisgo_obj.topology.mv_grid.generators_df.bus.values: + node_peak_gen_dict[node] = ( + edisgo_obj.topology.mv_grid.generators_df[ + edisgo_obj.topology.mv_grid.generators_df.bus == node + ].p_nom.sum() + / cos_phi_mv_gen + ) + node_peak_load_dict[node] = 0 + else: + # the lv_side of node + bus_node_lv = edisgo_obj.topology.transformers_df[ + edisgo_obj.topology.transformers_df.bus0 == node + ].bus1[0] + + grid_id = edisgo_obj.topology.buses_df[ + edisgo_obj.topology.buses_df.index.values == bus_node_lv + ].lv_grid_id[0] # get lv_grid lv_grid = edisgo_obj.topology.get_lv_grid(int(grid_id)) - node_peak_gen_dict[node] = ( - lv_grid.generators_df.p_nom.sum() / cos_phi_feedin + lv_grid.generators_df.p_nom.sum() / cos_phi_lv_gen + ) + loads_df_new = lv_grid.loads_df.apply( + lambda row: row.loc["p_set"] / cos_phi_lv_load + if row["type"] == "conventional_load" + else row.loc["p_set"] / cos_phi_lv_cp, + axis=1, ) - node_peak_load_dict[node] = lv_grid.loads_df.p_set.sum() / cos_phi_load + node_peak_load_dict[node] = loads_df_new.sum() elif node in edisgo_obj.topology.switches_df.bus_open.values: @@ -1633,27 +1656,31 @@ def _change_dataframe(cb_new_closed, cb_old_df): grid_id = edisgo_obj.topology.buses_df[ edisgo_obj.topology.buses_df.index.values == bus_node_lv ].lv_grid_id[0] - # get lv_grid - lv_grid = edisgo_obj.topology.get_lv_grid(int(grid_id)) node_peak_gen_dict[node] = 0 node_peak_load_dict[node] = 0 # Generators elif node in edisgo_obj.topology.generators_df.bus.values: node_peak_gen_dict[node] = ( - edisgo_obj.topology.generators_df[ - edisgo_obj.topology.generators_df.bus == node + edisgo_obj.topology.mv_grid.generators_df[ + edisgo_obj.topology.mv_grid.generators_df.bus == node ].p_nom.sum() - / cos_phi_feedin + / cos_phi_mv_gen ) node_peak_load_dict[node] = 0 + + # Loads elif node in edisgo_obj.topology.loads_df.bus.values: - node_peak_load_dict[node] = ( - edisgo_obj.topology.loads_df[ - edisgo_obj.topology.loads_df.bus == node - ].p_set.sum() - / cos_phi_feedin + loads_df = edisgo_obj.topology.loads_df[ + edisgo_obj.topology.loads_df.voltage_level == "mv" + ] + loads_df_new = loads_df.apply( + lambda row: row.loc["p_set"] / cos_phi_mv_load + if row["type"] == "conventional_load" + else row.loc["p_set"] / cos_phi_mv_cp, + axis=1, ) + node_peak_load_dict[node] = loads_df_new.sum() node_peak_gen_dict[node] = 0 # branchTees do not have any load and generation @@ -1946,7 +1973,8 @@ def split_feeder_at_2_3_length(edisgo_obj, grid, crit_nodes, split_mode="forward crit_line_name = G.get_edge_data(station_node, node_2_3)["branch_name"] crit_line = grid.lines_df[grid.lines_df.index.isin([crit_line_name])] # add same type of parallel line - lines_changes = add_same_type_of_parallel_line(edisgo_obj, crit_line) + line_added = add_same_type_of_parallel_line(edisgo_obj, crit_line) + lines_changes.update(line_added) logger.info( f"{grid} ==> voltage issue of {crit_line_name} solved by " f"adding same type of parallel line " @@ -1999,9 +2027,10 @@ def split_feeder_at_2_3_length(edisgo_obj, grid, crit_nodes, split_mode="forward def add_station_at_2_3_length(edisgo_obj, grid, crit_nodes): """ todo: docstring to be updated - If the number of overloaded feeders in the LV grid is more than 2, the feeders are - split at their 2/3-length, and the disconnected points are connected to the - new MV/LV station. + + If the number of feeders with voltage issues in the LV grid is more than 1 + (this can be changed 1 or 2), the feeders are split at their 2/3-length, and + the disconnected points are connected to the new MV/LV station. 1-The point at 2/3 the length of the feeders is found. @@ -2009,12 +2038,13 @@ def add_station_at_2_3_length(edisgo_obj, grid, crit_nodes): connection will be made. This node can only be a station. 3-This node is disconnected from the previous node and connected to a new station. 4-New MV/LV is connected to the existing MV/LV station with a line of which length - equals the line length between the node at the half-length (node_2_3) and its + equals the line length between the node at the 2_3 length (node_2_3) and its preceding node. Notes: - -If the number of overloaded lines in the LV grid is less than 3 and the node_2_3 - is the first node after the main station, the method is not applied. + -If the number of lines with voltage issues in the LV grid is less than 2 + (this can be changed 2 or 3) and the node_2_3 is the first node after the + main station, the method is not applied. -The name of the new grid will be the existing grid code (e.g. 40000) + 1001 = 400001001 -The name of the lines in the new LV grid is the same as the grid where the nodes @@ -2234,13 +2264,17 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): ] = [n for n in sub_nodes] nodes_tb_relocated[node_2_3] = nodes_path[nodes_path.index(node_2_3) :] pred_node = path[path.index(node_2_3) - 1] # predecessor node of node_2_3 - - line_removed = G.get_edge_data(node_2_3, pred_node)["branch_name"] # the line - line_added_lv = line_removed - lines_changes[line_added_lv] = 1 + if node_2_3 not in first_nodes_feeders.keys(): + line_removed = G.get_edge_data(node_2_3, pred_node)[ + "branch_name" + ] # the line + line_added_lv = line_removed + lines_changes[line_added_lv] = 1 # removed from exiting LV grid and converted to an MV line between new # and existing MV/LV station - if len(nodes_tb_relocated) > 2 and loop_counter == 0: + + # if the number of lines with voltage issues is more than 1 + if len(nodes_tb_relocated) > 1 and loop_counter == 0: # Create the bus-bar name of primary and secondary side of new MV/LV station lv_bus_new = create_bus_name(station_node, "lv") mv_bus_new = create_bus_name(station_node, "mv") @@ -2347,7 +2381,9 @@ def add_standard_transformer(edisgo_obj, grid, bus_lv, bus_mv): f"{grid} and located in new grid{repr(grid) + str(1001)} by method: " f"add_station_at_2_3_length " ) - if len(lines_changes) < 3: + # if the number of lines with voltage issues is not more than 1, do not add + # the line changes to the dict + if len(lines_changes) < 2: lines_changes = {} return transformers_changes, lines_changes