From 1afb2be95191d5a39220d0bc4ca4a810daf9a6dc Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Wed, 3 May 2023 14:09:38 +0200 Subject: [PATCH 01/23] added lv grid separation functionality --- edisgo/flex_opt/reinforce_measures.py | 382 ++++++++++++++++++++++++++ 1 file changed, 382 insertions(+) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index 56cb9d8a..7b9dc11d 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -1,6 +1,10 @@ +from __future__ import annotations + import logging import math +from typing import TYPE_CHECKING, Any + import networkx as nx import numpy as np import pandas as pd @@ -11,6 +15,9 @@ from edisgo.network.grids import LVGrid, MVGrid +if TYPE_CHECKING: + from edisgo import EDisGo + logger = logging.getLogger(__name__) @@ -725,3 +732,378 @@ def _replace_by_parallel_standard_lines(lines): _replace_by_parallel_standard_lines(relevant_lines.index) return lines_changes + + +# TODO: check docstrings +def separate_lv_grid( + edisgo_obj: EDisGo, grid: LVGrid +) -> tuple[dict[Any, Any], dict[str, int]]: + """ + Split LV grid by adding a new substation and connect half of each feeder. + + 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 : :class:`~.network.grids.MVGrid` or :class:`~.network.grids.LVGrid` + + 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: dict) -> float: + return data["length"] + + def create_bus_name(bus: str, voltage_level: str) -> str: + """ + Create an LV and MV bus-bar name with the same grid_id but added '1001' that + implies the separation + + Parameters + ---------- + bus : str + Bus name. E.g. 'BusBar_mvgd_460_lvgd_131573_LV' + voltage_level : str + '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] = f"{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( + f"Voltage level can only be 'mv' or 'lv'. Voltage level used: " + f"{voltage_level}." + ) + else: + raise IndexError(f"Station bus {bus} is not within the buses DataFrame.") + + return bus + + def add_standard_transformer( + edisgo_obj: EDisGo, grid: LVGrid, bus_lv: str, bus_mv: str + ) -> dict: + """ + 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] = f"{str(grid.id)}1001" + + transformer_s.s_nom = standard_transformer.S_nom + transformer_s.type_info = None + 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 = grid.lines_df.loc[ + (grid.lines_df.bus0 == station_node) | (grid.lines_df.bus1 == station_node) + ] + + logger.debug(f"{grid} has {len(relevant_lines)} feeder.") + + paths = {} + first_nodes_feeders = {} + + for node in G.nodes: + path = nx.shortest_path(G, station_node, node) + + for first_node in relevant_lines.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 = dict( + sorted( + first_nodes_feeders.items(), key=lambda item: len(item[1]), reverse=False + ) + ) + + count_inept = 0 + + for first_node, nodes_feeder in first_nodes_feeders.items(): + # first line of the feeder + first_line = relevant_lines[relevant_lines.bus1 == first_node].index[0] + + # the last node of the feeder + last_node = nodes_feeder[-1] + + # the length of each line (the shortest path) + path_length_dict_tmp = dijkstra_shortest_path_length( + G, station_node, get_weight, target=last_node + ) + + # path does not include the nodes branching from the node on the main path + path = paths[last_node] + + 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 + 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]: + raise NotImplementedError( + f" {grid}==>{first_line} and following lines could not be " + f"reinforced due to insufficient number of node in the feeder. " + f"A method to handle such cases is not yet implemented." + ) + + # NOTE: If node_1_2 is a representative (meaning it is already directly + # connected to the station) feeder cannot be split. Instead, every second + # inept feeder is assigned to the new grid + if node_1_2 not in first_nodes_feeders or count_inept % 2 == 1: + nodes_tb_relocated[node_1_2] = nodes_feeder[nodes_feeder.index(node_1_2) :] + + if node_1_2 in first_nodes_feeders: + count_inept += 1 + else: + count_inept += 1 + + if nodes_tb_relocated: + 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.at[ + grid.transformers_df.bus1[0], + "v_nom", + ] + v_nom_mv = edisgo_obj.topology.buses_df.at[ + grid.transformers_df.bus0[0], + "v_nom", + ] + + x_bus = grid.buses_df.at[station_node, "x"] + y_bus = grid.buses_df.at[station_node, "y"] + + # the new lv grid id: e.g. 496021001 + lv_grid_id_new = int(f"{str(grid.id)}1001") + building_bus = grid.buses_df.at[station_node, "in_building"] + + dist = 0.001 + + length_mv = np.sqrt(dist**2 + dist**2) + + # add lv busbar + edisgo_obj.topology.add_bus( + lv_bus_new, + v_nom_lv, + x=x_bus + dist, + y=y_bus + dist, + 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 + dist, + y=y_bus + dist, + 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.add_component( + comp_type="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 + + lv_standard_line = edisgo_obj.config["grid_expansion_standard_equipment"][ + "lv_line" + ] + + # changes on relocated lines to the new LV grid + # grid_ids + for node_1_2, nodes in nodes_tb_relocated.items(): + # the last node of the feeder + last_node = nodes[-1] + + # path does not include the nodes branching from the node on the main path + path = paths[last_node] + + nodes.append(node_1_2) + + edisgo_obj.topology.buses_df.loc[nodes, "lv_grid_id"] = lv_grid_id_new + + dist = dijkstra_shortest_path_length( + G, station_node, get_weight, target=node_1_2 + )[node_1_2] + + line_added_lv = edisgo_obj.add_component( + comp_type="line", + bus0=lv_bus_new, + bus1=node_1_2, + length=dist, + type_info=lv_standard_line, + ) + + lines_changes[line_added_lv] = 1 + + # predecessor node of node_1_2 + pred_node = path[path.index(node_1_2) - 1] + + # the line + line_removed = G.get_edge_data(node_1_2, pred_node)["branch_name"] + + edisgo_obj.remove_component( + comp_type="line", + comp_name=line_removed, + ) + + 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 " + ) + + else: + logger.warning( + f"{grid} was not reinforced because it has to few suitable feeders." + ) + + # TODO: check if changes are tracked correctly + return transformers_changes, lines_changes From ea772078d00ffa550620316410f7054511501eef Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Wed, 3 May 2023 14:17:19 +0200 Subject: [PATCH 02/23] added tests for lv grid separation --- edisgo/flex_opt/reinforce_measures.py | 1 + tests/flex_opt/test_reinforce_measures.py | 60 ++++++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index 7b9dc11d..bb6cf573 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -805,6 +805,7 @@ def create_bus_name(bus: str, voltage_level: str) -> str: if bus in edisgo_obj.topology.buses_df.index: bus = bus.split("_") grid_id_ind = bus.index(str(grid.id)) + # TODO: how to name new grids? bus[grid_id_ind] = f"{grid.id}1001" if voltage_level == "lv": diff --git a/tests/flex_opt/test_reinforce_measures.py b/tests/flex_opt/test_reinforce_measures.py index 40acf4ea..7696edf5 100644 --- a/tests/flex_opt/test_reinforce_measures.py +++ b/tests/flex_opt/test_reinforce_measures.py @@ -5,7 +5,7 @@ import pytest from edisgo import EDisGo -from edisgo.flex_opt import reinforce_measures +from edisgo.flex_opt import check_tech_constraints, reinforce_measures class TestReinforceMeasures: @@ -453,3 +453,61 @@ def test_reinforce_lines_overloading(self): assert np.isclose(line.x, 0.256 * 2 * np.pi * 50 / 1e3 * line.length) assert np.isclose(line.s_nom, 0.275 * 0.4 * np.sqrt(3)) assert line.num_parallel == 1 + + def test_separate_lv_grid(self): + self.edisgo = copy.deepcopy(self.edisgo_root) + + crit_lines_lv = check_tech_constraints.lv_line_max_relative_overload( + self.edisgo + ) + + lv_grid_ids = ( + self.edisgo.topology.buses_df.loc[ + self.edisgo.topology.lines_df.loc[crit_lines_lv.index].bus0 + ] + .lv_grid_id.unique() + .astype(int) + ) + + lv_grids = [ + lv_grid + for lv_grid in self.edisgo.topology.mv_grid.lv_grids + if lv_grid.id in lv_grid_ids + ] + + for lv_grid in lv_grids: + orig_g = copy.deepcopy(lv_grid) + grid_id = orig_g.id + + reinforce_measures.separate_lv_grid(self.edisgo, lv_grid) + + new_g_0 = [ + g for g in self.edisgo.topology.mv_grid.lv_grids if g.id == grid_id + ][0] + + try: + new_g_1 = [ + g + for g in self.edisgo.topology.mv_grid.lv_grids + if g.id == int(str(grid_id) + "1001") + ][0] + except IndexError: + continue + + assert np.isclose( + orig_g.charging_points_df.p_set.sum(), + new_g_0.charging_points_df.p_set.sum() + + new_g_1.charging_points_df.p_set.sum(), + ) + + assert np.isclose( + orig_g.generators_df.p_nom.sum(), + new_g_0.generators_df.p_nom.sum() + new_g_1.generators_df.p_nom.sum(), + ) + + assert np.isclose( + orig_g.loads_df.p_set.sum(), + new_g_0.loads_df.p_set.sum() + new_g_1.loads_df.p_set.sum(), + ) + + assert len(orig_g.lines_df) == len(new_g_0.lines_df) + len(new_g_1.lines_df) From a6793c222ee7d9ec1387365a6927997a4709024e Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Fri, 5 May 2023 10:38:35 +0200 Subject: [PATCH 03/23] Added workflow to separate all lv grids Workflow separates lv grids if a specific overloading threshold is reached. LV grids can be separated multiple times if necessary. Workflow is now a selectable part of the enhanced reinforcement method. Added simple tests. --- edisgo/edisgo.py | 14 ++- edisgo/flex_opt/reinforce_grid.py | 117 ++++++++++++++++++++++++-- edisgo/flex_opt/reinforce_measures.py | 113 +++++++++++++++++++++---- edisgo/io/electromobility_import.py | 14 ++- edisgo/network/components.py | 2 - tests/flex_opt/test_reinforce_grid.py | 38 ++++++++- tests/test_edisgo.py | 6 +- 7 files changed, 264 insertions(+), 40 deletions(-) diff --git a/edisgo/edisgo.py b/edisgo/edisgo.py index 700153bc..bc1084e9 100755 --- a/edisgo/edisgo.py +++ b/edisgo/edisgo.py @@ -1261,12 +1261,15 @@ def reinforce( run_analyze_at_the_end = False logger.info(f"Run the following reinforcements: {setting_list=}") + for setting in setting_list: logger.info(f"Run the following reinforcement: {setting=}") - if not catch_convergence_problems: - func = reinforce_grid - else: - func = catch_convergence_reinforce_grid + func = ( + catch_convergence_reinforce_grid + if catch_convergence_problems + else reinforce_grid + ) + func( edisgo_obj, max_while_iterations=max_while_iterations, @@ -1275,14 +1278,17 @@ def reinforce( n_minus_one=n_minus_one, **setting, ) + if run_analyze_at_the_end: lv_grid_id = kwargs.get("lv_grid_id", None) + if mode == "lv" and lv_grid_id: analyze_mode = "lv" elif mode == "lv": analyze_mode = None else: analyze_mode = mode + edisgo_obj.analyze( mode=analyze_mode, lv_grid_id=lv_grid_id, timesteps=timesteps_pfa ) diff --git a/edisgo/flex_opt/reinforce_grid.py b/edisgo/flex_opt/reinforce_grid.py index b3f06adb..974dab88 100644 --- a/edisgo/flex_opt/reinforce_grid.py +++ b/edisgo/flex_opt/reinforce_grid.py @@ -4,13 +4,16 @@ import datetime import logging +from numbers import Number from typing import TYPE_CHECKING +import numpy as np 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.flex_opt.reinforce_measures import separate_lv_grid from edisgo.tools import tools from edisgo.tools.temporal_complexity_reduction import get_most_critical_time_steps @@ -244,7 +247,6 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): or not overloaded_lv_stations.empty or not crit_lines.empty ) and while_counter < max_while_iterations: - if not overloaded_mv_station.empty: # reinforce substations transformer_changes = ( @@ -351,7 +353,6 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): while_counter = 0 while not crit_nodes.empty and while_counter < max_while_iterations: - # reinforce lines lines_changes = reinforce_measures.reinforce_lines_voltage_issues( edisgo, @@ -556,7 +557,6 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): or not overloaded_lv_stations.empty or not crit_lines.empty ) and while_counter < max_while_iterations: - if not overloaded_mv_station.empty: # reinforce substations transformer_changes = ( @@ -819,8 +819,13 @@ def reinforce(): return edisgo.results +# TODO: adapt docstring def enhanced_reinforce_grid( - edisgo_object: EDisGo, activate_cost_results_disturbing_mode: bool = False, **kwargs + edisgo_object: EDisGo, + activate_cost_results_disturbing_mode: bool = False, + separate_lv_grids: bool = True, + separation_threshold: Number = 2, + **kwargs, ) -> EDisGo: """ Reinforcement strategy to reinforce grids voltage level by voltage level in case @@ -861,30 +866,40 @@ def enhanced_reinforce_grid( edisgo_obj = copy.deepcopy(edisgo_object) else: edisgo_obj = edisgo_object + kwargs["copy_grid"] = False kwargs.pop("skip_mv_reinforcement", False) num_lv_grids_standard_lines = 0 num_lv_grids_aggregated = 0 + if separate_lv_grids: + logger.info( + "Separating lv grids. Set the parameter 'separate_lv_grids' to False if " + "this is not desired." + ) + run_separate_lv_grids(edisgo_obj, threshold=separation_threshold) + try: logger.info("Try initial enhanced reinforcement.") edisgo_obj.reinforce(mode=None, catch_convergence_problems=True, **kwargs) logger.info("Initial enhanced reinforcement succeeded.") - except: # noqa: E722 + except ValueError: logger.info("Initial enhanced reinforcement failed.") logger.info("Try mode 'mv' reinforcement.") + try: edisgo_obj.reinforce(mode="mv", catch_convergence_problems=True, **kwargs) logger.info("Mode 'mv' reinforcement succeeded.") - except: # noqa: E722 + except ValueError: logger.info("Mode 'mv' reinforcement failed.") logger.info("Try mode 'mvlv' reinforcement.") + try: edisgo_obj.reinforce(mode="mvlv", catch_convergence_problems=True, **kwargs) logger.info("Mode 'mvlv' reinforcement succeeded.") - except: # noqa: E722 + except ValueError: logger.info("Mode 'mvlv' reinforcement failed.") for lv_grid in list(edisgo_obj.topology.mv_grid.lv_grids): @@ -967,3 +982,91 @@ def enhanced_reinforce_grid( edisgo_obj.results.measures = msg return edisgo_obj + + +def run_separate_lv_grids(edisgo_obj: EDisGo, threshold: Number = 2) -> None: + lv_grids = list(edisgo_obj.topology.mv_grid.lv_grids) + n_grids_init = len(lv_grids) + + first_run = True + + active_str = "{}_active_power" + reactive_str = "{}_reactive_power" + tech_str = "{}_df" + techs = ["generators", "loads", "storage_units"] + + n = 0 + + while n_grids_init != len(list(edisgo_obj.topology.mv_grid.lv_grids)) or first_run: + print(n) + n += 1 + first_run = False + + lv_grids = list(edisgo_obj.topology.mv_grid.lv_grids) + n_grids_init = len(lv_grids) + + for lv_grid in lv_grids: + tech_dict = {} + + for tech in techs: + units = getattr(lv_grid, tech_str.format(tech)).index + active_power = ( + getattr( + edisgo_obj.timeseries, + active_str.format(tech), + ) + .loc[:, units] + .astype(float) + ) + + reactive_power = ( + getattr( + edisgo_obj.timeseries, + reactive_str.format(tech), + ) + .loc[:, units] + .astype(float) + ) + + if tech == "storage_units": + tech_dict[tech + "_loads"] = np.hypot( + active_power.clip(upper=0.0), + reactive_power.clip(upper=0.0), + ) + + tech_dict[tech + "_generators"] = np.hypot( + active_power.clip(lower=0.0), + reactive_power.clip(lower=0.0), + ) + else: + tech_dict[tech] = np.hypot(active_power, reactive_power) + + load = pd.concat( + [ + tech_dict["loads"], + tech_dict["storage_units_loads"], + ], + axis=1, + ).sum(axis=1) + + gen = pd.concat( + [ + tech_dict["generators"], + tech_dict["storage_units_generators"], + ], + axis=1, + ).sum(axis=1) + + worst_case = (gen - load).abs().max() + + transformers_s_nom = lv_grid.transformers_df.s_nom.sum() + + if worst_case > threshold * transformers_s_nom: + logger.info(f"Trying to separate {lv_grid}...") + # TODO: Save changes in results + _ = separate_lv_grid(edisgo_obj, lv_grid) + else: + logger.info( + f"The overloading in {lv_grid} does not surpass the set threshold " + f"of {threshold} and is therefore not separated." + ) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index bb6cf573..d64ec57c 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -786,7 +786,7 @@ def separate_lv_grid( def get_weight(u, v, data: dict) -> float: return data["length"] - def create_bus_name(bus: str, voltage_level: str) -> str: + def create_bus_name(bus: str, lv_grid_id_new: int, voltage_level: str) -> str: """ Create an LV and MV bus-bar name with the same grid_id but added '1001' that implies the separation @@ -804,9 +804,8 @@ def create_bus_name(bus: str, voltage_level: str) -> str: """ if bus in edisgo_obj.topology.buses_df.index: bus = bus.split("_") - grid_id_ind = bus.index(str(grid.id)) - # TODO: how to name new grids? - bus[grid_id_ind] = f"{grid.id}1001" + + bus[-2] = lv_grid_id_new if voltage_level == "lv": bus = "_".join([str(_) for _ in bus]) @@ -823,8 +822,9 @@ def create_bus_name(bus: str, voltage_level: str) -> str: return bus + # TODO: adapt docstring to describe multiple new transformers def add_standard_transformer( - edisgo_obj: EDisGo, grid: LVGrid, bus_lv: str, bus_mv: str + edisgo_obj: EDisGo, grid: LVGrid, bus_lv: str, bus_mv: str, lv_grid_id_new: int ) -> dict: """ Adds standard transformer to topology. @@ -867,7 +867,7 @@ def add_standard_transformer( 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] = f"{str(grid.id)}1001" + new_transformer_name[grid_id_ind] = lv_grid_id_new transformer_s.s_nom = standard_transformer.S_nom transformer_s.type_info = None @@ -879,33 +879,74 @@ def add_standard_transformer( new_transformer_df = transformer_s.to_frame().T + old_s_nom = 5 # grid.transformers_df.s_nom.sum() + + max_iterations = 10 + n = 0 + + while old_s_nom > new_transformer_df.s_nom.sum() and n < max_iterations: + n += 1 + + another_new_transformer = new_transformer_df.iloc[-1:, :] + + old_name = another_new_transformer.index[0] + + name = old_name.split("_") + + try: + name[-1] = str(int(name[-1]) + 1) + except ValueError: + name.append("_1") + + name = "_".join(name) + + another_new_transformer.rename(index={old_name: name}, inplace=True) + + new_transformer_df = pd.concat( + [new_transformer_df, another_new_transformer] + ) + edisgo_obj.topology.transformers_df = pd.concat( [edisgo_obj.topology.transformers_df, new_transformer_df] ) transformers_changes["added"][ - f"LVGrid_{str(grid.id)}1001" + f"LVGrid_{lv_grid_id_new}" ] = new_transformer_df.index.tolist() return transformers_changes G = grid.graph - station_node = list(G.nodes)[0] # main station + + # main station + station_node = list(G.nodes)[0] relevant_lines = grid.lines_df.loc[ (grid.lines_df.bus0 == station_node) | (grid.lines_df.bus1 == station_node) ] + if len(relevant_lines) <= 1: + logger.warning( + f"{grid} has only {len(relevant_lines)} feeder and is therefore not " + f"separated." + ) + + return {}, {} + logger.debug(f"{grid} has {len(relevant_lines)} feeder.") paths = {} first_nodes_feeders = {} for node in G.nodes: + if node == station_node: + continue + path = nx.shortest_path(G, station_node, node) for first_node in relevant_lines.bus1.values: if first_node in path: paths[node] = path + first_nodes_feeders.setdefault(path[1], []).append( node # first nodes and paths ) @@ -931,7 +972,9 @@ def add_standard_transformer( for first_node, nodes_feeder in first_nodes_feeders.items(): # first line of the feeder - first_line = relevant_lines[relevant_lines.bus1 == first_node].index[0] + first_line = relevant_lines[ + (relevant_lines.bus1 == first_node) | (relevant_lines.bus0 == first_node) + ].index[0] # the last node of the feeder last_node = nodes_feeder[-1] @@ -959,12 +1002,16 @@ def add_standard_transformer( node_1_2 = path[path.index(node_1_2) - 1] # break if node is station if node_1_2 is path[0]: - raise NotImplementedError( - f" {grid}==>{first_line} and following lines could not be " + logger.warning( + f"{grid} ==> {first_line} and following lines could not be " f"reinforced due to insufficient number of node in the feeder. " f"A method to handle such cases is not yet implemented." ) + node_1_2 = path[path.index(node_1_2) + 1] + + break + # NOTE: If node_1_2 is a representative (meaning it is already directly # connected to the station) feeder cannot be split. Instead, every second # inept feeder is assigned to the new grid @@ -978,9 +1025,26 @@ def add_standard_transformer( if nodes_tb_relocated: logger.info(f"{grid}==>method:add_station_at_half_length is running ") + + # the new lv grid id: e.g. 49602X + n = 0 + lv_grid_id_new = int(f"{grid.id}{n}") + + max_iterations = 10**4 + + while lv_grid_id_new in [g.id for g in edisgo_obj.topology.mv_grid.lv_grids]: + n += 1 + lv_grid_id_new = int(f"{grid.id}{n}") + + if n >= max_iterations: + raise ValueError( + f"No suitable name for the new LV grid originating from {grid} was " + f"found in {max_iterations=}." + ) + # 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") + lv_bus_new = create_bus_name(station_node, lv_grid_id_new, "lv") + mv_bus_new = create_bus_name(station_node, lv_grid_id_new, "mv") # ADD MV and LV bus v_nom_lv = edisgo_obj.topology.buses_df.at[ @@ -995,8 +1059,6 @@ def add_standard_transformer( x_bus = grid.buses_df.at[station_node, "x"] y_bus = grid.buses_df.at[station_node, "y"] - # the new lv grid id: e.g. 496021001 - lv_grid_id_new = int(f"{str(grid.id)}1001") building_bus = grid.buses_df.at[station_node, "in_building"] dist = 0.001 @@ -1012,6 +1074,7 @@ def add_standard_transformer( lv_grid_id=lv_grid_id_new, in_building=building_bus, ) + # add mv busbar edisgo_obj.topology.add_bus( mv_bus_new, @@ -1023,7 +1086,7 @@ def add_standard_transformer( # ADD TRANSFORMER transformer_changes = add_standard_transformer( - edisgo_obj, grid, lv_bus_new, mv_bus_new + edisgo_obj, grid, lv_bus_new, mv_bus_new, lv_grid_id_new ) transformers_changes.update(transformer_changes) @@ -1097,10 +1160,26 @@ def add_standard_transformer( 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"{grid} and located in new grid {lv_grid_id_new} by method: " f"add_station_at_half_length " ) + # check if new grids have isolated notes + grids = [ + g + for g in edisgo_obj.topology.mv_grid.lv_grids + if g.id in [grid.id, lv_grid_id_new] + ] + + for g in grids: + n = nx.number_of_isolates(g.graph) + + if n > 0 and len(g.buses_df) > 1: + raise ValueError( + f"There are isolated nodes in {g}. The following nodes are " + f"isolated: {list(nx.isolates(g.graph))}" + ) + else: logger.warning( f"{grid} was not reinforced because it has to few suitable feeders." diff --git a/edisgo/io/electromobility_import.py b/edisgo/io/electromobility_import.py index cfc87462..223cea02 100644 --- a/edisgo/io/electromobility_import.py +++ b/edisgo/io/electromobility_import.py @@ -1077,14 +1077,12 @@ def distribute_public_charging_demand(edisgo_obj, **kwargs): edisgo_obj.electromobility.charging_processes_df.at[ idx, "charging_point_id" ] = charging_point_id - try: - available_charging_points_df.loc[ - charging_point_id - ] = edisgo_obj.electromobility.charging_processes_df.loc[ - idx, available_charging_points_df.columns - ].tolist() - except Exception: - print("break") + + available_charging_points_df.loc[ + charging_point_id + ] = edisgo_obj.electromobility.charging_processes_df.loc[ + idx, available_charging_points_df.columns + ].tolist() designated_charging_point_capacity_df.at[ charging_park_id, "designated_charging_point_capacity" diff --git a/edisgo/network/components.py b/edisgo/network/components.py index 5e4df1fb..8c219340 100644 --- a/edisgo/network/components.py +++ b/edisgo/network/components.py @@ -894,8 +894,6 @@ def nearest_substation(self): """ substations = self._topology.buses_df.loc[self._topology.transformers_df.bus1] - if self.geometry.y > 90: - print("break") nearest_substation, distance = find_nearest_bus(self.geometry, substations) lv_grid_id = int(self._topology.buses_df.at[nearest_substation, "lv_grid_id"]) diff --git a/tests/flex_opt/test_reinforce_grid.py b/tests/flex_opt/test_reinforce_grid.py index 26e96ad7..73a5c2be 100644 --- a/tests/flex_opt/test_reinforce_grid.py +++ b/tests/flex_opt/test_reinforce_grid.py @@ -7,7 +7,7 @@ from pandas.testing import assert_frame_equal from edisgo import EDisGo -from edisgo.flex_opt.reinforce_grid import reinforce_grid +from edisgo.flex_opt.reinforce_grid import reinforce_grid, run_separate_lv_grids class TestReinforceGrid: @@ -63,3 +63,39 @@ def test_reinforce_grid(self): assert_frame_equal( res_reduced.equipment_changes, results_dict[None].equipment_changes ) + + def test_run_separate_lv_grids(self): + edisgo = copy.deepcopy(self.edisgo) + + edisgo.timeseries.scale_timeseries(p_scaling_factor=5, q_scaling_factor=5) + + lv_grids = [copy.deepcopy(g) for g in edisgo.topology.mv_grid.lv_grids] + + run_separate_lv_grids(edisgo) + + lv_grids_new = list(edisgo.topology.mv_grid.lv_grids) + + # check that no new lv grid only consist of the station + for g in lv_grids_new: + if g.id != 0: + assert len(g.buses_df) > 1 + + assert len(lv_grids_new) == 26 + + # check if all generators are still present + assert np.isclose( + sum(g.generators_df.p_nom.sum() for g in lv_grids), + sum(g.generators_df.p_nom.sum() for g in lv_grids_new), + ) + + # check if all loads are still present + assert np.isclose( + sum(g.loads_df.p_set.sum() for g in lv_grids), + sum(g.loads_df.p_set.sum() for g in lv_grids_new), + ) + + # check if all storages are still present + assert np.isclose( + sum(g.storage_units_df.p_nom.sum() for g in lv_grids), + sum(g.storage_units_df.p_nom.sum() for g in lv_grids_new), + ) diff --git a/tests/test_edisgo.py b/tests/test_edisgo.py index 12cad914..f89b2856 100755 --- a/tests/test_edisgo.py +++ b/tests/test_edisgo.py @@ -532,13 +532,17 @@ def test_reinforce_catch_convergence(self): @pytest.mark.slow def test_enhanced_reinforce_grid(self): self.setup_edisgo_object() + self.setup_worst_case_time_series() self.edisgo.timeseries.scale_timeseries( p_scaling_factor=100, q_scaling_factor=100 ) + edisgo_obj = copy.deepcopy(self.edisgo) edisgo_obj = enhanced_reinforce_grid( - edisgo_obj, activate_cost_results_disturbing_mode=True + edisgo_obj, + activate_cost_results_disturbing_mode=True, + separate_lv_grids=False, ) results = edisgo_obj.results From e720b79abfa424fcccfd54915c8a9bca4f6809e0 Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Fri, 5 May 2023 11:10:08 +0200 Subject: [PATCH 04/23] connect new lv grid to existing mv bus instead of creating a new mv bus --- edisgo/flex_opt/reinforce_measures.py | 47 +++++---------------------- 1 file changed, 8 insertions(+), 39 deletions(-) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index d64ec57c..7f0cd928 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -1032,7 +1032,9 @@ def add_standard_transformer( max_iterations = 10**4 - while lv_grid_id_new in [g.id for g in edisgo_obj.topology.mv_grid.lv_grids]: + g_ids = [g.id for g in edisgo_obj.topology.mv_grid.lv_grids] + + while lv_grid_id_new in g_ids: n += 1 lv_grid_id_new = int(f"{grid.id}{n}") @@ -1044,73 +1046,40 @@ def add_standard_transformer( # Create the bus-bar name of primary and secondary side of new MV/LV station lv_bus_new = create_bus_name(station_node, lv_grid_id_new, "lv") - mv_bus_new = create_bus_name(station_node, lv_grid_id_new, "mv") + mv_bus = grid.transformers_df.bus0.iat[0] # ADD MV and LV bus v_nom_lv = edisgo_obj.topology.buses_df.at[ grid.transformers_df.bus1[0], "v_nom", ] - v_nom_mv = edisgo_obj.topology.buses_df.at[ - grid.transformers_df.bus0[0], - "v_nom", - ] x_bus = grid.buses_df.at[station_node, "x"] y_bus = grid.buses_df.at[station_node, "y"] building_bus = grid.buses_df.at[station_node, "in_building"] - dist = 0.001 - - length_mv = np.sqrt(dist**2 + dist**2) - # add lv busbar edisgo_obj.topology.add_bus( lv_bus_new, v_nom_lv, - x=x_bus + dist, - y=y_bus + dist, + x=x_bus, + 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 + dist, - y=y_bus + dist, - in_building=building_bus, - ) - # ADD TRANSFORMER transformer_changes = add_standard_transformer( - edisgo_obj, grid, lv_bus_new, mv_bus_new, lv_grid_id_new + edisgo_obj, grid, lv_bus_new, mv_bus, lv_grid_id_new ) transformers_changes.update(transformer_changes) logger.debug( - f"{edisgo_obj.topology.mv_grid}==>A new grid {lv_grid_id_new} " + 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.add_component( - comp_type="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 - lv_standard_line = edisgo_obj.config["grid_expansion_standard_equipment"][ "lv_line" ] From 80fcbaea110ea82daecf9e076b5cea900a0c2d95 Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Fri, 5 May 2023 16:13:09 +0200 Subject: [PATCH 05/23] make sure that only downstream buses of bus_1_2 are relocated to th new lv grid --- edisgo/flex_opt/reinforce_measures.py | 5 ++++- edisgo/tools/tools.py | 11 ++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index 7f0cd928..e8489ac1 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -14,6 +14,7 @@ ) from edisgo.network.grids import LVGrid, MVGrid +from edisgo.tools.tools import get_downstream_buses if TYPE_CHECKING: from edisgo import EDisGo @@ -951,6 +952,8 @@ def add_standard_transformer( node # first nodes and paths ) + # TODO: make sure nodes are sorted correctly? + lines_changes = {} transformers_changes = {} nodes_tb_relocated = {} # nodes to be moved into the new grid @@ -1016,7 +1019,7 @@ def add_standard_transformer( # connected to the station) feeder cannot be split. Instead, every second # inept feeder is assigned to the new grid if node_1_2 not in first_nodes_feeders or count_inept % 2 == 1: - nodes_tb_relocated[node_1_2] = nodes_feeder[nodes_feeder.index(node_1_2) :] + nodes_tb_relocated[node_1_2] = get_downstream_buses(edisgo_obj, node_1_2) if node_1_2 in first_nodes_feeders: count_inept += 1 diff --git a/edisgo/tools/tools.py b/edisgo/tools/tools.py index c3856806..fb8c1ac3 100644 --- a/edisgo/tools/tools.py +++ b/edisgo/tools/tools.py @@ -420,17 +420,22 @@ def get_downstream_buses(edisgo_obj, comp_name, comp_type="bus"): bus = bus0 if len(path_to_station_bus0) > len(path_to_station_bus1) else bus1 bus_upstream = bus0 if bus == bus1 else bus1 else: - return None + raise ValueError( + f"Component type needs to be either 'bus' or 'line'. Given {comp_type=} is " + f"not valid." + ) # remove edge between bus and next bus upstream graph.remove_edge(bus, bus_upstream) # get subgraph containing relevant bus - subgraphs = list(graph.subgraph(c) for c in nx.connected_components(graph)) + subgraphs = [graph.subgraph(c) for c in nx.connected_components(graph)] + for subgraph in subgraphs: if bus in subgraph.nodes(): return list(subgraph.nodes()) - return None + + return [bus] def assign_voltage_level_to_component(df, buses_df): From fa4f2299756d5994ac404b1abcf85427bed3acab Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Fri, 5 May 2023 16:18:18 +0200 Subject: [PATCH 06/23] change logging string --- edisgo/flex_opt/reinforce_measures.py | 5 +---- edisgo/tools/tools.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index e8489ac1..45a27d72 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -1078,10 +1078,7 @@ def add_standard_transformer( ) transformers_changes.update(transformer_changes) - logger.debug( - f"{edisgo_obj.topology.mv_grid} ==> A new grid {lv_grid_id_new} " - f"added into topology" - ) + logger.debug(f"New grid {lv_grid_id_new} added into topology.") lv_standard_line = edisgo_obj.config["grid_expansion_standard_equipment"][ "lv_line" diff --git a/edisgo/tools/tools.py b/edisgo/tools/tools.py index fb8c1ac3..fccbf019 100644 --- a/edisgo/tools/tools.py +++ b/edisgo/tools/tools.py @@ -400,7 +400,7 @@ def get_downstream_buses(edisgo_obj, comp_name, comp_type="bus"): ------- list(str) List of buses (as in index of :attr:`~.network.topology.Topology.buses_df`) - downstream of the given component. + downstream of the given component incl. the initial bus. """ graph = edisgo_obj.topology.to_graph() From ac17817920b458a6bcef201dc54f3afee30c0762 Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Mon, 8 May 2023 10:02:59 +0200 Subject: [PATCH 07/23] Make sure that the nodes of a feeder are sorted correctly when separating a grid Also make sure to only use nodes from the main feeder of a feeder. --- edisgo/flex_opt/reinforce_measures.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index 45a27d72..248cb896 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -952,12 +952,6 @@ def add_standard_transformer( node # first nodes and paths ) - # TODO: make sure nodes are sorted correctly? - - 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 @@ -971,6 +965,20 @@ def add_standard_transformer( ) ) + # make sure nodes are sorted correctly and node_1_2 is part of the main feeder + for first_node, nodes_feeder in first_nodes_feeders.items(): + paths_first_node = { + node: path for node, path in paths.items() if path[1] == first_node + } + + first_nodes_feeders[first_node] = paths_first_node[ + max(paths_first_node, key=lambda x: len(paths_first_node[x])) + ] + + lines_changes = {} + transformers_changes = {} + nodes_tb_relocated = {} # nodes to be moved into the new grid + count_inept = 0 for first_node, nodes_feeder in first_nodes_feeders.items(): @@ -990,6 +998,8 @@ def add_standard_transformer( # path does not include the nodes branching from the node on the main path path = paths[last_node] + # TODO: replace this to be weighted by the connected load per bus incl. + # branched1 of feeders node_1_2 = next( j for j in path From 028d5eccc6d07130a11a0f7f2dae986e87e40890 Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Mon, 8 May 2023 11:12:17 +0200 Subject: [PATCH 08/23] add equipment changes to workflow --- edisgo/flex_opt/reinforce_grid.py | 147 ++++++++++++++++---------- edisgo/flex_opt/reinforce_measures.py | 10 +- tests/flex_opt/test_reinforce_grid.py | 7 ++ 3 files changed, 106 insertions(+), 58 deletions(-) diff --git a/edisgo/flex_opt/reinforce_grid.py b/edisgo/flex_opt/reinforce_grid.py index 974dab88..66937d62 100644 --- a/edisgo/flex_opt/reinforce_grid.py +++ b/edisgo/flex_opt/reinforce_grid.py @@ -4,7 +4,6 @@ import datetime import logging -from numbers import Number from typing import TYPE_CHECKING import numpy as np @@ -116,42 +115,6 @@ def reinforce_grid( reinforcement is conducted. """ - - def _add_lines_changes_to_equipment_changes(): - edisgo.results.equipment_changes = pd.concat( - [ - edisgo.results.equipment_changes, - pd.DataFrame( - { - "iteration_step": [iteration_step] * len(lines_changes), - "change": ["changed"] * len(lines_changes), - "equipment": edisgo.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.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.results.equipment_changes = pd.concat(df_list) - if n_minus_one is True: raise NotImplementedError("n-1 security can currently not be checked.") @@ -255,8 +218,12 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): ) ) # write added and removed transformers to results.equipment_changes - _add_transformer_changes_to_equipment_changes("added") - _add_transformer_changes_to_equipment_changes("removed") + _add_transformer_changes_to_equipment_changes( + edisgo, transformer_changes, iteration_step, "added" + ) + _add_transformer_changes_to_equipment_changes( + edisgo, transformer_changes, iteration_step, "removed" + ) if not overloaded_lv_stations.empty: # reinforce distribution substations @@ -266,8 +233,12 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): ) ) # write added and removed transformers to results.equipment_changes - _add_transformer_changes_to_equipment_changes("added") - _add_transformer_changes_to_equipment_changes("removed") + _add_transformer_changes_to_equipment_changes( + edisgo, transformer_changes, iteration_step, "added" + ) + _add_transformer_changes_to_equipment_changes( + edisgo, transformer_changes, iteration_step, "removed" + ) if not crit_lines.empty: # reinforce lines @@ -275,7 +246,9 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): edisgo, crit_lines ) # write changed lines to results.equipment_changes - _add_lines_changes_to_equipment_changes() + _add_lines_changes_to_equipment_changes( + edisgo, lines_changes, iteration_step + ) # run power flow analysis again (after updating pypsa object) and check # if all over-loading problems were solved @@ -360,7 +333,7 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): crit_nodes, ) # write changed lines to results.equipment_changes - _add_lines_changes_to_equipment_changes() + _add_lines_changes_to_equipment_changes(edisgo, lines_changes, iteration_step) # run power flow analysis again (after updating pypsa object) and check # if all over-voltage problems were solved @@ -421,7 +394,9 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): ) ) # write added transformers to results.equipment_changes - _add_transformer_changes_to_equipment_changes("added") + _add_transformer_changes_to_equipment_changes( + edisgo, transformer_changes, iteration_step, "added" + ) # run power flow analysis again (after updating pypsa object) and # check if all over-voltage problems were solved @@ -483,7 +458,9 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): crit_nodes[crit_nodes.lv_grid_id == grid_id], ) # write changed lines to results.equipment_changes - _add_lines_changes_to_equipment_changes() + _add_lines_changes_to_equipment_changes( + edisgo, lines_changes, iteration_step + ) # run power flow analysis again (after updating pypsa object) # and check if all over-voltage problems were solved @@ -565,8 +542,12 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): ) ) # write added and removed transformers to results.equipment_changes - _add_transformer_changes_to_equipment_changes("added") - _add_transformer_changes_to_equipment_changes("removed") + _add_transformer_changes_to_equipment_changes( + edisgo, transformer_changes, iteration_step, "added" + ) + _add_transformer_changes_to_equipment_changes( + edisgo, transformer_changes, iteration_step, "removed" + ) if not overloaded_lv_stations.empty: # reinforce substations @@ -576,8 +557,12 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): ) ) # write added and removed transformers to results.equipment_changes - _add_transformer_changes_to_equipment_changes("added") - _add_transformer_changes_to_equipment_changes("removed") + _add_transformer_changes_to_equipment_changes( + edisgo, transformer_changes, iteration_step, "added" + ) + _add_transformer_changes_to_equipment_changes( + edisgo, transformer_changes, iteration_step, "removed" + ) if not crit_lines.empty: # reinforce lines @@ -585,7 +570,9 @@ def _add_transformer_changes_to_equipment_changes(mode: str | None): edisgo, crit_lines ) # write changed lines to results.equipment_changes - _add_lines_changes_to_equipment_changes() + _add_lines_changes_to_equipment_changes( + edisgo, lines_changes, iteration_step + ) # run power flow analysis again (after updating pypsa object) and check # if all over-loading problems were solved @@ -824,7 +811,7 @@ def enhanced_reinforce_grid( edisgo_object: EDisGo, activate_cost_results_disturbing_mode: bool = False, separate_lv_grids: bool = True, - separation_threshold: Number = 2, + separation_threshold: int | float = 2, **kwargs, ) -> EDisGo: """ @@ -984,7 +971,8 @@ def enhanced_reinforce_grid( return edisgo_obj -def run_separate_lv_grids(edisgo_obj: EDisGo, threshold: Number = 2) -> None: +# TODO: docstring +def run_separate_lv_grids(edisgo_obj: EDisGo, threshold: int | float = 2) -> None: lv_grids = list(edisgo_obj.topology.mv_grid.lv_grids) n_grids_init = len(lv_grids) @@ -1064,9 +1052,62 @@ def run_separate_lv_grids(edisgo_obj: EDisGo, threshold: Number = 2) -> None: if worst_case > threshold * transformers_s_nom: logger.info(f"Trying to separate {lv_grid}...") # TODO: Save changes in results - _ = separate_lv_grid(edisgo_obj, lv_grid) + transformers_changes, lines_changes = separate_lv_grid( + edisgo_obj, lv_grid + ) + if len(lines_changes) > 0: + _add_lines_changes_to_equipment_changes( + edisgo_obj, lines_changes, 1 + ) + + if len(transformers_changes) > 0: + _add_transformer_changes_to_equipment_changes( + edisgo_obj, transformers_changes, 1, "added" + ) + else: logger.info( f"The overloading in {lv_grid} does not surpass the set threshold " f"of {threshold} and is therefore not separated." ) + + +def _add_lines_changes_to_equipment_changes( + edisgo: EDisGo, lines_changes: dict, iteration_step: int +) -> None: + edisgo.results.equipment_changes = pd.concat( + [ + edisgo.results.equipment_changes, + pd.DataFrame( + { + "iteration_step": [iteration_step] * len(lines_changes), + "change": ["changed"] * len(lines_changes), + "equipment": edisgo.topology.lines_df.loc[ + lines_changes.keys(), "type_info" + ].values, + "quantity": [_ for _ in lines_changes.values()], + }, + index=list(lines_changes.keys()), + ), + ], + ) + + +def _add_transformer_changes_to_equipment_changes( + edisgo: EDisGo, transformer_changes: dict, iteration_step: int, mode: str | None +) -> None: + df_list = [edisgo.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.results.equipment_changes = pd.concat(df_list) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index 248cb896..7494ba4d 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -863,7 +863,7 @@ def add_standard_transformer( except KeyError: raise KeyError("Standard MV/LV transformer is not in the equipment list.") - transformers_changes = {"added": {}} + transformer_changes = {"added": {}} transformer_s = grid.transformers_df.iloc[0] new_transformer_name = transformer_s.name.split("_") @@ -880,7 +880,7 @@ def add_standard_transformer( new_transformer_df = transformer_s.to_frame().T - old_s_nom = 5 # grid.transformers_df.s_nom.sum() + old_s_nom = grid.transformers_df.s_nom.sum() max_iterations = 10 n = 0 @@ -897,7 +897,7 @@ def add_standard_transformer( try: name[-1] = str(int(name[-1]) + 1) except ValueError: - name.append("_1") + name.append("1") name = "_".join(name) @@ -910,11 +910,11 @@ def add_standard_transformer( edisgo_obj.topology.transformers_df = pd.concat( [edisgo_obj.topology.transformers_df, new_transformer_df] ) - transformers_changes["added"][ + transformer_changes["added"][ f"LVGrid_{lv_grid_id_new}" ] = new_transformer_df.index.tolist() - return transformers_changes + return transformer_changes G = grid.graph diff --git a/tests/flex_opt/test_reinforce_grid.py b/tests/flex_opt/test_reinforce_grid.py index 73a5c2be..dd4ca4cb 100644 --- a/tests/flex_opt/test_reinforce_grid.py +++ b/tests/flex_opt/test_reinforce_grid.py @@ -7,6 +7,7 @@ from pandas.testing import assert_frame_equal from edisgo import EDisGo +from edisgo.flex_opt.costs import grid_expansion_costs from edisgo.flex_opt.reinforce_grid import reinforce_grid, run_separate_lv_grids @@ -73,6 +74,7 @@ def test_run_separate_lv_grids(self): run_separate_lv_grids(edisgo) + edisgo.results.grid_expansion_costs = grid_expansion_costs(edisgo) lv_grids_new = list(edisgo.topology.mv_grid.lv_grids) # check that no new lv grid only consist of the station @@ -81,6 +83,7 @@ def test_run_separate_lv_grids(self): assert len(g.buses_df) > 1 assert len(lv_grids_new) == 26 + assert np.isclose(edisgo.results.grid_expansion_costs.total_costs.sum(), 280.06) # check if all generators are still present assert np.isclose( @@ -99,3 +102,7 @@ def test_run_separate_lv_grids(self): sum(g.storage_units_df.p_nom.sum() for g in lv_grids), sum(g.storage_units_df.p_nom.sum() for g in lv_grids_new), ) + + # test if power flow works + edisgo.set_time_series_worst_case_analysis() + edisgo.analyze() From d7c1992484e3ef916b87a68052e878ccb89a7f30 Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Mon, 8 May 2023 11:16:52 +0200 Subject: [PATCH 09/23] adapted whatsnew --- doc/whatsnew/v0-3-0.rst | 1 + edisgo/flex_opt/reinforce_grid.py | 7 ++++--- edisgo/flex_opt/reinforce_measures.py | 1 - 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/whatsnew/v0-3-0.rst b/doc/whatsnew/v0-3-0.rst index c8462d0d..c0c5a3d1 100644 --- a/doc/whatsnew/v0-3-0.rst +++ b/doc/whatsnew/v0-3-0.rst @@ -21,3 +21,4 @@ Changes * Added method to iteratively reinforce a grid in case the power flow analysis does not always converge `#353 `_ * Added method to aggregate LV grid buses to station bus secondary side `#353 `_ * Added option to run reinforcement with reduced number of time steps `#379 `_ +* Added a new reinforcement method that separate lv grids when the overloading is very high `#380 `_ diff --git a/edisgo/flex_opt/reinforce_grid.py b/edisgo/flex_opt/reinforce_grid.py index 66937d62..e56e62c1 100644 --- a/edisgo/flex_opt/reinforce_grid.py +++ b/edisgo/flex_opt/reinforce_grid.py @@ -984,9 +984,11 @@ def run_separate_lv_grids(edisgo_obj: EDisGo, threshold: int | float = 2) -> Non techs = ["generators", "loads", "storage_units"] n = 0 + max_iterations = 100 - while n_grids_init != len(list(edisgo_obj.topology.mv_grid.lv_grids)) or first_run: - print(n) + while ( + n_grids_init != len(list(edisgo_obj.topology.mv_grid.lv_grids)) or first_run + ) and n < max_iterations: n += 1 first_run = False @@ -1051,7 +1053,6 @@ def run_separate_lv_grids(edisgo_obj: EDisGo, threshold: int | float = 2) -> Non if worst_case > threshold * transformers_s_nom: logger.info(f"Trying to separate {lv_grid}...") - # TODO: Save changes in results transformers_changes, lines_changes = separate_lv_grid( edisgo_obj, lv_grid ) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index 7494ba4d..693a7708 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -1164,5 +1164,4 @@ def add_standard_transformer( f"{grid} was not reinforced because it has to few suitable feeders." ) - # TODO: check if changes are tracked correctly return transformers_changes, lines_changes From 36b542e4a01257d0b9f30a3f09337e779669d232 Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Mon, 8 May 2023 16:50:10 +0200 Subject: [PATCH 10/23] docstrings --- edisgo/flex_opt/reinforce_grid.py | 35 ++++++++++++++++++-- edisgo/flex_opt/reinforce_measures.py | 46 ++++++++++++--------------- 2 files changed, 53 insertions(+), 28 deletions(-) diff --git a/edisgo/flex_opt/reinforce_grid.py b/edisgo/flex_opt/reinforce_grid.py index e56e62c1..36ccea20 100644 --- a/edisgo/flex_opt/reinforce_grid.py +++ b/edisgo/flex_opt/reinforce_grid.py @@ -806,7 +806,6 @@ def reinforce(): return edisgo.results -# TODO: adapt docstring def enhanced_reinforce_grid( edisgo_object: EDisGo, activate_cost_results_disturbing_mode: bool = False, @@ -822,7 +821,9 @@ def enhanced_reinforce_grid( After first grid reinforcement for all voltage levels at once fails, reinforcement is first conducted for the MV level only, afterwards for the MV level including - MV/LV stations and at last each LV grid separately. + MV/LV stations and at last each LV grid separately. Beforehand the separation of + highly overloaded LV grids can be done by setting 'separate_lv_grids' to True. See + :func:`~.flex_opt.reinforce_grid.run_separate_lv_grids` for more information. Parameters ---------- @@ -842,6 +843,12 @@ def enhanced_reinforce_grid( :func:`edisgo.flex_opt.reinforce_grid.reinforce_grid`, except `catch_convergence_problems` which will always be set to True, `mode` which is set to None, and `skip_mv_reinforcement` which will be ignored. + separate_lv_grids : bool + If True, all highly overloaded LV grids are separated in a first step. + separation_threshold : int or float + Overloading threshold for LV grid separation. If the overloading is higher than + the threshold times the total nominal apparent power of the MV/LV transformer(s) + the grid is separated. Returns ------- @@ -971,8 +978,30 @@ def enhanced_reinforce_grid( return edisgo_obj -# TODO: docstring def run_separate_lv_grids(edisgo_obj: EDisGo, threshold: int | float = 2) -> None: + """ + Separate all highly overloaded LV grids within the MV grid. + + The loading is approximated by aggregation of all load and generator time series + and comparison with the total nominal apparent power of the MV/LV transformer(s). + This approach is chosen because this method is aims at resolving highly overloaded + grid situations in which cases the power flow often does not converge. This method + ignores grid losses and voltage deviations. Originating and new LV grids can be + separated multiple times if the overloading is very high. + + Parameters + ---------- + edisgo_obj : :class:`~.EDisGo` + threshold : int or float + Overloading threshold. If the overloading is higher than the threshold times + the total nominal apparent power of the MV/LV transformer(s) the grid is + separated. + + Returns + ------- + :class:`~.EDisGo` + The reinforced eDisGo object. + """ lv_grids = list(edisgo_obj.topology.mv_grid.lv_grids) n_grids_init = len(lv_grids) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index 693a7708..3b013e87 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -512,7 +512,7 @@ def reinforce_lines_voltage_issues(edisgo_obj, grid, crit_nodes): ] = path_length_dict[node_2_3] edisgo_obj.topology.change_line_type([crit_line_name], standard_line) lines_changes[crit_line_name] = 1 - # ToDo: Include switch disconnector + # TODO: Include switch disconnector if not lines_changes: logger.debug( @@ -735,46 +735,43 @@ def _replace_by_parallel_standard_lines(lines): return lines_changes -# TODO: check docstrings def separate_lv_grid( edisgo_obj: EDisGo, grid: LVGrid ) -> tuple[dict[Any, Any], dict[str, int]]: """ - Split LV grid by adding a new substation and connect half of each feeder. + Separate LV grid by adding a new substation and connect half of each feeder. - 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. + Separate LV grid by adding a new substation and connect half of each feeder. If a + feeder cannot be split because it has to few nodes or too few nodes outside a + building each second inept feeder is connected to the new LV grid. The new LV grids + equipped with standard transformers until the nominal apparent power is at least the + same as from the originating LV grid. The new substation is at the same location as + the originating substation. The workflow is as following: - 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. + * New MV/LV station is connected to the existing MV/LV station. + * The point at half the length of the feeders is determined. + * The first node following this point is chosen as the point where the new + connection will be made. + * This node is disconnected from the previous node and connected to a new station. 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 + * The name of the new LV grid will be a combination of the originating existing grid + ID. E.g. 40000 + X = 40000X + * 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 + * Except line names, all the data frames are named based on the new grid name Parameters ---------- edisgo_obj : :class:`~.EDisGo` - grid: class : :class:`~.network.grids.MVGrid` or :class:`~.network.grids.LVGrid` + grid: class : :class:`~.network.grids.LVGrid` Returns ------- - line_changes= dict + dict Dictionary with name of lines as keys and the corresponding number of lines added as values. - transformer_changes= dict + dict Dictionary with added and removed transformers in the form:: {'added': {'Grid_1': ['transformer_reinforced_1', ..., @@ -823,7 +820,6 @@ def create_bus_name(bus: str, lv_grid_id_new: int, voltage_level: str) -> str: return bus - # TODO: adapt docstring to describe multiple new transformers def add_standard_transformer( edisgo_obj: EDisGo, grid: LVGrid, bus_lv: str, bus_mv: str, lv_grid_id_new: int ) -> dict: @@ -999,7 +995,7 @@ def add_standard_transformer( path = paths[last_node] # TODO: replace this to be weighted by the connected load per bus incl. - # branched1 of feeders + # branched of feeders node_1_2 = next( j for j in path From 92c82a520d0c1f3249fdcbd4ba0c8b77f09c66d2 Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Mon, 8 May 2023 16:55:10 +0200 Subject: [PATCH 11/23] also except RuntimeErrors in enhanced reinforcement --- edisgo/flex_opt/reinforce_grid.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/edisgo/flex_opt/reinforce_grid.py b/edisgo/flex_opt/reinforce_grid.py index 36ccea20..5c0624a2 100644 --- a/edisgo/flex_opt/reinforce_grid.py +++ b/edisgo/flex_opt/reinforce_grid.py @@ -878,14 +878,14 @@ def enhanced_reinforce_grid( logger.info("Try initial enhanced reinforcement.") edisgo_obj.reinforce(mode=None, catch_convergence_problems=True, **kwargs) logger.info("Initial enhanced reinforcement succeeded.") - except ValueError: + except (ValueError, RuntimeError): logger.info("Initial enhanced reinforcement failed.") logger.info("Try mode 'mv' reinforcement.") try: edisgo_obj.reinforce(mode="mv", catch_convergence_problems=True, **kwargs) logger.info("Mode 'mv' reinforcement succeeded.") - except ValueError: + except (ValueError, RuntimeError): logger.info("Mode 'mv' reinforcement failed.") logger.info("Try mode 'mvlv' reinforcement.") @@ -893,7 +893,7 @@ def enhanced_reinforce_grid( try: edisgo_obj.reinforce(mode="mvlv", catch_convergence_problems=True, **kwargs) logger.info("Mode 'mvlv' reinforcement succeeded.") - except ValueError: + except (ValueError, RuntimeError): logger.info("Mode 'mvlv' reinforcement failed.") for lv_grid in list(edisgo_obj.topology.mv_grid.lv_grids): @@ -906,7 +906,7 @@ def enhanced_reinforce_grid( **kwargs, ) logger.info(f"Mode 'lv' reinforcement for {lv_grid} successful.") - except: # noqa: E722 + except (ValueError, RuntimeError): logger.info(f"Mode 'lv' reinforcement for {lv_grid} failed.") if activate_cost_results_disturbing_mode: try: @@ -929,7 +929,7 @@ def enhanced_reinforce_grid( logger.info( f"Changed lines mode 'lv' for {lv_grid} successful." ) - except: # noqa: E722 + except (ValueError, RuntimeError): logger.info(f"Changed lines mode 'lv' for {lv_grid} failed.") logger.warning( f"Aggregate all nodes to station bus in {lv_grid=}." @@ -942,13 +942,16 @@ def enhanced_reinforce_grid( logger.info( f"Aggregate to station for {lv_grid} successful." ) - except: # noqa: E722 - logger.info(f"Aggregate to station for {lv_grid} failed.") + except Exception as e: + logger.info( + f"Aggregate to station for {lv_grid} failed with " + f"exception:\n{e}" + ) try: edisgo_obj.reinforce(mode=None, catch_convergence_problems=True, **kwargs) logger.info("Enhanced reinforcement succeeded.") - except Exception as e: # noqa: E722 + except Exception as e: logger.info("Enhanced reinforcement failed.") raise e From ea470eff8e76fdc381af677289989d2dd992aef2 Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Mon, 8 May 2023 17:07:58 +0200 Subject: [PATCH 12/23] fix docstrings --- edisgo/flex_opt/reinforce_measures.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index 3b013e87..4185761c 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -748,13 +748,15 @@ def separate_lv_grid( same as from the originating LV grid. The new substation is at the same location as the originating substation. The workflow is as following: - * New MV/LV station is connected to the existing MV/LV station. * The point at half the length of the feeders is determined. * The first node following this point is chosen as the point where the new connection will be made. - * This node is disconnected from the previous node and connected to a new station. + * New MV/LV station is connected to the existing MV/LV station. + * The determined nodes are disconnected from the previous nodes and connected to the + new MV/LV station. Notes: + * The name of the new LV grid will be a combination of the originating existing grid ID. E.g. 40000 + X = 40000X * The name of the lines in the new LV grid is the same as the grid where the nodes @@ -772,13 +774,14 @@ def separate_lv_grid( Dictionary with name of lines as keys and the corresponding number of lines added as values. dict - Dictionary with added and removed transformers in the form:: - {'added': {'Grid_1': ['transformer_reinforced_1', - ..., - 'transformer_reinforced_x'], - 'Grid_10': ['transformer_reinforced_10'] - } - } + Dictionary with added transformers in the form:: + + {'added': {'Grid_1': ['transformer_reinforced_1', + ..., + 'transformer_reinforced_x'], + 'Grid_10': ['transformer_reinforced_10'] + } + } """ def get_weight(u, v, data: dict) -> float: From 6684b9226f4f6d2347a6a9129b3f59837e909963 Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 8 May 2023 22:10:17 +0200 Subject: [PATCH 13/23] Minor doc and logging changes --- edisgo/flex_opt/reinforce_grid.py | 11 +++--- edisgo/flex_opt/reinforce_measures.py | 55 +++++++++++++-------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/edisgo/flex_opt/reinforce_grid.py b/edisgo/flex_opt/reinforce_grid.py index 5c0624a2..9ae90d2f 100644 --- a/edisgo/flex_opt/reinforce_grid.py +++ b/edisgo/flex_opt/reinforce_grid.py @@ -987,9 +987,9 @@ def run_separate_lv_grids(edisgo_obj: EDisGo, threshold: int | float = 2) -> Non The loading is approximated by aggregation of all load and generator time series and comparison with the total nominal apparent power of the MV/LV transformer(s). - This approach is chosen because this method is aims at resolving highly overloaded + This approach is chosen because this method aims at resolving highly overloaded grid situations in which cases the power flow often does not converge. This method - ignores grid losses and voltage deviations. Originating and new LV grids can be + ignores grid losses and voltage deviations. Original and new LV grids can be separated multiple times if the overloading is very high. Parameters @@ -997,13 +997,14 @@ def run_separate_lv_grids(edisgo_obj: EDisGo, threshold: int | float = 2) -> Non edisgo_obj : :class:`~.EDisGo` threshold : int or float Overloading threshold. If the overloading is higher than the threshold times - the total nominal apparent power of the MV/LV transformer(s) the grid is + the total nominal apparent power of the MV/LV transformer(s), the grid is separated. Returns ------- :class:`~.EDisGo` The reinforced eDisGo object. + """ lv_grids = list(edisgo_obj.topology.mv_grid.lv_grids) n_grids_init = len(lv_grids) @@ -1099,9 +1100,9 @@ def run_separate_lv_grids(edisgo_obj: EDisGo, threshold: int | float = 2) -> Non ) else: - logger.info( + logger.debug( f"The overloading in {lv_grid} does not surpass the set threshold " - f"of {threshold} and is therefore not separated." + f"of {threshold}. The grid is therefore not separated." ) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index 4185761c..b00cd7ce 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -741,12 +741,11 @@ def separate_lv_grid( """ Separate LV grid by adding a new substation and connect half of each feeder. - Separate LV grid by adding a new substation and connect half of each feeder. If a - feeder cannot be split because it has to few nodes or too few nodes outside a - building each second inept feeder is connected to the new LV grid. The new LV grids - equipped with standard transformers until the nominal apparent power is at least the - same as from the originating LV grid. The new substation is at the same location as - the originating substation. The workflow is as following: + If a feeder cannot be split because it has too few nodes or too few nodes outside a + building, each second inept feeder is connected to the new LV grid. The new LV grid + is equipped with standard transformers until the nominal apparent power is at least + the same as in the original LV grid. The new substation is at the same location as + the originating substation. The workflow is as follows: * The point at half the length of the feeders is determined. * The first node following this point is chosen as the point where the new @@ -759,14 +758,14 @@ def separate_lv_grid( * The name of the new LV grid will be a combination of the originating existing grid ID. E.g. 40000 + X = 40000X - * The name of the lines in the new LV grid is the same as the grid where the nodes - are removed + * The name of the lines in the new LV grid are the same as in the grid where the + nodes were removed * Except line names, all the data frames are named based on the new grid name Parameters ---------- edisgo_obj : :class:`~.EDisGo` - grid: class : :class:`~.network.grids.LVGrid` + grid : :class:`~.network.grids.LVGrid` Returns ------- @@ -782,6 +781,7 @@ def separate_lv_grid( 'Grid_10': ['transformer_reinforced_10'] } } + """ def get_weight(u, v, data: dict) -> float: @@ -789,8 +789,8 @@ def get_weight(u, v, data: dict) -> float: def create_bus_name(bus: str, lv_grid_id_new: int, voltage_level: str) -> str: """ - Create an LV and MV bus-bar name with the same grid_id but added '1001' that - implies the separation + Create an LV and MV bus-bar name with the same grid_id but added '1001' which + implies the separation. Parameters ---------- @@ -801,7 +801,9 @@ def create_bus_name(bus: str, lv_grid_id_new: int, voltage_level: str) -> str: Returns ---------- - bus: str New bus-bar name + str + New bus-bar name. + """ if bus in edisgo_obj.topology.buses_df.index: bus = bus.split("_") @@ -831,14 +833,17 @@ def add_standard_transformer( Parameters ---------- - edisgo_obj: class:`~.EDisGo` - grid: `~.network.grids.LVGrid` - bus_lv: Identifier of lv bus - bus_mv: Identifier of mv bus + edisgo_obj : class:`~.EDisGo` + grid : `~.network.grids.LVGrid` + bus_lv : str + Identifier of LV bus. + bus_mv : str + Identifier of MV bus. Returns ---------- - transformer_changes= dict + dict + """ if bus_lv not in edisgo_obj.topology.buses_df.index: raise ValueError( @@ -1036,9 +1041,8 @@ def add_standard_transformer( count_inept += 1 if nodes_tb_relocated: - logger.info(f"{grid}==>method:add_station_at_half_length is running ") - # the new lv grid id: e.g. 49602X + # generate new lv grid id n = 0 lv_grid_id_new = int(f"{grid.id}{n}") @@ -1060,7 +1064,7 @@ def add_standard_transformer( lv_bus_new = create_bus_name(station_node, lv_grid_id_new, "lv") mv_bus = grid.transformers_df.bus0.iat[0] - # ADD MV and LV bus + # Add MV and LV bus v_nom_lv = edisgo_obj.topology.buses_df.at[ grid.transformers_df.bus1[0], "v_nom", @@ -1087,7 +1091,7 @@ def add_standard_transformer( ) transformers_changes.update(transformer_changes) - logger.debug(f"New grid {lv_grid_id_new} added into topology.") + logger.info(f"New LV grid {lv_grid_id_new} added to topology.") lv_standard_line = edisgo_obj.config["grid_expansion_standard_equipment"][ "lv_line" @@ -1131,18 +1135,13 @@ def add_standard_transformer( comp_name=line_removed, ) - 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 {lv_grid_id_new} by method: " f"add_station_at_half_length " ) - # check if new grids have isolated notes + # check if new grids have isolated nodes grids = [ g for g in edisgo_obj.topology.mv_grid.lv_grids @@ -1160,7 +1159,7 @@ def add_standard_transformer( else: logger.warning( - f"{grid} was not reinforced because it has to few suitable feeders." + f"{grid} was not split because it has too few suitable feeders." ) return transformers_changes, lines_changes From f473b5ac6990fe55a5b46f1939769ff296d2b6c3 Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Tue, 9 May 2023 09:22:21 +0200 Subject: [PATCH 14/23] simplify apparent power calculation --- edisgo/flex_opt/reinforce_grid.py | 48 +++++++++++-------------------- 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/edisgo/flex_opt/reinforce_grid.py b/edisgo/flex_opt/reinforce_grid.py index 9ae90d2f..33b3a505 100644 --- a/edisgo/flex_opt/reinforce_grid.py +++ b/edisgo/flex_opt/reinforce_grid.py @@ -1029,58 +1029,44 @@ def run_separate_lv_grids(edisgo_obj: EDisGo, threshold: int | float = 2) -> Non n_grids_init = len(lv_grids) for lv_grid in lv_grids: - tech_dict = {} + active_power_dict = {} + reactive_power_dict = {} for tech in techs: units = getattr(lv_grid, tech_str.format(tech)).index - active_power = ( + active_power_dict[tech] = ( getattr( edisgo_obj.timeseries, active_str.format(tech), ) .loc[:, units] .astype(float) + .sum(axis=1) ) - reactive_power = ( + reactive_power_dict[tech] = ( getattr( edisgo_obj.timeseries, reactive_str.format(tech), ) .loc[:, units] .astype(float) + .sum(axis=1) ) - if tech == "storage_units": - tech_dict[tech + "_loads"] = np.hypot( - active_power.clip(upper=0.0), - reactive_power.clip(upper=0.0), - ) - - tech_dict[tech + "_generators"] = np.hypot( - active_power.clip(lower=0.0), - reactive_power.clip(lower=0.0), - ) - else: - tech_dict[tech] = np.hypot(active_power, reactive_power) - - load = pd.concat( - [ - tech_dict["loads"], - tech_dict["storage_units_loads"], - ], - axis=1, - ).sum(axis=1) + active_power = ( + active_power_dict["loads"] + - active_power_dict["generators"] + - active_power_dict["storage_units"] + ) - gen = pd.concat( - [ - tech_dict["generators"], - tech_dict["storage_units_generators"], - ], - axis=1, - ).sum(axis=1) + reactive_power = ( + reactive_power_dict["loads"] + - reactive_power_dict["generators"] + - reactive_power_dict["storage_units"] + ) - worst_case = (gen - load).abs().max() + worst_case = np.hypot(active_power, reactive_power).max() transformers_s_nom = lv_grid.transformers_df.s_nom.sum() From 48255d3f4ed21f750fb4b0ddb76791d1bd28c660 Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Tue, 9 May 2023 09:30:40 +0200 Subject: [PATCH 15/23] make sure to idntify station node correctly --- edisgo/flex_opt/reinforce_measures.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index b00cd7ce..300d0be3 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -415,7 +415,6 @@ def reinforce_lines_voltage_issues(edisgo_obj, grid, crit_nodes): lines_changes = {} for repr_node in nodes_feeder.keys(): - # find node farthest away get_weight = lambda u, v, data: data["length"] # noqa: E731 path_length = 0 @@ -923,7 +922,7 @@ def add_standard_transformer( G = grid.graph # main station - station_node = list(G.nodes)[0] + station_node = grid.transformers_df.bus1.iat[0] relevant_lines = grid.lines_df.loc[ (grid.lines_df.bus0 == station_node) | (grid.lines_df.bus1 == station_node) @@ -1041,7 +1040,6 @@ def add_standard_transformer( count_inept += 1 if nodes_tb_relocated: - # generate new lv grid id n = 0 lv_grid_id_new = int(f"{grid.id}{n}") @@ -1158,8 +1156,6 @@ def add_standard_transformer( ) else: - logger.warning( - f"{grid} was not split because it has too few suitable feeders." - ) + logger.warning(f"{grid} was not split because it has too few suitable feeders.") return transformers_changes, lines_changes From 3a28aa280d8569745d4ded2c11f5ccb906420da3 Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Tue, 9 May 2023 10:02:32 +0200 Subject: [PATCH 16/23] add comments at hard to undrstand parts --- edisgo/flex_opt/reinforce_measures.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index 300d0be3..4592e0fb 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -928,6 +928,10 @@ def add_standard_transformer( (grid.lines_df.bus0 == station_node) | (grid.lines_df.bus1 == station_node) ] + first_nodes = set(relevant_lines.bus0).union(set(relevant_lines.bus1)) - { + station_node, + } + if len(relevant_lines) <= 1: logger.warning( f"{grid} has only {len(relevant_lines)} feeder and is therefore not " @@ -941,17 +945,19 @@ def add_standard_transformer( paths = {} first_nodes_feeders = {} + # determine ordered shortest path between each node and the station node and each + # node per feeder for node in G.nodes: if node == station_node: continue path = nx.shortest_path(G, station_node, node) - for first_node in relevant_lines.bus1.values: + for first_node in first_nodes: if first_node in path: paths[node] = path - first_nodes_feeders.setdefault(path[1], []).append( + first_nodes_feeders.setdefault(first_node, []).append( node # first nodes and paths ) @@ -974,6 +980,7 @@ def add_standard_transformer( node: path for node, path in paths.items() if path[1] == first_node } + # identify main feeder by maximum number of nodes in path first_nodes_feeders[first_node] = paths_first_node[ max(paths_first_node, key=lambda x: len(paths_first_node[x])) ] From ba00bc938454fa4ea94dbbd7bb7432429fb107c5 Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Tue, 9 May 2023 11:51:52 +0200 Subject: [PATCH 17/23] fix test_separate_lv_grid --- tests/flex_opt/test_reinforce_measures.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/flex_opt/test_reinforce_measures.py b/tests/flex_opt/test_reinforce_measures.py index 7696edf5..5ea72006 100644 --- a/tests/flex_opt/test_reinforce_measures.py +++ b/tests/flex_opt/test_reinforce_measures.py @@ -461,6 +461,10 @@ def test_separate_lv_grid(self): self.edisgo ) + all_lv_grid_ids = [ + lv_grid.id for lv_grid in self.edisgo.topology.mv_grid.lv_grids + ] + lv_grid_ids = ( self.edisgo.topology.buses_df.loc[ self.edisgo.topology.lines_df.loc[crit_lines_lv.index].bus0 @@ -489,8 +493,10 @@ def test_separate_lv_grid(self): new_g_1 = [ g for g in self.edisgo.topology.mv_grid.lv_grids - if g.id == int(str(grid_id) + "1001") + if g.id not in all_lv_grid_ids ][0] + + all_lv_grid_ids.append(new_g_1.id) except IndexError: continue From 20db1e1c6b39785da4a78eb4514b640148c63b34 Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 9 May 2023 16:47:04 +0200 Subject: [PATCH 18/23] Minor doc change --- edisgo/flex_opt/reinforce_grid.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/edisgo/flex_opt/reinforce_grid.py b/edisgo/flex_opt/reinforce_grid.py index 33b3a505..25dfdebb 100644 --- a/edisgo/flex_opt/reinforce_grid.py +++ b/edisgo/flex_opt/reinforce_grid.py @@ -838,17 +838,17 @@ def enhanced_reinforce_grid( standard line type. Should this not be sufficient to solve non-convergence issues, all components in the LV grid are aggregated to the MV/LV station. Default: False. - kwargs : dict - Keyword arguments can be all parameters of function - :func:`edisgo.flex_opt.reinforce_grid.reinforce_grid`, except - `catch_convergence_problems` which will always be set to True, `mode` which - is set to None, and `skip_mv_reinforcement` which will be ignored. separate_lv_grids : bool If True, all highly overloaded LV grids are separated in a first step. separation_threshold : int or float Overloading threshold for LV grid separation. If the overloading is higher than the threshold times the total nominal apparent power of the MV/LV transformer(s) the grid is separated. + kwargs : dict + Keyword arguments can be all parameters of function + :func:`edisgo.flex_opt.reinforce_grid.reinforce_grid`, except + `catch_convergence_problems` which will always be set to True, `mode` which + is set to None, and `skip_mv_reinforcement` which will be ignored. Returns ------- From b7230c1427d1597f2a3d2a0118307a7977ced95e Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 9 May 2023 16:47:22 +0200 Subject: [PATCH 19/23] Bug fix avoid float becoming object type --- edisgo/flex_opt/reinforce_measures.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index 4592e0fb..b93e0f27 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -868,20 +868,18 @@ def add_standard_transformer( transformer_changes = {"added": {}} - transformer_s = grid.transformers_df.iloc[0] - new_transformer_name = transformer_s.name.split("_") + new_transformer_df = grid.transformers_df.iloc[[0]] + new_transformer_name = new_transformer_df.index[0].split("_") grid_id_ind = new_transformer_name.index(str(grid.id)) new_transformer_name[grid_id_ind] = lv_grid_id_new - transformer_s.s_nom = standard_transformer.S_nom - transformer_s.type_info = None - 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 + new_transformer_df.s_nom = standard_transformer.S_nom + new_transformer_df.type_info = None + new_transformer_df.r_pu = standard_transformer.r_pu + new_transformer_df.x_pu = standard_transformer.x_pu + new_transformer_df.index = ["_".join([str(_) for _ in new_transformer_name])] + new_transformer_df.bus0 = bus_mv + new_transformer_df.bus1 = bus_lv old_s_nom = grid.transformers_df.s_nom.sum() From efd4af028f0e198e41707865576a88a3886aa661 Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 9 May 2023 19:24:04 +0200 Subject: [PATCH 20/23] Correct docstring --- edisgo/edisgo.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/edisgo/edisgo.py b/edisgo/edisgo.py index bc1084e9..c1af5773 100755 --- a/edisgo/edisgo.py +++ b/edisgo/edisgo.py @@ -969,7 +969,9 @@ def analyze( -------- tuple(:pandas:`pandas.DatetimeIndex`,\ :pandas:`pandas.DatetimeIndex`) - Returns the time steps for which power flow analysis did not converge. + First index contains time steps for which power flow analysis did converge. + Second index contains time steps for which power flow analysis did not + converge. References -------- From 74ea1e786c88504bd98e1d1fd9b75b8a9da7dd82 Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 9 May 2023 19:24:42 +0200 Subject: [PATCH 21/23] Fix check if LV grid ID is provided --- edisgo/io/pypsa_io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edisgo/io/pypsa_io.py b/edisgo/io/pypsa_io.py index d795d327..b9ad3fb1 100755 --- a/edisgo/io/pypsa_io.py +++ b/edisgo/io/pypsa_io.py @@ -195,7 +195,7 @@ def _set_slack(grid): pypsa_network.mode = "lv" lv_grid_id = kwargs.get("lv_grid_id", None) - if not lv_grid_id: + if lv_grid_id is None: raise ValueError( "For exporting LV grids, ID or name of LV grid has to be provided" "using parameter `lv_grid_id`." From 5024be3225ac02df06262994bc3c1fec5463776d Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 9 May 2023 19:24:56 +0200 Subject: [PATCH 22/23] Minor logging typo fix --- edisgo/io/pypsa_io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edisgo/io/pypsa_io.py b/edisgo/io/pypsa_io.py index b9ad3fb1..3825a4d6 100755 --- a/edisgo/io/pypsa_io.py +++ b/edisgo/io/pypsa_io.py @@ -197,7 +197,7 @@ def _set_slack(grid): lv_grid_id = kwargs.get("lv_grid_id", None) if lv_grid_id is None: raise ValueError( - "For exporting LV grids, ID or name of LV grid has to be provided" + "For exporting LV grids, ID or name of LV grid has to be provided " "using parameter `lv_grid_id`." ) grid_object = edisgo_object.topology.get_lv_grid(lv_grid_id) From 9a9094276235412a31d8fdad5aeae98860e2d3d8 Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 9 May 2023 19:27:24 +0200 Subject: [PATCH 23/23] Add grid separation when power flow for LV grid does not converge --- edisgo/flex_opt/reinforce_grid.py | 34 +++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/edisgo/flex_opt/reinforce_grid.py b/edisgo/flex_opt/reinforce_grid.py index 25dfdebb..c5f17ea7 100644 --- a/edisgo/flex_opt/reinforce_grid.py +++ b/edisgo/flex_opt/reinforce_grid.py @@ -819,11 +819,18 @@ def enhanced_reinforce_grid( :func:`edisgo.flex_opt.reinforce_grid.catch_convergence_reinforce_grid` is not sufficient. - After first grid reinforcement for all voltage levels at once fails, reinforcement - is first conducted for the MV level only, afterwards for the MV level including - MV/LV stations and at last each LV grid separately. Beforehand the separation of - highly overloaded LV grids can be done by setting 'separate_lv_grids' to True. See + In a first step, if `separate_lv_grids` is set to True, LV grids with a large load, + specified through parameter `separation_threshold`, are split, so that part of the + load is served by a separate MV/LV station. See :func:`~.flex_opt.reinforce_grid.run_separate_lv_grids` for more information. + Afterwards it is tried to run the grid reinforcement for all voltage levels at once. + If this fails, reinforcement is first conducted for the MV level only, afterwards + for the MV level including MV/LV stations and at last for each LV grid separately. + For each LV grid is it checked, if all time steps converge in the power flow + analysis. If this is not the case, the grid is split. Afterwards it is tried to + be reinforced. If this fails and `activate_cost_results_disturbing_mode` + parameter is set to True, further measures are taken. See parameter documentation + for more information. Parameters ---------- @@ -897,6 +904,25 @@ def enhanced_reinforce_grid( logger.info("Mode 'mvlv' reinforcement failed.") for lv_grid in list(edisgo_obj.topology.mv_grid.lv_grids): + logger.info(f"Check convergence for {lv_grid=}.") + _, ts_not_converged = edisgo_obj.analyze( + mode="lv", raise_not_converged=False, lv_grid_id=lv_grid.id + ) + if len(ts_not_converged) > 0: + logger.info( + f"Not all time steps converged in power flow analysis for " + f"{lv_grid=}. It is therefore tried to be split.") + transformers_changes, lines_changes = separate_lv_grid( + edisgo_obj, lv_grid + ) + if len(lines_changes) > 0: + _add_lines_changes_to_equipment_changes( + edisgo_obj, lines_changes, 1 + ) + if len(transformers_changes) > 0: + _add_transformer_changes_to_equipment_changes( + edisgo_obj, transformers_changes, 1, "added" + ) try: logger.info(f"Try mode 'lv' reinforcement for {lv_grid=}.") edisgo_obj.reinforce(