diff --git a/doc/api/edisgo.tools.rst b/doc/api/edisgo.tools.rst index 2c268568c..1bc306a87 100644 --- a/doc/api/edisgo.tools.rst +++ b/doc/api/edisgo.tools.rst @@ -9,14 +9,6 @@ edisgo.tools.config module :undoc-members: :show-inheritance: -edisgo.tools.edisgo\_run module --------------------------------- - -.. automodule:: edisgo.tools.edisgo_run - :members: - :undoc-members: - :show-inheritance: - edisgo.tools.geo module ------------------------ @@ -33,6 +25,14 @@ edisgo.tools.geopandas\_helper module :undoc-members: :show-inheritance: +edisgo.tools.logger module +---------------------------------------- + +.. automodule:: edisgo.tools.logger + :members: + :undoc-members: + :show-inheritance: + edisgo.tools.networkx\_helper module ---------------------------------------- diff --git a/doc/conf.py b/doc/conf.py index fa800e92f..688f95197 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -92,7 +92,7 @@ "shapely": ("https://shapely.readthedocs.io/en/latest/manual.html#%s", "shapely."), "ding0": ("https://dingo.readthedocs.io/en/dev/api/ding0.html#%s", "Ding0"), "pypsa": ("https://pypsa.readthedocs.io/en/latest/components.html#%s", "pypsa"), - "plotly": ("https://plotly.com/python-api-reference/generated/#%s.html", "plotly"), + "plotly": ("https://plotly.com/python-api-reference/generated/%s.html", "plotly"), } # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] diff --git a/doc/features_in_detail.rst b/doc/features_in_detail.rst index 67b165647..afff615be 100644 --- a/doc/features_in_detail.rst +++ b/doc/features_in_detail.rst @@ -264,57 +264,57 @@ It is also possible to curtail specific generators internally, though a user fri .. _electromobility-integration-label: Electromobility integration --------------------- +---------------------------- The import and integration of electromobility data is implemented in :py:func:`~edisgo.io.electromobility_import`. Allocation of charging demand -^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The allocation of charging processes to charging stations is implemented in +The allocation of charging processes to charging stations is implemented in :py:func:`~edisgo.io.electromobility_import.distribute_charging_demand`. -After electromobility data is loaded, the charging demand from SimBEV is allocated to potential charging parks -from TracBEV. The allocation of the charging processes to the charging infrastructure is carried out with the -help of the weighting factor of the potential charging parks determined by TracBEV. This involves a random and +After electromobility data is loaded, the charging demand from SimBEV is allocated to potential charging parks +from TracBEV. The allocation of the charging processes to the charging infrastructure is carried out with the +help of the weighting factor of the potential charging parks determined by TracBEV. This involves a random and weighted selection of one charging park per charging process. In the case of private charging infrastructure, a -separate charging point is set up for each EV. All charging processes of the respective EV and charging use case -are assigned to this charging point. The allocation of private charging processes to charging stations is +separate charging point is set up for each electric vehicle (EV). All charging processes of the respective EV and charging use case +are assigned to this charging point. The allocation of private charging processes to charging stations is implemented in :py:func:`~edisgo.io.electromobility_import.distribute_private_charging_demand`. -For the public charging infrastructure, the allocation is made explicitly per charging process. For each charging -process it is determined whether a suitable charging point is already available. For this purpose it is checked -if the charging point is occupied by another EV in the corresponding period and whether it can provide the -corresponding charging capacity. If no suitable charging point is available, a charging point is determined -randomly and weighted in the same way as for private charging. The allocation of public charging processes to +For public charging infrastructure, the allocation is made explicitly per charging process. For each charging +process it is determined whether a suitable charging point is already available. For this purpose it is checked +whether the charging point is occupied by another EV in the corresponding period and whether it can provide the +corresponding charging capacity. If no suitable charging point is available, a charging point is determined +randomly and weighted in the same way as for private charging. The allocation of public charging processes to charging stations is implemented in :py:func:`~edisgo.io.electromobility_import.distribute_public_charging_demand`. +.. _charging_strategies-label: + Charging strategies ^^^^^^^^^^^^^^^^^^^^^^^^ -eDisGo right now provides three charging strategy methodologies called 'dumb', 'reduced' and 'residual', that -are implemented in :py:mod:`~edisgo.flex_opt.charging_strategies`. -The aim of the charging strategies is to generate the most grid-friendly charging behavior possible without -restricting the convenience for end users. Therefore, the boundary condition of all charging strategies is that -the charging requirement of each charging process must be fully covered. This means that charging processes can -only be used as a flexibility if the EV can be fully charged while it is stationary. Furthermore, only private -charging processes can be used as a flexibility, since the fulfillment of the service is the priority for public -charging processes. In order to be able to evaluate the three charging strategies, a reference charging strategy -is also examined. +eDisGo right now provides three charging strategy methodologies called 'dumb', 'reduced' and 'residual', that +are implemented in :py:mod:`~edisgo.flex_opt.charging_strategies`. +The aim of the charging strategies 'reduced' and 'residual' is to generate the most grid-friendly charging behavior possible without +restricting the convenience for end users. Therefore, the boundary condition of all charging strategies is that +the charging requirement of each charging process must be fully covered. This means that charging processes can +only be flexibilised if the EV can be fully charged while it is stationary. Furthermore, only private +charging processes can be used as a flexibility, since the fulfillment of the service is the priority for public +charging processes. 'dumb' """""""""""""""""" -Is the default charging strategy and corresponds to the reference charging. -The cars are charged directly after arrival with the maximum possible charging capacity. +In this charging strategy the cars are charged directly after arrival with the maximum possible charging capacity. 'reduced' """""""""""""""""" -Is a preventive charging strategy. The cars are charged directly after arrival with the minimum possible -charging power. The minimum possible charging power is determined by the parking time and the parameter +This is a preventive charging strategy. The cars are charged directly after arrival with the minimum possible +charging power. The minimum possible charging power is determined by the parking time and the parameter minimum_charging_capacity_factor. 'residual' """""""""""""""""" -Is an active charging strategy. The cars are charged when the residual load in the MV grid is lowest +This is an active charging strategy. The cars are charged when the residual load in the MV grid is lowest (high generation and low consumption). Charging processes with a low flexibility are given priority. diff --git a/doc/usage_details.rst b/doc/usage_details.rst index 6f98489d3..22c50053b 100644 --- a/doc/usage_details.rst +++ b/doc/usage_details.rst @@ -266,7 +266,8 @@ The charging strategies can be invoked as follows: edisgo.apply_charging_strategy() -See :attr:`~.edisgo.EDisGo.apply_charging_strategy` for more information. +See function docstring of :attr:`~.edisgo.EDisGo.apply_charging_strategy` or +documentation section :ref:`charging_strategies-label` for more information. Reactive power time series ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -349,27 +350,24 @@ Further information on the grid reinforcement methodology can be found in sectio Electromobility ----------------- -Electromobility data including charging processes necessary to apply different -charging strategies, as well as information on potential charging sites and -integrated charging parks can be integrated into the grid and are stored in -the :pandas:`pandas.DataFrames` or :geopandas:`GeoDataFrame` in the +Electromobility data including charging processes as well as information on potential charging sites and +integrated charging parks are stored in the :class:`~.network.electromobility.Electromobility` object. -You can access those dataframes/geodataframes as follows: +You can access these data as follows: .. code-block:: python - # Access DataFrame with all SimBEV charging processes. + # Access DataFrame with all SimBEV charging processes edisgo.electromobility.charging_processes_df - # Access GeoDataFrame with all TracBEV potential charging parks. + # Access GeoDataFrame with all TracBEV potential charging parks edisgo.electromobility.potential_charging_parks_gdf - - # Access DataFrame with all charging parks that got integrated. + + # Access DataFrame with all charging parks that got integrated edisgo.electromobility.integrated_charging_parks_df - -The integrated charging points are then also stored in the -:pandas:`pandas.DataFrames` in the :class:`~.network.topology.Topology` + +The integrated charging points are also stored in the :class:`~.network.topology.Topology` object and can be accessed as follows: .. code-block:: python @@ -378,21 +376,23 @@ object and can be accessed as follows: edisgo.topology.charging_points_df -So far, adding electormobility data to an eDisGo object requires electromobility +So far, adding electromobility data to an eDisGo object requires electromobility data from `SimBEV `_ (required version: `3083c5a `_) +86076c936940365587c9fba98a5b774e13083c5a>`_) and `TracBEV `_ (required version: `14d864c `_) to be stored in the directories -specified through the parameters simbev_directory and tracbev_directory. -SimBEV provides data on standing times, charging demand, etc. per vehicle, +03e335655770a377166c05293a966052314d864c>`_) to be stored in the directories +specified through the parameters simbev_directory and tracbev_directory. +SimBEV provides data on standing times, charging demand, etc. per vehicle, whereas TracBEV provides potential charging point locations. .. todo:: Add information on how to retrieve SimBEV and TracBEV data -Here is a small examples on how to import electromobility data and apply a -charging strategy to the charging processes. +Here is a small example on how to import electromobility data and apply a +charging strategy. A more extensive example can be found in +the example jupyter notebook +`electromobility_example `_. .. code-block:: python @@ -407,25 +407,27 @@ charging strategy to the charging processes. ) edisgo.set_time_series_active_power_predefined( fluctuating_generators_ts="oedb", - dispatchable_generators_ts=pd.DataFrame(data=1, columns=["other"], index=timeindex), + dispatchable_generators_ts=pd.DataFrame( + data=1, columns=["other"], index=timeindex), conventional_loads_ts="demandlib", ) edisgo.set_time_series_reactive_power_control() - - # Resample edisgo timeseries to 15-minute resolution to match simBEV and tracBEV data + + # Resample edisgo timeseries to 15-minute resolution to match with SimBEV and + # TracBEV data edisgo.resample_timeseries() - + # Import electromobility data edisgo.import_electromobility( simbev_directory=simbev_path, tracbev_directory=tracbev_path, ) - + # Apply charging strategy edisgo.apply_charging_strategy(strategy="dumb") -Further information on the electromobility integration methodology and the charging +Further information on the electromobility integration methodology and the charging strategies can be found in section :ref:`electromobility-integration-label`. @@ -565,18 +567,18 @@ line loading and node voltages in the MV grid or as a histograms. # plot voltage histogram edisgo.histogram_voltage() - + # draw a plotly html figure draw_plotly(edisgo) - # plot relative loading and voltage deviation, with grid coordinates + # plot relative loading and voltage deviation, with grid coordinates # modified to have the station in the origin draw_plotly( - edisgo, G=edisgo_obj.topology.mv_grid.graph, - line_color="relative_loading", node_color="voltage_deviation", + edisgo, G=edisgo_obj.topology.mv_grid.graph, + line_color="relative_loading", node_color="voltage_deviation", grid=edisgo.topology.mv_grid ) - + See :class:`~.EDisGo` class for more plots and plotting options. Results diff --git a/doc/whatsnew/v0-2-0.rst b/doc/whatsnew/v0-2-0.rst index aaabb5685..4a2d8af6a 100644 --- a/doc/whatsnew/v0-2-0.rst +++ b/doc/whatsnew/v0-2-0.rst @@ -9,11 +9,13 @@ Changes * added pre-commit hooks (flake8, black, isort, pyupgrade) `#229 `_ * added issue and pull request templates `#220 `_ * added Windows installation yml and documentation +* added functionality to set up different loggers with individual logging levels and where to write output `#295 `_ * added integrity checks of eDisGo object `#231 `_ * added functionality to save to and load from zip archive `#216 `_ * added option to not raise error in case power flow did not converge `#207 `_ * added pyplot `#214 `_ * added functionality to create geopandas dataframes `#224 `_ +* added functionality to resample time series `#269 `_ * added tests * major refactoring of loads and time series diff --git a/edisgo/config/config_grid_expansion_default.cfg b/edisgo/config/config_grid_expansion_default.cfg index 4917c2424..a82e027c9 100644 --- a/edisgo/config/config_grid_expansion_default.cfg +++ b/edisgo/config/config_grid_expansion_default.cfg @@ -13,7 +13,8 @@ # Standard equipment for grid expansion measures. Source: Rehtanz et. al.: "Verteilnetzstudie für das Land Baden-Württemberg", 2017. hv_mv_transformer = 40 MVA mv_lv_transformer = 630 kVA -mv_line = NA2XS2Y 3x1x185 RM/25 +mv_line_10kv = NA2XS2Y 3x1x185 RM/25 +mv_line_20kv = NA2XS2Y 3x1x240 lv_line = NAYY 4x1x150 [grid_expansion_allowed_voltage_deviations] diff --git a/edisgo/edisgo.py b/edisgo/edisgo.py index f3648fdb0..38897a62e 100755 --- a/edisgo/edisgo.py +++ b/edisgo/edisgo.py @@ -116,7 +116,7 @@ class EDisGo: def __init__(self, **kwargs): # load configuration - self._config = Config(config_path=kwargs.get("config_path", None)) + self._config = Config(config_path=kwargs.get("config_path", "default")) # instantiate topology object and load grid data self.topology = Topology(config=self.config) @@ -2039,30 +2039,46 @@ def check_integrity(self): def resample_timeseries(self, method: str = "ffill", freq: str = "15min"): """ - Returns timeseries resampled from hourly resolution to 15 minute resolution. + Resamples all generator, load and storage time series to a desired resolution. - Parameters - ---------- - method : str, optional - Method to choose from to fill missing values when upsampling. Possible - options are: + The following time series are affected by this: + + * :attr:`~.network.timeseries.TimeSeries.generators_active_power` + + * :attr:`~.network.timeseries.TimeSeries.loads_active_power` + + * :attr:`~.network.timeseries.TimeSeries.storage_units_active_power` - * 'ffill': propagate last valid observation forward to next valid - observation. See - https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.ffill.html - 'ffill' is the Default. + * :attr:`~.network.timeseries.TimeSeries.generators_reactive_power` - * 'bfill': use next valid observation to fill gap. See - https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.bfill.html + * :attr:`~.network.timeseries.TimeSeries.loads_reactive_power` - * 'interpolate': Fill NaN values using an interpolation method. See - https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.interpolate.html + * :attr:`~.network.timeseries.TimeSeries.storage_units_reactive_power` + + Both up- and down-sampling methods are possible. + + Parameters + ---------- + method : str, optional + Method to choose from to fill missing values when resampling. + Possible options are: + * 'ffill' + Propagate last valid observation forward to next valid + observation. See :pandas:`pandas.DataFrame.ffill`. + * 'bfill' + Use next valid observation to fill gap. See + :pandas:`pandas.DataFrame.bfill`. + * 'interpolate' + Fill NaN values using an interpolation method. See + :pandas:`pandas.DataFrame.interpolate`. + + Default: 'ffill'. freq : str, optional - Frequency that timeseries is resampled to. Can be any frequency up to one - hour. Offset aliases can be found here: - https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases - 15 minutes is the default. + Frequency that time series is resampled to. Offset aliases can be found + here: + https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases. + Default: '15min'. """ self.timeseries.resample_timeseries(method=method, freq=freq) diff --git a/edisgo/equipment/equipment-parameters_LV_cables.csv b/edisgo/equipment/equipment-parameters_LV_cables.csv index 475b3c58e..ac72e50fc 100644 --- a/edisgo/equipment/equipment-parameters_LV_cables.csv +++ b/edisgo/equipment/equipment-parameters_LV_cables.csv @@ -1,10 +1,10 @@ -name,U_n,I_max_th,R_per_km,L_per_km -#-,kV,kA,ohm/km,mH/km -NAYY 4x1x300,0.4,0.419,0.1,0.279 -NAYY 4x1x240,0.4,0.364,0.125,0.254 -NAYY 4x1x185,0.4,0.313,0.164,0.256 -NAYY 4x1x150,0.4,0.275,0.206,0.256 -NAYY 4x1x120,0.4,0.245,0.253,0.256 -NAYY 4x1x95,0.4,0.215,0.320,0.261 -NAYY 4x1x50,0.4,0.144,0.449,0.270 -NAYY 4x1x35,0.4,0.123,0.868,0.271 +name,U_n,I_max_th,R_per_km,L_per_km,C_per_km +#-,kV,kA,ohm/km,mH/km,uF/km +NAYY 4x1x300,0.4,0.419,0.1,0.279,0 +NAYY 4x1x240,0.4,0.364,0.125,0.254,0 +NAYY 4x1x185,0.4,0.313,0.164,0.256,0 +NAYY 4x1x150,0.4,0.275,0.206,0.256,0 +NAYY 4x1x120,0.4,0.245,0.253,0.256,0 +NAYY 4x1x95,0.4,0.215,0.320,0.261,0 +NAYY 4x1x50,0.4,0.144,0.449,0.270,0 +NAYY 4x1x35,0.4,0.123,0.868,0.271,0 diff --git a/edisgo/flex_opt/charging_strategies.py b/edisgo/flex_opt/charging_strategies.py index c92057004..9946fc227 100644 --- a/edisgo/flex_opt/charging_strategies.py +++ b/edisgo/flex_opt/charging_strategies.py @@ -32,7 +32,7 @@ ], } -logger = logging.getLogger("edisgo") +logger = logging.getLogger(__name__) # TODO: the dummy timeseries should be as long as the simulated days and not diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index cc9126a87..2fb896d27 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -398,7 +398,7 @@ def reinforce_lines_voltage_issues(edisgo_obj, grid, crit_nodes): ] elif isinstance(grid, MVGrid): standard_line = edisgo_obj.config["grid_expansion_standard_equipment"][ - "mv_line" + f"mv_line_{int(grid.nominal_voltage)}kv" ] else: raise ValueError("Inserted grid is invalid.") @@ -693,40 +693,52 @@ def _replace_by_parallel_standard_lines(lines): lines_changes.update(number_parallel_lines.to_dict()) - standard_line_type = edisgo_obj.config["grid_expansion_standard_equipment"][ - "{}_line".format(voltage_level) - ] - lines_changes = {} # chose lines of right grid level relevant_lines = edisgo_obj.topology.lines_df.loc[ crit_lines[crit_lines.voltage_level == voltage_level].index ] + if not relevant_lines.empty: + nominal_voltage = edisgo_obj.topology.buses_df.loc[ + edisgo_obj.topology.lines_df.loc[relevant_lines.index[0], "bus0"], "v_nom" + ] + if nominal_voltage == 0.4: + standard_line_type = edisgo_obj.config["grid_expansion_standard_equipment"][ + "lv_line" + ] + else: + standard_line_type = edisgo_obj.config["grid_expansion_standard_equipment"][ + f"mv_line_{int(nominal_voltage)}kv" + ] - # handling of standard lines - lines_standard = relevant_lines.loc[relevant_lines.type_info == standard_line_type] - if not lines_standard.empty: - _add_parallel_standard_lines(lines_standard.index) + # handling of standard lines + lines_standard = relevant_lines.loc[ + relevant_lines.type_info == standard_line_type + ] + if not lines_standard.empty: + _add_parallel_standard_lines(lines_standard.index) - # get lines that have not been updated yet (i.e. that are not standard - # lines) - relevant_lines = relevant_lines.loc[ - ~relevant_lines.index.isin(lines_standard.index) - ] - # handling of cables where adding one cable is sufficient - lines_single = ( - relevant_lines.loc[relevant_lines.num_parallel == 1] - .loc[relevant_lines.kind == "cable"] - .loc[crit_lines.max_rel_overload < 2] - ) - if not lines_single.empty: - _add_one_parallel_line_of_same_type(lines_single.index) + # get lines that have not been updated yet (i.e. that are not standard + # lines) + relevant_lines = relevant_lines.loc[ + ~relevant_lines.index.isin(lines_standard.index) + ] + # handling of cables where adding one cable is sufficient + lines_single = ( + relevant_lines.loc[relevant_lines.num_parallel == 1] + .loc[relevant_lines.kind == "cable"] + .loc[crit_lines.max_rel_overload < 2] + ) + if not lines_single.empty: + _add_one_parallel_line_of_same_type(lines_single.index) - # handle rest of lines (replace by as many parallel standard lines as - # needed) - relevant_lines = relevant_lines.loc[~relevant_lines.index.isin(lines_single.index)] - if not relevant_lines.empty: - _replace_by_parallel_standard_lines(relevant_lines.index) + # handle rest of lines (replace by as many parallel standard lines as + # needed) + relevant_lines = relevant_lines.loc[ + ~relevant_lines.index.isin(lines_single.index) + ] + if not relevant_lines.empty: + _replace_by_parallel_standard_lines(relevant_lines.index) return lines_changes diff --git a/edisgo/io/pypsa_io.py b/edisgo/io/pypsa_io.py index 50fd913a4..4f3e2df3a 100755 --- a/edisgo/io/pypsa_io.py +++ b/edisgo/io/pypsa_io.py @@ -106,7 +106,7 @@ def _set_slack(grid): ], "Line": edisgo_object.topology.lines_df.loc[ :, - ["bus0", "bus1", "x", "r", "s_nom", "num_parallel", "length"], + ["bus0", "bus1", "x", "r", "b", "s_nom", "num_parallel", "length"], ], "Transformer": edisgo_object.topology.transformers_df.loc[ :, ["bus0", "bus1", "x_pu", "r_pu", "type_info", "s_nom"] diff --git a/edisgo/network/electromobility.py b/edisgo/network/electromobility.py index 72739aa85..de6190391 100644 --- a/edisgo/network/electromobility.py +++ b/edisgo/network/electromobility.py @@ -12,7 +12,7 @@ if "READTHEDOCS" not in os.environ: import geopandas as gpd -logger = logging.getLogger("edisgo") +logger = logging.getLogger(__name__) COLUMNS = { "charging_processes_df": [ diff --git a/edisgo/network/timeseries.py b/edisgo/network/timeseries.py index 6e689a928..d8abbfad8 100644 --- a/edisgo/network/timeseries.py +++ b/edisgo/network/timeseries.py @@ -307,9 +307,11 @@ def reset(self): Resets all time series. Active and reactive power time series of all loads, generators and storage units - are deleted, as well as everything stored in :py:attr:`~time_series_raw`. + are deleted, as well as timeindex and everything stored in + :py:attr:`~time_series_raw`. """ + self.timeindex = pd.DatetimeIndex([]) self.generators_active_power = None self.loads_active_power = None self.storage_units_active_power = None @@ -2147,32 +2149,22 @@ def _check_if_components_exist( def resample_timeseries(self, method: str = "ffill", freq: str = "15min"): """ - Returns timeseries resampled to a desired resolution. Both up- and down- - sampling methods are available. + Resamples all generator, load and storage time series to a desired resolution. + + See :attr:`~.EDisGo.resample_timeseries` for more information. Parameters ---------- method : str, optional - Method to choose from to fill missing values when upsampling. Possible - options are: - - * 'ffill': propagate last valid observation forward to next valid - observation. See - https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.ffill.html - 'ffill' is the Default. - - * 'bfill': use next valid observation to fill gap. See - https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.bfill.html - - * 'interpolate': Fill NaN values using an interpolation method. See - https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.interpolate.html + See :attr:`~.EDisGo.resample_timeseries` for more information. freq : str, optional - Frequency that timeseries is resampled to. Offset aliases can be found here: - https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases - 15 minutes is the default. + See :attr:`~.EDisGo.resample_timeseries` for more information. """ + + # add time step at the end of the time series in case of up-sampling so that + # last time interval in the original time series is still included attrs = self._attributes freq_orig = self.timeindex[1] - self.timeindex[0] df_dict = {} @@ -2187,6 +2179,8 @@ def resample_timeseries(self, method: str = "ffill", freq: str = "15min"): .reindex(df_dict[attr].index.union(new_dates).unique().sort_values()) .ffill() ) + + # create new index if pd.Timedelta(freq) < freq_orig: # up-sampling index = pd.date_range( self.timeindex[0], @@ -2200,7 +2194,11 @@ def resample_timeseries(self, method: str = "ffill", freq: str = "15min"): self.timeindex[-1], freq=freq, ) + + # set new timeindex self._timeindex = index + + # resample time series if pd.Timedelta(freq) < freq_orig: # up-sampling if method == "interpolate": for attr in attrs: @@ -2221,7 +2219,7 @@ def resample_timeseries(self, method: str = "ffill", freq: str = "15min"): ) else: raise NotImplementedError( - 'Resampling method "{}" is not implemented.'.format(method) + f"Resampling method {method} is not implemented." ) else: # down-sampling for attr in attrs: diff --git a/edisgo/network/topology.py b/edisgo/network/topology.py index 1eeeddbdd..78cf9e84f 100755 --- a/edisgo/network/topology.py +++ b/edisgo/network/topology.py @@ -20,6 +20,7 @@ calculate_apparent_power, calculate_line_reactance, calculate_line_resistance, + calculate_line_susceptance, select_cable, ) @@ -48,6 +49,7 @@ "length", "x", "r", + "b", "s_nom", "num_parallel", "type_info", @@ -1247,7 +1249,7 @@ def add_line(self, bus0, bus1, length, **kwargs): Adds line to topology. Line name is generated automatically. - If `type_info` is provided, `x`, `r` and `s_nom` are calculated. + If `type_info` is provided, `x`, `r`, `b` and `s_nom` are calculated. Parameters ---------- @@ -1262,8 +1264,8 @@ def add_line(self, bus0, bus1, length, **kwargs): ------------------ kwargs : Kwargs may contain any further attributes in :py:attr:`~lines_df`. - It is necessary to either provide `type_info` to determine `x`, `r` - and `s_nom` of the line, or to provide `x`, `r` and `s_nom` + It is necessary to either provide `type_info` to determine `x`, `r`, `b` + and `s_nom` of the line, or to provide `x`, `r`, `b` and `s_nom` directly. """ @@ -1328,6 +1330,7 @@ def _get_line_data(): # unpack optional parameters x = kwargs.get("x", None) r = kwargs.get("r", None) + b = kwargs.get("b", 0.0) s_nom = kwargs.get("s_nom", None) num_parallel = kwargs.get("num_parallel", 1) type_info = kwargs.get("type_info", None) @@ -1335,10 +1338,10 @@ def _get_line_data(): # if type of line is specified calculate x, r and s_nom if type_info is not None: - if x is not None or r is not None or s_nom is not None: + if x is not None or r is not None or b is not None or s_nom is not None: warnings.warn( "When line 'type_info' is provided when creating a new " - "line, x, r and s_nom are calculated and provided " + "line, x, r, b and s_nom are calculated and provided " "parameters are overwritten." ) line_data = _get_line_data() @@ -1348,6 +1351,7 @@ def _get_line_data(): ).iloc[0, :] x = calculate_line_reactance(line_data.L_per_km, length, num_parallel) r = calculate_line_resistance(line_data.R_per_km, length, num_parallel) + b = calculate_line_susceptance(line_data.C_per_km, length, num_parallel) s_nom = calculate_apparent_power( line_data.U_n, line_data.I_max_th, num_parallel ) @@ -1374,6 +1378,7 @@ def _get_line_data(): "bus1": bus1, "x": x, "r": r, + "b": b, "length": length, "type_info": type_info, "num_parallel": num_parallel, @@ -1595,7 +1600,7 @@ def update_number_of_parallel_lines(self, lines_num_parallel): """ Changes number of parallel lines and updates line attributes. - When number of parallel lines changes, attributes x, r, and s_nom have + When number of parallel lines changes, attributes x, r, b, and s_nom have to be adapted, which is done in this function. Parameters @@ -1606,12 +1611,17 @@ def update_number_of_parallel_lines(self, lines_num_parallel): new number of parallel lines. """ - # update x, r and s_nom + # update x, r, b and s_nom self._lines_df.loc[lines_num_parallel.index, "x"] = ( self._lines_df.loc[lines_num_parallel.index, "x"] * self._lines_df.loc[lines_num_parallel.index, "num_parallel"] / lines_num_parallel ) + self._lines_df.loc[lines_num_parallel.index, "b"] = ( + self._lines_df.loc[lines_num_parallel.index, "b"] + / self._lines_df.loc[lines_num_parallel.index, "num_parallel"] + * lines_num_parallel + ) self._lines_df.loc[lines_num_parallel.index, "r"] = ( self._lines_df.loc[lines_num_parallel.index, "r"] * self._lines_df.loc[lines_num_parallel.index, "num_parallel"] @@ -1652,7 +1662,9 @@ def change_line_type(self, lines, new_line_type): data_new_line = self.equipment_data["lv_cables"].loc[new_line_type] except KeyError: try: - data_new_line = self.equipment_data["mv_cables"].loc[new_line_type] + data_new_line = ( + self.equipment_data["mv_cables"].loc[new_line_type].copy() + ) # in case of MV cable adapt nominal voltage to MV voltage grid_voltage = self.buses_df.at[ self.lines_df.at[lines[0], "bus0"], "v_nom" @@ -1679,19 +1691,25 @@ def change_line_type(self, lines, new_line_type): self._lines_df.loc[lines, "num_parallel"] = 1 self._lines_df.loc[lines, "kind"] = "cable" - self._lines_df.loc[lines, "r"] = ( - data_new_line.R_per_km * self.lines_df.loc[lines, "length"] + self._lines_df.loc[lines, "r"] = calculate_line_resistance( + data_new_line.R_per_km, + self.lines_df.loc[lines, "length"], + self._lines_df.loc[lines, "num_parallel"], + ) + self._lines_df.loc[lines, "x"] = calculate_line_reactance( + data_new_line.L_per_km, + self.lines_df.loc[lines, "length"], + self._lines_df.loc[lines, "num_parallel"], ) - self._lines_df.loc[lines, "x"] = ( - data_new_line.L_per_km - * 2 - * np.pi - * 50 - / 1e3 - * self.lines_df.loc[lines, "length"] + self._lines_df.loc[lines, "b"] = calculate_line_susceptance( + data_new_line.C_per_km, + self.lines_df.loc[lines, "length"], + self._lines_df.loc[lines, "num_parallel"], ) - self._lines_df.loc[lines, "s_nom"] = ( - np.sqrt(3) * data_new_line.U_n * data_new_line.I_max_th + self._lines_df.loc[lines, "s_nom"] = calculate_apparent_power( + data_new_line.U_n, + data_new_line.I_max_th, + self._lines_df.loc[lines, "num_parallel"], ) def connect_to_mv(self, edisgo_object, comp_data, comp_type="generator"): diff --git a/edisgo/tools/config.py b/edisgo/tools/config.py index 55c011781..c97bba4f5 100644 --- a/edisgo/tools/config.py +++ b/edisgo/tools/config.py @@ -53,31 +53,38 @@ class Config: Parameters ----------- - config_path : None or :obj:`str` or :obj:`dict` + config_path : None or str or :dict Path to the config directory. Options are: + * 'default' (default) + If `config_path` is set to 'default', the provided default config files + are used directly. + * str + If `config_path` is a string, configs will be loaded from the + directory specified by `config_path`. If the directory + does not exist, it is created. If config files don't exist, the + default config files are copied into the directory. + * dict + A dictionary can be used to specify different paths to the + different config files. The dictionary must have the following + keys: + + * 'config_db_tables' + + * 'config_grid' + + * 'config_grid_expansion' + + * 'config_timeseries' + + Values of the dictionary are paths to the corresponding + config file. In contrast to the other options, the directories + and config files must exist and are not automatically created. * None - If `config_path` is None configs are loaded from the edisgo - default config directory ($HOME$/.edisgo). If the directory - does not exist it is created. If config files don't exist the - default config files are copied into the directory. - * :obj:`str` - If `config_path` is a string configs will be loaded from the - directory specified by `config_path`. If the directory - does not exist it is created. If config files don't exist the - default config files are copied into the directory. - * :obj:`dict` - A dictionary can be used to specify different paths to the - different config files. The dictionary must have the following - keys: - * 'config_db_tables' - * 'config_grid' - * 'config_grid_expansion' - * 'config_timeseries' - - Values of the dictionary are paths to the corresponding - config file. In contrast to the other two options the directories - and config files must exist and are not automatically created. + If `config_path` is None, configs are loaded from the edisgo + default config directory ($HOME$/.edisgo). If the directory + does not exist, it is created. If config files don't exist, the + default config files are copied into the directory. Default: None. @@ -109,7 +116,7 @@ def _load_config(config_path=None): Parameters ----------- - config_path : None or :obj:`str` or dict + config_path : None or str or dict See class definition for more information. Returns @@ -128,7 +135,14 @@ def _load_config(config_path=None): ] # load configs - if isinstance(config_path, dict): + if config_path == "default": + for conf in config_files: + conf = conf + "_default" + load_config( + filename="{}.cfg".format(conf), + config_dir=os.path.join(package_path, "config"), + ) + elif isinstance(config_path, dict): for conf in config_files: load_config( filename="{}.cfg".format(conf), @@ -202,13 +216,13 @@ def load_config(filename, config_dir=None, copy_default_config=True): Parameters ----------- - filename : :obj:`str` + filename : str Config file name, e.g. 'config_grid.cfg'. - config_dir : :obj:`str`, optional + config_dir : str, optional Path to config file. If None uses default edisgo config directory specified in config file 'config_system.cfg' in section 'user_dirs' by subsections 'root_dir' and 'config_dir'. Default: None. - copy_default_config : Boolean + copy_default_config : bool If True copies a default config file into `config_dir` if the specified config file does not exist. Default: True. @@ -254,12 +268,12 @@ def get(section, key): Parameters ----------- - section : :obj:`str` - key : :obj:`str` + section : str + key : str Returns -------- - float or int or Boolean or str + float or int or bool or str The value which will be casted to float, int or boolean. If no cast is successful, the raw string is returned. @@ -285,7 +299,7 @@ def get_default_config_path(): Returns -------- - :obj:`str` + str Path to default edisgo config directory specified in config file 'config_system.cfg' in section 'user_dirs' by subsections 'root_dir' and 'config_dir'. @@ -335,7 +349,7 @@ def make_directory(directory): Parameters ----------- - directory : :obj:`str` + directory : str Directory path """ diff --git a/edisgo/tools/edisgo_run.py b/edisgo/tools/edisgo_run.py deleted file mode 100755 index 77e1f8aa9..000000000 --- a/edisgo/tools/edisgo_run.py +++ /dev/null @@ -1,612 +0,0 @@ -import argparse -import glob -import logging -import multiprocessing as mp -import os -import sys - -import multiprocess as mp2 -import pandas as pd - -from edisgo import EDisGo -from edisgo.flex_opt.exceptions import MaximumIterationError -from edisgo.network.results import Results - - -def setup_logging( - logfilename=None, - logfile_loglevel="debug", - console_loglevel="info", - **logging_kwargs -): - # a dict to help with log level definition - loglevel_dict = { - "info": logging.INFO, - "debug": logging.DEBUG, - "warn": logging.WARNING, - "warning": logging.WARNING, - "error": logging.ERROR, - "critical": logging.CRITICAL, - } - - if not (logfilename): - logfilename = "edisgo_run.log" - - logging.basicConfig( - filename=logfilename, - format="%(asctime)s - %(name)s -" + " %(levelname)s - %(message)s", - datefmt="%m/%d/%Y %H:%M:%S", - level=loglevel_dict[logfile_loglevel], - ) - - root_logger = logging.getLogger() - - console_stream = logging.StreamHandler() - console_stream.setLevel(loglevel_dict[console_loglevel]) - console_formatter = logging.Formatter( - fmt="%(asctime)s - %(name)s -" + " %(levelname)s - %(message)s", - datefmt="%m/%d/%Y %H:%M:%S", - ) - console_stream.setFormatter(console_formatter) - - # add stream handler to root logger - root_logger.addHandler(console_stream) - - return root_logger - - -def run_edisgo_basic( - ding0_path, generator_scenario=None, analysis="worst-case", *edisgo_grid -): - """ - Determine network expansion costs for given ding0 grid and scenario. - - Parameters - ---------- - ding0_path : str - Path to ding0 network csv data. - - analysis : str - Either 'worst-case' or 'timeseries'. - - generator_scenario : None or :obj:`str` - If provided defines which scenario of future generator park to use - and invokes import of these generators. Possible options are 'nep2035' - and 'ego100'. - - edisgo_grid : :class:`~.EDisGo` (optional) - If an EDisGo object is provided it is used instead of creating a new - object using parameters `ding0_path` and `analysis`. - - Returns - ------- - edisgo_grid : :class:`~.EDisGo` - costs : :pandas:`pandas.DataFrame` - Costs of network expansion - grid_issues : dict - Log for remaining grid issues after network expansion. For grids - resulting in an error this gives the error message. - - """ - - grid_issues = {} - - if edisgo_grid: # if an edisgo_grid is passed in arg then ignore everything else - edisgo_grid = edisgo_grid[0] - else: - try: - if "worst-case" in analysis: - edisgo_grid = EDisGo( - ding0_grid=ding0_path, worst_case_analysis=analysis - ) - elif "timeseries" in analysis: - edisgo_grid = EDisGo( - ding0_grid=ding0_path, - timeseries_generation_fluctuating="oedb", - timeseries_load="demandlib", - ) - except FileNotFoundError as e: - return ( - None, - pd.DataFrame(), - {"network": edisgo_grid, "msg": str(e)}, - ) - - logging.info("Grid expansion for MV network {}".format(edisgo_grid.topology.id)) - - # Import generators - if generator_scenario: - logging.info("Grid expansion for scenario '{}'.".format(generator_scenario)) - edisgo_grid.import_generators(generator_scenario=generator_scenario) - else: - logging.info("Grid expansion with status quo generator capacities.") - - try: - # Do network reinforcement - edisgo_grid.reinforce() - - # Get costs - costs_grouped = edisgo_grid.network.results.grid_expansion_costs.groupby( - ["type"] - ).sum() - costs = pd.DataFrame( - costs_grouped.values, - columns=costs_grouped.columns, - index=[ - [edisgo_grid.network.id] * len(costs_grouped), - costs_grouped.index, - ], - ).reset_index() - costs.rename(columns={"level_0": "network"}, inplace=True) - - grid_issues["network"] = None - grid_issues["msg"] = None - - logging.info("SUCCESS!") - except MaximumIterationError: - grid_issues["network"] = edisgo_grid.network.id - grid_issues["msg"] = str(edisgo_grid.network.results.unresolved_issues) - costs = pd.DataFrame(dtype=float) - logging.warning("Unresolved issues left after network expansion.") - except Exception as e: - grid_issues["network"] = edisgo_grid.network.id - grid_issues["msg"] = repr(e) - costs = pd.DataFrame(dtype=float) - logging.exception() - - return edisgo_grid, costs, grid_issues - - -def run_edisgo_twice(run_args): - """ - Run network analysis twice on same network: once w/ and once w/o new generators - - ToDo: adapt to refactored code! - - First run without connection of new generators approves sufficient network - hosting capacity. Otherwise, network is reinforced. - Second run assessment network extension needs in terms of RES integration - - Parameters - ---------- - run_args : list - Optional parameters for :func:`run_edisgo_basic`. - - Returns - ------- - all_costs_before_geno_import : :pandas:`pandas.Dataframe` - Grid extension cost before network connection of new generators - all_grid_issues_before_geno_import : dict - Remaining overloading or over-voltage issues in network - all_costs : :pandas:`pandas.Dataframe` - Grid extension cost due to network connection of new generators - all_grid_issues : dict - Remaining overloading or over-voltage issues in network - """ - - # base case with no generator import - ( - edisgo_grid, - costs_before_geno_import, - grid_issues_before_geno_import, - ) = run_edisgo_basic(*run_args) - - if edisgo_grid: - # clear the results object - edisgo_grid.results = Results(edisgo_grid) - edisgo_grid.config = None - - # case after generator import - # run_args = [ding0_filename] - # run_args.extend(run_args_opt) - run_args.append(edisgo_grid) - - _, costs, grid_issues = run_edisgo_basic(*run_args) - - return ( - costs_before_geno_import, - grid_issues_before_geno_import, - costs, - grid_issues, - ) - else: - return ( - costs_before_geno_import, - grid_issues_before_geno_import, - costs_before_geno_import, - grid_issues_before_geno_import, - ) - - -def run_edisgo_pool( - ding0_file_list, - run_args_opt=[None, "worst-case"], - workers=mp.cpu_count(), - worker_lifetime=1, -): - """ - Use python multiprocessing toolbox for parallelization - - Several grids are analyzed in parallel. - - Parameters - ---------- - ding0_file_list : list - Ding0 network data file names - run_args_opt : list - eDisGo options, see :func:`run_edisgo_basic` and - :func:`run_edisgo_twice`, has to contain generator_scenario and analysis as - entries - workers: int - Number of parallel process - worker_lifetime : int - Bunch of grids sequentially analyzed by a worker - - Returns - ------- - all_costs_before_geno_import : list - Grid extension cost before network connection of new generators - all_grid_issues_before_geno_import : list - Remaining overloading or over-voltage issues in network - all_costs : list - Grid extension cost due to network connection of new generators - all_grid_issues : list - Remaining overloading or over-voltage issues in network - """ - - def collect_pool_results(result): - results.append(result) - - results = [] - - pool = mp.Pool(workers, maxtasksperchild=worker_lifetime) - - for file in ding0_file_list: - edisgo_args = [file] + run_args_opt - pool.apply_async( - func=run_edisgo_twice, - args=(edisgo_args,), - callback=collect_pool_results, - ) - - pool.close() - pool.join() - - # process results data - all_costs_before_geno_import = [r[0] for r in results] - all_grid_issues_before_geno_import = [r[1] for r in results] - all_costs = [r[2] for r in results] - all_grid_issues = [r[3] for r in results] - - return ( - all_costs_before_geno_import, - all_grid_issues_before_geno_import, - all_costs, - all_grid_issues, - ) - - -def run_edisgo_pool_flexible( - ding0_id_list, - func, - func_arguments, - workers=mp2.cpu_count(), - worker_lifetime=1, -): - """ - Use python multiprocessing toolbox for parallelization - - Several grids are analyzed in parallel based on your custom function that - defines the specific application of eDisGo. - - Parameters - ---------- - ding0_id_list : list of int - List of ding0 network data IDs (also known as HV/MV substation IDs) - func : any function - Your custom function that shall be parallelized - func_arguments : tuple - Arguments to custom function ``func`` - workers: int - Number of parallel process - worker_lifetime : int - Bunch of grids sequentially analyzed by a worker - - Notes - ----- - Please note, the following requirements for the custom function which is to - be executed in parallel - - #. It must return an instance of the type :class:`~.edisgo.EDisGo`. - #. The first positional argument is the MV network district id (as int). It is - prepended to the tuple of arguments ``func_arguments`` - - - Returns - ------- - containers : dict of :class:`~.edisgo.EDisGo` - Dict of EDisGo instances keyed by its ID - """ - - def collect_pool_results(result): - """ - Store results from parallelized calculation in structured manner - - Parameters - ---------- - result: :class:`~.edisgo.EDisGo` - """ - results.update({result.network.id: result}) - - results = {} - - pool = mp2.Pool(workers, maxtasksperchild=worker_lifetime) - - def error_callback(key): - return lambda o: results.update({key: o}) - - for ding0_id in ding0_id_list: - edisgo_args = (ding0_id, *func_arguments) - pool.apply_async( - func=func, - args=edisgo_args, - callback=collect_pool_results, - error_callback=error_callback(ding0_id), - ) - - pool.close() - pool.join() - - return results - - -def edisgo_run(): - # create the argument parser - example_text = """Examples - - ...assumes all files located in PWD. - - Analyze a single network in 'worst-case' - - edisgo_run -f ding0_grids__997.pkl -wc - - - Analyze multiple grids in 'worst-case' using parallelization. Grid IDs are - specified by the grids_list.txt. - - edisgo_run -ds '' grids_list.txt ding0_grids__{}.pkl -wc --parallel - """ - parser = argparse.ArgumentParser( - description="Commandline running" + "of eDisGo", - epilog=example_text, - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - - # add the verbosity arguments - - ding0_files_parsegroup = parser.add_mutually_exclusive_group(required=True) - - ding0_files_parsegroup.add_argument( - "-f", - "--ding0-file-path", - type=str, - action="store", - dest="ding0_filename", - help="Path to a single ding0 file.", - ) - ding0_files_parsegroup.add_argument( - "-d", - "--ding0-files-directory", - type=str, - action="store", - dest="ding0_dirglob", - help="Path to a directory of ding0 files " - + "along with a file name pattern for glob input.", - ) - ding0_files_parsegroup.add_argument( - "-ds", - "--ding0-files-directory-selection", - type=str, - nargs=3, - action="store", - dest="ding0_dir_select", - help="Path to a directory of ding0 files, " - + "Path to file with list of network district numbers " - + "(one number per line), " - + "and file name template using {} where number " - + "is to be inserted . Convention is to use " - + "a double underscore before network district number " - + " like so '__{}'.", - ) - - analysis_parsegroup = parser.add_mutually_exclusive_group() - - analysis_parsegroup.add_argument( - "-wc", - "--worst-case", - action="store_true", - help="Performs a worst-case simulation with " + "a single snapshot", - ) - - analysis_parsegroup.add_argument( - "-ts", - "--timeseries", - action="store_true", - help="Performs a worst-case simulation with " + "a time-series", - ) - - parser.add_argument( - "-s", - "--scenario", - type=str, - default=None, - choices=[None, "nep2035", "ego100"], - help="'None' or 'string'\n" - + "If provided defines which scenario " - + "of future generator park to use " - + "and invokes import of these generators.\n" - + "Possible options are 'nep2035'and 'ego100'.", - ) - - parser.add_argument( - "-o", - "--output-dir", - nargs="?", - metavar="/path/to/output/", - dest="out_dir", - type=str, - default=os.path.join(sys.path[0]), - help="Absolute path to results data location.", - ) - - parser.add_argument( - "-p", - "--parallel", - action="store_true", - help="Parallel execution of multiple " - "grids. Parallelization is provided " - "by multiprocessing.", - ) - - parser.add_argument( - "-w", - "--workers", - nargs="?", - metavar="1..inf", - dest="workers", - type=int, - default=mp.cpu_count(), - help="Number of workers in parallel. In other words, " - "cores that are used for parallelization.", - ) - - parser.add_argument( - "-lw", - "--lifetime-workers", - nargs="?", - metavar="1..inf", - dest="worker_lifetime", - type=int, - default=None, - help="Lifetime of a worker of the cluster doing the " - "work. The lifetime is given is number of jobs a" - " worker does before it is replaced by a freshly " - "new one." - "The default sets the lifetime to the pools " - "lifetime. This can cause memory issues!", - ) - - args = parser.parse_args(sys.argv[1:]) - - # get current time for output file names - exec_time = pd.datetime.now().strftime("%Y-%m-%d_%H%M") - - logger = setup_logging( # noqa: F841 - logfilename="test.log", - logfile_loglevel="debug", - console_loglevel="info", - ) - - # get the list of files to run on - if args.ding0_filename: - ding0_file_list = [args.ding0_filename] - - elif args.ding0_dirglob: - ding0_file_list = glob.glob(args.ding0_dirglob) - - elif args.ding0_dir_select: - with open(args.ding0_dir_select[1], "r") as file_handle: - ding0_file_list_grid_district_numbers = list(file_handle) - ding0_file_list_grid_district_numbers = [ - _.splitlines()[0] for _ in ding0_file_list_grid_district_numbers - ] - - ding0_file_list = map( - lambda x: args.ding0_dir_select[0] + args.ding0_dir_select[2].format(x), - ding0_file_list_grid_district_numbers, - ) - else: - raise FileNotFoundError("Some of the Arguments for input files are missing.") - - # this is the serial version of the run system - run_func = run_edisgo_basic # noqa: F841 - - run_args_opt_no_scenario = [None] - run_args_opt = [args.scenario] - if args.worst_case: - run_args_opt_no_scenario.append("worst-case") - run_args_opt.append("worst-case") - elif args.timeseries: - run_args_opt_no_scenario.append("timeseries") - run_args_opt.append("timeseries") - - all_costs_before_geno_import = [] - all_grid_issues_before_geno_import = {"network": [], "msg": []} - all_costs = [] - all_grid_issues = {"network": [], "msg": []} - - if not args.parallel: - for ding0_filename in ding0_file_list: - grid_district = _get_griddistrict(ding0_filename) # noqa: F821, F841 - - run_args = [ding0_filename] - run_args.extend(run_args_opt_no_scenario) - - ( - costs_before_geno_import, - grid_issues_before_geno_import, - costs, - grid_issues, - ) = run_edisgo_twice(run_args) - - all_costs_before_geno_import.append(costs_before_geno_import) - all_grid_issues_before_geno_import["network"].append( - grid_issues_before_geno_import["network"] - ) - all_grid_issues_before_geno_import["msg"].append( - grid_issues_before_geno_import["msg"] - ) - all_costs.append(costs) - all_grid_issues["network"].append(grid_issues["network"]) - all_grid_issues["msg"].append(grid_issues["msg"]) - else: - ( - all_costs_before_geno_import, - all_grid_issues_before_geno_import, - all_costs, - all_grid_issues, - ) = run_edisgo_pool( - ding0_file_list, - run_args_opt_no_scenario, - args.workers, - args.worker_lifetime, - ) - - # consolidate costs for all the networks - all_costs_before_geno_import = pd.concat( - all_costs_before_geno_import, ignore_index=True - ) - all_costs = pd.concat(all_costs, ignore_index=True) - - # write costs and error messages to csv files - pd.DataFrame(all_grid_issues_before_geno_import).dropna(axis=0, how="all").to_csv( - args.out_dir + exec_time + "_" + "grid_issues_before_geno_import.csv", - index=False, - ) - - with open( - args.out_dir + exec_time + "_" + "costs_before_geno_import.csv", "a" - ) as f: - f.write(",,,# units: length in km,, total_costs in kEUR\n") - all_costs_before_geno_import.to_csv(f, index=False) - - pd.DataFrame(all_grid_issues).dropna(axis=0, how="all").to_csv( - args.out_dir + exec_time + "_" + "grid_issues.csv", index=False - ) - with open(args.out_dir + exec_time + "_" + "costs.csv", "a") as f: - f.write(",,,# units: length in km,, total_costs in kEUR\n") - all_costs.to_csv(f, index=False) - - -if __name__ == "__main__": - pass diff --git a/edisgo/tools/logger.py b/edisgo/tools/logger.py new file mode 100644 index 000000000..93381c9c3 --- /dev/null +++ b/edisgo/tools/logger.py @@ -0,0 +1,205 @@ +import logging +import os +import sys + +from datetime import datetime + +from edisgo.tools import config as cfg_edisgo + + +def setup_logger( + file_name=None, + log_dir=None, + loggers=None, + stream_output=sys.stdout, + debug_message=False, + reset_loggers=False, +): + """ + Setup different loggers with individual logging levels and where to write output. + + The following table from python 'Logging Howto' shows you when which logging level + is used. + + .. tabularcolumns:: |l|L| + + +--------------+---------------------------------------------+ + | Level | When it's used | + +==============+=============================================+ + | ``DEBUG`` | Detailed information, typically of interest | + | | only when diagnosing problems. | + +--------------+---------------------------------------------+ + | ``INFO`` | Confirmation that things are working as | + | | expected. | + +--------------+---------------------------------------------+ + | ``WARNING`` | An indication that something unexpected | + | | happened, or indicative of some problem in | + | | the near future (e.g. 'disk space low'). | + | | The software is still working as expected. | + +--------------+---------------------------------------------+ + | ``ERROR`` | Due to a more serious problem, the software | + | | has not been able to perform some function. | + +--------------+---------------------------------------------+ + | ``CRITICAL`` | A serious error, indicating that the program| + | | itself may be unable to continue running. | + +--------------+---------------------------------------------+ + + Parameters + ---------- + file_name : str or None + Specifies file name of file logging information is written to. Possible options + are: + + * None (default) + Saves log file with standard name `%Y_%m_%d-%H:%M:%S_edisgo.log`. + * str + Saves log file with the specified file name. + + log_dir : str or None + Specifies directory log file is saved to. Possible options are: + + * None (default) + Saves log file in current working directory. + * "default" + Saves log file into directory configured in the configs. + * str + Saves log file into the specified directory. + + loggers : None or list(dict) + + * None + Configuration as shown in the example below is used. Configures root logger + with file and stream level warning and the edisgo logger with file and + stream level debug. + * list(dict) + List of dicts with the logger configuration. Each dictionary must contain + the following keys and corresponding values: + + * 'name' + Specifies name of the logger as string, e.g. 'root' or 'edisgo'. + * 'file_level' + Specifies file logging level. Possible options are: + + * "debug" + Logs logging messages with logging level logging.DEBUG and above. + * "info" + Logs logging messages with logging level logging.INFO and above. + * "warning" + Logs logging messages with logging level logging.WARNING and above. + * "error" + Logs logging messages with logging level logging.ERROR and above. + * "critical" + Logs logging messages with logging level logging.CRITICAL. + * None + No logging messages are logged. + * 'stream_level' + Specifies stream logging level. Possible options are the same as for + `file_level`. + + stream_output : stream + Default sys.stdout is used. sys.stderr is also possible. + + debug_message : bool + If True the handlers of every configured logger is printed. + + reset_loggers : bool + If True the handlers of all loggers are cleared before configuring the loggers. + + Examples + -------- + >>> setup_logger( + >>> loggers=[ + >>> {"name": "root", "file_level": "warning", "stream_level": "warning"}, + >>> {"name": "edisgo", "file_level": "info", "stream_level": "info"} + >>> ] + >>> ) + + """ + + def create_dir(dir_path): + if not os.path.isdir(dir_path): + os.mkdir(dir_path) + + def get_default_root_dir(): + dir_path = str(cfg_edisgo.get("user_dirs", "root_dir")) + return os.path.join(os.path.expanduser("~"), dir_path) + + def create_home_dir(): + dir_path = get_default_root_dir() + create_dir(dir_path) + + cfg_edisgo.load_config("config_system.cfg") + + if file_name is None: + now = datetime.now() + file_name = now.strftime("%Y_%m_%d-%H:%M:%S_edisgo.log") + + if log_dir == "default": + create_home_dir() + log_dir = os.path.join( + get_default_root_dir(), cfg_edisgo.get("user_dirs", "log_dir") + ) + create_dir(log_dir) + + if log_dir is not None: + file_name = os.path.join(log_dir, file_name) + + if reset_loggers: + existing_loggers = [logging.getLogger()] # get the root logger + existing_loggers = existing_loggers + [ + logging.getLogger(name) for name in logging.root.manager.loggerDict + ] + + for logger in existing_loggers: + logger.handlers.clear() + + loglevel_dict = { + "debug": logging.DEBUG, + "info": logging.INFO, + "warning": logging.WARNING, + "error": logging.ERROR, + "critical": logging.CRITICAL, + None: logging.CRITICAL + 1, + } + + file_formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s: %(message)s" + ) + stream_formatter = logging.Formatter("%(name)s - %(levelname)s: %(message)s") + + if loggers is None: + loggers = [ + {"name": "root", "file_level": "warning", "stream_level": "warning"}, + {"name": "edisgo", "file_level": "info", "stream_level": "info"}, + ] + + for logger_config in loggers: + logger_name = logger_config["name"] + logger_file_level = loglevel_dict[logger_config["file_level"]] + logger_stream_level = loglevel_dict[logger_config["stream_level"]] + + if logger_name == "root": + logger = logging.getLogger() + else: + logger = logging.getLogger(logger_name) + logger.propagate = False + + if logger_file_level < logger_stream_level: + logger.setLevel(logger_file_level) + else: + logger.setLevel(logger_stream_level) + + if logger_file_level < logging.CRITICAL + 1: + file_handler = logging.FileHandler(file_name) + file_handler.setLevel(logger_file_level) + file_handler.setFormatter(file_formatter) + logger.addHandler(file_handler) + + if logger_stream_level < logging.CRITICAL + 1: + console_handler = logging.StreamHandler(stream=stream_output) + console_handler.setLevel(logger_stream_level) + console_handler.setFormatter(stream_formatter) + logger.addHandler(console_handler) + + if debug_message: + print(f"Handlers of logger {logger_name}: {logger.handlers}") diff --git a/edisgo/tools/plots.py b/edisgo/tools/plots.py index c9882822b..a9933eea3 100644 --- a/edisgo/tools/plots.py +++ b/edisgo/tools/plots.py @@ -20,6 +20,7 @@ from pypsa import Network as PyPSANetwork from edisgo.tools import session_scope, tools +from edisgo.tools.pseudo_coordinates import make_pseudo_coordinates_graph if TYPE_CHECKING: from numbers import Number @@ -865,8 +866,8 @@ def color_map_color( value: Number, vmin: Number, vmax: Number, - cmap_name: str = "coolwarm", -): + cmap_name: str | list = "coolwarm", +) -> str: """ Get matching color for a value on a matplotlib color map. @@ -878,8 +879,8 @@ def color_map_color( Minimum value on color map vmax : float or int Maximum value on color map - cmap_name : str - Name of color map to use + cmap_name : str or list + Name of color map to use, or the colormap Returns ------- @@ -888,54 +889,98 @@ def color_map_color( """ norm = matplotlib.colors.Normalize(vmin=vmin, vmax=vmax) - cmap = cm.get_cmap(cmap_name) + if isinstance(cmap_name, str): + cmap = cm.get_cmap(cmap_name) + else: + cmap = matplotlib.colors.LinearSegmentedColormap.from_list("mycmap", cmap_name) rgb = cmap(norm(abs(value)))[:3] color = matplotlib.colors.rgb2hex(rgb) return color -def draw_plotly( +def plot_plotly( edisgo_obj: EDisGo, - G: Graph | None = None, - line_color: str = "relative_loading", - node_color: str = "voltage_deviation", - grid: bool | Grid = False, + grid: Grid | None = None, + line_color: None | str = "relative_loading", + node_color: None | str = "voltage_deviation", + line_result_selection: str = "max", + node_result_selection: str = "max", + selected_timesteps: pd.Timestamp | list | None = None, + center_coordinates: bool = False, + pseudo_coordinates: bool = False, + node_selection: list | bool = False, ) -> BaseFigure: """ - Draw a plotly html figure + Draws a plotly html figure. Parameters ---------- edisgo_obj : :class:`~.EDisGo` - G : :networkx:`networkx.Graph`, optional - Graph representation of the grid as networkx Ordered Graph, where lines are - represented by edges in the graph, and buses and transformers are represented by - nodes. If no graph is given the mv grid graph of the edisgo object is used. - line_color : str, optional - Defines whereby to choose line colors (and implicitly size). Possible - options are: + Selected edisgo_obj to get plotting information from. + + grid : :class:`~.network.grids.Grid`, optional + Grid to plot. If None, the MVGrid of the edisgo_obj is plotted. Default: None. + + line_color : str or None, optional + Defines whereby to choose line colors. Possible options are: * 'loading' - Line color is set according to loading of the line. - * 'relative_loading' (Default) - Line color is set according to relative loading of the line. + Line color is set according to loading of the line. + * 'relative_loading' (default) + Line color is set according to relative loading of the line. * 'reinforce' - Line color is set according to investment costs of the line. + Line color is set according to investment costs of the line. + * None + Line color is black. This is also the fallback, in case other options fail. node_color : str or None, optional - Defines whereby to choose node colors (and implicitly size). Possible - options are: + Defines whereby to choose node colors. Possible options are: * 'adjacencies' - Node color as well as size is set according to the number of direct neighbors. + Node color as well as size is set according to the number of direct + neighbors. * 'voltage_deviation' (default) - Node color is set according to voltage deviation from 1 p.u.. + Node color is set according to voltage deviation from 1 p.u.. + * None + Line color is black. This is also the fallback, in case other options fail. + + line_result_selection : str, optional + Defines which values are shown for the load of the lines: + + * 'min' + Minimal line load of all time steps. + * 'max' (default) + Maximal line load of all time steps. + + node_result_selection : str, optional + Defines which values are shown for the voltage of the nodes: + + * 'min' + Minimal node voltage of all time steps. + * 'max' (default) + Maximal node voltage of all time steps. + + selected_timesteps : :pandas:`pandas.Timestamp` or \ + list(:pandas:`pandas.Timestamp`) or None, optional + Selected time steps to show results for. + + * None (default) + All time steps are used. + * list(:pandas:`pandas.Timestamp`) or \ + :pandas:`pandas.Timestamp` + Selected time steps are used. + + center_coordinates : bool, optional + Enables the centering of the coordinates. If True the transformer node is set + to the coordinates x=0 and y=0. Else, the coordinates from the HV/MV-station + of the MV grid are used. Default: False. - grid : :class:`~.network.grids.Grid` or bool, optional - Grid to use as root node. If a grid is given the transformer station is used - as root. If False the root is set to the coordinates x=0 and y=0. Else the - coordinates from the hv-mv-station of the mv grid are used. Default: False + pseudo_coordinates : bool, optional + Enable pseudo coordinates for the plotted grid. Default: False. + + node_selection : bool or list(str), optional + Only plot selected nodes. Default: False. Returns ------- @@ -943,259 +988,439 @@ def draw_plotly( Plotly figure with branches and nodes. """ - # initialization - transformer_4326_to_3035 = Transformer.from_crs( - "EPSG:4326", - "EPSG:3035", - always_xy=True, - ) + if grid is None: + grid = edisgo_obj.topology.mv_grid + + G = grid.graph - if G is None: - G = edisgo_obj.topology.mv_grid.graph + logger.debug(f"selected_timesteps={selected_timesteps}") - if hasattr(grid, "transformers_df"): - node_root = grid.transformers_df.bus1.iat[0] - x_root, y_root = G.nodes[node_root]["pos"] + if isinstance(selected_timesteps, pd.Timestamp) or isinstance( + selected_timesteps, str + ): + selected_timesteps = [selected_timesteps] + + if selected_timesteps is None: + selected_timesteps = edisgo_obj.results.s_res.index + + if edisgo_obj.results.s_res.empty: + power_flow_results = False + warning_message = "No power flow results. -> Run power flow." + elif len(selected_timesteps) == 0: + power_flow_results = False + warning_message = "No time steps selected." + else: + power_flow_results = True + warning_message = False - elif not grid: + try: + edisgo_obj.results.s_res.loc[selected_timesteps, :] + except KeyError: + power_flow_results = False + warning_message = "Time steps are not in the results." + + # check for existing reinforcement results + if edisgo_obj.results.equipment_changes.empty: + reinforcement_results = False + else: + reinforcement_results = True + + # check line_color input + line_color_options = ["loading", "relative_loading", "reinforce"] + if line_color not in line_color_options: + logger.warning(f"Line colors need to be one of {line_color_options}.") + line_color = None + elif (line_color in ["loading", "relative_loading"]) and (not power_flow_results): + logger.warning("No power flow results to show. -> Run power flow.") + line_color = None + elif (line_color in ["reinforce"]) and (not reinforcement_results): + logger.warning("No reinforcement results to show. -> Run reinforcement.") + line_color = None + + # check node_color input + node_color_options = ["voltage_deviation", "adjacencies"] + if node_color not in node_color_options: + logger.warning(f"Line colors need to be one of {node_color_options}.") + node_color = None + elif (node_color in ["voltage_deviation"]) and (not power_flow_results): + logger.warning("No power flow results to show. -> Run power flow.") + node_color = None + + if center_coordinates: + # Center transformer coordinates on (0,0). + if hasattr(grid, "transformers_df"): + node_root = grid.transformers_df.bus1.iat[0] + x_root, y_root = G.nodes[node_root]["pos"] + else: + node_root = edisgo_obj.topology.transformers_hvmv_df.bus1.iat[0] + x_root, y_root = G.nodes[node_root]["pos"] + else: x_root = 0 y_root = 0 - else: - node_root = edisgo_obj.topology.transformers_hvmv_df.bus1.iat[0] - x_root, y_root = G.nodes[node_root]["pos"] + if pseudo_coordinates: + G = make_pseudo_coordinates_graph( + G, edisgo_obj.config["grid_connection"]["branch_detour_factor"] + ) - x_root, y_root = transformer_4326_to_3035.transform(x_root, y_root) + if node_selection: + G = G.subgraph(node_selection) + if not list(G.nodes()): + raise ValueError("Selected nodes are not in the selected grid.") - # line text - middle_node_x = [] - middle_node_y = [] - middle_node_text = [] + # Select values for displaying results. + if power_flow_results: + s_res_view = edisgo_obj.results.s_res.columns.isin( + [edge[2]["branch_name"] for edge in G.edges.data()] + ) + v_res_view = edisgo_obj.results.v_res.columns.isin([node for node in G.nodes]) - for edge in G.edges(data=True): + s_res = edisgo_obj.results.s_res.loc[selected_timesteps, s_res_view] + v_res = edisgo_obj.results.v_res.loc[selected_timesteps, v_res_view] + + result_selection_options = ["min", "max"] + if line_result_selection == "min": + s_res = s_res.min() + elif line_result_selection == "max": + s_res = s_res.max() + else: + raise ValueError( + f"line_result_selection needs to be one of {result_selection_options}" + ) + if node_result_selection == "min": + v_res = v_res.min() + elif node_result_selection == "max": + v_res = v_res.max() + else: + raise ValueError( + f"node_result_selection needs to be one of {result_selection_options}" + ) + + # initialization coordinate transformation + transformer_4326_to_3035 = Transformer.from_crs( + "EPSG:4326", + "EPSG:3035", + always_xy=True, + ) + + def get_coordinates_for_edge(edge): x0, y0 = G.nodes[edge[0]]["pos"] x1, y1 = G.nodes[edge[1]]["pos"] x0, y0 = transformer_4326_to_3035.transform(x0, y0) x1, y1 = transformer_4326_to_3035.transform(x1, y1) - middle_node_x.append((x0 - x_root + x1 - x_root) / 2) - middle_node_y.append((y0 - y_root + y1 - y_root) / 2) + return x0, y0, x1, y1 - branch_name = edge[2]["branch_name"] + x_root, y_root = transformer_4326_to_3035.transform(x_root, y_root) - text = str(branch_name) - try: - loading = edisgo_obj.results.s_res.T.loc[branch_name].max() - text += "
" + "Loading = " + str(loading) - except KeyError: - logger.debug( - f"Could not find loading for branch {branch_name}", exc_info=True - ) - text = text + def plot_line_text(): + middle_node_x = [] + middle_node_y = [] + middle_node_text = [] + + for edge in G.edges(data=True): + x0, y0, x1, y1 = get_coordinates_for_edge(edge) + middle_node_x.append((x0 - x_root + x1 - x_root) / 2) + middle_node_y.append((y0 - y_root + y1 - y_root) / 2) + + branch_name = edge[2]["branch_name"] + + text = str(branch_name) + if power_flow_results: + text += "
" + "Loading = " + str(s_res.loc[branch_name]) - try: line_parameters = edisgo_obj.topology.lines_df.loc[branch_name, :] for index, value in line_parameters.iteritems(): text += "
" + str(index) + " = " + str(value) - except KeyError: - logger.debug( - f"Could not find line parameters for branch {branch_name}", - exc_info=True, - ) - text = text - middle_node_text.append(text) - - middle_node_trace = go.Scatter( - x=middle_node_x, - y=middle_node_y, - text=middle_node_text, - mode="markers", - hoverinfo="text", - marker=dict(opacity=0.0, size=10, color="white"), - ) + middle_node_text.append(text) - data = [middle_node_trace] - - # line plot - if line_color == "loading": - s_res_view = edisgo_obj.results.s_res.T.index.isin( - [edge[2]["branch_name"] for edge in G.edges.data()] + middle_node_scatter = go.Scatter( + x=middle_node_x, + y=middle_node_y, + text=middle_node_text, + mode="markers", + hoverinfo="text", + marker=dict( + opacity=0.0, + size=10, + color="white", + ), + showlegend=False, ) - color_min = edisgo_obj.results.s_res.T.loc[s_res_view].T.min().max() - color_max = edisgo_obj.results.s_res.T.loc[s_res_view].T.max().max() + return [middle_node_scatter] - elif line_color == "relative_loading": - color_min = 0 - color_max = 1 - - for edge in G.edges(data=True): - x0, y0 = G.nodes[edge[0]]["pos"] - x1, y1 = G.nodes[edge[1]]["pos"] - x0, y0 = transformer_4326_to_3035.transform(x0, y0) - x1, y1 = transformer_4326_to_3035.transform(x1, y1) + def plot_lines(): - edge_x = [x0 - x_root, x1 - x_root, None] - edge_y = [y0 - y_root, y1 - y_root, None] - - if line_color == "reinforce": - if edisgo_obj.results.grid_expansion_costs.index.isin( - [edge[2]["branch_name"]] - ).any(): - color = "lightgreen" - else: - color = "black" - - elif line_color == "loading": - loading = edisgo_obj.results.s_res.T.loc[edge[2]["branch_name"]].max() - color = color_map_color( - loading, - vmin=color_min, - vmax=color_max, - ) + showscale = True + if line_color == "loading": + color_min = s_res.min() + color_max = s_res.max() + colorscale = "YlOrRd" elif line_color == "relative_loading": - loading = edisgo_obj.results.s_res.T.loc[edge[2]["branch_name"]].max() - s_nom = edisgo_obj.topology.lines_df.s_nom.loc[edge[2]["branch_name"]] - color = color_map_color( - loading / s_nom, - vmin=color_min, - vmax=color_max, - ) - if loading > s_nom: - color = "green" + color_min = 0 + color_max = 1 + colorscale = [ + [0, "yellow"], + [0.45, "orange"], + [0.9, "crimson"], + [0.9, "indigo"], + [1, "indigo"], + ] + elif line_color == "reinforce": + color_min = 0 + color_max = 1 + colorscale = [[0, "green"], [0.5, "green"], [0.5, "red"], [1, "red"]] else: - color = "black" - - edge_trace = go.Scatter( - x=edge_x, - y=edge_y, - hoverinfo="none", - opacity=0.4, - mode="lines", - line=dict(width=2, color=color), - ) - data.append(edge_trace) + showscale = False - # node plot - node_x = [] - node_y = [] + data_line_plot = [] + for edge in G.edges(data=True): - for node in G.nodes(): - x, y = G.nodes[node]["pos"] - x, y = transformer_4326_to_3035.transform(x, y) - node_x.append(x - x_root) - node_y.append(y - y_root) + x0, y0, x1, y1 = get_coordinates_for_edge(edge) + edge_x = [x0 - x_root, x1 - x_root, None] + edge_y = [y0 - y_root, y1 - y_root, None] - if node_color == "voltage_deviation": - colors = [] + branch_name = edge[2]["branch_name"] - for node in G.nodes(): - v_res = edisgo_obj.results.v_res.T.loc[node] - v_min = v_res.min() - v_max = v_res.max() + if line_color == "reinforce": + # Possible distinction between added parallel + # lines and changed lines + if ( + edisgo_obj.results.equipment_changes.index[ + edisgo_obj.results.equipment_changes["change"] == "added" + ] + .isin([branch_name]) + .any() + ): + color = "green" + # Changed lines + elif ( + edisgo_obj.results.equipment_changes.index[ + edisgo_obj.results.equipment_changes["change"] == "changed" + ] + .isin([branch_name]) + .any() + ): + + color = "red" + else: + color = "black" + + elif line_color == "loading": + loading = s_res.loc[branch_name] + color = color_map_color( + loading, + vmin=color_min, + vmax=color_max, + cmap_name=colorscale, + ) - if abs(v_min - 1) > abs(v_max - 1): - color = v_min - 1 + elif line_color == "relative_loading": + loading = s_res.loc[branch_name] + s_nom = edisgo_obj.topology.lines_df.s_nom.loc[branch_name] + color = color_map_color( + loading / s_nom, + vmin=color_min, + vmax=color_max, + cmap_name=colorscale, + ) + if loading > s_nom: + color = "indigo" else: - color = v_max - 1 + color = "grey" + + edge_scatter = go.Scatter( + mode="lines", + x=edge_x, + y=edge_y, + hoverinfo="none", + opacity=0.8, + showlegend=False, + line=dict( + width=2, + color=color, + ), + ) + data_line_plot.append(edge_scatter) - colors.append(color) + if line_color: + line_color_title = { + "loading": "Loading in MVA", + "relative_loading": "Relative loading in p.u.", + "reinforce": "Reinforce", + } - colorbar = dict( - thickness=15, - title="Node Voltage Deviation", - xanchor="left", - titleside="right", - ) - colorscale = "RdBu" - cmid = 0 + colorbar_edge_scatter = go.Scatter( + mode="markers", + x=[None], + y=[None], + marker=dict( + colorbar=dict( + title=line_color_title[line_color], + xanchor="left", + titleside="right", + x=1.19, + thickness=15, + ), + colorscale=colorscale, + cmax=color_max, + cmin=color_min, + showscale=showscale, + ), + ) - else: - colors = [len(adjacencies[1]) for adjacencies in G.adjacency()] - colorscale = "YlGnBu" - cmid = None + if line_color == "reinforce": + colorbar_edge_scatter.marker.colorbar.tickmode = "array" + colorbar_edge_scatter.marker.colorbar.ticktext = ["added", "changed"] + colorbar_edge_scatter.marker.colorbar.tickvals = [0.25, 0.75] + elif line_color == "relative_loading": + colorbar_edge_scatter.marker.colorbar.tickmode = "array" + colorbar_edge_scatter.marker.colorbar.ticktext = [ + 0, + 0.2, + 0.4, + 0.6, + 0.8, + 1, + "Overloaded", + ] + colorbar_edge_scatter.marker.colorbar.tickvals = [ + 0, + 0.2 * 0.9, + 0.4 * 0.9, + 0.6 * 0.9, + 0.8 * 0.9, + 1 * 0.9, + 0.95, + ] + data_line_plot.append(colorbar_edge_scatter) + + return data_line_plot + + def plot_buses(): + node_x = [] + node_y = [] - colorbar = dict( - thickness=15, title="Node Connections", xanchor="left", titleside="right" - ) + for node in G.nodes(): + x, y = G.nodes[node]["pos"] + x, y = transformer_4326_to_3035.transform(x, y) + node_x.append(x - x_root) + node_y.append(y - y_root) + + if node_color == "voltage_deviation": + node_colors = [] + for node in G.nodes(): + color = v_res.loc[node] - 1 + node_colors.append(color) + + colorbar = dict( + thickness=15, + title="Node voltage deviation in p.u.", + xanchor="left", + titleside="right", + ) + colorscale = "RdBu" + cmid = 0 + showscale = True + + elif node_color == "adjacencies": + node_colors = [len(adjacencies[1]) for adjacencies in G.adjacency()] + colorscale = "YlGnBu" + cmid = None + + colorbar = dict( + thickness=15, + title="Node connections", + xanchor="left", + titleside="right", + ) + showscale = True - node_text = [] - for node in G.nodes(): - text = str(node) - try: - peak_load = edisgo_obj.topology.loads_df.loc[ - edisgo_obj.topology.loads_df.bus == node - ].p_set.sum() - text += "
" + "peak_load = " + str(peak_load) - p_nom = edisgo_obj.topology.generators_df.loc[ - edisgo_obj.topology.generators_df.bus == node - ].p_nom.sum() - text += "
" + "p_nom_gen = " + str(p_nom) - v_min = edisgo_obj.results.v_res.T.loc[node].min() - v_max = edisgo_obj.results.v_res.T.loc[node].max() - if abs(v_min - 1) > abs(v_max - 1): - text += "
" + "v = " + str(v_min) - else: - text += "
" + "v = " + str(v_max) - except KeyError: - logger.debug(f"Failed to add text for node {node}.", exc_info=True) - text = text + else: + node_colors = "grey" + cmid = None + colorscale = None + colorbar = None + showscale = False + + node_text = [] + for node in G.nodes(): + text = str(node) + if power_flow_results: + peak_load = edisgo_obj.topology.loads_df.loc[ + edisgo_obj.topology.loads_df.bus == node + ].p_set.sum() + text += "
" + "peak_load = " + str(peak_load) + + p_nom = edisgo_obj.topology.generators_df.loc[ + edisgo_obj.topology.generators_df.bus == node + ].p_nom.sum() + text += "
" + "p_nom_gen = " + str(p_nom) + + v = v_res.loc[node] + text += "
" + "v = " + str(v) - try: text = text + "
" + "Neighbors = " + str(G.degree(node)) - except KeyError: - logger.debug( - f"Failed to add neighbors to text for node {node}.", exc_info=True - ) - text = text - try: node_parameters = edisgo_obj.topology.buses_df.loc[node] for index, value in node_parameters.iteritems(): text += "
" + str(index) + " = " + str(value) - except KeyError: - logger.debug( - f"Failed to add neighbors to text for node {node}.", exc_info=True - ) - text = text - - node_text.append(text) - - node_trace = go.Scatter( - x=node_x, - y=node_y, - mode="markers", - hoverinfo="text", - text=node_text, - marker=dict( - showscale=True, - colorscale=colorscale, - reversescale=True, - color=colors, - size=8, - cmid=cmid, - line_width=2, - colorbar=colorbar, - ), - ) - data.append(node_trace) + node_text.append(text) + + node_scatter = go.Scatter( + x=node_x, + y=node_y, + mode="markers", + hoverinfo="text", + text=node_text, + marker=dict( + showscale=showscale, + colorscale=colorscale, + color=node_colors, + size=8, + cmid=cmid, + line_width=2, + colorbar=colorbar, + ), + ) + return [node_scatter] fig = go.Figure( - data=data, + data=plot_line_text() + plot_lines() + plot_buses(), layout=go.Layout( height=500, - titlefont_size=16, showlegend=False, hovermode="closest", margin=dict(b=20, l=5, r=5, t=40), - xaxis=dict(showgrid=True, zeroline=True, showticklabels=True), - yaxis=dict(showgrid=True, zeroline=True, showticklabels=True), + xaxis=dict( + showgrid=True, + zeroline=True, + showticklabels=True, + ), + yaxis=dict( + showgrid=True, + zeroline=True, + showticklabels=True, + scaleanchor="x", + scaleratio=1, + ), ), ) - - fig.update_yaxes(scaleanchor="x", scaleratio=1) - + if warning_message: + fig.add_annotation( + x=0, + y=1, + xref="paper", + yref="paper", + xanchor="left", + text=warning_message, + showarrow=False, + font=dict(size=16, color="#ffffff"), + bgcolor="red", + opacity=0.75, + ) return fig @@ -1244,32 +1469,27 @@ def chosen_graph( return G, grid -def dash_plot( - edisgo_objects: EDisGo | dict[str, EDisGo], - line_plot_modes: list[str] | None = None, - node_plot_modes: list[str] | None = None, +def plot_dash_app( + edisgo_objects: EDisGo | dict[str, EDisGo], debug: bool = False ) -> JupyterDash: """ Generates a jupyter dash app from given eDisGo object(s). - TODO: The app doesn't display two seperate colorbars for line and bus values atm - Parameters ---------- edisgo_objects : :class:`~.EDisGo` or dict[str, :class:`~.EDisGo`] eDisGo objects to show in plotly dash app. In the case of multiple edisgo objects pass a dictionary with the eDisGo objects as values and the respective eDisGo object names as keys. - line_plot_modes : list(str), optional - List of line plot modes to display in plotly dash app. See - :py:func:`~edisgo.tools.plots.draw_plotly` for more information. If None is - passed the modes 'reinforce', 'loading' and 'relative_loading' will be used. - Default: None - node_plot_modes : list(str), optional - List of line plot modes to display in plotly dash app. See - :py:func:`~edisgo.tools.plots.draw_plotly` for more information. If None is - passed the modes 'adjacencies' and 'voltage_deviation' will be used. - Default: None + + debug : bool, optional + Debugging for the dash app: + + * False (default) + Disable debugging for the dash app. + * True + Enable debugging for the dash app. + Returns ------- @@ -1280,54 +1500,262 @@ def dash_plot( if isinstance(edisgo_objects, dict): edisgo_name_list = list(edisgo_objects.keys()) edisgo_obj_1 = list(edisgo_objects.values())[0] + + edisgo_obj_1_mv_grid_name = str(edisgo_obj_1.topology.mv_grid) + for edisgo_obj in edisgo_objects.values(): + if edisgo_obj_1_mv_grid_name != str(edisgo_obj.topology.mv_grid): + raise ValueError("edisgo_objects are not matching.") + else: edisgo_name_list = ["edisgo_obj"] edisgo_obj_1 = edisgo_objects - grid_name_list = ["Grid"] + edisgo_obj_1.topology._grids_repr + mv_grid = edisgo_obj_1.topology.mv_grid + lv_grid_name_list = list(map(str, mv_grid.lv_grids)) + grid_name_list = ["Grid", str(mv_grid)] + lv_grid_name_list + + line_plot_modes = ["relative_loading", "loading", "reinforce"] + node_plot_modes = ["voltage_deviation", "adjacencies"] + + if edisgo_obj_1.results.v_res.empty: + timestep_values = ["No results"] + timestep_labels = ["No results"] + elif edisgo_obj_1.timeseries.is_worst_case: + timestep_values = edisgo_obj_1.results.v_res.index.to_list() + worst_case_series = edisgo_obj_1.timeseries.timeindex_worst_cases + timestep_labels = [ + worst_case_series.index[worst_case_series.to_list().index(value)] + for value in timestep_values + ] + else: + timestep_labels = edisgo_obj_1.results.v_res.index.to_list() + timestep_values = edisgo_obj_1.results.v_res.index.to_list() + + logger.debug(f"timestep_labels={timestep_labels}") + logger.debug(f"timestep_values={timestep_values}") + timestep_option = [ + {"label": timestep_labels[i], "value": str(timestep_values[i])} + for i in range(0, len(timestep_values)) + ] + logger.debug(f"timestep_option={timestep_option}") - if line_plot_modes is None: - line_plot_modes = ["reinforce", "loading", "relative_loading"] - if node_plot_modes is None: - node_plot_modes = ["adjacencies", "voltage_deviation"] + padding = 1 app = JupyterDash(__name__) + # Workaround to use standard python logging with plotly dash + logger.handlers.pop() + if debug: + app.logger.disabled = False + app.logger.setLevel(logging.DEBUG) if isinstance(edisgo_objects, dict) and len(edisgo_objects) > 1: app.layout = html.Div( [ html.Div( [ - dcc.Dropdown( - id="dropdown_edisgo_object_1", - options=[ - {"label": i, "value": i} for i in edisgo_name_list + html.Div( + [ + html.Label("Edisgo objects"), ], - value=edisgo_name_list[0], + style={"padding": padding, "flex": 1}, ), - dcc.Dropdown( - id="dropdown_edisgo_object_2", - options=[ - {"label": i, "value": i} for i in edisgo_name_list + html.Div( + [], + style={"padding": padding, "flex": 1}, + ), + ], + style={ + "display": "flex", + "flex-direction": "row", + "padding": 0, + "flex": 1, + }, + ), + html.Div( + [ + html.Div( + [ + dcc.Dropdown( + id="dropdown_edisgo_object_1", + options=[ + {"label": i, "value": i} + for i in edisgo_name_list + ], + value=edisgo_name_list[0], + ), ], - value=edisgo_name_list[1], + style={"padding": padding, "flex": 1}, ), - dcc.Dropdown( - id="dropdown_grid", - options=[{"label": i, "value": i} for i in grid_name_list], - value=grid_name_list[1], + html.Div( + [ + dcc.Dropdown( + id="dropdown_edisgo_object_2", + options=[ + {"label": i, "value": i} + for i in edisgo_name_list + ], + value=edisgo_name_list[1], + ), + ], + style={"padding": padding, "flex": 1}, ), - dcc.Dropdown( - id="dropdown_line_plot_mode", - options=[{"label": i, "value": i} for i in line_plot_modes], - value=line_plot_modes[0], + ], + style={ + "display": "flex", + "flex-direction": "row", + "padding": 0, + "flex": 1, + }, + ), + html.Div( + [ + html.Div( + [ + html.Label("Grid"), + dcc.Dropdown( + id="dropdown_grid", + options=[ + {"label": i, "value": i} for i in grid_name_list + ], + value=grid_name_list[1], + ), + ], + style={"padding": padding, "flex": 1}, ), - dcc.Dropdown( - id="dropdown_node_plot_mode", - options=[{"label": i, "value": i} for i in node_plot_modes], - value=node_plot_modes[0], + html.Div( + [ + html.Label("Line plot mode"), + dcc.Dropdown( + id="dropdown_line_plot_mode", + options=[ + {"label": i, "value": i} + for i in line_plot_modes + ], + value=line_plot_modes[0], + ), + ], + style={"padding": padding, "flex": 1}, ), - ] + html.Div( + [ + html.Label("Line result selection"), + dcc.Dropdown( + id="line_result_selection", + options=[ + {"label": "Min", "value": "min"}, + {"label": "Max", "value": "max"}, + ], + value="max", + ), + ], + style={"padding": padding, "flex": 1}, + ), + ], + style={ + "display": "flex", + "flex-direction": "row", + "padding": 0, + "flex": 1, + }, + ), + html.Div( + [ + html.Div( + [ + html.Label("Pseudo coordinates"), + dcc.RadioItems( + id="radioitems_pseudo_coordinates", + options=[ + {"label": "False", "value": False}, + {"label": "True", "value": True}, + ], + value=False, + ), + ], + style={"padding": padding, "flex": 1}, + ), + html.Div( + [ + html.Label("Node plot mode"), + dcc.Dropdown( + id="dropdown_node_plot_mode", + options=[ + {"label": i, "value": i} + for i in node_plot_modes + ], + value=node_plot_modes[0], + ), + ], + style={"padding": padding, "flex": 1}, + ), + html.Div( + [ + html.Label("Node result selection"), + dcc.Dropdown( + id="node_result_selection", + options=[ + {"label": "Min", "value": "min"}, + {"label": "Max", "value": "max"}, + ], + value="max", + ), + ], + style={"padding": padding, "flex": 1}, + ), + ], + style={ + "display": "flex", + "flex-direction": "row", + "padding": 0, + "flex": 1, + }, + ), + html.Div( + [ + html.Div( + [ + html.Label( + f"Time step mode - " + f"Time steps of {edisgo_name_list[0]}" + ), + dcc.RadioItems( + ["Single", "Range", "All"], + "All", + inline=True, + id="timestep_mode_radio", + ), + ], + style={"padding": padding, "flex": 1}, + ), + html.Div( + [ + html.Label("Time step start"), + dcc.Dropdown( + id="timestep_dropdown_start", + options=timestep_option, + value=timestep_option[0]["value"], + ), + ], + style={"padding": padding, "flex": 1}, + ), + html.Div( + [ + html.Label("Time step end"), + dcc.Dropdown( + id="timestep_dropdown_end", + options=timestep_option, + value=timestep_option[-1]["value"], + ), + ], + style={"padding": padding, "flex": 1}, + ), + ], + style={ + "display": "flex", + "flex-direction": "row", + "padding": 0, + "flex": 1, + }, ), html.Div( [ @@ -1340,40 +1768,102 @@ def dash_plot( style={"display": "flex", "flex-direction": "column"}, ) + @app.callback( + Output("timestep_dropdown_start", "disabled"), + Output("timestep_dropdown_end", "disabled"), + Input("timestep_mode_radio", "value"), + ) + def update_timestep_components_double(timestep_mode_radio): + if timestep_mode_radio == "Single": + timestep_dropdown_start = False + timestep_dropdown_end = True + elif timestep_mode_radio == "Range": + timestep_dropdown_start = False + timestep_dropdown_end = False + elif timestep_mode_radio == "All": + timestep_dropdown_start = True + timestep_dropdown_end = True + return (timestep_dropdown_start, timestep_dropdown_end) + @app.callback( Output("fig_1", "figure"), Output("fig_2", "figure"), - Input("dropdown_grid", "value"), Input("dropdown_edisgo_object_1", "value"), Input("dropdown_edisgo_object_2", "value"), + Input("dropdown_grid", "value"), Input("dropdown_line_plot_mode", "value"), Input("dropdown_node_plot_mode", "value"), + Input("radioitems_pseudo_coordinates", "value"), + Input("line_result_selection", "value"), + Input("node_result_selection", "value"), + Input("timestep_mode_radio", "value"), + Input("timestep_dropdown_start", "value"), + Input("timestep_dropdown_end", "value"), + log=True, ) - def update_figure( - selected_grid, + def update_figure_double( selected_edisgo_object_1, selected_edisgo_object_2, + selected_grid, selected_line_plot_mode, selected_node_plot_mode, + pseudo_coordinates, + line_result_selection, + node_result_selection, + timestep_mode, + timestep_dropdown_start, + timestep_dropdown_end, ): edisgo_obj = edisgo_objects[selected_edisgo_object_1] (G, grid) = chosen_graph(edisgo_obj, selected_grid) - fig_1 = draw_plotly( - edisgo_obj, - G, - selected_line_plot_mode, - selected_node_plot_mode, + + if timestep_mode == "Single": + selected_timesteps = timestep_dropdown_start + elif timestep_mode == "Range": + app.logger.debug( + f"timestep_dropdown_start={timestep_dropdown_start}, " + f"timestep_dropdown_end={timestep_dropdown_end}" + ) + if timestep_dropdown_start == timestep_dropdown_end: + selected_timesteps = timestep_dropdown_start + else: + selected_timesteps = edisgo_obj.results.v_res.loc[ + timestep_dropdown_start:timestep_dropdown_end, : + ].index.to_list() + if selected_timesteps == []: + selected_timesteps = edisgo_obj.results.v_res.loc[ + timestep_dropdown_end:timestep_dropdown_start, : + ].index.to_list() + elif timestep_mode == "All": + selected_timesteps = None + + app.logger.debug(f"selected_timesteps={selected_timesteps}") + + fig_1 = plot_plotly( + edisgo_obj=edisgo_obj, grid=grid, + line_color=selected_line_plot_mode, + node_color=selected_node_plot_mode, + line_result_selection=line_result_selection, + node_result_selection=node_result_selection, + selected_timesteps=selected_timesteps, + pseudo_coordinates=pseudo_coordinates, + center_coordinates=True, ) edisgo_obj = edisgo_objects[selected_edisgo_object_2] (G, grid) = chosen_graph(edisgo_obj, selected_grid) - fig_2 = draw_plotly( - edisgo_obj, - G, - selected_line_plot_mode, - selected_node_plot_mode, + + fig_2 = plot_plotly( + edisgo_obj=edisgo_obj, grid=grid, + line_color=selected_line_plot_mode, + node_color=selected_node_plot_mode, + line_result_selection=line_result_selection, + node_result_selection=node_result_selection, + selected_timesteps=selected_timesteps, + pseudo_coordinates=pseudo_coordinates, + center_coordinates=True, ) return fig_1, fig_2 @@ -1383,22 +1873,150 @@ def update_figure( [ html.Div( [ - dcc.Dropdown( - id="dropdown_grid", - options=[{"label": i, "value": i} for i in grid_name_list], - value=grid_name_list[1], + html.Div( + [ + html.Label("Grid"), + dcc.Dropdown( + id="dropdown_grid", + options=[ + {"label": i, "value": i} for i in grid_name_list + ], + value=grid_name_list[1], + ), + ], + style={"padding": padding, "flex": 1}, ), - dcc.Dropdown( - id="dropdown_line_plot_mode", - options=[{"label": i, "value": i} for i in line_plot_modes], - value=line_plot_modes[0], + html.Div( + [ + html.Label("Line plot mode"), + dcc.Dropdown( + id="dropdown_line_plot_mode", + options=[ + {"label": i, "value": i} + for i in line_plot_modes + ], + value=line_plot_modes[0], + ), + ], + style={"padding": padding, "flex": 1}, ), - dcc.Dropdown( - id="dropdown_node_plot_mode", - options=[{"label": i, "value": i} for i in node_plot_modes], - value=node_plot_modes[0], + html.Div( + [ + html.Label("Line result selection"), + dcc.Dropdown( + id="line_result_selection", + options=[ + {"label": "Min", "value": "min"}, + {"label": "Max", "value": "max"}, + ], + value="max", + ), + ], + style={"padding": padding, "flex": 1}, ), - ] + ], + style={ + "display": "flex", + "flex-direction": "row", + "padding": 0, + "flex": 1, + }, + ), + html.Div( + [ + html.Div( + [ + html.Label("Pseudo coordinates"), + dcc.RadioItems( + id="radioitems_pseudo_coordinates", + options=[ + {"label": "False", "value": False}, + {"label": "True", "value": True}, + ], + value=False, + ), + ], + style={"padding": padding, "flex": 1}, + ), + html.Div( + [ + html.Label("Node plot mode"), + dcc.Dropdown( + id="dropdown_node_plot_mode", + options=[ + {"label": i, "value": i} + for i in node_plot_modes + ], + value=node_plot_modes[0], + ), + ], + style={"padding": padding, "flex": 1}, + ), + html.Div( + [ + html.Label("Node result selection"), + dcc.Dropdown( + id="node_result_selection", + options=[ + {"label": "Min", "value": "min"}, + {"label": "Max", "value": "max"}, + ], + value="max", + ), + ], + style={"padding": padding, "flex": 1}, + ), + ], + style={ + "display": "flex", + "flex-direction": "row", + "padding": 0, + "flex": 1, + }, + ), + html.Div( + [ + html.Div( + [ + html.Label("Time step mode"), + dcc.RadioItems( + ["Single", "Range", "All"], + "All", + inline=True, + id="timestep_mode_radio", + ), + ], + style={"padding": padding, "flex": 1}, + ), + html.Div( + [ + html.Label("Time step start"), + dcc.Dropdown( + id="timestep_dropdown_start", + options=timestep_option, + value=timestep_option[0]["value"], + ), + ], + style={"padding": padding, "flex": 1}, + ), + html.Div( + [ + html.Label("Time step end"), + dcc.Dropdown( + id="timestep_dropdown_end", + options=timestep_option, + value=timestep_option[-1]["value"], + ), + ], + style={"padding": padding, "flex": 1}, + ), + ], + style={ + "display": "flex", + "flex-direction": "row", + "padding": 0, + "flex": 1, + }, ), html.Div( [html.Div([dcc.Graph(id="fig")], style={"flex": "auto"})], @@ -1408,23 +2026,121 @@ def update_figure( style={"display": "flex", "flex-direction": "column"}, ) + @app.callback( + Output("timestep_dropdown_start", "disabled"), + Output("timestep_dropdown_end", "disabled"), + Input("timestep_mode_radio", "value"), + ) + def update_timestep_components_single(timestep_mode_radio): + if timestep_mode_radio == "Single": + timestep_dropdown_start = False + timestep_dropdown_end = True + elif timestep_mode_radio == "Range": + timestep_dropdown_start = False + timestep_dropdown_end = False + elif timestep_mode_radio == "All": + timestep_dropdown_start = True + timestep_dropdown_end = True + return (timestep_dropdown_start, timestep_dropdown_end) + @app.callback( Output("fig", "figure"), Input("dropdown_grid", "value"), Input("dropdown_line_plot_mode", "value"), Input("dropdown_node_plot_mode", "value"), + Input("radioitems_pseudo_coordinates", "value"), + Input("line_result_selection", "value"), + Input("node_result_selection", "value"), + Input("timestep_mode_radio", "value"), + Input("timestep_dropdown_start", "value"), + Input("timestep_dropdown_end", "value"), + log=True, ) - def update_figure( - selected_grid, selected_line_plot_mode, selected_node_plot_mode + def update_figure_single( + selected_grid, + selected_line_plot_mode, + selected_node_plot_mode, + pseudo_coordinates, + line_result_selection, + node_result_selection, + timestep_mode, + timestep_dropdown_start, + timestep_dropdown_end, ): + if timestep_mode == "Single": + selected_timesteps = timestep_dropdown_start + elif timestep_mode == "Range": + app.logger.debug(f"timestep_dropdown_start={timestep_dropdown_start}") + app.logger.debug(f"timestep_dropdown_end={timestep_dropdown_end}") + + if timestep_dropdown_start == timestep_dropdown_end: + selected_timesteps = str(timestep_dropdown_start) + else: + selected_timesteps = edisgo_obj_1.results.v_res.loc[ + timestep_dropdown_start:timestep_dropdown_end, : + ].index + if len(selected_timesteps) == 0: + selected_timesteps = edisgo_obj_1.results.v_res.loc[ + timestep_dropdown_end:timestep_dropdown_start, : + ].index + selected_timesteps = list(map(str, selected_timesteps)) + elif timestep_mode == "All": + selected_timesteps = False + + app.logger.debug(f"selected_timesteps={selected_timesteps}") + (G, grid) = chosen_graph(edisgo_obj_1, selected_grid) - fig = draw_plotly( - edisgo_obj_1, - G, - selected_line_plot_mode, - selected_node_plot_mode, + fig = plot_plotly( + edisgo_obj=edisgo_obj_1, grid=grid, + line_color=selected_line_plot_mode, + node_color=selected_node_plot_mode, + line_result_selection=line_result_selection, + node_result_selection=node_result_selection, + selected_timesteps=selected_timesteps, + pseudo_coordinates=pseudo_coordinates, + center_coordinates=True, ) + return fig return app + + +def plot_dash( + edisgo_objects: EDisGo | dict[str, EDisGo], + mode: str = "inline", + debug: bool = False, + port: int = 8050, +): + """ + Shows the generated jupyter dash app from given eDisGo object(s). + + Parameters + ---------- + edisgo_objects : :class:`~.EDisGo` or dict[str, :class:`~.EDisGo`] + eDisGo objects to show in plotly dash app. In the case of multiple edisgo + objects pass a dictionary with the eDisGo objects as values and the respective + eDisGo object names as keys. + + mode : str + Display mode + + * "inline" (default) + Jupyter lab inline plotting. + * "jupyterlab" + Plotting in own Jupyter lab tab. + * "external" + Plotting in own browser tab. + + debug : bool + If True, enables debugging of the jupyter dash app. + + port : int + Port which the app uses. Default: 8050. + + """ + app = plot_dash_app(edisgo_objects, debug=debug) + log = logging.getLogger("werkzeug") + log.setLevel(logging.ERROR) + app.run_server(mode=mode, debug=debug, height=820, port=port) diff --git a/edisgo/tools/pseudo_coordinates.py b/edisgo/tools/pseudo_coordinates.py new file mode 100644 index 000000000..a42654ec9 --- /dev/null +++ b/edisgo/tools/pseudo_coordinates.py @@ -0,0 +1,272 @@ +from __future__ import annotations + +import copy +import logging +import math + +from time import time +from typing import TYPE_CHECKING + +import networkx as nx + +from pyproj import Transformer + +if TYPE_CHECKING: + from networkx import Graph + + from edisgo import EDisGo + +logger = logging.getLogger(__name__) + +# Transform coordinates to equidistant and back +coor_transform = Transformer.from_crs("EPSG:4326", "EPSG:3035", always_xy=True) +coor_transform_back = Transformer.from_crs("EPSG:3035", "EPSG:4326", always_xy=True) + + +# Pseudo coordinates +def _make_coordinates(graph_root: Graph, branch_detour_factor: float) -> Graph: + """ + Generates pseudo coordinates for a graph with equidistant coordinates. + + Parameters + ---------- + graph_root : :networkx:`networkx.Graph` + Graph object to generate pseudo coordinates for (with equidistant coordinates). + + branch_detour_factor : float + Defines the quotient of the line length and the distance of the buses. + + Returns + ------- + :networkx:`networkx.Graph` + Graph with equidistant pseudo coordinates for all nodes. + + """ + + # Make coordinates for the neighbours of the transformer node + # Nodes are distributed around the source node with the same angle + def coordinate_source(pos_start, length, node_numerator, node_total_numerator): + length = length / branch_detour_factor + angle = node_numerator * 360 / node_total_numerator + x0, y0 = pos_start + x1 = x0 + 1000 * length * math.cos(math.radians(angle)) + y1 = y0 + 1000 * length * math.sin(math.radians(angle)) + pos_end = (x1, y1) + origin_angle = math.degrees(math.atan2(y1 - y0, x1 - x0)) + return pos_end, origin_angle + + # Make coordinates for nodes which are not neighbors of the transformer, + # not in the longest path, not neighbors of the longest path. + # Nodes are placed in the half plane generated by the straight longest path, + # the angle between the neighbouring nodes are same, basically a tree. + def coordinate_branch( + pos_start, angle_offset, length, node_numerator, node_total_numerator + ): + length = length / branch_detour_factor + angle = node_numerator * 180 / (node_total_numerator + 1) + angle_offset - 90 + x0, y0 = pos_start + x1 = x0 + 1000 * length * math.cos(math.radians(angle)) + y1 = y0 + 1000 * length * math.sin(math.radians(angle)) + origin_angle = math.degrees(math.atan2(y1 - y0, x1 - x0)) + pos_end = (x1, y1) + return pos_end, origin_angle + + # Make coordinates for the nodes of the longest path + # Nodes are distributed in a straight line + def coordinate_longest_path(pos_start, angle_offset, length): + length = length / branch_detour_factor + angle = angle_offset + x0, y0 = pos_start + x1 = x0 + 1000 * length * math.cos(math.radians(angle)) + y1 = y0 + 1000 * length * math.sin(math.radians(angle)) + origin_angle = math.degrees(math.atan2(y1 - y0, x1 - x0)) + pos_end = (x1, y1) + return pos_end, origin_angle + + # Make coordinates for neighbours of nodes of the longest path + # Nodes are placed in an angle of 90 degrees to the longest path + # with alternating direction + def coordinate_longest_path_neighbor(pos_start, angle_offset, length, direction): + length = length / branch_detour_factor + if direction: + angle_random_offset = 90 + else: + angle_random_offset = -90 + angle = angle_offset + angle_random_offset + x0, y0 = pos_start + x1 = x0 + 1000 * length * math.cos(math.radians(angle)) + y1 = y0 + 1000 * length * math.sin(math.radians(angle)) + origin_angle = math.degrees(math.atan2(y1 - y0, x1 - x0)) + pos_end = (x1, y1) + + return pos_end, origin_angle + + # Find transformer node and copy graph + start_node = list(nx.nodes(graph_root))[0] + graph_root.nodes[start_node]["pos"] = (0, 0) + graph_copy = graph_root.copy() + + long_paths = [] + next_nodes = [] + + # Find longest paths + for i in range(0, len(list(nx.neighbors(graph_root, start_node)))): + path_length_to_transformer = [] + for node in graph_copy.nodes(): + try: + paths = list(nx.shortest_simple_paths(graph_copy, start_node, node)) + except nx.NetworkXNoPath: + paths = [[]] + path_length_to_transformer.append(len(paths[0])) + index = path_length_to_transformer.index(max(path_length_to_transformer)) + path_to_max_distance_node = list( + nx.shortest_simple_paths( + graph_copy, start_node, list(nx.nodes(graph_copy))[index] + ) + )[0] + path_to_max_distance_node.remove(start_node) + graph_copy.remove_nodes_from(path_to_max_distance_node) + for node in path_to_max_distance_node: + long_paths.append(node) + + path_to_max_distance_node = long_paths + n = 0 + + # make the coordinates + for node in list(nx.neighbors(graph_root, start_node)): + n = n + 1 + pos, origin_angle = coordinate_source( + graph_root.nodes[start_node]["pos"], + graph_root.edges[start_node, node]["length"], + n, + len(list(nx.neighbors(graph_root, start_node))), + ) + graph_root.nodes[node]["pos"] = pos + graph_root.nodes[node]["origin_angle"] = origin_angle + next_nodes.append(node) + + graph_copy = graph_root.copy() + graph_copy.remove_node(start_node) + while graph_copy.number_of_nodes() > 0: + next_node = next_nodes[0] + n = 0 + for node in list(nx.neighbors(graph_copy, next_node)): + n = n + 1 + if node in path_to_max_distance_node: + pos, origin_angle = coordinate_longest_path( + graph_root.nodes[next_node]["pos"], + graph_root.nodes[next_node]["origin_angle"], + graph_root.edges[next_node, node]["length"], + ) + elif next_node in path_to_max_distance_node: + direction = math.fmod( + len( + list( + nx.shortest_simple_paths(graph_root, start_node, next_node) + )[0] + ), + 2, + ) + pos, origin_angle = coordinate_longest_path_neighbor( + graph_root.nodes[next_node]["pos"], + graph_root.nodes[next_node]["origin_angle"], + graph_root.edges[next_node, node]["length"], + direction, + ) + else: + pos, origin_angle = coordinate_branch( + graph_root.nodes[next_node]["pos"], + graph_root.nodes[next_node]["origin_angle"], + graph_root.edges[next_node, node]["length"], + n, + len(list(nx.neighbors(graph_copy, next_node))), + ) + + graph_root.nodes[node]["pos"] = pos + graph_root.nodes[node]["origin_angle"] = origin_angle + next_nodes.append(node) + + graph_copy.remove_node(next_node) + next_nodes.remove(next_node) + + return graph_root + + +def make_pseudo_coordinates_graph(G: Graph, branch_detour_factor: float) -> Graph: + """ + Generates pseudo coordinates for one graph. + + Parameters + ---------- + G : :networkx:`networkx.Graph` + Graph object to generate pseudo coordinates for. + + branch_detour_factor : float + Defines the quotient of the line length and the distance of the buses. + + Returns + ------- + :networkx:`networkx.Graph` + Graph with pseudo coordinates for all nodes. + + """ + start_time = time() + logger.debug("Start - Making pseudo coordinates for graph") + + x0, y0 = G.nodes[list(nx.nodes(G))[0]]["pos"] + G = _make_coordinates(G, branch_detour_factor) + x0, y0 = coor_transform.transform(x0, y0) + for node in G.nodes(): + x, y = G.nodes[node]["pos"] + G.nodes[node]["pos"] = coor_transform_back.transform(x + x0, y + y0) + + logger.debug("Finished in {}s".format(time() - start_time)) + return G + + +def make_pseudo_coordinates( + edisgo_root: EDisGo, mv_coordinates: bool = False +) -> EDisGo: + """ + Generates pseudo coordinates for all LV grids and optionally MV grid. + + Parameters + ---------- + edisgo_root : :class:`~.EDisGo` + eDisGo object + mv_coordinates : bool, optional + If True pseudo coordinates are also generated for MV grid. + Default: False. + + Returns + ------- + :class:`~.EDisGo` + eDisGo object with pseudo coordinates for all LV nodes and optionally MV nodes. + + """ + start_time = time() + logger.debug( + "Start - Making pseudo coordinates for grid: {}".format( + str(edisgo_root.topology.mv_grid) + ) + ) + + edisgo_obj = copy.deepcopy(edisgo_root) + + grids = list(edisgo_obj.topology.mv_grid.lv_grids) + if mv_coordinates: + grids = [edisgo_obj.topology.mv_grid] + grids + + for grid in grids: + logger.debug("Make pseudo coordinates for: {}".format(grid)) + G = grid.graph + G = make_pseudo_coordinates_graph( + G, edisgo_obj.config["grid_connection"]["branch_detour_factor"] + ) + for node in G.nodes(): + x, y = G.nodes[node]["pos"] + edisgo_obj.topology.buses_df.loc[node, "x"] = x + edisgo_obj.topology.buses_df.loc[node, "y"] = y + + logger.debug("Finished in {}s".format(time() - start_time)) + return edisgo_obj diff --git a/edisgo/tools/tools.py b/edisgo/tools/tools.py index 6f558fec7..6a063db73 100644 --- a/edisgo/tools/tools.py +++ b/edisgo/tools/tools.py @@ -1,3 +1,4 @@ +import logging import os from math import pi, sqrt @@ -20,6 +21,9 @@ from shapely.wkt import loads as wkt_loads +logger = logging.getLogger(__name__) + + def select_worstcase_snapshots(edisgo_obj): """ Select two worst-case snapshots from time series @@ -149,6 +153,28 @@ def calculate_line_resistance(line_resistance_per_km, line_length, num_parallel) return line_resistance_per_km * line_length / num_parallel +def calculate_line_susceptance(line_capacitance_per_km, line_length, num_parallel): + """ + Calculates line shunt susceptance in Siemens. + + Parameters + ---------- + line_capacitance_per_km : float + Line capacitance in uF/km. + line_length : float + Length of line in km. + num_parallel : int + Number of parallel lines. + + Returns + ------- + float + Shunt susceptance in Siemens. + + """ + return line_capacitance_per_km / 1e6 * line_length * 2 * pi * 50 * num_parallel + + def calculate_apparent_power(nominal_voltage, current, num_parallel): """ Calculates apparent power in MVA from given voltage and current. @@ -532,3 +558,76 @@ def get_files_recursive(path, files=None): files.append(file) return files + + +def add_line_susceptance( + edisgo_obj, + mode="mv_b", +): + """ + Adds line susceptance information in Siemens to lines in existing grids. + + Parameters + ---------- + edisgo_obj : :class:`~.EDisGo` + EDisGo object to which line susceptance information is added. + + mode : str + Defines how the susceptance is added: + + * 'no_b' + Susceptance is set to 0 for all lines. + * 'mv_b' (Default) + Susceptance is for the MV lines set according to the equipment parameters + and for the LV lines it is set to zero. + * 'all_b' + Susceptance is for the MV lines set according to the equipment parameters + and for the LV lines 0.25 uF/km is chosen. + + Returns + ------- + :class:`~.EDisGo` + + """ + line_data_df = pd.concat( + [ + edisgo_obj.topology.equipment_data["mv_overhead_lines"], + edisgo_obj.topology.equipment_data["mv_cables"], + edisgo_obj.topology.equipment_data["lv_cables"], + ] + ) + + if mode == "no_b": + line_data_df.loc[:, "C_per_km"] = 0 + elif mode == "mv_b": + line_data_df.loc[ + edisgo_obj.topology.equipment_data["lv_cables"].index, "C_per_km" + ] = 0 + elif mode == "all_b": + line_data_df.loc[ + edisgo_obj.topology.equipment_data["lv_cables"].index, "C_per_km" + ] = 0.25 + else: + raise ValueError("Non-existing mode.") + + lines_df = edisgo_obj.topology.lines_df + buses_df = edisgo_obj.topology.buses_df + + for index, bus0, type_info, length, num_parallel in lines_df[ + ["bus0", "type_info", "length", "num_parallel"] + ].itertuples(): + v_nom = buses_df.loc[bus0].v_nom + + try: + line_capacitance_per_km = ( + line_data_df.loc[line_data_df.U_n == v_nom].loc[type_info].C_per_km + ) + except KeyError: + line_capacitance_per_km = line_data_df.loc[type_info].C_per_km + logger.warning(f"False voltage level for line {index}.") + + lines_df.loc[index, "b"] = calculate_line_susceptance( + line_capacitance_per_km, length, num_parallel + ) + + return edisgo_obj diff --git a/examples/edisgo_simple_example.ipynb b/examples/edisgo_simple_example.ipynb index 56b47186b..5af8aea6e 100755 --- a/examples/edisgo_simple_example.ipynb +++ b/examples/edisgo_simple_example.ipynb @@ -20,22 +20,10 @@ "\n", "This example shows you the first steps with eDisGo. Grid expansion costs for an example distribution grid are calculated assuming renewable and conventional power plant capacities as stated in the scenario framework of the German Grid Development Plan 2015 (Netzentwicklungsplan) for the year 2035 (scenario B2). Through this, the data structure used in eDisGo is explained and it is shown how to get distribution grid data, how to use the automatic grid reinforcement methodology to determine grid expansion needs and costs and how to evaluate your results.\n", "\n", - "\n", - "### Learn more about eDisGo\n", + "**Learn more about eDisGo**\n", "\n", "* __[eDisGo Source Code](https://github.com/openego/eDisGo)__\n", - "* __[eDisGo Documentation](http://edisgo.readthedocs.io/en/dev/)__\n", - "\n", - "### Table of Contents\n", - "\n", - "\n", - "* [Installation](#installation)\n", - "* [Settings](#settings)\n", - "* [eDisGo data structure](#network)\n", - "* [Future generator scenario](#generator_scenario)\n", - "* [Grid reinforcement](#grid_reinforcement)\n", - "* [Results evaluation](#evaluation)\n", - "* [References](#references)" + "* __[eDisGo Documentation](http://edisgo.readthedocs.io/en/dev/)__" ] }, { @@ -77,14 +65,38 @@ "\n", "from edisgo import EDisGo\n", "from edisgo.tools.plots import draw_plotly\n", - "from pathlib import Path" + "from pathlib import Path", + "from edisgo.tools.logger import setup_logger" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Set up logger" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# set up logger that streams edisgo logging messages with level info and above \n", + "# and other logging messages with level warning and above to stdout\n", + "setup_logger(\n", + " loggers=[\n", + " {\"name\": \"root\", \"file_level\": None, \"stream_level\": \"warning\"},\n", + " {\"name\": \"edisgo\", \"file_level\": None, \"stream_level\": \"info\"}\n", + " ]\n", + ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Settings \n", + "### Settings\n", "\n", "The class **EDisGo** serves as the top-level API for\n", "setting up your scenario, invocation of data import, power flow analysis, grid reinforcement and flexibility measures. It also provides access to all relevant data. See the [class documentation](http://edisgo.readthedocs.io/en/dev/api/edisgo.grid.html#edisgo.grid.network.EDisGo) for more information.\n", @@ -101,7 +113,7 @@ "[Ding0 documentation](https://dingo.readthedocs.io/en/dev/usage_details.html#ding0-examples)\n", "on how to generate grids yourself. A ding0 example grid can be viewed [here](https://github.com/openego/eDisGo/tree/dev/tests/data/ding0_test_network_2). It is possible to provide your own grid data if it is in the same format as the ding0 grid data. \n", "\n", - "The ding0 grid you want to use in your analysis is specified through the input parameter 'ding0_grid' of the EDisGo class. To use a different ding0 grid, just change the path below." + "This example works with any ding0 grid data. If you don't have grid data yet, you can execute the following to download the example grid data mentioned above." ] }, { @@ -110,9 +122,56 @@ "metadata": {}, "outputs": [], "source": [ - "# define path to ding0 network\n", - "data_dir = Path(edisgo.__file__).resolve().parent.parent / \"tests/data\"\n", - "ding0_grid = data_dir / \"ding0_test_network_4\"" + "import requests\n", + "\n", + "def download_ding0_example_grid():\n", + "\n", + " # create directories to save ding0 example grid into\n", + " ding0_example_grid_path = os.path.join(\n", + " os.path.expanduser(\"~\"), \".edisgo\", \"ding0_test_network\"\n", + " )\n", + " os.makedirs(ding0_example_grid_path, exist_ok=True)\n", + "\n", + " # download files\n", + " filenames = [\n", + " \"buses\",\n", + " \"generators\",\n", + " \"lines\",\n", + " \"loads\",\n", + " \"network\",\n", + " \"switches\",\n", + " \"transformers\",\n", + " \"transformers_hvmv\",\n", + " ]\n", + "\n", + " for file in filenames:\n", + " req = requests.get(\n", + " \"https://raw.githubusercontent.com/openego/eDisGo/dev/tests/data/ding0_test_network_4/{}.csv\".format(\n", + " file\n", + " )\n", + " )\n", + " filename = os.path.join(ding0_example_grid_path, \"{}.csv\".format(file))\n", + " with open(filename, \"wb\") as fout:\n", + " fout.write(req.content)\n", + "\n", + "\n", + "download_ding0_example_grid()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The ding0 grid you want to use in your analysis is specified through the input parameter 'ding0_grid' of the EDisGo class. The following assumes you want to use the ding0 example grid downloaded above. To use a different ding0 grid, just change the path below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ding0_grid = os.path.join(os.path.expanduser(\"~\"), \".edisgo\", \"ding0_test_network\")" ] }, { @@ -167,17 +226,14 @@ "outputs": [], "source": [ "edisgo = EDisGo(ding0_grid=ding0_grid)\n", - "\n", - "edisgo.set_time_series_worst_case_analysis(cases=cases)\n", - "\n", - "edisgo.timeseries.generators_active_power.head()" + "edisgo.set_time_series_worst_case_analysis(cases=cases)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### eDisGo data structure \n", + "### eDisGo data structure\n", "\n", "As stated above, the EDisGo class serves as the top-level API and provides access to all relevant data. It also enables plotting of the grid topology. In order to have a look at the MV grid topology, you can use the following plot." ] @@ -484,7 +540,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Future generator scenario \n", + "### Future generator scenario\n", "\n", "eDisGo was originally developed in the [open_eGo](https://openegoproject.wordpress.com/) research project. In the open_eGo project two future scenarios, the 'NEP 2035' and the 'ego 100' scenario, were developed. The 'NEP 2035' scenario closely follows the B2-Scenario 2035 from the German network developement plan (Netzentwicklungsplan NEP) 2015. The share of renewables is 65.8%, electricity demand is assumed to stay the same as in the status quo. The 'ego 100' scenario is based on the e-Highway 2050 scenario and assumes a share of renewables of 100% and again an equal electricity demand as in the status quo.\n", "\n", @@ -543,7 +599,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Grid reinforcement \n", + "### Grid reinforcement\n", "\n", "Now we can calculate grid expansion costs that arise from the integration of the new generators.\n", "\n", @@ -748,7 +804,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Evaluate results \n", + "### Evaluate results\n", "\n", "Results such as voltages at nodes and line loading from the power flow analysis as well as\n", "grid expansion costs are provided through the [Results](https://edisgo.readthedocs.io/en/dev/api/edisgo.network.html#edisgo.network.results.Results) class. Above it was already shown how to access \n", @@ -830,16 +886,7 @@ "\n", "```python\n", "edisgo.results.save('path/to/results/directory/')\n", - "```\n", - "\n", - "It can be chosen if results, topology, electromobility and timeseries should be saved. For each one, a separate directory is created, e.g. set parameter `save_results=False`, if you don't want to save the results in a csv file. Per default, all parameters are set to `True`.\n", - "\n", - "Further optional parameters are:\n", - " * `reduce_memory` : If set to True, the size of dataframes containing time series in the results and timeseries class is reduced. Type to convert to can be specified by providing `to_type` as keyword argument. Default: False.\n", - " * `to_type` : Data type to convert time series data to. This is a tradeoff between precision and memory. Default: \"float32\".\n", - " * `archive` : Save storage capacity by archiving the results in an archive. The archiving takes place after the generation of the CSVs and therefore temporarily the storage needs are higher. Default: False.\n", - " * `archive_type` : Set archive type. Default \"zip\"\n", - " * `drop_unarchived` : Drop the unarchived data if parameter archive is set to True. Default: True." + "```" ] }, { @@ -906,7 +953,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## References \n", + "## References\n", "\n", " [1] A.C. Agricola et al.: dena-Verteilnetzstudie: Ausbau- und Innovationsbedarf der Stromverteilnetze in Deutschland bis 2030. 2012.\n", "\n", diff --git a/examples/electromobility_example.ipynb b/examples/electromobility_example.ipynb new file mode 100644 index 000000000..7ff6f7d63 --- /dev/null +++ b/examples/electromobility_example.ipynb @@ -0,0 +1,711 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e9100083", + "metadata": {}, + "source": [ + "# Electromobility example\n", + "\n", + "This example shows you the first steps how to integrate electromobility into eDisGo using data from [SimBEV](https://github.com/rl-institut/simbev) and [TracBEV](https://github.com/rl-institut/tracbev). SimBEV provides data on standing times, charging demand, etc. per vehicle, whereas TracBEV provides potential charging point locations.\n", + "\n", + "**Learn more about eDisGo**\n", + "\n", + "* __[eDisGo Source Code](https://github.com/openego/eDisGo)__\n", + "* __[eDisGo Documentation](http://edisgo.readthedocs.io/en/dev/)__" + ] + }, + { + "cell_type": "markdown", + "id": "c74c4450", + "metadata": {}, + "source": [ + "## Installation and setup\n", + "\n", + "This notebook requires a working installation of eDisGo as well as additional packages like `jupyter notebook` to run the example and `plotly` to view the grid topology. You can install all of these as follows:\n", + "\n", + "```python\n", + "pip install -e .[examples]\n", + "```\n", + "\n", + "Checkout the eDisGo documentation on [how to install eDisGo](https://edisgo.readthedocs.io/en/dev/quickstart.html#getting-started) for more information." + ] + }, + { + "cell_type": "markdown", + "id": "ecefffc4", + "metadata": {}, + "source": [ + "### Import packages" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6898e8bd", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "import geopandas as gpd\n", + "import pandas as pd\n", + "import requests\n", + "import zipfile\n", + "\n", + "import matplotlib.pyplot as plt\n", + "\n", + "from copy import deepcopy\n", + "from pathlib import Path\n", + "\n", + "from edisgo.edisgo import EDisGo\n", + "from edisgo.tools.logger import setup_logger\n", + "from edisgo.tools.plots import plot_dash, plot_plotly" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6b5c46ca", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib notebook" + ] + }, + { + "cell_type": "markdown", + "id": "488bfb8c", + "metadata": {}, + "source": [ + "### Set up logger" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e3b60c43", + "metadata": {}, + "outputs": [], + "source": [ + "# set up logger that streams edisgo logging messages with level info and above \n", + "# and other logging messages with level error and above to stdout\n", + "setup_logger(\n", + " loggers=[\n", + " {\"name\": \"root\", \"file_level\": None, \"stream_level\": \"error\"},\n", + " {\"name\": \"edisgo\", \"file_level\": None, \"stream_level\": \"info\"}\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "fd735589", + "metadata": {}, + "source": [ + "### Download example grid" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "afe44b3f", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "\n", + "def download_ding0_example_grid():\n", + "\n", + " # create directories to save ding0 example grid into\n", + " ding0_example_grid_path = os.path.join(\n", + " os.path.expanduser(\"~\"), \".edisgo\", \"ding0_test_network\"\n", + " )\n", + " os.makedirs(ding0_example_grid_path, exist_ok=True)\n", + "\n", + " # download files\n", + " filenames = [\n", + " \"buses\",\n", + " \"generators\",\n", + " \"lines\",\n", + " \"loads\",\n", + " \"network\",\n", + " \"switches\",\n", + " \"transformers\",\n", + " \"transformers_hvmv\",\n", + " ]\n", + "\n", + " for file in filenames:\n", + " req = requests.get(\n", + " \"https://raw.githubusercontent.com/openego/eDisGo/features/%23261-emob-tests/tests/data/ding0_test_network_4/{}.csv\".format(\n", + " file\n", + " )\n", + " )\n", + " filename = os.path.join(ding0_example_grid_path, \"{}.csv\".format(file))\n", + " with open(filename, \"wb\") as fout:\n", + " fout.write(req.content)\n", + "\n", + "\n", + "download_ding0_example_grid()" + ] + }, + { + "cell_type": "markdown", + "id": "abddc320", + "metadata": {}, + "source": [ + "### Set up edisgo object" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b8a406ae", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "ding0_grid = os.path.join(os.path.expanduser(\"~\"), \".edisgo\", \"ding0_test_network\")\n", + "edisgo = EDisGo(ding0_grid=ding0_grid)\n", + "\n", + "# set up time series\n", + "timeindex = pd.date_range(\"1/1/2011\", periods=24 * 7, freq=\"H\")\n", + "edisgo.set_timeindex(timeindex)\n", + "edisgo.set_time_series_active_power_predefined(\n", + " fluctuating_generators_ts=\"oedb\",\n", + " dispatchable_generators_ts=pd.DataFrame(data=1, columns=[\"other\"], index=timeindex),\n", + " conventional_loads_ts=\"demandlib\",\n", + ")\n", + "edisgo.set_time_series_reactive_power_control()\n", + "\n", + "# resample time series to have a temporal resolution of 15 minutes, which is the same \n", + "# as the electromobility time series\n", + "edisgo.resample_timeseries()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "716fa083-0409-46a4-a55c-07cac583e387", + "metadata": {}, + "outputs": [], + "source": [ + "# plot feed-in, demand and residual load\n", + "\n", + "fig, ax = plt.subplots(figsize=(8, 6))\n", + "\n", + "edisgo.timeseries.generators_active_power.sum(axis=1).plot.line(ax=ax)\n", + "edisgo.timeseries.loads_active_power.sum(axis=1).plot.line(ax=ax)\n", + "edisgo.timeseries.residual_load.plot.line(ax=ax)\n", + "\n", + "ax.legend([\"Feed-in\", \"Demand\", \"Residual load\"])\n", + "ax.set_ylabel(\"Power in MW\")\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "4269ad12", + "metadata": {}, + "source": [ + "## Prerequisite data\n", + "\n", + "Currently, eDisGo only provides an automated process to obtain electromobility data from [SimBEV](https://github.com/rl-institut/simbev) and [TracBEV](https://github.com/rl-institut/tracbev).\n", + "\n", + "Since SimBEV and TracBEV generate data on municipality level, it is necessary to determine which municipalities lie within or intersect the network area. Therefore, municipality geodata is necessary. The download and how to find the municipalities that intersect the chosen MV grid district is shown in the following.\n", + "\n", + "### Download 'Verwaltungsgebiete' data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2bb2c59b", + "metadata": {}, + "outputs": [], + "source": [ + "vg250_path = Path.home() / \".edisgo\" / \"vg250\"\n", + "type(vg250_path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4fd15bcd", + "metadata": {}, + "outputs": [], + "source": [ + "vg250_path = Path(Path.home(), \".edisgo\", \"vg250\")\n", + "target = Path(vg250_path, \"vg250_01-01.geo84.shape.ebenen/vg250_ebenen_0101/VG250_GEM.shp\")\n", + "\n", + "if not target.is_file():\n", + " vg250_path.mkdir(parents=True, exist_ok=True)\n", + "\n", + " filename = os.path.join(vg250_path, \"vg250.geo84\")\n", + "\n", + " url = \"https://daten.gdz.bkg.bund.de/produkte/vg/vg250_ebenen_0101/2020/vg250_01-01.geo84.shape.ebenen.zip\"\n", + " req = requests.get(url)\n", + "\n", + " with open(filename, \"wb\") as fout:\n", + " fout.write(req.content)\n", + "\n", + " with zipfile.ZipFile(filename, \"r\") as zip_ref:\n", + " zip_ref.extractall(vg250_path)\n", + "\n", + "vg250 = gpd.read_file(target)\n", + "\n", + "vg250.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5a521d85-b213-4031-a7b5-a05603f86109", + "metadata": {}, + "outputs": [], + "source": [ + "# plot municipality shapes\n", + "fig, ax = plt.subplots(figsize=(5, 8))\n", + "vg250.plot(ax=ax)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "b2e81602", + "metadata": {}, + "source": [ + "### Check which 'Verwaltungsgebiete' intersect MV grid" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2947bbc4", + "metadata": {}, + "outputs": [], + "source": [ + "mv_grid_gdf = gpd.GeoDataFrame(\n", + " pd.DataFrame(data=edisgo.topology.grid_district[\"geom\"], columns=[\"geometry\"]),\n", + " crs=f\"EPSG:{edisgo.topology.grid_district['srid']}\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c23faa6b", + "metadata": {}, + "outputs": [], + "source": [ + "intersect_gdf = mv_grid_gdf.sjoin(vg250)\n", + "print(\"Intersecting AGS\")\n", + "intersect_gdf.AGS.to_list()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38e067dd", + "metadata": {}, + "outputs": [], + "source": [ + "# plot MV grid district (black line) and intersecting AGS (blue shapes)\n", + "fig, ax = plt.subplots(figsize=(5, 8))\n", + "\n", + "vg250.loc[vg250.AGS.isin(intersect_gdf.AGS)].plot(ax=ax)\n", + "mv_grid_gdf.boundary.plot(ax=ax, color=\"black\")\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "e2082ea8-3be5-4e69-8b3b-26023bedc71b", + "metadata": {}, + "source": [ + "As most municipalities only intersect the grid district at its border, only the electromobility data for one municipality needs to be generated." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0d4e721d-6be2-4e41-b6d0-349f9bbc2f5b", + "metadata": {}, + "outputs": [], + "source": [ + "# plot MV grid district (black line) and mainly intersecting AGS (blue shape)\n", + "fig, ax = plt.subplots(figsize=(5, 5))\n", + "\n", + "vg250.loc[vg250.AGS == \"05334032\"].plot(ax=ax)\n", + "mv_grid_gdf.boundary.plot(ax=ax, color=\"black\")\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "bfc8a701", + "metadata": {}, + "source": [ + "## Add electromobility to EDisGo object\n", + "\n", + "### Electromobility data\n", + "So far, adding electromobility data to an EDisGo object requires electromobility data from SimBEV (required version: [3083c5a](https://github.com/rl-institut/simbev/commit/86076c936940365587c9fba98a5b774e13083c5a))\n", + "and TracBEV (required version: [14d864c](https://github.com/rl-institut/tracbev/commit/03e335655770a377166c05293a966052314d864c)) to be pre-generated. The data is currently not created automatically.\n", + "\n", + "\n", + "\n", + "If you don't have SimBEV and TracBEV data yet, you can use the data provided for this example for the ding0 grid downloaded above.\n", + "\n", + "In order to import the electromobility data of the grid that you downloaded above and integrate charging points into the grid, you can use the function `EDisGo.import_electromobility`. Besides loading the electromobility data, the function also allocates the charging demand from SimBEV to charging sites from TracBEV and integrates the charging parks into the grid. This is further explained in the following.\n", + "\n", + "### Allocation of charging demand\n", + "\n", + "After electromobility data is loaded, the charging demand from SimBEV is allocated to potential charging parks from TracBEV. The allocation of the charging processes to the charging infrastructure is carried out with the help of the weighting factor of the potential charging parks determined by TracBEV. This involves a random and weighted selection of one charging park per charging process. In the case of private charging infrastructure, a separate charging point is set up for each EV. All charging processes of the respective EV and charging use case are assigned to this charging point.\n", + "\n", + "For the public charging infrastructure, the allocation is made explicitly per charging process. For each charging process it is determined whether a suitable charging point is already available. For this purpose it is checked whether the charging point is occupied by another EV in the corresponding period and whether it can provide the corresponding charging capacity. If no suitable charging point is available, a charging point is determined randomly and weighted in the same way as for private charging.\n", + "\n", + "### Integration of charging parks\n", + "\n", + "After the allocation of charging demand to specific charging sites, all potential charging parks with charging demand allocated to them are integrated into the grid. This is realised the following way:\n", + "\n", + "* If power rating is <= 0.3 MVA, the charging point is integrated into the LV grid, otherwise it is integrated into the MV grid.\n", + "* Integration into LV grid:\n", + " * The considered charging point is integrated into the LV grid whose distribution substation is closest (this is currently done this way because the LV grids are not georeferenced but only the MV grid including the MV-LV substations).\n", + " * If power rating is > 0.1 MVA, the charging point is directly connected to the distribution substation.\n", + " * If power rating is <= 0.1 MVA, the type of connection depends on the charging point use case:\n", + " - Use Case `home`: Charging point is connected to a random household load in the identified LV grid.\n", + " - Use Case `work`: Charging point is connected to a random commercial, industrial or agricultural consumer.\n", + " - Use Case `public`: Charging point is connected to a random grid connection point in the identified LV grid.\n", + "* Integration into MV grid:\n", + " * If the power rating of the charging point is > 4.5 MVA, it is directly connected to the HV-MV station.\n", + " * If the power rating of the charging point is <= 4.5 MVA, it is connected to the nearest grid connection point or cable. If a cable is selected, the line is cut at the point closest to the charging station and a new branch tee is added to which the charging station is connected." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1d65e6d6", + "metadata": {}, + "outputs": [], + "source": [ + "data_dir = os.path.join(os.path.dirname(os.getcwd()), \"tests\", \"data\")\n", + "edisgo.import_electromobility(\n", + " simbev_directory=os.path.join(data_dir, \"simbev_example_scenario_2\"),\n", + " tracbev_directory=os.path.join(data_dir, \"tracbev_example_scenario_2\")\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "ae9955f1", + "metadata": {}, + "source": [ + "### eDisGo electromobility data structure \n", + "\n", + "All data coming from SimBEV and TracBEV is stored in the `Electromobility` object that can be accessed through the `EDisGo` object as follows:\n", + "\n", + "```python\n", + "edisgo.electromobility\n", + "```\n", + "\n", + "Integrated charging parks can also be found in the `Topology` object:\n", + "\n", + "```python\n", + "edisgo.topology.loads_df[edisgo.topology.loads_df.type == \"charging_point\"]\n", + "```\n", + "\n", + "Data stored in the `Electromobility` object is shown in the following." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0e859c1e-6aba-4457-92f5-59b1a4b4ddae", + "metadata": {}, + "outputs": [], + "source": [ + "# SimBEV charging processes data\n", + "edisgo.electromobility.charging_processes_df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "964916d6-82fc-47fb-8ff4-d28173113128", + "metadata": {}, + "outputs": [], + "source": [ + "# SimBEV configuration data\n", + "edisgo.electromobility.simbev_config_df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "db648528-06dd-40cf-9fc0-4137280f21cb", + "metadata": {}, + "outputs": [], + "source": [ + "# TracBEV potential charging point data\n", + "edisgo.electromobility.potential_charging_parks_gdf.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f6663f9f-2481-403d-b1d8-c0cf364d3eba", + "metadata": {}, + "outputs": [], + "source": [ + "# Charging parks that got integrated into the network\n", + "edisgo.electromobility.integrated_charging_parks_df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c71977c0-e4e0-443e-afa1-ed632c30c54b", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.topology.loads_df[edisgo.topology.loads_df.type == \"charging_point\"].head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1b156984-4431-4312-a617-a23441e0d153", + "metadata": {}, + "outputs": [], + "source": [ + "# plotting the grid district and all potential charging parks\n", + "\n", + "fig, ax = plt.subplots(figsize=(11, 11))\n", + "\n", + "mv_grid_gdf.boundary.plot(ax=ax, color=\"black\")\n", + "\n", + "# plot potential charging parks\n", + "edisgo.electromobility.potential_charging_parks_gdf.plot(ax=ax, alpha=0.3)\n", + "\n", + "# plot integrated charging parks\n", + "edisgo.electromobility.potential_charging_parks_gdf.loc[\n", + " edisgo.electromobility.integrated_charging_parks_df.index\n", + "].plot(ax=ax, color=\"green\", markersize=50)\n", + "\n", + "# plot charging parks with charging demand but outside of the grid district\n", + "# and therefore not integrated\n", + "charging_parks_with_charging_demand = (\n", + " edisgo.electromobility.charging_processes_df.charging_park_id.unique()\n", + ")\n", + "charging_parks_not_integrated = set(charging_parks_with_charging_demand) - set(\n", + " edisgo.electromobility.integrated_charging_parks_df.index\n", + ")\n", + "\n", + "edisgo.electromobility.potential_charging_parks_gdf.loc[\n", + " charging_parks_not_integrated\n", + "].plot(ax=ax, color=\"red\", markersize=50)\n", + "\n", + "ax.legend(\n", + " [\n", + " \"Grid district\",\n", + " \"Potential charging parks\",\n", + " \"Integrated charging parks\",\n", + " \"Charging parks with charging demand not integrated\",\n", + " ]\n", + ")\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "b82b9f8f", + "metadata": {}, + "source": [ + "## Applying different charging strategies\n", + "\n", + "The `EDisGo.import_electromobility()` function does not yield charging time series per charging point but only charging processes taking place at each charging point. The actual charging time series are determined through applying a charging strategy using the function `EDisGo.apply_charging_strategy`." + ] + }, + { + "cell_type": "markdown", + "id": "0cc6707b", + "metadata": {}, + "source": [ + "The eDisGo tool currently offers three different charging strategies: `dumb`, `reduced` and `residual`.\n", + "The aim of the charging strategies 'reduced' and 'residual' is to generate the most grid-friendly charging behavior possible without restricting the convenience for end users. Therefore, the boundary condition of all charging strategies is that the charging requirement of each charging process must be fully covered. This means that charging processes can only be flexibilised if the EV can be fully charged while it is stationary. Furthermore, only private\n", + "charging processes can be used as a flexibility, since the fulfillment of the service is the priority for public \n", + "charging processes.\n", + "\n", + "\n", + "* `dumb`: In this charging strategy the cars are charged directly after arrival with the maximum possible charging capacity.\n", + "\n", + "* `reduced`: This is a preventive charging strategy. The cars are charged directly after arrival with the minimum possible charging power. The minimum possible charging power is determined by the parking time and the parameter `minimum_charging_capacity_factor`.\n", + "\n", + "* `residual`: This is an active charging strategy. The cars are charged when the residual load in the MV grid is lowest (high generation and low consumption). Charging processes with a low flexibility are given priority.\n", + "\n", + "In the following all three charging strategies are applied. To show their differences, three EDisGo objects are used." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18455dcc-0db7-4ade-9003-6c183552a12b", + "metadata": {}, + "outputs": [], + "source": [ + "# copy edisgo object to have three objects to apply charging strategies on\n", + "edisgo2 = deepcopy(edisgo)\n", + "edisgo3 = deepcopy(edisgo)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "685108f9-f15b-459e-8f22-2d99c678fb1c", + "metadata": {}, + "outputs": [], + "source": [ + "# apply default charging strategy \"dumb\"\n", + "edisgo.apply_charging_strategy()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b56ebbd4", + "metadata": {}, + "outputs": [], + "source": [ + "# conduct grid analysis\n", + "# to keep the calculation time low in this example, only worst-case timesteps are analysed\n", + "residual_load = edisgo.timeseries.residual_load\n", + "worst_case_time_steps = pd.DatetimeIndex(\n", + " [residual_load.idxmin(), residual_load.idxmax()]\n", + ")\n", + "edisgo.analyze(timesteps=worst_case_time_steps);" + ] + }, + { + "cell_type": "markdown", + "id": "b9cd3434", + "metadata": {}, + "source": [ + "To change the charging strategy from the default `dumb` to one of the other strategies, the `strategy` parameter has to be set accordingly:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a15eece2-951e-4749-9ab4-eaf3c22b0077", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo2.apply_charging_strategy(strategy=\"reduced\")\n", + "edisgo2.analyze(timesteps=worst_case_time_steps);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b61d2e2", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo3.apply_charging_strategy(strategy=\"residual\")\n", + "edisgo3.analyze(timesteps=worst_case_time_steps);" + ] + }, + { + "cell_type": "markdown", + "id": "3bd366aa-ea6e-4d1f-a66b-fee6bcaf3f4f", + "metadata": {}, + "source": [ + "**Plot charging time series for differenct charging strategies**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20d98ca8", + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(1, 1, figsize=(9, 5))\n", + "\n", + "edisgo.timeseries.charging_points_active_power(edisgo).sum(axis=1).plot.line(\n", + " ax=ax, color=\"blue\", legend=True, label=\"dumb\"\n", + ")\n", + "edisgo2.timeseries.charging_points_active_power(edisgo2).sum(axis=1).plot.line(\n", + " ax=ax, color=\"red\", legend=True, label=\"reduced\"\n", + ")\n", + "edisgo3.timeseries.charging_points_active_power(edisgo3).sum(axis=1).plot.line(\n", + " ax=ax, color=\"cyan\", legend=True, label=\"residual\"\n", + ")\n", + "\n", + "plt.tight_layout()\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0107c58d", + "metadata": {}, + "outputs": [], + "source": [ + "plot_dash(\n", + " edisgo_objects={\n", + " \"edisgo\": edisgo,\n", + " \"edisgo2\": edisgo2,\n", + " \"edisgo3\": edisgo3,\n", + " }\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": { + "height": "calc(100% - 180px)", + "left": "10px", + "top": "150px", + "width": "428.8px" + }, + "toc_section_display": true, + "toc_window_display": true + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/example_grid_reinforcement.py b/examples/example_grid_reinforcement.py index 803586a9d..f944d07bf 100644 --- a/examples/example_grid_reinforcement.py +++ b/examples/example_grid_reinforcement.py @@ -32,19 +32,29 @@ from edisgo import EDisGo from edisgo.network.results import Results +from edisgo.tools.logger import setup_logger logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) def run_example(): + + # set up logger that streams edisgo logging messages with level info and above + # and other logging messages with level warning and above to stdout + setup_logger( + loggers=[ + {"name": "root", "file_level": None, "stream_level": "warning"}, + {"name": "edisgo", "file_level": None, "stream_level": "info"}, + ] + ) + # Specify path to directory containing ding0 grid csv files edisgo_path = os.path.join(os.path.expanduser("~"), ".edisgo") dingo_grid_path = os.path.join(edisgo_path, "ding0_example_grid") # Download example grid data in case it does not yet exist - if not os.path.isdir(dingo_grid_path): + if not os.path.isdir(dingo_grid_path) or len(os.listdir(dingo_grid_path)) == 0: logger.debug("Download example grid data.") - os.makedirs(dingo_grid_path) + os.makedirs(dingo_grid_path, exist_ok=True) file_list = [ "buses.csv", "lines.csv", diff --git a/examples/plot_example.ipynb b/examples/plot_example.ipynb index 198302ec6..1901f285f 100755 --- a/examples/plot_example.ipynb +++ b/examples/plot_example.ipynb @@ -35,6 +35,16 @@ "#### Import packages" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "logging.basicConfig(level=logging.CRITICAL)" + ] + }, { "cell_type": "code", "execution_count": null, @@ -46,10 +56,11 @@ "import pandas as pd\n", "import requests\n", "import copy\n", + "import time\n", "\n", "from edisgo import EDisGo\n", - "from edisgo.tools.plots import draw_plotly\n", - "from edisgo.tools.plots import dash_plot" + "from edisgo.tools.plots import plot_plotly\n", + "from edisgo.tools.plots import plot_dash" ] }, { @@ -65,6 +76,8 @@ "metadata": {}, "outputs": [], "source": [ + "import requests\n", + "\n", "def download_ding0_example_grid():\n", "\n", " # create directories to save ding0 example grid into\n", @@ -100,30 +113,32 @@ ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "ding0_grid = os.path.join(os.path.expanduser(\"~\"), \".edisgo\", \"ding0_test_network\")" + "#### Create edisgo objects" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "#### Create edisgo objects" + "ding0_grid = os.path.join(os.path.expanduser(\"~\"), \".edisgo\", \"ding0_test_network\")" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "scrolled": true, + "tags": [] + }, "outputs": [], "source": [ "edisgo_root = EDisGo(ding0_grid=ding0_grid)\n", - "edisgo_root.set_time_series_worst_case_analysis()\n", - "edisgo_root.analyze()" + "edisgo_root.set_time_series_worst_case_analysis()" ] }, { @@ -132,7 +147,8 @@ "metadata": {}, "outputs": [], "source": [ - "edisgo_copy = copy.deepcopy(edisgo_root)" + "edisgo_analyzed = copy.deepcopy(edisgo_root)\n", + "edisgo_analyzed.analyze();" ] }, { @@ -142,7 +158,7 @@ "outputs": [], "source": [ "edisgo_reinforced = copy.deepcopy(edisgo_root)\n", - "edisgo_reinforced.reinforce()" + "edisgo_reinforced.reinforce();" ] }, { @@ -158,7 +174,7 @@ "tags": [] }, "source": [ - "### draw_plotly function" + "### plot_plotly function" ] }, { @@ -167,37 +183,46 @@ "tags": [] }, "source": [ - "In the following different plotting options are shown.\n", - "\n", - "The different options for node colors (set using parameter `mode_nodes`) and line colors (set using parameter `mode_lines`) are:\n", - "\n", - "- mode_nodes\n", - " - 'voltage_deviation' - shows the deviation of the node voltage relative to 1 p.u.\n", - " - 'adjacencies' - shows the the number of connections of the graph\n", - "- mode_lines\n", - " - 'relative_loading' - shows the line loading relative to the s_nom of the line\n", - " - 'loading' - shows the loading\n", - " - 'reinforce' - shows the reinforced lines in green\n", - "\n", - "\n", - "The different options for used coordinates (set using parameter `grid`) are:\n", + "In the following different plotting options are shown. For more information on different plotting options see [API docstring](https://edisgo.readthedocs.io/en/dev/api/edisgo.tools.html#edisgo.tools.plots.draw_plotly).\n", "\n", - "- grid\n", - " - grid object - the coordinate origin is set to the stations coordinates\n", - " - False - the coordinates are not modified \n", "\n", "Hovering over nodes and lines shows some information on them." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Plotting relative loading and voltage deviation, with grid coordinates modified to have the station in the origin" + ] + }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "edisgo_obj = edisgo_root\n", - "grid = edisgo_obj.topology.mv_grid\n", - "G = grid.graph" + "fig = plot_plotly(\n", + " edisgo_obj=edisgo_analyzed,\n", + " grid=None,\n", + " line_color=\"relative_loading\",\n", + " node_color=\"voltage_deviation\",\n", + " line_result_selection=\"max\",\n", + " node_result_selection=\"max\",\n", + " selected_timesteps=None,\n", + " center_coordinates=True,\n", + " pseudo_coordinates=False,\n", + " node_selection=False\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Plotting relative loading and voltage deviation, with time step selection" ] }, { @@ -206,14 +231,28 @@ "metadata": {}, "outputs": [], "source": [ - "# plotting relative loading and voltage deviation, with grid coordinates modified to have the station in the origin\n", - "mode_lines = \"relative_loading\"\n", - "mode_nodes = \"voltage_deviation\"\n", - "\n", - "fig = draw_plotly(\n", - " edisgo_obj, G, line_color=mode_lines, node_color=mode_nodes, grid=grid\n", - ")\n", - "fig.show()" + "plot_plotly(\n", + " edisgo_obj=edisgo_analyzed,\n", + " grid=None,\n", + " line_color=\"relative_loading\",\n", + " node_color=\"voltage_deviation\",\n", + " line_result_selection=\"max\",\n", + " node_result_selection=\"max\",\n", + " selected_timesteps=[\n", + " '1970-01-01 00:00:00', \n", + " '1970-01-01 01:00:00',\n", + " ],\n", + " center_coordinates=False,\n", + " pseudo_coordinates=False,\n", + " node_selection=False\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Plotting loading and voltage deviation, with pseudo coordinates" ] }, { @@ -222,14 +261,25 @@ "metadata": {}, "outputs": [], "source": [ - "# plotting loading and voltage deviation, with unchanged coordinates\n", - "mode_lines = \"loading\"\n", - "mode_nodes = \"voltage_deviation\"\n", - "\n", - "fig = draw_plotly(\n", - " edisgo_obj, G, line_color=mode_lines, node_color=mode_nodes, grid=False\n", - ")\n", - "fig.show()" + "plot_plotly(\n", + " edisgo_obj=edisgo_analyzed,\n", + " grid=None,\n", + " line_color=\"loading\",\n", + " node_color=\"voltage_deviation\",\n", + " line_result_selection=\"max\",\n", + " node_result_selection=\"max\",\n", + " selected_timesteps='1970-01-01 03:00:00',\n", + " center_coordinates=True,\n", + " pseudo_coordinates=True,\n", + " node_selection=False\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Plotting reinforced lines, node adjacencies and a node selection" ] }, { @@ -238,17 +288,26 @@ "metadata": {}, "outputs": [], "source": [ - "# plotting reinforced lines and node adjacencies\n", - "edisgo_obj = edisgo_reinforced\n", - "G = edisgo_obj.topology.mv_grid.graph\n", - "\n", - "mode_lines = \"reinforce\"\n", - "mode_nodes = \"adjacencies\"\n", - "\n", - "fig = draw_plotly(\n", - " edisgo_obj, G, line_color=mode_lines, node_color=mode_nodes, grid=False\n", - ")\n", - "fig.show()" + "plot_plotly(\n", + " edisgo_obj=edisgo_reinforced,\n", + " grid=None,\n", + " line_color=\"reinforce\",\n", + " node_color=\"adjacencies\",\n", + " line_result_selection=\"max\",\n", + " node_result_selection=\"max\",\n", + " selected_timesteps=None,\n", + " center_coordinates=True,\n", + " pseudo_coordinates=False,\n", + " node_selection=[\n", + " 'Bus_MVStation_1',\n", + " 'Bus_BranchTee_MVGrid_1_5',\n", + " 'Bus_BranchTee_MVGrid_1_10',\n", + " 'Bus_GeneratorFluctuating_4',\n", + " 'Bus_BranchTee_MVGrid_1_11',\n", + " 'Bus_GeneratorFluctuating_3',\n", + " 'BusBar_MVGrid_1_LVGrid_4_MV'\n", + " ]\n", + ")" ] }, { @@ -257,7 +316,7 @@ "tags": [] }, "source": [ - "### Dash plot app which calls draw_plotly\n", + "### Dash plot app which calls plot_plotly\n", "One edisgo object creates one large plot. Two or more edisgo objects create two adjacent plots, where the objects to be plotted are selected in the dropdown menu." ] }, @@ -273,11 +332,13 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "scrolled": true + }, "outputs": [], "source": [ - "app = dash_plot(edisgo_objects=edisgo_root)\n", - "app.run_server(mode=\"inline\")" + "plot_dash(edisgo_objects=edisgo_analyzed)\n", + "time.sleep(2)" ] }, { @@ -293,10 +354,8 @@ "metadata": {}, "outputs": [], "source": [ - "app = dash_plot(\n", - " edisgo_objects={\"edisgo_obj_1\": edisgo_root, \"edisgo_obj_2\": edisgo_reinforced}\n", - ")\n", - "app.run_server(mode=\"inline\")" + "plot_dash(edisgo_objects={\"edisgo_analyzed\": edisgo_analyzed, \"edisgo_reinforced\": edisgo_reinforced})\n", + "time.sleep(1)" ] }, { @@ -314,22 +373,14 @@ "metadata": {}, "outputs": [], "source": [ - "app = dash_plot(\n", + "plot_dash(\n", " edisgo_objects={\n", - " \"edisgo_obj_1\": edisgo_root,\n", - " \"edisgo_obj_2\": edisgo_reinforced,\n", - " \"edisgo_obj_3\": edisgo_copy,\n", + " \"edisgo_root\": edisgo_root,\n", + " \"edisgo_analyzed\": edisgo_analyzed,\n", + " \"edisgo_reinforced\": edisgo_reinforced\n", " }\n", - ")\n", - "app.run_server(mode=\"inline\")" + ")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -361,7 +412,7 @@ "toc_cell": false, "toc_position": {}, "toc_section_display": true, - "toc_window_display": false + "toc_window_display": true } }, "nbformat": 4, diff --git a/setup.py b/setup.py index dd0fd7221..40052998d 100644 --- a/setup.py +++ b/setup.py @@ -56,16 +56,16 @@ def read(fname): "contextily", "descartes", "plotly", - "dash==2.0.0", - "werkzeug==2.0.3", + "dash==2.6.0", + "werkzeug==2.2.0", ] examples_requirements = [ "jupyter", "jupyterlab", "plotly", - "dash==2.0.0", + "dash==2.6.0", "jupyter_dash", - "werkzeug==2.0.3", + "werkzeug==2.2.0", ] dev_requirements = [ "pytest", diff --git a/tests/conftest.py b/tests/conftest.py index c70132cb6..f8449feeb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,27 +5,37 @@ def pytest_configure(config): pytest.ding0_test_network_path = os.path.join( - os.path.realpath(os.path.dirname(__file__)), "data/ding0_test_network_1" + os.path.realpath(os.path.dirname(__file__)), "data", "ding0_test_network_1" ) pytest.ding0_test_network_2_path = os.path.join( - os.path.realpath(os.path.dirname(__file__)), "data/ding0_test_network_2" + os.path.realpath(os.path.dirname(__file__)), "data", "ding0_test_network_2" ) pytest.ding0_test_network_3_path = os.path.join( - os.path.realpath(os.path.dirname(__file__)), "data/ding0_test_network_3" + os.path.realpath(os.path.dirname(__file__)), "data", "ding0_test_network_3" ) pytest.ding0_test_network_4_path = os.path.join( - os.path.realpath(os.path.dirname(__file__)), "data/ding0_test_network_4" + os.path.realpath(os.path.dirname(__file__)), "data", "ding0_test_network_4" ) pytest.simbev_example_scenario_path = os.path.join( - os.path.realpath(os.path.dirname(__file__)), "data/simbev_example_scenario_2" + os.path.realpath(os.path.dirname(__file__)), "data", "simbev_example_scenario_2" ) pytest.tracbev_example_scenario_path = os.path.join( - os.path.realpath(os.path.dirname(__file__)), "data/tracbev_example_scenario_2" + os.path.realpath(os.path.dirname(__file__)), + "data", + "tracbev_example_scenario_2", + ) + + pytest.simbev_example_scenario_path_1 = os.path.join( + os.path.realpath(os.path.dirname(__file__)), "data", "simbev_example_scenario" + ) + + pytest.tracbev_example_scenario_path_1 = os.path.join( + os.path.realpath(os.path.dirname(__file__)), "data", "tracbev_example_scenario" ) config.addinivalue_line("markers", "slow: mark test as slow to run") diff --git a/tests/data/ding0_test_network_1/lines.csv b/tests/data/ding0_test_network_1/lines.csv index 02bd0b4c9..de0ae0b27 100755 --- a/tests/data/ding0_test_network_1/lines.csv +++ b/tests/data/ding0_test_network_1/lines.csv @@ -2,8 +2,8 @@ name,bus0,bus1,length,x,r,s_nom,num_parallel,kind,type_info Line_10003,Bus_MVStation_1,Bus_BranchTee_MVGrid_1_1,0.083904310632546,0.031103993574751,0.031044594934042,7.27461339178928,1,line,48-AL1/8-ST1A Line_10004,Bus_MVStation_1,Bus_BranchTee_MVGrid_1_4,0.226148217025158,0.083834938112983,0.083674840299309,7.27461339178928,1,line,48-AL1/8-ST1A Line_10005,Bus_MVStation_1,Bus_BranchTee_MVGrid_1_5,0.543380191002877,0.201435347506981,0.201050670671064,7.27461339178928,1,line,48-AL1/8-ST1A -Line_10006,Bus_MVStation_1,Bus_BranchTee_MVGrid_1_6,0.297650465459542,0.110341388843783,0.110130672220031,7.27461339178928,1,line,NA2XS2Y 3x1x185 RM/25 -Line_10007,Bus_BranchTee_MVGrid_1_1,BusBar_MVGrid_1_LVGrid_1_MV,0.722445826838636,0.267816399261118,0.267304955930295,7.27461339178928,1,line,NA2XS2Y 3x1x185 RM/25 +Line_10006,Bus_MVStation_1,Bus_BranchTee_MVGrid_1_6,0.297650465459542,0.03363542166704131,0.03869456050974046,7.27461339178928,1,line,NA2XS2Y 3x1x240 +Line_10007,Bus_BranchTee_MVGrid_1_1,BusBar_MVGrid_1_LVGrid_1_MV,0.722445826838636,0.08163860916459667,0.09391795748902268,7.27461339178928,1,line,NA2XS2Y 3x1x240 Line_10008,Bus_BranchTee_MVGrid_1_2,BusBar_MVGrid_1_LVGrid_1_MV,0.132867031507129,0.049254862630276,0.049160801657638,7.27461339178928,1,line,48-AL1/8-ST1A Line_10009,Bus_BranchTee_MVGrid_1_2,BusBar_MVGrid_1_LVGrid_2_MV,0.705785336662554,0.261640223383117,0.261140574565145,7.27461339178928,1,line,48-AL1/8-ST1A Line_10010,Bus_BranchTee_MVGrid_1_2,BusBar_MVGrid_1_LVGrid_5_MV,0.012061682815469,0.004471361506515,0.004462822641724,7.27461339178928,1,line,48-AL1/8-ST1A diff --git a/tests/flex_opt/test_charging_strategy.py b/tests/flex_opt/test_charging_strategy.py new file mode 100644 index 000000000..761c03bec --- /dev/null +++ b/tests/flex_opt/test_charging_strategy.py @@ -0,0 +1,85 @@ +import pandas as pd +import pytest + +from edisgo.edisgo import EDisGo +from edisgo.flex_opt.charging_strategies import charging_strategy + + +class TestChargingStrategy: + """ + Tests all charging strategies implemented in charging_strategies.py. + + """ + + @classmethod + def setup_class(cls): + cls.ding0_path = pytest.ding0_test_network_4_path + cls.simbev_path = pytest.simbev_example_scenario_path + cls.tracbev_path = pytest.tracbev_example_scenario_path + cls.standing_times_path = cls.simbev_path + cls.charging_strategies = ["dumb", "reduced", "residual"] + + cls.edisgo_obj = EDisGo(ding0_grid=cls.ding0_path) + timeindex = pd.date_range("1/1/2011", periods=24 * 7, freq="H") + cls.edisgo_obj.set_timeindex(timeindex) + + cls.edisgo_obj.resample_timeseries() + cls.edisgo_obj.import_electromobility(cls.simbev_path, cls.tracbev_path) + + def test_charging_strategy(self): + charging_demand_lst = [] + + for strategy in self.charging_strategies: + charging_strategy(self.edisgo_obj, strategy=strategy) + + ts = self.edisgo_obj.timeseries + + # Check if all charging points have a valid chargingdemand_kWh > 0 + df = ts.charging_points_active_power(self.edisgo_obj).loc[ + :, (ts.charging_points_active_power(self.edisgo_obj) <= 0).any(axis=0) + ] + + assert df.shape == ts.charging_points_active_power(self.edisgo_obj).shape + + charging_demand_lst.append( + ts.charging_points_active_power(self.edisgo_obj).sum() + ) + + # Check charging strategy for different timestamp_share_threshold value + charging_strategy( + self.edisgo_obj, strategy="dumb", timestamp_share_threshold=0.5 + ) + + ts = self.edisgo_obj.timeseries + + # Check if all charging points have a valid chargingdemand_kWh > 0 + df = ts.charging_points_active_power(self.edisgo_obj).loc[ + :, (ts.charging_points_active_power(self.edisgo_obj) <= 0).any(axis=0) + ] + + assert df.shape == ts.charging_points_active_power(self.edisgo_obj).shape + + # Check charging strategy for different minimum_charging_capacity_factor + charging_strategy( + self.edisgo_obj, strategy="reduced", minimum_charging_capacity_factor=0.5 + ) + + ts = self.edisgo_obj.timeseries + + # Check if all charging points have a valid chargingdemand_kWh > 0 + df = ts.charging_points_active_power(self.edisgo_obj).loc[ + :, (ts.charging_points_active_power(self.edisgo_obj) <= 0).any(axis=0) + ] + + assert df.shape == ts.charging_points_active_power(self.edisgo_obj).shape + + charging_demand_lst.append( + ts.charging_points_active_power(self.edisgo_obj).sum() + ) + + # the chargingdemand_kWh per charging point and therefore in total should + # always be the same + assert all( + (_.round(4) == charging_demand_lst[0].round(4)).all() + for _ in charging_demand_lst + ) diff --git a/tests/flex_opt/test_costs.py b/tests/flex_opt/test_costs.py index f5dea1b0a..6864ef461 100644 --- a/tests/flex_opt/test_costs.py +++ b/tests/flex_opt/test_costs.py @@ -78,7 +78,7 @@ def test_costs(self): costs = grid_expansion_costs(self.edisgo) - assert len(costs) == 5 + assert len(costs) == 4 assert ( costs.loc["MVStation_1_transformer_reinforced_2", "voltage_level"] == "mv/lv" @@ -91,11 +91,6 @@ def test_costs(self): ) assert costs.loc["LVStation_1_transformer_reinforced_1", "quantity"] == 1 assert costs.loc["LVStation_1_transformer_reinforced_1", "total_costs"] == 10 - assert np.isclose(costs.loc["Line_10006", "total_costs"], 29.765) - assert np.isclose(costs.loc["Line_10006", "length"], (0.29765 * 2)) - assert costs.loc["Line_10006", "quantity"] == 2 - assert costs.loc["Line_10006", "type"] == "NA2XS2Y 3x1x185 RM/25" - assert costs.loc["Line_10006", "voltage_level"] == "mv" assert np.isclose(costs.loc["Line_10019", "total_costs"], 32.3082) assert np.isclose(costs.loc["Line_10019", "length"], 0.40385) assert costs.loc["Line_10019", "quantity"] == 1 diff --git a/tests/flex_opt/test_reinforce_grid.py b/tests/flex_opt/test_reinforce_grid.py index 461655ecf..0f6c79c70 100644 --- a/tests/flex_opt/test_reinforce_grid.py +++ b/tests/flex_opt/test_reinforce_grid.py @@ -30,8 +30,10 @@ def test_reinforce_grid(self): target = ["mv"] elif mode == "mvlv": target = ["mv", "mv/lv"] - else: + elif mode == "lv": target = ["mv/lv", "lv"] + else: + raise ValueError("Non existing mode") assert_array_equal( np.sort(target), diff --git a/tests/flex_opt/test_reinforce_measures.py b/tests/flex_opt/test_reinforce_measures.py index c7f14bf24..dc3e93dd9 100644 --- a/tests/flex_opt/test_reinforce_measures.py +++ b/tests/flex_opt/test_reinforce_measures.py @@ -1,3 +1,5 @@ +import copy + import numpy as np import pandas as pd import pytest @@ -13,6 +15,7 @@ def setup_class(cls): cls.edisgo.set_time_series_worst_case_analysis() cls.edisgo.analyze() + cls.edisgo_root = copy.deepcopy(cls.edisgo) cls.timesteps = pd.date_range("1/1/1970", periods=2, freq="H") def test_reinforce_mv_lv_station_overloading(self): @@ -21,6 +24,8 @@ def test_reinforce_mv_lv_station_overloading(self): # create problems such that in LVGrid_1 existing transformer is # exchanged with standard transformer and in LVGrid_4 a third # transformer is added + self.edisgo = copy.deepcopy(self.edisgo_root) + crit_lv_stations = pd.DataFrame( { "s_missing": [0.17, 0.04], @@ -85,6 +90,8 @@ def test_reinforce_hv_mv_station_overloading(self): # implicitly checks function _station_overloading # check adding transformer of same MVA + self.edisgo = copy.deepcopy(self.edisgo_root) + crit_mv_station = pd.DataFrame( {"s_missing": [19], "time_index": [self.timesteps[1]]}, index=["MVGrid_1"], @@ -139,6 +146,8 @@ def test_reinforce_hv_mv_station_overloading(self): ) def test_reinforce_mv_lv_station_voltage_issues(self): + self.edisgo = copy.deepcopy(self.edisgo_root) + station_9 = pd.DataFrame( {"v_diff_max": [0.03], "time_index": [self.timesteps[0]]}, index=["Bus_secondary_LVStation_9"], @@ -187,6 +196,7 @@ def test_reinforce_lines_voltage_issues(self): # * check problem in same feeder => Bus_BranchTee_MVGrid_1_10 (node # has higher voltage issue than Bus_BranchTee_MVGrid_1_11, but # Bus_BranchTee_MVGrid_1_10 is farther away from station) + self.edisgo = copy.deepcopy(self.edisgo_root) crit_nodes = pd.DataFrame( { @@ -244,7 +254,7 @@ def test_reinforce_lines_voltage_issues(self): ) # check line parameters std_line_mv = self.edisgo.topology.equipment_data["mv_cables"].loc[ - self.edisgo.config["grid_expansion_standard_equipment"]["mv_line"] + self.edisgo.config["grid_expansion_standard_equipment"]["mv_line_20kv"] ] line = self.edisgo.topology.lines_df.loc["Line_10028"] assert line.type_info == std_line_mv.name @@ -353,6 +363,7 @@ def test_reinforce_lines_overloading(self): # and Line_50000002 # * check for replacement by parallel standard lines (MV and LV) => # problems at Line_10003 and Line_60000001 + self.edisgo = copy.deepcopy(self.edisgo_root) # create crit_lines dataframe crit_lines = pd.DataFrame( @@ -385,10 +396,10 @@ def test_reinforce_lines_overloading(self): # check lines that were already standard lines and where parallel # standard lines were added line = self.edisgo.topology.lines_df.loc["Line_10007"] - assert line.type_info == "NA2XS2Y 3x1x185 RM/25" - assert np.isclose(line.r, 0.164 * line.length / 3) - assert np.isclose(line.x, 0.38 * 2 * np.pi * 50 / 1e3 * line.length / 3) - assert np.isclose(line.s_nom, 0.357 * 20 * np.sqrt(3) * 3) + assert line.type_info == "NA2XS2Y 3x1x240" + assert np.isclose(line.r, 0.13 * line.length / 3) + assert np.isclose(line.x, 0.3597 * 2 * np.pi * 50 / 1e3 * line.length / 3) + assert np.isclose(line.s_nom, 7.27461339178928 * 3) assert line.num_parallel == 3 line = self.edisgo.topology.lines_df.loc["Line_70000006"] @@ -415,10 +426,10 @@ def test_reinforce_lines_overloading(self): # check lines that were exchanged by parallel standard lines line = self.edisgo.topology.lines_df.loc["Line_10003"] - assert line.type_info == "NA2XS2Y 3x1x185 RM/25" - assert np.isclose(line.r, 0.164 * line.length / 2) - assert np.isclose(line.x, 0.38 * 2 * np.pi * 50 / 1e3 * line.length / 2) - assert np.isclose(line.s_nom, 0.357 * 20 * np.sqrt(3) * 2) + assert line.type_info == "NA2XS2Y 3x1x240" + assert np.isclose(line.r, 0.13 * line.length / 2) + assert np.isclose(line.x, 0.3597 * 2 * np.pi * 50 / 1e3 * line.length / 2) + assert np.isclose(line.s_nom, 0.417 * 20 * np.sqrt(3) * 2) assert line.num_parallel == 2 line = self.edisgo.topology.lines_df.loc["Line_60000001"] diff --git a/tests/io/test_electromobility_import.py b/tests/io/test_electromobility_import.py index 6026a898f..e8e564901 100644 --- a/tests/io/test_electromobility_import.py +++ b/tests/io/test_electromobility_import.py @@ -4,7 +4,6 @@ import pytest from edisgo.edisgo import EDisGo -from edisgo.flex_opt.charging_strategies import charging_strategy from edisgo.io.electromobility_import import ( distribute_charging_demand, import_electromobility, @@ -81,7 +80,6 @@ def test_distribute_charging_demand(self): self.edisgo_obj = EDisGo(ding0_grid=self.ding0_path) timeindex = pd.date_range("1/1/2011", periods=24 * 7, freq="H") self.edisgo_obj.set_timeindex(timeindex) - self.edisgo_obj.resample_timeseries() import_electromobility(self.edisgo_obj, self.simbev_path, self.tracbev_path) @@ -107,7 +105,6 @@ def test_distribute_charging_demand(self): self.edisgo_obj = EDisGo(ding0_grid=self.ding0_path) timeindex = pd.date_range("1/1/2011", periods=24 * 7, freq="H") self.edisgo_obj.set_timeindex(timeindex) - self.edisgo_obj.resample_timeseries() import_electromobility(self.edisgo_obj, self.simbev_path, self.tracbev_path) @@ -166,29 +163,3 @@ def test_integrate_charging_parks(self): edisgo_ids_topology = sorted(topology.charging_points_df.index.tolist()) assert edisgo_ids_cp == edisgo_ids_topology - - def test_charging_strategy(self): - charging_demand_lst = [] - - for strategy in self.charging_strategies: - charging_strategy(self.edisgo_obj, strategy=strategy) - - ts = self.edisgo_obj.timeseries - - # Check if all charging points have a valid chargingdemand_kWh > 0 - df = ts.charging_points_active_power(self.edisgo_obj).loc[ - :, (ts.charging_points_active_power(self.edisgo_obj) <= 0).any(axis=0) - ] - - assert df.shape == ts.charging_points_active_power(self.edisgo_obj).shape - - charging_demand_lst.append( - ts.charging_points_active_power(self.edisgo_obj).sum() - ) - - # the chargingdemand_kWh per charging point and therefore in total should - # always be the same - assert all( - (_.round(4) == charging_demand_lst[0].round(4)).all() - for _ in charging_demand_lst - ) diff --git a/tests/io/test_generators_import.py b/tests/io/test_generators_import.py index d5f98a703..90da01f51 100644 --- a/tests/io/test_generators_import.py +++ b/tests/io/test_generators_import.py @@ -337,71 +337,6 @@ def test_oedb_with_worst_case_timeseries(self): # edisgo.timeseries.generators_reactive_power.loc[ # :, new_solar_gen.name] / new_solar_gen.p_nom).all() - # ToDo following test currently fails because gens_before doesn't contain wind - # generators - # - # check wind generator - # old_wind_gen = gens_before[gens_before.type == "wind"].iloc[0, :] - # new_wind_gen = gens_new[ - # (gens_new.type == "wind") - # & (gens_new.weather_cell_id == old_wind_gen.weather_cell_id) - # & (gens_new.p_nom == 0.001) - # ].iloc[0, :] - # # check if time series of old gen is the same as before - # assert np.isclose( - # gens_ts_active_before.loc[:, old_wind_gen.name].tolist(), - # edisgo.timeseries.generators_active_power.loc[ - # :, old_wind_gen.name - # ].tolist(), - # ).all() - # assert np.isclose( - # gens_ts_reactive_before.loc[:, old_wind_gen.name].tolist(), - # edisgo.timeseries.generators_reactive_power.loc[ - # :, old_wind_gen.name - # ].tolist(), - # ).all() - # # check if normalized time series of new gen is the same as normalized - # # time series of old gen - # assert np.isclose( - # ( - # gens_ts_active_before.loc[:, old_wind_gen.name] / old_wind_gen.p_nom - # ).tolist(), - # ( - # edisgo.timeseries.generators_active_power.loc[:, new_wind_gen.name] - # / new_wind_gen.p_nom - # ).tolist(), - # ).all() - # assert np.isclose( - # ( - # gens_ts_reactive_before.loc[:, old_wind_gen.name] / old_wind_gen.p_nom - # ).tolist(), - # ( - # edisgo.timeseries.generators_reactive_power.loc[:, new_wind_gen.name] - # / new_wind_gen.p_nom - # ).tolist(), - # ).all() - # ToDo following test currently fails because gens_new doesn't contain gas - # generators - # # check other generator - # new_gen = gens_new[gens_new.type == "gas"].iloc[0, :] - # - # # check if normalized time series of new gen is the same as normalized - # # time series of old gen - # assert np.isclose( - # ( - # edisgo.timeseries.generators_active_power.loc[:, new_gen.name] - # / new_gen.p_nom - # ).tolist(), - # [0.0, 0.0, 1.0, 1.0], - # ).all() - # assert np.isclose( - # ( - # edisgo.timeseries.generators_reactive_power.loc[:, new_gen.name] - # / new_gen.p_nom - # ).tolist(), - # [0.0, 0.0, -np.tan(np.arccos(0.95)), -np.tan(np.arccos(0.95))], - # ).all() - @pytest.mark.slow def test_oedb_with_timeseries_by_technology(self): diff --git a/tests/network/test_electromobility.py b/tests/network/test_electromobility.py new file mode 100644 index 000000000..04b5593b5 --- /dev/null +++ b/tests/network/test_electromobility.py @@ -0,0 +1,89 @@ +import os +import shutil + +import geopandas as gpd +import pandas as pd +import pytest + +from edisgo.edisgo import EDisGo +from edisgo.io.electromobility_import import ( + import_electromobility, + integrate_charging_parks, +) +from edisgo.network.electromobility import Electromobility + + +class TestElectromobility: + @classmethod + def setup_class(self): + self.edisgo_obj = EDisGo(ding0_grid=pytest.ding0_test_network_4_path) + self.simbev_path = pytest.simbev_example_scenario_path_1 + self.tracbev_path = pytest.tracbev_example_scenario_path_1 + import_electromobility(self.edisgo_obj, self.simbev_path, self.tracbev_path) + integrate_charging_parks(self.edisgo_obj) + + def test_charging_processes_df(self): + charging_processes_df = self.edisgo_obj.electromobility.charging_processes_df + assert len(charging_processes_df) == 45 + assert isinstance(charging_processes_df, pd.DataFrame) + + def test_potential_charging_parks_gdf(self): + potential_charging_parks_gdf = ( + self.edisgo_obj.electromobility.potential_charging_parks_gdf + ) + assert len(potential_charging_parks_gdf) == 452 + assert isinstance(potential_charging_parks_gdf, gpd.GeoDataFrame) + + def test_simbev_config_df(self): + simbev_config_df = self.edisgo_obj.electromobility.simbev_config_df + assert len(simbev_config_df) == 1 + assert isinstance(simbev_config_df, pd.DataFrame) + + def test_integrated_charging_parks_df(self): + integrated_charging_parks_df = ( + self.edisgo_obj.electromobility.integrated_charging_parks_df + ) + assert integrated_charging_parks_df.empty + assert isinstance(integrated_charging_parks_df, pd.DataFrame) + + def test_stepsize(self): + stepsize = self.edisgo_obj.electromobility.stepsize + assert stepsize == 15 + + def test_simulated_days(self): + simulated_days = self.edisgo_obj.electromobility.simulated_days + assert simulated_days == 7 + + def test_eta_charging_points(self): + eta_charging_points = self.edisgo_obj.electromobility.eta_charging_points + assert eta_charging_points == 0.9 + + def test_to_csv(self): + """Test for method to_csv.""" + dir = os.path.join(os.getcwd(), "electromobility") + self.edisgo_obj.electromobility.to_csv(dir) + + saved_files = os.listdir(dir) + assert len(saved_files) == 3 + assert "charging_processes.csv" in saved_files + + shutil.rmtree(dir) + + def test_from_csv(self): + """ + Test for method from_csv. + + """ + dir = os.path.join(os.getcwd(), "electromobility") + self.edisgo_obj.electromobility.to_csv(dir) + + # reset self.topology + self.edisgo_obj.electromobility = Electromobility() + + self.edisgo_obj.electromobility.from_csv(dir, self.edisgo_obj) + + assert len(self.edisgo_obj.electromobility.charging_processes_df) == 45 + assert len(self.edisgo_obj.electromobility.potential_charging_parks_gdf) == 452 + assert self.edisgo_obj.electromobility.integrated_charging_parks_df.empty + + shutil.rmtree(dir) diff --git a/tests/network/test_timeseries.py b/tests/network/test_timeseries.py index 6606086e1..9e276e1a6 100644 --- a/tests/network/test_timeseries.py +++ b/tests/network/test_timeseries.py @@ -1393,6 +1393,7 @@ def test_predefined_fluctuating_generators_by_technology(self): # time series for generators are set for those for which time series are # provided) self.edisgo.timeseries.reset() + self.edisgo.timeseries.timeindex = timeindex self.edisgo.timeseries.predefined_fluctuating_generators_by_technology( self.edisgo, gens_p ) @@ -2311,15 +2312,13 @@ def test_check_if_components_exist(self): assert "Load_residential_LVGrid_5_3" in component_names def test_resample_timeseries(self): - # add dummy time series - timeindex = pd.date_range("1/1/2011", periods=4, freq="H") - self.edisgo.set_timeindex(timeindex) - # add example data for active power - self.edisgo.set_time_series_active_power_predefined( - fluctuating_generators_ts="oedb" - ) + + self.edisgo.set_time_series_worst_case_analysis() + len_timeindex_orig = len(self.edisgo.timeseries.timeindex) mean_value_orig = self.edisgo.timeseries.generators_active_power.mean() + + # test up-sampling self.edisgo.timeseries.resample_timeseries() # check if resampled length of time index is 4 times original length of # timeindex @@ -2327,14 +2326,55 @@ def test_resample_timeseries(self): # check if mean value of resampled data is the same as mean value of original # data assert ( - self.edisgo.timeseries.generators_active_power.mean() == mean_value_orig - ).unique() - # Same tests for down-sampling + np.isclose( + self.edisgo.timeseries.generators_active_power.mean(), + mean_value_orig, + atol=1e-5, + ) + ).all() + + # same tests for down-sampling self.edisgo.timeseries.resample_timeseries(freq="2h") assert len(self.edisgo.timeseries.timeindex) == 0.5 * len_timeindex_orig assert ( - self.edisgo.timeseries.generators_active_power.mean() == mean_value_orig - ).unique() + np.isclose( + self.edisgo.timeseries.generators_active_power.mean(), + mean_value_orig, + atol=1e-5, + ) + ).all() + + # test bfill + self.edisgo.timeseries.resample_timeseries(method="bfill") + assert len(self.edisgo.timeseries.timeindex) == 4 * len_timeindex_orig + assert np.isclose( + self.edisgo.timeseries.generators_active_power.iloc[1:, :].loc[ + :, "GeneratorFluctuating_3" + ], + 2.26950, + atol=1e-5, + ).all() + + # test interpolate + self.edisgo.timeseries.reset() + self.edisgo.set_time_series_worst_case_analysis() + len_timeindex_orig = len(self.edisgo.timeseries.timeindex) + ts_orig = self.edisgo.timeseries.generators_active_power.loc[ + :, "GeneratorFluctuating_3" + ] + self.edisgo.timeseries.resample_timeseries(method="interpolate") + assert len(self.edisgo.timeseries.timeindex) == 4 * len_timeindex_orig + assert np.isclose( + self.edisgo.timeseries.generators_active_power.at[ + pd.Timestamp("1970-01-01 01:30:00"), "GeneratorFluctuating_3" + ], + ( + ts_orig.at[pd.Timestamp("1970-01-01 01:00:00")] + + ts_orig.at[pd.Timestamp("1970-01-01 02:00:00")] + ) + / 2, + atol=1e-5, + ) class TestTimeSeriesRaw: diff --git a/tests/network/test_topology.py b/tests/network/test_topology.py index 51431ac21..f3c161b71 100644 --- a/tests/network/test_topology.py +++ b/tests/network/test_topology.py @@ -419,7 +419,7 @@ def test_add_line(self): bus1 = "Bus_BranchTee_LVGrid_1_10" msg = ( "When line 'type_info' is provided when creating a new " - "line, x, r and s_nom are calculated and provided " + "line, x, r, b and s_nom are calculated and provided " "parameters are overwritten." ) with pytest.warns(UserWarning, match=msg): diff --git a/tests/test_edisgo.py b/tests/test_edisgo.py index ec479b15f..d53b70afa 100755 --- a/tests/test_edisgo.py +++ b/tests/test_edisgo.py @@ -14,8 +14,6 @@ from edisgo import EDisGo -# from edisgo.edisgo import import_edisgo_from_files - class TestEDisGo: @pytest.fixture(autouse=True) @@ -320,7 +318,7 @@ def test_to_graph(self): def test_generator_import(self): edisgo = EDisGo(ding0_grid=pytest.ding0_test_network_4_path) edisgo.import_generators("nep2035") - assert len(edisgo.topology.generators_df) == 524 # 1636 + assert len(edisgo.topology.generators_df) == 524 def test_analyze(self, caplog): self.setup_worst_case_time_series() @@ -921,10 +919,7 @@ def test_aggregate_components(self): def test_import_electromobility(self): self.edisgo = EDisGo(ding0_grid=pytest.ding0_test_network_4_path) - timeindex = pd.date_range("1/1/2011", periods=24 * 7, freq="H") - self.edisgo.set_timeindex(timeindex) - self.edisgo.resample_timeseries() # test with default parameters simbev_path = pytest.simbev_example_scenario_path tracbev_path = pytest.tracbev_example_scenario_path @@ -979,11 +974,6 @@ def test_import_electromobility(self): # test with kwargs self.edisgo = EDisGo(ding0_grid=pytest.ding0_test_network_4_path) - timeindex = pd.date_range("1/1/2011", periods=24 * 7, freq="H") - self.edisgo.set_timeindex(timeindex) - - self.edisgo.resample_timeseries() - self.edisgo.import_electromobility( simbev_path, tracbev_path, @@ -992,7 +982,7 @@ def test_import_electromobility(self): ) # Length of charging_processes_df, potential_charging_parks_gdf and - # integrated_charging_parks_df changed compared to test without kwagrs + # integrated_charging_parks_df changed compared to test without kwargs # TODO: needs to be checked if that is correct assert len(self.edisgo.electromobility.charging_processes_df) == 8845 assert len(self.edisgo.electromobility.potential_charging_parks_gdf) == 1634 @@ -1034,6 +1024,26 @@ def test_import_electromobility(self): ) # fmt: on + def test_apply_charging_strategy(self): + self.edisgo_obj = EDisGo(ding0_grid=pytest.ding0_test_network_4_path) + timeindex = pd.date_range("1/1/2011", periods=24 * 7, freq="H") + self.edisgo_obj.set_timeindex(timeindex) + + self.edisgo_obj.resample_timeseries() + # test with default parameters + simbev_path = pytest.simbev_example_scenario_path + tracbev_path = pytest.tracbev_example_scenario_path + self.edisgo_obj.import_electromobility(simbev_path, tracbev_path) + self.edisgo_obj.apply_charging_strategy() + + # Check if all charging points have a valid chargingdemand_kWh > 0 + cps = self.edisgo_obj.topology.loads_df[ + self.edisgo_obj.topology.loads_df.type == "charging_point" + ].index + ts = self.edisgo_obj.timeseries.loads_active_power.loc[:, cps] + df = ts.loc[:, (ts <= 0).any(axis=0)] + assert df.shape == ts.shape + def test_plot_mv_grid_topology(self): plt.ion() self.edisgo.plot_mv_grid_topology(technologies=True) diff --git a/tests/test_examples.py b/tests/test_examples.py index a71c7adcb..fa628f58e 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1,3 +1,4 @@ +import logging import os import shutil import subprocess @@ -65,6 +66,16 @@ def test_plot_example_ipynb(self): ) assert errors == [] + @pytest.mark.slow + def test_electromobility_example_ipynb(self): + examples_dir_path = os.path.join( + os.path.dirname(os.path.dirname(__file__)), "examples" + ) + nb, errors = self._notebook_run( + os.path.join(examples_dir_path, "electromobility_example.ipynb") + ) + assert errors == [] + # ToDo Uncomment once a smaller grid is used and execution does not take as long # @pytest.mark.slow # def test_edisgo_simple_example_ipynb(self): @@ -75,3 +86,8 @@ def test_plot_example_ipynb(self): # os.path.join(examples_dir_path, "edisgo_simple_example.ipynb") # ) # assert errors == [] + + @classmethod + def teardown_class(cls): + logger = logging.getLogger("edisgo") + logger.propagate = True diff --git a/tests/tools/test_logger.py b/tests/tools/test_logger.py new file mode 100644 index 000000000..f461c891b --- /dev/null +++ b/tests/tools/test_logger.py @@ -0,0 +1,59 @@ +import logging +import os + +from edisgo.tools.logger import setup_logger + + +class TestClass: + def test_setup_logger(self): + def check_file_output(output): + with open("edisgo.log", "r") as file: + last_line = file.readlines()[-1].split(" ")[3:] + last_line = " ".join(last_line) + assert last_line == output + + def reset_loggers(): + logger = logging.getLogger("edisgo") + logger.propagate = True + logger.handlers.clear() + logger = logging.getLogger() + logger.handlers.clear() + + if os.path.exists("edisgo.log"): + os.remove("edisgo.log") + + setup_logger( + loggers=[ + {"name": "root", "file_level": "debug", "stream_level": "debug"}, + {"name": "edisgo", "file_level": "debug", "stream_level": "debug"}, + ], + file_name="edisgo.log", + ) + + logger = logging.getLogger("edisgo") + # Test that edisgo logger writes to file. + logger.debug("root") + check_file_output("edisgo - DEBUG: root\n") + # Test that root logger writes to file. + logging.debug("root") + check_file_output("root - DEBUG: root\n") + + # reset_loggers() + + setup_logger( + loggers=[ + {"name": "edisgo", "file_level": "debug", "stream_level": "debug"}, + ], + file_name="edisgo.log", + reset_loggers=True, + debug_message=True, + ) + logger = logging.getLogger("edisgo") + # Test that edisgo logger writes to file. + logger.debug("edisgo") + check_file_output("edisgo - DEBUG: edisgo\n") + # Test that root logger doesn't writes to file. + logging.debug("edisgo") + check_file_output("edisgo - DEBUG: edisgo\n") + + os.remove("edisgo.log") diff --git a/tests/tools/test_plots.py b/tests/tools/test_plots.py index 0e385d2e0..c8d855ce8 100644 --- a/tests/tools/test_plots.py +++ b/tests/tools/test_plots.py @@ -1,27 +1,120 @@ +import copy + import pytest from edisgo import EDisGo -from edisgo.tools.plots import dash_plot +from edisgo.tools.plots import chosen_graph, plot_dash_app, plot_plotly class TestPlots: @classmethod def setup_class(cls): - cls.edisgo = EDisGo(ding0_grid=pytest.ding0_test_network_path) - cls.edisgo.set_time_series_worst_case_analysis() - cls.edisgo.reinforce() + cls.edisgo_root = EDisGo(ding0_grid=pytest.ding0_test_network_path) + cls.edisgo_root.set_time_series_worst_case_analysis() + cls.edisgo_analyzed = copy.deepcopy(cls.edisgo_root) + cls.edisgo_reinforced = copy.deepcopy(cls.edisgo_root) + cls.edisgo_analyzed.analyze() + cls.edisgo_reinforced.reinforce() + cls.edisgo_reinforced.results.equipment_changes.loc[ + "Line_10006", "change" + ] = "added" + + @pytest.mark.parametrize( + "line_color, node_color, line_result_selection, node_result_selection" + ", center_coordinates, pseudo_coordinates", + [ + ("loading", "voltage_deviation", "min", "min", True, True), + ("relative_loading", "adjacencies", "max", "max", False, False), + ("reinforce", "adjacencies", "max", "min", True, False), + ], + ) + @pytest.mark.parametrize( + "selected_timesteps", + [ + None, + "1970-01-01 01:00:00", + ["1970-01-01 01:00:00", "1970-01-01 03:00:00"], + ], + ) + @pytest.mark.parametrize( + "node_selection", [False, ["Bus_MVStation_1", "Bus_BranchTee_MVGrid_1_5"]] + ) + @pytest.mark.parametrize( + "edisgo_obj_name", ["edisgo_root", "edisgo_analyzed", "edisgo_reinforced"] + ) + @pytest.mark.parametrize("grid_name", ["None", "LVGrid"]) + def test_plot_plotly( + self, + edisgo_obj_name, + grid_name, + line_color, + node_color, + line_result_selection, + node_result_selection, + selected_timesteps, + center_coordinates, + pseudo_coordinates, + node_selection, + ): + + if edisgo_obj_name == "edisgo_root": + edisgo_obj = self.edisgo_root + elif edisgo_obj_name == "edisgo_analyzed": + edisgo_obj = self.edisgo_analyzed + elif edisgo_obj_name == "edisgo_reinforced": + edisgo_obj = self.edisgo_reinforced + + if grid_name == "None": + grid = None + elif grid_name == "LVGrid": + grid = list(edisgo_obj.topology.mv_grid.lv_grids)[1] + + if (grid_name == "LVGrid") and (node_selection is not False): + with pytest.raises(ValueError): + plot_plotly( + edisgo_obj=edisgo_obj, + grid=grid, + line_color=line_color, + node_color=node_color, + line_result_selection=line_result_selection, + node_result_selection=node_result_selection, + selected_timesteps=selected_timesteps, + center_coordinates=center_coordinates, + pseudo_coordinates=pseudo_coordinates, + node_selection=node_selection, + ) + else: + plot_plotly( + edisgo_obj=edisgo_obj, + grid=grid, + line_color=line_color, + node_color=node_color, + line_result_selection=line_result_selection, + node_result_selection=node_result_selection, + selected_timesteps=selected_timesteps, + center_coordinates=center_coordinates, + pseudo_coordinates=pseudo_coordinates, + node_selection=node_selection, + ) + + def test_chosen_graph(self): + chosen_graph(edisgo_obj=self.edisgo_root, selected_grid="Grid") + grid = str(self.edisgo_root.topology.mv_grid) + chosen_graph(edisgo_obj=self.edisgo_root, selected_grid=grid) + grid = list(map(str, self.edisgo_root.topology.mv_grid.lv_grids))[0] + chosen_graph(edisgo_obj=self.edisgo_root, selected_grid=grid) - def test_dash_plot(self): + def test_plot_dash_app(self): # TODO: at the moment this doesn't really test anything. Add meaningful tests. # test if any errors occur when only passing one edisgo object - app = dash_plot( - edisgo_objects=self.edisgo, + plot_dash_app( + edisgo_objects=self.edisgo_root, ) # test if any errors occur when passing multiple edisgo objects - app = dash_plot( # noqa: F841 + plot_dash_app( # noqa: F841 edisgo_objects={ - "edisgo_1": self.edisgo, - "edisgo_2": self.edisgo, + "edisgo_1": self.edisgo_root, + "edisgo_2": self.edisgo_reinforced, } ) diff --git a/tests/tools/test_pseudo_coordinates.py b/tests/tools/test_pseudo_coordinates.py new file mode 100644 index 000000000..4ada42f05 --- /dev/null +++ b/tests/tools/test_pseudo_coordinates.py @@ -0,0 +1,34 @@ +import numpy as np +import pytest + +from edisgo import EDisGo +from edisgo.tools.pseudo_coordinates import make_pseudo_coordinates + + +class TestPseudoCoordinates: + @classmethod + def setup_class(cls): + cls.edisgo_root = EDisGo(ding0_grid=pytest.ding0_test_network_path) + + def test_make_pseudo_coordinates(self): + # make pseudo coordinates + edisgo_pseudo_coordinates = make_pseudo_coordinates( + edisgo_root=self.edisgo_root, mv_coordinates=True + ) + + # test that coordinates change for one node + coordinates = self.edisgo_root.topology.buses_df.loc[ + "Bus_BranchTee_LVGrid_1_9", ["x", "y"] + ] + assert round(coordinates[0], 5) != round(7.943307, 5) + assert round(coordinates[1], 5) != round(48.080396, 5) + + # test if the right coordinates are set for one node + coordinates = edisgo_pseudo_coordinates.topology.buses_df.loc[ + "Bus_BranchTee_LVGrid_1_9", ["x", "y"] + ] + assert round(coordinates[0], 5) == round(7.943307, 5) + assert round(coordinates[1], 5) == round(48.080396, 5) + + assert self.edisgo_root.topology.buses_df.x.isin([np.NaN]).any() + assert not edisgo_pseudo_coordinates.topology.buses_df.x.isin([np.NaN]).any() diff --git a/tests/tools/test_tools.py b/tests/tools/test_tools.py index 367d6bf8c..3012311fc 100644 --- a/tests/tools/test_tools.py +++ b/tests/tools/test_tools.py @@ -1,3 +1,5 @@ +import copy + from math import sqrt import numpy as np @@ -73,6 +75,14 @@ def test_calculate_line_resistance(self): data = tools.calculate_line_resistance(np.array([2, 3]), 3, 2) assert_array_equal(data, np.array([3, 4.5])) + def test_calculate_line_susceptance(self): + # test single line + assert np.isclose(tools.calculate_line_susceptance(2, 3, 1), 0.00188495559) + # test parallel line + assert np.isclose(tools.calculate_line_susceptance(2, 3, 2), 2 * 0.00188495559) + # test line with c = 0 + assert np.isclose(tools.calculate_line_susceptance(0, 3, 1), 0) + def test_calculate_apparent_power(self): # test single line data = tools.calculate_apparent_power(20, 30, 1) @@ -238,3 +248,35 @@ def test_get_weather_cells_intersecting_with_grid_district(self): # but there are generators in the grid that have that weather cell # for some reason.. assert 1122074 in weather_cells + + def test_add_susceptance(self): + assert self.edisgo.topology.lines_df.loc["Line_10006", "b"] == 0 + assert self.edisgo.topology.lines_df.loc["Line_50000002", "b"] == 0 + + # test mode no_b + edisgo_root = copy.deepcopy(self.edisgo) + edisgo_root.topology.lines_df.loc["Line_10006", "b"] = 1 + edisgo_root.topology.lines_df.loc["Line_50000002", "b"] = 1 + edisgo_root = tools.add_line_susceptance(edisgo_root, mode="no_b") + assert edisgo_root.topology.lines_df.loc["Line_10006", "b"] == 0 + assert edisgo_root.topology.lines_df.loc["Line_50000002", "b"] == 0 + + # test mode mv_b + edisgo_root = copy.deepcopy(self.edisgo) + edisgo_root.topology.lines_df.loc["Line_10006", "b"] = 1 + edisgo_root.topology.lines_df.loc["Line_50000002", "b"] = 1 + edisgo_root = tools.add_line_susceptance(edisgo_root, mode="mv_b") + assert edisgo_root.topology.lines_df.loc[ + "Line_10006", "b" + ] == tools.calculate_line_susceptance(0.304, 0.297650465459542, 1) + assert edisgo_root.topology.lines_df.loc["Line_50000002", "b"] == 0 + + # test mode all_b + edisgo_root = copy.deepcopy(self.edisgo) + edisgo_root = tools.add_line_susceptance(edisgo_root, mode="all_b") + assert edisgo_root.topology.lines_df.loc[ + "Line_10006", "b" + ] == tools.calculate_line_susceptance(0.304, 0.297650465459542, 1) + assert edisgo_root.topology.lines_df.loc[ + "Line_50000002", "b" + ] == tools.calculate_line_susceptance(0.25, 0.03, 1)