diff --git a/edisgo/config/config_grid_expansion_default.cfg b/edisgo/config/config_grid_expansion_default.cfg index d57fd40b9..f03a031c6 100644 --- a/edisgo/config/config_grid_expansion_default.cfg +++ b/edisgo/config/config_grid_expansion_default.cfg @@ -77,12 +77,12 @@ mv_lv_station_feed-in_case_max_v_deviation = 0.015 mv_load_case_transformer = 0.5 mv_load_case_line = 0.5 mv_feed-in_case_transformer = 1.0 -mv_feed-in_case_line = 1.0 +mv_feed-in_case_line = 1 lv_load_case_transformer = 1.0 -lv_load_case_line = 1.0 +lv_load_case_line = 1 lv_feed-in_case_transformer = 1.0 -lv_feed-in_case_line = 1.0 +lv_feed-in_case_line = 1 # costs # ============ @@ -108,5 +108,10 @@ 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] + +circuit_breaker_installation_work=10 diff --git a/edisgo/equipment/equipment-parameters_LV_cables.csv b/edisgo/equipment/equipment-parameters_LV_cables.csv index ac72e50fc..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 -#-,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,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_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 diff --git a/edisgo/equipment/equipment-parameters_MV_cables.csv b/edisgo/equipment/equipment-parameters_MV_cables.csv index 86da0d102..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 -#-,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,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 cebce1a7b..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 +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,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/costs.py b/edisgo/flex_opt/costs.py index e7a428678..ed7ea82d5 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: @@ -173,7 +174,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, @@ -183,6 +184,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: @@ -225,6 +263,54 @@ def line_expansion_costs(edisgo_obj, lines_names): 'costs_earthworks', 'costs_cable', 'voltage_level' for each line """ + + 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) + ] + costs_cable = [] + 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] + 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) @@ -243,22 +329,24 @@ 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"]) + 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), @@ -271,7 +359,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), @@ -281,3 +372,54 @@ def line_expansion_costs(edisgo_obj, lines_names): ] ) return costs_lines.loc[lines_df.index] + + +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_df.iloc[ + ( + 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() + 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, + ) + else: + costs_lines = pd.DataFrame() + 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..f4fd55164 --- /dev/null +++ b/edisgo/flex_opt/reinforce_grid_alternative.py @@ -0,0 +1,856 @@ +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, + add_method=None, + timesteps_pfa=None, + copy_grid=False, + voltage_level=None, + loading_mode="load", + split_mode="back", + max_while_iterations=20, + 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. + + MV Grid Reinforcement: + 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 + 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 + The remaining overloaded lines are reinforced by add same type of parallel line + method + + Parameters + ---------- + + 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` + 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. + 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, + 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. + loading_mode: + Type of loading. + 1-'load' + 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` + Returns the Results object holding network expansion costs, equipment + changes, etc. + + Assumptions + ------ + 1-The removing cost of cables are not incorporated. + 2-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(), + ), + ], + ) + + 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) + + 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 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: + edisgo_reinforce = copy.deepcopy(edisgo) + else: + edisgo_reinforce = edisgo + + # 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": + 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 = [ + "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 + + edisgo_reinforce.analyze(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) + 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 any(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). + 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 + ): + logger.error( + 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"{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) + 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 + 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) + + # 1.2- Voltage level= LV + 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 + ): + logger.error( + " method:relocate_circuit_breaker is only applicable for MV grids" + ) + + 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 ) + # if the number of overloaded lines is more than 2 + + ( + transformer_changes, + lines_changes, + ) = reinforce_measures.add_station_at_half_length( + edisgo_reinforce, lv_grid, crit_lines_lv + ) + + 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: + 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) + + 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) + + logger.debug("==> Recheck line load.") + crit_lines = ( + pd.DataFrame(dtype=float) + if voltage_level == "lv" + else checks.mv_line_load(edisgo_reinforce) + ) + + if not voltage_level or voltage_level == "lv": + crit_lines = pd.concat( + [ + crit_lines, + checks.lv_line_load(edisgo_reinforce), + ] + ) + if "add_same_type_of_parallel_line" in add_method or add_method is None: + # 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: + if voltage_level is None: + grid_level = "MV and LV " + else: + grid_level = voltage_level + + logger.info( + 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) + + logger.debug("==> Recheck line load.") + crit_lines = ( + pd.DataFrame(dtype=float) + if voltage_level == "lv" + else checks.mv_line_load(edisgo_reinforce) + ) + + if not voltage_level or voltage_level == "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( + f"{edisgo_reinforce.topology.mv_grid}==>Overloading issues could not " + f"be solved after maximum allowed " + "iterations." + ) + else: + logger.info( + f"{edisgo_reinforce.topology.mv_grid}==> Load issues were solved in " + f"{while_counter} iteration step(s)." + ) + + if not crit_lines.empty: + logger.warning( + f"{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 + ) + + return edisgo_reinforce.results + + +def reinforce_lines_voltage_issues_alternative( + edisgo, + add_method=None, + timesteps_pfa=None, + split_mode="forward", + copy_grid=False, + voltage_level=None, + max_while_iterations=20, + 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. + 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, + 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( + [ + 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 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: + 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_station_at_2_3_length", + "add_same_type_parallel_line_voltage_issue", + ] + + 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 + + edisgo_reinforce.analyze(timesteps=timesteps_pfa) + + # REINFORCE BRANCHES DUE TO VOLTAGE ISSUES + + # 1.Voltage level= MV + logger.debug(f"{edisgo_reinforce.topology.mv_grid}==>Check voltage in MV topology.") + + voltage_level_mv = "mv_lv" if combined_analysis else "mv" + + # The nodes that have voltage issue + crit_nodes_mv = checks.mv_voltage_deviation( + 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 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 + + 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 + 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 " + ) + 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.") + 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}==>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 = "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_level_lv + ) + if not crit_nodes_lv: + logger.info( + f"{edisgo_reinforce.topology.mv_grid}==>there is no critical line in lv " + "grids " + ) + if (not voltage_level or voltage_level == "lv") and crit_nodes_lv: + + 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 or add_method is None: + logger.info( + f"{lv_grid}:==>method:split_feeder_at_2_3_length is running" + ) + 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.") + 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 + ) + 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() + + # 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 + ) + + 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( + 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 2fb896d27..4fbbe6eb1 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 logger = logging.getLogger(__name__) @@ -476,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 @@ -742,3 +743,1669 @@ def _replace_by_parallel_standard_lines(lines): _replace_by_parallel_standard_lines(relevant_lines.index) return lines_changes + + +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 + ------ + + """ + + 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, split_mode="back"): + """ + 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 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_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'). + 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 + ------- + 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 longest route + (not the feeder has more load) + """ + + if isinstance(grid, LVGrid): + + voltage_level = "lv" + relevant_lines = edisgo_obj.topology.lines_df.loc[ + crit_lines[crit_lines.voltage_level == voltage_level].index + ] + + elif isinstance(grid, MVGrid): + + voltage_level = "mv" + # 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 + ] + # TODO:to be deleted after decision + + 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] + + if isinstance(grid, LVGrid): + 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 + # todo:add radial feeders + + paths = {} + nodes_feeder = {} + for node in nodes: + # paths for the open and closed sides of CBs + 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 + + 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] + farthest_node = node_list[-1] + + 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 + ) + + # 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]: + logger.error( + f" {grid}==>{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 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"{grid}==>{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 = nodes_tb_selected[nodes_tb_selected.index(node_1_2) + 1] + except IndexError: + logger.error( + f" {grid}==>{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() and not len(node_1_2) == 0: + 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"] + + # 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 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_tmp[node_1_2] + line_added = line_removed + lines_changes[line_added] = 1 + if lines_changes: + logger.info( + f"{grid}==>{len(lines_changes)} line/s are reinforced by method: " + f"split feeder at half-length" + ) + + return lines_changes + + +def add_station_at_half_length(edisgo_obj, grid, crit_lines): + """ + 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. + 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 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 + -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_weight(u, v, data): + return data["length"] + + 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.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]) + 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 + + relevant_lines = edisgo_obj.topology.lines_df.loc[ + crit_lines[crit_lines.voltage_level == "lv"].index + ] + crit_lines_feeder = relevant_lines[relevant_lines["bus0"] == station_node] + + paths = {} + first_nodes_feeders = {} + + for node in G.nodes: + 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 + first_nodes_feeders.setdefault(path[1], []).append( + node # first nodes and paths + ) + + 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) + + 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=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[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" {grid}==>{first_line} and following lines could not be " + f"reinforced 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 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") + 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_1_2 of one of first + # feeders to be split in LV grid + + 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: + # 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_lv / 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_mv / 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"{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 + + 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_mv, + 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 method: " + f"add_station_at_half_length " + ) + # if the number of overloaded lines is more than 1 + if len(lines_changes) < 2: + lines_changes = {} + + return transformers_changes, lines_changes + + +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 + + 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, 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 + voltage. + + 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 the 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 + + """ + + def _sort_nodes(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 ring in rings: + ring.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 subtree connected to main nodes in the ring + (except main nodes) + + 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 subtree + """ + 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) + + return subtree_dict + + def _get_circuit_breaker_df(ring): + """ + Returns the circuit breaker df of the related ring + + Parameters + ---------- + ring: + obj:'dict` + Dictionary with name of sorted nodes in the ring + Returns + ------- + obj: dict + circuit breaker df + """ + 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 + ] + + return circuit_breaker_df + + def _change_dataframe(cb_new_closed, cb_old_df): + + # 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 G.adj[cb_new_closed]: + branch = G.adj[cb_new_closed][f"virtual_{node2}"]["branch_name"] + else: + branch = G.adj[cb_new_closed][node2]["branch_name"] + + # Update switches_df + # change bus0 + edisgo_obj.topology.switches_df.loc[ + cb_old_df.index[0], "bus_closed" + ] = cb_new_closed + # change bus1 + edisgo_obj.topology.switches_df.loc[ + cb_old_df.index[0], "bus_open" + ] = cb_new_open + # change branch + edisgo_obj.topology.switches_df.loc[cb_old_df.index[0], "branch"] = branch + + # 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 + ) + + # 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" + ) + + 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 + 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 G.nodes: + # for Bus-bars + if node in edisgo_obj.topology.transformers_df.bus0.values: + # 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_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] = loads_df_new.sum() + + 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] + + 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.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 + + # Loads + elif node in edisgo_obj.topology.loads_df.bus.values: + 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 + else: + node_peak_gen_dict[node] = 0 + node_peak_load_dict[node] = 0 + + rings = _sort_nodes(remove_mv_station=True) + for ring in rings: + # 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 + 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]) + + if mode == "load": + node_peak_data = nodes_peak_load + elif mode == "generation": + 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(nodes_peak_load) > sum(nodes_peak_generation): + node_peak_data = nodes_peak_load + else: + node_peak_data = nodes_peak_generation + else: + raise ValueError("parameter 'mode' is invalid!") + + # 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 node in edisgo_obj.topology.transformers_df.bus0.values + ] + ): + has_lv_station = True + else: + has_lv_station = False + logging.debug( + f"{grid}==>Ring {ring} does not have a LV station." + f"Switch disconnecter is installed at arbitrary " + "node." + ) + + # 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 ( + 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)]) + # 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 + cb_new_closed = ring[position] + + # check if node is last node of ring + 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 = ring[position + 1] + else: + 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"{grid}==>{len(circuit_breaker_changes)} circuit breakers are " + f"relocated " + ) + else: + logger.info(f"{grid}==>no circuit breaker is relocated") + return circuit_breaker_changes + + +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 + + 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. + 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 + ------- + 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 = {} + crit_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"{grid}==>Voltage issues at busbar in LV network " + f"should have been solved in previous steps." + ) + crit_nodes_feeder.setdefault(path[1], []).append(node) + + lines_changes = {} + 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 c_node in crit_nodes_feeder[repr_node]: + path_length_dict_tmp = dijkstra_shortest_path_length( + G, station_node, get_weight, target=c_node + ) + 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] + + # 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 + ) + # 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): + 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" {grid}==>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: + 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: + node_2_3 = nodes_tb_selected[nodes_tb_selected.index(node_2_3) + 1] + except IndexError: + 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 + 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 crit_nodes_feeder.keys(): + 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 + 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 " + ) + 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 + + if lines_changes: + logger.info( + f"{grid}==>{len(lines_changes)} line/s are reinforced by method: " + f"split feeder at 2_3-length" + ) + return lines_changes + + +def add_station_at_2_3_length(edisgo_obj, grid, crit_nodes): + """ + todo: docstring to be updated + + 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. + 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 2_3 length (node_2_3) and its + preceding node. + + Notes: + -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 + 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 = {} + crit_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"{grid}==>Voltage issues at busbar in LV network should have " + "been solved in previous steps." + ) + 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( + crit_nodes_feeder.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 c_node in first_nodes_feeders[first_node]: + path_length_dict_tmp = dijkstra_shortest_path_length( + G, station_node, get_weight, target=c_node + ) + 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] + + 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" {grid}==>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(): + 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 + 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 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") + + # 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_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) + # 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_lv / 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_mv / 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"{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 + + 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_mv, + 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 method: " + f"add_station_at_2_3_length " + ) + # 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 + + +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 diff --git a/edisgo/network/topology.py b/edisgo/network/topology.py index 19df7ada9..57dd1744b 100755 --- a/edisgo/network/topology.py +++ b/edisgo/network/topology.py @@ -1473,8 +1473,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 diff --git a/edisgo/tools/plots.py b/edisgo/tools/plots.py index 85eca050a..902832dff 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,