Source code for edisgo.io.pypsa_io

"""
This module provides tools to convert graph based representation of the network
topology to PyPSA data model. Call :func:`to_pypsa` to retrieve the PyPSA network
container.
"""

import numpy as np
import pandas as pd
from math import sqrt
from pypsa import Network as PyPSANetwork
from pypsa.io import import_series_from_dataframe
from networkx import connected_components
import collections


[docs]def to_pypsa(grid_object, timesteps, **kwargs): """ Export edisgo object to PyPSA Network For details from a user perspective see API documentation of :meth:`~edisgo.EDisGo.analyze` of the API class :class:`~.edisgo.EDisGo`. Translating eDisGo's network topology to PyPSA representation is structured into translating the topology and adding time series for components of the network. In both cases translation of MV network only (`mode='mv'`, `mode='mvlv'`), LV network only(`mode='lv'`), MV and LV (`mode=None`) share some code. The code is organized as follows: * Medium-voltage only (`mode='mv'`): All medium-voltage network components are exported including the medium voltage side of LV station. Transformers are not exported in this mode. LV network load and generation is considered using :func:`append_lv_components`. Time series are collected and imported to PyPSA network. * Medium-voltage including transformers (`mode='mvlv'`). Works similar as the first mode, only attaching LV components to the LV side of the LVStation and therefore also adding the transformers to the PyPSA network. * Low-voltage only (`mode='lv'`): LV network topology including the MV-LV transformer is exported. The slack is defind at primary side of the MV-LV transformer. * Both level MV+LV (`mode=None`): The entire network topology is translated to PyPSA in order to perform a complete power flow analysis in both levels together. First, both network levels are translated seperately and then merged. Time series are obtained at once for both network levels. This PyPSA interface is aware of translation errors and performs so checks on integrity of data converted to PyPSA network representation * Sub-graphs/ Sub-networks: It is ensured the network has no islanded parts * Completeness of time series: It is ensured each component has a time series * Buses available: Each component (load, generator, line, transformer) is connected to a bus. The PyPSA representation is check for completeness of buses. * Duplicate labels in components DataFrames and components' time series DataFrames Parameters ---------- grid_object: :class:`~.EDisGo` or :class:`~.network.grids.Grid` EDisGo or grid object mode : str Determines network levels that are translated to `PyPSA network representation <https://www.pypsa.org/doc/components.html#network>`_. Specify * None to export MV and LV network levels. None is the default. * 'mv' to export MV network level only. This includes cumulative load and generation from underlying LV network aggregated at respective LV station's primary side. * 'mvlv' to export MV network level only. This includes cumulative load and generation from underlying LV network aggregated at respective LV station's secondary side. #ToDo change name of this mode or use kwarg to define where to aggregate lv loads and generation * 'lv' to export specified LV network only. timesteps : :pandas:`pandas.DatetimeIndex<DatetimeIndex>` or \ :pandas:`pandas.Timestamp<Timestamp>` Timesteps specifies which time steps to export to pypsa representation and use in power flow analysis. Other Parameters ----------------- use_seed : bool Use a seed for the initial guess for the Newton-Raphson algorithm. Only available when MV level is included in the power flow analysis. If True, uses voltage magnitude results of previous power flow analyses as initial guess in case of PQ buses. PV buses currently do not occur and are therefore currently not supported. Default: False. Returns ------- :pypsa:`pypsa.Network<network>` The `PyPSA network <https://www.pypsa.org/doc/components.html#network>`_ container. """ def _set_slack(grid): """ Sets slack at given grid's station secondary side. It is assumed that bus of secondary side is always given in transformer's bus1. Parameters ----------- grid : :class:`~.network.grids.Grid` Low or medium voltage grid to position slack in. Returns ------- """ slack_bus = grid.transformers_df.bus1.iloc[0] return pd.DataFrame( data={"bus": slack_bus, "control": "Slack"}, index=["Generator_slack"], ) mode = kwargs.get("mode", None) aggregate_loads = kwargs.get("aggregate_loads", None) aggregate_generators = kwargs.get("aggregate_generators", None) aggregate_storages = kwargs.get("aggregate_storages", None) aggregated_lv_components = {"Generator": {}, "Load": {}, "StorageUnit": {}} # check if timesteps is array-like, otherwise convert to list (necessary # to obtain a dataframe when using .loc in time series functions) if not hasattr(timesteps, "__len__"): timesteps = [timesteps] # create power flow problem pypsa_network = PyPSANetwork() pypsa_network.set_snapshots(timesteps) # define edisgo_obj, buses_df, slack_df and components for each use case if mode is None: pypsa_network.mode = "mv" edisgo_obj = grid_object buses_df = grid_object.topology.buses_df.loc[:, ["v_nom"]] slack_df = _set_slack(edisgo_obj.topology.mv_grid) components = { "Load": grid_object.topology.loads_df.loc[ :, ["bus", "peak_load"] ].rename(columns={"peak_load": "p_set"}).append( grid_object.topology.charging_points_df.loc[ :, ['bus', 'p_nom']].rename( columns={'p_nom': 'p_set'})), "Generator": grid_object.topology.generators_df.loc[ :, ["bus", "control", "p_nom"] ], "StorageUnit": grid_object.topology.storage_units_df.loc[ :, ["bus", "control"] ], "Line": grid_object.topology.lines_df.loc[ :, ["bus0", "bus1", "x", "r", "s_nom", "num_parallel", "length"], ], "Transformer": grid_object.topology.transformers_df.loc[ :, ["bus0", "bus1", "x_pu", "r_pu", "type_info", "s_nom"] ].rename(columns={"r_pu": "r", "x_pu": "x"}), } elif "mv" in mode: pypsa_network.mode = "mv" edisgo_obj = grid_object.edisgo_obj buses_df = grid_object.buses_df.loc[:, ["v_nom"]] slack_df = _set_slack(grid_object) # MV components mv_components = _get_grid_component_dict(grid_object) mv_components["Generator"][ "fluctuating" ] = grid_object.generators_df.type.isin(["solar", "wind"]) if mode is "mv": mv_components["Transformer"] = pd.DataFrame() elif mode is "mvlv": # get all MV/LV transformers mv_components[ "Transformer" ] = edisgo_obj.topology.transformers_df.loc[ :, ["bus0", "bus1", "x_pu", "r_pu", "type_info", "s_nom"] ].rename( columns={"r_pu": "r", "x_pu": "x"} ) else: raise ValueError("Provide proper mode for mv network export.") # LV components lv_components_to_aggregate = { "Load": ["loads_df", "charging_points_df"], "Generator": ["generators_df"], "StorageUnit": ["storage_units_df"], } lv_components = { key: pd.DataFrame() for key in lv_components_to_aggregate } for lv_grid in grid_object.lv_grids: if mode is "mv": # get primary side of station to append loads and generators to station_bus = grid_object.buses_df.loc[ lv_grid.transformers_df.bus0.unique() ] elif mode is "mvlv": # get secondary side of station to append loads and generators # to station_bus = lv_grid.buses_df.loc[ [lv_grid.transformers_df.bus1.unique()[0]] ] buses_df = buses_df.append(station_bus.loc[:, ["v_nom"]]) # handle one gate components for comp, dfs in lv_components_to_aggregate.items(): comps = pd.DataFrame() for df in dfs: comps_tmp=getattr(lv_grid, df).copy() if df == "charging_points_df": comps_tmp['sector'] = 'EV_charging' comps_tmp=comps_tmp.rename(columns={'p_nom': 'peak_load'}) comps = comps.append(comps_tmp) comps.bus = station_bus.index.values[0] aggregated_lv_components[comp].update( _append_lv_components( comp, comps, lv_components, repr(lv_grid), aggregate_loads=aggregate_loads, aggregate_generators=aggregate_generators, aggregate_storages=aggregate_storages, ) ) # merge components components = collections.defaultdict(pd.DataFrame) for comps in (mv_components, lv_components): for key, value in comps.items(): components[key] = components[key].append(value) elif mode is "lv": pypsa_network.mode = "lv" edisgo_obj = grid_object.edisgo_obj buses_df = grid_object.buses_df.loc[:, ["v_nom"]] slack_df = _set_slack(grid_object) components = _get_grid_component_dict(grid_object) else: raise ValueError( "Provide proper mode or leave it empty to export " "entire network topology." ) # import network topology to PyPSA network # buses are created first to avoid warnings pypsa_network.import_components_from_dataframe(buses_df, "Bus") pypsa_network.import_components_from_dataframe(slack_df, "Generator") for k, comps in components.items(): pypsa_network.import_components_from_dataframe(comps, k) # import time series to PyPSA network # set all voltages except for slack import_series_from_dataframe( pypsa_network, _buses_voltage_set_point( edisgo_obj, buses_df.index, slack_df.loc["Generator_slack", "bus"], timesteps, ), "Bus", "v_mag_pu_set", ) # set slack time series slack_ts = pd.DataFrame( data=[0] * len(timesteps), columns=[slack_df.index[0]], index=timesteps, ) import_series_from_dataframe( pypsa_network, slack_ts, "Generator", "p_set" ) import_series_from_dataframe( pypsa_network, slack_ts, "Generator", "q_set" ) # set generator time series in pypsa if len(components["Generator"].index) > 0: if len(aggregated_lv_components["Generator"]) > 0: ( generators_timeseries_active, generators_timeseries_reactive, ) = _get_timeseries_with_aggregated_elements( edisgo_obj, timesteps, ["generators"], components["Generator"].index, aggregated_lv_components["Generator"], ) else: generators_timeseries_active = edisgo_obj.timeseries.generators_active_power.loc[ timesteps, components["Generator"].index ] generators_timeseries_reactive = edisgo_obj.timeseries.generators_reactive_power.loc[ timesteps, components["Generator"].index ] import_series_from_dataframe( pypsa_network, generators_timeseries_active, "Generator", "p_set" ) import_series_from_dataframe( pypsa_network, generators_timeseries_reactive, "Generator", "q_set" ) if len(components["Load"].index) > 0: if len(aggregated_lv_components["Load"]) > 0: ( loads_timeseries_active, loads_timeseries_reactive, ) = _get_timeseries_with_aggregated_elements( edisgo_obj, timesteps, ["loads", "charging_points"], components["Load"].index, aggregated_lv_components["Load"], ) else: loads_timeseries_active = pd.concat( [edisgo_obj.timeseries.loads_active_power, edisgo_obj.timeseries.charging_points_active_power ], axis=1).loc[timesteps, components["Load"].index] loads_timeseries_reactive = pd.concat( [edisgo_obj.timeseries.loads_reactive_power, edisgo_obj.timeseries.charging_points_reactive_power ], axis=1).loc[timesteps, components["Load"].index] import_series_from_dataframe( pypsa_network, loads_timeseries_active, "Load", "p_set" ) import_series_from_dataframe( pypsa_network, loads_timeseries_reactive, "Load", "q_set" ) if len(components["StorageUnit"].index) > 0: if len(aggregated_lv_components["StorageUnit"]) > 0: ( storages_timeseries_active, storages_timeseries_reactive, ) = _get_timeseries_with_aggregated_elements( edisgo_obj, timesteps, ["storage_units"], components["StorageUnit"].index, aggregated_lv_components["StorageUnit"], ) else: storages_timeseries_active = edisgo_obj.timeseries.storage_units_active_power.loc[ timesteps, components["StorageUnit"].index ] storages_timeseries_reactive = edisgo_obj.timeseries.storage_units_reactive_power.loc[ timesteps, components["StorageUnit"].index ] import_series_from_dataframe( pypsa_network, storages_timeseries_active.apply(pd.to_numeric), "StorageUnit", "p_set", ) import_series_from_dataframe( pypsa_network, storages_timeseries_reactive.apply(pd.to_numeric), "StorageUnit", "q_set", ) if kwargs.get("use_seed", False) and pypsa_network.mode == "mv": set_seed(edisgo_obj, pypsa_network) _check_integrity_of_pypsa(pypsa_network) return pypsa_network
[docs]def set_seed(edisgo_obj, pypsa_network): """ Set initial guess for the Newton-Raphson algorithm. In `PyPSA <https://pypsa.readthedocs.io/en/latest/index.html/>`_ an initial guess for the Newton-Raphson algorithm used in the power flow analysis can be provided to speed up calculations. For PQ buses, which besides the slack bus, is the only bus type in edisgo, voltage magnitude and angle need to be guessed. If the power flow was already conducted for the required time steps and buses, the voltage magnitude and angle results from previously conducted power flows stored in :attr:`~.network.results.Results.pfa_v_mag_pu_seed` and :attr:`~.network.results.Results.pfa_v_ang_seed` are used as the initial guess. Always the latest power flow calculation is used and only results from power flow analyses including the MV level are considered, as analysing single LV grids is currently not in the focus of edisgo and does not require as much speeding up, as analysing single LV grids is usually already quite quick. If for some buses or time steps no power flow results are available, default values are used. For the voltage magnitude the default value is 1 and for the voltage angle 0. Parameters ---------- edisgo_obj : :class:`~.EDisGo` pypsa_network : :pypsa:`pypsa.Network<network>` Pypsa network in which seed is set. """ # get all PQ buses for which seed needs to be set pq_buses = pypsa_network.buses[pypsa_network.buses.control == "PQ"].index # get voltage magnitude and angle results from previous power flow analyses pfa_v_mag_pu_seed = edisgo_obj.results.pfa_v_mag_pu_seed pfa_v_ang_seed = edisgo_obj.results.pfa_v_ang_seed # get busses seed cannot be set for from previous power flow analyses # and add default values for those buses_missing = [_ for _ in pq_buses if _ not in pfa_v_mag_pu_seed.columns] if len(buses_missing) > 0: pfa_v_mag_pu_seed = pd.concat( [pfa_v_mag_pu_seed, pd.DataFrame( data=1., columns=buses_missing, index=pfa_v_ang_seed.index )], axis=1 ) pfa_v_ang_seed = pd.concat( [pfa_v_ang_seed, pd.DataFrame( data=0., columns=buses_missing, index=pfa_v_ang_seed.index )], axis=1 ) # select only PQ buses pfa_v_mag_pu_seed = pfa_v_mag_pu_seed.loc[:, pq_buses] pfa_v_ang_seed = pfa_v_ang_seed.loc[:, pq_buses] # get time steps seed cannot be set for from previous power flow analyses # and add default values for those ts_missing = [_ for _ in pypsa_network.snapshots if _ not in pfa_v_mag_pu_seed.index] if len(ts_missing) > 0: pfa_v_mag_pu_seed = pd.concat( [pfa_v_mag_pu_seed, pd.DataFrame( data=1., columns=pq_buses, index=ts_missing )], axis=0 ) pfa_v_ang_seed = pd.concat( [pfa_v_ang_seed, pd.DataFrame( data=0., columns=pq_buses, index=ts_missing )], axis=0 ) # select only snapshots pfa_v_mag_pu_seed = pfa_v_mag_pu_seed.loc[pypsa_network.snapshots, :] pfa_v_ang_seed = pfa_v_ang_seed.loc[pypsa_network.snapshots, :] pypsa_network.buses_t.v_mag_pu = pfa_v_mag_pu_seed pypsa_network.buses_t.v_ang = pfa_v_ang_seed
def _get_grid_component_dict(grid_object): """ Method to extract component dictionary from given grid object. Components are devided into "Load", "Generator", "StorageUnit" and "Line". Used for translation into pypsa network. Parameters ---------- grid_object: MV or LV grid object Returns ------- dict Component dictionary divided into "Load", "Generator", "StorageUnit" and "Line" """ components = { "Load": grid_object.loads_df.loc[:, ["bus", "peak_load"]].rename( columns={"peak_load": "p_set"}).append( grid_object.charging_points_df.loc[:, ['bus', 'p_nom']].rename( columns={'p_nom': 'p_set'}) ), "Generator": grid_object.generators_df.loc[ :, ["bus", "control", "p_nom"] ], "StorageUnit": grid_object.storage_units_df.loc[ :, ["bus", "control"] ], "Line": grid_object.lines_df.loc[ :, ["bus0", "bus1", "x", "r", "s_nom", "num_parallel", "length"], ], } return components def _append_lv_components( comp, comps, lv_components, lv_grid_name, aggregate_loads=None, aggregate_generators=None, aggregate_storages=None, ): """ Method to append lv components to component dictionary. Used when only exporting mv grid topology. All underlaying LV components of an LVGrid are then connected to one side of the LVStation. If required, the LV components can be aggregated in different modes. As an example, loads can be aggregated sector-wise or all loads can be aggregated into one representative load. The sum of p_nom or peak_load of all cumulated components is calculated. Parameters ---------- comp: str indicator for component type, can be 'Load', 'Generator' or 'StorageUnit' comps: `pandas.DataFrame<DataFrame>` component dataframe of elements to be aggregated lv_components: dict dictionary of LV grid components, keys are the 'Load', 'Generator' and 'StorageUnit' lv_grid_name: str representative of LV grid of which components are aggregated aggregate_loads: str mode for load aggregation, can be 'sectoral' aggregating the loads sector-wise or 'all' aggregating all loads into one. Defaults to None, not aggregating loads but appending them to the station one by one. aggregate_generators: str mode for generator aggregation, can be 'type' resulting in aggregated generator for each generator type, 'curtailable' aggregating 'solar' and 'wind' generators into one and all other generators into another generator. Defaults to None, when no aggregation is undertaken and generators are addded one by one. aggregate_storages: str mode for storage unit aggregation. Can be 'all' where all storage units in the grid are replaced by one storage. Defaults to None, where no aggregation is conducted and storage units are added one by one. Returns ------- dict dict of aggregated elements for timeseries creation. Keys are names of aggregated elements and entries is a list of the names of all components aggregated in that respective key component. An example could look the following way: {'LVGrid_1_loads': ['Load_agricultural_LVGrid_1_1', 'Load_retail_LVGrid_1_2']} """ aggregated_elements = {} if len(comps) > 0: bus = comps.bus.unique()[0] else: return {} if comp is "Load": if aggregate_loads is None: comps_aggr = comps.loc[:, ["bus", "peak_load"]].rename( columns={"peak_load": "p_set"} ) elif aggregate_loads == "sectoral": comps_aggr = ( comps.loc[:, ["peak_load", "sector"]].groupby("sector") .sum() .rename(columns={"peak_load": "p_set"}) .loc[:, ["p_set"]] ) for sector in comps_aggr.index.values: aggregated_elements[lv_grid_name + "_" + sector] = comps[ comps.sector == sector ].index.values comps_aggr.index = lv_grid_name + "_" + comps_aggr.index comps_aggr["bus"] = bus elif aggregate_loads == "all": comps_aggr = pd.DataFrame( {"bus": [bus], "p_set": [sum(comps.peak_load)]}, index=[lv_grid_name + "_loads"], ) aggregated_elements[lv_grid_name + "_loads"] = comps.index.values else: raise ValueError("Aggregation type for loads invalid.") lv_components[comp] = lv_components[comp].append(comps_aggr) elif comp is "Generator": flucts = ["wind", "solar"] if aggregate_generators is None: comps_aggr = comps.loc[:, ["bus", "control", "p_nom"]] comps_aggr["fluctuating"] = comps.type.isin(flucts) elif aggregate_generators == "type": comps_aggr = comps.groupby("type").sum().reindex(columns=["bus", "control", "p_nom"]) comps_aggr.bus = bus comps_aggr.control = "PQ" comps_aggr["fluctuating"] = comps_aggr.index.isin(flucts) for gen_type in comps_aggr.index.values: aggregated_elements[lv_grid_name + "_" + gen_type] = comps[ comps.type == gen_type ].index.values comps_aggr.index = lv_grid_name + "_" + comps_aggr.index elif aggregate_generators == "curtailable": comps_fluct = comps[comps.type.isin(flucts)] comps_disp = comps[~comps.index.isin(comps_fluct.index)] comps_aggr = pd.DataFrame(columns=["bus", "control", "p_nom"]) if len(comps_fluct) > 0: comps_aggr = comps_aggr.append( pd.DataFrame( { "bus": [bus], "control": ["PQ"], "p_nom": [sum(comps_fluct.p_nom)], "fluctuating": [True], }, index=[lv_grid_name + "_fluctuating"], ) ) aggregated_elements[ lv_grid_name + "_fluctuating" ] = comps_fluct.index.values if len(comps_disp) > 0: comps_aggr = comps_aggr.append( pd.DataFrame( { "bus": [bus], "control": ["PQ"], "p_nom": [sum(comps_disp.p_nom)], "fluctuating": [False], }, index=[lv_grid_name + "_dispatchable"], ) ) aggregated_elements[ lv_grid_name + "_dispatchable" ] = comps_disp.index.values elif aggregate_generators == "all": comps_aggr = pd.DataFrame( { "bus": [bus], "control": ["PQ"], "p_nom": [sum(comps.p_nom)], "fluctuating": [ True if (comps.type.isin(flucts)).all() else False if ~comps.type.isin(flucts).any() else "Mixed" ], }, index=[lv_grid_name + "_generators"], ) aggregated_elements[ lv_grid_name + "_generators" ] = comps.index.values else: raise ValueError("Aggregation type for generators invalid.") lv_components[comp] = lv_components[comp].append(comps_aggr) elif comp is "StorageUnit": if aggregate_storages == None: comps_aggr = comps.loc[:, ["bus", "control"]] elif aggregate_storages == "all": comps_aggr = pd.DataFrame( {"bus": [bus], "control": ["PQ"]}, index=[lv_grid_name + "_storages"], ) aggregated_elements[ lv_grid_name + "_storages" ] = comps.index.values else: raise ValueError("Aggregation type for storages invalid.") lv_components[comp] = lv_components[comp].append(comps_aggr) else: raise ValueError("Component type not defined.") return aggregated_elements def _get_timeseries_with_aggregated_elements( edisgo_obj, timesteps, element_types, elements, aggr_dict ): """ Creates timeseries for aggregated LV components by summing up the single timeseries and adding the respective entry to edisgo_obj.timeseries. Parameters ---------- edisgo_obj: :class:`~.self.edisgo.EDisGo` the eDisGo network container timesteps: timesteps of format :pandas:`pandas.Timestamp<Timestamp>` index timesteps for component's load or generation timeseries element_types: list of str type of element which was aggregated. Can be 'loads', 'generators' or 'storage_units' elements: `pandas.DataFrame<DataFrame>` component dataframe of all elements for which timeseries are added aggr_dict: dict dictionary containing aggregated elements as values and the representing new component as key. See :meth:`_append_lv_components` for structure of dictionary. Returns ------- tuple of `pandas.DataFrame<DataFrame>` active and reactive power timeseries for chosen elements. Dataframes with timesteps as index and name of elements as columns. """ # get relevant timeseries elements_timeseries_active_all = pd.DataFrame() elements_timeseries_reactive_all = pd.DataFrame() for element_type in element_types: elements_timeseries_active_all = pd.concat( [elements_timeseries_active_all, getattr(edisgo_obj.timeseries, element_type + "_active_power")], axis=1) elements_timeseries_reactive_all = pd.concat( [elements_timeseries_reactive_all, getattr(edisgo_obj.timeseries, element_type + "_reactive_power")], axis=1) # handle not aggregated elements non_aggregated_elements = elements[~elements.isin(aggr_dict.keys())] # get timeseries for non aggregated elements elements_timeseries_active = elements_timeseries_active_all.loc[ timesteps, non_aggregated_elements] elements_timeseries_reactive = elements_timeseries_reactive_all.loc[ timesteps, non_aggregated_elements] # append timeseries for aggregated elements for aggr_gen in aggr_dict.keys(): elements_timeseries_active[aggr_gen] = elements_timeseries_active_all.\ loc[timesteps, aggr_dict[aggr_gen]].sum(axis=1) elements_timeseries_reactive[aggr_gen] = \ elements_timeseries_reactive_all.loc[ timesteps, aggr_dict[aggr_gen]].sum(axis=1) return elements_timeseries_active, elements_timeseries_reactive def _buses_voltage_set_point(edisgo_obj, buses, slack_bus, timesteps): """ Time series in PyPSA compatible format for bus instances Set all buses except for the slack bus to voltage of 1 p.u. (it is assumed this setting is entirely ignored during solving the power flow problem). The slack bus voltage is set based on a given HV/MV transformer offset and a control deviation, both defined in the config files. The control deviation is added to the offset in the reverse power flow case and subtracted from the offset in the heavy load flow case. Parameters ---------- edisgo_obj: :class:`~.self.edisgo.EDisGo` The eDisGo model overall container timesteps : array_like Timesteps is an array-like object with entries of type :pandas:`pandas.Timestamp<Timestamp>` specifying which time steps to export to pypsa representation and use in power flow analysis. buses : list Buses names slack_bus : str Returns ------- :pandas:`pandas.DataFrame<DataFrame>` Time series table in PyPSA format """ # set all buses to nominal voltage v_nom = pd.DataFrame(1, columns=buses, index=timesteps) # set slack bus to operational voltage (includes offset and control # deviation) control_deviation = edisgo_obj.config[ "grid_expansion_allowed_voltage_deviations" ]["hv_mv_trafo_control_deviation"] if control_deviation != 0: control_deviation_ts = edisgo_obj.timeseries.timesteps_load_feedin_case.apply( lambda _: control_deviation if _ == "feedin_case" else -control_deviation ).loc[ timesteps ] else: control_deviation_ts = pd.Series(0, index=timesteps) slack_voltage_pu = ( control_deviation_ts + 1 + edisgo_obj.config["grid_expansion_allowed_voltage_deviations"][ "hv_mv_trafo_offset" ] ) v_nom.loc[timesteps, slack_bus] = slack_voltage_pu return v_nom def _check_integrity_of_pypsa(pypsa_network): """ Checks whether the provided pypsa network is calculable. Isolated nodes, duplicate labels, that every load, generator and storage unit has a time series for active and reactive power, and completeness of buses and branch elements are checked. Parameters ---------- pypsa_network: :pypsa:`pypsa.Network<network>` The `PyPSA network <https://www.pypsa.org/doc/components.html#network>`_ container. """ # check for sub-networks subgraphs = list( pypsa_network.graph().subgraph(c) for c in connected_components(pypsa_network.graph()) ) pypsa_network.determine_network_topology() if len(subgraphs) > 1 or len(pypsa_network.sub_networks) > 1: raise ValueError("The pypsa graph has isolated nodes or edges.") # check for duplicate labels of components comps_dfs = [pypsa_network.buses, pypsa_network.generators, pypsa_network.loads, pypsa_network.storage_units, pypsa_network.transformers, pypsa_network.lines] for comp_type in comps_dfs: if any(comp_type.index.duplicated()): raise ValueError( "Pypsa network has duplicated entries: {}.".format( comp_type.index.duplicated()) ) # check consistency of topology and time series data comp_df_dict = { # exclude Slack from check "gens": pypsa_network.generators[ pypsa_network.generators.control != "Slack"], "loads": pypsa_network.loads, "storage_units": pypsa_network.storage_units} comp_ts_dict = { "gens": pypsa_network.generators_t, "loads": pypsa_network.loads_t, "storage_units": pypsa_network.storage_units_t} for comp_type, ts in comp_ts_dict.items(): for i in ["p_set", "q_set"]: missing = comp_df_dict[comp_type].loc[ ~comp_df_dict[comp_type].index.isin( ts[i].dropna(axis=1).columns ) ] if not missing.empty: raise ValueError( "The following components have no `{}` time " "series.".format( missing.index, i) ) missing = pypsa_network.buses.loc[ ~pypsa_network.buses.index.isin( pypsa_network.buses_t["v_mag_pu_set"].columns.tolist() ) ] if not missing.empty: raise ValueError( "The following components have no `v_mag_pu_set` time " "series.".format( missing.index) ) # check for duplicates in p_set and q_set comp_ts = [ pypsa_network.loads_t, pypsa_network.generators_t, pypsa_network.storage_units_t ] for comp in comp_ts: for i in ["p_set", "q_set"]: if any(comp[i].columns.duplicated()): raise ValueError( "Pypsa timeseries have duplicated entries: {}".format( comp[i].columns.duplicated()) ) if any(pypsa_network.buses_t["v_mag_pu_set"].columns.duplicated()): raise ValueError( "Pypsa timeseries have duplicated entries: {}".format( pypsa_network.buses_t["v_mag_pu_set"].columns.duplicated()) )
[docs]def process_pfa_results(edisgo, pypsa, timesteps): """ Passing power flow results from PyPSA to :class:`~.network.results.Results`. Parameters ---------- edisgo : :class:`~.EDisGo` pypsa : :pypsa:`pypsa.Network<network>` The PyPSA `Network container <https://www.pypsa.org/doc/components.html#network>`_ timesteps : :pandas:`pandas.DatetimeIndex<DatetimeIndex>` or :pandas:`pandas.Timestamp<Timestamp>` Time steps for which latest power flow analysis was conducted and for which to retrieve pypsa results. Notes ----- P and Q are returned from the line ending/transformer side with highest apparent power S, exemplary written as .. math:: S_{max} = max(\sqrt{P_0^2 + Q_0^2}, \sqrt{P_1^2 + Q_1^2}) \\ P = P_0 P_1(S_{max}) \\ Q = Q_0 Q_1(S_{max}) See Also -------- :class:`~.network.results.Results` to understand how results of power flow analysis are structured in eDisGo. """ # get the absolute losses in the system (in MW and Mvar) # subtracting total generation (including slack) from total load grid_losses = { "p": ( abs(pypsa.generators_t["p"].sum(axis=1) - pypsa.loads_t["p"].sum(axis=1)) ), "q": ( abs(pypsa.generators_t["q"].sum(axis=1) - pypsa.loads_t["q"].sum(axis=1)) ), } edisgo.results.grid_losses = pd.DataFrame(grid_losses).reindex(index=timesteps) # get slack results in MW and Mvar pfa_slack = { "p": (pypsa.generators_t["p"]["Generator_slack"]), "q": (pypsa.generators_t["q"]["Generator_slack"]), } edisgo.results.pfa_slack = pd.DataFrame(pfa_slack).reindex(index=timesteps) # get P and Q of lines and transformers in MW and Mvar q0 = pd.concat( [np.abs(pypsa.lines_t["q0"]), np.abs(pypsa.transformers_t["q0"])], axis=1, sort=False ).reindex(index=timesteps) q1 = pd.concat( [np.abs(pypsa.lines_t["q1"]), np.abs(pypsa.transformers_t["q1"])], axis=1, sort=False ).reindex(index=timesteps) p0 = pd.concat( [np.abs(pypsa.lines_t["p0"]), np.abs(pypsa.transformers_t["p0"])], axis=1, sort=False ).reindex(index=timesteps) p1 = pd.concat( [np.abs(pypsa.lines_t["p1"]), np.abs(pypsa.transformers_t["p1"])], axis=1, sort=False ).reindex(index=timesteps) # determine apparent power at line endings/transformer sides s0 = np.hypot(p0, q0) s1 = np.hypot(p1, q1) # choose P and Q from line ending with max(s0,s1) edisgo.results.pfa_p = p0.where(s0 > s1, p1) edisgo.results.pfa_q = q0.where(s0 > s1, q1) # calculate line currents in kA lines_bus0 = pypsa.lines["bus0"].to_dict() bus0_v_mag_pu = ( pypsa.buses_t["v_mag_pu"].T.loc[list(lines_bus0.values()), :].copy() ) bus0_v_mag_pu.index = list(lines_bus0.keys()) current = np.hypot( pypsa.lines_t["p0"], pypsa.lines_t["q0"] ).truediv(pypsa.lines["v_nom"] * bus0_v_mag_pu.T, axis="columns") / sqrt(3) edisgo.results._i_res = current.reindex(index=timesteps) # get voltage results in kV edisgo.results._v_res = pypsa.buses_t["v_mag_pu"].reindex(index=timesteps) # save seeds edisgo.results.pfa_v_mag_pu_seed = pd.concat( [edisgo.results.pfa_v_mag_pu_seed, pypsa.buses_t["v_mag_pu"].reindex(index=timesteps) ] ) edisgo.results.pfa_v_mag_pu_seed = edisgo.results.pfa_v_mag_pu_seed[ ~edisgo.results.pfa_v_mag_pu_seed.index.duplicated( keep='last')].fillna(1) edisgo.results.pfa_v_ang_seed = pd.concat( [edisgo.results.pfa_v_ang_seed, pypsa.buses_t["v_ang"].reindex(index=timesteps) ] ) edisgo.results.pfa_v_ang_seed = edisgo.results.pfa_v_ang_seed[ ~edisgo.results.pfa_v_ang_seed.index.duplicated( keep='last')].fillna(0)