Source code for edisgo.tools.pypsa_io

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

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

from edisgo.grid.components import Transformer, Line, LVStation, MVStation
from edisgo.grid.grids import LVGrid


[docs]def to_pypsa(network, mode, timesteps): """ Translate graph based grid representation to PyPSA Network For details from a user perspective see API documentation of :meth:`~.grid.network.EDisGo.analyze` of the API class :class:`~.grid.network.EDisGo`. Translating eDisGo's grid topology to PyPSA representation is structured into translating the topology and adding time series for components of the grid. In both cases translation of MV grid only (`mode='mv'`), LV grid 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 grid components are exported by :func:`mv_to_pypsa` including the LV station. LV grid load and generation is considered using :func:`add_aggregated_lv_components`. Time series are collected by `_pypsa_load_timeseries` (as example for loads, generators and buses) specifying `mode='mv'`). Timeseries for aggregated load/generation at substations are determined individually. * Low-voltage only (`mode='lv'`): LV grid 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 grid topology is translated to PyPSA in order to perform a complete power flow analysis in both levels together. First, both grid levels are translated seperately using :func:`mv_to_pypsa` and :func:`lv_to_pypsa`. Those are merge by :func:`combine_mv_and_lv`. Time series are obtained at once for both grid levels. This PyPSA interface is aware of translation errors and performs so checks on integrity of data converted to PyPSA grid representation * Sub-graphs/ Sub-networks: It is ensured the grid 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 ---------- network : :class:`~.grid.network.Network` eDisGo grid container mode : str Determines grid levels that are translated to `PyPSA grid representation <https://www.pypsa.org/doc/components.html#network>`_. Specify * None to export MV and LV grid levels. None is the default. * ('mv' to export MV grid level only. This includes cumulative load and generation from underlying LV grid aggregated at respective LV station. This option is implemented, though the rest of edisgo does not handle it yet.) * ('lv' to export LV grid level only. This option is not yet implemented) 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. Returns ------- :pypsa:`pypsa.Network<network>` The `PyPSA network <https://www.pypsa.org/doc/components.html#network>`_ container. """ # 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] # get topology and time series data if mode is None: mv_components = mv_to_pypsa(network) lv_components = lv_to_pypsa(network) components = combine_mv_and_lv(mv_components, lv_components) if list(components['Load'].index.values): timeseries_load_p, timeseries_load_q = _pypsa_load_timeseries( network, mode=mode, timesteps=timesteps) if len(list(components['Generator'].index.values)) > 1: timeseries_gen_p, timeseries_gen_q = _pypsa_generator_timeseries( network, mode=mode, timesteps=timesteps) if list(components['Bus'].index.values): timeseries_bus_v_set = _pypsa_bus_timeseries( network, components['Bus'].index.tolist(), timesteps=timesteps) if len(list(components['StorageUnit'].index.values)) > 0: timeseries_storage_p, timeseries_storage_q = \ _pypsa_storage_timeseries( network, mode=mode, timesteps=timesteps) elif mode is 'mv': # the pypsa export works but NotImplementedError is raised since the # rest of edisgo (handling of results from pfa, grid expansion, etc.) # does not yet work raise NotImplementedError mv_components = mv_to_pypsa(network) components = add_aggregated_lv_components(network, mv_components) if list(components['Load'].index.values): timeseries_load_p, timeseries_load_q = _pypsa_load_timeseries( network, mode=mode, timesteps=timesteps) if len(list(components['Generator'].index.values)) > 1: timeseries_gen_p, timeseries_gen_q = _pypsa_generator_timeseries( network, mode=mode, timesteps=timesteps) if list(components['Bus'].index.values): timeseries_bus_v_set = _pypsa_bus_timeseries( network, components['Bus'].index.tolist(), timesteps=timesteps) if len(list(components['StorageUnit'].index.values)) > 0: timeseries_storage_p, timeseries_storage_q = \ _pypsa_storage_timeseries( network, mode=mode, timesteps=timesteps) elif mode is 'lv': raise NotImplementedError #lv_to_pypsa(network) else: raise ValueError("Provide proper mode or leave it empty to export " "entire grid topology.") # check topology _check_topology(components) # create power flow problem pypsa_network = PyPSANetwork() pypsa_network.edisgo_mode = mode pypsa_network.set_snapshots(timesteps) # import grid topology to PyPSA network # buses are created first to avoid warnings pypsa_network.import_components_from_dataframe(components['Bus'], 'Bus') for k, comps in components.items(): if k is not 'Bus' and not comps.empty: pypsa_network.import_components_from_dataframe(comps, k) # import time series to PyPSA network if len(list(components['Generator'].index.values)) > 1: import_series_from_dataframe(pypsa_network, timeseries_gen_p, 'Generator', 'p_set') import_series_from_dataframe(pypsa_network, timeseries_gen_q, 'Generator', 'q_set') if list(components['Load'].index.values): import_series_from_dataframe(pypsa_network, timeseries_load_p, 'Load', 'p_set') import_series_from_dataframe(pypsa_network, timeseries_load_q, 'Load', 'q_set') if list(components['Bus'].index.values): import_series_from_dataframe(pypsa_network, timeseries_bus_v_set, 'Bus', 'v_mag_pu_set') if len(list(components['StorageUnit'].index.values)) > 0: import_series_from_dataframe(pypsa_network, timeseries_storage_p, 'StorageUnit', 'p_set') import_series_from_dataframe(pypsa_network, timeseries_storage_q, 'StorageUnit', 'q_set') _check_integrity_of_pypsa(pypsa_network) return pypsa_network
[docs]def mv_to_pypsa(network): """Translate MV grid topology representation to PyPSA format MV grid topology translated here includes * MV station (no transformer, see :meth:`~.grid.network.EDisGo.analyze`) * Loads, Generators, Lines, Storages, Branch Tees of MV grid level as well as LV stations. LV stations do not have load and generation of LV level. Parameters ---------- network : Network eDisGo grid container Returns ------- dict of :pandas:`pandas.DataFrame<dataframe>` A DataFrame for each type of PyPSA components constituting the grid topology. Keys included * 'Generator' * 'Load' * 'Line' * 'BranchTee' * 'Transformer' * 'StorageUnit' .. warning:: PyPSA takes resistance R and reactance X in p.u. The conversion from values in ohm to pu notation is performed by following equations .. math:: r_{p.u.} = R_{\Omega} / Z_{B} x_{p.u.} = X_{\Omega} / Z_{B} with Z_{B} = V_B / S_B I'm quite sure, but its not 100 % clear if the base voltage V_B is chosen correctly. We take the primary side voltage of transformer as the transformers base voltage. See `#54 <https://github.com/openego/eDisGo/issues/54>`_ for discussion. """ generators = network.mv_grid.generators loads = network.mv_grid.graph.nodes_by_attribute('load') branch_tees = network.mv_grid.graph.nodes_by_attribute('branch_tee') lines = list(network.mv_grid.graph.lines()) lv_stations = network.mv_grid.graph.nodes_by_attribute('lv_station') mv_stations = network.mv_grid.graph.nodes_by_attribute('mv_station') disconnecting_points = network.mv_grid.graph.nodes_by_attribute( 'mv_disconnecting_point') storages = network.mv_grid.graph.nodes_by_attribute( 'storage') omega = 2 * pi * 50 # define required dataframe columns for components generator = {'name': [], 'bus': [], 'control': [], 'p_nom': [], 'type': []} bus = {'name': [], 'v_nom': [], 'x': [], 'y': []} load = {'name': [], 'bus': []} line = {'name': [], 'bus0': [], 'bus1': [], 'type': [], 'x': [], 'r': [], 's_nom': [], 'length': []} transformer = {'name': [], 'bus0': [], 'bus1': [], 'type': [], 'model': [], 'x': [], 'r': [], 's_nom': [], 'tap_ratio': []} storage = { 'name': [], 'bus': [], 'p_nom': [], 'state_of_charge_initial': [], 'efficiency_store': [], 'efficiency_dispatch': [], 'standing_loss': []} # create dataframe representing generators and associated buses for gen in generators: bus_name = '_'.join(['Bus', repr(gen)]) generator['name'].append(repr(gen)) generator['bus'].append(bus_name) generator['control'].append('PQ') generator['p_nom'].append(gen.nominal_capacity / 1e3) generator['type'].append('_'.join([gen.type, gen.subtype])) bus['name'].append(bus_name) bus['v_nom'].append(gen.grid.voltage_nom) bus['x'].append(gen.geom.x) bus['y'].append(gen.geom.y) # create dataframe representing branch tees for bt in branch_tees: bus['name'].append('_'.join(['Bus', repr(bt)])) bus['v_nom'].append(bt.grid.voltage_nom) bus['x'].append(bt.geom.x) bus['y'].append(bt.geom.y) # create dataframes representing loads and associated buses for lo in loads: bus_name = '_'.join(['Bus', repr(lo)]) load['name'].append(repr(lo)) load['bus'].append(bus_name) bus['name'].append(bus_name) bus['v_nom'].append(lo.grid.voltage_nom) bus['x'].append(lo.geom.x) bus['y'].append(lo.geom.y) # create dataframe for lines for l in lines: line['name'].append(repr(l['line'])) if l['adj_nodes'][0] in lv_stations: line['bus0'].append( '_'.join(['Bus', l['adj_nodes'][0].__repr__(side='mv')])) else: line['bus0'].append('_'.join(['Bus', repr(l['adj_nodes'][0])])) if l['adj_nodes'][1] in lv_stations: line['bus1'].append( '_'.join(['Bus', l['adj_nodes'][1].__repr__(side='mv')])) else: line['bus1'].append('_'.join(['Bus', repr(l['adj_nodes'][1])])) line['type'].append("") line['x'].append( l['line'].type['L_per_km'] / l['line'].quantity * omega / 1e3 * l['line'].length) line['r'].append(l['line'].type['R_per_km'] / l['line'].quantity * l['line'].length) line['s_nom'].append( sqrt(3) * l['line'].type['I_max_th'] * l['line'].type['U_n'] * l['line'].quantity / 1e3) line['length'].append(l['line'].length) # create dataframe for LV stations incl. primary/secondary side bus for lv_st in lv_stations: transformer_count = 1 # add primary side bus (bus0) bus0_name = '_'.join(['Bus', lv_st.__repr__(side='mv')]) bus['name'].append(bus0_name) bus['v_nom'].append(lv_st.mv_grid.voltage_nom) bus['x'].append(lv_st.geom.x) bus['y'].append(lv_st.geom.y) # add secondary side bus (bus1) bus1_name = '_'.join(['Bus', lv_st.__repr__(side='lv')]) bus['name'].append(bus1_name) bus['v_nom'].append(lv_st.transformers[0].voltage_op) bus['x'].append(None) bus['y'].append(None) # we choose voltage of transformers' primary side v_base = lv_st.mv_grid.voltage_nom for tr in lv_st.transformers: transformer['name'].append( '_'.join([repr(lv_st), 'transformer', str(transformer_count)])) transformer['bus0'].append(bus0_name) transformer['bus1'].append(bus1_name) transformer['type'].append("") transformer['model'].append('pi') # hier evtl. anpassen wenn spaltenname in equipment geändert wird (auch in lv_to_pypsa transformer['r'].append(tr.type.r_pu) transformer['x'].append(tr.type.x_pu) transformer['s_nom'].append(tr.type.S_nom / 1e3) transformer['tap_ratio'].append(1) transformer_count += 1 # create dataframe for MV stations (only secondary side bus) for mv_st in mv_stations: # add secondary side bus (bus1) bus1_name = '_'.join(['Bus', mv_st.__repr__(side='mv')]) bus['name'].append(bus1_name) bus['v_nom'].append(mv_st.transformers[0].voltage_op) bus['x'].append(mv_st.geom.x) bus['y'].append(mv_st.geom.y) # create dataframe representing disconnecting points for dp in disconnecting_points: bus['name'].append('_'.join(['Bus', repr(dp)])) bus['v_nom'].append(dp.grid.voltage_nom) bus['x'].append(dp.geom.x) bus['y'].append(dp.geom.y) # create dataframe representing storages for sto in storages: bus_name = '_'.join(['Bus', repr(sto)]) storage['name'].append(repr(sto)) storage['bus'].append(bus_name) storage['p_nom'].append(sto.nominal_power / 1e3) storage['state_of_charge_initial'].append(sto.soc_initial) storage['efficiency_store'].append(sto.efficiency_in) storage['efficiency_dispatch'].append(sto.efficiency_out) storage['standing_loss'].append(sto.standing_loss) bus['name'].append(bus_name) bus['v_nom'].append(sto.grid.voltage_nom) bus['x'].append(sto.geom.x) bus['y'].append(sto.geom.y) # Add separate slack generator at MV station secondary side bus bar generator['name'].append("Generator_slack") generator['bus'].append(bus1_name) generator['control'].append('Slack') generator['p_nom'].append(0) generator['type'].append('Slack generator') components = { 'Generator': pd.DataFrame(generator).set_index('name'), 'Bus': pd.DataFrame(bus).set_index('name'), 'Load': pd.DataFrame(load).set_index('name'), 'Line': pd.DataFrame(line).set_index('name'), 'Transformer': pd.DataFrame(transformer).set_index('name'), 'StorageUnit': pd.DataFrame(storage).set_index('name')} return components
[docs]def lv_to_pypsa(network): """ Convert LV grid topology to PyPSA representation Includes grid topology of all LV grids of :attr:`~.grid.grid.Grid.lv_grids` Parameters ---------- network : Network eDisGo grid container Returns ------- dict of :pandas:`pandas.DataFrame<dataframe>` A DataFrame for each type of PyPSA components constituting the grid topology. Keys included * 'Generator' * 'Load' * 'Line' * 'BranchTee' * 'StorageUnit' """ generators = [] loads = [] branch_tees = [] lines = [] lv_stations = [] storages = [] for lv_grid in network.mv_grid.lv_grids: generators.extend(lv_grid.generators) loads.extend(lv_grid.graph.nodes_by_attribute('load')) branch_tees.extend(lv_grid.graph.nodes_by_attribute('branch_tee')) lines.extend(lv_grid.graph.lines()) lv_stations.extend(lv_grid.graph.nodes_by_attribute('lv_station')) storages.extend(lv_grid.graph.nodes_by_attribute('storage')) omega = 2 * pi * 50 generator = {'name': [], 'bus': [], 'control': [], 'p_nom': [], 'type': []} bus = {'name': [], 'v_nom': [], 'x': [], 'y': []} load = {'name': [], 'bus': []} line = {'name': [], 'bus0': [], 'bus1': [], 'type': [], 'x': [], 'r': [], 's_nom': [], 'length': []} storage = { 'name': [], 'bus': [], 'p_nom': [], 'state_of_charge_initial': [], 'efficiency_store': [], 'efficiency_dispatch': [], 'standing_loss': []} # create dictionary representing generators and associated buses for gen in generators: bus_name = '_'.join(['Bus', repr(gen)]) generator['name'].append(repr(gen)) generator['bus'].append(bus_name) generator['control'].append('PQ') generator['p_nom'].append(gen.nominal_capacity / 1e3) generator['type'].append('_'.join([gen.type, gen.subtype])) bus['name'].append(bus_name) bus['v_nom'].append(gen.grid.voltage_nom) bus['x'].append(None) bus['y'].append(None) # create dictionary representing branch tees for bt in branch_tees: bus['name'].append('_'.join(['Bus', repr(bt)])) bus['v_nom'].append(bt.grid.voltage_nom) bus['x'].append(None) bus['y'].append(None) # create dataframes representing loads and associated buses for lo in loads: bus_name = '_'.join(['Bus', repr(lo)]) load['name'].append(repr(lo)) load['bus'].append(bus_name) bus['name'].append(bus_name) bus['v_nom'].append(lo.grid.voltage_nom) bus['x'].append(None) bus['y'].append(None) # create dataframe for lines for l in lines: line['name'].append(repr(l['line'])) if l['adj_nodes'][0] in lv_stations: line['bus0'].append( '_'.join(['Bus', l['adj_nodes'][0].__repr__(side='lv')])) else: line['bus0'].append('_'.join(['Bus', repr(l['adj_nodes'][0])])) if l['adj_nodes'][1] in lv_stations: line['bus1'].append( '_'.join(['Bus', l['adj_nodes'][1].__repr__(side='lv')])) else: line['bus1'].append('_'.join(['Bus', repr(l['adj_nodes'][1])])) line['type'].append("") line['x'].append( l['line'].type['L_per_km'] * omega / 1e3 * l['line'].length/ l['line'].quantity) line['r'].append(l['line'].type['R_per_km'] * l['line'].length/ l['line'].quantity) line['s_nom'].append( sqrt(3) * l['line'].type['I_max_th'] * l['line'].type['U_n'] * l['line'].quantity / 1e3) line['length'].append(l['line'].length) # create dataframe representing storages for sto in storages: bus_name = '_'.join(['Bus', repr(sto)]) storage['name'].append(repr(sto)) storage['bus'].append(bus_name) storage['p_nom'].append(sto.nominal_power) storage['state_of_charge_initial'].append(sto.soc_initial) storage['efficiency_store'].append(sto.efficiency_in) storage['efficiency_dispatch'].append(sto.efficiency_out) storage['standing_loss'].append(sto.standing_loss) bus['name'].append(bus_name) bus['v_nom'].append(sto.grid.voltage_nom) bus['x'].append(None) bus['y'].append(None) lv_components = { 'Generator': pd.DataFrame(generator).set_index('name'), 'Bus': pd.DataFrame(bus).set_index('name'), 'Load': pd.DataFrame(load).set_index('name'), 'Line': pd.DataFrame(line).set_index('name'), 'StorageUnit': pd.DataFrame(storage).set_index('name')} return lv_components
[docs]def combine_mv_and_lv(mv, lv): """Combine MV and LV grid topology in PyPSA format """ combined = { c: pd.concat([mv[c], lv[c]], axis=0) for c in list(lv.keys()) } combined['Transformer'] = mv['Transformer'] return combined
[docs]def add_aggregated_lv_components(network, components): """ Aggregates LV load and generation at LV stations Use this function if you aim for MV calculation only. The according DataFrames of `components` are extended by load and generators representing these aggregated respecting the technology type. Parameters ---------- network : Network The eDisGo grid topology model overall container components : dict of :pandas:`pandas.DataFrame<dataframe>` PyPSA components in tabular format Returns ------- :obj:`dict` of :pandas:`pandas.DataFrame<dataframe>` The dictionary components passed to the function is returned altered. """ generators = {} loads = {} # collect aggregated generation capacity by type and subtype # collect aggregated load grouped by sector for lv_grid in network.mv_grid.lv_grids: generators.setdefault(lv_grid, {}) for gen in lv_grid.generators: generators[lv_grid].setdefault(gen.type, {}) generators[lv_grid][gen.type].setdefault(gen.subtype, {}) generators[lv_grid][gen.type][gen.subtype].setdefault( 'capacity', 0) generators[lv_grid][gen.type][gen.subtype][ 'capacity'] += gen.nominal_capacity generators[lv_grid][gen.type][gen.subtype].setdefault( 'name', '_'.join([gen.type, gen.subtype, 'aggregated', 'LV_grid', str(lv_grid.id)])) loads.setdefault(lv_grid, {}) for lo in lv_grid.graph.nodes_by_attribute('load'): for sector, val in lo.consumption.items(): loads[lv_grid].setdefault(sector, 0) loads[lv_grid][sector] += val # define dict for DataFrame creation of aggr. generation and load generator = {'name': [], 'bus': [], 'control': [], 'p_nom': [], 'type': []} load = {'name': [], 'bus': []} # fill generators dictionary for DataFrame creation for lv_grid_obj, lv_grid in generators.items(): for _, gen_type in lv_grid.items(): for _, gen_subtype in gen_type.items(): generator['name'].append(gen_subtype['name']) generator['bus'].append( '_'.join(['Bus', lv_grid_obj.station.__repr__('lv')])) generator['control'].append('PQ') generator['p_nom'].append(gen_subtype['capacity']) generator['type'].append("") # fill loads dictionary for DataFrame creation for lv_grid_obj, lv_grid in loads.items(): for sector, val in lv_grid.items(): load['name'].append('_'.join(['Load', sector, repr(lv_grid_obj)])) load['bus'].append( '_'.join(['Bus', lv_grid_obj.station.__repr__('lv')])) components['Generator'] = pd.concat( [components['Generator'], pd.DataFrame(generator).set_index('name')]) components['Load'] = pd.concat( [components['Load'], pd.DataFrame(load).set_index('name')]) return components
def _pypsa_load_timeseries(network, timesteps, mode=None): """ Time series in PyPSA compatible format for load instances Parameters ---------- network : Network The eDisGo grid topology 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. mode : str, optional Specifically retrieve load time series for MV or LV grid level or both. Either choose 'mv' or 'lv'. Defaults to None, which returns both timeseries for MV and LV in a single DataFrame. Returns ------- :pandas:`pandas.DataFrame<dataframe>` Time series table in PyPSA format """ mv_load_timeseries_p = [] mv_load_timeseries_q = [] lv_load_timeseries_p = [] lv_load_timeseries_q = [] # add MV grid loads if mode is 'mv' or mode is None: for load in network.mv_grid.graph.nodes_by_attribute('load'): mv_load_timeseries_q.append(load.pypsa_timeseries('q').rename( repr(load)).to_frame().loc[timesteps]) mv_load_timeseries_p.append(load.pypsa_timeseries('p').rename( repr(load)).to_frame().loc[timesteps]) if mode is 'mv': lv_load_timeseries_p, lv_load_timeseries_q = \ _pypsa_load_timeseries_aggregated_at_lv_station( network, timesteps) # add LV grid's loads if mode is 'lv' or mode is None: for lv_grid in network.mv_grid.lv_grids: for load in lv_grid.graph.nodes_by_attribute('load'): lv_load_timeseries_q.append(load.pypsa_timeseries('q').rename( repr(load)).to_frame().loc[timesteps]) lv_load_timeseries_p.append(load.pypsa_timeseries('p').rename( repr(load)).to_frame().loc[timesteps]) load_df_p = pd.concat(mv_load_timeseries_p + lv_load_timeseries_p, axis=1) load_df_q = pd.concat(mv_load_timeseries_q + lv_load_timeseries_q, axis=1) return load_df_p, load_df_q def _pypsa_generator_timeseries(network, timesteps, mode=None): """Timeseries in PyPSA compatible format for generator instances Parameters ---------- network : Network The eDisGo grid topology 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. mode : str, optional Specifically retrieve generator time series for MV or LV grid level or both. Either choose 'mv' or 'lv'. Defaults to None, which returns both timeseries for MV and LV in a single DataFrame. Returns ------- :pandas:`pandas.DataFrame<dataframe>` Time series table in PyPSA format """ mv_gen_timeseries_q = [] mv_gen_timeseries_p = [] lv_gen_timeseries_q = [] lv_gen_timeseries_p = [] # MV generator timeseries if mode is 'mv' or mode is None: for gen in network.mv_grid.generators: mv_gen_timeseries_q.append(gen.pypsa_timeseries('q').rename( repr(gen)).to_frame().loc[timesteps]) mv_gen_timeseries_p.append(gen.pypsa_timeseries('p').rename( repr(gen)).to_frame().loc[timesteps]) if mode is 'mv': lv_gen_timeseries_p, lv_gen_timeseries_q = \ _pypsa_generator_timeseries_aggregated_at_lv_station( network, timesteps) # LV generator timeseries if mode is 'lv' or mode is None: for lv_grid in network.mv_grid.lv_grids: for gen in lv_grid.generators: lv_gen_timeseries_q.append(gen.pypsa_timeseries('q').rename( repr(gen)).to_frame().loc[timesteps]) lv_gen_timeseries_p.append(gen.pypsa_timeseries('p').rename( repr(gen)).to_frame().loc[timesteps]) gen_df_p = pd.concat(mv_gen_timeseries_p + lv_gen_timeseries_p, axis=1) gen_df_q = pd.concat(mv_gen_timeseries_q + lv_gen_timeseries_q, axis=1) return gen_df_p, gen_df_q def _pypsa_storage_timeseries(network, timesteps, mode=None): """ Timeseries in PyPSA compatible format for storage instances Parameters ---------- network : Network The eDisGo grid topology 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. mode : str, optional Specifically retrieve generator time series for MV or LV grid level or both. Either choose 'mv' or 'lv'. Defaults to None, which returns both timeseries for MV and LV in a single DataFrame. Returns ------- :pandas:`pandas.DataFrame<dataframe>` Time series table in PyPSA format """ mv_storage_timeseries_q = [] mv_storage_timeseries_p = [] lv_storage_timeseries_q = [] lv_storage_timeseries_p = [] # MV storage time series if mode is 'mv' or mode is None: for storage in network.mv_grid.graph.nodes_by_attribute('storage'): mv_storage_timeseries_q.append( storage.pypsa_timeseries('q').rename( repr(storage)).to_frame().loc[timesteps]) mv_storage_timeseries_p.append( storage.pypsa_timeseries('p').rename( repr(storage)).to_frame().loc[timesteps]) # LV storage time series if mode is 'lv' or mode is None: for lv_grid in network.mv_grid.lv_grids: for storage in lv_grid.graph.nodes_by_attribute('storage'): lv_storage_timeseries_q.append( storage.pypsa_timeseries('q').rename( repr(storage)).to_frame().loc[timesteps]) lv_storage_timeseries_p.append( storage.pypsa_timeseries('p').rename( repr(storage)).to_frame().loc[timesteps]) storage_df_p = pd.concat( mv_storage_timeseries_p + lv_storage_timeseries_p, axis=1) storage_df_q = pd.concat( mv_storage_timeseries_q + lv_storage_timeseries_q, axis=1) return storage_df_p, storage_df_q def _pypsa_bus_timeseries(network, buses, timesteps): """ Time series in PyPSA compatible format for bus instances Set all buses except for the slack bus to voltage of 1 pu (it is assumed this setting is entirely ignored during solving the power flow problem). This slack bus is set to an operational voltage which is typically greater than nominal voltage plus a control deviation. The control deviation is always added positively to the operational voltage. For example, the operational voltage (offset) is set to 1.025 pu plus the control deviation of 0.015 pu. This adds up to a set voltage of the slack bus of 1.04 pu. .. warning:: Voltage settings for the slack bus defined by this function assume the feedin case (reverse power flow case) as the worst-case for the power system. Thus, the set point for the slack is always greater 1. Parameters ---------- network : Network The eDisGo grid topology 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 Returns ------- :pandas:`pandas.DataFrame<dataframe>` Time series table in PyPSA format """ # get slack bus label slack_bus = '_'.join( ['Bus', network.mv_grid.station.__repr__(side='mv')]) # set all buses (except slack bus) to nominal voltage v_set_dict = {bus: 1 for bus in buses if bus != slack_bus} # Set slack bus to operational voltage (includes offset and control # deviation control_deviation = network.config[ 'grid_expansion_allowed_voltage_deviations'][ 'hv_mv_trafo_control_deviation'] if control_deviation != 0: control_deviation_ts = \ network.timeseries.timesteps_load_feedin_case.case.apply( lambda _: control_deviation if _ == 'feedin_case' else -control_deviation) else: control_deviation_ts = 0 slack_voltage_pu = control_deviation_ts + 1 + \ network.config[ 'grid_expansion_allowed_voltage_deviations'][ 'hv_mv_trafo_offset'] v_set_dict.update({slack_bus: slack_voltage_pu}) # Convert to PyPSA compatible dataframe v_set_df = pd.DataFrame(v_set_dict, index=timesteps) return v_set_df def _pypsa_generator_timeseries_aggregated_at_lv_station(network, timesteps): """ Aggregates generator time series per generator subtype and LV grid. Parameters ---------- network : Network The eDisGo grid topology 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. Returns ------- tuple of :pandas:`pandas.DataFrame<dataframe>` Tuple of size two containing DataFrames that represent 1. 'p_set' of aggregated Generation per subtype at each LV station 2. 'q_set' of aggregated Generation per subtype at each LV station """ generation_p = [] generation_q = [] for lv_grid in network.mv_grid.lv_grids: # Determine aggregated generation at LV stations generation = {} for gen in lv_grid.generators: # for type in gen.type: # for subtype in gen.subtype: gen_name = '_'.join([gen.type, gen.subtype, 'aggregated', 'LV_grid', str(lv_grid.id)]) generation.setdefault(gen.type, {}) generation[gen.type].setdefault(gen.subtype, {}) generation[gen.type][gen.subtype].setdefault('timeseries_p', []) generation[gen.type][gen.subtype].setdefault('timeseries_q', []) generation[gen.type][gen.subtype]['timeseries_p'].append( gen.pypsa_timeseries('p').rename(gen_name).to_frame().loc[ timesteps]) generation[gen.type][gen.subtype]['timeseries_q'].append( gen.pypsa_timeseries('q').rename(gen_name).to_frame().loc[ timesteps]) for k_type, v_type in generation.items(): for k_type, v_subtype in v_type.items(): col_name = v_subtype['timeseries_p'][0].columns[0] generation_p.append( pd.concat(v_subtype['timeseries_p'], axis=1).sum(axis=1).rename(col_name).to_frame()) generation_q.append( pd.concat(v_subtype['timeseries_q'], axis=1).sum( axis=1).rename(col_name).to_frame()) return generation_p, generation_q def _pypsa_load_timeseries_aggregated_at_lv_station(network, timesteps): """ Aggregates load time series per sector and LV grid. Parameters ---------- network : Network The eDisGo grid topology 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. Returns ------- tuple of :pandas:`pandas.DataFrame<dataframe>` Tuple of size two containing DataFrames that represent 1. 'p_set' of aggregated Load per sector at each LV station 2. 'q_set' of aggregated Load per sector at each LV station """ # ToDo: Load.pypsa_timeseries is not differentiated by sector so this # function will not work (either change here and in # add_aggregated_lv_components or in Load class) load_p = [] load_q = [] for lv_grid in network.mv_grid.lv_grids: # Determine aggregated load at LV stations load = {} for lo in lv_grid.graph.nodes_by_attribute('load'): for sector, val in lo.consumption.items(): load.setdefault(sector, {}) load[sector].setdefault('timeseries_p', []) load[sector].setdefault('timeseries_q', []) load[sector]['timeseries_p'].append( lo.pypsa_timeseries('p').rename(repr(lo)).to_frame().loc[ timesteps]) load[sector]['timeseries_q'].append( lo.pypsa_timeseries('q').rename(repr(lo)).to_frame().loc[ timesteps]) for sector, val in load.items(): load_p.append( pd.concat(val['timeseries_p'], axis=1).sum(axis=1).rename( '_'.join(['Load', sector, repr(lv_grid)])).to_frame()) load_q.append( pd.concat(val['timeseries_q'], axis=1).sum(axis=1).rename( '_'.join(['Load', sector, repr(lv_grid)])).to_frame()) return load_p, load_q def _check_topology(components): buses = components['Bus'].index.tolist() line_buses = components['Line']['bus0'].tolist() + \ components['Line']['bus1'].tolist() load_buses = components['Load']['bus'].tolist() generator_buses = components['Generator']['bus'].tolist() transformer_buses = components['Transformer']['bus0'].tolist() + \ components['Transformer']['bus1'].tolist() buses_to_check = line_buses + load_buses + generator_buses + \ transformer_buses missing_buses = [] missing_buses.extend([_ for _ in buses_to_check if _ not in buses]) if missing_buses: raise ValueError("Buses {buses} are not defined.".format( buses=missing_buses)) # check if there are duplicate components and print them for k, comps in components.items(): if len(list(comps.index.values)) != len(set(comps.index.values)): raise ValueError("There are duplicates in the {comp} list: {dupl}" .format(comp=k, dupl=[item for item, count in collections.Counter(comps.index.values).items() if count > 1]) ) def _check_integrity_of_pypsa(pypsa_network): """""" # 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 graph has isolated nodes or edges") # check consistency of topology and time series data generators_ts_p_missing = pypsa_network.generators.loc[ ~pypsa_network.generators.index.isin( pypsa_network.generators_t['p_set'].columns.tolist())] generators_ts_q_missing = pypsa_network.generators.loc[ ~pypsa_network.generators.index.isin( pypsa_network.generators_t['q_set'].columns.tolist())] loads_ts_p_missing = pypsa_network.loads.loc[ ~pypsa_network.loads.index.isin( pypsa_network.loads_t['p_set'].columns.tolist())] loads_ts_q_missing = pypsa_network.loads.loc[ ~pypsa_network.loads.index.isin( pypsa_network.loads_t['q_set'].columns.tolist())] bus_v_set_missing = pypsa_network.buses.loc[ ~pypsa_network.buses.index.isin( pypsa_network.buses_t['v_mag_pu_set'].columns.tolist())] # Comparison of generators excludes slack generators (have no time series) if (not generators_ts_p_missing.empty and not all( generators_ts_p_missing['control'] == 'Slack')): raise ValueError("Following generators have no `p_set` time series " "{generators}".format( generators=generators_ts_p_missing)) if (not generators_ts_q_missing.empty and not all( generators_ts_q_missing['control'] == 'Slack')): raise ValueError("Following generators have no `q_set` time series " "{generators}".format( generators=generators_ts_q_missing)) if not loads_ts_p_missing.empty: raise ValueError("Following loads have no `p_set` time series " "{loads}".format( loads=loads_ts_p_missing)) if not loads_ts_q_missing.empty: raise ValueError("Following loads have no `q_set` time series " "{loads}".format( loads=loads_ts_q_missing)) if not bus_v_set_missing.empty: raise ValueError("Following loads have no `v_mag_pu_set` time series " "{buses}".format( buses=bus_v_set_missing)) # check for duplicate labels (of components) duplicated_labels = [] if any(pypsa_network.buses.index.duplicated()): duplicated_labels.append(pypsa_network.buses.index[ pypsa_network.buses.index.duplicated()]) if any(pypsa_network.generators.index.duplicated()): duplicated_labels.append(pypsa_network.generators.index[ pypsa_network.generators.index.duplicated()]) if any(pypsa_network.loads.index.duplicated()): duplicated_labels.append(pypsa_network.loads.index[ pypsa_network.loads.index.duplicated()]) if any(pypsa_network.transformers.index.duplicated()): duplicated_labels.append(pypsa_network.transformers.index[ pypsa_network.transformers.index.duplicated()]) if any(pypsa_network.lines.index.duplicated()): duplicated_labels.append(pypsa_network.lines.index[ pypsa_network.lines.index.duplicated()]) if duplicated_labels: raise ValueError("{labels} have duplicate entry in " "one of the components dataframes".format( labels=duplicated_labels)) # duplicate p_sets and q_set duplicate_p_sets = [] duplicate_q_sets = [] if any(pypsa_network.loads_t['p_set'].columns.duplicated()): duplicate_p_sets.append(pypsa_network.loads_t['p_set'].columns[ pypsa_network.loads_t[ 'p_set'].columns.duplicated()]) if any(pypsa_network.loads_t['q_set'].columns.duplicated()): duplicate_q_sets.append(pypsa_network.loads_t['q_set'].columns[ pypsa_network.loads_t[ 'q_set'].columns.duplicated()]) if any(pypsa_network.generators_t['p_set'].columns.duplicated()): duplicate_p_sets.append(pypsa_network.generators_t['p_set'].columns[ pypsa_network.generators_t[ 'p_set'].columns.duplicated()]) if any(pypsa_network.generators_t['q_set'].columns.duplicated()): duplicate_q_sets.append(pypsa_network.generators_t['q_set'].columns[ pypsa_network.generators_t[ 'q_set'].columns.duplicated()]) if duplicate_p_sets: raise ValueError("{labels} have duplicate entry in " "generators_t['p_set']" " or loads_t['p_set']".format( labels=duplicate_p_sets)) if duplicate_q_sets: raise ValueError("{labels} have duplicate entry in " "generators_t['q_set']" " or loads_t['q_set']".format( labels=duplicate_q_sets)) # find duplicate v_mag_set entries duplicate_v_mag_set = [] if any(pypsa_network.buses_t['v_mag_pu_set'].columns.duplicated()): duplicate_v_mag_set.append(pypsa_network.buses_t['v_mag_pu_set'].columns[ pypsa_network.buses_t[ 'v_mag_pu_set'].columns.duplicated()]) if duplicate_v_mag_set: raise ValueError("{labels} have duplicate entry in buses_t".format( labels=duplicate_v_mag_set))
[docs]def process_pfa_results(network, pypsa, timesteps): """ Assing values from PyPSA to :meth:`results <edisgo.grid.network.Network.results>` Parameters ---------- network : Network The eDisGo grid topology model overall container 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 for and for which to retrieve pypsa results. Notes ----- P and Q (and respectively later S) are returned from the line ending/ transformer side with highest apparent power S, exemplary written as .. math:: S_{max} = max(\sqrt{P0^2 + Q0^2}, \sqrt{P1^2 + Q1^2}) P = P0P1(S_{max}) Q = Q0Q1(S_{max}) See Also -------- :class:`~.grid.network.Results` Understand how results of power flow analysis are structured in eDisGo. """ # get the absolute losses in the system # subtracting total generation (including slack) from total load grid_losses = {'p': 1e3 * (pypsa.generators_t['p'].sum(axis=1) - pypsa.loads_t['p'].sum(axis=1)), 'q': 1e3 * (pypsa.generators_t['q'].sum(axis=1) - pypsa.loads_t['q'].sum(axis=1))} network.results.grid_losses = pd.DataFrame(grid_losses).loc[timesteps, :] # get slack results grid_exchanges = {'p': 1e3 * (pypsa.generators_t['p']['Generator_slack']), 'q': 1e3 * (pypsa.generators_t['q']['Generator_slack'])} network.results.hv_mv_exchanges = pd.DataFrame(grid_exchanges).loc[timesteps, :] # get p and q of lines, LV transformers and MV Station (slack generator) # in absolute values q0 = pd.concat( [np.abs(pypsa.lines_t['q0']), np.abs(pypsa.transformers_t['q0']), np.abs(pypsa.generators_t['q']['Generator_slack'].rename( repr(network.mv_grid.station)))], axis=1).loc[timesteps, :] q1 = pd.concat( [np.abs(pypsa.lines_t['q1']), np.abs(pypsa.transformers_t['q1']), np.abs(pypsa.generators_t['q']['Generator_slack'].rename( repr(network.mv_grid.station)))], axis=1).loc[timesteps, :] p0 = pd.concat( [np.abs(pypsa.lines_t['p0']), np.abs(pypsa.transformers_t['p0']), np.abs(pypsa.generators_t['p']['Generator_slack'].rename( repr(network.mv_grid.station)))], axis=1).loc[timesteps, :] p1 = pd.concat( [np.abs(pypsa.lines_t['p1']), np.abs(pypsa.transformers_t['p1']), np.abs(pypsa.generators_t['p']['Generator_slack'].rename( repr(network.mv_grid.station)))], axis=1).loc[timesteps, :] # determine apparent power and line endings/transformers' side s0 = np.hypot(p0, q0) s1 = np.hypot(p1, q1) # choose p and q from line ending with max(s0,s1) network.results.pfa_p = p0.where(s0 > s1, p1) * 1e3 network.results.pfa_q = q0.where(s0 > s1, q1) * 1e3 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()) lines_bus1 = pypsa.lines['bus1'].to_dict() bus1_v_mag_pu = pypsa.buses_t['v_mag_pu'].T.loc[ list(lines_bus1.values()), :].copy() bus1_v_mag_pu.index = list(lines_bus1.keys()) # Get line current network.results._i_res = np.hypot( pypsa.lines_t['p0'], pypsa.lines_t['q0']).truediv( pypsa.lines['v_nom'] * bus0_v_mag_pu.T, axis='columns') / sqrt(3) * 1e3 # process results at nodes generators_names = [repr(g) for g in network.mv_grid.generators] generators_mapping = {v: k for k, v in pypsa.generators.loc[generators_names][ 'bus'].to_dict().items()} storages_names = [repr(g) for g in network.mv_grid.graph.nodes_by_attribute('storage')] storages_mapping = {v: k for k, v in pypsa.storage_units.loc[storages_names][ 'bus'].to_dict().items()} branch_t_names = [repr(bt) for bt in network.mv_grid.graph.nodes_by_attribute('branch_tee')] branch_t_mapping = {'_'.join(['Bus', v]): v for v in branch_t_names} mv_station_names = [repr(m) for m in network.mv_grid.graph.nodes_by_attribute('mv_station')] mv_station_mapping_sec = {'_'.join(['Bus', v]): v for v in mv_station_names} mv_switch_disconnector_names = [repr(sd) for sd in network.mv_grid.graph.nodes_by_attribute( 'mv_disconnecting_point')] mv_switch_disconnector_mapping = {'_'.join(['Bus', v]): v for v in mv_switch_disconnector_names} lv_station_mapping_pri = { '_'.join(['Bus', l.__repr__('mv')]): repr(l) for l in network.mv_grid.graph.nodes_by_attribute('lv_station')} lv_station_mapping_sec = { '_'.join(['Bus', l.__repr__('lv')]): repr(l) for l in network.mv_grid.graph.nodes_by_attribute('lv_station')} loads_names = [repr(lo) for lo in network.mv_grid.graph.nodes_by_attribute('load')] loads_mapping = {v: k for k, v in pypsa.loads.loc[loads_names][ 'bus'].to_dict().items()} lv_generators_names = [] lv_storages_names = [] lv_branch_t_names = [] lv_loads_names = [] for lv_grid in network.mv_grid.lv_grids: lv_generators_names.extend([repr(g) for g in lv_grid.graph.nodes_by_attribute( 'generator')]) lv_storages_names.extend([repr(g) for g in lv_grid.graph.nodes_by_attribute( 'storage')]) lv_branch_t_names.extend([repr(bt) for bt in lv_grid.graph.nodes_by_attribute('branch_tee')]) lv_loads_names.extend([repr(lo) for lo in lv_grid.graph.nodes_by_attribute('load')]) lv_generators_mapping = {v: k for k, v in pypsa.generators.loc[lv_generators_names][ 'bus'].to_dict().items()} lv_storages_mapping = {v: k for k, v in pypsa.storage_units.loc[lv_storages_names][ 'bus'].to_dict().items()} lv_branch_t_mapping = {'_'.join(['Bus', v]): v for v in lv_branch_t_names} lv_loads_mapping = {v: k for k, v in pypsa.loads.loc[lv_loads_names][ 'bus'].to_dict().items()} names_mapping = { **generators_mapping, **storages_mapping, **branch_t_mapping, **mv_station_mapping_sec, **lv_station_mapping_pri, **lv_station_mapping_sec, **mv_switch_disconnector_mapping, **loads_mapping, **lv_generators_mapping, **lv_storages_mapping, **lv_loads_mapping, **lv_branch_t_mapping } # write voltage levels obtained from power flow to results object pfa_v_mag_pu_mv = (pypsa.buses_t['v_mag_pu'][ list(generators_mapping) + list(storages_mapping) + list(branch_t_mapping) + list(mv_station_mapping_sec) + list(mv_switch_disconnector_mapping) + list(lv_station_mapping_pri) + list(loads_mapping)]).rename(columns=names_mapping) pfa_v_mag_pu_lv = (pypsa.buses_t['v_mag_pu'][ list(lv_station_mapping_sec) + list(lv_generators_mapping) + list(lv_storages_mapping) + list(lv_branch_t_mapping) + list(lv_loads_mapping)]).rename(columns=names_mapping) network.results.pfa_v_mag_pu = pd.concat( {'mv': pfa_v_mag_pu_mv.loc[timesteps, :], 'lv': pfa_v_mag_pu_lv.loc[timesteps, :]}, axis=1)
[docs]def update_pypsa_generator_import(network): """ Translate graph based grid representation to PyPSA Network For details from a user perspective see API documentation of :meth:`~.grid.network.EDisGo.analyze` of the API class :class:`~.grid.network.EDisGo`. Translating eDisGo's grid topology to PyPSA representation is structured into translating the topology and adding time series for components of the grid. In both cases translation of MV grid only (`mode='mv'`), LV grid 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 grid components are exported by :func:`mv_to_pypsa` including the LV station. LV grid load and generation is considered using :func:`add_aggregated_lv_components`. Time series are collected by `_pypsa_load_timeseries` (as example for loads, generators and buses) specifying `mode='mv'`). Timeseries for aggregated load/generation at substations are determined individually. * Low-voltage only (`mode='lv'`): LV grid 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 grid topology is translated to PyPSA in order to perform a complete power flow analysis in both levels together. First, both grid levels are translated seperately using :func:`mv_to_pypsa` and :func:`lv_to_pypsa`. Those are merge by :func:`combine_mv_and_lv`. Time series are obtained at once for both grid levels. This PyPSA interface is aware of translation errors and performs so checks on integrity of data converted to PyPSA grid representation * Sub-graphs/ Sub-networks: It is ensured the grid 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 ---------- network : :class:`~.grid.network.Network` eDisGo grid container mode : str Determines grid levels that are translated to `PyPSA grid representation <https://www.pypsa.org/doc/components.html#network>`_. Specify * None to export MV and LV grid levels. None is the default. * ('mv' to export MV grid level only. This includes cumulative load and generation from underlying LV grid aggregated at respective LV station. This option is implemented, though the rest of edisgo does not handle it yet.) * ('lv' to export LV grid level only. This option is not yet implemented) 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. Returns ------- :pypsa:`pypsa.Network<network>` The `PyPSA network <https://www.pypsa.org/doc/components.html#network>`_ container. """ # get topology and time series data if network.pypsa.edisgo_mode is None: mv_components = mv_to_pypsa(network) lv_components = lv_to_pypsa(network) components = combine_mv_and_lv(mv_components, lv_components) elif network.pypsa.edisgo_mode is 'mv': raise NotImplementedError elif network.pypsa.edisgo_mode is 'lv': raise NotImplementedError else: raise ValueError("Provide proper mode or leave it empty to export " "entire grid topology.") # check topology _check_topology(components) # create power flow problem pypsa_network = PyPSANetwork() pypsa_network.edisgo_mode = network.pypsa.edisgo_mode pypsa_network.set_snapshots(network.pypsa.snapshots) # import grid topology to PyPSA network # buses are created first to avoid warnings pypsa_network.import_components_from_dataframe(components['Bus'], 'Bus') for k, comps in components.items(): if k is not 'Bus' and not comps.empty: pypsa_network.import_components_from_dataframe(comps, k) # import time series to PyPSA network pypsa_network.generators_t.p_set = network.pypsa.generators_t.p_set pypsa_network.generators_t.q_set = network.pypsa.generators_t.q_set pypsa_network.loads_t.p_set = network.pypsa.loads_t.p_set pypsa_network.loads_t.q_set = network.pypsa.loads_t.q_set pypsa_network.storage_units_t.p_set = network.pypsa.storage_units_t.p_set pypsa_network.storage_units_t.q_set = network.pypsa.storage_units_t.q_set pypsa_network.buses_t.v_mag_pu_set = network.pypsa.buses_t.v_mag_pu_set network.pypsa = pypsa_network if len(list(components['Generator'].index.values)) > 1: update_pypsa_generator_timeseries(network) if list(components['Bus'].index.values): update_pypsa_bus_timeseries(network) if len(list(components['StorageUnit'].index.values)) > 0: update_pypsa_storage_timeseries(network) _check_integrity_of_pypsa(pypsa_network)
[docs]def update_pypsa_grid_reinforcement(network, equipment_changes): """ Update equipment data of lines and transformers after grid reinforcement. During grid reinforcement (cf. :func:`edisgo.flex_opt.reinforce_grid.reinforce_grid`) grid topology and equipment of lines and transformers are changed. In order to save time and not do a full translation of eDisGo's grid topology to the PyPSA format, this function provides an updater for data that may change during grid reinforcement. The PyPSA grid topology :meth:`edisgo.grid.network.Network.pypsa` is update by changed equipment stored in :attr:`edisgo.grid.network.Network.equipment_changes`. Parameters ---------- network : Network eDisGo grid container equipment_changes : `pandas.DataFrame<dataframe>` Dataframe with latest equipment changes (of latest iteration step) from grid reinforcement. See `equipment_changes` property of :class:`~.grid.network.Results` for more information on the Dataframe. """ # Step 1: Update transformers transformers = equipment_changes[ equipment_changes['equipment'].apply(isinstance, args=(Transformer,))] # HV/MV transformers are excluded because it's not part of the PFA removed_transformers = [repr(_) for _ in transformers[transformers['change'] == 'removed'][ 'equipment'].tolist() if _.voltage_op < 10] added_transformers = transformers[transformers['change'] == 'added'] transformer = {'name': [], 'bus0': [], 'bus1': [], 'type': [], 'model': [], 'x': [], 'r': [], 's_nom': [], 'tap_ratio': []} for idx, row in added_transformers.iterrows(): if isinstance(idx, LVStation): # we choose voltage of transformers' primary side transformer['bus0'].append('_'.join(['Bus', idx.__repr__( side='mv')])) transformer['bus1'].append('_'.join(['Bus', idx.__repr__( side='lv')])) transformer['name'].append(repr(row['equipment'])) transformer['type'].append("") transformer['model'].append('pi') transformer['r'].append(row['equipment'].type.r_pu) transformer['x'].append(row['equipment'].type.x_pu) transformer['s_nom'].append(row['equipment'].type.S_nom / 1e3) transformer['tap_ratio'].append(1) network.pypsa.transformers.drop(removed_transformers, inplace=True) if transformer['name']: network.pypsa.import_components_from_dataframe( pd.DataFrame(transformer).set_index('name'), 'Transformer') # Step 2: Update lines lines = equipment_changes.loc[equipment_changes.index[ equipment_changes.reset_index()['index'].apply( isinstance, args=(Line,))]] changed_lines = lines[lines['change'] == 'changed'] lv_stations = network.mv_grid.graph.nodes_by_attribute('lv_station') omega = 2 * pi * 50 for idx, row in changed_lines.iterrows(): # Update line parameters network.pypsa.lines.loc[repr(idx), 'r'] = ( idx.type['R_per_km'] / idx.quantity * idx.length) network.pypsa.lines.loc[repr(idx), 'x'] = ( idx.type['L_per_km'] / 1e3 * omega / idx.quantity * idx.length) network.pypsa.lines.loc[repr(idx), 's_nom'] = ( sqrt(3) * idx.type['I_max_th'] * idx.type[ 'U_n'] * idx.quantity / 1e3) network.pypsa.lines.loc[repr(idx), 'length'] = idx.length # Update buses line is connected to adj_nodes = idx.grid.graph.nodes_from_line(idx) if adj_nodes[0] in lv_stations: side = 'lv' if isinstance(idx.grid, LVGrid) else 'mv' bus0 = '_'.join(['Bus', adj_nodes[0].__repr__(side=side)]) else: bus0 = '_'.join(['Bus', repr(adj_nodes[0])]) if adj_nodes[1] in lv_stations: side = 'lv' if isinstance(idx.grid, LVGrid) else 'mv' bus1 = '_'.join(['Bus', adj_nodes[1].__repr__(side=side)]) else: bus1 = '_'.join(['Bus', repr(adj_nodes[1])]) network.pypsa.lines.loc[repr(idx), 'bus0'] = bus0 network.pypsa.lines.loc[repr(idx), 'bus1'] = bus1
[docs]def update_pypsa_storage(pypsa, storages, storages_lines): """ Adds storages and their lines to pypsa representation of the edisgo graph. This function effects the following attributes of the pypsa network: components ('StorageUnit'), storage_units, storage_units_t (p_set, q_set), buses, lines Parameters ----------- pypsa : :pypsa:`pypsa.Network<network>` storages : :obj:`list` List with storages of type :class:`~.grid.components.Storage` to add to pypsa network. storages_lines : :obj:`list` List with lines of type :class:`~.grid.components.Line` that connect storages to the grid. """ bus = {'name': [], 'v_nom': [], 'x': [], 'y': []} line = {'name': [], 'bus0': [], 'bus1': [], 'type': [], 'x': [], 'r': [], 's_nom': [], 'length': []} storage = { 'name': [], 'bus': [], 'p_nom': [], 'state_of_charge_initial': [], 'efficiency_store': [], 'efficiency_dispatch': [], 'standing_loss': []} for s in storages: bus_name = '_'.join(['Bus', repr(s)]) storage['name'].append(repr(s)) storage['bus'].append(bus_name) storage['p_nom'].append(s.nominal_power / 1e3) storage['state_of_charge_initial'].append(s.soc_initial) storage['efficiency_store'].append(s.efficiency_in) storage['efficiency_dispatch'].append(s.efficiency_out) storage['standing_loss'].append(s.standing_loss) bus['name'].append(bus_name) bus['v_nom'].append(s.grid.voltage_nom) bus['x'].append(s.geom.x) bus['y'].append(s.geom.y) omega = 2 * pi * 50 for l in storages_lines: line['name'].append(repr(l)) adj_nodes = l.grid.graph.nodes_from_line(l) if isinstance(l.grid, LVGrid): if isinstance(adj_nodes[0], LVStation): line['bus0'].append( '_'.join(['Bus', adj_nodes[0].__repr__(side='lv')])) else: line['bus0'].append('_'.join(['Bus', repr(adj_nodes[0])])) if isinstance(adj_nodes[1], LVStation): line['bus1'].append( '_'.join(['Bus', adj_nodes[1].__repr__(side='lv')])) else: line['bus1'].append('_'.join(['Bus', repr(adj_nodes[1])])) else: if isinstance(adj_nodes[0], LVStation): line['bus0'].append( '_'.join(['Bus', adj_nodes[0].__repr__(side='mv')])) elif isinstance(adj_nodes[0], MVStation): line['bus0'].append( '_'.join(['Bus', adj_nodes[0].__repr__(side='lv')])) else: line['bus0'].append('_'.join(['Bus', repr(adj_nodes[0])])) if isinstance(adj_nodes[1], LVStation): line['bus1'].append( '_'.join(['Bus', adj_nodes[1].__repr__(side='mv')])) elif isinstance(adj_nodes[1], MVStation): line['bus1'].append( '_'.join(['Bus', adj_nodes[1].__repr__(side='lv')])) else: line['bus1'].append('_'.join(['Bus', repr(adj_nodes[1])])) line['type'].append("") line['x'].append(l.type['L_per_km'] * omega / 1e3 * l.length) line['r'].append(l.type['R_per_km'] * l.length) line['s_nom'].append( sqrt(3) * l.type['I_max_th'] * l.type['U_n'] / 1e3) line['length'].append(l.length) # import new components to pypsa pypsa.import_components_from_dataframe( pd.DataFrame(bus).set_index('name'), 'Bus') pypsa.import_components_from_dataframe( pd.DataFrame(storage).set_index('name'), 'StorageUnit') pypsa.import_components_from_dataframe( pd.DataFrame(line).set_index('name'), 'Line') # import time series of storages and buses to pypsa timeseries_storage_p = pd.DataFrame() timeseries_storage_q = pd.DataFrame() for s in storages: timeseries_storage_p[repr(s)] = s.pypsa_timeseries('p').loc[ pypsa.storage_units_t.p_set.index] timeseries_storage_q[repr(s)] = s.pypsa_timeseries('q').loc[ pypsa.storage_units_t.q_set.index] import_series_from_dataframe(pypsa, timeseries_storage_p, 'StorageUnit', 'p_set') import_series_from_dataframe(pypsa, timeseries_storage_q, 'StorageUnit', 'q_set')
[docs]def update_pypsa_timeseries(network, loads_to_update=None, generators_to_update=None, storages_to_update=None, timesteps=None): """ Updates load, generator, storage and bus time series in pypsa network. See functions :func:`update_pypsa_load_timeseries`, :func:`update_pypsa_generator_timeseries`, :func:`update_pypsa_storage_timeseries`, and :func:`update_pypsa_bus_timeseries` for more information. Parameters ---------- network : Network The eDisGo grid topology model overall container loads_to_update : :obj:`list`, optional List with all loads (of type :class:`~.grid.components.Load`) that need to be updated. If None all loads are updated depending on mode. See :meth:`~.tools.pypsa_io.to_pypsa` for more information. generators_to_update : :obj:`list`, optional List with all generators (of type :class:`~.grid.components.Generator`) that need to be updated. If None all generators are updated depending on mode. See :meth:`~.tools.pypsa_io.to_pypsa` for more information. storages_to_update : :obj:`list`, optional List with all storages (of type :class:`~.grid.components.Storage`) that need to be updated. If None all storages are updated depending on mode. See :meth:`~.tools.pypsa_io.to_pypsa` for more information. timesteps : :pandas:`pandas.DatetimeIndex<datetimeindex>` or :pandas:`pandas.Timestamp<timestamp>` Timesteps specifies which time steps of the load time series to export to pypsa representation and use in power flow analysis. If None all time steps currently existing in pypsa representation are updated. If not None current time steps are overwritten by given time steps. Default: None. """ update_pypsa_load_timeseries( network, loads_to_update=loads_to_update, timesteps=timesteps) update_pypsa_generator_timeseries( network, generators_to_update=generators_to_update, timesteps=timesteps) update_pypsa_storage_timeseries( network, storages_to_update=storages_to_update, timesteps=timesteps) update_pypsa_bus_timeseries(network, timesteps=timesteps) # update pypsa snapshots if timesteps is None: timesteps = network.pypsa.buses_t.v_mag_pu_set.index network.pypsa.set_snapshots(timesteps)
[docs]def update_pypsa_load_timeseries(network, loads_to_update=None, timesteps=None): """ Updates load time series in pypsa representation. This function overwrites p_set and q_set of loads_t attribute of pypsa network. Be aware that if you call this function with `timesteps` and thus overwrite current time steps it may lead to inconsistencies in the pypsa network since only load time series are updated but none of the other time series or the snapshots attribute of the pypsa network. Use the function :func:`update_pypsa_timeseries` to change the time steps you want to analyse in the power flow analysis. This function will also raise an error when a load that is currently not in the pypsa representation is added. Parameters ---------- network : Network The eDisGo grid topology model overall container loads_to_update : :obj:`list`, optional List with all loads (of type :class:`~.grid.components.Load`) that need to be updated. If None all loads are updated depending on mode. See :meth:`~.tools.pypsa_io.to_pypsa` for more information. timesteps : :pandas:`pandas.DatetimeIndex<datetimeindex>` or :pandas:`pandas.Timestamp<timestamp>` Timesteps specifies which time steps of the load time series to export to pypsa representation. If None all time steps currently existing in pypsa representation are updated. If not None current time steps are overwritten by given time steps. Default: None. """ _update_pypsa_timeseries_by_type( network, type='load', components_to_update=loads_to_update, timesteps=timesteps)
[docs]def update_pypsa_generator_timeseries(network, generators_to_update=None, timesteps=None): """ Updates generator time series in pypsa representation. This function overwrites p_set and q_set of generators_t attribute of pypsa network. Be aware that if you call this function with `timesteps` and thus overwrite current time steps it may lead to inconsistencies in the pypsa network since only generator time series are updated but none of the other time series or the snapshots attribute of the pypsa network. Use the function :func:`update_pypsa_timeseries` to change the time steps you want to analyse in the power flow analysis. This function will also raise an error when a generator that is currently not in the pypsa representation is added. Parameters ---------- network : Network The eDisGo grid topology model overall container generators_to_update : :obj:`list`, optional List with all generators (of type :class:`~.grid.components.Generator`) that need to be updated. If None all generators are updated depending on mode. See :meth:`~.tools.pypsa_io.to_pypsa` for more information. timesteps : :pandas:`pandas.DatetimeIndex<datetimeindex>` or :pandas:`pandas.Timestamp<timestamp>` Timesteps specifies which time steps of the generator time series to export to pypsa representation. If None all time steps currently existing in pypsa representation are updated. If not None current time steps are overwritten by given time steps. Default: None. """ _update_pypsa_timeseries_by_type( network, type='generator', components_to_update=generators_to_update, timesteps=timesteps)
[docs]def update_pypsa_storage_timeseries(network, storages_to_update=None, timesteps=None): """ Updates storage time series in pypsa representation. This function overwrites p_set and q_set of storage_unit_t attribute of pypsa network. Be aware that if you call this function with `timesteps` and thus overwrite current time steps it may lead to inconsistencies in the pypsa network since only storage time series are updated but none of the other time series or the snapshots attribute of the pypsa network. Use the function :func:`update_pypsa_timeseries` to change the time steps you want to analyse in the power flow analysis. This function will also raise an error when a storage that is currently not in the pypsa representation is added. Parameters ---------- network : Network The eDisGo grid topology model overall container storages_to_update : :obj:`list`, optional List with all storages (of type :class:`~.grid.components.Storage`) that need to be updated. If None all storages are updated depending on mode. See :meth:`~.tools.pypsa_io.to_pypsa` for more information. timesteps : :pandas:`pandas.DatetimeIndex<datetimeindex>` or :pandas:`pandas.Timestamp<timestamp>` Timesteps specifies which time steps of the storage time series to export to pypsa representation. If None all time steps currently existing in pypsa representation are updated. If not None current time steps are overwritten by given time steps. Default: None. """ _update_pypsa_timeseries_by_type( network, type='storage', components_to_update=storages_to_update, timesteps=timesteps)
[docs]def update_pypsa_bus_timeseries(network, timesteps=None): """ Updates buses voltage time series in pypsa representation. This function overwrites v_mag_pu_set of buses_t attribute of pypsa network. Be aware that if you call this function with `timesteps` and thus overwrite current time steps it may lead to inconsistencies in the pypsa network since only bus time series are updated but none of the other time series or the snapshots attribute of the pypsa network. Use the function :func:`update_pypsa_timeseries` to change the time steps you want to analyse in the power flow analysis. Parameters ---------- network : Network The eDisGo grid topology model overall container timesteps : :pandas:`pandas.DatetimeIndex<datetimeindex>` or :pandas:`pandas.Timestamp<timestamp>` Timesteps specifies which time steps of the time series to export to pypsa representation. If None all time steps currently existing in pypsa representation are updated. If not None current time steps are overwritten by given time steps. Default: None. """ if timesteps is None: timesteps = network.pypsa.buses_t.v_mag_pu_set.index # check if timesteps is array-like, otherwise convert to list if not hasattr(timesteps, "__len__"): timesteps = [timesteps] buses = network.pypsa.buses.index v_mag_pu_set = _pypsa_bus_timeseries(network, buses, timesteps) network.pypsa.buses_t.v_mag_pu_set = v_mag_pu_set
def _update_pypsa_timeseries_by_type(network, type, components_to_update=None, timesteps=None): """ Updates time series of specified component in pypsa representation. Be aware that if you call this function with `timesteps` and thus overwrite current time steps it may lead to inconsistencies in the pypsa network since only time series of the specified component are updated but none of the other time series or the snapshots attribute of the pypsa network. Use the function :func:`update_pypsa_timeseries` to change the time steps you want to analyse in the power flow analysis. This function will raise an error when a component that is currently not in the pypsa representation is added. Parameters ---------- network : Network The eDisGo grid topology model overall container type : :obj:`str` Type specifies the type of component (load, generator or storage) that is updated. components_to_update : :obj:`list`, optional List with all components (either of type :class:`~.grid.components.Load`, :class:`~.grid.components.Generator` or :class:`~.grid.components.Storage`) that need to be updated. Possible options are 'load', 'generator' and 'storage'. Components in list must all be of the same type. If None all components specified by `type` are updated depending on the mode. See :meth:`~.tools.pypsa_io.to_pypsa` for more information on mode. timesteps : :pandas:`pandas.DatetimeIndex<datetimeindex>` or :pandas:`pandas.Timestamp<timestamp>` Timesteps specifies which time steps of the time series to export to pypsa representation. If None all time steps currently existing in pypsa representation are updated. If not None current time steps are overwritten by given time steps. Default: None. """ # pypsa dataframe to update if type == 'load': pypsa_ts = network.pypsa.loads_t components_in_pypsa = network.pypsa.loads.index elif type == 'generator': pypsa_ts = network.pypsa.generators_t components_in_pypsa = network.pypsa.generators.index elif type == 'storage': pypsa_ts = network.pypsa.storage_units_t components_in_pypsa = network.pypsa.storage_units.index else: raise ValueError('{} is not a valid type.'.format(type)) # MV and LV loads if network.pypsa.edisgo_mode is None: # if no components are specified get all components of specified type # in whole grid if components_to_update is None: grids = [network.mv_grid] + list(network.mv_grid.lv_grids) if type == 'generator': components_to_update = list(itertools.chain( *[grid.generators for grid in grids])) else: components_to_update = list(itertools.chain( *[grid.graph.nodes_by_attribute(type) for grid in grids])) # if no time steps are specified update all time steps currently # contained in pypsa representation if timesteps is None: timesteps = pypsa_ts.p_set.index # check if timesteps is array-like, otherwise convert to list # (necessary to avoid getting a scalar using .loc) if not hasattr(timesteps, "__len__"): timesteps = [timesteps] p_set = pd.DataFrame() q_set = pd.DataFrame() for comp in components_to_update: if repr(comp) in components_in_pypsa: p_set[repr(comp)] = comp.pypsa_timeseries('p').loc[timesteps] q_set[repr(comp)] = comp.pypsa_timeseries('q').loc[timesteps] else: raise KeyError("Tried to update component {} but could not " "find it in pypsa network.".format(comp)) # overwrite pypsa time series pypsa_ts.p_set = p_set pypsa_ts.q_set = q_set # MV and aggregated LV loads elif network.pypsa.edisgo_mode is 'mv': raise NotImplementedError # LV only elif network.pypsa.edisgo_mode is 'lv': raise NotImplementedError