From 93b697c047a4cc72e31ba18b0b2379ca2016f062 Mon Sep 17 00:00:00 2001 From: gplessm Date: Wed, 6 Sep 2017 10:30:56 +0200 Subject: [PATCH 01/31] Comment reinforce stuff Not working for me... --- edisgo/examples/example.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/edisgo/examples/example.py b/edisgo/examples/example.py index 6c1647166..395046ff2 100644 --- a/edisgo/examples/example.py +++ b/edisgo/examples/example.py @@ -22,21 +22,21 @@ # for now create results object # ToDo: Werte in DataFrame als List oder Array? -results = Results() -results.pfa_edges = pd.read_csv('Exemplary_PyPSA_line_results.csv', - index_col=0, - converters={'p0': literal_eval, - 'q0': literal_eval, - 'p1': literal_eval, - 'q1': literal_eval}) -results.pfa_edges['p0'] = results.pfa_edges['p0'].apply(lambda x: np.array(x)) -results.pfa_edges['q0'] = results.pfa_edges['q0'].apply(lambda x: np.array(x)) -results.pfa_edges['p1'] = results.pfa_edges['p1'].apply(lambda x: np.array(x)) -results.pfa_edges['q1'] = results.pfa_edges['q1'].apply(lambda x: np.array(x)) -results.pfa_nodes = pd.read_csv('Exemplary_PyPSA_bus_results.csv', index_col=0, - converters={'v_mag_pu': literal_eval}) -results.pfa_nodes['v_mag_pu'] = results.pfa_nodes['v_mag_pu'].apply( - lambda x: np.array(x)) +# results = Results() +# results.pfa_edges = pd.read_csv('Exemplary_PyPSA_line_results.csv', +# index_col=0, +# converters={'p0': literal_eval, +# 'q0': literal_eval, +# 'p1': literal_eval, +# 'q1': literal_eval}) +# results.pfa_edges['p0'] = results.pfa_edges['p0'].apply(lambda x: np.array(x)) +# results.pfa_edges['q0'] = results.pfa_edges['q0'].apply(lambda x: np.array(x)) +# results.pfa_edges['p1'] = results.pfa_edges['p1'].apply(lambda x: np.array(x)) +# results.pfa_edges['q1'] = results.pfa_edges['q1'].apply(lambda x: np.array(x)) +# results.pfa_nodes = pd.read_csv('Exemplary_PyPSA_bus_results.csv', index_col=0, +# converters={'v_mag_pu': literal_eval}) +# results.pfa_nodes['v_mag_pu'] = results.pfa_nodes['v_mag_pu'].apply( +# lambda x: np.array(x)) # # MV generators # gens = network.mv_grid.graph.nodes_by_attribute('generator') @@ -56,7 +56,7 @@ # else: # print("O MWh") -reinforce_grid.reinforce_grid(network, results) +# reinforce_grid.reinforce_grid(network, results) # liste aller lv grids # [_ for _ in network.mv_grid.lv_grids] From fb90ce85c19865087da8f6d6e72833972d55fc7e Mon Sep 17 00:00:00 2001 From: gplessm Date: Wed, 6 Sep 2017 11:51:00 +0200 Subject: [PATCH 02/31] Assign p,q to Network.results --- edisgo/grid/network.py | 29 ++++++++++++++++++++++++----- edisgo/tools/interfaces.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/edisgo/grid/network.py b/edisgo/grid/network.py index b4d3b3acc..2d5e4c91b 100644 --- a/edisgo/grid/network.py +++ b/edisgo/grid/network.py @@ -189,6 +189,8 @@ def analyze(self, mode=None): # TODO: if missing, add slack generator self.pypsa.pf(self.pypsa.snapshots) + interfaces.process_pfa_results(self, self.pypsa) + def reinforce(self): """Reinforces the grid @@ -491,8 +493,25 @@ class Results: # TODO: maybe initialize DataFrames `pfa_nodes` different. Like with index of all components of similarly def __init__(self): - self.measures = ['original'] - self.pfa_p = pd.DataFrame() - self.pfa_q = pd.DataFrame() - self.pfa_v_mag_pu = pd.DataFrame() - self.equipment_changes = pd.DataFrame() + self._measures = ['original'] + self._pfa_p = None + self._pfa_q = None + self._pfa_v_mag_pu = None + self._equipment_changes = pd.DataFrame() + + @property + def pfa_p(self): + return self._pfa_p + + @pfa_p.setter + def pfa_p(self, pypsa): + self._pfa_p = pypsa + + @property + def pfa_q(self): + #tODO: return columns selected by passed grid topology components + return self._pfa_q + + @pfa_q.setter + def pfa_q(self, pypsa): + self._pfa_q = pypsa diff --git a/edisgo/tools/interfaces.py b/edisgo/tools/interfaces.py index bf261de38..9aa11650e 100644 --- a/edisgo/tools/interfaces.py +++ b/edisgo/tools/interfaces.py @@ -667,3 +667,34 @@ def _check_integrity_of_pypsa(pypsa_network): raise ValueError("Following loads have no `v_mag_pu_set` time series " "{buses}".format( buses=bus_v_set_missing)) + + +def process_pfa_results(network, pypsa): + """ + Assing values from PyPSA to + :meth:`results ` + + Parameters + ---------- + network : Network + The eDisGo grid topology model overall container + pypsa : :pypsa:`pypsa.Network` + Network container of `PyPSA `_ + + Returns + ------- + + See Also + -------- + edisgo.grid.network.Network.results : Understand how results of power flow + analysis are structured in eDisGo. + + """ + + # line results + q0 = abs(pypsa.lines_t['q0']) + q1 = abs(pypsa.lines_t['q1']) + p0 = abs(pypsa.lines_t['p0']) + p1 = abs(pypsa.lines_t['p1']) + network.results.pfa_p = p0.where(p0 > p1, p1) + network.results.pfa_q = q0.where(q0 > q1, q1) From 1a37856f9a0755cc1b5f8e70ef3fc3035d19c61d Mon Sep 17 00:00:00 2001 From: gplessm Date: Wed, 6 Sep 2017 14:52:49 +0200 Subject: [PATCH 03/31] Distinguish MV and LV station classes --- edisgo/data/import_data.py | 8 ++++---- edisgo/grid/components.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/edisgo/data/import_data.py b/edisgo/data/import_data.py index 7ccd2c5f4..f5b795bb1 100644 --- a/edisgo/data/import_data.py +++ b/edisgo/data/import_data.py @@ -2,7 +2,7 @@ from ding0.core.network.stations import LVStationDing0 from ding0.core.structure.regions import LVLoadAreaCentreDing0 from ..grid.components import Load, Generator, MVDisconnectingPoint, BranchTee,\ - Station, Line, Transformer + MVStation, Line, Transformer, LVStation from ..grid.grids import MVGrid, LVGrid import pandas as pd import numpy as np @@ -115,7 +115,7 @@ def _build_lv_grid(ding0_grid, network): network=network) # Create LV station instances - station = Station(id=ding0_lv_grid._station.id_db, + station = LVStation(id=ding0_lv_grid._station.id_db, geom=ding0_lv_grid._station.geo_data, grid=lv_grid, transformers=[Transformer( @@ -261,7 +261,7 @@ def _build_mv_grid(ding0_grid, network): grid.graph.add_nodes_from(branch_tees.values(), type='branch_tee') # Create list of LV station instances and add these to grid's graph - stations = {_: Station(id=_.id_db, + stations = {_: LVStation(id=_.id_db, geom=_.geo_data, grid=grid, transformers=[Transformer( @@ -281,7 +281,7 @@ def _build_mv_grid(ding0_grid, network): grid.graph.add_nodes_from(stations.values(), type='lv_station') # Create HV-MV station add to graph - mv_station = Station( + mv_station = MVStation( id=ding0_grid.station().id_db, geom=ding0_grid.station().geo_data, transformers=[Transformer( diff --git a/edisgo/grid/components.py b/edisgo/grid/components.py index 7f82702ed..5ae90095e 100644 --- a/edisgo/grid/components.py +++ b/edisgo/grid/components.py @@ -317,6 +317,21 @@ def __init__(self, **kwargs): super().__init__(**kwargs) +class LVStation(Station): + """LV Station object""" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def __repr__(self, side=None): + if side == 'mv': + return 'primary' + elif side == 'lv': + return 'secondary' + else: + return '' + + class Line(Component): """ Line object From 0d3f58fab2fb2da0d95b6681722ee9b558b95150 Mon Sep 17 00:00:00 2001 From: gplessm Date: Wed, 6 Sep 2017 15:59:41 +0200 Subject: [PATCH 04/31] Specifically define MV/LV station repr() --- edisgo/grid/components.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/edisgo/grid/components.py b/edisgo/grid/components.py index 5ae90095e..cecdfa5b5 100644 --- a/edisgo/grid/components.py +++ b/edisgo/grid/components.py @@ -316,6 +316,20 @@ class MVStation(Station): def __init__(self, **kwargs): super().__init__(**kwargs) + def __repr__(self, side=None): + repr_base = super().__repr__() + + # As we don't consider HV-MV transformers in PFA, we don't have to care + # about primary side bus of MV station. Hence, the general repr() + # currently returned, implicitely refers to the secondary side (MV level) + # if side == 'hv': + # return ''.join(['primary', repr_base]) + # elif side == 'mv': + # return ''.join(['secondary', repr_base]) + # else: + # return repr_base + return repr_base + class LVStation(Station): """LV Station object""" @@ -324,13 +338,14 @@ def __init__(self, **kwargs): super().__init__(**kwargs) def __repr__(self, side=None): + repr_base = super().__repr__() + if side == 'mv': - return 'primary' + return ''.join(['primary', repr_base]) elif side == 'lv': - return 'secondary' + return ''.join(['secondary', repr_base]) else: - return '' - + return repr_base class Line(Component): """ From 9271da511aac8680c381b24f671ef338c97ca2c3 Mon Sep 17 00:00:00 2001 From: gplessm Date: Wed, 6 Sep 2017 16:00:21 +0200 Subject: [PATCH 05/31] Use station's side specifiy representative --- edisgo/tools/interfaces.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/edisgo/tools/interfaces.py b/edisgo/tools/interfaces.py index 9aa11650e..16bd7762c 100644 --- a/edisgo/tools/interfaces.py +++ b/edisgo/tools/interfaces.py @@ -184,19 +184,19 @@ def mv_to_pypsa(network): if l['adj_nodes'][0] in lv_stations: line['bus0'].append( - '_'.join(['Bus', 'primary', repr(l['adj_nodes'][0])])) + '_'.join(['Bus', l['adj_nodes'][0].__repr__(side='mv')])) elif l['adj_nodes'][0] is network.mv_grid.station: line['bus0'].append( - '_'.join(['Bus', 'secondary', repr(network.mv_grid.station)])) + '_'.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', 'primary', repr(l['adj_nodes'][1])])) + '_'.join(['Bus', l['adj_nodes'][1].__repr__(side='mv')])) elif l['adj_nodes'][1] is network.mv_grid.station: line['bus1'].append( - '_'.join(['Bus', 'secondary', repr(network.mv_grid.station)])) + '_'.join(['Bus', l['adj_nodes'][1].__repr__(side='lv')])) else: line['bus1'].append('_'.join(['Bus', repr(l['adj_nodes'][1])])) @@ -212,12 +212,12 @@ def mv_to_pypsa(network): for lv_st in lv_stations: transformer_count = 1 # add primary side bus (bus0) - bus0_name = '_'.join(['Bus', 'primary', repr(lv_st)]) + bus0_name = '_'.join(['Bus', lv_st.__repr__(side='mv')]) bus['name'].append(bus0_name) bus['v_nom'].append(lv_st.grid.voltage_nom) # add secondary side bus (bus1) - bus1_name = '_'.join(['Bus', 'secondary', repr(lv_st)]) + bus1_name = '_'.join(['Bus', lv_st.__repr__(side='lv')]) bus['name'].append(bus1_name) bus['v_nom'].append(lv_st.transformers[0].voltage_op) @@ -238,7 +238,7 @@ def mv_to_pypsa(network): # create dataframe for MV stations (only secondary side bus) for mv_st in mv_stations: # add secondary side bus (bus1) - bus1_name = '_'.join(['Bus', 'secondary', repr(mv_st)]) + bus1_name = '_'.join(['Bus', mv_st.__repr__(side='mv')]) bus['name'].append(bus1_name) bus['v_nom'].append(mv_st.transformers[0].voltage_op) @@ -323,7 +323,7 @@ def attach_aggregated_lv_components(network, components): for _, gen_subtype in gen_type.items(): generator['name'].append(gen_subtype['name']) generator['bus'].append( - '_'.join(['Bus', 'secondary', repr(lv_grid_obj.station)])) + '_'.join(['Bus', lv_grid_obj.station.__repr__('lv')])) generator['control'].append('PQ') generator['p_nom'].append(gen_subtype['capacity']) generator['type'].append("") @@ -333,7 +333,7 @@ def attach_aggregated_lv_components(network, components): for sector, val in lv_grid.items(): load['name'].append('_'.join(['Load', sector, repr(lv_grid_obj)])) load['bus'].append( - '_'.join(['Bus', 'secondary', repr(lv_grid_obj.station)])) + '_'.join(['Bus', lv_grid_obj.station.__repr__('lv')])) components['Generator'] = pd.concat( [components['Generator'],pd.DataFrame(generator).set_index('name')]) @@ -408,11 +408,7 @@ def attach_aggregated_lv_timeseries(network): load.setdefault(sector, {}) load[sector].setdefault('timeseries_p', []) load[sector].setdefault('timeseries_q', []) - # load[sector]['timeseries_p'].append( - # lo.pypsa_timeseries(sector, 'p').rename( - # '_'.join([repr(lo), sector])).to_frame()) - # load[sector]['timeseries_q'].append(lo.pypsa_timeseries(sector, 'q').rename( - # '_'.join([repr(lo), sector])).to_frame()) + load[sector]['timeseries_p'].append( lo.pypsa_timeseries(sector, 'p').rename( '_'.join( @@ -603,7 +599,6 @@ def pypsa_bus_timeseries(network, buses, mode=None): :pandas:`pandas.DataFrame` Time series table in PyPSA format """ - # TODO: replace input for `set_snapshots` by DatetimeIndex constructed based on user input v_set_dict = {bus: [1, 1] for bus in buses} From f8f51b05f2f011e8bd299c14bb19ab7df8168360 Mon Sep 17 00:00:00 2001 From: gplessm Date: Wed, 6 Sep 2017 16:43:10 +0200 Subject: [PATCH 06/31] Add missing underscore --- edisgo/grid/components.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/edisgo/grid/components.py b/edisgo/grid/components.py index cecdfa5b5..1daa059a9 100644 --- a/edisgo/grid/components.py +++ b/edisgo/grid/components.py @@ -323,9 +323,9 @@ def __repr__(self, side=None): # about primary side bus of MV station. Hence, the general repr() # currently returned, implicitely refers to the secondary side (MV level) # if side == 'hv': - # return ''.join(['primary', repr_base]) + # return '_'.join(['primary', repr_base]) # elif side == 'mv': - # return ''.join(['secondary', repr_base]) + # return '_'.join(['secondary', repr_base]) # else: # return repr_base return repr_base @@ -341,9 +341,9 @@ def __repr__(self, side=None): repr_base = super().__repr__() if side == 'mv': - return ''.join(['primary', repr_base]) + return '_'.join(['primary', repr_base]) elif side == 'lv': - return ''.join(['secondary', repr_base]) + return '_'.join(['secondary', repr_base]) else: return repr_base From 056835a897cad215bd39b2d13c96bd139ddc4072 Mon Sep 17 00:00:00 2001 From: gplessm Date: Wed, 6 Sep 2017 18:58:27 +0200 Subject: [PATCH 07/31] One individual load per sector for loads in aggr. LAs --- edisgo/data/import_data.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/edisgo/data/import_data.py b/edisgo/data/import_data.py index f5b795bb1..dffd2e0fc 100644 --- a/edisgo/data/import_data.py +++ b/edisgo/data/import_data.py @@ -521,23 +521,23 @@ def _attach_aggregated(grid, aggregated, ding0_grid): grid=grid) } grid.graph.add_edge(grid.station, gen, line, type='line') - - load = Load( - geom=grid.station.geom, - consumption=la['load'], - grid=grid, - id='_'.join(['Load_aggregated', repr(grid)])) - - grid.graph.add_node(load, type='load') - - # connect aggregated load to MV station - line = {'line': Line( - id='line_aggr_load', - type=aggr_line_type, - length=.5, - grid=grid) - } - grid.graph.add_edge(grid.station, load, line, type='line') + for sector, sectoral_load in la['load'].items(): + load = Load( + geom=grid.station.geom, + consumption={sector: sectoral_load}, + grid=grid, + id='_'.join(['Load_aggregated', sector, repr(grid)])) + + grid.graph.add_node(load, type='load') + + # connect aggregated load to MV station + line = {'line': Line( + id='_'.join(['line_aggr_load', sector]), + type=aggr_line_type, + length=.5, + grid=grid) + } + grid.graph.add_edge(grid.station, load, line, type='line') def _validate_ding0_grid_import(mv_grid, ding0_mv_grid, lv_grid_mapping): From 1c1a64ea20e7a046804835c3c7aa2bddf5f7fd92 Mon Sep 17 00:00:00 2001 From: gplessm Date: Wed, 6 Sep 2017 22:49:45 +0200 Subject: [PATCH 08/31] Represent sectoral loads individually --- edisgo/grid/components.py | 31 ++++++++++++++----------------- edisgo/tools/interfaces.py | 32 ++++++++++++-------------------- 2 files changed, 26 insertions(+), 37 deletions(-) diff --git a/edisgo/grid/components.py b/edisgo/grid/components.py index 1daa059a9..0973e4fa2 100644 --- a/edisgo/grid/components.py +++ b/edisgo/grid/components.py @@ -104,21 +104,18 @@ def __init__(self, **kwargs): cos_phi = 0.95 - q_factor = tan(acos(0.95)) + q_factor = tan(acos(cos_phi)) + - avg_hourly_load = {k: v / hours_of_the_year / 1e3 - for k, v in self.consumption.items()} + avg_hourly_load = self.consumption[list(self.consumption.keys())[0]] / \ + hours_of_the_year / 1e3 rng = pd.date_range('1/1/2011', periods=hours_of_the_year, freq='H') ts_dict_p = { - (k, 'p'): [avg_hourly_load[k] * ( - 1 - q_factor)] * hours_of_the_year - for k in avg_hourly_load.keys()} + 'p': [avg_hourly_load * (1 - q_factor)] * hours_of_the_year} ts_dict_q = { - (k, 'q'): [avg_hourly_load[k] * ( - q_factor)] * hours_of_the_year - for k in avg_hourly_load.keys()} + 'q': [avg_hourly_load * (q_factor)] * hours_of_the_year} ts_dict = {**ts_dict_p, **ts_dict_q} self._timeseries = pd.DataFrame(ts_dict, index=rng) @@ -139,22 +136,16 @@ def timeseries(self): return self._timeseries - # @property - def pypsa_timeseries(self, sector, attr): + def pypsa_timeseries(self, attr): """Return time series in PyPSA format Parameters ---------- - sector : str - Sectoral load that is of interest. Valid sectors {residential, - retail, agricultural, industrial} attr : str Attribute name (PyPSA conventions). Choose from {p_set, q_set} """ - pypsa_component_name = '_'.join([repr(self), sector]) - - return self._timeseries[(sector, attr)] + return self._timeseries[attr] @property def consumption(self): @@ -179,6 +170,12 @@ def consumption(self): def consumption(self, cons_dict): self._consumption = cons_dict + def __repr__(self): + return '_'.join(['Load', + list(self.consumption.keys())[0], + repr(self.grid), + str(self.id)]) + class Generator(Component): """Generator object diff --git a/edisgo/tools/interfaces.py b/edisgo/tools/interfaces.py index 16bd7762c..05a503cea 100644 --- a/edisgo/tools/interfaces.py +++ b/edisgo/tools/interfaces.py @@ -171,9 +171,8 @@ def mv_to_pypsa(network): # create dataframes representing loads and associated buses for lo in loads: bus_name = '_'.join(['Bus', repr(lo)]) - for sector, val in lo.consumption.items(): - load['name'].append('_'.join([repr(lo), sector])) - load['bus'].append(bus_name) + load['name'].append(repr(lo)) + load['bus'].append(bus_name) bus['name'].append(bus_name) bus['v_nom'].append(lo.grid.voltage_nom) @@ -410,13 +409,9 @@ def attach_aggregated_lv_timeseries(network): load[sector].setdefault('timeseries_q', []) load[sector]['timeseries_p'].append( - lo.pypsa_timeseries(sector, 'p').rename( - '_'.join( - [repr(lo), repr(lv_grid), sector, 'p'])).to_frame()) + lo.pypsa_timeseries('p').rename(repr(lo)).to_frame()) load[sector]['timeseries_q'].append( - lo.pypsa_timeseries(sector, 'q').rename( - '_'.join( - [repr(lo), repr(lv_grid), sector, 'q'])).to_frame()) + lo.pypsa_timeseries('q').rename(repr(lo)).to_frame()) # TODO: now, there are single loads from eDisGo topology. Summarize these by sector and LV grid for sector, val in load.items(): @@ -481,13 +476,10 @@ def pypsa_load_timeseries(network, mode=None): # add MV grid loads if mode is 'mv' or mode is None: for load in network.mv_grid.graph.nodes_by_attribute('load'): - for sector in list(load.consumption.keys()): - mv_load_timeseries_q.append( - load.pypsa_timeseries(sector, 'q').rename( - '_'.join([repr(load), sector])).to_frame()) - mv_load_timeseries_p.append( - load.pypsa_timeseries(sector, 'p').rename( - '_'.join([repr(load), sector])).to_frame()) + mv_load_timeseries_q.append( + load.pypsa_timeseries('q').rename(repr(load)).to_frame()) + mv_load_timeseries_p.append( + load.pypsa_timeseries('p').rename(repr(load)).to_frame()) # add LV grid's loads if mode is 'lv' or mode is None: @@ -495,11 +487,11 @@ def pypsa_load_timeseries(network, mode=None): for load in lv_grid.graph.nodes_by_attribute('load'): for sector in list(load.consumption.keys()): lv_load_timeseries_q.append( - load.pypsa_timeseries(sector, 'q').rename( - '_'.join([repr(load), sector])).to_frame()) + load.pypsa_timeseries('q').rename( + repr(load)).to_frame()) lv_load_timeseries_p.append( - load.pypsa_timeseries(sector, 'p').rename( - '_'.join([repr(load), sector])).to_frame()) + load.pypsa_timeseries('p').rename( + repr(load)).to_frame()) 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) From fe5c79e6945f1a0ed80eee4ea02a60920ec81fe0 Mon Sep 17 00:00:00 2001 From: gplessm Date: Wed, 6 Sep 2017 23:29:09 +0200 Subject: [PATCH 09/31] Write voltage results to Results() --- edisgo/grid/network.py | 13 ++++++++++++ edisgo/tools/interfaces.py | 43 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/edisgo/grid/network.py b/edisgo/grid/network.py index 2d5e4c91b..13b79566a 100644 --- a/edisgo/grid/network.py +++ b/edisgo/grid/network.py @@ -515,3 +515,16 @@ def pfa_q(self): @pfa_q.setter def pfa_q(self, pypsa): self._pfa_q = pypsa + + @property + def pfa_v_mag_pu(self): + return self._pfa_v_mag_pu + + @pfa_v_mag_pu.setter + def pfa_v_mag_pu(self, pypsa): + if not self._pfa_v_mag_pu: + self._pfa_v_mag_pu = pypsa + else: + self._pfa_v_mag_pu = pd.concat([self._pfa_v_mag_pu, pypsa], axis=1) + + diff --git a/edisgo/tools/interfaces.py b/edisgo/tools/interfaces.py index 05a503cea..44a8272c7 100644 --- a/edisgo/tools/interfaces.py +++ b/edisgo/tools/interfaces.py @@ -678,6 +678,9 @@ def process_pfa_results(network, pypsa): """ + # TODO: move level specification to appropriate location + level = 'mv' + # line results q0 = abs(pypsa.lines_t['q0']) q1 = abs(pypsa.lines_t['q1']) @@ -685,3 +688,43 @@ def process_pfa_results(network, pypsa): p1 = abs(pypsa.lines_t['p1']) network.results.pfa_p = p0.where(p0 > p1, p1) network.results.pfa_q = q0.where(q0 > q1, q1) + + # results at nodes + + generators_names = [repr(g) for g in + network.mv_grid.graph.nodes_by_attribute('generator')] + generators_mapping = {v: k for k, v in + pypsa.generators.loc[generators_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} + lv_station_names = [repr(l) for l in + network.mv_grid.graph.nodes_by_attribute('lv_station')] + 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()} + + names_mapping = { + **generators_mapping, + **branch_t_mapping, + **mv_station_mapping_sec, + **lv_station_mapping_pri, + **lv_station_mapping_sec, + **loads_mapping + } + + # example how to rename generators + network.results.pfa_v_mag_pu = pypsa.buses_t['v_mag_pu'].rename( + columns=names_mapping) \ No newline at end of file From d9c22cb129e0dbc71962c009cdc5a9efdf6e4149 Mon Sep 17 00:00:00 2001 From: gplessm Date: Thu, 7 Sep 2017 20:38:21 +0200 Subject: [PATCH 10/31] Assign v_mag_pu in Multi Index columns --- edisgo/grid/network.py | 2 +- edisgo/tools/interfaces.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/edisgo/grid/network.py b/edisgo/grid/network.py index 13b79566a..f1b8781af 100644 --- a/edisgo/grid/network.py +++ b/edisgo/grid/network.py @@ -522,7 +522,7 @@ def pfa_v_mag_pu(self): @pfa_v_mag_pu.setter def pfa_v_mag_pu(self, pypsa): - if not self._pfa_v_mag_pu: + if self._pfa_v_mag_pu is None: self._pfa_v_mag_pu = pypsa else: self._pfa_v_mag_pu = pd.concat([self._pfa_v_mag_pu, pypsa], axis=1) diff --git a/edisgo/tools/interfaces.py b/edisgo/tools/interfaces.py index 44a8272c7..292ffeb8c 100644 --- a/edisgo/tools/interfaces.py +++ b/edisgo/tools/interfaces.py @@ -726,5 +726,11 @@ def process_pfa_results(network, pypsa): } # example how to rename generators - network.results.pfa_v_mag_pu = pypsa.buses_t['v_mag_pu'].rename( - columns=names_mapping) \ No newline at end of file + pfa_v_mag_pu = pypsa.buses_t['v_mag_pu'].rename(columns=names_mapping) + network.results.pfa_v_mag_pu = pd.concat( + {'mv': pfa_v_mag_pu[list(generators_mapping.values()) + + list(branch_t_mapping.values()) + + list(mv_station_mapping_sec.values()) + + list(lv_station_mapping_pri.values()) + + list(loads_mapping.values())], + 'lv': pfa_v_mag_pu[list(lv_station_mapping_sec.values())]}, axis=1) \ No newline at end of file From 85a9f79ca974d32958f9e2dd32b372739000bfd5 Mon Sep 17 00:00:00 2001 From: gplessm Date: Thu, 7 Sep 2017 21:05:03 +0200 Subject: [PATCH 11/31] Remove variable and correct comments --- edisgo/tools/interfaces.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/edisgo/tools/interfaces.py b/edisgo/tools/interfaces.py index 292ffeb8c..e77e95d5d 100644 --- a/edisgo/tools/interfaces.py +++ b/edisgo/tools/interfaces.py @@ -678,9 +678,6 @@ def process_pfa_results(network, pypsa): """ - # TODO: move level specification to appropriate location - level = 'mv' - # line results q0 = abs(pypsa.lines_t['q0']) q1 = abs(pypsa.lines_t['q1']) @@ -689,8 +686,7 @@ def process_pfa_results(network, pypsa): network.results.pfa_p = p0.where(p0 > p1, p1) network.results.pfa_q = q0.where(q0 > q1, q1) - # results at nodes - + # process results at nodes generators_names = [repr(g) for g in network.mv_grid.graph.nodes_by_attribute('generator')] generators_mapping = {v: k for k, v in @@ -725,7 +721,7 @@ def process_pfa_results(network, pypsa): **loads_mapping } - # example how to rename generators + # write voltage levels obtained from power flow to results object pfa_v_mag_pu = pypsa.buses_t['v_mag_pu'].rename(columns=names_mapping) network.results.pfa_v_mag_pu = pd.concat( {'mv': pfa_v_mag_pu[list(generators_mapping.values()) + From fc3f1860a6fff8503fa8d1bbe247e7621b963d44 Mon Sep 17 00:00:00 2001 From: gplessm Date: Thu, 7 Sep 2017 21:06:06 +0200 Subject: [PATCH 12/31] Provide access to voltage levels of specific nodes --- edisgo/examples/example.py | 11 +++++++++-- edisgo/grid/network.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/edisgo/examples/example.py b/edisgo/examples/example.py index 395046ff2..8b74a62ef 100644 --- a/edisgo/examples/example.py +++ b/edisgo/examples/example.py @@ -17,9 +17,16 @@ # pickle.dump(network, open('test_network.pkl', 'wb')) # network = pickle.load(open('test_network.pkl', 'rb')) -# export to pypsa +# Do non-linear power flow analysis with PyPSA # network.analyze(mode='mv') +# Print LV station secondary side voltage levels returned by PFA +# print(network.results.v_res( +# network.mv_grid.graph.nodes_by_attribute('lv_station'), 'lv')) + +# Print voltage level of all nodes +# print(network.results.pfa_v_mag_pu) + # for now create results object # ToDo: Werte in DataFrame als List oder Array? # results = Results() @@ -56,7 +63,7 @@ # else: # print("O MWh") -# reinforce_grid.reinforce_grid(network, results) +# from # liste aller lv grids # [_ for _ in network.mv_grid.lv_grids] diff --git a/edisgo/grid/network.py b/edisgo/grid/network.py index f1b8781af..411935cc2 100644 --- a/edisgo/grid/network.py +++ b/edisgo/grid/network.py @@ -528,3 +528,38 @@ def pfa_v_mag_pu(self, pypsa): self._pfa_v_mag_pu = pd.concat([self._pfa_v_mag_pu, pypsa], axis=1) + def v_res(self, nodes, level): + """ + Get resulting voltage level at node + + Parameters + ---------- + nodes : grid topology component or `list` grid topology components + level : str + Either 'mv' or 'lv'. Depending which grid level results you are + interested in. It is required to provide this argument in order + to distinguish voltage levels at primary and secondary side of the + transformer/LV station. + + Notes + ----- + Limitations + * When power flow analysis is performed for MV only (with aggregated + LV loads and generators) this methods only returns voltage at + secondary side busbar and not at load/generator + + """ + + labels = [repr(_) for _ in nodes] + + not_included = [_ for _ in labels + if _ not in list(self.pfa_v_mag_pu[level].columns)] + + labels_included = [_ for _ in labels if _ not in not_included] + + if not_included: + print("Voltage levels for {nodes} are not returned from PFA".format( + nodes=not_included)) + + + return self.pfa_v_mag_pu['lv'][labels_included] \ No newline at end of file From 1a62f0c472298f62b07640bdb47ed0ae58369cb2 Mon Sep 17 00:00:00 2001 From: gplessm Date: Fri, 8 Sep 2017 09:46:36 +0200 Subject: [PATCH 13/31] Add access to s_res of Results() --- edisgo/examples/example.py | 6 ++++++ edisgo/grid/network.py | 41 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/edisgo/examples/example.py b/edisgo/examples/example.py index 8b74a62ef..1819ac095 100644 --- a/edisgo/examples/example.py +++ b/edisgo/examples/example.py @@ -27,6 +27,12 @@ # Print voltage level of all nodes # print(network.results.pfa_v_mag_pu) +# Print apparent power at lines +# print(network.results.s_res([_['line'] for _ in network.mv_grid.graph.graph_edges()])) + +# Print voltage levels for all lines +# print(network.results.s_res()) + # for now create results object # ToDo: Werte in DataFrame als List oder Array? # results = Results() diff --git a/edisgo/grid/network.py b/edisgo/grid/network.py index acce59ff5..20e4cc2ea 100644 --- a/edisgo/grid/network.py +++ b/edisgo/grid/network.py @@ -6,6 +6,7 @@ from os import path import pandas as pd from edisgo.tools import interfaces +from math import sqrt class Network: @@ -526,6 +527,46 @@ def pfa_v_mag_pu(self, pypsa): self._pfa_v_mag_pu = pd.concat([self._pfa_v_mag_pu, pypsa], axis=1) + + def s_res(self, lines=None): + """ + Get resulting apparent power at line(s) + + Parameters + ---------- + lines : line object or list of + + Returns + ------- + DataFrame + Apparent power for `lines` + + """ + # TODO: exclud and report on lines where results are missing + # TODO: return all results if `lines` not given + + labels_included = [] + labels_not_included = [] + + labels = [repr(l) for l in lines] + + if lines is not None: + for label in labels: + if label in list(self.pfa_p.columns) and label in list(self.pfa_q.columns): + labels_included.append(label) + else: + labels_not_included.append(label) + print( + "Apparent power for {lines} are not returned from PFA".format( + lines=labels_not_included)) + else: + labels_included = labels + + s_res = ((self.pfa_p[labels_included] ** 2 + self.pfa_q[ + labels_included] ** 2) * 1e3).applymap(sqrt) + + return s_res + def v_res(self, nodes, level): """ Get resulting voltage level at node From 682fab0744fd8557dec71d6da0ae25fd23083031 Mon Sep 17 00:00:00 2001 From: gplessm Date: Sat, 9 Sep 2017 10:22:10 +0200 Subject: [PATCH 14/31] Remove TODOs --- edisgo/grid/network.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/edisgo/grid/network.py b/edisgo/grid/network.py index 20e4cc2ea..4ab91dd51 100644 --- a/edisgo/grid/network.py +++ b/edisgo/grid/network.py @@ -35,7 +35,6 @@ class Network: """ def __init__(self, **kwargs): - # TODO: sort out realistic use cases for data from PyPSA network if 'pypsa' not in kwargs.keys(): self._id = kwargs.get('id', None) self._metadata = kwargs.get('metadata', None) @@ -179,13 +178,7 @@ def analyze(self, mode=None): """ self.pypsa = mode - # TODO: remove export prior to merge - self.pypsa.export_to_csv_folder('edisgo2pypsa_export') - - # TODO: check if timeseries dataframes contain all data - # TODO: maybe 'v_mag_pu_set' is required for buses - # TODO: maybe there are lv station without load and generation at secondary side - # TODO: if missing, add slack generator + # run power flow analysis self.pypsa.pf(self.pypsa.snapshots) interfaces.process_pfa_results(self, self.pypsa) @@ -508,7 +501,6 @@ def pfa_p(self, pypsa): @property def pfa_q(self): - #tODO: return columns selected by passed grid topology components return self._pfa_q @pfa_q.setter @@ -542,8 +534,6 @@ def s_res(self, lines=None): Apparent power for `lines` """ - # TODO: exclud and report on lines where results are missing - # TODO: return all results if `lines` not given labels_included = [] labels_not_included = [] From 2345c332b2543304fbebc7875b149a3651ca6ef7 Mon Sep 17 00:00:00 2001 From: gplessm Date: Sat, 9 Sep 2017 10:35:31 +0200 Subject: [PATCH 15/31] Move functional call --- edisgo/grid/network.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/edisgo/grid/network.py b/edisgo/grid/network.py index 4ab91dd51..050d5f41b 100644 --- a/edisgo/grid/network.py +++ b/edisgo/grid/network.py @@ -176,7 +176,7 @@ def analyze(self, mode=None): representation to the PyPSA format and stores it to :attr:`self._pypsa`. """ - self.pypsa = mode + self.pypsa = interfaces.to_pypsa(self, mode) # run power flow analysis self.pypsa.pf(self.pypsa.snapshots) @@ -236,7 +236,7 @@ def pypsa(self): return self._pypsa @pypsa.setter - def pypsa(self, mode=None): + def pypsa(self, pypsa): """ Convert NetworkX based grid topology representation to PyPSA grid representation based on :pandas:`pandas.DataFrame` @@ -249,6 +249,8 @@ def pypsa(self, mode=None): for the conversion of the MV grid topology or `mode='lv'` for the conversion of the LV grid topology. Defaults to None which equals converting MV + LV. + pypsa : :pypsa:`pypsa.Network` + PyPSA network container Notes ----- @@ -261,11 +263,8 @@ def pypsa(self, mode=None): https://github.com/openego/eDisGo/issues/18 * Where to find and adjust power flow analysis defining parameters - Returns - ------- - .. TODO: describe return """ - self._pypsa = interfaces.to_pypsa(self, mode) + self._pypsa = pypsa @property def scenario(self): From 889491b703b0b15c5d7872cf92999d0b24b65a44 Mon Sep 17 00:00:00 2001 From: gplessm Date: Sat, 9 Sep 2017 13:12:35 +0200 Subject: [PATCH 16/31] Refactor PyPSA interface file * Reorganize code * Rename functions * Update docstrings --- edisgo/grid/network.py | 6 +- edisgo/tools/{interfaces.py => pypsa_io.py} | 360 +++++++++++--------- 2 files changed, 209 insertions(+), 157 deletions(-) rename edisgo/tools/{interfaces.py => pypsa_io.py} (87%) diff --git a/edisgo/grid/network.py b/edisgo/grid/network.py index 050d5f41b..aef4c3bb8 100644 --- a/edisgo/grid/network.py +++ b/edisgo/grid/network.py @@ -5,7 +5,7 @@ from os import path import pandas as pd -from edisgo.tools import interfaces +from edisgo.tools import pypsa_io from math import sqrt @@ -176,12 +176,12 @@ def analyze(self, mode=None): representation to the PyPSA format and stores it to :attr:`self._pypsa`. """ - self.pypsa = interfaces.to_pypsa(self, mode) + self.pypsa = pypsa_io.to_pypsa(self, mode) # run power flow analysis self.pypsa.pf(self.pypsa.snapshots) - interfaces.process_pfa_results(self, self.pypsa) + pypsa_io.process_pfa_results(self, self.pypsa) def reinforce(self): diff --git a/edisgo/tools/interfaces.py b/edisgo/tools/pypsa_io.py similarity index 87% rename from edisgo/tools/interfaces.py rename to edisgo/tools/pypsa_io.py index e77e95d5d..ddc08a662 100644 --- a/edisgo/tools/interfaces.py +++ b/edisgo/tools/pypsa_io.py @@ -1,3 +1,10 @@ +""" +This modules 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 pandas as pd from math import pi, sqrt, floor from pypsa import Network as PyPSANetwork @@ -7,17 +14,52 @@ def to_pypsa(network, mode): """ - See `Network.pypsa` - + Translate graph based grid representation to PyPSA Network + + For details from a user perspective see API documention of + :meth:`~.grid.network.Network.analyze` of the grid container + :class:`~.grid.network.Network`. + + Translating eDisGo's grid topology to PyPSA representation is structured + into tranlating 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. + Parameters ---------- - network - mode + network : Network + eDisGo grid container + mode : str + Determines grid levels that are translated to + `PyPSA grid representation + `_. Specify + + * 'mv' to export MV grid level only. This includes cumulative load and + generation from underlying LV grid aggregated at respective LV + station. + * 'lv' to export LV grid level only Returns ------- - :pypsa:`pypsa.Network` - PyPSA representation of grid topology + + PyPSA Network """ # get topology and time series data @@ -28,25 +70,25 @@ def to_pypsa(network, mode): lv_components) elif mode is 'mv': mv_components = mv_to_pypsa(network) - components = attach_aggregated_lv_components( + components = add_aggregated_lv_components( network, mv_components) - timeseries_load_p, timeseries_load_q = pypsa_load_timeseries( + timeseries_load_p, timeseries_load_q = _pypsa_load_timeseries( network, - mode='mv') + mode=mode) - timeseries_gen_p, timeseries_gen_q = pypsa_generator_timeseries( + timeseries_gen_p, timeseries_gen_q = _pypsa_generator_timeseries( network, - mode='mv') + mode=mode) - timeseries_bus_v_set = pypsa_bus_timeseries( + timeseries_bus_v_set = _pypsa_bus_timeseries( network, components['Bus'].index.tolist()) ts_gen_lv_aggr_p, \ ts_gen_lv_aggr_q, \ - ts_load_lv_aggr_p, ts_load_lv_aggr_q = attach_aggregated_lv_timeseries( + ts_load_lv_aggr_p, ts_load_lv_aggr_q = _pypsa_timeseries_aggregated_at_lv_station( network) # Concat MV and LV (aggregated) time series @@ -109,7 +151,19 @@ def to_pypsa(network, mode): def mv_to_pypsa(network): - """Translate grid topology representation to PyPSA format""" + """Translate MV grid topology representation to PyPSA format + + MV grid topology translated here includes + + * MV station (no transformer, see :meth:`~.grid.network.Network.analyze`) + * Loads, Generators, Lines, 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 + """ generators = network.mv_grid.graph.nodes_by_attribute('generator') loads = network.mv_grid.graph.nodes_by_attribute('load') @@ -258,13 +312,35 @@ def mv_to_pypsa(network): return components -def attach_aggregated_lv_components(network, components): +def lv_to_pypsa(): + """ + Convert LV grid topology to PyPSA representation + + Returns + ------- + + """ + + +def combine_mv_and_lv(): + """Combine MV and LV grid topology in PyPSA format + + Idea for implementation + ----------------------- + Merge all DataFrames except for LV transformers which are already included + in the MV grid PyPSA representation + + """ + pass + + +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 repesting the technology type. + these aggregated respecting the technology type. Parameters ---------- @@ -275,7 +351,7 @@ def attach_aggregated_lv_components(network, components): Returns ------- - dict of :pandas:`pandas.DataFrame` + :obj:`dict` of :pandas:`pandas.DataFrame` The dictionary components passed to the function is returned altered. """ generators = {} @@ -342,115 +418,7 @@ def attach_aggregated_lv_components(network, components): return components -def attach_aggregated_lv_timeseries(network): - """ - - Parameters - ---------- - network : Network - The eDisGo grid topology model overall container - - Returns - ------- - tuple of :pandas:`pandas.DataFrame` - Altered time series DataFrames. Tuple of size for containing DataFrames - that represent - - 1. 'p_set' of aggregated Generation at each LV station - 2. 'q_set' of aggregated Generation at each LV station - 3. 'p_set' of aggregated Load at each LV station - 4. 'q_set' of aggregated Load at each LV station - - - """ - - generation_p = [] - generation_q = [] - load_p = [] - load_q = [] - - for lv_grid in network.mv_grid.lv_grids: - # Determine aggregated generation at LV stations - generation = {} - for gen in lv_grid.graph.nodes_by_attribute('generator'): - # 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()) - generation[gen.type][gen.subtype]['timeseries_q'].append( - gen.pypsa_timeseries('q').rename(gen_name).to_frame()) - - 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()) - - # 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()) - load[sector]['timeseries_q'].append( - lo.pypsa_timeseries('q').rename(repr(lo)).to_frame()) - # TODO: now, there are single loads from eDisGo topology. Summarize these by sector and LV grid - - 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 pd.concat(generation_p, axis=1), \ - pd.concat(generation_q, axis=1), \ - pd.concat(load_p, axis=1), \ - pd.concat(load_q, axis=1) - - -def lv_to_pypsa(): - """ - Convert LV grid topology to PyPSA representation - - Returns - ------- - - """ - - -def combine_mv_and_lv(): - """Combine MV and LV grid topology in PyPSA format - - Idea for implementation - ----------------------- - Merge all DataFrames except for LV transformers which are already included - in the MV grid PyPSA representation - - """ - pass - - -def pypsa_load_timeseries(network, mode=None): +def _pypsa_load_timeseries(network, mode=None): """Timeseries in PyPSA compatible format for load instances Parameters @@ -501,7 +469,7 @@ def pypsa_load_timeseries(network, mode=None): return load_df_p, load_df_q -def pypsa_generator_timeseries(network, mode=None): +def _pypsa_generator_timeseries(network, mode=None): """Timeseries in PyPSA compatible format for generator instances Parameters @@ -549,29 +517,7 @@ def pypsa_generator_timeseries(network, mode=None): return gen_df_p, gen_df_q - -def _check_topology(mv_components): - buses = mv_components['Bus'].index.tolist() - line_buses = mv_components['Line']['bus0'].tolist() + \ - mv_components['Line']['bus1'].tolist() - load_buses = mv_components['Load']['bus'].tolist() - generator_buses = mv_components['Generator']['bus'].tolist() - transformer_buses = mv_components['Transformer']['bus0'].tolist() + \ - mv_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)) - - -def pypsa_bus_timeseries(network, buses, mode=None): +def _pypsa_bus_timeseries(network, buses, mode=None): """Timeseries in PyPSA compatible format for generator instances Parameters @@ -600,6 +546,111 @@ def pypsa_bus_timeseries(network, buses, mode=None): return v_set_df +def _pypsa_timeseries_aggregated_at_lv_station(network): + """ + + Parameters + ---------- + network : Network + The eDisGo grid topology model overall container + + Returns + ------- + tuple of :pandas:`pandas.DataFrame` + Altered time series DataFrames. Tuple of size for containing DataFrames + that represent + + 1. 'p_set' of aggregated Generation at each LV station + 2. 'q_set' of aggregated Generation at each LV station + 3. 'p_set' of aggregated Load at each LV station + 4. 'q_set' of aggregated Load at each LV station + """ + + generation_p = [] + generation_q = [] + load_p = [] + load_q = [] + + for lv_grid in network.mv_grid.lv_grids: + # Determine aggregated generation at LV stations + generation = {} + for gen in lv_grid.graph.nodes_by_attribute('generator'): + # 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()) + generation[gen.type][gen.subtype]['timeseries_q'].append( + gen.pypsa_timeseries('q').rename(gen_name).to_frame()) + + 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()) + + # 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()) + load[sector]['timeseries_q'].append( + lo.pypsa_timeseries('q').rename(repr(lo)).to_frame()) + # TODO: now, there are single loads from eDisGo topology. Summarize these by sector and LV grid + + 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 pd.concat(generation_p, axis=1), \ + pd.concat(generation_q, axis=1), \ + pd.concat(load_p, axis=1), \ + pd.concat(load_q, axis=1) + + +def _check_topology(mv_components): + buses = mv_components['Bus'].index.tolist() + line_buses = mv_components['Line']['bus0'].tolist() + \ + mv_components['Line']['bus1'].tolist() + load_buses = mv_components['Load']['bus'].tolist() + generator_buses = mv_components['Generator']['bus'].tolist() + transformer_buses = mv_components['Transformer']['bus0'].tolist() + \ + mv_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)) + + def _check_integrity_of_pypsa(pypsa_network): """""" @@ -665,16 +716,17 @@ def process_pfa_results(network, pypsa): ---------- network : Network The eDisGo grid topology model overall container - pypsa : :pypsa:`pypsa.Network` - Network container of `PyPSA `_ + pypsa : + `Network container `_ + of PyPSA Returns ------- See Also -------- - edisgo.grid.network.Network.results : Understand how results of power flow - analysis are structured in eDisGo. + :class:`~.grid.network.Results` + Understand how results of power flow analysis are structured in eDisGo. """ From 1bf3117eb4457432fbc964d99847640ee60670af Mon Sep 17 00:00:00 2001 From: gplessm Date: Sat, 9 Sep 2017 13:12:53 +0200 Subject: [PATCH 17/31] Update docs --- doc/api/edisgo.data.rst | 8 +++---- doc/api/edisgo.flex_opt.rst | 46 +++++++++++++++++++++++++++++++++++++ doc/api/edisgo.grid.rst | 16 ++++++------- doc/api/edisgo.rst | 1 + doc/api/edisgo.tools.rst | 30 ++++++++++++++++++++++++ doc/conf.py | 4 +++- 6 files changed, 92 insertions(+), 13 deletions(-) create mode 100644 doc/api/edisgo.flex_opt.rst create mode 100644 doc/api/edisgo.tools.rst diff --git a/doc/api/edisgo.data.rst b/doc/api/edisgo.data.rst index 3694b88d3..9548a2823 100644 --- a/doc/api/edisgo.data.rst +++ b/doc/api/edisgo.data.rst @@ -1,11 +1,11 @@ -edisgo.data package -=================== +edisgo\.data package +==================== Submodules ---------- -edisgo.data.import_data module ------------------------------- +edisgo\.data\.import\_data module +--------------------------------- .. automodule:: edisgo.data.import_data :members: diff --git a/doc/api/edisgo.flex_opt.rst b/doc/api/edisgo.flex_opt.rst new file mode 100644 index 000000000..0e1e310a7 --- /dev/null +++ b/doc/api/edisgo.flex_opt.rst @@ -0,0 +1,46 @@ +edisgo\.flex\_opt package +========================= + +Submodules +---------- + +edisgo\.flex\_opt\.check\_tech\_constraints module +-------------------------------------------------- + +.. automodule:: edisgo.flex_opt.check_tech_constraints + :members: + :undoc-members: + :show-inheritance: + +edisgo\.flex\_opt\.costs module +------------------------------- + +.. automodule:: edisgo.flex_opt.costs + :members: + :undoc-members: + :show-inheritance: + +edisgo\.flex\_opt\.reinforce\_grid module +----------------------------------------- + +.. automodule:: edisgo.flex_opt.reinforce_grid + :members: + :undoc-members: + :show-inheritance: + +edisgo\.flex\_opt\.reinforce\_measures module +--------------------------------------------- + +.. automodule:: edisgo.flex_opt.reinforce_measures + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: edisgo.flex_opt + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/edisgo.grid.rst b/doc/api/edisgo.grid.rst index 9564f3fd7..34a250124 100644 --- a/doc/api/edisgo.grid.rst +++ b/doc/api/edisgo.grid.rst @@ -1,27 +1,27 @@ -edisgo.grid package -=================== +edisgo\.grid package +==================== Submodules ---------- -edisgo.grid.components module ------------------------------ +edisgo\.grid\.components module +------------------------------- .. automodule:: edisgo.grid.components :members: :undoc-members: :show-inheritance: -edisgo.grid.grids module ------------------------- +edisgo\.grid\.grids module +-------------------------- .. automodule:: edisgo.grid.grids :members: :undoc-members: :show-inheritance: -edisgo.grid.network module --------------------------- +edisgo\.grid\.network module +---------------------------- .. automodule:: edisgo.grid.network :members: diff --git a/doc/api/edisgo.rst b/doc/api/edisgo.rst index 5615cd80f..690fe154d 100644 --- a/doc/api/edisgo.rst +++ b/doc/api/edisgo.rst @@ -7,6 +7,7 @@ Subpackages .. toctree:: edisgo.data + edisgo.flex_opt edisgo.grid edisgo.tools diff --git a/doc/api/edisgo.tools.rst b/doc/api/edisgo.tools.rst new file mode 100644 index 000000000..33e6bc020 --- /dev/null +++ b/doc/api/edisgo.tools.rst @@ -0,0 +1,30 @@ +edisgo\.tools package +===================== + +Submodules +---------- + +edisgo\.tools\.config module +---------------------------- + +.. automodule:: edisgo.tools.config + :members: + :undoc-members: + :show-inheritance: + +edisgo\.tools\.pypsa\_io module +------------------------------- + +.. automodule:: edisgo.tools.pypsa_io + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: edisgo.tools + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/conf.py b/doc/conf.py index f8acff2b1..52529c838 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -355,4 +355,6 @@ def __getattr__(cls, name): intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} # Numbered figures -numfig = True \ No newline at end of file +numfig = True + +autodoc_member_order = 'bysource' \ No newline at end of file From e82ca862e852173247fae6ebdb36431f0ca2997d Mon Sep 17 00:00:00 2001 From: gplessm Date: Sat, 9 Sep 2017 13:33:03 +0200 Subject: [PATCH 18/31] Extend `to_pypsa` docstring --- edisgo/tools/pypsa_io.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/edisgo/tools/pypsa_io.py b/edisgo/tools/pypsa_io.py index ddc08a662..9953d6f0d 100644 --- a/edisgo/tools/pypsa_io.py +++ b/edisgo/tools/pypsa_io.py @@ -42,6 +42,16 @@ def to_pypsa(network, mode): :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. + Parameters ---------- network : Network From 219ed81edbd34763eeb5ef44a1efc6a67189a569 Mon Sep 17 00:00:00 2001 From: gplessm Date: Sat, 9 Sep 2017 14:10:27 +0200 Subject: [PATCH 19/31] Add PyPSA units --- doc/units_table.csv | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/units_table.csv b/doc/units_table.csv index 9060d7484..4269348bc 100644 --- a/doc/units_table.csv +++ b/doc/units_table.csv @@ -1,9 +1,9 @@ Variable;Symbol;Unit;Comment Current;I;A; Length;l;km; -Active Power;P;kW; -Reactive Power;Q;kvar; -Apparent Power;S;kVA; +Active Power;P;kW;In PyPSA representation (:attr:`~.grid.network.Network.pypsa`) MW are used +Reactive Power;Q;kvar;In PyPSA representation (:attr:`~.grid.network.Network.pypsa`) MVar are used +Apparent Power;S;kVA;In PyPSA representation (:attr:`~.grid.network.Network.pypsa`) MVA are used Resistance;R;Ohm or Ohm/km;Ohm/km applies to lines Reactance;X;Ohm or Ohm/km;Ohm/km applies to lines Voltage;V;kV; From 51274c16514fe0d0cbe2cf03819cedc1b7de2a55 Mon Sep 17 00:00:00 2001 From: gplessm Date: Sat, 9 Sep 2017 16:02:26 +0200 Subject: [PATCH 20/31] Improve docs --- edisgo/grid/network.py | 111 ++++++++++++++++++++++++----------------- 1 file changed, 65 insertions(+), 46 deletions(-) diff --git a/edisgo/grid/network.py b/edisgo/grid/network.py index aef4c3bb8..a050f3398 100644 --- a/edisgo/grid/network.py +++ b/edisgo/grid/network.py @@ -14,24 +14,31 @@ class Network: Used as container for all data related to a single :class:`~.grid.grids.MVGrid`. + Provides the top-level API for invocation of data import, analysis of + hosting capacity, grid reinforce and flexibility measures. + + Examples + -------- + Assuming you the Ding0 `ding0_data.pkl` in CWD + + Create eDisGo Network object by loading Ding0 file + + >>> from edisgo.grid.network import Network + >>> network = Network.import_from_ding0('ding0_data.pkl')) + + Analyze hosting capacity for MV grid level + + >>> network.analyze(mode='mv') + + Print LV station secondary side voltage levels returned by PFA + + >>> lv_stations = network.mv_grid.graph.nodes_by_attribute('lv_station') + >>> print(network.results.v_res(lv_stations, 'lv')) Attributes ---------- - _id : :obj:`str` - Name of network - _equipment_data : :obj:`dict` of :pandas:`pandas.DataFrame` - Electrical equipment such as lines and transformers - _config : ??? - #TODO: TBD _metadata : :obj:`dict` Metadata of Network such as ? - _data_sources : :obj:`dict` of :obj:`str` - Data Sources of grid, generators etc. - Keys: 'grid', 'generators', ? - _scenario : :class:`~.grid.grids.Scenario` - Scenario which is used for calculations - _pypsa : :pypsa:`pypsa.Network` - PyPSA representation of grid topology """ def __init__(self, **kwargs): @@ -154,12 +161,21 @@ def import_generators(self): def analyze(self, mode=None): """Analyzes the grid by power flow analysis - .. TODO: explain a bunch of details and example how to use - * what type of power flow is performed - * how to select time range for analysis - * representation of grid in PFA - * No HV-MV transformer - * ... + Analyze the grid for violations of hosting capacity. Means, perform a + power flow analysis and obtain voltages at nodes (load, generator, + stations/transformers and branch tees) and active/reactive power at + lines. + + The power flow analysis can be performed for both grid levels MV and LV + and for both of them individually. Use `mode` to choose (defaults to + MV + LV). + + A static `non-linear power flow analysis is performed using PyPSA + `_. + The high-voltage to medium-voltage transformer are not included in the + analysis. The slack bus is defined at secondary side of these + transformers assuming an ideal tap changer. Hence, potential overloading + of the transformers is not studied here. Parameters ---------- @@ -175,6 +191,11 @@ def analyze(self, mode=None): The current implementation always translates the grid topology representation to the PyPSA format and stores it to :attr:`self._pypsa`. + TODO: extend doctring by + + * How power plants are modeled, if possible use a link + * Where to find and adjust power flow analysis defining parameters + """ self.pypsa = pypsa_io.to_pypsa(self, mode) @@ -204,7 +225,11 @@ def config(self): @property def equipment_data(self): - """Returns equipment data object""" + """Returns equipment data object + + Electrical equipment such as lines and transformers + :obj:`dict` of :pandas:`pandas.DataFrame` + """ return self._equipment_data @property @@ -233,37 +258,30 @@ def set_data_source(self, key, data_source): @property def pypsa(self): - return self._pypsa - - @pypsa.setter - def pypsa(self, pypsa): """ - Convert NetworkX based grid topology representation to PyPSA grid - representation based on :pandas:`pandas.DataFrame` + PyPSA grid representation + + A grid topology representation based on + :pandas:`pandas.DataFrame`. The overall container object of + this data model the + `PyPSA network `_ + is assigned to this attribute. + This allows as well to overwrite data. Parameters ---------- - mode: str - Allows to toggle between converting the whole grid topology - (MV + LV), only MV or only LV. Therefore, either specify `mode='mv'` - for the conversion of the MV grid topology or `mode='lv'` - for the conversion of the LV grid topology. - Defaults to None which equals converting MV + LV. - pypsa : :pypsa:`pypsa.Network` - PyPSA network container - - Notes - ----- - Tell about - * How the PyPSA interface is constructed, i.e. splitted in MV and LV - with combination or attachment of aggregated LV load and generation - to MV part - * How power plants are modeled, if possible use a link - * Recommendations for further development: - https://github.com/openego/eDisGo/issues/18 - * Where to find and adjust power flow analysis defining parameters + pypsa: + The `PyPSA network `_ + container + Returns + ------- + PyPSA grid representation """ + return self._pypsa + + @pypsa.setter + def pypsa(self, pypsa): self._pypsa = pypsa @property @@ -562,7 +580,8 @@ def v_res(self, nodes, level): Parameters ---------- - nodes : grid topology component or `list` grid topology components + nodes : :obj:`list` of :class:`~.grid.components.Load`, :class:`~.grid.components.Generator`, ... + grid topology component or `list` grid topology components level : str Either 'mv' or 'lv'. Depending which grid level results you are interested in. It is required to provide this argument in order From 57961f788fed0f6fd07170f96bfb674ac365f248 Mon Sep 17 00:00:00 2001 From: gplessm Date: Sat, 9 Sep 2017 18:17:29 +0200 Subject: [PATCH 21/31] Add return doc --- edisgo/tools/pypsa_io.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/edisgo/tools/pypsa_io.py b/edisgo/tools/pypsa_io.py index 9953d6f0d..0592631b7 100644 --- a/edisgo/tools/pypsa_io.py +++ b/edisgo/tools/pypsa_io.py @@ -173,6 +173,18 @@ def mv_to_pypsa(network): ---------- network : Network eDisGo grid container + + Returns + ------- + dict of :pandas:`pandas.DataFrame` + A DataFrame for each type of PyPSA components constituting the grid + topology. Keys included + + * 'Generator' + * 'Load' + * 'Line' + * 'BranchTee' + * 'Tranformer' """ generators = network.mv_grid.graph.nodes_by_attribute('generator') From 40812481d2a2cd17856cc8286403eee216fc25b9 Mon Sep 17 00:00:00 2001 From: gplessm Date: Sun, 10 Sep 2017 18:45:29 +0200 Subject: [PATCH 22/31] Put grid id into representative of BranchTee --- edisgo/grid/components.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/edisgo/grid/components.py b/edisgo/grid/components.py index 0973e4fa2..e198b5d3d 100644 --- a/edisgo/grid/components.py +++ b/edisgo/grid/components.py @@ -306,6 +306,9 @@ class BranchTee(Component): def __init__(self, **kwargs): super().__init__(**kwargs) + def __repr__(self): + return '_'.join([self.__class__.__name__, repr(self.grid), str(self._id)]) + class MVStation(Station): """MV Station object""" From 00d783d18222d9cead94575a2c932bcd0b4c5a91 Mon Sep 17 00:00:00 2001 From: gplessm Date: Sun, 10 Sep 2017 19:02:46 +0200 Subject: [PATCH 23/31] Check for uniqueness of labels in PyPSA DataFrames --- edisgo/grid/network.py | 5 +++ edisgo/tools/pypsa_io.py | 69 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/edisgo/grid/network.py b/edisgo/grid/network.py index a050f3398..795b38971 100644 --- a/edisgo/grid/network.py +++ b/edisgo/grid/network.py @@ -196,6 +196,11 @@ def analyze(self, mode=None): * How power plants are modeled, if possible use a link * Where to find and adjust power flow analysis defining parameters + See Also + -------- + :func:~.tools.pypsa_io.to_pypsa + Translator to PyPSA data format + """ self.pypsa = pypsa_io.to_pypsa(self, mode) diff --git a/edisgo/tools/pypsa_io.py b/edisgo/tools/pypsa_io.py index 0592631b7..db383bfcc 100644 --- a/edisgo/tools/pypsa_io.py +++ b/edisgo/tools/pypsa_io.py @@ -51,6 +51,8 @@ def to_pypsa(network, mode): * 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 ---------- @@ -728,6 +730,73 @@ def _check_integrity_of_pypsa(pypsa_network): "{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)) + + def process_pfa_results(network, pypsa): """ From ab13787d5f8eb06ab00d9a1781b0e9ab95cf08d3 Mon Sep 17 00:00:00 2001 From: gplessm Date: Sun, 10 Sep 2017 21:18:05 +0200 Subject: [PATCH 24/31] Implement LV grid PyPSA translation for combined MV+LV analysis --- edisgo/tools/pypsa_io.py | 159 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 148 insertions(+), 11 deletions(-) diff --git a/edisgo/tools/pypsa_io.py b/edisgo/tools/pypsa_io.py index db383bfcc..b57e5aed8 100644 --- a/edisgo/tools/pypsa_io.py +++ b/edisgo/tools/pypsa_io.py @@ -80,6 +80,20 @@ def to_pypsa(network, mode): lv_components = lv_to_pypsa(network) components = combine_mv_and_lv(mv_components, lv_components) + #TODO: translate voltage appropriately: in kVA + # TODO: check unit of nominal capacity of generators: should be MW + + timeseries_load_p, timeseries_load_q = _pypsa_load_timeseries( + network, + mode=mode) + + timeseries_gen_p, timeseries_gen_q = _pypsa_generator_timeseries( + network, + mode=mode) + + timeseries_bus_v_set = _pypsa_bus_timeseries( + network, + components['Bus'].index.tolist()) elif mode is 'mv': mv_components = mv_to_pypsa(network) components = add_aggregated_lv_components( @@ -235,7 +249,7 @@ def mv_to_pypsa(network): generator['bus'].append(bus_name) # TODO: revisit 'control' for dispatchable power plants generator['control'].append('PQ') - generator['p_nom'].append(gen.nominal_capacity) + generator['p_nom'].append(gen.nominal_capacity / 1e3) generator['type'].append('_'.join([gen.type, gen.subtype])) bus['name'].append(bus_name) @@ -336,17 +350,124 @@ def mv_to_pypsa(network): return components -def lv_to_pypsa(): +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` + A DataFrame for each type of PyPSA components constituting the grid + topology. Keys included + * 'Generator' + * 'Load' + * 'Line' + * 'BranchTee' """ + generators = [] + loads = [] + branch_tees = [] + lines = [] + lv_stations = [] + + for lv_grid in network.mv_grid.lv_grids: + generators.extend(lv_grid.graph.nodes_by_attribute('generator')) + 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.graph_edges()) + lv_stations.extend(lv_grid.graph.nodes_by_attribute('lv_station')) -def combine_mv_and_lv(): + omega = 2 * pi * 50 + + generator = {'name': [], + 'bus': [], + 'control': [], + 'p_nom': [], + 'type': []} + + bus = {'name': [], 'v_nom': []} + + load = {'name': [], 'bus': []} + + line = {'name': [], + 'bus0': [], + 'bus1': [], + 'type': [], + 'x': [], + 'r': [], + 's_nom': [], + 'length': []} + + # 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) + # TODO: revisit 'control' for dispatchable power plants + 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 / 1e3) + + # 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 / 1e3) + + # 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 / 1e3) + + # 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'] * omega / 1e3 * l['line'].length / 1e3) + line['r'].append(l['line'].type['R'] * l['line'].length / 1e3) + line['s_nom'].append( + sqrt(3) * l['line'].type['I_max_th'] * l['line'].type['U_n'] / 1e3) + line['length'].append(l['line'].length / 1e3) + + 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')} + + return lv_components + + +def combine_mv_and_lv(mv, lv): """Combine MV and LV grid topology in PyPSA format Idea for implementation @@ -355,7 +476,14 @@ def combine_mv_and_lv(): in the MV grid PyPSA representation """ - pass + + combined = { + c: pd.concat([mv[c], lv[c]], axis=0) for c in list(lv.keys()) + } + + combined['Transformer'] = mv['Transformer'] + + return combined def add_aggregated_lv_components(network, components): @@ -478,12 +606,21 @@ def _pypsa_load_timeseries(network, mode=None): for lv_grid in network.mv_grid.lv_grids: for load in lv_grid.graph.nodes_by_attribute('load'): for sector in list(load.consumption.keys()): - lv_load_timeseries_q.append( - load.pypsa_timeseries('q').rename( - repr(load)).to_frame()) - lv_load_timeseries_p.append( - load.pypsa_timeseries('p').rename( - repr(load)).to_frame()) + # for sector in list(list(load.consumption.keys())[0]): + # TODO: remove consideration of only first sector + # now, if a load object has consumption in multiple sectors + # (like currently only industrial/retail) the consumption is + # implicitly assigned to the industrial sector when being + # exported to pypsa. + if sector != 'retail': + lv_load_timeseries_q.append( + load.pypsa_timeseries('q').rename( + repr(load)).to_frame()) + lv_load_timeseries_p.append( + load.pypsa_timeseries('p').rename( + repr(load)).to_frame()) + else: + print("retail") 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) @@ -763,7 +900,7 @@ def _check_integrity_of_pypsa(pypsa_network): 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[ From e719f363a22cb5bdf3044115dbb50499916ff73c Mon Sep 17 00:00:00 2001 From: gplessm Date: Mon, 11 Sep 2017 00:14:36 +0200 Subject: [PATCH 25/31] Repair s_res --- edisgo/grid/network.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/edisgo/grid/network.py b/edisgo/grid/network.py index 795b38971..b65679c95 100644 --- a/edisgo/grid/network.py +++ b/edisgo/grid/network.py @@ -557,12 +557,11 @@ def s_res(self, lines=None): """ - labels_included = [] - labels_not_included = [] - - labels = [repr(l) for l in lines] if lines is not None: + labels_included = [] + labels_not_included = [] + labels = [repr(l) for l in lines] for label in labels: if label in list(self.pfa_p.columns) and label in list(self.pfa_q.columns): labels_included.append(label) @@ -572,7 +571,7 @@ def s_res(self, lines=None): "Apparent power for {lines} are not returned from PFA".format( lines=labels_not_included)) else: - labels_included = labels + labels_included = self.pfa_p.columns s_res = ((self.pfa_p[labels_included] ** 2 + self.pfa_q[ labels_included] ** 2) * 1e3).applymap(sqrt) From 571dd4508bf134220f4a14e75b829d87e92ae9f3 Mon Sep 17 00:00:00 2001 From: gplessm Date: Mon, 11 Sep 2017 00:15:07 +0200 Subject: [PATCH 26/31] Add LV results to Network.results.v_mag_pu --- edisgo/tools/pypsa_io.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/edisgo/tools/pypsa_io.py b/edisgo/tools/pypsa_io.py index b57e5aed8..045b423e3 100644 --- a/edisgo/tools/pypsa_io.py +++ b/edisgo/tools/pypsa_io.py @@ -992,13 +992,35 @@ def process_pfa_results(network, pypsa): pypsa.loads.loc[loads_names][ 'bus'].to_dict().items()} + lv_generators_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_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_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, **branch_t_mapping, **mv_station_mapping_sec, **lv_station_mapping_pri, **lv_station_mapping_sec, - **loads_mapping + **loads_mapping, + **lv_generators_mapping, + **lv_loads_mapping, + **lv_branch_t_mapping } # write voltage levels obtained from power flow to results object @@ -1009,4 +1031,7 @@ def process_pfa_results(network, pypsa): list(mv_station_mapping_sec.values()) + list(lv_station_mapping_pri.values()) + list(loads_mapping.values())], - 'lv': pfa_v_mag_pu[list(lv_station_mapping_sec.values())]}, axis=1) \ No newline at end of file + 'lv': pfa_v_mag_pu[list(lv_station_mapping_sec.values()) + + list(lv_generators_mapping.values()) + + list(lv_branch_t_mapping.values()) + + list(lv_loads_mapping.values())]}, axis=1) \ No newline at end of file From c56d76f22b1bef24aeec56e9b0df0779f104a46b Mon Sep 17 00:00:00 2001 From: gplessm Date: Mon, 11 Sep 2017 10:27:29 +0200 Subject: [PATCH 27/31] Finalize v_res, s_res of Results() --- edisgo/grid/network.py | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/edisgo/grid/network.py b/edisgo/grid/network.py index b65679c95..31b48b030 100644 --- a/edisgo/grid/network.py +++ b/edisgo/grid/network.py @@ -548,11 +548,14 @@ def s_res(self, lines=None): Parameters ---------- - lines : line object or list of + lines : :class:`~.grid.components.Load` or list of :class:`~.grid.components.Load` + + Line objects of grid topology. If not provided (respectively None) + defaults to return `s_res` of all lines in the grid. Returns ------- - DataFrame + :pandas:`pandas.DataFrame` Apparent power for `lines` """ @@ -578,30 +581,42 @@ def s_res(self, lines=None): return s_res - def v_res(self, nodes, level): + def v_res(self, nodes=None, level=None): """ Get resulting voltage level at node Parameters ---------- - nodes : :obj:`list` of :class:`~.grid.components.Load`, :class:`~.grid.components.Generator`, ... + nodes : {:class:`~.grid.components.Load`, :class:`~.grid.components.Generator`, ...} or :obj:`list` of grid topology component or `list` grid topology components + If not provided defaults to column names available in grid level + `level` level : str - Either 'mv' or 'lv'. Depending which grid level results you are + Either 'mv' or 'lv' or None (default). Depending which grid level results you are interested in. It is required to provide this argument in order to distinguish voltage levels at primary and secondary side of the transformer/LV station. + If not provided (respectively None) defaults to `['mv', 'lv']. + + Returns + ------- + :pandas:`pandas.DataFrame` + Resulting voltage levels obtained from power flow analysis Notes ----- - Limitations - * When power flow analysis is performed for MV only (with aggregated - LV loads and generators) this methods only returns voltage at - secondary side busbar and not at load/generator + Limitation: When power flow analysis is performed for MV only + (with aggregated LV loads and generators) this methods only returns + voltage at secondary side busbar and not at load/generator. """ + if level is None: + level = ['mv', 'lv'] - labels = [repr(_) for _ in nodes] + if nodes is None: + labels = list(self.pfa_v_mag_pu[level]) + else: + labels = [repr(_) for _ in nodes] not_included = [_ for _ in labels if _ not in list(self.pfa_v_mag_pu[level].columns)] @@ -613,4 +628,4 @@ def v_res(self, nodes, level): nodes=not_included)) - return self.pfa_v_mag_pu['lv'][labels_included] \ No newline at end of file + return self.pfa_v_mag_pu[level][labels_included] \ No newline at end of file From 29ec5c912df7c3e8532b0dff19fc38a896fefa1b Mon Sep 17 00:00:00 2001 From: gplessm Date: Mon, 11 Sep 2017 10:50:54 +0200 Subject: [PATCH 28/31] Improve docstrings --- edisgo/grid/network.py | 100 ++++++++++++++++++++++++++++++++--------- 1 file changed, 80 insertions(+), 20 deletions(-) diff --git a/edisgo/grid/network.py b/edisgo/grid/network.py index 31b48b030..a924934d3 100644 --- a/edisgo/grid/network.py +++ b/edisgo/grid/network.py @@ -465,26 +465,6 @@ class Results: A stack that details the history of measures to increase grid's hosting capacity. The last item refers to the latest measure. The key `original` refers to the state of the grid topology as it was initially imported. - pfa_p: :pandas:`pandas.DataFrame` - Holds power flow analysis results for active power for the last - iteration step. Index of the DataFrame is a DatetimeIndex indicating - the time period the power flow analysis was conducted for; columns - of the DataFrame are the edges as well as stations of the grid - topology. - ToDo: add unit - pfa_q: :pandas:`pandas.DataFrame` - Holds power flow analysis results for reactive power for the last - iteration step. Index of the DataFrame is a DatetimeIndex indicating - the time period the power flow analysis was conducted for; columns - of the DataFrame are the edges as well as stations of the grid - topology. - ToDo: add unit - pfa_v_mag_pu: :pandas:`pandas.DataFrame` - Holds power flow analysis results for relative voltage deviation for - the last iteration step. Index of the DataFrame is a DatetimeIndex - indicating the time period the power flow analysis was conducted for; - columns of the DataFrame are the nodes as well as stations of the grid - topology. equipment_changes: :pandas:`pandas.DataFrame` Tracks changes in the equipment (replaced or added cable, batteries added, curtailment set to a generator, ...). This is indexed by the @@ -515,6 +495,30 @@ def __init__(self): @property def pfa_p(self): + """ + Active power results from power flow analysis + + Holds power flow analysis results for active power for the last + iteration step. Index of the DataFrame is a DatetimeIndex indicating + the time period the power flow analysis was conducted for; columns + of the DataFrame are the edges as well as stations of the grid + topology. + ToDo: add unit + + Parameters + ---------- + pypsa: `pandas.DataFrame` + Results time series of active power P from the + `PyPSA network `_ + + Provide this if you want to set values. For retrieval of data do not + pass an argument + + Returns + ------- + :pandas:`pandas.DataFrame` + Active power results from power flow analysis + """ return self._pfa_p @pfa_p.setter @@ -523,6 +527,31 @@ def pfa_p(self, pypsa): @property def pfa_q(self): + """ + Reactive power results from power flow analysis + + Holds power flow analysis results for reactive power for the last + iteration step. Index of the DataFrame is a DatetimeIndex indicating + the time period the power flow analysis was conducted for; columns + of the DataFrame are the edges as well as stations of the grid + topology. + ToDo: add unit + + Parameters + ---------- + pypsa: `pandas.DataFrame` + Results time series of reactive power Q from the + `PyPSA network `_ + + Provide this if you want to set values. For retrieval of data do not + pass an argument + + Returns + ------- + :pandas:`pandas.DataFrame` + Reactive power results from power flow analysis + + """ return self._pfa_q @pfa_q.setter @@ -531,6 +560,30 @@ def pfa_q(self, pypsa): @property def pfa_v_mag_pu(self): + """ + Voltage deviation at node in p.u. + + Holds power flow analysis results for relative voltage deviation for + the last iteration step. Index of the DataFrame is a DatetimeIndex + indicating the time period the power flow analysis was conducted for; + columns of the DataFrame are the nodes as well as stations of the grid + topology. + + Parameters + ---------- + pypsa: `pandas.DataFrame` + Results time series of voltage deviation from the + `PyPSA network `_ + + Provide this if you want to set values. For retrieval of data do not + pass an argument + + Returns + ------- + :pandas:`pandas.DataFrame` + Voltage level nodes of grid + + """ return self._pfa_v_mag_pu @pfa_v_mag_pu.setter @@ -546,6 +599,13 @@ def s_res(self, lines=None): """ Get resulting apparent power at line(s) + The apparent power at a line determines from the maximum values of + active power P and reactive power Q. + + :math:: + + S = sqrt(max(p0, p1)^2 + max(q0, q1)^2) + Parameters ---------- lines : :class:`~.grid.components.Load` or list of :class:`~.grid.components.Load` From e31ff496396f24d37b082e489ecf837268ff8b1f Mon Sep 17 00:00:00 2001 From: gplessm Date: Mon, 11 Sep 2017 11:03:18 +0200 Subject: [PATCH 29/31] Improve docstrings further --- edisgo/grid/network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/edisgo/grid/network.py b/edisgo/grid/network.py index a924934d3..b623f44e1 100644 --- a/edisgo/grid/network.py +++ b/edisgo/grid/network.py @@ -602,9 +602,9 @@ def s_res(self, lines=None): The apparent power at a line determines from the maximum values of active power P and reactive power Q. - :math:: + .. math:: - S = sqrt(max(p0, p1)^2 + max(q0, q1)^2) + S = \sqrt(max(p0, p1)^2 + max(q0, q1)^2) Parameters ---------- From 6d3d549002e9bb0dfa12af46fe51953e99155592 Mon Sep 17 00:00:00 2001 From: gplessm Date: Mon, 11 Sep 2017 11:20:37 +0200 Subject: [PATCH 30/31] Remove resolved TODOs --- edisgo/grid/network.py | 2 -- edisgo/tools/pypsa_io.py | 12 +++--------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/edisgo/grid/network.py b/edisgo/grid/network.py index b623f44e1..30067f654 100644 --- a/edisgo/grid/network.py +++ b/edisgo/grid/network.py @@ -484,8 +484,6 @@ class Results: # TODO: maybe add setter to alter list of measures - # TODO: maybe initialize DataFrames `pfa_nodes` different. Like with index of all components of similarly - def __init__(self): self._measures = ['original'] self._pfa_p = None diff --git a/edisgo/tools/pypsa_io.py b/edisgo/tools/pypsa_io.py index 045b423e3..ddd125484 100644 --- a/edisgo/tools/pypsa_io.py +++ b/edisgo/tools/pypsa_io.py @@ -80,8 +80,6 @@ def to_pypsa(network, mode): lv_components = lv_to_pypsa(network) components = combine_mv_and_lv(mv_components, lv_components) - #TODO: translate voltage appropriately: in kVA - # TODO: check unit of nominal capacity of generators: should be MW timeseries_load_p, timeseries_load_q = _pypsa_load_timeseries( network, @@ -607,11 +605,12 @@ def _pypsa_load_timeseries(network, mode=None): for load in lv_grid.graph.nodes_by_attribute('load'): for sector in list(load.consumption.keys()): # for sector in list(list(load.consumption.keys())[0]): - # TODO: remove consideration of only first sector + # TODO: remove consideration of only industrial sector # now, if a load object has consumption in multiple sectors # (like currently only industrial/retail) the consumption is # implicitly assigned to the industrial sector when being # exported to pypsa. + # TODO: resolve this in the importer if sector != 'retail': lv_load_timeseries_q.append( load.pypsa_timeseries('q').rename( @@ -619,14 +618,10 @@ def _pypsa_load_timeseries(network, mode=None): lv_load_timeseries_p.append( load.pypsa_timeseries('p').rename( repr(load)).to_frame()) - else: - print("retail") 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) - # TODO: maybe names of load object have to be changed to distinguish between different grids - return load_df_p, load_df_q @@ -674,10 +669,9 @@ def _pypsa_generator_timeseries(network, mode=None): 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) - # TODO: maybe names of load object have to be changed to distinguish between different grids - return gen_df_p, gen_df_q + def _pypsa_bus_timeseries(network, buses, mode=None): """Timeseries in PyPSA compatible format for generator instances From 08438fc4d5428c6e5301a4780c3cac8bac8b1a35 Mon Sep 17 00:00:00 2001 From: gplessm Date: Mon, 11 Sep 2017 11:28:27 +0200 Subject: [PATCH 31/31] Add calls to retrieve results in example.py --- edisgo/examples/example.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/edisgo/examples/example.py b/edisgo/examples/example.py index 1819ac095..f88fd1cff 100644 --- a/edisgo/examples/example.py +++ b/edisgo/examples/example.py @@ -17,13 +17,22 @@ # pickle.dump(network, open('test_network.pkl', 'wb')) # network = pickle.load(open('test_network.pkl', 'rb')) -# Do non-linear power flow analysis with PyPSA -# network.analyze(mode='mv') +# Do non-linear power flow analysis with PyPSA (MV+LV) +# network.analyze() # Print LV station secondary side voltage levels returned by PFA # print(network.results.v_res( # network.mv_grid.graph.nodes_by_attribute('lv_station'), 'lv')) +# Print voltage levels for entire LV grid +# for attr in ['lv_station', 'load', 'generator', 'branch_tee']: +# objs = [] +# for lv_grid in network.mv_grid.lv_grids: +# objs.extend(lv_grid.graph.nodes_by_attribute(attr)) +# print("\n\n\n{}\n".format(attr)) +# print(network.results.v_res( +# objs, 'lv')) + # Print voltage level of all nodes # print(network.results.pfa_v_mag_pu)