diff --git a/doc/whatsnew/v0-3-0.rst b/doc/whatsnew/v0-3-0.rst index 7cb37554..7cf4110a 100644 --- a/doc/whatsnew/v0-3-0.rst +++ b/doc/whatsnew/v0-3-0.rst @@ -27,3 +27,4 @@ Changes * Added a new reinforcement method that separate lv grids when the overloading is very high `#380 `_ * Move function to assign feeder to Topology class and add methods to the Grid class to get information on the feeders `#360 `_ * Added a storage operation strategy where the storage is charged when PV feed-in is higher than electricity demand of the household and discharged when electricity demand exceeds PV generation `#386 `_ +* Added an estimation of the voltage deviation over a cable when selecting a suitable cable to connect a new component `#411 `_ diff --git a/edisgo/config/config_grid_default.cfg b/edisgo/config/config_grid_default.cfg index f48c68d8..1cd82925 100644 --- a/edisgo/config/config_grid_default.cfg +++ b/edisgo/config/config_grid_default.cfg @@ -50,6 +50,11 @@ upper_limit_voltage_level_6 = 0.2 upper_limit_voltage_level_5 = 5.5 upper_limit_voltage_level_4 = 20.0 +# from VDE-AR-N 4100 (VDE-AR-N 4100) Anwendungsregel: 2019-04, table 3 +lv_max_voltage_deviation = 0.03 +# from VDE-AR-N 4110 (VDE-AR-N 4110) Anwendungsregel: 2023-09, 5.3.2 Zulässige Spannungsänderung +mv_max_voltage_deviation = 0.02 + [disconnecting_point] # Positioning of disconnecting points: Can be position at location of most diff --git a/edisgo/config/config_timeseries_default.cfg b/edisgo/config/config_timeseries_default.cfg index bfb97351..a1373e89 100644 --- a/edisgo/config/config_timeseries_default.cfg +++ b/edisgo/config/config_timeseries_default.cfg @@ -88,16 +88,16 @@ lv_load_case_hp = 1.0 # =========================== # power factors used to generate reactive power time series for loads and generators -mv_gen = 0.9 -mv_load = 0.9 -mv_storage = 0.9 -mv_cp = 1.0 -mv_hp = 1.0 -lv_gen = 0.95 -lv_load = 0.95 -lv_storage = 0.95 -lv_cp = 1.0 -lv_hp = 1.0 +mv_generator = 0.9 +mv_conventional_load = 0.9 +mv_storage_unit = 0.9 +mv_charging_point = 1.0 +mv_heat_pump = 1.0 +lv_generator = 0.95 +lv_conventional_load = 0.95 +lv_storage_unit = 0.95 +lv_charging_point = 1.0 +lv_heat_pump = 1.0 [reactive_power_mode] @@ -105,16 +105,16 @@ lv_hp = 1.0 # =========================== # power factor modes used to generate reactive power time series for loads and generators -mv_gen = inductive -mv_load = inductive -mv_storage = inductive -mv_cp = inductive -mv_hp = inductive -lv_gen = inductive -lv_load = inductive -lv_storage = inductive -lv_cp = inductive -lv_hp = inductive +mv_generator = inductive +mv_conventional_load = inductive +mv_storage_unit = inductive +mv_charging_point = inductive +mv_heat_pump = inductive +lv_generator = inductive +lv_conventional_load = inductive +lv_storage_unit = inductive +lv_charging_point = inductive +lv_heat_pump = inductive [demandlib] diff --git a/edisgo/flex_opt/q_control.py b/edisgo/flex_opt/q_control.py index a6e98578..07183a7d 100644 --- a/edisgo/flex_opt/q_control.py +++ b/edisgo/flex_opt/q_control.py @@ -92,22 +92,6 @@ def fixed_cosphi(active_power, q_sign, power_factor): return active_power * q_sign * np.tan(np.arccos(power_factor)) -def _get_component_dict(): - """ - Helper function to translate from component type term used in function to the one - used in the config files. - - """ - comp_dict = { - "generators": "gen", - "storage_units": "storage", - "conventional_loads": "load", - "charging_points": "cp", - "heat_pumps": "hp", - } - return comp_dict - - def _fixed_cosphi_default_power_factor(comp_df, component_type, configs): """ Gets fixed cosphi default reactive power factor for each given component. @@ -116,15 +100,15 @@ def _fixed_cosphi_default_power_factor(comp_df, component_type, configs): ----------- comp_df : :pandas:`pandas.DataFrame` Dataframe with component names (in the index) of all components - reactive power factor needs to be set. Only required column is + reactive power factor needs to be set for. Only required column is column 'voltage_level', giving the voltage level the component is in (the voltage level can be set using the function :func:`~.tools.tools.assign_voltage_level_to_component`). All components must have the same `component_type`. component_type : str The component type determines the reactive power factor and mode used. - Possible options are 'generators', 'storage_units', 'conventional_loads', - 'charging_points', and 'heat_pumps'. + Possible options are 'generator', 'storage_unit', 'conventional_load', + 'charging_point', and 'heat_pump'. configs : :class:`~.tools.config.Config` eDisGo configuration data. @@ -136,22 +120,28 @@ def _fixed_cosphi_default_power_factor(comp_df, component_type, configs): """ reactive_power_factor = configs["reactive_power_factor"] - comp_dict = _get_component_dict() - - if component_type in comp_dict.keys(): - comp = comp_dict[component_type] + allowed_types = [ + "generator", + "storage_unit", + "conventional_load", + "charging_point", + "heat_pump", + ] + if component_type in allowed_types: # write series with power factor for each component power_factor = pd.Series(index=comp_df.index, dtype=float) for voltage_level in comp_df.voltage_level.unique(): cols = comp_df.index[comp_df.voltage_level == voltage_level] if len(cols) > 0: - power_factor[cols] = reactive_power_factor[f"{voltage_level}_{comp}"] + power_factor[cols] = reactive_power_factor[ + f"{voltage_level}_{component_type}" + ] return power_factor else: raise ValueError( "Given 'component_type' is not valid. Valid options are " - "'generators','storage_units', 'conventional_loads', 'charging_points', " - "and 'heat_pumps'." + "'generator', 'storage_unit', 'conventional_load', 'charging_point', " + "and 'heat_pump'." ) @@ -170,8 +160,8 @@ def _fixed_cosphi_default_reactive_power_sign(comp_df, component_type, configs): All components must have the same `component_type`. component_type : str The component type determines the reactive power factor and mode used. - Possible options are 'generators', 'storage_units', 'conventional_loads', - 'charging_points', and 'heat_pumps'. + Possible options are 'generator', 'storage_unit', 'conventional_load', + 'charging_point', and 'heat_pump'. configs : :class:`~.tools.config.Config` eDisGo configuration data. @@ -183,17 +173,15 @@ def _fixed_cosphi_default_reactive_power_sign(comp_df, component_type, configs): """ reactive_power_mode = configs["reactive_power_mode"] - comp_dict = _get_component_dict() q_sign_dict = { - "generators": get_q_sign_generator, - "storage_units": get_q_sign_generator, - "conventional_loads": get_q_sign_load, - "charging_points": get_q_sign_load, - "heat_pumps": get_q_sign_load, + "generator": get_q_sign_generator, + "storage_unit": get_q_sign_generator, + "conventional_load": get_q_sign_load, + "charging_point": get_q_sign_load, + "heat_pump": get_q_sign_load, } - if component_type in comp_dict.keys(): - comp = comp_dict[component_type] + if component_type in q_sign_dict.keys(): get_q_sign = q_sign_dict[component_type] # write series with power factor for each component q_sign = pd.Series(index=comp_df.index, dtype=float) @@ -201,12 +189,12 @@ def _fixed_cosphi_default_reactive_power_sign(comp_df, component_type, configs): cols = comp_df.index[comp_df.voltage_level == voltage_level] if len(cols) > 0: q_sign[cols] = get_q_sign( - reactive_power_mode[f"{voltage_level}_{comp}"] + reactive_power_mode[f"{voltage_level}_{component_type}"] ) return q_sign else: raise ValueError( "Given 'component_type' is not valid. Valid options are " - "'generators','storage_units', 'conventional_loads', 'charging_points', " - "and 'heat_pumps'." + "'generator', 'storage_unit', 'conventional_load', 'charging_point', " + "and 'heat_pump'." ) diff --git a/edisgo/io/powermodels_io.py b/edisgo/io/powermodels_io.py index b0fb2278..541e01ff 100644 --- a/edisgo/io/powermodels_io.py +++ b/edisgo/io/powermodels_io.py @@ -667,7 +667,7 @@ def _build_gen(edisgo_obj, psa_net, pm, flexible_storage_units, s_base): gen.bus[gen_i], flexible_storage_units=flexible_storage_units, ) - pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "gen") + pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "generator") q = [ sign * np.tan(np.arccos(pf)) * gen.p_nom[gen_i], sign * np.tan(np.arccos(pf)) * gen.p_nom_min[gen_i], @@ -704,7 +704,7 @@ def _build_gen(edisgo_obj, psa_net, pm, flexible_storage_units, s_base): psa_net.storage_units.bus.loc[inflexible_storage_units[stor_i]], flexible_storage_units=flexible_storage_units, ) - pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "storage") + pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "storage_unit") p_g = max( [ psa_net.storage_units_t.p_set[inflexible_storage_units[stor_i]][0], @@ -837,7 +837,7 @@ def _build_branch(edisgo_obj, psa_net, pm, flexible_storage_units, s_base): flexible_storage_units=flexible_storage_units, ) # retrieve power factor from config - pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "storage") + pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "storage_unit") pm["branch"][str(stor_i + len(branches.index) + 1)] = { "name": "bss_branch_" + str(stor_i + 1), @@ -919,22 +919,22 @@ def _build_load( edisgo_obj.topology.loads_df.loc[loads_df.index[load_i]].type == "conventional_load" ): - pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "load") + pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "conventional_load") elif ( edisgo_obj.topology.loads_df.loc[loads_df.index[load_i]].type == "heat_pump" ): - pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "hp") + pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "heat_pump") elif ( edisgo_obj.topology.loads_df.loc[loads_df.index[load_i]].type == "charging_point" ): - pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "cp") + pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "charging_point") else: logger.warning( "No type specified for load {}. Power factor and sign will" "be set for conventional load.".format(loads_df.index[load_i]) ) - pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "load") + pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "conventional_load") p_d = psa_net.loads_t.p_set[loads_df.index[load_i]] q_d = psa_net.loads_t.q_set[loads_df.index[load_i]] pm["load"][str(load_i + 1)] = { @@ -955,7 +955,7 @@ def _build_load( psa_net.storage_units.bus.loc[inflexible_storage_units[stor_i]], flexible_storage_units=flexible_storage_units, ) - pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "storage") + pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "storage_unit") p_d = -min( [ psa_net.storage_units_t.p_set[inflexible_storage_units[stor_i]][0], @@ -1036,7 +1036,7 @@ def _build_battery_storage( flexible_storage_units=flexible_storage_units, ) # retrieve power factor from config - pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "storage") + pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "storage_unit") e_max = ( psa_net.storage_units.p_nom.loc[flexible_storage_units[stor_i]] * psa_net.storage_units.max_hours.loc[flexible_storage_units[stor_i]] @@ -1151,7 +1151,7 @@ def _build_electromobility(edisgo_obj, psa_net, pm, s_base, flexible_cps): eta = edisgo_obj.electromobility.simbev_config_df.eta_cp.values[0] except IndexError: eta = 0.9 - pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "cp") + pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "charging_point") q = ( sign * np.tan(np.arccos(pf)) @@ -1218,7 +1218,7 @@ def _build_heatpump(psa_net, pm, edisgo_obj, s_base, flexible_hps): for hp_i in np.arange(len(heat_df.index)): idx_bus = _mapping(psa_net, edisgo_obj, heat_df.bus[hp_i]) # retrieve power factor and sign from config - pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "hp") + pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "heat_pump") q = sign * np.tan(np.arccos(pf)) * heat_df.p_set[hp_i] p_d = heat_df2[heat_df.index[hp_i]] pm["heatpumps"][str(hp_i + 1)] = { @@ -1446,7 +1446,7 @@ def _build_dsm(edisgo_obj, psa_net, pm, s_base, flexible_loads): for dsm_i in np.arange(len(dsm_df.index)): idx_bus = _mapping(psa_net, edisgo_obj, dsm_df.bus[dsm_i]) # retrieve power factor and sign from config - pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "load") + pf, sign = _get_pf(edisgo_obj, pm, idx_bus, "conventional_load") p_max = edisgo_obj.dsm.p_max[dsm_df.index[dsm_i]] p_min = edisgo_obj.dsm.p_min[dsm_df.index[dsm_i]] e_min = edisgo_obj.dsm.e_min[dsm_df.index[dsm_i]] @@ -2053,7 +2053,8 @@ def _get_pf(edisgo_obj, pm, idx_bus, kind): idx_bus : int Bus index from PowerModels bus dictionary. kind : str - Must be one of ["gen", "load", "storage", "hp", "cp"]. + Must be one of ["generator", "conventional_load", "storage_unit", "heat_pump", + "charging_point"]. Returns ------- @@ -2061,18 +2062,14 @@ def _get_pf(edisgo_obj, pm, idx_bus, kind): """ grid_level = pm["bus"][str(idx_bus)]["grid_level"] - pf = edisgo_obj.config._data["reactive_power_factor"][ - "{}_{}".format(grid_level, kind) - ] - sign = edisgo_obj.config._data["reactive_power_mode"][ - "{}_{}".format(grid_level, kind) - ] - if kind in ["gen", "storage"]: + pf = edisgo_obj.config["reactive_power_factor"]["{}_{}".format(grid_level, kind)] + sign = edisgo_obj.config["reactive_power_mode"]["{}_{}".format(grid_level, kind)] + if kind in ["generator", "storage_unit"]: if sign == "inductive": sign = -1 else: sign = 1 - elif kind in ["load", "hp", "cp"]: + elif kind in ["conventional_load", "heat_pump", "charging_point"]: if sign == "inductive": sign = 1 else: diff --git a/edisgo/network/timeseries.py b/edisgo/network/timeseries.py index 15337f06..6cf4a7b4 100644 --- a/edisgo/network/timeseries.py +++ b/edisgo/network/timeseries.py @@ -821,10 +821,10 @@ def _worst_case_generators(self, cases, df, configs): # reactive power # get worst case configurations for each generator power_factor = q_control._fixed_cosphi_default_power_factor( - df, "generators", configs + df, "generator", configs ) q_sign = q_control._fixed_cosphi_default_reactive_power_sign( - df, "generators", configs + df, "generator", configs ) # write reactive power configuration to TimeSeriesRaw self.time_series_raw.q_control.drop(df.index, errors="ignore", inplace=True) @@ -899,10 +899,10 @@ def _worst_case_conventional_load(self, cases, df, configs): # reactive power # get worst case configurations for each load power_factor = q_control._fixed_cosphi_default_power_factor( - df, "conventional_loads", configs + df, "conventional_load", configs ) q_sign = q_control._fixed_cosphi_default_reactive_power_sign( - df, "conventional_loads", configs + df, "conventional_load", configs ) # write reactive power configuration to TimeSeriesRaw self.time_series_raw.q_control.drop(df.index, errors="ignore", inplace=True) @@ -999,10 +999,10 @@ def _worst_case_charging_points(self, cases, df, configs): # reactive power # get worst case configurations for each charging point power_factor = q_control._fixed_cosphi_default_power_factor( - df, "charging_points", configs + df, "charging_point", configs ) q_sign = q_control._fixed_cosphi_default_reactive_power_sign( - df, "charging_points", configs + df, "charging_point", configs ) # write reactive power configuration to TimeSeriesRaw self.time_series_raw.q_control.drop(df.index, errors="ignore", inplace=True) @@ -1077,10 +1077,10 @@ def _worst_case_heat_pumps(self, cases, df, configs): # reactive power # get worst case configurations for each heat pump power_factor = q_control._fixed_cosphi_default_power_factor( - df, "heat_pumps", configs + df, "heat_pump", configs ) q_sign = q_control._fixed_cosphi_default_reactive_power_sign( - df, "heat_pumps", configs + df, "heat_pump", configs ) # write reactive power configuration to TimeSeriesRaw self.time_series_raw.q_control.drop(df.index, errors="ignore", inplace=True) @@ -1153,10 +1153,10 @@ def _worst_case_storage_units(self, cases, df, configs): # reactive power # get worst case configurations for each load power_factor = q_control._fixed_cosphi_default_power_factor( - df, "storage_units", configs + df, "storage_unit", configs ) q_sign = q_control._fixed_cosphi_default_reactive_power_sign( - df, "storage_units", configs + df, "storage_unit", configs ) # write reactive power configuration to TimeSeriesRaw self.time_series_raw.q_control.drop(df.index, errors="ignore", inplace=True) @@ -1606,7 +1606,7 @@ def _get_q_sign_and_power_factor_per_component( q_sign, q_control._fixed_cosphi_default_reactive_power_sign( df[df["type"] == load_type], - f"{load_type}s", + load_type, edisgo_object.config, ), ] @@ -1616,17 +1616,17 @@ def _get_q_sign_and_power_factor_per_component( power_factor, q_control._fixed_cosphi_default_power_factor( df[df["type"] == load_type], - f"{load_type}s", + load_type, edisgo_object.config, ), ] ) else: q_sign = q_control._fixed_cosphi_default_reactive_power_sign( - df, type, edisgo_object.config + df, type[:-1], edisgo_object.config ) power_factor = q_control._fixed_cosphi_default_power_factor( - df, type, edisgo_object.config + df, type[:-1], edisgo_object.config ) elif isinstance(parametrisation, pd.DataFrame): # check if all given components exist in network and only use existing @@ -1659,7 +1659,7 @@ def _get_q_sign_and_power_factor_per_component( q_sign, default_func( df[df["type"] == load_type], - f"{load_type}s", + load_type, edisgo_object.config, ), ] @@ -1668,7 +1668,9 @@ def _get_q_sign_and_power_factor_per_component( q_sign = pd.concat( [ q_sign, - default_func(df, type, edisgo_object.config), + default_func( + df, type[:-1], edisgo_object.config + ), ] ) else: @@ -1692,7 +1694,7 @@ def _get_q_sign_and_power_factor_per_component( power_factor, default_func( df[df["type"] == load_type], - f"{load_type}s", + load_type, edisgo_object.config, ), ] @@ -1701,7 +1703,9 @@ def _get_q_sign_and_power_factor_per_component( power_factor = pd.concat( [ power_factor, - default_func(df, type, edisgo_object.config), + default_func( + df, type[:-1], edisgo_object.config + ), ] ) else: diff --git a/edisgo/network/topology.py b/edisgo/network/topology.py index 2462314f..d7694476 100755 --- a/edisgo/network/topology.py +++ b/edisgo/network/topology.py @@ -1928,7 +1928,13 @@ def connect_to_mv(self, edisgo_object, comp_data, comp_type="generator"): # avoid very short lines by limiting line length to at least 1m line_length = max(line_length, 0.001) - line_type, num_parallel = select_cable(edisgo_object, "mv", power) + line_type, num_parallel = select_cable( + edisgo_obj=edisgo_object, + level="mv", + apparent_power=power, + length=line_length, + component_type=comp_type, + ) line_name = self.add_line( bus0=self.mv_grid.station.index[0], @@ -1975,13 +1981,12 @@ def connect_to_mv(self, edisgo_object, comp_data, comp_type="generator"): for dist_min_obj in conn_objects_min_stack: # do not allow connection to virtual busses if "virtual" not in dist_min_obj["repr"]: - line_type, num_parallel = select_cable(edisgo_object, "mv", power) target_obj_result = self._connect_mv_bus_to_target_object( edisgo_object=edisgo_object, bus=self.buses_df.loc[bus, :], target_obj=dist_min_obj, - line_type=line_type.name, - number_parallel_lines=num_parallel, + comp_type=comp_type, + power=power, ) if target_obj_result is not None: @@ -2448,7 +2453,12 @@ def connect_to_lv_based_on_geolocation( return comp_name def _connect_mv_bus_to_target_object( - self, edisgo_object, bus, target_obj, line_type, number_parallel_lines + self, + edisgo_object, + bus, + target_obj, + comp_type, + power, ): """ Connects given MV bus to given target object (MV line or bus). @@ -2477,11 +2487,12 @@ def _connect_mv_bus_to_target_object( * shp : :shapely:`Shapely Point object` or \ :shapely:`Shapely Line object` Geometry of line or bus to connect to. - - line_type : str - Line type to use to connect new component with. - number_parallel_lines : int - Number of parallel lines to connect new component with. + comp_type : str + Type of added component. Can be 'generator', 'charging_point', 'heat_pump' + or 'storage_unit'. + Default: 'generator'. + power : float + Nominal power of the new component to be connected. Returns ------- @@ -2598,6 +2609,13 @@ def _connect_mv_bus_to_target_object( "branch_detour_factor" ], ) + line_type, num_parallel = select_cable( + edisgo_obj=edisgo_object, + level="mv", + apparent_power=power, + length=line_length, + component_type=comp_type, + ) # avoid very short lines by limiting line length to at least 1m if line_length < 0.001: line_length = 0.001 @@ -2606,8 +2624,8 @@ def _connect_mv_bus_to_target_object( bus1=bus.name, length=line_length, kind="cable", - type_info=line_type, - num_parallel=number_parallel_lines, + type_info=line_type.name, + num_parallel=num_parallel, ) # add line to equipment changes edisgo_object.results._add_line_to_equipment_changes( @@ -2624,7 +2642,7 @@ def _connect_mv_bus_to_target_object( # bus is the nearest connection point else: - # add new branch for satellite (station to station) + # add new line between new bus and closest bus line_length = geo.calc_geo_dist_vincenty( grid_topology=self, bus_source=bus.name, @@ -2633,6 +2651,13 @@ def _connect_mv_bus_to_target_object( "branch_detour_factor" ], ) + line_type, num_parallel = select_cable( + edisgo_obj=edisgo_object, + level="mv", + apparent_power=power, + length=line_length, + component_type=comp_type, + ) # avoid very short lines by limiting line length to at least 1m if line_length < 0.001: line_length = 0.001 @@ -2642,8 +2667,8 @@ def _connect_mv_bus_to_target_object( bus1=bus.name, length=line_length, kind="cable", - type_info=line_type, - num_parallel=number_parallel_lines, + type_info=line_type.name, + num_parallel=num_parallel, ) # add line to equipment changes @@ -2721,7 +2746,13 @@ def _connect_to_lv_bus(self, edisgo_object, target_bus, comp_type, comp_data): line_length = max(line_length, 0.001) # get suitable line type - line_type, num_parallel = select_cable(edisgo_object, "lv", comp_data["p"]) + line_type, num_parallel = select_cable( + edisgo_obj=edisgo_object, + level="lv", + apparent_power=comp_data["p"], + component_type=comp_type, + length=line_length, + ) line_name = self.add_line( bus0=target_bus, bus1=b, diff --git a/edisgo/tools/config.py b/edisgo/tools/config.py index 54fc08a3..7494943a 100644 --- a/edisgo/tools/config.py +++ b/edisgo/tools/config.py @@ -116,7 +116,7 @@ class Config: Get reactive power factor for generators in the MV network - >>> config['reactive_power_factor']['mv_gen'] + >>> config['reactive_power_factor']['mv_generator'] """ diff --git a/edisgo/tools/tools.py b/edisgo/tools/tools.py index d05fe1b8..061e02ed 100644 --- a/edisgo/tools/tools.py +++ b/edisgo/tools/tools.py @@ -14,7 +14,7 @@ from sqlalchemy.engine.base import Engine -from edisgo.flex_opt import exceptions +from edisgo.flex_opt import exceptions, q_control from edisgo.io.db import session_scope_egon_data, sql_grid_geom, sql_intersects from edisgo.tools import session_scope @@ -193,13 +193,157 @@ def drop_duplicated_columns(df, keep="last"): return df.loc[:, ~df.columns.duplicated(keep=keep)] -def select_cable(edisgo_obj, level, apparent_power): +def calculate_voltage_diff_pu_per_line( + s_max: float | np.ndarray, + r_total: float | np.ndarray, + x_total: float | np.ndarray, + v_nom: float | np.ndarray, + q_sign: int, + power_factor: float, +) -> float | np.ndarray: """ - Selects suitable cable type and quantity using given apparent power. + Calculate the voltage difference across a line in p.u.. - Cable is selected to be able to carry the given `apparent_power`, no load - factor is considered. Overhead lines are not considered in choosing a - suitable cable. + Parameters + ---------- + s_max : float or array-like + Apparent power the cable must carry in MVA. + r_total : float or array-like + Total resistance of the line in Ohms. + x_total : float or array-like + Total reactance of the line in Ohms. + v_nom : float or array-like + Nominal voltage of the line in kV. + q_sign : int + `q_sign` defines whether the reactive power is positive or + negative and must either be -1 or +1. In case of generators and storage units, + inductive reactive power is negative. In case of loads, inductive reactive + power is positive. + power_factor : :pandas:`pandas.Series` or float + Ratio of real to apparent power. + + Returns + ------- + float or array-like + Voltage difference in p.u.. If positive, the voltage difference behaves like + expected, it rises for generators and drops for loads. If negative, + the voltage difference behaves counterintuitively, it drops for generators + and rises for loads. + + """ + sin_phi = np.sqrt(1 - power_factor**2) + # Calculate the voltage difference using the formula from VDE-AR-N 4105 + voltage_diff = (s_max / (v_nom**2)) * ( + r_total * power_factor + q_sign * x_total * sin_phi + ) + return voltage_diff # in pu + + +def calculate_voltage_diff_pu_per_line_from_type( + edisgo_obj: EDisGo, + cable_names: str | np.ndarray, + length: float, + num_parallel: int, + v_nom: float | np.ndarray, + s_max: float | np.ndarray, + component_type: str, +) -> float | np.ndarray: + """ + Calculate the voltage difference across a line in p.u. depending on line type + and component type. + + This function serves as a helper function for function + :py:func:`calculate_voltage_diff_pu_per_line`, as it automatically obtains the + equipment data per line type from the provided equipment data and default reactive + power data per component type from the configuration files. + + Parameters + ---------- + edisgo_obj : :class:`~.EDisGo` + cable_names : str or array-like + Resistance per kilometer of the cable in ohm/km. + length : float + Length of the cable in km. + num_parallel : int + Number of parallel cables. + v_nom : int + Nominal voltage of the cable(s) in kV. + s_max : float + Apparent power the cable must carry in MVA. + component_type : str, optional + Type of the component to be connected, used to obtain the default reactive power + mode and power factor from the configuration file. If this is given, + `reactive_power_mode` and `power_factor` are not considered. + Possible options are "generator", "conventional_load", "charging_point", + "heat_pump" and "storage_unit". + + Returns + ------- + float or array-like + Voltage difference in p.u.. If positive, the voltage difference behaves like + expected, it rises for generators and drops for loads. If negative, + the voltage difference behaves counterintuitively, it drops for generators + and rises for loads. + + """ + # calculate total resistance and reactance for the given length and + # number of parallel cables for given cable types + config_type = "mv_cables" if v_nom > 1.0 else "lv_cables" + cable_data = edisgo_obj.topology.equipment_data[config_type] + r_total = calculate_line_resistance( + cable_data.loc[cable_names, "R_per_km"], length, num_parallel + ) + x_total = calculate_line_reactance( + cable_data.loc[cable_names, "L_per_km"], length, num_parallel + ) + + # get sign of reactive power based on component type + config_type = f"mv_{component_type}" if v_nom > 1.0 else f"lv_{component_type}" + if component_type in ["generator", "storage_unit"]: + q_sign = q_control.get_q_sign_generator( + edisgo_obj.config["reactive_power_mode"][config_type] + ) + elif component_type in ["conventional_load", "heat_pump", "charging_point"]: + q_sign = q_control.get_q_sign_load( + edisgo_obj.config["reactive_power_mode"][config_type] + ) + else: + raise ValueError( + "Specified component type is not valid. " + "Must either be 'generator', 'conventional_load', 'charging_point', " + "'heat_pump' or 'storage_unit'." + ) + + # get power factor based on component type + power_factor = edisgo_obj.config["reactive_power_factor"][config_type] + + # Calculate the voltage drop or increase + return calculate_voltage_diff_pu_per_line( + s_max, + r_total, + x_total, + v_nom, + q_sign, + power_factor, + ) + + +def select_cable( + edisgo_obj: EDisGo, + level: str, + apparent_power: float, + component_type: str | None = None, + length: float = 0.0, + max_voltage_diff: float | None = None, + max_cables: int = 7, +) -> tuple[pd.Series, int]: + """ + Selects suitable cable type and quantity based on apparent power and + voltage deviation. + + The cable is selected to carry the given `apparent_power` and to ensure + acceptable voltage deviation over the cable. + Overhead lines are not considered in choosing a suitable cable. Parameters ---------- @@ -209,49 +353,96 @@ def select_cable(edisgo_obj, level, apparent_power): 'lv'. apparent_power : float Apparent power the cable must carry in MVA. + component_type : str + Type of the component to be connected. Possible options are "generator", + "conventional_load", "charging_point", "heat_pump" or "storage_unit". + Only needed in case a cable length is given and thus the voltage difference over + the cable can be taken into account for selecting a suitable cable. In that case + it is used to obtain the default power factor and reactive power mode from the + configuration files in sections `reactive_power_factor` and + `reactive_power_mode`. + Default: None. + length : float + Length of the cable in km. Default: 0. + max_voltage_diff : float + Maximum allowed voltage difference in p.u.. + If None, it defaults to the value specified in the configuration file + under the `grid_connection` section for the respective voltage level + (lv_max_voltage_deviation for LV and mv_max_voltage_deviation for MV). + Default: None. + max_cables : int + Maximum number of cables to consider. Default: 7. Returns ------- - :pandas:`pandas.Series` - Series with attributes of selected cable as in equipment data and - cable type as series name. - int - Number of necessary parallel cables. + tuple[:pandas:`pandas.Series`, int] + A tuple containing information on the selected cable type and the quantity + needed. """ - - cable_count = 1 - if level == "mv": cable_data = edisgo_obj.topology.equipment_data["mv_cables"] available_cables = cable_data[ cable_data["U_n"] == edisgo_obj.topology.mv_grid.nominal_voltage ] + if not max_voltage_diff: + max_voltage_diff = edisgo_obj.config["grid_connection"][ + "mv_max_voltage_deviation" + ] elif level == "lv": available_cables = edisgo_obj.topology.equipment_data["lv_cables"] + if not max_voltage_diff: + max_voltage_diff = edisgo_obj.config["grid_connection"][ + "lv_max_voltage_deviation" + ] else: raise ValueError( "Specified voltage level is not valid. Must either be 'mv' or 'lv'." ) + cable_count = 1 suitable_cables = available_cables[ calculate_apparent_power( available_cables["U_n"], available_cables["I_max_th"], cable_count ) > apparent_power ] + if length != 0: + suitable_cables = suitable_cables[ + calculate_voltage_diff_pu_per_line_from_type( + edisgo_obj=edisgo_obj, + cable_names=suitable_cables.index, + length=length, + num_parallel=cable_count, + v_nom=available_cables["U_n"].values[0], + s_max=apparent_power, + component_type=component_type, + ) + < max_voltage_diff + ] # increase cable count until appropriate cable type is found - while suitable_cables.empty and cable_count < 7: + while suitable_cables.empty and cable_count < max_cables: # parameter cable_count += 1 suitable_cables = available_cables[ calculate_apparent_power( - available_cables["U_n"], - available_cables["I_max_th"], - cable_count, + available_cables["U_n"], available_cables["I_max_th"], cable_count ) > apparent_power ] + if length != 0: + suitable_cables = suitable_cables[ + calculate_voltage_diff_pu_per_line_from_type( + edisgo_obj=edisgo_obj, + cable_names=available_cables.index, + length=length, + num_parallel=cable_count, + v_nom=available_cables["U_n"].values[0], + s_max=apparent_power, + component_type=component_type, + ) + < max_voltage_diff + ] if suitable_cables.empty: raise exceptions.MaximumIterationError( "Could not find a suitable cable for apparent power of " diff --git a/tests/flex_opt/test_q_control.py b/tests/flex_opt/test_q_control.py index 9595ec1c..a028c154 100644 --- a/tests/flex_opt/test_q_control.py +++ b/tests/flex_opt/test_q_control.py @@ -101,7 +101,7 @@ def test__fixed_cosphi_default_power_factor( # test for component_type="generators" pf = q_control._fixed_cosphi_default_power_factor( - comp_df=df, component_type="generators", configs=config + comp_df=df, component_type="generator", configs=config ) assert pf.shape == (3,) @@ -112,7 +112,7 @@ def test__fixed_cosphi_default_power_factor( # test for component_type="loads" pf = q_control._fixed_cosphi_default_power_factor( - comp_df=df, component_type="conventional_loads", configs=config + comp_df=df, component_type="conventional_load", configs=config ) assert pf.shape == (3,) @@ -123,7 +123,7 @@ def test__fixed_cosphi_default_power_factor( # test for component_type="charging_points" pf = q_control._fixed_cosphi_default_power_factor( - comp_df=df, component_type="charging_points", configs=config + comp_df=df, component_type="charging_point", configs=config ) assert pf.shape == (3,) @@ -134,7 +134,7 @@ def test__fixed_cosphi_default_power_factor( # test for component_type="heat_pumps" pf = q_control._fixed_cosphi_default_power_factor( - comp_df=df, component_type="heat_pumps", configs=config + comp_df=df, component_type="heat_pump", configs=config ) assert pf.shape == (3,) @@ -145,7 +145,7 @@ def test__fixed_cosphi_default_power_factor( # test for component_type="storage_units" pf = q_control._fixed_cosphi_default_power_factor( - comp_df=df, component_type="storage_units", configs=config + comp_df=df, component_type="storage_unit", configs=config ) assert pf.shape == (3,) @@ -165,7 +165,7 @@ def test__fixed_cosphi_default_reactive_power_sign( # test for component_type="generators" pf = q_control._fixed_cosphi_default_reactive_power_sign( - comp_df=df, component_type="generators", configs=config + comp_df=df, component_type="generator", configs=config ) assert pf.shape == (3,) @@ -176,7 +176,7 @@ def test__fixed_cosphi_default_reactive_power_sign( # test for component_type="conventional_loads" pf = q_control._fixed_cosphi_default_reactive_power_sign( - comp_df=df, component_type="conventional_loads", configs=config + comp_df=df, component_type="conventional_load", configs=config ) assert pf.shape == (3,) @@ -187,7 +187,7 @@ def test__fixed_cosphi_default_reactive_power_sign( # test for component_type="charging_points" pf = q_control._fixed_cosphi_default_reactive_power_sign( - comp_df=df, component_type="charging_points", configs=config + comp_df=df, component_type="charging_point", configs=config ) assert pf.shape == (3,) @@ -198,7 +198,7 @@ def test__fixed_cosphi_default_reactive_power_sign( # test for component_type="heat_pumps" pf = q_control._fixed_cosphi_default_reactive_power_sign( - comp_df=df, component_type="heat_pumps", configs=config + comp_df=df, component_type="heat_pump", configs=config ) assert pf.shape == (3,) @@ -209,7 +209,7 @@ def test__fixed_cosphi_default_reactive_power_sign( # test for component_type="storage_units" pf = q_control._fixed_cosphi_default_reactive_power_sign( - comp_df=df, component_type="storage_units", configs=config + comp_df=df, component_type="storage_unit", configs=config ) assert pf.shape == (3,) diff --git a/tests/io/test_powermodels_io.py b/tests/io/test_powermodels_io.py index 4d0d7842..b3bfab03 100644 --- a/tests/io/test_powermodels_io.py +++ b/tests/io/test_powermodels_io.py @@ -310,7 +310,7 @@ def test__get_pf(self): # test mode None powermodels_network, hv_flex_dict = powermodels_io.to_powermodels(self.edisgo) - for component in ["gen", "storage"]: + for component in ["generator", "storage_unit"]: pf, sign = powermodels_io._get_pf( self.edisgo, powermodels_network, 1, component ) @@ -322,10 +322,10 @@ def test__get_pf(self): assert pf == 0.95 assert sign == -1 - for component in ["hp", "cp"]: + for component in ["heat_pump", "charging_point"]: for bus in [1, 29]: pf, sign = powermodels_io._get_pf( - self.edisgo, powermodels_network, 1, component + self.edisgo, powermodels_network, bus, component ) assert pf == 1 assert sign == 1 diff --git a/tests/network/test_topology.py b/tests/network/test_topology.py index 0baf02f3..e977a5bd 100644 --- a/tests/network/test_topology.py +++ b/tests/network/test_topology.py @@ -1720,7 +1720,7 @@ def test_connect_to_lv(self): loads_before = self.edisgo.topology.loads_df test_hp = { - "p_set": 0.3, + "p_set": 0.1, "geom": geom, "voltage_level": 6, "mvlv_subst_id": 6, @@ -1751,7 +1751,7 @@ def test_connect_to_lv(self): new_line_df.loc[new_line_df.index[0], ["bus0", "bus1"]] ) # check new heat pump - assert self.edisgo.topology.loads_df.at[comp_name, "p_set"] == 0.3 + assert self.edisgo.topology.loads_df.at[comp_name, "p_set"] == 0.1 # ############# storage unit ################# # test existing substation ID (voltage level 7) diff --git a/tests/tools/test_tools.py b/tests/tools/test_tools.py index 40c34a63..66216ca6 100644 --- a/tests/tools/test_tools.py +++ b/tests/tools/test_tools.py @@ -30,6 +30,184 @@ def test_calculate_line_reactance(self): data = tools.calculate_line_reactance(np.array([2, 3]), 3, 2) assert_allclose(data, np.array([1.88496 / 2, 2.82743 / 2]), rtol=1e-5) + def test_calculate_voltage_diff_pu_per_line(self): + correct_value_positive_sign = 0.03261946832784687 + correct_value_negative_sign = 0.06008053167215312 + r_total = 0.412 + x_total = 0.252 + + # test generator, float + data = tools.calculate_voltage_diff_pu_per_line( + s_max=50, + r_total=r_total, + x_total=x_total, + v_nom=20, + q_sign=-1, + power_factor=0.9, + ) + assert np.isclose(data, correct_value_positive_sign) + # test generator, array + data = tools.calculate_voltage_diff_pu_per_line( + s_max=np.array([50, 50]), + r_total=np.array([r_total, r_total]), + x_total=np.array([x_total, x_total]), + v_nom=20, + q_sign=-1, + power_factor=0.9, + ) + assert_allclose( + data, + np.array([correct_value_positive_sign, correct_value_positive_sign]), + rtol=1e-5, + ) + # test generator, float, higher voltage + data = tools.calculate_voltage_diff_pu_per_line( + s_max=50, + r_total=r_total, + x_total=x_total, + v_nom=40, + q_sign=-1, + power_factor=0.9, + ) + assert np.isclose(data, correct_value_positive_sign / 4) + # test generator, array, larger cable + data = tools.calculate_voltage_diff_pu_per_line( + s_max=np.array([100, 100]), + r_total=np.array([r_total, r_total]), + x_total=np.array([x_total, x_total]), + v_nom=np.array([20, 20]), + q_sign=-1, + power_factor=0.9, + ) + assert_allclose( + data, + np.array( + [correct_value_positive_sign * 2, correct_value_positive_sign * 2] + ), + rtol=1e-5, + ) + # test generator, capacitive + data = tools.calculate_voltage_diff_pu_per_line( + s_max=100, + r_total=r_total, + x_total=x_total, + v_nom=20, + q_sign=1, + power_factor=0.9, + ) + assert np.isclose(data, correct_value_negative_sign * 2) + # test load, capacitive + data = tools.calculate_voltage_diff_pu_per_line( + s_max=100, + r_total=r_total, + x_total=x_total, + v_nom=20, + q_sign=-1, + power_factor=0.9, + ) + assert np.isclose(data, correct_value_positive_sign * 2) + + # test the examples from VDE-AR-N 4105 attachment D + data = tools.calculate_voltage_diff_pu_per_line( + s_max=0.02, + r_total=0.2001, + x_total=0.1258, + v_nom=0.4, + q_sign=-1, + power_factor=1, + ) + assert np.isclose(data, 0.025, rtol=1e-2) + + data = tools.calculate_voltage_diff_pu_per_line( + s_max=0.022, + r_total=0.2001, + x_total=0.1258, + v_nom=0.4, + q_sign=-1, + power_factor=0.9, + ) + assert np.isclose(data, 0.0173, rtol=1e-2) + + def test_calculate_voltage_diff_pu_per_line_from_type(self): + correct_value_negative_sign = 0.4916578234319946 * 1e-2 + correct_value_positive_sign = 0.017583421765680056 + data = tools.calculate_voltage_diff_pu_per_line_from_type( + edisgo_obj=self.edisgo, + cable_names="NA2XS(FL)2Y 3x1x300 RM/25", + length=1, + num_parallel=1, + v_nom=20, + s_max=50, + component_type="generator", + ) + assert np.isclose(data, correct_value_negative_sign) + data = tools.calculate_voltage_diff_pu_per_line_from_type( + edisgo_obj=self.edisgo, + cable_names=np.array( + ["NA2XS(FL)2Y 3x1x300 RM/25", "NA2XS(FL)2Y 3x1x300 RM/25"] + ), + length=1, + num_parallel=1, + v_nom=20, + s_max=50, + component_type="generator", + ) + assert_allclose( + data, + np.array([correct_value_negative_sign, correct_value_negative_sign]), + rtol=1e-5, + ) + data = tools.calculate_voltage_diff_pu_per_line_from_type( + edisgo_obj=self.edisgo, + cable_names="NA2XS(FL)2Y 3x1x300 RM/25", + length=2, + num_parallel=1, + v_nom=20, + s_max=50, + component_type="generator", + ) + assert np.isclose(data, 2 * correct_value_negative_sign) + data = tools.calculate_voltage_diff_pu_per_line_from_type( + edisgo_obj=self.edisgo, + cable_names=np.array( + ["NA2XS(FL)2Y 3x1x300 RM/25", "NA2XS(FL)2Y 3x1x300 RM/25"] + ), + length=2, + num_parallel=1, + v_nom=20, + s_max=50, + component_type="generator", + ) + assert_allclose( + data, + np.array( + [2 * correct_value_negative_sign, 2 * correct_value_negative_sign] + ), + rtol=1e-5, + ) + + data = tools.calculate_voltage_diff_pu_per_line_from_type( + edisgo_obj=self.edisgo, + cable_names="NA2XS(FL)2Y 3x1x300 RM/25", + length=1, + num_parallel=2, + v_nom=20, + s_max=50, + component_type="generator", + ) + assert np.isclose(data, correct_value_negative_sign / 2) + + data = tools.calculate_voltage_diff_pu_per_line_from_type( + edisgo_obj=self.edisgo, + cable_names="NA2XS(FL)2Y 3x1x300 RM/25", + length=1, + num_parallel=2, + v_nom=20, + s_max=50, + component_type="conventional_load", + ) + assert np.isclose(data, correct_value_positive_sign / 2) + def test_calculate_line_resistance(self): # test single line data = tools.calculate_line_resistance(2, 3, 1) @@ -97,18 +275,103 @@ def test_drop_duplicated_columns(self): assert (check_df.loc[:, "a"] == [4, 5, 6]).all() def test_select_cable(self): - cable_data, num_parallel_cables = tools.select_cable(self.edisgo, "mv", 5.1) + # no length given + cable_data, num_parallel_cables = tools.select_cable( + self.edisgo, + "mv", + 5.1, + ) assert cable_data.name == "NA2XS2Y 3x1x150 RE/25" assert num_parallel_cables == 1 - cable_data, num_parallel_cables = tools.select_cable(self.edisgo, "mv", 40) + cable_data, num_parallel_cables = tools.select_cable( + self.edisgo, + "mv", + 40, + ) assert cable_data.name == "NA2XS(FL)2Y 3x1x500 RM/35" assert num_parallel_cables == 2 - cable_data, num_parallel_cables = tools.select_cable(self.edisgo, "lv", 0.18) + cable_data, num_parallel_cables = tools.select_cable( + self.edisgo, + "lv", + 0.18, + ) assert cable_data.name == "NAYY 4x1x150" assert num_parallel_cables == 1 + # length given + cable_data, num_parallel_cables = tools.select_cable( + self.edisgo, + "mv", + 5.1, + length=2, + component_type="conventional_load", + ) + assert cable_data.name == "NA2XS2Y 3x1x150 RE/25" + assert num_parallel_cables == 1 + + cable_data, num_parallel_cables = tools.select_cable( + self.edisgo, + "mv", + 40, + length=1, + component_type="conventional_load", + ) + assert cable_data.name == "NA2XS(FL)2Y 3x1x500 RM/35" + assert num_parallel_cables == 2 + + cable_data, num_parallel_cables = tools.select_cable( + self.edisgo, + "lv", + 0.18, + length=1, + component_type="conventional_load", + ) + assert cable_data.name == "NAYY 4x1x300" + assert num_parallel_cables == 5 + + cable_data, num_parallel_cables = tools.select_cable( + self.edisgo, + "lv", + 0.18, + length=1, + max_voltage_diff=0.01, + max_cables=100, + component_type="conventional_load", + ) + assert cable_data.name == "NAYY 4x1x300" + assert num_parallel_cables == 14 + + cable_data, num_parallel_cables = tools.select_cable( + self.edisgo, + "lv", + 0.18, + length=1, + max_voltage_diff=0.01, + max_cables=100, + component_type="generator", + ) + assert cable_data.name == "NAYY 4x1x300" + assert num_parallel_cables == 8 + + try: + tools.select_cable( + self.edisgo, + "lv", + 0.18, + length=1, + max_voltage_diff=0.01, + max_cables=100, + component_type="fail", + ) + except ValueError as e: + assert ( + str(e) == "Specified component type is not valid. " + "Must either be 'generator', 'conventional_load', 'charging_point', " + "'heat_pump' or 'storage_unit'." + ) + def test_get_downstream_buses(self): # ######## test with LV bus ######## buses_downstream = tools.get_downstream_buses(