From 0edfd4e9b97da21f5600c7350d85b198a8d5d76e Mon Sep 17 00:00:00 2001 From: wolfbunke Date: Mon, 10 Dec 2018 11:19:16 +0100 Subject: [PATCH 001/126] update version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4fba29e4..f54c12d5 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ def read(fname): setup( name='eGo', - version='0.3.3', + version='0.3.4', author='wolfbunke, maltesc', author_email='wolf-dieter.bunke@uni-flensburg.de', description=("A cross-grid-level electricity grid and storage " From 513c4d10455ae8e9374934d950b2a657099676dd Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 5 Sep 2022 17:07:47 +0200 Subject: [PATCH 002/126] Changes to ETraGoData class --- ego/tools/edisgo_integration.py | 34 +++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index b8d512bc..f29dae5c 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1246,23 +1246,33 @@ def _get_bus_id_from_mv_grid(self, session, subst_id): class _ETraGoData: """ - Container for minimal eTraGo network. This minimal network is required - for the parallelization of eDisGo. + Container for minimal eTraGo network. + + This minimal network only contains information relevant for eDisGo. + + Parameters + ---------- + etrago_network : :pypsa:`PyPSA.Network` """ def __init__(self, etrago_network): - self.snapshots = getattr( - etrago_network, "snapshots") - self.storage_units = getattr( - etrago_network, "storage_units") - self.storage_units_t = getattr( - etrago_network, "storage_units_t") - self.generators = getattr( - etrago_network, "generators") - self.generators_t = getattr( - etrago_network, "generators_t") + # define needed attributes + # ToDo Further attributes needed? Only _t.p? + attr_list = [ + "snapshots", # nichts filtern + "storage_units", # Batteriekapazität pro MV grid, filter battery (eTraGo function filter_by_carrier) + "storage_units_t", # Batterie dispatch, p, battery + "stores", # thermal storage capacity + "generators", + "generators_t", # Info über Abregelung und Einsatz von regelbaren Kraftwerken + "links", + "links_t", # DSM, HP (eTraGo function filter_by_carrier) + "loads_t", + ] + for attr in attr_list: + setattr(self, attr, getattr(etrago_network, attr)) class _EDisGoImported: From fbabc673bb935e738a1285f436d6b9df0d479145 Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 5 Sep 2022 18:13:09 +0200 Subject: [PATCH 003/126] Update edisgo imports --- ego/tools/edisgo_integration.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index f29dae5c..12479f41 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -49,10 +49,10 @@ from egoio.db_tables import model_draft, grid from egoio.tools import db - from edisgo.grid.network import Results, TimeSeriesControl - from edisgo.grid import tools + from edisgo.edisgo import import_edisgo_from_files, EDisGo + from edisgo.network.results import Results + from edisgo.tools import tools from edisgo.tools.plots import mv_grid_topology - from edisgo.grid.network import EDisGo from ego.tools.specs import ( get_etragospecs_direct From 061178f8be14737f02869c3ffaa2a67ba3482bad Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 5 Sep 2022 18:13:49 +0200 Subject: [PATCH 004/126] Update load_edisgo_results function --- doc/conf.py | 1 + ego/tools/edisgo_integration.py | 150 ++++---------------------------- 2 files changed, 18 insertions(+), 133 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index a64f5fd5..69568187 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -86,6 +86,7 @@ # Dictionary of external links extlinks = {'pandas':('http://pandas.pydata.org/pandas-docs/stable/api.html#%s', 'pandas.'), + "pypsa": ("https://pypsa.readthedocs.io/en/latest/components.html#%s", "pypsa"), 'sqlalchemy':('http://docs.sqlalchemy.org/en/latest/orm/session_basics.html%s', 'SQLAlchemy session object'), 'shapely':('http://toblerity.org/shapely/manual.html#%s', diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 12479f41..89329eb5 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -104,7 +104,7 @@ def __init__(self, json_file, etrago_network): self._edisgo_grids = {} if self._csv_import: - self._laod_edisgo_results() + self._load_edisgo_results() self._successfull_grids = self._successfull_grids() self._grid_investment_costs = edisgo_grid_investment( self, @@ -353,7 +353,7 @@ def _init_status(self): def _status_update(self, mv_grid_id, time, message=None, show=True): """ - Updtaed eDisGo's status files + Updated eDisGo's status files """ status = pd.read_csv( self._status_path, @@ -827,7 +827,7 @@ def _run_edisgo_pool(self): self._csv_import = self._json_file['eDisGo']['results'] self._save_edisgo_results() - self._laod_edisgo_results() + self._load_edisgo_results() self._run_finished = True def _run_edisgo( @@ -1072,7 +1072,7 @@ def _save_edisgo_results(self): self._grid_choice.to_csv(self._results + '/grid_choice.csv') - def _laod_edisgo_results(self): + def _load_edisgo_results(self): # Load the grid choice form CSV self._grid_choice = pd.read_csv( @@ -1085,72 +1085,19 @@ def _laod_edisgo_results(self): mv_grid_id = int(row['the_selected_network_id']) try: - # Grid expansion costs - file_path = os.path.join( - self._csv_import, - str(mv_grid_id), - 'grid_expansion_results', - 'grid_expansion_costs.csv') - - grid_expansion_costs = pd.read_csv( - file_path, - index_col=0) - - # powerflow results - pf_path = os.path.join( - self._csv_import, - str(mv_grid_id), - 'powerflow_results', - 'apparent_powers.csv') - - s_res = pd.read_csv( - pf_path, - index_col=0, - parse_dates=True) - - # Configs - config_path = os.path.join( - self._csv_import, - str(mv_grid_id), - 'configs.csv') - - edisgo_config = {} - with open(config_path, 'r') as f: - reader = csv.reader(f) - for row in reader: - a = iter(row[1:]) - edisgo_config[row[0]] = dict(zip(a, a)) - - # PyPSA network - pypsa_path = os.path.join( - self._csv_import, - str(mv_grid_id), - 'pypsa_network') - - imported_pypsa = pypsa.Network() - imported_pypsa.import_from_csv_folder(pypsa_path) - - # Storages - storage_path = os.path.join( - self._csv_import, - str(mv_grid_id), - 'storage_integration_results', - 'storages.csv') - - if os.path.exists(storage_path): - storages = pd.read_csv( - storage_path, - index_col=0) - else: - storages = pd.DataFrame( - columns=['nominal_power', 'voltage_level']) - - edisgo_grid = _EDisGoImported( - grid_expansion_costs, - s_res, - storages, - imported_pypsa, - edisgo_config) + edisgo_grid = import_edisgo_from_files( + edisgo_path=os.path.join( + self._csv_import, + str(mv_grid_id)), + import_topology=True, + import_timeseries=False, + import_results=True, + import_electromobility=False, + from_zip_archive=True, + dtype="float32", + parameters={"powerflow_results": ["pfa_p", "pfa_q"], + "grid_expansion_results": ["grid_expansion_costs"]} + ) self._edisgo_grids[ mv_grid_id @@ -1275,69 +1222,6 @@ def __init__(self, etrago_network): setattr(self, attr, getattr(etrago_network, attr)) -class _EDisGoImported: - """ - Imported (reduced) eDisGo class. - This class allows the import reduction to only the attributes used in eGo - """ - - def __init__( - self, - grid_expansion_costs, - s_res, - storages, - pypsa, - edisgo_config): - - self.network = _NetworkImported( - grid_expansion_costs, - s_res, - storages, - pypsa, - edisgo_config) - - -class _NetworkImported: - """ - Reduced eDisG network class, used of eGo's reimport - """ - - def __init__( - self, - grid_expansion_costs, - s_res, - storages, - pypsa, - edisgo_config): - - self.results = _ResultsImported( - grid_expansion_costs, - s_res, - storages) - - self.pypsa = pypsa - self.config = edisgo_config - - -class _ResultsImported: - """ - Reduced eDisG results class, used of eGo's reimport - """ - - def __init__( - self, - grid_expansion_costs, - s_res, - storages): - - self.grid_expansion_costs = grid_expansion_costs - self.storages = storages - self._s_res = s_res - - def s_res(self): - return self._s_res - - def parallelizer( ding0_id_list, func, From 9ffc74ad0e87afd3896b3ea49ca3306a470e41e3 Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 5 Sep 2022 18:14:12 +0200 Subject: [PATCH 005/126] Delete unnecessary function --- ego/tools/edisgo_integration.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 89329eb5..e670a459 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -94,9 +94,6 @@ def __init__(self, json_file, etrago_network): self._etrago_network = _ETraGoData(etrago_network) del etrago_network - # eDisGo specific naming - self._edisgo_scenario_translation() - # Program information self._run_finished = False @@ -524,16 +521,6 @@ def _set_scenario_settings(self): else: self._versioned = False - def _edisgo_scenario_translation(self): - - # Scenario translation - if self._scn_name == 'Status Quo': - self._generator_scn = None - elif self._scn_name == 'NEP 2035': - self._generator_scn = 'nep2035' - elif self._scn_name == 'eGo 100': - self._generator_scn = 'ego100' - def _successfull_grids(self): """ Calculates the relative number of successfully calculated grids, From 531701d5554683f3f4c2bcdc60cd05be9feffdc1 Mon Sep 17 00:00:00 2001 From: Malte Jahn Date: Tue, 6 Sep 2022 12:24:36 +0200 Subject: [PATCH 006/126] Updated the _ETraGoData class and added a script to test it --- ...ript_test_edisgo_integration_ETraGoData.py | 31 +++++ ego/tools/edisgo_integration.py | 127 ++++++++++++++++-- 2 files changed, 144 insertions(+), 14 deletions(-) create mode 100644 ego/examples/script_test_edisgo_integration_ETraGoData.py diff --git a/ego/examples/script_test_edisgo_integration_ETraGoData.py b/ego/examples/script_test_edisgo_integration_ETraGoData.py new file mode 100644 index 00000000..76ba2711 --- /dev/null +++ b/ego/examples/script_test_edisgo_integration_ETraGoData.py @@ -0,0 +1,31 @@ +import logging +import sys + +from pypsa import Network + +from ego.tools.edisgo_integration import _ETraGoData + +logger = logging.getLogger(__name__) + +if __name__ == "__main__": + + logger = logging.getLogger(__name__) + logger.propagate = False + log_level = logging.DEBUG + logger.setLevel(log_level) + + console_handler = logging.StreamHandler(stream=sys.stdout) + console_handler.setLevel(log_level) + stream_formatter = logging.Formatter("%(name)s - %(levelname)s: %(message)s") + console_handler.setFormatter(stream_formatter) + logger.addHandler(console_handler) + + class ETraGo: + def __init__(self): + self.network = Network() + self.network.import_from_csv_folder( + "/home/local/RL-INSTITUT/malte.jahn/Desktop/etrago-results" + ) + + etrago_obj = ETraGo() + etrago_data_obj = _ETraGoData(etrago_obj) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index f29dae5c..9da23b2c 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1257,22 +1257,121 @@ class _ETraGoData: """ def __init__(self, etrago_network): - # define needed attributes - # ToDo Further attributes needed? Only _t.p? - attr_list = [ - "snapshots", # nichts filtern - "storage_units", # Batteriekapazität pro MV grid, filter battery (eTraGo function filter_by_carrier) - "storage_units_t", # Batterie dispatch, p, battery - "stores", # thermal storage capacity - "generators", - "generators_t", # Info über Abregelung und Einsatz von regelbaren Kraftwerken + + def filter_by_carrier( + etrago_network_obj, component, carrier, like=True, timeseries=True + ): + def filter_df_by_carrier(df): + if isinstance(carrier, str): + if like: + return df[df.carrier.str.contains(carrier)] + else: + return df[df.carrier == carrier] + elif isinstance(carrier, list): + return df[df.carrier.isin(carrier)] + elif carrier is None: + return df + + if timeseries: + attribute_to_save = { + "links": "p0", + "generators": "p", + "stores": "p", + "storage_units": "p", + } + attribute_to_save = attribute_to_save[component] + + df_to_filter = getattr( + getattr(etrago_network_obj, component + "_t"), attribute_to_save + ) + df = df_to_filter.loc[ + :, + filter_df_by_carrier(getattr(etrago_network_obj, component)).index, + ] + else: + columns_to_save = { + "links": ["carrier", "p_nom"], + "generators": ["carrier", "p_nom"], + "stores": ["carrier", "e_nom"], + "storage_units": ["carrier", "p_nom", "max_hours"], + } + columns_to_save = columns_to_save[component] + + df_to_filter = getattr(etrago_network_obj, component) + df = filter_df_by_carrier(df_to_filter) + df = df[columns_to_save] + + logger.debug( + f"{component}, {carrier}, {timeseries}, {df.shape}, " + f"{filter_df_by_carrier(getattr(etrago_network_obj, component)).carrier.unique()}" + ) + + return df + + logger.debug( + f"Carriers in links {etrago_network.network.links.carrier.unique()}" + ) + logger.debug( + f"Carriers in generators {etrago_network.network.generators.carrier.unique()}" + ) + logger.debug( + f"Carriers in stores {etrago_network.network.stores.carrier.unique()}" + ) + logger.debug( + f"Carriers in storage_units {etrago_network.network.storage_units.carrier.unique()}" + ) + + self.snapshots = etrago_network.network.snapshots + + self.bev_charger = filter_by_carrier( + etrago_network.network, "links", "BEV", timeseries=False + ) + self.bev_charger_t = filter_by_carrier( + etrago_network.network, "links", "BEV", timeseries=True + ) + self.dsm = filter_by_carrier( + etrago_network.network, "links", "dsm", timeseries=False + ) + self.dsm_t = filter_by_carrier( + etrago_network.network, "links", "dsm", timeseries=True + ) + + self.rural_heat_t = filter_by_carrier( + etrago_network.network, "links", "rural_heat_pump", timeseries=True + ) + self.rural_heat_store = filter_by_carrier( + etrago_network.network, "stores", "rural_heat_store", timeseries=False + ) + + self.central_heat_t = filter_by_carrier( + etrago_network.network, "links", - "links_t", # DSM, HP (eTraGo function filter_by_carrier) - "loads_t", - ] - for attr in attr_list: - setattr(self, attr, getattr(etrago_network, attr)) + ["central_heat_pump", "central_resistive_heater"], + timeseries=True, + ) + self.central_heat_store = filter_by_carrier( + etrago_network.network, "stores", "central_heat_store", timeseries=False + ) + + self.central_gas_chp_t = filter_by_carrier( + etrago_network.network, "links", "central_gas_chp_t", timeseries=True + ) + + # + self.generators = filter_by_carrier( + etrago_network.network, "generators", None, timeseries=False + ) + self.generators_t = filter_by_carrier( + etrago_network.network, "generators", None, timeseries=True + ) + + self.battery_storage_units = filter_by_carrier( + etrago_network.network, "storage_units", "battery", timeseries=False + ) + self.battery_storage_units_t = filter_by_carrier( + etrago_network.network, "storage_units", "battery", timeseries=True + ) class _EDisGoImported: From 90f2382183618bf0f0c3cff64b0a78213044242b Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 6 Sep 2022 14:11:21 +0200 Subject: [PATCH 007/126] Rename scenario_setting parameter --- doc/api/modules.rst | 6 +++--- ego/scenario_setting.json | 2 +- ego/tools/edisgo_integration.py | 8 ++++---- ego/tools/mv_cluster.py | 16 +++++++++------- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/doc/api/modules.rst b/doc/api/modules.rst index 6570597a..1b957d76 100644 --- a/doc/api/modules.rst +++ b/doc/api/modules.rst @@ -51,12 +51,12 @@ The file can be found on :property string db: Name of your database (e.g.``''oedb''``). eDisGo queries generator data from this database. Please note that this parameters is automatically overwritten in eDisGo's configuration files. :property string gridversion: ``null`` or *open_eGo* dataset version (e.g. ``''v0.4.5''``). If ``null``, *open_eGo*'s model_draft is used. Please note that this parameters is automatically overwritten in eDisGo's configuration files. - :property string ding0_files: Path to the MV grid files (created by `ding0 `_) (e.g. ``''data/MV_grids/20180713110719''``) - :property string choice_mode: Mode that eGo uses to chose MV grids out of the files in **ding0_files** (e.g. ``''manual''``, ``''cluster''`` or ``''all''``). If ``''manual''`` is chosen, the parameter **manual_grids** must contain a list of the desired grids. If ``''cluster''`` is chosen, **no_grids** must specify the desired number of clusters and **cluster_attributes** must specify the applied cluster attributes. If ``''all''`` is chosen, all MV grids from **ding0_files** are calculated. + :property string ding0_path: Path to the MV grid files (created by `ding0 `_) (e.g. ``''data/MV_grids/20180713110719''``) + :property string choice_mode: Mode that eGo uses to chose MV grids out of the files in **ding0_path** (e.g. ``''manual''``, ``''cluster''`` or ``''all''``). If ``''manual''`` is chosen, the parameter **manual_grids** must contain a list of the desired grids. If ``''cluster''`` is chosen, **no_grids** must specify the desired number of clusters and **cluster_attributes** must specify the applied cluster attributes. If ``''all''`` is chosen, all MV grids from **ding0_path** are calculated. :property list cluster_attributes: List of strings containing the desired cluster attributes. Available attributes are: ``''farthest_node''``, ``''wind_cap''``, ``''solar_cap''`` and ``''extended_storage''``, thus an exemplary list looks like ``["farthest_node", "wind_cap", "solar_cap", "extended_storage"]``. ``''farthest_node''`` represents the longest path within each grid, ``''wind_cap''`` the installed wind capacity within each grid, ``''solar_cap''`` the installed solar capacity within each grid and ``''extended_storage''`` the installed storage units (as calculated by eTraGo). Please note that ``''extended_storage''`` is only available in combination with eTraGo datasets that optimized storage extension. Otherwise this attribute is ignored. :property bool only_cluster: If ``true``, eGo only identifies cluster results, but performs no eDisGo run. Please note that for **only_cluster** an eTraGo run or dataset must be provided. :property list manual_grids: List of MV grid ID's (*open_eGo* HV/MV substation ID's) is case of **choice_mode** = ``''manual''`` (e.g. ``[1718,1719]``). Ohterwise this parameter is ignored. - :property int no_grids: Number of MV grid clusters (from all files in **ding0_files**, a specified number of representative clusters is calculated) in case of **choice_mode** = ``''cluster''``. Otherwise this parameter is ignored. + :property int no_grids: Number of MV grid clusters (from all grids in **ding0_path**, a specified number of representative clusters is calculated) in case of **choice_mode** = ``''cluster''``. Otherwise this parameter is ignored. :property bool parallelization: If ``false``, eDisgo is used in a consecutive way (this may take very long time). In order to increase the performance of MV grid simulations, ``true`` allows the parallel calculation of MV grids. If **parallelization** = ``true``, **max_calc_time** and **max_workers** must be specified. :property float max_calc_time: Maximum calculation time in hours for eDisGo simulations. The calculation is terminated after this time and all costs are extrapolated based on the unfinished simulation. Please note that this parameter is only used if **parallelization** = ``true``. :property ing max_workers: Number of workers (cpus) that are allocated to the simulation. If the given value exceeds the number of available workers, it is reduced to the number of available workers. Please note that this parameter is only used if **parallelization** = ``true``. diff --git a/ego/scenario_setting.json b/ego/scenario_setting.json index a996e105..ed900a67 100644 --- a/ego/scenario_setting.json +++ b/ego/scenario_setting.json @@ -41,7 +41,7 @@ "eDisGo": { "db": "oedb", "gridversion": "v0.4.5", - "ding0_files": "/path/to_your/.dingo/grids", + "ding0_path": "/path/to_your/.dingo/grids", "choice_mode": "cluster", "cluster_attributes":["farthest_node", "wind_cap", "solar_cap", "extended_storage"], "only_cluster": false, diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index e670a459..457cc61d 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -548,7 +548,7 @@ def _analyze_cluster_attributes(self): These are considered the "standard" attributes for the MV grid clustering. """ - analyze_attributes(self._ding0_files) + analyze_attributes(self._ding0_path) def _cluster_mv_grids( self, @@ -572,14 +572,14 @@ def _cluster_mv_grids( # TODO: This first dataframe contains the standard attributes... # ...Create an Interface in order to use attributes more flexibly. # Make this function more generic. - attributes_path = self._ding0_files + '/attributes.csv' + attributes_path = self._ding0_path + '/attributes.csv' if not os.path.isfile(attributes_path): logger.info('Attributes file is missing') logger.info('Attributes will be calculated') self._analyze_cluster_attributes() - df = pd.read_csv(self._ding0_files + '/attributes.csv') + df = pd.read_csv(self._ding0_path + '/attributes.csv') df = df.set_index('id') df.drop(['Unnamed: 0'], inplace=True, axis=1) df.rename( @@ -682,7 +682,7 @@ def _check_available_mv_grids(self): """ mv_grids = [] - for file in os.listdir(self._ding0_files): + for file in os.listdir(self._ding0_path): if file.endswith('.pkl'): mv_grids.append( int(file.replace( diff --git a/ego/tools/mv_cluster.py b/ego/tools/mv_cluster.py index 0e8da824..12e35f29 100644 --- a/ego/tools/mv_cluster.py +++ b/ego/tools/mv_cluster.py @@ -41,18 +41,22 @@ logger = logging.getLogger(__name__) -def analyze_attributes(ding0_files): + +def analyze_attributes(ding0_path): """ Calculates the attributes wind and solar capacity and farthest node - for all files in ding0_files. Results are written to ding0_files - + for all grids in ding0_path. Results are written to ding0_path + + # ToDo Revise! Which attributes do we want to cluster by and where can they be + # obtained from? eGon_data needs to be finished for this. + Parameters ---------- - ding0_files : :obj:`str` + ding0_path : :obj:`str` Path to ding0 files """ - base_path = ding0_files + base_path = ding0_path not_found = [] tccs = [] # Total Cumulative Capacity of Solar @@ -197,8 +201,6 @@ def cluster_mv_grids( Parameters ---------- - ding0_files : :obj:`str` - Path to ding0 files no_grids : int Desired number of clusters (of MV grids) From 7adcf151143de8b7333dd5f037d32d2e9b860095 Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 6 Sep 2022 14:12:39 +0200 Subject: [PATCH 008/126] Rename attribute --- ego/tools/edisgo_integration.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 457cc61d..37cbbc81 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -102,7 +102,7 @@ def __init__(self, json_file, etrago_network): if self._csv_import: self._load_edisgo_results() - self._successfull_grids = self._successfull_grids() + self._successful_grids = self._successful_grids() self._grid_investment_costs = edisgo_grid_investment( self, self._json_file @@ -124,7 +124,7 @@ def __init__(self, json_file, etrago_network): if self._results: self._save_edisgo_results() - self._successfull_grids = self._successfull_grids() + self._successful_grids = self._successful_grids() self._grid_investment_costs = edisgo_grid_investment( self, @@ -158,7 +158,7 @@ def grid_choice(self): return self._grid_choice @property - def successfull_grids(self): + def successful_grids(self): """ Relative number of successfully calculated MV grids (Includes clustering weighting) @@ -169,7 +169,7 @@ def successfull_grids(self): Relative number of grids """ - return self._successfull_grids + return self._successful_grids @property def grid_investment_costs(self): From b34e79ccda4f91c142a20eb210915f5d168f4fde Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 6 Sep 2022 14:13:48 +0200 Subject: [PATCH 009/126] Use readonly in database connection --- ego/tools/edisgo_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 37cbbc81..7413f7e2 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -841,7 +841,7 @@ def _run_edisgo( logger.info( 'MV grid {}: Calculating interface values'.format(mv_grid_id)) - conn = db.connection(section=self._db_section) + conn = db.connection(readonly=True) session_factory = sessionmaker(bind=conn) Session = scoped_session(session_factory) session = Session() From 524071e08373b67b608e149e3bdc889e0fd27a61 Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 6 Sep 2022 14:59:57 +0200 Subject: [PATCH 010/126] Remove unnecessary scenario_setting parameters --- doc/api/modules.rst | 3 --- ego/scenario_setting.json | 3 --- 2 files changed, 6 deletions(-) diff --git a/doc/api/modules.rst b/doc/api/modules.rst index 1b957d76..2868e6d4 100644 --- a/doc/api/modules.rst +++ b/doc/api/modules.rst @@ -61,9 +61,6 @@ The file can be found on :property float max_calc_time: Maximum calculation time in hours for eDisGo simulations. The calculation is terminated after this time and all costs are extrapolated based on the unfinished simulation. Please note that this parameter is only used if **parallelization** = ``true``. :property ing max_workers: Number of workers (cpus) that are allocated to the simulation. If the given value exceeds the number of available workers, it is reduced to the number of available workers. Please note that this parameter is only used if **parallelization** = ``true``. :property bool initial_reinforcement: This parameter must be set ``true``. - :property bool apply_curtailment: If ``true``, eDisGo applies and optimizes the curtailment (as calculated by eTraGo) within each MV grid. - :property float curtailment_voltage_threshold: p.u. overvoltage limit (e.g. ``0.05``). If this p.u. overvoltage is exceeded at any bus, curtailment is applied. - :property bool storage_distribution: If ``true``, eDisGo attempts to integrate battery storages (as calculated by eTraGo) into MV grids in order to reduce grid reinforcement. :property float max_cos_phi_renewable: Maximum power factor for wind and solar generators in MV grids (e.g. ``0.9``). If the reactive power (as calculated by eTraGo) exceeds this power factor, the reactive power is reduced in order to reach the power factor conditions. :property string solver: Solver eDisGo uses to optimize the curtailment and storage integration (e.g. ``''gurobi''``). :property string timesteps_pfa: Method eDisGo uses for the storage integration (e.g. ``''snapshot_analysis''``). diff --git a/ego/scenario_setting.json b/ego/scenario_setting.json index ed900a67..613ab50a 100644 --- a/ego/scenario_setting.json +++ b/ego/scenario_setting.json @@ -51,9 +51,6 @@ "max_calc_time": 0.5, "max_workers":2, "initial_reinforcement":true, - "apply_curtailment":true, - "curtailment_voltage_threshold": 0, - "storage_distribution":true, "max_cos_phi_renewable": 0.9, "results": "results/another_result", "solver": "gurobi", From 4386440c32b69e3f4d14ce59f03604753b5451ca Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 6 Sep 2022 15:00:52 +0200 Subject: [PATCH 011/126] Update run_edisgo --- ego/tools/edisgo_integration.py | 213 ++++++++++++++++++++------------ 1 file changed, 134 insertions(+), 79 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 13f38a30..67dfde24 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -860,28 +860,32 @@ def _run_edisgo( self._max_cos_phi_renewable) Session.remove() - # Get ding0 (MV grid) form folder - ding0_filepath = ( - self._ding0_files - + '/ding0_grids__' - + str(mv_grid_id) - + '.pkl') - - if not os.path.isfile(ding0_filepath): - msg = 'No MV grid file for MV grid {}'.format(mv_grid_id) + # get ding0 MV grid path + ding0_grid_path = os.path.join( + self._ding0_path, + str(mv_grid_id)) + + if not os.path.isdir(ding0_grid_path): + msg = 'No grid data for MV grid {}'.format(mv_grid_id) logger.error(msg) raise Exception(msg) - # Initalize eDisGo with this MV grid - logger.info(("MV grid {}: Initialize MV grid").format(mv_grid_id)) - edisgo_grid = EDisGo(ding0_grid=ding0_filepath, - worst_case_analysis='worst-case') + + # Initialize MV grid + logger.info(f"MV grid {mv_grid_id}: Initialize MV grid") + + edisgo_grid = EDisGo( + ding0_grid=ding0_grid_path + ) + + # ##################### Conduct initial grid reinforcement #################### + edisgo_grid.set_time_series_worst_case_analysis() logger.info(("MV grid {}: Changing eDisGo's voltage configurations " + "for initial reinforcement").format(mv_grid_id)) - edisgo_grid.network.config[ + edisgo_grid.config[ 'grid_expansion_allowed_voltage_deviations'] = { 'hv_mv_trafo_offset': 0.04, 'hv_mv_trafo_control_deviation': 0.0, @@ -902,12 +906,12 @@ def _run_edisgo( # Get costs for initial reinforcement # TODO: Implement a separate cost function costs_grouped = \ - edisgo_grid.network.results.grid_expansion_costs.groupby( + edisgo_grid.results.grid_expansion_costs.groupby( ['type']).sum() costs = pd.DataFrame( costs_grouped.values, columns=costs_grouped.columns, - index=[[edisgo_grid.network.id] * len(costs_grouped), + index=[[edisgo_grid.topology.id] * len(costs_grouped), costs_grouped.index]).reset_index() costs.rename(columns={'level_0': 'grid'}, inplace=True) @@ -923,10 +927,14 @@ def _run_edisgo( logger.info(( "MV grid {}: Resetting grid after initial reinforcement" ).format(mv_grid_id)) - edisgo_grid.network.results = Results(edisgo_grid.network) + edisgo_grid.results = Results(edisgo_grid) # Reload the (original) eDisGo configs - edisgo_grid.network.config = None + edisgo_grid.config = "default" + edisgo_grid.timeseries = TimeSeries( + timeindex=specs['conv_dispatch'].index + ) + # ########################################################################### # eTraGo case begins here logger.info("MV grid {}: eTraGo feed-in case".format(mv_grid_id)) @@ -936,7 +944,18 @@ def _run_edisgo( # Update configs with eGo's scenario settings self._update_edisgo_configs(edisgo_grid) - # Generator import for NEP 2035 and eGo 100 scenarios + # Set conventional load time series (active and reactive power) + edisgo_grid.set_time_series_active_power_predefined( + conventional_loads_ts="oedb" + ) + edisgo_grid.set_time_series_reactive_power_control( + control="fixed_cosphi", + generators_parametrisation=None, + loads_parametrisation="default", + storage_units_parametrisation=None + ) + + # Generator import for future scenario if self._generator_scn: logger.info( 'Importing generators for scenario {}'.format( @@ -949,61 +968,85 @@ def _run_edisgo( 'No generators imported for scenario {}'.format( self._scn_name) ) - edisgo_grid.network.pypsa = None - # Time Series from eTraGo - logger.info('Updating eDisGo timeseries with eTraGo values') + # Set dispatchable generator time series + # Active power + edisgo_grid.set_time_series_active_power_predefined( + dispatchable_generators_ts=specs['conv_dispatch'], + ) + # Reactive power + gens = edisgo_grid.topology.generators_df[ + ~edisgo_grid.topology.generators_df.type.isin(["solar", "wind"]) + ].index if self._pf_post_lopf: - logger.info('(Including reactive power)') - edisgo_grid.network.timeseries = TimeSeriesControl( - network=edisgo_grid.network, - timeseries_generation_fluctuating=specs['ren_potential'], - timeseries_generation_dispatchable=specs['conv_dispatch'], - timeseries_generation_reactive_power=specs['reactive_power'], - timeseries_load='demandlib', - timeindex=specs['conv_dispatch'].index).timeseries + # ToDo Use eTraGo time series to set reactive power (scale by nominal power) + edisgo_grid.set_time_series_manual( + generators_q=specs['reactive_power'].loc[:, []], + ) + pass else: - logger.info('(Only active power)') - edisgo_grid.network.timeseries = TimeSeriesControl( - network=edisgo_grid.network, - timeseries_generation_fluctuating=specs['ren_potential'], - timeseries_generation_dispatchable=specs['conv_dispatch'], - timeseries_load='demandlib', - timeindex=specs['conv_dispatch'].index).timeseries - - # Curtailment - if apply_curtailment: - logger.info('Including Curtailment') - - gens_df = tools.get_gen_info(edisgo_grid.network) - solar_wind_capacities = gens_df.groupby( - by=['type', 'weather_cell_id'] - )['nominal_capacity'].sum() - - curt_cols = [ - i for i in specs['ren_curtailment'].columns - if i in solar_wind_capacities.index - ] - - if not curt_cols: - raise ImportError( - ("MV grid {}: Data doesn't match").format(mv_grid_id)) - - curt_abs = pd.DataFrame( - columns=pd.MultiIndex.from_tuples(curt_cols)) - - for col in curt_abs: - curt_abs[col] = ( - specs['ren_curtailment'][col] - * solar_wind_capacities[col]) - - edisgo_grid.curtail( - curtailment_timeseries=curt_abs, - methodology='voltage-based', - solver=self._solver, - voltage_threshold=self._curtailment_voltage_threshold) + edisgo_grid.set_time_series_reactive_power_control( + generators_parametrisation=pd.DataFrame( + { + "components": [[gens]], + "mode": ["default"], + "power_factor": ["default"], + }, + index=[1], + ), + loads_parametrisation=None, + storage_units_parametrisation=None + ) + + # Set fluctuating generator time series + # Active power + edisgo_grid.set_time_series_active_power_predefined( + fluctuating_generators_ts=specs['ren_potential'], + ) + # Reactive power + gens = edisgo_grid.topology.generators_df[ + ~edisgo_grid.topology.generators_df.type.isin(["solar", "wind"]) + ].index + if self._pf_post_lopf: + # ToDo Use eTraGo time series to set reactive power (scale by nominal power) + edisgo_grid.set_time_series_manual( + generators_q=specs['reactive_power'].loc[:, []] + ) + pass else: - logger.info('No curtailment applied') + + edisgo_grid.set_time_series_reactive_power_control( + generators_parametrisation=pd.DataFrame( + { + "components": [[gens]], + "mode": ["default"], + "power_factor": ["default"], + }, + index=[1], + ), + loads_parametrisation=None, + storage_units_parametrisation=None + ) + + # Get curtailment requirements + gens_df = edisgo_grid.topology.generators_df + solar_wind_capacities = gens_df.groupby( + by=['type', 'weather_cell_id'] + )['nominal_capacity'].sum() + curt_cols = [ + i for i in specs['ren_curtailment'].columns + if i in solar_wind_capacities.index + ] + if not curt_cols: + raise ImportError( + f"MV grid {mv_grid_id}: Data doesn't match" + ) + curt_abs = pd.DataFrame( + columns=pd.MultiIndex.from_tuples(curt_cols)) + for col in curt_abs: + curt_abs[col] = ( + specs['ren_curtailment'][col] + * solar_wind_capacities[col]) # Storage Integration costs_without_storage = None @@ -1023,29 +1066,41 @@ def _run_edisgo( else: logger.info('No storage integration') - logger.info("MV grid {}: eDisGo grid analysis".format(mv_grid_id)) - + # ToDo Call optimisation edisgo_grid.reinforce(timesteps_pfa=self._timesteps_pfa) + # ToDo Is the following still necessary? if costs_without_storage is not None: costs_with_storage = ( - edisgo_grid.network.results.grid_expansion_costs[ + edisgo_grid.results.grid_expansion_costs[ 'total_costs'].sum()) if costs_with_storage >= costs_without_storage: logger.warning( "Storage did not benefit MV grid {}".format( mv_grid_id)) - st = edisgo_grid.network.mv_grid.graph.nodes_by_attribute( - 'storage') - for storage in st: - tools.disconnect_storage(edisgo_grid.network, storage) + storage_units = edisgo_grid.topology.storage_units_df + for storage in storage_units.index: + edisgo_grid.topology.remove_storage_unit(storage) self._status_update(mv_grid_id, 'end') path = os.path.join(self._results, str(mv_grid_id)) - edisgo_grid.network.results.save(path) + edisgo_grid.save( + directory=path, + save_topology=True, + save_timeseries=True, + save_results=True, + save_electromobility=False, + reduce_memory=True, + archive=True, + archive_type="zip", + parameters={ + "powerflow_results": ["pfa_p", "pfa_q", "v_res"], + "grid_expansion_results": ["grid_expansion_costs", "equipment_changes"] + } + ) - return {edisgo_grid.network.id: path} + return {edisgo_grid.topology.id: path} def _save_edisgo_results(self): From 4ba52940c38da234477b25531afb2e03e6c82319 Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 6 Sep 2022 15:01:31 +0200 Subject: [PATCH 012/126] Update EDisGoNetworks class --- ego/tools/edisgo_integration.py | 93 ++++++++++++++++++++++----------- 1 file changed, 63 insertions(+), 30 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 67dfde24..ecc70bab 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -51,6 +51,7 @@ from edisgo.edisgo import import_edisgo_from_files, EDisGo from edisgo.network.results import Results + from edisgo.network.timeseries import TimeSeries from edisgo.tools import tools from edisgo.tools.plots import mv_grid_topology @@ -134,12 +135,12 @@ def __init__(self, json_file, etrago_network): @property def network(self): """ - Container for eDisGo grids, including all results + Container for EDisGo objects, including all results Returns ------- - :obj:`dict` of :class:`edisgo.grid.network.EDisGo` - Dictionary of eDisGo objects, keyed by MV grid ID + dict[int, :class:`edisgo.EDisGo`] + Dictionary of EDisGo objects, keyed by MV grid ID """ return self._edisgo_grids @@ -153,6 +154,7 @@ def grid_choice(self): ------- :pandas:`pandas.DataFrame` Dataframe containing the chosen grids and their weightings + 'no_of_points_per_cluster', 'the_selected_network_id', 'represented_grids' """ return self._grid_choice @@ -318,21 +320,31 @@ def plot_mv_grid_topology(self, mv_grid_id, **kwargs): def _init_status(self): """ - Creates a Status file where all eDisGo statuses are tracked... + Creates a status csv file where statuses of MV grid calculations are tracked. + + The file is saved to the directory 'status'. Filename indicates date and time + the file was created. + + File contains the following information: + + * 'MV grid id' (index) + * 'cluster_perc' - percentage of grids represented by this grid + * 'start_time' - start time of calculation + * 'end_time' - end time of calculation + """ self._status_dir = 'status' if not os.path.exists(self._status_dir): os.makedirs(self._status_dir) - self._status_file = 'eGo_' + strftime("%Y-%m-%d_%H%M%S", localtime()) + self._status_file_name = 'eGo_' + strftime("%Y-%m-%d_%H%M%S", localtime()) status = self._grid_choice.copy() status = status.set_index('the_selected_network_id') - status.index.names = ['MV grid'] - - tot_reprs = self._grid_choice['no_of_points_per_cluster'].sum() + status.index.names = ['MV grid id'] - status['cluster_perc'] = status['no_of_points_per_cluster'] / tot_reprs + status['cluster_perc'] = (status['no_of_points_per_cluster'] / + self._grid_choice['no_of_points_per_cluster'].sum()) status['start_time'] = 'Not started yet' status['end_time'] = 'Not finished yet' @@ -342,18 +354,34 @@ def _init_status(self): axis=1, inplace=True) - self._status_path = os.path.join( + self._status_file_path = os.path.join( self._status_dir, - self._status_file + '.csv') + self._status_file_name + '.csv') - status.to_csv(self._status_path) + status.to_csv(self._status_file_path) def _status_update(self, mv_grid_id, time, message=None, show=True): """ - Updated eDisGo's status files + Updates status csv file where statuses of MV grid calculations are tracked. + + Parameters + ---------- + mv_grid_id : int + MV grid ID of the ding0 grid. + time : str + Can be either 'start' to set information on when the calculation started + or 'end' to set information on when the calculation ended. In case a + message is provided through parameter `message`, the message instead of the + time is set. + message : str or None (optional) + Message to set for 'start_time' or 'end_time'. If None, the current time + is set. Default: None. + show : bool (optional) + If True, shows a logging message with the status information. Default: True. + """ status = pd.read_csv( - self._status_path, + self._status_file_path, index_col=0) status['start_time'] = status['start_time'].astype(str) @@ -369,16 +397,23 @@ def _status_update(self, mv_grid_id, time, message=None, show=True): elif time == 'end': status.at[mv_grid_id, 'end_time'] = now if show: - logger.info("\n\neDisGo Status: \n\n" + logger.info("\n\neDisGo status: \n\n" + status.to_string() + "\n\n") - status.to_csv(self._status_path) + status.to_csv(self._status_file_path) def _update_edisgo_configs(self, edisgo_grid): """ This function overwrites some eDisGo configurations with eGo settings. + + The overwritten configs are: + + * config['db_connection']['section'] + * config['data_source']['oedb_data_source'] + * config['versioned']['version'] + """ # Info and Warning handling if not hasattr(self, '_suppress_log'): @@ -401,7 +436,7 @@ def _update_edisgo_configs(self, edisgo_grid): # Versioned ego_gridversion = self._grid_version - if ego_gridversion == None: + if ego_gridversion is None: ego_versioned = 'model_draft' if not self._suppress_log: logger.info("eGo's grid_version == None is " @@ -486,15 +521,11 @@ def _set_scenario_settings(self): self._grid_version = self._edisgo_args['gridversion'] self._timesteps_pfa = self._edisgo_args['timesteps_pfa'] self._solver = self._edisgo_args['solver'] - self._curtailment_voltage_threshold = self._edisgo_args[ - 'curtailment_voltage_threshold'] - self._ding0_files = self._edisgo_args['ding0_files'] + self._ding0_path = self._edisgo_args['ding0_path'] self._choice_mode = self._edisgo_args['choice_mode'] self._parallelization = self._edisgo_args['parallelization'] self._initial_reinforcement = self._edisgo_args[ 'initial_reinforcement'] - self._storage_distribution = self._edisgo_args['storage_distribution'] - self._apply_curtailment = self._edisgo_args['apply_curtailment'] self._cluster_attributes = self._edisgo_args['cluster_attributes'] self._only_cluster = self._edisgo_args['only_cluster'] self._max_workers = self._edisgo_args['max_workers'] @@ -504,9 +535,6 @@ def _set_scenario_settings(self): self._max_calc_time = self._edisgo_args['max_calc_time'] # Some basic checks - if (self._storage_distribution is True) & (self._ext_storage is False): - logger.warning("Storage distribution (MV grids) is active, " - + "but eTraGo dataset has no extendable storages") if not self._initial_reinforcement: raise NotImplementedError( "Skipping the initial reinforcement is not yet implemented" @@ -521,7 +549,7 @@ def _set_scenario_settings(self): else: self._versioned = False - def _successfull_grids(self): + def _successful_grids(self): """ Calculates the relative number of successfully calculated grids, including the cluster weightings @@ -835,9 +863,6 @@ def _run_edisgo( """ self._status_update(mv_grid_id, 'start', show=False) - storage_integration = self._storage_distribution - apply_curtailment = self._apply_curtailment - logger.info( 'MV grid {}: Calculating interface values'.format(mv_grid_id)) @@ -1115,8 +1140,16 @@ def _save_edisgo_results(self): self._grid_choice.to_csv(self._results + '/grid_choice.csv') def _load_edisgo_results(self): + """ + Loads eDisGo data for all specified grids + + Returns + -------- + dict[] + + """ - # Load the grid choice form CSV + # Load the grid choice from CSV self._grid_choice = pd.read_csv( os.path.join(self._csv_import, 'grid_choice.csv'), index_col=0) From d6246ceaf3822cedad24de5f6f185742275a05ca Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 6 Sep 2022 15:02:13 +0200 Subject: [PATCH 013/126] Remove unused imports --- ego/tools/edisgo_integration.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index ecc70bab..5a8e21f8 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -32,9 +32,6 @@ import os import pickle import logging -import traceback -import pypsa -import csv import dill import pandas as pd from time import localtime, sleep, strftime @@ -52,7 +49,6 @@ from edisgo.edisgo import import_edisgo_from_files, EDisGo from edisgo.network.results import Results from edisgo.network.timeseries import TimeSeries - from edisgo.tools import tools from edisgo.tools.plots import mv_grid_topology from ego.tools.specs import ( From 6543f7682f91b84738bc36937d34b1611f6d2378 Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 6 Sep 2022 15:07:01 +0200 Subject: [PATCH 014/126] Remove unused classes --- ego/tools/edisgo_integration.py | 63 --------------------------------- 1 file changed, 63 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 5a8e21f8..c794d4af 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1391,69 +1391,6 @@ def filter_df_by_carrier(df): ) -class _EDisGoImported: - """ - Imported (reduced) eDisGo class. - This class allows the import reduction to only the attributes used in eGo - """ - - def __init__( - self, - grid_expansion_costs, - s_res, - storages, - pypsa, - edisgo_config): - - self.network = _NetworkImported( - grid_expansion_costs, - s_res, - storages, - pypsa, - edisgo_config) - - -class _NetworkImported: - """ - Reduced eDisG network class, used of eGo's reimport - """ - - def __init__( - self, - grid_expansion_costs, - s_res, - storages, - pypsa, - edisgo_config): - - self.results = _ResultsImported( - grid_expansion_costs, - s_res, - storages) - - self.pypsa = pypsa - self.config = edisgo_config - - -class _ResultsImported: - """ - Reduced eDisG results class, used of eGo's reimport - """ - - def __init__( - self, - grid_expansion_costs, - s_res, - storages): - - self.grid_expansion_costs = grid_expansion_costs - self.storages = storages - self._s_res = s_res - - def s_res(self): - return self._s_res - - def parallelizer( ding0_id_list, func, From 19241267496cb19e65dcc6dc20ccefdba7a38164 Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 6 Sep 2022 15:17:06 +0200 Subject: [PATCH 015/126] Update eTraGo specs --- ego/tools/specs.py | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/ego/tools/specs.py b/ego/tools/specs.py index 1b7d2958..a051e80c 100644 --- a/ego/tools/specs.py +++ b/ego/tools/specs.py @@ -50,7 +50,7 @@ def get_etragospecs_direct(session, pf_post_lopf, max_cos_phi_renewable): """ - Reads eTraGo Results from Database and returns and returns + Reads eTraGo Results from Database and returns the interface values as a dictionary of corresponding dataframes Parameters @@ -64,13 +64,36 @@ def get_etragospecs_direct(session, scn_name : str Name of used scenario 'Status Quo', 'NEP 2035' or 'eGo 100' - Returns ------- :obj:`dict` of :pandas:`pandas.DataFrame` - Dataframes used as eDisGo inputs + Dataframes used as eDisGo inputs. + + * 'conv_dispatch' + Normalised dispatch of dispatchable generators per technology in p.u. + at the given bus. + * 'ren_dispatch' + Normalised dispatch of fluctuating generators per technology and weather + cell ID in p.u. at the given bus. + * 'ren_potential' + Normalised weather dependent feed-in potential of fluctuating generators + per technology and weather cell ID in p.u. at the given bus. + * 'ren_curtailment' + Normalised curtailment of fluctuating generators + per technology and weather cell ID in p.u. at the given bus. + * 'reactive_power' + Normalised reactive power time series of dispatchable and fluctuating + generators per technology (and weather cell ID) in p.u. at the given bus. + * 'battery_p_series' + Normalised dispatch of battery storage units in p.u. + at the given bus. + * 'battery_q_series' + Normalised reactive power time series of battery storage units in p.u. at + the given bus. + """ + logger.info('Specs for bus {}'.format(bus_id)) if pf_post_lopf: logger.info('Active and reactive power interface') @@ -115,17 +138,14 @@ def get_etragospecs_direct(session, all_gens_df = all_gens_df.rename(columns={"carrier": "name"}) all_gens_df = all_gens_df[all_gens_df['name'] != 'wind_offshore'] - - for index, row in all_gens_df.iterrows(): - name = row['name'] - if name == 'wind_onshore': - all_gens_df.at[index, 'name'] = 'wind' + all_gens_df.loc[ + all_gens_df["name"] == "wind_onshore", "name"] = "wind" # Conventionals t1 = time.perf_counter() performance.update({'Generator Data Processing': t1-t0}) - conv_df = all_gens_df[~all_gens_df.name.isin(weather_dpdnt)] + conv_df = all_gens_df[~all_gens_df['name'].isin(weather_dpdnt)] conv_dsptch = pd.DataFrame(0.0, index=snap_idx, @@ -209,9 +229,6 @@ def get_etragospecs_direct(session, dispatch = pd.DataFrame(0.0, index=snap_idx, columns=aggr_gens['ren_id']) - curtailment = pd.DataFrame(0.0, - index=snap_idx, - columns=aggr_gens['ren_id']) if pf_post_lopf: reactive_power = pd.DataFrame(0.0, index=snap_idx, From f8199faa85d0213725e8052f380a55c329139d4e Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 6 Sep 2022 15:17:33 +0200 Subject: [PATCH 016/126] Add minimum working example for edisgo interface --- ego/examples/edisgo_mwe.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 ego/examples/edisgo_mwe.py diff --git a/ego/examples/edisgo_mwe.py b/ego/examples/edisgo_mwe.py new file mode 100644 index 00000000..c624b5d6 --- /dev/null +++ b/ego/examples/edisgo_mwe.py @@ -0,0 +1,21 @@ +import os + +from pypsa import Network as PyPSANetwork +from ego.tools.edisgo_integration import EDisGoNetworks +from ego.tools.utilities import get_scenario_setting + +base_path = "/home/birgit/virtualenvs/eGo_interface_development/git_repos" + +# eTraGo network is not yet disaggregated +etrago_network = PyPSANetwork(os.path.join(base_path, "data/eTraGo_results")) +# manually overwrite bus ID to have busses in the chosen grid +etrago_network.generators.loc[etrago_network.generators.bus == "16", "bus"] = "26533" + +json_file = get_scenario_setting() + +edisgo_networks = EDisGoNetworks( + json_file=json_file, + etrago_network=etrago_network +) + +print("x") \ No newline at end of file From 44a998f883fa87102b7097035723ced64855cbcf Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 6 Sep 2022 15:37:29 +0200 Subject: [PATCH 017/126] Scenario settings for edisgo mwe --- ego/examples/scenario_setting.json | 59 ++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 ego/examples/scenario_setting.json diff --git a/ego/examples/scenario_setting.json b/ego/examples/scenario_setting.json new file mode 100644 index 00000000..15513362 --- /dev/null +++ b/ego/examples/scenario_setting.json @@ -0,0 +1,59 @@ +{ + "eGo": { + "eTraGo": true, + "eDisGo": true, + "csv_import_eTraGo": false, + "csv_import_eDisGo": false + }, + "eTraGo": { + "db": "oedb", + "gridversion": "v0.4.5", + "method": "lopf", + "pf_post_lopf": false, + "start_snapshot": 1000, + "end_snapshot" : 1005, + "solver": "gurobi", + "solver_options":{}, + "scn_name": "eGo 100", + "scn_extension": null, + "scn_decommissioning": null, + "lpfile": false, + "csv_export": "results/your_results", + "db_export": false, + "extendable": ["storage", "network"], + "generator_noise": 789456, + "minimize_loading": false, + "ramp_limits": false, + "extra_functionality": null, + "network_clustering_kmeans": 10, + "load_cluster": false, + "network_clustering_ehv": false, + "disaggregation": "uniform", + "snapshot_clustering": false, + "parallelisation": false, + "skip_snapshots": false, + "line_grouping": false, + "branch_capacity_factor": {"HV": 0.5, "eHV" : 0.7}, + "load_shedding": false, + "foreign_lines" :{"carrier": "AC", "capacity": "osmTGmod"}, + "comments": "" + }, + "eDisGo": { + "db": "oedb", + "gridversion": "v0.4.5", + "ding0_path": "/home/birgit/virtualenvs/eGo_interface_development/git_repos/data/ding0_grids", + "choice_mode": "manual", + "cluster_attributes":["farthest_node", "wind_cap", "solar_cap", "extended_storage"], + "only_cluster": false, + "manual_grids": [3280], + "no_grids": 2, + "parallelization":false, + "max_calc_time": 0.5, + "max_workers":2, + "initial_reinforcement":true, + "max_cos_phi_renewable": 0.9, + "results": "/home/birgit/virtualenvs/eGo_interface_development/git_repos/data", + "solver": "glpk", + "timesteps_pfa": "snapshot_analysis" + } +} From 617ffdfc03c221ed9b748e000b364d6eddc88d1e Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 7 Sep 2022 11:24:30 +0200 Subject: [PATCH 018/126] Update run_edisgo --- ego/tools/edisgo_integration.py | 51 +++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index c794d4af..c15b7c0c 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -50,6 +50,7 @@ from edisgo.network.results import Results from edisgo.network.timeseries import TimeSeries from edisgo.tools.plots import mv_grid_topology + from edisgo.flex_opt import q_control from ego.tools.specs import ( get_etragospecs_direct @@ -854,8 +855,9 @@ def _run_edisgo( Returns ------- - :class:`edisgo.grid.network.EDisGo` + :class:`edisgo.EDisGo` Returns the complete eDisGo container, also including results + """ self._status_update(mv_grid_id, 'start', show=False) @@ -1035,7 +1037,6 @@ def _run_edisgo( ) pass else: - edisgo_grid.set_time_series_reactive_power_control( generators_parametrisation=pd.DataFrame( { @@ -1069,23 +1070,37 @@ def _run_edisgo( specs['ren_curtailment'][col] * solar_wind_capacities[col]) - # Storage Integration - costs_without_storage = None - if storage_integration: - if self._ext_storage: - if not specs['battery_p_series'] is None: - logger.info('Integrating storages in MV grid') - edisgo_grid.integrate_storage( - timeseries=specs['battery_p_series'], - position='distribute_storages_mv', - timeseries_reactive_power=specs[ - 'battery_q_series' - ]) # None if no pf_post_lopf - costs_without_storage = ( - edisgo_grid.network.results.storages_costs_reduction[ - 'grid_expansion_costs_initial'].values[0]) + # Get storage information + storage_capacity = specs["battery_capacity"] + storage_dispatch_active_power = specs['battery_p_series'] + if self._pf_post_lopf: + storage_dispatch_reactive_power = specs['battery_q_series'] else: - logger.info('No storage integration') + q_sign = q_control.get_q_sign_generator( + edisgo_grid.config["reactive_power_mode"]["mv_storage"]) + power_factor = q_control.get_q_sign_generator( + edisgo_grid.config["reactive_power_factor"]["mv_storage"]) + storage_dispatch_reactive_power = q_control.fixed_cosphi( + storage_dispatch_active_power, q_sign, power_factor + ) + + # DSM + dsm_active_power = specs["dsm_active_power"] + # ToDo: Get DSM potential per load (needs to be added as a function to eDisGo) + + # Heat pumps + # Import heat pumps - also gets heat demand and COP time series per heat pump + edisgo_grid.import_heat_pumps(scenario=self._scn_name) + heat_pump_rural_active_power = specs["rural_heat_t"] + heat_pump_central_active_power = specs["central_heat_t"] + # thermal storage units + heat_pump_rural_capacity = specs["rural_heat_store"] + heat_pump_central_capacity = specs["central_heat_store"] + # ToDo: Distribute total thermal storage capacity to heat pumps (needs to be + # added as a function to eDisGo) and write storage capacity to + # thermal_storage_units_df + + # ToDo BEV # ToDo Call optimisation edisgo_grid.reinforce(timesteps_pfa=self._timesteps_pfa) From d8fc9092e18e70c4fd69defeafd6078432f0049f Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 7 Sep 2022 12:05:46 +0200 Subject: [PATCH 019/126] Update run_edisgo --- ego/tools/edisgo_integration.py | 73 ++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index c15b7c0c..e0773d92 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -954,7 +954,7 @@ def _run_edisgo( # Reload the (original) eDisGo configs edisgo_grid.config = "default" edisgo_grid.timeseries = TimeSeries( - timeindex=specs['conv_dispatch'].index + timeindex=specs['dispatchable_generators_active_power'].index ) # ########################################################################### @@ -995,7 +995,7 @@ def _run_edisgo( # Set dispatchable generator time series # Active power edisgo_grid.set_time_series_active_power_predefined( - dispatchable_generators_ts=specs['conv_dispatch'], + dispatchable_generators_ts=specs['dispatchable_generators_active_power'], ) # Reactive power gens = edisgo_grid.topology.generators_df[ @@ -1004,7 +1004,7 @@ def _run_edisgo( if self._pf_post_lopf: # ToDo Use eTraGo time series to set reactive power (scale by nominal power) edisgo_grid.set_time_series_manual( - generators_q=specs['reactive_power'].loc[:, []], + generators_q=specs['generators_reactive_power'].loc[:, []], ) pass else: @@ -1024,7 +1024,7 @@ def _run_edisgo( # Set fluctuating generator time series # Active power edisgo_grid.set_time_series_active_power_predefined( - fluctuating_generators_ts=specs['ren_potential'], + fluctuating_generators_ts=specs['renewables_potential'], ) # Reactive power gens = edisgo_grid.topology.generators_df[ @@ -1033,7 +1033,7 @@ def _run_edisgo( if self._pf_post_lopf: # ToDo Use eTraGo time series to set reactive power (scale by nominal power) edisgo_grid.set_time_series_manual( - generators_q=specs['reactive_power'].loc[:, []] + generators_q=specs['generators_reactive_power'].loc[:, []] ) pass else: @@ -1056,7 +1056,7 @@ def _run_edisgo( by=['type', 'weather_cell_id'] )['nominal_capacity'].sum() curt_cols = [ - i for i in specs['ren_curtailment'].columns + i for i in specs['renewables_curtailment'].columns if i in solar_wind_capacities.index ] if not curt_cols: @@ -1067,21 +1067,20 @@ def _run_edisgo( columns=pd.MultiIndex.from_tuples(curt_cols)) for col in curt_abs: curt_abs[col] = ( - specs['ren_curtailment'][col] + specs['renewables_curtailment'][col] * solar_wind_capacities[col]) # Get storage information - storage_capacity = specs["battery_capacity"] - storage_dispatch_active_power = specs['battery_p_series'] + storage_units_capacity = specs["storage_units_capacity"] + storage_units_active_power = specs['storage_units_active_power'] if self._pf_post_lopf: - storage_dispatch_reactive_power = specs['battery_q_series'] + storage_units_reactive_power = specs['storage_units_reactive_power'] else: q_sign = q_control.get_q_sign_generator( edisgo_grid.config["reactive_power_mode"]["mv_storage"]) - power_factor = q_control.get_q_sign_generator( - edisgo_grid.config["reactive_power_factor"]["mv_storage"]) - storage_dispatch_reactive_power = q_control.fixed_cosphi( - storage_dispatch_active_power, q_sign, power_factor + power_factor = edisgo_grid.config["reactive_power_factor"]["mv_storage"] + storage_units_reactive_power = q_control.fixed_cosphi( + storage_units_active_power, q_sign, power_factor ) # DSM @@ -1091,33 +1090,43 @@ def _run_edisgo( # Heat pumps # Import heat pumps - also gets heat demand and COP time series per heat pump edisgo_grid.import_heat_pumps(scenario=self._scn_name) - heat_pump_rural_active_power = specs["rural_heat_t"] - heat_pump_central_active_power = specs["central_heat_t"] - # thermal storage units - heat_pump_rural_capacity = specs["rural_heat_store"] - heat_pump_central_capacity = specs["central_heat_store"] + # Active power + heat_pump_rural_active_power = specs["heat_pump_rural_active_power"] + heat_pump_central_active_power = specs["heat_pump_central_active_power"] + # Reactive power + if self._pf_post_lopf: + heat_pump_rural_reactive_power = specs["heat_pump_rural_reactive_power"] + heat_pump_central_reactive_power = specs["heat_pump_central_reactive_power"] + else: + q_sign = q_control.get_q_sign_load( + edisgo_grid.config["reactive_power_mode"]["mv_hp"]) + power_factor = edisgo_grid.config["reactive_power_factor"]["mv_hp"] + heat_pump_rural_reactive_power = q_control.fixed_cosphi( + heat_pump_rural_active_power, q_sign, power_factor + ) + heat_pump_central_reactive_power = q_control.fixed_cosphi( + heat_pump_central_active_power, q_sign, power_factor + ) + + # Thermal storage units + thermal_storage_rural_capacity = specs["thermal_storage_rural_capacity"] + thermal_storage_central_capacity = specs["thermal_storage_central_capacity"] # ToDo: Distribute total thermal storage capacity to heat pumps (needs to be # added as a function to eDisGo) and write storage capacity to # thermal_storage_units_df # ToDo BEV + # Import charging points with standing times, etc. + edisgo_grid.import_electromobility( + simbev_directory="oedb", + tracbev_directory="oedb" + ) + electromobility_active_power = specs["electromobility_active_power"] # ToDo Call optimisation + edisgo_grid.check_integrity() edisgo_grid.reinforce(timesteps_pfa=self._timesteps_pfa) - # ToDo Is the following still necessary? - if costs_without_storage is not None: - costs_with_storage = ( - edisgo_grid.results.grid_expansion_costs[ - 'total_costs'].sum()) - if costs_with_storage >= costs_without_storage: - logger.warning( - "Storage did not benefit MV grid {}".format( - mv_grid_id)) - storage_units = edisgo_grid.topology.storage_units_df - for storage in storage_units.index: - edisgo_grid.topology.remove_storage_unit(storage) - self._status_update(mv_grid_id, 'end') path = os.path.join(self._results, str(mv_grid_id)) From 191b793cc63e2ae67c3def794a13b0a77620ea29 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 7 Sep 2022 14:34:58 +0200 Subject: [PATCH 020/126] Add geo- and solarthermal energy --- ego/tools/edisgo_integration.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index e0773d92..9a194970 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1115,7 +1115,12 @@ def _run_edisgo( # added as a function to eDisGo) and write storage capacity to # thermal_storage_units_df - # ToDo BEV + # Solar- and geothermal energy (district heating) + geothermal_energy_feedin_district_heating = specs[ + "geothermal_energy_feedin_district_heating"] + solarthermal_energy_feedin_district_heating = specs[ + "solarthermal_energy_feedin_district_heating"] + # Import charging points with standing times, etc. edisgo_grid.import_electromobility( simbev_directory="oedb", From 71b597f821b5c8fb8591c48d5e3e069c4bbfc956 Mon Sep 17 00:00:00 2001 From: Malte Jahn Date: Thu, 22 Sep 2022 12:33:08 +0200 Subject: [PATCH 021/126] Update the interface between eTraGo and eDisGo - Change the _ETraGoData Class to ETraGoMinimalData and move from edisgo_integration.py to interface.py - Change the get_etragospecs_direct function to get_etrago_results_per_bus and move from specs.py to interface.py - Add tests for the ETraGoMinimalData Class and the get_etragospecs_direct function --- ego/tools/edisgo_integration.py | 11 +- ego/tools/interface.py | 760 ++++++ ego/tools/specs.py | 383 --- pytest.ini | 6 + tests/conftest.py | 35 + tests/data/create_test_grid.ipynb | 2052 +++++++++++++++++ tests/data/etrago_test_network_1/buses.csv | 10 + .../etrago_test_network_1/generators-p.csv | 4 + .../generators-p_max_pu.csv | 4 + .../generators-p_min_pu.csv | 4 + .../etrago_test_network_1/generators-q.csv | 4 + .../data/etrago_test_network_1/generators.csv | 31 + tests/data/etrago_test_network_1/links-p0.csv | 4 + tests/data/etrago_test_network_1/links-p1.csv | 4 + tests/data/etrago_test_network_1/links.csv | 18 + .../data/etrago_test_network_1/snapshots.csv | 4 + .../etrago_test_network_1/storage_units-p.csv | 4 + .../etrago_test_network_1/storage_units-q.csv | 4 + .../etrago_test_network_1/storage_units.csv | 4 + tests/data/etrago_test_network_1/stores.csv | 5 + .../dispatchable_generators_active_power.csv | 4 + ...dispatchable_generators_reactive_power.csv | 4 + .../dsm_active_power.csv | 4 + .../dsm_reactive_power.csv | 4 + .../electromobility_active_power.csv | 4 + .../electromobility_reactive_power.csv | 4 + ...thermal_energy_feedin_district_heating.csv | 4 + .../heat_central_active_power.csv | 4 + .../heat_central_reactive_power.csv | 4 + .../heat_pump_rural_active_power.csv | 4 + .../heat_pump_rural_reactive_power.csv | 4 + .../renewables_curtailment.csv | 6 + .../renewables_dispatch_reactive_power.csv | 6 + .../renewables_potential.csv | 6 + ...thermal_energy_feedin_district_heating.csv | 4 + .../storage_units_active_power.csv | 4 + .../storage_units_reactive_power.csv | 4 + .../dispatchable_generators_active_power.csv | 4 + ...dispatchable_generators_reactive_power.csv | 4 + .../dsm_active_power.csv | 4 + .../dsm_reactive_power.csv | 4 + .../electromobility_active_power.csv | 4 + .../electromobility_reactive_power.csv | 4 + ...thermal_energy_feedin_district_heating.csv | 4 + .../heat_central_active_power.csv | 4 + .../heat_central_reactive_power.csv | 4 + .../heat_pump_rural_active_power.csv | 4 + .../heat_pump_rural_reactive_power.csv | 4 + .../renewables_curtailment.csv | 6 + .../renewables_dispatch_reactive_power.csv | 6 + .../renewables_potential.csv | 6 + ...thermal_energy_feedin_district_heating.csv | 4 + .../storage_units_active_power.csv | 4 + .../storage_units_reactive_power.csv | 4 + tests/tools/test_interface.py | 265 +++ 55 files changed, 3375 insertions(+), 389 deletions(-) create mode 100644 ego/tools/interface.py delete mode 100644 ego/tools/specs.py create mode 100644 pytest.ini create mode 100644 tests/conftest.py create mode 100644 tests/data/create_test_grid.ipynb create mode 100644 tests/data/etrago_test_network_1/buses.csv create mode 100644 tests/data/etrago_test_network_1/generators-p.csv create mode 100644 tests/data/etrago_test_network_1/generators-p_max_pu.csv create mode 100644 tests/data/etrago_test_network_1/generators-p_min_pu.csv create mode 100644 tests/data/etrago_test_network_1/generators-q.csv create mode 100644 tests/data/etrago_test_network_1/generators.csv create mode 100644 tests/data/etrago_test_network_1/links-p0.csv create mode 100644 tests/data/etrago_test_network_1/links-p1.csv create mode 100644 tests/data/etrago_test_network_1/links.csv create mode 100644 tests/data/etrago_test_network_1/snapshots.csv create mode 100644 tests/data/etrago_test_network_1/storage_units-p.csv create mode 100644 tests/data/etrago_test_network_1/storage_units-q.csv create mode 100644 tests/data/etrago_test_network_1/storage_units.csv create mode 100644 tests/data/etrago_test_network_1/stores.csv create mode 100644 tests/data/interface_results_reference_data/dispatchable_generators_active_power.csv create mode 100644 tests/data/interface_results_reference_data/dispatchable_generators_reactive_power.csv create mode 100644 tests/data/interface_results_reference_data/dsm_active_power.csv create mode 100644 tests/data/interface_results_reference_data/dsm_reactive_power.csv create mode 100644 tests/data/interface_results_reference_data/electromobility_active_power.csv create mode 100644 tests/data/interface_results_reference_data/electromobility_reactive_power.csv create mode 100644 tests/data/interface_results_reference_data/geothermal_energy_feedin_district_heating.csv create mode 100644 tests/data/interface_results_reference_data/heat_central_active_power.csv create mode 100644 tests/data/interface_results_reference_data/heat_central_reactive_power.csv create mode 100644 tests/data/interface_results_reference_data/heat_pump_rural_active_power.csv create mode 100644 tests/data/interface_results_reference_data/heat_pump_rural_reactive_power.csv create mode 100644 tests/data/interface_results_reference_data/renewables_curtailment.csv create mode 100644 tests/data/interface_results_reference_data/renewables_dispatch_reactive_power.csv create mode 100644 tests/data/interface_results_reference_data/renewables_potential.csv create mode 100644 tests/data/interface_results_reference_data/solarthermal_energy_feedin_district_heating.csv create mode 100644 tests/data/interface_results_reference_data/storage_units_active_power.csv create mode 100644 tests/data/interface_results_reference_data/storage_units_reactive_power.csv create mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/dispatchable_generators_active_power.csv create mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/dispatchable_generators_reactive_power.csv create mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/dsm_active_power.csv create mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/dsm_reactive_power.csv create mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/electromobility_active_power.csv create mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/electromobility_reactive_power.csv create mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/geothermal_energy_feedin_district_heating.csv create mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/heat_central_active_power.csv create mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/heat_central_reactive_power.csv create mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/heat_pump_rural_active_power.csv create mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/heat_pump_rural_reactive_power.csv create mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/renewables_curtailment.csv create mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/renewables_dispatch_reactive_power.csv create mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/renewables_potential.csv create mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/solarthermal_energy_feedin_district_heating.csv create mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/storage_units_active_power.csv create mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/storage_units_reactive_power.csv create mode 100644 tests/tools/test_interface.py diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 9a194970..1fc2fce2 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -52,9 +52,8 @@ from edisgo.tools.plots import mv_grid_topology from edisgo.flex_opt import q_control - from ego.tools.specs import ( - get_etragospecs_direct - ) + from ego.tools.interface import ETraGoMinimalData + from ego.tools.interface import get_etrago_results_per_bus from ego.tools.mv_cluster import ( analyze_attributes, cluster_mv_grids) @@ -89,7 +88,7 @@ def __init__(self, json_file, etrago_network): self._set_scenario_settings() # Create reduced eTraGo network - self._etrago_network = _ETraGoData(etrago_network) + self._etrago_network = ETraGoMinimalData(etrago_network) del etrago_network # Program information @@ -873,7 +872,7 @@ def _run_edisgo( bus_id = self._get_bus_id_from_mv_grid(session, mv_grid_id) # Calculate Interface values for this MV grid - specs = get_etragospecs_direct( + specs = get_etrago_results_per_bus( session, bus_id, self._etrago_network, @@ -954,7 +953,7 @@ def _run_edisgo( # Reload the (original) eDisGo configs edisgo_grid.config = "default" edisgo_grid.timeseries = TimeSeries( - timeindex=specs['dispatchable_generators_active_power'].index + timeindex=specs['timeindex'] ) # ########################################################################### diff --git a/ego/tools/interface.py b/ego/tools/interface.py new file mode 100644 index 00000000..2b8176e3 --- /dev/null +++ b/ego/tools/interface.py @@ -0,0 +1,760 @@ +# -*- coding: utf-8 -*- +# Copyright 2016-2018 Europa-Universität Flensburg, +# Flensburg University of Applied Sciences, +# Centre for Sustainable Energy Systems +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# File description +""" +This files contains all eGo interface functions +""" + +__copyright__ = "Europa-Universität Flensburg, " "Centre for Sustainable Energy Systems" +__license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" +__author__ = "wolf_bunke,maltesc,mltja" + +# Import +# General Packages +import os +import pandas as pd +import time +import math + + +if not "READTHEDOCS" in os.environ: + from egoio.db_tables import model_draft + from egoio.db_tables import supply + +import logging + +logger = logging.getLogger(__name__) + + +class ETraGoMinimalData: + """ + Container for minimal eTraGo network. + + This minimal network only contains information relevant for eDisGo. + + Parameters + ---------- + etrago_network : :pypsa:`PyPSA.Network` + + """ + + def __init__(self, etrago_network): + def set_filtered_attribute(etrago_network_obj, component): + + # filter components + columns_to_save = { + "links": ["bus0", "bus1", "carrier", "p_nom", "p_nom_opt"], + "generators": ["bus", "carrier", "p_nom", "p_nom_opt"], + "stores": ["bus", "carrier", "e_nom", "e_nom_opt"], + "storage_units": [ + "bus", + "carrier", + "p_nom_opt", + "p_nom_extendable", + "max_hours", + ], + "loads": ["bus", "p_set"], + } + columns_to_save = columns_to_save[component] + + df = getattr(etrago_network_obj, component) + + logger.info( + f"Component: {component} has unique carriers: {df.carrier.unique()}" + ) + + setattr(self, component, df[columns_to_save]) + + # filter components timeseries + attribute_to_save = { + "links": ["p0", "p1"], + "generators": ["p", "p_max_pu", "q"], + "stores": ["p"], + "storage_units": ["p", "q"], + "loads": ["p"], + } + attribute_to_save = attribute_to_save[component] + + component_timeseries_dict = getattr(etrago_network_obj, component + "_t") + + new_component_timeseries_dict = { + attribute: component_timeseries_dict[attribute] + for attribute in attribute_to_save + } + + setattr(self, component + "_t", new_component_timeseries_dict) + + t_start = time.perf_counter() + + self.snapshots = etrago_network.snapshots + + components = ["storage_units", "stores", "generators", "links", "loads"] + for component in components: + set_filtered_attribute(etrago_network, component) + + logger.info(f"Data selection time {time.perf_counter()-t_start}") + + +# Functions +def get_weather_id_for_generator(grid_version, session, generator_index, scn_name): + # ToDo: Refactor function + if grid_version is None: + logger.warning("Weather_id taken from model_draft (not tested)") + + ormclass_gen_single = model_draft.__getattribute__("EgoSupplyPfGeneratorSingle") + + weather_id = ( + session.query(ormclass_gen_single.w_id) + .filter( + ormclass_gen_single.aggr_id == generator_index, + ormclass_gen_single.scn_name == scn_name, + ) + .limit(1) + .scalar() + ) + + else: + ormclass_aggr_w = supply.__getattribute__("EgoAggrWeather") + + weather_id = ( + session.query(ormclass_aggr_w.w_id) + .filter( + ormclass_aggr_w.aggr_id == generator_index, + # ormclass_aggr_w.scn_name == scn_name, + ormclass_aggr_w.version == grid_version, + ) + .limit(1) + .scalar() + ) + + return weather_id + + +def get_etrago_results_per_bus( + session, + bus_id, + etrago_network, + grid_version, + scn_name, + pf_post_lopf, + max_cos_phi_renewable, +): + """ + Reads eTraGo Results from Database and returns + the interface values as a dictionary of corresponding dataframes + + Parameters + ---------- + session : sqlalchemy.orm.session.Session + Handles conversations with the database. + bus_id : int + ID of the corresponding HV bus + etrago_network: :class:`etrago.tools.io.NetworkScenario` + eTraGo network object compiled by :meth:`etrago.appl.etrago` + scn_name : str + Name of used scenario 'Status Quo', 'NEP 2035' or 'eGo 100' + pf_post_lopf : bool + Variable if pf after lopf was run. + max_cos_phi_renewable : float or None + If not None, the maximum reactive power is set by the given power factor + according to the dispatched active power. + + Returns + ------- + :obj:`dict` of :pandas:`pandas.DataFrame` + Dataframes used as eDisGo inputs. + + * 'timeindex' + Timeindex of the etrago-object. + Type: pd.Datetimeindex + + * 'dispatchable_generators_active_power' + Normalised dispatch of active power of dispatchable generators per + technology in p.u. at the given bus. + Type: pd.DataFrame + Columns: Carrier + Unit: pu + + * 'dispatchable_generators_reactive_power' + Normalised dispatch of reactive power of dispatchable generators per + technology in p.u. at the given bus. + Type: pd.DataFrame + Columns: Carrier + Unit: pu + + * 'renewables_potential' + Normalised weather dependent feed-in potential of fluctuating generators + per technology and weather cell ID in p.u. at the given bus. + Type: pd.DataFrame + Columns: Carrier / Weather Cell ID + Unit: pu + + * 'renewables_curtailment' + Normalised curtailment of fluctuating generators per + technology and weather cell ID in p.u. at the given bus. + Type: pd.DataFrame + Columns: Carrier / Weather Cell ID + Unit: pu + + * 'renewables_dispatch_reactive_power' + Normalised reactive power time series of fluctuating generators per + technology and weather cell ID in p.u. at the given bus. + Type: pd.DataFrame + Columns: Carrier / Weather Cell ID + Unit: pu + + * 'storage_units_capacity' + Storage unit capacity at the given bus. + Type: float + Unit: MWh + + * 'storage_units_active_power' + Active power time series of battery storage units at the given bus. + Type: pd.DataFrame + Columns: Carrier + Unit: MW + + * 'storage_units_reactive_power' + Reactive power time series of battery storage units at the given bus. + Type: pd.DataFrame + Columns: Carrier + Unit: MVar + + * 'dsm_active_power' + Active power time series of DSM units at the given bus. + Type: pd.DataFrame + Columns: Carrier + Unit: MW + + * 'dsm_reactive_power' + Reactive power time series of DSM units at the given bus. + Type: pd.DataFrame + Columns: Carrier + Unit: MVar + + * 'heat_central_active_power' + Active power time series of central heat units at the given bus. + Type: pd.DataFrame + Columns: Carrier + Unit: MW + + * 'heat_central_reactive_power' + Reactive power time series of central heat units at the given bus. + Type: pd.DataFrame + Columns: Carrier + Unit: MVar + + * 'thermal_storage_central_capacity' + Capacity of the storage at the bus where the central heat units feed in. + Type: float + Unit: MWh + + * 'geothermal_energy_feedin_district_heating' + Geothermal feedin time series at the heat bus. + Type: pd.DataFrame + Columns: Carrier + Unit: MW + + * 'solarthermal_energy_feedin_district_heating' + Solarthermal feedin time series at the heat bus. + Type: pd.DataFrame + Columns: Carrier + Unit: MW + + * 'heat_pump_rural_active_power' + Active power time series of rural heat pump units at the given bus. + Type: pd.DataFrame + Columns: Carrier + Unit: MW + + * 'heat_pump_rural_reactive_power' + Reactive power time series of rural heat pump units at the given bus. + Type: pd.DataFrame + Columns: Carrier + Unit: MVar + + * 'thermal_storage_rural_capacity' + Capacity of the storage at the bus where the rural heat units feed in. + Type: float + Unit: MWh + + * 'electromobility_active_power' + Active power time series of electromobility units at the given bus. + Type: pd.DataFrame + Columns: Carrier + Unit: MW + + * 'electromobility_reactive_power' + Reactive power time series of electromobility units at the given bus. + Type: pd.DataFrame + Columns: Carrier + Unit: MVar + + """ + performance = {} + t0 = time.perf_counter() + + logger.info("Specs for bus {}".format(bus_id)) + if pf_post_lopf: + logger.info("Active and reactive power interface") + else: + logger.info("Only active power interface") + + etrago_results_per_bus = {} + timeseries_index = etrago_network.snapshots + etrago_results_per_bus["timeindex"] = timeseries_index + # Prefill dict with None + result_keys = [ + "dispatchable_generators_active_power", + "dispatchable_generators_reactive_power", + "renewables_potential", + "renewables_curtailment", + "renewables_dispatch_reactive_power", + "storage_units_capacity", + "storage_units_active_power", + "storage_units_reactive_power", + "dsm_active_power", + "dsm_reactive_power", + "heat_central_active_power", + "heat_central_reactive_power", + "thermal_storage_central_capacity", + "geothermal_energy_feedin_district_heating", + "solarthermal_energy_feedin_district_heating", + "heat_pump_rural_active_power", + "heat_pump_rural_reactive_power", + "thermal_storage_rural_capacity", + "electromobility_active_power", + "electromobility_reactive_power", + ] + for key in result_keys: + etrago_results_per_bus[key] = None + + # Filter dataframes + # Generators + generators_df = etrago_network.generators[ + etrago_network.generators["bus"] == str(bus_id) + ] + # Links + links_df = etrago_network.links[ + (etrago_network.links["bus0"] == str(bus_id)) + | (etrago_network.links["bus1"] == str(bus_id)) + ] + t1 = time.perf_counter() + performance.update({"General Data Processing": t1 - t0}) + + # Dispatchable generators + dispatchable_generators_df_p = pd.DataFrame(index=timeseries_index) + if pf_post_lopf: + dispatchable_generators_df_q = pd.DataFrame(index=timeseries_index) + + dispatchable_generators = [ + "biomass", + "central_biomass_CHP", + "industrial_biomass_CHP", + "run_of_river", + "gas", + "other_non_renewable", + "reservoir", + "oil", + "other_renewable", + "coal", + "nuclear", + "lignite", + "CH4", + "central_biomass_CHP_heat", + ] + dispatchable_generators_df = generators_df[ + generators_df["carrier"].isin(dispatchable_generators) + ] + if not dispatchable_generators_df.empty: + # technology_dict = { + # "biomass": ["biomass", "central_biomass_CHP"], + # "run_of_river": ["run_of_river"], + # "gas": ["gas"], + # "other_non_renewable": ["other_non_renewable"], + # "reservoir": ["reservoir"], + # } + # for key, item in technology_dict.items(): + # for carrier in item: + # dispatchable_generators_df.loc[dispatchable_generators_df["carrier"] == carrier, "carrier"] = key + + for carrier in dispatchable_generators_df["carrier"].unique(): + p_nom = dispatchable_generators_df.loc[ + dispatchable_generators_df["carrier"] == carrier, "p_nom" + ].sum() + columns_to_aggregate = dispatchable_generators_df[ + dispatchable_generators_df["carrier"] == carrier + ].index + + dispatchable_generators_df_p[carrier] = ( + etrago_network.generators_t["p"][columns_to_aggregate].sum( + axis="columns" + ) + / p_nom + ) + if pf_post_lopf: + dispatchable_generators_df_q[carrier] = ( + etrago_network.generators_t["q"][columns_to_aggregate].sum( + axis="columns" + ) + / p_nom + ) + + # Add CHP to conventional generators + chp_df = links_df[links_df["carrier"] == "central_gas_CHP"] + if not chp_df.empty: + p_nom = chp_df["p_nom_opt"].sum() + dispatchable_generators_df_p["central_gas_CHP"] = ( + etrago_network.links_t["p1"][chp_df.index].sum(axis="columns") / p_nom + ) + if pf_post_lopf: + dispatchable_generators_df_q["central_gas_CHP"] = ( + 0 * dispatchable_generators_df_p["central_gas_CHP"] + ) + + etrago_results_per_bus[ + "dispatchable_generators_active_power" + ] = dispatchable_generators_df_p + if pf_post_lopf: + etrago_results_per_bus[ + "dispatchable_generators_reactive_power" + ] = dispatchable_generators_df_q + + t2 = time.perf_counter() + performance.update({"Dispatchable generators": t2 - t1}) + + # Renewables + weather_dependent_generators = [ + "solar", + "solar_rooftop", + "wind_onshore", + ] + weather_dependent_generators_df = generators_df[ + generators_df.carrier.isin(weather_dependent_generators) + ] + if not weather_dependent_generators_df.empty: + for generator_index in weather_dependent_generators_df.index: + weather_id = get_weather_id_for_generator( + grid_version, session, generator_index, scn_name + ) + weather_dependent_generators_df.loc[generator_index, "w_id"] = str( + weather_id + ) + + technology_dict = { + "solar": ["solar", "solar_rooftop"], + } + for key, item in technology_dict.items(): + for carrier in item: + weather_dependent_generators_df.loc[ + weather_dependent_generators_df["carrier"] == carrier, "carrier" + ] = key + + # Aggregation of p_nom + aggregated_weather_dependent_generators_df = ( + weather_dependent_generators_df.groupby(["carrier", "w_id"]) + .agg({"p_nom": "sum"}) + .reset_index() + ) + + # Dispatch and Curtailment + weather_dependent_generators_df_potential_p = pd.DataFrame( + 0.0, + index=timeseries_index, + columns=aggregated_weather_dependent_generators_df.index, + ) + weather_dependent_generators_df_dispatch_p = pd.DataFrame( + 0.0, + index=timeseries_index, + columns=aggregated_weather_dependent_generators_df.index, + ) + if pf_post_lopf: + weather_dependent_generators_df_dispatch_q = pd.DataFrame( + 0.0, + index=timeseries_index, + columns=aggregated_weather_dependent_generators_df.index, + ) + + for index, carrier, w_id, p_nom in weather_dependent_generators_df[ + ["carrier", "w_id", "p_nom"] + ].itertuples(): + aggregated_idx = aggregated_weather_dependent_generators_df[ + (aggregated_weather_dependent_generators_df["carrier"] == carrier) + & (aggregated_weather_dependent_generators_df["w_id"] == w_id) + ].index.values[0] + p_nom_aggregated = aggregated_weather_dependent_generators_df.loc[ + aggregated_idx, "p_nom" + ] + + p_series = etrago_network.generators_t["p"][index] + p_normed_series = p_series / p_nom_aggregated + + p_max_pu_series = etrago_network.generators_t["p_max_pu"][index] + p_max_pu_normed_series = p_max_pu_series * p_nom / p_nom_aggregated + + if pf_post_lopf: + if max_cos_phi_renewable: + logger.info( + "Applying Q limit (max cos(phi)={})".format( + max_cos_phi_renewable + ) + ) + + phi = math.acos(max_cos_phi_renewable) + + q_series = pd.Series(0, index=timeseries_index) + + for timestep in timeseries_index: + p = etrago_network.generators_t["p"].loc[timestep, index] + q = etrago_network.generators_t["q"].loc[timestep, index] + + q_max = p * math.tan(phi) + q_min = -p * math.tan(phi) + + if q > q_max: + q = q_max + elif q < q_min: + q = q_min + + q_series[timestep] = q + else: + q_series = etrago_network.generators_t["q"][index] + + q_normed_series = q_series / p_nom_aggregated + + weather_dependent_generators_df_dispatch_p[aggregated_idx] = ( + weather_dependent_generators_df_dispatch_p[aggregated_idx] + + p_normed_series + ) + weather_dependent_generators_df_potential_p[aggregated_idx] = ( + weather_dependent_generators_df_potential_p[aggregated_idx] + + p_max_pu_normed_series + ) + if pf_post_lopf: + weather_dependent_generators_df_dispatch_q[aggregated_idx] = ( + weather_dependent_generators_df_dispatch_q[aggregated_idx] + + q_normed_series + ) + + weather_dependent_generators_df_curtailment_p = ( + weather_dependent_generators_df_potential_p + - weather_dependent_generators_df_dispatch_p + ) + + # Renaming columns + new_columns = [ + ( + aggregated_weather_dependent_generators_df.at[column, "carrier"], + aggregated_weather_dependent_generators_df.at[column, "w_id"], + ) + for column in weather_dependent_generators_df_potential_p.columns + ] + new_columns = pd.MultiIndex.from_tuples(new_columns) + weather_dependent_generators_df_potential_p.columns = new_columns + weather_dependent_generators_df_dispatch_p.columns = new_columns + weather_dependent_generators_df_curtailment_p.columns = new_columns + if pf_post_lopf: + weather_dependent_generators_df_dispatch_q.columns = new_columns + + etrago_results_per_bus[ + "renewables_potential" + ] = weather_dependent_generators_df_potential_p + etrago_results_per_bus[ + "renewables_curtailment" + ] = weather_dependent_generators_df_curtailment_p + if pf_post_lopf: + etrago_results_per_bus[ + "renewables_dispatch_reactive_power" + ] = weather_dependent_generators_df_dispatch_q + + t3 = time.perf_counter() + performance.update({"Renewable Dispatch and Curt.": t3 - t2}) + + # Storage + # Filter batteries + min_extended = 0 + logger.info(f"Minimum storage of {min_extended} MW") + + storages_df = etrago_network.storage_units.loc[ + (etrago_network.storage_units["carrier"] == "battery") + & (etrago_network.storage_units["bus"] == str(bus_id)) + & (etrago_network.storage_units["p_nom_extendable"] == True) + & (etrago_network.storage_units["p_nom_opt"] > min_extended) + # & (etrago_network.storage_units["max_hours"] <= 20.0) + ] + if not storages_df.empty: + # Capactiy + storages_df_capacity = ( + storages_df["p_nom_opt"] * storages_df["max_hours"] + ).values[0] + + storages_df_p = etrago_network.storage_units_t["p"][storages_df.index] + storages_df_p.columns = storages_df["carrier"] + if pf_post_lopf: + storages_df_q = etrago_network.storage_units_t["q"][storages_df.index] + storages_df_q.columns = storages_df["carrier"] + + etrago_results_per_bus["storage_units_capacity"] = storages_df_capacity + etrago_results_per_bus["storage_units_active_power"] = storages_df_p + if pf_post_lopf: + etrago_results_per_bus["storage_units_reactive_power"] = storages_df_q + + t4 = time.perf_counter() + performance.update({"Storage Data Processing": t4 - t3}) + + # DSM + dsm_df = links_df.loc[ + (links_df["carrier"] == "dsm") & (links_df["bus0"] == str(bus_id)) + ] + if not dsm_df.empty: + if dsm_df.shape[0] > 1: + raise ValueError(f"More than one dsm link at bus {bus_id}") + dsm_df_p = etrago_network.links_t["p0"][dsm_df.index] + dsm_df_p.columns = dsm_df["carrier"] + if pf_post_lopf: + dsm_df_q = 0 * dsm_df_p + + etrago_results_per_bus["dsm_active_power"] = dsm_df_p + if pf_post_lopf: + etrago_results_per_bus["dsm_reactive_power"] = dsm_df_q + + t5 = time.perf_counter() + performance.update({"DSM Data Processing": t5 - t4}) + + # Heat + # Central heat + # Power2Heat + central_heat_carriers = ["central_heat_pump", "central_resistive_heater"] + central_heat_df = links_df.loc[ + links_df["carrier"].isin(central_heat_carriers) + & (links_df["bus0"] == str(bus_id)) + ] + if not central_heat_df.empty: + # Timeseries + central_heat_df_p = etrago_network.links_t["p0"][central_heat_df.index] + central_heat_df_p.columns = central_heat_df["carrier"] + if pf_post_lopf: + central_heat_df_q = 0 * central_heat_df_p + + etrago_results_per_bus["heat_central_active_power"] = central_heat_df_p + if pf_post_lopf: + etrago_results_per_bus["heat_central_reactive_power"] = central_heat_df_q + + # Stores + central_heat_bus = central_heat_df["bus1"].values[0] + central_heat_store_bus = etrago_network.links.loc[ + etrago_network.links["bus0"] == central_heat_bus, "bus1" + ].values[0] + central_heat_store_capacity = etrago_network.stores.loc[ + (etrago_network.stores["carrier"] == "central_heat_store") + & (etrago_network.stores["bus"] == central_heat_store_bus), + "e_nom_opt", + ].values[0] + + etrago_results_per_bus[ + "thermal_storage_central_capacity" + ] = central_heat_store_capacity + + # Feedin + geothermal_feedin_df = etrago_network.generators[ + (etrago_network.generators["carrier"] == "geo_thermal") + & (etrago_network.generators["bus"] == central_heat_bus) + ] + geothermal_feedin_df_p = etrago_network.generators_t["p"][ + geothermal_feedin_df.index + ] + geothermal_feedin_df_p.columns = geothermal_feedin_df["carrier"] + etrago_results_per_bus[ + "geothermal_energy_feedin_district_heating" + ] = geothermal_feedin_df_p + + solarthermal_feedin_df = etrago_network.generators[ + (etrago_network.generators["carrier"] == "solar_thermal_collector") + & (etrago_network.generators["bus"] == central_heat_bus) + ] + solarthermal_feedin_df_p = etrago_network.generators_t["p"][ + solarthermal_feedin_df.index + ] + solarthermal_feedin_df_p.columns = solarthermal_feedin_df["carrier"] + etrago_results_per_bus[ + "solarthermal_energy_feedin_district_heating" + ] = solarthermal_feedin_df_p + + t6 = time.perf_counter() + performance.update({"Central Heat Data Processing": t6 - t5}) + + # Rural heat + # Power2Heat + rural_heat_carriers = ["rural_heat_pump"] + rural_heat_df = links_df.loc[ + links_df["carrier"].isin(rural_heat_carriers) + & (links_df["bus0"] == str(bus_id)) + ] + if not rural_heat_df.empty: + # Timeseries + rural_heat_df_p = etrago_network.links_t["p0"][rural_heat_df.index] + rural_heat_df_p.columns = rural_heat_df["carrier"] + if pf_post_lopf: + rural_heat_df_q = 0 * rural_heat_df_p + + etrago_results_per_bus["heat_pump_rural_active_power"] = rural_heat_df_p + if pf_post_lopf: + etrago_results_per_bus["heat_pump_rural_reactive_power"] = rural_heat_df_q + + # Stores + rural_heat_bus = rural_heat_df["bus1"].values[0] + rural_heat_store_bus = etrago_network.links.loc[ + etrago_network.links["bus0"] == rural_heat_bus, "bus1" + ].values[0] + rural_heat_store_capacity = etrago_network.stores.loc[ + (etrago_network.stores["carrier"] == "rural_heat_store") + & (etrago_network.stores["bus"] == rural_heat_store_bus), + "e_nom_opt", + ].values[0] + + etrago_results_per_bus[ + "thermal_storage_rural_capacity" + ] = rural_heat_store_capacity + + t7 = time.perf_counter() + performance.update({"Rural Heat Data Processing": t7 - t6}) + + # BEV charger + bev_charger_df = links_df.loc[ + (links_df["carrier"] == "BEV charger") & (links_df["bus0"] == str(bus_id)) + ] + if not bev_charger_df.empty: + if bev_charger_df.shape[0] > 1: + raise ValueError(f"More than one dsm link at bus {bus_id}") + + bev_charger_df_p = etrago_network.links_t["p0"][bev_charger_df.index] + bev_charger_df_p.columns = bev_charger_df["carrier"] + if pf_post_lopf: + bev_charger_df_q = 0 * bev_charger_df_p + + etrago_results_per_bus["electromobility_active_power"] = bev_charger_df_p + if pf_post_lopf: + etrago_results_per_bus["electromobility_reactive_power"] = bev_charger_df_q + + t8 = time.perf_counter() + performance.update({"BEV Data Processing": t8 - t7}) + performance.update({"Overall time": t8 - t0}) + logger.info(performance) + + return etrago_results_per_bus diff --git a/ego/tools/specs.py b/ego/tools/specs.py deleted file mode 100644 index a051e80c..00000000 --- a/ego/tools/specs.py +++ /dev/null @@ -1,383 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2016-2018 Europa-Universität Flensburg, -# Flensburg University of Applied Sciences, -# Centre for Sustainable Energy Systems -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation; either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -# File description -""" -This files contains all eGo interface functions -""" - -__copyright__ = ("Europa-Universität Flensburg, " - "Centre for Sustainable Energy Systems") -__license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" -__author__ = "wolf_bunke,maltesc" - -# Import -# General Packages -import os -import pandas as pd -import time -if not 'READTHEDOCS' in os.environ: - from egoio.db_tables import model_draft - from egoio.db_tables import supply - import math - -import logging -logger = logging.getLogger(__name__) - - -# Functions - -def get_etragospecs_direct(session, - bus_id, - etrago_network, - scn_name, - grid_version, - pf_post_lopf, - max_cos_phi_renewable): - """ - Reads eTraGo Results from Database and returns - the interface values as a dictionary of corresponding dataframes - - Parameters - ---------- - session : sqlalchemy.orm.session.Session - Handles conversations with the database. - bus_id : int - ID of the corresponding HV bus - etrago_network: :class:`etrago.tools.io.NetworkScenario` - eTraGo network object compiled by :meth:`etrago.appl.etrago` - scn_name : str - Name of used scenario 'Status Quo', 'NEP 2035' or 'eGo 100' - - Returns - ------- - :obj:`dict` of :pandas:`pandas.DataFrame` - Dataframes used as eDisGo inputs. - - * 'conv_dispatch' - Normalised dispatch of dispatchable generators per technology in p.u. - at the given bus. - * 'ren_dispatch' - Normalised dispatch of fluctuating generators per technology and weather - cell ID in p.u. at the given bus. - * 'ren_potential' - Normalised weather dependent feed-in potential of fluctuating generators - per technology and weather cell ID in p.u. at the given bus. - * 'ren_curtailment' - Normalised curtailment of fluctuating generators - per technology and weather cell ID in p.u. at the given bus. - * 'reactive_power' - Normalised reactive power time series of dispatchable and fluctuating - generators per technology (and weather cell ID) in p.u. at the given bus. - * 'battery_p_series' - Normalised dispatch of battery storage units in p.u. - at the given bus. - * 'battery_q_series' - Normalised reactive power time series of battery storage units in p.u. at - the given bus. - - - """ - - logger.info('Specs for bus {}'.format(bus_id)) - if pf_post_lopf: - logger.info('Active and reactive power interface') - else: - logger.info('Only active power interface') - - specs_meta_data = {} - performance = {} - - specs_meta_data.update({'TG Bus ID': bus_id}) - - if grid_version is None: - logger.warning('Weather_id taken from model_draft (not tested)') - - ormclass_gen_single = model_draft.__getattribute__( - 'EgoSupplyPfGeneratorSingle') - else: - ormclass_aggr_w = supply.__getattribute__( - 'EgoAggrWeather') - - snap_idx = etrago_network.snapshots - - # Generators - t0 = time.perf_counter() - - weather_dpdnt = ['wind', 'solar', 'wind_onshore', 'wind_offshore'] - - # DF procesing - all_gens_df = etrago_network.generators[ - etrago_network.generators['bus'] == str(bus_id) - ] - all_gens_df.index.name = 'generator_id' - - all_gens_df.reset_index(inplace=True) - - all_gens_df = all_gens_df[[ - 'generator_id', - 'p_nom', - 'p_nom_opt', - 'carrier']] - - all_gens_df = all_gens_df.rename(columns={"carrier": "name"}) - - all_gens_df = all_gens_df[all_gens_df['name'] != 'wind_offshore'] - all_gens_df.loc[ - all_gens_df["name"] == "wind_onshore", "name"] = "wind" - - # Conventionals - t1 = time.perf_counter() - performance.update({'Generator Data Processing': t1-t0}) - - conv_df = all_gens_df[~all_gens_df['name'].isin(weather_dpdnt)] - - conv_dsptch = pd.DataFrame(0.0, - index=snap_idx, - columns=list(set(conv_df['name']))) - conv_reactive_power = pd.DataFrame(0.0, - index=snap_idx, - columns=list(set(conv_df['name']))) - - if not conv_df.empty: - conventionals = True - conv_cap = conv_df[['p_nom', 'name']].groupby('name').sum().T - - for index, row in conv_df.iterrows(): - generator_id = row['generator_id'] - source = row['name'] - p = etrago_network.generators_t.p[str(generator_id)] - p_norm = p / conv_cap[source]['p_nom'] - conv_dsptch[source] = conv_dsptch[source] + p_norm - if pf_post_lopf: - q = etrago_network.generators_t.q[str(generator_id)] - # q normalized with p_nom - q_norm = q / conv_cap[source]['p_nom'] - conv_reactive_power[source] = ( - conv_reactive_power[source] - + q_norm) - - if pf_post_lopf: - new_columns = [ - (col, '') for col in conv_reactive_power.columns - ] - conv_reactive_power.columns = pd.MultiIndex.from_tuples( - new_columns) - - else: - conventionals = False - logger.warning('No conventional generators at bus {}'.format(bus_id)) - - # Renewables - t2 = time.perf_counter() - performance.update({'Conventional Dispatch': t2-t1}) - # Capacities - ren_df = all_gens_df[all_gens_df.name.isin(weather_dpdnt)] - if ren_df.empty: - logger.warning('No renewable generators at bus {}'.format(bus_id)) - - for index, row in ren_df.iterrows(): - aggr_id = row['generator_id'] - if grid_version is None: - w_id = session.query( - ormclass_gen_single.w_id - ).filter( - ormclass_gen_single.aggr_id == aggr_id, - ormclass_gen_single.scn_name == scn_name - ).limit(1).scalar() - else: - w_id = session.query( - ormclass_aggr_w.w_id - ).filter( - ormclass_aggr_w.aggr_id == aggr_id, - #ormclass_aggr_w.scn_name == scn_name, - ormclass_aggr_w.version == grid_version - ).limit(1).scalar() - - ren_df.at[index, 'w_id'] = w_id - - ren_df.dropna(inplace=True) - - aggr_gens = ren_df.groupby([ - 'name', - 'w_id' - ]).agg({'p_nom': 'sum'}).reset_index() - - aggr_gens.rename(columns={'p_nom': 'p_nom_aggr'}, inplace=True) - - aggr_gens['ren_id'] = aggr_gens.index - - ### Dispatch and Curteilment - potential = pd.DataFrame(0.0, - index=snap_idx, - columns=aggr_gens['ren_id']) - dispatch = pd.DataFrame(0.0, - index=snap_idx, - columns=aggr_gens['ren_id']) - if pf_post_lopf: - reactive_power = pd.DataFrame(0.0, - index=snap_idx, - columns=aggr_gens['ren_id']) - - for index, row in ren_df.iterrows(): - gen_id = row['generator_id'] - name = row['name'] - w_id = row['w_id'] - ren_id = int(aggr_gens[ - (aggr_gens['name'] == name) & - (aggr_gens['w_id'] == w_id)]['ren_id']) - - p_nom_aggr = float( - aggr_gens[aggr_gens['ren_id'] == ren_id]['p_nom_aggr']) - p_nom = row['p_nom'] - - p_series = etrago_network.generators_t.p[str(gen_id)] - p_norm_tot_series = p_series / p_nom_aggr - - p_max_pu_series = etrago_network.generators_t.p_max_pu[str(gen_id)] - p_max_norm_tot_series = p_max_pu_series * p_nom / p_nom_aggr - - potential[ren_id] = potential[ren_id] + p_max_norm_tot_series - dispatch[ren_id] = dispatch[ren_id] + p_norm_tot_series - - if pf_post_lopf: - q_series = etrago_network.generators_t.q[str(gen_id)] - q_norm_tot_series = q_series / p_nom_aggr - reactive_power[ren_id] = ( - reactive_power[ren_id] - + q_norm_tot_series) - - curtailment = potential.sub(dispatch) - - new_columns = [ - (aggr_gens[aggr_gens.ren_id == col].name.iloc[0], - aggr_gens[aggr_gens.ren_id == col].w_id.iloc[0]) - for col in potential.columns] - potential.columns = pd.MultiIndex.from_tuples(new_columns) - - new_columns = [ - (aggr_gens[aggr_gens.ren_id == col].name.iloc[0], - aggr_gens[aggr_gens.ren_id == col].w_id.iloc[0]) - for col in dispatch.columns] - dispatch.columns = pd.MultiIndex.from_tuples(new_columns) - - new_columns = [ - (aggr_gens[aggr_gens.ren_id == col].name.iloc[0], - aggr_gens[aggr_gens.ren_id == col].w_id.iloc[0]) - for col in curtailment.columns] - curtailment.columns = pd.MultiIndex.from_tuples(new_columns) - - if pf_post_lopf: - new_columns = [ - (aggr_gens[aggr_gens.ren_id == col].name.iloc[0], - aggr_gens[aggr_gens.ren_id == col].w_id.iloc[0]) - for col in reactive_power.columns] - reactive_power.columns = pd.MultiIndex.from_tuples(new_columns) - - # Q limit calculation - if max_cos_phi_renewable: - logger.info('Applying Q limit (max cos(phi)={})'.format( - max_cos_phi_renewable)) - - phi = math.acos(max_cos_phi_renewable) - - for col in reactive_power: - for idx in reactive_power.index: - p = dispatch.loc[idx][col] - q = reactive_power.loc[idx][col] - - q_max, q_min = p * math.tan(phi), -p * math.tan(phi) - - if q > q_max: - q = q_max - elif q < q_min: - q = q_min - - reactive_power.at[idx, col] = q - - # Reactive Power concat - if conventionals: - all_reactive_power = pd.concat([ - conv_reactive_power, - reactive_power], axis=1) - else: - all_reactive_power = reactive_power - - # Storage - t3 = time.perf_counter() - performance.update({'Renewable Dispatch and Curt.': t3-t2}) - # Capactiy - min_extended = 0.3 - stor_df = etrago_network.storage_units.loc[ - (etrago_network.storage_units['bus'] == str(bus_id)) - & (etrago_network.storage_units['p_nom_extendable'] == True) - & (etrago_network.storage_units['p_nom_opt'] > min_extended) - & (etrago_network.storage_units['max_hours'] <= 20.)] # Only batteries - - logger.warning('Minimum storage of {} MW'.format(min_extended)) - - ext_found = False - if len(stor_df) == 1: - logger.info('Extendable storage unit found') - ext_found = True - - stor_id = stor_df.index[0] - - stor_p_series_kW = etrago_network.storage_units_t.p[ - str(stor_id)] * 1000 - - if pf_post_lopf: - try: - stor_q_series_kvar = etrago_network.storage_units_t.q[ - str(stor_id)] * 1000 - except: - logger.warning("No Q series found for storage unit {}".format( - stor_id)) - stor_q_series_kvar = etrago_network.storage_units_t.p[ - str(stor_id)] * 0 - - if ext_found == False: - logger.info( - "No extendable storage unit found at bus {}".format(bus_id)) - - t4 = time.perf_counter() - performance.update({'Storage Data Processing and Dispatch': t4-t3}) - - specs = { - 'conv_dispatch': conv_dsptch, - 'ren_dispatch': dispatch, - 'ren_potential': potential, - 'ren_curtailment': curtailment - } - - if ext_found: - specs['battery_p_series'] = stor_p_series_kW - - if pf_post_lopf: - specs['battery_q_series'] = stor_q_series_kvar - - else: - specs['battery_p_series'] = specs['battery_q_series'] = None - - if pf_post_lopf: - specs['reactive_power'] = all_reactive_power - - t5 = time.perf_counter() - performance.update({'Overall time': t5-t0}) - - return specs diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..51f7681e --- /dev/null +++ b/pytest.ini @@ -0,0 +1,6 @@ +# pytest.ini +[pytest] +log_cli = True +log_level = INFO +testpaths = + tests \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..b9f3e64f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,35 @@ +import os + +import pytest + + +def pytest_configure(config): + pytest.etrago_test_network_1_path = os.path.join( + os.path.realpath(os.path.dirname(__file__)), "data/etrago_test_network_1" + ) + pytest.interface_results_reference_data_path = os.path.join( + os.path.realpath(os.path.dirname(__file__)), + "data/interface_results_reference_data", + ) + pytest.interface_results_reference_data_set_max_cos_phi_path = os.path.join( + os.path.realpath(os.path.dirname(__file__)), + "data/interface_results_reference_data_set_max_cos_phi", + ) + + config.addinivalue_line("markers", "slow: mark test as slow to run") + + +def pytest_addoption(parser): + parser.addoption( + "--runslow", action="store_true", default=False, help="run slow tests" + ) + + +def pytest_collection_modifyitems(config, items): + if config.getoption("--runslow"): + # --runslow given in cli: do not skip slow tests + return + skip_slow = pytest.mark.skip(reason="need --runslow option to run") + for item in items: + if "slow" in item.keywords: + item.add_marker(skip_slow) diff --git a/tests/data/create_test_grid.ipynb b/tests/data/create_test_grid.ipynb new file mode 100644 index 00000000..282707f7 --- /dev/null +++ b/tests/data/create_test_grid.ipynb @@ -0,0 +1,2052 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "28c7d874-4076-4330-84ec-e0ef88d29239", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import numpy as np\n", + "import os" + ] + }, + { + "cell_type": "markdown", + "id": "ad5d278e-1d45-4ada-aba6-3040c69e1a13", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "# generators" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "1ba5c7cc-ca82-4051-9cd0-62b51254aeb2", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
namebuscarrierp_nomp_nom_opt
00 biomass0biomass1.01.0
110 biomass10biomass10.010.0
20 central_biomass_CHP0central_biomass_CHP1.01.0
310 central_biomass_CHP10central_biomass_CHP10.010.0
40 run_of_river0run_of_river1.01.0
510 run_of_river10run_of_river10.010.0
60 gas0gas1.01.0
710 gas10gas10.010.0
80 other_non_renewable0other_non_renewable1.01.0
910 other_non_renewable10other_non_renewable10.010.0
100 reservoir0reservoir1.01.0
1110 reservoir10reservoir10.010.0
120 solar_00solar1.01.0
130 solar_10solar10.010.0
1410 solar_010solar1.01.0
1510 solar_110solar10.010.0
160 solar_rooftop_00solar_rooftop1.01.0
170 solar_rooftop_10solar_rooftop10.010.0
1810 solar_rooftop_010solar_rooftop1.01.0
1910 solar_rooftop_110solar_rooftop10.010.0
200 wind_onshore_00wind_onshore1.01.0
210 wind_onshore_10wind_onshore10.010.0
2210 wind_onshore_010wind_onshore1.01.0
2310 wind_onshore_110wind_onshore10.010.0
244 solar_thermal_collector4solar_thermal_collector1.01.0
2510 solar_thermal_collector10solar_thermal_collector10.010.0
264 geo_thermal4geo_thermal1.01.0
2710 geo_thermal10geo_thermal10.010.0
280 junk0junk100.0100.0
2910 junk10junk100.0100.0
\n", + "
" + ], + "text/plain": [ + " name bus carrier p_nom p_nom_opt\n", + "0 0 biomass 0 biomass 1.0 1.0\n", + "1 10 biomass 10 biomass 10.0 10.0\n", + "2 0 central_biomass_CHP 0 central_biomass_CHP 1.0 1.0\n", + "3 10 central_biomass_CHP 10 central_biomass_CHP 10.0 10.0\n", + "4 0 run_of_river 0 run_of_river 1.0 1.0\n", + "5 10 run_of_river 10 run_of_river 10.0 10.0\n", + "6 0 gas 0 gas 1.0 1.0\n", + "7 10 gas 10 gas 10.0 10.0\n", + "8 0 other_non_renewable 0 other_non_renewable 1.0 1.0\n", + "9 10 other_non_renewable 10 other_non_renewable 10.0 10.0\n", + "10 0 reservoir 0 reservoir 1.0 1.0\n", + "11 10 reservoir 10 reservoir 10.0 10.0\n", + "12 0 solar_0 0 solar 1.0 1.0\n", + "13 0 solar_1 0 solar 10.0 10.0\n", + "14 10 solar_0 10 solar 1.0 1.0\n", + "15 10 solar_1 10 solar 10.0 10.0\n", + "16 0 solar_rooftop_0 0 solar_rooftop 1.0 1.0\n", + "17 0 solar_rooftop_1 0 solar_rooftop 10.0 10.0\n", + "18 10 solar_rooftop_0 10 solar_rooftop 1.0 1.0\n", + "19 10 solar_rooftop_1 10 solar_rooftop 10.0 10.0\n", + "20 0 wind_onshore_0 0 wind_onshore 1.0 1.0\n", + "21 0 wind_onshore_1 0 wind_onshore 10.0 10.0\n", + "22 10 wind_onshore_0 10 wind_onshore 1.0 1.0\n", + "23 10 wind_onshore_1 10 wind_onshore 10.0 10.0\n", + "24 4 solar_thermal_collector 4 solar_thermal_collector 1.0 1.0\n", + "25 10 solar_thermal_collector 10 solar_thermal_collector 10.0 10.0\n", + "26 4 geo_thermal 4 geo_thermal 1.0 1.0\n", + "27 10 geo_thermal 10 geo_thermal 10.0 10.0\n", + "28 0 junk 0 junk 100.0 100.0\n", + "29 10 junk 10 junk 100.0 100.0" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "generators_df = pd.read_csv(\"etrago_test_network_1/generators.csv\")\n", + "generators_df" + ] + }, + { + "cell_type": "markdown", + "id": "c8a91022-57c5-47f6-98b2-66aa9ad5a333", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "## p" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "1b017098-0d72-470c-ac65-d4847ab43354", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ,\n", + " 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ,\n", + " 0. , 0. , 0. , 0. ],\n", + " [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,\n", + " 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,\n", + " 0.5, 0.5, 0.5, 0.5],\n", + " [1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. ,\n", + " 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. ,\n", + " 1. , 1. , 1. , 1. ]])" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "base = np.ones((3,generators_df.shape[0]))\n", + "scale = np.array([0.0,0.5,1.0])\n", + "\n", + "for i in range(scale.shape[0]):\n", + " base[i,:] = scale[i] * base[i,:]\n", + " \n", + "base" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "a057d570-bd42-4162-a24f-7b23cf61df85", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0 0 biomass\n", + "1 10 biomass\n", + "2 0 central_biomass_CHP\n", + "3 10 central_biomass_CHP\n", + "4 0 run_of_river\n", + "5 10 run_of_river\n", + "6 0 gas\n", + "7 10 gas\n", + "8 0 other_non_renewable\n", + "9 10 other_non_renewable\n", + "10 0 reservoir\n", + "11 10 reservoir\n", + "12 0 solar_0\n", + "13 0 solar_1\n", + "14 10 solar_0\n", + "15 10 solar_1\n", + "16 0 solar_rooftop_0\n", + "17 0 solar_rooftop_1\n", + "18 10 solar_rooftop_0\n", + "19 10 solar_rooftop_1\n", + "20 0 wind_onshore_0\n", + "21 0 wind_onshore_1\n", + "22 10 wind_onshore_0\n", + "23 10 wind_onshore_1\n", + "24 4 solar_thermal_collector\n", + "25 10 solar_thermal_collector\n", + "26 4 geo_thermal\n", + "27 10 geo_thermal\n", + "28 0 junk\n", + "29 10 junk\n", + "Name: name, dtype: object" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "generators_df[\"name\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "1ea9ed1a-89eb-4ab1-a645-8e8418fd1531", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
0 biomass10 biomass0 central_biomass_CHP10 central_biomass_CHP0 run_of_river10 run_of_river0 gas10 gas0 other_non_renewable10 other_non_renewable...0 wind_onshore_00 wind_onshore_110 wind_onshore_010 wind_onshore_14 solar_thermal_collector10 solar_thermal_collector4 geo_thermal10 geo_thermal0 junk10 junk
00.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
10.50.50.50.50.50.50.50.50.50.5...0.50.50.50.50.50.50.50.50.50.5
21.01.01.01.01.01.01.01.01.01.0...1.01.01.01.01.01.01.01.01.01.0
\n", + "

3 rows × 30 columns

\n", + "
" + ], + "text/plain": [ + " 0 biomass 10 biomass 0 central_biomass_CHP 10 central_biomass_CHP \\\n", + "0 0.0 0.0 0.0 0.0 \n", + "1 0.5 0.5 0.5 0.5 \n", + "2 1.0 1.0 1.0 1.0 \n", + "\n", + " 0 run_of_river 10 run_of_river 0 gas 10 gas 0 other_non_renewable \\\n", + "0 0.0 0.0 0.0 0.0 0.0 \n", + "1 0.5 0.5 0.5 0.5 0.5 \n", + "2 1.0 1.0 1.0 1.0 1.0 \n", + "\n", + " 10 other_non_renewable ... 0 wind_onshore_0 0 wind_onshore_1 \\\n", + "0 0.0 ... 0.0 0.0 \n", + "1 0.5 ... 0.5 0.5 \n", + "2 1.0 ... 1.0 1.0 \n", + "\n", + " 10 wind_onshore_0 10 wind_onshore_1 4 solar_thermal_collector \\\n", + "0 0.0 0.0 0.0 \n", + "1 0.5 0.5 0.5 \n", + "2 1.0 1.0 1.0 \n", + "\n", + " 10 solar_thermal_collector 4 geo_thermal 10 geo_thermal 0 junk 10 junk \n", + "0 0.0 0.0 0.0 0.0 0.0 \n", + "1 0.5 0.5 0.5 0.5 0.5 \n", + "2 1.0 1.0 1.0 1.0 1.0 \n", + "\n", + "[3 rows x 30 columns]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "power_df = pd.DataFrame(base, columns=generators_df[\"name\"].to_list())\n", + "power_df" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "296705d3-bf09-47f6-a563-6249618f8c60", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "power_df.to_csv(\"etrago_test_network_1/generators-p.csv\")\n", + "power_df.to_csv(\"etrago_test_network_1/generators-p_min_pu.csv\")" + ] + }, + { + "cell_type": "markdown", + "id": "b0578ab0-4a79-4d70-96c7-49587f1041a1", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "## q" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "a85d39bb-1929-4351-8ee0-a9f94a649f22", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[ 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ,\n", + " 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ,\n", + " 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ],\n", + " [ 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,\n", + " 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,\n", + " 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5],\n", + " [-1. , -1. , -1. , -1. , -1. , -1. , -1. , -1. , -1. , -1. , -1. ,\n", + " -1. , -1. , -1. , -1. , -1. , -1. , -1. , -1. , -1. , -1. , -1. ,\n", + " -1. , -1. , -1. , -1. , -1. , -1. , -1. , -1. ]])" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "base = np.ones((3,generators_df.shape[0]))\n", + "scale = np.array([0.0,0.5,-1.0])\n", + "\n", + "for i in range(scale.shape[0]):\n", + " base[i,:] = scale[i] * base[i,:]\n", + " \n", + "base" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "aa4bcd4f-22ce-4733-8898-b159d5f103b9", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0 0 biomass\n", + "1 10 biomass\n", + "2 0 central_biomass_CHP\n", + "3 10 central_biomass_CHP\n", + "4 0 run_of_river\n", + "5 10 run_of_river\n", + "6 0 gas\n", + "7 10 gas\n", + "8 0 other_non_renewable\n", + "9 10 other_non_renewable\n", + "10 0 reservoir\n", + "11 10 reservoir\n", + "12 0 solar_0\n", + "13 0 solar_1\n", + "14 10 solar_0\n", + "15 10 solar_1\n", + "16 0 solar_rooftop_0\n", + "17 0 solar_rooftop_1\n", + "18 10 solar_rooftop_0\n", + "19 10 solar_rooftop_1\n", + "20 0 wind_onshore_0\n", + "21 0 wind_onshore_1\n", + "22 10 wind_onshore_0\n", + "23 10 wind_onshore_1\n", + "24 4 solar_thermal_collector\n", + "25 10 solar_thermal_collector\n", + "26 4 geo_thermal\n", + "27 10 geo_thermal\n", + "28 0 junk\n", + "29 10 junk\n", + "Name: name, dtype: object" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "generators_df[\"name\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "a0decf15-9db6-4a75-ae65-ae58a2990b49", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
0 biomass10 biomass0 central_biomass_CHP10 central_biomass_CHP0 run_of_river10 run_of_river0 gas10 gas0 other_non_renewable10 other_non_renewable...0 wind_onshore_00 wind_onshore_110 wind_onshore_010 wind_onshore_14 solar_thermal_collector10 solar_thermal_collector4 geo_thermal10 geo_thermal0 junk10 junk
00.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
10.50.50.50.50.50.50.50.50.50.5...0.50.50.50.50.50.50.50.50.50.5
2-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0...-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0
\n", + "

3 rows × 30 columns

\n", + "
" + ], + "text/plain": [ + " 0 biomass 10 biomass 0 central_biomass_CHP 10 central_biomass_CHP \\\n", + "0 0.0 0.0 0.0 0.0 \n", + "1 0.5 0.5 0.5 0.5 \n", + "2 -1.0 -1.0 -1.0 -1.0 \n", + "\n", + " 0 run_of_river 10 run_of_river 0 gas 10 gas 0 other_non_renewable \\\n", + "0 0.0 0.0 0.0 0.0 0.0 \n", + "1 0.5 0.5 0.5 0.5 0.5 \n", + "2 -1.0 -1.0 -1.0 -1.0 -1.0 \n", + "\n", + " 10 other_non_renewable ... 0 wind_onshore_0 0 wind_onshore_1 \\\n", + "0 0.0 ... 0.0 0.0 \n", + "1 0.5 ... 0.5 0.5 \n", + "2 -1.0 ... -1.0 -1.0 \n", + "\n", + " 10 wind_onshore_0 10 wind_onshore_1 4 solar_thermal_collector \\\n", + "0 0.0 0.0 0.0 \n", + "1 0.5 0.5 0.5 \n", + "2 -1.0 -1.0 -1.0 \n", + "\n", + " 10 solar_thermal_collector 4 geo_thermal 10 geo_thermal 0 junk 10 junk \n", + "0 0.0 0.0 0.0 0.0 0.0 \n", + "1 0.5 0.5 0.5 0.5 0.5 \n", + "2 -1.0 -1.0 -1.0 -1.0 -1.0 \n", + "\n", + "[3 rows x 30 columns]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "power_df = pd.DataFrame(base, columns=generators_df[\"name\"].to_list())\n", + "power_df" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "62bd950a-8d91-44b2-8e9d-3b2e5f73f0f5", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "power_df.to_csv(\"etrago_test_network_1/generators-q.csv\")" + ] + }, + { + "cell_type": "markdown", + "id": "e8f25f92-7f8b-4063-8f8f-d08790ac046f", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "## p_max_pu" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "423bbe9c-174c-4612-9247-5a524dd389b5", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[0.5 , 0.5 , 0.5 , 0.5 , 0.5 , 0.5 , 0.5 , 0.5 , 0.5 , 0.5 , 0.5 ,\n", + " 0.5 , 0.5 , 0.5 , 0.5 , 0.5 , 0.5 , 0.5 , 0.5 , 0.5 , 0.5 , 0.5 ,\n", + " 0.5 , 0.5 , 0.5 , 0.5 , 0.5 , 0.5 , 0.5 , 0.5 ],\n", + " [0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75,\n", + " 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75,\n", + " 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 0.75],\n", + " [1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. ,\n", + " 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. ,\n", + " 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. ]])" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "base = np.ones((3,generators_df.shape[0]))\n", + "scale = np.array([0.5,0.75,1.0])\n", + "\n", + "for i in range(scale.shape[0]):\n", + " base[i,:] = scale[i] * base[i,:]\n", + " \n", + "base" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "9904d1b0-8bb4-4571-920e-a3ffc956d542", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
0 biomass10 biomass0 central_biomass_CHP10 central_biomass_CHP0 run_of_river10 run_of_river0 gas10 gas0 other_non_renewable10 other_non_renewable...0 wind_onshore_00 wind_onshore_110 wind_onshore_010 wind_onshore_14 solar_thermal_collector10 solar_thermal_collector4 geo_thermal10 geo_thermal0 junk10 junk
00.500.500.500.500.500.500.500.500.500.50...0.500.500.500.500.500.500.500.500.500.50
10.750.750.750.750.750.750.750.750.750.75...0.750.750.750.750.750.750.750.750.750.75
21.001.001.001.001.001.001.001.001.001.00...1.001.001.001.001.001.001.001.001.001.00
\n", + "

3 rows × 30 columns

\n", + "
" + ], + "text/plain": [ + " 0 biomass 10 biomass 0 central_biomass_CHP 10 central_biomass_CHP \\\n", + "0 0.50 0.50 0.50 0.50 \n", + "1 0.75 0.75 0.75 0.75 \n", + "2 1.00 1.00 1.00 1.00 \n", + "\n", + " 0 run_of_river 10 run_of_river 0 gas 10 gas 0 other_non_renewable \\\n", + "0 0.50 0.50 0.50 0.50 0.50 \n", + "1 0.75 0.75 0.75 0.75 0.75 \n", + "2 1.00 1.00 1.00 1.00 1.00 \n", + "\n", + " 10 other_non_renewable ... 0 wind_onshore_0 0 wind_onshore_1 \\\n", + "0 0.50 ... 0.50 0.50 \n", + "1 0.75 ... 0.75 0.75 \n", + "2 1.00 ... 1.00 1.00 \n", + "\n", + " 10 wind_onshore_0 10 wind_onshore_1 4 solar_thermal_collector \\\n", + "0 0.50 0.50 0.50 \n", + "1 0.75 0.75 0.75 \n", + "2 1.00 1.00 1.00 \n", + "\n", + " 10 solar_thermal_collector 4 geo_thermal 10 geo_thermal 0 junk 10 junk \n", + "0 0.50 0.50 0.50 0.50 0.50 \n", + "1 0.75 0.75 0.75 0.75 0.75 \n", + "2 1.00 1.00 1.00 1.00 1.00 \n", + "\n", + "[3 rows x 30 columns]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "power_df = pd.DataFrame(base, columns=generators_df[\"name\"].to_list())\n", + "power_df" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "62fc415e-4e2a-427e-aa54-449606bca7e3", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "power_df.to_csv(\"etrago_test_network_1/generators-p_max_pu.csv\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "cbe9aa85-74ac-4309-bab4-dab26c656811", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "biomass\n", + "central_biomass_CHP\n", + "run_of_river\n", + "gas\n", + "other_non_renewable\n", + "reservoir\n", + "solar\n", + "solar_rooftop\n", + "wind_onshore\n", + "solar_thermal_collector\n", + "geo_thermal\n", + "junk\n" + ] + } + ], + "source": [ + "for carrier in generators_df[\"carrier\"].unique():\n", + " print(carrier)" + ] + }, + { + "cell_type": "markdown", + "id": "0198d501-447f-4c9d-ae61-b8b14f0ca510", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "# links" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "b35ed6df-2178-46e4-82ae-ba073ae4dbe7", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
namebus0bus1carrierp_nom_opt
0010central_gas_CHP1.0
111010central_gas_CHP10.0
2203dsm1.0
331010dsm10.0
4404central_heat_pump1.0
551010central_heat_pump10.0
6604central_resistive_heater1.0
771010central_resistive_heater10.0
8845central_heat_store_charger1.0
991010central_heat_store_charger10.0
101006rural_heat_pump1.0
11111010rural_heat_pump10.0
121267rural_heat_store_charger1.0
13131010rural_heat_store_charger10.0
141403BEV charger1.0
15151010BEV charger10.0
16161010junk100.0
\n", + "
" + ], + "text/plain": [ + " name bus0 bus1 carrier p_nom_opt\n", + "0 0 1 0 central_gas_CHP 1.0\n", + "1 1 10 10 central_gas_CHP 10.0\n", + "2 2 0 3 dsm 1.0\n", + "3 3 10 10 dsm 10.0\n", + "4 4 0 4 central_heat_pump 1.0\n", + "5 5 10 10 central_heat_pump 10.0\n", + "6 6 0 4 central_resistive_heater 1.0\n", + "7 7 10 10 central_resistive_heater 10.0\n", + "8 8 4 5 central_heat_store_charger 1.0\n", + "9 9 10 10 central_heat_store_charger 10.0\n", + "10 10 0 6 rural_heat_pump 1.0\n", + "11 11 10 10 rural_heat_pump 10.0\n", + "12 12 6 7 rural_heat_store_charger 1.0\n", + "13 13 10 10 rural_heat_store_charger 10.0\n", + "14 14 0 3 BEV charger 1.0\n", + "15 15 10 10 BEV charger 10.0\n", + "16 16 10 10 junk 100.0" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "links_df = pd.read_csv(\"etrago_test_network_1/links.csv\")\n", + "links_df" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "181d99ef-6b16-44ca-a5c4-4bb18b15e954", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ,\n", + " 0. , 0. , 0. , 0. ],\n", + " [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,\n", + " 0.5, 0.5, 0.5, 0.5],\n", + " [1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. , 1. ,\n", + " 1. , 1. , 1. , 1. ]])" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "base = np.ones((3,links_df.shape[0]))\n", + "scale = np.array([0.0,0.5,1.0])\n", + "\n", + "for i in range(scale.shape[0]):\n", + " base[i,:] = scale[i] * base[i,:]\n", + " \n", + "base" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "c36b5043-d0f9-4c84-b86e-fb8aaf1eb180", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
012345678910111213141516
00.00.00.00.00.00.00.00.00.00.00.00.00.00.00.00.00.0
10.50.50.50.50.50.50.50.50.50.50.50.50.50.50.50.50.5
21.01.01.01.01.01.01.01.01.01.01.01.01.01.01.01.01.0
\n", + "
" + ], + "text/plain": [ + " 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 \\\n", + "0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", + "1 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 \n", + "2 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 \n", + "\n", + " 15 16 \n", + "0 0.0 0.0 \n", + "1 0.5 0.5 \n", + "2 1.0 1.0 " + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "power_df = pd.DataFrame(base, columns=links_df[\"name\"].to_list())\n", + "power_df" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "07268ffa-c649-492b-85df-a5304b2c005b", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "power_df.to_csv(\"etrago_test_network_1/links-p0.csv\")\n", + "power_df.to_csv(\"etrago_test_network_1/links-p1.csv\")" + ] + }, + { + "cell_type": "markdown", + "id": "c85c0234-20ad-4a2b-b104-f889a289cdfb", + "metadata": { + "pycharm": { + "name": "#%% md\n" + }, + "tags": [] + }, + "source": [ + "# storage_units" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "3f67735e-6081-421e-b9bd-09ca1ecff15c", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
namebuscarrierp_nom_optp_nom_extendablemax_hours
00 battery0battery1.0True10.0
110 battery10battery10.0True10.0
210 junk10junk10.0True10.0
\n", + "
" + ], + "text/plain": [ + " name bus carrier p_nom_opt p_nom_extendable max_hours\n", + "0 0 battery 0 battery 1.0 True 10.0\n", + "1 10 battery 10 battery 10.0 True 10.0\n", + "2 10 junk 10 junk 10.0 True 10.0" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "storages_df = pd.read_csv(\"etrago_test_network_1/storage_units.csv\")\n", + "storages_df" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "c181baf9-e79b-44a9-a411-6db3d90bb7e1", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[0. , 0. , 0. ],\n", + " [0.5, 0.5, 0.5],\n", + " [1. , 1. , 1. ]])" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "base = np.ones((3,storages_df.shape[0]))\n", + "scale = np.array([0.0,0.5,1.0])\n", + "\n", + "for i in range(scale.shape[0]):\n", + " base[i,:] = scale[i] * base[i,:]\n", + " \n", + "base" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "6efc3e6a-3cff-432c-bc0a-2e93c59629f6", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
0 battery10 battery10 junk
00.00.00.0
10.50.50.5
21.01.01.0
\n", + "
" + ], + "text/plain": [ + " 0 battery 10 battery 10 junk\n", + "0 0.0 0.0 0.0\n", + "1 0.5 0.5 0.5\n", + "2 1.0 1.0 1.0" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "power_df = pd.DataFrame(base, columns=storages_df[\"name\"].to_list())\n", + "power_df" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "e29b6a1c-c986-45a0-92be-527ee3bb0701", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "power_df.to_csv(\"etrago_test_network_1/storage_units-p.csv\")\n", + "power_df.to_csv(\"etrago_test_network_1/storage_units-q.csv\")" + ] + }, + { + "cell_type": "markdown", + "id": "bc1eeab7-a906-4a1f-8629-9b05df81435c", + "metadata": { + "pycharm": { + "name": "#%% md\n" + }, + "tags": [] + }, + "source": [ + "# test" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "9be7b621-1931-46d6-84f0-6bf777a8d722", + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
0 biomass10 biomass0 central_biomass_CHP10 central_biomass_CHP0 run_of_river10 run_of_river0 gas10 gas0 other_non_renewable10 other_non_renewable...0 wind_onshore_00 wind_onshore_110 wind_onshore_010 wind_onshore_14 solar_thermal_collector10 solar_thermal_collector4 geo_thermal10 geo_thermal0 junk10 junk
00.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
10.50.50.50.50.50.50.50.50.50.5...0.50.50.50.50.50.50.50.50.50.5
21.01.01.01.01.01.01.01.01.01.0...1.01.01.01.01.01.01.01.01.01.0
\n", + "

3 rows × 30 columns

\n", + "
" + ], + "text/plain": [ + " 0 biomass 10 biomass 0 central_biomass_CHP 10 central_biomass_CHP \\\n", + "0 0.0 0.0 0.0 0.0 \n", + "1 0.5 0.5 0.5 0.5 \n", + "2 1.0 1.0 1.0 1.0 \n", + "\n", + " 0 run_of_river 10 run_of_river 0 gas 10 gas 0 other_non_renewable \\\n", + "0 0.0 0.0 0.0 0.0 0.0 \n", + "1 0.5 0.5 0.5 0.5 0.5 \n", + "2 1.0 1.0 1.0 1.0 1.0 \n", + "\n", + " 10 other_non_renewable ... 0 wind_onshore_0 0 wind_onshore_1 \\\n", + "0 0.0 ... 0.0 0.0 \n", + "1 0.5 ... 0.5 0.5 \n", + "2 1.0 ... 1.0 1.0 \n", + "\n", + " 10 wind_onshore_0 10 wind_onshore_1 4 solar_thermal_collector \\\n", + "0 0.0 0.0 0.0 \n", + "1 0.5 0.5 0.5 \n", + "2 1.0 1.0 1.0 \n", + "\n", + " 10 solar_thermal_collector 4 geo_thermal 10 geo_thermal 0 junk 10 junk \n", + "0 0.0 0.0 0.0 0.0 0.0 \n", + "1 0.5 0.5 0.5 0.5 0.5 \n", + "2 1.0 1.0 1.0 1.0 1.0 \n", + "\n", + "[3 rows x 30 columns]" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "test_df = pd.read_csv(\"etrago_test_network_1/generators-p.csv\", index_col=0)\n", + "test_df" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/tests/data/etrago_test_network_1/buses.csv b/tests/data/etrago_test_network_1/buses.csv new file mode 100644 index 00000000..7df91445 --- /dev/null +++ b/tests/data/etrago_test_network_1/buses.csv @@ -0,0 +1,10 @@ +name,carrier +0,AC +1,CH4 +2,dsm +3,Li ion +4,central_heat +5,central_heat_store +6,rural_heat +7,rural_heat_store +10,junk diff --git a/tests/data/etrago_test_network_1/generators-p.csv b/tests/data/etrago_test_network_1/generators-p.csv new file mode 100644 index 00000000..7dd05d57 --- /dev/null +++ b/tests/data/etrago_test_network_1/generators-p.csv @@ -0,0 +1,4 @@ +,0 biomass,10 biomass,0 central_biomass_CHP,10 central_biomass_CHP,0 run_of_river,10 run_of_river,0 gas,10 gas,0 other_non_renewable,10 other_non_renewable,0 reservoir,10 reservoir,0 solar_0,0 solar_1,10 solar_0,10 solar_1,0 solar_rooftop_0,0 solar_rooftop_1,10 solar_rooftop_0,10 solar_rooftop_1,0 wind_onshore_0,0 wind_onshore_1,10 wind_onshore_0,10 wind_onshore_1,4 solar_thermal_collector,10 solar_thermal_collector,4 geo_thermal,10 geo_thermal,0 junk,10 junk +0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 +1,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5 +2,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0 diff --git a/tests/data/etrago_test_network_1/generators-p_max_pu.csv b/tests/data/etrago_test_network_1/generators-p_max_pu.csv new file mode 100644 index 00000000..b5ffc47e --- /dev/null +++ b/tests/data/etrago_test_network_1/generators-p_max_pu.csv @@ -0,0 +1,4 @@ +,0 biomass,10 biomass,0 central_biomass_CHP,10 central_biomass_CHP,0 run_of_river,10 run_of_river,0 gas,10 gas,0 other_non_renewable,10 other_non_renewable,0 reservoir,10 reservoir,0 solar_0,0 solar_1,10 solar_0,10 solar_1,0 solar_rooftop_0,0 solar_rooftop_1,10 solar_rooftop_0,10 solar_rooftop_1,0 wind_onshore_0,0 wind_onshore_1,10 wind_onshore_0,10 wind_onshore_1,4 solar_thermal_collector,10 solar_thermal_collector,4 geo_thermal,10 geo_thermal,0 junk,10 junk +0,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5 +1,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75 +2,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0 diff --git a/tests/data/etrago_test_network_1/generators-p_min_pu.csv b/tests/data/etrago_test_network_1/generators-p_min_pu.csv new file mode 100644 index 00000000..7dd05d57 --- /dev/null +++ b/tests/data/etrago_test_network_1/generators-p_min_pu.csv @@ -0,0 +1,4 @@ +,0 biomass,10 biomass,0 central_biomass_CHP,10 central_biomass_CHP,0 run_of_river,10 run_of_river,0 gas,10 gas,0 other_non_renewable,10 other_non_renewable,0 reservoir,10 reservoir,0 solar_0,0 solar_1,10 solar_0,10 solar_1,0 solar_rooftop_0,0 solar_rooftop_1,10 solar_rooftop_0,10 solar_rooftop_1,0 wind_onshore_0,0 wind_onshore_1,10 wind_onshore_0,10 wind_onshore_1,4 solar_thermal_collector,10 solar_thermal_collector,4 geo_thermal,10 geo_thermal,0 junk,10 junk +0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 +1,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5 +2,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0 diff --git a/tests/data/etrago_test_network_1/generators-q.csv b/tests/data/etrago_test_network_1/generators-q.csv new file mode 100644 index 00000000..223fb84f --- /dev/null +++ b/tests/data/etrago_test_network_1/generators-q.csv @@ -0,0 +1,4 @@ +,0 biomass,10 biomass,0 central_biomass_CHP,10 central_biomass_CHP,0 run_of_river,10 run_of_river,0 gas,10 gas,0 other_non_renewable,10 other_non_renewable,0 reservoir,10 reservoir,0 solar_0,0 solar_1,10 solar_0,10 solar_1,0 solar_rooftop_0,0 solar_rooftop_1,10 solar_rooftop_0,10 solar_rooftop_1,0 wind_onshore_0,0 wind_onshore_1,10 wind_onshore_0,10 wind_onshore_1,4 solar_thermal_collector,10 solar_thermal_collector,4 geo_thermal,10 geo_thermal,0 junk,10 junk +0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 +1,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5 +2,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0 diff --git a/tests/data/etrago_test_network_1/generators.csv b/tests/data/etrago_test_network_1/generators.csv new file mode 100644 index 00000000..a07d4562 --- /dev/null +++ b/tests/data/etrago_test_network_1/generators.csv @@ -0,0 +1,31 @@ +name,bus,carrier,p_nom,p_nom_opt +0 biomass,0,biomass,1.0,1.0 +10 biomass,10,biomass,10.0,10.0 +0 central_biomass_CHP,0,central_biomass_CHP,1.0,1.0 +10 central_biomass_CHP,10,central_biomass_CHP,10.0,10.0 +0 run_of_river,0,run_of_river,1.0,1.0 +10 run_of_river,10,run_of_river,10.0,10.0 +0 gas,0,gas,1.0,1.0 +10 gas,10,gas,10.0,10.0 +0 other_non_renewable,0,other_non_renewable,1.0,1.0 +10 other_non_renewable,10,other_non_renewable,10.0,10.0 +0 reservoir,0,reservoir,1.0,1.0 +10 reservoir,10,reservoir,10.0,10.0 +0 solar_0,0,solar,1.0,1.0 +0 solar_1,0,solar,10.0,10.0 +10 solar_0,10,solar,1.0,1.0 +10 solar_1,10,solar,10.0,10.0 +0 solar_rooftop_0,0,solar_rooftop,1.0,1.0 +0 solar_rooftop_1,0,solar_rooftop,10.0,10.0 +10 solar_rooftop_0,10,solar_rooftop,1.0,1.0 +10 solar_rooftop_1,10,solar_rooftop,10.0,10.0 +0 wind_onshore_0,0,wind_onshore,1.0,1.0 +0 wind_onshore_1,0,wind_onshore,10.0,10.0 +10 wind_onshore_0,10,wind_onshore,1.0,1.0 +10 wind_onshore_1,10,wind_onshore,10.0,10.0 +4 solar_thermal_collector,4,solar_thermal_collector,1.0,1.0 +10 solar_thermal_collector,10,solar_thermal_collector,10.0,10.0 +4 geo_thermal,4,geo_thermal,1.0,1.0 +10 geo_thermal,10,geo_thermal,10.0,10.0 +0 junk,0,junk,100.0,100.0 +10 junk,10,junk,100.0,100.0 \ No newline at end of file diff --git a/tests/data/etrago_test_network_1/links-p0.csv b/tests/data/etrago_test_network_1/links-p0.csv new file mode 100644 index 00000000..b2de47da --- /dev/null +++ b/tests/data/etrago_test_network_1/links-p0.csv @@ -0,0 +1,4 @@ +,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16 +0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 +1,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5 +2,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0 diff --git a/tests/data/etrago_test_network_1/links-p1.csv b/tests/data/etrago_test_network_1/links-p1.csv new file mode 100644 index 00000000..b2de47da --- /dev/null +++ b/tests/data/etrago_test_network_1/links-p1.csv @@ -0,0 +1,4 @@ +,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16 +0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0 +1,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5 +2,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0 diff --git a/tests/data/etrago_test_network_1/links.csv b/tests/data/etrago_test_network_1/links.csv new file mode 100644 index 00000000..368aca62 --- /dev/null +++ b/tests/data/etrago_test_network_1/links.csv @@ -0,0 +1,18 @@ +name,bus0,bus1,carrier,p_nom_opt +0,1,0,central_gas_CHP,1.0 +1,10,10,central_gas_CHP,10.0 +2,0,3,dsm,1.0 +3,10,10,dsm,10.0 +4,0,4,central_heat_pump,1.0 +5,10,10,central_heat_pump,10.0 +6,0,4,central_resistive_heater,1.0 +7,10,10,central_resistive_heater,10.0 +8,4,5,central_heat_store_charger,1.0 +9,10,10,central_heat_store_charger,10.0 +10,0,6,rural_heat_pump,1.0 +11,10,10,rural_heat_pump,10.0 +12,6,7,rural_heat_store_charger,1.0 +13,10,10,rural_heat_store_charger,10.0 +14,0,3,BEV charger,1.0 +15,10,10,BEV charger,10.0 +16,10,10,junk,100.0 diff --git a/tests/data/etrago_test_network_1/snapshots.csv b/tests/data/etrago_test_network_1/snapshots.csv new file mode 100644 index 00000000..07542072 --- /dev/null +++ b/tests/data/etrago_test_network_1/snapshots.csv @@ -0,0 +1,4 @@ +,snapshot +0,2011-01-01 00:00:00 +1,2011-01-01 12:00:00 +2,2011-01-02 00:00:00 \ No newline at end of file diff --git a/tests/data/etrago_test_network_1/storage_units-p.csv b/tests/data/etrago_test_network_1/storage_units-p.csv new file mode 100644 index 00000000..506a427a --- /dev/null +++ b/tests/data/etrago_test_network_1/storage_units-p.csv @@ -0,0 +1,4 @@ +,0 battery,10 battery,10 junk +0,0.0,0.0,0.0 +1,0.5,0.5,0.5 +2,1.0,1.0,1.0 diff --git a/tests/data/etrago_test_network_1/storage_units-q.csv b/tests/data/etrago_test_network_1/storage_units-q.csv new file mode 100644 index 00000000..506a427a --- /dev/null +++ b/tests/data/etrago_test_network_1/storage_units-q.csv @@ -0,0 +1,4 @@ +,0 battery,10 battery,10 junk +0,0.0,0.0,0.0 +1,0.5,0.5,0.5 +2,1.0,1.0,1.0 diff --git a/tests/data/etrago_test_network_1/storage_units.csv b/tests/data/etrago_test_network_1/storage_units.csv new file mode 100644 index 00000000..b71d12c9 --- /dev/null +++ b/tests/data/etrago_test_network_1/storage_units.csv @@ -0,0 +1,4 @@ +name,bus,carrier,p_nom_opt,p_nom_extendable,max_hours +0 battery,0,battery,1.0,True,10.0 +10 battery,10,battery,10.0,True,10.0 +10 junk,10,junk,10.0,True,10.0 \ No newline at end of file diff --git a/tests/data/etrago_test_network_1/stores.csv b/tests/data/etrago_test_network_1/stores.csv new file mode 100644 index 00000000..9c392fe6 --- /dev/null +++ b/tests/data/etrago_test_network_1/stores.csv @@ -0,0 +1,5 @@ +name,bus,carrier,e_nom_opt +5 central_heat_store,5,central_heat_store,1 +10 central_heat_store,10,central_heat_store,10 +7 rural_heat_store,7,rural_heat_store,1 +10 rural_heat_store,10,rural_heat_store,10 \ No newline at end of file diff --git a/tests/data/interface_results_reference_data/dispatchable_generators_active_power.csv b/tests/data/interface_results_reference_data/dispatchable_generators_active_power.csv new file mode 100644 index 00000000..0ed50dc0 --- /dev/null +++ b/tests/data/interface_results_reference_data/dispatchable_generators_active_power.csv @@ -0,0 +1,4 @@ +snapshot,biomass,central_biomass_CHP,run_of_river,gas,other_non_renewable,reservoir,central_gas_CHP +2011-01-01 00:00:00,0.0,0.0,0.0,0.0,0.0,0.0,0.0 +2011-01-01 12:00:00,0.5,0.5,0.5,0.5,0.5,0.5,0.5 +2011-01-02 00:00:00,1.0,1.0,1.0,1.0,1.0,1.0,1.0 diff --git a/tests/data/interface_results_reference_data/dispatchable_generators_reactive_power.csv b/tests/data/interface_results_reference_data/dispatchable_generators_reactive_power.csv new file mode 100644 index 00000000..915fb77e --- /dev/null +++ b/tests/data/interface_results_reference_data/dispatchable_generators_reactive_power.csv @@ -0,0 +1,4 @@ +snapshot,biomass,central_biomass_CHP,run_of_river,gas,other_non_renewable,reservoir,central_gas_CHP +2011-01-01 00:00:00,0.0,0.0,0.0,0.0,0.0,0.0,0.0 +2011-01-01 12:00:00,0.5,0.5,0.5,0.5,0.5,0.5,0.0 +2011-01-02 00:00:00,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,0.0 diff --git a/tests/data/interface_results_reference_data/dsm_active_power.csv b/tests/data/interface_results_reference_data/dsm_active_power.csv new file mode 100644 index 00000000..9a217ea7 --- /dev/null +++ b/tests/data/interface_results_reference_data/dsm_active_power.csv @@ -0,0 +1,4 @@ +snapshot,dsm +2011-01-01 00:00:00,0.0 +2011-01-01 12:00:00,0.5 +2011-01-02 00:00:00,1.0 diff --git a/tests/data/interface_results_reference_data/dsm_reactive_power.csv b/tests/data/interface_results_reference_data/dsm_reactive_power.csv new file mode 100644 index 00000000..f90539eb --- /dev/null +++ b/tests/data/interface_results_reference_data/dsm_reactive_power.csv @@ -0,0 +1,4 @@ +snapshot,dsm +2011-01-01 00:00:00,0.0 +2011-01-01 12:00:00,0.0 +2011-01-02 00:00:00,0.0 diff --git a/tests/data/interface_results_reference_data/electromobility_active_power.csv b/tests/data/interface_results_reference_data/electromobility_active_power.csv new file mode 100644 index 00000000..4e95b784 --- /dev/null +++ b/tests/data/interface_results_reference_data/electromobility_active_power.csv @@ -0,0 +1,4 @@ +snapshot,BEV charger +2011-01-01 00:00:00,0.0 +2011-01-01 12:00:00,0.5 +2011-01-02 00:00:00,1.0 diff --git a/tests/data/interface_results_reference_data/electromobility_reactive_power.csv b/tests/data/interface_results_reference_data/electromobility_reactive_power.csv new file mode 100644 index 00000000..cd21048c --- /dev/null +++ b/tests/data/interface_results_reference_data/electromobility_reactive_power.csv @@ -0,0 +1,4 @@ +snapshot,BEV charger +2011-01-01 00:00:00,0.0 +2011-01-01 12:00:00,0.0 +2011-01-02 00:00:00,0.0 diff --git a/tests/data/interface_results_reference_data/geothermal_energy_feedin_district_heating.csv b/tests/data/interface_results_reference_data/geothermal_energy_feedin_district_heating.csv new file mode 100644 index 00000000..27d65dcf --- /dev/null +++ b/tests/data/interface_results_reference_data/geothermal_energy_feedin_district_heating.csv @@ -0,0 +1,4 @@ +snapshot,geo_thermal +2011-01-01 00:00:00,0.0 +2011-01-01 12:00:00,0.5 +2011-01-02 00:00:00,1.0 diff --git a/tests/data/interface_results_reference_data/heat_central_active_power.csv b/tests/data/interface_results_reference_data/heat_central_active_power.csv new file mode 100644 index 00000000..80ab6dd6 --- /dev/null +++ b/tests/data/interface_results_reference_data/heat_central_active_power.csv @@ -0,0 +1,4 @@ +snapshot,central_heat_pump,central_resistive_heater +2011-01-01 00:00:00,0.0,0.0 +2011-01-01 12:00:00,0.5,0.5 +2011-01-02 00:00:00,1.0,1.0 diff --git a/tests/data/interface_results_reference_data/heat_central_reactive_power.csv b/tests/data/interface_results_reference_data/heat_central_reactive_power.csv new file mode 100644 index 00000000..9ac30ecb --- /dev/null +++ b/tests/data/interface_results_reference_data/heat_central_reactive_power.csv @@ -0,0 +1,4 @@ +snapshot,central_heat_pump,central_resistive_heater +2011-01-01 00:00:00,0.0,0.0 +2011-01-01 12:00:00,0.0,0.0 +2011-01-02 00:00:00,0.0,0.0 diff --git a/tests/data/interface_results_reference_data/heat_pump_rural_active_power.csv b/tests/data/interface_results_reference_data/heat_pump_rural_active_power.csv new file mode 100644 index 00000000..13e33170 --- /dev/null +++ b/tests/data/interface_results_reference_data/heat_pump_rural_active_power.csv @@ -0,0 +1,4 @@ +snapshot,rural_heat_pump +2011-01-01 00:00:00,0.0 +2011-01-01 12:00:00,0.5 +2011-01-02 00:00:00,1.0 diff --git a/tests/data/interface_results_reference_data/heat_pump_rural_reactive_power.csv b/tests/data/interface_results_reference_data/heat_pump_rural_reactive_power.csv new file mode 100644 index 00000000..b7845c2e --- /dev/null +++ b/tests/data/interface_results_reference_data/heat_pump_rural_reactive_power.csv @@ -0,0 +1,4 @@ +snapshot,rural_heat_pump +2011-01-01 00:00:00,0.0 +2011-01-01 12:00:00,0.0 +2011-01-02 00:00:00,0.0 diff --git a/tests/data/interface_results_reference_data/renewables_curtailment.csv b/tests/data/interface_results_reference_data/renewables_curtailment.csv new file mode 100644 index 00000000..2e091ab1 --- /dev/null +++ b/tests/data/interface_results_reference_data/renewables_curtailment.csv @@ -0,0 +1,6 @@ +,solar,solar,wind_onshore,wind_onshore +,0,1,0,1 +snapshot,,,, +2011-01-01 00:00:00,0.5,0.5,0.5,0.5 +2011-01-01 12:00:00,0.25,0.7,0.25,0.7 +2011-01-02 00:00:00,0.0,0.9,0.0,0.9 diff --git a/tests/data/interface_results_reference_data/renewables_dispatch_reactive_power.csv b/tests/data/interface_results_reference_data/renewables_dispatch_reactive_power.csv new file mode 100644 index 00000000..6bbad759 --- /dev/null +++ b/tests/data/interface_results_reference_data/renewables_dispatch_reactive_power.csv @@ -0,0 +1,6 @@ +,solar,solar,wind_onshore,wind_onshore +,0,1,0,1 +snapshot,,,, +2011-01-01 00:00:00,0.0,0.0,0.0,0.0 +2011-01-01 12:00:00,0.5,0.05,0.5,0.05 +2011-01-02 00:00:00,-1.0,-0.1,-1.0,-0.1 diff --git a/tests/data/interface_results_reference_data/renewables_potential.csv b/tests/data/interface_results_reference_data/renewables_potential.csv new file mode 100644 index 00000000..5aa79156 --- /dev/null +++ b/tests/data/interface_results_reference_data/renewables_potential.csv @@ -0,0 +1,6 @@ +,solar,solar,wind_onshore,wind_onshore +,0,1,0,1 +snapshot,,,, +2011-01-01 00:00:00,0.5,0.5,0.5,0.5 +2011-01-01 12:00:00,0.75,0.75,0.75,0.75 +2011-01-02 00:00:00,1.0,1.0,1.0,1.0 diff --git a/tests/data/interface_results_reference_data/solarthermal_energy_feedin_district_heating.csv b/tests/data/interface_results_reference_data/solarthermal_energy_feedin_district_heating.csv new file mode 100644 index 00000000..d4403e1e --- /dev/null +++ b/tests/data/interface_results_reference_data/solarthermal_energy_feedin_district_heating.csv @@ -0,0 +1,4 @@ +snapshot,solar_thermal_collector +2011-01-01 00:00:00,0.0 +2011-01-01 12:00:00,0.5 +2011-01-02 00:00:00,1.0 diff --git a/tests/data/interface_results_reference_data/storage_units_active_power.csv b/tests/data/interface_results_reference_data/storage_units_active_power.csv new file mode 100644 index 00000000..afc27bff --- /dev/null +++ b/tests/data/interface_results_reference_data/storage_units_active_power.csv @@ -0,0 +1,4 @@ +snapshot,battery +2011-01-01 00:00:00,0.0 +2011-01-01 12:00:00,0.5 +2011-01-02 00:00:00,1.0 diff --git a/tests/data/interface_results_reference_data/storage_units_reactive_power.csv b/tests/data/interface_results_reference_data/storage_units_reactive_power.csv new file mode 100644 index 00000000..afc27bff --- /dev/null +++ b/tests/data/interface_results_reference_data/storage_units_reactive_power.csv @@ -0,0 +1,4 @@ +snapshot,battery +2011-01-01 00:00:00,0.0 +2011-01-01 12:00:00,0.5 +2011-01-02 00:00:00,1.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/dispatchable_generators_active_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/dispatchable_generators_active_power.csv new file mode 100644 index 00000000..0ed50dc0 --- /dev/null +++ b/tests/data/interface_results_reference_data_set_max_cos_phi/dispatchable_generators_active_power.csv @@ -0,0 +1,4 @@ +snapshot,biomass,central_biomass_CHP,run_of_river,gas,other_non_renewable,reservoir,central_gas_CHP +2011-01-01 00:00:00,0.0,0.0,0.0,0.0,0.0,0.0,0.0 +2011-01-01 12:00:00,0.5,0.5,0.5,0.5,0.5,0.5,0.5 +2011-01-02 00:00:00,1.0,1.0,1.0,1.0,1.0,1.0,1.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/dispatchable_generators_reactive_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/dispatchable_generators_reactive_power.csv new file mode 100644 index 00000000..915fb77e --- /dev/null +++ b/tests/data/interface_results_reference_data_set_max_cos_phi/dispatchable_generators_reactive_power.csv @@ -0,0 +1,4 @@ +snapshot,biomass,central_biomass_CHP,run_of_river,gas,other_non_renewable,reservoir,central_gas_CHP +2011-01-01 00:00:00,0.0,0.0,0.0,0.0,0.0,0.0,0.0 +2011-01-01 12:00:00,0.5,0.5,0.5,0.5,0.5,0.5,0.0 +2011-01-02 00:00:00,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,0.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/dsm_active_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/dsm_active_power.csv new file mode 100644 index 00000000..9a217ea7 --- /dev/null +++ b/tests/data/interface_results_reference_data_set_max_cos_phi/dsm_active_power.csv @@ -0,0 +1,4 @@ +snapshot,dsm +2011-01-01 00:00:00,0.0 +2011-01-01 12:00:00,0.5 +2011-01-02 00:00:00,1.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/dsm_reactive_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/dsm_reactive_power.csv new file mode 100644 index 00000000..f90539eb --- /dev/null +++ b/tests/data/interface_results_reference_data_set_max_cos_phi/dsm_reactive_power.csv @@ -0,0 +1,4 @@ +snapshot,dsm +2011-01-01 00:00:00,0.0 +2011-01-01 12:00:00,0.0 +2011-01-02 00:00:00,0.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/electromobility_active_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/electromobility_active_power.csv new file mode 100644 index 00000000..4e95b784 --- /dev/null +++ b/tests/data/interface_results_reference_data_set_max_cos_phi/electromobility_active_power.csv @@ -0,0 +1,4 @@ +snapshot,BEV charger +2011-01-01 00:00:00,0.0 +2011-01-01 12:00:00,0.5 +2011-01-02 00:00:00,1.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/electromobility_reactive_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/electromobility_reactive_power.csv new file mode 100644 index 00000000..cd21048c --- /dev/null +++ b/tests/data/interface_results_reference_data_set_max_cos_phi/electromobility_reactive_power.csv @@ -0,0 +1,4 @@ +snapshot,BEV charger +2011-01-01 00:00:00,0.0 +2011-01-01 12:00:00,0.0 +2011-01-02 00:00:00,0.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/geothermal_energy_feedin_district_heating.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/geothermal_energy_feedin_district_heating.csv new file mode 100644 index 00000000..27d65dcf --- /dev/null +++ b/tests/data/interface_results_reference_data_set_max_cos_phi/geothermal_energy_feedin_district_heating.csv @@ -0,0 +1,4 @@ +snapshot,geo_thermal +2011-01-01 00:00:00,0.0 +2011-01-01 12:00:00,0.5 +2011-01-02 00:00:00,1.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/heat_central_active_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/heat_central_active_power.csv new file mode 100644 index 00000000..80ab6dd6 --- /dev/null +++ b/tests/data/interface_results_reference_data_set_max_cos_phi/heat_central_active_power.csv @@ -0,0 +1,4 @@ +snapshot,central_heat_pump,central_resistive_heater +2011-01-01 00:00:00,0.0,0.0 +2011-01-01 12:00:00,0.5,0.5 +2011-01-02 00:00:00,1.0,1.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/heat_central_reactive_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/heat_central_reactive_power.csv new file mode 100644 index 00000000..9ac30ecb --- /dev/null +++ b/tests/data/interface_results_reference_data_set_max_cos_phi/heat_central_reactive_power.csv @@ -0,0 +1,4 @@ +snapshot,central_heat_pump,central_resistive_heater +2011-01-01 00:00:00,0.0,0.0 +2011-01-01 12:00:00,0.0,0.0 +2011-01-02 00:00:00,0.0,0.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/heat_pump_rural_active_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/heat_pump_rural_active_power.csv new file mode 100644 index 00000000..13e33170 --- /dev/null +++ b/tests/data/interface_results_reference_data_set_max_cos_phi/heat_pump_rural_active_power.csv @@ -0,0 +1,4 @@ +snapshot,rural_heat_pump +2011-01-01 00:00:00,0.0 +2011-01-01 12:00:00,0.5 +2011-01-02 00:00:00,1.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/heat_pump_rural_reactive_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/heat_pump_rural_reactive_power.csv new file mode 100644 index 00000000..b7845c2e --- /dev/null +++ b/tests/data/interface_results_reference_data_set_max_cos_phi/heat_pump_rural_reactive_power.csv @@ -0,0 +1,4 @@ +snapshot,rural_heat_pump +2011-01-01 00:00:00,0.0 +2011-01-01 12:00:00,0.0 +2011-01-02 00:00:00,0.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/renewables_curtailment.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/renewables_curtailment.csv new file mode 100644 index 00000000..2e091ab1 --- /dev/null +++ b/tests/data/interface_results_reference_data_set_max_cos_phi/renewables_curtailment.csv @@ -0,0 +1,6 @@ +,solar,solar,wind_onshore,wind_onshore +,0,1,0,1 +snapshot,,,, +2011-01-01 00:00:00,0.5,0.5,0.5,0.5 +2011-01-01 12:00:00,0.25,0.7,0.25,0.7 +2011-01-02 00:00:00,0.0,0.9,0.0,0.9 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/renewables_dispatch_reactive_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/renewables_dispatch_reactive_power.csv new file mode 100644 index 00000000..064b1ce2 --- /dev/null +++ b/tests/data/interface_results_reference_data_set_max_cos_phi/renewables_dispatch_reactive_power.csv @@ -0,0 +1,6 @@ +,solar,solar,wind_onshore,wind_onshore +,0,1,0,1 +snapshot,,,, +2011-01-01 00:00:00,0.0,0.0,0.0,0.0 +2011-01-01 12:00:00,0.24216105241892627,0.024216105241892626,0.24216105241892627,0.024216105241892626 +2011-01-02 00:00:00,-0.48432210483785254,-0.04843221048378525,-0.48432210483785254,-0.04843221048378525 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/renewables_potential.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/renewables_potential.csv new file mode 100644 index 00000000..5aa79156 --- /dev/null +++ b/tests/data/interface_results_reference_data_set_max_cos_phi/renewables_potential.csv @@ -0,0 +1,6 @@ +,solar,solar,wind_onshore,wind_onshore +,0,1,0,1 +snapshot,,,, +2011-01-01 00:00:00,0.5,0.5,0.5,0.5 +2011-01-01 12:00:00,0.75,0.75,0.75,0.75 +2011-01-02 00:00:00,1.0,1.0,1.0,1.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/solarthermal_energy_feedin_district_heating.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/solarthermal_energy_feedin_district_heating.csv new file mode 100644 index 00000000..d4403e1e --- /dev/null +++ b/tests/data/interface_results_reference_data_set_max_cos_phi/solarthermal_energy_feedin_district_heating.csv @@ -0,0 +1,4 @@ +snapshot,solar_thermal_collector +2011-01-01 00:00:00,0.0 +2011-01-01 12:00:00,0.5 +2011-01-02 00:00:00,1.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/storage_units_active_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/storage_units_active_power.csv new file mode 100644 index 00000000..afc27bff --- /dev/null +++ b/tests/data/interface_results_reference_data_set_max_cos_phi/storage_units_active_power.csv @@ -0,0 +1,4 @@ +snapshot,battery +2011-01-01 00:00:00,0.0 +2011-01-01 12:00:00,0.5 +2011-01-02 00:00:00,1.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/storage_units_reactive_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/storage_units_reactive_power.csv new file mode 100644 index 00000000..afc27bff --- /dev/null +++ b/tests/data/interface_results_reference_data_set_max_cos_phi/storage_units_reactive_power.csv @@ -0,0 +1,4 @@ +snapshot,battery +2011-01-01 00:00:00,0.0 +2011-01-01 12:00:00,0.5 +2011-01-02 00:00:00,1.0 diff --git a/tests/tools/test_interface.py b/tests/tools/test_interface.py new file mode 100644 index 00000000..cc4c4659 --- /dev/null +++ b/tests/tools/test_interface.py @@ -0,0 +1,265 @@ +import random + +import pytest +from pypsa import Network as PyPSANetwork +from ego.tools.interface import ETraGoMinimalData, get_etrago_results_per_bus + + +import pandas as pd +import os +import logging + +logger = logging.getLogger(__name__) + +random.seed(42) + + +def mock_get_weather_id_for_generator(grid_version, session, generator_index, scn_name): + generator_number = int(generator_index.split("_")[-1]) + + if generator_number in [0]: + weather_id = 0 + elif generator_number in [1]: + weather_id = 1 + + return weather_id + + +class TestSpecs: + @classmethod + def setup_class(cls): + cls.etrago_network = PyPSANetwork(pytest.etrago_test_network_1_path) + + def test_class_etrago_minimal_data(self): + etrago_network = ETraGoMinimalData(self.etrago_network) + assert not "p_min_pu" in etrago_network.generators_t + + def test_get_etrago_results_per_bus(self, monkeypatch): + monkeypatch.setattr( + "ego.tools.interface.get_weather_id_for_generator", + mock_get_weather_id_for_generator, + ) + + session = None + bus_id = 0 + etrago_network = ETraGoMinimalData(self.etrago_network) + grid_version = None + scn_name = "ego 100" + pf_post_lopf = True + max_cos_phi_renewable = False + + etrago_results_per_bus = get_etrago_results_per_bus( + session, + bus_id, + etrago_network, + grid_version, + scn_name, + pf_post_lopf, + max_cos_phi_renewable, + ) + + for key, value in etrago_results_per_bus.items(): + logger.info(f"Check Result: {key}") + if key == "timeindex": + assert type(value) is pd.DatetimeIndex + pd.testing.assert_index_equal( + value, + pd.DatetimeIndex( + data=[ + "2011-01-01 00:00:00", + "2011-01-01 12:00:00", + "2011-01-02 00:00:00", + ], + name="snapshot", + ), + ) + elif key == "storage_units_capacity": + assert value == 10.0 + elif key == "thermal_storage_central_capacity": + assert value == 1.0 + elif key == "thermal_storage_rural_capacity": + assert value == 1.0 + else: + assert type(value) is pd.DataFrame + path_reference_df = os.path.join( + pytest.interface_results_reference_data_path, f"{key}.csv" + ) + # value.to_csv(path_reference_df) + + if key in [ + "renewables_potential", + "renewables_curtailment", + "renewables_dispatch_reactive_power", + ]: + reference_df = pd.read_csv( + path_reference_df, index_col=0, header=[0, 1], parse_dates=True + ) + else: + reference_df = pd.read_csv( + path_reference_df, index_col=0, parse_dates=True + ) + pd.testing.assert_frame_equal( + value, reference_df, check_index_type=False, check_names=False + ) + + def test_get_etrago_results_per_bus_no_non_linear_pf(self, monkeypatch): + monkeypatch.setattr( + "ego.tools.interface.get_weather_id_for_generator", + mock_get_weather_id_for_generator, + ) + + session = None + bus_id = 0 + etrago_network = ETraGoMinimalData(self.etrago_network) + grid_version = None + scn_name = "ego 100" + pf_post_lopf = False + max_cos_phi_renewable = False + + etrago_results_per_bus = get_etrago_results_per_bus( + session, + bus_id, + etrago_network, + grid_version, + scn_name, + pf_post_lopf, + max_cos_phi_renewable, + ) + + none_results = [ + "dispatchable_generators_reactive_power", + "renewables_dispatch_reactive_power", + "storage_units_reactive_power", + "dsm_reactive_power", + "heat_central_reactive_power", + "heat_pump_rural_reactive_power", + "electromobility_reactive_power", + ] + + for key, value in etrago_results_per_bus.items(): + if value is None: + none_results.remove(key) + + assert len(none_results) == 0 + + def test_get_etrago_results_per_bus_empty(self, monkeypatch): + monkeypatch.setattr( + "ego.tools.interface.get_weather_id_for_generator", + mock_get_weather_id_for_generator, + ) + + session = None + bus_id = 11 + etrago_network = ETraGoMinimalData(self.etrago_network) + grid_version = None + scn_name = "ego 100" + pf_post_lopf = True + max_cos_phi_renewable = False + + etrago_results_per_bus = get_etrago_results_per_bus( + session, + bus_id, + etrago_network, + grid_version, + scn_name, + pf_post_lopf, + max_cos_phi_renewable, + ) + + none_results = [ + "dispatchable_generators_active_power", + "dispatchable_generators_reactive_power", + "renewables_potential", + "renewables_curtailment", + "renewables_dispatch_reactive_power", + "storage_units_capacity", + "storage_units_active_power", + "storage_units_reactive_power", + "dsm_active_power", + "dsm_reactive_power", + "heat_central_active_power", + "heat_central_reactive_power", + "thermal_storage_central_capacity", + "geothermal_energy_feedin_district_heating", + "solarthermal_energy_feedin_district_heating", + "heat_pump_rural_active_power", + "heat_pump_rural_reactive_power", + "thermal_storage_rural_capacity", + "electromobility_active_power", + "electromobility_reactive_power", + ] + + for key, value in etrago_results_per_bus.items(): + if value is None: + none_results.remove(key) + + assert len(none_results) == 0 + + def test_get_etrago_results_per_bus_with_set_max_cosphi(self, monkeypatch): + monkeypatch.setattr( + "ego.tools.interface.get_weather_id_for_generator", + mock_get_weather_id_for_generator, + ) + + session = None + bus_id = 0 + etrago_network = ETraGoMinimalData(self.etrago_network) + grid_version = None + scn_name = "ego 100" + pf_post_lopf = True + max_cos_phi_renewable = 0.9 + + etrago_results_per_bus = get_etrago_results_per_bus( + session, + bus_id, + etrago_network, + grid_version, + scn_name, + pf_post_lopf, + max_cos_phi_renewable, + ) + + for key, value in etrago_results_per_bus.items(): + logger.info(f"Check Result: {key}") + if key == "timeindex": + assert type(value) is pd.DatetimeIndex + pd.testing.assert_index_equal( + value, + pd.DatetimeIndex( + data=[ + "2011-01-01 00:00:00", + "2011-01-01 12:00:00", + "2011-01-02 00:00:00", + ], + name="snapshot", + ), + ) + elif key == "storage_units_capacity": + assert value == 10.0 + elif key == "thermal_storage_central_capacity": + assert value == 1.0 + elif key == "thermal_storage_rural_capacity": + assert value == 1.0 + else: + assert type(value) is pd.DataFrame + path_reference_df = os.path.join( + pytest.interface_results_reference_data_set_max_cos_phi_path, + f"{key}.csv", + ) + # value.to_csv(path_reference_df) + + if key in [ + "renewables_potential", + "renewables_curtailment", + "renewables_dispatch_reactive_power", + ]: + reference_df = pd.read_csv( + path_reference_df, index_col=0, header=[0, 1], parse_dates=True + ) + else: + reference_df = pd.read_csv( + path_reference_df, index_col=0, parse_dates=True + ) + pd.testing.assert_frame_equal( + value, reference_df, check_index_type=False, check_names=False + ) From 380cec823e68236411d303c83e6b41758ab0325a Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein <46815982+khelfen@users.noreply.github.com> Date: Fri, 23 Sep 2022 10:29:00 +0200 Subject: [PATCH 022/126] Feature/add pre commit hooks (#158) * added dev requirements * removed old dependencies * reformatted files with black and isort * adapted readme --- .flake8 | 7 + .isort.cfg | 4 + .pre-commit-config.yaml | 24 + README.rst | 16 +- ego/__init__.py | 6 +- ego/appl.py | 19 +- ego/run_test.py | 97 +-- ego/scenario_setting.json | 125 ++-- ego/tools/economics.py | 413 ++++++----- ego/tools/edisgo_integration.py | 1019 +++++++++++++-------------- ego/tools/io.py | 714 +++++++++---------- ego/tools/mv_cluster.py | 179 ++--- ego/tools/plots.py | 1160 +++++++++++++++++-------------- ego/tools/results.py | 72 +- ego/tools/specs.py | 301 ++++---- ego/tools/storages.py | 156 +++-- ego/tools/utilities.py | 153 ++-- setup.py | 100 ++- 18 files changed, 2364 insertions(+), 2201 deletions(-) create mode 100644 .flake8 create mode 100644 .isort.cfg create mode 100644 .pre-commit-config.yaml diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..3376e4aa --- /dev/null +++ b/.flake8 @@ -0,0 +1,7 @@ +[flake8] +extend-exclude = docs +max-line-length = 88 +extend-ignore = E203 +count = true +statistics = true +show-source = true diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 00000000..cc5b9a8a --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,4 @@ +[settings] +profile = black +multi_line_output = 3 +lines_between_types = 1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..cc65366d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace +- repo: https://github.com/psf/black + rev: 22.8.0 + hooks: + - id: black +- repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort + name: isort (python) +- repo: https://github.com/asottile/pyupgrade + rev: v2.38.0 + hooks: + - id: pyupgrade +- repo: https://github.com/pycqa/flake8 + rev: 5.0.4 + hooks: + - id: flake8 diff --git a/README.rst b/README.rst index 3aa23ff0..a0ac88ab 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ .. image:: https://openegoproject.files.wordpress.com/2017/02/open_ego_logo_breit.png?w=400 - + *A cross-grid-level electricity grid and storage optimization tool* | `openegoproject.wordpress.com `_ @@ -16,8 +16,8 @@ eGo Integrated optimization of flexibility options and grid extension measures for power grids based on `eTraGo `_ and -`eDisGo `_. The Documentation of the eGo tool -can be found on +`eDisGo `_. The Documentation of the eGo tool +can be found on `openego.readthedocs.io `_ . .. contents:: @@ -35,7 +35,7 @@ In case of installation errors of pypsa-fork use: .. code-block:: - $ pip3 install -e git+https://github.com/openego/PyPSA@master#egg=0.11.0fork + $ pip3 install -e git+https://github.com/openego/PyPSA@master#egg=0.11.0fork ---------------------------- @@ -46,10 +46,12 @@ Create a virtualenvironment and activate it: .. code-block:: - $ virtualenv venv --clear -p python3.5 + $ virtualenv venv --clear -p python3.8 $ source venv/bin/activate $ cd venv - $ pip3 install -e git+https://github.com/openego/eGo@dev#egg=eGo --process-dependency-links + $ python -m pip install -e .[full] + $ pre-commit install # install pre-commit hooks + ------- License @@ -84,6 +86,6 @@ this program. If not, see https://www.gnu.org/licenses/. .. |readthedocs| image:: https://readthedocs.org/projects/openego/badge/?version=master :target: http://openego.readthedocs.io/en/latest/?badge=master :alt: Documentation Status - + .. |zenodo| image:: https://zenodo.org/badge/87306120.svg :target: https://zenodo.org/badge/latestdoi/87306120 diff --git a/ego/__init__.py b/ego/__init__.py index c87bd1d7..ee083176 100644 --- a/ego/__init__.py +++ b/ego/__init__.py @@ -11,11 +11,13 @@ # along with this program. If not, see . __version__ = "0.3.4" -__copyright__ = ("Europa-Universität Flensburg, " - " Centre for Sustainable Energy Systems") +__copyright__ = ( + "Europa-Universität Flensburg, " " Centre for Sustainable Energy Systems" +) __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolf_bunke" import logging + logging.basicConfig(level=logging.INFO) diff --git a/ego/appl.py b/ego/appl.py index 627d7d99..0539ba3d 100644 --- a/ego/appl.py +++ b/ego/appl.py @@ -29,23 +29,26 @@ import os -if not 'READTHEDOCS' in os.environ: +if not "READTHEDOCS" in os.environ: from tools.io import eGo from tools.utilities import define_logging - logger = define_logging(name='ego') -__copyright__ = ("Flensburg University of Applied Sciences, " - "Europa-Universität Flensburg, " - "Centre for Sustainable Energy Systems") + logger = define_logging(name="ego") + +__copyright__ = ( + "Flensburg University of Applied Sciences, " + "Europa-Universität Flensburg, " + "Centre for Sustainable Energy Systems" +) __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolf_bunke, maltesc" -if __name__ == '__main__': +if __name__ == "__main__": - logger.info('Start calculation') + logger.info("Start calculation") - ego = eGo(jsonpath='scenario_setting.json') + ego = eGo(jsonpath="scenario_setting.json") # logger.info('Print results') # ego.etrago_line_loading() # print(ego.etrago.generator) diff --git a/ego/run_test.py b/ego/run_test.py index 23c020f3..c9d016c5 100644 --- a/ego/run_test.py +++ b/ego/run_test.py @@ -1,32 +1,37 @@ # -*- coding: utf-8 -*- -from datetime import datetime -from tools.io import eGo +import os import sys -from pycallgraph import PyCallGraph -from pycallgraph.output import GraphvizOutput -from pycallgraph import Config -import pandas as pd + +from datetime import datetime + import matplotlib.pyplot as plt -import os +import pandas as pd + +from pycallgraph import Config, PyCallGraph +from pycallgraph.output import GraphvizOutput +from tools.io import eGo from tools.utilities import define_logging -logger = define_logging(name='ego') + +logger = define_logging(name="ego") def ego_testing(ego): - """ Call and test all ego Funktion - """ + """Call and test all ego Funktion""" # full networks try: logger.info("ego.etrago.network: {} ".format(ego.etrago.network)) - logger.info("ego.etrago.disaggregated_network: {} ".format( - ego.etrago.disaggregated_network)) + logger.info( + "ego.etrago.disaggregated_network: {} ".format( + ego.etrago.disaggregated_network + ) + ) # aggregated results logger.info("Testing of aggregated results ego.etrago. ") - logger.info("storage_investment_costs: {} ".format( - ego.etrago.storage_investment_costs)) - logger.info("storage_charges: {} ".format( - ego.etrago.storage_charges)) + logger.info( + "storage_investment_costs: {} ".format(ego.etrago.storage_investment_costs) + ) + logger.info("storage_charges: {} ".format(ego.etrago.storage_charges)) ego.etrago.operating_costs ego.etrago.generator @@ -50,39 +55,39 @@ def ego_testing(ego): logger.info("eTraGo failed testing") # eDisGo try: - logger.info("ego.edisgo: {} ".format( - ego.edisgo)) + logger.info("ego.edisgo: {} ".format(ego.edisgo)) except: logger.info("ego.ego.edisgo failed testing") try: - logger.info("ego.edisgo.network: {} ".format( - ego.edisgo.network)) + logger.info("ego.edisgo.network: {} ".format(ego.edisgo.network)) except: logger.info("ego.edisgo.network failed testing") try: - logger.info("ego.edisgo.grid_investment_costs: {} ".format( - ego.edisgo.grid_investment_costs)) + logger.info( + "ego.edisgo.grid_investment_costs: {} ".format( + ego.edisgo.grid_investment_costs + ) + ) except: logger.info("ego.edisgo.grid_investment_costs failed testing") try: - logger.info("ego.edisgo.grid_choice: {} ".format( - ego.edisgo.grid_choice)) + logger.info("ego.edisgo.grid_choice: {} ".format(ego.edisgo.grid_choice)) except: logger.info("ego.edisgo.grid_choice failed testing") try: - logger.info("ego.edisgo.successfull_grids: {} ".format( - ego.edisgo.successfull_grids)) + logger.info( + "ego.edisgo.successfull_grids: {} ".format(ego.edisgo.successfull_grids) + ) except: logger.info("ego.edisgo.successfull_grids failed testing") # eGo - logger.info("ego.total_investment_costs: {} ".format( - ego.total_investment_costs)) - logger.info("ego.total_operation_costs: {} ".format( - ego.total_operation_costs)) + logger.info("ego.total_investment_costs: {} ".format(ego.total_investment_costs)) + logger.info("ego.total_operation_costs: {} ".format(ego.total_operation_costs)) # ego plot functions try: ego.plot_total_investment_costs( - filename="results/plot_total_investment_costs.pdf") + filename="results/plot_total_investment_costs.pdf" + ) except: logger.info("ego.plot_total_investment_costs failed testing") try: @@ -102,39 +107,43 @@ def ego_testing(ego): except: logger.info(" plot_edisgo_cluster failed testing") try: - ego.plot_line_expansion(column='investment_costs', - filename="results/investment_costs.pdf") + ego.plot_line_expansion( + column="investment_costs", filename="results/investment_costs.pdf" + ) except: logger.info(" plot_line_expansion failed testing") try: - ego.plot_line_expansion(column='overnight_costs', - filename="results/overnight_costs.pdf") + ego.plot_line_expansion( + column="overnight_costs", filename="results/overnight_costs.pdf" + ) except: logger.info(" plot_line_expansion failed testing") try: - ego.plot_line_expansion(column='s_nom_expansion', - filename="results/s_nom_expansion.pdf") + ego.plot_line_expansion( + column="s_nom_expansion", filename="results/s_nom_expansion.pdf" + ) except: logger.info(" plot_line_expansion failed testing") try: - ego.plot_storage_expansion(column='overnight_costs', - filename="results/storage_capital_investment.pdf") + ego.plot_storage_expansion( + column="overnight_costs", filename="results/storage_capital_investment.pdf" + ) except: logger.info(" plot_storage_expansion failed testing") def main(): - logger.info('Start calculation') + logger.info("Start calculation") graphviz = GraphvizOutput() date = str(datetime.now().strftime("%Y-%m-%d-%H-%M-%S")) - graphviz.output_file = 'results/'+str(date)+'_basic_process_plot.png' + graphviz.output_file = "results/" + str(date) + "_basic_process_plot.png" logger.info("Time: {} ".format(date)) with PyCallGraph(output=graphviz, config=Config(groups=True)): - ego = eGo(jsonpath='scenario_setting_local.json') - logger.info('Start testing') + ego = eGo(jsonpath="scenario_setting_local.json") + logger.info("Start testing") ego_testing(ego) # object size @@ -143,5 +152,5 @@ def main(): logger.info("Time: {} ".format(str(datetime.now()))) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/ego/scenario_setting.json b/ego/scenario_setting.json index a996e105..80089304 100644 --- a/ego/scenario_setting.json +++ b/ego/scenario_setting.json @@ -1,62 +1,67 @@ { - "eGo": { - "eTraGo": true, - "eDisGo": true, - "csv_import_eTraGo": false, - "csv_import_eDisGo": false - }, - "eTraGo": { - "db": "oedb", - "gridversion": "v0.4.5", - "method": "lopf", - "pf_post_lopf": true, - "start_snapshot": 1000, - "end_snapshot" : 1005, - "solver": "gurobi", - "solver_options":{}, - "scn_name": "eGo 100", - "scn_extension": null, - "scn_decommissioning": null, - "lpfile": false, - "csv_export": "results/your_results", - "db_export": false, - "extendable": ["storage", "network"], - "generator_noise": 789456, - "minimize_loading": false, - "ramp_limits": false, - "extra_functionality": null, - "network_clustering_kmeans": 10, - "load_cluster": false, - "network_clustering_ehv": false, - "disaggregation": "uniform", - "snapshot_clustering": false, - "parallelisation": false, - "skip_snapshots": false, - "line_grouping": false, - "branch_capacity_factor": {"HV": 0.5, "eHV" : 0.7}, - "load_shedding": false, - "foreign_lines" :{"carrier": "AC", "capacity": "osmTGmod"}, - "comments": "" - }, - "eDisGo": { - "db": "oedb", - "gridversion": "v0.4.5", - "ding0_files": "/path/to_your/.dingo/grids", - "choice_mode": "cluster", - "cluster_attributes":["farthest_node", "wind_cap", "solar_cap", "extended_storage"], - "only_cluster": false, - "manual_grids": [], - "no_grids": 2, - "parallelization":true, - "max_calc_time": 0.5, - "max_workers":2, - "initial_reinforcement":true, - "apply_curtailment":true, - "curtailment_voltage_threshold": 0, - "storage_distribution":true, - "max_cos_phi_renewable": 0.9, - "results": "results/another_result", - "solver": "gurobi", - "timesteps_pfa": "snapshot_analysis" - } + "eGo": { + "eTraGo": true, + "eDisGo": true, + "csv_import_eTraGo": false, + "csv_import_eDisGo": false, + }, + "eTraGo": { + "db": "oedb", + "gridversion": "v0.4.5", + "method": "lopf", + "pf_post_lopf": true, + "start_snapshot": 1000, + "end_snapshot": 1005, + "solver": "gurobi", + "solver_options": {}, + "scn_name": "eGo 100", + "scn_extension": null, + "scn_decommissioning": null, + "lpfile": false, + "csv_export": "results/your_results", + "db_export": false, + "extendable": ["storage", "network"], + "generator_noise": 789456, + "minimize_loading": false, + "ramp_limits": false, + "extra_functionality": null, + "network_clustering_kmeans": 10, + "load_cluster": false, + "network_clustering_ehv": false, + "disaggregation": "uniform", + "snapshot_clustering": false, + "parallelisation": false, + "skip_snapshots": false, + "line_grouping": false, + "branch_capacity_factor": {"HV": 0.5, "eHV": 0.7}, + "load_shedding": false, + "foreign_lines": {"carrier": "AC", "capacity": "osmTGmod"}, + "comments": "", + }, + "eDisGo": { + "db": "oedb", + "gridversion": "v0.4.5", + "ding0_files": "/path/to_your/.dingo/grids", + "choice_mode": "cluster", + "cluster_attributes": [ + "farthest_node", + "wind_cap", + "solar_cap", + "extended_storage", + ], + "only_cluster": false, + "manual_grids": [], + "no_grids": 2, + "parallelization": true, + "max_calc_time": 0.5, + "max_workers": 2, + "initial_reinforcement": true, + "apply_curtailment": true, + "curtailment_voltage_threshold": 0, + "storage_distribution": true, + "max_cos_phi_renewable": 0.9, + "results": "results/another_result", + "solver": "gurobi", + "timesteps_pfa": "snapshot_analysis", + }, } diff --git a/ego/tools/economics.py b/ego/tools/economics.py index b430b386..a04e57d9 100644 --- a/ego/tools/economics.py +++ b/ego/tools/economics.py @@ -22,26 +22,31 @@ """ import io -import pkgutil -import os import logging -logger = logging.getLogger('ego') +import os +import pkgutil -if not 'READTHEDOCS' in os.environ: - import pandas as pd +logger = logging.getLogger("ego") + +if not "READTHEDOCS" in os.environ: import numpy as np - from ego.tools.utilities import get_time_steps + import pandas as pd + from etrago.tools.utilities import geolocation_buses -__copyright__ = "Flensburg University of Applied Sciences, Europa-Universität"\ + from ego.tools.utilities import get_time_steps + +__copyright__ = ( + "Flensburg University of Applied Sciences, Europa-Universität" "Flensburg, Centre for Sustainable Energy Systems" +) __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolfbunke" # calculate annuity per time step or periode def annuity_per_period(capex, n, wacc, t, p): - """ Calculate per given period + """Calculate per given period Parameters ---------- @@ -63,7 +68,7 @@ def annuity_per_period(capex, n, wacc, t, p): def edisgo_convert_capital_costs(overnight_cost, t, p, json_file): - """ Get scenario and calculation specific annuity cost by given capital + """Get scenario and calculation specific annuity cost by given capital costs and lifetime. @@ -97,21 +102,20 @@ def edisgo_convert_capital_costs(overnight_cost, t, p, json_file): # https://github.com/openego/eTraGo/blob/dev/etrago/tools/utilities.py#L651 # Calculate present value of an annuity (PVA) - PVA = (1 / p) - (1 / (p*(1 + p) ** t)) + PVA = (1 / p) - (1 / (p * (1 + p) ** t)) year = 8760 # get period of calculation - period = (json_file['eTraGo']['end_snapshot'] - - json_file['eTraGo']['start_snapshot']) + period = json_file["eTraGo"]["end_snapshot"] - json_file["eTraGo"]["start_snapshot"] # calculation of capital_cost - annuity_cost = (overnight_cost / (PVA * (year/(period+1)))) + annuity_cost = overnight_cost / (PVA * (year / (period + 1))) return annuity_cost def etrago_convert_overnight_cost(annuity_cost, json_file, t=40, p=0.05): - """ Get annuity cost of simulation and calculation total + """Get annuity cost of simulation and calculation total ``overnight_costs`` by given capital costs and lifetime. Parameters @@ -146,21 +150,20 @@ def etrago_convert_overnight_cost(annuity_cost, json_file, t=40, p=0.05): # https://github.com/openego/eTraGo/blob/dev/etrago/tools/utilities.py#L651 # Calculate present value of an annuity (PVA) - PVA = (1 / p) - (1 / (p*(1 + p) ** t)) + PVA = (1 / p) - (1 / (p * (1 + p) ** t)) year = 8760 # get period of calculation - period = (json_file['eTraGo']['end_snapshot'] - - json_file['eTraGo']['start_snapshot']) + period = json_file["eTraGo"]["end_snapshot"] - json_file["eTraGo"]["start_snapshot"] # calculation of overnight_cost - overnight_cost = annuity_cost*(PVA * (year/(period+1))) + overnight_cost = annuity_cost * (PVA * (year / (period + 1))) return overnight_cost def etrago_operating_costs(network): - """ Function to get all operating costs of eTraGo. + """Function to get all operating costs of eTraGo. Parameters ---------- @@ -197,77 +200,88 @@ def etrago_operating_costs(network): etg = network # get v_nom - _bus = pd.DataFrame(etg.buses['v_nom']) + _bus = pd.DataFrame(etg.buses["v_nom"]) _bus.index.name = "name" _bus.reset_index(level=0, inplace=True) # Add voltage level idx = etg.generators.index - etg.generators = pd.merge(etg.generators, _bus, - left_on='bus', right_on='name') + etg.generators = pd.merge(etg.generators, _bus, left_on="bus", right_on="name") etg.generators.index = idx - etg.generators['voltage_level'] = 'unknown' + etg.generators["voltage_level"] = "unknown" # add ehv - ix_ehv = etg.generators[etg.generators['v_nom'] >= 380].index - etg.generators.set_value(ix_ehv, 'voltage_level', 'ehv') + ix_ehv = etg.generators[etg.generators["v_nom"] >= 380].index + etg.generators.set_value(ix_ehv, "voltage_level", "ehv") # add hv - ix_hv = etg.generators[(etg.generators['v_nom'] <= 220) & - (etg.generators['v_nom'] >= 110)].index - etg.generators.set_value(ix_hv, 'voltage_level', 'hv') + ix_hv = etg.generators[ + (etg.generators["v_nom"] <= 220) & (etg.generators["v_nom"] >= 110) + ].index + etg.generators.set_value(ix_hv, "voltage_level", "hv") # get voltage_level index - ix_by_ehv = etg.generators[etg.generators.voltage_level == 'ehv'].index - ix_by_hv = etg.generators[etg.generators.voltage_level == 'hv'].index - ix_slack = etg.generators[etg.generators.control != 'Slack'].index + ix_by_ehv = etg.generators[etg.generators.voltage_level == "ehv"].index + ix_by_hv = etg.generators[etg.generators.voltage_level == "hv"].index + ix_slack = etg.generators[etg.generators.control != "Slack"].index - ix_by_ehv = ix_slack.join(ix_by_ehv, how='left', level=None, - return_indexers=False, sort=False) - ix_by_hv = ix_slack.join(ix_by_hv, how='right', level=None, - return_indexers=False, sort=False) + ix_by_ehv = ix_slack.join( + ix_by_ehv, how="left", level=None, return_indexers=False, sort=False + ) + ix_by_hv = ix_slack.join( + ix_by_hv, how="right", level=None, return_indexers=False, sort=False + ) # groupby v_nom ehv - operating_costs_ehv = (etg.generators_t.p[ix_by_ehv] * - etg.generators. marginal_cost[ix_by_ehv]) - operating_costs_ehv = operating_costs_ehv.groupby( - etg.generators.carrier, axis=1).sum().sum() + operating_costs_ehv = ( + etg.generators_t.p[ix_by_ehv] * etg.generators.marginal_cost[ix_by_ehv] + ) + operating_costs_ehv = ( + operating_costs_ehv.groupby(etg.generators.carrier, axis=1).sum().sum() + ) operating_costs = pd.DataFrame(operating_costs_ehv) - operating_costs.columns = ['operation_costs'] - operating_costs['voltage_level'] = 'ehv' + operating_costs.columns = ["operation_costs"] + operating_costs["voltage_level"] = "ehv" # groupby v_nom ehv - operating_costs_hv = (etg.generators_t.p[ix_by_hv] * - etg.generators. marginal_cost[ix_by_hv]) - operating_costs_hv = operating_costs_hv.groupby( - etg.generators.carrier, axis=1).sum().sum() + operating_costs_hv = ( + etg.generators_t.p[ix_by_hv] * etg.generators.marginal_cost[ix_by_hv] + ) + operating_costs_hv = ( + operating_costs_hv.groupby(etg.generators.carrier, axis=1).sum().sum() + ) opt_costs_hv = pd.DataFrame(operating_costs_hv) - opt_costs_hv.columns = ['operation_costs'] - opt_costs_hv['voltage_level'] = 'hv' + opt_costs_hv.columns = ["operation_costs"] + opt_costs_hv["voltage_level"] = "hv" # add df operating_costs = operating_costs.append(opt_costs_hv) - tpc_ehv = pd.DataFrame(operating_costs_ehv.sum(), - columns=['operation_costs'], - index=['total_power_costs']) - tpc_ehv['voltage_level'] = 'ehv' + tpc_ehv = pd.DataFrame( + operating_costs_ehv.sum(), + columns=["operation_costs"], + index=["total_power_costs"], + ) + tpc_ehv["voltage_level"] = "ehv" operating_costs = operating_costs.append(tpc_ehv) - tpc_hv = pd.DataFrame(operating_costs_hv.sum(), - columns=['operation_costs'], - index=['total_power_costs']) - tpc_hv['voltage_level'] = 'hv' + tpc_hv = pd.DataFrame( + operating_costs_hv.sum(), + columns=["operation_costs"], + index=["total_power_costs"], + ) + tpc_hv["voltage_level"] = "hv" operating_costs = operating_costs.append(tpc_hv) # add Grid and Transform Costs try: - etg.lines['voltage_level'] = 'unknown' - ix_ehv = etg.lines[etg.lines['v_nom'] >= 380].index - etg.lines.set_value(ix_ehv, 'voltage_level', 'ehv') - ix_hv = etg.lines[(etg.lines['v_nom'] <= 220) & - (etg.lines['v_nom'] >= 110)].index - etg.lines.set_value(ix_hv, 'voltage_level', 'hv') + etg.lines["voltage_level"] = "unknown" + ix_ehv = etg.lines[etg.lines["v_nom"] >= 380].index + etg.lines.set_value(ix_ehv, "voltage_level", "ehv") + ix_hv = etg.lines[ + (etg.lines["v_nom"] <= 220) & (etg.lines["v_nom"] >= 110) + ].index + etg.lines.set_value(ix_hv, "voltage_level", "hv") losses_total = sum(etg.lines.losses) + sum(etg.transformers.losses) losses_costs = losses_total * np.average(etg.buses_t.marginal_price) @@ -277,24 +291,26 @@ def etrago_operating_costs(network): # axis=0).sum().reset_index() except AttributeError: - logger.info("No Transform and Line losses are calcualted! \n" - "Use eTraGo pf_post_lopf method") + logger.info( + "No Transform and Line losses are calcualted! \n" + "Use eTraGo pf_post_lopf method" + ) losses_total = 0 losses_costs = 0 # total grid losses costs - tgc = pd.DataFrame(losses_costs, - columns=['operation_costs'], - index=['total_grid_losses']) - tgc['voltage_level'] = 'ehv/hv' + tgc = pd.DataFrame( + losses_costs, columns=["operation_costs"], index=["total_grid_losses"] + ) + tgc["voltage_level"] = "ehv/hv" operating_costs = operating_costs.append(tgc) - #power_price = power_price.T.iloc[0] + # power_price = power_price.T.iloc[0] return operating_costs def etrago_grid_investment(network, json_file, session): - """ Function to get grid expantion costs from eTraGo + """Function to get grid expantion costs from eTraGo Parameters ---------- @@ -329,53 +345,60 @@ def etrago_grid_investment(network, json_file, session): """ # check settings for extendable - if 'network' not in json_file['eTraGo']['extendable']: - logger.info("The optimizition was not using parameter" - " 'extendable': network \n" - "No grid expantion costs from etrago") + if "network" not in json_file["eTraGo"]["extendable"]: + logger.info( + "The optimizition was not using parameter" + " 'extendable': network \n" + "No grid expantion costs from etrago" + ) - if 'network' in json_file['eTraGo']['extendable']: + if "network" in json_file["eTraGo"]["extendable"]: network = geolocation_buses(network, session) # differentiation by country_code - network.lines['differentiation'] = 'none' + network.lines["differentiation"] = "none" - network.lines['bus0_c'] = network.lines.bus0.map( - network.buses.country_code) - network.lines['bus1_c'] = network.lines.bus1.map( - network.buses.country_code) + network.lines["bus0_c"] = network.lines.bus0.map(network.buses.country_code) + network.lines["bus1_c"] = network.lines.bus1.map(network.buses.country_code) for idx, val in network.lines.iterrows(): - check = val['bus0_c'] + val['bus1_c'] + check = val["bus0_c"] + val["bus1_c"] if "DE" in check: - network.lines['differentiation'][idx] = 'cross-border' + network.lines["differentiation"][idx] = "cross-border" if "DEDE" in check: - network.lines['differentiation'][idx] = 'domestic' + network.lines["differentiation"][idx] = "domestic" if "DE" not in check: - network.lines['differentiation'][idx] = 'foreign' - - lines = network.lines[['v_nom', 'capital_cost', 's_nom', - 's_nom_min', 's_nom_opt', 'differentiation'] - ].reset_index() - - lines['s_nom_expansion'] = lines.s_nom_opt.subtract( - lines.s_nom, axis='index') - lines['capital_cost'] = lines.s_nom_expansion.multiply( - lines.capital_cost, axis='index') - lines['number_of_expansion'] = lines.s_nom_expansion > 0.0 - lines['time_step'] = get_time_steps(json_file) + network.lines["differentiation"][idx] = "foreign" + + lines = network.lines[ + [ + "v_nom", + "capital_cost", + "s_nom", + "s_nom_min", + "s_nom_opt", + "differentiation", + ] + ].reset_index() + + lines["s_nom_expansion"] = lines.s_nom_opt.subtract(lines.s_nom, axis="index") + lines["capital_cost"] = lines.s_nom_expansion.multiply( + lines.capital_cost, axis="index" + ) + lines["number_of_expansion"] = lines.s_nom_expansion > 0.0 + lines["time_step"] = get_time_steps(json_file) # add v_level - lines['voltage_level'] = 'unknown' + lines["voltage_level"] = "unknown" - ix_ehv = lines[lines['v_nom'] >= 380].index - lines.set_value(ix_ehv, 'voltage_level', 'ehv') + ix_ehv = lines[lines["v_nom"] >= 380].index + lines.set_value(ix_ehv, "voltage_level", "ehv") - ix_hv = lines[(lines['v_nom'] <= 220) & (lines['v_nom'] >= 110)].index - lines.set_value(ix_hv, 'voltage_level', 'hv') + ix_hv = lines[(lines["v_nom"] <= 220) & (lines["v_nom"] >= 110)].index + lines.set_value(ix_hv, "voltage_level", "hv") # based on eTraGo Function: # https://github.com/openego/eTraGo/blob/dev/etrago/tools/utilities.py#L651 @@ -383,48 +406,58 @@ def etrago_grid_investment(network, json_file, session): trafo = pd.DataFrame() # get costs of transfomers - if json_file['eTraGo']['network_clustering_kmeans'] == False: + if json_file["eTraGo"]["network_clustering_kmeans"] == False: - network.transformers['differentiation'] = 'none' + network.transformers["differentiation"] = "none" - trafos = network.transformers[['v_nom0', 'v_nom1', 'capital_cost', - 's_nom_extendable', 's_nom', - 's_nom_opt']] + trafos = network.transformers[ + [ + "v_nom0", + "v_nom1", + "capital_cost", + "s_nom_extendable", + "s_nom", + "s_nom_opt", + ] + ] trafos.columns.name = "" trafos.index.name = "" trafos.reset_index() - trafos['s_nom_extendable'] = trafos.s_nom_opt.subtract( - trafos.s_nom, axis='index') + trafos["s_nom_extendable"] = trafos.s_nom_opt.subtract( + trafos.s_nom, axis="index" + ) - trafos['capital_cost'] = trafos.s_nom_extendable.multiply( - trafos.capital_cost, axis='index') - trafos['number_of_expansion'] = trafos.s_nom_extendable > 0.0 - trafos['time_step'] = get_time_steps(json_file) + trafos["capital_cost"] = trafos.s_nom_extendable.multiply( + trafos.capital_cost, axis="index" + ) + trafos["number_of_expansion"] = trafos.s_nom_extendable > 0.0 + trafos["time_step"] = get_time_steps(json_file) # add v_level - trafos['voltage_level'] = 'unknown' + trafos["voltage_level"] = "unknown" # TODO check - ix_ehv = trafos[trafos['v_nom0'] >= 380].index - trafos.set_value(ix_ehv, 'voltage_level', 'ehv') + ix_ehv = trafos[trafos["v_nom0"] >= 380].index + trafos.set_value(ix_ehv, "voltage_level", "ehv") - ix_hv = trafos[(trafos['v_nom0'] <= 220) & - (trafos['v_nom0'] >= 110)].index - trafos.set_value(ix_hv, 'voltage_level', 'hv') + ix_hv = trafos[(trafos["v_nom0"] <= 220) & (trafos["v_nom0"] >= 110)].index + trafos.set_value(ix_hv, "voltage_level", "hv") # aggregate trafo - trafo = trafos[['voltage_level', - 'capital_cost', - 'differentiation']].groupby(['differentiation', - 'voltage_level'] - ).sum().reset_index() + trafo = ( + trafos[["voltage_level", "capital_cost", "differentiation"]] + .groupby(["differentiation", "voltage_level"]) + .sum() + .reset_index() + ) # aggregate lines - line = lines[['voltage_level', - 'capital_cost', - 'differentiation']].groupby(['differentiation', - 'voltage_level'] - ).sum().reset_index() + line = ( + lines[["voltage_level", "capital_cost", "differentiation"]] + .groupby(["differentiation", "voltage_level"]) + .sum() + .reset_index() + ) # merge trafos and line frames = [line, trafo] @@ -455,117 +488,113 @@ def edisgo_grid_investment(edisgo, json_file): t = 40 p = 0.05 - logger.info('For all components T={} and p={} is used'.format(t, p)) + logger.info("For all components T={} and p={} is used".format(t, p)) - costs = pd.DataFrame( - columns=['voltage_level', 'annuity_costs', 'overnight_costs']) + costs = pd.DataFrame(columns=["voltage_level", "annuity_costs", "overnight_costs"]) # Loop through all calculated eDisGo grids for key, value in edisgo.network.items(): - if not hasattr(value, 'network'): - logger.warning('No results available for grid {}'.format(key)) + if not hasattr(value, "network"): + logger.warning("No results available for grid {}".format(key)) continue # eDisGo results (overnight costs) for this grid costs_single = value.network.results.grid_expansion_costs - costs_single.rename( - columns={'total_costs': 'overnight_costs'}, - inplace=True) + costs_single.rename(columns={"total_costs": "overnight_costs"}, inplace=True) # continue if this grid was not reinforced - if (costs_single['overnight_costs'].sum() == 0.): - logger.info('No expansion costs for grid {}'.format(key)) + if costs_single["overnight_costs"].sum() == 0.0: + logger.info("No expansion costs for grid {}".format(key)) continue # Overnight cost translated in annuity costs - costs_single['capital_cost'] = edisgo_convert_capital_costs( - costs_single['overnight_costs'], - t=t, - p=p, - json_file=json_file) + costs_single["capital_cost"] = edisgo_convert_capital_costs( + costs_single["overnight_costs"], t=t, p=p, json_file=json_file + ) # Weighting (retrieves the singe (absolute) weighting for this grid) choice = edisgo.grid_choice - weighting = choice.loc[ - choice['the_selected_network_id'] == key - ][ - 'no_of_points_per_cluster' + weighting = choice.loc[choice["the_selected_network_id"] == key][ + "no_of_points_per_cluster" ].values[0] - costs_single[['capital_cost', 'overnight_costs']] = ( - costs_single[['capital_cost', 'overnight_costs']] - * weighting) + costs_single[["capital_cost", "overnight_costs"]] = ( + costs_single[["capital_cost", "overnight_costs"]] * weighting + ) # Append costs of this grid costs = costs.append( - costs_single[[ - 'voltage_level', - 'capital_cost', - 'overnight_costs']], ignore_index=True) + costs_single[["voltage_level", "capital_cost", "overnight_costs"]], + ignore_index=True, + ) if len(costs) == 0: - logger.info('No expansion costs in any MV grid') + logger.info("No expansion costs in any MV grid") return None else: - aggr_costs = costs.groupby( - ['voltage_level']).sum().reset_index() + aggr_costs = costs.groupby(["voltage_level"]).sum().reset_index() # In eDisGo all costs are in kEuro (eGo only takes Euro) - aggr_costs[['capital_cost', 'overnight_costs']] = ( - aggr_costs[['capital_cost', 'overnight_costs']] - * 1000) + aggr_costs[["capital_cost", "overnight_costs"]] = ( + aggr_costs[["capital_cost", "overnight_costs"]] * 1000 + ) successfull_grids = edisgo.successfull_grids if successfull_grids < 1: logger.warning( - 'Only {} % of the grids were calculated.\n'.format( - "{0:,.2f}".format(successfull_grids * 100) - ) + 'Costs are extrapolated...') + "Only {} % of the grids were calculated.\n".format( + "{:,.2f}".format(successfull_grids * 100) + ) + + "Costs are extrapolated..." + ) - aggr_costs[['capital_cost', 'overnight_costs']] = ( - aggr_costs[['capital_cost', 'overnight_costs']] - / successfull_grids) + aggr_costs[["capital_cost", "overnight_costs"]] = ( + aggr_costs[["capital_cost", "overnight_costs"]] / successfull_grids + ) return aggr_costs def get_generator_investment(network, scn_name): - """ Get investment costs per carrier/ generator. - - """ + """Get investment costs per carrier/ generator.""" etg = network try: - data = pkgutil.get_data('ego', 'data/investment_costs.csv') - invest = pd.read_csv(io.BytesIO(data), - encoding='utf8', sep=",", - index_col="carriers") + data = pkgutil.get_data("ego", "data/investment_costs.csv") + invest = pd.read_csv( + io.BytesIO(data), encoding="utf8", sep=",", index_col="carriers" + ) except FileNotFoundError: path = os.getcwd() - filename = 'investment_costs.csv' - invest = pd.DataFrame.from_csv(path + '/data/'+filename) - - if scn_name in ['SH Status Quo', 'Status Quo']: - invest_scn = 'Status Quo' - - if scn_name in ['SH NEP 2035', 'NEP 2035']: - invest_scn = 'NEP 2035' - - if scn_name in ['SH eGo 100', 'eGo 100']: - invest_scn = 'eGo 100' - - gen_invest = pd.concat([invest[invest_scn], - etg.generators.groupby('carrier')['p_nom'].sum()], - axis=1, join='inner') - - gen_invest = pd.concat([invest[invest_scn], etg.generators.groupby( - 'carrier') - ['p_nom'].sum()], axis=1, join='inner') - gen_invest['carrier_costs'] = gen_invest[invest_scn] * \ - gen_invest['p_nom'] * 1000 # in MW + filename = "investment_costs.csv" + invest = pd.DataFrame.from_csv(path + "/data/" + filename) + + if scn_name in ["SH Status Quo", "Status Quo"]: + invest_scn = "Status Quo" + + if scn_name in ["SH NEP 2035", "NEP 2035"]: + invest_scn = "NEP 2035" + + if scn_name in ["SH eGo 100", "eGo 100"]: + invest_scn = "eGo 100" + + gen_invest = pd.concat( + [invest[invest_scn], etg.generators.groupby("carrier")["p_nom"].sum()], + axis=1, + join="inner", + ) + + gen_invest = pd.concat( + [invest[invest_scn], etg.generators.groupby("carrier")["p_nom"].sum()], + axis=1, + join="inner", + ) + gen_invest["carrier_costs"] = ( + gen_invest[invest_scn] * gen_invest["p_nom"] * 1000 + ) # in MW return gen_invest diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index b8d512bc..a06ec3fc 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -21,54 +21,53 @@ This file is part of the the eGo toolbox. It contains the class definition for multiple eDisGo networks. """ -__copyright__ = ("Flensburg University of Applied Sciences, " - "Europa-Universität Flensburg, " - "Centre for Sustainable Energy Systems") +__copyright__ = ( + "Flensburg University of Applied Sciences, " + "Europa-Universität Flensburg, " + "Centre for Sustainable Energy Systems" +) __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolf_bunke, maltesc" -# Import -from traceback import TracebackException +import csv +import json +import logging import os import pickle -import logging import traceback -import pypsa -import csv -import dill -import pandas as pd + +from datetime import datetime +from datetime import timedelta as td from time import localtime, sleep, strftime -from datetime import datetime, timedelta as td -import json -from sqlalchemy.orm import sessionmaker -from sqlalchemy.orm import scoped_session + +# Import +from traceback import TracebackException + +import dill import multiprocess as mp2 +import pandas as pd +import pypsa -if not 'READTHEDOCS' in os.environ: +from sqlalchemy.orm import scoped_session, sessionmaker - from egoio.db_tables import model_draft, grid - from egoio.tools import db +if not "READTHEDOCS" in os.environ: - from edisgo.grid.network import Results, TimeSeriesControl from edisgo.grid import tools + from edisgo.grid.network import EDisGo, Results, TimeSeriesControl from edisgo.tools.plots import mv_grid_topology - from edisgo.grid.network import EDisGo + from egoio.db_tables import grid, model_draft + from egoio.tools import db - from ego.tools.specs import ( - get_etragospecs_direct - ) - from ego.tools.mv_cluster import ( - analyze_attributes, - cluster_mv_grids) - from ego.tools.economics import ( - edisgo_grid_investment) + from ego.tools.economics import edisgo_grid_investment + from ego.tools.mv_cluster import analyze_attributes, cluster_mv_grids + from ego.tools.specs import get_etragospecs_direct # Logging logger = logging.getLogger(__name__) pickle.DEFAULT_PROTOCOL = 4 -dill.settings['protocol'] = 4 +dill.settings["protocol"] = 4 class EDisGoNetworks: @@ -106,10 +105,7 @@ def __init__(self, json_file, etrago_network): if self._csv_import: self._laod_edisgo_results() self._successfull_grids = self._successfull_grids() - self._grid_investment_costs = edisgo_grid_investment( - self, - self._json_file - ) + self._grid_investment_costs = edisgo_grid_investment(self, self._json_file) else: # Only clustering results @@ -130,8 +126,7 @@ def __init__(self, json_file, etrago_network): self._successfull_grids = self._successfull_grids() self._grid_investment_costs = edisgo_grid_investment( - self, - self._json_file + self, self._json_file ) @property @@ -249,14 +244,16 @@ def plot_storage_integration(self, mv_grid_id, **kwargs): mv_grid_topology( self._edisgo_grids[mv_grid_id].network.pypsa, self._edisgo_grids[mv_grid_id].network.config, - node_color=kwargs.get('storage_integration', None), - filename=kwargs.get('filename', None), - grid_district_geom=kwargs.get('grid_district_geom', True), - background_map=kwargs.get('background_map', True), - xlim=kwargs.get('xlim', None), ylim=kwargs.get('ylim', None), - title=kwargs.get('title', '')) - - def plot_grid_expansion_costs(self, mv_grid_id, ** kwargs): + node_color=kwargs.get("storage_integration", None), + filename=kwargs.get("filename", None), + grid_district_geom=kwargs.get("grid_district_geom", True), + background_map=kwargs.get("background_map", True), + xlim=kwargs.get("xlim", None), + ylim=kwargs.get("ylim", None), + title=kwargs.get("title", ""), + ) + + def plot_grid_expansion_costs(self, mv_grid_id, **kwargs): """ Plots costs per MV line. For more information see :func:`edisgo.tools.plots.mv_grid_topology`. @@ -265,18 +262,23 @@ def plot_grid_expansion_costs(self, mv_grid_id, ** kwargs): mv_grid_topology( self._edisgo_grids[mv_grid_id].network.pypsa, self._edisgo_grids[mv_grid_id].network.config, - line_color='expansion_costs', + line_color="expansion_costs", grid_expansion_costs=( - self._edisgo_grids[mv_grid_id].network. - results.grid_expansion_costs.rename(columns={ - "overnight_costs": "total_costs"})), - filename=kwargs.get('filename', None), - grid_district_geom=kwargs.get('grid_district_geom', True), - background_map=kwargs.get('background_map', True), - limits_cb_lines=kwargs.get('limits_cb_lines', None), - xlim=kwargs.get('xlim', None), ylim=kwargs.get('ylim', None), - lines_cmap=kwargs.get('lines_cmap', 'inferno_r'), - title=kwargs.get('title', '')) + self._edisgo_grids[ + mv_grid_id + ].network.results.grid_expansion_costs.rename( + columns={"overnight_costs": "total_costs"} + ) + ), + filename=kwargs.get("filename", None), + grid_district_geom=kwargs.get("grid_district_geom", True), + background_map=kwargs.get("background_map", True), + limits_cb_lines=kwargs.get("limits_cb_lines", None), + xlim=kwargs.get("xlim", None), + ylim=kwargs.get("ylim", None), + lines_cmap=kwargs.get("lines_cmap", "inferno_r"), + title=kwargs.get("title", ""), + ) def plot_line_loading(self, mv_grid_id, **kwargs): """ @@ -288,20 +290,22 @@ def plot_line_loading(self, mv_grid_id, **kwargs): mv_grid_topology( self._edisgo_grids[mv_grid_id].network.pypsa, self._edisgo_grids[mv_grid_id].network.config, - timestep=kwargs.get('timestep', None), - line_color='loading', - node_color=kwargs.get('node_color', None), + timestep=kwargs.get("timestep", None), + line_color="loading", + node_color=kwargs.get("node_color", None), line_load=self._edisgo_grids[mv_grid_id].network.results.s_res(), - filename=kwargs.get('filename', None), - arrows=kwargs.get('arrows', None), - grid_district_geom=kwargs.get('grid_district_geom', True), - background_map=kwargs.get('background_map', True), + filename=kwargs.get("filename", None), + arrows=kwargs.get("arrows", None), + grid_district_geom=kwargs.get("grid_district_geom", True), + background_map=kwargs.get("background_map", True), voltage=None, # change API - limits_cb_lines=kwargs.get('limits_cb_lines', None), - limits_cb_nodes=kwargs.get('limits_cb_nodes', None), - xlim=kwargs.get('xlim', None), ylim=kwargs.get('ylim', None), - lines_cmap=kwargs.get('lines_cmap', 'inferno_r'), - title=kwargs.get('title', '')) + limits_cb_lines=kwargs.get("limits_cb_lines", None), + limits_cb_nodes=kwargs.get("limits_cb_nodes", None), + xlim=kwargs.get("xlim", None), + ylim=kwargs.get("ylim", None), + lines_cmap=kwargs.get("lines_cmap", "inferno_r"), + title=kwargs.get("title", ""), + ) def plot_mv_grid_topology(self, mv_grid_id, **kwargs): """ @@ -309,45 +313,43 @@ def plot_mv_grid_topology(self, mv_grid_id, **kwargs): For more information see :func:`edisgo.tools.plots.mv_grid_topology`. """ - mv_grid_topology(self._edisgo_grids[mv_grid_id].network.pypsa, - self._edisgo_grids[mv_grid_id].network.config, - filename=kwargs.get('filename', None), - grid_district_geom=kwargs.get( - 'grid_district_geom', True), - background_map=kwargs.get('background_map', True), - xlim=kwargs.get('xlim', None), - ylim=kwargs.get('ylim', None), - title=kwargs.get('title', '')) + mv_grid_topology( + self._edisgo_grids[mv_grid_id].network.pypsa, + self._edisgo_grids[mv_grid_id].network.config, + filename=kwargs.get("filename", None), + grid_district_geom=kwargs.get("grid_district_geom", True), + background_map=kwargs.get("background_map", True), + xlim=kwargs.get("xlim", None), + ylim=kwargs.get("ylim", None), + title=kwargs.get("title", ""), + ) def _init_status(self): """ Creates a Status file where all eDisGo statuses are tracked... """ - self._status_dir = 'status' + self._status_dir = "status" if not os.path.exists(self._status_dir): os.makedirs(self._status_dir) - self._status_file = 'eGo_' + strftime("%Y-%m-%d_%H%M%S", localtime()) + self._status_file = "eGo_" + strftime("%Y-%m-%d_%H%M%S", localtime()) status = self._grid_choice.copy() - status = status.set_index('the_selected_network_id') - status.index.names = ['MV grid'] + status = status.set_index("the_selected_network_id") + status.index.names = ["MV grid"] - tot_reprs = self._grid_choice['no_of_points_per_cluster'].sum() + tot_reprs = self._grid_choice["no_of_points_per_cluster"].sum() - status['cluster_perc'] = status['no_of_points_per_cluster'] / tot_reprs + status["cluster_perc"] = status["no_of_points_per_cluster"] / tot_reprs - status['start_time'] = 'Not started yet' - status['end_time'] = 'Not finished yet' + status["start_time"] = "Not started yet" + status["end_time"] = "Not finished yet" status.drop( - ['no_of_points_per_cluster', 'represented_grids'], - axis=1, - inplace=True) + ["no_of_points_per_cluster", "represented_grids"], axis=1, inplace=True + ) - self._status_path = os.path.join( - self._status_dir, - self._status_file + '.csv') + self._status_path = os.path.join(self._status_dir, self._status_file + ".csv") status.to_csv(self._status_path) @@ -355,26 +357,22 @@ def _status_update(self, mv_grid_id, time, message=None, show=True): """ Updtaed eDisGo's status files """ - status = pd.read_csv( - self._status_path, - index_col=0) + status = pd.read_csv(self._status_path, index_col=0) - status['start_time'] = status['start_time'].astype(str) - status['end_time'] = status['end_time'].astype(str) + status["start_time"] = status["start_time"].astype(str) + status["end_time"] = status["end_time"].astype(str) if message: now = message else: now = strftime("%Y-%m-%d_%H:%M", localtime()) - if time == 'start': - status.at[mv_grid_id, 'start_time'] = now - elif time == 'end': - status.at[mv_grid_id, 'end_time'] = now + if time == "start": + status.at[mv_grid_id, "start_time"] = now + elif time == "end": + status.at[mv_grid_id, "end_time"] = now if show: - logger.info("\n\neDisGo Status: \n\n" - + status.to_string() - + "\n\n") + logger.info("\n\neDisGo Status: \n\n" + status.to_string() + "\n\n") status.to_csv(self._status_path) @@ -384,139 +382,141 @@ def _update_edisgo_configs(self, edisgo_grid): settings. """ # Info and Warning handling - if not hasattr(self, '_suppress_log'): + if not hasattr(self, "_suppress_log"): self._suppress_log = False # Only in the first run warnings and # info get thrown # Database section ego_db = self._db_section - edisgo_db = edisgo_grid.network.config['db_connection']['section'] + edisgo_db = edisgo_grid.network.config["db_connection"]["section"] if not ego_db == edisgo_db: if not self._suppress_log: logger.warning( - ("eDisGo database configuration (db: '{}') " - + "will be overwritten with database configuration " - + "from eGo's scenario settings (db: '{}')").format( - edisgo_db, - ego_db)) - edisgo_grid.network.config['db_connection']['section'] = ego_db + ( + "eDisGo database configuration (db: '{}') " + + "will be overwritten with database configuration " + + "from eGo's scenario settings (db: '{}')" + ).format(edisgo_db, ego_db) + ) + edisgo_grid.network.config["db_connection"]["section"] = ego_db # Versioned ego_gridversion = self._grid_version if ego_gridversion == None: - ego_versioned = 'model_draft' + ego_versioned = "model_draft" if not self._suppress_log: - logger.info("eGo's grid_version == None is " - + "evaluated as data source: model_draft") + logger.info( + "eGo's grid_version == None is " + + "evaluated as data source: model_draft" + ) else: - ego_versioned = 'versioned' + ego_versioned = "versioned" if not self._suppress_log: - logger.info(("eGo's grid_version == '{}' is " - + "evaluated as data source: versioned").format( - ego_gridversion)) + logger.info( + ( + "eGo's grid_version == '{}' is " + + "evaluated as data source: versioned" + ).format(ego_gridversion) + ) - edisgo_versioned = edisgo_grid.network.config[ - 'data_source']['oedb_data_source'] + edisgo_versioned = edisgo_grid.network.config["data_source"]["oedb_data_source"] if not ego_versioned == edisgo_versioned: if not self._suppress_log: logger.warning( - ("eDisGo data source configuration ('{}') " - + "will be overwritten with data source config. from " - + "eGo's scenario settings (data source: '{}')" - ).format( - edisgo_versioned, - ego_versioned)) - edisgo_grid.network.config[ - 'data_source']['oedb_data_source'] = ego_versioned + ( + "eDisGo data source configuration ('{}') " + + "will be overwritten with data source config. from " + + "eGo's scenario settings (data source: '{}')" + ).format(edisgo_versioned, ego_versioned) + ) + edisgo_grid.network.config["data_source"][ + "oedb_data_source" + ] = ego_versioned # Gridversion ego_gridversion = self._grid_version - edisgo_gridversion = edisgo_grid.network.config[ - 'versioned']['version'] + edisgo_gridversion = edisgo_grid.network.config["versioned"]["version"] if not ego_gridversion == edisgo_gridversion: if not self._suppress_log: logger.warning( - ("eDisGo version configuration (version: '{}') " - + "will be overwritten with version configuration " - + "from eGo's scenario settings (version: '{}')" - ).format( - edisgo_gridversion, - ego_gridversion)) - edisgo_grid.network.config[ - 'versioned']['version'] = ego_gridversion + ( + "eDisGo version configuration (version: '{}') " + + "will be overwritten with version configuration " + + "from eGo's scenario settings (version: '{}')" + ).format(edisgo_gridversion, ego_gridversion) + ) + edisgo_grid.network.config["versioned"]["version"] = ego_gridversion self._suppress_log = True def _set_scenario_settings(self): - self._csv_import = self._json_file['eGo']['csv_import_eDisGo'] + self._csv_import = self._json_file["eGo"]["csv_import_eDisGo"] # eTraGo args - self._etrago_args = self._json_file['eTraGo'] - self._scn_name = self._etrago_args['scn_name'] - self._ext_storage = ( - 'storage' in self._etrago_args['extendable'] - ) + self._etrago_args = self._json_file["eTraGo"] + self._scn_name = self._etrago_args["scn_name"] + self._ext_storage = "storage" in self._etrago_args["extendable"] if self._ext_storage: logger.info("eTraGo Dataset used extendable storage") - self._pf_post_lopf = self._etrago_args['pf_post_lopf'] + self._pf_post_lopf = self._etrago_args["pf_post_lopf"] # eDisGo args import if self._csv_import: # raise NotImplementedError - with open(os.path.join( - self._csv_import, - 'edisgo_args.json')) as f: + with open(os.path.join(self._csv_import, "edisgo_args.json")) as f: edisgo_args = json.load(f) - self._json_file['eDisGo'] = edisgo_args - logger.info("All eDisGo settings are taken from CSV folder" - + "(scenario settings are ignored)") + self._json_file["eDisGo"] = edisgo_args + logger.info( + "All eDisGo settings are taken from CSV folder" + + "(scenario settings are ignored)" + ) # This overwrites the original object... # Imported or directly from the Settings # eDisGo section of the settings - self._edisgo_args = self._json_file['eDisGo'] + self._edisgo_args = self._json_file["eDisGo"] # Reading all eDisGo settings # TODO: Integrate into a for-loop - self._db_section = self._edisgo_args['db'] - self._grid_version = self._edisgo_args['gridversion'] - self._timesteps_pfa = self._edisgo_args['timesteps_pfa'] - self._solver = self._edisgo_args['solver'] + self._db_section = self._edisgo_args["db"] + self._grid_version = self._edisgo_args["gridversion"] + self._timesteps_pfa = self._edisgo_args["timesteps_pfa"] + self._solver = self._edisgo_args["solver"] self._curtailment_voltage_threshold = self._edisgo_args[ - 'curtailment_voltage_threshold'] - self._ding0_files = self._edisgo_args['ding0_files'] - self._choice_mode = self._edisgo_args['choice_mode'] - self._parallelization = self._edisgo_args['parallelization'] - self._initial_reinforcement = self._edisgo_args[ - 'initial_reinforcement'] - self._storage_distribution = self._edisgo_args['storage_distribution'] - self._apply_curtailment = self._edisgo_args['apply_curtailment'] - self._cluster_attributes = self._edisgo_args['cluster_attributes'] - self._only_cluster = self._edisgo_args['only_cluster'] - self._max_workers = self._edisgo_args['max_workers'] - self._max_cos_phi_renewable = self._edisgo_args[ - 'max_cos_phi_renewable'] - self._results = self._edisgo_args['results'] - self._max_calc_time = self._edisgo_args['max_calc_time'] + "curtailment_voltage_threshold" + ] + self._ding0_files = self._edisgo_args["ding0_files"] + self._choice_mode = self._edisgo_args["choice_mode"] + self._parallelization = self._edisgo_args["parallelization"] + self._initial_reinforcement = self._edisgo_args["initial_reinforcement"] + self._storage_distribution = self._edisgo_args["storage_distribution"] + self._apply_curtailment = self._edisgo_args["apply_curtailment"] + self._cluster_attributes = self._edisgo_args["cluster_attributes"] + self._only_cluster = self._edisgo_args["only_cluster"] + self._max_workers = self._edisgo_args["max_workers"] + self._max_cos_phi_renewable = self._edisgo_args["max_cos_phi_renewable"] + self._results = self._edisgo_args["results"] + self._max_calc_time = self._edisgo_args["max_calc_time"] # Some basic checks if (self._storage_distribution is True) & (self._ext_storage is False): - logger.warning("Storage distribution (MV grids) is active, " - + "but eTraGo dataset has no extendable storages") + logger.warning( + "Storage distribution (MV grids) is active, " + + "but eTraGo dataset has no extendable storages" + ) if not self._initial_reinforcement: raise NotImplementedError( "Skipping the initial reinforcement is not yet implemented" ) if self._only_cluster: - logger.warning( - "\n\nThis eDisGo run only returns cluster results\n\n") + logger.warning("\n\nThis eDisGo run only returns cluster results\n\n") # Versioning if self._grid_version is not None: @@ -527,12 +527,12 @@ def _set_scenario_settings(self): def _edisgo_scenario_translation(self): # Scenario translation - if self._scn_name == 'Status Quo': + if self._scn_name == "Status Quo": self._generator_scn = None - elif self._scn_name == 'NEP 2035': - self._generator_scn = 'nep2035' - elif self._scn_name == 'eGo 100': - self._generator_scn = 'ego100' + elif self._scn_name == "NEP 2035": + self._generator_scn = "nep2035" + elif self._scn_name == "eGo 100": + self._generator_scn = "ego100" def _successfull_grids(self): """ @@ -544,15 +544,15 @@ def _successfull_grids(self): for key, value in self._edisgo_grids.items(): weight = self._grid_choice.loc[ - self._grid_choice['the_selected_network_id'] == key - ]['no_of_points_per_cluster'].values[0] + self._grid_choice["the_selected_network_id"] == key + ]["no_of_points_per_cluster"].values[0] total += weight - if hasattr(value, 'network'): + if hasattr(value, "network"): success += weight else: fail += weight - return success/total + return success / total def _analyze_cluster_attributes(self): """ @@ -563,9 +563,7 @@ def _analyze_cluster_attributes(self): """ analyze_attributes(self._ding0_files) - def _cluster_mv_grids( - self, - no_grids): + def _cluster_mv_grids(self, no_grids): """ Clusters the MV grids based on the attributes, for a given number of MV grids @@ -585,62 +583,56 @@ def _cluster_mv_grids( # TODO: This first dataframe contains the standard attributes... # ...Create an Interface in order to use attributes more flexibly. # Make this function more generic. - attributes_path = self._ding0_files + '/attributes.csv' + attributes_path = self._ding0_files + "/attributes.csv" if not os.path.isfile(attributes_path): - logger.info('Attributes file is missing') - logger.info('Attributes will be calculated') + logger.info("Attributes file is missing") + logger.info("Attributes will be calculated") self._analyze_cluster_attributes() - df = pd.read_csv(self._ding0_files + '/attributes.csv') - df = df.set_index('id') - df.drop(['Unnamed: 0'], inplace=True, axis=1) + df = pd.read_csv(self._ding0_files + "/attributes.csv") + df = df.set_index("id") + df.drop(["Unnamed: 0"], inplace=True, axis=1) df.rename( columns={ "Solar_cumulative_capacity": "solar_cap", "Wind_cumulative_capacity": "wind_cap", - "The_Farthest_node": "farthest_node"}, - inplace=True) + "The_Farthest_node": "farthest_node", + }, + inplace=True, + ) - if 'extended_storage' in self._cluster_attributes: + if "extended_storage" in self._cluster_attributes: if self._ext_storage: storages = self._identify_extended_storages() - if not (storages.max().values[0] == 0.): + if not (storages.max().values[0] == 0.0): df = pd.concat([df, storages], axis=1) df.rename( - columns={"storage_p_nom": "extended_storage"}, - inplace=True) + columns={"storage_p_nom": "extended_storage"}, inplace=True + ) else: - logger.warning('Extended storages all 0. \ + logger.warning( + "Extended storages all 0. \ Therefore, extended storages \ - are excluded from clustering') + are excluded from clustering" + ) - found_atts = [ - i for i in self._cluster_attributes if i in df.columns - ] - missing_atts = [ - i for i in self._cluster_attributes if i not in df.columns - ] + found_atts = [i for i in self._cluster_attributes if i in df.columns] + missing_atts = [i for i in self._cluster_attributes if i not in df.columns] - logger.info( - 'Available attributes are: {}'.format(df.columns.tolist()) - ) - logger.info( - 'Chosen/found attributes are: {}'.format(found_atts) - ) + logger.info("Available attributes are: {}".format(df.columns.tolist())) + logger.info("Chosen/found attributes are: {}".format(found_atts)) if len(missing_atts) > 0: - logger.warning( - 'Missing attributes: {}'.format(missing_atts) - ) - if 'extended_storage' in missing_atts: - logger.info('Hint: eTraGo dataset must contain ' - 'extendable storage in order to include ' - 'storage extension in MV grid clustering.') + logger.warning("Missing attributes: {}".format(missing_atts)) + if "extended_storage" in missing_atts: + logger.info( + "Hint: eTraGo dataset must contain " + "extendable storage in order to include " + "storage extension in MV grid clustering." + ) - return cluster_mv_grids( - no_grids, - cluster_base=df) + return cluster_mv_grids(no_grids, cluster_base=df) def _identify_extended_storages(self): @@ -651,34 +643,28 @@ def _identify_extended_storages(self): all_mv_grids = self._check_available_mv_grids() - storages = pd.DataFrame( - index=all_mv_grids, - columns=['storage_p_nom']) + storages = pd.DataFrame(index=all_mv_grids, columns=["storage_p_nom"]) - logger.info('Identifying extended storage') + logger.info("Identifying extended storage") for mv_grid in all_mv_grids: bus_id = self._get_bus_id_from_mv_grid(session, mv_grid) min_extended = 0.3 stor_p_nom = self._etrago_network.storage_units.loc[ - (self._etrago_network.storage_units['bus'] == str(bus_id)) - & (self._etrago_network.storage_units[ - 'p_nom_extendable' - ] == True) - & (self._etrago_network.storage_units[ - 'p_nom_opt' - ] > min_extended) - & (self._etrago_network.storage_units['max_hours'] <= 20.) - ]['p_nom_opt'] + (self._etrago_network.storage_units["bus"] == str(bus_id)) + & (self._etrago_network.storage_units["p_nom_extendable"] == True) + & (self._etrago_network.storage_units["p_nom_opt"] > min_extended) + & (self._etrago_network.storage_units["max_hours"] <= 20.0) + ]["p_nom_opt"] if len(stor_p_nom) == 1: stor_p_nom = stor_p_nom.values[0] elif len(stor_p_nom) == 0: - stor_p_nom = 0. + stor_p_nom = 0.0 else: raise IndexError - storages.at[mv_grid, 'storage_p_nom'] = stor_p_nom + storages.at[mv_grid, "storage_p_nom"] = stor_p_nom Session.remove() @@ -696,11 +682,10 @@ def _check_available_mv_grids(self): """ mv_grids = [] for file in os.listdir(self._ding0_files): - if file.endswith('.pkl'): + if file.endswith(".pkl"): mv_grids.append( - int(file.replace( - 'ding0_grids__', '' - ).replace('.pkl', ''))) + int(file.replace("ding0_grids__", "").replace(".pkl", "")) + ) return mv_grids @@ -712,57 +697,47 @@ def _set_grid_choice(self): choice_df = pd.DataFrame( columns=[ - 'no_of_points_per_cluster', - 'the_selected_network_id', - 'represented_grids']) + "no_of_points_per_cluster", + "the_selected_network_id", + "represented_grids", + ] + ) - if self._choice_mode == 'cluster': - no_grids = self._edisgo_args['no_grids'] - logger.info('Clustering to {} MV grids'.format(no_grids)) + if self._choice_mode == "cluster": + no_grids = self._edisgo_args["no_grids"] + logger.info("Clustering to {} MV grids".format(no_grids)) cluster_df = self._cluster_mv_grids(no_grids) - choice_df[ - 'the_selected_network_id' - ] = cluster_df['the_selected_network_id'] - choice_df[ - 'no_of_points_per_cluster' - ] = cluster_df['no_of_points_per_cluster'] - choice_df[ - 'represented_grids' - ] = cluster_df['represented_grids'] - - elif self._choice_mode == 'manual': - man_grids = self._edisgo_args['manual_grids'] - - choice_df['the_selected_network_id'] = man_grids - choice_df['no_of_points_per_cluster'] = 1 - choice_df['represented_grids'] = [ - [mv_grid_id] - for mv_grid_id - in choice_df['the_selected_network_id']] + choice_df["the_selected_network_id"] = cluster_df["the_selected_network_id"] + choice_df["no_of_points_per_cluster"] = cluster_df[ + "no_of_points_per_cluster" + ] + choice_df["represented_grids"] = cluster_df["represented_grids"] - logger.info( - 'Calculating manually chosen MV grids {}'.format(man_grids) - ) + elif self._choice_mode == "manual": + man_grids = self._edisgo_args["manual_grids"] + + choice_df["the_selected_network_id"] = man_grids + choice_df["no_of_points_per_cluster"] = 1 + choice_df["represented_grids"] = [ + [mv_grid_id] for mv_grid_id in choice_df["the_selected_network_id"] + ] - elif self._choice_mode == 'all': + logger.info("Calculating manually chosen MV grids {}".format(man_grids)) + + elif self._choice_mode == "all": mv_grids = self._check_available_mv_grids() - choice_df['the_selected_network_id'] = mv_grids - choice_df['no_of_points_per_cluster'] = 1 - choice_df['represented_grids'] = [ - [mv_grid_id] - for mv_grid_id - in choice_df['the_selected_network_id']] + choice_df["the_selected_network_id"] = mv_grids + choice_df["no_of_points_per_cluster"] = 1 + choice_df["represented_grids"] = [ + [mv_grid_id] for mv_grid_id in choice_df["the_selected_network_id"] + ] no_grids = len(mv_grids) - logger.info( - 'Calculating all available {} MV grids'.format(no_grids) - ) + logger.info("Calculating all available {} MV grids".format(no_grids)) - choice_df = choice_df.sort_values( - 'no_of_points_per_cluster', - ascending=False) + choice_df = choice_df.sort_values("no_of_points_per_cluster", ascending=False) self._grid_choice = choice_df @@ -777,15 +752,14 @@ def _run_edisgo_pool(self): os.makedirs(self._results) if parallelization is True: - logger.info('Run eDisGo parallel') - mv_grids = self._grid_choice['the_selected_network_id'].tolist() + logger.info("Run eDisGo parallel") + mv_grids = self._grid_choice["the_selected_network_id"].tolist() no_cpu = mp2.cpu_count() if no_cpu > self._max_workers: no_cpu = self._max_workers logger.info( - 'Number of workers limited to {} by user'.format( - self._max_workers - )) + "Number of workers limited to {} by user".format(self._max_workers) + ) self._edisgo_grids = set(mv_grids) self._edisgo_grids = parallelizer( @@ -793,46 +767,37 @@ def _run_edisgo_pool(self): lambda *xs: xs[1]._run_edisgo(xs[0]), (self,), self._max_calc_time, - workers=no_cpu) + workers=no_cpu, + ) for g in mv_grids: if not g in self._edisgo_grids: - self._edisgo_grids[g] = 'Timeout' + self._edisgo_grids[g] = "Timeout" else: - logger.info('Run eDisGo sequencial') + logger.info("Run eDisGo sequencial") no_grids = len(self._grid_choice) count = 0 for idx, row in self._grid_choice.iterrows(): - prog = '%.1f' % (count / no_grids * 100) - logger.info( - '{} % Calculated by eDisGo'.format(prog) - ) + prog = "%.1f" % (count / no_grids * 100) + logger.info("{} % Calculated by eDisGo".format(prog)) - mv_grid_id = int(row['the_selected_network_id']) - logger.info( - 'MV grid {}'.format(mv_grid_id) - ) + mv_grid_id = int(row["the_selected_network_id"]) + logger.info("MV grid {}".format(mv_grid_id)) try: edisgo_grid = self._run_edisgo(mv_grid_id) - self._edisgo_grids[ - mv_grid_id - ] = edisgo_grid + self._edisgo_grids[mv_grid_id] = edisgo_grid except Exception as e: self._edisgo_grids[mv_grid_id] = e - logger.exception( - 'MV grid {} failed: \n'.format(mv_grid_id) - ) + logger.exception("MV grid {} failed: \n".format(mv_grid_id)) count += 1 - self._csv_import = self._json_file['eDisGo']['results'] + self._csv_import = self._json_file["eDisGo"]["results"] self._save_edisgo_results() self._laod_edisgo_results() self._run_finished = True - def _run_edisgo( - self, - mv_grid_id): + def _run_edisgo(self, mv_grid_id): """ Performs a single eDisGo run @@ -846,13 +811,12 @@ def _run_edisgo( :class:`edisgo.grid.network.EDisGo` Returns the complete eDisGo container, also including results """ - self._status_update(mv_grid_id, 'start', show=False) + self._status_update(mv_grid_id, "start", show=False) storage_integration = self._storage_distribution apply_curtailment = self._apply_curtailment - logger.info( - 'MV grid {}: Calculating interface values'.format(mv_grid_id)) + logger.info("MV grid {}: Calculating interface values".format(mv_grid_id)) conn = db.connection(section=self._db_section) session_factory = sessionmaker(bind=conn) @@ -870,72 +834,78 @@ def _run_edisgo( self._scn_name, self._grid_version, self._pf_post_lopf, - self._max_cos_phi_renewable) + self._max_cos_phi_renewable, + ) Session.remove() # Get ding0 (MV grid) form folder - ding0_filepath = ( - self._ding0_files - + '/ding0_grids__' - + str(mv_grid_id) - + '.pkl') + ding0_filepath = self._ding0_files + "/ding0_grids__" + str(mv_grid_id) + ".pkl" if not os.path.isfile(ding0_filepath): - msg = 'No MV grid file for MV grid {}'.format(mv_grid_id) + msg = "No MV grid file for MV grid {}".format(mv_grid_id) logger.error(msg) raise Exception(msg) # Initalize eDisGo with this MV grid logger.info(("MV grid {}: Initialize MV grid").format(mv_grid_id)) - edisgo_grid = EDisGo(ding0_grid=ding0_filepath, - worst_case_analysis='worst-case') - - logger.info(("MV grid {}: Changing eDisGo's voltage configurations " - + "for initial reinforcement").format(mv_grid_id)) - - edisgo_grid.network.config[ - 'grid_expansion_allowed_voltage_deviations'] = { - 'hv_mv_trafo_offset': 0.04, - 'hv_mv_trafo_control_deviation': 0.0, - 'mv_load_case_max_v_deviation': 0.055, - 'mv_feedin_case_max_v_deviation': 0.02, - 'lv_load_case_max_v_deviation': 0.065, - 'lv_feedin_case_max_v_deviation': 0.03, - 'mv_lv_station_load_case_max_v_deviation': 0.02, - 'mv_lv_station_feedin_case_max_v_deviation': 0.01 + edisgo_grid = EDisGo( + ding0_grid=ding0_filepath, worst_case_analysis="worst-case" + ) + + logger.info( + ( + "MV grid {}: Changing eDisGo's voltage configurations " + + "for initial reinforcement" + ).format(mv_grid_id) + ) + + edisgo_grid.network.config["grid_expansion_allowed_voltage_deviations"] = { + "hv_mv_trafo_offset": 0.04, + "hv_mv_trafo_control_deviation": 0.0, + "mv_load_case_max_v_deviation": 0.055, + "mv_feedin_case_max_v_deviation": 0.02, + "lv_load_case_max_v_deviation": 0.065, + "lv_feedin_case_max_v_deviation": 0.03, + "mv_lv_station_load_case_max_v_deviation": 0.02, + "mv_lv_station_feedin_case_max_v_deviation": 0.01, } # Inital grid reinforcements - logger.info(("MV grid {}: Initial MV grid reinforcement " - + "(worst-case anaylsis)").format(mv_grid_id)) + logger.info( + ( + "MV grid {}: Initial MV grid reinforcement " + "(worst-case anaylsis)" + ).format(mv_grid_id) + ) edisgo_grid.reinforce() # Get costs for initial reinforcement # TODO: Implement a separate cost function - costs_grouped = \ - edisgo_grid.network.results.grid_expansion_costs.groupby( - ['type']).sum() + costs_grouped = edisgo_grid.network.results.grid_expansion_costs.groupby( + ["type"] + ).sum() costs = pd.DataFrame( costs_grouped.values, columns=costs_grouped.columns, - index=[[edisgo_grid.network.id] * len(costs_grouped), - costs_grouped.index]).reset_index() - costs.rename(columns={'level_0': 'grid'}, inplace=True) + index=[[edisgo_grid.network.id] * len(costs_grouped), costs_grouped.index], + ).reset_index() + costs.rename(columns={"level_0": "grid"}, inplace=True) costs_before = costs - total_costs_before_EUR = costs_before['total_costs'].sum() * 1000 + total_costs_before_EUR = costs_before["total_costs"].sum() * 1000 + logger.info( + ("MV grid {}: Costs for initial " + "reinforcement: EUR {}").format( + mv_grid_id, "{:,.2f}".format(total_costs_before_EUR) + ) + ) + logger.info( - ("MV grid {}: Costs for initial " - + "reinforcement: EUR {}").format( - mv_grid_id, - "{0:,.2f}".format(total_costs_before_EUR))) - - logger.info(( - "MV grid {}: Resetting grid after initial reinforcement" - ).format(mv_grid_id)) + ("MV grid {}: Resetting grid after initial reinforcement").format( + mv_grid_id + ) + ) edisgo_grid.network.results = Results(edisgo_grid.network) # Reload the (original) eDisGo configs edisgo_grid.network.config = None @@ -944,116 +914,108 @@ def _run_edisgo( logger.info("MV grid {}: eTraGo feed-in case".format(mv_grid_id)) # Update eDisGo settings (from config files) with scenario settings - logger.info("MV grid {}: Updating eDisgo configuration".format( - mv_grid_id)) + logger.info("MV grid {}: Updating eDisgo configuration".format(mv_grid_id)) # Update configs with eGo's scenario settings self._update_edisgo_configs(edisgo_grid) # Generator import for NEP 2035 and eGo 100 scenarios if self._generator_scn: - logger.info( - 'Importing generators for scenario {}'.format( - self._scn_name) - ) - edisgo_grid.import_generators( - generator_scenario=self._generator_scn) + logger.info("Importing generators for scenario {}".format(self._scn_name)) + edisgo_grid.import_generators(generator_scenario=self._generator_scn) else: - logger.info( - 'No generators imported for scenario {}'.format( - self._scn_name) - ) + logger.info("No generators imported for scenario {}".format(self._scn_name)) edisgo_grid.network.pypsa = None # Time Series from eTraGo - logger.info('Updating eDisGo timeseries with eTraGo values') + logger.info("Updating eDisGo timeseries with eTraGo values") if self._pf_post_lopf: - logger.info('(Including reactive power)') + logger.info("(Including reactive power)") edisgo_grid.network.timeseries = TimeSeriesControl( network=edisgo_grid.network, - timeseries_generation_fluctuating=specs['ren_potential'], - timeseries_generation_dispatchable=specs['conv_dispatch'], - timeseries_generation_reactive_power=specs['reactive_power'], - timeseries_load='demandlib', - timeindex=specs['conv_dispatch'].index).timeseries + timeseries_generation_fluctuating=specs["ren_potential"], + timeseries_generation_dispatchable=specs["conv_dispatch"], + timeseries_generation_reactive_power=specs["reactive_power"], + timeseries_load="demandlib", + timeindex=specs["conv_dispatch"].index, + ).timeseries else: - logger.info('(Only active power)') + logger.info("(Only active power)") edisgo_grid.network.timeseries = TimeSeriesControl( network=edisgo_grid.network, - timeseries_generation_fluctuating=specs['ren_potential'], - timeseries_generation_dispatchable=specs['conv_dispatch'], - timeseries_load='demandlib', - timeindex=specs['conv_dispatch'].index).timeseries + timeseries_generation_fluctuating=specs["ren_potential"], + timeseries_generation_dispatchable=specs["conv_dispatch"], + timeseries_load="demandlib", + timeindex=specs["conv_dispatch"].index, + ).timeseries # Curtailment if apply_curtailment: - logger.info('Including Curtailment') + logger.info("Including Curtailment") gens_df = tools.get_gen_info(edisgo_grid.network) - solar_wind_capacities = gens_df.groupby( - by=['type', 'weather_cell_id'] - )['nominal_capacity'].sum() + solar_wind_capacities = gens_df.groupby(by=["type", "weather_cell_id"])[ + "nominal_capacity" + ].sum() curt_cols = [ - i for i in specs['ren_curtailment'].columns + i + for i in specs["ren_curtailment"].columns if i in solar_wind_capacities.index ] if not curt_cols: - raise ImportError( - ("MV grid {}: Data doesn't match").format(mv_grid_id)) + raise ImportError(("MV grid {}: Data doesn't match").format(mv_grid_id)) - curt_abs = pd.DataFrame( - columns=pd.MultiIndex.from_tuples(curt_cols)) + curt_abs = pd.DataFrame(columns=pd.MultiIndex.from_tuples(curt_cols)) for col in curt_abs: curt_abs[col] = ( - specs['ren_curtailment'][col] - * solar_wind_capacities[col]) + specs["ren_curtailment"][col] * solar_wind_capacities[col] + ) edisgo_grid.curtail( curtailment_timeseries=curt_abs, - methodology='voltage-based', + methodology="voltage-based", solver=self._solver, - voltage_threshold=self._curtailment_voltage_threshold) + voltage_threshold=self._curtailment_voltage_threshold, + ) else: - logger.info('No curtailment applied') + logger.info("No curtailment applied") # Storage Integration costs_without_storage = None if storage_integration: if self._ext_storage: - if not specs['battery_p_series'] is None: - logger.info('Integrating storages in MV grid') + if not specs["battery_p_series"] is None: + logger.info("Integrating storages in MV grid") edisgo_grid.integrate_storage( - timeseries=specs['battery_p_series'], - position='distribute_storages_mv', - timeseries_reactive_power=specs[ - 'battery_q_series' - ]) # None if no pf_post_lopf + timeseries=specs["battery_p_series"], + position="distribute_storages_mv", + timeseries_reactive_power=specs["battery_q_series"], + ) # None if no pf_post_lopf costs_without_storage = ( edisgo_grid.network.results.storages_costs_reduction[ - 'grid_expansion_costs_initial'].values[0]) + "grid_expansion_costs_initial" + ].values[0] + ) else: - logger.info('No storage integration') + logger.info("No storage integration") logger.info("MV grid {}: eDisGo grid analysis".format(mv_grid_id)) edisgo_grid.reinforce(timesteps_pfa=self._timesteps_pfa) if costs_without_storage is not None: - costs_with_storage = ( - edisgo_grid.network.results.grid_expansion_costs[ - 'total_costs'].sum()) + costs_with_storage = edisgo_grid.network.results.grid_expansion_costs[ + "total_costs" + ].sum() if costs_with_storage >= costs_without_storage: - logger.warning( - "Storage did not benefit MV grid {}".format( - mv_grid_id)) - st = edisgo_grid.network.mv_grid.graph.nodes_by_attribute( - 'storage') + logger.warning("Storage did not benefit MV grid {}".format(mv_grid_id)) + st = edisgo_grid.network.mv_grid.graph.nodes_by_attribute("storage") for storage in st: tools.disconnect_storage(edisgo_grid.network, storage) - self._status_update(mv_grid_id, 'end') + self._status_update(mv_grid_id, "end") path = os.path.join(self._results, str(mv_grid_id)) edisgo_grid.network.results.save(path) @@ -1065,57 +1027,52 @@ def _save_edisgo_results(self): if not os.path.exists(self._results): os.makedirs(self._results) - with open( - os.path.join(self._results, 'edisgo_args.json'), - 'w') as fp: + with open(os.path.join(self._results, "edisgo_args.json"), "w") as fp: json.dump(self._edisgo_args, fp) - self._grid_choice.to_csv(self._results + '/grid_choice.csv') + self._grid_choice.to_csv(self._results + "/grid_choice.csv") def _laod_edisgo_results(self): # Load the grid choice form CSV self._grid_choice = pd.read_csv( - os.path.join(self._csv_import, 'grid_choice.csv'), - index_col=0) - self._grid_choice['represented_grids'] = self._grid_choice.apply( - lambda x: eval(x['represented_grids']), axis=1) + os.path.join(self._csv_import, "grid_choice.csv"), index_col=0 + ) + self._grid_choice["represented_grids"] = self._grid_choice.apply( + lambda x: eval(x["represented_grids"]), axis=1 + ) for idx, row in self._grid_choice.iterrows(): - mv_grid_id = int(row['the_selected_network_id']) + mv_grid_id = int(row["the_selected_network_id"]) try: # Grid expansion costs file_path = os.path.join( self._csv_import, str(mv_grid_id), - 'grid_expansion_results', - 'grid_expansion_costs.csv') + "grid_expansion_results", + "grid_expansion_costs.csv", + ) - grid_expansion_costs = pd.read_csv( - file_path, - index_col=0) + grid_expansion_costs = pd.read_csv(file_path, index_col=0) # powerflow results pf_path = os.path.join( self._csv_import, str(mv_grid_id), - 'powerflow_results', - 'apparent_powers.csv') + "powerflow_results", + "apparent_powers.csv", + ) - s_res = pd.read_csv( - pf_path, - index_col=0, - parse_dates=True) + s_res = pd.read_csv(pf_path, index_col=0, parse_dates=True) # Configs config_path = os.path.join( - self._csv_import, - str(mv_grid_id), - 'configs.csv') + self._csv_import, str(mv_grid_id), "configs.csv" + ) edisgo_config = {} - with open(config_path, 'r') as f: + with open(config_path, "r") as f: reader = csv.reader(f) for row in reader: a = iter(row[1:]) @@ -1123,9 +1080,8 @@ def _laod_edisgo_results(self): # PyPSA network pypsa_path = os.path.join( - self._csv_import, - str(mv_grid_id), - 'pypsa_network') + self._csv_import, str(mv_grid_id), "pypsa_network" + ) imported_pypsa = pypsa.Network() imported_pypsa.import_from_csv_folder(pypsa_path) @@ -1134,36 +1090,26 @@ def _laod_edisgo_results(self): storage_path = os.path.join( self._csv_import, str(mv_grid_id), - 'storage_integration_results', - 'storages.csv') + "storage_integration_results", + "storages.csv", + ) if os.path.exists(storage_path): - storages = pd.read_csv( - storage_path, - index_col=0) + storages = pd.read_csv(storage_path, index_col=0) else: - storages = pd.DataFrame( - columns=['nominal_power', 'voltage_level']) + storages = pd.DataFrame(columns=["nominal_power", "voltage_level"]) edisgo_grid = _EDisGoImported( - grid_expansion_costs, - s_res, - storages, - imported_pypsa, - edisgo_config) + grid_expansion_costs, s_res, storages, imported_pypsa, edisgo_config + ) - self._edisgo_grids[ - mv_grid_id - ] = edisgo_grid + self._edisgo_grids[mv_grid_id] = edisgo_grid logger.info("Imported MV grid {}".format(mv_grid_id)) except: - self._edisgo_grids[ - mv_grid_id - ] = "This grid failed to reimport" + self._edisgo_grids[mv_grid_id] = "This grid failed to reimport" - logger.warning( - "MV grid {} could not be loaded".format(mv_grid_id)) + logger.warning("MV grid {} could not be loaded".format(mv_grid_id)) def _get_mv_grid_from_bus_id(self, session, bus_id): """ @@ -1182,25 +1128,23 @@ def _get_mv_grid_from_bus_id(self, session, bus_id): """ if self._versioned is True: - ormclass_hvmv_subst = grid.__getattribute__( - 'EgoDpHvmvSubstation' + ormclass_hvmv_subst = grid.__getattribute__("EgoDpHvmvSubstation") + subst_id = ( + session.query(ormclass_hvmv_subst.subst_id) + .filter( + ormclass_hvmv_subst.otg_id == bus_id, + ormclass_hvmv_subst.version == self._grid_version, + ) + .scalar() ) - subst_id = session.query( - ormclass_hvmv_subst.subst_id - ).filter( - ormclass_hvmv_subst.otg_id == bus_id, - ormclass_hvmv_subst.version == self._grid_version - ).scalar() if self._versioned is False: - ormclass_hvmv_subst = model_draft.__getattribute__( - 'EgoGridHvmvSubstation' + ormclass_hvmv_subst = model_draft.__getattribute__("EgoGridHvmvSubstation") + subst_id = ( + session.query(ormclass_hvmv_subst.subst_id) + .filter(ormclass_hvmv_subst.otg_id == bus_id) + .scalar() ) - subst_id = session.query( - ormclass_hvmv_subst.subst_id - ).filter( - ormclass_hvmv_subst.otg_id == bus_id - ).scalar() return subst_id @@ -1221,25 +1165,23 @@ def _get_bus_id_from_mv_grid(self, session, subst_id): """ if self._versioned is True: - ormclass_hvmv_subst = grid.__getattribute__( - 'EgoDpHvmvSubstation' + ormclass_hvmv_subst = grid.__getattribute__("EgoDpHvmvSubstation") + bus_id = ( + session.query(ormclass_hvmv_subst.otg_id) + .filter( + ormclass_hvmv_subst.subst_id == subst_id, + ormclass_hvmv_subst.version == self._grid_version, + ) + .scalar() ) - bus_id = session.query( - ormclass_hvmv_subst.otg_id - ).filter( - ormclass_hvmv_subst.subst_id == subst_id, - ormclass_hvmv_subst.version == self._grid_version - ).scalar() if self._versioned is False: - ormclass_hvmv_subst = model_draft.__getattribute__( - 'EgoGridHvmvSubstation' + ormclass_hvmv_subst = model_draft.__getattribute__("EgoGridHvmvSubstation") + bus_id = ( + session.query(ormclass_hvmv_subst.otg_id) + .filter(ormclass_hvmv_subst.subst_id == subst_id) + .scalar() ) - bus_id = session.query( - ormclass_hvmv_subst.otg_id - ).filter( - ormclass_hvmv_subst.subst_id == subst_id - ).scalar() return bus_id @@ -1253,16 +1195,11 @@ class _ETraGoData: def __init__(self, etrago_network): - self.snapshots = getattr( - etrago_network, "snapshots") - self.storage_units = getattr( - etrago_network, "storage_units") - self.storage_units_t = getattr( - etrago_network, "storage_units_t") - self.generators = getattr( - etrago_network, "generators") - self.generators_t = getattr( - etrago_network, "generators_t") + self.snapshots = getattr(etrago_network, "snapshots") + self.storage_units = getattr(etrago_network, "storage_units") + self.storage_units_t = getattr(etrago_network, "storage_units_t") + self.generators = getattr(etrago_network, "generators") + self.generators_t = getattr(etrago_network, "generators_t") class _EDisGoImported: @@ -1271,20 +1208,11 @@ class _EDisGoImported: This class allows the import reduction to only the attributes used in eGo """ - def __init__( - self, - grid_expansion_costs, - s_res, - storages, - pypsa, - edisgo_config): + def __init__(self, grid_expansion_costs, s_res, storages, pypsa, edisgo_config): self.network = _NetworkImported( - grid_expansion_costs, - s_res, - storages, - pypsa, - edisgo_config) + grid_expansion_costs, s_res, storages, pypsa, edisgo_config + ) class _NetworkImported: @@ -1292,18 +1220,9 @@ class _NetworkImported: Reduced eDisG network class, used of eGo's reimport """ - def __init__( - self, - grid_expansion_costs, - s_res, - storages, - pypsa, - edisgo_config): + def __init__(self, grid_expansion_costs, s_res, storages, pypsa, edisgo_config): - self.results = _ResultsImported( - grid_expansion_costs, - s_res, - storages) + self.results = _ResultsImported(grid_expansion_costs, s_res, storages) self.pypsa = pypsa self.config = edisgo_config @@ -1314,11 +1233,7 @@ class _ResultsImported: Reduced eDisG results class, used of eGo's reimport """ - def __init__( - self, - grid_expansion_costs, - s_res, - storages): + def __init__(self, grid_expansion_costs, s_res, storages): self.grid_expansion_costs = grid_expansion_costs self.storages = storages @@ -1329,12 +1244,13 @@ def s_res(self): def parallelizer( - ding0_id_list, - func, - func_arguments, - max_calc_time, - workers=mp2.cpu_count(), - worker_lifetime=1): + ding0_id_list, + func, + func_arguments, + max_calc_time, + workers=mp2.cpu_count(), + worker_lifetime=1, +): """ Use python multiprocessing toolbox for parallelization @@ -1369,6 +1285,7 @@ def parallelizer( containers : dict of :class:`~.edisgo.EDisGo` Dict of EDisGo instances keyed by its ID """ + def collect_pool_results(result): """ Store results from parallelized calculation in structured manner @@ -1390,14 +1307,13 @@ def error_callback(key): def initializer(): import pickle + pickle.DEFAULT_PROTOCOL = 4 import dill - dill.settings['protocol'] = 4 - pool = mp2.Pool( - workers, - initializer=initializer, - maxtasksperchild=worker_lifetime) + dill.settings["protocol"] = 4 + + pool = mp2.Pool(workers, initializer=initializer, maxtasksperchild=worker_lifetime) result_objects = {} for ding0_id in ding0_id_list: @@ -1407,33 +1323,31 @@ def initializer(): func=func, args=edisgo_args, callback=collect_pool_results, - error_callback=error_callback(ding0_id)) + error_callback=error_callback(ding0_id), + ) errors = {} successes = {} start = datetime.now() - end = (start + td(hours=max_calc_time)).isoformat(' ') - logger.info( - "Jobs started. They will time out at {}." - .format(end[:end.index('.')])) + end = (start + td(hours=max_calc_time)).isoformat(" ") + logger.info("Jobs started. They will time out at {}.".format(end[: end.index(".")])) current = datetime.now() time_spent = 0 - while (result_objects and - ((current - start).seconds <= max_calc_time_seconds)): + while result_objects and ((current - start).seconds <= max_calc_time_seconds): done = [] tick = (current - start).seconds * 100 / max_calc_time_seconds if tick - time_spent >= 1 or tick > 100: hours_to_go = (current - start).seconds / 3600 - logger.info("{:.2f}% ({:.2f}/{}h) spent" - .format(tick, hours_to_go, max_calc_time)) - logger.info("Jobs time out in {:.2f}h." - .format(max_calc_time - hours_to_go)) + logger.info( + "{:.2f}% ({:.2f}/{}h) spent".format(tick, hours_to_go, max_calc_time) + ) + logger.info("Jobs time out in {:.2f}h.".format(max_calc_time - hours_to_go)) time_spent = tick for grid, result in result_objects.items(): if result.ready(): logger.info( - "MV grid {} ready. Trying to `get` the result." - .format(grid)) + "MV grid {} ready. Trying to `get` the result.".format(grid) + ) done.append(grid) if not result.successful(): try: @@ -1443,16 +1357,13 @@ def initializer(): result.get() except Exception as e: logger.warning( - "MV grid {} failed due to {e!r}: '{e}'." - .format(grid, e=e)) + "MV grid {} failed due to {e!r}: '{e}'.".format(grid, e=e) + ) errors[grid] = e else: - logger.info( - "MV grid {} calculated successfully.".format(grid)) + logger.info("MV grid {} calculated successfully.".format(grid)) successes[grid] = result.get() - logger.info( - "Done `get`ting the result for MV grid {}." - .format(grid)) + logger.info("Done `get`ting the result for MV grid {}.".format(grid)) for grid in done: del result_objects[grid] sleep(1) @@ -1471,8 +1382,7 @@ def initializer(): end = datetime.now() delta = end - start - logger.info("Execution finished after {:.2f} hours".format( - delta.seconds / 3600)) + logger.info("Execution finished after {:.2f} hours".format(delta.seconds / 3600)) done = [] for grid, result in result_objects.items(): @@ -1481,8 +1391,7 @@ def initializer(): successes[grid] = result.get(timeout=0) logger.info("MV grid {} calculated successfully.".format(grid)) except Exception as e: - logger.warning( - "MV grid {} failed due to {e!r}: '{e}'.".format(grid, e=e)) + logger.warning("MV grid {} failed due to {e!r}: '{e}'.".format(grid, e=e)) errors[grid] = e for grid in done: del result_objects[grid] diff --git a/ego/tools/io.py b/ego/tools/io.py index 84b3cba2..9b7cdac3 100644 --- a/ego/tools/io.py +++ b/ego/tools/io.py @@ -20,58 +20,80 @@ """This file contains the eGo main class as well as input & output functions of eGo in order to build the eGo application container. """ -import sys -import os import json import logging -logger = logging.getLogger('ego') -import pandas as pd -import numpy as np +import os +import sys + +logger = logging.getLogger("ego") import json -if not 'READTHEDOCS' in os.environ: - import pyproj as proj - from shapely.geometry import Polygon, Point, MultiPolygon - from sqlalchemy import MetaData, create_engine, and_, func - from sqlalchemy.orm import sessionmaker +import numpy as np +import pandas as pd + +if not "READTHEDOCS" in os.environ: + import re + + from importlib import import_module + import oedialect - from geoalchemy2 import * + import pyproj as proj + import pypsa + from egoio.db_tables import grid, model_draft + from egoio.db_tables.model_draft import EgoGridPfHvSource as Source + from egoio.db_tables.model_draft import EgoGridPfHvTempResolution as TempResolution + from egoio.db_tables.model_draft import RenpassGisParameterRegion from egoio.tools import db + from etrago.appl import etrago from etrago.tools.io import load_config_file - from egoio.db_tables.model_draft import EgoGridPfHvSource as Source,\ - EgoGridPfHvTempResolution as TempResolution - from ego.tools.results import (create_etrago_results) - from ego.tools.storages import (etrago_storages_investment, etrago_storages) + from etrago.tools.plot import ( + curtailment, + full_load_hours, + gen_dist, + gen_dist_diff, + max_load, + nodal_gen_dispatch, + nodal_production_balance, + plot_line_loading, + plot_line_loading_diff, + plot_q_flows, + plot_residual_load, + plot_stacked_gen, + plot_voltage, + storage_distribution, + storage_expansion, + ) + from geoalchemy2 import * + from shapely.geometry import MultiPolygon, Point, Polygon + from sqlalchemy import MetaData, and_, create_engine, func + from sqlalchemy.orm import sessionmaker + from ego.tools.economics import ( - etrago_operating_costs, + etrago_convert_overnight_cost, etrago_grid_investment, + etrago_operating_costs, get_generator_investment, - etrago_convert_overnight_cost) - from ego.tools.utilities import (get_scenario_setting, - get_time_steps, fix_leading_separator) + ) from ego.tools.edisgo_integration import EDisGoNetworks - from egoio.db_tables.model_draft import RenpassGisParameterRegion - from egoio.db_tables import model_draft, grid - from etrago.tools.plot import (plot_line_loading, plot_stacked_gen, - curtailment, gen_dist, storage_distribution, - plot_voltage, plot_residual_load, - plot_line_loading_diff, full_load_hours, - nodal_gen_dispatch, plot_q_flows, - max_load, storage_expansion, - nodal_production_balance, gen_dist_diff) - from etrago.appl import etrago - from importlib import import_module - import pypsa - import re - from ego.tools.plots import (plot_grid_storage_investment, - power_price_plot, plot_storage_use, igeoplot, - plot_edisgo_cluster, - plot_line_expansion, - plot_storage_expansion) - -__copyright__ = ("Europa-Universität Flensburg, " - "Centre for Sustainable Energy Systems") + from ego.tools.plots import ( + igeoplot, + plot_edisgo_cluster, + plot_grid_storage_investment, + plot_line_expansion, + plot_storage_expansion, + plot_storage_use, + power_price_plot, + ) + from ego.tools.results import create_etrago_results + from ego.tools.storages import etrago_storages, etrago_storages_investment + from ego.tools.utilities import ( + fix_leading_separator, + get_scenario_setting, + get_time_steps, + ) + +__copyright__ = "Europa-Universität Flensburg, " "Centre for Sustainable Energy Systems" __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolf_bunke,maltesc" @@ -97,8 +119,7 @@ class egoBasic(object): """ def __init__(self, *args, **kwargs): - """ - """ + """ """ logger.info("Using scenario setting: {}".format(self.jsonpath)) @@ -110,15 +131,15 @@ def __init__(self, *args, **kwargs): # Database connection from json_file try: - conn = db.connection(section=self.json_file['eTraGo']['db']) + conn = db.connection(section=self.json_file["eTraGo"]["db"]) Session = sessionmaker(bind=conn) self.session = Session() - logger.info('Connected to Database') + logger.info("Connected to Database") except: - logger.error('Failed connection to Database', exc_info=True) + logger.error("Failed connection to Database", exc_info=True) # get scn_name - self.scn_name = self.json_file['eTraGo']['scn_name'] + self.scn_name = self.json_file["eTraGo"]["scn_name"] class eTraGoResults(egoBasic): @@ -134,147 +155,145 @@ class eTraGoResults(egoBasic): """ def __init__(self, *args, **kwargs): - """ - """ + """ """ super(eTraGoResults, self).__init__(self, *args, **kwargs) self.etrago = None self._etrago_network = None self._etrago_disaggregated_network = None - logger.info('eTraGo section started') + logger.info("eTraGo section started") - if self.json_file['eGo']['result_id'] != None: + if self.json_file["eGo"]["result_id"] != None: # Delete arguments from scenario_setting - logger.info('Remove given eTraGo settings from scenario_setting') + logger.info("Remove given eTraGo settings from scenario_setting") try: - self.json_file['eGo']['eTraGo'] = False + self.json_file["eGo"]["eTraGo"] = False - for key in self.json_file['eTraGo'].keys(): + for key in self.json_file["eTraGo"].keys(): - self.json_file['eTraGo'][key] = 'removed by DB recover' + self.json_file["eTraGo"][key] = "removed by DB recover" # ToDo add scenario_setting for results - self.json_file['eTraGo']['db'] = self.json_file['eTraGo']['db'] - logger.info( - 'Add eTraGo scenario_setting from oedb result') + self.json_file["eTraGo"]["db"] = self.json_file["eTraGo"]["db"] + logger.info("Add eTraGo scenario_setting from oedb result") # To do .... - _prefix = 'EgoGridPfHvResult' - schema = 'model_draft' - packagename = 'egoio.db_tables' - _pkg = import_module(packagename + '.' + schema) + _prefix = "EgoGridPfHvResult" + schema = "model_draft" + packagename = "egoio.db_tables" + _pkg = import_module(packagename + "." + schema) # get metadata - orm_meta = getattr(_pkg, _prefix + 'Meta') - self.jsonpath = recover_resultsettings(self.session, - self.json_file, - orm_meta, - self.json_file['eGo'] - ['result_id']) + orm_meta = getattr(_pkg, _prefix + "Meta") + self.jsonpath = recover_resultsettings( + self.session, + self.json_file, + orm_meta, + self.json_file["eGo"]["result_id"], + ) # add etrago_disaggregated_network from DB logger.info( "Recovered eTraGo network uses kmeans: {}".format( - self.json_file['eTraGo']['network_clustering_kmeans'])) + self.json_file["eTraGo"]["network_clustering_kmeans"] + ) + ) except KeyError: pass - logger.info('Create eTraGo network from oedb result') - self._etrago_network = etrago_from_oedb( - self.session, self.json_file) + logger.info("Create eTraGo network from oedb result") + self._etrago_network = etrago_from_oedb(self.session, self.json_file) - if self.json_file['eTraGo']['disaggregation'] != False: + if self.json_file["eTraGo"]["disaggregation"] != False: self._etrago_disaggregated_network = self._etrago_network else: - logger.warning('No disaggregated network found in DB') + logger.warning("No disaggregated network found in DB") self._etrago_disaggregated_network = None # create eTraGo NetworkScenario - if self.json_file['eGo']['eTraGo'] is True: + if self.json_file["eGo"]["eTraGo"] is True: - if self.json_file['eGo'].get('csv_import_eTraGo') != False: + if self.json_file["eGo"].get("csv_import_eTraGo") != False: - logger.info('Caution, import disaggregation ' - 'data of former Cluster') + logger.info("Caution, import disaggregation " "data of former Cluster") # get pathway - pathway = self.json_file['eGo'].get('csv_import_eTraGo') + pathway = self.json_file["eGo"].get("csv_import_eTraGo") try: # create Network from csv self._etrago_network = pypsa.Network() self._etrago_network.import_from_csv_folder(pathway) - logger.info('Create eTraGo network from CSV result') + logger.info("Create eTraGo network from CSV result") # get disaggregation self._etrago_disaggregated_network = pypsa.Network() - self._etrago_disaggregated_network.\ - import_from_csv_folder(pathway+'/disaggregated') - logger.info('Create eTraGo disaggregated network ' - 'from CSV result') + self._etrago_disaggregated_network.import_from_csv_folder( + pathway + "/disaggregated" + ) + logger.info( + "Create eTraGo disaggregated network " "from CSV result" + ) except TypeError: file_path = "disaggregated/network.csv" - fix_leading_separator(pathway+"/"+file_path) + fix_leading_separator(pathway + "/" + file_path) file_path = "network.csv" - fix_leading_separator(pathway+"/"+file_path) + fix_leading_separator(pathway + "/" + file_path) self._etrago_network = pypsa.Network() self._etrago_network.import_from_csv_folder(pathway) - logger.info('Create eTraGo network from CSV result') + logger.info("Create eTraGo network from CSV result") # get disaggregation self._etrago_disaggregated_network = pypsa.Network() - self._etrago_disaggregated_network.\ - import_from_csv_folder(pathway+'/disaggregated') - logger.info('Create eTraGo disaggregated network' - 'from CSV result') + self._etrago_disaggregated_network.import_from_csv_folder( + pathway + "/disaggregated" + ) + logger.info("Create eTraGo disaggregated network" "from CSV result") args_name = "args.json" - with open(pathway+'/'+args_name) as f: + with open(pathway + "/" + args_name) as f: etrago_args = json.load(f) - logger.info('Using argument file') + logger.info("Using argument file") - if etrago_args.get('extendable') == ['network', 'storages']: - etrago_args.update( - {'extendable': ['network', 'storage']}) - logger.info( - 'Changed naming of storages to storage of args') + if etrago_args.get("extendable") == ["network", "storages"]: + etrago_args.update({"extendable": ["network", "storage"]}) + logger.info("Changed naming of storages to storage of args") - if etrago_args.get('extendable') == ['storages']: - etrago_args.update({'extendable': ['storage']}) - logger.info( - 'Changed naming of storages to storage of args') + if etrago_args.get("extendable") == ["storages"]: + etrago_args.update({"extendable": ["storage"]}) + logger.info("Changed naming of storages to storage of args") - for key in self.json_file['eTraGo'].keys(): + for key in self.json_file["eTraGo"].keys(): try: - self.json_file['eTraGo'][key] = etrago_args[key] + self.json_file["eTraGo"][key] = etrago_args[key] except KeyError: pass else: - logger.info('Create eTraGo network calcualted by eGo') + logger.info("Create eTraGo network calcualted by eGo") - if self.json_file['eTraGo']['disaggregation'] != False: + if self.json_file["eTraGo"]["disaggregation"] != False: etrago_network, etrago_disaggregated_network = etrago( - self.json_file['eTraGo']) + self.json_file["eTraGo"] + ) self._etrago_network = etrago_network - self._etrago_disaggregated_network = ( - etrago_disaggregated_network) + self._etrago_disaggregated_network = etrago_disaggregated_network else: logger.warning("Only one network is used.") etrago_network, etrago_disaggregated_network = etrago( - self.json_file['eTraGo']) + self.json_file["eTraGo"] + ) self._etrago_network = etrago_network - self._etrago_disaggregated_network = ( - etrago_disaggregated_network) + self._etrago_disaggregated_network = etrago_disaggregated_network # Add selected results to results container # ----------------------------------------- @@ -285,16 +304,17 @@ def __init__(self, *args, **kwargs): # Add function self.etrago.storage_investment_costs = etrago_storages_investment( - self.etrago.network, self.json_file, self.session) + self.etrago.network, self.json_file, self.session + ) self.etrago.storage_charges = etrago_storages(self.etrago.network) - self.etrago.operating_costs = etrago_operating_costs( - self.etrago.network) - self.etrago.generator = create_etrago_results(self.etrago.network, - self.scn_name) - self.etrago.grid_investment_costs = \ - etrago_grid_investment(self.etrago.network, - self.json_file, self.session) + self.etrago.operating_costs = etrago_operating_costs(self.etrago.network) + self.etrago.generator = create_etrago_results( + self.etrago.network, self.scn_name + ) + self.etrago.grid_investment_costs = etrago_grid_investment( + self.etrago.network, self.json_file, self.session + ) # add functions direct # self._etrago_network.etrago_line_loading = etrago_line_loading @@ -306,17 +326,15 @@ def __init__(self, *args, **kwargs): self.etrago.plot_line_loading_diff = self._line_loading_diff self.etrago.plot_residual_load = self._residual_load self.etrago.plot_voltage = self._voltage - self.etrago.plot_nodal_gen_dispatch = \ - self._nodal_gen_dispatch + self.etrago.plot_nodal_gen_dispatch = self._nodal_gen_dispatch self.etrago.plot_full_load_hours = self._full_load_hours self.etrago.plot_q_flows = self._plot_q_flows self.etrago.plot_max_load = self._max_load self.etrago.plot_storage_expansion = self._storage_expansion - self.etrago.plot_nodal_production_balance = ( - self._nodal_production_balance) + self.etrago.plot_nodal_production_balance = self._nodal_production_balance self.etrago.plot_gen_dist_diff = self._gen_dist_diff - if not 'READTHEDOCS' in os.environ: + if not "READTHEDOCS" in os.environ: # include eTraGo functions and methods def _gen_dist_diff(self, **kwargs): """ @@ -324,8 +342,7 @@ def _gen_dist_diff(self, **kwargs): For more information see: """ - return gen_dist_diff(networkA=self.etrago.network, - **kwargs) + return gen_dist_diff(networkA=self.etrago.network, **kwargs) def _nodal_production_balance(self, **kwargs): """ @@ -333,8 +350,7 @@ def _nodal_production_balance(self, **kwargs): For more information see: """ - return nodal_production_balance(network=self.etrago.network, - **kwargs) + return nodal_production_balance(network=self.etrago.network, **kwargs) def _storage_expansion(self, **kwargs): """ @@ -342,8 +358,7 @@ def _storage_expansion(self, **kwargs): For more information see: """ - return storage_expansion(network=self.etrago.network, - **kwargs) + return storage_expansion(network=self.etrago.network, **kwargs) def _max_load(self, **kwargs): """ @@ -351,8 +366,7 @@ def _max_load(self, **kwargs): For more information see: """ - return max_load(network=self.etrago.network, - **kwargs) + return max_load(network=self.etrago.network, **kwargs) def _plot_q_flows(self): """ @@ -396,8 +410,9 @@ def _storage_distribution(self, scaling=1, **kwargs): Integrate function from eTraGo. For more information see: """ - return storage_distribution(network=self.etrago.network, - scaling=1, **kwargs) + return storage_distribution( + network=self.etrago.network, scaling=1, **kwargs + ) def _voltage(self, **kwargs): """ @@ -418,16 +433,14 @@ def _line_loading_diff(self, **kwargs): Integrate function from eTraGo. For more information see: """ - return plot_line_loading_diff(networkA=self.etrago.network, - **kwargs) + return plot_line_loading_diff(networkA=self.etrago.network, **kwargs) def _nodal_gen_dispatch(self, **kwargs): """ Integrate function from eTraGo. For more information see: """ - return nodal_gen_dispatch(network=self.etrago.network, - **kwargs) + return nodal_gen_dispatch(network=self.etrago.network, **kwargs) def _full_load_hours(self, **kwargs): """ @@ -446,15 +459,16 @@ class eDisGoResults(eTraGoResults): def __init__(self, *args, **kwargs): super(eDisGoResults, self).__init__(self, *args, **kwargs) - if self.json_file['eGo']['eDisGo'] is True: - logger.info('Create eDisGo network') + if self.json_file["eGo"]["eDisGo"] is True: + logger.info("Create eDisGo network") self._edisgo = EDisGoNetworks( json_file=self.json_file, - etrago_network=self.etrago.disaggregated_network) + etrago_network=self.etrago.disaggregated_network, + ) else: self._edisgo = None - logger.info('No eDisGo network') + logger.info("No eDisGo network") @property def edisgo(self): @@ -489,7 +503,7 @@ class eGo(eDisGoResults): def __init__(self, jsonpath, *args, **kwargs): self.jsonpath = jsonpath - super(eGo, self).__init__(self, *args, **kwargs) + super(eGo, self).__init__(self, *args, **kwargs) # add total results here self._total_investment_costs = None @@ -499,49 +513,49 @@ def __init__(self, jsonpath, *args, **kwargs): self._ehv_grid_costs = None self._mv_grid_costs = None - def _calculate_investment_cost( - self, - storage_mv_integration=True): - """ Get total investment costs of all voltage level for storages + def _calculate_investment_cost(self, storage_mv_integration=True): + """Get total investment costs of all voltage level for storages and grid expansion """ - self._total_inv_cost = pd.DataFrame(columns=['component', - 'voltage_level', - 'capital_cost' - ]) + self._total_inv_cost = pd.DataFrame( + columns=["component", "voltage_level", "capital_cost"] + ) _grid_ehv = None - if 'network' in self.json_file['eTraGo']['extendable']: + if "network" in self.json_file["eTraGo"]["extendable"]: _grid_ehv = self.etrago.grid_investment_costs - _grid_ehv['component'] = 'grid' + _grid_ehv["component"] = "grid" - self._total_inv_cost = self._total_inv_cost.\ - append(_grid_ehv, ignore_index=True) + self._total_inv_cost = self._total_inv_cost.append( + _grid_ehv, ignore_index=True + ) _storage = None - if 'storage' in self.json_file['eTraGo']['extendable']: + if "storage" in self.json_file["eTraGo"]["extendable"]: _storage = self.etrago.storage_investment_costs - _storage['component'] = 'storage' + _storage["component"] = "storage" - self._total_inv_cost = self._total_inv_cost.\ - append(_storage, ignore_index=True) + self._total_inv_cost = self._total_inv_cost.append( + _storage, ignore_index=True + ) _grid_mv_lv = None - if self.json_file['eGo']['eDisGo'] is True: + if self.json_file["eGo"]["eDisGo"] is True: _grid_mv_lv = self.edisgo.grid_investment_costs if _grid_mv_lv is not None: - _grid_mv_lv['component'] = 'grid' - _grid_mv_lv['differentiation'] = 'domestic' + _grid_mv_lv["component"] = "grid" + _grid_mv_lv["differentiation"] = "domestic" - self._total_inv_cost = self._total_inv_cost.\ - append(_grid_mv_lv, ignore_index=True) + self._total_inv_cost = self._total_inv_cost.append( + _grid_mv_lv, ignore_index=True + ) # add overnight costs self._total_investment_costs = self._total_inv_cost - self._total_investment_costs[ - 'overnight_costs'] = etrago_convert_overnight_cost( - self._total_investment_costs['capital_cost'], self.json_file) + self._total_investment_costs["overnight_costs"] = etrago_convert_overnight_cost( + self._total_investment_costs["capital_cost"], self.json_file + ) # Include MV storages into the _total_investment_costs dataframe if storage_mv_integration is True: @@ -549,11 +563,13 @@ def _calculate_investment_cost( self._integrate_mv_storage_investment() # sort values - self._total_investment_costs['voltage_level'] = pd.Categorical( - self._total_investment_costs['voltage_level'], ['ehv', 'hv', 'mv', - 'lv', 'mv/lv']) - self._total_investment_costs = ( - self._total_investment_costs.sort_values('voltage_level')) + self._total_investment_costs["voltage_level"] = pd.Categorical( + self._total_investment_costs["voltage_level"], + ["ehv", "hv", "mv", "lv", "mv/lv"], + ) + self._total_investment_costs = self._total_investment_costs.sort_values( + "voltage_level" + ) self._storage_costs = _storage self._ehv_grid_costs = _grid_ehv @@ -577,44 +593,39 @@ def _integrate_mv_storage_investment(self): if integrated_share > 0: ehv_stor_idx = costs_df.index[ - (costs_df['component'] == 'storage') - & (costs_df['voltage_level'] == 'ehv')][0] - - int_capital_costs = costs_df.loc[ehv_stor_idx][ - 'capital_cost' - ] * integrated_share - int_overnight_costs = costs_df.loc[ehv_stor_idx][ - 'overnight_costs' - ] * integrated_share - - costs_df.at[ - ehv_stor_idx, - 'capital_cost' - ] = ( - costs_df.loc[ehv_stor_idx]['capital_cost'] - - int_capital_costs) - - costs_df.at[ - ehv_stor_idx, - 'overnight_costs' - ] = ( - costs_df.loc[ehv_stor_idx]['overnight_costs'] - - int_overnight_costs) + (costs_df["component"] == "storage") + & (costs_df["voltage_level"] == "ehv") + ][0] + + int_capital_costs = ( + costs_df.loc[ehv_stor_idx]["capital_cost"] * integrated_share + ) + int_overnight_costs = ( + costs_df.loc[ehv_stor_idx]["overnight_costs"] * integrated_share + ) + + costs_df.at[ehv_stor_idx, "capital_cost"] = ( + costs_df.loc[ehv_stor_idx]["capital_cost"] - int_capital_costs + ) + + costs_df.at[ehv_stor_idx, "overnight_costs"] = ( + costs_df.loc[ehv_stor_idx]["overnight_costs"] - int_overnight_costs + ) new_storage_row = { - 'component': ['storage'], - 'voltage_level': ['mv'], - 'differentiation': ['domestic'], - 'capital_cost': [int_capital_costs], - 'overnight_costs': [int_overnight_costs]} + "component": ["storage"], + "voltage_level": ["mv"], + "differentiation": ["domestic"], + "capital_cost": [int_capital_costs], + "overnight_costs": [int_overnight_costs], + } new_storage_row = pd.DataFrame(new_storage_row) costs_df = costs_df.append(new_storage_row) self._total_investment_costs = costs_df except: - logger.info( - 'Something went wrong with the MV storage distribution.') + logger.info("Something went wrong with the MV storage distribution.") def _calculate_all_extended_storages(self): """ @@ -623,11 +634,12 @@ def _calculate_all_extended_storages(self): etrago_network = self._etrago_disaggregated_network stor_df = etrago_network.storage_units.loc[ - (etrago_network.storage_units['p_nom_extendable'] == True)] + (etrago_network.storage_units["p_nom_extendable"] == True) + ] - stor_df = stor_df[['bus', 'p_nom_opt']] + stor_df = stor_df[["bus", "p_nom_opt"]] - all_extended_storages = stor_df['p_nom_opt'].sum() + all_extended_storages = stor_df["p_nom_opt"].sum() return all_extended_storages @@ -639,46 +651,51 @@ def _calculate_mv_storage(self): min_extended = 0.3 stor_df = etrago_network.storage_units.loc[ - (etrago_network.storage_units['p_nom_extendable'] == True) - & (etrago_network.storage_units['p_nom_opt'] > min_extended) - & (etrago_network.storage_units['max_hours'] <= 20.)] + (etrago_network.storage_units["p_nom_extendable"] == True) + & (etrago_network.storage_units["p_nom_opt"] > min_extended) + & (etrago_network.storage_units["max_hours"] <= 20.0) + ] - stor_df = stor_df[['bus', 'p_nom_opt']] + stor_df = stor_df[["bus", "p_nom_opt"]] - integrated_storage = .0 # Storage integrated in MV grids + integrated_storage = 0.0 # Storage integrated in MV grids for idx, row in stor_df.iterrows(): - bus_id = row['bus'] - p_nom_opt = row['p_nom_opt'] + bus_id = row["bus"] + p_nom_opt = row["p_nom_opt"] mv_grid_id = self.edisgo.get_mv_grid_from_bus_id(bus_id) if not mv_grid_id: continue - logger.info("Checking storage integration for MV grid {}".format( - mv_grid_id)) + logger.info( + "Checking storage integration for MV grid {}".format(mv_grid_id) + ) grid_choice = self.edisgo.grid_choice cluster = grid_choice.loc[ - [mv_grid_id in repr_grids for repr_grids in grid_choice[ - 'represented_grids']]] + [ + mv_grid_id in repr_grids + for repr_grids in grid_choice["represented_grids"] + ] + ] if len(cluster) == 0: continue else: - representative_grid = cluster[ - 'the_selected_network_id'].values[0] + representative_grid = cluster["the_selected_network_id"].values[0] - if hasattr(self.edisgo.network[representative_grid], 'network'): + if hasattr(self.edisgo.network[representative_grid], "network"): integration_df = self.edisgo.network[ - representative_grid].network.results.storages + representative_grid + ].network.results.storages - integrated_power = integration_df['nominal_power'].sum() / 1000 + integrated_power = integration_df["nominal_power"].sum() / 1000 else: - integrated_power = 0. + integrated_power = 0.0 if integrated_power > p_nom_opt: integrated_power = p_nom_opt @@ -715,25 +732,19 @@ def total_operation_costs(self): return self._total_operation_costs - def plot_total_investment_costs(self, - filename=None, - display=False, **kwargs): - """ Plot total investment costs - """ + def plot_total_investment_costs(self, filename=None, display=False, **kwargs): + """Plot total investment costs""" if filename is None: filename = "results/plot_total_investment_costs.pdf" display = True return plot_grid_storage_investment( - self._total_investment_costs, - filename=filename, - display=display, - **kwargs) + self._total_investment_costs, filename=filename, display=display, **kwargs + ) def plot_power_price(self, filename=None, display=False): - """ Plot power prices per carrier of calculation - """ + """Plot power prices per carrier of calculation""" if filename is None: filename = "results/plot_power_price.pdf" display = True @@ -741,45 +752,38 @@ def plot_power_price(self, filename=None, display=False): return power_price_plot(self, filename=filename, display=display) def plot_storage_usage(self, filename=None, display=False): - """ Plot storage usage by charge and discharge - """ + """Plot storage usage by charge and discharge""" if filename is None: filename = "results/plot_storage_usage.pdf" display = True return plot_storage_use(self, filename=filename, display=display) - def plot_edisgo_cluster(self, filename=None, display=False, - **kwargs): - """ Plot the Clustering of selected Dingo networks - """ + def plot_edisgo_cluster(self, filename=None, display=False, **kwargs): + """Plot the Clustering of selected Dingo networks""" if filename is None: filename = "results/plot_edisgo_cluster.pdf" display = True - return plot_edisgo_cluster(self, filename=filename, display=display, - **kwargs) + return plot_edisgo_cluster(self, filename=filename, display=display, **kwargs) def plot_line_expansion(self, **kwargs): - """Plot line expantion per line - """ + """Plot line expantion per line""" return plot_line_expansion(self, **kwargs) def plot_storage_expansion(self, **kwargs): - """Plot storage expantion per bus - """ + """Plot storage expantion per bus""" return plot_storage_expansion(self, **kwargs) @property def iplot(self): - """ Get iplot of results as html - """ + """Get iplot of results as html""" return igeoplot(self) # write_results_to_db(): - logging.info('Initialisation of eGo Results') + logging.info("Initialisation of eGo Results") def results_to_excel(ego): @@ -789,12 +793,12 @@ def results_to_excel(ego): # Write the results as xlsx file # ToDo add time of calculation to file name # add xlsxwriter to setup - writer = pd.ExcelWriter('open_ego_results.xlsx', engine='xlsxwriter') + writer = pd.ExcelWriter("open_ego_results.xlsx", engine="xlsxwriter") # write results of installed Capacity by fuels - ego.total_investment_costs.to_excel(writer, - index=False, - sheet_name='Total Calculation') + ego.total_investment_costs.to_excel( + writer, index=False, sheet_name="Total Calculation" + ) # Close the Pandas Excel writer and output the Excel file. writer.save() @@ -819,7 +823,7 @@ def etrago_from_oedb(session, json_file): """ - result_id = json_file['eGo']['result_id'] + result_id = json_file["eGo"]["result_id"] # functions def map_ormclass(name): @@ -830,7 +834,7 @@ def map_ormclass(name): _mapped[name] = getattr(_pkg, _prefix + name) except AttributeError: - logger.warning('Relation %s does not exist.' % name) + logger.warning("Relation %s does not exist." % name) return _mapped @@ -854,18 +858,16 @@ def dataframe_results(name, session, result_id, ormclass): query = session.query(ormclass).filter(ormclass.result_id == result_id) - if name == 'Transformer': - name = 'Trafo' + if name == "Transformer": + name = "Trafo" - df = pd.read_sql(query.statement, - session.bind, - index_col=name.lower() + '_id') + df = pd.read_sql(query.statement, session.bind, index_col=name.lower() + "_id") - if name == 'Link': - df['bus0'] = df.bus0.astype(int) - df['bus1'] = df.bus1.astype(int) + if name == "Link": + df["bus0"] = df.bus0.astype(int) + df["bus1"] = df.bus1.astype(int) - if 'source' in df: + if "source" in df: source_orm = Source @@ -873,7 +875,7 @@ def dataframe_results(name, session, result_id, ormclass): df.source = df.source.map(id_to_source(source_query)) - if str(ormclass)[:-2].endswith('T'): + if str(ormclass)[:-2].endswith("T"): df = pd.Dataframe() return df @@ -891,20 +893,16 @@ def series_results(name, column, session, result_id, ormclass): # TODO - check index of bus_t and soon is wrong! # TODO: pls make more robust - id_column = re.findall(r'[A-Z][^A-Z]*', name)[0] + '_' + 'id' + id_column = re.findall(r"[A-Z][^A-Z]*", name)[0] + "_" + "id" id_column = id_column.lower() query = session.query( - getattr(ormclass, id_column), - getattr(ormclass, column). - label(column)).filter(and_( - ormclass.result_id == result_id - )) + getattr(ormclass, id_column), getattr(ormclass, column).label(column) + ).filter(and_(ormclass.result_id == result_id)) - df = pd.io.sql.read_sql(query.statement, - session.bind, - columns=[column], - index_col=id_column) + df = pd.io.sql.read_sql( + query.statement, session.bind, columns=[column], index_col=id_column + ) df.index = df.index.astype(str) @@ -915,37 +913,37 @@ def series_results(name, column, session, result_id, ormclass): assert not df.empty df.index = timeindex except AssertionError: - logger.warning("No data for %s in column %s." % (name, column)) + logger.warning("No data for {} in column {}.".format(name, column)) return df # create config for results path = os.getcwd() # add meta_args with args of results - config = load_config_file(path+'/tools/config.json')['results'] + config = load_config_file(path + "/tools/config.json")["results"] # map and Database settings of etrago_from_oedb() - _prefix = 'EgoGridPfHvResult' - schema = 'model_draft' - packagename = 'egoio.db_tables' - _pkg = import_module(packagename + '.' + schema) - temp_ormclass = 'TempResolution' - carr_ormclass = 'Source' + _prefix = "EgoGridPfHvResult" + schema = "model_draft" + packagename = "egoio.db_tables" + _pkg = import_module(packagename + "." + schema) + temp_ormclass = "TempResolution" + carr_ormclass = "Source" _mapped = {} # get metadata - orm_meta = getattr(_pkg, _prefix + 'Meta') + orm_meta = getattr(_pkg, _prefix + "Meta") # check result_id - result_id_in = session.query( - orm_meta.result_id).filter(orm_meta. - result_id == result_id).all() + result_id_in = ( + session.query(orm_meta.result_id).filter(orm_meta.result_id == result_id).all() + ) if result_id_in: - logger.info('Choosen result_id %s found in DB', result_id) + logger.info("Choosen result_id %s found in DB", result_id) else: - logger.info('Error: result_id not found in DB') + logger.info("Error: result_id not found in DB") # get meta data as args meta_args = recover_resultsettings(session, json_file, orm_meta, result_id) @@ -953,15 +951,17 @@ def series_results(name, column, session, result_id, ormclass): # get TempResolution temp = TempResolution - tr = session.query(temp.temp_id, temp.timesteps, - temp.resolution, temp.start_time).one() + tr = session.query( + temp.temp_id, temp.timesteps, temp.resolution, temp.start_time + ).one() - timeindex = pd.DatetimeIndex(start=tr.start_time, - periods=tr.timesteps, - freq=tr.resolution) + timeindex = pd.DatetimeIndex( + start=tr.start_time, periods=tr.timesteps, freq=tr.resolution + ) - timeindex = timeindex[meta_args['eTraGo']['start_snapshot'] - - 1: meta_args['eTraGo']['end_snapshot']] + timeindex = timeindex[ + meta_args["eTraGo"]["start_snapshot"] - 1 : meta_args["eTraGo"]["end_snapshot"] + ] # create df for PyPSA network @@ -970,37 +970,42 @@ def series_results(name, column, session, result_id, ormclass): timevarying_override = False - if pypsa.__version__ == '0.11.0': - old_to_new_name = {'Generator': - {'p_min_pu_fixed': 'p_min_pu', - 'p_max_pu_fixed': 'p_max_pu', - 'source': 'carrier', - 'dispatch': 'former_dispatch'}, - 'Bus': - {'current_type': 'carrier'}, - 'Transformer': - {'trafo_id': 'transformer_id'}, - 'Storage': - {'p_min_pu_fixed': 'p_min_pu', - 'p_max_pu_fixed': 'p_max_pu', - 'soc_cyclic': 'cyclic_state_of_charge', - 'soc_initial': 'state_of_charge_initial', - 'source': 'carrier'}} + if pypsa.__version__ == "0.11.0": + old_to_new_name = { + "Generator": { + "p_min_pu_fixed": "p_min_pu", + "p_max_pu_fixed": "p_max_pu", + "source": "carrier", + "dispatch": "former_dispatch", + }, + "Bus": {"current_type": "carrier"}, + "Transformer": {"trafo_id": "transformer_id"}, + "Storage": { + "p_min_pu_fixed": "p_min_pu", + "p_max_pu_fixed": "p_max_pu", + "soc_cyclic": "cyclic_state_of_charge", + "soc_initial": "state_of_charge_initial", + "source": "carrier", + }, + } timevarying_override = True else: - old_to_new_name = {'Storage': - {'soc_cyclic': 'cyclic_state_of_charge', - 'soc_initial': 'state_of_charge_initial'}} + old_to_new_name = { + "Storage": { + "soc_cyclic": "cyclic_state_of_charge", + "soc_initial": "state_of_charge_initial", + } + } # get data into dataframes - logger.info('Start building eTraGo results network') + logger.info("Start building eTraGo results network") for comp, comp_t_dict in config.items(): orm_dict = map_ormclass(comp) - pypsa_comp_name = 'StorageUnit' if comp == 'Storage' else comp + pypsa_comp_name = "StorageUnit" if comp == "Storage" else comp ormclass = orm_dict[comp] if not comp_t_dict: @@ -1019,77 +1024,78 @@ def series_results(name, column, session, result_id, ormclass): name = name[:-1] pypsa_comp_name = name - if name == 'Storage': - pypsa_comp_name = 'StorageUnit' - if name == 'Transformer': - name = 'Trafo' + if name == "Storage": + pypsa_comp_name = "StorageUnit" + if name == "Transformer": + name = "Trafo" for col in columns: - df_series = series_results( - name, col, session, result_id, ormclass) + df_series = series_results(name, col, session, result_id, ormclass) # TODO: VMagPuSet? - if timevarying_override and comp == 'Generator': - idx = df[df.former_dispatch == 'flexible'].index + if timevarying_override and comp == "Generator": + idx = df[df.former_dispatch == "flexible"].index idx = [i for i in idx if i in df_series.columns] df_series.drop(idx, axis=1, inplace=True) try: pypsa.io.import_series_from_dataframe( - network, - df_series, - pypsa_comp_name, - col) + network, df_series, pypsa_comp_name, col + ) except (ValueError, AttributeError): - logger.warning("Series %s of component %s could not be" - " imported" % (col, pypsa_comp_name)) + logger.warning( + "Series %s of component %s could not be" + " imported" % (col, pypsa_comp_name) + ) - logger.info('Imported eTraGo results of id = %s ', result_id) + logger.info("Imported eTraGo results of id = %s ", result_id) return network def recover_resultsettings(session, json_file, orm_meta, result_id): - """ Recover scenario_setting from database - """ + """Recover scenario_setting from database""" # check result_id - result_id_in = session.query( - orm_meta.result_id).filter(orm_meta. - result_id == result_id).all() + result_id_in = ( + session.query(orm_meta.result_id).filter(orm_meta.result_id == result_id).all() + ) # get meta data as json_file - meta = session.query(orm_meta.result_id, orm_meta.scn_name, - orm_meta.calc_date, - orm_meta.user_name, orm_meta.method, - orm_meta.start_snapshot, - orm_meta.end_snapshot, orm_meta.solver, - orm_meta.settings - ).filter(orm_meta.result_id == result_id) - - meta_df = pd.read_sql( - meta.statement, meta.session.bind, index_col='result_id') + meta = session.query( + orm_meta.result_id, + orm_meta.scn_name, + orm_meta.calc_date, + orm_meta.user_name, + orm_meta.method, + orm_meta.start_snapshot, + orm_meta.end_snapshot, + orm_meta.solver, + orm_meta.settings, + ).filter(orm_meta.result_id == result_id) + + meta_df = pd.read_sql(meta.statement, meta.session.bind, index_col="result_id") # update json_file with main data by result_id - json_file['eTraGo']['scn_name'] = meta_df.scn_name[result_id] - json_file['eTraGo']['method'] = meta_df.method[result_id] - json_file['eTraGo']['start_snapshot'] = meta_df.start_snapshot[result_id] - json_file['eTraGo']['end_snapshot'] = meta_df.end_snapshot[result_id] - json_file['eTraGo']['solver'] = meta_df.solver[result_id] + json_file["eTraGo"]["scn_name"] = meta_df.scn_name[result_id] + json_file["eTraGo"]["method"] = meta_df.method[result_id] + json_file["eTraGo"]["start_snapshot"] = meta_df.start_snapshot[result_id] + json_file["eTraGo"]["end_snapshot"] = meta_df.end_snapshot[result_id] + json_file["eTraGo"]["solver"] = meta_df.solver[result_id] # update json_file with specific data by result_id meta_set = dict(meta_df.settings[result_id]) - for key in json_file['eTraGo'].keys(): + for key in json_file["eTraGo"].keys(): try: - json_file['eTraGo'][key] = meta_set[key] + json_file["eTraGo"][key] = meta_set[key] except KeyError: pass return json_file -if __name__ == '__main__': +if __name__ == "__main__": pass diff --git a/ego/tools/mv_cluster.py b/ego/tools/mv_cluster.py index 0e8da824..2d28b39e 100644 --- a/ego/tools/mv_cluster.py +++ b/ego/tools/mv_cluster.py @@ -20,37 +20,41 @@ """ This file contains all functions regarding the clustering of MV grids """ -__copyright__ = ("Flensburg University of Applied Sciences, " - "Europa-Universität Flensburg, " - "Centre for Sustainable Energy Systems") +__copyright__ = ( + "Flensburg University of Applied Sciences, " + "Europa-Universität Flensburg, " + "Centre for Sustainable Energy Systems" +) __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolf_bunke, maltesc" +import logging + # Import -#from __future__ import print_function +# from __future__ import print_function import os -import logging -if not 'READTHEDOCS' in os.environ: +if not "READTHEDOCS" in os.environ: import pickle - + + import numpy as np import pandas as pd - + from sklearn.cluster import KMeans - import numpy as np - + logger = logging.getLogger(__name__) + def analyze_attributes(ding0_files): """ Calculates the attributes wind and solar capacity and farthest node for all files in ding0_files. Results are written to ding0_files - + Parameters ---------- ding0_files : :obj:`str` Path to ding0 files - + """ base_path = ding0_files @@ -63,10 +67,9 @@ def analyze_attributes(ding0_files): for district_number in list(range(1, 4000)): try: - pickle_name = 'ding0_grids__{}.pkl'.format( - district_number) - nd = pickle.load(open(os.path.join(base_path, pickle_name), 'rb')) - print('District no.', district_number, 'found!') + pickle_name = "ding0_grids__{}.pkl".format(district_number) + nd = pickle.load(open(os.path.join(base_path, pickle_name), "rb")) + print("District no.", district_number, "found!") except: not_found.append(district_number) continue @@ -79,9 +82,9 @@ def analyze_attributes(ding0_files): # cumulative capacity of solar and wind in MV for geno in nd._mv_grid_districts[0].mv_grid.generators(): - if geno.type == 'solar': + if geno.type == "solar": mv_cum_solar_MV += geno.capacity - if geno.type == 'wind': + if geno.type == "wind": mv_cum_wind_MV += geno.capacity lvg = 0 @@ -93,9 +96,9 @@ def analyze_attributes(ding0_files): for lvgs1 in lvgs.lv_grid_districts(): lvg += len(list(lvgs1.lv_grid.generators())) for deno in lvgs1.lv_grid.generators(): - if deno.type == 'solar': + if deno.type == "solar": mv_cum_solar_LV += deno.capacity - if deno.type == 'wind': + if deno.type == "wind": mv_cum_wind_LV += deno.capacity # Total solar cumulative capacity in lv and mv @@ -116,55 +119,60 @@ def analyze_attributes(ding0_files): max_of_max = 0 # make CB open (normal operation case) - nd.control_circuit_breakers(mode='open') + nd.control_circuit_breakers(mode="open") # setting the root to measure the path from root_mv = nd._mv_grid_districts[0].mv_grid.station() # 1st from MV substation to LV station node # Iteration through nodes for node2 in nd._mv_grid_districts[0].mv_grid._graph.nodes(): # select only LV station nodes - if isinstance( - node2, - LVStationDing0) and not node2.lv_load_area.is_aggregated: + if ( + isinstance(node2, LVStationDing0) + and not node2.lv_load_area.is_aggregated + ): length_from_MV_to_LV_station = 0 # Distance from MV substation to LV station node - length_from_MV_to_LV_station = nd._mv_grid_districts[ - 0 - ].mv_grid.graph_path_length( - node_source=node2, node_target=root_mv) / 1000 + length_from_MV_to_LV_station = ( + nd._mv_grid_districts[0].mv_grid.graph_path_length( + node_source=node2, node_target=root_mv + ) + / 1000 + ) # Iteration through lv load areas for lvgs in nd._mv_grid_districts[0].lv_load_areas(): - for lvgs1 in lvgs.lv_grid_districts(): + for lvgs1 in lvgs.lv_grid_districts(): if lvgs1.lv_grid._station == node2: root_lv = node2 # setting a new root - for node1 in lvgs1.lv_grid._graph.nodes(): + for node1 in lvgs1.lv_grid._graph.nodes(): length_from_LV_staion_to_LV_node = 0 - + # Distance from LV station to LV nodes length_from_LV_staion_to_LV_node = ( - lvgs1.lv_grid.graph_path_length( - node_source=node1, - node_target=root_lv) / 1000) + lvgs1.lv_grid.graph_path_length( + node_source=node1, node_target=root_lv + ) + / 1000 + ) length_from_LV_node_to_MV_substation = 0 - + # total distances in both grids MV and LV length_from_LV_node_to_MV_substation = ( - length_from_MV_to_LV_station - + length_from_LV_staion_to_LV_node) + length_from_MV_to_LV_station + + length_from_LV_staion_to_LV_node + ) # append the total distance to a list - tot_dist.append( - length_from_LV_node_to_MV_substation) - if any(tot_dist): + tot_dist.append(length_from_LV_node_to_MV_substation) + if any(tot_dist): max_length = max(tot_dist) - + # append max lengths of all grids to a list max_length_list.append(max_length) - if any(max_length_list): + if any(max_length_list): # to pick up max of max max_of_max = max(max_length_list) @@ -172,11 +180,14 @@ def analyze_attributes(ding0_files): MV_id_list.append(MV_id) # append the network id to a new list # export results to dataframes - d = {'id': MV_id_list, 'Solar_cumulative_capacity': tccs, - 'Wind_cumulative_capacity': tccw, - 'The_Farthest_node': fnlvmv} # assign lists to columns + d = { + "id": MV_id_list, + "Solar_cumulative_capacity": tccs, + "Wind_cumulative_capacity": tccw, + "The_Farthest_node": fnlvmv, + } # assign lists to columns # not founded networks - are_not_found = {'District_files_that_are_not_found': not_found} + are_not_found = {"District_files_that_are_not_found": not_found} df = pd.DataFrame(d) # dataframe for results @@ -184,24 +195,22 @@ def analyze_attributes(ding0_files): df_are_not_found = pd.DataFrame(are_not_found) # Exporting dataframe to CSV files - df.to_csv(base_path + '/' + 'attributes.csv', sep=',') - df_are_not_found.to_csv(base_path + '/' + 'Not_found_grids.csv', sep=',') + df.to_csv(base_path + "/" + "attributes.csv", sep=",") + df_are_not_found.to_csv(base_path + "/" + "Not_found_grids.csv", sep=",") -def cluster_mv_grids( - no_grids, - cluster_base): +def cluster_mv_grids(no_grids, cluster_base): """ Clusters the MV grids based on the attributes, for a given number of MV grids - + Parameters ---------- ding0_files : :obj:`str` Path to ding0 files no_grids : int Desired number of clusters (of MV grids) - + Returns ------- :pandas:`pandas.DataFrame` @@ -209,11 +218,11 @@ def cluster_mv_grids( """ cluster_base_pu = pd.DataFrame() - + for attribute in cluster_base: attribute_max = cluster_base[attribute].max() cluster_base_pu[attribute] = cluster_base[attribute] / attribute_max - + id_ = [] m = [] for idx, row in cluster_base_pu.iterrows(): @@ -221,23 +230,21 @@ def cluster_mv_grids( f = [] for attribute in row: f.append(attribute) - + m.append(f) - + X = np.array(m) - - logger.info( - 'Used Clustering Attributes: \n {}'.format( - list(cluster_base.columns))) - - no_clusters = no_grids - + + logger.info("Used Clustering Attributes: \n {}".format(list(cluster_base.columns))) + + no_clusters = no_grids + ran_state = 1808 # Starting KMeans clustering kmeans = KMeans(n_clusters=no_clusters, random_state=ran_state) - # Return a label for each point + # Return a label for each point cluster_labels = kmeans.fit_predict(X) # Centers of clusters @@ -252,22 +259,25 @@ def cluster_mv_grids( # Distance from that point to cluster's center (3d coordinates) dist = ( - (X[i][0] - centroids[clus][0]) ** 2 - + (X[i][1] - centroids[clus][1]) ** 2 - + (X[i][2] - centroids[clus][2]) ** 2) ** (1 / 2) + (X[i][0] - centroids[clus][0]) ** 2 + + (X[i][1] - centroids[clus][1]) ** 2 + + (X[i][2] - centroids[clus][2]) ** 2 + ) ** (1 / 2) id_clus_dist.setdefault(clus, []).append({id_[i]: dist}) - + cluster_df = pd.DataFrame( - columns=[ - 'no_of_points_per_cluster', - 'cluster_percentage', - 'the_selected_network_id', - 'represented_grids']) - cluster_df.index.name = 'cluster_id' - + columns=[ + "no_of_points_per_cluster", + "cluster_percentage", + "the_selected_network_id", + "represented_grids", + ] + ) + cluster_df.index.name = "cluster_id" + for key, value in id_clus_dist.items(): - no_points_clus = sum(1 for v in value if v) + no_points_clus = sum(1 for v in value if v) # percentage of points per cluster clus_perc = (no_points_clus / len(X)) * 100 @@ -275,13 +285,14 @@ def cluster_mv_grids( for value_1 in value: id_dist.update(value_1) - # returns the shortest distance point (selected network) + # returns the shortest distance point (selected network) short_dist_net_id_dist = min(id_dist.items(), key=lambda x: x[1]) - + cluster_df.loc[key] = [ - no_points_clus, - round(clus_perc, 2), - short_dist_net_id_dist[0], - list(id_dist.keys())] - + no_points_clus, + round(clus_perc, 2), + short_dist_net_id_dist[0], + list(id_dist.keys()), + ] + return cluster_df diff --git a/ego/tools/plots.py b/ego/tools/plots.py index cb4018d5..40f7a90b 100644 --- a/ego/tools/plots.py +++ b/ego/tools/plots.py @@ -21,54 +21,73 @@ eGo results. """ +import os + import numpy as np import pandas as pd -import os + geopandas = True -if not 'READTHEDOCS' in os.environ: - from etrago.tools.plot import (plot_line_loading, plot_stacked_gen, - add_coordinates, curtailment, gen_dist, - storage_distribution, - plot_voltage, plot_residual_load, coloring) - from ego.tools.economics import etrago_convert_overnight_cost - from ego.tools.utilities import open_oedb_session - from pypsa import Network as PyPSANetwork +if not "READTHEDOCS" in os.environ: + from math import log10, sqrt + import pyproj as proj - from math import sqrt, log10 - from shapely.geometry import Polygon, Point, MultiPolygon + + from etrago.tools.plot import ( + add_coordinates, + coloring, + curtailment, + gen_dist, + plot_line_loading, + plot_residual_load, + plot_stacked_gen, + plot_voltage, + storage_distribution, + ) from geoalchemy2 import * + from pypsa import Network as PyPSANetwork + from shapely.geometry import MultiPolygon, Point, Polygon + + from ego.tools.economics import etrago_convert_overnight_cost + from ego.tools.utilities import open_oedb_session + try: - import geopandas as gpd + import branca.colormap as cm import folium + import geopandas as gpd + from folium import plugins - from folium.plugins import FloatImage from folium.features import CustomIcon - import branca.colormap as cm + from folium.plugins import FloatImage except: geopandas = False import oedialect import webbrowser import subprocess from egoio.db_tables.model_draft import ( - EgoGridMvGriddistrict, RenpassGisParameterRegion) + EgoGridMvGriddistrict, + RenpassGisParameterRegion, + ) from egoio.db_tables.grid import EgoDpMvGriddistrict import matplotlib.pyplot as plt import matplotlib as mpl import matplotlib.colors as mcolors import logging -logger = logging.getLogger('ego') -__copyright__ = "Flensburg University of Applied Sciences, Europa-Universität"\ +logger = logging.getLogger("ego") + +__copyright__ = ( + "Flensburg University of Applied Sciences, Europa-Universität" "Flensburg, Centre for Sustainable Energy Systems" +) __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolfbunke" # plot colore of Carriers def carriers_colore(): - """ Return matplotlib colore set per carrier (technologies of + """Return matplotlib colore set per carrier (technologies of generators) of eTraGo. Returns @@ -77,51 +96,54 @@ def carriers_colore(): List of carriers and matplotlib colores """ - colors = {'biomass': 'green', - 'coal': 'k', - 'gas': 'orange', - 'eeg_gas': 'olive', - 'geothermal': 'purple', - 'lignite': 'brown', - 'oil': 'darkgrey', - 'other_non_renewable': 'pink', - 'reservoir': 'navy', - 'run_of_river': 'aqua', - 'pumped_storage': 'steelblue', - 'solar': 'yellow', - 'uranium': 'lime', - 'waste': 'sienna', - 'wind': 'skyblue', - 'slack': 'pink', - 'load shedding': 'red', - 'nan': 'm', - 'imports': 'salmon', - '': 'm'} + colors = { + "biomass": "green", + "coal": "k", + "gas": "orange", + "eeg_gas": "olive", + "geothermal": "purple", + "lignite": "brown", + "oil": "darkgrey", + "other_non_renewable": "pink", + "reservoir": "navy", + "run_of_river": "aqua", + "pumped_storage": "steelblue", + "solar": "yellow", + "uranium": "lime", + "waste": "sienna", + "wind": "skyblue", + "slack": "pink", + "load shedding": "red", + "nan": "m", + "imports": "salmon", + "": "m", + } return colors def ego_colore(): - """ Get the four eGo colores + """Get the four eGo colores Returns ------- colors : :obj:`dict` List of eGo matplotlib hex colores """ - colors = {'egoblue1': '#1F567D', - 'egoblue2': '#84A2B8', - 'egoblue3': '#A3B9C9', - 'egoblue4': '#C7D5DE' - } + colors = { + "egoblue1": "#1F567D", + "egoblue2": "#84A2B8", + "egoblue3": "#A3B9C9", + "egoblue4": "#C7D5DE", + } return colors -def plot_storage_expansion(ego, filename=None, dpi=300, - column='overnight_costs', - scaling=1): - """ Plot line expantion +def plot_storage_expansion( + ego, filename=None, dpi=300, column="overnight_costs", scaling=1 +): + """Plot line expantion Parameters ---------- @@ -148,38 +170,45 @@ def plot_storage_expansion(ego, filename=None, dpi=300, json_file = ego.json_file # get storage values - if 'storage' in ego.json_file['eTraGo']['extendable']: - storage_inv = network.storage_units[network.storage_units. - capital_cost > 0.] - storage_inv['investment_costs'] = (storage_inv.capital_cost * - storage_inv.p_nom_opt) - storage_inv['overnight_costs'] = etrago_convert_overnight_cost( - storage_inv['investment_costs'], json_file) + if "storage" in ego.json_file["eTraGo"]["extendable"]: + storage_inv = network.storage_units[network.storage_units.capital_cost > 0.0] + storage_inv["investment_costs"] = ( + storage_inv.capital_cost * storage_inv.p_nom_opt + ) + storage_inv["overnight_costs"] = etrago_convert_overnight_cost( + storage_inv["investment_costs"], json_file + ) msd_max = storage_inv[column].max() msd_median = storage_inv[column].median() msd_min = storage_inv[column].min() - if (msd_max - msd_min) > 1.e+5: + if (msd_max - msd_min) > 1.0e5: if msd_max != 0: LabelVal = int(log10(msd_max)) else: LabelVal = 0 if LabelVal < 0: - LabelUnit = '€' - msd_max, msd_median, msd_min = msd_max * \ - 1000, msd_median * 1000, msd_min * 1000 + LabelUnit = "€" + msd_max, msd_median, msd_min = ( + msd_max * 1000, + msd_median * 1000, + msd_min * 1000, + ) storage_inv[column] = storage_inv[column] * 1000 elif LabelVal < 3: - LabelUnit = 'k €' + LabelUnit = "k €" else: - LabelUnit = 'M €' - msd_max, msd_median, msd_min = msd_max / \ - 1000, msd_median / 1000, msd_min / 1000 + LabelUnit = "M €" + msd_max, msd_median, msd_min = ( + msd_max / 1000, + msd_median / 1000, + msd_min / 1000, + ) storage_inv[column] = storage_inv[column] / 1000 else: - LabelUnit = '€' + LabelUnit = "€" # start plotting figsize = 6, 6 @@ -187,48 +216,53 @@ def plot_storage_expansion(ego, filename=None, dpi=300, bus_sizes = storage_inv[column] * scaling - if column == 'investment_costs': - title = 'Annualized Storage costs per timestep' - ltitel = 'Storage costs' - if column == 'overnight_costs': - title = 'Total Expansion Costs Overnight' - ltitel = 'Storage costs' - if column == 'p_nom_opt': - title = 'Storage Expansion in MVA' - ltitel = 'Storage size' - LabelUnit = 'kW' - if column not in ['investment_costs', 'overnight_costs', 'p_nom_opt']: - title = 'unknown' - ltitel = 'unknown' - LabelUnit = 'unknown' + if column == "investment_costs": + title = "Annualized Storage costs per timestep" + ltitel = "Storage costs" + if column == "overnight_costs": + title = "Total Expansion Costs Overnight" + ltitel = "Storage costs" + if column == "p_nom_opt": + title = "Storage Expansion in MVA" + ltitel = "Storage size" + LabelUnit = "kW" + if column not in ["investment_costs", "overnight_costs", "p_nom_opt"]: + title = "unknown" + ltitel = "unknown" + LabelUnit = "unknown" if sum(storage_inv[column]) == 0: - sc = network.plot(bus_sizes=0, - ax=ax, - title="No storage expantion") + sc = network.plot(bus_sizes=0, ax=ax, title="No storage expantion") else: sc = network.plot( bus_sizes=bus_sizes, - bus_colors='g', + bus_colors="g", # bus_cmap= # line_colors='gray', title=title, - line_widths=0.3 + line_widths=0.3, ) ax.set_alpha(0.4) # add legend for area in [msd_max, msd_median, msd_min]: - plt.scatter([], [], c='white', s=area * scaling, - label='= ' + str(round(area, 0)) + LabelUnit + ' ') + plt.scatter( + [], + [], + c="white", + s=area * scaling, + label="= " + str(round(area, 0)) + LabelUnit + " ", + ) - plt.legend(scatterpoints=1, - labelspacing=1, - title=ltitel, - loc='upper left', - shadow=True, - fontsize='x-large') + plt.legend( + scatterpoints=1, + labelspacing=1, + title=ltitel, + loc="upper left", + shadow=True, + fontsize="x-large", + ) ax.autoscale(tight=True) @@ -237,12 +271,12 @@ def plot_storage_expansion(ego, filename=None, dpi=300, else: fig = ax.get_figure() fig.set_size_inches(10, 8, forward=True) - fig.savefig(filename, dpi=dpi) + fig.savefig(filename, dpi=dpi) plt.close() -def plot_line_expansion(ego, filename=None, dpi=300, column='overnight_costs'): - """ Plot line expantion +def plot_line_expansion(ego, filename=None, dpi=300, column="overnight_costs"): + """Plot line expantion Parameters ---------- @@ -267,18 +301,21 @@ def plot_line_expansion(ego, filename=None, dpi=300, column='overnight_costs'): json_file = ego.json_file # get values - if 'network' in ego.json_file['eTraGo']['extendable']: - network.lines['s_nom_expansion'] = network.lines.s_nom_opt.subtract( - network.lines.s_nom, axis='index') - network.lines['investment_costs'] = network.lines.s_nom_expansion.\ - multiply(network.lines.capital_cost, axis='index') - network.lines['overnight_costs'] = etrago_convert_overnight_cost( - network.lines['investment_costs'], json_file) + if "network" in ego.json_file["eTraGo"]["extendable"]: + network.lines["s_nom_expansion"] = network.lines.s_nom_opt.subtract( + network.lines.s_nom, axis="index" + ) + network.lines["investment_costs"] = network.lines.s_nom_expansion.multiply( + network.lines.capital_cost, axis="index" + ) + network.lines["overnight_costs"] = etrago_convert_overnight_cost( + network.lines["investment_costs"], json_file + ) else: - network.lines['s_nom_expansion'] = None - network.lines['investment_costs'] = None - network.lines['overnight_costs'] = None + network.lines["s_nom_expansion"] = None + network.lines["investment_costs"] = None + network.lines["overnight_costs"] = None # start plotting figsize = 10, 8 @@ -286,40 +323,40 @@ def plot_line_expansion(ego, filename=None, dpi=300, column='overnight_costs'): cmap = plt.cm.jet - if column == 's_nom_expansion': + if column == "s_nom_expansion": line_value = network.lines[column] title = "Line expansion in MVA" - if column == 'overnight_costs': + if column == "overnight_costs": line_value = network.lines[column] title = "Total Expansion Costs in € per line" - if column == 'investment_costs': + if column == "investment_costs": line_value = network.lines[column] title = "Annualized Expansion Costs in € per line and time step" - line_widths = (line_value/line_value.max()) + line_widths = line_value / line_value.max() - lc = network.plot(ax=ax, line_colors=line_value, - line_cmap=cmap, - title=title, - line_widths=line_widths) + lc = network.plot( + ax=ax, + line_colors=line_value, + line_cmap=cmap, + title=title, + line_widths=line_widths, + ) - boundaries = [min(line_value), - max(line_value)] + boundaries = [min(line_value), max(line_value)] v = np.linspace(boundaries[0], boundaries[1], 101) print(v.dtype.name) # colorbar - cb = plt.colorbar(lc[1], boundaries=v, - ticks=v[0:101:10], - ax=ax) + cb = plt.colorbar(lc[1], boundaries=v, ticks=v[0:101:10], ax=ax) cb.set_clim(vmin=boundaries[0], vmax=boundaries[1]) - if column == 's_nom_expansion': - cb.set_label('Expansion in MVA per line') - if column == 'overnight_costs': - cb.set_label('Total Expansion Costs in € per line') - if column == 'investment_costs': - cb.set_label('Annualized Expansion Costs in € per line') + if column == "s_nom_expansion": + cb.set_label("Expansion in MVA per line") + if column == "overnight_costs": + cb.set_label("Total Expansion Costs in € per line") + if column == "investment_costs": + cb.set_label("Annualized Expansion Costs in € per line") ax.autoscale(tight=True) @@ -328,7 +365,7 @@ def plot_line_expansion(ego, filename=None, dpi=300, column='overnight_costs'): else: fig = ax.get_figure() fig.set_size_inches(10, 8, forward=True) - fig.savefig(filename, dpi=dpi) + fig.savefig(filename, dpi=dpi) plt.close() @@ -358,51 +395,44 @@ def plot_grid_storage_investment(costs_df, filename, display, var=None): bar_width = 0.35 opacity = 0.4 - if var == 'overnight_cost': - tic = costs_df[['component', - 'overnight_costs', - 'voltage_level', - 'differentiation']] - tic.set_index(['voltage_level', 'component', - 'differentiation'], inplace=True) - ax = tic.unstack().plot(kind='bar', - stacked=False, - - rot=0, - color=([colors.get(key) - for key in - ['egoblue1', - 'egoblue2', - 'egoblue4']]), - legend=False) + if var == "overnight_cost": + tic = costs_df[ + ["component", "overnight_costs", "voltage_level", "differentiation"] + ] + tic.set_index(["voltage_level", "component", "differentiation"], inplace=True) + ax = tic.unstack().plot( + kind="bar", + stacked=False, + rot=0, + color=([colors.get(key) for key in ["egoblue1", "egoblue2", "egoblue4"]]), + legend=False, + ) ax.set_ylabel("Overnight costs of simulation") - ax.set_title("Total costs of simulation, " - "voltage level and component", y=1.08) + ax.set_title( + "Total costs of simulation, " "voltage level and component", y=1.08 + ) else: - tic = costs_df[['component', - 'capital_cost', - 'voltage_level', - 'differentiation']] - tic.set_index(['voltage_level', 'component', - 'differentiation'], inplace=True) - ax = tic.unstack().plot(kind='bar', - rot=0, - stacked=False, - - color=([colors.get(key) - for key in - ['egoblue1', - 'egoblue2', - 'egoblue3']]), - legend=False) + tic = costs_df[ + ["component", "capital_cost", "voltage_level", "differentiation"] + ] + tic.set_index(["voltage_level", "component", "differentiation"], inplace=True) + ax = tic.unstack().plot( + kind="bar", + rot=0, + stacked=False, + color=([colors.get(key) for key in ["egoblue1", "egoblue2", "egoblue3"]]), + legend=False, + ) ax.set_ylabel("Annualized costs per simulation periods") - ax.set_title("Annualized costs per simulation periods, " - "voltage level and component", y=1.08) + ax.set_title( + "Annualized costs per simulation periods, " "voltage level and component", + y=1.08, + ) - ax.set_xlabel('Voltage level and component') + ax.set_xlabel("Voltage level and component") ax.set_yscale("symlog") - ax.legend(('cross-border', 'domestic', 'foreign')) + ax.legend(("cross-border", "domestic", "foreign")) ax.autoscale() if display is True: @@ -410,7 +440,7 @@ def plot_grid_storage_investment(costs_df, filename, display, var=None): else: fig = ax.get_figure() fig.set_size_inches(10, 8, forward=True) - fig.savefig(filename, dpi=100) + fig.savefig(filename, dpi=100) plt.close() @@ -433,29 +463,29 @@ def power_price_plot(ego, filename, display): https://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.show """ plt.rcdefaults() -# colors = ego_colore() + # colors = ego_colore() carrier_colors = coloring() fig, ax = plt.subplots() # plot power_price - prc = ego.etrago.generator['power_price'] + prc = ego.etrago.generator["power_price"] bar_width = 0.35 opacity = 0.4 - ind = np.arange(len(prc.index)) # the x locations for the groups - width = 0.35 # the width of the bars: can also be len(x) sequence + ind = np.arange(len(prc.index)) # the x locations for the groups + width = 0.35 # the width of the bars: can also be len(x) sequence plt_colors = [carrier_colors[carrier] for carrier in prc.index] -# plt_colors = colors['egoblue1'] + # plt_colors = colors['egoblue1'] - ax.barh(ind, prc, align='center', color=plt_colors) + ax.barh(ind, prc, align="center", color=plt_colors) ax.set_yticks(ind) ax.set_yticklabels(prc.index) ax.invert_yaxis() - ax.set_xlabel('Power price in €/MWh') - ax.set_title('Power Costs per Carrier') + ax.set_xlabel("Power price in €/MWh") + ax.set_title("Power Costs per Carrier") ax.autoscale(tight=True) @@ -464,7 +494,7 @@ def power_price_plot(ego, filename, display): else: fig = ax.get_figure() fig.set_size_inches(10, 8, forward=True) - fig.savefig(filename, dpi=100) + fig.savefig(filename, dpi=100) def plot_storage_use(ego, filename, display): @@ -486,18 +516,15 @@ def plot_storage_use(ego, filename, display): """ colors = ego_colore() - ax = ego.etrago.\ - storage_charges[['charge', 'discharge']].plot(kind='bar', - title="Storage usage", - stacked=True, - color=([colors.get(key) - for key in - ['egoblue1', - 'egoblue2']]), - figsize=( - 15, 10), - legend=True, - fontsize=12) + ax = ego.etrago.storage_charges[["charge", "discharge"]].plot( + kind="bar", + title="Storage usage", + stacked=True, + color=([colors.get(key) for key in ["egoblue1", "egoblue2"]]), + figsize=(15, 10), + legend=True, + fontsize=12, + ) ax.set_xlabel("Kind of Storage", fontsize=12) ax.set_ylabel("Charge and Discharge in MWh", fontsize=12) ax.autoscale(tight=False) @@ -508,7 +535,7 @@ def plot_storage_use(ego, filename, display): fig = ax.get_figure() fig.set_size_inches(10, 8, forward=True) fig.subplots_adjust(bottom=0.25) - fig.savefig(filename, dpi=100) + fig.savefig(filename, dpi=100) def get_country(session, region=None): @@ -530,35 +557,38 @@ def get_country(session, region=None): if region is None: # Define regions 'FR', - region = ['DE', 'DK', 'BE', 'LU', - 'NO', 'PL', 'CH', 'CZ', 'SE', 'NL'] + region = ["DE", "DK", "BE", "LU", "NO", "PL", "CH", "CZ", "SE", "NL"] else: region # get database tabel - query = session.query(RenpassGisParameterRegion.gid, - RenpassGisParameterRegion.stat_level, - RenpassGisParameterRegion.u_region_id, - RenpassGisParameterRegion.geom, - RenpassGisParameterRegion.geom_point) + query = session.query( + RenpassGisParameterRegion.gid, + RenpassGisParameterRegion.stat_level, + RenpassGisParameterRegion.u_region_id, + RenpassGisParameterRegion.geom, + RenpassGisParameterRegion.geom_point, + ) # get regions by query and filter - Regions = [(gid, u_region_id, stat_level, - shape.to_shape(geom), - shape.to_shape(geom_point)) for gid, u_region_id, stat_level, - geom, geom_point in query.filter( - RenpassGisParameterRegion.u_region_id. - in_(region)).all()] + Regions = [ + (gid, u_region_id, stat_level, shape.to_shape(geom), shape.to_shape(geom_point)) + for gid, u_region_id, stat_level, geom, geom_point in query.filter( + RenpassGisParameterRegion.u_region_id.in_(region) + ).all() + ] # define SRID - crs = {'init': 'epsg:4326'} + crs = {"init": "epsg:4326"} country = gpd.GeoDataFrame( - Regions, columns=['gid', 'stat_level', 'u_region_id', - 'geometry', 'point_geom'], crs=crs) + Regions, + columns=["gid", "stat_level", "u_region_id", "geometry", "point_geom"], + crs=crs, + ) return country def prepareGD(session, subst_id=None, version=None): - """ Get MV grid districts for plotting form oedb. + """Get MV grid districts for plotting form oedb. Parameters ---------- @@ -577,45 +607,66 @@ def prepareGD(session, subst_id=None, version=None): if version: - query = session.query(EgoDpMvGriddistrict.subst_id, - EgoDpMvGriddistrict.geom) + query = session.query(EgoDpMvGriddistrict.subst_id, EgoDpMvGriddistrict.geom) if isinstance(subst_id, list): - Regions = [(subst_id, shape.to_shape(geom)) for subst_id, geom in - query.filter(EgoDpMvGriddistrict.version == version, - EgoDpMvGriddistrict.subst_id.in_( - subst_id)).all()] + Regions = [ + (subst_id, shape.to_shape(geom)) + for subst_id, geom in query.filter( + EgoDpMvGriddistrict.version == version, + EgoDpMvGriddistrict.subst_id.in_(subst_id), + ).all() + ] elif subst_id == "all": - Regions = [(subst_id, shape.to_shape(geom)) for subst_id, geom in - query.filter(EgoDpMvGriddistrict.version == - version).all()] + Regions = [ + (subst_id, shape.to_shape(geom)) + for subst_id, geom in query.filter( + EgoDpMvGriddistrict.version == version + ).all() + ] else: # ToDo query doesn't looks stable - Regions = [(subst_id, shape.to_shape(geom)) for subst_id, geom in - query.filter(EgoDpMvGriddistrict.version == - version).all()] + Regions = [ + (subst_id, shape.to_shape(geom)) + for subst_id, geom in query.filter( + EgoDpMvGriddistrict.version == version + ).all() + ] # toDo add values of sub_id etc. to popup else: # from model_draft - query = session.query(EgoGridMvGriddistrict.subst_id, - EgoGridMvGriddistrict.geom) - Regions = [(subst_id, shape.to_shape(geom)) for subst_id, geom in - query.filter(EgoGridMvGriddistrict.subst_id.in_( - subst_id)).all()] - - crs = {'init': 'epsg:3035'} - region = gpd.GeoDataFrame( - Regions, columns=['subst_id', 'geometry'], crs=crs) - region = region.to_crs({'init': 'epsg:4326'}) + query = session.query( + EgoGridMvGriddistrict.subst_id, EgoGridMvGriddistrict.geom + ) + Regions = [ + (subst_id, shape.to_shape(geom)) + for subst_id, geom in query.filter( + EgoGridMvGriddistrict.subst_id.in_(subst_id) + ).all() + ] + + crs = {"init": "epsg:3035"} + region = gpd.GeoDataFrame(Regions, columns=["subst_id", "geometry"], crs=crs) + region = region.to_crs({"init": "epsg:4326"}) return region -def plot_edisgo_cluster(ego, filename, region=['DE'], display=False, dpi=150, - add_ehv_storage=False, grid_choice=None, title="", - cmap="jet", labelsize=10, fontsize=10): +def plot_edisgo_cluster( + ego, + filename, + region=["DE"], + display=False, + dpi=150, + add_ehv_storage=False, + grid_choice=None, + title="", + cmap="jet", + labelsize=10, + fontsize=10, +): """Plot the Clustering of selected Dingo networks Parameters @@ -645,47 +696,47 @@ def plot_edisgo_cluster(ego, filename, region=['DE'], display=False, dpi=150, """ session = ego.session - version = ego.json_file['eTraGo']['gridversion'] + version = ego.json_file["eTraGo"]["gridversion"] # get cluster if grid_choice: cluster = pd.read_csv(grid_choice, index_col=0) - cluster['represented_grids'] = cluster.apply( - lambda x: eval(x['represented_grids']), axis=1) + cluster["represented_grids"] = cluster.apply( + lambda x: eval(x["represented_grids"]), axis=1 + ) else: cluster = ego.edisgo.grid_choice - cluster = cluster.rename( - columns={"the_selected_network_id": "subst_id"}) + cluster = cluster.rename(columns={"the_selected_network_id": "subst_id"}) cluster_id = list(cluster.subst_id) # get country Polygon cnty = get_country(session, region=region) # get grid districts singel - if ego.json_file['eGo']['eDisGo'] is True: + if ego.json_file["eGo"]["eDisGo"] is True: gridcluster = prepareGD(session, cluster_id, version) - gridcluster = gridcluster.merge(cluster, on='subst_id') + gridcluster = gridcluster.merge(cluster, on="subst_id") # add percentage of grid representation - gridcluster['percentage'] = ((gridcluster.no_of_points_per_cluster / - gridcluster.no_of_points_per_cluster.sum())*100) - gridcluster['percentage'] = gridcluster['percentage'].astype( - float).round(2) + gridcluster["percentage"] = ( + gridcluster.no_of_points_per_cluster + / gridcluster.no_of_points_per_cluster.sum() + ) * 100 + gridcluster["percentage"] = gridcluster["percentage"].astype(float).round(2) # get represented grids - repre_grids = pd.DataFrame(columns=['subst_id', - 'geometry', - 'cluster_id', - 'style']) + repre_grids = pd.DataFrame( + columns=["subst_id", "geometry", "cluster_id", "style"] + ) for cluster in gridcluster.index: rep_id = gridcluster.represented_grids[cluster] # represented_grids repre_grid = prepareGD(session, rep_id, version) - repre_grid['cluster_id'] = gridcluster.subst_id[cluster] + repre_grid["cluster_id"] = gridcluster.subst_id[cluster] repre_grids = repre_grids.append(repre_grid, ignore_index=True) # add common SRID - crs = {'init': 'epsg:4326'} + crs = {"init": "epsg:4326"} repre_grids = gpd.GeoDataFrame(repre_grids, crs=crs) # get all MV grids @@ -696,29 +747,35 @@ def plot_edisgo_cluster(ego, filename, region=['DE'], display=False, dpi=150, figsize = 5, 5 fig, ax = plt.subplots(1, 1, figsize=(figsize)) - cnty.plot(ax=ax, color='white', - edgecolor='whitesmoke', alpha=0.5, linewidth=0.1) - mvgrids.plot(ax=ax, color='white', alpha=0.1, linewidth=0.1) + cnty.plot(ax=ax, color="white", edgecolor="whitesmoke", alpha=0.5, linewidth=0.1) + mvgrids.plot(ax=ax, color="white", alpha=0.1, linewidth=0.1) - if ego.json_file['eGo']['eDisGo'] is True: + if ego.json_file["eGo"]["eDisGo"] is True: - repre_grids.plot(ax=ax, column='cluster_id', - cmap=cmap, - edgecolor='whitesmoke', - linewidth=0.005, - alpha=1, - legend=False) + repre_grids.plot( + ax=ax, + column="cluster_id", + cmap=cmap, + edgecolor="whitesmoke", + linewidth=0.005, + alpha=1, + legend=False, + ) # subplot - gridcluster.plot(ax=ax, column='percentage', - cmap=cmap, - edgecolor='black', - linewidth=1, - legend=True) + gridcluster.plot( + ax=ax, + column="percentage", + cmap=cmap, + edgecolor="black", + linewidth=1, + legend=True, + ) # add storage distribution if add_ehv_storage: - _storage_distribution(ego.etrago.network, scaling=1, filename=None, - ax=ax, fig=fig) + _storage_distribution( + ego.etrago.network, scaling=1, filename=None, ax=ax, fig=fig + ) ax.set_title(title) # ax.legend(title="id of cluster representative") @@ -727,8 +784,7 @@ def plot_edisgo_cluster(ego, filename, region=['DE'], display=False, dpi=150, # cb = plt.colorbar(ax) # cb.ax.tick_params(labelsize=17) - ax.set_ylabel("weighting of MV grid cluster in %", - fontsize=fontsize, rotation=270) + ax.set_ylabel("weighting of MV grid cluster in %", fontsize=fontsize, rotation=270) ax.yaxis.set_label_coords(1.2, 0.5) ax.autoscale(tight=True) @@ -738,7 +794,7 @@ def plot_edisgo_cluster(ego, filename, region=['DE'], display=False, dpi=150, else: fig = ax.get_figure() fig.set_size_inches(10, 8, forward=True) - fig.savefig(filename, dpi=dpi) + fig.savefig(filename, dpi=dpi) plt.close() @@ -762,43 +818,43 @@ def igeoplot(ego, tiles=None, geoloc=None, save_image=False): ------- plot: html HTML file with .js plot - """ + """ network = ego.etrago.network session = open_oedb_session(ego) # get scenario name from args - scn_name = ego.json_file['eTraGo']['scn_name'] - version = ego.json_file['eTraGo']['gridversion'] + scn_name = ego.json_file["eTraGo"]["scn_name"] + version = ego.json_file["eTraGo"]["gridversion"] # define SRID - crs = {'init': 'epsg:4326'} + crs = {"init": "epsg:4326"} if geoloc is None: geoloc = [network.buses.y.mean(), network.buses.x.mean()] - mp = folium.Map(tiles=None, location=geoloc, - control_scale=True, zoom_start=6) + mp = folium.Map(tiles=None, location=geoloc, control_scale=True, zoom_start=6) # add Nasa light background - if tiles == 'Nasa': - tiles = ("https://map1.vis.earthdata.nasa.gov/wmts-webmerc/" + - "VIIRS_CityLights_2012/default/GoogleMapsCompatible_" + - "Level8/{z}/{y}/{x}.jpg") - attr = ('© OpenStreetMap contributors, © CartoDB') + if tiles == "Nasa": + tiles = ( + "https://map1.vis.earthdata.nasa.gov/wmts-webmerc/" + + "VIIRS_CityLights_2012/default/GoogleMapsCompatible_" + + "Level8/{z}/{y}/{x}.jpg" + ) + attr = '© OpenStreetMap contributors, © CartoDB' folium.raster_layers.TileLayer(tiles=tiles, attr=attr).add_to(mp) else: - attr = ('© OpenStreetMap contributors, © OpenEnergy-Platform') + attr = '© OpenStreetMap contributors, © OpenEnergy-Platform' - folium.raster_layers.TileLayer('OpenStreetMap', attr=attr).add_to(mp) + folium.raster_layers.TileLayer("OpenStreetMap", attr=attr).add_to(mp) # Legend name - bus_group = folium.FeatureGroup( - name='Bus information (ehv/hv)') # , show=True + bus_group = folium.FeatureGroup(name="Bus information (ehv/hv)") # , show=True # create icon - #url = 'https://raw.githubusercontent.com/openego/eGo/master/doc/images/{}'.format - #icon_image = url('trafo.png') + # url = 'https://raw.githubusercontent.com/openego/eGo/master/doc/images/{}'.format + # icon_image = url('trafo.png') # bus_icon = CustomIcon(icon_image, # icon_size=(27, 47)) @@ -818,36 +874,43 @@ def igeoplot(ego, tiles=None, geoloc=None, save_image=False): v_mag_pu_max: {}
sub_network: {}
Version: {}
- """.format(row.name, scn_name, row['carrier'], - row['control'], row['type'], row['v_nom'], - row['v_mag_pu_set'], - row['v_mag_pu_min'], row['v_mag_pu_max'], - row['sub_network'], version) + """.format( + row.name, + scn_name, + row["carrier"], + row["control"], + row["type"], + row["v_nom"], + row["v_mag_pu_set"], + row["v_mag_pu_min"], + row["v_mag_pu_max"], + row["sub_network"], + version, + ) # add Popup values use HTML for formating - folium.Marker([row["y"], row["x"]], popup=popup - ).add_to(bus_group) # icon=bus_icon + folium.Marker([row["y"], row["x"]], popup=popup).add_to( + bus_group + ) # icon=bus_icon - logger.info('Added Busses') + logger.info("Added Busses") def convert_to_hex(rgba_color): - """Convert rgba colors to hex - """ - red = str(hex(int(rgba_color[0]*255)))[2:].capitalize() - green = str(hex(int(rgba_color[1]*255)))[2:].capitalize() - blue = str(hex(int(rgba_color[2]*255)))[2:].capitalize() + """Convert rgba colors to hex""" + red = str(hex(int(rgba_color[0] * 255)))[2:].capitalize() + green = str(hex(int(rgba_color[1] * 255)))[2:].capitalize() + blue = str(hex(int(rgba_color[2] * 255)))[2:].capitalize() - if blue == '0': - blue = '00' - if red == '0': - red = '00' - if green == '0': - green = '00' + if blue == "0": + blue = "00" + if red == "0": + red = "00" + if green == "0": + green = "00" - return '#' + red + green + blue + return "#" + red + green + blue # Prepare lines - line_group = folium.FeatureGroup( - name='Line Loading (ehv/hv)') # , show=False + line_group = folium.FeatureGroup(name="Line Loading (ehv/hv)") # , show=False # get line Coordinates x0 = network.lines.bus0.map(network.buses.x) @@ -861,54 +924,60 @@ def convert_to_hex(rgba_color): cols = list(network.lines.columns) # color map lines - colormap = cm.linear.YlOrRd_09.scale( - lines.s_nom.min(), lines.s_nom.max()).to_step(6) + colormap = cm.linear.YlOrRd_09.scale(lines.s_nom.min(), lines.s_nom.max()).to_step( + 6 + ) # add parameter for line in network.lines.index: popup = """ Line: {}
- version: {}
""".format(line, version) + version: {}
""".format( + line, version + ) for col in cols: popup += """ {}: {}
""".format(col, lines[col][line]) # change colore function - l_color = colormapper_lines( - colormap, lines, line, column="s_nom") + l_color = colormapper_lines(colormap, lines, line, column="s_nom") # ToDo make it more generic - folium.PolyLine(([y0[line], x0[line]], [y1[line], x1[line]]), - popup=popup, color=convert_to_hex(l_color)).\ - add_to(line_group) + folium.PolyLine( + ([y0[line], x0[line]], [y1[line], x1[line]]), + popup=popup, + color=convert_to_hex(l_color), + ).add_to(line_group) # Add results # add expansion costs per line lines = network.lines - if 'network' in ego.json_file['eTraGo']['extendable']: - lines['s_nom_expansion'] = lines.s_nom_opt.subtract( - lines.s_nom, axis='index') - lines['annuity'] = lines.s_nom_expansion.multiply( - lines.capital_cost, axis='index') - lines['overnight_cost'] = etrago_convert_overnight_cost( - lines['annuity'], - ego.json_file, t=40, p=0.05) - lines['overnight_cost'] = lines['overnight_cost'].astype(float).round(0) + if "network" in ego.json_file["eTraGo"]["extendable"]: + lines["s_nom_expansion"] = lines.s_nom_opt.subtract(lines.s_nom, axis="index") + lines["annuity"] = lines.s_nom_expansion.multiply( + lines.capital_cost, axis="index" + ) + lines["overnight_cost"] = etrago_convert_overnight_cost( + lines["annuity"], ego.json_file, t=40, p=0.05 + ) + lines["overnight_cost"] = lines["overnight_cost"].astype(float).round(0) else: - lines['s_nom_expansion'] = 0. - lines['annuity'] = 0. - lines['overnight_cost'] = 0. + lines["s_nom_expansion"] = 0.0 + lines["annuity"] = 0.0 + lines["overnight_cost"] = 0.0 # Prepare lines line_results_group = folium.FeatureGroup( - name='Line costs by annuity costs (ehv/hv)') + name="Line costs by annuity costs (ehv/hv)" + ) # color map lines colormap2 = cm.linear.YlGn_09.scale( - lines.annuity.min(), lines.annuity.max()).to_step(4) + lines.annuity.min(), lines.annuity.max() + ).to_step(4) # add parameter cols = list(ego.etrago.network.lines.columns) - res = ('overnight_cost', 's_nom_expansion', 'annuity') - unit = ('EUR', 'MVA', 'EUR') + res = ("overnight_cost", "s_nom_expansion", "annuity") + unit = ("EUR", "MVA", "EUR") cols = [x for x in cols if x not in res] for line in network.lines.index: @@ -916,7 +985,9 @@ def convert_to_hex(rgba_color): popup = """ Line: {}
version: {}

- Line parameter:
""".format(line, version) + Line parameter:
""".format( + line, version + ) for col in cols: popup += """ {}: {}
""".format(col, lines[col][line]) @@ -924,39 +995,49 @@ def convert_to_hex(rgba_color): popup += """
Results:
""" for idx, val in enumerate(res): - popup += """{}: {:,} in {}
""".format(val, - lines[val][line], - unit[idx]) + popup += """{}: {:,} in {}
""".format(val, lines[val][line], unit[idx]) # change colore function - lr_color = colormapper_lines( - colormap2, lines, line, column="annuity") + lr_color = colormapper_lines(colormap2, lines, line, column="annuity") # ToDo make it more generic - folium.PolyLine(([y0[line], x0[line]], [y1[line], x1[line]]), - popup=popup, - color=convert_to_hex(lr_color) - ).add_to(line_results_group) + folium.PolyLine( + ([y0[line], x0[line]], [y1[line], x1[line]]), + popup=popup, + color=convert_to_hex(lr_color), + ).add_to(line_results_group) - logger.info('Added Lines') + logger.info("Added Lines") # Create ehv/hv storage expantion plot - store_group = folium.FeatureGroup( - name='Storage expantion (ehv/hv)') # , show=True + store_group = folium.FeatureGroup(name="Storage expantion (ehv/hv)") # , show=True - stores = network.storage_units[network.storage_units.carrier == - 'extendable_storage'] + stores = network.storage_units[ + network.storage_units.carrier == "extendable_storage" + ] # differentiation of storage units batteries = stores[stores.max_hours == 6] hydrogen = stores[stores.max_hours == 168] # sum by type and bus - storage_distribution = network.storage_units.p_nom_opt[stores.index].groupby( - network.storage_units.bus).sum().reindex(network.buses.index, fill_value=0.) - battery_distribution = network.storage_units.p_nom_opt[batteries.index].groupby( - network.storage_units.bus).sum().reindex(network.buses.index, fill_value=0.) - hydrogen_distribution = network.storage_units.p_nom_opt[hydrogen.index].groupby( - network.storage_units.bus).sum().reindex(network.buses.index, fill_value=0.) + storage_distribution = ( + network.storage_units.p_nom_opt[stores.index] + .groupby(network.storage_units.bus) + .sum() + .reindex(network.buses.index, fill_value=0.0) + ) + battery_distribution = ( + network.storage_units.p_nom_opt[batteries.index] + .groupby(network.storage_units.bus) + .sum() + .reindex(network.buses.index, fill_value=0.0) + ) + hydrogen_distribution = ( + network.storage_units.p_nom_opt[hydrogen.index] + .groupby(network.storage_units.bus) + .sum() + .reindex(network.buses.index, fill_value=0.0) + ) # add Coordinates sto_x = stores.bus.map(network.buses.x) @@ -970,44 +1051,49 @@ def convert_to_hex(rgba_color): popup = """ Storage: {}
version: {}

- Parameter:
""".format(store, version) + Parameter:
""".format( + store, version + ) for col in cols: popup += """ {}: {}
""".format(col, stores[col][store]) # get storage radius by p_nom_opt (MW) if lager as 1 KW - if ((stores['p_nom_opt'][store] > 7.4e-04) & - (stores['capital_cost'][store] > 10)): + if (stores["p_nom_opt"][store] > 7.4e-04) & ( + stores["capital_cost"][store] > 10 + ): - radius = (3**(1+stores['p_nom_opt'][store]/sto_max)) + radius = 3 ** (1 + stores["p_nom_opt"][store] / sto_max) # add singel storage folium.CircleMarker( location=([sto_y[store], sto_x[store]]), radius=radius, popup=popup, - color='#3186cc', + color="#3186cc", fill=True, - fill_color='#3186cc', - weight=1).add_to(store_group) + fill_color="#3186cc", + weight=1, + ).add_to(store_group) - logger.info('Added storages') + logger.info("Added storages") ###################### # add MV line loading # add grid districs - if ego.json_file['eGo']['eDisGo'] is True: + if ego.json_file["eGo"]["eDisGo"] is True: grid_group = folium.FeatureGroup( - name='Represented MV Grid district') # , show=False + name="Represented MV Grid district" + ) # , show=False subst_id = list(ego.edisgo.grid_choice.the_selected_network_id) district = prepareGD(session, subst_id, version) # Add for loop - crs = {'init': 'epsg:4326'} + crs = {"init": "epsg:4326"} for name, row in district.iterrows(): - mv_grid_id = row['subst_id'] + mv_grid_id = row["subst_id"] if not isinstance(ego.edisgo.network[mv_grid_id], str): lv, mv = _get_mv_plot_res(ego, mv_grid_id) @@ -1017,50 +1103,57 @@ def convert_to_hex(rgba_color): pop = """Grid district: {}

MV results:
- """.format(row['subst_id']) + """.format( + row["subst_id"] + ) for idxs in mv.index: pop += """ {} : {} €
- """.format(idxs, mv[0][idxs].astype( - float).round(2)) + """.format( + idxs, mv[0][idxs].astype(float).round(2) + ) pop += """LV results:
""" for idxs in lv.index: pop += """ {} : {} €
- """.format(idxs, lv[0][idxs].astype( - float).round(2)) + """.format( + idxs, lv[0][idxs].astype(float).round(2) + ) else: pop = """Grid district: {}

- """.format(row['subst_id']) + """.format( + row["subst_id"] + ) # folium.GeoJson(row['geometry']).add_to( # grid_group).add_child(folium.Popup(pop)) - geojson = folium.GeoJson(row['geometry']) + geojson = folium.GeoJson(row["geometry"]) popup = folium.Popup(pop) popup.add_to(geojson) geojson.add_to(grid_group) # Add cluster grids repgrid_group = folium.FeatureGroup( - name='Represented MV Grids per Cluster') # , show=False + name="Represented MV Grids per Cluster" + ) # , show=False cluster = ego.edisgo.grid_choice - cluster = cluster.rename( - columns={"the_selected_network_id": "subst_id"}) + cluster = cluster.rename(columns={"the_selected_network_id": "subst_id"}) - repre_grids = pd.DataFrame(columns=['subst_id', - 'geometry', - 'cluster_id', - 'color']) + repre_grids = pd.DataFrame( + columns=["subst_id", "geometry", "cluster_id", "color"] + ) - style_function = (lambda x: { - 'fillColor': x['properties']['color'], - 'weight': 0.5, 'color': 'black'}) + style_function = lambda x: { + "fillColor": x["properties"]["color"], + "weight": 0.5, + "color": "black", + } # simplify MultiPolygon tolerance = 0.002 @@ -1069,22 +1162,24 @@ def convert_to_hex(rgba_color): cluster_id = list(cluster.represented_grids[idx]) # represented_grids repre_grid = prepareGD(session, cluster_id, version) - repre_grid['cluster_id'] = cluster.subst_id[idx] + repre_grid["cluster_id"] = cluster.subst_id[idx] repre_grids = repre_grids.append(repre_grid, ignore_index=True) # prepare cluster colore - normal = mpl.colors.Normalize(vmin=repre_grids.cluster_id.min(), - vmax=repre_grids.cluster_id.max(), - clip=True) + normal = mpl.colors.Normalize( + vmin=repre_grids.cluster_id.min(), + vmax=repre_grids.cluster_id.max(), + clip=True, + ) mapper = plt.cm.ScalarMappable(norm=normal, cmap=plt.cm.viridis) # add colors to column - repre_grids['color'] = repre_grids['cluster_id'].apply( - lambda x: mcolors.to_hex(mapper.to_rgba(x))) + repre_grids["color"] = repre_grids["cluster_id"].apply( + lambda x: mcolors.to_hex(mapper.to_rgba(x)) + ) - repre_grids = gpd.GeoDataFrame( - repre_grids, geometry='geometry', crs=crs) + repre_grids = gpd.GeoDataFrame(repre_grids, geometry="geometry", crs=crs) # simplify Polygon geometry repre_grids.geometry = repre_grids.geometry.simplify(tolerance) @@ -1092,20 +1187,18 @@ def convert_to_hex(rgba_color): # add popup for name, row in repre_grids.iterrows(): - pops = """Represented Grid: {}
""".format( - row['cluster_id']) + pops = """Represented Grid: {}
""".format(row["cluster_id"]) - folium.GeoJson(repre_grids[name:name+1], - style_function=style_function, - name='represented grids' - ).add_to(repgrid_group - ).add_child(folium.Popup(pops)) + folium.GeoJson( + repre_grids[name : name + 1], + style_function=style_function, + name="represented grids", + ).add_to(repgrid_group).add_child(folium.Popup(pops)) - logger.info('Added MV Grids') + logger.info("Added MV Grids") # Prepare MV lines - mv_line_group = folium.FeatureGroup( - name='MV Grids (>=10kV)') # show=False + mv_line_group = folium.FeatureGroup(name="MV Grids (>=10kV)") # show=False mv_list = ego.edisgo.grid_choice.the_selected_network_id @@ -1119,66 +1212,73 @@ def convert_to_hex(rgba_color): # get line Coordinates x0 = mv_network.lines.bus0.loc[mv_network.lines.v_nom >= 10].map( - mv_network.buses.x) + mv_network.buses.x + ) x1 = mv_network.lines.bus1.loc[mv_network.lines.v_nom >= 10].map( - mv_network.buses.x) + mv_network.buses.x + ) y0 = mv_network.lines.bus0.loc[mv_network.lines.v_nom >= 10].map( - mv_network.buses.y) + mv_network.buses.y + ) y1 = mv_network.lines.bus1.loc[mv_network.lines.v_nom >= 10].map( - mv_network.buses.y) + mv_network.buses.y + ) # get content grid_expansion_costs = ego.edisgo.network[ - mv_grid_id].network.results.grid_expansion_costs - lines = pd.concat([mv_network.lines, - grid_expansion_costs], - axis=1, - join_axes=[mv_network.lines.index]) + mv_grid_id + ].network.results.grid_expansion_costs + lines = pd.concat( + [mv_network.lines, grid_expansion_costs], + axis=1, + join_axes=[mv_network.lines.index], + ) lines = lines.loc[mv_network.lines.v_nom >= 10] lines = lines.reindex() cols = list(lines.columns) - res_mv = ('overnight_costs', 'capital_cost') - unit = ('EUR', 'EUR/time step') + res_mv = ("overnight_costs", "capital_cost") + unit = ("EUR", "EUR/time step") cols = [x for x in cols if x not in res_mv] # save results as csv csv_print = False if csv_print == True: - geo_lines2 = pd.concat([y0, x0, y1, x1], - axis=1, - join_axes=[y0.index]) + geo_lines2 = pd.concat( + [y0, x0, y1, x1], axis=1, join_axes=[y0.index] + ) - line_export = pd.concat([lines, geo_lines2], - axis=1, - join_axes=[lines.index]) + line_export = pd.concat( + [lines, geo_lines2], axis=1, join_axes=[lines.index] + ) - line_export.to_csv("results/mv_line_results_" + - str(mv_grid_id)+".csv") + line_export.to_csv( + "results/mv_line_results_" + str(mv_grid_id) + ".csv" + ) # color map lines try: mv_colormap = cm.linear.YlGnBu_09.scale( - lines.overnight_costs.min(), - lines.overnight_costs.max()).to_step(6) + lines.overnight_costs.min(), lines.overnight_costs.max() + ).to_step(6) except: - mv_colormap = cm.linear.YlGnBu_09.scale( - 0, 0).to_step(6) + mv_colormap = cm.linear.YlGnBu_09.scale(0, 0).to_step(6) - mv_colormap.caption = 'Line investment of overnight cost (mv)' + mv_colormap.caption = "Line investment of overnight cost (mv)" # add parameter for line in lines.index: popup = """ Line: {}
- version: {}

""".format(line, version) + version: {}

""".format( + line, version + ) popup += """MV line parameter:
""" for col in cols: try: - popup += """ {}: {}
""".format(col, - lines[col][line]) + popup += """ {}: {}
""".format(col, lines[col][line]) except: popup += """ """ @@ -1186,34 +1286,34 @@ def convert_to_hex(rgba_color): for idx, val in enumerate(res_mv): try: - popup += """{}: {} in {}
""".format(val, - lines[val][line], - unit[idx]) + popup += """{}: {} in {}
""".format( + val, lines[val][line], unit[idx] + ) except: popup += """ """ # change colore function mv_color = colormapper_lines( - mv_colormap, lines, line, column="overnight_costs") + mv_colormap, lines, line, column="overnight_costs" + ) # ToDo make it more generic try: - folium.PolyLine(([y0[line], x0[line]], - [y1[line], x1[line]]), - popup=popup, color=convert_to_hex( - mv_color) + folium.PolyLine( + ([y0[line], x0[line]], [y1[line], x1[line]]), + popup=popup, + color=convert_to_hex(mv_color), ).add_to(mv_line_group) except: logger.disabled = True - logger.info('Cound not find a geometry') + logger.info("Cound not find a geometry") logger.disabled = False else: - logger.info(str(mv_grid_id)+" " + - str(ego.edisgo.network[mv_grid_id])) + logger.info(str(mv_grid_id) + " " + str(ego.edisgo.network[mv_grid_id])) mp.add_child(mv_colormap) # Add MV Storage # Legend name - mv_sto_group = folium.FeatureGroup(name='MV storages') # ,show=False + mv_sto_group = folium.FeatureGroup(name="MV storages") # ,show=False # add mv storages mv_grid_id = list(ego.edisgo.grid_choice.the_selected_network_id) @@ -1225,7 +1325,9 @@ def convert_to_hex(rgba_color): # create pypsa network only containing MV buses and lines pypsa_plot = PyPSANetwork() - pypsa_plot.buses = pypsa_network.buses.loc[pypsa_network.buses.v_nom >= 10] + pypsa_plot.buses = pypsa_network.buses.loc[ + pypsa_network.buses.v_nom >= 10 + ] # add Coordinates sto_x = pypsa_plot.storage_units.bus.map(pypsa_plot.buses.x) @@ -1238,32 +1340,36 @@ def convert_to_hex(rgba_color): for store in pypsa_plot.storage_units.index: popup = """ Storage: {}

- Parameter:
""".format(store,) + Parameter:
""".format( + store, + ) for col in sto_cols: popup += """ {}: {}
- """.format(col, - pypsa_plot.storage_units[col][store]) + """.format( + col, pypsa_plot.storage_units[col][store] + ) folium.CircleMarker( location=([sto_y[store], sto_x[store]]), - radius=pypsa_plot.storage_units['p_nom'], + radius=pypsa_plot.storage_units["p_nom"], popup=popup, - color='#3186cc', + color="#3186cc", fill=True, - fill_color='#3186cc', - weight=1).add_to(mv_sto_group) + fill_color="#3186cc", + weight=1, + ).add_to(mv_sto_group) - logger.info('Added MV stores') + logger.info("Added MV stores") # add layers and others - colormap.caption = 'Line loading s_nom (ehv/hv)' - colormap2.caption = 'Line investment of annuity costs (ehv/hv)' + colormap.caption = "Line loading s_nom (ehv/hv)" + colormap2.caption = "Line investment of annuity costs (ehv/hv)" mp.add_child(colormap) mp.add_child(colormap2) # add legend # add layer groups - if ego.json_file['eGo']['eDisGo'] is True: + if ego.json_file["eGo"]["eDisGo"] is True: repgrid_group.add_to(mp) grid_group.add_to(mp) @@ -1278,22 +1384,22 @@ def convert_to_hex(rgba_color): folium.LayerControl().add_to(mp) plugins.Fullscreen( - position='topright', - title='Fullscreen', - title_cancel='Exit me', - force_separate_button=True).add_to(mp) + position="topright", + title="Fullscreen", + title_cancel="Exit me", + force_separate_button=True, + ).add_to(mp) - url = ('https://openego.readthedocs.io/en/master/_images/open_ego_icon_web.png') + url = "https://openego.readthedocs.io/en/master/_images/open_ego_icon_web.png" FloatImage(url, bottom=0, left=5).add_to(mp) - if ego.json_file['eGo']['eDisGo'] is True: - mp = iplot_griddistrict_legend( - mp=mp, repre_grids=repre_grids, start=True) + if ego.json_file["eGo"]["eDisGo"] is True: + mp = iplot_griddistrict_legend(mp=mp, repre_grids=repre_grids, start=True) mp = iplot_totalresults_legend(mp=mp, ego=ego, start=True) # Save Map - html_dir = 'results/html' + html_dir = "results/html" if not os.path.exists(html_dir): os.makedirs(html_dir) mp.save("results/html/iplot_map.html") @@ -1309,16 +1415,16 @@ def convert_to_hex(rgba_color): if save_image: url2 = "file://{}/{}".format(os.getcwd(), url) outfn = os.path.join(html_dir, "outfig.png") - subprocess.check_call(["cutycapt", "--url={}".format(url2), - "--out={}".format(outfn)]) + subprocess.check_call( + ["cutycapt", "--url={}".format(url2), "--out={}".format(outfn)] + ) # close oedb session.close() - logger.info('Done') + logger.info("Done") def colormapper_lines(colormap, lines, line, column="s_nom"): - """ Make Colore Map for lines. - """ + """Make Colore Map for lines.""" # TODO: make it more generic l_color = [] @@ -1336,7 +1442,7 @@ def colormapper_lines(colormap, lines, line, column="s_nom"): elif colormap.index[1] >= lines[column][line] >= colormap.index[0]: l_color = colormap.colors[0] else: - l_color = (0., 0., 0., 1.) + l_color = (0.0, 0.0, 0.0, 1.0) if len(colormap.index) == 5: if colormap.index[4] >= lines[column][line] > colormap.index[3]: @@ -1348,7 +1454,7 @@ def colormapper_lines(colormap, lines, line, column="s_nom"): elif colormap.index[1] >= lines[column][line] >= colormap.index[0]: l_color = colormap.colors[0] else: - l_color = (0., 0., 0., 1.) + l_color = (0.0, 0.0, 0.0, 1.0) return l_color @@ -1367,9 +1473,12 @@ def _storage_distribution(network, ax, fig, scaling=1, filename=None): """ stores = network.storage_units - storage_distribution = network.storage_units.p_nom_opt[stores.index]\ - .groupby(network.storage_units.bus)\ - .sum().reindex(network.buses.index, fill_value=0.) + storage_distribution = ( + network.storage_units.p_nom_opt[stores.index] + .groupby(network.storage_units.bus) + .sum() + .reindex(network.buses.index, fill_value=0.0) + ) msd_max = storage_distribution.max() msd_median = storage_distribution[storage_distribution != 0].median() @@ -1380,45 +1489,41 @@ def _storage_distribution(network, ax, fig, scaling=1, filename=None): else: LabelVal = 0 if LabelVal < 0: - LabelUnit = 'kW' - msd_max, msd_median, msd_min = msd_max * \ - 1000, msd_median * 1000, msd_min * 1000 + LabelUnit = "kW" + msd_max, msd_median, msd_min = msd_max * 1000, msd_median * 1000, msd_min * 1000 storage_distribution = storage_distribution * 1000 elif LabelVal < 3: - LabelUnit = 'MW' + LabelUnit = "MW" else: - LabelUnit = 'GW' - msd_max, msd_median, msd_min = msd_max / \ - 1000, msd_median / 1000, msd_min / 1000 + LabelUnit = "GW" + msd_max, msd_median, msd_min = msd_max / 1000, msd_median / 1000, msd_min / 1000 storage_distribution = storage_distribution / 1000 if sum(storage_distribution) == 0: network.plot(bus_sizes=0, ax=ax) else: - network.plot( - bus_sizes=storage_distribution * scaling, - ax=ax, - line_widths=0.3 - ) + network.plot(bus_sizes=storage_distribution * scaling, ax=ax, line_widths=0.3) def iplot_griddistrict_legend(mp, repre_grids, start=False): - """Add legend to iplot function of mv grids. - - """ + """Add legend to iplot function of mv grids.""" # from branca.element import Template, MacroElement from string import Template if start: legends = [] - for name, row in repre_grids.groupby(['cluster_id', 'color']).count().iterrows(): + for name, row in ( + repre_grids.groupby(["cluster_id", "color"]).count().iterrows() + ): color = name[1] grid_no = name[0] entry = """
  • - Represented by Grid {}
  • """.format(color, grid_no) + Represented by Grid {} """.format( + color, grid_no + ) legends.append(entry) @@ -1612,7 +1717,7 @@ def iplot_griddistrict_legend(mp, repre_grids, start=False): t = Template(temp_2) temp_2 = t.substitute(legend=legend) - temps = temp_1+temp_2+temp_3 + temps = temp_1 + temp_2 + temp_3 # macro = MacroElement(**leg) # macro._template = Template(template) @@ -1621,28 +1726,36 @@ def iplot_griddistrict_legend(mp, repre_grids, start=False): def iplot_totalresults_legend(mp, ego, start=False): - """ Add total results as legend to iplot function. - """ + """Add total results as legend to iplot function.""" from string import Template if start: # get data total = ego.total_investment_costs.rename( - columns={"capital_cost": "annuity_costs"}) + columns={"capital_cost": "annuity_costs"} + ) # change format - total['overnight_costs'] = ( - total['overnight_costs']/1000000).map('M€ {:,.2f}'.format) + total["overnight_costs"] = (total["overnight_costs"] / 1000000).map( + "M€ {:,.2f}".format + ) - total['annuity_costs'] = (total['annuity_costs'] / - 1000).map('T€ {:,.2f}'.format) + total["annuity_costs"] = (total["annuity_costs"] / 1000).map( + "T€ {:,.2f}".format + ) - total = total[['component', 'voltage_level', - 'differentiation', 'overnight_costs', - 'annuity_costs']].to_html(index=False) + total = total[ + [ + "component", + "voltage_level", + "differentiation", + "overnight_costs", + "annuity_costs", + ] + ].to_html(index=False) # inclued grafic - html_dir = 'results/html' + html_dir = "results/html" if not os.path.exists(html_dir): os.makedirs(html_dir) @@ -1697,8 +1810,7 @@ def iplot_totalresults_legend(mp, ego, start=False): def _get_mv_plot_res(ego, mv_grid_id): - """ Prepare mv results. - """ + """Prepare mv results.""" logger.disabled = True pypsa_network = ego.edisgo.network[mv_grid_id].network.pypsa @@ -1707,39 +1819,49 @@ def _get_mv_plot_res(ego, mv_grid_id): pypsa_plot = PyPSANetwork() pypsa_plot.buses = pypsa_network.buses.loc[pypsa_network.buses.v_nom >= 10] # filter buses of aggregated loads and generators - pypsa_plot.buses = pypsa_plot.buses[ - ~pypsa_plot.buses.index.str.contains("agg")] + pypsa_plot.buses = pypsa_plot.buses[~pypsa_plot.buses.index.str.contains("agg")] pypsa_plot.lines = pypsa_network.lines[ - pypsa_network.lines.bus0.isin(pypsa_plot.buses.index)][ - pypsa_network.lines.bus1.isin(pypsa_plot.buses.index)] + pypsa_network.lines.bus0.isin(pypsa_plot.buses.index) + ][pypsa_network.lines.bus1.isin(pypsa_plot.buses.index)] - grid_expansion_costs = ego.edisgo.network[mv_grid_id].network.results.grid_expansion_costs + grid_expansion_costs = ego.edisgo.network[ + mv_grid_id + ].network.results.grid_expansion_costs - bus_cost = pd.concat([pypsa_plot.buses, grid_expansion_costs], axis=1, - join_axes=[pypsa_plot.buses.index]) + bus_cost = pd.concat( + [pypsa_plot.buses, grid_expansion_costs], + axis=1, + join_axes=[pypsa_plot.buses.index], + ) costs_lv_stations = grid_expansion_costs[ - grid_expansion_costs.index.str.contains("LVStation")] - costs_lv_stations['station'] = \ - costs_lv_stations.reset_index()['index'].apply( - lambda _: '_'.join(_.split('_')[0:2])).values - costs_lv_stations = costs_lv_stations.groupby('station').sum() + grid_expansion_costs.index.str.contains("LVStation") + ] + costs_lv_stations["station"] = ( + costs_lv_stations.reset_index()["index"] + .apply(lambda _: "_".join(_.split("_")[0:2])) + .values + ) + costs_lv_stations = costs_lv_stations.groupby("station").sum() costs_mv_station = grid_expansion_costs[ - grid_expansion_costs.index.str.contains("MVStation")] - costs_mv_station['station'] = \ - costs_mv_station.reset_index()['index'].apply( - lambda _: '_'.join(_.split('_')[0:2])).values - costs_mv_station = costs_mv_station.groupby('station').sum() + grid_expansion_costs.index.str.contains("MVStation") + ] + costs_mv_station["station"] = ( + costs_mv_station.reset_index()["index"] + .apply(lambda _: "_".join(_.split("_")[0:2])) + .values + ) + costs_mv_station = costs_mv_station.groupby("station").sum() - costs_lv_stations_total = costs_lv_stations[['overnight_costs', - 'capital_cost']].sum() + costs_lv_stations_total = costs_lv_stations[ + ["overnight_costs", "capital_cost"] + ].sum() - costs_mv_station_total = costs_mv_station[['overnight_costs', - 'capital_cost']].sum() + costs_mv_station_total = costs_mv_station[["overnight_costs", "capital_cost"]].sum() costs_lv_stations_total = pd.DataFrame(costs_lv_stations_total) costs_mv_station_total = pd.DataFrame(costs_mv_station_total) logger.disabled = False - return costs_lv_stations_total, costs_mv_station_total + return costs_lv_stations_total, costs_mv_station_total diff --git a/ego/tools/results.py b/ego/tools/results.py index d41eec13..e0164191 100644 --- a/ego/tools/results.py +++ b/ego/tools/results.py @@ -23,17 +23,21 @@ # TODO - write results to database import io -import os import logging -logger = logging.getLogger('ego') +import os -if not 'READTHEDOCS' in os.environ: - import pandas as pd +logger = logging.getLogger("ego") + +if not "READTHEDOCS" in os.environ: import numpy as np + import pandas as pd + from ego.tools.economics import get_generator_investment -__copyright__ = "Flensburg University of Applied Sciences, Europa-Universität"\ +__copyright__ = ( + "Flensburg University of Applied Sciences, Europa-Universität" "Flensburg, Centre for Sustainable Energy Systems" +) __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolfbunke" @@ -62,44 +66,50 @@ def create_etrago_results(network, scn_name): # rename function etg = network etrago = pd.DataFrame() - etrago['p_nom'] = etg.generators.groupby('carrier')['p_nom'].sum() # in MW - etrago['p_nom_opt'] = etg.generators.groupby('carrier')[ - 'p_nom_opt'].sum() # in MW + etrago["p_nom"] = etg.generators.groupby("carrier")["p_nom"].sum() # in MW + etrago["p_nom_opt"] = etg.generators.groupby("carrier")["p_nom_opt"].sum() # in MW # power price - etrago['marginal_cost'] = etg.generators.groupby('carrier' - )['marginal_cost'].mean() + etrago["marginal_cost"] = etg.generators.groupby("carrier")["marginal_cost"].mean() # in in [EUR] # get power price by production MWh _t.p * marginal_cost - power_price = etg.generators_t.p[etg.generators[etg.generators. - control != 'Slack'] - .index] * etg.generators.\ - marginal_cost[etg.generators[etg.generators. - control != 'Slack'].index] # without Slack + power_price = ( + etg.generators_t.p[etg.generators[etg.generators.control != "Slack"].index] + * etg.generators.marginal_cost[ + etg.generators[etg.generators.control != "Slack"].index + ] + ) # without Slack - power_price = power_price.groupby( - etg.generators.carrier, axis=1).sum().sum() - etrago['power_price'] = power_price + power_price = power_price.groupby(etg.generators.carrier, axis=1).sum().sum() + etrago["power_price"] = power_price # use country code - p_by_carrier = pd.concat([etg.generators_t.p - [etg.generators[etg.generators.control != - 'Slack'].index], - etg.generators_t.p[etg.generators[ - etg. - generators.control == 'Slack'].index - ].iloc[:, 0]. - apply(lambda x: x if x > 0 else 0)], axis=1).\ - groupby(etg.generators.carrier, axis=1).sum() # in MWh - - etrago['p'] = p_by_carrier.sum() + p_by_carrier = ( + pd.concat( + [ + etg.generators_t.p[ + etg.generators[etg.generators.control != "Slack"].index + ], + etg.generators_t.p[ + etg.generators[etg.generators.control == "Slack"].index + ] + .iloc[:, 0] + .apply(lambda x: x if x > 0 else 0), + ], + axis=1, + ) + .groupby(etg.generators.carrier, axis=1) + .sum() + ) # in MWh + + etrago["p"] = p_by_carrier.sum() # add invetment result_invest = get_generator_investment(network, scn_name) - etrago = etrago.assign(investment_costs=result_invest['carrier_costs']) + etrago = etrago.assign(investment_costs=result_invest["carrier_costs"]) return etrago -if __name__ == '__main__': +if __name__ == "__main__": pass diff --git a/ego/tools/specs.py b/ego/tools/specs.py index 1b7d2958..69f50a7f 100644 --- a/ego/tools/specs.py +++ b/ego/tools/specs.py @@ -21,34 +21,39 @@ This files contains all eGo interface functions """ -__copyright__ = ("Europa-Universität Flensburg, " - "Centre for Sustainable Energy Systems") +__copyright__ = "Europa-Universität Flensburg, " "Centre for Sustainable Energy Systems" __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolf_bunke,maltesc" # Import # General Packages import os -import pandas as pd import time -if not 'READTHEDOCS' in os.environ: + +import pandas as pd + +if "READTHEDOCS" not in os.environ: from egoio.db_tables import model_draft from egoio.db_tables import supply import math import logging + logger = logging.getLogger(__name__) # Functions -def get_etragospecs_direct(session, - bus_id, - etrago_network, - scn_name, - grid_version, - pf_post_lopf, - max_cos_phi_renewable): + +def get_etragospecs_direct( + session, + bus_id, + etrago_network, + scn_name, + grid_version, + pf_post_lopf, + max_cos_phi_renewable, +): """ Reads eTraGo Results from Database and returns and returns the interface values as a dictionary of corresponding dataframes @@ -71,163 +76,146 @@ def get_etragospecs_direct(session, Dataframes used as eDisGo inputs """ - logger.info('Specs for bus {}'.format(bus_id)) + logger.info("Specs for bus {}".format(bus_id)) if pf_post_lopf: - logger.info('Active and reactive power interface') + logger.info("Active and reactive power interface") else: - logger.info('Only active power interface') + logger.info("Only active power interface") specs_meta_data = {} performance = {} - specs_meta_data.update({'TG Bus ID': bus_id}) + specs_meta_data.update({"TG Bus ID": bus_id}) if grid_version is None: - logger.warning('Weather_id taken from model_draft (not tested)') + logger.warning("Weather_id taken from model_draft (not tested)") - ormclass_gen_single = model_draft.__getattribute__( - 'EgoSupplyPfGeneratorSingle') + ormclass_gen_single = model_draft.__getattribute__("EgoSupplyPfGeneratorSingle") else: - ormclass_aggr_w = supply.__getattribute__( - 'EgoAggrWeather') + ormclass_aggr_w = supply.__getattribute__("EgoAggrWeather") snap_idx = etrago_network.snapshots # Generators t0 = time.perf_counter() - weather_dpdnt = ['wind', 'solar', 'wind_onshore', 'wind_offshore'] + weather_dpdnt = ["wind", "solar", "wind_onshore", "wind_offshore"] # DF procesing all_gens_df = etrago_network.generators[ - etrago_network.generators['bus'] == str(bus_id) + etrago_network.generators["bus"] == str(bus_id) ] - all_gens_df.index.name = 'generator_id' + all_gens_df.index.name = "generator_id" all_gens_df.reset_index(inplace=True) - all_gens_df = all_gens_df[[ - 'generator_id', - 'p_nom', - 'p_nom_opt', - 'carrier']] + all_gens_df = all_gens_df[["generator_id", "p_nom", "p_nom_opt", "carrier"]] all_gens_df = all_gens_df.rename(columns={"carrier": "name"}) - all_gens_df = all_gens_df[all_gens_df['name'] != 'wind_offshore'] + all_gens_df = all_gens_df[all_gens_df["name"] != "wind_offshore"] for index, row in all_gens_df.iterrows(): - name = row['name'] - if name == 'wind_onshore': - all_gens_df.at[index, 'name'] = 'wind' + name = row["name"] + if name == "wind_onshore": + all_gens_df.at[index, "name"] = "wind" # Conventionals t1 = time.perf_counter() - performance.update({'Generator Data Processing': t1-t0}) + performance.update({"Generator Data Processing": t1 - t0}) conv_df = all_gens_df[~all_gens_df.name.isin(weather_dpdnt)] - conv_dsptch = pd.DataFrame(0.0, - index=snap_idx, - columns=list(set(conv_df['name']))) - conv_reactive_power = pd.DataFrame(0.0, - index=snap_idx, - columns=list(set(conv_df['name']))) + conv_dsptch = pd.DataFrame(0.0, index=snap_idx, columns=list(set(conv_df["name"]))) + conv_reactive_power = pd.DataFrame( + 0.0, index=snap_idx, columns=list(set(conv_df["name"])) + ) if not conv_df.empty: conventionals = True - conv_cap = conv_df[['p_nom', 'name']].groupby('name').sum().T + conv_cap = conv_df[["p_nom", "name"]].groupby("name").sum().T for index, row in conv_df.iterrows(): - generator_id = row['generator_id'] - source = row['name'] + generator_id = row["generator_id"] + source = row["name"] p = etrago_network.generators_t.p[str(generator_id)] - p_norm = p / conv_cap[source]['p_nom'] + p_norm = p / conv_cap[source]["p_nom"] conv_dsptch[source] = conv_dsptch[source] + p_norm if pf_post_lopf: q = etrago_network.generators_t.q[str(generator_id)] # q normalized with p_nom - q_norm = q / conv_cap[source]['p_nom'] - conv_reactive_power[source] = ( - conv_reactive_power[source] - + q_norm) + q_norm = q / conv_cap[source]["p_nom"] + conv_reactive_power[source] = conv_reactive_power[source] + q_norm if pf_post_lopf: - new_columns = [ - (col, '') for col in conv_reactive_power.columns - ] - conv_reactive_power.columns = pd.MultiIndex.from_tuples( - new_columns) + new_columns = [(col, "") for col in conv_reactive_power.columns] + conv_reactive_power.columns = pd.MultiIndex.from_tuples(new_columns) else: conventionals = False - logger.warning('No conventional generators at bus {}'.format(bus_id)) + logger.warning("No conventional generators at bus {}".format(bus_id)) # Renewables t2 = time.perf_counter() - performance.update({'Conventional Dispatch': t2-t1}) + performance.update({"Conventional Dispatch": t2 - t1}) # Capacities ren_df = all_gens_df[all_gens_df.name.isin(weather_dpdnt)] if ren_df.empty: - logger.warning('No renewable generators at bus {}'.format(bus_id)) + logger.warning("No renewable generators at bus {}".format(bus_id)) for index, row in ren_df.iterrows(): - aggr_id = row['generator_id'] + aggr_id = row["generator_id"] if grid_version is None: - w_id = session.query( - ormclass_gen_single.w_id - ).filter( - ormclass_gen_single.aggr_id == aggr_id, - ormclass_gen_single.scn_name == scn_name - ).limit(1).scalar() + w_id = ( + session.query(ormclass_gen_single.w_id) + .filter( + ormclass_gen_single.aggr_id == aggr_id, + ormclass_gen_single.scn_name == scn_name, + ) + .limit(1) + .scalar() + ) else: - w_id = session.query( - ormclass_aggr_w.w_id - ).filter( - ormclass_aggr_w.aggr_id == aggr_id, - #ormclass_aggr_w.scn_name == scn_name, - ormclass_aggr_w.version == grid_version - ).limit(1).scalar() - - ren_df.at[index, 'w_id'] = w_id + w_id = ( + session.query(ormclass_aggr_w.w_id) + .filter( + ormclass_aggr_w.aggr_id == aggr_id, + # ormclass_aggr_w.scn_name == scn_name, + ormclass_aggr_w.version == grid_version, + ) + .limit(1) + .scalar() + ) + + ren_df.at[index, "w_id"] = w_id ren_df.dropna(inplace=True) - aggr_gens = ren_df.groupby([ - 'name', - 'w_id' - ]).agg({'p_nom': 'sum'}).reset_index() - - aggr_gens.rename(columns={'p_nom': 'p_nom_aggr'}, inplace=True) - - aggr_gens['ren_id'] = aggr_gens.index - - ### Dispatch and Curteilment - potential = pd.DataFrame(0.0, - index=snap_idx, - columns=aggr_gens['ren_id']) - dispatch = pd.DataFrame(0.0, - index=snap_idx, - columns=aggr_gens['ren_id']) - curtailment = pd.DataFrame(0.0, - index=snap_idx, - columns=aggr_gens['ren_id']) + aggr_gens = ren_df.groupby(["name", "w_id"]).agg({"p_nom": "sum"}).reset_index() + + aggr_gens.rename(columns={"p_nom": "p_nom_aggr"}, inplace=True) + + aggr_gens["ren_id"] = aggr_gens.index + + # Dispatch and Curteilment + potential = pd.DataFrame(0.0, index=snap_idx, columns=aggr_gens["ren_id"]) + dispatch = pd.DataFrame(0.0, index=snap_idx, columns=aggr_gens["ren_id"]) + curtailment = pd.DataFrame(0.0, index=snap_idx, columns=aggr_gens["ren_id"]) if pf_post_lopf: - reactive_power = pd.DataFrame(0.0, - index=snap_idx, - columns=aggr_gens['ren_id']) + reactive_power = pd.DataFrame(0.0, index=snap_idx, columns=aggr_gens["ren_id"]) for index, row in ren_df.iterrows(): - gen_id = row['generator_id'] - name = row['name'] - w_id = row['w_id'] - ren_id = int(aggr_gens[ - (aggr_gens['name'] == name) & - (aggr_gens['w_id'] == w_id)]['ren_id']) + gen_id = row["generator_id"] + name = row["name"] + w_id = row["w_id"] + ren_id = int( + aggr_gens[(aggr_gens["name"] == name) & (aggr_gens["w_id"] == w_id)][ + "ren_id" + ] + ) - p_nom_aggr = float( - aggr_gens[aggr_gens['ren_id'] == ren_id]['p_nom_aggr']) - p_nom = row['p_nom'] + p_nom_aggr = float(aggr_gens[aggr_gens["ren_id"] == ren_id]["p_nom_aggr"]) + p_nom = row["p_nom"] p_series = etrago_network.generators_t.p[str(gen_id)] p_norm_tot_series = p_series / p_nom_aggr @@ -241,41 +229,52 @@ def get_etragospecs_direct(session, if pf_post_lopf: q_series = etrago_network.generators_t.q[str(gen_id)] q_norm_tot_series = q_series / p_nom_aggr - reactive_power[ren_id] = ( - reactive_power[ren_id] - + q_norm_tot_series) + reactive_power[ren_id] = reactive_power[ren_id] + q_norm_tot_series curtailment = potential.sub(dispatch) new_columns = [ - (aggr_gens[aggr_gens.ren_id == col].name.iloc[0], - aggr_gens[aggr_gens.ren_id == col].w_id.iloc[0]) - for col in potential.columns] + ( + aggr_gens[aggr_gens.ren_id == col].name.iloc[0], + aggr_gens[aggr_gens.ren_id == col].w_id.iloc[0], + ) + for col in potential.columns + ] potential.columns = pd.MultiIndex.from_tuples(new_columns) new_columns = [ - (aggr_gens[aggr_gens.ren_id == col].name.iloc[0], - aggr_gens[aggr_gens.ren_id == col].w_id.iloc[0]) - for col in dispatch.columns] + ( + aggr_gens[aggr_gens.ren_id == col].name.iloc[0], + aggr_gens[aggr_gens.ren_id == col].w_id.iloc[0], + ) + for col in dispatch.columns + ] dispatch.columns = pd.MultiIndex.from_tuples(new_columns) new_columns = [ - (aggr_gens[aggr_gens.ren_id == col].name.iloc[0], - aggr_gens[aggr_gens.ren_id == col].w_id.iloc[0]) - for col in curtailment.columns] + ( + aggr_gens[aggr_gens.ren_id == col].name.iloc[0], + aggr_gens[aggr_gens.ren_id == col].w_id.iloc[0], + ) + for col in curtailment.columns + ] curtailment.columns = pd.MultiIndex.from_tuples(new_columns) if pf_post_lopf: new_columns = [ - (aggr_gens[aggr_gens.ren_id == col].name.iloc[0], - aggr_gens[aggr_gens.ren_id == col].w_id.iloc[0]) - for col in reactive_power.columns] + ( + aggr_gens[aggr_gens.ren_id == col].name.iloc[0], + aggr_gens[aggr_gens.ren_id == col].w_id.iloc[0], + ) + for col in reactive_power.columns + ] reactive_power.columns = pd.MultiIndex.from_tuples(new_columns) # Q limit calculation if max_cos_phi_renewable: - logger.info('Applying Q limit (max cos(phi)={})'.format( - max_cos_phi_renewable)) + logger.info( + "Applying Q limit (max cos(phi)={})".format(max_cos_phi_renewable) + ) phi = math.acos(max_cos_phi_renewable) @@ -295,72 +294,70 @@ def get_etragospecs_direct(session, # Reactive Power concat if conventionals: - all_reactive_power = pd.concat([ - conv_reactive_power, - reactive_power], axis=1) + all_reactive_power = pd.concat( + [conv_reactive_power, reactive_power], axis=1 + ) else: all_reactive_power = reactive_power # Storage t3 = time.perf_counter() - performance.update({'Renewable Dispatch and Curt.': t3-t2}) + performance.update({"Renewable Dispatch and Curt.": t3 - t2}) # Capactiy min_extended = 0.3 stor_df = etrago_network.storage_units.loc[ - (etrago_network.storage_units['bus'] == str(bus_id)) - & (etrago_network.storage_units['p_nom_extendable'] == True) - & (etrago_network.storage_units['p_nom_opt'] > min_extended) - & (etrago_network.storage_units['max_hours'] <= 20.)] # Only batteries + (etrago_network.storage_units["bus"] == str(bus_id)) + & (etrago_network.storage_units["p_nom_extendable"] == True) + & (etrago_network.storage_units["p_nom_opt"] > min_extended) + & (etrago_network.storage_units["max_hours"] <= 20.0) + ] # Only batteries - logger.warning('Minimum storage of {} MW'.format(min_extended)) + logger.warning("Minimum storage of {} MW".format(min_extended)) ext_found = False if len(stor_df) == 1: - logger.info('Extendable storage unit found') + logger.info("Extendable storage unit found") ext_found = True stor_id = stor_df.index[0] - stor_p_series_kW = etrago_network.storage_units_t.p[ - str(stor_id)] * 1000 + stor_p_series_kW = etrago_network.storage_units_t.p[str(stor_id)] * 1000 if pf_post_lopf: try: - stor_q_series_kvar = etrago_network.storage_units_t.q[ - str(stor_id)] * 1000 - except: - logger.warning("No Q series found for storage unit {}".format( - stor_id)) - stor_q_series_kvar = etrago_network.storage_units_t.p[ - str(stor_id)] * 0 - - if ext_found == False: - logger.info( - "No extendable storage unit found at bus {}".format(bus_id)) + stor_q_series_kvar = ( + etrago_network.storage_units_t.q[str(stor_id)] * 1000 + ) + except Exception: + logger.warning("No Q series found for storage unit {}".format(stor_id)) + stor_q_series_kvar = etrago_network.storage_units_t.p[str(stor_id)] * 0 + + if ext_found is False: + logger.info("No extendable storage unit found at bus {}".format(bus_id)) t4 = time.perf_counter() - performance.update({'Storage Data Processing and Dispatch': t4-t3}) + performance.update({"Storage Data Processing and Dispatch": t4 - t3}) specs = { - 'conv_dispatch': conv_dsptch, - 'ren_dispatch': dispatch, - 'ren_potential': potential, - 'ren_curtailment': curtailment + "conv_dispatch": conv_dsptch, + "ren_dispatch": dispatch, + "ren_potential": potential, + "ren_curtailment": curtailment, } if ext_found: - specs['battery_p_series'] = stor_p_series_kW + specs["battery_p_series"] = stor_p_series_kW if pf_post_lopf: - specs['battery_q_series'] = stor_q_series_kvar + specs["battery_q_series"] = stor_q_series_kvar else: - specs['battery_p_series'] = specs['battery_q_series'] = None + specs["battery_p_series"] = specs["battery_q_series"] = None if pf_post_lopf: - specs['reactive_power'] = all_reactive_power + specs["reactive_power"] = all_reactive_power t5 = time.perf_counter() - performance.update({'Overall time': t5-t0}) + performance.update({"Overall time": t5 - t0}) return specs diff --git a/ego/tools/storages.py b/ego/tools/storages.py index b8323a95..c49509a0 100644 --- a/ego/tools/storages.py +++ b/ego/tools/storages.py @@ -21,17 +21,18 @@ """ import io -import os import logging -logger = logging.getLogger('ego') +import os -if not 'READTHEDOCS' in os.environ: - import pandas as pd +logger = logging.getLogger("ego") + +if not "READTHEDOCS" in os.environ: import numpy as np + import pandas as pd + from etrago.tools.utilities import geolocation_buses -__copyright__ = ("Europa-Universität Flensburg, " - "Centre for Sustainable Energy Systems") +__copyright__ = "Europa-Universität Flensburg, " "Centre for Sustainable Energy Systems" __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolf_bunke,maltesc" @@ -66,36 +67,56 @@ def etrago_storages(network): Sum of optimal installed power capacity """ if len(network.storage_units_t.p.sum()) > 0: - charge = network.storage_units_t.\ - p[network.storage_units_t.p[network. - storage_units[network.storage_units. - p_nom_opt > 0].index]. - values > 0.].groupby(network.storage_units. - carrier, axis=1).sum().sum() - - discharge = network.storage_units_t.p[network.storage_units_t. - p[network. - storage_units[ - network.storage_units. - p_nom_opt > 0]. - index].values < 0.].\ - groupby(network.storage_units.carrier, axis=1).sum().sum() - - count = network.storage_units.bus[network.storage_units.p_nom_opt > 0].\ - groupby(network.storage_units.carrier, axis=0).count() - - p_nom_sum = network.storage_units.p_nom.groupby(network.storage_units. - carrier, axis=0).sum() + charge = ( + network.storage_units_t.p[ + network.storage_units_t.p[ + network.storage_units[network.storage_units.p_nom_opt > 0].index + ].values + > 0.0 + ] + .groupby(network.storage_units.carrier, axis=1) + .sum() + .sum() + ) + + discharge = ( + network.storage_units_t.p[ + network.storage_units_t.p[ + network.storage_units[network.storage_units.p_nom_opt > 0].index + ].values + < 0.0 + ] + .groupby(network.storage_units.carrier, axis=1) + .sum() + .sum() + ) + + count = ( + network.storage_units.bus[network.storage_units.p_nom_opt > 0] + .groupby(network.storage_units.carrier, axis=0) + .count() + ) + + p_nom_sum = network.storage_units.p_nom.groupby( + network.storage_units.carrier, axis=0 + ).sum() p_nom_o_sum = network.storage_units.p_nom_opt.groupby( - network.storage_units. - carrier, axis=0).sum() + network.storage_units.carrier, axis=0 + ).sum() p_nom_o = p_nom_sum - p_nom_o_sum # Zubau - results = pd.concat([charge.rename('charge'), - discharge.rename('discharge'), - p_nom_sum, count.rename('total_units'), p_nom_o - .rename('extension'), ], axis=1, join='outer') + results = pd.concat( + [ + charge.rename("charge"), + discharge.rename("discharge"), + p_nom_sum, + count.rename("total_units"), + p_nom_o.rename("extension"), + ], + axis=1, + join="outer", + ) else: logger.info("No timeseries p for storages!") @@ -121,64 +142,67 @@ def etrago_storages_investment(network, json_file, session): """ # check spelling of storages and storage - logger.info(json_file['eTraGo']['extendable']) + logger.info(json_file["eTraGo"]["extendable"]) - stos = 'storage' + stos = "storage" # check settings for extendable - if stos not in json_file['eTraGo']['extendable']: - logger.info("The optimizition was not using parameter " - " 'extendable': storage" - "No storage expantion costs from etrago") + if stos not in json_file["eTraGo"]["extendable"]: + logger.info( + "The optimizition was not using parameter " + " 'extendable': storage" + "No storage expantion costs from etrago" + ) - if stos in json_file['eTraGo']['extendable']: + if stos in json_file["eTraGo"]["extendable"]: network = geolocation_buses(network, session) # get v_nom - _bus = pd.DataFrame(network.buses[['v_nom', 'country_code']]) + _bus = pd.DataFrame(network.buses[["v_nom", "country_code"]]) _bus.index.name = "name" _bus.reset_index(level=0, inplace=True) - _storage = network.storage_units[ - network.storage_units.p_nom_extendable == True] + _storage = network.storage_units[network.storage_units.p_nom_extendable == True] _storage.reset_index(level=0, inplace=True) # provide storage installation costs per voltage level - installed_storages = \ - pd.merge(_storage, _bus, left_on='bus', right_on='name') + installed_storages = pd.merge(_storage, _bus, left_on="bus", right_on="name") - installed_storages['investment_costs'] = (installed_storages. - capital_cost * - installed_storages.p_nom_opt) + installed_storages["investment_costs"] = ( + installed_storages.capital_cost * installed_storages.p_nom_opt + ) # add voltage_level - installed_storages['voltage_level'] = 'unknown' + installed_storages["voltage_level"] = "unknown" - ix_ehv = installed_storages[installed_storages['v_nom'] >= 380].index - installed_storages.set_value(ix_ehv, 'voltage_level', 'ehv') + ix_ehv = installed_storages[installed_storages["v_nom"] >= 380].index + installed_storages.set_value(ix_ehv, "voltage_level", "ehv") - ix_hv = installed_storages[(installed_storages['v_nom'] <= 220) & - (installed_storages['v_nom'] >= 110)].index - installed_storages.set_value(ix_hv, 'voltage_level', 'hv') + ix_hv = installed_storages[ + (installed_storages["v_nom"] <= 220) & (installed_storages["v_nom"] >= 110) + ].index + installed_storages.set_value(ix_hv, "voltage_level", "hv") # add country differentiation - installed_storages['differentiation'] = 'none' + installed_storages["differentiation"] = "none" for idx, val in installed_storages.iterrows(): - check = val['country_code'] + check = val["country_code"] if "DE" in check: - installed_storages['differentiation'][idx] = 'domestic' + installed_storages["differentiation"][idx] = "domestic" if "DE" not in check: - installed_storages['differentiation'][idx] = 'foreign' - - storages_investment = installed_storages[ - ['voltage_level', 'investment_costs', - 'differentiation']].groupby(['differentiation', - 'voltage_level'] - ).sum().reset_index() - - storages_investment = storages_investment.\ - rename(columns={'investment_costs': 'capital_cost'}) + installed_storages["differentiation"][idx] = "foreign" + + storages_investment = ( + installed_storages[["voltage_level", "investment_costs", "differentiation"]] + .groupby(["differentiation", "voltage_level"]) + .sum() + .reset_index() + ) + + storages_investment = storages_investment.rename( + columns={"investment_costs": "capital_cost"} + ) return storages_investment diff --git a/ego/tools/utilities.py b/ego/tools/utilities.py index f4030f5c..1a9f3112 100644 --- a/ego/tools/utilities.py +++ b/ego/tools/utilities.py @@ -20,25 +20,29 @@ """This module contains utility functions for the eGo application. """ import csv -import os -import pandas as pd import json -import csv -import sys import logging +import os +import sys + +import pandas as pd + logger = logging.getLogger(__name__) from time import localtime, strftime -if not 'READTHEDOCS' in os.environ: + +if not "READTHEDOCS" in os.environ: from egoio.db_tables import model_draft, grid from egoio.tools import db -from sqlalchemy.orm import sessionmaker -from sqlalchemy.orm import scoped_session -__copyright__ = ("Flensburg University of Applied Sciences, " - "Europa-Universität Flensburg, " - "Centre for Sustainable Energy Systems") +from sqlalchemy.orm import scoped_session, sessionmaker + +__copyright__ = ( + "Flensburg University of Applied Sciences, " + "Europa-Universität Flensburg, " + "Centre for Sustainable Energy Systems" +) __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolf_bunke" @@ -61,24 +65,23 @@ def define_logging(name): # add pypsa and other logger INFO to ego.log now = strftime("%Y-%m-%d_%H%M%S", localtime()) - log_dir = 'logs' + log_dir = "logs" if not os.path.exists(log_dir): os.makedirs(log_dir) # Logging - logging.basicConfig(stream=sys.stdout, - format='%(asctime)s %(message)s', - level=logging.INFO) + logging.basicConfig( + stream=sys.stdout, format="%(asctime)s %(message)s", level=logging.INFO + ) logger = logging.getLogger(name) formatter = logging.Formatter( - '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) -# logger = logging.FileHandler(log_name, mode='w') - fh = logging.FileHandler( - log_dir + '/' + name + '_' + now + '.log', mode='w') + # logger = logging.FileHandler(log_name, mode='w') + fh = logging.FileHandler(log_dir + "/" + name + "_" + now + ".log", mode="w") fh.setLevel(logging.INFO) fh.setFormatter(formatter) logger.addHandler(fh) @@ -86,7 +89,7 @@ def define_logging(name): return logger -def get_scenario_setting(jsonpath='scenario_setting.json'): +def get_scenario_setting(jsonpath="scenario_setting.json"): """Get and open json file with scenaio settings of eGo. The settings incluede eGo, eTraGo and eDisGo specific settings of arguments and parameters for a reproducible @@ -107,83 +110,86 @@ def get_scenario_setting(jsonpath='scenario_setting.json'): # add try ego/ logger.info("Your path is: {}".format(path)) - with open(path + '/' + jsonpath) as f: + with open(path + "/" + jsonpath) as f: json_file = json.load(f) # fix remove result_id - json_file['eGo'].update({'result_id': None}) + json_file["eGo"].update({"result_id": None}) # check settings - if (json_file['eGo']['eTraGo'] is False and json_file['eGo']['eDisGo'] - is False): - logger.warning("Something went wrong! \n" - "Please contoll your settings and restart. \n" - "Set at least eTraGo = true") + if json_file["eGo"]["eTraGo"] is False and json_file["eGo"]["eDisGo"] is False: + logger.warning( + "Something went wrong! \n" + "Please contoll your settings and restart. \n" + "Set at least eTraGo = true" + ) return - if (json_file['eGo']['eTraGo'] is None and json_file['eGo']['eDisGo'] - is None): - logger.warning("Something went wrong! \n" - "Please contoll your settings and restart. \n" - "Set at least eTraGo = true") + if json_file["eGo"]["eTraGo"] is None and json_file["eGo"]["eDisGo"] is None: + logger.warning( + "Something went wrong! \n" + "Please contoll your settings and restart. \n" + "Set at least eTraGo = true" + ) return - if json_file['eGo']['result_id'] and json_file['eGo']['csv_import_eTraGo']: + if json_file["eGo"]["result_id"] and json_file["eGo"]["csv_import_eTraGo"]: logger.warning( "You set a DB result_id and a csv import path! \n" - "Please remove on of this settings") + "Please remove on of this settings" + ) return # or ? json_file['eGo']['result_id'] = None - if json_file['eGo']['eTraGo'] is None and json_file['eGo']['eDisGo']: - logger.info( - "eDisGo needs eTraGo results. Please change your settings!\n") + if json_file["eGo"]["eTraGo"] is None and json_file["eGo"]["eDisGo"]: + logger.info("eDisGo needs eTraGo results. Please change your settings!\n") return - if json_file['eGo']['eTraGo'] is False and json_file['eGo']['eDisGo']: - logger.info( - "eDisGo needs eTraGo results. Please change your settings!\n") + if json_file["eGo"]["eTraGo"] is False and json_file["eGo"]["eDisGo"]: + logger.info("eDisGo needs eTraGo results. Please change your settings!\n") return - if (json_file['eGo']['result_id'] is None and - json_file['eGo']['csv_import_eTraGo'] is None): + if ( + json_file["eGo"]["result_id"] is None + and json_file["eGo"]["csv_import_eTraGo"] is None + ): logger.info( - "No data import from results is set \n" - "eGo runs by given settings") + "No data import from results is set \n" "eGo runs by given settings" + ) - if (json_file['eGo']['csv_import_eTraGo'] and - json_file['eGo']['csv_import_eDisGo']): - logger.info( - "eDisGo and eTraGo results will be imported from csv\n") + if json_file["eGo"]["csv_import_eTraGo"] and json_file["eGo"]["csv_import_eDisGo"]: + logger.info("eDisGo and eTraGo results will be imported from csv\n") - if json_file['eGo'].get('eTraGo') == True: + if json_file["eGo"].get("eTraGo") == True: - logger.info('Using and importing eTraGo settings') + logger.info("Using and importing eTraGo settings") # special case of SH and model_draft # TODO: check and maybe remove this part sh_scen = ["SH Status Quo", "SH NEP 2035", "SH eGo 100"] - if json_file['eTraGo'].get('scn_name') in sh_scen and json_file['eTraGo'].\ - get('gridversion') is not None: - json_file['eTraGo']['gridversion'] = None + if ( + json_file["eTraGo"].get("scn_name") in sh_scen + and json_file["eTraGo"].get("gridversion") is not None + ): + json_file["eTraGo"]["gridversion"] = None - if json_file['eTraGo'].get('extendable') == "['network', 'storages']": - json_file['eTraGo'].update({'extendable': ['network', 'storage']}) + if json_file["eTraGo"].get("extendable") == "['network', 'storages']": + json_file["eTraGo"].update({"extendable": ["network", "storage"]}) - if json_file['eTraGo'].get('extendable') == "['network', 'storage']": - json_file['eTraGo'].update({'extendable': ['network', 'storage']}) + if json_file["eTraGo"].get("extendable") == "['network', 'storage']": + json_file["eTraGo"].update({"extendable": ["network", "storage"]}) - if json_file['eTraGo'].get('extendable') == "['network']": - json_file['eTraGo'].update({'extendable': ['network']}) + if json_file["eTraGo"].get("extendable") == "['network']": + json_file["eTraGo"].update({"extendable": ["network"]}) - if json_file['eTraGo'].get('extendable') == "['storages']": - json_file['eTraGo'].update({'extendable': ['storage']}) + if json_file["eTraGo"].get("extendable") == "['storages']": + json_file["eTraGo"].update({"extendable": ["storage"]}) - if json_file['eTraGo'].get('extendable') == "['storage']": - json_file['eTraGo'].update({'extendable': ['storage']}) + if json_file["eTraGo"].get("extendable") == "['storage']": + json_file["eTraGo"].update({"extendable": ["storage"]}) - if json_file['eGo'].get('eDisGo') == True: - logger.info('Using and importing eDisGo settings') + if json_file["eGo"].get("eDisGo") == True: + logger.info("Using and importing eDisGo settings") return json_file @@ -194,15 +200,15 @@ def fix_leading_separator(csv_file, **kwargs): separator in its header, this field is deleted. If this is done the second field of every row is removed, too. """ - with open(csv_file, 'r') as f: + with open(csv_file, "r") as f: lines = csv.reader(f, **kwargs) if not lines: - raise Exception('File %s contained no data' % csv_file) + raise Exception("File %s contained no data" % csv_file) first_line = next(lines) - if first_line[0] == '': + if first_line[0] == "": path, fname = os.path.split(csv_file) - tmp_file = os.path.join(path, 'tmp_' + fname) - with open(tmp_file, 'w+') as out: + tmp_file = os.path.join(path, "tmp_" + fname) + with open(tmp_file, "w+") as out: writer = csv.writer(out, **kwargs) writer.writerow(first_line[1:]) for line in lines: @@ -213,7 +219,7 @@ def fix_leading_separator(csv_file, **kwargs): def get_time_steps(json_file): - """ Get time step of calculation by scenario settings. + """Get time step of calculation by scenario settings. Parameters ---------- @@ -226,16 +232,15 @@ def get_time_steps(json_file): Number of timesteps of the calculation. """ - end = json_file['eTraGo'].get('end_snapshot') - start = json_file['eTraGo'].get('start_snapshot') + end = json_file["eTraGo"].get("end_snapshot") + start = json_file["eTraGo"].get("start_snapshot") time_step = end - start return time_step def open_oedb_session(ego): - """ - """ + """ """ _db_section = ego.json_file["eTraGo"]["db"] conn = db.connection(section=_db_section) session_factory = sessionmaker(bind=conn) diff --git a/setup.py b/setup.py index f54c12d5..28e87b5f 100644 --- a/setup.py +++ b/setup.py @@ -1,69 +1,63 @@ # -*- coding: utf-8 -*- import os + from setuptools import find_packages, setup -from pip._internal.req import parse_requirements -__copyright__ = ("Flensburg University of Applied Sciences, " - "Europa-Universität Flensburg, " - "Centre for Sustainable Energy Systems") +__copyright__ = ( + "Flensburg University of Applied Sciences, " + "Europa-Universität Flensburg, " + "Centre for Sustainable Energy Systems" +) __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolf_bunke, maltesc" + def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() - + + +req = [] + +dev_req = [ + "pre-commit", + "black", + "isort", + "pyupgrade", + "flake8", +] + +doc_req = [] + +full_req = list(set(dev_req + doc_req)) + +extras = { + "dev": dev_req, + "doc": doc_req, + "full": full_req, +} + setup( - name='eGo', - version='0.3.4', - author='wolfbunke, maltesc', - author_email='wolf-dieter.bunke@uni-flensburg.de', - description=("A cross-grid-level electricity grid and storage " - "optimization tool "), - long_description= read('README.rst'), - url='https://github.com/openego/eGo', + name="eGo", + version="0.3.4", + author="wolfbunke, maltesc", + author_email="wolf-dieter.bunke@uni-flensburg.de", + description=("A cross-grid-level electricity grid and storage optimization tool."), + long_description=read("README.rst"), + url="https://github.com/openego/eGo", license="GNU Affero General Public License Version 3 (AGPL-3.0)", packages=find_packages(), - package_dir={'ego': 'ego'}, + package_dir={"ego": "ego"}, include_package_data=True, - install_requires=['egoio == 0.4.5', - 'ding0 == v0.1.9', - 'pycallgraph', - 'eDisGo == v0.0.9', - 'eTraGo == 0.7.1', - 'scikit-learn == 0.19.0', - 'pandas ==0.20.3', - 'pypsa==0.11.0fork', - 'sqlalchemy == 1.2.0', - 'geoalchemy2 >= 0.3.0, <=0.4.0', - 'tsam==0.9.9', - 'geopandas', - 'matplotlib == 3.0.0', - 'Rtree', - 'descartes', - 'pyproj', - 'plotly==2.2.3', - 'shapely', - 'multiprocess', - 'folium', - 'oedialect' - ], + install_requires=req, dependency_links=[ - ('git+https://git@github.com/openego/PyPSA.git' - '@master#egg=pypsa-0.11.0fork')], - extras_require={ - 'doc': [ - 'sphinx >= 1.4', - 'sphinx_rtd_theme', - 'sphinxcontrib-httpdomain', - 'numpydoc == 0.7.0', - 'aiohttp_jinja2', - 'sphinx-jsondomain']}, + ("git+https://git@github.com/openego/PyPSA.git" "@master#egg=pypsa-0.11.0fork") + ], + extras_require=extras, package_data={ - 'ego': [os.path.join('tools', '*.csv')], - 'ego': [os.path.join('tools', '*.json')], - 'ego': [os.path.join('', '*.json')], - 'ego.data': ['*.csv'] - } - ) - + "ego": [os.path.join("tools", "*.csv")] + + [os.path.join("tools", "*.json")] + + [os.path.join("", "*.json")], + "ego.data": ["*.csv"], + }, +) From 383e9047dd46cc93a3705a4b299671db8521725a Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Fri, 23 Sep 2022 10:30:11 +0200 Subject: [PATCH 023/126] minor fix in readme --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index a0ac88ab..5931403a 100644 --- a/README.rst +++ b/README.rst @@ -48,7 +48,7 @@ Create a virtualenvironment and activate it: $ virtualenv venv --clear -p python3.8 $ source venv/bin/activate - $ cd venv + $ cd path/to/eGo $ python -m pip install -e .[full] $ pre-commit install # install pre-commit hooks From ff58b3013346f2e2c2005b46fc5d451ed8fe182b Mon Sep 17 00:00:00 2001 From: Malte Jahn Date: Fri, 3 Feb 2023 10:32:28 +0100 Subject: [PATCH 024/126] Run pre-commit hooks first time in branch --- .flake8 | 7 + .isort.cfg | 4 + .pre-commit-config.yaml | 24 + README.rst | 10 +- doc/_static/ego_example_iplot_map.html | 83700 ++++++++-------- doc/api/modules.rst | 22 +- doc/conf.py | 271 +- doc/developer.rst | 20 +- doc/getting_started.rst | 13 +- doc/images/open_ego_icon.svg | 2 +- doc/installation.rst | 10 +- doc/theoretical_background.rst | 49 +- doc/welcome.rst | 9 +- doc/whatsnew.rst | 6 +- doc/whatsnew/v0-2-0.rst | 20 +- doc/whatsnew/v0-3-0.rst | 15 +- doc/whatsnew/v0-3-1.rst | 6 +- doc/whatsnew/v0-3-2.rst | 1 - doc/whatsnew/v0-3-3.rst | 3 +- doc/whatsnew/v0-3-4.rst | 1 - ego/__init__.py | 6 +- ego/appl.py | 19 +- ego/examples/edisgo_mwe.py | 8 +- ego/examples/tutorials/requirements.yml | 1 - ego/run_test.py | 97 +- ego/tools/economics.py | 413 +- ego/tools/edisgo_integration.py | 876 +- ego/tools/interface.py | 5 +- ego/tools/io.py | 714 +- ego/tools/mv_cluster.py | 176 +- ego/tools/plots.py | 1160 +- ego/tools/results.py | 72 +- ego/tools/storages.py | 156 +- ego/tools/utilities.py | 153 +- pytest.ini | 2 +- setup.py | 111 +- tests/data/create_test_grid.ipynb | 2 +- .../data/etrago_test_network_1/generators.csv | 2 +- .../data/etrago_test_network_1/snapshots.csv | 2 +- .../etrago_test_network_1/storage_units.csv | 2 +- tests/data/etrago_test_network_1/stores.csv | 2 +- tests/tools/test_interface.py | 10 +- 42 files changed, 44188 insertions(+), 43994 deletions(-) create mode 100644 .flake8 create mode 100644 .isort.cfg create mode 100644 .pre-commit-config.yaml diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..3376e4aa --- /dev/null +++ b/.flake8 @@ -0,0 +1,7 @@ +[flake8] +extend-exclude = docs +max-line-length = 88 +extend-ignore = E203 +count = true +statistics = true +show-source = true diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 00000000..cc5b9a8a --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,4 @@ +[settings] +profile = black +multi_line_output = 3 +lines_between_types = 1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..cc65366d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace +- repo: https://github.com/psf/black + rev: 22.8.0 + hooks: + - id: black +- repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort + name: isort (python) +- repo: https://github.com/asottile/pyupgrade + rev: v2.38.0 + hooks: + - id: pyupgrade +- repo: https://github.com/pycqa/flake8 + rev: 5.0.4 + hooks: + - id: flake8 diff --git a/README.rst b/README.rst index 3aa23ff0..5b27c5eb 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ .. image:: https://openegoproject.files.wordpress.com/2017/02/open_ego_logo_breit.png?w=400 - + *A cross-grid-level electricity grid and storage optimization tool* | `openegoproject.wordpress.com `_ @@ -16,8 +16,8 @@ eGo Integrated optimization of flexibility options and grid extension measures for power grids based on `eTraGo `_ and -`eDisGo `_. The Documentation of the eGo tool -can be found on +`eDisGo `_. The Documentation of the eGo tool +can be found on `openego.readthedocs.io `_ . .. contents:: @@ -35,7 +35,7 @@ In case of installation errors of pypsa-fork use: .. code-block:: - $ pip3 install -e git+https://github.com/openego/PyPSA@master#egg=0.11.0fork + $ pip3 install -e git+https://github.com/openego/PyPSA@master#egg=0.11.0fork ---------------------------- @@ -84,6 +84,6 @@ this program. If not, see https://www.gnu.org/licenses/. .. |readthedocs| image:: https://readthedocs.org/projects/openego/badge/?version=master :target: http://openego.readthedocs.io/en/latest/?badge=master :alt: Documentation Status - + .. |zenodo| image:: https://zenodo.org/badge/87306120.svg :target: https://zenodo.org/badge/latestdoi/87306120 diff --git a/doc/_static/ego_example_iplot_map.html b/doc/_static/ego_example_iplot_map.html index 16f51970..34c5d0ab 100644 --- a/doc/_static/ego_example_iplot_map.html +++ b/doc/_static/ego_example_iplot_map.html @@ -1,5 +1,5 @@ - + @@ -14,7 +14,7 @@ - + - + - - + + @@ -92,7 +92,7 @@ - +
    .maplegend .legend-title { text-align: left; @@ -221,8 +221,8 @@ - - + +
    - - + +
    - + float_image - + - diff --git a/doc/api/modules.rst b/doc/api/modules.rst index 2868e6d4..f613d4c1 100644 --- a/doc/api/modules.rst +++ b/doc/api/modules.rst @@ -12,12 +12,12 @@ Overview of modules :maxdepth: 7 ego.tools - + scenario_settings.json ====================== -With the ``scenario_settings.json`` file you set up your calcualtion. -The file can be found on +With the ``scenario_settings.json`` file you set up your calcualtion. +The file can be found on `github `_. .. json:object:: scenario_setting.json @@ -33,24 +33,24 @@ The file can be found on .. json:object:: global - + :property bool eTraGo: Decide if you want to run the eTraGo tool (HV/EHV grid optimization). :property bool eDisGo: Decide if you want to run the eDisGo tool (MV grid optimiztaion). Please note: eDisGo requires eTraGo= ``true``. - :property string csv_import_eTraGo: ``false`` or path to previously calculated eTraGo results (in order to reload the results instead of performing a new run). - :property string csv_import_eDisGo: ``false`` or path to previously calculated eDisGo results (in order to reload the results instead of performing a new run). + :property string csv_import_eTraGo: ``false`` or path to previously calculated eTraGo results (in order to reload the results instead of performing a new run). + :property string csv_import_eDisGo: ``false`` or path to previously calculated eDisGo results (in order to reload the results instead of performing a new run). + - .. json:object:: eTraGo This section of :json:object:`scenario_setting.json` contains all input parameters for the eTraGo tool. A description of the parameters can be found `here. `_ - + .. json:object:: eDisGo This section of :json:object:`scenario_setting.json` contains all input parameters for the eDisGo tool and the clustering of MV grids. - :property string db: Name of your database (e.g.``''oedb''``). eDisGo queries generator data from this database. Please note that this parameters is automatically overwritten in eDisGo's configuration files. - :property string gridversion: ``null`` or *open_eGo* dataset version (e.g. ``''v0.4.5''``). If ``null``, *open_eGo*'s model_draft is used. Please note that this parameters is automatically overwritten in eDisGo's configuration files. + :property string db: Name of your database (e.g.``''oedb''``). eDisGo queries generator data from this database. Please note that this parameters is automatically overwritten in eDisGo's configuration files. + :property string gridversion: ``null`` or *open_eGo* dataset version (e.g. ``''v0.4.5''``). If ``null``, *open_eGo*'s model_draft is used. Please note that this parameters is automatically overwritten in eDisGo's configuration files. :property string ding0_path: Path to the MV grid files (created by `ding0 `_) (e.g. ``''data/MV_grids/20180713110719''``) :property string choice_mode: Mode that eGo uses to chose MV grids out of the files in **ding0_path** (e.g. ``''manual''``, ``''cluster''`` or ``''all''``). If ``''manual''`` is chosen, the parameter **manual_grids** must contain a list of the desired grids. If ``''cluster''`` is chosen, **no_grids** must specify the desired number of clusters and **cluster_attributes** must specify the applied cluster attributes. If ``''all''`` is chosen, all MV grids from **ding0_path** are calculated. :property list cluster_attributes: List of strings containing the desired cluster attributes. Available attributes are: ``''farthest_node''``, ``''wind_cap''``, ``''solar_cap''`` and ``''extended_storage''``, thus an exemplary list looks like ``["farthest_node", "wind_cap", "solar_cap", "extended_storage"]``. ``''farthest_node''`` represents the longest path within each grid, ``''wind_cap''`` the installed wind capacity within each grid, ``''solar_cap''`` the installed solar capacity within each grid and ``''extended_storage''`` the installed storage units (as calculated by eTraGo). Please note that ``''extended_storage''`` is only available in combination with eTraGo datasets that optimized storage extension. Otherwise this attribute is ignored. @@ -65,7 +65,7 @@ The file can be found on :property string solver: Solver eDisGo uses to optimize the curtailment and storage integration (e.g. ``''gurobi''``). :property string timesteps_pfa: Method eDisGo uses for the storage integration (e.g. ``''snapshot_analysis''``). :property string results: Path to folder where eDisGo's results will be saved. - + appl.py diff --git a/doc/conf.py b/doc/conf.py index a64f5fd5..0f2d2351 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -25,43 +25,45 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys import os import shlex +import sys + from unittest.mock import MagicMock -#from mock import Mock as MagicMock + +# from mock import Mock as MagicMock # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) -sys.path.insert(0, os.path.abspath('../')) -sys.path.insert(0, os.path.abspath('../..')) +# sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath("../")) +sys.path.insert(0, os.path.abspath("../..")) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.imgmath' , - 'sphinx.ext.viewcode', - 'sphinx.ext.autosummary', -# 'sphinxcontrib.napoleon',#enable Napoleon interpreter of docstrings Sphinx v<=1.2 - 'sphinx.ext.napoleon', #enable Napoleon Sphinx v>1.3 -# 'sphinx_paramlinks',#to have links to the types of the parameters of the functions - 'numpydoc', - 'sphinxcontrib.httpdomain', # for restfull API - 'sphinxcontrib.autohttp.flask', - 'sphinx.ext.extlinks', # enables external links with a key - 'sphinxjsondomain' + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.imgmath", + "sphinx.ext.viewcode", + "sphinx.ext.autosummary", + # 'sphinxcontrib.napoleon',#enable Napoleon interpreter of docstrings Sphinx v<=1.2 + "sphinx.ext.napoleon", # enable Napoleon Sphinx v>1.3 + # 'sphinx_paramlinks',#to have links to the types of the parameters of the functions + "numpydoc", + "sphinxcontrib.httpdomain", # for restfull API + "sphinxcontrib.autohttp.flask", + "sphinx.ext.extlinks", # enables external links with a key + "sphinxjsondomain", ] @@ -84,13 +86,14 @@ # Dictionary of external links -extlinks = {'pandas':('http://pandas.pydata.org/pandas-docs/stable/api.html#%s', - 'pandas.'), - 'sqlalchemy':('http://docs.sqlalchemy.org/en/latest/orm/session_basics.html%s', - 'SQLAlchemy session object'), - 'shapely':('http://toblerity.org/shapely/manual.html#%s', - 'Shapely object') - } +extlinks = { + "pandas": ("http://pandas.pydata.org/pandas-docs/stable/api.html#%s", "pandas."), + "sqlalchemy": ( + "http://docs.sqlalchemy.org/en/latest/orm/session_basics.html%s", + "SQLAlchemy session object", + ), + "shapely": ("http://toblerity.org/shapely/manual.html#%s", "Shapely object"), +} # test oedb implementation def rstjinja(app, docname, source): @@ -98,61 +101,61 @@ def rstjinja(app, docname, source): Render our pages as a jinja template for fancy templating goodness. """ # Make sure we're outputting HTML - if app.builder.format != 'html': + if app.builder.format != "html": return src = source[0] - rendered = app.builder.templates.render_string( - src, app.config.html_context - ) + rendered = app.builder.templates.render_string(src, app.config.html_context) source[0] = rendered + def setup(app): app.connect("source-read", rstjinja) -#import requests -#oep_url= 'http://oep.iks.cs.ovgu.de/' +# import requests + +# oep_url= 'http://oep.iks.cs.ovgu.de/' # get data from oedb test -#power_class = requests.get(oep_url+'/api/v0/schema/model_draft/tables/ego_power_class/rows/', ).json() +# power_class = requests.get(oep_url+'/api/v0/schema/model_draft/tables/ego_power_class/rows/', ).json() -#import json -#path = os.getcwd() -#json_file = '../ego/scenario_setting.json' +# import json +# path = os.getcwd() +# json_file = '../ego/scenario_setting.json' -#with open(path +'/'+json_file) as f: +# with open(path +'/'+json_file) as f: # scn_set = json.load(f) # -#json_global = list(scn_set['eTraGo']) +# json_global = list(scn_set['eTraGo']) # -#html_context = { +# html_context = { # 'power_class': power_class, # 'scn_setting': scn_set -#} +# } # add RestFull API -httpexample_scheme = 'https' +httpexample_scheme = "https" # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'eGo' -copyright = u'2015-2018, open_eGo-Team' -author = u'open_eGo-Team' +project = "eGo" +copyright = "2015-2018, open_eGo-Team" +author = "open_eGo-Team" # The version info for the project you're documenting, acts as replacement for @@ -160,9 +163,9 @@ def setup(app): # built documents. # # The short X.Y version. -version = '0.3.4' +version = "0.3.4" # The full version, including alpha/beta/rc tags. -release = '0.3.4' +release = "0.3.4" # The language for content autogenerated by Sphinx. Refer to documentation @@ -174,37 +177,37 @@ def setup(app): # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build', 'whatsnew', '_static'] +exclude_patterns = ["_build", "whatsnew", "_static"] # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True @@ -214,18 +217,27 @@ def setup(app): # see http://read-the-docs.readthedocs.io/en/latest/faq.html#i-get-import-errors-on-libraries-that-depend-on-c-modules -if 'READTHEDOCS' in os.environ: +if "READTHEDOCS" in os.environ: + class Mock(MagicMock): @classmethod def __getattr__(cls, name): - return MagicMock() + return MagicMock() - MOCK_MODULES = ['ding0', 'ding0.results', 'shapely'] + MOCK_MODULES = ["ding0", "ding0.results", "shapely"] sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) - MOCK_MODULES = ['libgeos', 'geos', 'libgeos_c', 'geos_c','libgeos_c.so.1', - 'libgeos_c.so', 'shapely', 'geoalchemy2', 'geoalchemy2.shape '] - + MOCK_MODULES = [ + "libgeos", + "geos", + "libgeos_c", + "geos_c", + "libgeos_c.so.1", + "libgeos_c.so", + "shapely", + "geoalchemy2", + "geoalchemy2.shape ", + ] # -- Options for HTML output ---------------------------------------------- @@ -235,157 +247,151 @@ def __getattr__(cls, name): # html_theme = 'alabaster' import sphinx_rtd_theme + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' -#html_search_language = 'en' +# html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value -#html_search_options = {'type': 'default'} +# html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' +# html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'eGodoc' +htmlhelp_basename = "eGodoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', - -# Latex figure (float) alignment -#'figure_align': 'htbp', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', + # Latex figure (float) alignment + #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'eGo.tex', u'eGo Documentation', - u'open\_eGo-Team', 'manual'), + (master_doc, "eGo.tex", "eGo Documentation", r"open\_eGo-Team", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'eGo', u'eGo Documentation', - [author], 1) -] +man_pages = [(master_doc, "eGo", "eGo Documentation", [author], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -394,36 +400,33 @@ def __getattr__(cls, name): # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'eGo', u'eGo Documentation', - author, 'eGo', 'Titel', - 'Miscellaneous'), + (master_doc, "eGo", "eGo Documentation", author, "eGo", "Titel", "Miscellaneous"), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'python': ('https://docs.python.org/3', None), - 'etrago': ('https://etrago.readthedocs.io/en/latest', - None), - 'edisgo': ('http://edisgo.readthedocs.io/en/dev',None), - 'ding0': ('https://dingo.readthedocs.io/en/dev',None), - 'pypsa': ('https://pypsa.org/doc/',None), - 'sqlalchemy': ('https://docs.sqlalchemy.org/en/latest/', - None), - } +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "etrago": ("https://etrago.readthedocs.io/en/latest", None), + "edisgo": ("http://edisgo.readthedocs.io/en/dev", None), + "ding0": ("https://dingo.readthedocs.io/en/dev", None), + "pypsa": ("https://pypsa.org/doc/", None), + "sqlalchemy": ("https://docs.sqlalchemy.org/en/latest/", None), +} # Numbered figures numfig = True -autodoc_member_order = 'bysource' +autodoc_member_order = "bysource" diff --git a/doc/developer.rst b/doc/developer.rst index 88019905..adad24f5 100644 --- a/doc/developer.rst +++ b/doc/developer.rst @@ -9,8 +9,8 @@ Installation .. note:: Installation is only tested on (Ubuntu 16.04 ) linux OS. -Please read the Installation Guideline :ref:`ego.doc.installation`. - +Please read the Installation Guideline :ref:`ego.doc.installation`. + 1. Use virtual environment -------------------------- @@ -58,17 +58,17 @@ script, which can be found under ``ding0/ding0/examples/``. `Learn more about Dingo `_. Before you run the script check also the configs of Dingo and eDisGo in order to use the right database version. You find this files under -``ding0/ding0/config/config_db_tables.cfg`` and +``ding0/ding0/config/config_db_tables.cfg`` and ``~.edisgo/config/config_db_tables.cfg``. Your created ding0 grids are stored in -``~.ding0/..``. - +``~.ding0/..``. + eDisGo and eTraGo ----------------- -Please read the Developer notes of -`eDisGo `_ and +Please read the Developer notes of +`eDisGo `_ and `eTraGo `_. @@ -79,17 +79,17 @@ Error handling ``pip install --upgrade pip==18.1`` 2. Installation Error of eTraGo, eDisGo, Pypsa fork or ding0. - If you have problems with one of those packages please clone it from + If you have problems with one of those packages please clone it from *github.com* and install it from the master or dev branch. For example ``pip3 install -e git+https://github.com/openego//PyPSA.git@master#egg=pypsafork`` 3. Matplotlib error on server and few other systems. Please change your settings - in ``matplotlibrc`` from ``backend : TkAgg`` to ``backend : PDF``. You can + in ``matplotlibrc`` from ``backend : TkAgg`` to ``backend : PDF``. You can find the file for example in a virtual environment under ``~/env/lib/python3.5/site-packages/matplotlib/mpl-data$ vim matplotlibrc``. `Learn more here. `_. 4. Geopandas error caused by Rtree ``Could not find libspatialindex_c library`` - Please reinstall Rtree with ``sudo pip3 install Rtree`` or install + Please reinstall Rtree with ``sudo pip3 install Rtree`` or install ``libspatialindex_c`` via ``sudo apt install python3-rtree``. On Windows or macOS you maybe install ``libspatialindex_c`` straight from source. diff --git a/doc/getting_started.rst b/doc/getting_started.rst index 256ca7e5..40c211e6 100644 --- a/doc/getting_started.rst +++ b/doc/getting_started.rst @@ -28,11 +28,11 @@ Steps to run eGo ``eGo/ego`` and ``>>> python3 appl.py`` . You can also use any other Python Terminal, Jupyter Notebook or Editor. - + How to use eGo? =============== - + Start and use eGo from the terminal. .. code-block:: bash @@ -71,13 +71,13 @@ on `jupyter.org `_. `Workshop open_eGo Session eGo (in German) `_ `Workshop open_eGo Session eTraGo (in German) `_ - + `Workshop open_eGo Session DinGo (in German) `_ `Workshop open_eGo Session eDisGo (in German) `_ - + `OpenMod eTraGo Tutorial (in English) `_ - + @@ -89,7 +89,7 @@ A small example of the eGo results is displayed below. The full page can be foun .. raw:: html - + @@ -101,4 +101,3 @@ The plot is created by the eGo function: .. code-block:: python ego.iplot - diff --git a/doc/images/open_ego_icon.svg b/doc/images/open_ego_icon.svg index dcdf778b..f65c3e13 100644 --- a/doc/images/open_ego_icon.svg +++ b/doc/images/open_ego_icon.svg @@ -108,4 +108,4 @@ id="path4581-6" cx="-194.7018" cy="-183.20657" - r="67.5" /> \ No newline at end of file + r="67.5" /> diff --git a/doc/installation.rst b/doc/installation.rst index 3d741d7c..e98548fc 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -12,13 +12,13 @@ command in order to install eGo: $ pip3 install eGo --process-dependency-links Please ensure, that you are using the pip version 18.1. -Use ``pip install --upgrade pip==18.1`` to get the right pip version. +Use ``pip install --upgrade pip==18.1`` to get the right pip version. In Case of problems with the Installation and the ``dependency_links`` of the PyPSA fork, please istall PyPSA from the github.com/openego Repository. .. code-block:: bash - $ pip3 install -e git+https://github.com/openego/PyPSA@master#egg=0.11.0fork + $ pip3 install -e git+https://github.com/openego/PyPSA@master#egg=0.11.0fork Using virtual environment @@ -117,7 +117,7 @@ Old developer connection host = oe2.iws.cs.ovgu.de port = 5432 pw = YourOEDBPassword - - - + + + Please find more information on *Developer notes*. diff --git a/doc/theoretical_background.rst b/doc/theoretical_background.rst index 2f3d5dcb..f0e5141a 100644 --- a/doc/theoretical_background.rst +++ b/doc/theoretical_background.rst @@ -46,26 +46,26 @@ Subsequent to the MV grid simulations with the reduced number of representative Economic calculation ==================== -The tool *eGo* unites the extra high (ehv) and high voltage (hv) models with the -medium (mv) and low voltage (lv) models to ascertain the costs per selected -measure and scenario. This results in a cross-grid-level economic result of +The tool *eGo* unites the extra high (ehv) and high voltage (hv) models with the +medium (mv) and low voltage (lv) models to ascertain the costs per selected +measure and scenario. This results in a cross-grid-level economic result of the electrical grid and storage optimisation. Overnight costs --------------- -The *overnight costs* represents the investment costs of the components or -construction project without any interest, as if the project was completed +The *overnight costs* represents the investment costs of the components or +construction project without any interest, as if the project was completed "overnight". The overnight costs (:math:`C_{\text{Overnight}}` ) of the grid measures (lines and transformers) are calculated as: .. math:: - C_{Line~extension} = S_{Extension}~[MVA] * C_{assumtion}~[\frac{EUR}{MVA}] * L_{Line~length}~[km] + C_{Line~extension} = S_{Extension}~[MVA] * C_{assumtion}~[\frac{EUR}{MVA}] * L_{Line~length}~[km] .. math:: - C_{Transformer~extension} = S_{Extension}~[MVA] * C_{assumtion}~[\frac{EUR}{MVA}] + C_{Transformer~extension} = S_{Extension}~[MVA] * C_{assumtion}~[\frac{EUR}{MVA}] The total overnight grid extension costs are given by: @@ -85,13 +85,13 @@ Annuity costs ------------- The *annuity costs* represents project investment costs with an interest as present -value of an annuity. The investment years *T* and the interest rate *p* are +value of an annuity. The investment years *T* and the interest rate *p* are defined as default in *eGo* with an interest rate ( :math:`p` ) of ``0.05`` -and a number of investment years ( :math:`T` ) of ``40 years``. The values are +and a number of investment years ( :math:`T` ) of ``40 years``. The values are based on the [StromNEV_A1]_ for the grid investment regulation in Germany. The present value of an annuity (PVA) is calculated as: - + .. math:: PVA = \frac{1}{p}- \frac{1}{\left ( p*\left (1 + p \right )^T \right )} @@ -115,14 +115,14 @@ Investment costs ehv/hv ----------------------- The investment costs of the grid and storage expansion are taken from the studies -[NEP2015a]_ for the extra and high voltage components and the [Dena]_. The -given costs are transformed in respect to PyPSA *[€/MVA]* format [PyPSA]_ +[NEP2015a]_ for the extra and high voltage components and the [Dena]_. The +given costs are transformed in respect to PyPSA *[€/MVA]* format [PyPSA]_ components for the optimisation. - + **Overview of grid cost assumtions:** -The table displays the transformer and line costs which are used for the +The table displays the transformer and line costs which are used for the calculation with *eTraGo*. .. csv-table:: Overview of grid cost assumtions @@ -131,8 +131,8 @@ calculation with *eTraGo*. :header-rows: 1 The *eTraGo* calculation of the annuity costs per simulation period is defined -in :func:`~etrago.tools.utilities.set_line_costs` and -:func:`~etrago.tools.utilities.set_trafo_costs`. +in :func:`~etrago.tools.utilities.set_line_costs` and +:func:`~etrago.tools.utilities.set_trafo_costs`. **Overview of storage cost assumtions:** @@ -143,10 +143,10 @@ in :func:`~etrago.tools.utilities.set_line_costs` and Investment costs mv/lv ---------------------- -The tool *eDisGO* is calculating all grid expansion measures as capital or -*overnight* costs. In order to get the annuity costs of eDisGo's optimisation +The tool *eDisGO* is calculating all grid expansion measures as capital or +*overnight* costs. In order to get the annuity costs of eDisGo's optimisation results the function :func:`~ego.tools.economics.edisgo_convert_capital_costs` -is used. The cost assumption of [eDisGo]_ are taken from the [Dena]_ +is used. The cost assumption of [eDisGo]_ are taken from the [Dena]_ and [CONSENTEC]_ study. Based on the component the costs including earthwork costs can depend on population density according to [Dena]_. @@ -157,7 +157,7 @@ References .. [NEP2015a] Übertragungsnetzbetreiber Deutschland. (2015). - *Netzentwicklungsplan Strom 2025 - Kostenschaetzungen*, Version 2015, + *Netzentwicklungsplan Strom 2025 - Kostenschaetzungen*, Version 2015, 1. Entwurf, 2015. (``_) @@ -176,20 +176,17 @@ References (``_) .. [Overnight cost] Wikipedia (2018). - *Definition of overnight cost*. + *Definition of overnight cost*. (``_) .. [eDisGo] eDisGo - grid expantion costs (2018). - *Cost assumption on mv and lv grid components*. + *Cost assumption on mv and lv grid components*. (``_) .. [CONSENTEC] CONSENTEC et.al (2006). *Untersuchung der Voraussetzungen und möglicher Anwendung analytischer* - *Kostenmodelle in der deutschen Energiewirtschaft *. + *Kostenmodelle in der deutschen Energiewirtschaft *. (``_) - - - diff --git a/doc/welcome.rst b/doc/welcome.rst index ab439808..0460e992 100644 --- a/doc/welcome.rst +++ b/doc/welcome.rst @@ -28,7 +28,7 @@ in order to use eGo a registration on the OpenEnergy Platform is required. For m information see `openenergy-platform `_ and login. -The OpenEnergy platform mainly addresses students, researchers and scientists in +The OpenEnergy platform mainly addresses students, researchers and scientists in the field of energy modelling and analytics, but also welcomes all other interested parties. The platform provides great tools to make your energy system modelling process transparent. Data of the open_eGo project are stored on @@ -73,7 +73,7 @@ grid data for whole Germany. `Learn more here `_. Dataprocessing -------------- -For the open_eGo project several python packages are developed which are feeded +For the open_eGo project several python packages are developed which are feeded by the input data of the data processing. The dataprocessing is written in SQL and Python. `Learn more here `_. @@ -115,7 +115,7 @@ License .. image:: images/open_ego_icon_web.png :scale: 100% :align: right - + © Copyright 2015-2018 Flensburg University of Applied Sciences, @@ -150,7 +150,7 @@ Partner :alt: ZNES Flensburg :target: http://www.znes-flensburg.de/project/150?language=en :align: right - + .. image:: https://i0.wp.com/reiner-lemoine-institut.de/wp-content/uploads/2015/08/RLI_Logo.png :scale: 90% :width: 180px @@ -176,4 +176,3 @@ Partner :alt: Uni Magdeburg :target: http://iks.cs.ovgu.de/IKS.html :align: left - diff --git a/doc/whatsnew.rst b/doc/whatsnew.rst index a5bb64e2..f1308877 100644 --- a/doc/whatsnew.rst +++ b/doc/whatsnew.rst @@ -7,9 +7,9 @@ What's new :local: :backlinks: top -.. include:: whatsnew/v0-3-4.rst -.. include:: whatsnew/v0-3-3.rst -.. include:: whatsnew/v0-3-2.rst +.. include:: whatsnew/v0-3-4.rst +.. include:: whatsnew/v0-3-3.rst +.. include:: whatsnew/v0-3-2.rst .. include:: whatsnew/v0-3-1.rst .. include:: whatsnew/v0-3-0.rst .. include:: whatsnew/v0-2-0.rst diff --git a/doc/whatsnew/v0-2-0.rst b/doc/whatsnew/v0-2-0.rst index 13e9f8d8..f9b5bd01 100644 --- a/doc/whatsnew/v0-2-0.rst +++ b/doc/whatsnew/v0-2-0.rst @@ -1,15 +1,15 @@ Release v0.2.0 (July 18, 2018) ++++++++++++++++++++++++++++++ -Fundamental structural changes of the eGo tool are included in this release. -A new feature is the integration of the MV grid power flow simulations, -performed by the tool `eDisGo. `_. -Thereby, eGo can be used to perform power flow simulations and optimizations +Fundamental structural changes of the eGo tool are included in this release. +A new feature is the integration of the MV grid power flow simulations, +performed by the tool `eDisGo. `_. +Thereby, eGo can be used to perform power flow simulations and optimizations for EHV, HV (*eTraGo*) and MV (*eDisGo*) grids. -Moreover, the use of the Dataprocessing versions -``''v0.4.1''`` and ``''v0.4.2''`` is supported. Please note, that this release -is still under construction and only recommended for developers of +Moreover, the use of the Dataprocessing versions +``''v0.4.1''`` and ``''v0.4.2''`` is supported. Please note, that this release +is still under construction and only recommended for developers of the *open_eGo* project. Furthermore, overall cost aggregation functions are available. @@ -42,8 +42,8 @@ Notes ----- * As an external user you need to have an account on the `openenergy-platform.org/login `_ -* In future versions, all MV grids (*ding0* grids) will be queried from your - database. However, in this version all MV grids have to be generated with - the tool `ding0 `_ and stored in *eGo*'s +* In future versions, all MV grids (*ding0* grids) will be queried from your + database. However, in this version all MV grids have to be generated with + the tool `ding0 `_ and stored in *eGo*'s *data* folder. * Total operational costs are missing in this release diff --git a/doc/whatsnew/v0-3-0.rst b/doc/whatsnew/v0-3-0.rst index 91c53cf0..65ce2499 100644 --- a/doc/whatsnew/v0-3-0.rst +++ b/doc/whatsnew/v0-3-0.rst @@ -1,10 +1,10 @@ Release v0.3.0 (September 07, 2018) +++++++++++++++++++++++++++++++++++ -Power Flow and Clustering. eGo is now using eTraGo non-linear power flows based -on optimization results and its disaggregation of clustered results -to an original spatial complexities. With the release of eDisGo speed-up options, -a new storage integration methodology and more are now available. +Power Flow and Clustering. eGo is now using eTraGo non-linear power flows based +on optimization results and its disaggregation of clustered results +to an original spatial complexities. With the release of eDisGo speed-up options, +a new storage integration methodology and more are now available. Added features @@ -14,7 +14,7 @@ Added features * Implementing of Ding0 grid parallelization * Redesign of scenario settings and API simplifications * Adding and using the Power Flow of eTraGo in eGo -* Testing and using new dataprocessing Version v0.4.3, v0.4.4 and v0.4.5 +* Testing and using new dataprocessing Version v0.4.3, v0.4.4 and v0.4.5 * make eGo installable from pip via ``pip3 install eGo -- process-dependency-links`` * Implementing eDisGo's storage distribution for MV and LV grids * Improved logging and the creation of status files @@ -23,7 +23,4 @@ Added features * Storage-related investment costs are also allocated to MV grids * Update of cluster plots * Plot of investment costs per line and bus -* Update of ``ego.iplot`` for an interactive visualization - - - +* Update of ``ego.iplot`` for an interactive visualization diff --git a/doc/whatsnew/v0-3-1.rst b/doc/whatsnew/v0-3-1.rst index 03231c53..8761126d 100644 --- a/doc/whatsnew/v0-3-1.rst +++ b/doc/whatsnew/v0-3-1.rst @@ -1,7 +1,7 @@ Release v0.3.1 (October 27, 2018) +++++++++++++++++++++++++++++++++ -This release contains documentation and bug fixes for the new features +This release contains documentation and bug fixes for the new features introduced in 0.3.0. Added features @@ -14,10 +14,10 @@ Added features * Change and update of API file scenario_setting.json * Improved cluster plot of ``ego.plot_edisgo_cluster()`` * Improved cost differentiation -* Add jupyter notebook eGo tutorials +* Add jupyter notebook eGo tutorials Fixes ----- * Fix installation problems of the pypsa 0.11.0 fork (use pip 18.1) -* Fix parallel calculation of mv results +* Fix parallel calculation of mv results diff --git a/doc/whatsnew/v0-3-2.rst b/doc/whatsnew/v0-3-2.rst index b5c5a5b7..262b3f45 100644 --- a/doc/whatsnew/v0-3-2.rst +++ b/doc/whatsnew/v0-3-2.rst @@ -7,4 +7,3 @@ Added features -------------- * Registration at zenodo.org - diff --git a/doc/whatsnew/v0-3-3.rst b/doc/whatsnew/v0-3-3.rst index c103d9ac..c1e895c7 100644 --- a/doc/whatsnew/v0-3-3.rst +++ b/doc/whatsnew/v0-3-3.rst @@ -13,6 +13,5 @@ Fixes ----- * Fix bug of period calculation -* removed duplicate matplotlib from setup.py +* removed duplicate matplotlib from setup.py * fixed csv import - diff --git a/doc/whatsnew/v0-3-4.rst b/doc/whatsnew/v0-3-4.rst index d20cef9c..00680e50 100644 --- a/doc/whatsnew/v0-3-4.rst +++ b/doc/whatsnew/v0-3-4.rst @@ -7,4 +7,3 @@ Added features -------------- * Update eDisGo version from 0.0.8 to 0.0.9 - diff --git a/ego/__init__.py b/ego/__init__.py index c87bd1d7..ee083176 100644 --- a/ego/__init__.py +++ b/ego/__init__.py @@ -11,11 +11,13 @@ # along with this program. If not, see . __version__ = "0.3.4" -__copyright__ = ("Europa-Universität Flensburg, " - " Centre for Sustainable Energy Systems") +__copyright__ = ( + "Europa-Universität Flensburg, " " Centre for Sustainable Energy Systems" +) __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolf_bunke" import logging + logging.basicConfig(level=logging.INFO) diff --git a/ego/appl.py b/ego/appl.py index 627d7d99..0539ba3d 100644 --- a/ego/appl.py +++ b/ego/appl.py @@ -29,23 +29,26 @@ import os -if not 'READTHEDOCS' in os.environ: +if not "READTHEDOCS" in os.environ: from tools.io import eGo from tools.utilities import define_logging - logger = define_logging(name='ego') -__copyright__ = ("Flensburg University of Applied Sciences, " - "Europa-Universität Flensburg, " - "Centre for Sustainable Energy Systems") + logger = define_logging(name="ego") + +__copyright__ = ( + "Flensburg University of Applied Sciences, " + "Europa-Universität Flensburg, " + "Centre for Sustainable Energy Systems" +) __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolf_bunke, maltesc" -if __name__ == '__main__': +if __name__ == "__main__": - logger.info('Start calculation') + logger.info("Start calculation") - ego = eGo(jsonpath='scenario_setting.json') + ego = eGo(jsonpath="scenario_setting.json") # logger.info('Print results') # ego.etrago_line_loading() # print(ego.etrago.generator) diff --git a/ego/examples/edisgo_mwe.py b/ego/examples/edisgo_mwe.py index c624b5d6..7e0a3fb7 100644 --- a/ego/examples/edisgo_mwe.py +++ b/ego/examples/edisgo_mwe.py @@ -1,6 +1,7 @@ import os from pypsa import Network as PyPSANetwork + from ego.tools.edisgo_integration import EDisGoNetworks from ego.tools.utilities import get_scenario_setting @@ -13,9 +14,6 @@ json_file = get_scenario_setting() -edisgo_networks = EDisGoNetworks( - json_file=json_file, - etrago_network=etrago_network -) +edisgo_networks = EDisGoNetworks(json_file=json_file, etrago_network=etrago_network) -print("x") \ No newline at end of file +print("x") diff --git a/ego/examples/tutorials/requirements.yml b/ego/examples/tutorials/requirements.yml index a672c934..7fe086c7 100644 --- a/ego/examples/tutorials/requirements.yml +++ b/ego/examples/tutorials/requirements.yml @@ -25,4 +25,3 @@ dependencies: - "--editable=git+git@github.com:openego/eTraGo.git@0.5.1#egg=etrago" # eTraGo==0.5.1 - "--editable=git+git@github.com:openego/tsam.git@master#egg=tsam" - "--editable=git+git@github.com:openego/eDisGo.git@dev#egg=edisgo" - diff --git a/ego/run_test.py b/ego/run_test.py index 23c020f3..c9d016c5 100644 --- a/ego/run_test.py +++ b/ego/run_test.py @@ -1,32 +1,37 @@ # -*- coding: utf-8 -*- -from datetime import datetime -from tools.io import eGo +import os import sys -from pycallgraph import PyCallGraph -from pycallgraph.output import GraphvizOutput -from pycallgraph import Config -import pandas as pd + +from datetime import datetime + import matplotlib.pyplot as plt -import os +import pandas as pd + +from pycallgraph import Config, PyCallGraph +from pycallgraph.output import GraphvizOutput +from tools.io import eGo from tools.utilities import define_logging -logger = define_logging(name='ego') + +logger = define_logging(name="ego") def ego_testing(ego): - """ Call and test all ego Funktion - """ + """Call and test all ego Funktion""" # full networks try: logger.info("ego.etrago.network: {} ".format(ego.etrago.network)) - logger.info("ego.etrago.disaggregated_network: {} ".format( - ego.etrago.disaggregated_network)) + logger.info( + "ego.etrago.disaggregated_network: {} ".format( + ego.etrago.disaggregated_network + ) + ) # aggregated results logger.info("Testing of aggregated results ego.etrago. ") - logger.info("storage_investment_costs: {} ".format( - ego.etrago.storage_investment_costs)) - logger.info("storage_charges: {} ".format( - ego.etrago.storage_charges)) + logger.info( + "storage_investment_costs: {} ".format(ego.etrago.storage_investment_costs) + ) + logger.info("storage_charges: {} ".format(ego.etrago.storage_charges)) ego.etrago.operating_costs ego.etrago.generator @@ -50,39 +55,39 @@ def ego_testing(ego): logger.info("eTraGo failed testing") # eDisGo try: - logger.info("ego.edisgo: {} ".format( - ego.edisgo)) + logger.info("ego.edisgo: {} ".format(ego.edisgo)) except: logger.info("ego.ego.edisgo failed testing") try: - logger.info("ego.edisgo.network: {} ".format( - ego.edisgo.network)) + logger.info("ego.edisgo.network: {} ".format(ego.edisgo.network)) except: logger.info("ego.edisgo.network failed testing") try: - logger.info("ego.edisgo.grid_investment_costs: {} ".format( - ego.edisgo.grid_investment_costs)) + logger.info( + "ego.edisgo.grid_investment_costs: {} ".format( + ego.edisgo.grid_investment_costs + ) + ) except: logger.info("ego.edisgo.grid_investment_costs failed testing") try: - logger.info("ego.edisgo.grid_choice: {} ".format( - ego.edisgo.grid_choice)) + logger.info("ego.edisgo.grid_choice: {} ".format(ego.edisgo.grid_choice)) except: logger.info("ego.edisgo.grid_choice failed testing") try: - logger.info("ego.edisgo.successfull_grids: {} ".format( - ego.edisgo.successfull_grids)) + logger.info( + "ego.edisgo.successfull_grids: {} ".format(ego.edisgo.successfull_grids) + ) except: logger.info("ego.edisgo.successfull_grids failed testing") # eGo - logger.info("ego.total_investment_costs: {} ".format( - ego.total_investment_costs)) - logger.info("ego.total_operation_costs: {} ".format( - ego.total_operation_costs)) + logger.info("ego.total_investment_costs: {} ".format(ego.total_investment_costs)) + logger.info("ego.total_operation_costs: {} ".format(ego.total_operation_costs)) # ego plot functions try: ego.plot_total_investment_costs( - filename="results/plot_total_investment_costs.pdf") + filename="results/plot_total_investment_costs.pdf" + ) except: logger.info("ego.plot_total_investment_costs failed testing") try: @@ -102,39 +107,43 @@ def ego_testing(ego): except: logger.info(" plot_edisgo_cluster failed testing") try: - ego.plot_line_expansion(column='investment_costs', - filename="results/investment_costs.pdf") + ego.plot_line_expansion( + column="investment_costs", filename="results/investment_costs.pdf" + ) except: logger.info(" plot_line_expansion failed testing") try: - ego.plot_line_expansion(column='overnight_costs', - filename="results/overnight_costs.pdf") + ego.plot_line_expansion( + column="overnight_costs", filename="results/overnight_costs.pdf" + ) except: logger.info(" plot_line_expansion failed testing") try: - ego.plot_line_expansion(column='s_nom_expansion', - filename="results/s_nom_expansion.pdf") + ego.plot_line_expansion( + column="s_nom_expansion", filename="results/s_nom_expansion.pdf" + ) except: logger.info(" plot_line_expansion failed testing") try: - ego.plot_storage_expansion(column='overnight_costs', - filename="results/storage_capital_investment.pdf") + ego.plot_storage_expansion( + column="overnight_costs", filename="results/storage_capital_investment.pdf" + ) except: logger.info(" plot_storage_expansion failed testing") def main(): - logger.info('Start calculation') + logger.info("Start calculation") graphviz = GraphvizOutput() date = str(datetime.now().strftime("%Y-%m-%d-%H-%M-%S")) - graphviz.output_file = 'results/'+str(date)+'_basic_process_plot.png' + graphviz.output_file = "results/" + str(date) + "_basic_process_plot.png" logger.info("Time: {} ".format(date)) with PyCallGraph(output=graphviz, config=Config(groups=True)): - ego = eGo(jsonpath='scenario_setting_local.json') - logger.info('Start testing') + ego = eGo(jsonpath="scenario_setting_local.json") + logger.info("Start testing") ego_testing(ego) # object size @@ -143,5 +152,5 @@ def main(): logger.info("Time: {} ".format(str(datetime.now()))) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/ego/tools/economics.py b/ego/tools/economics.py index b430b386..a04e57d9 100644 --- a/ego/tools/economics.py +++ b/ego/tools/economics.py @@ -22,26 +22,31 @@ """ import io -import pkgutil -import os import logging -logger = logging.getLogger('ego') +import os +import pkgutil -if not 'READTHEDOCS' in os.environ: - import pandas as pd +logger = logging.getLogger("ego") + +if not "READTHEDOCS" in os.environ: import numpy as np - from ego.tools.utilities import get_time_steps + import pandas as pd + from etrago.tools.utilities import geolocation_buses -__copyright__ = "Flensburg University of Applied Sciences, Europa-Universität"\ + from ego.tools.utilities import get_time_steps + +__copyright__ = ( + "Flensburg University of Applied Sciences, Europa-Universität" "Flensburg, Centre for Sustainable Energy Systems" +) __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolfbunke" # calculate annuity per time step or periode def annuity_per_period(capex, n, wacc, t, p): - """ Calculate per given period + """Calculate per given period Parameters ---------- @@ -63,7 +68,7 @@ def annuity_per_period(capex, n, wacc, t, p): def edisgo_convert_capital_costs(overnight_cost, t, p, json_file): - """ Get scenario and calculation specific annuity cost by given capital + """Get scenario and calculation specific annuity cost by given capital costs and lifetime. @@ -97,21 +102,20 @@ def edisgo_convert_capital_costs(overnight_cost, t, p, json_file): # https://github.com/openego/eTraGo/blob/dev/etrago/tools/utilities.py#L651 # Calculate present value of an annuity (PVA) - PVA = (1 / p) - (1 / (p*(1 + p) ** t)) + PVA = (1 / p) - (1 / (p * (1 + p) ** t)) year = 8760 # get period of calculation - period = (json_file['eTraGo']['end_snapshot'] - - json_file['eTraGo']['start_snapshot']) + period = json_file["eTraGo"]["end_snapshot"] - json_file["eTraGo"]["start_snapshot"] # calculation of capital_cost - annuity_cost = (overnight_cost / (PVA * (year/(period+1)))) + annuity_cost = overnight_cost / (PVA * (year / (period + 1))) return annuity_cost def etrago_convert_overnight_cost(annuity_cost, json_file, t=40, p=0.05): - """ Get annuity cost of simulation and calculation total + """Get annuity cost of simulation and calculation total ``overnight_costs`` by given capital costs and lifetime. Parameters @@ -146,21 +150,20 @@ def etrago_convert_overnight_cost(annuity_cost, json_file, t=40, p=0.05): # https://github.com/openego/eTraGo/blob/dev/etrago/tools/utilities.py#L651 # Calculate present value of an annuity (PVA) - PVA = (1 / p) - (1 / (p*(1 + p) ** t)) + PVA = (1 / p) - (1 / (p * (1 + p) ** t)) year = 8760 # get period of calculation - period = (json_file['eTraGo']['end_snapshot'] - - json_file['eTraGo']['start_snapshot']) + period = json_file["eTraGo"]["end_snapshot"] - json_file["eTraGo"]["start_snapshot"] # calculation of overnight_cost - overnight_cost = annuity_cost*(PVA * (year/(period+1))) + overnight_cost = annuity_cost * (PVA * (year / (period + 1))) return overnight_cost def etrago_operating_costs(network): - """ Function to get all operating costs of eTraGo. + """Function to get all operating costs of eTraGo. Parameters ---------- @@ -197,77 +200,88 @@ def etrago_operating_costs(network): etg = network # get v_nom - _bus = pd.DataFrame(etg.buses['v_nom']) + _bus = pd.DataFrame(etg.buses["v_nom"]) _bus.index.name = "name" _bus.reset_index(level=0, inplace=True) # Add voltage level idx = etg.generators.index - etg.generators = pd.merge(etg.generators, _bus, - left_on='bus', right_on='name') + etg.generators = pd.merge(etg.generators, _bus, left_on="bus", right_on="name") etg.generators.index = idx - etg.generators['voltage_level'] = 'unknown' + etg.generators["voltage_level"] = "unknown" # add ehv - ix_ehv = etg.generators[etg.generators['v_nom'] >= 380].index - etg.generators.set_value(ix_ehv, 'voltage_level', 'ehv') + ix_ehv = etg.generators[etg.generators["v_nom"] >= 380].index + etg.generators.set_value(ix_ehv, "voltage_level", "ehv") # add hv - ix_hv = etg.generators[(etg.generators['v_nom'] <= 220) & - (etg.generators['v_nom'] >= 110)].index - etg.generators.set_value(ix_hv, 'voltage_level', 'hv') + ix_hv = etg.generators[ + (etg.generators["v_nom"] <= 220) & (etg.generators["v_nom"] >= 110) + ].index + etg.generators.set_value(ix_hv, "voltage_level", "hv") # get voltage_level index - ix_by_ehv = etg.generators[etg.generators.voltage_level == 'ehv'].index - ix_by_hv = etg.generators[etg.generators.voltage_level == 'hv'].index - ix_slack = etg.generators[etg.generators.control != 'Slack'].index + ix_by_ehv = etg.generators[etg.generators.voltage_level == "ehv"].index + ix_by_hv = etg.generators[etg.generators.voltage_level == "hv"].index + ix_slack = etg.generators[etg.generators.control != "Slack"].index - ix_by_ehv = ix_slack.join(ix_by_ehv, how='left', level=None, - return_indexers=False, sort=False) - ix_by_hv = ix_slack.join(ix_by_hv, how='right', level=None, - return_indexers=False, sort=False) + ix_by_ehv = ix_slack.join( + ix_by_ehv, how="left", level=None, return_indexers=False, sort=False + ) + ix_by_hv = ix_slack.join( + ix_by_hv, how="right", level=None, return_indexers=False, sort=False + ) # groupby v_nom ehv - operating_costs_ehv = (etg.generators_t.p[ix_by_ehv] * - etg.generators. marginal_cost[ix_by_ehv]) - operating_costs_ehv = operating_costs_ehv.groupby( - etg.generators.carrier, axis=1).sum().sum() + operating_costs_ehv = ( + etg.generators_t.p[ix_by_ehv] * etg.generators.marginal_cost[ix_by_ehv] + ) + operating_costs_ehv = ( + operating_costs_ehv.groupby(etg.generators.carrier, axis=1).sum().sum() + ) operating_costs = pd.DataFrame(operating_costs_ehv) - operating_costs.columns = ['operation_costs'] - operating_costs['voltage_level'] = 'ehv' + operating_costs.columns = ["operation_costs"] + operating_costs["voltage_level"] = "ehv" # groupby v_nom ehv - operating_costs_hv = (etg.generators_t.p[ix_by_hv] * - etg.generators. marginal_cost[ix_by_hv]) - operating_costs_hv = operating_costs_hv.groupby( - etg.generators.carrier, axis=1).sum().sum() + operating_costs_hv = ( + etg.generators_t.p[ix_by_hv] * etg.generators.marginal_cost[ix_by_hv] + ) + operating_costs_hv = ( + operating_costs_hv.groupby(etg.generators.carrier, axis=1).sum().sum() + ) opt_costs_hv = pd.DataFrame(operating_costs_hv) - opt_costs_hv.columns = ['operation_costs'] - opt_costs_hv['voltage_level'] = 'hv' + opt_costs_hv.columns = ["operation_costs"] + opt_costs_hv["voltage_level"] = "hv" # add df operating_costs = operating_costs.append(opt_costs_hv) - tpc_ehv = pd.DataFrame(operating_costs_ehv.sum(), - columns=['operation_costs'], - index=['total_power_costs']) - tpc_ehv['voltage_level'] = 'ehv' + tpc_ehv = pd.DataFrame( + operating_costs_ehv.sum(), + columns=["operation_costs"], + index=["total_power_costs"], + ) + tpc_ehv["voltage_level"] = "ehv" operating_costs = operating_costs.append(tpc_ehv) - tpc_hv = pd.DataFrame(operating_costs_hv.sum(), - columns=['operation_costs'], - index=['total_power_costs']) - tpc_hv['voltage_level'] = 'hv' + tpc_hv = pd.DataFrame( + operating_costs_hv.sum(), + columns=["operation_costs"], + index=["total_power_costs"], + ) + tpc_hv["voltage_level"] = "hv" operating_costs = operating_costs.append(tpc_hv) # add Grid and Transform Costs try: - etg.lines['voltage_level'] = 'unknown' - ix_ehv = etg.lines[etg.lines['v_nom'] >= 380].index - etg.lines.set_value(ix_ehv, 'voltage_level', 'ehv') - ix_hv = etg.lines[(etg.lines['v_nom'] <= 220) & - (etg.lines['v_nom'] >= 110)].index - etg.lines.set_value(ix_hv, 'voltage_level', 'hv') + etg.lines["voltage_level"] = "unknown" + ix_ehv = etg.lines[etg.lines["v_nom"] >= 380].index + etg.lines.set_value(ix_ehv, "voltage_level", "ehv") + ix_hv = etg.lines[ + (etg.lines["v_nom"] <= 220) & (etg.lines["v_nom"] >= 110) + ].index + etg.lines.set_value(ix_hv, "voltage_level", "hv") losses_total = sum(etg.lines.losses) + sum(etg.transformers.losses) losses_costs = losses_total * np.average(etg.buses_t.marginal_price) @@ -277,24 +291,26 @@ def etrago_operating_costs(network): # axis=0).sum().reset_index() except AttributeError: - logger.info("No Transform and Line losses are calcualted! \n" - "Use eTraGo pf_post_lopf method") + logger.info( + "No Transform and Line losses are calcualted! \n" + "Use eTraGo pf_post_lopf method" + ) losses_total = 0 losses_costs = 0 # total grid losses costs - tgc = pd.DataFrame(losses_costs, - columns=['operation_costs'], - index=['total_grid_losses']) - tgc['voltage_level'] = 'ehv/hv' + tgc = pd.DataFrame( + losses_costs, columns=["operation_costs"], index=["total_grid_losses"] + ) + tgc["voltage_level"] = "ehv/hv" operating_costs = operating_costs.append(tgc) - #power_price = power_price.T.iloc[0] + # power_price = power_price.T.iloc[0] return operating_costs def etrago_grid_investment(network, json_file, session): - """ Function to get grid expantion costs from eTraGo + """Function to get grid expantion costs from eTraGo Parameters ---------- @@ -329,53 +345,60 @@ def etrago_grid_investment(network, json_file, session): """ # check settings for extendable - if 'network' not in json_file['eTraGo']['extendable']: - logger.info("The optimizition was not using parameter" - " 'extendable': network \n" - "No grid expantion costs from etrago") + if "network" not in json_file["eTraGo"]["extendable"]: + logger.info( + "The optimizition was not using parameter" + " 'extendable': network \n" + "No grid expantion costs from etrago" + ) - if 'network' in json_file['eTraGo']['extendable']: + if "network" in json_file["eTraGo"]["extendable"]: network = geolocation_buses(network, session) # differentiation by country_code - network.lines['differentiation'] = 'none' + network.lines["differentiation"] = "none" - network.lines['bus0_c'] = network.lines.bus0.map( - network.buses.country_code) - network.lines['bus1_c'] = network.lines.bus1.map( - network.buses.country_code) + network.lines["bus0_c"] = network.lines.bus0.map(network.buses.country_code) + network.lines["bus1_c"] = network.lines.bus1.map(network.buses.country_code) for idx, val in network.lines.iterrows(): - check = val['bus0_c'] + val['bus1_c'] + check = val["bus0_c"] + val["bus1_c"] if "DE" in check: - network.lines['differentiation'][idx] = 'cross-border' + network.lines["differentiation"][idx] = "cross-border" if "DEDE" in check: - network.lines['differentiation'][idx] = 'domestic' + network.lines["differentiation"][idx] = "domestic" if "DE" not in check: - network.lines['differentiation'][idx] = 'foreign' - - lines = network.lines[['v_nom', 'capital_cost', 's_nom', - 's_nom_min', 's_nom_opt', 'differentiation'] - ].reset_index() - - lines['s_nom_expansion'] = lines.s_nom_opt.subtract( - lines.s_nom, axis='index') - lines['capital_cost'] = lines.s_nom_expansion.multiply( - lines.capital_cost, axis='index') - lines['number_of_expansion'] = lines.s_nom_expansion > 0.0 - lines['time_step'] = get_time_steps(json_file) + network.lines["differentiation"][idx] = "foreign" + + lines = network.lines[ + [ + "v_nom", + "capital_cost", + "s_nom", + "s_nom_min", + "s_nom_opt", + "differentiation", + ] + ].reset_index() + + lines["s_nom_expansion"] = lines.s_nom_opt.subtract(lines.s_nom, axis="index") + lines["capital_cost"] = lines.s_nom_expansion.multiply( + lines.capital_cost, axis="index" + ) + lines["number_of_expansion"] = lines.s_nom_expansion > 0.0 + lines["time_step"] = get_time_steps(json_file) # add v_level - lines['voltage_level'] = 'unknown' + lines["voltage_level"] = "unknown" - ix_ehv = lines[lines['v_nom'] >= 380].index - lines.set_value(ix_ehv, 'voltage_level', 'ehv') + ix_ehv = lines[lines["v_nom"] >= 380].index + lines.set_value(ix_ehv, "voltage_level", "ehv") - ix_hv = lines[(lines['v_nom'] <= 220) & (lines['v_nom'] >= 110)].index - lines.set_value(ix_hv, 'voltage_level', 'hv') + ix_hv = lines[(lines["v_nom"] <= 220) & (lines["v_nom"] >= 110)].index + lines.set_value(ix_hv, "voltage_level", "hv") # based on eTraGo Function: # https://github.com/openego/eTraGo/blob/dev/etrago/tools/utilities.py#L651 @@ -383,48 +406,58 @@ def etrago_grid_investment(network, json_file, session): trafo = pd.DataFrame() # get costs of transfomers - if json_file['eTraGo']['network_clustering_kmeans'] == False: + if json_file["eTraGo"]["network_clustering_kmeans"] == False: - network.transformers['differentiation'] = 'none' + network.transformers["differentiation"] = "none" - trafos = network.transformers[['v_nom0', 'v_nom1', 'capital_cost', - 's_nom_extendable', 's_nom', - 's_nom_opt']] + trafos = network.transformers[ + [ + "v_nom0", + "v_nom1", + "capital_cost", + "s_nom_extendable", + "s_nom", + "s_nom_opt", + ] + ] trafos.columns.name = "" trafos.index.name = "" trafos.reset_index() - trafos['s_nom_extendable'] = trafos.s_nom_opt.subtract( - trafos.s_nom, axis='index') + trafos["s_nom_extendable"] = trafos.s_nom_opt.subtract( + trafos.s_nom, axis="index" + ) - trafos['capital_cost'] = trafos.s_nom_extendable.multiply( - trafos.capital_cost, axis='index') - trafos['number_of_expansion'] = trafos.s_nom_extendable > 0.0 - trafos['time_step'] = get_time_steps(json_file) + trafos["capital_cost"] = trafos.s_nom_extendable.multiply( + trafos.capital_cost, axis="index" + ) + trafos["number_of_expansion"] = trafos.s_nom_extendable > 0.0 + trafos["time_step"] = get_time_steps(json_file) # add v_level - trafos['voltage_level'] = 'unknown' + trafos["voltage_level"] = "unknown" # TODO check - ix_ehv = trafos[trafos['v_nom0'] >= 380].index - trafos.set_value(ix_ehv, 'voltage_level', 'ehv') + ix_ehv = trafos[trafos["v_nom0"] >= 380].index + trafos.set_value(ix_ehv, "voltage_level", "ehv") - ix_hv = trafos[(trafos['v_nom0'] <= 220) & - (trafos['v_nom0'] >= 110)].index - trafos.set_value(ix_hv, 'voltage_level', 'hv') + ix_hv = trafos[(trafos["v_nom0"] <= 220) & (trafos["v_nom0"] >= 110)].index + trafos.set_value(ix_hv, "voltage_level", "hv") # aggregate trafo - trafo = trafos[['voltage_level', - 'capital_cost', - 'differentiation']].groupby(['differentiation', - 'voltage_level'] - ).sum().reset_index() + trafo = ( + trafos[["voltage_level", "capital_cost", "differentiation"]] + .groupby(["differentiation", "voltage_level"]) + .sum() + .reset_index() + ) # aggregate lines - line = lines[['voltage_level', - 'capital_cost', - 'differentiation']].groupby(['differentiation', - 'voltage_level'] - ).sum().reset_index() + line = ( + lines[["voltage_level", "capital_cost", "differentiation"]] + .groupby(["differentiation", "voltage_level"]) + .sum() + .reset_index() + ) # merge trafos and line frames = [line, trafo] @@ -455,117 +488,113 @@ def edisgo_grid_investment(edisgo, json_file): t = 40 p = 0.05 - logger.info('For all components T={} and p={} is used'.format(t, p)) + logger.info("For all components T={} and p={} is used".format(t, p)) - costs = pd.DataFrame( - columns=['voltage_level', 'annuity_costs', 'overnight_costs']) + costs = pd.DataFrame(columns=["voltage_level", "annuity_costs", "overnight_costs"]) # Loop through all calculated eDisGo grids for key, value in edisgo.network.items(): - if not hasattr(value, 'network'): - logger.warning('No results available for grid {}'.format(key)) + if not hasattr(value, "network"): + logger.warning("No results available for grid {}".format(key)) continue # eDisGo results (overnight costs) for this grid costs_single = value.network.results.grid_expansion_costs - costs_single.rename( - columns={'total_costs': 'overnight_costs'}, - inplace=True) + costs_single.rename(columns={"total_costs": "overnight_costs"}, inplace=True) # continue if this grid was not reinforced - if (costs_single['overnight_costs'].sum() == 0.): - logger.info('No expansion costs for grid {}'.format(key)) + if costs_single["overnight_costs"].sum() == 0.0: + logger.info("No expansion costs for grid {}".format(key)) continue # Overnight cost translated in annuity costs - costs_single['capital_cost'] = edisgo_convert_capital_costs( - costs_single['overnight_costs'], - t=t, - p=p, - json_file=json_file) + costs_single["capital_cost"] = edisgo_convert_capital_costs( + costs_single["overnight_costs"], t=t, p=p, json_file=json_file + ) # Weighting (retrieves the singe (absolute) weighting for this grid) choice = edisgo.grid_choice - weighting = choice.loc[ - choice['the_selected_network_id'] == key - ][ - 'no_of_points_per_cluster' + weighting = choice.loc[choice["the_selected_network_id"] == key][ + "no_of_points_per_cluster" ].values[0] - costs_single[['capital_cost', 'overnight_costs']] = ( - costs_single[['capital_cost', 'overnight_costs']] - * weighting) + costs_single[["capital_cost", "overnight_costs"]] = ( + costs_single[["capital_cost", "overnight_costs"]] * weighting + ) # Append costs of this grid costs = costs.append( - costs_single[[ - 'voltage_level', - 'capital_cost', - 'overnight_costs']], ignore_index=True) + costs_single[["voltage_level", "capital_cost", "overnight_costs"]], + ignore_index=True, + ) if len(costs) == 0: - logger.info('No expansion costs in any MV grid') + logger.info("No expansion costs in any MV grid") return None else: - aggr_costs = costs.groupby( - ['voltage_level']).sum().reset_index() + aggr_costs = costs.groupby(["voltage_level"]).sum().reset_index() # In eDisGo all costs are in kEuro (eGo only takes Euro) - aggr_costs[['capital_cost', 'overnight_costs']] = ( - aggr_costs[['capital_cost', 'overnight_costs']] - * 1000) + aggr_costs[["capital_cost", "overnight_costs"]] = ( + aggr_costs[["capital_cost", "overnight_costs"]] * 1000 + ) successfull_grids = edisgo.successfull_grids if successfull_grids < 1: logger.warning( - 'Only {} % of the grids were calculated.\n'.format( - "{0:,.2f}".format(successfull_grids * 100) - ) + 'Costs are extrapolated...') + "Only {} % of the grids were calculated.\n".format( + "{:,.2f}".format(successfull_grids * 100) + ) + + "Costs are extrapolated..." + ) - aggr_costs[['capital_cost', 'overnight_costs']] = ( - aggr_costs[['capital_cost', 'overnight_costs']] - / successfull_grids) + aggr_costs[["capital_cost", "overnight_costs"]] = ( + aggr_costs[["capital_cost", "overnight_costs"]] / successfull_grids + ) return aggr_costs def get_generator_investment(network, scn_name): - """ Get investment costs per carrier/ generator. - - """ + """Get investment costs per carrier/ generator.""" etg = network try: - data = pkgutil.get_data('ego', 'data/investment_costs.csv') - invest = pd.read_csv(io.BytesIO(data), - encoding='utf8', sep=",", - index_col="carriers") + data = pkgutil.get_data("ego", "data/investment_costs.csv") + invest = pd.read_csv( + io.BytesIO(data), encoding="utf8", sep=",", index_col="carriers" + ) except FileNotFoundError: path = os.getcwd() - filename = 'investment_costs.csv' - invest = pd.DataFrame.from_csv(path + '/data/'+filename) - - if scn_name in ['SH Status Quo', 'Status Quo']: - invest_scn = 'Status Quo' - - if scn_name in ['SH NEP 2035', 'NEP 2035']: - invest_scn = 'NEP 2035' - - if scn_name in ['SH eGo 100', 'eGo 100']: - invest_scn = 'eGo 100' - - gen_invest = pd.concat([invest[invest_scn], - etg.generators.groupby('carrier')['p_nom'].sum()], - axis=1, join='inner') - - gen_invest = pd.concat([invest[invest_scn], etg.generators.groupby( - 'carrier') - ['p_nom'].sum()], axis=1, join='inner') - gen_invest['carrier_costs'] = gen_invest[invest_scn] * \ - gen_invest['p_nom'] * 1000 # in MW + filename = "investment_costs.csv" + invest = pd.DataFrame.from_csv(path + "/data/" + filename) + + if scn_name in ["SH Status Quo", "Status Quo"]: + invest_scn = "Status Quo" + + if scn_name in ["SH NEP 2035", "NEP 2035"]: + invest_scn = "NEP 2035" + + if scn_name in ["SH eGo 100", "eGo 100"]: + invest_scn = "eGo 100" + + gen_invest = pd.concat( + [invest[invest_scn], etg.generators.groupby("carrier")["p_nom"].sum()], + axis=1, + join="inner", + ) + + gen_invest = pd.concat( + [invest[invest_scn], etg.generators.groupby("carrier")["p_nom"].sum()], + axis=1, + join="inner", + ) + gen_invest["carrier_costs"] = ( + gen_invest[invest_scn] * gen_invest["p_nom"] * 1000 + ) # in MW return gen_invest diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 1fc2fce2..5d9f398b 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -21,51 +21,52 @@ This file is part of the the eGo toolbox. It contains the class definition for multiple eDisGo networks. """ -__copyright__ = ("Flensburg University of Applied Sciences, " - "Europa-Universität Flensburg, " - "Centre for Sustainable Energy Systems") +__copyright__ = ( + "Flensburg University of Applied Sciences, " + "Europa-Universität Flensburg, " + "Centre for Sustainable Energy Systems" +) __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolf_bunke, maltesc" -# Import -from traceback import TracebackException +import json +import logging import os import pickle -import logging -import dill -import pandas as pd + +from datetime import datetime +from datetime import timedelta as td from time import localtime, sleep, strftime -from datetime import datetime, timedelta as td -import json -from sqlalchemy.orm import sessionmaker -from sqlalchemy.orm import scoped_session + +# Import +from traceback import TracebackException + +import dill import multiprocess as mp2 +import pandas as pd -if not 'READTHEDOCS' in os.environ: +from sqlalchemy.orm import scoped_session, sessionmaker - from egoio.db_tables import model_draft, grid - from egoio.tools import db +if not "READTHEDOCS" in os.environ: - from edisgo.edisgo import import_edisgo_from_files, EDisGo + from edisgo.edisgo import EDisGo, import_edisgo_from_files + from edisgo.flex_opt import q_control from edisgo.network.results import Results from edisgo.network.timeseries import TimeSeries from edisgo.tools.plots import mv_grid_topology - from edisgo.flex_opt import q_control + from egoio.db_tables import grid, model_draft + from egoio.tools import db - from ego.tools.interface import ETraGoMinimalData - from ego.tools.interface import get_etrago_results_per_bus - from ego.tools.mv_cluster import ( - analyze_attributes, - cluster_mv_grids) - from ego.tools.economics import ( - edisgo_grid_investment) + from ego.tools.economics import edisgo_grid_investment + from ego.tools.interface import ETraGoMinimalData, get_etrago_results_per_bus + from ego.tools.mv_cluster import analyze_attributes, cluster_mv_grids # Logging logger = logging.getLogger(__name__) pickle.DEFAULT_PROTOCOL = 4 -dill.settings['protocol'] = 4 +dill.settings["protocol"] = 4 class EDisGoNetworks: @@ -100,10 +101,7 @@ def __init__(self, json_file, etrago_network): if self._csv_import: self._load_edisgo_results() self._successful_grids = self._successful_grids() - self._grid_investment_costs = edisgo_grid_investment( - self, - self._json_file - ) + self._grid_investment_costs = edisgo_grid_investment(self, self._json_file) else: # Only clustering results @@ -124,8 +122,7 @@ def __init__(self, json_file, etrago_network): self._successful_grids = self._successful_grids() self._grid_investment_costs = edisgo_grid_investment( - self, - self._json_file + self, self._json_file ) @property @@ -244,14 +241,16 @@ def plot_storage_integration(self, mv_grid_id, **kwargs): mv_grid_topology( self._edisgo_grids[mv_grid_id].network.pypsa, self._edisgo_grids[mv_grid_id].network.config, - node_color=kwargs.get('storage_integration', None), - filename=kwargs.get('filename', None), - grid_district_geom=kwargs.get('grid_district_geom', True), - background_map=kwargs.get('background_map', True), - xlim=kwargs.get('xlim', None), ylim=kwargs.get('ylim', None), - title=kwargs.get('title', '')) - - def plot_grid_expansion_costs(self, mv_grid_id, ** kwargs): + node_color=kwargs.get("storage_integration", None), + filename=kwargs.get("filename", None), + grid_district_geom=kwargs.get("grid_district_geom", True), + background_map=kwargs.get("background_map", True), + xlim=kwargs.get("xlim", None), + ylim=kwargs.get("ylim", None), + title=kwargs.get("title", ""), + ) + + def plot_grid_expansion_costs(self, mv_grid_id, **kwargs): """ Plots costs per MV line. For more information see :func:`edisgo.tools.plots.mv_grid_topology`. @@ -260,18 +259,23 @@ def plot_grid_expansion_costs(self, mv_grid_id, ** kwargs): mv_grid_topology( self._edisgo_grids[mv_grid_id].network.pypsa, self._edisgo_grids[mv_grid_id].network.config, - line_color='expansion_costs', + line_color="expansion_costs", grid_expansion_costs=( - self._edisgo_grids[mv_grid_id].network. - results.grid_expansion_costs.rename(columns={ - "overnight_costs": "total_costs"})), - filename=kwargs.get('filename', None), - grid_district_geom=kwargs.get('grid_district_geom', True), - background_map=kwargs.get('background_map', True), - limits_cb_lines=kwargs.get('limits_cb_lines', None), - xlim=kwargs.get('xlim', None), ylim=kwargs.get('ylim', None), - lines_cmap=kwargs.get('lines_cmap', 'inferno_r'), - title=kwargs.get('title', '')) + self._edisgo_grids[ + mv_grid_id + ].network.results.grid_expansion_costs.rename( + columns={"overnight_costs": "total_costs"} + ) + ), + filename=kwargs.get("filename", None), + grid_district_geom=kwargs.get("grid_district_geom", True), + background_map=kwargs.get("background_map", True), + limits_cb_lines=kwargs.get("limits_cb_lines", None), + xlim=kwargs.get("xlim", None), + ylim=kwargs.get("ylim", None), + lines_cmap=kwargs.get("lines_cmap", "inferno_r"), + title=kwargs.get("title", ""), + ) def plot_line_loading(self, mv_grid_id, **kwargs): """ @@ -283,20 +287,22 @@ def plot_line_loading(self, mv_grid_id, **kwargs): mv_grid_topology( self._edisgo_grids[mv_grid_id].network.pypsa, self._edisgo_grids[mv_grid_id].network.config, - timestep=kwargs.get('timestep', None), - line_color='loading', - node_color=kwargs.get('node_color', None), + timestep=kwargs.get("timestep", None), + line_color="loading", + node_color=kwargs.get("node_color", None), line_load=self._edisgo_grids[mv_grid_id].network.results.s_res(), - filename=kwargs.get('filename', None), - arrows=kwargs.get('arrows', None), - grid_district_geom=kwargs.get('grid_district_geom', True), - background_map=kwargs.get('background_map', True), + filename=kwargs.get("filename", None), + arrows=kwargs.get("arrows", None), + grid_district_geom=kwargs.get("grid_district_geom", True), + background_map=kwargs.get("background_map", True), voltage=None, # change API - limits_cb_lines=kwargs.get('limits_cb_lines', None), - limits_cb_nodes=kwargs.get('limits_cb_nodes', None), - xlim=kwargs.get('xlim', None), ylim=kwargs.get('ylim', None), - lines_cmap=kwargs.get('lines_cmap', 'inferno_r'), - title=kwargs.get('title', '')) + limits_cb_lines=kwargs.get("limits_cb_lines", None), + limits_cb_nodes=kwargs.get("limits_cb_nodes", None), + xlim=kwargs.get("xlim", None), + ylim=kwargs.get("ylim", None), + lines_cmap=kwargs.get("lines_cmap", "inferno_r"), + title=kwargs.get("title", ""), + ) def plot_mv_grid_topology(self, mv_grid_id, **kwargs): """ @@ -304,15 +310,16 @@ def plot_mv_grid_topology(self, mv_grid_id, **kwargs): For more information see :func:`edisgo.tools.plots.mv_grid_topology`. """ - mv_grid_topology(self._edisgo_grids[mv_grid_id].network.pypsa, - self._edisgo_grids[mv_grid_id].network.config, - filename=kwargs.get('filename', None), - grid_district_geom=kwargs.get( - 'grid_district_geom', True), - background_map=kwargs.get('background_map', True), - xlim=kwargs.get('xlim', None), - ylim=kwargs.get('ylim', None), - title=kwargs.get('title', '')) + mv_grid_topology( + self._edisgo_grids[mv_grid_id].network.pypsa, + self._edisgo_grids[mv_grid_id].network.config, + filename=kwargs.get("filename", None), + grid_district_geom=kwargs.get("grid_district_geom", True), + background_map=kwargs.get("background_map", True), + xlim=kwargs.get("xlim", None), + ylim=kwargs.get("ylim", None), + title=kwargs.get("title", ""), + ) def _init_status(self): """ @@ -329,30 +336,31 @@ def _init_status(self): * 'end_time' - end time of calculation """ - self._status_dir = 'status' + self._status_dir = "status" if not os.path.exists(self._status_dir): os.makedirs(self._status_dir) - self._status_file_name = 'eGo_' + strftime("%Y-%m-%d_%H%M%S", localtime()) + self._status_file_name = "eGo_" + strftime("%Y-%m-%d_%H%M%S", localtime()) status = self._grid_choice.copy() - status = status.set_index('the_selected_network_id') - status.index.names = ['MV grid id'] + status = status.set_index("the_selected_network_id") + status.index.names = ["MV grid id"] - status['cluster_perc'] = (status['no_of_points_per_cluster'] / - self._grid_choice['no_of_points_per_cluster'].sum()) + status["cluster_perc"] = ( + status["no_of_points_per_cluster"] + / self._grid_choice["no_of_points_per_cluster"].sum() + ) - status['start_time'] = 'Not started yet' - status['end_time'] = 'Not finished yet' + status["start_time"] = "Not started yet" + status["end_time"] = "Not finished yet" status.drop( - ['no_of_points_per_cluster', 'represented_grids'], - axis=1, - inplace=True) + ["no_of_points_per_cluster", "represented_grids"], axis=1, inplace=True + ) self._status_file_path = os.path.join( - self._status_dir, - self._status_file_name + '.csv') + self._status_dir, self._status_file_name + ".csv" + ) status.to_csv(self._status_file_path) @@ -376,26 +384,22 @@ def _status_update(self, mv_grid_id, time, message=None, show=True): If True, shows a logging message with the status information. Default: True. """ - status = pd.read_csv( - self._status_file_path, - index_col=0) + status = pd.read_csv(self._status_file_path, index_col=0) - status['start_time'] = status['start_time'].astype(str) - status['end_time'] = status['end_time'].astype(str) + status["start_time"] = status["start_time"].astype(str) + status["end_time"] = status["end_time"].astype(str) if message: now = message else: now = strftime("%Y-%m-%d_%H:%M", localtime()) - if time == 'start': - status.at[mv_grid_id, 'start_time'] = now - elif time == 'end': - status.at[mv_grid_id, 'end_time'] = now + if time == "start": + status.at[mv_grid_id, "start_time"] = now + elif time == "end": + status.at[mv_grid_id, "end_time"] = now if show: - logger.info("\n\neDisGo status: \n\n" - + status.to_string() - + "\n\n") + logger.info("\n\neDisGo status: \n\n" + status.to_string() + "\n\n") status.to_csv(self._status_file_path) @@ -412,123 +416,123 @@ def _update_edisgo_configs(self, edisgo_grid): """ # Info and Warning handling - if not hasattr(self, '_suppress_log'): + if not hasattr(self, "_suppress_log"): self._suppress_log = False # Only in the first run warnings and # info get thrown # Database section ego_db = self._db_section - edisgo_db = edisgo_grid.network.config['db_connection']['section'] + edisgo_db = edisgo_grid.network.config["db_connection"]["section"] if not ego_db == edisgo_db: if not self._suppress_log: logger.warning( - ("eDisGo database configuration (db: '{}') " - + "will be overwritten with database configuration " - + "from eGo's scenario settings (db: '{}')").format( - edisgo_db, - ego_db)) - edisgo_grid.network.config['db_connection']['section'] = ego_db + ( + "eDisGo database configuration (db: '{}') " + + "will be overwritten with database configuration " + + "from eGo's scenario settings (db: '{}')" + ).format(edisgo_db, ego_db) + ) + edisgo_grid.network.config["db_connection"]["section"] = ego_db # Versioned ego_gridversion = self._grid_version if ego_gridversion is None: - ego_versioned = 'model_draft' + ego_versioned = "model_draft" if not self._suppress_log: - logger.info("eGo's grid_version == None is " - + "evaluated as data source: model_draft") + logger.info( + "eGo's grid_version == None is " + + "evaluated as data source: model_draft" + ) else: - ego_versioned = 'versioned' + ego_versioned = "versioned" if not self._suppress_log: - logger.info(("eGo's grid_version == '{}' is " - + "evaluated as data source: versioned").format( - ego_gridversion)) + logger.info( + ( + "eGo's grid_version == '{}' is " + + "evaluated as data source: versioned" + ).format(ego_gridversion) + ) - edisgo_versioned = edisgo_grid.network.config[ - 'data_source']['oedb_data_source'] + edisgo_versioned = edisgo_grid.network.config["data_source"]["oedb_data_source"] if not ego_versioned == edisgo_versioned: if not self._suppress_log: logger.warning( - ("eDisGo data source configuration ('{}') " - + "will be overwritten with data source config. from " - + "eGo's scenario settings (data source: '{}')" - ).format( - edisgo_versioned, - ego_versioned)) - edisgo_grid.network.config[ - 'data_source']['oedb_data_source'] = ego_versioned + ( + "eDisGo data source configuration ('{}') " + + "will be overwritten with data source config. from " + + "eGo's scenario settings (data source: '{}')" + ).format(edisgo_versioned, ego_versioned) + ) + edisgo_grid.network.config["data_source"][ + "oedb_data_source" + ] = ego_versioned # Gridversion ego_gridversion = self._grid_version - edisgo_gridversion = edisgo_grid.network.config[ - 'versioned']['version'] + edisgo_gridversion = edisgo_grid.network.config["versioned"]["version"] if not ego_gridversion == edisgo_gridversion: if not self._suppress_log: logger.warning( - ("eDisGo version configuration (version: '{}') " - + "will be overwritten with version configuration " - + "from eGo's scenario settings (version: '{}')" - ).format( - edisgo_gridversion, - ego_gridversion)) - edisgo_grid.network.config[ - 'versioned']['version'] = ego_gridversion + ( + "eDisGo version configuration (version: '{}') " + + "will be overwritten with version configuration " + + "from eGo's scenario settings (version: '{}')" + ).format(edisgo_gridversion, ego_gridversion) + ) + edisgo_grid.network.config["versioned"]["version"] = ego_gridversion self._suppress_log = True def _set_scenario_settings(self): - self._csv_import = self._json_file['eGo']['csv_import_eDisGo'] + self._csv_import = self._json_file["eGo"]["csv_import_eDisGo"] # eTraGo args - self._etrago_args = self._json_file['eTraGo'] - self._scn_name = self._etrago_args['scn_name'] - self._ext_storage = ( - 'storage' in self._etrago_args['extendable'] - ) + self._etrago_args = self._json_file["eTraGo"] + self._scn_name = self._etrago_args["scn_name"] + self._ext_storage = "storage" in self._etrago_args["extendable"] if self._ext_storage: logger.info("eTraGo Dataset used extendable storage") - self._pf_post_lopf = self._etrago_args['pf_post_lopf'] + self._pf_post_lopf = self._etrago_args["pf_post_lopf"] # eDisGo args import if self._csv_import: # raise NotImplementedError - with open(os.path.join( - self._csv_import, - 'edisgo_args.json')) as f: + with open(os.path.join(self._csv_import, "edisgo_args.json")) as f: edisgo_args = json.load(f) - self._json_file['eDisGo'] = edisgo_args - logger.info("All eDisGo settings are taken from CSV folder" - + "(scenario settings are ignored)") + self._json_file["eDisGo"] = edisgo_args + logger.info( + "All eDisGo settings are taken from CSV folder" + + "(scenario settings are ignored)" + ) # This overwrites the original object... # Imported or directly from the Settings # eDisGo section of the settings - self._edisgo_args = self._json_file['eDisGo'] + self._edisgo_args = self._json_file["eDisGo"] # Reading all eDisGo settings # TODO: Integrate into a for-loop - self._db_section = self._edisgo_args['db'] - self._grid_version = self._edisgo_args['gridversion'] - self._timesteps_pfa = self._edisgo_args['timesteps_pfa'] - self._solver = self._edisgo_args['solver'] - self._ding0_path = self._edisgo_args['ding0_path'] - self._choice_mode = self._edisgo_args['choice_mode'] - self._parallelization = self._edisgo_args['parallelization'] - self._initial_reinforcement = self._edisgo_args[ - 'initial_reinforcement'] - self._cluster_attributes = self._edisgo_args['cluster_attributes'] - self._only_cluster = self._edisgo_args['only_cluster'] - self._max_workers = self._edisgo_args['max_workers'] - self._max_cos_phi_renewable = self._edisgo_args[ - 'max_cos_phi_renewable'] - self._results = self._edisgo_args['results'] - self._max_calc_time = self._edisgo_args['max_calc_time'] + self._db_section = self._edisgo_args["db"] + self._grid_version = self._edisgo_args["gridversion"] + self._timesteps_pfa = self._edisgo_args["timesteps_pfa"] + self._solver = self._edisgo_args["solver"] + self._ding0_path = self._edisgo_args["ding0_path"] + self._choice_mode = self._edisgo_args["choice_mode"] + self._parallelization = self._edisgo_args["parallelization"] + self._initial_reinforcement = self._edisgo_args["initial_reinforcement"] + self._cluster_attributes = self._edisgo_args["cluster_attributes"] + self._only_cluster = self._edisgo_args["only_cluster"] + self._max_workers = self._edisgo_args["max_workers"] + self._max_cos_phi_renewable = self._edisgo_args["max_cos_phi_renewable"] + self._results = self._edisgo_args["results"] + self._max_calc_time = self._edisgo_args["max_calc_time"] # Some basic checks if not self._initial_reinforcement: @@ -536,8 +540,7 @@ def _set_scenario_settings(self): "Skipping the initial reinforcement is not yet implemented" ) if self._only_cluster: - logger.warning( - "\n\nThis eDisGo run only returns cluster results\n\n") + logger.warning("\n\nThis eDisGo run only returns cluster results\n\n") # Versioning if self._grid_version is not None: @@ -555,15 +558,15 @@ def _successful_grids(self): for key, value in self._edisgo_grids.items(): weight = self._grid_choice.loc[ - self._grid_choice['the_selected_network_id'] == key - ]['no_of_points_per_cluster'].values[0] + self._grid_choice["the_selected_network_id"] == key + ]["no_of_points_per_cluster"].values[0] total += weight - if hasattr(value, 'network'): + if hasattr(value, "network"): success += weight else: fail += weight - return success/total + return success / total def _analyze_cluster_attributes(self): """ @@ -574,9 +577,7 @@ def _analyze_cluster_attributes(self): """ analyze_attributes(self._ding0_path) - def _cluster_mv_grids( - self, - no_grids): + def _cluster_mv_grids(self, no_grids): """ Clusters the MV grids based on the attributes, for a given number of MV grids @@ -596,62 +597,56 @@ def _cluster_mv_grids( # TODO: This first dataframe contains the standard attributes... # ...Create an Interface in order to use attributes more flexibly. # Make this function more generic. - attributes_path = self._ding0_path + '/attributes.csv' + attributes_path = self._ding0_path + "/attributes.csv" if not os.path.isfile(attributes_path): - logger.info('Attributes file is missing') - logger.info('Attributes will be calculated') + logger.info("Attributes file is missing") + logger.info("Attributes will be calculated") self._analyze_cluster_attributes() - df = pd.read_csv(self._ding0_path + '/attributes.csv') - df = df.set_index('id') - df.drop(['Unnamed: 0'], inplace=True, axis=1) + df = pd.read_csv(self._ding0_path + "/attributes.csv") + df = df.set_index("id") + df.drop(["Unnamed: 0"], inplace=True, axis=1) df.rename( columns={ "Solar_cumulative_capacity": "solar_cap", "Wind_cumulative_capacity": "wind_cap", - "The_Farthest_node": "farthest_node"}, - inplace=True) + "The_Farthest_node": "farthest_node", + }, + inplace=True, + ) - if 'extended_storage' in self._cluster_attributes: + if "extended_storage" in self._cluster_attributes: if self._ext_storage: storages = self._identify_extended_storages() - if not (storages.max().values[0] == 0.): + if not (storages.max().values[0] == 0.0): df = pd.concat([df, storages], axis=1) df.rename( - columns={"storage_p_nom": "extended_storage"}, - inplace=True) + columns={"storage_p_nom": "extended_storage"}, inplace=True + ) else: - logger.warning('Extended storages all 0. \ + logger.warning( + "Extended storages all 0. \ Therefore, extended storages \ - are excluded from clustering') + are excluded from clustering" + ) - found_atts = [ - i for i in self._cluster_attributes if i in df.columns - ] - missing_atts = [ - i for i in self._cluster_attributes if i not in df.columns - ] + found_atts = [i for i in self._cluster_attributes if i in df.columns] + missing_atts = [i for i in self._cluster_attributes if i not in df.columns] - logger.info( - 'Available attributes are: {}'.format(df.columns.tolist()) - ) - logger.info( - 'Chosen/found attributes are: {}'.format(found_atts) - ) + logger.info("Available attributes are: {}".format(df.columns.tolist())) + logger.info("Chosen/found attributes are: {}".format(found_atts)) if len(missing_atts) > 0: - logger.warning( - 'Missing attributes: {}'.format(missing_atts) - ) - if 'extended_storage' in missing_atts: - logger.info('Hint: eTraGo dataset must contain ' - 'extendable storage in order to include ' - 'storage extension in MV grid clustering.') + logger.warning("Missing attributes: {}".format(missing_atts)) + if "extended_storage" in missing_atts: + logger.info( + "Hint: eTraGo dataset must contain " + "extendable storage in order to include " + "storage extension in MV grid clustering." + ) - return cluster_mv_grids( - no_grids, - cluster_base=df) + return cluster_mv_grids(no_grids, cluster_base=df) def _identify_extended_storages(self): @@ -662,34 +657,28 @@ def _identify_extended_storages(self): all_mv_grids = self._check_available_mv_grids() - storages = pd.DataFrame( - index=all_mv_grids, - columns=['storage_p_nom']) + storages = pd.DataFrame(index=all_mv_grids, columns=["storage_p_nom"]) - logger.info('Identifying extended storage') + logger.info("Identifying extended storage") for mv_grid in all_mv_grids: bus_id = self._get_bus_id_from_mv_grid(session, mv_grid) min_extended = 0.3 stor_p_nom = self._etrago_network.storage_units.loc[ - (self._etrago_network.storage_units['bus'] == str(bus_id)) - & (self._etrago_network.storage_units[ - 'p_nom_extendable' - ] == True) - & (self._etrago_network.storage_units[ - 'p_nom_opt' - ] > min_extended) - & (self._etrago_network.storage_units['max_hours'] <= 20.) - ]['p_nom_opt'] + (self._etrago_network.storage_units["bus"] == str(bus_id)) + & (self._etrago_network.storage_units["p_nom_extendable"] == True) + & (self._etrago_network.storage_units["p_nom_opt"] > min_extended) + & (self._etrago_network.storage_units["max_hours"] <= 20.0) + ]["p_nom_opt"] if len(stor_p_nom) == 1: stor_p_nom = stor_p_nom.values[0] elif len(stor_p_nom) == 0: - stor_p_nom = 0. + stor_p_nom = 0.0 else: raise IndexError - storages.at[mv_grid, 'storage_p_nom'] = stor_p_nom + storages.at[mv_grid, "storage_p_nom"] = stor_p_nom Session.remove() @@ -707,11 +696,10 @@ def _check_available_mv_grids(self): """ mv_grids = [] for file in os.listdir(self._ding0_path): - if file.endswith('.pkl'): + if file.endswith(".pkl"): mv_grids.append( - int(file.replace( - 'ding0_grids__', '' - ).replace('.pkl', ''))) + int(file.replace("ding0_grids__", "").replace(".pkl", "")) + ) return mv_grids @@ -723,57 +711,47 @@ def _set_grid_choice(self): choice_df = pd.DataFrame( columns=[ - 'no_of_points_per_cluster', - 'the_selected_network_id', - 'represented_grids']) + "no_of_points_per_cluster", + "the_selected_network_id", + "represented_grids", + ] + ) - if self._choice_mode == 'cluster': - no_grids = self._edisgo_args['no_grids'] - logger.info('Clustering to {} MV grids'.format(no_grids)) + if self._choice_mode == "cluster": + no_grids = self._edisgo_args["no_grids"] + logger.info("Clustering to {} MV grids".format(no_grids)) cluster_df = self._cluster_mv_grids(no_grids) - choice_df[ - 'the_selected_network_id' - ] = cluster_df['the_selected_network_id'] - choice_df[ - 'no_of_points_per_cluster' - ] = cluster_df['no_of_points_per_cluster'] - choice_df[ - 'represented_grids' - ] = cluster_df['represented_grids'] - - elif self._choice_mode == 'manual': - man_grids = self._edisgo_args['manual_grids'] - - choice_df['the_selected_network_id'] = man_grids - choice_df['no_of_points_per_cluster'] = 1 - choice_df['represented_grids'] = [ - [mv_grid_id] - for mv_grid_id - in choice_df['the_selected_network_id']] + choice_df["the_selected_network_id"] = cluster_df["the_selected_network_id"] + choice_df["no_of_points_per_cluster"] = cluster_df[ + "no_of_points_per_cluster" + ] + choice_df["represented_grids"] = cluster_df["represented_grids"] - logger.info( - 'Calculating manually chosen MV grids {}'.format(man_grids) - ) + elif self._choice_mode == "manual": + man_grids = self._edisgo_args["manual_grids"] + + choice_df["the_selected_network_id"] = man_grids + choice_df["no_of_points_per_cluster"] = 1 + choice_df["represented_grids"] = [ + [mv_grid_id] for mv_grid_id in choice_df["the_selected_network_id"] + ] - elif self._choice_mode == 'all': + logger.info("Calculating manually chosen MV grids {}".format(man_grids)) + + elif self._choice_mode == "all": mv_grids = self._check_available_mv_grids() - choice_df['the_selected_network_id'] = mv_grids - choice_df['no_of_points_per_cluster'] = 1 - choice_df['represented_grids'] = [ - [mv_grid_id] - for mv_grid_id - in choice_df['the_selected_network_id']] + choice_df["the_selected_network_id"] = mv_grids + choice_df["no_of_points_per_cluster"] = 1 + choice_df["represented_grids"] = [ + [mv_grid_id] for mv_grid_id in choice_df["the_selected_network_id"] + ] no_grids = len(mv_grids) - logger.info( - 'Calculating all available {} MV grids'.format(no_grids) - ) + logger.info("Calculating all available {} MV grids".format(no_grids)) - choice_df = choice_df.sort_values( - 'no_of_points_per_cluster', - ascending=False) + choice_df = choice_df.sort_values("no_of_points_per_cluster", ascending=False) self._grid_choice = choice_df @@ -788,15 +766,14 @@ def _run_edisgo_pool(self): os.makedirs(self._results) if parallelization is True: - logger.info('Run eDisGo parallel') - mv_grids = self._grid_choice['the_selected_network_id'].tolist() + logger.info("Run eDisGo parallel") + mv_grids = self._grid_choice["the_selected_network_id"].tolist() no_cpu = mp2.cpu_count() if no_cpu > self._max_workers: no_cpu = self._max_workers logger.info( - 'Number of workers limited to {} by user'.format( - self._max_workers - )) + "Number of workers limited to {} by user".format(self._max_workers) + ) self._edisgo_grids = set(mv_grids) self._edisgo_grids = parallelizer( @@ -804,46 +781,37 @@ def _run_edisgo_pool(self): lambda *xs: xs[1]._run_edisgo(xs[0]), (self,), self._max_calc_time, - workers=no_cpu) + workers=no_cpu, + ) for g in mv_grids: if not g in self._edisgo_grids: - self._edisgo_grids[g] = 'Timeout' + self._edisgo_grids[g] = "Timeout" else: - logger.info('Run eDisGo sequencial') + logger.info("Run eDisGo sequencial") no_grids = len(self._grid_choice) count = 0 for idx, row in self._grid_choice.iterrows(): - prog = '%.1f' % (count / no_grids * 100) - logger.info( - '{} % Calculated by eDisGo'.format(prog) - ) + prog = "%.1f" % (count / no_grids * 100) + logger.info("{} % Calculated by eDisGo".format(prog)) - mv_grid_id = int(row['the_selected_network_id']) - logger.info( - 'MV grid {}'.format(mv_grid_id) - ) + mv_grid_id = int(row["the_selected_network_id"]) + logger.info("MV grid {}".format(mv_grid_id)) try: edisgo_grid = self._run_edisgo(mv_grid_id) - self._edisgo_grids[ - mv_grid_id - ] = edisgo_grid + self._edisgo_grids[mv_grid_id] = edisgo_grid except Exception as e: self._edisgo_grids[mv_grid_id] = e - logger.exception( - 'MV grid {} failed: \n'.format(mv_grid_id) - ) + logger.exception("MV grid {} failed: \n".format(mv_grid_id)) count += 1 - self._csv_import = self._json_file['eDisGo']['results'] + self._csv_import = self._json_file["eDisGo"]["results"] self._save_edisgo_results() self._load_edisgo_results() self._run_finished = True - def _run_edisgo( - self, - mv_grid_id): + def _run_edisgo(self, mv_grid_id): """ Performs a single eDisGo run @@ -858,10 +826,9 @@ def _run_edisgo( Returns the complete eDisGo container, also including results """ - self._status_update(mv_grid_id, 'start', show=False) + self._status_update(mv_grid_id, "start", show=False) - logger.info( - 'MV grid {}: Calculating interface values'.format(mv_grid_id)) + logger.info("MV grid {}: Calculating interface values".format(mv_grid_id)) conn = db.connection(readonly=True) session_factory = sessionmaker(bind=conn) @@ -879,90 +846,88 @@ def _run_edisgo( self._scn_name, self._grid_version, self._pf_post_lopf, - self._max_cos_phi_renewable) + self._max_cos_phi_renewable, + ) Session.remove() # get ding0 MV grid path - ding0_grid_path = os.path.join( - self._ding0_path, - str(mv_grid_id)) + ding0_grid_path = os.path.join(self._ding0_path, str(mv_grid_id)) if not os.path.isdir(ding0_grid_path): - msg = 'No grid data for MV grid {}'.format(mv_grid_id) + msg = "No grid data for MV grid {}".format(mv_grid_id) logger.error(msg) raise Exception(msg) - - # Initialize MV grid logger.info(f"MV grid {mv_grid_id}: Initialize MV grid") - edisgo_grid = EDisGo( - ding0_grid=ding0_grid_path - ) + edisgo_grid = EDisGo(ding0_grid=ding0_grid_path) # ##################### Conduct initial grid reinforcement #################### edisgo_grid.set_time_series_worst_case_analysis() - logger.info(("MV grid {}: Changing eDisGo's voltage configurations " - + "for initial reinforcement").format(mv_grid_id)) - - edisgo_grid.config[ - 'grid_expansion_allowed_voltage_deviations'] = { - 'hv_mv_trafo_offset': 0.04, - 'hv_mv_trafo_control_deviation': 0.0, - 'mv_load_case_max_v_deviation': 0.055, - 'mv_feedin_case_max_v_deviation': 0.02, - 'lv_load_case_max_v_deviation': 0.065, - 'lv_feedin_case_max_v_deviation': 0.03, - 'mv_lv_station_load_case_max_v_deviation': 0.02, - 'mv_lv_station_feedin_case_max_v_deviation': 0.01 + logger.info( + ( + "MV grid {}: Changing eDisGo's voltage configurations " + + "for initial reinforcement" + ).format(mv_grid_id) + ) + + edisgo_grid.config["grid_expansion_allowed_voltage_deviations"] = { + "hv_mv_trafo_offset": 0.04, + "hv_mv_trafo_control_deviation": 0.0, + "mv_load_case_max_v_deviation": 0.055, + "mv_feedin_case_max_v_deviation": 0.02, + "lv_load_case_max_v_deviation": 0.065, + "lv_feedin_case_max_v_deviation": 0.03, + "mv_lv_station_load_case_max_v_deviation": 0.02, + "mv_lv_station_feedin_case_max_v_deviation": 0.01, } # Inital grid reinforcements - logger.info(("MV grid {}: Initial MV grid reinforcement " - + "(worst-case anaylsis)").format(mv_grid_id)) + logger.info( + ( + "MV grid {}: Initial MV grid reinforcement " + "(worst-case anaylsis)" + ).format(mv_grid_id) + ) edisgo_grid.reinforce() # Get costs for initial reinforcement # TODO: Implement a separate cost function - costs_grouped = \ - edisgo_grid.results.grid_expansion_costs.groupby( - ['type']).sum() + costs_grouped = edisgo_grid.results.grid_expansion_costs.groupby(["type"]).sum() costs = pd.DataFrame( costs_grouped.values, columns=costs_grouped.columns, - index=[[edisgo_grid.topology.id] * len(costs_grouped), - costs_grouped.index]).reset_index() - costs.rename(columns={'level_0': 'grid'}, inplace=True) + index=[[edisgo_grid.topology.id] * len(costs_grouped), costs_grouped.index], + ).reset_index() + costs.rename(columns={"level_0": "grid"}, inplace=True) costs_before = costs - total_costs_before_EUR = costs_before['total_costs'].sum() * 1000 + total_costs_before_EUR = costs_before["total_costs"].sum() * 1000 logger.info( - ("MV grid {}: Costs for initial " - + "reinforcement: EUR {}").format( - mv_grid_id, - "{0:,.2f}".format(total_costs_before_EUR))) - - logger.info(( - "MV grid {}: Resetting grid after initial reinforcement" - ).format(mv_grid_id)) + ("MV grid {}: Costs for initial " + "reinforcement: EUR {}").format( + mv_grid_id, "{:,.2f}".format(total_costs_before_EUR) + ) + ) + + logger.info( + ("MV grid {}: Resetting grid after initial reinforcement").format( + mv_grid_id + ) + ) edisgo_grid.results = Results(edisgo_grid) # Reload the (original) eDisGo configs edisgo_grid.config = "default" - edisgo_grid.timeseries = TimeSeries( - timeindex=specs['timeindex'] - ) + edisgo_grid.timeseries = TimeSeries(timeindex=specs["timeindex"]) # ########################################################################### # eTraGo case begins here logger.info("MV grid {}: eTraGo feed-in case".format(mv_grid_id)) # Update eDisGo settings (from config files) with scenario settings - logger.info("MV grid {}: Updating eDisgo configuration".format( - mv_grid_id)) + logger.info("MV grid {}: Updating eDisgo configuration".format(mv_grid_id)) # Update configs with eGo's scenario settings self._update_edisgo_configs(edisgo_grid) @@ -974,27 +939,20 @@ def _run_edisgo( control="fixed_cosphi", generators_parametrisation=None, loads_parametrisation="default", - storage_units_parametrisation=None + storage_units_parametrisation=None, ) # Generator import for future scenario if self._generator_scn: - logger.info( - 'Importing generators for scenario {}'.format( - self._scn_name) - ) - edisgo_grid.import_generators( - generator_scenario=self._generator_scn) + logger.info("Importing generators for scenario {}".format(self._scn_name)) + edisgo_grid.import_generators(generator_scenario=self._generator_scn) else: - logger.info( - 'No generators imported for scenario {}'.format( - self._scn_name) - ) + logger.info("No generators imported for scenario {}".format(self._scn_name)) # Set dispatchable generator time series # Active power edisgo_grid.set_time_series_active_power_predefined( - dispatchable_generators_ts=specs['dispatchable_generators_active_power'], + dispatchable_generators_ts=specs["dispatchable_generators_active_power"], ) # Reactive power gens = edisgo_grid.topology.generators_df[ @@ -1003,7 +961,7 @@ def _run_edisgo( if self._pf_post_lopf: # ToDo Use eTraGo time series to set reactive power (scale by nominal power) edisgo_grid.set_time_series_manual( - generators_q=specs['generators_reactive_power'].loc[:, []], + generators_q=specs["generators_reactive_power"].loc[:, []], ) pass else: @@ -1017,13 +975,13 @@ def _run_edisgo( index=[1], ), loads_parametrisation=None, - storage_units_parametrisation=None + storage_units_parametrisation=None, ) # Set fluctuating generator time series # Active power edisgo_grid.set_time_series_active_power_predefined( - fluctuating_generators_ts=specs['renewables_potential'], + fluctuating_generators_ts=specs["renewables_potential"], ) # Reactive power gens = edisgo_grid.topology.generators_df[ @@ -1032,7 +990,7 @@ def _run_edisgo( if self._pf_post_lopf: # ToDo Use eTraGo time series to set reactive power (scale by nominal power) edisgo_grid.set_time_series_manual( - generators_q=specs['generators_reactive_power'].loc[:, []] + generators_q=specs["generators_reactive_power"].loc[:, []] ) pass else: @@ -1046,37 +1004,36 @@ def _run_edisgo( index=[1], ), loads_parametrisation=None, - storage_units_parametrisation=None + storage_units_parametrisation=None, ) # Get curtailment requirements gens_df = edisgo_grid.topology.generators_df - solar_wind_capacities = gens_df.groupby( - by=['type', 'weather_cell_id'] - )['nominal_capacity'].sum() + solar_wind_capacities = gens_df.groupby(by=["type", "weather_cell_id"])[ + "nominal_capacity" + ].sum() curt_cols = [ - i for i in specs['renewables_curtailment'].columns + i + for i in specs["renewables_curtailment"].columns if i in solar_wind_capacities.index ] if not curt_cols: - raise ImportError( - f"MV grid {mv_grid_id}: Data doesn't match" - ) - curt_abs = pd.DataFrame( - columns=pd.MultiIndex.from_tuples(curt_cols)) + raise ImportError(f"MV grid {mv_grid_id}: Data doesn't match") + curt_abs = pd.DataFrame(columns=pd.MultiIndex.from_tuples(curt_cols)) for col in curt_abs: curt_abs[col] = ( - specs['renewables_curtailment'][col] - * solar_wind_capacities[col]) + specs["renewables_curtailment"][col] * solar_wind_capacities[col] + ) # Get storage information storage_units_capacity = specs["storage_units_capacity"] - storage_units_active_power = specs['storage_units_active_power'] + storage_units_active_power = specs["storage_units_active_power"] if self._pf_post_lopf: - storage_units_reactive_power = specs['storage_units_reactive_power'] + storage_units_reactive_power = specs["storage_units_reactive_power"] else: q_sign = q_control.get_q_sign_generator( - edisgo_grid.config["reactive_power_mode"]["mv_storage"]) + edisgo_grid.config["reactive_power_mode"]["mv_storage"] + ) power_factor = edisgo_grid.config["reactive_power_factor"]["mv_storage"] storage_units_reactive_power = q_control.fixed_cosphi( storage_units_active_power, q_sign, power_factor @@ -1098,7 +1055,8 @@ def _run_edisgo( heat_pump_central_reactive_power = specs["heat_pump_central_reactive_power"] else: q_sign = q_control.get_q_sign_load( - edisgo_grid.config["reactive_power_mode"]["mv_hp"]) + edisgo_grid.config["reactive_power_mode"]["mv_hp"] + ) power_factor = edisgo_grid.config["reactive_power_factor"]["mv_hp"] heat_pump_rural_reactive_power = q_control.fixed_cosphi( heat_pump_rural_active_power, q_sign, power_factor @@ -1116,14 +1074,15 @@ def _run_edisgo( # Solar- and geothermal energy (district heating) geothermal_energy_feedin_district_heating = specs[ - "geothermal_energy_feedin_district_heating"] + "geothermal_energy_feedin_district_heating" + ] solarthermal_energy_feedin_district_heating = specs[ - "solarthermal_energy_feedin_district_heating"] + "solarthermal_energy_feedin_district_heating" + ] # Import charging points with standing times, etc. edisgo_grid.import_electromobility( - simbev_directory="oedb", - tracbev_directory="oedb" + simbev_directory="oedb", tracbev_directory="oedb" ) electromobility_active_power = specs["electromobility_active_power"] @@ -1131,7 +1090,7 @@ def _run_edisgo( edisgo_grid.check_integrity() edisgo_grid.reinforce(timesteps_pfa=self._timesteps_pfa) - self._status_update(mv_grid_id, 'end') + self._status_update(mv_grid_id, "end") path = os.path.join(self._results, str(mv_grid_id)) edisgo_grid.save( @@ -1145,8 +1104,8 @@ def _run_edisgo( archive_type="zip", parameters={ "powerflow_results": ["pfa_p", "pfa_q", "v_res"], - "grid_expansion_results": ["grid_expansion_costs", "equipment_changes"] - } + "grid_expansion_results": ["grid_expansion_costs", "equipment_changes"], + }, ) return {edisgo_grid.topology.id: path} @@ -1156,12 +1115,10 @@ def _save_edisgo_results(self): if not os.path.exists(self._results): os.makedirs(self._results) - with open( - os.path.join(self._results, 'edisgo_args.json'), - 'w') as fp: + with open(os.path.join(self._results, "edisgo_args.json"), "w") as fp: json.dump(self._edisgo_args, fp) - self._grid_choice.to_csv(self._results + '/grid_choice.csv') + self._grid_choice.to_csv(self._results + "/grid_choice.csv") def _load_edisgo_results(self): """ @@ -1175,41 +1132,37 @@ def _load_edisgo_results(self): # Load the grid choice from CSV self._grid_choice = pd.read_csv( - os.path.join(self._csv_import, 'grid_choice.csv'), - index_col=0) - self._grid_choice['represented_grids'] = self._grid_choice.apply( - lambda x: eval(x['represented_grids']), axis=1) + os.path.join(self._csv_import, "grid_choice.csv"), index_col=0 + ) + self._grid_choice["represented_grids"] = self._grid_choice.apply( + lambda x: eval(x["represented_grids"]), axis=1 + ) for idx, row in self._grid_choice.iterrows(): - mv_grid_id = int(row['the_selected_network_id']) + mv_grid_id = int(row["the_selected_network_id"]) try: edisgo_grid = import_edisgo_from_files( - edisgo_path=os.path.join( - self._csv_import, - str(mv_grid_id)), + edisgo_path=os.path.join(self._csv_import, str(mv_grid_id)), import_topology=True, import_timeseries=False, import_results=True, import_electromobility=False, from_zip_archive=True, dtype="float32", - parameters={"powerflow_results": ["pfa_p", "pfa_q"], - "grid_expansion_results": ["grid_expansion_costs"]} + parameters={ + "powerflow_results": ["pfa_p", "pfa_q"], + "grid_expansion_results": ["grid_expansion_costs"], + }, ) - self._edisgo_grids[ - mv_grid_id - ] = edisgo_grid + self._edisgo_grids[mv_grid_id] = edisgo_grid logger.info("Imported MV grid {}".format(mv_grid_id)) except: - self._edisgo_grids[ - mv_grid_id - ] = "This grid failed to reimport" + self._edisgo_grids[mv_grid_id] = "This grid failed to reimport" - logger.warning( - "MV grid {} could not be loaded".format(mv_grid_id)) + logger.warning("MV grid {} could not be loaded".format(mv_grid_id)) def _get_mv_grid_from_bus_id(self, session, bus_id): """ @@ -1228,25 +1181,23 @@ def _get_mv_grid_from_bus_id(self, session, bus_id): """ if self._versioned is True: - ormclass_hvmv_subst = grid.__getattribute__( - 'EgoDpHvmvSubstation' + ormclass_hvmv_subst = grid.__getattribute__("EgoDpHvmvSubstation") + subst_id = ( + session.query(ormclass_hvmv_subst.subst_id) + .filter( + ormclass_hvmv_subst.otg_id == bus_id, + ormclass_hvmv_subst.version == self._grid_version, + ) + .scalar() ) - subst_id = session.query( - ormclass_hvmv_subst.subst_id - ).filter( - ormclass_hvmv_subst.otg_id == bus_id, - ormclass_hvmv_subst.version == self._grid_version - ).scalar() if self._versioned is False: - ormclass_hvmv_subst = model_draft.__getattribute__( - 'EgoGridHvmvSubstation' + ormclass_hvmv_subst = model_draft.__getattribute__("EgoGridHvmvSubstation") + subst_id = ( + session.query(ormclass_hvmv_subst.subst_id) + .filter(ormclass_hvmv_subst.otg_id == bus_id) + .scalar() ) - subst_id = session.query( - ormclass_hvmv_subst.subst_id - ).filter( - ormclass_hvmv_subst.otg_id == bus_id - ).scalar() return subst_id @@ -1267,25 +1218,23 @@ def _get_bus_id_from_mv_grid(self, session, subst_id): """ if self._versioned is True: - ormclass_hvmv_subst = grid.__getattribute__( - 'EgoDpHvmvSubstation' + ormclass_hvmv_subst = grid.__getattribute__("EgoDpHvmvSubstation") + bus_id = ( + session.query(ormclass_hvmv_subst.otg_id) + .filter( + ormclass_hvmv_subst.subst_id == subst_id, + ormclass_hvmv_subst.version == self._grid_version, + ) + .scalar() ) - bus_id = session.query( - ormclass_hvmv_subst.otg_id - ).filter( - ormclass_hvmv_subst.subst_id == subst_id, - ormclass_hvmv_subst.version == self._grid_version - ).scalar() if self._versioned is False: - ormclass_hvmv_subst = model_draft.__getattribute__( - 'EgoGridHvmvSubstation' + ormclass_hvmv_subst = model_draft.__getattribute__("EgoGridHvmvSubstation") + bus_id = ( + session.query(ormclass_hvmv_subst.otg_id) + .filter(ormclass_hvmv_subst.subst_id == subst_id) + .scalar() ) - bus_id = session.query( - ormclass_hvmv_subst.otg_id - ).filter( - ormclass_hvmv_subst.subst_id == subst_id - ).scalar() return bus_id @@ -1303,7 +1252,6 @@ class _ETraGoData: """ def __init__(self, etrago_network): - def filter_by_carrier( etrago_network_obj, component, carrier, like=True, timeseries=True ): @@ -1420,12 +1368,13 @@ def filter_df_by_carrier(df): def parallelizer( - ding0_id_list, - func, - func_arguments, - max_calc_time, - workers=mp2.cpu_count(), - worker_lifetime=1): + ding0_id_list, + func, + func_arguments, + max_calc_time, + workers=mp2.cpu_count(), + worker_lifetime=1, +): """ Use python multiprocessing toolbox for parallelization @@ -1460,6 +1409,7 @@ def parallelizer( containers : dict of :class:`~.edisgo.EDisGo` Dict of EDisGo instances keyed by its ID """ + def collect_pool_results(result): """ Store results from parallelized calculation in structured manner @@ -1481,14 +1431,13 @@ def error_callback(key): def initializer(): import pickle + pickle.DEFAULT_PROTOCOL = 4 import dill - dill.settings['protocol'] = 4 - pool = mp2.Pool( - workers, - initializer=initializer, - maxtasksperchild=worker_lifetime) + dill.settings["protocol"] = 4 + + pool = mp2.Pool(workers, initializer=initializer, maxtasksperchild=worker_lifetime) result_objects = {} for ding0_id in ding0_id_list: @@ -1498,33 +1447,31 @@ def initializer(): func=func, args=edisgo_args, callback=collect_pool_results, - error_callback=error_callback(ding0_id)) + error_callback=error_callback(ding0_id), + ) errors = {} successes = {} start = datetime.now() - end = (start + td(hours=max_calc_time)).isoformat(' ') - logger.info( - "Jobs started. They will time out at {}." - .format(end[:end.index('.')])) + end = (start + td(hours=max_calc_time)).isoformat(" ") + logger.info("Jobs started. They will time out at {}.".format(end[: end.index(".")])) current = datetime.now() time_spent = 0 - while (result_objects and - ((current - start).seconds <= max_calc_time_seconds)): + while result_objects and ((current - start).seconds <= max_calc_time_seconds): done = [] tick = (current - start).seconds * 100 / max_calc_time_seconds if tick - time_spent >= 1 or tick > 100: hours_to_go = (current - start).seconds / 3600 - logger.info("{:.2f}% ({:.2f}/{}h) spent" - .format(tick, hours_to_go, max_calc_time)) - logger.info("Jobs time out in {:.2f}h." - .format(max_calc_time - hours_to_go)) + logger.info( + "{:.2f}% ({:.2f}/{}h) spent".format(tick, hours_to_go, max_calc_time) + ) + logger.info("Jobs time out in {:.2f}h.".format(max_calc_time - hours_to_go)) time_spent = tick for grid, result in result_objects.items(): if result.ready(): logger.info( - "MV grid {} ready. Trying to `get` the result." - .format(grid)) + "MV grid {} ready. Trying to `get` the result.".format(grid) + ) done.append(grid) if not result.successful(): try: @@ -1534,16 +1481,13 @@ def initializer(): result.get() except Exception as e: logger.warning( - "MV grid {} failed due to {e!r}: '{e}'." - .format(grid, e=e)) + "MV grid {} failed due to {e!r}: '{e}'.".format(grid, e=e) + ) errors[grid] = e else: - logger.info( - "MV grid {} calculated successfully.".format(grid)) + logger.info("MV grid {} calculated successfully.".format(grid)) successes[grid] = result.get() - logger.info( - "Done `get`ting the result for MV grid {}." - .format(grid)) + logger.info("Done `get`ting the result for MV grid {}.".format(grid)) for grid in done: del result_objects[grid] sleep(1) @@ -1562,8 +1506,7 @@ def initializer(): end = datetime.now() delta = end - start - logger.info("Execution finished after {:.2f} hours".format( - delta.seconds / 3600)) + logger.info("Execution finished after {:.2f} hours".format(delta.seconds / 3600)) done = [] for grid, result in result_objects.items(): @@ -1572,8 +1515,7 @@ def initializer(): successes[grid] = result.get(timeout=0) logger.info("MV grid {} calculated successfully.".format(grid)) except Exception as e: - logger.warning( - "MV grid {} failed due to {e!r}: '{e}'.".format(grid, e=e)) + logger.warning("MV grid {} failed due to {e!r}: '{e}'.".format(grid, e=e)) errors[grid] = e for grid in done: del result_objects[grid] diff --git a/ego/tools/interface.py b/ego/tools/interface.py index 2b8176e3..29bd40f2 100644 --- a/ego/tools/interface.py +++ b/ego/tools/interface.py @@ -25,13 +25,14 @@ __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolf_bunke,maltesc,mltja" +import math + # Import # General Packages import os -import pandas as pd import time -import math +import pandas as pd if not "READTHEDOCS" in os.environ: from egoio.db_tables import model_draft diff --git a/ego/tools/io.py b/ego/tools/io.py index 84b3cba2..9b7cdac3 100644 --- a/ego/tools/io.py +++ b/ego/tools/io.py @@ -20,58 +20,80 @@ """This file contains the eGo main class as well as input & output functions of eGo in order to build the eGo application container. """ -import sys -import os import json import logging -logger = logging.getLogger('ego') -import pandas as pd -import numpy as np +import os +import sys + +logger = logging.getLogger("ego") import json -if not 'READTHEDOCS' in os.environ: - import pyproj as proj - from shapely.geometry import Polygon, Point, MultiPolygon - from sqlalchemy import MetaData, create_engine, and_, func - from sqlalchemy.orm import sessionmaker +import numpy as np +import pandas as pd + +if not "READTHEDOCS" in os.environ: + import re + + from importlib import import_module + import oedialect - from geoalchemy2 import * + import pyproj as proj + import pypsa + from egoio.db_tables import grid, model_draft + from egoio.db_tables.model_draft import EgoGridPfHvSource as Source + from egoio.db_tables.model_draft import EgoGridPfHvTempResolution as TempResolution + from egoio.db_tables.model_draft import RenpassGisParameterRegion from egoio.tools import db + from etrago.appl import etrago from etrago.tools.io import load_config_file - from egoio.db_tables.model_draft import EgoGridPfHvSource as Source,\ - EgoGridPfHvTempResolution as TempResolution - from ego.tools.results import (create_etrago_results) - from ego.tools.storages import (etrago_storages_investment, etrago_storages) + from etrago.tools.plot import ( + curtailment, + full_load_hours, + gen_dist, + gen_dist_diff, + max_load, + nodal_gen_dispatch, + nodal_production_balance, + plot_line_loading, + plot_line_loading_diff, + plot_q_flows, + plot_residual_load, + plot_stacked_gen, + plot_voltage, + storage_distribution, + storage_expansion, + ) + from geoalchemy2 import * + from shapely.geometry import MultiPolygon, Point, Polygon + from sqlalchemy import MetaData, and_, create_engine, func + from sqlalchemy.orm import sessionmaker + from ego.tools.economics import ( - etrago_operating_costs, + etrago_convert_overnight_cost, etrago_grid_investment, + etrago_operating_costs, get_generator_investment, - etrago_convert_overnight_cost) - from ego.tools.utilities import (get_scenario_setting, - get_time_steps, fix_leading_separator) + ) from ego.tools.edisgo_integration import EDisGoNetworks - from egoio.db_tables.model_draft import RenpassGisParameterRegion - from egoio.db_tables import model_draft, grid - from etrago.tools.plot import (plot_line_loading, plot_stacked_gen, - curtailment, gen_dist, storage_distribution, - plot_voltage, plot_residual_load, - plot_line_loading_diff, full_load_hours, - nodal_gen_dispatch, plot_q_flows, - max_load, storage_expansion, - nodal_production_balance, gen_dist_diff) - from etrago.appl import etrago - from importlib import import_module - import pypsa - import re - from ego.tools.plots import (plot_grid_storage_investment, - power_price_plot, plot_storage_use, igeoplot, - plot_edisgo_cluster, - plot_line_expansion, - plot_storage_expansion) - -__copyright__ = ("Europa-Universität Flensburg, " - "Centre for Sustainable Energy Systems") + from ego.tools.plots import ( + igeoplot, + plot_edisgo_cluster, + plot_grid_storage_investment, + plot_line_expansion, + plot_storage_expansion, + plot_storage_use, + power_price_plot, + ) + from ego.tools.results import create_etrago_results + from ego.tools.storages import etrago_storages, etrago_storages_investment + from ego.tools.utilities import ( + fix_leading_separator, + get_scenario_setting, + get_time_steps, + ) + +__copyright__ = "Europa-Universität Flensburg, " "Centre for Sustainable Energy Systems" __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolf_bunke,maltesc" @@ -97,8 +119,7 @@ class egoBasic(object): """ def __init__(self, *args, **kwargs): - """ - """ + """ """ logger.info("Using scenario setting: {}".format(self.jsonpath)) @@ -110,15 +131,15 @@ def __init__(self, *args, **kwargs): # Database connection from json_file try: - conn = db.connection(section=self.json_file['eTraGo']['db']) + conn = db.connection(section=self.json_file["eTraGo"]["db"]) Session = sessionmaker(bind=conn) self.session = Session() - logger.info('Connected to Database') + logger.info("Connected to Database") except: - logger.error('Failed connection to Database', exc_info=True) + logger.error("Failed connection to Database", exc_info=True) # get scn_name - self.scn_name = self.json_file['eTraGo']['scn_name'] + self.scn_name = self.json_file["eTraGo"]["scn_name"] class eTraGoResults(egoBasic): @@ -134,147 +155,145 @@ class eTraGoResults(egoBasic): """ def __init__(self, *args, **kwargs): - """ - """ + """ """ super(eTraGoResults, self).__init__(self, *args, **kwargs) self.etrago = None self._etrago_network = None self._etrago_disaggregated_network = None - logger.info('eTraGo section started') + logger.info("eTraGo section started") - if self.json_file['eGo']['result_id'] != None: + if self.json_file["eGo"]["result_id"] != None: # Delete arguments from scenario_setting - logger.info('Remove given eTraGo settings from scenario_setting') + logger.info("Remove given eTraGo settings from scenario_setting") try: - self.json_file['eGo']['eTraGo'] = False + self.json_file["eGo"]["eTraGo"] = False - for key in self.json_file['eTraGo'].keys(): + for key in self.json_file["eTraGo"].keys(): - self.json_file['eTraGo'][key] = 'removed by DB recover' + self.json_file["eTraGo"][key] = "removed by DB recover" # ToDo add scenario_setting for results - self.json_file['eTraGo']['db'] = self.json_file['eTraGo']['db'] - logger.info( - 'Add eTraGo scenario_setting from oedb result') + self.json_file["eTraGo"]["db"] = self.json_file["eTraGo"]["db"] + logger.info("Add eTraGo scenario_setting from oedb result") # To do .... - _prefix = 'EgoGridPfHvResult' - schema = 'model_draft' - packagename = 'egoio.db_tables' - _pkg = import_module(packagename + '.' + schema) + _prefix = "EgoGridPfHvResult" + schema = "model_draft" + packagename = "egoio.db_tables" + _pkg = import_module(packagename + "." + schema) # get metadata - orm_meta = getattr(_pkg, _prefix + 'Meta') - self.jsonpath = recover_resultsettings(self.session, - self.json_file, - orm_meta, - self.json_file['eGo'] - ['result_id']) + orm_meta = getattr(_pkg, _prefix + "Meta") + self.jsonpath = recover_resultsettings( + self.session, + self.json_file, + orm_meta, + self.json_file["eGo"]["result_id"], + ) # add etrago_disaggregated_network from DB logger.info( "Recovered eTraGo network uses kmeans: {}".format( - self.json_file['eTraGo']['network_clustering_kmeans'])) + self.json_file["eTraGo"]["network_clustering_kmeans"] + ) + ) except KeyError: pass - logger.info('Create eTraGo network from oedb result') - self._etrago_network = etrago_from_oedb( - self.session, self.json_file) + logger.info("Create eTraGo network from oedb result") + self._etrago_network = etrago_from_oedb(self.session, self.json_file) - if self.json_file['eTraGo']['disaggregation'] != False: + if self.json_file["eTraGo"]["disaggregation"] != False: self._etrago_disaggregated_network = self._etrago_network else: - logger.warning('No disaggregated network found in DB') + logger.warning("No disaggregated network found in DB") self._etrago_disaggregated_network = None # create eTraGo NetworkScenario - if self.json_file['eGo']['eTraGo'] is True: + if self.json_file["eGo"]["eTraGo"] is True: - if self.json_file['eGo'].get('csv_import_eTraGo') != False: + if self.json_file["eGo"].get("csv_import_eTraGo") != False: - logger.info('Caution, import disaggregation ' - 'data of former Cluster') + logger.info("Caution, import disaggregation " "data of former Cluster") # get pathway - pathway = self.json_file['eGo'].get('csv_import_eTraGo') + pathway = self.json_file["eGo"].get("csv_import_eTraGo") try: # create Network from csv self._etrago_network = pypsa.Network() self._etrago_network.import_from_csv_folder(pathway) - logger.info('Create eTraGo network from CSV result') + logger.info("Create eTraGo network from CSV result") # get disaggregation self._etrago_disaggregated_network = pypsa.Network() - self._etrago_disaggregated_network.\ - import_from_csv_folder(pathway+'/disaggregated') - logger.info('Create eTraGo disaggregated network ' - 'from CSV result') + self._etrago_disaggregated_network.import_from_csv_folder( + pathway + "/disaggregated" + ) + logger.info( + "Create eTraGo disaggregated network " "from CSV result" + ) except TypeError: file_path = "disaggregated/network.csv" - fix_leading_separator(pathway+"/"+file_path) + fix_leading_separator(pathway + "/" + file_path) file_path = "network.csv" - fix_leading_separator(pathway+"/"+file_path) + fix_leading_separator(pathway + "/" + file_path) self._etrago_network = pypsa.Network() self._etrago_network.import_from_csv_folder(pathway) - logger.info('Create eTraGo network from CSV result') + logger.info("Create eTraGo network from CSV result") # get disaggregation self._etrago_disaggregated_network = pypsa.Network() - self._etrago_disaggregated_network.\ - import_from_csv_folder(pathway+'/disaggregated') - logger.info('Create eTraGo disaggregated network' - 'from CSV result') + self._etrago_disaggregated_network.import_from_csv_folder( + pathway + "/disaggregated" + ) + logger.info("Create eTraGo disaggregated network" "from CSV result") args_name = "args.json" - with open(pathway+'/'+args_name) as f: + with open(pathway + "/" + args_name) as f: etrago_args = json.load(f) - logger.info('Using argument file') + logger.info("Using argument file") - if etrago_args.get('extendable') == ['network', 'storages']: - etrago_args.update( - {'extendable': ['network', 'storage']}) - logger.info( - 'Changed naming of storages to storage of args') + if etrago_args.get("extendable") == ["network", "storages"]: + etrago_args.update({"extendable": ["network", "storage"]}) + logger.info("Changed naming of storages to storage of args") - if etrago_args.get('extendable') == ['storages']: - etrago_args.update({'extendable': ['storage']}) - logger.info( - 'Changed naming of storages to storage of args') + if etrago_args.get("extendable") == ["storages"]: + etrago_args.update({"extendable": ["storage"]}) + logger.info("Changed naming of storages to storage of args") - for key in self.json_file['eTraGo'].keys(): + for key in self.json_file["eTraGo"].keys(): try: - self.json_file['eTraGo'][key] = etrago_args[key] + self.json_file["eTraGo"][key] = etrago_args[key] except KeyError: pass else: - logger.info('Create eTraGo network calcualted by eGo') + logger.info("Create eTraGo network calcualted by eGo") - if self.json_file['eTraGo']['disaggregation'] != False: + if self.json_file["eTraGo"]["disaggregation"] != False: etrago_network, etrago_disaggregated_network = etrago( - self.json_file['eTraGo']) + self.json_file["eTraGo"] + ) self._etrago_network = etrago_network - self._etrago_disaggregated_network = ( - etrago_disaggregated_network) + self._etrago_disaggregated_network = etrago_disaggregated_network else: logger.warning("Only one network is used.") etrago_network, etrago_disaggregated_network = etrago( - self.json_file['eTraGo']) + self.json_file["eTraGo"] + ) self._etrago_network = etrago_network - self._etrago_disaggregated_network = ( - etrago_disaggregated_network) + self._etrago_disaggregated_network = etrago_disaggregated_network # Add selected results to results container # ----------------------------------------- @@ -285,16 +304,17 @@ def __init__(self, *args, **kwargs): # Add function self.etrago.storage_investment_costs = etrago_storages_investment( - self.etrago.network, self.json_file, self.session) + self.etrago.network, self.json_file, self.session + ) self.etrago.storage_charges = etrago_storages(self.etrago.network) - self.etrago.operating_costs = etrago_operating_costs( - self.etrago.network) - self.etrago.generator = create_etrago_results(self.etrago.network, - self.scn_name) - self.etrago.grid_investment_costs = \ - etrago_grid_investment(self.etrago.network, - self.json_file, self.session) + self.etrago.operating_costs = etrago_operating_costs(self.etrago.network) + self.etrago.generator = create_etrago_results( + self.etrago.network, self.scn_name + ) + self.etrago.grid_investment_costs = etrago_grid_investment( + self.etrago.network, self.json_file, self.session + ) # add functions direct # self._etrago_network.etrago_line_loading = etrago_line_loading @@ -306,17 +326,15 @@ def __init__(self, *args, **kwargs): self.etrago.plot_line_loading_diff = self._line_loading_diff self.etrago.plot_residual_load = self._residual_load self.etrago.plot_voltage = self._voltage - self.etrago.plot_nodal_gen_dispatch = \ - self._nodal_gen_dispatch + self.etrago.plot_nodal_gen_dispatch = self._nodal_gen_dispatch self.etrago.plot_full_load_hours = self._full_load_hours self.etrago.plot_q_flows = self._plot_q_flows self.etrago.plot_max_load = self._max_load self.etrago.plot_storage_expansion = self._storage_expansion - self.etrago.plot_nodal_production_balance = ( - self._nodal_production_balance) + self.etrago.plot_nodal_production_balance = self._nodal_production_balance self.etrago.plot_gen_dist_diff = self._gen_dist_diff - if not 'READTHEDOCS' in os.environ: + if not "READTHEDOCS" in os.environ: # include eTraGo functions and methods def _gen_dist_diff(self, **kwargs): """ @@ -324,8 +342,7 @@ def _gen_dist_diff(self, **kwargs): For more information see: """ - return gen_dist_diff(networkA=self.etrago.network, - **kwargs) + return gen_dist_diff(networkA=self.etrago.network, **kwargs) def _nodal_production_balance(self, **kwargs): """ @@ -333,8 +350,7 @@ def _nodal_production_balance(self, **kwargs): For more information see: """ - return nodal_production_balance(network=self.etrago.network, - **kwargs) + return nodal_production_balance(network=self.etrago.network, **kwargs) def _storage_expansion(self, **kwargs): """ @@ -342,8 +358,7 @@ def _storage_expansion(self, **kwargs): For more information see: """ - return storage_expansion(network=self.etrago.network, - **kwargs) + return storage_expansion(network=self.etrago.network, **kwargs) def _max_load(self, **kwargs): """ @@ -351,8 +366,7 @@ def _max_load(self, **kwargs): For more information see: """ - return max_load(network=self.etrago.network, - **kwargs) + return max_load(network=self.etrago.network, **kwargs) def _plot_q_flows(self): """ @@ -396,8 +410,9 @@ def _storage_distribution(self, scaling=1, **kwargs): Integrate function from eTraGo. For more information see: """ - return storage_distribution(network=self.etrago.network, - scaling=1, **kwargs) + return storage_distribution( + network=self.etrago.network, scaling=1, **kwargs + ) def _voltage(self, **kwargs): """ @@ -418,16 +433,14 @@ def _line_loading_diff(self, **kwargs): Integrate function from eTraGo. For more information see: """ - return plot_line_loading_diff(networkA=self.etrago.network, - **kwargs) + return plot_line_loading_diff(networkA=self.etrago.network, **kwargs) def _nodal_gen_dispatch(self, **kwargs): """ Integrate function from eTraGo. For more information see: """ - return nodal_gen_dispatch(network=self.etrago.network, - **kwargs) + return nodal_gen_dispatch(network=self.etrago.network, **kwargs) def _full_load_hours(self, **kwargs): """ @@ -446,15 +459,16 @@ class eDisGoResults(eTraGoResults): def __init__(self, *args, **kwargs): super(eDisGoResults, self).__init__(self, *args, **kwargs) - if self.json_file['eGo']['eDisGo'] is True: - logger.info('Create eDisGo network') + if self.json_file["eGo"]["eDisGo"] is True: + logger.info("Create eDisGo network") self._edisgo = EDisGoNetworks( json_file=self.json_file, - etrago_network=self.etrago.disaggregated_network) + etrago_network=self.etrago.disaggregated_network, + ) else: self._edisgo = None - logger.info('No eDisGo network') + logger.info("No eDisGo network") @property def edisgo(self): @@ -489,7 +503,7 @@ class eGo(eDisGoResults): def __init__(self, jsonpath, *args, **kwargs): self.jsonpath = jsonpath - super(eGo, self).__init__(self, *args, **kwargs) + super(eGo, self).__init__(self, *args, **kwargs) # add total results here self._total_investment_costs = None @@ -499,49 +513,49 @@ def __init__(self, jsonpath, *args, **kwargs): self._ehv_grid_costs = None self._mv_grid_costs = None - def _calculate_investment_cost( - self, - storage_mv_integration=True): - """ Get total investment costs of all voltage level for storages + def _calculate_investment_cost(self, storage_mv_integration=True): + """Get total investment costs of all voltage level for storages and grid expansion """ - self._total_inv_cost = pd.DataFrame(columns=['component', - 'voltage_level', - 'capital_cost' - ]) + self._total_inv_cost = pd.DataFrame( + columns=["component", "voltage_level", "capital_cost"] + ) _grid_ehv = None - if 'network' in self.json_file['eTraGo']['extendable']: + if "network" in self.json_file["eTraGo"]["extendable"]: _grid_ehv = self.etrago.grid_investment_costs - _grid_ehv['component'] = 'grid' + _grid_ehv["component"] = "grid" - self._total_inv_cost = self._total_inv_cost.\ - append(_grid_ehv, ignore_index=True) + self._total_inv_cost = self._total_inv_cost.append( + _grid_ehv, ignore_index=True + ) _storage = None - if 'storage' in self.json_file['eTraGo']['extendable']: + if "storage" in self.json_file["eTraGo"]["extendable"]: _storage = self.etrago.storage_investment_costs - _storage['component'] = 'storage' + _storage["component"] = "storage" - self._total_inv_cost = self._total_inv_cost.\ - append(_storage, ignore_index=True) + self._total_inv_cost = self._total_inv_cost.append( + _storage, ignore_index=True + ) _grid_mv_lv = None - if self.json_file['eGo']['eDisGo'] is True: + if self.json_file["eGo"]["eDisGo"] is True: _grid_mv_lv = self.edisgo.grid_investment_costs if _grid_mv_lv is not None: - _grid_mv_lv['component'] = 'grid' - _grid_mv_lv['differentiation'] = 'domestic' + _grid_mv_lv["component"] = "grid" + _grid_mv_lv["differentiation"] = "domestic" - self._total_inv_cost = self._total_inv_cost.\ - append(_grid_mv_lv, ignore_index=True) + self._total_inv_cost = self._total_inv_cost.append( + _grid_mv_lv, ignore_index=True + ) # add overnight costs self._total_investment_costs = self._total_inv_cost - self._total_investment_costs[ - 'overnight_costs'] = etrago_convert_overnight_cost( - self._total_investment_costs['capital_cost'], self.json_file) + self._total_investment_costs["overnight_costs"] = etrago_convert_overnight_cost( + self._total_investment_costs["capital_cost"], self.json_file + ) # Include MV storages into the _total_investment_costs dataframe if storage_mv_integration is True: @@ -549,11 +563,13 @@ def _calculate_investment_cost( self._integrate_mv_storage_investment() # sort values - self._total_investment_costs['voltage_level'] = pd.Categorical( - self._total_investment_costs['voltage_level'], ['ehv', 'hv', 'mv', - 'lv', 'mv/lv']) - self._total_investment_costs = ( - self._total_investment_costs.sort_values('voltage_level')) + self._total_investment_costs["voltage_level"] = pd.Categorical( + self._total_investment_costs["voltage_level"], + ["ehv", "hv", "mv", "lv", "mv/lv"], + ) + self._total_investment_costs = self._total_investment_costs.sort_values( + "voltage_level" + ) self._storage_costs = _storage self._ehv_grid_costs = _grid_ehv @@ -577,44 +593,39 @@ def _integrate_mv_storage_investment(self): if integrated_share > 0: ehv_stor_idx = costs_df.index[ - (costs_df['component'] == 'storage') - & (costs_df['voltage_level'] == 'ehv')][0] - - int_capital_costs = costs_df.loc[ehv_stor_idx][ - 'capital_cost' - ] * integrated_share - int_overnight_costs = costs_df.loc[ehv_stor_idx][ - 'overnight_costs' - ] * integrated_share - - costs_df.at[ - ehv_stor_idx, - 'capital_cost' - ] = ( - costs_df.loc[ehv_stor_idx]['capital_cost'] - - int_capital_costs) - - costs_df.at[ - ehv_stor_idx, - 'overnight_costs' - ] = ( - costs_df.loc[ehv_stor_idx]['overnight_costs'] - - int_overnight_costs) + (costs_df["component"] == "storage") + & (costs_df["voltage_level"] == "ehv") + ][0] + + int_capital_costs = ( + costs_df.loc[ehv_stor_idx]["capital_cost"] * integrated_share + ) + int_overnight_costs = ( + costs_df.loc[ehv_stor_idx]["overnight_costs"] * integrated_share + ) + + costs_df.at[ehv_stor_idx, "capital_cost"] = ( + costs_df.loc[ehv_stor_idx]["capital_cost"] - int_capital_costs + ) + + costs_df.at[ehv_stor_idx, "overnight_costs"] = ( + costs_df.loc[ehv_stor_idx]["overnight_costs"] - int_overnight_costs + ) new_storage_row = { - 'component': ['storage'], - 'voltage_level': ['mv'], - 'differentiation': ['domestic'], - 'capital_cost': [int_capital_costs], - 'overnight_costs': [int_overnight_costs]} + "component": ["storage"], + "voltage_level": ["mv"], + "differentiation": ["domestic"], + "capital_cost": [int_capital_costs], + "overnight_costs": [int_overnight_costs], + } new_storage_row = pd.DataFrame(new_storage_row) costs_df = costs_df.append(new_storage_row) self._total_investment_costs = costs_df except: - logger.info( - 'Something went wrong with the MV storage distribution.') + logger.info("Something went wrong with the MV storage distribution.") def _calculate_all_extended_storages(self): """ @@ -623,11 +634,12 @@ def _calculate_all_extended_storages(self): etrago_network = self._etrago_disaggregated_network stor_df = etrago_network.storage_units.loc[ - (etrago_network.storage_units['p_nom_extendable'] == True)] + (etrago_network.storage_units["p_nom_extendable"] == True) + ] - stor_df = stor_df[['bus', 'p_nom_opt']] + stor_df = stor_df[["bus", "p_nom_opt"]] - all_extended_storages = stor_df['p_nom_opt'].sum() + all_extended_storages = stor_df["p_nom_opt"].sum() return all_extended_storages @@ -639,46 +651,51 @@ def _calculate_mv_storage(self): min_extended = 0.3 stor_df = etrago_network.storage_units.loc[ - (etrago_network.storage_units['p_nom_extendable'] == True) - & (etrago_network.storage_units['p_nom_opt'] > min_extended) - & (etrago_network.storage_units['max_hours'] <= 20.)] + (etrago_network.storage_units["p_nom_extendable"] == True) + & (etrago_network.storage_units["p_nom_opt"] > min_extended) + & (etrago_network.storage_units["max_hours"] <= 20.0) + ] - stor_df = stor_df[['bus', 'p_nom_opt']] + stor_df = stor_df[["bus", "p_nom_opt"]] - integrated_storage = .0 # Storage integrated in MV grids + integrated_storage = 0.0 # Storage integrated in MV grids for idx, row in stor_df.iterrows(): - bus_id = row['bus'] - p_nom_opt = row['p_nom_opt'] + bus_id = row["bus"] + p_nom_opt = row["p_nom_opt"] mv_grid_id = self.edisgo.get_mv_grid_from_bus_id(bus_id) if not mv_grid_id: continue - logger.info("Checking storage integration for MV grid {}".format( - mv_grid_id)) + logger.info( + "Checking storage integration for MV grid {}".format(mv_grid_id) + ) grid_choice = self.edisgo.grid_choice cluster = grid_choice.loc[ - [mv_grid_id in repr_grids for repr_grids in grid_choice[ - 'represented_grids']]] + [ + mv_grid_id in repr_grids + for repr_grids in grid_choice["represented_grids"] + ] + ] if len(cluster) == 0: continue else: - representative_grid = cluster[ - 'the_selected_network_id'].values[0] + representative_grid = cluster["the_selected_network_id"].values[0] - if hasattr(self.edisgo.network[representative_grid], 'network'): + if hasattr(self.edisgo.network[representative_grid], "network"): integration_df = self.edisgo.network[ - representative_grid].network.results.storages + representative_grid + ].network.results.storages - integrated_power = integration_df['nominal_power'].sum() / 1000 + integrated_power = integration_df["nominal_power"].sum() / 1000 else: - integrated_power = 0. + integrated_power = 0.0 if integrated_power > p_nom_opt: integrated_power = p_nom_opt @@ -715,25 +732,19 @@ def total_operation_costs(self): return self._total_operation_costs - def plot_total_investment_costs(self, - filename=None, - display=False, **kwargs): - """ Plot total investment costs - """ + def plot_total_investment_costs(self, filename=None, display=False, **kwargs): + """Plot total investment costs""" if filename is None: filename = "results/plot_total_investment_costs.pdf" display = True return plot_grid_storage_investment( - self._total_investment_costs, - filename=filename, - display=display, - **kwargs) + self._total_investment_costs, filename=filename, display=display, **kwargs + ) def plot_power_price(self, filename=None, display=False): - """ Plot power prices per carrier of calculation - """ + """Plot power prices per carrier of calculation""" if filename is None: filename = "results/plot_power_price.pdf" display = True @@ -741,45 +752,38 @@ def plot_power_price(self, filename=None, display=False): return power_price_plot(self, filename=filename, display=display) def plot_storage_usage(self, filename=None, display=False): - """ Plot storage usage by charge and discharge - """ + """Plot storage usage by charge and discharge""" if filename is None: filename = "results/plot_storage_usage.pdf" display = True return plot_storage_use(self, filename=filename, display=display) - def plot_edisgo_cluster(self, filename=None, display=False, - **kwargs): - """ Plot the Clustering of selected Dingo networks - """ + def plot_edisgo_cluster(self, filename=None, display=False, **kwargs): + """Plot the Clustering of selected Dingo networks""" if filename is None: filename = "results/plot_edisgo_cluster.pdf" display = True - return plot_edisgo_cluster(self, filename=filename, display=display, - **kwargs) + return plot_edisgo_cluster(self, filename=filename, display=display, **kwargs) def plot_line_expansion(self, **kwargs): - """Plot line expantion per line - """ + """Plot line expantion per line""" return plot_line_expansion(self, **kwargs) def plot_storage_expansion(self, **kwargs): - """Plot storage expantion per bus - """ + """Plot storage expantion per bus""" return plot_storage_expansion(self, **kwargs) @property def iplot(self): - """ Get iplot of results as html - """ + """Get iplot of results as html""" return igeoplot(self) # write_results_to_db(): - logging.info('Initialisation of eGo Results') + logging.info("Initialisation of eGo Results") def results_to_excel(ego): @@ -789,12 +793,12 @@ def results_to_excel(ego): # Write the results as xlsx file # ToDo add time of calculation to file name # add xlsxwriter to setup - writer = pd.ExcelWriter('open_ego_results.xlsx', engine='xlsxwriter') + writer = pd.ExcelWriter("open_ego_results.xlsx", engine="xlsxwriter") # write results of installed Capacity by fuels - ego.total_investment_costs.to_excel(writer, - index=False, - sheet_name='Total Calculation') + ego.total_investment_costs.to_excel( + writer, index=False, sheet_name="Total Calculation" + ) # Close the Pandas Excel writer and output the Excel file. writer.save() @@ -819,7 +823,7 @@ def etrago_from_oedb(session, json_file): """ - result_id = json_file['eGo']['result_id'] + result_id = json_file["eGo"]["result_id"] # functions def map_ormclass(name): @@ -830,7 +834,7 @@ def map_ormclass(name): _mapped[name] = getattr(_pkg, _prefix + name) except AttributeError: - logger.warning('Relation %s does not exist.' % name) + logger.warning("Relation %s does not exist." % name) return _mapped @@ -854,18 +858,16 @@ def dataframe_results(name, session, result_id, ormclass): query = session.query(ormclass).filter(ormclass.result_id == result_id) - if name == 'Transformer': - name = 'Trafo' + if name == "Transformer": + name = "Trafo" - df = pd.read_sql(query.statement, - session.bind, - index_col=name.lower() + '_id') + df = pd.read_sql(query.statement, session.bind, index_col=name.lower() + "_id") - if name == 'Link': - df['bus0'] = df.bus0.astype(int) - df['bus1'] = df.bus1.astype(int) + if name == "Link": + df["bus0"] = df.bus0.astype(int) + df["bus1"] = df.bus1.astype(int) - if 'source' in df: + if "source" in df: source_orm = Source @@ -873,7 +875,7 @@ def dataframe_results(name, session, result_id, ormclass): df.source = df.source.map(id_to_source(source_query)) - if str(ormclass)[:-2].endswith('T'): + if str(ormclass)[:-2].endswith("T"): df = pd.Dataframe() return df @@ -891,20 +893,16 @@ def series_results(name, column, session, result_id, ormclass): # TODO - check index of bus_t and soon is wrong! # TODO: pls make more robust - id_column = re.findall(r'[A-Z][^A-Z]*', name)[0] + '_' + 'id' + id_column = re.findall(r"[A-Z][^A-Z]*", name)[0] + "_" + "id" id_column = id_column.lower() query = session.query( - getattr(ormclass, id_column), - getattr(ormclass, column). - label(column)).filter(and_( - ormclass.result_id == result_id - )) + getattr(ormclass, id_column), getattr(ormclass, column).label(column) + ).filter(and_(ormclass.result_id == result_id)) - df = pd.io.sql.read_sql(query.statement, - session.bind, - columns=[column], - index_col=id_column) + df = pd.io.sql.read_sql( + query.statement, session.bind, columns=[column], index_col=id_column + ) df.index = df.index.astype(str) @@ -915,37 +913,37 @@ def series_results(name, column, session, result_id, ormclass): assert not df.empty df.index = timeindex except AssertionError: - logger.warning("No data for %s in column %s." % (name, column)) + logger.warning("No data for {} in column {}.".format(name, column)) return df # create config for results path = os.getcwd() # add meta_args with args of results - config = load_config_file(path+'/tools/config.json')['results'] + config = load_config_file(path + "/tools/config.json")["results"] # map and Database settings of etrago_from_oedb() - _prefix = 'EgoGridPfHvResult' - schema = 'model_draft' - packagename = 'egoio.db_tables' - _pkg = import_module(packagename + '.' + schema) - temp_ormclass = 'TempResolution' - carr_ormclass = 'Source' + _prefix = "EgoGridPfHvResult" + schema = "model_draft" + packagename = "egoio.db_tables" + _pkg = import_module(packagename + "." + schema) + temp_ormclass = "TempResolution" + carr_ormclass = "Source" _mapped = {} # get metadata - orm_meta = getattr(_pkg, _prefix + 'Meta') + orm_meta = getattr(_pkg, _prefix + "Meta") # check result_id - result_id_in = session.query( - orm_meta.result_id).filter(orm_meta. - result_id == result_id).all() + result_id_in = ( + session.query(orm_meta.result_id).filter(orm_meta.result_id == result_id).all() + ) if result_id_in: - logger.info('Choosen result_id %s found in DB', result_id) + logger.info("Choosen result_id %s found in DB", result_id) else: - logger.info('Error: result_id not found in DB') + logger.info("Error: result_id not found in DB") # get meta data as args meta_args = recover_resultsettings(session, json_file, orm_meta, result_id) @@ -953,15 +951,17 @@ def series_results(name, column, session, result_id, ormclass): # get TempResolution temp = TempResolution - tr = session.query(temp.temp_id, temp.timesteps, - temp.resolution, temp.start_time).one() + tr = session.query( + temp.temp_id, temp.timesteps, temp.resolution, temp.start_time + ).one() - timeindex = pd.DatetimeIndex(start=tr.start_time, - periods=tr.timesteps, - freq=tr.resolution) + timeindex = pd.DatetimeIndex( + start=tr.start_time, periods=tr.timesteps, freq=tr.resolution + ) - timeindex = timeindex[meta_args['eTraGo']['start_snapshot'] - - 1: meta_args['eTraGo']['end_snapshot']] + timeindex = timeindex[ + meta_args["eTraGo"]["start_snapshot"] - 1 : meta_args["eTraGo"]["end_snapshot"] + ] # create df for PyPSA network @@ -970,37 +970,42 @@ def series_results(name, column, session, result_id, ormclass): timevarying_override = False - if pypsa.__version__ == '0.11.0': - old_to_new_name = {'Generator': - {'p_min_pu_fixed': 'p_min_pu', - 'p_max_pu_fixed': 'p_max_pu', - 'source': 'carrier', - 'dispatch': 'former_dispatch'}, - 'Bus': - {'current_type': 'carrier'}, - 'Transformer': - {'trafo_id': 'transformer_id'}, - 'Storage': - {'p_min_pu_fixed': 'p_min_pu', - 'p_max_pu_fixed': 'p_max_pu', - 'soc_cyclic': 'cyclic_state_of_charge', - 'soc_initial': 'state_of_charge_initial', - 'source': 'carrier'}} + if pypsa.__version__ == "0.11.0": + old_to_new_name = { + "Generator": { + "p_min_pu_fixed": "p_min_pu", + "p_max_pu_fixed": "p_max_pu", + "source": "carrier", + "dispatch": "former_dispatch", + }, + "Bus": {"current_type": "carrier"}, + "Transformer": {"trafo_id": "transformer_id"}, + "Storage": { + "p_min_pu_fixed": "p_min_pu", + "p_max_pu_fixed": "p_max_pu", + "soc_cyclic": "cyclic_state_of_charge", + "soc_initial": "state_of_charge_initial", + "source": "carrier", + }, + } timevarying_override = True else: - old_to_new_name = {'Storage': - {'soc_cyclic': 'cyclic_state_of_charge', - 'soc_initial': 'state_of_charge_initial'}} + old_to_new_name = { + "Storage": { + "soc_cyclic": "cyclic_state_of_charge", + "soc_initial": "state_of_charge_initial", + } + } # get data into dataframes - logger.info('Start building eTraGo results network') + logger.info("Start building eTraGo results network") for comp, comp_t_dict in config.items(): orm_dict = map_ormclass(comp) - pypsa_comp_name = 'StorageUnit' if comp == 'Storage' else comp + pypsa_comp_name = "StorageUnit" if comp == "Storage" else comp ormclass = orm_dict[comp] if not comp_t_dict: @@ -1019,77 +1024,78 @@ def series_results(name, column, session, result_id, ormclass): name = name[:-1] pypsa_comp_name = name - if name == 'Storage': - pypsa_comp_name = 'StorageUnit' - if name == 'Transformer': - name = 'Trafo' + if name == "Storage": + pypsa_comp_name = "StorageUnit" + if name == "Transformer": + name = "Trafo" for col in columns: - df_series = series_results( - name, col, session, result_id, ormclass) + df_series = series_results(name, col, session, result_id, ormclass) # TODO: VMagPuSet? - if timevarying_override and comp == 'Generator': - idx = df[df.former_dispatch == 'flexible'].index + if timevarying_override and comp == "Generator": + idx = df[df.former_dispatch == "flexible"].index idx = [i for i in idx if i in df_series.columns] df_series.drop(idx, axis=1, inplace=True) try: pypsa.io.import_series_from_dataframe( - network, - df_series, - pypsa_comp_name, - col) + network, df_series, pypsa_comp_name, col + ) except (ValueError, AttributeError): - logger.warning("Series %s of component %s could not be" - " imported" % (col, pypsa_comp_name)) + logger.warning( + "Series %s of component %s could not be" + " imported" % (col, pypsa_comp_name) + ) - logger.info('Imported eTraGo results of id = %s ', result_id) + logger.info("Imported eTraGo results of id = %s ", result_id) return network def recover_resultsettings(session, json_file, orm_meta, result_id): - """ Recover scenario_setting from database - """ + """Recover scenario_setting from database""" # check result_id - result_id_in = session.query( - orm_meta.result_id).filter(orm_meta. - result_id == result_id).all() + result_id_in = ( + session.query(orm_meta.result_id).filter(orm_meta.result_id == result_id).all() + ) # get meta data as json_file - meta = session.query(orm_meta.result_id, orm_meta.scn_name, - orm_meta.calc_date, - orm_meta.user_name, orm_meta.method, - orm_meta.start_snapshot, - orm_meta.end_snapshot, orm_meta.solver, - orm_meta.settings - ).filter(orm_meta.result_id == result_id) - - meta_df = pd.read_sql( - meta.statement, meta.session.bind, index_col='result_id') + meta = session.query( + orm_meta.result_id, + orm_meta.scn_name, + orm_meta.calc_date, + orm_meta.user_name, + orm_meta.method, + orm_meta.start_snapshot, + orm_meta.end_snapshot, + orm_meta.solver, + orm_meta.settings, + ).filter(orm_meta.result_id == result_id) + + meta_df = pd.read_sql(meta.statement, meta.session.bind, index_col="result_id") # update json_file with main data by result_id - json_file['eTraGo']['scn_name'] = meta_df.scn_name[result_id] - json_file['eTraGo']['method'] = meta_df.method[result_id] - json_file['eTraGo']['start_snapshot'] = meta_df.start_snapshot[result_id] - json_file['eTraGo']['end_snapshot'] = meta_df.end_snapshot[result_id] - json_file['eTraGo']['solver'] = meta_df.solver[result_id] + json_file["eTraGo"]["scn_name"] = meta_df.scn_name[result_id] + json_file["eTraGo"]["method"] = meta_df.method[result_id] + json_file["eTraGo"]["start_snapshot"] = meta_df.start_snapshot[result_id] + json_file["eTraGo"]["end_snapshot"] = meta_df.end_snapshot[result_id] + json_file["eTraGo"]["solver"] = meta_df.solver[result_id] # update json_file with specific data by result_id meta_set = dict(meta_df.settings[result_id]) - for key in json_file['eTraGo'].keys(): + for key in json_file["eTraGo"].keys(): try: - json_file['eTraGo'][key] = meta_set[key] + json_file["eTraGo"][key] = meta_set[key] except KeyError: pass return json_file -if __name__ == '__main__': +if __name__ == "__main__": pass diff --git a/ego/tools/mv_cluster.py b/ego/tools/mv_cluster.py index 12e35f29..696349a4 100644 --- a/ego/tools/mv_cluster.py +++ b/ego/tools/mv_cluster.py @@ -20,25 +20,28 @@ """ This file contains all functions regarding the clustering of MV grids """ -__copyright__ = ("Flensburg University of Applied Sciences, " - "Europa-Universität Flensburg, " - "Centre for Sustainable Energy Systems") +__copyright__ = ( + "Flensburg University of Applied Sciences, " + "Europa-Universität Flensburg, " + "Centre for Sustainable Energy Systems" +) __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolf_bunke, maltesc" +import logging + # Import -#from __future__ import print_function +# from __future__ import print_function import os -import logging -if not 'READTHEDOCS' in os.environ: +if not "READTHEDOCS" in os.environ: import pickle - + + import numpy as np import pandas as pd - + from sklearn.cluster import KMeans - import numpy as np - + logger = logging.getLogger(__name__) @@ -54,7 +57,7 @@ def analyze_attributes(ding0_path): ---------- ding0_path : :obj:`str` Path to ding0 files - + """ base_path = ding0_path @@ -67,10 +70,9 @@ def analyze_attributes(ding0_path): for district_number in list(range(1, 4000)): try: - pickle_name = 'ding0_grids__{}.pkl'.format( - district_number) - nd = pickle.load(open(os.path.join(base_path, pickle_name), 'rb')) - print('District no.', district_number, 'found!') + pickle_name = "ding0_grids__{}.pkl".format(district_number) + nd = pickle.load(open(os.path.join(base_path, pickle_name), "rb")) + print("District no.", district_number, "found!") except: not_found.append(district_number) continue @@ -83,9 +85,9 @@ def analyze_attributes(ding0_path): # cumulative capacity of solar and wind in MV for geno in nd._mv_grid_districts[0].mv_grid.generators(): - if geno.type == 'solar': + if geno.type == "solar": mv_cum_solar_MV += geno.capacity - if geno.type == 'wind': + if geno.type == "wind": mv_cum_wind_MV += geno.capacity lvg = 0 @@ -97,9 +99,9 @@ def analyze_attributes(ding0_path): for lvgs1 in lvgs.lv_grid_districts(): lvg += len(list(lvgs1.lv_grid.generators())) for deno in lvgs1.lv_grid.generators(): - if deno.type == 'solar': + if deno.type == "solar": mv_cum_solar_LV += deno.capacity - if deno.type == 'wind': + if deno.type == "wind": mv_cum_wind_LV += deno.capacity # Total solar cumulative capacity in lv and mv @@ -120,55 +122,60 @@ def analyze_attributes(ding0_path): max_of_max = 0 # make CB open (normal operation case) - nd.control_circuit_breakers(mode='open') + nd.control_circuit_breakers(mode="open") # setting the root to measure the path from root_mv = nd._mv_grid_districts[0].mv_grid.station() # 1st from MV substation to LV station node # Iteration through nodes for node2 in nd._mv_grid_districts[0].mv_grid._graph.nodes(): # select only LV station nodes - if isinstance( - node2, - LVStationDing0) and not node2.lv_load_area.is_aggregated: + if ( + isinstance(node2, LVStationDing0) + and not node2.lv_load_area.is_aggregated + ): length_from_MV_to_LV_station = 0 # Distance from MV substation to LV station node - length_from_MV_to_LV_station = nd._mv_grid_districts[ - 0 - ].mv_grid.graph_path_length( - node_source=node2, node_target=root_mv) / 1000 + length_from_MV_to_LV_station = ( + nd._mv_grid_districts[0].mv_grid.graph_path_length( + node_source=node2, node_target=root_mv + ) + / 1000 + ) # Iteration through lv load areas for lvgs in nd._mv_grid_districts[0].lv_load_areas(): - for lvgs1 in lvgs.lv_grid_districts(): + for lvgs1 in lvgs.lv_grid_districts(): if lvgs1.lv_grid._station == node2: root_lv = node2 # setting a new root - for node1 in lvgs1.lv_grid._graph.nodes(): + for node1 in lvgs1.lv_grid._graph.nodes(): length_from_LV_staion_to_LV_node = 0 - + # Distance from LV station to LV nodes length_from_LV_staion_to_LV_node = ( - lvgs1.lv_grid.graph_path_length( - node_source=node1, - node_target=root_lv) / 1000) + lvgs1.lv_grid.graph_path_length( + node_source=node1, node_target=root_lv + ) + / 1000 + ) length_from_LV_node_to_MV_substation = 0 - + # total distances in both grids MV and LV length_from_LV_node_to_MV_substation = ( - length_from_MV_to_LV_station - + length_from_LV_staion_to_LV_node) + length_from_MV_to_LV_station + + length_from_LV_staion_to_LV_node + ) # append the total distance to a list - tot_dist.append( - length_from_LV_node_to_MV_substation) - if any(tot_dist): + tot_dist.append(length_from_LV_node_to_MV_substation) + if any(tot_dist): max_length = max(tot_dist) - + # append max lengths of all grids to a list max_length_list.append(max_length) - if any(max_length_list): + if any(max_length_list): # to pick up max of max max_of_max = max(max_length_list) @@ -176,11 +183,14 @@ def analyze_attributes(ding0_path): MV_id_list.append(MV_id) # append the network id to a new list # export results to dataframes - d = {'id': MV_id_list, 'Solar_cumulative_capacity': tccs, - 'Wind_cumulative_capacity': tccw, - 'The_Farthest_node': fnlvmv} # assign lists to columns + d = { + "id": MV_id_list, + "Solar_cumulative_capacity": tccs, + "Wind_cumulative_capacity": tccw, + "The_Farthest_node": fnlvmv, + } # assign lists to columns # not founded networks - are_not_found = {'District_files_that_are_not_found': not_found} + are_not_found = {"District_files_that_are_not_found": not_found} df = pd.DataFrame(d) # dataframe for results @@ -188,22 +198,20 @@ def analyze_attributes(ding0_path): df_are_not_found = pd.DataFrame(are_not_found) # Exporting dataframe to CSV files - df.to_csv(base_path + '/' + 'attributes.csv', sep=',') - df_are_not_found.to_csv(base_path + '/' + 'Not_found_grids.csv', sep=',') + df.to_csv(base_path + "/" + "attributes.csv", sep=",") + df_are_not_found.to_csv(base_path + "/" + "Not_found_grids.csv", sep=",") -def cluster_mv_grids( - no_grids, - cluster_base): +def cluster_mv_grids(no_grids, cluster_base): """ Clusters the MV grids based on the attributes, for a given number of MV grids - + Parameters ---------- no_grids : int Desired number of clusters (of MV grids) - + Returns ------- :pandas:`pandas.DataFrame` @@ -211,11 +219,11 @@ def cluster_mv_grids( """ cluster_base_pu = pd.DataFrame() - + for attribute in cluster_base: attribute_max = cluster_base[attribute].max() cluster_base_pu[attribute] = cluster_base[attribute] / attribute_max - + id_ = [] m = [] for idx, row in cluster_base_pu.iterrows(): @@ -223,23 +231,21 @@ def cluster_mv_grids( f = [] for attribute in row: f.append(attribute) - + m.append(f) - + X = np.array(m) - - logger.info( - 'Used Clustering Attributes: \n {}'.format( - list(cluster_base.columns))) - - no_clusters = no_grids - + + logger.info("Used Clustering Attributes: \n {}".format(list(cluster_base.columns))) + + no_clusters = no_grids + ran_state = 1808 # Starting KMeans clustering kmeans = KMeans(n_clusters=no_clusters, random_state=ran_state) - # Return a label for each point + # Return a label for each point cluster_labels = kmeans.fit_predict(X) # Centers of clusters @@ -254,22 +260,25 @@ def cluster_mv_grids( # Distance from that point to cluster's center (3d coordinates) dist = ( - (X[i][0] - centroids[clus][0]) ** 2 - + (X[i][1] - centroids[clus][1]) ** 2 - + (X[i][2] - centroids[clus][2]) ** 2) ** (1 / 2) + (X[i][0] - centroids[clus][0]) ** 2 + + (X[i][1] - centroids[clus][1]) ** 2 + + (X[i][2] - centroids[clus][2]) ** 2 + ) ** (1 / 2) id_clus_dist.setdefault(clus, []).append({id_[i]: dist}) - + cluster_df = pd.DataFrame( - columns=[ - 'no_of_points_per_cluster', - 'cluster_percentage', - 'the_selected_network_id', - 'represented_grids']) - cluster_df.index.name = 'cluster_id' - + columns=[ + "no_of_points_per_cluster", + "cluster_percentage", + "the_selected_network_id", + "represented_grids", + ] + ) + cluster_df.index.name = "cluster_id" + for key, value in id_clus_dist.items(): - no_points_clus = sum(1 for v in value if v) + no_points_clus = sum(1 for v in value if v) # percentage of points per cluster clus_perc = (no_points_clus / len(X)) * 100 @@ -277,13 +286,14 @@ def cluster_mv_grids( for value_1 in value: id_dist.update(value_1) - # returns the shortest distance point (selected network) + # returns the shortest distance point (selected network) short_dist_net_id_dist = min(id_dist.items(), key=lambda x: x[1]) - + cluster_df.loc[key] = [ - no_points_clus, - round(clus_perc, 2), - short_dist_net_id_dist[0], - list(id_dist.keys())] - + no_points_clus, + round(clus_perc, 2), + short_dist_net_id_dist[0], + list(id_dist.keys()), + ] + return cluster_df diff --git a/ego/tools/plots.py b/ego/tools/plots.py index cb4018d5..40f7a90b 100644 --- a/ego/tools/plots.py +++ b/ego/tools/plots.py @@ -21,54 +21,73 @@ eGo results. """ +import os + import numpy as np import pandas as pd -import os + geopandas = True -if not 'READTHEDOCS' in os.environ: - from etrago.tools.plot import (plot_line_loading, plot_stacked_gen, - add_coordinates, curtailment, gen_dist, - storage_distribution, - plot_voltage, plot_residual_load, coloring) - from ego.tools.economics import etrago_convert_overnight_cost - from ego.tools.utilities import open_oedb_session - from pypsa import Network as PyPSANetwork +if not "READTHEDOCS" in os.environ: + from math import log10, sqrt + import pyproj as proj - from math import sqrt, log10 - from shapely.geometry import Polygon, Point, MultiPolygon + + from etrago.tools.plot import ( + add_coordinates, + coloring, + curtailment, + gen_dist, + plot_line_loading, + plot_residual_load, + plot_stacked_gen, + plot_voltage, + storage_distribution, + ) from geoalchemy2 import * + from pypsa import Network as PyPSANetwork + from shapely.geometry import MultiPolygon, Point, Polygon + + from ego.tools.economics import etrago_convert_overnight_cost + from ego.tools.utilities import open_oedb_session + try: - import geopandas as gpd + import branca.colormap as cm import folium + import geopandas as gpd + from folium import plugins - from folium.plugins import FloatImage from folium.features import CustomIcon - import branca.colormap as cm + from folium.plugins import FloatImage except: geopandas = False import oedialect import webbrowser import subprocess from egoio.db_tables.model_draft import ( - EgoGridMvGriddistrict, RenpassGisParameterRegion) + EgoGridMvGriddistrict, + RenpassGisParameterRegion, + ) from egoio.db_tables.grid import EgoDpMvGriddistrict import matplotlib.pyplot as plt import matplotlib as mpl import matplotlib.colors as mcolors import logging -logger = logging.getLogger('ego') -__copyright__ = "Flensburg University of Applied Sciences, Europa-Universität"\ +logger = logging.getLogger("ego") + +__copyright__ = ( + "Flensburg University of Applied Sciences, Europa-Universität" "Flensburg, Centre for Sustainable Energy Systems" +) __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolfbunke" # plot colore of Carriers def carriers_colore(): - """ Return matplotlib colore set per carrier (technologies of + """Return matplotlib colore set per carrier (technologies of generators) of eTraGo. Returns @@ -77,51 +96,54 @@ def carriers_colore(): List of carriers and matplotlib colores """ - colors = {'biomass': 'green', - 'coal': 'k', - 'gas': 'orange', - 'eeg_gas': 'olive', - 'geothermal': 'purple', - 'lignite': 'brown', - 'oil': 'darkgrey', - 'other_non_renewable': 'pink', - 'reservoir': 'navy', - 'run_of_river': 'aqua', - 'pumped_storage': 'steelblue', - 'solar': 'yellow', - 'uranium': 'lime', - 'waste': 'sienna', - 'wind': 'skyblue', - 'slack': 'pink', - 'load shedding': 'red', - 'nan': 'm', - 'imports': 'salmon', - '': 'm'} + colors = { + "biomass": "green", + "coal": "k", + "gas": "orange", + "eeg_gas": "olive", + "geothermal": "purple", + "lignite": "brown", + "oil": "darkgrey", + "other_non_renewable": "pink", + "reservoir": "navy", + "run_of_river": "aqua", + "pumped_storage": "steelblue", + "solar": "yellow", + "uranium": "lime", + "waste": "sienna", + "wind": "skyblue", + "slack": "pink", + "load shedding": "red", + "nan": "m", + "imports": "salmon", + "": "m", + } return colors def ego_colore(): - """ Get the four eGo colores + """Get the four eGo colores Returns ------- colors : :obj:`dict` List of eGo matplotlib hex colores """ - colors = {'egoblue1': '#1F567D', - 'egoblue2': '#84A2B8', - 'egoblue3': '#A3B9C9', - 'egoblue4': '#C7D5DE' - } + colors = { + "egoblue1": "#1F567D", + "egoblue2": "#84A2B8", + "egoblue3": "#A3B9C9", + "egoblue4": "#C7D5DE", + } return colors -def plot_storage_expansion(ego, filename=None, dpi=300, - column='overnight_costs', - scaling=1): - """ Plot line expantion +def plot_storage_expansion( + ego, filename=None, dpi=300, column="overnight_costs", scaling=1 +): + """Plot line expantion Parameters ---------- @@ -148,38 +170,45 @@ def plot_storage_expansion(ego, filename=None, dpi=300, json_file = ego.json_file # get storage values - if 'storage' in ego.json_file['eTraGo']['extendable']: - storage_inv = network.storage_units[network.storage_units. - capital_cost > 0.] - storage_inv['investment_costs'] = (storage_inv.capital_cost * - storage_inv.p_nom_opt) - storage_inv['overnight_costs'] = etrago_convert_overnight_cost( - storage_inv['investment_costs'], json_file) + if "storage" in ego.json_file["eTraGo"]["extendable"]: + storage_inv = network.storage_units[network.storage_units.capital_cost > 0.0] + storage_inv["investment_costs"] = ( + storage_inv.capital_cost * storage_inv.p_nom_opt + ) + storage_inv["overnight_costs"] = etrago_convert_overnight_cost( + storage_inv["investment_costs"], json_file + ) msd_max = storage_inv[column].max() msd_median = storage_inv[column].median() msd_min = storage_inv[column].min() - if (msd_max - msd_min) > 1.e+5: + if (msd_max - msd_min) > 1.0e5: if msd_max != 0: LabelVal = int(log10(msd_max)) else: LabelVal = 0 if LabelVal < 0: - LabelUnit = '€' - msd_max, msd_median, msd_min = msd_max * \ - 1000, msd_median * 1000, msd_min * 1000 + LabelUnit = "€" + msd_max, msd_median, msd_min = ( + msd_max * 1000, + msd_median * 1000, + msd_min * 1000, + ) storage_inv[column] = storage_inv[column] * 1000 elif LabelVal < 3: - LabelUnit = 'k €' + LabelUnit = "k €" else: - LabelUnit = 'M €' - msd_max, msd_median, msd_min = msd_max / \ - 1000, msd_median / 1000, msd_min / 1000 + LabelUnit = "M €" + msd_max, msd_median, msd_min = ( + msd_max / 1000, + msd_median / 1000, + msd_min / 1000, + ) storage_inv[column] = storage_inv[column] / 1000 else: - LabelUnit = '€' + LabelUnit = "€" # start plotting figsize = 6, 6 @@ -187,48 +216,53 @@ def plot_storage_expansion(ego, filename=None, dpi=300, bus_sizes = storage_inv[column] * scaling - if column == 'investment_costs': - title = 'Annualized Storage costs per timestep' - ltitel = 'Storage costs' - if column == 'overnight_costs': - title = 'Total Expansion Costs Overnight' - ltitel = 'Storage costs' - if column == 'p_nom_opt': - title = 'Storage Expansion in MVA' - ltitel = 'Storage size' - LabelUnit = 'kW' - if column not in ['investment_costs', 'overnight_costs', 'p_nom_opt']: - title = 'unknown' - ltitel = 'unknown' - LabelUnit = 'unknown' + if column == "investment_costs": + title = "Annualized Storage costs per timestep" + ltitel = "Storage costs" + if column == "overnight_costs": + title = "Total Expansion Costs Overnight" + ltitel = "Storage costs" + if column == "p_nom_opt": + title = "Storage Expansion in MVA" + ltitel = "Storage size" + LabelUnit = "kW" + if column not in ["investment_costs", "overnight_costs", "p_nom_opt"]: + title = "unknown" + ltitel = "unknown" + LabelUnit = "unknown" if sum(storage_inv[column]) == 0: - sc = network.plot(bus_sizes=0, - ax=ax, - title="No storage expantion") + sc = network.plot(bus_sizes=0, ax=ax, title="No storage expantion") else: sc = network.plot( bus_sizes=bus_sizes, - bus_colors='g', + bus_colors="g", # bus_cmap= # line_colors='gray', title=title, - line_widths=0.3 + line_widths=0.3, ) ax.set_alpha(0.4) # add legend for area in [msd_max, msd_median, msd_min]: - plt.scatter([], [], c='white', s=area * scaling, - label='= ' + str(round(area, 0)) + LabelUnit + ' ') + plt.scatter( + [], + [], + c="white", + s=area * scaling, + label="= " + str(round(area, 0)) + LabelUnit + " ", + ) - plt.legend(scatterpoints=1, - labelspacing=1, - title=ltitel, - loc='upper left', - shadow=True, - fontsize='x-large') + plt.legend( + scatterpoints=1, + labelspacing=1, + title=ltitel, + loc="upper left", + shadow=True, + fontsize="x-large", + ) ax.autoscale(tight=True) @@ -237,12 +271,12 @@ def plot_storage_expansion(ego, filename=None, dpi=300, else: fig = ax.get_figure() fig.set_size_inches(10, 8, forward=True) - fig.savefig(filename, dpi=dpi) + fig.savefig(filename, dpi=dpi) plt.close() -def plot_line_expansion(ego, filename=None, dpi=300, column='overnight_costs'): - """ Plot line expantion +def plot_line_expansion(ego, filename=None, dpi=300, column="overnight_costs"): + """Plot line expantion Parameters ---------- @@ -267,18 +301,21 @@ def plot_line_expansion(ego, filename=None, dpi=300, column='overnight_costs'): json_file = ego.json_file # get values - if 'network' in ego.json_file['eTraGo']['extendable']: - network.lines['s_nom_expansion'] = network.lines.s_nom_opt.subtract( - network.lines.s_nom, axis='index') - network.lines['investment_costs'] = network.lines.s_nom_expansion.\ - multiply(network.lines.capital_cost, axis='index') - network.lines['overnight_costs'] = etrago_convert_overnight_cost( - network.lines['investment_costs'], json_file) + if "network" in ego.json_file["eTraGo"]["extendable"]: + network.lines["s_nom_expansion"] = network.lines.s_nom_opt.subtract( + network.lines.s_nom, axis="index" + ) + network.lines["investment_costs"] = network.lines.s_nom_expansion.multiply( + network.lines.capital_cost, axis="index" + ) + network.lines["overnight_costs"] = etrago_convert_overnight_cost( + network.lines["investment_costs"], json_file + ) else: - network.lines['s_nom_expansion'] = None - network.lines['investment_costs'] = None - network.lines['overnight_costs'] = None + network.lines["s_nom_expansion"] = None + network.lines["investment_costs"] = None + network.lines["overnight_costs"] = None # start plotting figsize = 10, 8 @@ -286,40 +323,40 @@ def plot_line_expansion(ego, filename=None, dpi=300, column='overnight_costs'): cmap = plt.cm.jet - if column == 's_nom_expansion': + if column == "s_nom_expansion": line_value = network.lines[column] title = "Line expansion in MVA" - if column == 'overnight_costs': + if column == "overnight_costs": line_value = network.lines[column] title = "Total Expansion Costs in € per line" - if column == 'investment_costs': + if column == "investment_costs": line_value = network.lines[column] title = "Annualized Expansion Costs in € per line and time step" - line_widths = (line_value/line_value.max()) + line_widths = line_value / line_value.max() - lc = network.plot(ax=ax, line_colors=line_value, - line_cmap=cmap, - title=title, - line_widths=line_widths) + lc = network.plot( + ax=ax, + line_colors=line_value, + line_cmap=cmap, + title=title, + line_widths=line_widths, + ) - boundaries = [min(line_value), - max(line_value)] + boundaries = [min(line_value), max(line_value)] v = np.linspace(boundaries[0], boundaries[1], 101) print(v.dtype.name) # colorbar - cb = plt.colorbar(lc[1], boundaries=v, - ticks=v[0:101:10], - ax=ax) + cb = plt.colorbar(lc[1], boundaries=v, ticks=v[0:101:10], ax=ax) cb.set_clim(vmin=boundaries[0], vmax=boundaries[1]) - if column == 's_nom_expansion': - cb.set_label('Expansion in MVA per line') - if column == 'overnight_costs': - cb.set_label('Total Expansion Costs in € per line') - if column == 'investment_costs': - cb.set_label('Annualized Expansion Costs in € per line') + if column == "s_nom_expansion": + cb.set_label("Expansion in MVA per line") + if column == "overnight_costs": + cb.set_label("Total Expansion Costs in € per line") + if column == "investment_costs": + cb.set_label("Annualized Expansion Costs in € per line") ax.autoscale(tight=True) @@ -328,7 +365,7 @@ def plot_line_expansion(ego, filename=None, dpi=300, column='overnight_costs'): else: fig = ax.get_figure() fig.set_size_inches(10, 8, forward=True) - fig.savefig(filename, dpi=dpi) + fig.savefig(filename, dpi=dpi) plt.close() @@ -358,51 +395,44 @@ def plot_grid_storage_investment(costs_df, filename, display, var=None): bar_width = 0.35 opacity = 0.4 - if var == 'overnight_cost': - tic = costs_df[['component', - 'overnight_costs', - 'voltage_level', - 'differentiation']] - tic.set_index(['voltage_level', 'component', - 'differentiation'], inplace=True) - ax = tic.unstack().plot(kind='bar', - stacked=False, - - rot=0, - color=([colors.get(key) - for key in - ['egoblue1', - 'egoblue2', - 'egoblue4']]), - legend=False) + if var == "overnight_cost": + tic = costs_df[ + ["component", "overnight_costs", "voltage_level", "differentiation"] + ] + tic.set_index(["voltage_level", "component", "differentiation"], inplace=True) + ax = tic.unstack().plot( + kind="bar", + stacked=False, + rot=0, + color=([colors.get(key) for key in ["egoblue1", "egoblue2", "egoblue4"]]), + legend=False, + ) ax.set_ylabel("Overnight costs of simulation") - ax.set_title("Total costs of simulation, " - "voltage level and component", y=1.08) + ax.set_title( + "Total costs of simulation, " "voltage level and component", y=1.08 + ) else: - tic = costs_df[['component', - 'capital_cost', - 'voltage_level', - 'differentiation']] - tic.set_index(['voltage_level', 'component', - 'differentiation'], inplace=True) - ax = tic.unstack().plot(kind='bar', - rot=0, - stacked=False, - - color=([colors.get(key) - for key in - ['egoblue1', - 'egoblue2', - 'egoblue3']]), - legend=False) + tic = costs_df[ + ["component", "capital_cost", "voltage_level", "differentiation"] + ] + tic.set_index(["voltage_level", "component", "differentiation"], inplace=True) + ax = tic.unstack().plot( + kind="bar", + rot=0, + stacked=False, + color=([colors.get(key) for key in ["egoblue1", "egoblue2", "egoblue3"]]), + legend=False, + ) ax.set_ylabel("Annualized costs per simulation periods") - ax.set_title("Annualized costs per simulation periods, " - "voltage level and component", y=1.08) + ax.set_title( + "Annualized costs per simulation periods, " "voltage level and component", + y=1.08, + ) - ax.set_xlabel('Voltage level and component') + ax.set_xlabel("Voltage level and component") ax.set_yscale("symlog") - ax.legend(('cross-border', 'domestic', 'foreign')) + ax.legend(("cross-border", "domestic", "foreign")) ax.autoscale() if display is True: @@ -410,7 +440,7 @@ def plot_grid_storage_investment(costs_df, filename, display, var=None): else: fig = ax.get_figure() fig.set_size_inches(10, 8, forward=True) - fig.savefig(filename, dpi=100) + fig.savefig(filename, dpi=100) plt.close() @@ -433,29 +463,29 @@ def power_price_plot(ego, filename, display): https://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.show """ plt.rcdefaults() -# colors = ego_colore() + # colors = ego_colore() carrier_colors = coloring() fig, ax = plt.subplots() # plot power_price - prc = ego.etrago.generator['power_price'] + prc = ego.etrago.generator["power_price"] bar_width = 0.35 opacity = 0.4 - ind = np.arange(len(prc.index)) # the x locations for the groups - width = 0.35 # the width of the bars: can also be len(x) sequence + ind = np.arange(len(prc.index)) # the x locations for the groups + width = 0.35 # the width of the bars: can also be len(x) sequence plt_colors = [carrier_colors[carrier] for carrier in prc.index] -# plt_colors = colors['egoblue1'] + # plt_colors = colors['egoblue1'] - ax.barh(ind, prc, align='center', color=plt_colors) + ax.barh(ind, prc, align="center", color=plt_colors) ax.set_yticks(ind) ax.set_yticklabels(prc.index) ax.invert_yaxis() - ax.set_xlabel('Power price in €/MWh') - ax.set_title('Power Costs per Carrier') + ax.set_xlabel("Power price in €/MWh") + ax.set_title("Power Costs per Carrier") ax.autoscale(tight=True) @@ -464,7 +494,7 @@ def power_price_plot(ego, filename, display): else: fig = ax.get_figure() fig.set_size_inches(10, 8, forward=True) - fig.savefig(filename, dpi=100) + fig.savefig(filename, dpi=100) def plot_storage_use(ego, filename, display): @@ -486,18 +516,15 @@ def plot_storage_use(ego, filename, display): """ colors = ego_colore() - ax = ego.etrago.\ - storage_charges[['charge', 'discharge']].plot(kind='bar', - title="Storage usage", - stacked=True, - color=([colors.get(key) - for key in - ['egoblue1', - 'egoblue2']]), - figsize=( - 15, 10), - legend=True, - fontsize=12) + ax = ego.etrago.storage_charges[["charge", "discharge"]].plot( + kind="bar", + title="Storage usage", + stacked=True, + color=([colors.get(key) for key in ["egoblue1", "egoblue2"]]), + figsize=(15, 10), + legend=True, + fontsize=12, + ) ax.set_xlabel("Kind of Storage", fontsize=12) ax.set_ylabel("Charge and Discharge in MWh", fontsize=12) ax.autoscale(tight=False) @@ -508,7 +535,7 @@ def plot_storage_use(ego, filename, display): fig = ax.get_figure() fig.set_size_inches(10, 8, forward=True) fig.subplots_adjust(bottom=0.25) - fig.savefig(filename, dpi=100) + fig.savefig(filename, dpi=100) def get_country(session, region=None): @@ -530,35 +557,38 @@ def get_country(session, region=None): if region is None: # Define regions 'FR', - region = ['DE', 'DK', 'BE', 'LU', - 'NO', 'PL', 'CH', 'CZ', 'SE', 'NL'] + region = ["DE", "DK", "BE", "LU", "NO", "PL", "CH", "CZ", "SE", "NL"] else: region # get database tabel - query = session.query(RenpassGisParameterRegion.gid, - RenpassGisParameterRegion.stat_level, - RenpassGisParameterRegion.u_region_id, - RenpassGisParameterRegion.geom, - RenpassGisParameterRegion.geom_point) + query = session.query( + RenpassGisParameterRegion.gid, + RenpassGisParameterRegion.stat_level, + RenpassGisParameterRegion.u_region_id, + RenpassGisParameterRegion.geom, + RenpassGisParameterRegion.geom_point, + ) # get regions by query and filter - Regions = [(gid, u_region_id, stat_level, - shape.to_shape(geom), - shape.to_shape(geom_point)) for gid, u_region_id, stat_level, - geom, geom_point in query.filter( - RenpassGisParameterRegion.u_region_id. - in_(region)).all()] + Regions = [ + (gid, u_region_id, stat_level, shape.to_shape(geom), shape.to_shape(geom_point)) + for gid, u_region_id, stat_level, geom, geom_point in query.filter( + RenpassGisParameterRegion.u_region_id.in_(region) + ).all() + ] # define SRID - crs = {'init': 'epsg:4326'} + crs = {"init": "epsg:4326"} country = gpd.GeoDataFrame( - Regions, columns=['gid', 'stat_level', 'u_region_id', - 'geometry', 'point_geom'], crs=crs) + Regions, + columns=["gid", "stat_level", "u_region_id", "geometry", "point_geom"], + crs=crs, + ) return country def prepareGD(session, subst_id=None, version=None): - """ Get MV grid districts for plotting form oedb. + """Get MV grid districts for plotting form oedb. Parameters ---------- @@ -577,45 +607,66 @@ def prepareGD(session, subst_id=None, version=None): if version: - query = session.query(EgoDpMvGriddistrict.subst_id, - EgoDpMvGriddistrict.geom) + query = session.query(EgoDpMvGriddistrict.subst_id, EgoDpMvGriddistrict.geom) if isinstance(subst_id, list): - Regions = [(subst_id, shape.to_shape(geom)) for subst_id, geom in - query.filter(EgoDpMvGriddistrict.version == version, - EgoDpMvGriddistrict.subst_id.in_( - subst_id)).all()] + Regions = [ + (subst_id, shape.to_shape(geom)) + for subst_id, geom in query.filter( + EgoDpMvGriddistrict.version == version, + EgoDpMvGriddistrict.subst_id.in_(subst_id), + ).all() + ] elif subst_id == "all": - Regions = [(subst_id, shape.to_shape(geom)) for subst_id, geom in - query.filter(EgoDpMvGriddistrict.version == - version).all()] + Regions = [ + (subst_id, shape.to_shape(geom)) + for subst_id, geom in query.filter( + EgoDpMvGriddistrict.version == version + ).all() + ] else: # ToDo query doesn't looks stable - Regions = [(subst_id, shape.to_shape(geom)) for subst_id, geom in - query.filter(EgoDpMvGriddistrict.version == - version).all()] + Regions = [ + (subst_id, shape.to_shape(geom)) + for subst_id, geom in query.filter( + EgoDpMvGriddistrict.version == version + ).all() + ] # toDo add values of sub_id etc. to popup else: # from model_draft - query = session.query(EgoGridMvGriddistrict.subst_id, - EgoGridMvGriddistrict.geom) - Regions = [(subst_id, shape.to_shape(geom)) for subst_id, geom in - query.filter(EgoGridMvGriddistrict.subst_id.in_( - subst_id)).all()] - - crs = {'init': 'epsg:3035'} - region = gpd.GeoDataFrame( - Regions, columns=['subst_id', 'geometry'], crs=crs) - region = region.to_crs({'init': 'epsg:4326'}) + query = session.query( + EgoGridMvGriddistrict.subst_id, EgoGridMvGriddistrict.geom + ) + Regions = [ + (subst_id, shape.to_shape(geom)) + for subst_id, geom in query.filter( + EgoGridMvGriddistrict.subst_id.in_(subst_id) + ).all() + ] + + crs = {"init": "epsg:3035"} + region = gpd.GeoDataFrame(Regions, columns=["subst_id", "geometry"], crs=crs) + region = region.to_crs({"init": "epsg:4326"}) return region -def plot_edisgo_cluster(ego, filename, region=['DE'], display=False, dpi=150, - add_ehv_storage=False, grid_choice=None, title="", - cmap="jet", labelsize=10, fontsize=10): +def plot_edisgo_cluster( + ego, + filename, + region=["DE"], + display=False, + dpi=150, + add_ehv_storage=False, + grid_choice=None, + title="", + cmap="jet", + labelsize=10, + fontsize=10, +): """Plot the Clustering of selected Dingo networks Parameters @@ -645,47 +696,47 @@ def plot_edisgo_cluster(ego, filename, region=['DE'], display=False, dpi=150, """ session = ego.session - version = ego.json_file['eTraGo']['gridversion'] + version = ego.json_file["eTraGo"]["gridversion"] # get cluster if grid_choice: cluster = pd.read_csv(grid_choice, index_col=0) - cluster['represented_grids'] = cluster.apply( - lambda x: eval(x['represented_grids']), axis=1) + cluster["represented_grids"] = cluster.apply( + lambda x: eval(x["represented_grids"]), axis=1 + ) else: cluster = ego.edisgo.grid_choice - cluster = cluster.rename( - columns={"the_selected_network_id": "subst_id"}) + cluster = cluster.rename(columns={"the_selected_network_id": "subst_id"}) cluster_id = list(cluster.subst_id) # get country Polygon cnty = get_country(session, region=region) # get grid districts singel - if ego.json_file['eGo']['eDisGo'] is True: + if ego.json_file["eGo"]["eDisGo"] is True: gridcluster = prepareGD(session, cluster_id, version) - gridcluster = gridcluster.merge(cluster, on='subst_id') + gridcluster = gridcluster.merge(cluster, on="subst_id") # add percentage of grid representation - gridcluster['percentage'] = ((gridcluster.no_of_points_per_cluster / - gridcluster.no_of_points_per_cluster.sum())*100) - gridcluster['percentage'] = gridcluster['percentage'].astype( - float).round(2) + gridcluster["percentage"] = ( + gridcluster.no_of_points_per_cluster + / gridcluster.no_of_points_per_cluster.sum() + ) * 100 + gridcluster["percentage"] = gridcluster["percentage"].astype(float).round(2) # get represented grids - repre_grids = pd.DataFrame(columns=['subst_id', - 'geometry', - 'cluster_id', - 'style']) + repre_grids = pd.DataFrame( + columns=["subst_id", "geometry", "cluster_id", "style"] + ) for cluster in gridcluster.index: rep_id = gridcluster.represented_grids[cluster] # represented_grids repre_grid = prepareGD(session, rep_id, version) - repre_grid['cluster_id'] = gridcluster.subst_id[cluster] + repre_grid["cluster_id"] = gridcluster.subst_id[cluster] repre_grids = repre_grids.append(repre_grid, ignore_index=True) # add common SRID - crs = {'init': 'epsg:4326'} + crs = {"init": "epsg:4326"} repre_grids = gpd.GeoDataFrame(repre_grids, crs=crs) # get all MV grids @@ -696,29 +747,35 @@ def plot_edisgo_cluster(ego, filename, region=['DE'], display=False, dpi=150, figsize = 5, 5 fig, ax = plt.subplots(1, 1, figsize=(figsize)) - cnty.plot(ax=ax, color='white', - edgecolor='whitesmoke', alpha=0.5, linewidth=0.1) - mvgrids.plot(ax=ax, color='white', alpha=0.1, linewidth=0.1) + cnty.plot(ax=ax, color="white", edgecolor="whitesmoke", alpha=0.5, linewidth=0.1) + mvgrids.plot(ax=ax, color="white", alpha=0.1, linewidth=0.1) - if ego.json_file['eGo']['eDisGo'] is True: + if ego.json_file["eGo"]["eDisGo"] is True: - repre_grids.plot(ax=ax, column='cluster_id', - cmap=cmap, - edgecolor='whitesmoke', - linewidth=0.005, - alpha=1, - legend=False) + repre_grids.plot( + ax=ax, + column="cluster_id", + cmap=cmap, + edgecolor="whitesmoke", + linewidth=0.005, + alpha=1, + legend=False, + ) # subplot - gridcluster.plot(ax=ax, column='percentage', - cmap=cmap, - edgecolor='black', - linewidth=1, - legend=True) + gridcluster.plot( + ax=ax, + column="percentage", + cmap=cmap, + edgecolor="black", + linewidth=1, + legend=True, + ) # add storage distribution if add_ehv_storage: - _storage_distribution(ego.etrago.network, scaling=1, filename=None, - ax=ax, fig=fig) + _storage_distribution( + ego.etrago.network, scaling=1, filename=None, ax=ax, fig=fig + ) ax.set_title(title) # ax.legend(title="id of cluster representative") @@ -727,8 +784,7 @@ def plot_edisgo_cluster(ego, filename, region=['DE'], display=False, dpi=150, # cb = plt.colorbar(ax) # cb.ax.tick_params(labelsize=17) - ax.set_ylabel("weighting of MV grid cluster in %", - fontsize=fontsize, rotation=270) + ax.set_ylabel("weighting of MV grid cluster in %", fontsize=fontsize, rotation=270) ax.yaxis.set_label_coords(1.2, 0.5) ax.autoscale(tight=True) @@ -738,7 +794,7 @@ def plot_edisgo_cluster(ego, filename, region=['DE'], display=False, dpi=150, else: fig = ax.get_figure() fig.set_size_inches(10, 8, forward=True) - fig.savefig(filename, dpi=dpi) + fig.savefig(filename, dpi=dpi) plt.close() @@ -762,43 +818,43 @@ def igeoplot(ego, tiles=None, geoloc=None, save_image=False): ------- plot: html HTML file with .js plot - """ + """ network = ego.etrago.network session = open_oedb_session(ego) # get scenario name from args - scn_name = ego.json_file['eTraGo']['scn_name'] - version = ego.json_file['eTraGo']['gridversion'] + scn_name = ego.json_file["eTraGo"]["scn_name"] + version = ego.json_file["eTraGo"]["gridversion"] # define SRID - crs = {'init': 'epsg:4326'} + crs = {"init": "epsg:4326"} if geoloc is None: geoloc = [network.buses.y.mean(), network.buses.x.mean()] - mp = folium.Map(tiles=None, location=geoloc, - control_scale=True, zoom_start=6) + mp = folium.Map(tiles=None, location=geoloc, control_scale=True, zoom_start=6) # add Nasa light background - if tiles == 'Nasa': - tiles = ("https://map1.vis.earthdata.nasa.gov/wmts-webmerc/" + - "VIIRS_CityLights_2012/default/GoogleMapsCompatible_" + - "Level8/{z}/{y}/{x}.jpg") - attr = ('© OpenStreetMap contributors, © CartoDB') + if tiles == "Nasa": + tiles = ( + "https://map1.vis.earthdata.nasa.gov/wmts-webmerc/" + + "VIIRS_CityLights_2012/default/GoogleMapsCompatible_" + + "Level8/{z}/{y}/{x}.jpg" + ) + attr = '© OpenStreetMap contributors, © CartoDB' folium.raster_layers.TileLayer(tiles=tiles, attr=attr).add_to(mp) else: - attr = ('© OpenStreetMap contributors, © OpenEnergy-Platform') + attr = '© OpenStreetMap contributors, © OpenEnergy-Platform' - folium.raster_layers.TileLayer('OpenStreetMap', attr=attr).add_to(mp) + folium.raster_layers.TileLayer("OpenStreetMap", attr=attr).add_to(mp) # Legend name - bus_group = folium.FeatureGroup( - name='Bus information (ehv/hv)') # , show=True + bus_group = folium.FeatureGroup(name="Bus information (ehv/hv)") # , show=True # create icon - #url = 'https://raw.githubusercontent.com/openego/eGo/master/doc/images/{}'.format - #icon_image = url('trafo.png') + # url = 'https://raw.githubusercontent.com/openego/eGo/master/doc/images/{}'.format + # icon_image = url('trafo.png') # bus_icon = CustomIcon(icon_image, # icon_size=(27, 47)) @@ -818,36 +874,43 @@ def igeoplot(ego, tiles=None, geoloc=None, save_image=False): v_mag_pu_max: {}
    sub_network: {}
    Version: {}
    - """.format(row.name, scn_name, row['carrier'], - row['control'], row['type'], row['v_nom'], - row['v_mag_pu_set'], - row['v_mag_pu_min'], row['v_mag_pu_max'], - row['sub_network'], version) + """.format( + row.name, + scn_name, + row["carrier"], + row["control"], + row["type"], + row["v_nom"], + row["v_mag_pu_set"], + row["v_mag_pu_min"], + row["v_mag_pu_max"], + row["sub_network"], + version, + ) # add Popup values use HTML for formating - folium.Marker([row["y"], row["x"]], popup=popup - ).add_to(bus_group) # icon=bus_icon + folium.Marker([row["y"], row["x"]], popup=popup).add_to( + bus_group + ) # icon=bus_icon - logger.info('Added Busses') + logger.info("Added Busses") def convert_to_hex(rgba_color): - """Convert rgba colors to hex - """ - red = str(hex(int(rgba_color[0]*255)))[2:].capitalize() - green = str(hex(int(rgba_color[1]*255)))[2:].capitalize() - blue = str(hex(int(rgba_color[2]*255)))[2:].capitalize() + """Convert rgba colors to hex""" + red = str(hex(int(rgba_color[0] * 255)))[2:].capitalize() + green = str(hex(int(rgba_color[1] * 255)))[2:].capitalize() + blue = str(hex(int(rgba_color[2] * 255)))[2:].capitalize() - if blue == '0': - blue = '00' - if red == '0': - red = '00' - if green == '0': - green = '00' + if blue == "0": + blue = "00" + if red == "0": + red = "00" + if green == "0": + green = "00" - return '#' + red + green + blue + return "#" + red + green + blue # Prepare lines - line_group = folium.FeatureGroup( - name='Line Loading (ehv/hv)') # , show=False + line_group = folium.FeatureGroup(name="Line Loading (ehv/hv)") # , show=False # get line Coordinates x0 = network.lines.bus0.map(network.buses.x) @@ -861,54 +924,60 @@ def convert_to_hex(rgba_color): cols = list(network.lines.columns) # color map lines - colormap = cm.linear.YlOrRd_09.scale( - lines.s_nom.min(), lines.s_nom.max()).to_step(6) + colormap = cm.linear.YlOrRd_09.scale(lines.s_nom.min(), lines.s_nom.max()).to_step( + 6 + ) # add parameter for line in network.lines.index: popup = """ Line: {}
    - version: {}
    """.format(line, version) + version: {}
    """.format( + line, version + ) for col in cols: popup += """ {}: {}
    """.format(col, lines[col][line]) # change colore function - l_color = colormapper_lines( - colormap, lines, line, column="s_nom") + l_color = colormapper_lines(colormap, lines, line, column="s_nom") # ToDo make it more generic - folium.PolyLine(([y0[line], x0[line]], [y1[line], x1[line]]), - popup=popup, color=convert_to_hex(l_color)).\ - add_to(line_group) + folium.PolyLine( + ([y0[line], x0[line]], [y1[line], x1[line]]), + popup=popup, + color=convert_to_hex(l_color), + ).add_to(line_group) # Add results # add expansion costs per line lines = network.lines - if 'network' in ego.json_file['eTraGo']['extendable']: - lines['s_nom_expansion'] = lines.s_nom_opt.subtract( - lines.s_nom, axis='index') - lines['annuity'] = lines.s_nom_expansion.multiply( - lines.capital_cost, axis='index') - lines['overnight_cost'] = etrago_convert_overnight_cost( - lines['annuity'], - ego.json_file, t=40, p=0.05) - lines['overnight_cost'] = lines['overnight_cost'].astype(float).round(0) + if "network" in ego.json_file["eTraGo"]["extendable"]: + lines["s_nom_expansion"] = lines.s_nom_opt.subtract(lines.s_nom, axis="index") + lines["annuity"] = lines.s_nom_expansion.multiply( + lines.capital_cost, axis="index" + ) + lines["overnight_cost"] = etrago_convert_overnight_cost( + lines["annuity"], ego.json_file, t=40, p=0.05 + ) + lines["overnight_cost"] = lines["overnight_cost"].astype(float).round(0) else: - lines['s_nom_expansion'] = 0. - lines['annuity'] = 0. - lines['overnight_cost'] = 0. + lines["s_nom_expansion"] = 0.0 + lines["annuity"] = 0.0 + lines["overnight_cost"] = 0.0 # Prepare lines line_results_group = folium.FeatureGroup( - name='Line costs by annuity costs (ehv/hv)') + name="Line costs by annuity costs (ehv/hv)" + ) # color map lines colormap2 = cm.linear.YlGn_09.scale( - lines.annuity.min(), lines.annuity.max()).to_step(4) + lines.annuity.min(), lines.annuity.max() + ).to_step(4) # add parameter cols = list(ego.etrago.network.lines.columns) - res = ('overnight_cost', 's_nom_expansion', 'annuity') - unit = ('EUR', 'MVA', 'EUR') + res = ("overnight_cost", "s_nom_expansion", "annuity") + unit = ("EUR", "MVA", "EUR") cols = [x for x in cols if x not in res] for line in network.lines.index: @@ -916,7 +985,9 @@ def convert_to_hex(rgba_color): popup = """ Line: {}
    version: {}

    - Line parameter:
    """.format(line, version) + Line parameter:
    """.format( + line, version + ) for col in cols: popup += """ {}: {}
    """.format(col, lines[col][line]) @@ -924,39 +995,49 @@ def convert_to_hex(rgba_color): popup += """
    Results:
    """ for idx, val in enumerate(res): - popup += """{}: {:,} in {}
    """.format(val, - lines[val][line], - unit[idx]) + popup += """{}: {:,} in {}
    """.format(val, lines[val][line], unit[idx]) # change colore function - lr_color = colormapper_lines( - colormap2, lines, line, column="annuity") + lr_color = colormapper_lines(colormap2, lines, line, column="annuity") # ToDo make it more generic - folium.PolyLine(([y0[line], x0[line]], [y1[line], x1[line]]), - popup=popup, - color=convert_to_hex(lr_color) - ).add_to(line_results_group) + folium.PolyLine( + ([y0[line], x0[line]], [y1[line], x1[line]]), + popup=popup, + color=convert_to_hex(lr_color), + ).add_to(line_results_group) - logger.info('Added Lines') + logger.info("Added Lines") # Create ehv/hv storage expantion plot - store_group = folium.FeatureGroup( - name='Storage expantion (ehv/hv)') # , show=True + store_group = folium.FeatureGroup(name="Storage expantion (ehv/hv)") # , show=True - stores = network.storage_units[network.storage_units.carrier == - 'extendable_storage'] + stores = network.storage_units[ + network.storage_units.carrier == "extendable_storage" + ] # differentiation of storage units batteries = stores[stores.max_hours == 6] hydrogen = stores[stores.max_hours == 168] # sum by type and bus - storage_distribution = network.storage_units.p_nom_opt[stores.index].groupby( - network.storage_units.bus).sum().reindex(network.buses.index, fill_value=0.) - battery_distribution = network.storage_units.p_nom_opt[batteries.index].groupby( - network.storage_units.bus).sum().reindex(network.buses.index, fill_value=0.) - hydrogen_distribution = network.storage_units.p_nom_opt[hydrogen.index].groupby( - network.storage_units.bus).sum().reindex(network.buses.index, fill_value=0.) + storage_distribution = ( + network.storage_units.p_nom_opt[stores.index] + .groupby(network.storage_units.bus) + .sum() + .reindex(network.buses.index, fill_value=0.0) + ) + battery_distribution = ( + network.storage_units.p_nom_opt[batteries.index] + .groupby(network.storage_units.bus) + .sum() + .reindex(network.buses.index, fill_value=0.0) + ) + hydrogen_distribution = ( + network.storage_units.p_nom_opt[hydrogen.index] + .groupby(network.storage_units.bus) + .sum() + .reindex(network.buses.index, fill_value=0.0) + ) # add Coordinates sto_x = stores.bus.map(network.buses.x) @@ -970,44 +1051,49 @@ def convert_to_hex(rgba_color): popup = """ Storage: {}
    version: {}

    - Parameter:
    """.format(store, version) + Parameter:
    """.format( + store, version + ) for col in cols: popup += """ {}: {}
    """.format(col, stores[col][store]) # get storage radius by p_nom_opt (MW) if lager as 1 KW - if ((stores['p_nom_opt'][store] > 7.4e-04) & - (stores['capital_cost'][store] > 10)): + if (stores["p_nom_opt"][store] > 7.4e-04) & ( + stores["capital_cost"][store] > 10 + ): - radius = (3**(1+stores['p_nom_opt'][store]/sto_max)) + radius = 3 ** (1 + stores["p_nom_opt"][store] / sto_max) # add singel storage folium.CircleMarker( location=([sto_y[store], sto_x[store]]), radius=radius, popup=popup, - color='#3186cc', + color="#3186cc", fill=True, - fill_color='#3186cc', - weight=1).add_to(store_group) + fill_color="#3186cc", + weight=1, + ).add_to(store_group) - logger.info('Added storages') + logger.info("Added storages") ###################### # add MV line loading # add grid districs - if ego.json_file['eGo']['eDisGo'] is True: + if ego.json_file["eGo"]["eDisGo"] is True: grid_group = folium.FeatureGroup( - name='Represented MV Grid district') # , show=False + name="Represented MV Grid district" + ) # , show=False subst_id = list(ego.edisgo.grid_choice.the_selected_network_id) district = prepareGD(session, subst_id, version) # Add for loop - crs = {'init': 'epsg:4326'} + crs = {"init": "epsg:4326"} for name, row in district.iterrows(): - mv_grid_id = row['subst_id'] + mv_grid_id = row["subst_id"] if not isinstance(ego.edisgo.network[mv_grid_id], str): lv, mv = _get_mv_plot_res(ego, mv_grid_id) @@ -1017,50 +1103,57 @@ def convert_to_hex(rgba_color): pop = """Grid district: {}

    MV results:
    - """.format(row['subst_id']) + """.format( + row["subst_id"] + ) for idxs in mv.index: pop += """ {} : {} €
    - """.format(idxs, mv[0][idxs].astype( - float).round(2)) + """.format( + idxs, mv[0][idxs].astype(float).round(2) + ) pop += """LV results:
    """ for idxs in lv.index: pop += """ {} : {} €
    - """.format(idxs, lv[0][idxs].astype( - float).round(2)) + """.format( + idxs, lv[0][idxs].astype(float).round(2) + ) else: pop = """Grid district: {}

    - """.format(row['subst_id']) + """.format( + row["subst_id"] + ) # folium.GeoJson(row['geometry']).add_to( # grid_group).add_child(folium.Popup(pop)) - geojson = folium.GeoJson(row['geometry']) + geojson = folium.GeoJson(row["geometry"]) popup = folium.Popup(pop) popup.add_to(geojson) geojson.add_to(grid_group) # Add cluster grids repgrid_group = folium.FeatureGroup( - name='Represented MV Grids per Cluster') # , show=False + name="Represented MV Grids per Cluster" + ) # , show=False cluster = ego.edisgo.grid_choice - cluster = cluster.rename( - columns={"the_selected_network_id": "subst_id"}) + cluster = cluster.rename(columns={"the_selected_network_id": "subst_id"}) - repre_grids = pd.DataFrame(columns=['subst_id', - 'geometry', - 'cluster_id', - 'color']) + repre_grids = pd.DataFrame( + columns=["subst_id", "geometry", "cluster_id", "color"] + ) - style_function = (lambda x: { - 'fillColor': x['properties']['color'], - 'weight': 0.5, 'color': 'black'}) + style_function = lambda x: { + "fillColor": x["properties"]["color"], + "weight": 0.5, + "color": "black", + } # simplify MultiPolygon tolerance = 0.002 @@ -1069,22 +1162,24 @@ def convert_to_hex(rgba_color): cluster_id = list(cluster.represented_grids[idx]) # represented_grids repre_grid = prepareGD(session, cluster_id, version) - repre_grid['cluster_id'] = cluster.subst_id[idx] + repre_grid["cluster_id"] = cluster.subst_id[idx] repre_grids = repre_grids.append(repre_grid, ignore_index=True) # prepare cluster colore - normal = mpl.colors.Normalize(vmin=repre_grids.cluster_id.min(), - vmax=repre_grids.cluster_id.max(), - clip=True) + normal = mpl.colors.Normalize( + vmin=repre_grids.cluster_id.min(), + vmax=repre_grids.cluster_id.max(), + clip=True, + ) mapper = plt.cm.ScalarMappable(norm=normal, cmap=plt.cm.viridis) # add colors to column - repre_grids['color'] = repre_grids['cluster_id'].apply( - lambda x: mcolors.to_hex(mapper.to_rgba(x))) + repre_grids["color"] = repre_grids["cluster_id"].apply( + lambda x: mcolors.to_hex(mapper.to_rgba(x)) + ) - repre_grids = gpd.GeoDataFrame( - repre_grids, geometry='geometry', crs=crs) + repre_grids = gpd.GeoDataFrame(repre_grids, geometry="geometry", crs=crs) # simplify Polygon geometry repre_grids.geometry = repre_grids.geometry.simplify(tolerance) @@ -1092,20 +1187,18 @@ def convert_to_hex(rgba_color): # add popup for name, row in repre_grids.iterrows(): - pops = """Represented Grid: {}
    """.format( - row['cluster_id']) + pops = """Represented Grid: {}
    """.format(row["cluster_id"]) - folium.GeoJson(repre_grids[name:name+1], - style_function=style_function, - name='represented grids' - ).add_to(repgrid_group - ).add_child(folium.Popup(pops)) + folium.GeoJson( + repre_grids[name : name + 1], + style_function=style_function, + name="represented grids", + ).add_to(repgrid_group).add_child(folium.Popup(pops)) - logger.info('Added MV Grids') + logger.info("Added MV Grids") # Prepare MV lines - mv_line_group = folium.FeatureGroup( - name='MV Grids (>=10kV)') # show=False + mv_line_group = folium.FeatureGroup(name="MV Grids (>=10kV)") # show=False mv_list = ego.edisgo.grid_choice.the_selected_network_id @@ -1119,66 +1212,73 @@ def convert_to_hex(rgba_color): # get line Coordinates x0 = mv_network.lines.bus0.loc[mv_network.lines.v_nom >= 10].map( - mv_network.buses.x) + mv_network.buses.x + ) x1 = mv_network.lines.bus1.loc[mv_network.lines.v_nom >= 10].map( - mv_network.buses.x) + mv_network.buses.x + ) y0 = mv_network.lines.bus0.loc[mv_network.lines.v_nom >= 10].map( - mv_network.buses.y) + mv_network.buses.y + ) y1 = mv_network.lines.bus1.loc[mv_network.lines.v_nom >= 10].map( - mv_network.buses.y) + mv_network.buses.y + ) # get content grid_expansion_costs = ego.edisgo.network[ - mv_grid_id].network.results.grid_expansion_costs - lines = pd.concat([mv_network.lines, - grid_expansion_costs], - axis=1, - join_axes=[mv_network.lines.index]) + mv_grid_id + ].network.results.grid_expansion_costs + lines = pd.concat( + [mv_network.lines, grid_expansion_costs], + axis=1, + join_axes=[mv_network.lines.index], + ) lines = lines.loc[mv_network.lines.v_nom >= 10] lines = lines.reindex() cols = list(lines.columns) - res_mv = ('overnight_costs', 'capital_cost') - unit = ('EUR', 'EUR/time step') + res_mv = ("overnight_costs", "capital_cost") + unit = ("EUR", "EUR/time step") cols = [x for x in cols if x not in res_mv] # save results as csv csv_print = False if csv_print == True: - geo_lines2 = pd.concat([y0, x0, y1, x1], - axis=1, - join_axes=[y0.index]) + geo_lines2 = pd.concat( + [y0, x0, y1, x1], axis=1, join_axes=[y0.index] + ) - line_export = pd.concat([lines, geo_lines2], - axis=1, - join_axes=[lines.index]) + line_export = pd.concat( + [lines, geo_lines2], axis=1, join_axes=[lines.index] + ) - line_export.to_csv("results/mv_line_results_" + - str(mv_grid_id)+".csv") + line_export.to_csv( + "results/mv_line_results_" + str(mv_grid_id) + ".csv" + ) # color map lines try: mv_colormap = cm.linear.YlGnBu_09.scale( - lines.overnight_costs.min(), - lines.overnight_costs.max()).to_step(6) + lines.overnight_costs.min(), lines.overnight_costs.max() + ).to_step(6) except: - mv_colormap = cm.linear.YlGnBu_09.scale( - 0, 0).to_step(6) + mv_colormap = cm.linear.YlGnBu_09.scale(0, 0).to_step(6) - mv_colormap.caption = 'Line investment of overnight cost (mv)' + mv_colormap.caption = "Line investment of overnight cost (mv)" # add parameter for line in lines.index: popup = """ Line: {}
    - version: {}

    """.format(line, version) + version: {}

    """.format( + line, version + ) popup += """MV line parameter:
    """ for col in cols: try: - popup += """ {}: {}
    """.format(col, - lines[col][line]) + popup += """ {}: {}
    """.format(col, lines[col][line]) except: popup += """ """ @@ -1186,34 +1286,34 @@ def convert_to_hex(rgba_color): for idx, val in enumerate(res_mv): try: - popup += """{}: {} in {}
    """.format(val, - lines[val][line], - unit[idx]) + popup += """{}: {} in {}
    """.format( + val, lines[val][line], unit[idx] + ) except: popup += """ """ # change colore function mv_color = colormapper_lines( - mv_colormap, lines, line, column="overnight_costs") + mv_colormap, lines, line, column="overnight_costs" + ) # ToDo make it more generic try: - folium.PolyLine(([y0[line], x0[line]], - [y1[line], x1[line]]), - popup=popup, color=convert_to_hex( - mv_color) + folium.PolyLine( + ([y0[line], x0[line]], [y1[line], x1[line]]), + popup=popup, + color=convert_to_hex(mv_color), ).add_to(mv_line_group) except: logger.disabled = True - logger.info('Cound not find a geometry') + logger.info("Cound not find a geometry") logger.disabled = False else: - logger.info(str(mv_grid_id)+" " + - str(ego.edisgo.network[mv_grid_id])) + logger.info(str(mv_grid_id) + " " + str(ego.edisgo.network[mv_grid_id])) mp.add_child(mv_colormap) # Add MV Storage # Legend name - mv_sto_group = folium.FeatureGroup(name='MV storages') # ,show=False + mv_sto_group = folium.FeatureGroup(name="MV storages") # ,show=False # add mv storages mv_grid_id = list(ego.edisgo.grid_choice.the_selected_network_id) @@ -1225,7 +1325,9 @@ def convert_to_hex(rgba_color): # create pypsa network only containing MV buses and lines pypsa_plot = PyPSANetwork() - pypsa_plot.buses = pypsa_network.buses.loc[pypsa_network.buses.v_nom >= 10] + pypsa_plot.buses = pypsa_network.buses.loc[ + pypsa_network.buses.v_nom >= 10 + ] # add Coordinates sto_x = pypsa_plot.storage_units.bus.map(pypsa_plot.buses.x) @@ -1238,32 +1340,36 @@ def convert_to_hex(rgba_color): for store in pypsa_plot.storage_units.index: popup = """ Storage: {}

    - Parameter:
    """.format(store,) + Parameter:
    """.format( + store, + ) for col in sto_cols: popup += """ {}: {}
    - """.format(col, - pypsa_plot.storage_units[col][store]) + """.format( + col, pypsa_plot.storage_units[col][store] + ) folium.CircleMarker( location=([sto_y[store], sto_x[store]]), - radius=pypsa_plot.storage_units['p_nom'], + radius=pypsa_plot.storage_units["p_nom"], popup=popup, - color='#3186cc', + color="#3186cc", fill=True, - fill_color='#3186cc', - weight=1).add_to(mv_sto_group) + fill_color="#3186cc", + weight=1, + ).add_to(mv_sto_group) - logger.info('Added MV stores') + logger.info("Added MV stores") # add layers and others - colormap.caption = 'Line loading s_nom (ehv/hv)' - colormap2.caption = 'Line investment of annuity costs (ehv/hv)' + colormap.caption = "Line loading s_nom (ehv/hv)" + colormap2.caption = "Line investment of annuity costs (ehv/hv)" mp.add_child(colormap) mp.add_child(colormap2) # add legend # add layer groups - if ego.json_file['eGo']['eDisGo'] is True: + if ego.json_file["eGo"]["eDisGo"] is True: repgrid_group.add_to(mp) grid_group.add_to(mp) @@ -1278,22 +1384,22 @@ def convert_to_hex(rgba_color): folium.LayerControl().add_to(mp) plugins.Fullscreen( - position='topright', - title='Fullscreen', - title_cancel='Exit me', - force_separate_button=True).add_to(mp) + position="topright", + title="Fullscreen", + title_cancel="Exit me", + force_separate_button=True, + ).add_to(mp) - url = ('https://openego.readthedocs.io/en/master/_images/open_ego_icon_web.png') + url = "https://openego.readthedocs.io/en/master/_images/open_ego_icon_web.png" FloatImage(url, bottom=0, left=5).add_to(mp) - if ego.json_file['eGo']['eDisGo'] is True: - mp = iplot_griddistrict_legend( - mp=mp, repre_grids=repre_grids, start=True) + if ego.json_file["eGo"]["eDisGo"] is True: + mp = iplot_griddistrict_legend(mp=mp, repre_grids=repre_grids, start=True) mp = iplot_totalresults_legend(mp=mp, ego=ego, start=True) # Save Map - html_dir = 'results/html' + html_dir = "results/html" if not os.path.exists(html_dir): os.makedirs(html_dir) mp.save("results/html/iplot_map.html") @@ -1309,16 +1415,16 @@ def convert_to_hex(rgba_color): if save_image: url2 = "file://{}/{}".format(os.getcwd(), url) outfn = os.path.join(html_dir, "outfig.png") - subprocess.check_call(["cutycapt", "--url={}".format(url2), - "--out={}".format(outfn)]) + subprocess.check_call( + ["cutycapt", "--url={}".format(url2), "--out={}".format(outfn)] + ) # close oedb session.close() - logger.info('Done') + logger.info("Done") def colormapper_lines(colormap, lines, line, column="s_nom"): - """ Make Colore Map for lines. - """ + """Make Colore Map for lines.""" # TODO: make it more generic l_color = [] @@ -1336,7 +1442,7 @@ def colormapper_lines(colormap, lines, line, column="s_nom"): elif colormap.index[1] >= lines[column][line] >= colormap.index[0]: l_color = colormap.colors[0] else: - l_color = (0., 0., 0., 1.) + l_color = (0.0, 0.0, 0.0, 1.0) if len(colormap.index) == 5: if colormap.index[4] >= lines[column][line] > colormap.index[3]: @@ -1348,7 +1454,7 @@ def colormapper_lines(colormap, lines, line, column="s_nom"): elif colormap.index[1] >= lines[column][line] >= colormap.index[0]: l_color = colormap.colors[0] else: - l_color = (0., 0., 0., 1.) + l_color = (0.0, 0.0, 0.0, 1.0) return l_color @@ -1367,9 +1473,12 @@ def _storage_distribution(network, ax, fig, scaling=1, filename=None): """ stores = network.storage_units - storage_distribution = network.storage_units.p_nom_opt[stores.index]\ - .groupby(network.storage_units.bus)\ - .sum().reindex(network.buses.index, fill_value=0.) + storage_distribution = ( + network.storage_units.p_nom_opt[stores.index] + .groupby(network.storage_units.bus) + .sum() + .reindex(network.buses.index, fill_value=0.0) + ) msd_max = storage_distribution.max() msd_median = storage_distribution[storage_distribution != 0].median() @@ -1380,45 +1489,41 @@ def _storage_distribution(network, ax, fig, scaling=1, filename=None): else: LabelVal = 0 if LabelVal < 0: - LabelUnit = 'kW' - msd_max, msd_median, msd_min = msd_max * \ - 1000, msd_median * 1000, msd_min * 1000 + LabelUnit = "kW" + msd_max, msd_median, msd_min = msd_max * 1000, msd_median * 1000, msd_min * 1000 storage_distribution = storage_distribution * 1000 elif LabelVal < 3: - LabelUnit = 'MW' + LabelUnit = "MW" else: - LabelUnit = 'GW' - msd_max, msd_median, msd_min = msd_max / \ - 1000, msd_median / 1000, msd_min / 1000 + LabelUnit = "GW" + msd_max, msd_median, msd_min = msd_max / 1000, msd_median / 1000, msd_min / 1000 storage_distribution = storage_distribution / 1000 if sum(storage_distribution) == 0: network.plot(bus_sizes=0, ax=ax) else: - network.plot( - bus_sizes=storage_distribution * scaling, - ax=ax, - line_widths=0.3 - ) + network.plot(bus_sizes=storage_distribution * scaling, ax=ax, line_widths=0.3) def iplot_griddistrict_legend(mp, repre_grids, start=False): - """Add legend to iplot function of mv grids. - - """ + """Add legend to iplot function of mv grids.""" # from branca.element import Template, MacroElement from string import Template if start: legends = [] - for name, row in repre_grids.groupby(['cluster_id', 'color']).count().iterrows(): + for name, row in ( + repre_grids.groupby(["cluster_id", "color"]).count().iterrows() + ): color = name[1] grid_no = name[0] entry = """
  • - Represented by Grid {}
  • """.format(color, grid_no) + Represented by Grid {} """.format( + color, grid_no + ) legends.append(entry) @@ -1612,7 +1717,7 @@ def iplot_griddistrict_legend(mp, repre_grids, start=False): t = Template(temp_2) temp_2 = t.substitute(legend=legend) - temps = temp_1+temp_2+temp_3 + temps = temp_1 + temp_2 + temp_3 # macro = MacroElement(**leg) # macro._template = Template(template) @@ -1621,28 +1726,36 @@ def iplot_griddistrict_legend(mp, repre_grids, start=False): def iplot_totalresults_legend(mp, ego, start=False): - """ Add total results as legend to iplot function. - """ + """Add total results as legend to iplot function.""" from string import Template if start: # get data total = ego.total_investment_costs.rename( - columns={"capital_cost": "annuity_costs"}) + columns={"capital_cost": "annuity_costs"} + ) # change format - total['overnight_costs'] = ( - total['overnight_costs']/1000000).map('M€ {:,.2f}'.format) + total["overnight_costs"] = (total["overnight_costs"] / 1000000).map( + "M€ {:,.2f}".format + ) - total['annuity_costs'] = (total['annuity_costs'] / - 1000).map('T€ {:,.2f}'.format) + total["annuity_costs"] = (total["annuity_costs"] / 1000).map( + "T€ {:,.2f}".format + ) - total = total[['component', 'voltage_level', - 'differentiation', 'overnight_costs', - 'annuity_costs']].to_html(index=False) + total = total[ + [ + "component", + "voltage_level", + "differentiation", + "overnight_costs", + "annuity_costs", + ] + ].to_html(index=False) # inclued grafic - html_dir = 'results/html' + html_dir = "results/html" if not os.path.exists(html_dir): os.makedirs(html_dir) @@ -1697,8 +1810,7 @@ def iplot_totalresults_legend(mp, ego, start=False): def _get_mv_plot_res(ego, mv_grid_id): - """ Prepare mv results. - """ + """Prepare mv results.""" logger.disabled = True pypsa_network = ego.edisgo.network[mv_grid_id].network.pypsa @@ -1707,39 +1819,49 @@ def _get_mv_plot_res(ego, mv_grid_id): pypsa_plot = PyPSANetwork() pypsa_plot.buses = pypsa_network.buses.loc[pypsa_network.buses.v_nom >= 10] # filter buses of aggregated loads and generators - pypsa_plot.buses = pypsa_plot.buses[ - ~pypsa_plot.buses.index.str.contains("agg")] + pypsa_plot.buses = pypsa_plot.buses[~pypsa_plot.buses.index.str.contains("agg")] pypsa_plot.lines = pypsa_network.lines[ - pypsa_network.lines.bus0.isin(pypsa_plot.buses.index)][ - pypsa_network.lines.bus1.isin(pypsa_plot.buses.index)] + pypsa_network.lines.bus0.isin(pypsa_plot.buses.index) + ][pypsa_network.lines.bus1.isin(pypsa_plot.buses.index)] - grid_expansion_costs = ego.edisgo.network[mv_grid_id].network.results.grid_expansion_costs + grid_expansion_costs = ego.edisgo.network[ + mv_grid_id + ].network.results.grid_expansion_costs - bus_cost = pd.concat([pypsa_plot.buses, grid_expansion_costs], axis=1, - join_axes=[pypsa_plot.buses.index]) + bus_cost = pd.concat( + [pypsa_plot.buses, grid_expansion_costs], + axis=1, + join_axes=[pypsa_plot.buses.index], + ) costs_lv_stations = grid_expansion_costs[ - grid_expansion_costs.index.str.contains("LVStation")] - costs_lv_stations['station'] = \ - costs_lv_stations.reset_index()['index'].apply( - lambda _: '_'.join(_.split('_')[0:2])).values - costs_lv_stations = costs_lv_stations.groupby('station').sum() + grid_expansion_costs.index.str.contains("LVStation") + ] + costs_lv_stations["station"] = ( + costs_lv_stations.reset_index()["index"] + .apply(lambda _: "_".join(_.split("_")[0:2])) + .values + ) + costs_lv_stations = costs_lv_stations.groupby("station").sum() costs_mv_station = grid_expansion_costs[ - grid_expansion_costs.index.str.contains("MVStation")] - costs_mv_station['station'] = \ - costs_mv_station.reset_index()['index'].apply( - lambda _: '_'.join(_.split('_')[0:2])).values - costs_mv_station = costs_mv_station.groupby('station').sum() + grid_expansion_costs.index.str.contains("MVStation") + ] + costs_mv_station["station"] = ( + costs_mv_station.reset_index()["index"] + .apply(lambda _: "_".join(_.split("_")[0:2])) + .values + ) + costs_mv_station = costs_mv_station.groupby("station").sum() - costs_lv_stations_total = costs_lv_stations[['overnight_costs', - 'capital_cost']].sum() + costs_lv_stations_total = costs_lv_stations[ + ["overnight_costs", "capital_cost"] + ].sum() - costs_mv_station_total = costs_mv_station[['overnight_costs', - 'capital_cost']].sum() + costs_mv_station_total = costs_mv_station[["overnight_costs", "capital_cost"]].sum() costs_lv_stations_total = pd.DataFrame(costs_lv_stations_total) costs_mv_station_total = pd.DataFrame(costs_mv_station_total) logger.disabled = False - return costs_lv_stations_total, costs_mv_station_total + return costs_lv_stations_total, costs_mv_station_total diff --git a/ego/tools/results.py b/ego/tools/results.py index d41eec13..e0164191 100644 --- a/ego/tools/results.py +++ b/ego/tools/results.py @@ -23,17 +23,21 @@ # TODO - write results to database import io -import os import logging -logger = logging.getLogger('ego') +import os -if not 'READTHEDOCS' in os.environ: - import pandas as pd +logger = logging.getLogger("ego") + +if not "READTHEDOCS" in os.environ: import numpy as np + import pandas as pd + from ego.tools.economics import get_generator_investment -__copyright__ = "Flensburg University of Applied Sciences, Europa-Universität"\ +__copyright__ = ( + "Flensburg University of Applied Sciences, Europa-Universität" "Flensburg, Centre for Sustainable Energy Systems" +) __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolfbunke" @@ -62,44 +66,50 @@ def create_etrago_results(network, scn_name): # rename function etg = network etrago = pd.DataFrame() - etrago['p_nom'] = etg.generators.groupby('carrier')['p_nom'].sum() # in MW - etrago['p_nom_opt'] = etg.generators.groupby('carrier')[ - 'p_nom_opt'].sum() # in MW + etrago["p_nom"] = etg.generators.groupby("carrier")["p_nom"].sum() # in MW + etrago["p_nom_opt"] = etg.generators.groupby("carrier")["p_nom_opt"].sum() # in MW # power price - etrago['marginal_cost'] = etg.generators.groupby('carrier' - )['marginal_cost'].mean() + etrago["marginal_cost"] = etg.generators.groupby("carrier")["marginal_cost"].mean() # in in [EUR] # get power price by production MWh _t.p * marginal_cost - power_price = etg.generators_t.p[etg.generators[etg.generators. - control != 'Slack'] - .index] * etg.generators.\ - marginal_cost[etg.generators[etg.generators. - control != 'Slack'].index] # without Slack + power_price = ( + etg.generators_t.p[etg.generators[etg.generators.control != "Slack"].index] + * etg.generators.marginal_cost[ + etg.generators[etg.generators.control != "Slack"].index + ] + ) # without Slack - power_price = power_price.groupby( - etg.generators.carrier, axis=1).sum().sum() - etrago['power_price'] = power_price + power_price = power_price.groupby(etg.generators.carrier, axis=1).sum().sum() + etrago["power_price"] = power_price # use country code - p_by_carrier = pd.concat([etg.generators_t.p - [etg.generators[etg.generators.control != - 'Slack'].index], - etg.generators_t.p[etg.generators[ - etg. - generators.control == 'Slack'].index - ].iloc[:, 0]. - apply(lambda x: x if x > 0 else 0)], axis=1).\ - groupby(etg.generators.carrier, axis=1).sum() # in MWh - - etrago['p'] = p_by_carrier.sum() + p_by_carrier = ( + pd.concat( + [ + etg.generators_t.p[ + etg.generators[etg.generators.control != "Slack"].index + ], + etg.generators_t.p[ + etg.generators[etg.generators.control == "Slack"].index + ] + .iloc[:, 0] + .apply(lambda x: x if x > 0 else 0), + ], + axis=1, + ) + .groupby(etg.generators.carrier, axis=1) + .sum() + ) # in MWh + + etrago["p"] = p_by_carrier.sum() # add invetment result_invest = get_generator_investment(network, scn_name) - etrago = etrago.assign(investment_costs=result_invest['carrier_costs']) + etrago = etrago.assign(investment_costs=result_invest["carrier_costs"]) return etrago -if __name__ == '__main__': +if __name__ == "__main__": pass diff --git a/ego/tools/storages.py b/ego/tools/storages.py index b8323a95..c49509a0 100644 --- a/ego/tools/storages.py +++ b/ego/tools/storages.py @@ -21,17 +21,18 @@ """ import io -import os import logging -logger = logging.getLogger('ego') +import os -if not 'READTHEDOCS' in os.environ: - import pandas as pd +logger = logging.getLogger("ego") + +if not "READTHEDOCS" in os.environ: import numpy as np + import pandas as pd + from etrago.tools.utilities import geolocation_buses -__copyright__ = ("Europa-Universität Flensburg, " - "Centre for Sustainable Energy Systems") +__copyright__ = "Europa-Universität Flensburg, " "Centre for Sustainable Energy Systems" __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolf_bunke,maltesc" @@ -66,36 +67,56 @@ def etrago_storages(network): Sum of optimal installed power capacity """ if len(network.storage_units_t.p.sum()) > 0: - charge = network.storage_units_t.\ - p[network.storage_units_t.p[network. - storage_units[network.storage_units. - p_nom_opt > 0].index]. - values > 0.].groupby(network.storage_units. - carrier, axis=1).sum().sum() - - discharge = network.storage_units_t.p[network.storage_units_t. - p[network. - storage_units[ - network.storage_units. - p_nom_opt > 0]. - index].values < 0.].\ - groupby(network.storage_units.carrier, axis=1).sum().sum() - - count = network.storage_units.bus[network.storage_units.p_nom_opt > 0].\ - groupby(network.storage_units.carrier, axis=0).count() - - p_nom_sum = network.storage_units.p_nom.groupby(network.storage_units. - carrier, axis=0).sum() + charge = ( + network.storage_units_t.p[ + network.storage_units_t.p[ + network.storage_units[network.storage_units.p_nom_opt > 0].index + ].values + > 0.0 + ] + .groupby(network.storage_units.carrier, axis=1) + .sum() + .sum() + ) + + discharge = ( + network.storage_units_t.p[ + network.storage_units_t.p[ + network.storage_units[network.storage_units.p_nom_opt > 0].index + ].values + < 0.0 + ] + .groupby(network.storage_units.carrier, axis=1) + .sum() + .sum() + ) + + count = ( + network.storage_units.bus[network.storage_units.p_nom_opt > 0] + .groupby(network.storage_units.carrier, axis=0) + .count() + ) + + p_nom_sum = network.storage_units.p_nom.groupby( + network.storage_units.carrier, axis=0 + ).sum() p_nom_o_sum = network.storage_units.p_nom_opt.groupby( - network.storage_units. - carrier, axis=0).sum() + network.storage_units.carrier, axis=0 + ).sum() p_nom_o = p_nom_sum - p_nom_o_sum # Zubau - results = pd.concat([charge.rename('charge'), - discharge.rename('discharge'), - p_nom_sum, count.rename('total_units'), p_nom_o - .rename('extension'), ], axis=1, join='outer') + results = pd.concat( + [ + charge.rename("charge"), + discharge.rename("discharge"), + p_nom_sum, + count.rename("total_units"), + p_nom_o.rename("extension"), + ], + axis=1, + join="outer", + ) else: logger.info("No timeseries p for storages!") @@ -121,64 +142,67 @@ def etrago_storages_investment(network, json_file, session): """ # check spelling of storages and storage - logger.info(json_file['eTraGo']['extendable']) + logger.info(json_file["eTraGo"]["extendable"]) - stos = 'storage' + stos = "storage" # check settings for extendable - if stos not in json_file['eTraGo']['extendable']: - logger.info("The optimizition was not using parameter " - " 'extendable': storage" - "No storage expantion costs from etrago") + if stos not in json_file["eTraGo"]["extendable"]: + logger.info( + "The optimizition was not using parameter " + " 'extendable': storage" + "No storage expantion costs from etrago" + ) - if stos in json_file['eTraGo']['extendable']: + if stos in json_file["eTraGo"]["extendable"]: network = geolocation_buses(network, session) # get v_nom - _bus = pd.DataFrame(network.buses[['v_nom', 'country_code']]) + _bus = pd.DataFrame(network.buses[["v_nom", "country_code"]]) _bus.index.name = "name" _bus.reset_index(level=0, inplace=True) - _storage = network.storage_units[ - network.storage_units.p_nom_extendable == True] + _storage = network.storage_units[network.storage_units.p_nom_extendable == True] _storage.reset_index(level=0, inplace=True) # provide storage installation costs per voltage level - installed_storages = \ - pd.merge(_storage, _bus, left_on='bus', right_on='name') + installed_storages = pd.merge(_storage, _bus, left_on="bus", right_on="name") - installed_storages['investment_costs'] = (installed_storages. - capital_cost * - installed_storages.p_nom_opt) + installed_storages["investment_costs"] = ( + installed_storages.capital_cost * installed_storages.p_nom_opt + ) # add voltage_level - installed_storages['voltage_level'] = 'unknown' + installed_storages["voltage_level"] = "unknown" - ix_ehv = installed_storages[installed_storages['v_nom'] >= 380].index - installed_storages.set_value(ix_ehv, 'voltage_level', 'ehv') + ix_ehv = installed_storages[installed_storages["v_nom"] >= 380].index + installed_storages.set_value(ix_ehv, "voltage_level", "ehv") - ix_hv = installed_storages[(installed_storages['v_nom'] <= 220) & - (installed_storages['v_nom'] >= 110)].index - installed_storages.set_value(ix_hv, 'voltage_level', 'hv') + ix_hv = installed_storages[ + (installed_storages["v_nom"] <= 220) & (installed_storages["v_nom"] >= 110) + ].index + installed_storages.set_value(ix_hv, "voltage_level", "hv") # add country differentiation - installed_storages['differentiation'] = 'none' + installed_storages["differentiation"] = "none" for idx, val in installed_storages.iterrows(): - check = val['country_code'] + check = val["country_code"] if "DE" in check: - installed_storages['differentiation'][idx] = 'domestic' + installed_storages["differentiation"][idx] = "domestic" if "DE" not in check: - installed_storages['differentiation'][idx] = 'foreign' - - storages_investment = installed_storages[ - ['voltage_level', 'investment_costs', - 'differentiation']].groupby(['differentiation', - 'voltage_level'] - ).sum().reset_index() - - storages_investment = storages_investment.\ - rename(columns={'investment_costs': 'capital_cost'}) + installed_storages["differentiation"][idx] = "foreign" + + storages_investment = ( + installed_storages[["voltage_level", "investment_costs", "differentiation"]] + .groupby(["differentiation", "voltage_level"]) + .sum() + .reset_index() + ) + + storages_investment = storages_investment.rename( + columns={"investment_costs": "capital_cost"} + ) return storages_investment diff --git a/ego/tools/utilities.py b/ego/tools/utilities.py index f4030f5c..1a9f3112 100644 --- a/ego/tools/utilities.py +++ b/ego/tools/utilities.py @@ -20,25 +20,29 @@ """This module contains utility functions for the eGo application. """ import csv -import os -import pandas as pd import json -import csv -import sys import logging +import os +import sys + +import pandas as pd + logger = logging.getLogger(__name__) from time import localtime, strftime -if not 'READTHEDOCS' in os.environ: + +if not "READTHEDOCS" in os.environ: from egoio.db_tables import model_draft, grid from egoio.tools import db -from sqlalchemy.orm import sessionmaker -from sqlalchemy.orm import scoped_session -__copyright__ = ("Flensburg University of Applied Sciences, " - "Europa-Universität Flensburg, " - "Centre for Sustainable Energy Systems") +from sqlalchemy.orm import scoped_session, sessionmaker + +__copyright__ = ( + "Flensburg University of Applied Sciences, " + "Europa-Universität Flensburg, " + "Centre for Sustainable Energy Systems" +) __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolf_bunke" @@ -61,24 +65,23 @@ def define_logging(name): # add pypsa and other logger INFO to ego.log now = strftime("%Y-%m-%d_%H%M%S", localtime()) - log_dir = 'logs' + log_dir = "logs" if not os.path.exists(log_dir): os.makedirs(log_dir) # Logging - logging.basicConfig(stream=sys.stdout, - format='%(asctime)s %(message)s', - level=logging.INFO) + logging.basicConfig( + stream=sys.stdout, format="%(asctime)s %(message)s", level=logging.INFO + ) logger = logging.getLogger(name) formatter = logging.Formatter( - '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) -# logger = logging.FileHandler(log_name, mode='w') - fh = logging.FileHandler( - log_dir + '/' + name + '_' + now + '.log', mode='w') + # logger = logging.FileHandler(log_name, mode='w') + fh = logging.FileHandler(log_dir + "/" + name + "_" + now + ".log", mode="w") fh.setLevel(logging.INFO) fh.setFormatter(formatter) logger.addHandler(fh) @@ -86,7 +89,7 @@ def define_logging(name): return logger -def get_scenario_setting(jsonpath='scenario_setting.json'): +def get_scenario_setting(jsonpath="scenario_setting.json"): """Get and open json file with scenaio settings of eGo. The settings incluede eGo, eTraGo and eDisGo specific settings of arguments and parameters for a reproducible @@ -107,83 +110,86 @@ def get_scenario_setting(jsonpath='scenario_setting.json'): # add try ego/ logger.info("Your path is: {}".format(path)) - with open(path + '/' + jsonpath) as f: + with open(path + "/" + jsonpath) as f: json_file = json.load(f) # fix remove result_id - json_file['eGo'].update({'result_id': None}) + json_file["eGo"].update({"result_id": None}) # check settings - if (json_file['eGo']['eTraGo'] is False and json_file['eGo']['eDisGo'] - is False): - logger.warning("Something went wrong! \n" - "Please contoll your settings and restart. \n" - "Set at least eTraGo = true") + if json_file["eGo"]["eTraGo"] is False and json_file["eGo"]["eDisGo"] is False: + logger.warning( + "Something went wrong! \n" + "Please contoll your settings and restart. \n" + "Set at least eTraGo = true" + ) return - if (json_file['eGo']['eTraGo'] is None and json_file['eGo']['eDisGo'] - is None): - logger.warning("Something went wrong! \n" - "Please contoll your settings and restart. \n" - "Set at least eTraGo = true") + if json_file["eGo"]["eTraGo"] is None and json_file["eGo"]["eDisGo"] is None: + logger.warning( + "Something went wrong! \n" + "Please contoll your settings and restart. \n" + "Set at least eTraGo = true" + ) return - if json_file['eGo']['result_id'] and json_file['eGo']['csv_import_eTraGo']: + if json_file["eGo"]["result_id"] and json_file["eGo"]["csv_import_eTraGo"]: logger.warning( "You set a DB result_id and a csv import path! \n" - "Please remove on of this settings") + "Please remove on of this settings" + ) return # or ? json_file['eGo']['result_id'] = None - if json_file['eGo']['eTraGo'] is None and json_file['eGo']['eDisGo']: - logger.info( - "eDisGo needs eTraGo results. Please change your settings!\n") + if json_file["eGo"]["eTraGo"] is None and json_file["eGo"]["eDisGo"]: + logger.info("eDisGo needs eTraGo results. Please change your settings!\n") return - if json_file['eGo']['eTraGo'] is False and json_file['eGo']['eDisGo']: - logger.info( - "eDisGo needs eTraGo results. Please change your settings!\n") + if json_file["eGo"]["eTraGo"] is False and json_file["eGo"]["eDisGo"]: + logger.info("eDisGo needs eTraGo results. Please change your settings!\n") return - if (json_file['eGo']['result_id'] is None and - json_file['eGo']['csv_import_eTraGo'] is None): + if ( + json_file["eGo"]["result_id"] is None + and json_file["eGo"]["csv_import_eTraGo"] is None + ): logger.info( - "No data import from results is set \n" - "eGo runs by given settings") + "No data import from results is set \n" "eGo runs by given settings" + ) - if (json_file['eGo']['csv_import_eTraGo'] and - json_file['eGo']['csv_import_eDisGo']): - logger.info( - "eDisGo and eTraGo results will be imported from csv\n") + if json_file["eGo"]["csv_import_eTraGo"] and json_file["eGo"]["csv_import_eDisGo"]: + logger.info("eDisGo and eTraGo results will be imported from csv\n") - if json_file['eGo'].get('eTraGo') == True: + if json_file["eGo"].get("eTraGo") == True: - logger.info('Using and importing eTraGo settings') + logger.info("Using and importing eTraGo settings") # special case of SH and model_draft # TODO: check and maybe remove this part sh_scen = ["SH Status Quo", "SH NEP 2035", "SH eGo 100"] - if json_file['eTraGo'].get('scn_name') in sh_scen and json_file['eTraGo'].\ - get('gridversion') is not None: - json_file['eTraGo']['gridversion'] = None + if ( + json_file["eTraGo"].get("scn_name") in sh_scen + and json_file["eTraGo"].get("gridversion") is not None + ): + json_file["eTraGo"]["gridversion"] = None - if json_file['eTraGo'].get('extendable') == "['network', 'storages']": - json_file['eTraGo'].update({'extendable': ['network', 'storage']}) + if json_file["eTraGo"].get("extendable") == "['network', 'storages']": + json_file["eTraGo"].update({"extendable": ["network", "storage"]}) - if json_file['eTraGo'].get('extendable') == "['network', 'storage']": - json_file['eTraGo'].update({'extendable': ['network', 'storage']}) + if json_file["eTraGo"].get("extendable") == "['network', 'storage']": + json_file["eTraGo"].update({"extendable": ["network", "storage"]}) - if json_file['eTraGo'].get('extendable') == "['network']": - json_file['eTraGo'].update({'extendable': ['network']}) + if json_file["eTraGo"].get("extendable") == "['network']": + json_file["eTraGo"].update({"extendable": ["network"]}) - if json_file['eTraGo'].get('extendable') == "['storages']": - json_file['eTraGo'].update({'extendable': ['storage']}) + if json_file["eTraGo"].get("extendable") == "['storages']": + json_file["eTraGo"].update({"extendable": ["storage"]}) - if json_file['eTraGo'].get('extendable') == "['storage']": - json_file['eTraGo'].update({'extendable': ['storage']}) + if json_file["eTraGo"].get("extendable") == "['storage']": + json_file["eTraGo"].update({"extendable": ["storage"]}) - if json_file['eGo'].get('eDisGo') == True: - logger.info('Using and importing eDisGo settings') + if json_file["eGo"].get("eDisGo") == True: + logger.info("Using and importing eDisGo settings") return json_file @@ -194,15 +200,15 @@ def fix_leading_separator(csv_file, **kwargs): separator in its header, this field is deleted. If this is done the second field of every row is removed, too. """ - with open(csv_file, 'r') as f: + with open(csv_file, "r") as f: lines = csv.reader(f, **kwargs) if not lines: - raise Exception('File %s contained no data' % csv_file) + raise Exception("File %s contained no data" % csv_file) first_line = next(lines) - if first_line[0] == '': + if first_line[0] == "": path, fname = os.path.split(csv_file) - tmp_file = os.path.join(path, 'tmp_' + fname) - with open(tmp_file, 'w+') as out: + tmp_file = os.path.join(path, "tmp_" + fname) + with open(tmp_file, "w+") as out: writer = csv.writer(out, **kwargs) writer.writerow(first_line[1:]) for line in lines: @@ -213,7 +219,7 @@ def fix_leading_separator(csv_file, **kwargs): def get_time_steps(json_file): - """ Get time step of calculation by scenario settings. + """Get time step of calculation by scenario settings. Parameters ---------- @@ -226,16 +232,15 @@ def get_time_steps(json_file): Number of timesteps of the calculation. """ - end = json_file['eTraGo'].get('end_snapshot') - start = json_file['eTraGo'].get('start_snapshot') + end = json_file["eTraGo"].get("end_snapshot") + start = json_file["eTraGo"].get("start_snapshot") time_step = end - start return time_step def open_oedb_session(ego): - """ - """ + """ """ _db_section = ego.json_file["eTraGo"]["db"] conn = db.connection(section=_db_section) session_factory = sessionmaker(bind=conn) diff --git a/pytest.ini b/pytest.ini index 51f7681e..174a20ae 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,4 +3,4 @@ log_cli = True log_level = INFO testpaths = - tests \ No newline at end of file + tests diff --git a/setup.py b/setup.py index f54c12d5..8965806f 100644 --- a/setup.py +++ b/setup.py @@ -1,69 +1,76 @@ # -*- coding: utf-8 -*- import os -from setuptools import find_packages, setup + from pip._internal.req import parse_requirements +from setuptools import find_packages, setup -__copyright__ = ("Flensburg University of Applied Sciences, " - "Europa-Universität Flensburg, " - "Centre for Sustainable Energy Systems") +__copyright__ = ( + "Flensburg University of Applied Sciences, " + "Europa-Universität Flensburg, " + "Centre for Sustainable Energy Systems" +) __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolf_bunke, maltesc" + def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() - + setup( - name='eGo', - version='0.3.4', - author='wolfbunke, maltesc', - author_email='wolf-dieter.bunke@uni-flensburg.de', - description=("A cross-grid-level electricity grid and storage " - "optimization tool "), - long_description= read('README.rst'), - url='https://github.com/openego/eGo', + name="eGo", + version="0.3.4", + author="wolfbunke, maltesc", + author_email="wolf-dieter.bunke@uni-flensburg.de", + description=( + "A cross-grid-level electricity grid and storage " "optimization tool " + ), + long_description=read("README.rst"), + url="https://github.com/openego/eGo", license="GNU Affero General Public License Version 3 (AGPL-3.0)", packages=find_packages(), - package_dir={'ego': 'ego'}, + package_dir={"ego": "ego"}, include_package_data=True, - install_requires=['egoio == 0.4.5', - 'ding0 == v0.1.9', - 'pycallgraph', - 'eDisGo == v0.0.9', - 'eTraGo == 0.7.1', - 'scikit-learn == 0.19.0', - 'pandas ==0.20.3', - 'pypsa==0.11.0fork', - 'sqlalchemy == 1.2.0', - 'geoalchemy2 >= 0.3.0, <=0.4.0', - 'tsam==0.9.9', - 'geopandas', - 'matplotlib == 3.0.0', - 'Rtree', - 'descartes', - 'pyproj', - 'plotly==2.2.3', - 'shapely', - 'multiprocess', - 'folium', - 'oedialect' - ], + install_requires=[ + "egoio == 0.4.5", + "ding0 == v0.1.9", + "pycallgraph", + "eDisGo == v0.0.9", + "eTraGo == 0.7.1", + "scikit-learn == 0.19.0", + "pandas ==0.20.3", + "pypsa==0.11.0fork", + "sqlalchemy == 1.2.0", + "geoalchemy2 >= 0.3.0, <=0.4.0", + "tsam==0.9.9", + "geopandas", + "matplotlib == 3.0.0", + "Rtree", + "descartes", + "pyproj", + "plotly==2.2.3", + "shapely", + "multiprocess", + "folium", + "oedialect", + ], dependency_links=[ - ('git+https://git@github.com/openego/PyPSA.git' - '@master#egg=pypsa-0.11.0fork')], + ("git+https://git@github.com/openego/PyPSA.git" "@master#egg=pypsa-0.11.0fork") + ], extras_require={ - 'doc': [ - 'sphinx >= 1.4', - 'sphinx_rtd_theme', - 'sphinxcontrib-httpdomain', - 'numpydoc == 0.7.0', - 'aiohttp_jinja2', - 'sphinx-jsondomain']}, + "doc": [ + "sphinx >= 1.4", + "sphinx_rtd_theme", + "sphinxcontrib-httpdomain", + "numpydoc == 0.7.0", + "aiohttp_jinja2", + "sphinx-jsondomain", + ] + }, package_data={ - 'ego': [os.path.join('tools', '*.csv')], - 'ego': [os.path.join('tools', '*.json')], - 'ego': [os.path.join('', '*.json')], - 'ego.data': ['*.csv'] - } - ) - + "ego": [os.path.join("tools", "*.csv")], + "ego": [os.path.join("tools", "*.json")], + "ego": [os.path.join("", "*.json")], + "ego.data": ["*.csv"], + }, +) diff --git a/tests/data/create_test_grid.ipynb b/tests/data/create_test_grid.ipynb index 282707f7..063b8c91 100644 --- a/tests/data/create_test_grid.ipynb +++ b/tests/data/create_test_grid.ipynb @@ -2049,4 +2049,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/tests/data/etrago_test_network_1/generators.csv b/tests/data/etrago_test_network_1/generators.csv index a07d4562..2e943126 100644 --- a/tests/data/etrago_test_network_1/generators.csv +++ b/tests/data/etrago_test_network_1/generators.csv @@ -28,4 +28,4 @@ name,bus,carrier,p_nom,p_nom_opt 4 geo_thermal,4,geo_thermal,1.0,1.0 10 geo_thermal,10,geo_thermal,10.0,10.0 0 junk,0,junk,100.0,100.0 -10 junk,10,junk,100.0,100.0 \ No newline at end of file +10 junk,10,junk,100.0,100.0 diff --git a/tests/data/etrago_test_network_1/snapshots.csv b/tests/data/etrago_test_network_1/snapshots.csv index 07542072..583437ce 100644 --- a/tests/data/etrago_test_network_1/snapshots.csv +++ b/tests/data/etrago_test_network_1/snapshots.csv @@ -1,4 +1,4 @@ ,snapshot 0,2011-01-01 00:00:00 1,2011-01-01 12:00:00 -2,2011-01-02 00:00:00 \ No newline at end of file +2,2011-01-02 00:00:00 diff --git a/tests/data/etrago_test_network_1/storage_units.csv b/tests/data/etrago_test_network_1/storage_units.csv index b71d12c9..f0c5c016 100644 --- a/tests/data/etrago_test_network_1/storage_units.csv +++ b/tests/data/etrago_test_network_1/storage_units.csv @@ -1,4 +1,4 @@ name,bus,carrier,p_nom_opt,p_nom_extendable,max_hours 0 battery,0,battery,1.0,True,10.0 10 battery,10,battery,10.0,True,10.0 -10 junk,10,junk,10.0,True,10.0 \ No newline at end of file +10 junk,10,junk,10.0,True,10.0 diff --git a/tests/data/etrago_test_network_1/stores.csv b/tests/data/etrago_test_network_1/stores.csv index 9c392fe6..6511fbff 100644 --- a/tests/data/etrago_test_network_1/stores.csv +++ b/tests/data/etrago_test_network_1/stores.csv @@ -2,4 +2,4 @@ name,bus,carrier,e_nom_opt 5 central_heat_store,5,central_heat_store,1 10 central_heat_store,10,central_heat_store,10 7 rural_heat_store,7,rural_heat_store,1 -10 rural_heat_store,10,rural_heat_store,10 \ No newline at end of file +10 rural_heat_store,10,rural_heat_store,10 diff --git a/tests/tools/test_interface.py b/tests/tools/test_interface.py index cc4c4659..873c4f12 100644 --- a/tests/tools/test_interface.py +++ b/tests/tools/test_interface.py @@ -1,13 +1,13 @@ +import logging +import os import random +import pandas as pd import pytest -from pypsa import Network as PyPSANetwork -from ego.tools.interface import ETraGoMinimalData, get_etrago_results_per_bus +from pypsa import Network as PyPSANetwork -import pandas as pd -import os -import logging +from ego.tools.interface import ETraGoMinimalData, get_etrago_results_per_bus logger = logging.getLogger(__name__) From e4c548358aa6504ae6ef9222fe31828621f57be4 Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Fri, 23 Sep 2022 10:30:11 +0200 Subject: [PATCH 025/126] minor fix in readme --- README.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 5b27c5eb..5931403a 100644 --- a/README.rst +++ b/README.rst @@ -46,10 +46,12 @@ Create a virtualenvironment and activate it: .. code-block:: - $ virtualenv venv --clear -p python3.5 + $ virtualenv venv --clear -p python3.8 $ source venv/bin/activate - $ cd venv - $ pip3 install -e git+https://github.com/openego/eGo@dev#egg=eGo --process-dependency-links + $ cd path/to/eGo + $ python -m pip install -e .[full] + $ pre-commit install # install pre-commit hooks + ------- License From 2d71813da4a4340418b8a78e901638843db1977a Mon Sep 17 00:00:00 2001 From: Malte Jahn Date: Thu, 16 Feb 2023 08:53:04 +0100 Subject: [PATCH 026/126] Refactor interface.py - Put code in functions for better readability --- ego/examples/edisgo_mwe.py | 13 +- ego/tools/interface.py | 738 ++++++++++++++++++++----------------- 2 files changed, 401 insertions(+), 350 deletions(-) diff --git a/ego/examples/edisgo_mwe.py b/ego/examples/edisgo_mwe.py index 7e0a3fb7..8f028cfc 100644 --- a/ego/examples/edisgo_mwe.py +++ b/ego/examples/edisgo_mwe.py @@ -5,15 +5,22 @@ from ego.tools.edisgo_integration import EDisGoNetworks from ego.tools.utilities import get_scenario_setting -base_path = "/home/birgit/virtualenvs/eGo_interface_development/git_repos" +base_path = os.path.join(os.path.expanduser("~"), "git-repos", "data") # eTraGo network is not yet disaggregated -etrago_network = PyPSANetwork(os.path.join(base_path, "data/eTraGo_results")) +etrago_network = PyPSANetwork( + os.path.join(base_path, "etrago_results/disaggregated_network") +) # manually overwrite bus ID to have busses in the chosen grid -etrago_network.generators.loc[etrago_network.generators.bus == "16", "bus"] = "26533" +# etrago_network.generators.loc[etrago_network.generators.bus == "16", "bus"] = "26533" json_file = get_scenario_setting() edisgo_networks = EDisGoNetworks(json_file=json_file, etrago_network=etrago_network) + +etrago_network.generators.carrier.unique().tolist() +etrago_network.links.carrier.unique().tolist() +etrago_network.storage_units.carrier.unique().tolist() +etrago_network.stores.carrier.unique().tolist() print("x") diff --git a/ego/tools/interface.py b/ego/tools/interface.py index 29bd40f2..5155d8d7 100644 --- a/ego/tools/interface.py +++ b/ego/tools/interface.py @@ -34,7 +34,7 @@ import pandas as pd -if not "READTHEDOCS" in os.environ: +if "READTHEDOCS" not in os.environ: from egoio.db_tables import model_draft from egoio.db_tables import supply @@ -356,405 +356,449 @@ def get_etrago_results_per_bus( (etrago_network.links["bus0"] == str(bus_id)) | (etrago_network.links["bus1"] == str(bus_id)) ] - t1 = time.perf_counter() - performance.update({"General Data Processing": t1 - t0}) - # Dispatchable generators - dispatchable_generators_df_p = pd.DataFrame(index=timeseries_index) - if pf_post_lopf: + def dispatchable_generators(): + # Dispatchable generators + dispatchable_generators_df_p = pd.DataFrame(index=timeseries_index) dispatchable_generators_df_q = pd.DataFrame(index=timeseries_index) - dispatchable_generators = [ - "biomass", - "central_biomass_CHP", - "industrial_biomass_CHP", - "run_of_river", - "gas", - "other_non_renewable", - "reservoir", - "oil", - "other_renewable", - "coal", - "nuclear", - "lignite", - "CH4", - "central_biomass_CHP_heat", - ] - dispatchable_generators_df = generators_df[ - generators_df["carrier"].isin(dispatchable_generators) - ] - if not dispatchable_generators_df.empty: - # technology_dict = { - # "biomass": ["biomass", "central_biomass_CHP"], - # "run_of_river": ["run_of_river"], - # "gas": ["gas"], - # "other_non_renewable": ["other_non_renewable"], - # "reservoir": ["reservoir"], - # } - # for key, item in technology_dict.items(): - # for carrier in item: - # dispatchable_generators_df.loc[dispatchable_generators_df["carrier"] == carrier, "carrier"] = key - - for carrier in dispatchable_generators_df["carrier"].unique(): - p_nom = dispatchable_generators_df.loc[ - dispatchable_generators_df["carrier"] == carrier, "p_nom" - ].sum() - columns_to_aggregate = dispatchable_generators_df[ - dispatchable_generators_df["carrier"] == carrier - ].index - - dispatchable_generators_df_p[carrier] = ( - etrago_network.generators_t["p"][columns_to_aggregate].sum( - axis="columns" - ) - / p_nom - ) - if pf_post_lopf: - dispatchable_generators_df_q[carrier] = ( - etrago_network.generators_t["q"][columns_to_aggregate].sum( + dispatchable_generators = [ + # "CH4", + # "CH4_NG", + # "CH4_biogas", + "biomass", + "central_biomass_CHP", + "central_biomass_CHP_heat", + # "coal", + # "geo_thermal", + "industrial_biomass_CHP", + # "lignite", + # "nuclear", + # "oil", + "others", + "reservoir", + "run_of_river", + # "solar", + # "solar_rooftop", + # "solar_thermal_collector", + # "wind_offshore", + # "wind_onshore", + ] + dispatchable_generators_df = generators_df[ + generators_df["carrier"].isin(dispatchable_generators) + ] + if not dispatchable_generators_df.empty: + for carrier in dispatchable_generators_df["carrier"].unique(): + p_nom = dispatchable_generators_df.loc[ + dispatchable_generators_df["carrier"] == carrier, "p_nom" + ].sum() + columns_to_aggregate = dispatchable_generators_df[ + dispatchable_generators_df["carrier"] == carrier + ].index + + dispatchable_generators_df_p[carrier] = ( + etrago_network.generators_t["p"][columns_to_aggregate].sum( axis="columns" ) / p_nom ) + if pf_post_lopf: + dispatchable_generators_df_q[carrier] = ( + etrago_network.generators_t["q"][columns_to_aggregate].sum( + axis="columns" + ) + / p_nom + ) + else: + dispatchable_generators_df_q[carrier] = ( + etrago_network.generators_t["q"][columns_to_aggregate].sum( + axis="columns" + ) + / p_nom + ) - # Add CHP to conventional generators - chp_df = links_df[links_df["carrier"] == "central_gas_CHP"] - if not chp_df.empty: - p_nom = chp_df["p_nom_opt"].sum() - dispatchable_generators_df_p["central_gas_CHP"] = ( - etrago_network.links_t["p1"][chp_df.index].sum(axis="columns") / p_nom - ) - if pf_post_lopf: - dispatchable_generators_df_q["central_gas_CHP"] = ( - 0 * dispatchable_generators_df_p["central_gas_CHP"] + # Add CHP to conventional generators + chp_df = links_df[links_df["carrier"] == "central_gas_CHP"] + if not chp_df.empty: + p_nom = chp_df["p_nom"].sum() + dispatchable_generators_df_p["central_gas_CHP"] = ( + etrago_network.links_t["p1"][chp_df.index].sum(axis="columns") / p_nom ) + if not pf_post_lopf: + dispatchable_generators_df_q["central_gas_CHP"] = ( + 0 * dispatchable_generators_df_p["central_gas_CHP"] + ) + else: + dispatchable_generators_df_q["central_gas_CHP"] = ( + 0 * dispatchable_generators_df_p["central_gas_CHP"] + ) - etrago_results_per_bus[ - "dispatchable_generators_active_power" - ] = dispatchable_generators_df_p - if pf_post_lopf: etrago_results_per_bus[ - "dispatchable_generators_reactive_power" - ] = dispatchable_generators_df_q - - t2 = time.perf_counter() - performance.update({"Dispatchable generators": t2 - t1}) + "dispatchable_generators_active_power" + ] = dispatchable_generators_df_p + if pf_post_lopf: + etrago_results_per_bus[ + "dispatchable_generators_reactive_power" + ] = dispatchable_generators_df_q + + def renewable_generators(): + # Renewables + weather_dependent_generators = [ + # "CH4", + # "CH4_NG", + # "CH4_biogas", + # "biomass", + # "central_biomass_CHP", + # "central_biomass_CHP_heat", + # "coal", + # "geo_thermal", + # "industrial_biomass_CHP", + # "lignite", + # "nuclear", + # "oil", + # "others", + # "reservoir", + # "run_of_river", + "solar", + "solar_rooftop", + # "solar_thermal_collector", + # "wind_offshore", + "wind_onshore", + ] + weather_dependent_generators_df = generators_df[ + generators_df.carrier.isin(weather_dependent_generators) + ] + if not weather_dependent_generators_df.empty: + for generator_index in weather_dependent_generators_df.index: + weather_id = get_weather_id_for_generator( + grid_version, session, generator_index, scn_name + ) + weather_dependent_generators_df.loc[generator_index, "w_id"] = str( + weather_id + ) - # Renewables - weather_dependent_generators = [ - "solar", - "solar_rooftop", - "wind_onshore", - ] - weather_dependent_generators_df = generators_df[ - generators_df.carrier.isin(weather_dependent_generators) - ] - if not weather_dependent_generators_df.empty: - for generator_index in weather_dependent_generators_df.index: - weather_id = get_weather_id_for_generator( - grid_version, session, generator_index, scn_name - ) - weather_dependent_generators_df.loc[generator_index, "w_id"] = str( - weather_id + technology_dict = { + "solar": ["solar", "solar_rooftop"], + } + for key, item in technology_dict.items(): + for carrier in item: + weather_dependent_generators_df.loc[ + weather_dependent_generators_df["carrier"] == carrier, "carrier" + ] = key + + # Aggregation of p_nom + aggregated_weather_dependent_generators_df = ( + weather_dependent_generators_df.groupby(["carrier", "w_id"]) + .agg({"p_nom": "sum"}) + .reset_index() ) - technology_dict = { - "solar": ["solar", "solar_rooftop"], - } - for key, item in technology_dict.items(): - for carrier in item: - weather_dependent_generators_df.loc[ - weather_dependent_generators_df["carrier"] == carrier, "carrier" - ] = key - - # Aggregation of p_nom - aggregated_weather_dependent_generators_df = ( - weather_dependent_generators_df.groupby(["carrier", "w_id"]) - .agg({"p_nom": "sum"}) - .reset_index() - ) - - # Dispatch and Curtailment - weather_dependent_generators_df_potential_p = pd.DataFrame( - 0.0, - index=timeseries_index, - columns=aggregated_weather_dependent_generators_df.index, - ) - weather_dependent_generators_df_dispatch_p = pd.DataFrame( - 0.0, - index=timeseries_index, - columns=aggregated_weather_dependent_generators_df.index, - ) - if pf_post_lopf: - weather_dependent_generators_df_dispatch_q = pd.DataFrame( + # Dispatch and Curtailment + weather_dependent_generators_df_potential_p = pd.DataFrame( + 0.0, + index=timeseries_index, + columns=aggregated_weather_dependent_generators_df.index, + ) + weather_dependent_generators_df_dispatch_p = pd.DataFrame( 0.0, index=timeseries_index, columns=aggregated_weather_dependent_generators_df.index, ) + if pf_post_lopf: + weather_dependent_generators_df_dispatch_q = pd.DataFrame( + 0.0, + index=timeseries_index, + columns=aggregated_weather_dependent_generators_df.index, + ) - for index, carrier, w_id, p_nom in weather_dependent_generators_df[ - ["carrier", "w_id", "p_nom"] - ].itertuples(): - aggregated_idx = aggregated_weather_dependent_generators_df[ - (aggregated_weather_dependent_generators_df["carrier"] == carrier) - & (aggregated_weather_dependent_generators_df["w_id"] == w_id) - ].index.values[0] - p_nom_aggregated = aggregated_weather_dependent_generators_df.loc[ - aggregated_idx, "p_nom" - ] + for index, carrier, w_id, p_nom in weather_dependent_generators_df[ + ["carrier", "w_id", "p_nom"] + ].itertuples(): + aggregated_idx = aggregated_weather_dependent_generators_df[ + (aggregated_weather_dependent_generators_df["carrier"] == carrier) + & (aggregated_weather_dependent_generators_df["w_id"] == w_id) + ].index.values[0] + p_nom_aggregated = aggregated_weather_dependent_generators_df.loc[ + aggregated_idx, "p_nom" + ] + + p_series = etrago_network.generators_t["p"][index] + p_normed_series = p_series / p_nom_aggregated + + p_max_pu_series = etrago_network.generators_t["p_max_pu"][index] + p_max_pu_normed_series = p_max_pu_series * p_nom / p_nom_aggregated + + if pf_post_lopf: + if max_cos_phi_renewable: + logger.info( + "Applying Q limit (max cos(phi)={})".format( + max_cos_phi_renewable + ) + ) - p_series = etrago_network.generators_t["p"][index] - p_normed_series = p_series / p_nom_aggregated + phi = math.acos(max_cos_phi_renewable) - p_max_pu_series = etrago_network.generators_t["p_max_pu"][index] - p_max_pu_normed_series = p_max_pu_series * p_nom / p_nom_aggregated + q_series = pd.Series(0, index=timeseries_index) - if pf_post_lopf: - if max_cos_phi_renewable: - logger.info( - "Applying Q limit (max cos(phi)={})".format( - max_cos_phi_renewable - ) - ) + for timestep in timeseries_index: + p = etrago_network.generators_t["p"].loc[timestep, index] + q = etrago_network.generators_t["q"].loc[timestep, index] - phi = math.acos(max_cos_phi_renewable) + q_max = p * math.tan(phi) + q_min = -p * math.tan(phi) - q_series = pd.Series(0, index=timeseries_index) + if q > q_max: + q = q_max + elif q < q_min: + q = q_min - for timestep in timeseries_index: - p = etrago_network.generators_t["p"].loc[timestep, index] - q = etrago_network.generators_t["q"].loc[timestep, index] + q_series[timestep] = q + else: + q_series = etrago_network.generators_t["q"][index] - q_max = p * math.tan(phi) - q_min = -p * math.tan(phi) + q_normed_series = q_series / p_nom_aggregated - if q > q_max: - q = q_max - elif q < q_min: - q = q_min + weather_dependent_generators_df_dispatch_p[aggregated_idx] = ( + weather_dependent_generators_df_dispatch_p[aggregated_idx] + + p_normed_series + ) + weather_dependent_generators_df_potential_p[aggregated_idx] = ( + weather_dependent_generators_df_potential_p[aggregated_idx] + + p_max_pu_normed_series + ) + if pf_post_lopf: + weather_dependent_generators_df_dispatch_q[aggregated_idx] = ( + weather_dependent_generators_df_dispatch_q[aggregated_idx] + + q_normed_series + ) - q_series[timestep] = q - else: - q_series = etrago_network.generators_t["q"][index] + weather_dependent_generators_df_curtailment_p = ( + weather_dependent_generators_df_potential_p + - weather_dependent_generators_df_dispatch_p + ) - q_normed_series = q_series / p_nom_aggregated + # Renaming columns + new_columns = [ + ( + aggregated_weather_dependent_generators_df.at[column, "carrier"], + aggregated_weather_dependent_generators_df.at[column, "w_id"], + ) + for column in weather_dependent_generators_df_potential_p.columns + ] + new_columns = pd.MultiIndex.from_tuples(new_columns) + weather_dependent_generators_df_potential_p.columns = new_columns + weather_dependent_generators_df_dispatch_p.columns = new_columns + weather_dependent_generators_df_curtailment_p.columns = new_columns + if pf_post_lopf: + weather_dependent_generators_df_dispatch_q.columns = new_columns - weather_dependent_generators_df_dispatch_p[aggregated_idx] = ( - weather_dependent_generators_df_dispatch_p[aggregated_idx] - + p_normed_series - ) - weather_dependent_generators_df_potential_p[aggregated_idx] = ( - weather_dependent_generators_df_potential_p[aggregated_idx] - + p_max_pu_normed_series - ) + etrago_results_per_bus[ + "renewables_potential" + ] = weather_dependent_generators_df_potential_p + etrago_results_per_bus[ + "renewables_curtailment" + ] = weather_dependent_generators_df_curtailment_p if pf_post_lopf: - weather_dependent_generators_df_dispatch_q[aggregated_idx] = ( - weather_dependent_generators_df_dispatch_q[aggregated_idx] - + q_normed_series - ) + etrago_results_per_bus[ + "renewables_dispatch_reactive_power" + ] = weather_dependent_generators_df_dispatch_q + + def storages(): + # Storage + # Filter batteries + min_extended = 0 + logger.info(f"Minimum storage of {min_extended} MW") + + storages_df = etrago_network.storage_units.loc[ + (etrago_network.storage_units["carrier"] == "battery") + & (etrago_network.storage_units["bus"] == str(bus_id)) + & (etrago_network.storage_units["p_nom_extendable"]) + & (etrago_network.storage_units["p_nom_opt"] > min_extended) + # & (etrago_network.storage_units["max_hours"] <= 20.0) + ] + if not storages_df.empty: + # Capactiy + storages_df_capacity = ( + storages_df["p_nom_opt"] * storages_df["max_hours"] + ).values[0] + + storages_df_p = etrago_network.storage_units_t["p"][storages_df.index] + storages_df_p.columns = storages_df["carrier"] + if pf_post_lopf: + storages_df_q = etrago_network.storage_units_t["q"][storages_df.index] + storages_df_q.columns = storages_df["carrier"] - weather_dependent_generators_df_curtailment_p = ( - weather_dependent_generators_df_potential_p - - weather_dependent_generators_df_dispatch_p - ) + etrago_results_per_bus["storage_units_capacity"] = storages_df_capacity + etrago_results_per_bus["storage_units_active_power"] = storages_df_p + if pf_post_lopf: + etrago_results_per_bus["storage_units_reactive_power"] = storages_df_q - # Renaming columns - new_columns = [ - ( - aggregated_weather_dependent_generators_df.at[column, "carrier"], - aggregated_weather_dependent_generators_df.at[column, "w_id"], - ) - for column in weather_dependent_generators_df_potential_p.columns + def dsm(): + # DSM + dsm_df = links_df.loc[ + (links_df["carrier"] == "dsm") & (links_df["bus0"] == str(bus_id)) ] - new_columns = pd.MultiIndex.from_tuples(new_columns) - weather_dependent_generators_df_potential_p.columns = new_columns - weather_dependent_generators_df_dispatch_p.columns = new_columns - weather_dependent_generators_df_curtailment_p.columns = new_columns - if pf_post_lopf: - weather_dependent_generators_df_dispatch_q.columns = new_columns - - etrago_results_per_bus[ - "renewables_potential" - ] = weather_dependent_generators_df_potential_p - etrago_results_per_bus[ - "renewables_curtailment" - ] = weather_dependent_generators_df_curtailment_p - if pf_post_lopf: + if not dsm_df.empty: + if dsm_df.shape[0] > 1: + raise ValueError(f"More than one dsm link at bus {bus_id}") + dsm_df_p = etrago_network.links_t["p0"][dsm_df.index] + dsm_df_p.columns = dsm_df["carrier"] + if pf_post_lopf: + dsm_df_q = 0 * dsm_df_p + + etrago_results_per_bus["dsm_active_power"] = dsm_df_p + if pf_post_lopf: + etrago_results_per_bus["dsm_reactive_power"] = dsm_df_q + + def central_heat(): + # Heat + # Central heat + # Power2Heat + central_heat_carriers = ["central_heat_pump", "central_resistive_heater"] + central_heat_df = links_df.loc[ + links_df["carrier"].isin(central_heat_carriers) + & (links_df["bus0"] == str(bus_id)) + ] + if not central_heat_df.empty: + # Timeseries + central_heat_df_p = etrago_network.links_t["p0"][central_heat_df.index] + central_heat_df_p.columns = central_heat_df["carrier"] + if pf_post_lopf: + central_heat_df_q = 0 * central_heat_df_p + + etrago_results_per_bus["heat_central_active_power"] = central_heat_df_p + if pf_post_lopf: + etrago_results_per_bus[ + "heat_central_reactive_power" + ] = central_heat_df_q + + # Stores + central_heat_bus = central_heat_df["bus1"].values[0] + central_heat_store_bus = etrago_network.links.loc[ + etrago_network.links["bus0"] == central_heat_bus, "bus1" + ].values[0] + central_heat_store_capacity = etrago_network.stores.loc[ + (etrago_network.stores["carrier"] == "central_heat_store") + & (etrago_network.stores["bus"] == central_heat_store_bus), + "e_nom_opt", + ].values[0] + etrago_results_per_bus[ - "renewables_dispatch_reactive_power" - ] = weather_dependent_generators_df_dispatch_q + "thermal_storage_central_capacity" + ] = central_heat_store_capacity + # Feedin + geothermal_feedin_df = etrago_network.generators[ + (etrago_network.generators["carrier"] == "geo_thermal") + & (etrago_network.generators["bus"] == central_heat_bus) + ] + geothermal_feedin_df_p = etrago_network.generators_t["p"][ + geothermal_feedin_df.index + ] + geothermal_feedin_df_p.columns = geothermal_feedin_df["carrier"] + etrago_results_per_bus[ + "geothermal_energy_feedin_district_heating" + ] = geothermal_feedin_df_p + + solarthermal_feedin_df = etrago_network.generators[ + (etrago_network.generators["carrier"] == "solar_thermal_collector") + & (etrago_network.generators["bus"] == central_heat_bus) + ] + solarthermal_feedin_df_p = etrago_network.generators_t["p"][ + solarthermal_feedin_df.index + ] + solarthermal_feedin_df_p.columns = solarthermal_feedin_df["carrier"] + etrago_results_per_bus[ + "solarthermal_energy_feedin_district_heating" + ] = solarthermal_feedin_df_p + + def rural_heat(): + # Rural heat + # Power2Heat + rural_heat_carriers = ["rural_heat_pump"] + rural_heat_df = links_df.loc[ + links_df["carrier"].isin(rural_heat_carriers) + & (links_df["bus0"] == str(bus_id)) + ] + if not rural_heat_df.empty: + # Timeseries + rural_heat_df_p = etrago_network.links_t["p0"][rural_heat_df.index] + rural_heat_df_p.columns = rural_heat_df["carrier"] + if pf_post_lopf: + rural_heat_df_q = 0 * rural_heat_df_p + + etrago_results_per_bus["heat_pump_rural_active_power"] = rural_heat_df_p + if pf_post_lopf: + etrago_results_per_bus[ + "heat_pump_rural_reactive_power" + ] = rural_heat_df_q + + # Stores + rural_heat_bus = rural_heat_df["bus1"].values[0] + rural_heat_store_bus = etrago_network.links.loc[ + etrago_network.links["bus0"] == rural_heat_bus, "bus1" + ].values[0] + rural_heat_store_capacity = etrago_network.stores.loc[ + (etrago_network.stores["carrier"] == "rural_heat_store") + & (etrago_network.stores["bus"] == rural_heat_store_bus), + "e_nom_opt", + ].values[0] + + etrago_results_per_bus[ + "thermal_storage_rural_capacity" + ] = rural_heat_store_capacity + + def bev_charger(): + # BEV charger + bev_charger_df = links_df.loc[ + (links_df["carrier"] == "BEV charger") & (links_df["bus0"] == str(bus_id)) + ] + if not bev_charger_df.empty: + if bev_charger_df.shape[0] > 1: + raise ValueError(f"More than one dsm link at bus {bus_id}") + + bev_charger_df_p = etrago_network.links_t["p0"][bev_charger_df.index] + bev_charger_df_p.columns = bev_charger_df["carrier"] + if pf_post_lopf: + bev_charger_df_q = 0 * bev_charger_df_p + + etrago_results_per_bus["electromobility_active_power"] = bev_charger_df_p + if pf_post_lopf: + etrago_results_per_bus[ + "electromobility_reactive_power" + ] = bev_charger_df_q + + t1 = time.perf_counter() + performance.update({"General Data Processing": t1 - t0}) + + dispatchable_generators() + t2 = time.perf_counter() + performance.update({"Dispatchable generators": t2 - t1}) + + renewable_generators() t3 = time.perf_counter() performance.update({"Renewable Dispatch and Curt.": t3 - t2}) - # Storage - # Filter batteries - min_extended = 0 - logger.info(f"Minimum storage of {min_extended} MW") - - storages_df = etrago_network.storage_units.loc[ - (etrago_network.storage_units["carrier"] == "battery") - & (etrago_network.storage_units["bus"] == str(bus_id)) - & (etrago_network.storage_units["p_nom_extendable"] == True) - & (etrago_network.storage_units["p_nom_opt"] > min_extended) - # & (etrago_network.storage_units["max_hours"] <= 20.0) - ] - if not storages_df.empty: - # Capactiy - storages_df_capacity = ( - storages_df["p_nom_opt"] * storages_df["max_hours"] - ).values[0] - - storages_df_p = etrago_network.storage_units_t["p"][storages_df.index] - storages_df_p.columns = storages_df["carrier"] - if pf_post_lopf: - storages_df_q = etrago_network.storage_units_t["q"][storages_df.index] - storages_df_q.columns = storages_df["carrier"] - - etrago_results_per_bus["storage_units_capacity"] = storages_df_capacity - etrago_results_per_bus["storage_units_active_power"] = storages_df_p - if pf_post_lopf: - etrago_results_per_bus["storage_units_reactive_power"] = storages_df_q - + storages() t4 = time.perf_counter() performance.update({"Storage Data Processing": t4 - t3}) - # DSM - dsm_df = links_df.loc[ - (links_df["carrier"] == "dsm") & (links_df["bus0"] == str(bus_id)) - ] - if not dsm_df.empty: - if dsm_df.shape[0] > 1: - raise ValueError(f"More than one dsm link at bus {bus_id}") - dsm_df_p = etrago_network.links_t["p0"][dsm_df.index] - dsm_df_p.columns = dsm_df["carrier"] - if pf_post_lopf: - dsm_df_q = 0 * dsm_df_p - - etrago_results_per_bus["dsm_active_power"] = dsm_df_p - if pf_post_lopf: - etrago_results_per_bus["dsm_reactive_power"] = dsm_df_q - + dsm() t5 = time.perf_counter() performance.update({"DSM Data Processing": t5 - t4}) - # Heat - # Central heat - # Power2Heat - central_heat_carriers = ["central_heat_pump", "central_resistive_heater"] - central_heat_df = links_df.loc[ - links_df["carrier"].isin(central_heat_carriers) - & (links_df["bus0"] == str(bus_id)) - ] - if not central_heat_df.empty: - # Timeseries - central_heat_df_p = etrago_network.links_t["p0"][central_heat_df.index] - central_heat_df_p.columns = central_heat_df["carrier"] - if pf_post_lopf: - central_heat_df_q = 0 * central_heat_df_p - - etrago_results_per_bus["heat_central_active_power"] = central_heat_df_p - if pf_post_lopf: - etrago_results_per_bus["heat_central_reactive_power"] = central_heat_df_q - - # Stores - central_heat_bus = central_heat_df["bus1"].values[0] - central_heat_store_bus = etrago_network.links.loc[ - etrago_network.links["bus0"] == central_heat_bus, "bus1" - ].values[0] - central_heat_store_capacity = etrago_network.stores.loc[ - (etrago_network.stores["carrier"] == "central_heat_store") - & (etrago_network.stores["bus"] == central_heat_store_bus), - "e_nom_opt", - ].values[0] - - etrago_results_per_bus[ - "thermal_storage_central_capacity" - ] = central_heat_store_capacity - - # Feedin - geothermal_feedin_df = etrago_network.generators[ - (etrago_network.generators["carrier"] == "geo_thermal") - & (etrago_network.generators["bus"] == central_heat_bus) - ] - geothermal_feedin_df_p = etrago_network.generators_t["p"][ - geothermal_feedin_df.index - ] - geothermal_feedin_df_p.columns = geothermal_feedin_df["carrier"] - etrago_results_per_bus[ - "geothermal_energy_feedin_district_heating" - ] = geothermal_feedin_df_p - - solarthermal_feedin_df = etrago_network.generators[ - (etrago_network.generators["carrier"] == "solar_thermal_collector") - & (etrago_network.generators["bus"] == central_heat_bus) - ] - solarthermal_feedin_df_p = etrago_network.generators_t["p"][ - solarthermal_feedin_df.index - ] - solarthermal_feedin_df_p.columns = solarthermal_feedin_df["carrier"] - etrago_results_per_bus[ - "solarthermal_energy_feedin_district_heating" - ] = solarthermal_feedin_df_p - + central_heat() t6 = time.perf_counter() performance.update({"Central Heat Data Processing": t6 - t5}) - # Rural heat - # Power2Heat - rural_heat_carriers = ["rural_heat_pump"] - rural_heat_df = links_df.loc[ - links_df["carrier"].isin(rural_heat_carriers) - & (links_df["bus0"] == str(bus_id)) - ] - if not rural_heat_df.empty: - # Timeseries - rural_heat_df_p = etrago_network.links_t["p0"][rural_heat_df.index] - rural_heat_df_p.columns = rural_heat_df["carrier"] - if pf_post_lopf: - rural_heat_df_q = 0 * rural_heat_df_p - - etrago_results_per_bus["heat_pump_rural_active_power"] = rural_heat_df_p - if pf_post_lopf: - etrago_results_per_bus["heat_pump_rural_reactive_power"] = rural_heat_df_q - - # Stores - rural_heat_bus = rural_heat_df["bus1"].values[0] - rural_heat_store_bus = etrago_network.links.loc[ - etrago_network.links["bus0"] == rural_heat_bus, "bus1" - ].values[0] - rural_heat_store_capacity = etrago_network.stores.loc[ - (etrago_network.stores["carrier"] == "rural_heat_store") - & (etrago_network.stores["bus"] == rural_heat_store_bus), - "e_nom_opt", - ].values[0] - - etrago_results_per_bus[ - "thermal_storage_rural_capacity" - ] = rural_heat_store_capacity - + rural_heat() t7 = time.perf_counter() performance.update({"Rural Heat Data Processing": t7 - t6}) - # BEV charger - bev_charger_df = links_df.loc[ - (links_df["carrier"] == "BEV charger") & (links_df["bus0"] == str(bus_id)) - ] - if not bev_charger_df.empty: - if bev_charger_df.shape[0] > 1: - raise ValueError(f"More than one dsm link at bus {bus_id}") - - bev_charger_df_p = etrago_network.links_t["p0"][bev_charger_df.index] - bev_charger_df_p.columns = bev_charger_df["carrier"] - if pf_post_lopf: - bev_charger_df_q = 0 * bev_charger_df_p - - etrago_results_per_bus["electromobility_active_power"] = bev_charger_df_p - if pf_post_lopf: - etrago_results_per_bus["electromobility_reactive_power"] = bev_charger_df_q - + bev_charger() t8 = time.perf_counter() performance.update({"BEV Data Processing": t8 - t7}) + performance.update({"Overall time": t8 - t0}) logger.info(performance) From e4e76e2c1769f56cda2851168a5b9220de94e1b8 Mon Sep 17 00:00:00 2001 From: ClaraBuettner Date: Thu, 16 Feb 2023 18:12:54 +0100 Subject: [PATCH 027/126] Update etrago integration --- ego/tools/io.py | 255 ++---------------------------------------------- 1 file changed, 8 insertions(+), 247 deletions(-) diff --git a/ego/tools/io.py b/ego/tools/io.py index 9b7cdac3..a6c0b06c 100644 --- a/ego/tools/io.py +++ b/ego/tools/io.py @@ -45,25 +45,9 @@ from egoio.db_tables.model_draft import EgoGridPfHvTempResolution as TempResolution from egoio.db_tables.model_draft import RenpassGisParameterRegion from egoio.tools import db - from etrago.appl import etrago - from etrago.tools.io import load_config_file - from etrago.tools.plot import ( - curtailment, - full_load_hours, - gen_dist, - gen_dist_diff, - max_load, - nodal_gen_dispatch, - nodal_production_balance, - plot_line_loading, - plot_line_loading_diff, - plot_q_flows, - plot_residual_load, - plot_stacked_gen, - plot_voltage, - storage_distribution, - storage_expansion, - ) + from etrago import Etrago + from etrago.appl import run_etrago + from geoalchemy2 import * from shapely.geometry import MultiPolygon, Point, Polygon from sqlalchemy import MetaData, and_, create_engine, func @@ -158,8 +142,6 @@ def __init__(self, *args, **kwargs): """ """ super(eTraGoResults, self).__init__(self, *args, **kwargs) self.etrago = None - self._etrago_network = None - self._etrago_disaggregated_network = None logger.info("eTraGo section started") @@ -217,237 +199,16 @@ def __init__(self, *args, **kwargs): if self.json_file["eGo"].get("csv_import_eTraGo") != False: - logger.info("Caution, import disaggregation " "data of former Cluster") - - # get pathway - pathway = self.json_file["eGo"].get("csv_import_eTraGo") - - try: - # create Network from csv - self._etrago_network = pypsa.Network() - self._etrago_network.import_from_csv_folder(pathway) - logger.info("Create eTraGo network from CSV result") - - # get disaggregation - self._etrago_disaggregated_network = pypsa.Network() - self._etrago_disaggregated_network.import_from_csv_folder( - pathway + "/disaggregated" - ) - logger.info( - "Create eTraGo disaggregated network " "from CSV result" - ) - - except TypeError: - file_path = "disaggregated/network.csv" - fix_leading_separator(pathway + "/" + file_path) + logger.info("Import eTraGo network from csv files") - file_path = "network.csv" - fix_leading_separator(pathway + "/" + file_path) - - self._etrago_network = pypsa.Network() - self._etrago_network.import_from_csv_folder(pathway) - logger.info("Create eTraGo network from CSV result") - - # get disaggregation - self._etrago_disaggregated_network = pypsa.Network() - self._etrago_disaggregated_network.import_from_csv_folder( - pathway + "/disaggregated" - ) - logger.info("Create eTraGo disaggregated network" "from CSV result") - - args_name = "args.json" - with open(pathway + "/" + args_name) as f: - etrago_args = json.load(f) - logger.info("Using argument file") - - if etrago_args.get("extendable") == ["network", "storages"]: - etrago_args.update({"extendable": ["network", "storage"]}) - logger.info("Changed naming of storages to storage of args") - - if etrago_args.get("extendable") == ["storages"]: - etrago_args.update({"extendable": ["storage"]}) - logger.info("Changed naming of storages to storage of args") - - for key in self.json_file["eTraGo"].keys(): - try: - self.json_file["eTraGo"][key] = etrago_args[key] - except KeyError: - pass + self.etrago = Etrago( + csv_folder_name = self.json_file['eGo'].get('csv_import_eTraGo')) else: logger.info("Create eTraGo network calcualted by eGo") - if self.json_file["eTraGo"]["disaggregation"] != False: - - etrago_network, etrago_disaggregated_network = etrago( - self.json_file["eTraGo"] - ) - - self._etrago_network = etrago_network - self._etrago_disaggregated_network = etrago_disaggregated_network - else: - logger.warning("Only one network is used.") - - etrago_network, etrago_disaggregated_network = etrago( - self.json_file["eTraGo"] - ) - - self._etrago_network = etrago_network - self._etrago_disaggregated_network = etrago_disaggregated_network - - # Add selected results to results container - # ----------------------------------------- - - self.etrago = pd.DataFrame() - self.etrago.network = self._etrago_network - self.etrago.disaggregated_network = self._etrago_disaggregated_network - - # Add function - self.etrago.storage_investment_costs = etrago_storages_investment( - self.etrago.network, self.json_file, self.session - ) - self.etrago.storage_charges = etrago_storages(self.etrago.network) - - self.etrago.operating_costs = etrago_operating_costs(self.etrago.network) - self.etrago.generator = create_etrago_results( - self.etrago.network, self.scn_name - ) - self.etrago.grid_investment_costs = etrago_grid_investment( - self.etrago.network, self.json_file, self.session - ) - - # add functions direct - # self._etrago_network.etrago_line_loading = etrago_line_loading - self.etrago.plot_line_loading = self._line_loading - self.etrago.plot_stacked_gen = self._stacked_gen - self.etrago.plot_curtailment = self._curtailment - self.etrago.plot_gen_dist = self._gen_dist - self.etrago.plot_storage_distribution = self._storage_distribution - self.etrago.plot_line_loading_diff = self._line_loading_diff - self.etrago.plot_residual_load = self._residual_load - self.etrago.plot_voltage = self._voltage - self.etrago.plot_nodal_gen_dispatch = self._nodal_gen_dispatch - self.etrago.plot_full_load_hours = self._full_load_hours - self.etrago.plot_q_flows = self._plot_q_flows - self.etrago.plot_max_load = self._max_load - self.etrago.plot_storage_expansion = self._storage_expansion - self.etrago.plot_nodal_production_balance = self._nodal_production_balance - self.etrago.plot_gen_dist_diff = self._gen_dist_diff - - if not "READTHEDOCS" in os.environ: - # include eTraGo functions and methods - def _gen_dist_diff(self, **kwargs): - """ - Integrate and use function from eTraGo. - For more information see: - """ - - return gen_dist_diff(networkA=self.etrago.network, **kwargs) - - def _nodal_production_balance(self, **kwargs): - """ - Integrate and use function from eTraGo. - For more information see: - """ - - return nodal_production_balance(network=self.etrago.network, **kwargs) - - def _storage_expansion(self, **kwargs): - """ - Integrate and use function from eTraGo. - For more information see: - """ - - return storage_expansion(network=self.etrago.network, **kwargs) - - def _max_load(self, **kwargs): - """ - Integrate and use function from eTraGo. - For more information see: - """ - - return max_load(network=self.etrago.network, **kwargs) - - def _plot_q_flows(self): - """ - Integrate and use function from eTraGo. - For more information see: - """ - - return plot_q_flows(network=self.etrago.network) - - def _line_loading(self, **kwargs): - """ - Integrate and use function from eTraGo. - For more information see: - """ - # add if time_step <1 -> plot - return plot_line_loading(network=self.etrago.network, **kwargs) - - def _stacked_gen(self, **kwargs): - """ - Integrate function from eTraGo. - For more information see: - """ - return plot_stacked_gen(network=self.etrago.network, **kwargs) - - def _curtailment(self, **kwargs): - """ - Integrate function from eTraGo. - For more information see: - """ - return curtailment(network=self.etrago.network, **kwargs) - - def _gen_dist(self, **kwargs): - """ - Integrate function from eTraGo. - For more information see: - """ - return gen_dist(network=self.etrago.network, **kwargs) - - def _storage_distribution(self, scaling=1, **kwargs): - """ - Integrate function from eTraGo. - For more information see: - """ - return storage_distribution( - network=self.etrago.network, scaling=1, **kwargs - ) - - def _voltage(self, **kwargs): - """ - Integrate function from eTraGo. - For more information see: - """ - return plot_voltage(network=self.etrago.network, **kwargs) - - def _residual_load(self, **kwargs): - """ - Integrate function from eTraGo. - For more information see: - """ - return plot_residual_load(network=self.etrago.network, **kwargs) - - def _line_loading_diff(self, **kwargs): - """ - Integrate function from eTraGo. - For more information see: - """ - return plot_line_loading_diff(networkA=self.etrago.network, **kwargs) - - def _nodal_gen_dispatch(self, **kwargs): - """ - Integrate function from eTraGo. - For more information see: - """ - return nodal_gen_dispatch(network=self.etrago.network, **kwargs) - - def _full_load_hours(self, **kwargs): - """ - Integrate function from eTraGo. - For more information see: - """ - return full_load_hours(network=self.etrago.network, **kwargs) + run_etrago(args= self.json_file['eTraGo'], + json_path=None) class eDisGoResults(eTraGoResults): From 5e94020019fd0738b012356f84138877c37f7228 Mon Sep 17 00:00:00 2001 From: ClaraBuettner Date: Thu, 16 Feb 2023 18:14:33 +0100 Subject: [PATCH 028/126] Delete import of old etrago plots --- ego/tools/plots.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/ego/tools/plots.py b/ego/tools/plots.py index 40f7a90b..2a4acdd4 100644 --- a/ego/tools/plots.py +++ b/ego/tools/plots.py @@ -33,17 +33,6 @@ import pyproj as proj - from etrago.tools.plot import ( - add_coordinates, - coloring, - curtailment, - gen_dist, - plot_line_loading, - plot_residual_load, - plot_stacked_gen, - plot_voltage, - storage_distribution, - ) from geoalchemy2 import * from pypsa import Network as PyPSANetwork from shapely.geometry import MultiPolygon, Point, Polygon From 92eb1005d095908d0ed00f390bf7fd443da6895b Mon Sep 17 00:00:00 2001 From: ClaraBuettner Date: Thu, 16 Feb 2023 18:17:04 +0100 Subject: [PATCH 029/126] Update etrago parameters --- ego/scenario_setting.json | 201 ++++++++++++++++++++++++++------------ 1 file changed, 140 insertions(+), 61 deletions(-) diff --git a/ego/scenario_setting.json b/ego/scenario_setting.json index 80089304..19f5ecab 100644 --- a/ego/scenario_setting.json +++ b/ego/scenario_setting.json @@ -1,67 +1,146 @@ { - "eGo": { - "eTraGo": true, - "eDisGo": true, - "csv_import_eTraGo": false, - "csv_import_eDisGo": false, + "eGo": { + "eTraGo": true, + "eDisGo": true, + "csv_import_eTraGo": false, + "csv_import_eDisGo": false + }, + "eTraGo": { + "db": "egon-data", + "gridversion": null, + "method": { + "type": "lopf", + "n_iter": 4, + "pyomo": true }, - "eTraGo": { - "db": "oedb", - "gridversion": "v0.4.5", - "method": "lopf", - "pf_post_lopf": true, - "start_snapshot": 1000, - "end_snapshot": 1005, - "solver": "gurobi", - "solver_options": {}, - "scn_name": "eGo 100", - "scn_extension": null, - "scn_decommissioning": null, - "lpfile": false, - "csv_export": "results/your_results", - "db_export": false, - "extendable": ["storage", "network"], - "generator_noise": 789456, - "minimize_loading": false, - "ramp_limits": false, - "extra_functionality": null, - "network_clustering_kmeans": 10, - "load_cluster": false, - "network_clustering_ehv": false, - "disaggregation": "uniform", - "snapshot_clustering": false, - "parallelisation": false, - "skip_snapshots": false, - "line_grouping": false, - "branch_capacity_factor": {"HV": 0.5, "eHV": 0.7}, - "load_shedding": false, - "foreign_lines": {"carrier": "AC", "capacity": "osmTGmod"}, - "comments": "", + "pf_post_lopf": { + "active": true, + "add_foreign_lopf": true, + "q_allocation": "p_nom" }, - "eDisGo": { - "db": "oedb", - "gridversion": "v0.4.5", - "ding0_files": "/path/to_your/.dingo/grids", - "choice_mode": "cluster", - "cluster_attributes": [ - "farthest_node", - "wind_cap", - "solar_cap", - "extended_storage", + "start_snapshot": 1, + "end_snapshot": 2, + "solver": "gurobi", + "solver_options": { + "BarConvTol": 1e-05, + "FeasibilityTol": 1e-05, + "method": 2, + "crossover": 0, + "logFile": "solver_etragos.log", + "threads": 4 + }, + "model_formulation": "kirchhoff", + "scn_name": "eGon2035", + "scn_extension": null, + "scn_decommissioning": null, + "lpfile": false, + "csv_export": "test", + "extendable": { + "extendable_components": [ + "as_in_db" ], - "only_cluster": false, - "manual_grids": [], - "no_grids": 2, - "parallelization": true, - "max_calc_time": 0.5, - "max_workers": 2, - "initial_reinforcement": true, - "apply_curtailment": true, - "curtailment_voltage_threshold": 0, - "storage_distribution": true, - "max_cos_phi_renewable": 0.9, - "results": "results/another_result", - "solver": "gurobi", - "timesteps_pfa": "snapshot_analysis", + "upper_bounds_grid": { + "grid_max_D": null, + "grid_max_abs_D": { + "380": { + "i": 1020, + "wires": 4, + "circuits": 4 + }, + "220": { + "i": 1020, + "wires": 4, + "circuits": 4 + }, + "110": { + "i": 1020, + "wires": 4, + "circuits": 2 + }, + "dc": 0 + }, + "grid_max_foreign": 4, + "grid_max_abs_foreign": null + } + }, + "generator_noise": 789456, + "extra_functionality": {}, + "network_clustering": { + "random_state": 42, + "active": true, + "method": "kmedoids-dijkstra", + "n_clusters_AC": 30, + "cluster_foreign_AC": false, + "method_gas": "kmedoids-dijkstra", + "n_clusters_gas": 20, + "cluster_foreign_gas": false, + "k_busmap": false, + "kmeans_gas_busmap": false, + "line_length_factor": 1, + "remove_stubs": false, + "use_reduced_coordinates": false, + "bus_weight_tocsv": null, + "bus_weight_fromcsv": null, + "gas_weight_tocsv": null, + "gas_weight_fromcsv": null, + "n_init": 10, + "max_iter": 100, + "tol": 1e-06, + "CPU_cores": 4 + }, + "sector_coupled_clustering": { + "active": true, + "carrier_data": { + "central_heat": { + "base": [ + "CH4", + "AC" + ], + "strategy": "simultaneous" + } + } + }, + "network_clustering_ehv": false, + "disaggregation": "uniform", + "snapshot_clustering": { + "active": false, + "method": "segmentation", + "extreme_periods": null, + "how": "daily", + "storage_constraints": "soc_constraints", + "n_clusters": 5, + "n_segments": 5 + }, + "skip_snapshots": false, + "dispatch_disaggregation": false, + "branch_capacity_factor": { + "HV": 0.5, + "eHV": 0.7 + }, + "load_shedding": false, + "foreign_lines": { + "carrier": "AC", + "capacity": "osmTGmod" }, + "comments": null +}, + "eDisGo": { + "db": "oedb", + "gridversion": "v0.4.5", + "ding0_path": "/path/to_your/.dingo/grids", + "choice_mode": "cluster", + "cluster_attributes":["farthest_node", "wind_cap", "solar_cap", "extended_storage"], + "only_cluster": false, + "manual_grids": [], + "no_grids": 2, + "parallelization":true, + "max_calc_time": 0.5, + "max_workers":2, + "initial_reinforcement":true, + "max_cos_phi_renewable": 0.9, + "results": "results/another_result", + "solver": "gurobi", + "timesteps_pfa": "snapshot_analysis" + } + } From b1712c192a5e2d2562b5487a76a4594c1cfea943 Mon Sep 17 00:00:00 2001 From: Malte Jahn Date: Thu, 23 Feb 2023 09:27:59 +0100 Subject: [PATCH 030/126] Refactor mv grid clustering - Add a blacklist to select only working grids in the clustering. - Add a new method to connect to the egon-data database. - Get the cluster attributes from database not from ding0. - Solve some flake8 problems - Excluded "F841 from checking local variable ... is assigned to but never used" - In this project state, there are much unused local variables, which are later used. --- .flake8 | 2 +- ego/mv_clustering/__init__.py | 1 + ego/mv_clustering/database.py | 101 ++++++++++ ego/mv_clustering/egon_data_io.py | 167 ++++++++++++++++ ego/mv_clustering/mv_clustering.py | 192 ++++++++++++++++++ ego/tools/edisgo_integration.py | 169 ++++++---------- ego/tools/mv_cluster.py | 299 ----------------------------- 7 files changed, 523 insertions(+), 408 deletions(-) create mode 100644 ego/mv_clustering/__init__.py create mode 100644 ego/mv_clustering/database.py create mode 100644 ego/mv_clustering/egon_data_io.py create mode 100644 ego/mv_clustering/mv_clustering.py delete mode 100644 ego/tools/mv_cluster.py diff --git a/.flake8 b/.flake8 index 3376e4aa..c981be61 100644 --- a/.flake8 +++ b/.flake8 @@ -1,7 +1,7 @@ [flake8] extend-exclude = docs max-line-length = 88 -extend-ignore = E203 +extend-ignore = E203, F841 count = true statistics = true show-source = true diff --git a/ego/mv_clustering/__init__.py b/ego/mv_clustering/__init__.py new file mode 100644 index 00000000..593130ae --- /dev/null +++ b/ego/mv_clustering/__init__.py @@ -0,0 +1 @@ +from ego.mv_clustering.mv_clustering import cluster_workflow # noqa: F401 diff --git a/ego/mv_clustering/database.py b/ego/mv_clustering/database.py new file mode 100644 index 00000000..9d672ce1 --- /dev/null +++ b/ego/mv_clustering/database.py @@ -0,0 +1,101 @@ +import logging +import subprocess +import sys +import time + +from contextlib import contextmanager +from functools import wraps + +import saio + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +logger = logging.getLogger(__name__) + + +def get_engine(config=None): + config = config["mv_clustering"]["database"] + engine = create_engine( + f"postgresql+psycopg2://{config['user']}:" + f"{config['password']}@{config['host']}:" + f"{int(config['port'])}/{config['database_name']}", + echo=False, + ) + logger.info(f"Created engine: {engine}.") + return engine + + +@contextmanager +def sshtunnel(config=None): + ssh_config = config["mv_clustering"]["database"]["ssh"] + if ssh_config["enabled"]: + try: + logger.info("Open ssh tunnel.") + proc = subprocess.Popen( + [ + "ssh", + "-N", + "-L", + f"{ssh_config['local_port']}" + f":{ssh_config['local_address']}" + f":{ssh_config['port']}", + f"{ssh_config['user']}@{ssh_config['ip']}", + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + time.sleep(2) + yield proc + finally: + logger.info("Close ssh tunnel.") + proc.kill() + outs, errs = proc.communicate() + logger.info( + f"SSH process output {outs.decode('utf-8')=}, {errs.decode('utf-8')=}" + ) + else: + try: + logger.info("Don't use an ssh tunnel.") + yield None + finally: + logger.info("Close contextmanager.") + + +@contextmanager +def session_scope(engine): + Session = sessionmaker(bind=engine) + session = Session() + try: + yield session + session.commit() + except: # noqa: E722 + session.rollback() + raise + finally: + session.close() + + +def session_decorator(f): + @wraps(f) + def wrapper(*args, **kwargs): + with session_scope(kwargs["engine"]) as session: + kwargs["session"] = session + kwargs.pop("engine") + logger.info(f"Calling {f.__name__}") + return f(*args, **kwargs) + + return wrapper + + +def register_tables_in_saio(engine, config=None): + db_tables = config["mv_clustering"]["database"]["tables"] + orm = {} + + for name, table_str in db_tables.items(): + table_list = table_str.split(".") + table_schema = table_list[0] + table_name = table_list[1] + saio.register_schema(table_schema, engine) + orm[name] = sys.modules[f"saio.{table_schema}"].__getattr__(table_name) + return orm diff --git a/ego/mv_clustering/egon_data_io.py b/ego/mv_clustering/egon_data_io.py new file mode 100644 index 00000000..ec83a77d --- /dev/null +++ b/ego/mv_clustering/egon_data_io.py @@ -0,0 +1,167 @@ +import logging + +import pandas as pd + +from sqlalchemy import func + +from ego.mv_clustering.database import session_decorator + +logger = logging.getLogger(__name__) + + +def func_within(geom_a, geom_b, srid=3035): + return func.ST_Within( + func.ST_Transform( + geom_a, + srid, + ), + func.ST_Transform( + geom_b, + srid, + ), + ) + + +@session_decorator +def get_grid_ids(orm=None, session=None): + query = session.query( + orm["egon_hvmv_substation"].bus_id.label("bus_id"), + orm["egon_hvmv_substation"].point.label("geom"), + ) + mv_data = pd.read_sql_query(query.statement, session.bind, index_col="bus_id") + return mv_data + + +@session_decorator +def get_solar_capacity(orm=None, session=None): + # Get PV open space join weather cell id + query = ( + session.query( + orm["generators_pv"].bus_id, + func.sum(orm["generators_pv"].capacity).label("p_openspace"), + ) + .filter( + orm["generators_pv"].bus_id > 0, + orm["generators_pv"].site_type == "Freifläche", + orm["generators_pv"].status == "InBetrieb", + orm["generators_pv"].voltage_level.in_([4, 5, 6, 7]), + ) + .group_by( + orm["generators_pv"].bus_id, + ) + ) + generators_pv_open_space_df = pd.read_sql( + sql=query.statement, con=session.bind, index_col=None + ) + + query = ( + session.query( + orm["generators_pv_rooftop"].bus_id, + func.sum(orm["generators_pv_rooftop"].capacity).label("p_rooftop"), + ) + .filter( + orm["generators_pv_rooftop"].bus_id > 0, + orm["generators_pv_rooftop"].scenario == "status_quo", + orm["generators_pv_rooftop"].voltage_level.in_([4, 5, 6, 7]), + ) + .group_by( + orm["generators_pv_rooftop"].bus_id, + ) + ) + generators_pv_rooftop_df = pd.read_sql( + sql=query.statement, con=session.bind, index_col=None + ) + + renewable_generators_df = generators_pv_open_space_df.set_index("bus_id").join( + generators_pv_rooftop_df.set_index("bus_id"), how="outer" + ) + renewable_generators_df.fillna(value=0, inplace=True) + renewable_generators_df["solar_cap"] = renewable_generators_df.sum(axis="columns") + return renewable_generators_df[["solar_cap"]] + + +@session_decorator +def get_wind_capacity(orm=None, session=None): + # Get generators wind join weather cells + query = ( + session.query( + orm["generators_wind"].bus_id, + func.sum(orm["generators_wind"].capacity).label("wind_capacity"), + ) + .filter( + orm["generators_wind"].bus_id > 0, + orm["generators_wind"].site_type == "Windkraft an Land", + orm["generators_wind"].status == "InBetrieb", + orm["generators_wind"].voltage_level.in_([4, 5, 6, 7]), + ) + .group_by( + orm["generators_wind"].bus_id, + ) + ) + generators_wind_df = pd.read_sql( + sql=query.statement, con=session.bind, index_col=None + ) + + renewable_generators_df = generators_wind_df.set_index("bus_id") + renewable_generators_df["wind_cap"] = renewable_generators_df.sum(axis="columns") + return renewable_generators_df[["wind_cap"]] + + +@session_decorator +def get_emobility_capacity(orm=None, session=None): + load_timeseries_nested = ( + session.query( + orm["etrago_load_timeseries"].scn_name, + orm["etrago_load_timeseries"].load_id, + orm["etrago_load_timeseries"].temp_id, + orm["etrago_load_timeseries"].p_set, + orm["etrago_load"].bus.label("bus_id"), + ) + .join( + orm["etrago_load_timeseries"], + orm["etrago_load_timeseries"].load_id == orm["etrago_load"].load_id, + ) + .filter( + orm["etrago_load"].scn_name == "eGon2035_lowflex", + orm["etrago_load"].carrier == "land transport EV", + ) + ).subquery(name="load_timeseries_nested") + load_timeseries_unnested = ( + session.query( + load_timeseries_nested.c.bus_id, + load_timeseries_nested.c.scn_name, + load_timeseries_nested.c.load_id, + load_timeseries_nested.c.temp_id, + func.unnest(load_timeseries_nested.c.p_set).label("p_set"), + ) + ).subquery(name="load_timeseries_unnested") + load_timeseries_maximal = ( + session.query( + load_timeseries_unnested.c.bus_id, + load_timeseries_unnested.c.scn_name, + load_timeseries_unnested.c.load_id, + load_timeseries_unnested.c.temp_id, + func.max(load_timeseries_unnested.c.p_set).label("p_set_max"), + ).group_by( + load_timeseries_unnested.c.bus_id, + load_timeseries_unnested.c.scn_name, + load_timeseries_unnested.c.load_id, + load_timeseries_unnested.c.temp_id, + ) + ).subquery(name="load_timeseries_maximal") + load_p_nom = session.query( + load_timeseries_maximal.c.bus_id, + func.sum(load_timeseries_maximal.c.p_set_max).label("emob_cap"), + ).group_by( + load_timeseries_maximal.c.bus_id, + ) + emobility_capacity_df = pd.read_sql( + sql=load_p_nom.statement, con=session.bind, index_col=None + ) + emobility_capacity_df.set_index("bus_id", inplace=True) + return emobility_capacity_df + + +@session_decorator +def get_cummulative_storage_capacity(bus_id_list, orm=None, session=None): + return diff --git a/ego/mv_clustering/mv_clustering.py b/ego/mv_clustering/mv_clustering.py new file mode 100644 index 00000000..a69a917c --- /dev/null +++ b/ego/mv_clustering/mv_clustering.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +# Copyright 2016-2018 Europa-Universität Flensburg, +# Flensburg University of Applied Sciences, +# Centre for Sustainable Energy Systems +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# File description +""" +This file contains all functions regarding the clustering of MV grids +""" +__copyright__ = ( + "Flensburg University of Applied Sciences, " + "Europa-Universität Flensburg, " + "Centre for Sustainable Energy Systems" +) +__license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" +__author__ = "wolf_bunke, maltesc, mltja" + +import logging +import os + +if "READTHEDOCS" not in os.environ: + import numpy as np + import pandas as pd + + from sklearn.cluster import KMeans + + import ego.mv_clustering.egon_data_io as db_io + + from ego.mv_clustering.database import ( + get_engine, + register_tables_in_saio, + sshtunnel, + ) + +logger = logging.getLogger(__name__) + + +def cluster_attributes_to_csv(attributes_path, config=None): + """ + Calculates the attributes to cluster + + Parameters + ---------- + attributes_path : :obj:`str` + Path to attributes csv + + config : :obj:`dict` + Config dict. + + """ + + with sshtunnel(config=config): + engine = get_engine(config=config) + orm = register_tables_in_saio(engine, config=config) + + grid_ids_df = db_io.get_grid_ids(engine=engine, orm=orm) + solar_capacity_df = db_io.get_solar_capacity(engine=engine, orm=orm) + wind_capacity_df = db_io.get_wind_capacity(engine=engine, orm=orm) + emobility_capacity_df = db_io.get_emobility_capacity(engine=engine, orm=orm) + + df = pd.concat( + [grid_ids_df, solar_capacity_df, wind_capacity_df, emobility_capacity_df], + axis="columns", + ) + df.fillna(0, inplace=True) + + df.to_csv(attributes_path) + + +def mv_grid_clustering(data_df, working_grids=None, config=None): + """ + Clusters the MV grids based on the attributes, for a given number + of MV grids + + Parameters + ---------- + n_cluster : int + Desired number of clusters (of MV grids) + + Returns + ------- + :pandas:`pandas.DataFrame` + Dataframe containing the clustered MV grids and their weightings + + """ + random_seed = config["eGo"]["random_seed"] + n_clusters = config["eDisGo"]["n_clusters"] + + # Norm attributes + for attribute in data_df: + attribute_max = data_df[attribute].max() + data_df[attribute] = data_df[attribute] / attribute_max + + # Starting KMeans clustering + logger.info(f"Used Clustering Attributes: {data_df.columns.to_list()}") + kmeans = KMeans(n_clusters=n_clusters, random_state=random_seed) + data_array = data_df.to_numpy() + labels = kmeans.fit_predict(data_array) + centroids = kmeans.cluster_centers_ + + result_df = pd.DataFrame(index=data_df.index) + result_df["label"] = labels + # For each sample, calculate the distance to its assigned centroid. + result_df["centroid_distance"] = np.linalg.norm( + data_array - centroids[labels], axis=1 + ) + result_df["representative"] = False + + if working_grids is None: + result_df["working"] = True + else: + result_df["working"] = result_df.join(working_grids).fillna(False)["working"] + + failing_labels = [] + for label in np.unique(labels): + try: + rep = result_df.loc[ + result_df["working"] & (result_df["label"] == label), + "centroid_distance", + ].idxmin() + result_df.loc[rep, "representative"] = True + except ValueError: + failing_labels.append(label) + + n_grids = result_df.shape[0] + df_data = [] + columns = [ + "representative", + "n_grids_per_cluster", + "relative_representation", + "represented_grids", + ] + for label in np.unique(labels): + represented_grids = result_df[result_df["label"] == label].index.to_list() + n_grids_per_cluster = len(represented_grids) + relative_representation = (n_grids_per_cluster / n_grids) * 100 + try: + representative = result_df[ + result_df["representative"] & (result_df["label"] == label) + ].index.values[0] + except IndexError: + representative = False + + row = [ + representative, + n_grids_per_cluster, + relative_representation, + represented_grids, + ] + df_data.append(row) + + cluster_df = pd.DataFrame(df_data, index=np.unique(labels), columns=columns) + cluster_df.index.name = "cluster_id" + + return cluster_df + + +def cluster_workflow(config=None): + attributes_path = os.path.join(config["eDisGo"]["ding0_path"], "attributes.csv") + working_grids_path = os.path.join( + config["eDisGo"]["ding0_path"], "working_grids.csv" + ) + + if not os.path.isfile(attributes_path): + logger.info("Attributes file is missing, get attributes from egon-data.") + cluster_attributes_to_csv(attributes_path=attributes_path, config=config) + + data_to_cluster = pd.read_csv(attributes_path, index_col=0)[ + config["eDisGo"]["cluster_attributes"] + ] + if os.path.isfile(working_grids_path): + working_grids = pd.read_csv(working_grids_path, index_col=0) + else: + logger.info("'working_grids.csv' is missing, select representative grids.") + working_grids = None + + return mv_grid_clustering( + data_to_cluster, working_grids=working_grids, config=config + ) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 5d9f398b..53d76d39 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -27,7 +27,7 @@ "Centre for Sustainable Energy Systems" ) __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" -__author__ = "wolf_bunke, maltesc" +__author__ = "wolf_bunke, maltesc, mltja" import json import logging @@ -47,7 +47,7 @@ from sqlalchemy.orm import scoped_session, sessionmaker -if not "READTHEDOCS" in os.environ: +if "READTHEDOCS" not in os.environ: from edisgo.edisgo import EDisGo, import_edisgo_from_files from edisgo.flex_opt import q_control @@ -57,9 +57,9 @@ from egoio.db_tables import grid, model_draft from egoio.tools import db + from ego.mv_clustering import cluster_workflow from ego.tools.economics import edisgo_grid_investment from ego.tools.interface import ETraGoMinimalData, get_etrago_results_per_bus - from ego.tools.mv_cluster import analyze_attributes, cluster_mv_grids # Logging @@ -568,85 +568,21 @@ def _successful_grids(self): fail += weight return success / total - def _analyze_cluster_attributes(self): - """ - Analyses the attributes wind and solar capacity and farthest node - for clustering. - These are considered the "standard" attributes for the MV grid - clustering. - """ - analyze_attributes(self._ding0_path) - - def _cluster_mv_grids(self, no_grids): + def _cluster_mv_grids(self): """ Clusters the MV grids based on the attributes, for a given number of MV grids - Parameters - ---------- - no_grids : int - Desired number of clusters (of MV grids) - Returns ------- :pandas:`pandas.DataFrame` Dataframe containing the clustered MV grids and their weightings """ - - # TODO: This first dataframe contains the standard attributes... - # ...Create an Interface in order to use attributes more flexibly. - # Make this function more generic. - attributes_path = self._ding0_path + "/attributes.csv" - - if not os.path.isfile(attributes_path): - logger.info("Attributes file is missing") - logger.info("Attributes will be calculated") - self._analyze_cluster_attributes() - - df = pd.read_csv(self._ding0_path + "/attributes.csv") - df = df.set_index("id") - df.drop(["Unnamed: 0"], inplace=True, axis=1) - df.rename( - columns={ - "Solar_cumulative_capacity": "solar_cap", - "Wind_cumulative_capacity": "wind_cap", - "The_Farthest_node": "farthest_node", - }, - inplace=True, - ) - - if "extended_storage" in self._cluster_attributes: - if self._ext_storage: - storages = self._identify_extended_storages() - if not (storages.max().values[0] == 0.0): - df = pd.concat([df, storages], axis=1) - df.rename( - columns={"storage_p_nom": "extended_storage"}, inplace=True - ) - else: - logger.warning( - "Extended storages all 0. \ - Therefore, extended storages \ - are excluded from clustering" - ) - - found_atts = [i for i in self._cluster_attributes if i in df.columns] - missing_atts = [i for i in self._cluster_attributes if i not in df.columns] - - logger.info("Available attributes are: {}".format(df.columns.tolist())) - logger.info("Chosen/found attributes are: {}".format(found_atts)) - - if len(missing_atts) > 0: - logger.warning("Missing attributes: {}".format(missing_atts)) - if "extended_storage" in missing_atts: - logger.info( - "Hint: eTraGo dataset must contain " - "extendable storage in order to include " - "storage extension in MV grid clustering." - ) - - return cluster_mv_grids(no_grids, cluster_base=df) + cluster_df = cluster_workflow(config=self._json_file) + # Filter for clusters with representatives. + cluster_df = cluster_df[cluster_df["representative"].astype(bool)] + return cluster_df def _identify_extended_storages(self): @@ -666,7 +602,10 @@ def _identify_extended_storages(self): min_extended = 0.3 stor_p_nom = self._etrago_network.storage_units.loc[ (self._etrago_network.storage_units["bus"] == str(bus_id)) - & (self._etrago_network.storage_units["p_nom_extendable"] == True) + & ( + self._etrago_network.storage_units["p_nom_extendable"] + == True # noqa: E712 + ) & (self._etrago_network.storage_units["p_nom_opt"] > min_extended) & (self._etrago_network.storage_units["max_hours"] <= 20.0) ]["p_nom_opt"] @@ -718,14 +657,20 @@ def _set_grid_choice(self): ) if self._choice_mode == "cluster": - no_grids = self._edisgo_args["no_grids"] - logger.info("Clustering to {} MV grids".format(no_grids)) + cluster_df = self._cluster_mv_grids() - cluster_df = self._cluster_mv_grids(no_grids) - choice_df["the_selected_network_id"] = cluster_df["the_selected_network_id"] - choice_df["no_of_points_per_cluster"] = cluster_df[ - "no_of_points_per_cluster" - ] + n_clusters = self._json_file["eDisGo"]["n_clusters"] + n_clusters_found = cluster_df.shape[0] + if n_clusters != n_clusters_found: + logger.info(f"Clustering to {n_clusters} MV grids") + else: + logger.warning( + f"For {n_clusters} only for {n_clusters_found} clusters " + f"found working grids." + ) + + choice_df["the_selected_network_id"] = cluster_df["representative"] + choice_df["no_of_points_per_cluster"] = cluster_df["n_grids_per_cluster"] choice_df["represented_grids"] = cluster_df["represented_grids"] elif self._choice_mode == "manual": @@ -785,7 +730,7 @@ def _run_edisgo_pool(self): ) for g in mv_grids: - if not g in self._edisgo_grids: + if g not in self._edisgo_grids: self._edisgo_grids[g] = "Timeout" else: @@ -1159,7 +1104,7 @@ def _load_edisgo_results(self): self._edisgo_grids[mv_grid_id] = edisgo_grid logger.info("Imported MV grid {}".format(mv_grid_id)) - except: + except: # noqa: E722 self._edisgo_grids[mv_grid_id] = "This grid failed to reimport" logger.warning("MV grid {} could not be loaded".format(mv_grid_id)) @@ -1295,24 +1240,28 @@ def filter_df_by_carrier(df): df = filter_df_by_carrier(df_to_filter) df = df[columns_to_save] + unique_carriers = filter_df_by_carrier( + getattr(etrago_network_obj, component) + ).carrier.unique() logger.debug( - f"{component}, {carrier}, {timeseries}, {df.shape}, " - f"{filter_df_by_carrier(getattr(etrago_network_obj, component)).carrier.unique()}" + f"{component}, {carrier}, {timeseries}, {df.shape}, {unique_carriers}" ) return df logger.debug( - f"Carriers in links {etrago_network.network.links.carrier.unique()}" + f"Carriers in links " f"{etrago_network.network.links.carrier.unique()}" ) logger.debug( - f"Carriers in generators {etrago_network.network.generators.carrier.unique()}" + f"Carriers in generators " + f"{etrago_network.network.generators.carrier.unique()}" ) logger.debug( - f"Carriers in stores {etrago_network.network.stores.carrier.unique()}" + f"Carriers in stores " f"{etrago_network.network.stores.carrier.unique()}" ) logger.debug( - f"Carriers in storage_units {etrago_network.network.storage_units.carrier.unique()}" + f"Carriers in storage_units " + f"{etrago_network.network.storage_units.carrier.unique()}" ) self.snapshots = etrago_network.network.snapshots @@ -1467,12 +1416,12 @@ def initializer(): ) logger.info("Jobs time out in {:.2f}h.".format(max_calc_time - hours_to_go)) time_spent = tick - for grid, result in result_objects.items(): + for grid_id, result in result_objects.items(): if result.ready(): logger.info( - "MV grid {} ready. Trying to `get` the result.".format(grid) + "MV grid {} ready. Trying to `get` the result.".format(grid_id) ) - done.append(grid) + done.append(grid_id) if not result.successful(): try: # We already know that this was not successful, so the @@ -1481,15 +1430,17 @@ def initializer(): result.get() except Exception as e: logger.warning( - "MV grid {} failed due to {e!r}: '{e}'.".format(grid, e=e) + "MV grid {} failed due to {e!r}: '{e}'.".format( + grid_id, e=e + ) ) - errors[grid] = e + errors[grid_id] = e else: - logger.info("MV grid {} calculated successfully.".format(grid)) - successes[grid] = result.get() - logger.info("Done `get`ting the result for MV grid {}.".format(grid)) - for grid in done: - del result_objects[grid] + logger.info("MV grid {} calculated successfully.".format(grid_id)) + successes[grid_id] = result.get() + logger.info("Done `get`ting the result for MV grid {}.".format(grid_id)) + for grid_id in done: + del result_objects[grid_id] sleep(1) current = datetime.now() @@ -1509,21 +1460,23 @@ def initializer(): logger.info("Execution finished after {:.2f} hours".format(delta.seconds / 3600)) done = [] - for grid, result in result_objects.items(): - done.append(grid) + for grid_id, result in result_objects.items(): + done.append(grid_id) try: - successes[grid] = result.get(timeout=0) - logger.info("MV grid {} calculated successfully.".format(grid)) + successes[grid_id] = result.get(timeout=0) + logger.info("MV grid {} calculated successfully.".format(grid_id)) except Exception as e: - logger.warning("MV grid {} failed due to {e!r}: '{e}'.".format(grid, e=e)) - errors[grid] = e - for grid in done: - del result_objects[grid] + logger.warning( + "MV grid {} failed due to {e!r}: '{e}'.".format(grid_id, e=e) + ) + errors[grid_id] = e + for grid_id in done: + del result_objects[grid_id] if errors: logger.info("MV grid calculation error details:") - for grid, error in errors.items(): - logger.info(" {}".format(grid)) + for grid_id, error in errors.items(): + logger.info(" {}".format(grid_id)) strings = TracebackException.from_exception(error).format() lines = [line for string in strings for line in string.split("\n")] for line in lines: diff --git a/ego/tools/mv_cluster.py b/ego/tools/mv_cluster.py deleted file mode 100644 index 696349a4..00000000 --- a/ego/tools/mv_cluster.py +++ /dev/null @@ -1,299 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2016-2018 Europa-Universität Flensburg, -# Flensburg University of Applied Sciences, -# Centre for Sustainable Energy Systems -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation; either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -# File description -""" -This file contains all functions regarding the clustering of MV grids -""" -__copyright__ = ( - "Flensburg University of Applied Sciences, " - "Europa-Universität Flensburg, " - "Centre for Sustainable Energy Systems" -) -__license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" -__author__ = "wolf_bunke, maltesc" - -import logging - -# Import -# from __future__ import print_function -import os - -if not "READTHEDOCS" in os.environ: - import pickle - - import numpy as np - import pandas as pd - - from sklearn.cluster import KMeans - -logger = logging.getLogger(__name__) - - -def analyze_attributes(ding0_path): - """ - Calculates the attributes wind and solar capacity and farthest node - for all grids in ding0_path. Results are written to ding0_path - - # ToDo Revise! Which attributes do we want to cluster by and where can they be - # obtained from? eGon_data needs to be finished for this. - - Parameters - ---------- - ding0_path : :obj:`str` - Path to ding0 files - - """ - base_path = ding0_path - - not_found = [] - tccs = [] # Total Cumulative Capacity of Solar - tccw = [] # Total Cumulative Capacity of Wind - fnlvmv = [] # the Farthest Node in both networks (lv and mv) - MV_id_list = [] # Distrct id list - - for district_number in list(range(1, 4000)): - - try: - pickle_name = "ding0_grids__{}.pkl".format(district_number) - nd = pickle.load(open(os.path.join(base_path, pickle_name), "rb")) - print("District no.", district_number, "found!") - except: - not_found.append(district_number) - continue - - MV_id = 0 - MV_id = nd._mv_grid_districts[0].id_db - - mv_cum_solar_MV = 0 # Solar cumulative capacity in MV - mv_cum_wind_MV = 0 # Solar cumulative capacity in MV - - # cumulative capacity of solar and wind in MV - for geno in nd._mv_grid_districts[0].mv_grid.generators(): - if geno.type == "solar": - mv_cum_solar_MV += geno.capacity - if geno.type == "wind": - mv_cum_wind_MV += geno.capacity - - lvg = 0 - mv_cum_solar_LV = 0 - mv_cum_wind_LV = 0 - - # cumulative capacity of solar and wind in LV - for lvgs in nd._mv_grid_districts[0].lv_load_areas(): - for lvgs1 in lvgs.lv_grid_districts(): - lvg += len(list(lvgs1.lv_grid.generators())) - for deno in lvgs1.lv_grid.generators(): - if deno.type == "solar": - mv_cum_solar_LV += deno.capacity - if deno.type == "wind": - mv_cum_wind_LV += deno.capacity - - # Total solar cumulative capacity in lv and mv - total_cum_solar = mv_cum_solar_MV + mv_cum_solar_LV - # Total wind cumulative capacity in lv and mv - total_cum_wind = mv_cum_wind_MV + mv_cum_wind_LV - - # append to lists - tccs.append(total_cum_solar) - tccw.append(total_cum_wind) - - # The farthest node length from MV substation - from ding0.core.network.stations import LVStationDing0 - - tot_dist = [] - max_length = 0 - max_length_list = [] - max_of_max = 0 - - # make CB open (normal operation case) - nd.control_circuit_breakers(mode="open") - # setting the root to measure the path from - root_mv = nd._mv_grid_districts[0].mv_grid.station() - # 1st from MV substation to LV station node - # Iteration through nodes - for node2 in nd._mv_grid_districts[0].mv_grid._graph.nodes(): - # select only LV station nodes - if ( - isinstance(node2, LVStationDing0) - and not node2.lv_load_area.is_aggregated - ): - - length_from_MV_to_LV_station = 0 - # Distance from MV substation to LV station node - length_from_MV_to_LV_station = ( - nd._mv_grid_districts[0].mv_grid.graph_path_length( - node_source=node2, node_target=root_mv - ) - / 1000 - ) - - # Iteration through lv load areas - for lvgs in nd._mv_grid_districts[0].lv_load_areas(): - for lvgs1 in lvgs.lv_grid_districts(): - if lvgs1.lv_grid._station == node2: - root_lv = node2 # setting a new root - for node1 in lvgs1.lv_grid._graph.nodes(): - - length_from_LV_staion_to_LV_node = 0 - - # Distance from LV station to LV nodes - length_from_LV_staion_to_LV_node = ( - lvgs1.lv_grid.graph_path_length( - node_source=node1, node_target=root_lv - ) - / 1000 - ) - - length_from_LV_node_to_MV_substation = 0 - - # total distances in both grids MV and LV - length_from_LV_node_to_MV_substation = ( - length_from_MV_to_LV_station - + length_from_LV_staion_to_LV_node - ) - - # append the total distance to a list - tot_dist.append(length_from_LV_node_to_MV_substation) - if any(tot_dist): - max_length = max(tot_dist) - - # append max lengths of all grids to a list - max_length_list.append(max_length) - if any(max_length_list): - # to pick up max of max - max_of_max = max(max_length_list) - - fnlvmv.append(max_of_max) # append to a new list - MV_id_list.append(MV_id) # append the network id to a new list - - # export results to dataframes - d = { - "id": MV_id_list, - "Solar_cumulative_capacity": tccs, - "Wind_cumulative_capacity": tccw, - "The_Farthest_node": fnlvmv, - } # assign lists to columns - # not founded networks - are_not_found = {"District_files_that_are_not_found": not_found} - - df = pd.DataFrame(d) # dataframe for results - - # dataframe for not found files id - df_are_not_found = pd.DataFrame(are_not_found) - - # Exporting dataframe to CSV files - df.to_csv(base_path + "/" + "attributes.csv", sep=",") - df_are_not_found.to_csv(base_path + "/" + "Not_found_grids.csv", sep=",") - - -def cluster_mv_grids(no_grids, cluster_base): - """ - Clusters the MV grids based on the attributes, for a given number - of MV grids - - Parameters - ---------- - no_grids : int - Desired number of clusters (of MV grids) - - Returns - ------- - :pandas:`pandas.DataFrame` - Dataframe containing the clustered MV grids and their weightings - - """ - cluster_base_pu = pd.DataFrame() - - for attribute in cluster_base: - attribute_max = cluster_base[attribute].max() - cluster_base_pu[attribute] = cluster_base[attribute] / attribute_max - - id_ = [] - m = [] - for idx, row in cluster_base_pu.iterrows(): - id_.append(idx) - f = [] - for attribute in row: - f.append(attribute) - - m.append(f) - - X = np.array(m) - - logger.info("Used Clustering Attributes: \n {}".format(list(cluster_base.columns))) - - no_clusters = no_grids - - ran_state = 1808 - - # Starting KMeans clustering - kmeans = KMeans(n_clusters=no_clusters, random_state=ran_state) - - # Return a label for each point - cluster_labels = kmeans.fit_predict(X) - - # Centers of clusters - centroids = kmeans.cluster_centers_ - - id_clus_dist = {} - - # Iterate through each point in dataset array X - for i in range(len(X)): - clus = cluster_labels[i] # point's cluster id - cent = centroids[cluster_labels[i]] # Cluster's center coordinates - - # Distance from that point to cluster's center (3d coordinates) - dist = ( - (X[i][0] - centroids[clus][0]) ** 2 - + (X[i][1] - centroids[clus][1]) ** 2 - + (X[i][2] - centroids[clus][2]) ** 2 - ) ** (1 / 2) - - id_clus_dist.setdefault(clus, []).append({id_[i]: dist}) - - cluster_df = pd.DataFrame( - columns=[ - "no_of_points_per_cluster", - "cluster_percentage", - "the_selected_network_id", - "represented_grids", - ] - ) - cluster_df.index.name = "cluster_id" - - for key, value in id_clus_dist.items(): - no_points_clus = sum(1 for v in value if v) - # percentage of points per cluster - clus_perc = (no_points_clus / len(X)) * 100 - - id_dist = {} - for value_1 in value: - id_dist.update(value_1) - - # returns the shortest distance point (selected network) - short_dist_net_id_dist = min(id_dist.items(), key=lambda x: x[1]) - - cluster_df.loc[key] = [ - no_points_clus, - round(clus_perc, 2), - short_dist_net_id_dist[0], - list(id_dist.keys()), - ] - - return cluster_df From fedd5abf7340948a71f4025b64517faa7c14aa74 Mon Sep 17 00:00:00 2001 From: Malte Jahn Date: Thu, 23 Feb 2023 09:33:32 +0100 Subject: [PATCH 031/126] Add examples for the clustering --- ego/examples/__init__.py | 0 .../mv_clustering/cluster_setting.json | 86 +++++++++++++++++++ .../mv_clustering/example_clustering.py | 29 +++++++ .../example_clustering_full_workflow.py | 35 ++++++++ .../example_database_connection.py | 42 +++++++++ 5 files changed, 192 insertions(+) delete mode 100644 ego/examples/__init__.py create mode 100644 ego/examples/mv_clustering/cluster_setting.json create mode 100644 ego/examples/mv_clustering/example_clustering.py create mode 100644 ego/examples/mv_clustering/example_clustering_full_workflow.py create mode 100644 ego/examples/mv_clustering/example_database_connection.py diff --git a/ego/examples/__init__.py b/ego/examples/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/ego/examples/mv_clustering/cluster_setting.json b/ego/examples/mv_clustering/cluster_setting.json new file mode 100644 index 00000000..2e59cf01 --- /dev/null +++ b/ego/examples/mv_clustering/cluster_setting.json @@ -0,0 +1,86 @@ +{ + "eGo": { + "eTraGo": true, + "eDisGo": true, + "csv_import_eTraGo": false, + "csv_import_eDisGo": false, + "random_seed": 42 + }, + "eTraGo": { + "db": "oedb", + "gridversion": "v0.4.5", + "method": "lopf", + "pf_post_lopf": false, + "start_snapshot": 1000, + "end_snapshot" : 1005, + "solver": "gurobi", + "solver_options":{}, + "scn_name": "eGo 100", + "scn_extension": null, + "scn_decommissioning": null, + "lpfile": false, + "csv_export": "results/your_results", + "db_export": false, + "extendable": ["storage", "network"], + "generator_noise": 789456, + "minimize_loading": false, + "ramp_limits": false, + "extra_functionality": null, + "network_clustering_kmeans": 10, + "load_cluster": false, + "network_clustering_ehv": false, + "disaggregation": "uniform", + "snapshot_clustering": false, + "parallelisation": false, + "skip_snapshots": false, + "line_grouping": false, + "branch_capacity_factor": {"HV": 0.5, "eHV" : 0.7}, + "load_shedding": false, + "foreign_lines" :{"carrier": "AC", "capacity": "osmTGmod"}, + "comments": "" + }, + "eDisGo": { + "db": "oedb", + "gridversion": "v0.5.0", + "ding0_path": "/home/local/RL-INSTITUT/malte.jahn/git-repos/data/ego/ding0_path", + "choice_mode": "cluster", + "cluster_attributes":["wind_cap", "solar_cap", "emob_cap"], + "only_cluster": false, + "manual_grids": [3280], + "n_clusters": 50, + "parallelization":false, + "max_calc_time": 0.5, + "max_workers":2, + "initial_reinforcement":true, + "max_cos_phi_renewable": 0.9, + "results": "/home/local/RL-INSTITUT/malte.jahn/git-repos/data/ego", + "solver": "glpk", + "timesteps_pfa": "snapshot_analysis" + }, + "mv_clustering": { + "database": { + "database_name": "egon-data", + "host": "127.0.0.1", + "port": "port", + "user": "username", + "password": "password", + "tables": { + "egon_mv_grid_district": "grid.egon_mv_grid_district", + "egon_hvmv_substation": "grid.egon_hvmv_substation", + "generators_pv": "supply.egon_power_plants_pv", + "generators_pv_rooftop": "supply.egon_power_plants_pv_roof_building", + "generators_wind": "supply.egon_power_plants_wind", + "etrago_load": "grid.egon_etrago_load", + "etrago_load_timeseries": "grid.egon_etrago_load_timeseries" + }, + "ssh": { + "enabled": true, + "user": "username", + "ip": "ip", + "port": "port", + "local_address": "127.0.0.1", + "local_port": "local_port" + } + } + } +} diff --git a/ego/examples/mv_clustering/example_clustering.py b/ego/examples/mv_clustering/example_clustering.py new file mode 100644 index 00000000..2d1b9b63 --- /dev/null +++ b/ego/examples/mv_clustering/example_clustering.py @@ -0,0 +1,29 @@ +# flake8: noqa: E402 +import warnings + +warnings.filterwarnings("ignore") + +import os + +from edisgo.tools.logger import setup_logger + +from ego.mv_clustering import cluster_workflow +from ego.tools.utilities import get_scenario_setting + +setup_logger( + loggers=[ + {"name": "root", "file_level": None, "stream_level": "warning"}, + {"name": "ego", "file_level": None, "stream_level": "debug"}, + {"name": "edisgo", "file_level": None, "stream_level": "info"}, + ] +) + +base_path = os.path.join(os.path.expanduser("~"), "git-repos", "data", "ego") + +os.remove(os.path.join(base_path, "ding0_path", "attributes.csv")) + +config = get_scenario_setting(jsonpath="cluster_setting.json") + +cluster_workflow(config=config) + +print("THE END") diff --git a/ego/examples/mv_clustering/example_clustering_full_workflow.py b/ego/examples/mv_clustering/example_clustering_full_workflow.py new file mode 100644 index 00000000..06e4a07e --- /dev/null +++ b/ego/examples/mv_clustering/example_clustering_full_workflow.py @@ -0,0 +1,35 @@ +# flake8: noqa: E402 +import warnings + +warnings.filterwarnings("ignore") + +import os + +from edisgo.tools.logger import setup_logger +from pypsa import Network as PyPSANetwork + +from ego.tools.edisgo_integration import EDisGoNetworks +from ego.tools.utilities import get_scenario_setting + +setup_logger( + loggers=[ + {"name": "root", "file_level": None, "stream_level": "warning"}, + {"name": "ego", "file_level": None, "stream_level": "debug"}, + {"name": "edisgo", "file_level": None, "stream_level": "info"}, + ] +) + +base_path = os.path.join(os.path.expanduser("~"), "git-repos", "data", "ego") +config = get_scenario_setting(jsonpath="cluster_setting.json") + +pf_post_lopf = config["eTraGo"]["pf_post_lopf"] + +if pf_post_lopf: + file_name = "etrago_disaggregated_pf_post_lopf_true" +else: + file_name = "etrago_disaggregated_pf_post_lopf_false" + +etrago_network = PyPSANetwork(os.path.join(base_path, file_name)) +edisgo_networks = EDisGoNetworks(json_file=config, etrago_network=etrago_network) + +print("THE END") diff --git a/ego/examples/mv_clustering/example_database_connection.py b/ego/examples/mv_clustering/example_database_connection.py new file mode 100644 index 00000000..94e7b500 --- /dev/null +++ b/ego/examples/mv_clustering/example_database_connection.py @@ -0,0 +1,42 @@ +# flake8: noqa: E402 +import warnings + +warnings.filterwarnings("ignore") + +import os + +import pandas as pd + +from edisgo.tools.logger import setup_logger + +import ego.mv_clustering.egon_data_io as db_io + +from ego.mv_clustering.database import get_engine, register_tables_in_saio, sshtunnel +from ego.tools.utilities import get_scenario_setting + +setup_logger( + loggers=[ + {"name": "root", "file_level": None, "stream_level": "warning"}, + {"name": "ego", "file_level": None, "stream_level": "debug"}, + {"name": "edisgo", "file_level": None, "stream_level": "info"}, + ] +) +base_path = os.path.join(os.path.expanduser("~"), "git-repos", "data", "ego") +config = get_scenario_setting(jsonpath="cluster_setting.json") + +with sshtunnel(config=config): + engine = get_engine(config=config) + orm = register_tables_in_saio(engine, config=config) + + grid_ids_df = db_io.get_grid_ids(engine=engine, orm=orm) + solar_capacity_df = db_io.get_solar_capacity(engine=engine, orm=orm) + wind_capacity_df = db_io.get_wind_capacity(engine=engine, orm=orm) + emobility_capacity_df = db_io.get_emobility_capacity(engine=engine, orm=orm) + + df = pd.concat( + [grid_ids_df, solar_capacity_df, wind_capacity_df, emobility_capacity_df], + axis="columns", + ) + df.fillna(0, inplace=True) + + print("THE END") From fc4707f36e28f9f9bc5a717fe673c55905b9aeb7 Mon Sep 17 00:00:00 2001 From: Malte Jahn Date: Thu, 23 Feb 2023 10:28:38 +0100 Subject: [PATCH 032/126] Rename variables, use edisgo grid instead of ding0 grids - Use in edisgo pre edited grid instead of ding0 grids. - Rename ding0_path to grid_path --- doc/api/modules.rst | 6 +++--- ego/examples/mv_clustering/cluster_setting.json | 2 +- ego/examples/mv_clustering/example_clustering.py | 2 +- ego/examples/scenario_setting.json | 2 +- ego/mv_clustering/mv_clustering.py | 4 ++-- ego/scenario_setting.json | 2 +- ego/tools/edisgo_integration.py | 13 ++++++------- 7 files changed, 15 insertions(+), 16 deletions(-) diff --git a/doc/api/modules.rst b/doc/api/modules.rst index f613d4c1..7a416ba0 100644 --- a/doc/api/modules.rst +++ b/doc/api/modules.rst @@ -51,12 +51,12 @@ The file can be found on :property string db: Name of your database (e.g.``''oedb''``). eDisGo queries generator data from this database. Please note that this parameters is automatically overwritten in eDisGo's configuration files. :property string gridversion: ``null`` or *open_eGo* dataset version (e.g. ``''v0.4.5''``). If ``null``, *open_eGo*'s model_draft is used. Please note that this parameters is automatically overwritten in eDisGo's configuration files. - :property string ding0_path: Path to the MV grid files (created by `ding0 `_) (e.g. ``''data/MV_grids/20180713110719''``) - :property string choice_mode: Mode that eGo uses to chose MV grids out of the files in **ding0_path** (e.g. ``''manual''``, ``''cluster''`` or ``''all''``). If ``''manual''`` is chosen, the parameter **manual_grids** must contain a list of the desired grids. If ``''cluster''`` is chosen, **no_grids** must specify the desired number of clusters and **cluster_attributes** must specify the applied cluster attributes. If ``''all''`` is chosen, all MV grids from **ding0_path** are calculated. + :property string grid_path: Path to the MV grid files (created by `ding0 `_) (e.g. ``''data/MV_grids/20180713110719''``) + :property string choice_mode: Mode that eGo uses to chose MV grids out of the files in **grid_path** (e.g. ``''manual''``, ``''cluster''`` or ``''all''``). If ``''manual''`` is chosen, the parameter **manual_grids** must contain a list of the desired grids. If ``''cluster''`` is chosen, **no_grids** must specify the desired number of clusters and **cluster_attributes** must specify the applied cluster attributes. If ``''all''`` is chosen, all MV grids from **grid_path** are calculated. :property list cluster_attributes: List of strings containing the desired cluster attributes. Available attributes are: ``''farthest_node''``, ``''wind_cap''``, ``''solar_cap''`` and ``''extended_storage''``, thus an exemplary list looks like ``["farthest_node", "wind_cap", "solar_cap", "extended_storage"]``. ``''farthest_node''`` represents the longest path within each grid, ``''wind_cap''`` the installed wind capacity within each grid, ``''solar_cap''`` the installed solar capacity within each grid and ``''extended_storage''`` the installed storage units (as calculated by eTraGo). Please note that ``''extended_storage''`` is only available in combination with eTraGo datasets that optimized storage extension. Otherwise this attribute is ignored. :property bool only_cluster: If ``true``, eGo only identifies cluster results, but performs no eDisGo run. Please note that for **only_cluster** an eTraGo run or dataset must be provided. :property list manual_grids: List of MV grid ID's (*open_eGo* HV/MV substation ID's) is case of **choice_mode** = ``''manual''`` (e.g. ``[1718,1719]``). Ohterwise this parameter is ignored. - :property int no_grids: Number of MV grid clusters (from all grids in **ding0_path**, a specified number of representative clusters is calculated) in case of **choice_mode** = ``''cluster''``. Otherwise this parameter is ignored. + :property int no_grids: Number of MV grid clusters (from all grids in **grid_path**, a specified number of representative clusters is calculated) in case of **choice_mode** = ``''cluster''``. Otherwise this parameter is ignored. :property bool parallelization: If ``false``, eDisgo is used in a consecutive way (this may take very long time). In order to increase the performance of MV grid simulations, ``true`` allows the parallel calculation of MV grids. If **parallelization** = ``true``, **max_calc_time** and **max_workers** must be specified. :property float max_calc_time: Maximum calculation time in hours for eDisGo simulations. The calculation is terminated after this time and all costs are extrapolated based on the unfinished simulation. Please note that this parameter is only used if **parallelization** = ``true``. :property ing max_workers: Number of workers (cpus) that are allocated to the simulation. If the given value exceeds the number of available workers, it is reduced to the number of available workers. Please note that this parameter is only used if **parallelization** = ``true``. diff --git a/ego/examples/mv_clustering/cluster_setting.json b/ego/examples/mv_clustering/cluster_setting.json index 2e59cf01..9afa2e1d 100644 --- a/ego/examples/mv_clustering/cluster_setting.json +++ b/ego/examples/mv_clustering/cluster_setting.json @@ -42,7 +42,7 @@ "eDisGo": { "db": "oedb", "gridversion": "v0.5.0", - "ding0_path": "/home/local/RL-INSTITUT/malte.jahn/git-repos/data/ego/ding0_path", + "grid_path": "/home/local/RL-INSTITUT/malte.jahn/git-repos/data/ego/grids", "choice_mode": "cluster", "cluster_attributes":["wind_cap", "solar_cap", "emob_cap"], "only_cluster": false, diff --git a/ego/examples/mv_clustering/example_clustering.py b/ego/examples/mv_clustering/example_clustering.py index 2d1b9b63..37e5ed54 100644 --- a/ego/examples/mv_clustering/example_clustering.py +++ b/ego/examples/mv_clustering/example_clustering.py @@ -20,7 +20,7 @@ base_path = os.path.join(os.path.expanduser("~"), "git-repos", "data", "ego") -os.remove(os.path.join(base_path, "ding0_path", "attributes.csv")) +os.remove(os.path.join(base_path, "grids", "attributes.csv")) config = get_scenario_setting(jsonpath="cluster_setting.json") diff --git a/ego/examples/scenario_setting.json b/ego/examples/scenario_setting.json index 15513362..b2e201fc 100644 --- a/ego/examples/scenario_setting.json +++ b/ego/examples/scenario_setting.json @@ -41,7 +41,7 @@ "eDisGo": { "db": "oedb", "gridversion": "v0.4.5", - "ding0_path": "/home/birgit/virtualenvs/eGo_interface_development/git_repos/data/ding0_grids", + "grid_path": "/home/birgit/virtualenvs/eGo_interface_development/git_repos/data/ding0_grids", "choice_mode": "manual", "cluster_attributes":["farthest_node", "wind_cap", "solar_cap", "extended_storage"], "only_cluster": false, diff --git a/ego/mv_clustering/mv_clustering.py b/ego/mv_clustering/mv_clustering.py index a69a917c..7b434895 100644 --- a/ego/mv_clustering/mv_clustering.py +++ b/ego/mv_clustering/mv_clustering.py @@ -169,9 +169,9 @@ def mv_grid_clustering(data_df, working_grids=None, config=None): def cluster_workflow(config=None): - attributes_path = os.path.join(config["eDisGo"]["ding0_path"], "attributes.csv") + attributes_path = os.path.join(config["eDisGo"]["grid_path"], "attributes.csv") working_grids_path = os.path.join( - config["eDisGo"]["ding0_path"], "working_grids.csv" + config["eDisGo"]["grid_path"], "working_grids.csv" ) if not os.path.isfile(attributes_path): diff --git a/ego/scenario_setting.json b/ego/scenario_setting.json index 613ab50a..d5ebde61 100644 --- a/ego/scenario_setting.json +++ b/ego/scenario_setting.json @@ -41,7 +41,7 @@ "eDisGo": { "db": "oedb", "gridversion": "v0.4.5", - "ding0_path": "/path/to_your/.dingo/grids", + "grid_path": "/path/to_your/.dingo/grids", "choice_mode": "cluster", "cluster_attributes":["farthest_node", "wind_cap", "solar_cap", "extended_storage"], "only_cluster": false, diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 53d76d39..307696cb 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -49,7 +49,7 @@ if "READTHEDOCS" not in os.environ: - from edisgo.edisgo import EDisGo, import_edisgo_from_files + from edisgo.edisgo import import_edisgo_from_files from edisgo.flex_opt import q_control from edisgo.network.results import Results from edisgo.network.timeseries import TimeSeries @@ -523,7 +523,7 @@ def _set_scenario_settings(self): self._grid_version = self._edisgo_args["gridversion"] self._timesteps_pfa = self._edisgo_args["timesteps_pfa"] self._solver = self._edisgo_args["solver"] - self._ding0_path = self._edisgo_args["ding0_path"] + self._grid_path = self._edisgo_args["grid_path"] self._choice_mode = self._edisgo_args["choice_mode"] self._parallelization = self._edisgo_args["parallelization"] self._initial_reinforcement = self._edisgo_args["initial_reinforcement"] @@ -634,7 +634,7 @@ def _check_available_mv_grids(self): """ mv_grids = [] - for file in os.listdir(self._ding0_path): + for file in os.listdir(self._grid_path): if file.endswith(".pkl"): mv_grids.append( int(file.replace("ding0_grids__", "").replace(".pkl", "")) @@ -796,9 +796,9 @@ def _run_edisgo(self, mv_grid_id): Session.remove() # get ding0 MV grid path - ding0_grid_path = os.path.join(self._ding0_path, str(mv_grid_id)) + grid_path = os.path.join(self._grid_path, "working_grids", str(mv_grid_id)) - if not os.path.isdir(ding0_grid_path): + if not os.path.isdir(grid_path): msg = "No grid data for MV grid {}".format(mv_grid_id) logger.error(msg) raise Exception(msg) @@ -806,8 +806,7 @@ def _run_edisgo(self, mv_grid_id): # Initialize MV grid logger.info(f"MV grid {mv_grid_id}: Initialize MV grid") - edisgo_grid = EDisGo(ding0_grid=ding0_grid_path) - + edisgo_grid = import_edisgo_from_files(edisgo_path=grid_path) # ##################### Conduct initial grid reinforcement #################### edisgo_grid.set_time_series_worst_case_analysis() From 1b0e5ba49671cea8ff89d599db0f9a252bdb9a89 Mon Sep 17 00:00:00 2001 From: Malte Jahn Date: Sun, 26 Feb 2023 15:04:55 +0100 Subject: [PATCH 033/126] Refactor interface and clustering - add arguments for example_clustering_full_workflow.py for lopf and post lopf - add rename some variables - refactor pf post lopf mode - add 0s in results if for the bus_id component doesn't exist - add function to search weather_id for the subst_id --- .../mv_clustering/cluster_setting.json | 3 +- .../example_clustering_full_workflow.py | 15 +- .../example_database_connection.py | 2 +- ego/mv_clustering/egon_data_io.py | 27 +- ego/mv_clustering/mv_clustering.py | 15 +- ego/tools/edisgo_integration.py | 58 +- ego/tools/interface.py | 779 +++++++++--------- tests/tools/test_interface.py | 14 +- 8 files changed, 455 insertions(+), 458 deletions(-) diff --git a/ego/examples/mv_clustering/cluster_setting.json b/ego/examples/mv_clustering/cluster_setting.json index 9afa2e1d..a21b7cd2 100644 --- a/ego/examples/mv_clustering/cluster_setting.json +++ b/ego/examples/mv_clustering/cluster_setting.json @@ -71,7 +71,8 @@ "generators_pv_rooftop": "supply.egon_power_plants_pv_roof_building", "generators_wind": "supply.egon_power_plants_wind", "etrago_load": "grid.egon_etrago_load", - "etrago_load_timeseries": "grid.egon_etrago_load_timeseries" + "etrago_load_timeseries": "grid.egon_etrago_load_timeseries", + "weather_cells": "supply.egon_era5_weather_cells" }, "ssh": { "enabled": true, diff --git a/ego/examples/mv_clustering/example_clustering_full_workflow.py b/ego/examples/mv_clustering/example_clustering_full_workflow.py index 06e4a07e..1c39922f 100644 --- a/ego/examples/mv_clustering/example_clustering_full_workflow.py +++ b/ego/examples/mv_clustering/example_clustering_full_workflow.py @@ -4,10 +4,12 @@ warnings.filterwarnings("ignore") import os +import sys from edisgo.tools.logger import setup_logger from pypsa import Network as PyPSANetwork +from ego.mv_clustering.database import sshtunnel from ego.tools.edisgo_integration import EDisGoNetworks from ego.tools.utilities import get_scenario_setting @@ -19,10 +21,18 @@ ] ) +if (sys.argv[1] == "--pf_post_lopf") and (sys.argv[2] == "true"): + pf_post_lopf = True +elif (sys.argv[1] == "--pf_post_lopf") and (sys.argv[2] == "false"): + pf_post_lopf = False +else: + pf_post_lopf = False + print(f"Warning: Variable pf_post_lopf is not configured, set it to {False}.") + base_path = os.path.join(os.path.expanduser("~"), "git-repos", "data", "ego") config = get_scenario_setting(jsonpath="cluster_setting.json") -pf_post_lopf = config["eTraGo"]["pf_post_lopf"] +config["eTraGo"]["pf_post_lopf"] = pf_post_lopf if pf_post_lopf: file_name = "etrago_disaggregated_pf_post_lopf_true" @@ -30,6 +40,7 @@ file_name = "etrago_disaggregated_pf_post_lopf_false" etrago_network = PyPSANetwork(os.path.join(base_path, file_name)) -edisgo_networks = EDisGoNetworks(json_file=config, etrago_network=etrago_network) +with sshtunnel(config=config): + edisgo_networks = EDisGoNetworks(json_file=config, etrago_network=etrago_network) print("THE END") diff --git a/ego/examples/mv_clustering/example_database_connection.py b/ego/examples/mv_clustering/example_database_connection.py index 94e7b500..4710836c 100644 --- a/ego/examples/mv_clustering/example_database_connection.py +++ b/ego/examples/mv_clustering/example_database_connection.py @@ -31,7 +31,7 @@ grid_ids_df = db_io.get_grid_ids(engine=engine, orm=orm) solar_capacity_df = db_io.get_solar_capacity(engine=engine, orm=orm) wind_capacity_df = db_io.get_wind_capacity(engine=engine, orm=orm) - emobility_capacity_df = db_io.get_emobility_capacity(engine=engine, orm=orm) + emobility_capacity_df = db_io.get_emob_capacity(engine=engine, orm=orm) df = pd.concat( [grid_ids_df, solar_capacity_df, wind_capacity_df, emobility_capacity_df], diff --git a/ego/mv_clustering/egon_data_io.py b/ego/mv_clustering/egon_data_io.py index ec83a77d..a4e5ae07 100644 --- a/ego/mv_clustering/egon_data_io.py +++ b/ego/mv_clustering/egon_data_io.py @@ -108,7 +108,7 @@ def get_wind_capacity(orm=None, session=None): @session_decorator -def get_emobility_capacity(orm=None, session=None): +def get_emob_capacity(orm=None, session=None): load_timeseries_nested = ( session.query( orm["etrago_load_timeseries"].scn_name, @@ -155,13 +155,32 @@ def get_emobility_capacity(orm=None, session=None): ).group_by( load_timeseries_maximal.c.bus_id, ) - emobility_capacity_df = pd.read_sql( + emob_capacity_df = pd.read_sql( sql=load_p_nom.statement, con=session.bind, index_col=None ) - emobility_capacity_df.set_index("bus_id", inplace=True) - return emobility_capacity_df + emob_capacity_df.set_index("bus_id", inplace=True) + return emob_capacity_df @session_decorator def get_cummulative_storage_capacity(bus_id_list, orm=None, session=None): return + + +@session_decorator +def get_weather_id_for_generator(bus_id, orm=None, session=None): + query = ( + session.query( + orm["egon_hvmv_substation"].bus_id, + orm["weather_cells"].w_id, + ) + .join( + orm["egon_hvmv_substation"], + func_within(orm["egon_hvmv_substation"].point, orm["weather_cells"].geom), + ) + .filter( + orm["egon_hvmv_substation"].bus_id == bus_id, + ) + ) + weather_id = query.all()[0][1] + return weather_id diff --git a/ego/mv_clustering/mv_clustering.py b/ego/mv_clustering/mv_clustering.py index 7b434895..2659fa7d 100644 --- a/ego/mv_clustering/mv_clustering.py +++ b/ego/mv_clustering/mv_clustering.py @@ -69,10 +69,10 @@ def cluster_attributes_to_csv(attributes_path, config=None): grid_ids_df = db_io.get_grid_ids(engine=engine, orm=orm) solar_capacity_df = db_io.get_solar_capacity(engine=engine, orm=orm) wind_capacity_df = db_io.get_wind_capacity(engine=engine, orm=orm) - emobility_capacity_df = db_io.get_emobility_capacity(engine=engine, orm=orm) + emob_capacity_df = db_io.get_emob_capacity(engine=engine, orm=orm) df = pd.concat( - [grid_ids_df, solar_capacity_df, wind_capacity_df, emobility_capacity_df], + [grid_ids_df, solar_capacity_df, wind_capacity_df, emob_capacity_df], axis="columns", ) df.fillna(0, inplace=True) @@ -87,12 +87,15 @@ def mv_grid_clustering(data_df, working_grids=None, config=None): Parameters ---------- - n_cluster : int - Desired number of clusters (of MV grids) - + config : dict + Config dict. + working_grids : pandas.DataFrame + DataFrame of working grids, only working grids are used as cluster centers. + data_df : pandas.DataFrame + Attributes to cluster. Returns ------- - :pandas:`pandas.DataFrame` + pandas.DataFrame Dataframe containing the clustered MV grids and their weightings """ diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 307696cb..fa3c894a 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -58,10 +58,10 @@ from egoio.tools import db from ego.mv_clustering import cluster_workflow + from ego.mv_clustering.database import get_engine, register_tables_in_saio from ego.tools.economics import edisgo_grid_investment from ego.tools.interface import ETraGoMinimalData, get_etrago_results_per_bus - # Logging logger = logging.getLogger(__name__) @@ -775,25 +775,18 @@ def _run_edisgo(self, mv_grid_id): logger.info("MV grid {}: Calculating interface values".format(mv_grid_id)) - conn = db.connection(readonly=True) - session_factory = sessionmaker(bind=conn) - Session = scoped_session(session_factory) - session = Session() - - # Query bus ID for this MV grid - bus_id = self._get_bus_id_from_mv_grid(session, mv_grid_id) - + config = self._json_file + engine = get_engine(config=config) + orm = register_tables_in_saio(engine, config=config) # Calculate Interface values for this MV grid specs = get_etrago_results_per_bus( - session, - bus_id, + mv_grid_id, self._etrago_network, - self._scn_name, - self._grid_version, self._pf_post_lopf, self._max_cos_phi_renewable, + engine=engine, + orm=orm, ) - Session.remove() # get ding0 MV grid path grid_path = os.path.join(self._grid_path, "working_grids", str(mv_grid_id)) @@ -1145,43 +1138,6 @@ def _get_mv_grid_from_bus_id(self, session, bus_id): return subst_id - def _get_bus_id_from_mv_grid(self, session, subst_id): - """ - Queries the eTraGo bus ID for given MV grid (ding0) ID - - Parameters - ---------- - subst_id : int - MV grid (ding0) ID - - Returns - ------- - int - eTraGo bus ID - - """ - - if self._versioned is True: - ormclass_hvmv_subst = grid.__getattribute__("EgoDpHvmvSubstation") - bus_id = ( - session.query(ormclass_hvmv_subst.otg_id) - .filter( - ormclass_hvmv_subst.subst_id == subst_id, - ormclass_hvmv_subst.version == self._grid_version, - ) - .scalar() - ) - - if self._versioned is False: - ormclass_hvmv_subst = model_draft.__getattribute__("EgoGridHvmvSubstation") - bus_id = ( - session.query(ormclass_hvmv_subst.otg_id) - .filter(ormclass_hvmv_subst.subst_id == subst_id) - .scalar() - ) - - return bus_id - class _ETraGoData: """ diff --git a/ego/tools/interface.py b/ego/tools/interface.py index 5155d8d7..cc6c220c 100644 --- a/ego/tools/interface.py +++ b/ego/tools/interface.py @@ -25,20 +25,15 @@ __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolf_bunke,maltesc,mltja" +import logging import math - -# Import -# General Packages import os import time import pandas as pd if "READTHEDOCS" not in os.environ: - from egoio.db_tables import model_draft - from egoio.db_tables import supply - -import logging + import ego.mv_clustering.egon_data_io as db_io logger = logging.getLogger(__name__) @@ -106,55 +101,19 @@ def set_filtered_attribute(etrago_network_obj, component): self.snapshots = etrago_network.snapshots components = ["storage_units", "stores", "generators", "links", "loads"] - for component in components: - set_filtered_attribute(etrago_network, component) - - logger.info(f"Data selection time {time.perf_counter()-t_start}") - - -# Functions -def get_weather_id_for_generator(grid_version, session, generator_index, scn_name): - # ToDo: Refactor function - if grid_version is None: - logger.warning("Weather_id taken from model_draft (not tested)") - - ormclass_gen_single = model_draft.__getattribute__("EgoSupplyPfGeneratorSingle") - - weather_id = ( - session.query(ormclass_gen_single.w_id) - .filter( - ormclass_gen_single.aggr_id == generator_index, - ormclass_gen_single.scn_name == scn_name, - ) - .limit(1) - .scalar() - ) - - else: - ormclass_aggr_w = supply.__getattribute__("EgoAggrWeather") - - weather_id = ( - session.query(ormclass_aggr_w.w_id) - .filter( - ormclass_aggr_w.aggr_id == generator_index, - # ormclass_aggr_w.scn_name == scn_name, - ormclass_aggr_w.version == grid_version, - ) - .limit(1) - .scalar() - ) + for selected_component in components: + set_filtered_attribute(etrago_network, selected_component) - return weather_id + logger.info(f"Data selection time {time.perf_counter() - t_start}") def get_etrago_results_per_bus( - session, bus_id, - etrago_network, - grid_version, - scn_name, + etrago_obj, pf_post_lopf, - max_cos_phi_renewable, + max_cos_phi_ren, + engine=None, + orm=None, ): """ Reads eTraGo Results from Database and returns @@ -162,17 +121,17 @@ def get_etrago_results_per_bus( Parameters ---------- - session : sqlalchemy.orm.session.Session - Handles conversations with the database. + engine: + Engine of the database. + orm: + Object relational model dict. bus_id : int ID of the corresponding HV bus - etrago_network: :class:`etrago.tools.io.NetworkScenario` + etrago_obj: :class:`etrago.tools.io.NetworkScenario` eTraGo network object compiled by :meth:`etrago.appl.etrago` - scn_name : str - Name of used scenario 'Status Quo', 'NEP 2035' or 'eGo 100' pf_post_lopf : bool Variable if pf after lopf was run. - max_cos_phi_renewable : float or None + max_cos_phi_ren : float or None If not None, the maximum reactive power is set by the given power factor according to the dispatched active power. @@ -308,67 +267,20 @@ def get_etrago_results_per_bus( Unit: MVar """ - performance = {} - t0 = time.perf_counter() - - logger.info("Specs for bus {}".format(bus_id)) - if pf_post_lopf: - logger.info("Active and reactive power interface") - else: - logger.info("Only active power interface") - - etrago_results_per_bus = {} - timeseries_index = etrago_network.snapshots - etrago_results_per_bus["timeindex"] = timeseries_index - # Prefill dict with None - result_keys = [ - "dispatchable_generators_active_power", - "dispatchable_generators_reactive_power", - "renewables_potential", - "renewables_curtailment", - "renewables_dispatch_reactive_power", - "storage_units_capacity", - "storage_units_active_power", - "storage_units_reactive_power", - "dsm_active_power", - "dsm_reactive_power", - "heat_central_active_power", - "heat_central_reactive_power", - "thermal_storage_central_capacity", - "geothermal_energy_feedin_district_heating", - "solarthermal_energy_feedin_district_heating", - "heat_pump_rural_active_power", - "heat_pump_rural_reactive_power", - "thermal_storage_rural_capacity", - "electromobility_active_power", - "electromobility_reactive_power", - ] - for key in result_keys: - etrago_results_per_bus[key] = None - - # Filter dataframes - # Generators - generators_df = etrago_network.generators[ - etrago_network.generators["bus"] == str(bus_id) - ] - # Links - links_df = etrago_network.links[ - (etrago_network.links["bus0"] == str(bus_id)) - | (etrago_network.links["bus1"] == str(bus_id)) - ] + # Defining inner functions - def dispatchable_generators(): + def dispatchable_gens(): # Dispatchable generators - dispatchable_generators_df_p = pd.DataFrame(index=timeseries_index) - dispatchable_generators_df_q = pd.DataFrame(index=timeseries_index) + dispatchable_gens_df_p = pd.DataFrame(index=timeseries_index) + dispatchable_gens_df_q = pd.DataFrame(index=timeseries_index) - dispatchable_generators = [ + dispatchable_gens_carriers = [ # "CH4", # "CH4_NG", # "CH4_biogas", "biomass", "central_biomass_CHP", - "central_biomass_CHP_heat", + # "central_biomass_CHP_heat", # "coal", # "geo_thermal", "industrial_biomass_CHP", @@ -384,66 +296,93 @@ def dispatchable_generators(): # "wind_offshore", # "wind_onshore", ] - dispatchable_generators_df = generators_df[ - generators_df["carrier"].isin(dispatchable_generators) + # Filter generators_df for selected carriers. + dispatchable_gens_df = generators_df[ + generators_df["carrier"].isin(dispatchable_gens_carriers) ] - if not dispatchable_generators_df.empty: - for carrier in dispatchable_generators_df["carrier"].unique(): - p_nom = dispatchable_generators_df.loc[ - dispatchable_generators_df["carrier"] == carrier, "p_nom" + for carrier in dispatchable_gens_carriers: + if not dispatchable_gens_df[ + dispatchable_gens_df["carrier"] == carrier + ].empty: + p_nom = dispatchable_gens_df.loc[ + dispatchable_gens_df["carrier"] == carrier, "p_nom" ].sum() - columns_to_aggregate = dispatchable_generators_df[ - dispatchable_generators_df["carrier"] == carrier + columns_to_aggregate = dispatchable_gens_df[ + dispatchable_gens_df["carrier"] == carrier ].index - dispatchable_generators_df_p[carrier] = ( - etrago_network.generators_t["p"][columns_to_aggregate].sum( + dispatchable_gens_df_p[carrier] = ( + etrago_obj.generators_t["p"][columns_to_aggregate].sum( axis="columns" ) / p_nom ) if pf_post_lopf: - dispatchable_generators_df_q[carrier] = ( - etrago_network.generators_t["q"][columns_to_aggregate].sum( + dispatchable_gens_df_q[carrier] = ( + etrago_obj.generators_t["q"][columns_to_aggregate].sum( axis="columns" ) / p_nom ) else: - dispatchable_generators_df_q[carrier] = ( - etrago_network.generators_t["q"][columns_to_aggregate].sum( - axis="columns" - ) - / p_nom + dispatchable_gens_df_q[carrier] = pd.Series( + data=0, index=timeseries_index, dtype=float ) + else: + dispatchable_gens_df_p[carrier] = pd.Series( + data=0, index=timeseries_index, dtype=float + ) + dispatchable_gens_df_q[carrier] = pd.Series( + data=0, index=timeseries_index, dtype=float + ) # Add CHP to conventional generators - chp_df = links_df[links_df["carrier"] == "central_gas_CHP"] + if pf_post_lopf: + chp_df = generators_df[generators_df["carrier"] == "central_gas_CHP"] + else: + chp_df = links_df[links_df["carrier"] == "central_gas_CHP"] if not chp_df.empty: p_nom = chp_df["p_nom"].sum() - dispatchable_generators_df_p["central_gas_CHP"] = ( - etrago_network.links_t["p1"][chp_df.index].sum(axis="columns") / p_nom - ) - if not pf_post_lopf: - dispatchable_generators_df_q["central_gas_CHP"] = ( - 0 * dispatchable_generators_df_p["central_gas_CHP"] + if pf_post_lopf: + dispatchable_gens_df_p["central_gas_CHP"] = ( + etrago_obj.generators_t["p"][chp_df.index].sum(axis="columns") + / p_nom + ) + dispatchable_gens_df_q["central_gas_CHP"] = ( + etrago_obj.generators_t["q"][chp_df.index].sum(axis="columns") + / p_nom ) else: - dispatchable_generators_df_q["central_gas_CHP"] = ( - 0 * dispatchable_generators_df_p["central_gas_CHP"] + dispatchable_gens_df_p["central_gas_CHP"] = ( + etrago_obj.links_t["p1"][chp_df.index].sum(axis="columns") / p_nom ) + dispatchable_gens_df_q["central_gas_CHP"] = pd.Series( + data=0, index=timeseries_index, dtype=float + ) + else: + dispatchable_gens_df_p["central_gas_CHP"] = pd.Series( + data=0, index=timeseries_index, dtype=float + ) + dispatchable_gens_df_q["central_gas_CHP"] = pd.Series( + data=0, index=timeseries_index, dtype=float + ) - etrago_results_per_bus[ - "dispatchable_generators_active_power" - ] = dispatchable_generators_df_p - if pf_post_lopf: - etrago_results_per_bus[ - "dispatchable_generators_reactive_power" - ] = dispatchable_generators_df_q + results["dispatchable_generators_active_power"] = dispatchable_gens_df_p + results["dispatchable_generators_reactive_power"] = dispatchable_gens_df_q def renewable_generators(): + """ # Renewables - weather_dependent_generators = [ + weather_dependent = weather_dep + generators = gens + weather_id = w_id + aggregated = agg + potential = pot + dispatch = dis + curtailment = curt + """ + + weather_dep_gens = [ # "CH4", # "CH4_NG", # "CH4_biogas", @@ -465,143 +404,149 @@ def renewable_generators(): # "wind_offshore", "wind_onshore", ] - weather_dependent_generators_df = generators_df[ - generators_df.carrier.isin(weather_dependent_generators) + renaming_carrier_dict = { + "solar": ["solar", "solar_rooftop"], + "wind": ["wind_onshore"], + } + weather_dep_gens_df = generators_df[ + generators_df.carrier.isin(weather_dep_gens) ] - if not weather_dependent_generators_df.empty: - for generator_index in weather_dependent_generators_df.index: - weather_id = get_weather_id_for_generator( - grid_version, session, generator_index, scn_name - ) - weather_dependent_generators_df.loc[generator_index, "w_id"] = str( - weather_id - ) - - technology_dict = { - "solar": ["solar", "solar_rooftop"], - } - for key, item in technology_dict.items(): - for carrier in item: - weather_dependent_generators_df.loc[ - weather_dependent_generators_df["carrier"] == carrier, "carrier" - ] = key - - # Aggregation of p_nom - aggregated_weather_dependent_generators_df = ( - weather_dependent_generators_df.groupby(["carrier", "w_id"]) - .agg({"p_nom": "sum"}) - .reset_index() - ) - # Dispatch and Curtailment - weather_dependent_generators_df_potential_p = pd.DataFrame( - 0.0, - index=timeseries_index, - columns=aggregated_weather_dependent_generators_df.index, + # Add weather ids + for gens_index in weather_dep_gens_df.index: + weather_id = db_io.get_weather_id_for_generator( + bus_id, engine=engine, orm=orm ) - weather_dependent_generators_df_dispatch_p = pd.DataFrame( - 0.0, - index=timeseries_index, - columns=aggregated_weather_dependent_generators_df.index, - ) - if pf_post_lopf: - weather_dependent_generators_df_dispatch_q = pd.DataFrame( - 0.0, - index=timeseries_index, - columns=aggregated_weather_dependent_generators_df.index, - ) - - for index, carrier, w_id, p_nom in weather_dependent_generators_df[ - ["carrier", "w_id", "p_nom"] - ].itertuples(): - aggregated_idx = aggregated_weather_dependent_generators_df[ - (aggregated_weather_dependent_generators_df["carrier"] == carrier) - & (aggregated_weather_dependent_generators_df["w_id"] == w_id) - ].index.values[0] - p_nom_aggregated = aggregated_weather_dependent_generators_df.loc[ - aggregated_idx, "p_nom" - ] - - p_series = etrago_network.generators_t["p"][index] - p_normed_series = p_series / p_nom_aggregated - - p_max_pu_series = etrago_network.generators_t["p_max_pu"][index] - p_max_pu_normed_series = p_max_pu_series * p_nom / p_nom_aggregated - - if pf_post_lopf: - if max_cos_phi_renewable: - logger.info( - "Applying Q limit (max cos(phi)={})".format( - max_cos_phi_renewable - ) - ) - - phi = math.acos(max_cos_phi_renewable) - - q_series = pd.Series(0, index=timeseries_index) + weather_dep_gens_df.loc[gens_index, "w_id"] = str(weather_id) + # Rename carrier to aggregate to carriers + + for new_carrier_name, item in renaming_carrier_dict.items(): + for carrier in item: + weather_dep_gens_df.loc[ + weather_dep_gens_df["carrier"] == carrier, "carrier" + ] = new_carrier_name + + # Aggregation of p_nom + agg_weather_dep_gens_df = ( + weather_dep_gens_df.groupby(["carrier", "w_id"]) + .agg({"p_nom": "sum"}) + .reset_index() + ) - for timestep in timeseries_index: - p = etrago_network.generators_t["p"].loc[timestep, index] - q = etrago_network.generators_t["q"].loc[timestep, index] + # Initialize dfs + weather_dep_gens_df_pot_p = pd.DataFrame( + 0.0, + index=timeseries_index, + columns=agg_weather_dep_gens_df.index, + ) + weather_dep_gens_df_dis_p = pd.DataFrame( + 0.0, + index=timeseries_index, + columns=agg_weather_dep_gens_df.index, + ) + weather_dep_gens_df_dis_q = pd.DataFrame( + 0.0, + index=timeseries_index, + columns=agg_weather_dep_gens_df.index, + ) + weather_dep_gens_df_curt_p = pd.DataFrame( + 0.0, + index=timeseries_index, + columns=agg_weather_dep_gens_df.index, + ) - q_max = p * math.tan(phi) - q_min = -p * math.tan(phi) + for index, carrier, w_id, p_nom in weather_dep_gens_df[ + ["carrier", "w_id", "p_nom"] + ].itertuples(): + agg_idx = agg_weather_dep_gens_df[ + (agg_weather_dep_gens_df["carrier"] == carrier) + & (agg_weather_dep_gens_df["w_id"] == w_id) + ].index.values[0] + p_nom_agg = agg_weather_dep_gens_df.loc[agg_idx, "p_nom"] - if q > q_max: - q = q_max - elif q < q_min: - q = q_min + p_series = etrago_obj.generators_t["p"][index] + p_normed_series = p_series / p_nom_agg - q_series[timestep] = q - else: - q_series = etrago_network.generators_t["q"][index] + p_max_pu_series = etrago_obj.generators_t["p_max_pu"][index] + p_max_pu_normed_series = p_max_pu_series * p_nom / p_nom_agg - q_normed_series = q_series / p_nom_aggregated + if pf_post_lopf: + q_series = etrago_obj.generators_t["q"][index] + else: + q_series = pd.Series(0.0, index=timeseries_index) - weather_dependent_generators_df_dispatch_p[aggregated_idx] = ( - weather_dependent_generators_df_dispatch_p[aggregated_idx] - + p_normed_series - ) - weather_dependent_generators_df_potential_p[aggregated_idx] = ( - weather_dependent_generators_df_potential_p[aggregated_idx] - + p_max_pu_normed_series + # If set limit maximum reactive power + if max_cos_phi_ren: + logger.info( + "Applying Q limit (max cos(phi)={})".format(max_cos_phi_ren) ) - if pf_post_lopf: - weather_dependent_generators_df_dispatch_q[aggregated_idx] = ( - weather_dependent_generators_df_dispatch_q[aggregated_idx] - + q_normed_series - ) - - weather_dependent_generators_df_curtailment_p = ( - weather_dependent_generators_df_potential_p - - weather_dependent_generators_df_dispatch_p + phi = math.acos(max_cos_phi_ren) + for timestep in timeseries_index: + p = p_series[timestep] + q = q_series[timestep] + q_max = p * math.tan(phi) + q_min = -p * math.tan(phi) + if q > q_max: + q = q_max + elif q < q_min: + q = q_min + q_series[timestep] = q + + q_normed_series = q_series / p_nom_agg + + weather_dep_gens_df_dis_p[agg_idx] = ( + weather_dep_gens_df_dis_p[agg_idx] + p_normed_series ) + weather_dep_gens_df_pot_p[agg_idx] = ( + weather_dep_gens_df_pot_p[agg_idx] + p_max_pu_normed_series + ) + weather_dep_gens_df_dis_q[agg_idx] = ( + weather_dep_gens_df_dis_q[agg_idx] + q_normed_series + ) + weather_dep_gens_df_curt_p[agg_idx] = weather_dep_gens_df_curt_p[ + agg_idx + ] + (p_max_pu_series * p_nom - p_series) + + # Renaming columns + new_columns = [ + ( + agg_weather_dep_gens_df.at[column, "carrier"], + agg_weather_dep_gens_df.at[column, "w_id"], + ) + for column in weather_dep_gens_df_pot_p.columns + ] + new_columns = pd.MultiIndex.from_tuples(new_columns) + weather_dep_gens_df_pot_p.columns = new_columns + weather_dep_gens_df_dis_p.columns = new_columns + weather_dep_gens_df_curt_p.columns = new_columns + weather_dep_gens_df_dis_q.columns = new_columns + + # Add zero for empty carriers + for carrier in renaming_carrier_dict.keys(): + for w_id in set(weather_dep_gens_df["w_id"]): + column = [(carrier, w_id)] + if column[0] not in new_columns: + empty_df = pd.DataFrame( + 0.0, + index=timeseries_index, + columns=pd.MultiIndex.from_tuples(column), + ) + weather_dep_gens_df_pot_p = pd.concat( + [weather_dep_gens_df_pot_p, empty_df.copy()], axis="columns" + ) + weather_dep_gens_df_dis_p = pd.concat( + [weather_dep_gens_df_dis_p, empty_df.copy()], axis="columns" + ) + weather_dep_gens_df_curt_p = pd.concat( + [weather_dep_gens_df_curt_p, empty_df.copy()], axis="columns" + ) + weather_dep_gens_df_dis_q = pd.concat( + [weather_dep_gens_df_dis_q, empty_df.copy()], axis="columns" + ) - # Renaming columns - new_columns = [ - ( - aggregated_weather_dependent_generators_df.at[column, "carrier"], - aggregated_weather_dependent_generators_df.at[column, "w_id"], - ) - for column in weather_dependent_generators_df_potential_p.columns - ] - new_columns = pd.MultiIndex.from_tuples(new_columns) - weather_dependent_generators_df_potential_p.columns = new_columns - weather_dependent_generators_df_dispatch_p.columns = new_columns - weather_dependent_generators_df_curtailment_p.columns = new_columns - if pf_post_lopf: - weather_dependent_generators_df_dispatch_q.columns = new_columns - - etrago_results_per_bus[ - "renewables_potential" - ] = weather_dependent_generators_df_potential_p - etrago_results_per_bus[ - "renewables_curtailment" - ] = weather_dependent_generators_df_curtailment_p - if pf_post_lopf: - etrago_results_per_bus[ - "renewables_dispatch_reactive_power" - ] = weather_dependent_generators_df_dispatch_q + results["renewables_potential"] = weather_dep_gens_df_pot_p + results["renewables_curtailment"] = weather_dep_gens_df_curt_p + results["renewables_dispatch_reactive_power"] = weather_dep_gens_df_dis_q def storages(): # Storage @@ -609,29 +554,44 @@ def storages(): min_extended = 0 logger.info(f"Minimum storage of {min_extended} MW") - storages_df = etrago_network.storage_units.loc[ - (etrago_network.storage_units["carrier"] == "battery") - & (etrago_network.storage_units["bus"] == str(bus_id)) - & (etrago_network.storage_units["p_nom_extendable"]) - & (etrago_network.storage_units["p_nom_opt"] > min_extended) - # & (etrago_network.storage_units["max_hours"] <= 20.0) + storages_df = etrago_obj.storage_units.loc[ + (etrago_obj.storage_units["carrier"] == "battery") + & (etrago_obj.storage_units["bus"] == str(bus_id)) + & (etrago_obj.storage_units["p_nom_extendable"]) + & (etrago_obj.storage_units["p_nom_opt"] > min_extended) + # & (etrago_obj.storage_units["max_hours"] <= 20.0) ] if not storages_df.empty: - # Capactiy + # Capacity storages_df_capacity = ( storages_df["p_nom_opt"] * storages_df["max_hours"] ).values[0] - storages_df_p = etrago_network.storage_units_t["p"][storages_df.index] + storages_df_p = etrago_obj.storage_units_t["p"][storages_df.index] storages_df_p.columns = storages_df["carrier"] if pf_post_lopf: - storages_df_q = etrago_network.storage_units_t["q"][storages_df.index] - storages_df_q.columns = storages_df["carrier"] + # ToDo: No q timeseries? + # storages_df_q = etrago_obj.storage_units_t["q"][storages_df.index] + # storages_df_q.columns = storages_df["carrier"] + storages_df_q = pd.DataFrame( + 0.0, index=timeseries_index, columns=[storages_df["carrier"]] + ) + else: + storages_df_q = pd.DataFrame( + 0.0, index=timeseries_index, columns=[storages_df["carrier"]] + ) + else: + storages_df_capacity = 0 + storages_df_p = pd.DataFrame( + 0.0, index=timeseries_index, columns=[storages_df["carrier"]] + ) + storages_df_q = pd.DataFrame( + 0.0, index=timeseries_index, columns=[storages_df["carrier"]] + ) - etrago_results_per_bus["storage_units_capacity"] = storages_df_capacity - etrago_results_per_bus["storage_units_active_power"] = storages_df_p - if pf_post_lopf: - etrago_results_per_bus["storage_units_reactive_power"] = storages_df_q + results["storage_units_capacity"] = storages_df_capacity + results["storage_units_active_power"] = storages_df_p + results["storage_units_reactive_power"] = storages_df_q def dsm(): # DSM @@ -639,16 +599,20 @@ def dsm(): (links_df["carrier"] == "dsm") & (links_df["bus0"] == str(bus_id)) ] if not dsm_df.empty: - if dsm_df.shape[0] > 1: - raise ValueError(f"More than one dsm link at bus {bus_id}") - dsm_df_p = etrago_network.links_t["p0"][dsm_df.index] + dsm_df_p = etrago_obj.links_t["p0"][dsm_df.index] dsm_df_p.columns = dsm_df["carrier"] - if pf_post_lopf: - dsm_df_q = 0 * dsm_df_p - - etrago_results_per_bus["dsm_active_power"] = dsm_df_p - if pf_post_lopf: - etrago_results_per_bus["dsm_reactive_power"] = dsm_df_q + dsm_df_q = pd.DataFrame( + 0.0, index=timeseries_index, columns=[dsm_df["carrier"]] + ) + else: + dsm_df_p = pd.DataFrame( + 0.0, index=timeseries_index, columns=[dsm_df["carrier"]] + ) + dsm_df_q = pd.DataFrame( + 0.0, index=timeseries_index, columns=[dsm_df["carrier"]] + ) + results["dsm_active_power"] = dsm_df_p + results["dsm_reactive_power"] = dsm_df_q def central_heat(): # Heat @@ -661,56 +625,74 @@ def central_heat(): ] if not central_heat_df.empty: # Timeseries - central_heat_df_p = etrago_network.links_t["p0"][central_heat_df.index] + central_heat_df_p = etrago_obj.links_t["p0"][central_heat_df.index] central_heat_df_p.columns = central_heat_df["carrier"] - if pf_post_lopf: - central_heat_df_q = 0 * central_heat_df_p - - etrago_results_per_bus["heat_central_active_power"] = central_heat_df_p - if pf_post_lopf: - etrago_results_per_bus[ - "heat_central_reactive_power" - ] = central_heat_df_q + central_heat_df_q = pd.DataFrame( + 0.0, index=timeseries_index, columns=[central_heat_df["carrier"]] + ) # Stores central_heat_bus = central_heat_df["bus1"].values[0] - central_heat_store_bus = etrago_network.links.loc[ - etrago_network.links["bus0"] == central_heat_bus, "bus1" + central_heat_store_bus = etrago_obj.links.loc[ + etrago_obj.links["bus0"] == central_heat_bus, "bus1" ].values[0] - central_heat_store_capacity = etrago_network.stores.loc[ - (etrago_network.stores["carrier"] == "central_heat_store") - & (etrago_network.stores["bus"] == central_heat_store_bus), + central_heat_store_capacity = etrago_obj.stores.loc[ + (etrago_obj.stores["carrier"] == "central_heat_store") + & (etrago_obj.stores["bus"] == central_heat_store_bus), "e_nom_opt", ].values[0] - etrago_results_per_bus[ - "thermal_storage_central_capacity" - ] = central_heat_store_capacity - # Feedin - geothermal_feedin_df = etrago_network.generators[ - (etrago_network.generators["carrier"] == "geo_thermal") - & (etrago_network.generators["bus"] == central_heat_bus) + geothermal_feedin_df = etrago_obj.generators[ + (etrago_obj.generators["carrier"] == "geo_thermal") + & (etrago_obj.generators["bus"] == central_heat_bus) ] - geothermal_feedin_df_p = etrago_network.generators_t["p"][ - geothermal_feedin_df.index - ] - geothermal_feedin_df_p.columns = geothermal_feedin_df["carrier"] - etrago_results_per_bus[ - "geothermal_energy_feedin_district_heating" - ] = geothermal_feedin_df_p - - solarthermal_feedin_df = etrago_network.generators[ - (etrago_network.generators["carrier"] == "solar_thermal_collector") - & (etrago_network.generators["bus"] == central_heat_bus) - ] - solarthermal_feedin_df_p = etrago_network.generators_t["p"][ - solarthermal_feedin_df.index + if not geothermal_feedin_df.empty: + geothermal_feedin_df_p = etrago_obj.generators_t["p"][ + geothermal_feedin_df.index + ] + geothermal_feedin_df_p.columns = geothermal_feedin_df["carrier"] + else: + geothermal_feedin_df_p = pd.DataFrame( + 0.0, index=timeseries_index, columns=["geo_thermal"] + ) + + solarthermal_feedin_df = etrago_obj.generators[ + (etrago_obj.generators["carrier"] == "solar_thermal_collector") + & (etrago_obj.generators["bus"] == central_heat_bus) ] - solarthermal_feedin_df_p.columns = solarthermal_feedin_df["carrier"] - etrago_results_per_bus[ - "solarthermal_energy_feedin_district_heating" - ] = solarthermal_feedin_df_p + if not solarthermal_feedin_df.empty: + solarthermal_feedin_df_p = etrago_obj.generators_t["p"][ + solarthermal_feedin_df.index + ] + solarthermal_feedin_df_p.columns = solarthermal_feedin_df["carrier"] + else: + solarthermal_feedin_df_p = pd.DataFrame( + 0.0, index=timeseries_index, columns=["solar_thermal_collector"] + ) + else: + column_names = central_heat_df["carrier"].to_list() + central_heat_df_p = pd.DataFrame( + 0.0, index=timeseries_index, columns=column_names + ) + central_heat_df_q = pd.DataFrame( + 0.0, index=timeseries_index, columns=column_names + ) + central_heat_store_capacity = 0 + geothermal_feedin_df_p = pd.DataFrame( + 0.0, index=timeseries_index, columns=["geo_thermal"] + ) + solarthermal_feedin_df_p = pd.DataFrame( + 0.0, index=timeseries_index, columns=["solar_thermal_collector"] + ) + # ToDo: Overlying grid no resistive heater + results["heat_central_active_power"] = central_heat_df_p + results["heat_central_reactive_power"] = central_heat_df_q + results["thermal_storage_central_capacity"] = central_heat_store_capacity + results["geothermal_energy_feedin_district_heating"] = geothermal_feedin_df_p + results[ + "solarthermal_energy_feedin_district_heating" + ] = solarthermal_feedin_df_p def rural_heat(): # Rural heat @@ -722,31 +704,35 @@ def rural_heat(): ] if not rural_heat_df.empty: # Timeseries - rural_heat_df_p = etrago_network.links_t["p0"][rural_heat_df.index] + rural_heat_df_p = etrago_obj.links_t["p0"][rural_heat_df.index] rural_heat_df_p.columns = rural_heat_df["carrier"] - if pf_post_lopf: - rural_heat_df_q = 0 * rural_heat_df_p - - etrago_results_per_bus["heat_pump_rural_active_power"] = rural_heat_df_p - if pf_post_lopf: - etrago_results_per_bus[ - "heat_pump_rural_reactive_power" - ] = rural_heat_df_q + rural_heat_df_q = pd.DataFrame( + 0.0, index=timeseries_index, columns=[rural_heat_df["carrier"]] + ) # Stores rural_heat_bus = rural_heat_df["bus1"].values[0] - rural_heat_store_bus = etrago_network.links.loc[ - etrago_network.links["bus0"] == rural_heat_bus, "bus1" + rural_heat_store_bus = etrago_obj.links.loc[ + etrago_obj.links["bus0"] == rural_heat_bus, "bus1" ].values[0] - rural_heat_store_capacity = etrago_network.stores.loc[ - (etrago_network.stores["carrier"] == "rural_heat_store") - & (etrago_network.stores["bus"] == rural_heat_store_bus), + rural_heat_store_capacity = etrago_obj.stores.loc[ + (etrago_obj.stores["carrier"] == "rural_heat_store") + & (etrago_obj.stores["bus"] == rural_heat_store_bus), "e_nom_opt", ].values[0] + else: + column_names = rural_heat_df["carrier"].to_list() + rural_heat_df_p = pd.DataFrame( + 0.0, index=timeseries_index, columns=column_names + ) + rural_heat_df_q = pd.DataFrame( + 0.0, index=timeseries_index, columns=column_names + ) + rural_heat_store_capacity = 0 - etrago_results_per_bus[ - "thermal_storage_rural_capacity" - ] = rural_heat_store_capacity + results["heat_pump_rural_active_power"] = rural_heat_df_p + results["heat_pump_rural_reactive_power"] = rural_heat_df_q + results["thermal_storage_rural_capacity"] = rural_heat_store_capacity def bev_charger(): # BEV charger @@ -754,52 +740,77 @@ def bev_charger(): (links_df["carrier"] == "BEV charger") & (links_df["bus0"] == str(bus_id)) ] if not bev_charger_df.empty: - if bev_charger_df.shape[0] > 1: - raise ValueError(f"More than one dsm link at bus {bus_id}") - - bev_charger_df_p = etrago_network.links_t["p0"][bev_charger_df.index] + bev_charger_df_p = etrago_obj.links_t["p0"][bev_charger_df.index] bev_charger_df_p.columns = bev_charger_df["carrier"] - if pf_post_lopf: - bev_charger_df_q = 0 * bev_charger_df_p + bev_charger_df_q = pd.DataFrame( + 0.0, index=timeseries_index, columns=[bev_charger_df["carrier"]] + ) + else: + bev_charger_df_p = pd.DataFrame( + 0.0, index=timeseries_index, columns=[bev_charger_df["carrier"]] + ) + bev_charger_df_q = pd.DataFrame( + 0.0, index=timeseries_index, columns=[bev_charger_df["carrier"]] + ) - etrago_results_per_bus["electromobility_active_power"] = bev_charger_df_p - if pf_post_lopf: - etrago_results_per_bus[ - "electromobility_reactive_power" - ] = bev_charger_df_q + results["electromobility_active_power"] = bev_charger_df_p + results["electromobility_reactive_power"] = bev_charger_df_q - t1 = time.perf_counter() - performance.update({"General Data Processing": t1 - t0}) + # Function part + t_start = time.perf_counter() - dispatchable_generators() - t2 = time.perf_counter() - performance.update({"Dispatchable generators": t2 - t1}) + logger.info("Specs for bus {}".format(bus_id)) + if pf_post_lopf: + logger.info("Active and reactive power interface") + else: + logger.info("Only active power interface") - renewable_generators() - t3 = time.perf_counter() - performance.update({"Renewable Dispatch and Curt.": t3 - t2}) + results = {} + timeseries_index = etrago_obj.snapshots + results["timeindex"] = timeseries_index + # Prefill dict with None + result_keys = [ + "dispatchable_generators_active_power", + "dispatchable_generators_reactive_power", + "renewables_potential", + "renewables_curtailment", + "renewables_dispatch_reactive_power", + "storage_units_capacity", + "storage_units_active_power", + "storage_units_reactive_power", + "dsm_active_power", + "dsm_reactive_power", + "heat_central_active_power", + "heat_central_reactive_power", + "thermal_storage_central_capacity", + "geothermal_energy_feedin_district_heating", + "solarthermal_energy_feedin_district_heating", + "heat_pump_rural_active_power", + "heat_pump_rural_reactive_power", + "thermal_storage_rural_capacity", + "electromobility_active_power", + "electromobility_reactive_power", + ] + for result_key in result_keys: + results[result_key] = None - storages() - t4 = time.perf_counter() - performance.update({"Storage Data Processing": t4 - t3}) + # Filter dataframes by bus_id + # Generators + generators_df = etrago_obj.generators[etrago_obj.generators["bus"] == str(bus_id)] + # Links + links_df = etrago_obj.links[ + (etrago_obj.links["bus0"] == str(bus_id)) + | (etrago_obj.links["bus1"] == str(bus_id)) + ] + # Fill results + dispatchable_gens() + renewable_generators() + storages() dsm() - t5 = time.perf_counter() - performance.update({"DSM Data Processing": t5 - t4}) - central_heat() - t6 = time.perf_counter() - performance.update({"Central Heat Data Processing": t6 - t5}) - rural_heat() - t7 = time.perf_counter() - performance.update({"Rural Heat Data Processing": t7 - t6}) - bev_charger() - t8 = time.perf_counter() - performance.update({"BEV Data Processing": t8 - t7}) - - performance.update({"Overall time": t8 - t0}) - logger.info(performance) + logger.info(f"Overall time: {time.perf_counter() - t_start}") - return etrago_results_per_bus + return results diff --git a/tests/tools/test_interface.py b/tests/tools/test_interface.py index 873c4f12..fd270fae 100644 --- a/tests/tools/test_interface.py +++ b/tests/tools/test_interface.py @@ -32,7 +32,7 @@ def setup_class(cls): def test_class_etrago_minimal_data(self): etrago_network = ETraGoMinimalData(self.etrago_network) - assert not "p_min_pu" in etrago_network.generators_t + assert "p_min_pu" not in etrago_network.generators_t def test_get_etrago_results_per_bus(self, monkeypatch): monkeypatch.setattr( @@ -49,13 +49,12 @@ def test_get_etrago_results_per_bus(self, monkeypatch): max_cos_phi_renewable = False etrago_results_per_bus = get_etrago_results_per_bus( - session, bus_id, etrago_network, grid_version, scn_name, pf_post_lopf, - max_cos_phi_renewable, + session=session, ) for key, value in etrago_results_per_bus.items(): @@ -117,13 +116,12 @@ def test_get_etrago_results_per_bus_no_non_linear_pf(self, monkeypatch): max_cos_phi_renewable = False etrago_results_per_bus = get_etrago_results_per_bus( - session, bus_id, etrago_network, grid_version, scn_name, pf_post_lopf, - max_cos_phi_renewable, + session=session, ) none_results = [ @@ -157,13 +155,12 @@ def test_get_etrago_results_per_bus_empty(self, monkeypatch): max_cos_phi_renewable = False etrago_results_per_bus = get_etrago_results_per_bus( - session, bus_id, etrago_network, grid_version, scn_name, pf_post_lopf, - max_cos_phi_renewable, + session=session, ) none_results = [ @@ -210,13 +207,12 @@ def test_get_etrago_results_per_bus_with_set_max_cosphi(self, monkeypatch): max_cos_phi_renewable = 0.9 etrago_results_per_bus = get_etrago_results_per_bus( - session, bus_id, etrago_network, grid_version, scn_name, pf_post_lopf, - max_cos_phi_renewable, + session=session, ) for key, value in etrago_results_per_bus.items(): From ddba2193d8493d0f7a3e093189e7486ba7d617a3 Mon Sep 17 00:00:00 2001 From: Malte Jahn Date: Mon, 6 Mar 2023 16:19:54 +0100 Subject: [PATCH 034/126] Update requirements from dev --- setup.py | 71 +++++++++++++++++++++++--------------------------------- 1 file changed, 29 insertions(+), 42 deletions(-) diff --git a/setup.py b/setup.py index 8965806f..8d84b9cb 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- + +# flake8: noqa: F401, F601 import os from pip._internal.req import parse_requirements @@ -17,60 +19,45 @@ def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() +req = [] + +dev_req = [ + "pre-commit", + "black", + "isort", + "pyupgrade", + "flake8", +] + +doc_req = [] + +full_req = list(set(dev_req + doc_req)) + +extras = { + "dev": dev_req, + "doc": doc_req, + "full": full_req, +} + + setup( name="eGo", version="0.3.4", author="wolfbunke, maltesc", author_email="wolf-dieter.bunke@uni-flensburg.de", - description=( - "A cross-grid-level electricity grid and storage " "optimization tool " - ), + description=("A cross-grid-level electricity grid and storage optimization tool."), long_description=read("README.rst"), url="https://github.com/openego/eGo", license="GNU Affero General Public License Version 3 (AGPL-3.0)", packages=find_packages(), package_dir={"ego": "ego"}, include_package_data=True, - install_requires=[ - "egoio == 0.4.5", - "ding0 == v0.1.9", - "pycallgraph", - "eDisGo == v0.0.9", - "eTraGo == 0.7.1", - "scikit-learn == 0.19.0", - "pandas ==0.20.3", - "pypsa==0.11.0fork", - "sqlalchemy == 1.2.0", - "geoalchemy2 >= 0.3.0, <=0.4.0", - "tsam==0.9.9", - "geopandas", - "matplotlib == 3.0.0", - "Rtree", - "descartes", - "pyproj", - "plotly==2.2.3", - "shapely", - "multiprocess", - "folium", - "oedialect", - ], - dependency_links=[ - ("git+https://git@github.com/openego/PyPSA.git" "@master#egg=pypsa-0.11.0fork") - ], - extras_require={ - "doc": [ - "sphinx >= 1.4", - "sphinx_rtd_theme", - "sphinxcontrib-httpdomain", - "numpydoc == 0.7.0", - "aiohttp_jinja2", - "sphinx-jsondomain", - ] - }, + install_requires=req, + extras_require=extras, package_data={ - "ego": [os.path.join("tools", "*.csv")], - "ego": [os.path.join("tools", "*.json")], - "ego": [os.path.join("", "*.json")], + "ego": [os.path.join("tools", "*.csv")] + + [os.path.join("tools", "*.json")] + + [os.path.join("", "*.json")], "ego.data": ["*.csv"], }, ) From 0ebff2209f740f4df5376a4bc251aef529c0cc51 Mon Sep 17 00:00:00 2001 From: Malte Jahn Date: Thu, 9 Mar 2023 10:29:39 +0100 Subject: [PATCH 035/126] Fixing some flake8 errors --- .../tutorials/edisgo_simple_example.ipynb | 50 +++++++++---------- ego/tools/utilities.py | 21 ++++---- 2 files changed, 34 insertions(+), 37 deletions(-) diff --git a/ego/examples/tutorials/edisgo_simple_example.ipynb b/ego/examples/tutorials/edisgo_simple_example.ipynb index 0581729a..4321ab40 100644 --- a/ego/examples/tutorials/edisgo_simple_example.ipynb +++ b/ego/examples/tutorials/edisgo_simple_example.ipynb @@ -1168,32 +1168,32 @@ "evalue": "(psycopg2.ProgrammingError) relation \"model_draft.ego_supply_res_powerplant_nep2035_mview\" does not exist\nLINE 2: FROM model_draft.ego_supply_res_powerplant_nep2035_mview \n ^\n [SQL: 'SELECT model_draft.ego_supply_res_powerplant_nep2035_mview.id, model_draft.ego_supply_res_powerplant_nep2035_mview.subst_id, model_draft.ego_supply_res_powerplant_nep2035_mview.la_id, model_draft.ego_supply_res_powerplant_nep2035_mview.mvlv_subst_id, model_draft.ego_supply_res_powerplant_nep2035_mview.electrical_capacity, model_draft.ego_supply_res_powerplant_nep2035_mview.generation_type, model_draft.ego_supply_res_powerplant_nep2035_mview.generation_subtype, model_draft.ego_supply_res_powerplant_nep2035_mview.voltage_level, ST_AsText(ST_Transform(model_draft.ego_supply_res_powerplant_nep2035_mview.rea_geom_new, %(ST_Transform_1)s)) AS geom, ST_AsText(ST_Transform(model_draft.ego_supply_res_powerplant_nep2035_mview.geom, %(ST_Transform_2)s)) AS geom_em \\nFROM model_draft.ego_supply_res_powerplant_nep2035_mview \\nWHERE model_draft.ego_supply_res_powerplant_nep2035_mview.subst_id = %(subst_id_1)s AND model_draft.ego_supply_res_powerplant_nep2035_mview.generation_type IN (%(generation_type_1)s, %(generation_type_2)s) AND model_draft.ego_supply_res_powerplant_nep2035_mview.voltage_level IN (%(voltage_level_1)s, %(voltage_level_2)s)'] [parameters: {'ST_Transform_1': 4326, 'ST_Transform_2': 4326, 'subst_id_1': 239, 'generation_type_1': 'solar', 'generation_type_2': 'wind', 'voltage_level_1': 4, 'voltage_level_2': 5}] (Background on this error at: http://sqlalche.me/e/f405)", "output_type": "error", "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mProgrammingError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/base.py\u001b[0m in \u001b[0;36m_execute_context\u001b[0;34m(self, dialect, constructor, statement, parameters, *args)\u001b[0m\n\u001b[1;32m 1192\u001b[0m \u001b[0mparameters\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1193\u001b[0;31m context)\n\u001b[0m\u001b[1;32m 1194\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mBaseException\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/default.py\u001b[0m in \u001b[0;36mdo_execute\u001b[0;34m(self, cursor, statement, parameters, context)\u001b[0m\n\u001b[1;32m 506\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mdo_execute\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcursor\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstatement\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mparameters\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcontext\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 507\u001b[0;31m \u001b[0mcursor\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexecute\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstatement\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mparameters\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 508\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mProgrammingError\u001b[0m: relation \"model_draft.ego_supply_res_powerplant_nep2035_mview\" does not exist\nLINE 2: FROM model_draft.ego_supply_res_powerplant_nep2035_mview \n ^\n", + "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m", + "\u001B[0;31mProgrammingError\u001B[0m Traceback (most recent call last)", + "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/base.py\u001B[0m in \u001B[0;36m_execute_context\u001B[0;34m(self, dialect, constructor, statement, parameters, *args)\u001B[0m\n\u001B[1;32m 1192\u001B[0m \u001B[0mparameters\u001B[0m\u001B[0;34m,\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m-> 1193\u001B[0;31m context)\n\u001B[0m\u001B[1;32m 1194\u001B[0m \u001B[0;32mexcept\u001B[0m \u001B[0mBaseException\u001B[0m \u001B[0;32mas\u001B[0m \u001B[0me\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", + "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/default.py\u001B[0m in \u001B[0;36mdo_execute\u001B[0;34m(self, cursor, statement, parameters, context)\u001B[0m\n\u001B[1;32m 506\u001B[0m \u001B[0;32mdef\u001B[0m \u001B[0mdo_execute\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mcursor\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mstatement\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mparameters\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mcontext\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0;32mNone\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m--> 507\u001B[0;31m \u001B[0mcursor\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mexecute\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mstatement\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mparameters\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 508\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n", + "\u001B[0;31mProgrammingError\u001B[0m: relation \"model_draft.ego_supply_res_powerplant_nep2035_mview\" does not exist\nLINE 2: FROM model_draft.ego_supply_res_powerplant_nep2035_mview \n ^\n", "\nThe above exception was the direct cause of the following exception:\n", - "\u001b[0;31mProgrammingError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m# Import generators\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0mscenario\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m'nep2035'\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0medisgo\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mimport_generators\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mgenerator_scenario\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mscenario\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[0;32m~/virtualenvs/open_ego_notebook/git_repos/eDisGo/edisgo/grid/network.py\u001b[0m in \u001b[0;36mimport_generators\u001b[0;34m(self, generator_scenario)\u001b[0m\n\u001b[1;32m 326\u001b[0m \u001b[0mdata_source\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m'oedb'\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 327\u001b[0m import_generators(network=self.network,\n\u001b[0;32m--> 328\u001b[0;31m data_source=data_source)\n\u001b[0m\u001b[1;32m 329\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 330\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0manalyze\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmode\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/virtualenvs/open_ego_notebook/git_repos/eDisGo/edisgo/data/import_data.py\u001b[0m in \u001b[0;36mimport_generators\u001b[0;34m(network, data_source, file)\u001b[0m\n\u001b[1;32m 984\u001b[0m logging.warning('Right now only solar and wind generators can be '\n\u001b[1;32m 985\u001b[0m 'imported from the oedb.')\n\u001b[0;32m--> 986\u001b[0;31m \u001b[0m_import_genos_from_oedb\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnetwork\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mnetwork\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 987\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0mdata_source\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m'pypsa'\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 988\u001b[0m \u001b[0m_import_genos_from_pypsa\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnetwork\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mnetwork\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfile\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mfile\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/virtualenvs/open_ego_notebook/git_repos/eDisGo/edisgo/data/import_data.py\u001b[0m in \u001b[0;36m_import_genos_from_oedb\u001b[0;34m(network)\u001b[0m\n\u001b[1;32m 1844\u001b[0m \u001b[0;31m#generators_conv_mv = _import_conv_generators()\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1845\u001b[0m generators_res_mv, generators_res_lv = _import_res_generators(\n\u001b[0;32m-> 1846\u001b[0;31m types_condition)\n\u001b[0m\u001b[1;32m 1847\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1848\u001b[0m \u001b[0;31m#generators_mv = generators_conv_mv.append(generators_res_mv)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/virtualenvs/open_ego_notebook/git_repos/eDisGo/edisgo/data/import_data.py\u001b[0m in \u001b[0;36m_import_res_generators\u001b[0;34m(types_filter)\u001b[0m\n\u001b[1;32m 1089\u001b[0m generators_mv = pd.read_sql_query(generators_mv_sqla.statement,\n\u001b[1;32m 1090\u001b[0m \u001b[0msession\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mbind\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1091\u001b[0;31m index_col='id')\n\u001b[0m\u001b[1;32m 1092\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1093\u001b[0m \u001b[0;31m# define generators with unknown subtype as 'unknown'\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/pandas/io/sql.py\u001b[0m in \u001b[0;36mread_sql_query\u001b[0;34m(sql, con, index_col, coerce_float, params, parse_dates, chunksize)\u001b[0m\n\u001b[1;32m 330\u001b[0m return pandas_sql.read_query(\n\u001b[1;32m 331\u001b[0m \u001b[0msql\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mindex_col\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mindex_col\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mparams\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mparams\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcoerce_float\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mcoerce_float\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 332\u001b[0;31m parse_dates=parse_dates, chunksize=chunksize)\n\u001b[0m\u001b[1;32m 333\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 334\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/pandas/io/sql.py\u001b[0m in \u001b[0;36mread_query\u001b[0;34m(self, sql, index_col, coerce_float, parse_dates, params, chunksize)\u001b[0m\n\u001b[1;32m 1085\u001b[0m \u001b[0margs\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_convert_params\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msql\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mparams\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1086\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1087\u001b[0;31m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexecute\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1088\u001b[0m \u001b[0mcolumns\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mkeys\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1089\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/pandas/io/sql.py\u001b[0m in \u001b[0;36mexecute\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 976\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mexecute\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 977\u001b[0m \u001b[0;34m\"\"\"Simple passthrough to SQLAlchemy connectable\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 978\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mconnectable\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexecute\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 979\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 980\u001b[0m def read_table(self, table_name, index_col=None, coerce_float=True,\n", - "\u001b[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/base.py\u001b[0m in \u001b[0;36mexecute\u001b[0;34m(self, statement, *multiparams, **params)\u001b[0m\n\u001b[1;32m 2073\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2074\u001b[0m \u001b[0mconnection\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcontextual_connect\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclose_with_result\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mTrue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 2075\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mconnection\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexecute\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstatement\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0mmultiparams\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mparams\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2076\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2077\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mscalar\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstatement\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0mmultiparams\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mparams\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/base.py\u001b[0m in \u001b[0;36mexecute\u001b[0;34m(self, object, *multiparams, **params)\u001b[0m\n\u001b[1;32m 946\u001b[0m \u001b[0;32mraise\u001b[0m \u001b[0mexc\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mObjectNotExecutableError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mobject\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 947\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 948\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mmeth\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmultiparams\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mparams\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 949\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 950\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m_execute_function\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfunc\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmultiparams\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mparams\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/sql/elements.py\u001b[0m in \u001b[0;36m_execute_on_connection\u001b[0;34m(self, connection, multiparams, params)\u001b[0m\n\u001b[1;32m 267\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m_execute_on_connection\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mconnection\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmultiparams\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mparams\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 268\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msupports_execution\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 269\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mconnection\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_execute_clauseelement\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmultiparams\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mparams\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 270\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 271\u001b[0m \u001b[0;32mraise\u001b[0m \u001b[0mexc\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mObjectNotExecutableError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/base.py\u001b[0m in \u001b[0;36m_execute_clauseelement\u001b[0;34m(self, elem, multiparams, params)\u001b[0m\n\u001b[1;32m 1058\u001b[0m \u001b[0mcompiled_sql\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1059\u001b[0m \u001b[0mdistilled_params\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1060\u001b[0;31m \u001b[0mcompiled_sql\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdistilled_params\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1061\u001b[0m )\n\u001b[1;32m 1062\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_has_events\u001b[0m \u001b[0;32mor\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mengine\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_has_events\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/base.py\u001b[0m in \u001b[0;36m_execute_context\u001b[0;34m(self, dialect, constructor, statement, parameters, *args)\u001b[0m\n\u001b[1;32m 1198\u001b[0m \u001b[0mparameters\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1199\u001b[0m \u001b[0mcursor\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1200\u001b[0;31m context)\n\u001b[0m\u001b[1;32m 1201\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1202\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_has_events\u001b[0m \u001b[0;32mor\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mengine\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_has_events\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/base.py\u001b[0m in \u001b[0;36m_handle_dbapi_exception\u001b[0;34m(self, e, statement, parameters, cursor, context)\u001b[0m\n\u001b[1;32m 1411\u001b[0m util.raise_from_cause(\n\u001b[1;32m 1412\u001b[0m \u001b[0msqlalchemy_exception\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1413\u001b[0;31m \u001b[0mexc_info\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1414\u001b[0m )\n\u001b[1;32m 1415\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/util/compat.py\u001b[0m in \u001b[0;36mraise_from_cause\u001b[0;34m(exception, exc_info)\u001b[0m\n\u001b[1;32m 201\u001b[0m \u001b[0mexc_type\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mexc_value\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mexc_tb\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mexc_info\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 202\u001b[0m \u001b[0mcause\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mexc_value\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mexc_value\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mexception\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 203\u001b[0;31m \u001b[0mreraise\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtype\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mexception\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mexception\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtb\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mexc_tb\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcause\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mcause\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 204\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 205\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mpy3k\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/util/compat.py\u001b[0m in \u001b[0;36mreraise\u001b[0;34m(tp, value, tb, cause)\u001b[0m\n\u001b[1;32m 184\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__cause__\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcause\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 185\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__traceback__\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mtb\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 186\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mwith_traceback\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtb\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 187\u001b[0m \u001b[0;32mraise\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 188\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/base.py\u001b[0m in \u001b[0;36m_execute_context\u001b[0;34m(self, dialect, constructor, statement, parameters, *args)\u001b[0m\n\u001b[1;32m 1191\u001b[0m \u001b[0mstatement\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1192\u001b[0m \u001b[0mparameters\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1193\u001b[0;31m context)\n\u001b[0m\u001b[1;32m 1194\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mBaseException\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1195\u001b[0m self._handle_dbapi_exception(\n", - "\u001b[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/default.py\u001b[0m in \u001b[0;36mdo_execute\u001b[0;34m(self, cursor, statement, parameters, context)\u001b[0m\n\u001b[1;32m 505\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 506\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mdo_execute\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcursor\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstatement\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mparameters\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcontext\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 507\u001b[0;31m \u001b[0mcursor\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexecute\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstatement\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mparameters\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 508\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 509\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mdo_execute_no_params\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcursor\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstatement\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcontext\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mProgrammingError\u001b[0m: (psycopg2.ProgrammingError) relation \"model_draft.ego_supply_res_powerplant_nep2035_mview\" does not exist\nLINE 2: FROM model_draft.ego_supply_res_powerplant_nep2035_mview \n ^\n [SQL: 'SELECT model_draft.ego_supply_res_powerplant_nep2035_mview.id, model_draft.ego_supply_res_powerplant_nep2035_mview.subst_id, model_draft.ego_supply_res_powerplant_nep2035_mview.la_id, model_draft.ego_supply_res_powerplant_nep2035_mview.mvlv_subst_id, model_draft.ego_supply_res_powerplant_nep2035_mview.electrical_capacity, model_draft.ego_supply_res_powerplant_nep2035_mview.generation_type, model_draft.ego_supply_res_powerplant_nep2035_mview.generation_subtype, model_draft.ego_supply_res_powerplant_nep2035_mview.voltage_level, ST_AsText(ST_Transform(model_draft.ego_supply_res_powerplant_nep2035_mview.rea_geom_new, %(ST_Transform_1)s)) AS geom, ST_AsText(ST_Transform(model_draft.ego_supply_res_powerplant_nep2035_mview.geom, %(ST_Transform_2)s)) AS geom_em \\nFROM model_draft.ego_supply_res_powerplant_nep2035_mview \\nWHERE model_draft.ego_supply_res_powerplant_nep2035_mview.subst_id = %(subst_id_1)s AND model_draft.ego_supply_res_powerplant_nep2035_mview.generation_type IN (%(generation_type_1)s, %(generation_type_2)s) AND model_draft.ego_supply_res_powerplant_nep2035_mview.voltage_level IN (%(voltage_level_1)s, %(voltage_level_2)s)'] [parameters: {'ST_Transform_1': 4326, 'ST_Transform_2': 4326, 'subst_id_1': 239, 'generation_type_1': 'solar', 'generation_type_2': 'wind', 'voltage_level_1': 4, 'voltage_level_2': 5}] (Background on this error at: http://sqlalche.me/e/f405)" + "\u001B[0;31mProgrammingError\u001B[0m Traceback (most recent call last)", + "\u001B[0;32m\u001B[0m in \u001B[0;36m\u001B[0;34m()\u001B[0m\n\u001B[1;32m 1\u001B[0m \u001B[0;31m# Import generators\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 2\u001B[0m \u001B[0mscenario\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0;34m'nep2035'\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m----> 3\u001B[0;31m \u001B[0medisgo\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mimport_generators\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mgenerator_scenario\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0mscenario\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m", + "\u001B[0;32m~/virtualenvs/open_ego_notebook/git_repos/eDisGo/edisgo/grid/network.py\u001B[0m in \u001B[0;36mimport_generators\u001B[0;34m(self, generator_scenario)\u001B[0m\n\u001B[1;32m 326\u001B[0m \u001B[0mdata_source\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0;34m'oedb'\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 327\u001B[0m import_generators(network=self.network,\n\u001B[0;32m--> 328\u001B[0;31m data_source=data_source)\n\u001B[0m\u001B[1;32m 329\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 330\u001B[0m \u001B[0;32mdef\u001B[0m \u001B[0manalyze\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mmode\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0;32mNone\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", + "\u001B[0;32m~/virtualenvs/open_ego_notebook/git_repos/eDisGo/edisgo/data/import_data.py\u001B[0m in \u001B[0;36mimport_generators\u001B[0;34m(network, data_source, file)\u001B[0m\n\u001B[1;32m 984\u001B[0m logging.warning('Right now only solar and wind generators can be '\n\u001B[1;32m 985\u001B[0m 'imported from the oedb.')\n\u001B[0;32m--> 986\u001B[0;31m \u001B[0m_import_genos_from_oedb\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mnetwork\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0mnetwork\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 987\u001B[0m \u001B[0;32melif\u001B[0m \u001B[0mdata_source\u001B[0m \u001B[0;34m==\u001B[0m \u001B[0;34m'pypsa'\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 988\u001B[0m \u001B[0m_import_genos_from_pypsa\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mnetwork\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0mnetwork\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mfile\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0mfile\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", + "\u001B[0;32m~/virtualenvs/open_ego_notebook/git_repos/eDisGo/edisgo/data/import_data.py\u001B[0m in \u001B[0;36m_import_genos_from_oedb\u001B[0;34m(network)\u001B[0m\n\u001B[1;32m 1844\u001B[0m \u001B[0;31m#generators_conv_mv = _import_conv_generators()\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 1845\u001B[0m generators_res_mv, generators_res_lv = _import_res_generators(\n\u001B[0;32m-> 1846\u001B[0;31m types_condition)\n\u001B[0m\u001B[1;32m 1847\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 1848\u001B[0m \u001B[0;31m#generators_mv = generators_conv_mv.append(generators_res_mv)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", + "\u001B[0;32m~/virtualenvs/open_ego_notebook/git_repos/eDisGo/edisgo/data/import_data.py\u001B[0m in \u001B[0;36m_import_res_generators\u001B[0;34m(types_filter)\u001B[0m\n\u001B[1;32m 1089\u001B[0m generators_mv = pd.read_sql_query(generators_mv_sqla.statement,\n\u001B[1;32m 1090\u001B[0m \u001B[0msession\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mbind\u001B[0m\u001B[0;34m,\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m-> 1091\u001B[0;31m index_col='id')\n\u001B[0m\u001B[1;32m 1092\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 1093\u001B[0m \u001B[0;31m# define generators with unknown subtype as 'unknown'\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", + "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/pandas/io/sql.py\u001B[0m in \u001B[0;36mread_sql_query\u001B[0;34m(sql, con, index_col, coerce_float, params, parse_dates, chunksize)\u001B[0m\n\u001B[1;32m 330\u001B[0m return pandas_sql.read_query(\n\u001B[1;32m 331\u001B[0m \u001B[0msql\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mindex_col\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0mindex_col\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mparams\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0mparams\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mcoerce_float\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0mcoerce_float\u001B[0m\u001B[0;34m,\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m--> 332\u001B[0;31m parse_dates=parse_dates, chunksize=chunksize)\n\u001B[0m\u001B[1;32m 333\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 334\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n", + "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/pandas/io/sql.py\u001B[0m in \u001B[0;36mread_query\u001B[0;34m(self, sql, index_col, coerce_float, parse_dates, params, chunksize)\u001B[0m\n\u001B[1;32m 1085\u001B[0m \u001B[0margs\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0m_convert_params\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0msql\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mparams\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 1086\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m-> 1087\u001B[0;31m \u001B[0mresult\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mexecute\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;34m*\u001B[0m\u001B[0margs\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 1088\u001B[0m \u001B[0mcolumns\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0mresult\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mkeys\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 1089\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n", + "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/pandas/io/sql.py\u001B[0m in \u001B[0;36mexecute\u001B[0;34m(self, *args, **kwargs)\u001B[0m\n\u001B[1;32m 976\u001B[0m \u001B[0;32mdef\u001B[0m \u001B[0mexecute\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0;34m*\u001B[0m\u001B[0margs\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0;34m**\u001B[0m\u001B[0mkwargs\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 977\u001B[0m \u001B[0;34m\"\"\"Simple passthrough to SQLAlchemy connectable\"\"\"\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m--> 978\u001B[0;31m \u001B[0;32mreturn\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mconnectable\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mexecute\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;34m*\u001B[0m\u001B[0margs\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0;34m**\u001B[0m\u001B[0mkwargs\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 979\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 980\u001B[0m def read_table(self, table_name, index_col=None, coerce_float=True,\n", + "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/base.py\u001B[0m in \u001B[0;36mexecute\u001B[0;34m(self, statement, *multiparams, **params)\u001B[0m\n\u001B[1;32m 2073\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 2074\u001B[0m \u001B[0mconnection\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mcontextual_connect\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mclose_with_result\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0;32mTrue\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m-> 2075\u001B[0;31m \u001B[0;32mreturn\u001B[0m \u001B[0mconnection\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mexecute\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mstatement\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0;34m*\u001B[0m\u001B[0mmultiparams\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0;34m**\u001B[0m\u001B[0mparams\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 2076\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 2077\u001B[0m \u001B[0;32mdef\u001B[0m \u001B[0mscalar\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mstatement\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0;34m*\u001B[0m\u001B[0mmultiparams\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0;34m**\u001B[0m\u001B[0mparams\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", + "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/base.py\u001B[0m in \u001B[0;36mexecute\u001B[0;34m(self, object, *multiparams, **params)\u001B[0m\n\u001B[1;32m 946\u001B[0m \u001B[0;32mraise\u001B[0m \u001B[0mexc\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mObjectNotExecutableError\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mobject\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 947\u001B[0m \u001B[0;32melse\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m--> 948\u001B[0;31m \u001B[0;32mreturn\u001B[0m \u001B[0mmeth\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mmultiparams\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mparams\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 949\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 950\u001B[0m \u001B[0;32mdef\u001B[0m \u001B[0m_execute_function\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mfunc\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mmultiparams\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mparams\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", + "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/sql/elements.py\u001B[0m in \u001B[0;36m_execute_on_connection\u001B[0;34m(self, connection, multiparams, params)\u001B[0m\n\u001B[1;32m 267\u001B[0m \u001B[0;32mdef\u001B[0m \u001B[0m_execute_on_connection\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mconnection\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mmultiparams\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mparams\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 268\u001B[0m \u001B[0;32mif\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0msupports_execution\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m--> 269\u001B[0;31m \u001B[0;32mreturn\u001B[0m \u001B[0mconnection\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m_execute_clauseelement\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mmultiparams\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mparams\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 270\u001B[0m \u001B[0;32melse\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 271\u001B[0m \u001B[0;32mraise\u001B[0m \u001B[0mexc\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mObjectNotExecutableError\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", + "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/base.py\u001B[0m in \u001B[0;36m_execute_clauseelement\u001B[0;34m(self, elem, multiparams, params)\u001B[0m\n\u001B[1;32m 1058\u001B[0m \u001B[0mcompiled_sql\u001B[0m\u001B[0;34m,\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 1059\u001B[0m \u001B[0mdistilled_params\u001B[0m\u001B[0;34m,\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m-> 1060\u001B[0;31m \u001B[0mcompiled_sql\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mdistilled_params\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 1061\u001B[0m )\n\u001B[1;32m 1062\u001B[0m \u001B[0;32mif\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m_has_events\u001B[0m \u001B[0;32mor\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mengine\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m_has_events\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", + "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/base.py\u001B[0m in \u001B[0;36m_execute_context\u001B[0;34m(self, dialect, constructor, statement, parameters, *args)\u001B[0m\n\u001B[1;32m 1198\u001B[0m \u001B[0mparameters\u001B[0m\u001B[0;34m,\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 1199\u001B[0m \u001B[0mcursor\u001B[0m\u001B[0;34m,\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m-> 1200\u001B[0;31m context)\n\u001B[0m\u001B[1;32m 1201\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 1202\u001B[0m \u001B[0;32mif\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m_has_events\u001B[0m \u001B[0;32mor\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mengine\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m_has_events\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", + "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/base.py\u001B[0m in \u001B[0;36m_handle_dbapi_exception\u001B[0;34m(self, e, statement, parameters, cursor, context)\u001B[0m\n\u001B[1;32m 1411\u001B[0m util.raise_from_cause(\n\u001B[1;32m 1412\u001B[0m \u001B[0msqlalchemy_exception\u001B[0m\u001B[0;34m,\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m-> 1413\u001B[0;31m \u001B[0mexc_info\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 1414\u001B[0m )\n\u001B[1;32m 1415\u001B[0m \u001B[0;32melse\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", + "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/util/compat.py\u001B[0m in \u001B[0;36mraise_from_cause\u001B[0;34m(exception, exc_info)\u001B[0m\n\u001B[1;32m 201\u001B[0m \u001B[0mexc_type\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mexc_value\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mexc_tb\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0mexc_info\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 202\u001B[0m \u001B[0mcause\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0mexc_value\u001B[0m \u001B[0;32mif\u001B[0m \u001B[0mexc_value\u001B[0m \u001B[0;32mis\u001B[0m \u001B[0;32mnot\u001B[0m \u001B[0mexception\u001B[0m \u001B[0;32melse\u001B[0m \u001B[0;32mNone\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m--> 203\u001B[0;31m \u001B[0mreraise\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mtype\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mexception\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mexception\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mtb\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0mexc_tb\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mcause\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0mcause\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 204\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 205\u001B[0m \u001B[0;32mif\u001B[0m \u001B[0mpy3k\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", + "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/util/compat.py\u001B[0m in \u001B[0;36mreraise\u001B[0;34m(tp, value, tb, cause)\u001B[0m\n\u001B[1;32m 184\u001B[0m \u001B[0mvalue\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m__cause__\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0mcause\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 185\u001B[0m \u001B[0;32mif\u001B[0m \u001B[0mvalue\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m__traceback__\u001B[0m \u001B[0;32mis\u001B[0m \u001B[0;32mnot\u001B[0m \u001B[0mtb\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m--> 186\u001B[0;31m \u001B[0;32mraise\u001B[0m \u001B[0mvalue\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mwith_traceback\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mtb\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 187\u001B[0m \u001B[0;32mraise\u001B[0m \u001B[0mvalue\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 188\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n", + "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/base.py\u001B[0m in \u001B[0;36m_execute_context\u001B[0;34m(self, dialect, constructor, statement, parameters, *args)\u001B[0m\n\u001B[1;32m 1191\u001B[0m \u001B[0mstatement\u001B[0m\u001B[0;34m,\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 1192\u001B[0m \u001B[0mparameters\u001B[0m\u001B[0;34m,\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m-> 1193\u001B[0;31m context)\n\u001B[0m\u001B[1;32m 1194\u001B[0m \u001B[0;32mexcept\u001B[0m \u001B[0mBaseException\u001B[0m \u001B[0;32mas\u001B[0m \u001B[0me\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 1195\u001B[0m self._handle_dbapi_exception(\n", + "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/default.py\u001B[0m in \u001B[0;36mdo_execute\u001B[0;34m(self, cursor, statement, parameters, context)\u001B[0m\n\u001B[1;32m 505\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 506\u001B[0m \u001B[0;32mdef\u001B[0m \u001B[0mdo_execute\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mcursor\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mstatement\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mparameters\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mcontext\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0;32mNone\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m--> 507\u001B[0;31m \u001B[0mcursor\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mexecute\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mstatement\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mparameters\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 508\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 509\u001B[0m \u001B[0;32mdef\u001B[0m \u001B[0mdo_execute_no_params\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mcursor\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mstatement\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mcontext\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0;32mNone\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", + "\u001B[0;31mProgrammingError\u001B[0m: (psycopg2.ProgrammingError) relation \"model_draft.ego_supply_res_powerplant_nep2035_mview\" does not exist\nLINE 2: FROM model_draft.ego_supply_res_powerplant_nep2035_mview \n ^\n [SQL: 'SELECT model_draft.ego_supply_res_powerplant_nep2035_mview.id, model_draft.ego_supply_res_powerplant_nep2035_mview.subst_id, model_draft.ego_supply_res_powerplant_nep2035_mview.la_id, model_draft.ego_supply_res_powerplant_nep2035_mview.mvlv_subst_id, model_draft.ego_supply_res_powerplant_nep2035_mview.electrical_capacity, model_draft.ego_supply_res_powerplant_nep2035_mview.generation_type, model_draft.ego_supply_res_powerplant_nep2035_mview.generation_subtype, model_draft.ego_supply_res_powerplant_nep2035_mview.voltage_level, ST_AsText(ST_Transform(model_draft.ego_supply_res_powerplant_nep2035_mview.rea_geom_new, %(ST_Transform_1)s)) AS geom, ST_AsText(ST_Transform(model_draft.ego_supply_res_powerplant_nep2035_mview.geom, %(ST_Transform_2)s)) AS geom_em \\nFROM model_draft.ego_supply_res_powerplant_nep2035_mview \\nWHERE model_draft.ego_supply_res_powerplant_nep2035_mview.subst_id = %(subst_id_1)s AND model_draft.ego_supply_res_powerplant_nep2035_mview.generation_type IN (%(generation_type_1)s, %(generation_type_2)s) AND model_draft.ego_supply_res_powerplant_nep2035_mview.voltage_level IN (%(voltage_level_1)s, %(voltage_level_2)s)'] [parameters: {'ST_Transform_1': 4326, 'ST_Transform_2': 4326, 'subst_id_1': 239, 'generation_type_1': 'solar', 'generation_type_2': 'wind', 'voltage_level_1': 4, 'voltage_level_2': 5}] (Background on this error at: http://sqlalche.me/e/f405)" ] } ], diff --git a/ego/tools/utilities.py b/ego/tools/utilities.py index 1a9f3112..86b6fcb9 100644 --- a/ego/tools/utilities.py +++ b/ego/tools/utilities.py @@ -25,18 +25,15 @@ import os import sys -import pandas as pd - -logger = logging.getLogger(__name__) - from time import localtime, strftime -if not "READTHEDOCS" in os.environ: +from sqlalchemy.orm import scoped_session, sessionmaker - from egoio.db_tables import model_draft, grid +if "READTHEDOCS" not in os.environ: from egoio.tools import db -from sqlalchemy.orm import scoped_session, sessionmaker +logger = logging.getLogger(__name__) + __copyright__ = ( "Flensburg University of Applied Sciences, " @@ -160,7 +157,7 @@ def get_scenario_setting(jsonpath="scenario_setting.json"): if json_file["eGo"]["csv_import_eTraGo"] and json_file["eGo"]["csv_import_eDisGo"]: logger.info("eDisGo and eTraGo results will be imported from csv\n") - if json_file["eGo"].get("eTraGo") == True: + if json_file["eGo"].get("eTraGo") is True: logger.info("Using and importing eTraGo settings") @@ -188,7 +185,7 @@ def get_scenario_setting(jsonpath="scenario_setting.json"): if json_file["eTraGo"].get("extendable") == "['storage']": json_file["eTraGo"].update({"extendable": ["storage"]}) - if json_file["eGo"].get("eDisGo") == True: + if json_file["eGo"].get("eDisGo") is True: logger.info("Using and importing eDisGo settings") return json_file @@ -212,9 +209,9 @@ def fix_leading_separator(csv_file, **kwargs): writer = csv.writer(out, **kwargs) writer.writerow(first_line[1:]) for line in lines: - l = line[2:] - l.insert(0, line[0]) - writer.writerow(l, **kwargs) + line_selection = line[2:] + line_selection.insert(0, line[0]) + writer.writerow(line_selection, **kwargs) os.rename(tmp_file, csv_file) From a20bcb0f449da48e292c2c355d02c52c1cebd630 Mon Sep 17 00:00:00 2001 From: Malte Jahn Date: Thu, 9 Mar 2023 10:29:50 +0100 Subject: [PATCH 036/126] Read database and ssh from secondary config --- .../mv_clustering/cluster_setting.json | 1 + .../read_config/example_read_setting.py | 22 +++++ .../read_config/external_config_setting.json | 86 +++++++++++++++++++ .../no_external_config_setting.json | 86 +++++++++++++++++++ ego/tools/utilities.py | 10 +++ 5 files changed, 205 insertions(+) create mode 100644 ego/examples/read_config/example_read_setting.py create mode 100644 ego/examples/read_config/external_config_setting.json create mode 100644 ego/examples/read_config/no_external_config_setting.json diff --git a/ego/examples/mv_clustering/cluster_setting.json b/ego/examples/mv_clustering/cluster_setting.json index a21b7cd2..b5f6d7ad 100644 --- a/ego/examples/mv_clustering/cluster_setting.json +++ b/ego/examples/mv_clustering/cluster_setting.json @@ -57,6 +57,7 @@ "solver": "glpk", "timesteps_pfa": "snapshot_analysis" }, + "enable_external_config":true, "mv_clustering": { "database": { "database_name": "egon-data", diff --git a/ego/examples/read_config/example_read_setting.py b/ego/examples/read_config/example_read_setting.py new file mode 100644 index 00000000..ae90100e --- /dev/null +++ b/ego/examples/read_config/example_read_setting.py @@ -0,0 +1,22 @@ +# flake8: noqa: E402 +import warnings + +warnings.filterwarnings("ignore") + +from edisgo.tools.logger import setup_logger + +from ego.tools.utilities import get_scenario_setting + +setup_logger( + loggers=[ + {"name": "root", "file_level": None, "stream_level": "warning"}, + {"name": "ego", "file_level": None, "stream_level": "debug"}, + {"name": "edisgo", "file_level": None, "stream_level": "info"}, + ] +) + +config = get_scenario_setting(jsonpath="external_config_setting.json") + +config = get_scenario_setting(jsonpath="no_external_config_setting.json") + +print("THE END") diff --git a/ego/examples/read_config/external_config_setting.json b/ego/examples/read_config/external_config_setting.json new file mode 100644 index 00000000..8505d2a9 --- /dev/null +++ b/ego/examples/read_config/external_config_setting.json @@ -0,0 +1,86 @@ +{ + "eGo": { + "eTraGo": true, + "eDisGo": true, + "csv_import_eTraGo": false, + "csv_import_eDisGo": false, + "random_seed": 42 + }, + "eTraGo": { + "db": "oedb", + "gridversion": "v0.4.5", + "method": "lopf", + "pf_post_lopf": false, + "start_snapshot": 1000, + "end_snapshot" : 1005, + "solver": "gurobi", + "solver_options":{}, + "scn_name": "eGo 100", + "scn_extension": null, + "scn_decommissioning": null, + "lpfile": false, + "csv_export": "results/your_results", + "db_export": false, + "extendable": ["storage", "network"], + "generator_noise": 789456, + "minimize_loading": false, + "ramp_limits": false, + "extra_functionality": null, + "network_clustering_kmeans": 10, + "load_cluster": false, + "network_clustering_ehv": false, + "disaggregation": "uniform", + "snapshot_clustering": false, + "parallelisation": false, + "skip_snapshots": false, + "line_grouping": false, + "branch_capacity_factor": {"HV": 0.5, "eHV" : 0.7}, + "load_shedding": false, + "foreign_lines" :{"carrier": "AC", "capacity": "osmTGmod"}, + "comments": "" + }, + "eDisGo": { + "db": "oedb", + "gridversion": "v0.5.0", + "grid_path": "/home/local/RL-INSTITUT/malte.jahn/git-repos/data/ego/grids", + "choice_mode": "cluster", + "cluster_attributes":["wind_cap", "solar_cap", "emob_cap"], + "only_cluster": false, + "manual_grids": [3280], + "n_clusters": 50, + "parallelization":false, + "max_calc_time": 0.5, + "max_workers":2, + "initial_reinforcement":true, + "max_cos_phi_renewable": 0.9, + "results": "/home/local/RL-INSTITUT/malte.jahn/git-repos/data/ego", + "solver": "glpk", + "timesteps_pfa": "snapshot_analysis" + }, + "database": { + "database_name": "egon-data", + "host": "127.0.0.1", + "port": "port", + "user": "username", + "password": "password", + "tables": { + "egon_mv_grid_district": "grid.egon_mv_grid_district", + "egon_hvmv_substation": "grid.egon_hvmv_substation", + "generators_pv": "supply.egon_power_plants_pv", + "generators_pv_rooftop": "supply.egon_power_plants_pv_roof_building", + "generators_wind": "supply.egon_power_plants_wind", + "etrago_load": "grid.egon_etrago_load", + "etrago_load_timeseries": "grid.egon_etrago_load_timeseries", + "weather_cells": "supply.egon_era5_weather_cells" + } + }, + "ssh": { + "enabled": false, + "user": "username", + "ip": "ip", + "port": "port", + "local_address": "127.0.0.1", + "local_port": "local_port" + }, + "external_config": "~/.ego/secondary_ego_config.json" +} diff --git a/ego/examples/read_config/no_external_config_setting.json b/ego/examples/read_config/no_external_config_setting.json new file mode 100644 index 00000000..32e35d33 --- /dev/null +++ b/ego/examples/read_config/no_external_config_setting.json @@ -0,0 +1,86 @@ +{ + "eGo": { + "eTraGo": true, + "eDisGo": true, + "csv_import_eTraGo": false, + "csv_import_eDisGo": false, + "random_seed": 42 + }, + "eTraGo": { + "db": "oedb", + "gridversion": "v0.4.5", + "method": "lopf", + "pf_post_lopf": false, + "start_snapshot": 1000, + "end_snapshot" : 1005, + "solver": "gurobi", + "solver_options":{}, + "scn_name": "eGo 100", + "scn_extension": null, + "scn_decommissioning": null, + "lpfile": false, + "csv_export": "results/your_results", + "db_export": false, + "extendable": ["storage", "network"], + "generator_noise": 789456, + "minimize_loading": false, + "ramp_limits": false, + "extra_functionality": null, + "network_clustering_kmeans": 10, + "load_cluster": false, + "network_clustering_ehv": false, + "disaggregation": "uniform", + "snapshot_clustering": false, + "parallelisation": false, + "skip_snapshots": false, + "line_grouping": false, + "branch_capacity_factor": {"HV": 0.5, "eHV" : 0.7}, + "load_shedding": false, + "foreign_lines" :{"carrier": "AC", "capacity": "osmTGmod"}, + "comments": "" + }, + "eDisGo": { + "db": "oedb", + "gridversion": "v0.5.0", + "grid_path": "/home/local/RL-INSTITUT/malte.jahn/git-repos/data/ego/grids", + "choice_mode": "cluster", + "cluster_attributes":["wind_cap", "solar_cap", "emob_cap"], + "only_cluster": false, + "manual_grids": [3280], + "n_clusters": 50, + "parallelization":false, + "max_calc_time": 0.5, + "max_workers":2, + "initial_reinforcement":true, + "max_cos_phi_renewable": 0.9, + "results": "/home/local/RL-INSTITUT/malte.jahn/git-repos/data/ego", + "solver": "glpk", + "timesteps_pfa": "snapshot_analysis" + }, + "database": { + "database_name": "egon-data", + "host": "127.0.0.1", + "port": "port", + "user": "username", + "password": "password", + "tables": { + "egon_mv_grid_district": "grid.egon_mv_grid_district", + "egon_hvmv_substation": "grid.egon_hvmv_substation", + "generators_pv": "supply.egon_power_plants_pv", + "generators_pv_rooftop": "supply.egon_power_plants_pv_roof_building", + "generators_wind": "supply.egon_power_plants_wind", + "etrago_load": "grid.egon_etrago_load", + "etrago_load_timeseries": "grid.egon_etrago_load_timeseries", + "weather_cells": "supply.egon_era5_weather_cells" + } + }, + "ssh": { + "enabled": false, + "user": "username", + "ip": "ip", + "port": "port", + "local_address": "127.0.0.1", + "local_port": "local_port" + }, + "external_config": false +} diff --git a/ego/tools/utilities.py b/ego/tools/utilities.py index 86b6fcb9..15a7f0ff 100644 --- a/ego/tools/utilities.py +++ b/ego/tools/utilities.py @@ -188,6 +188,16 @@ def get_scenario_setting(jsonpath="scenario_setting.json"): if json_file["eGo"].get("eDisGo") is True: logger.info("Using and importing eDisGo settings") + if isinstance(json_file["external_config"], str): + path_external_config = os.path.expanduser(json_file["external_config"]) + logger.info(f"Load external config with path: {path_external_config}.") + with open(path_external_config) as f: + external_config = json.load(f) + for key in ["database", "ssh"]: + json_file[key].update(external_config[key]) + else: + logger.info("Don't load external config.") + return json_file From b5897a003158a5f0216e67e5f8c56dc5f3aea50a Mon Sep 17 00:00:00 2001 From: Malte Jahn Date: Thu, 9 Mar 2023 11:25:06 +0100 Subject: [PATCH 037/126] Update edisgo_mwe.py and settings - add data_in and data_out to config --- ego/examples/edisgo_mwe.py | 26 --------------- ego/examples/edisgo_workflow/edisgo_mwe.py | 17 ++++++++++ .../scenario_setting.json | 32 ++++++++++++++----- .../read_config/external_config_setting.json | 15 ++------- .../no_external_config_setting.json | 2 ++ ego/tools/utilities.py | 13 ++++++-- 6 files changed, 55 insertions(+), 50 deletions(-) delete mode 100644 ego/examples/edisgo_mwe.py create mode 100644 ego/examples/edisgo_workflow/edisgo_mwe.py rename ego/examples/{ => edisgo_workflow}/scenario_setting.json (60%) diff --git a/ego/examples/edisgo_mwe.py b/ego/examples/edisgo_mwe.py deleted file mode 100644 index 8f028cfc..00000000 --- a/ego/examples/edisgo_mwe.py +++ /dev/null @@ -1,26 +0,0 @@ -import os - -from pypsa import Network as PyPSANetwork - -from ego.tools.edisgo_integration import EDisGoNetworks -from ego.tools.utilities import get_scenario_setting - -base_path = os.path.join(os.path.expanduser("~"), "git-repos", "data") - -# eTraGo network is not yet disaggregated -etrago_network = PyPSANetwork( - os.path.join(base_path, "etrago_results/disaggregated_network") -) -# manually overwrite bus ID to have busses in the chosen grid -# etrago_network.generators.loc[etrago_network.generators.bus == "16", "bus"] = "26533" - -json_file = get_scenario_setting() - -edisgo_networks = EDisGoNetworks(json_file=json_file, etrago_network=etrago_network) - - -etrago_network.generators.carrier.unique().tolist() -etrago_network.links.carrier.unique().tolist() -etrago_network.storage_units.carrier.unique().tolist() -etrago_network.stores.carrier.unique().tolist() -print("x") diff --git a/ego/examples/edisgo_workflow/edisgo_mwe.py b/ego/examples/edisgo_workflow/edisgo_mwe.py new file mode 100644 index 00000000..ee0f713f --- /dev/null +++ b/ego/examples/edisgo_workflow/edisgo_mwe.py @@ -0,0 +1,17 @@ +import os + +from pypsa import Network as PyPSANetwork + +from ego.tools.edisgo_integration import EDisGoNetworks +from ego.tools.utilities import get_scenario_setting + +config = get_scenario_setting() +data_dir = config["eGo"]["data_dir"] + +etrago_network = PyPSANetwork( + os.path.join(data_dir, "etrago_disaggregated_pf_post_lopf_false") +) + +edisgo_networks = EDisGoNetworks(json_file=config, etrago_network=etrago_network) + +print("THE END") diff --git a/ego/examples/scenario_setting.json b/ego/examples/edisgo_workflow/scenario_setting.json similarity index 60% rename from ego/examples/scenario_setting.json rename to ego/examples/edisgo_workflow/scenario_setting.json index b2e201fc..bfeaa2ae 100644 --- a/ego/examples/scenario_setting.json +++ b/ego/examples/edisgo_workflow/scenario_setting.json @@ -3,7 +3,10 @@ "eTraGo": true, "eDisGo": true, "csv_import_eTraGo": false, - "csv_import_eDisGo": false + "csv_import_eDisGo": false, + "data_dir": ".../data_in", + "results_dir": ".../data_out", + "random_seed": 42 }, "eTraGo": { "db": "oedb", @@ -40,20 +43,33 @@ }, "eDisGo": { "db": "oedb", - "gridversion": "v0.4.5", - "grid_path": "/home/birgit/virtualenvs/eGo_interface_development/git_repos/data/ding0_grids", - "choice_mode": "manual", - "cluster_attributes":["farthest_node", "wind_cap", "solar_cap", "extended_storage"], + "gridversion": "v0.5.0", + "grid_path": "grids", + "choice_mode": "cluster", + "cluster_attributes":["wind_cap", "solar_cap", "emob_cap"], "only_cluster": false, "manual_grids": [3280], - "no_grids": 2, + "n_clusters": 50, "parallelization":false, "max_calc_time": 0.5, "max_workers":2, "initial_reinforcement":true, "max_cos_phi_renewable": 0.9, - "results": "/home/birgit/virtualenvs/eGo_interface_development/git_repos/data", + "results": "results_edisgo", "solver": "glpk", "timesteps_pfa": "snapshot_analysis" - } + }, + "database": { + "tables": { + "egon_mv_grid_district": "grid.egon_mv_grid_district", + "egon_hvmv_substation": "grid.egon_hvmv_substation", + "generators_pv": "supply.egon_power_plants_pv", + "generators_pv_rooftop": "supply.egon_power_plants_pv_roof_building", + "generators_wind": "supply.egon_power_plants_wind", + "etrago_load": "grid.egon_etrago_load", + "etrago_load_timeseries": "grid.egon_etrago_load_timeseries", + "weather_cells": "supply.egon_era5_weather_cells" + } + }, + "external_config": "~/.ego/secondary_ego_config.json" } diff --git a/ego/examples/read_config/external_config_setting.json b/ego/examples/read_config/external_config_setting.json index 8505d2a9..5a0cc58b 100644 --- a/ego/examples/read_config/external_config_setting.json +++ b/ego/examples/read_config/external_config_setting.json @@ -4,6 +4,8 @@ "eDisGo": true, "csv_import_eTraGo": false, "csv_import_eDisGo": false, + "data_dir": ".../data_in", + "results_dir": ".../data_out", "random_seed": 42 }, "eTraGo": { @@ -58,11 +60,6 @@ "timesteps_pfa": "snapshot_analysis" }, "database": { - "database_name": "egon-data", - "host": "127.0.0.1", - "port": "port", - "user": "username", - "password": "password", "tables": { "egon_mv_grid_district": "grid.egon_mv_grid_district", "egon_hvmv_substation": "grid.egon_hvmv_substation", @@ -74,13 +71,5 @@ "weather_cells": "supply.egon_era5_weather_cells" } }, - "ssh": { - "enabled": false, - "user": "username", - "ip": "ip", - "port": "port", - "local_address": "127.0.0.1", - "local_port": "local_port" - }, "external_config": "~/.ego/secondary_ego_config.json" } diff --git a/ego/examples/read_config/no_external_config_setting.json b/ego/examples/read_config/no_external_config_setting.json index 32e35d33..e7cf1a9a 100644 --- a/ego/examples/read_config/no_external_config_setting.json +++ b/ego/examples/read_config/no_external_config_setting.json @@ -4,6 +4,8 @@ "eDisGo": true, "csv_import_eTraGo": false, "csv_import_eDisGo": false, + "data_dir": ".../data_in", + "results_dir": ".../data_out", "random_seed": 42 }, "eTraGo": { diff --git a/ego/tools/utilities.py b/ego/tools/utilities.py index 15a7f0ff..c5a01c7f 100644 --- a/ego/tools/utilities.py +++ b/ego/tools/utilities.py @@ -190,14 +190,21 @@ def get_scenario_setting(jsonpath="scenario_setting.json"): if isinstance(json_file["external_config"], str): path_external_config = os.path.expanduser(json_file["external_config"]) - logger.info(f"Load external config with path: {path_external_config}.") + logger.info(f"Load external config with path: {path_external_config}") with open(path_external_config) as f: external_config = json.load(f) - for key in ["database", "ssh"]: - json_file[key].update(external_config[key]) + for key in external_config.keys(): + try: + json_file[key].update(external_config[key]) + except KeyError: + json_file[key] = external_config[key] else: logger.info("Don't load external config.") + # expand directories + for key in ["data_dir", "results_dir"]: + json_file["eGo"][key] = os.path.expanduser(json_file["eGo"][key]) + return json_file From 0a6cd9530c60be5d1a18c62779f22b803bb3b8ad Mon Sep 17 00:00:00 2001 From: Malte Jahn Date: Thu, 9 Mar 2023 12:33:08 +0100 Subject: [PATCH 038/126] Save config as a results --- ego/tools/utilities.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ego/tools/utilities.py b/ego/tools/utilities.py index c5a01c7f..8107cbc1 100644 --- a/ego/tools/utilities.py +++ b/ego/tools/utilities.py @@ -205,6 +205,15 @@ def get_scenario_setting(jsonpath="scenario_setting.json"): for key in ["data_dir", "results_dir"]: json_file["eGo"][key] = os.path.expanduser(json_file["eGo"][key]) + # Serializing json + json_object = json.dumps(json_file, indent=4) + + # Writing to sample.json + with open( + os.path.join(json_file["eGo"]["results_dir"], "config.json"), "w" + ) as outfile: + outfile.write(json_object) + return json_file From 35f20b8ea6b2388cc1e4ee2612021e5f915711b3 Mon Sep 17 00:00:00 2001 From: Malte Jahn Date: Fri, 10 Mar 2023 11:38:21 +0100 Subject: [PATCH 039/126] Update filepaths, logging and run_edisgo behaviour - Update the levels of the config - Try to use data_dir and results_dir of the config in the edisgo part - Log edisgo runs to separate files --- ego/examples/edisgo_workflow/edisgo_mwe.py | 20 +++++- ego/mv_clustering/database.py | 9 +-- ego/mv_clustering/mv_clustering.py | 9 +-- ego/tools/edisgo_integration.py | 84 +++++++++++++++------- 4 files changed, 86 insertions(+), 36 deletions(-) diff --git a/ego/examples/edisgo_workflow/edisgo_mwe.py b/ego/examples/edisgo_workflow/edisgo_mwe.py index ee0f713f..459157f3 100644 --- a/ego/examples/edisgo_workflow/edisgo_mwe.py +++ b/ego/examples/edisgo_workflow/edisgo_mwe.py @@ -1,17 +1,35 @@ import os +from edisgo.tools.logger import setup_logger from pypsa import Network as PyPSANetwork +from ego.mv_clustering.database import sshtunnel from ego.tools.edisgo_integration import EDisGoNetworks from ego.tools.utilities import get_scenario_setting config = get_scenario_setting() + +data_dir = config["eGo"]["data_dir"] +results_dir = config["eGo"]["results_dir"] + +setup_logger( + loggers=[ + {"name": "root", "file_level": "warning", "stream_level": "warning"}, + {"name": "ego", "file_level": "debug", "stream_level": "debug"}, + {"name": "edisgo", "file_level": None, "stream_level": None}, + ], + file_name="ego.log", + log_dir=results_dir, +) + +base_path = os.path.join(os.path.expanduser("~"), "git-repos", "data", "ego") data_dir = config["eGo"]["data_dir"] etrago_network = PyPSANetwork( os.path.join(data_dir, "etrago_disaggregated_pf_post_lopf_false") ) -edisgo_networks = EDisGoNetworks(json_file=config, etrago_network=etrago_network) +with sshtunnel(config=config): + edisgo_networks = EDisGoNetworks(json_file=config, etrago_network=etrago_network) print("THE END") diff --git a/ego/mv_clustering/database.py b/ego/mv_clustering/database.py index 9d672ce1..5dc424be 100644 --- a/ego/mv_clustering/database.py +++ b/ego/mv_clustering/database.py @@ -15,7 +15,7 @@ def get_engine(config=None): - config = config["mv_clustering"]["database"] + config = config["database"] engine = create_engine( f"postgresql+psycopg2://{config['user']}:" f"{config['password']}@{config['host']}:" @@ -28,7 +28,7 @@ def get_engine(config=None): @contextmanager def sshtunnel(config=None): - ssh_config = config["mv_clustering"]["database"]["ssh"] + ssh_config = config["ssh"] if ssh_config["enabled"]: try: logger.info("Open ssh tunnel.") @@ -52,7 +52,8 @@ def sshtunnel(config=None): proc.kill() outs, errs = proc.communicate() logger.info( - f"SSH process output {outs.decode('utf-8')=}, {errs.decode('utf-8')=}" + f"SSH process output STDOUT:{outs.decode('utf-8')}, " + f"STDERR:{errs.decode('utf-8')}" ) else: try: @@ -89,7 +90,7 @@ def wrapper(*args, **kwargs): def register_tables_in_saio(engine, config=None): - db_tables = config["mv_clustering"]["database"]["tables"] + db_tables = config["database"]["tables"] orm = {} for name, table_str in db_tables.items(): diff --git a/ego/mv_clustering/mv_clustering.py b/ego/mv_clustering/mv_clustering.py index 2659fa7d..3f5f7bfe 100644 --- a/ego/mv_clustering/mv_clustering.py +++ b/ego/mv_clustering/mv_clustering.py @@ -172,9 +172,9 @@ def mv_grid_clustering(data_df, working_grids=None, config=None): def cluster_workflow(config=None): - attributes_path = os.path.join(config["eDisGo"]["grid_path"], "attributes.csv") + attributes_path = os.path.join(config["eGo"]["results_dir"], "attributes.csv") working_grids_path = os.path.join( - config["eDisGo"]["grid_path"], "working_grids.csv" + config["eGo"]["data_dir"], config["eDisGo"]["grid_path"], "working_grids.csv" ) if not os.path.isfile(attributes_path): @@ -187,8 +187,9 @@ def cluster_workflow(config=None): if os.path.isfile(working_grids_path): working_grids = pd.read_csv(working_grids_path, index_col=0) else: - logger.info("'working_grids.csv' is missing, select representative grids.") - working_grids = None + raise FileNotFoundError("Working_grids aure missing.") + # logger.info("'working_grids.csv' is missing, select representative grids.") + # working_grids = None return mv_grid_clustering( data_to_cluster, working_grids=working_grids, config=config diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index fa3c894a..db99181f 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -53,6 +53,7 @@ from edisgo.flex_opt import q_control from edisgo.network.results import Results from edisgo.network.timeseries import TimeSeries + from edisgo.tools.logger import setup_logger from edisgo.tools.plots import mv_grid_topology from egoio.db_tables import grid, model_draft from egoio.tools import db @@ -336,7 +337,7 @@ def _init_status(self): * 'end_time' - end time of calculation """ - self._status_dir = "status" + self._status_dir = os.path.join(self._json_file["eGo"]["results_dir"], "status") if not os.path.exists(self._status_dir): os.makedirs(self._status_dir) @@ -661,7 +662,7 @@ def _set_grid_choice(self): n_clusters = self._json_file["eDisGo"]["n_clusters"] n_clusters_found = cluster_df.shape[0] - if n_clusters != n_clusters_found: + if n_clusters == n_clusters_found: logger.info(f"Clustering to {n_clusters} MV grids") else: logger.warning( @@ -707,8 +708,9 @@ def _run_edisgo_pool(self): """ parallelization = self._parallelization - if not os.path.exists(self._results): - os.makedirs(self._results) + results_dir = os.path.join(self._json_file["eGo"]["results_dir"], self._results) + if not os.path.exists(results_dir): + os.makedirs(results_dir) if parallelization is True: logger.info("Run eDisGo parallel") @@ -771,6 +773,25 @@ def _run_edisgo(self, mv_grid_id): Returns the complete eDisGo container, also including results """ + results_dir = os.path.join( + self._json_file["eGo"]["results_dir"], self._results, str(mv_grid_id) + ) + if not os.path.exists(results_dir): + os.makedirs(results_dir) + if self._parallelization: + stream_level = None + else: + stream_level = "debug" + setup_logger( + loggers=[ + # {"name": "root", "file_level": None, "stream_level": None}, + # {"name": "ego", "file_level": None, "stream_level": None}, + {"name": "edisgo", "file_level": "debug", "stream_level": stream_level}, + ], + file_name=f"run_edisgo_{mv_grid_id}.log", + log_dir=results_dir, + ) + logger = logging.getLogger("edisgo.external.ego._run_edisgo") self._status_update(mv_grid_id, "start", show=False) logger.info("MV grid {}: Calculating interface values".format(mv_grid_id)) @@ -789,7 +810,12 @@ def _run_edisgo(self, mv_grid_id): ) # get ding0 MV grid path - grid_path = os.path.join(self._grid_path, "working_grids", str(mv_grid_id)) + grid_path = os.path.join( + config["eGo"]["data_dir"], + config["eDisGo"]["grid_path"], + "working_grids", + str(mv_grid_id), + ) if not os.path.isdir(grid_path): msg = "No grid data for MV grid {}".format(mv_grid_id) @@ -799,7 +825,11 @@ def _run_edisgo(self, mv_grid_id): # Initialize MV grid logger.info(f"MV grid {mv_grid_id}: Initialize MV grid") - edisgo_grid = import_edisgo_from_files(edisgo_path=grid_path) + edisgo_grid = import_edisgo_from_files( + edisgo_path=grid_path, import_config=False + ) + # Reload the original/default eDisGo configs + edisgo_grid.config = {"from_json": False} # ##################### Conduct initial grid reinforcement #################### edisgo_grid.set_time_series_worst_case_analysis() @@ -810,16 +840,17 @@ def _run_edisgo(self, mv_grid_id): ).format(mv_grid_id) ) - edisgo_grid.config["grid_expansion_allowed_voltage_deviations"] = { - "hv_mv_trafo_offset": 0.04, - "hv_mv_trafo_control_deviation": 0.0, - "mv_load_case_max_v_deviation": 0.055, - "mv_feedin_case_max_v_deviation": 0.02, - "lv_load_case_max_v_deviation": 0.065, - "lv_feedin_case_max_v_deviation": 0.03, - "mv_lv_station_load_case_max_v_deviation": 0.02, - "mv_lv_station_feedin_case_max_v_deviation": 0.01, - } + # ToDo: Not complete missing "feed-in_case_lower" and "load_case_upper" + # edisgo_grid.config["grid_expansion_allowed_voltage_deviations"] = { + # "hv_mv_trafo_offset": 0.04, + # "hv_mv_trafo_control_deviation": 0.0, + # "mv_load_case_max_v_deviation": 0.055, + # "mv_feedin_case_max_v_deviation": 0.02, + # "lv_load_case_max_v_deviation": 0.065, + # "lv_feedin_case_max_v_deviation": 0.03, + # "mv_lv_station_load_case_max_v_deviation": 0.02, + # "mv_lv_station_feedin_case_max_v_deviation": 0.01, + # } # Inital grid reinforcements logger.info( @@ -855,8 +886,7 @@ def _run_edisgo(self, mv_grid_id): ) ) edisgo_grid.results = Results(edisgo_grid) - # Reload the (original) eDisGo configs - edisgo_grid.config = "default" + edisgo_grid.timeseries = TimeSeries(timeindex=specs["timeindex"]) # ########################################################################### @@ -1029,9 +1059,8 @@ def _run_edisgo(self, mv_grid_id): self._status_update(mv_grid_id, "end") - path = os.path.join(self._results, str(mv_grid_id)) edisgo_grid.save( - directory=path, + directory=results_dir, save_topology=True, save_timeseries=True, save_results=True, @@ -1045,17 +1074,17 @@ def _run_edisgo(self, mv_grid_id): }, ) - return {edisgo_grid.topology.id: path} + return {edisgo_grid.topology.id: results_dir} def _save_edisgo_results(self): + results_dir = os.path.join(self._json_file["eGo"]["results_dir"], self._results) + if not os.path.exists(results_dir): + os.makedirs(results_dir) - if not os.path.exists(self._results): - os.makedirs(self._results) - - with open(os.path.join(self._results, "edisgo_args.json"), "w") as fp: + with open(os.path.join(results_dir, "edisgo_args.json"), "w") as fp: json.dump(self._edisgo_args, fp) - self._grid_choice.to_csv(self._results + "/grid_choice.csv") + self._grid_choice.to_csv(os.path.join(results_dir, "grid_choice.csv")) def _load_edisgo_results(self): """ @@ -1068,8 +1097,9 @@ def _load_edisgo_results(self): """ # Load the grid choice from CSV + results_dir = os.path.join(self._json_file["eGo"]["results_dir"], self._results) self._grid_choice = pd.read_csv( - os.path.join(self._csv_import, "grid_choice.csv"), index_col=0 + os.path.join(results_dir, "grid_choice.csv"), index_col=0 ) self._grid_choice["represented_grids"] = self._grid_choice.apply( lambda x: eval(x["represented_grids"]), axis=1 From fd88a216460cfef0b09079c35cd4f40d1d260c1d Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 21 Mar 2023 09:38:32 +0100 Subject: [PATCH 040/126] Change storage interface --- ego/tools/interface.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/ego/tools/interface.py b/ego/tools/interface.py index cc6c220c..18db5f85 100644 --- a/ego/tools/interface.py +++ b/ego/tools/interface.py @@ -551,6 +551,13 @@ def renewable_generators(): def storages(): # Storage # Filter batteries + # ToDo @Malte subtract p_min from p_nom_opt in order to determine additional + # storage capacity from optimisation - hab ich unten schon gemacht, aber p_min + # fehlt noch im storages_df + # ToDo @Malte add 'storage_units_p_nom' (with p_nom_opt - p_min in MW) to + # results dictionary - hab ich schon gemacht, bitte prüfen und docstring anpassen + # ToDo @Malte return max_hours instead of capacity - hab ich schon gemacht, + # bitte prüfen und docstring anpassen min_extended = 0 logger.info(f"Minimum storage of {min_extended} MW") @@ -562,9 +569,13 @@ def storages(): # & (etrago_obj.storage_units["max_hours"] <= 20.0) ] if not storages_df.empty: + # p_nom + storages_df_p_nom = ( + storages_df["p_nom_opt"]# - storages_df["p_min"] + ).values[0] # Capacity - storages_df_capacity = ( - storages_df["p_nom_opt"] * storages_df["max_hours"] + storages_df_max_hours = ( + storages_df["max_hours"] ).values[0] storages_df_p = etrago_obj.storage_units_t["p"][storages_df.index] @@ -581,7 +592,8 @@ def storages(): 0.0, index=timeseries_index, columns=[storages_df["carrier"]] ) else: - storages_df_capacity = 0 + storages_df_p_nom = 0 + storages_df_max_hours = 0 storages_df_p = pd.DataFrame( 0.0, index=timeseries_index, columns=[storages_df["carrier"]] ) @@ -589,7 +601,8 @@ def storages(): 0.0, index=timeseries_index, columns=[storages_df["carrier"]] ) - results["storage_units_capacity"] = storages_df_capacity + results["storage_units_p_nom"] = storages_df_p_nom + results["storage_units_max_hours"] = storages_df_max_hours results["storage_units_active_power"] = storages_df_p results["storage_units_reactive_power"] = storages_df_q From dfcd31775e1944ea18206261920d823fc49e5117 Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 21 Mar 2023 09:42:00 +0100 Subject: [PATCH 041/126] Bug fix create directory if it does not exist --- ego/tools/utilities.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ego/tools/utilities.py b/ego/tools/utilities.py index 8107cbc1..404099b1 100644 --- a/ego/tools/utilities.py +++ b/ego/tools/utilities.py @@ -209,8 +209,11 @@ def get_scenario_setting(jsonpath="scenario_setting.json"): json_object = json.dumps(json_file, indent=4) # Writing to sample.json + results_dir = os.path.join(json_file["eGo"]["results_dir"]) + if not os.path.exists(results_dir): + os.makedirs(results_dir) with open( - os.path.join(json_file["eGo"]["results_dir"], "config.json"), "w" + os.path.join(results_dir, "config.json"), "w" ) as outfile: outfile.write(json_object) From a955e2517074f612ce1ce2724cabee81ca6cb92a Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 21 Mar 2023 09:42:48 +0100 Subject: [PATCH 042/126] Change scenario settings for MWE --- ego/examples/edisgo_workflow/scenario_setting.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ego/examples/edisgo_workflow/scenario_setting.json b/ego/examples/edisgo_workflow/scenario_setting.json index bfeaa2ae..f5f5994d 100644 --- a/ego/examples/edisgo_workflow/scenario_setting.json +++ b/ego/examples/edisgo_workflow/scenario_setting.json @@ -2,7 +2,7 @@ "eGo": { "eTraGo": true, "eDisGo": true, - "csv_import_eTraGo": false, + "csv_import_eTraGo": true, "csv_import_eDisGo": false, "data_dir": ".../data_in", "results_dir": ".../data_out", @@ -17,7 +17,7 @@ "end_snapshot" : 1005, "solver": "gurobi", "solver_options":{}, - "scn_name": "eGo 100", + "scn_name": "eGon2035", "scn_extension": null, "scn_decommissioning": null, "lpfile": false, @@ -45,14 +45,14 @@ "db": "oedb", "gridversion": "v0.5.0", "grid_path": "grids", - "choice_mode": "cluster", + "choice_mode": "manual", "cluster_attributes":["wind_cap", "solar_cap", "emob_cap"], "only_cluster": false, - "manual_grids": [3280], + "manual_grids": [33532], "n_clusters": 50, "parallelization":false, "max_calc_time": 0.5, - "max_workers":2, + "max_workers":1, "initial_reinforcement":true, "max_cos_phi_renewable": 0.9, "results": "results_edisgo", From 2981cf7906ffe457e7a88b1b9cee077ed1734185 Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 21 Mar 2023 21:52:23 +0100 Subject: [PATCH 043/126] Remove weather cell ID from renewables dataframes --- ego/tools/interface.py | 67 ++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/ego/tools/interface.py b/ego/tools/interface.py index 18db5f85..c9cffbea 100644 --- a/ego/tools/interface.py +++ b/ego/tools/interface.py @@ -412,14 +412,15 @@ def renewable_generators(): generators_df.carrier.isin(weather_dep_gens) ] - # Add weather ids - for gens_index in weather_dep_gens_df.index: - weather_id = db_io.get_weather_id_for_generator( - bus_id, engine=engine, orm=orm - ) - weather_dep_gens_df.loc[gens_index, "w_id"] = str(weather_id) - # Rename carrier to aggregate to carriers + # ToDo @Malte please check + # # Add weather ids + # for gens_index in weather_dep_gens_df.index: + # weather_id = db_io.get_weather_id_for_generator( + # bus_id, engine=engine, orm=orm + # ) + # weather_dep_gens_df.loc[gens_index, "w_id"] = str(weather_id) + # Rename carrier to aggregate to carriers for new_carrier_name, item in renaming_carrier_dict.items(): for carrier in item: weather_dep_gens_df.loc[ @@ -428,7 +429,7 @@ def renewable_generators(): # Aggregation of p_nom agg_weather_dep_gens_df = ( - weather_dep_gens_df.groupby(["carrier", "w_id"]) + weather_dep_gens_df.groupby(["carrier"]) .agg({"p_nom": "sum"}) .reset_index() ) @@ -455,12 +456,11 @@ def renewable_generators(): columns=agg_weather_dep_gens_df.index, ) - for index, carrier, w_id, p_nom in weather_dep_gens_df[ - ["carrier", "w_id", "p_nom"] + for index, carrier, p_nom in weather_dep_gens_df[ + ["carrier", "p_nom"] ].itertuples(): agg_idx = agg_weather_dep_gens_df[ - (agg_weather_dep_gens_df["carrier"] == carrier) - & (agg_weather_dep_gens_df["w_id"] == w_id) + agg_weather_dep_gens_df["carrier"] == carrier ].index.values[0] p_nom_agg = agg_weather_dep_gens_df.loc[agg_idx, "p_nom"] @@ -510,12 +510,11 @@ def renewable_generators(): # Renaming columns new_columns = [ ( - agg_weather_dep_gens_df.at[column, "carrier"], - agg_weather_dep_gens_df.at[column, "w_id"], + agg_weather_dep_gens_df.at[column, "carrier"] ) for column in weather_dep_gens_df_pot_p.columns ] - new_columns = pd.MultiIndex.from_tuples(new_columns) + #new_columns = pd.MultiIndex.from_tuples(new_columns) weather_dep_gens_df_pot_p.columns = new_columns weather_dep_gens_df_dis_p.columns = new_columns weather_dep_gens_df_curt_p.columns = new_columns @@ -523,26 +522,24 @@ def renewable_generators(): # Add zero for empty carriers for carrier in renaming_carrier_dict.keys(): - for w_id in set(weather_dep_gens_df["w_id"]): - column = [(carrier, w_id)] - if column[0] not in new_columns: - empty_df = pd.DataFrame( - 0.0, - index=timeseries_index, - columns=pd.MultiIndex.from_tuples(column), - ) - weather_dep_gens_df_pot_p = pd.concat( - [weather_dep_gens_df_pot_p, empty_df.copy()], axis="columns" - ) - weather_dep_gens_df_dis_p = pd.concat( - [weather_dep_gens_df_dis_p, empty_df.copy()], axis="columns" - ) - weather_dep_gens_df_curt_p = pd.concat( - [weather_dep_gens_df_curt_p, empty_df.copy()], axis="columns" - ) - weather_dep_gens_df_dis_q = pd.concat( - [weather_dep_gens_df_dis_q, empty_df.copy()], axis="columns" - ) + if carrier not in weather_dep_gens_df_pot_p.columns: + empty_df = pd.DataFrame( + 0.0, + index=timeseries_index, + columns=[carrier], + ) + weather_dep_gens_df_pot_p = pd.concat( + [weather_dep_gens_df_pot_p, empty_df.copy()], axis="columns" + ) + weather_dep_gens_df_dis_p = pd.concat( + [weather_dep_gens_df_dis_p, empty_df.copy()], axis="columns" + ) + weather_dep_gens_df_curt_p = pd.concat( + [weather_dep_gens_df_curt_p, empty_df.copy()], axis="columns" + ) + weather_dep_gens_df_dis_q = pd.concat( + [weather_dep_gens_df_dis_q, empty_df.copy()], axis="columns" + ) results["renewables_potential"] = weather_dep_gens_df_pot_p results["renewables_curtailment"] = weather_dep_gens_df_curt_p From bdd735ec3fd1e58a44fcb31bc7fc579543ca0dfd Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 21 Mar 2023 21:52:52 +0100 Subject: [PATCH 044/126] Rename central heat pump data to make naming more consistent --- ego/tools/interface.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ego/tools/interface.py b/ego/tools/interface.py index c9cffbea..743a458e 100644 --- a/ego/tools/interface.py +++ b/ego/tools/interface.py @@ -208,13 +208,13 @@ def get_etrago_results_per_bus( Columns: Carrier Unit: MVar - * 'heat_central_active_power' + * 'heat_pump_central_active_power' Active power time series of central heat units at the given bus. Type: pd.DataFrame Columns: Carrier Unit: MW - * 'heat_central_reactive_power' + * 'heat_pump_central_reactive_power' Reactive power time series of central heat units at the given bus. Type: pd.DataFrame Columns: Carrier @@ -696,8 +696,8 @@ def central_heat(): 0.0, index=timeseries_index, columns=["solar_thermal_collector"] ) # ToDo: Overlying grid no resistive heater - results["heat_central_active_power"] = central_heat_df_p - results["heat_central_reactive_power"] = central_heat_df_q + results["heat_pump_central_active_power"] = central_heat_df_p + results["heat_pump_central_reactive_power"] = central_heat_df_q results["thermal_storage_central_capacity"] = central_heat_store_capacity results["geothermal_energy_feedin_district_heating"] = geothermal_feedin_df_p results[ @@ -790,8 +790,8 @@ def bev_charger(): "storage_units_reactive_power", "dsm_active_power", "dsm_reactive_power", - "heat_central_active_power", - "heat_central_reactive_power", + "heat_pump_central_active_power", + "heat_pump_central_reactive_power", "thermal_storage_central_capacity", "geothermal_energy_feedin_district_heating", "solarthermal_energy_feedin_district_heating", From b11623a3b8b1d0b9aa71429cd866312f0730df06 Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 21 Mar 2023 21:53:28 +0100 Subject: [PATCH 045/126] Adapt edisgo workflow to changes in edisgo --- ego/tools/edisgo_integration.py | 393 ++++++++++++++------------------ 1 file changed, 177 insertions(+), 216 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index db99181f..9eb2fbdf 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -51,6 +51,7 @@ from edisgo.edisgo import import_edisgo_from_files from edisgo.flex_opt import q_control + from edisgo.io.db import engine from edisgo.network.results import Results from edisgo.network.timeseries import TimeSeries from edisgo.tools.logger import setup_logger @@ -773,11 +774,22 @@ def _run_edisgo(self, mv_grid_id): Returns the complete eDisGo container, also including results """ + self._status_update(mv_grid_id, "start", show=False) + + # ##################### general settings #################### + config = self._json_file + engine = get_engine(config=config) + orm = register_tables_in_saio(engine, config=config) + scenario = config["eTraGo"]["scn_name"] + + # results directory results_dir = os.path.join( - self._json_file["eGo"]["results_dir"], self._results, str(mv_grid_id) + config["eGo"]["results_dir"], self._results, str(mv_grid_id) ) if not os.path.exists(results_dir): os.makedirs(results_dir) + + # logger if self._parallelization: stream_level = None else: @@ -791,15 +803,12 @@ def _run_edisgo(self, mv_grid_id): file_name=f"run_edisgo_{mv_grid_id}.log", log_dir=results_dir, ) + # use edisgo logger in order to have all logging information for one grid go + # to the same file logger = logging.getLogger("edisgo.external.ego._run_edisgo") - self._status_update(mv_grid_id, "start", show=False) - - logger.info("MV grid {}: Calculating interface values".format(mv_grid_id)) - config = self._json_file - engine = get_engine(config=config) - orm = register_tables_in_saio(engine, config=config) - # Calculate Interface values for this MV grid + # ################### get requirements from overlying grid ################## + logger.info(f"MV grid {mv_grid_id}: Calculating interface values.") specs = get_etrago_results_per_bus( mv_grid_id, self._etrago_network, @@ -809,98 +818,33 @@ def _run_edisgo(self, mv_grid_id): orm=orm, ) - # get ding0 MV grid path + # ################### start setting up edisgo object ################## + logger.info(f"MV grid {mv_grid_id}: Initialize MV grid.") grid_path = os.path.join( config["eGo"]["data_dir"], config["eDisGo"]["grid_path"], - "working_grids", str(mv_grid_id), ) - if not os.path.isdir(grid_path): - msg = "No grid data for MV grid {}".format(mv_grid_id) + msg = f"MV grid {mv_grid_id}: No grid data found." logger.error(msg) raise Exception(msg) - # Initialize MV grid - logger.info(f"MV grid {mv_grid_id}: Initialize MV grid") - - edisgo_grid = import_edisgo_from_files( - edisgo_path=grid_path, import_config=False - ) - # Reload the original/default eDisGo configs - edisgo_grid.config = {"from_json": False} - # ##################### Conduct initial grid reinforcement #################### - edisgo_grid.set_time_series_worst_case_analysis() - - logger.info( - ( - "MV grid {}: Changing eDisGo's voltage configurations " - + "for initial reinforcement" - ).format(mv_grid_id) - ) - - # ToDo: Not complete missing "feed-in_case_lower" and "load_case_upper" - # edisgo_grid.config["grid_expansion_allowed_voltage_deviations"] = { - # "hv_mv_trafo_offset": 0.04, - # "hv_mv_trafo_control_deviation": 0.0, - # "mv_load_case_max_v_deviation": 0.055, - # "mv_feedin_case_max_v_deviation": 0.02, - # "lv_load_case_max_v_deviation": 0.065, - # "lv_feedin_case_max_v_deviation": 0.03, - # "mv_lv_station_load_case_max_v_deviation": 0.02, - # "mv_lv_station_feedin_case_max_v_deviation": 0.01, - # } - - # Inital grid reinforcements - logger.info( - ( - "MV grid {}: Initial MV grid reinforcement " + "(worst-case anaylsis)" - ).format(mv_grid_id) + # ToDo change back + # edisgo_grid = import_edisgo_from_files( + # edisgo_path=grid_path + # ) + from edisgo import EDisGo + edisgo_grid = EDisGo( + ding0_grid=grid_path, legacy_ding0_grids=False ) + edisgo_grid.set_timeindex(specs["timeindex"]) + #self._update_edisgo_configs(edisgo_grid) - edisgo_grid.reinforce() - - # Get costs for initial reinforcement - # TODO: Implement a separate cost function - costs_grouped = edisgo_grid.results.grid_expansion_costs.groupby(["type"]).sum() - costs = pd.DataFrame( - costs_grouped.values, - columns=costs_grouped.columns, - index=[[edisgo_grid.topology.id] * len(costs_grouped), costs_grouped.index], - ).reset_index() - costs.rename(columns={"level_0": "grid"}, inplace=True) - - costs_before = costs - - total_costs_before_EUR = costs_before["total_costs"].sum() * 1000 - logger.info( - ("MV grid {}: Costs for initial " + "reinforcement: EUR {}").format( - mv_grid_id, "{:,.2f}".format(total_costs_before_EUR) - ) - ) - - logger.info( - ("MV grid {}: Resetting grid after initial reinforcement").format( - mv_grid_id - ) - ) - edisgo_grid.results = Results(edisgo_grid) - - edisgo_grid.timeseries = TimeSeries(timeindex=specs["timeindex"]) - - # ########################################################################### - # eTraGo case begins here - logger.info("MV grid {}: eTraGo feed-in case".format(mv_grid_id)) - - # Update eDisGo settings (from config files) with scenario settings - logger.info("MV grid {}: Updating eDisgo configuration".format(mv_grid_id)) - # Update configs with eGo's scenario settings - self._update_edisgo_configs(edisgo_grid) - - # Set conventional load time series (active and reactive power) + # set conventional load time series (active and reactive power) edisgo_grid.set_time_series_active_power_predefined( - conventional_loads_ts="oedb" + conventional_loads_ts="oedb", engine=engine, + scenario=scenario ) edisgo_grid.set_time_series_reactive_power_control( control="fixed_cosphi", @@ -908,23 +852,20 @@ def _run_edisgo(self, mv_grid_id): loads_parametrisation="default", storage_units_parametrisation=None, ) + # ToDo change p_set of conventional loads to peak in time series? - # Generator import for future scenario - if self._generator_scn: - logger.info("Importing generators for scenario {}".format(self._scn_name)) - edisgo_grid.import_generators(generator_scenario=self._generator_scn) - else: - logger.info("No generators imported for scenario {}".format(self._scn_name)) + # ########################### generator data ############################# + logger.info("Set up generator data.") + # import generator park of future scenario + edisgo_grid.import_generators(generator_scenario=scenario, engine=engine) - # Set dispatchable generator time series - # Active power + # set generator time series + # active power edisgo_grid.set_time_series_active_power_predefined( dispatchable_generators_ts=specs["dispatchable_generators_active_power"], + fluctuating_generators_ts=specs["renewables_potential"], ) - # Reactive power - gens = edisgo_grid.topology.generators_df[ - ~edisgo_grid.topology.generators_df.type.isin(["solar", "wind"]) - ].index + # reactive power if self._pf_post_lopf: # ToDo Use eTraGo time series to set reactive power (scale by nominal power) edisgo_grid.set_time_series_manual( @@ -933,147 +874,167 @@ def _run_edisgo(self, mv_grid_id): pass else: edisgo_grid.set_time_series_reactive_power_control( - generators_parametrisation=pd.DataFrame( - { - "components": [[gens]], - "mode": ["default"], - "power_factor": ["default"], - }, - index=[1], - ), + control="fixed_cosphi", + generators_parametrisation="default", loads_parametrisation=None, storage_units_parametrisation=None, ) - # Set fluctuating generator time series - # Active power - edisgo_grid.set_time_series_active_power_predefined( - fluctuating_generators_ts=specs["renewables_potential"], - ) - # Reactive power - gens = edisgo_grid.topology.generators_df[ - ~edisgo_grid.topology.generators_df.type.isin(["solar", "wind"]) - ].index - if self._pf_post_lopf: - # ToDo Use eTraGo time series to set reactive power (scale by nominal power) - edisgo_grid.set_time_series_manual( - generators_q=specs["generators_reactive_power"].loc[:, []] - ) - pass - else: - edisgo_grid.set_time_series_reactive_power_control( - generators_parametrisation=pd.DataFrame( - { - "components": [[gens]], - "mode": ["default"], - "power_factor": ["default"], - }, - index=[1], - ), - loads_parametrisation=None, - storage_units_parametrisation=None, - ) + # requirements overlying grid + edisgo_grid.overlying_grid.renewables_curtailment = specs[ + "renewables_curtailment"] - # Get curtailment requirements - gens_df = edisgo_grid.topology.generators_df - solar_wind_capacities = gens_df.groupby(by=["type", "weather_cell_id"])[ - "nominal_capacity" - ].sum() - curt_cols = [ - i - for i in specs["renewables_curtailment"].columns - if i in solar_wind_capacities.index - ] - if not curt_cols: - raise ImportError(f"MV grid {mv_grid_id}: Data doesn't match") - curt_abs = pd.DataFrame(columns=pd.MultiIndex.from_tuples(curt_cols)) - for col in curt_abs: - curt_abs[col] = ( - specs["renewables_curtailment"][col] * solar_wind_capacities[col] - ) + # check that all generators and conventional loads have time series data + # ToDo can caplog be used to check for warnings or does it interfere with + # logger? + edisgo_grid.check_integrity() - # Get storage information - storage_units_capacity = specs["storage_units_capacity"] - storage_units_active_power = specs["storage_units_active_power"] - if self._pf_post_lopf: - storage_units_reactive_power = specs["storage_units_reactive_power"] - else: - q_sign = q_control.get_q_sign_generator( - edisgo_grid.config["reactive_power_mode"]["mv_storage"] - ) - power_factor = edisgo_grid.config["reactive_power_factor"]["mv_storage"] - storage_units_reactive_power = q_control.fixed_cosphi( - storage_units_active_power, q_sign, power_factor - ) + # ########################## battery storage ########################## + logger.info("Set up storage data.") + # import home storage units + edisgo_grid.import_home_batteries(scenario=scenario, engine=engine) + + # requirements overlying grid + edisgo_grid.overlying_grid.storage_units_active_power = specs[ + "storage_units_active_power"] + + # ToDo distribute storage capacity to home storage and large storage (right now + # only work around to have storage capacity in the grid) + edisgo_grid.add_component( + comp_type="storage_unit", + bus=edisgo_grid.topology.mv_grid.station.index[0], + p_nom=specs["storage_units_p_nom"], + max_hours=specs["storage_units_max_hours"], + type="large_storage", + ) - # DSM - dsm_active_power = specs["dsm_active_power"] + # ################################# DSM ################################## + # logger.info("Set up DSM data.") + # dsm_active_power = specs["dsm_active_power"] # ToDo: Get DSM potential per load (needs to be added as a function to eDisGo) - # Heat pumps - # Import heat pumps - also gets heat demand and COP time series per heat pump - edisgo_grid.import_heat_pumps(scenario=self._scn_name) - # Active power - heat_pump_rural_active_power = specs["heat_pump_rural_active_power"] - heat_pump_central_active_power = specs["heat_pump_central_active_power"] - # Reactive power - if self._pf_post_lopf: - heat_pump_rural_reactive_power = specs["heat_pump_rural_reactive_power"] - heat_pump_central_reactive_power = specs["heat_pump_central_reactive_power"] - else: - q_sign = q_control.get_q_sign_load( - edisgo_grid.config["reactive_power_mode"]["mv_hp"] + # ####################### district and individual heating ##################### + logger.info("Set up heat supply and demand data.") + # import heat pumps - also gets heat demand and COP time series per heat pump + edisgo_grid.import_heat_pumps(scenario=scenario, engine=engine) + + # thermal storage units + # decentral + hp_decentral = edisgo_grid.topology.loads_df[ + edisgo_grid.topology.loads_df.sector == "individual_heating"] + if hp_decentral.empty and specs["thermal_storage_rural_capacity"] > 0: + raise ValueError( + "There are thermal storage units for individual heating but no " + "heat pumps." ) - power_factor = edisgo_grid.config["reactive_power_factor"]["mv_hp"] - heat_pump_rural_reactive_power = q_control.fixed_cosphi( - heat_pump_rural_active_power, q_sign, power_factor + if specs["thermal_storage_rural_capacity"] > 0: + tes_cap = ( + edisgo_grid.topology.loads_df.loc[hp_decentral.index, "p_set"] * + specs["thermal_storage_rural_capacity"] / + edisgo_grid.topology.loads_df.loc[hp_decentral.index, "p_set"].sum() ) - heat_pump_central_reactive_power = q_control.fixed_cosphi( - heat_pump_central_active_power, q_sign, power_factor + # ToDo get efficiency from specs + edisgo_grid.heat_pump.thermal_storage_units_df = pd.DataFrame( + data={ + "capacity": tes_cap, + "efficiency": 0.9, + } + ) + # district heating + hp_dh = edisgo_grid.topology.loads_df[ + edisgo_grid.topology.loads_df.sector == "district_heating"] + if hp_dh.empty and specs["thermal_storage_central_capacity"] > 0: + raise ValueError( + "There are thermal storage units for district heating but no " + "heat pumps." + ) + if specs["thermal_storage_central_capacity"] > 0: + tes_cap = ( + edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"] * + specs["thermal_storage_central_capacity"] / + edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"].sum() + ) + edisgo_grid.heat_pump.thermal_storage_units_df = pd.concat( + [edisgo_grid.heat_pump.thermal_storage_units_df, + pd.DataFrame( + data={ + "capacity": tes_cap, + "efficiency": 0.9, + } + )] ) - # Thermal storage units - thermal_storage_rural_capacity = specs["thermal_storage_rural_capacity"] - thermal_storage_central_capacity = specs["thermal_storage_central_capacity"] - # ToDo: Distribute total thermal storage capacity to heat pumps (needs to be - # added as a function to eDisGo) and write storage capacity to - # thermal_storage_units_df - - # Solar- and geothermal energy (district heating) - geothermal_energy_feedin_district_heating = specs[ - "geothermal_energy_feedin_district_heating" - ] - solarthermal_energy_feedin_district_heating = specs[ - "solarthermal_energy_feedin_district_heating" - ] - - # Import charging points with standing times, etc. + # requirements overlying grid + edisgo_grid.overlying_grid.heat_pump_decentral_active_power = specs[ + "heat_pump_rural_active_power"] + edisgo_grid.overlying_grid.heat_pump_central_active_power = specs[ + "heat_pump_central_active_power"] + edisgo_grid.overlying_grid.geothermal_energy_feedin_district_heating = specs[ + "geothermal_energy_feedin_district_heating"] + edisgo_grid.overlying_grid.solarthermal_energy_feedin_district_heating = specs[ + "solarthermal_energy_feedin_district_heating"] + + # ########################## electromobility ########################## + logger.info("Set up electromobility data.") + # import charging points with standing times, etc. edisgo_grid.import_electromobility( - simbev_directory="oedb", tracbev_directory="oedb" + data_source="oedb", scenario=scenario, engine=engine ) - electromobility_active_power = specs["electromobility_active_power"] + # apply charging strategy so that public charging points have a charging + # time series + edisgo_grid.apply_charging_strategy(strategy="dumb") + # get flexibility bands for home and work charging points + edisgo_grid.electromobility.get_flexibility_bands(use_case=["home", "work"]) - # ToDo Call optimisation - edisgo_grid.check_integrity() - edisgo_grid.reinforce(timesteps_pfa=self._timesteps_pfa) - - self._status_update(mv_grid_id, "end") + # requirements overlying grid + edisgo_grid.overlying_grid.electromobility_active_power = specs[ + "electromobility_active_power"] + # ToDo Malte add intermediate storage of edisgo grid in case of errors later on edisgo_grid.save( - directory=results_dir, + directory=os.path.join(results_dir, "grid_data"), save_topology=True, save_timeseries=True, - save_results=True, - save_electromobility=False, + save_results=False, + save_electromobility=True, + #save_dsm=True, + save_heatpump=True, + save_overlying_grid=True, reduce_memory=True, archive=True, archive_type="zip", - parameters={ - "powerflow_results": ["pfa_p", "pfa_q", "v_res"], - "grid_expansion_results": ["grid_expansion_costs", "equipment_changes"], - }, ) + # ########################## checks ########################## + # ToDo Birgit expand + edisgo_grid.check_integrity() + + # ########################## optimisation ########################## + # ToDo Maike Call optimisation + + # ########################## reinforcement ########################## + # edisgo_grid.reinforce() + + # ########################## save results ########################## + self._status_update(mv_grid_id, "end") + # edisgo_grid.save( + # directory=os.path.join(results_dir, "reinforce_data"), + # save_topology=True, + # save_timeseries=True, + # save_results=True, + # save_electromobility=False, + # # save_dsm=True, + # save_heatpump=False, + # save_overlying_grid=False, + # reduce_memory=True, + # archive=True, + # archive_type="zip", + # parameters={ + # "powerflow_results": ["pfa_p", "pfa_q", "v_res"], + # "grid_expansion_results": ["grid_expansion_costs", "equipment_changes"], + # }, + # ) + return {edisgo_grid.topology.id: results_dir} def _save_edisgo_results(self): From dbce8cbc6827bf1f3d0e1ee9f3ba298988bc9922 Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 21 Mar 2023 23:06:54 +0100 Subject: [PATCH 046/126] Minor change to edisgo_mwe --- ego/examples/edisgo_workflow/edisgo_mwe.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/ego/examples/edisgo_workflow/edisgo_mwe.py b/ego/examples/edisgo_workflow/edisgo_mwe.py index 459157f3..86fc2c3a 100644 --- a/ego/examples/edisgo_workflow/edisgo_mwe.py +++ b/ego/examples/edisgo_workflow/edisgo_mwe.py @@ -1,5 +1,3 @@ -import os - from edisgo.tools.logger import setup_logger from pypsa import Network as PyPSANetwork @@ -8,10 +6,7 @@ from ego.tools.utilities import get_scenario_setting config = get_scenario_setting() - -data_dir = config["eGo"]["data_dir"] results_dir = config["eGo"]["results_dir"] - setup_logger( loggers=[ {"name": "root", "file_level": "warning", "stream_level": "warning"}, @@ -22,11 +17,8 @@ log_dir=results_dir, ) -base_path = os.path.join(os.path.expanduser("~"), "git-repos", "data", "ego") -data_dir = config["eGo"]["data_dir"] - etrago_network = PyPSANetwork( - os.path.join(data_dir, "etrago_disaggregated_pf_post_lopf_false") + config["eGo"]["csv_import_eTraGo"] ) with sshtunnel(config=config): From 5306d4edf5c2c90b17dc3457eb4633c255b169c7 Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 21 Mar 2023 23:07:11 +0100 Subject: [PATCH 047/126] Bug fix add edisgo object --- ego/tools/edisgo_integration.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 9eb2fbdf..8e694805 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -984,7 +984,8 @@ def _run_edisgo(self, mv_grid_id): # time series edisgo_grid.apply_charging_strategy(strategy="dumb") # get flexibility bands for home and work charging points - edisgo_grid.electromobility.get_flexibility_bands(use_case=["home", "work"]) + edisgo_grid.electromobility.get_flexibility_bands( + edisgo_obj=edisgo_grid, use_case=["home", "work"]) # requirements overlying grid edisgo_grid.overlying_grid.electromobility_active_power = specs[ From 60c341e234a316239c7b778453eea2f7c2a2ca0b Mon Sep 17 00:00:00 2001 From: Malte Jahn Date: Thu, 23 Mar 2023 11:32:22 +0100 Subject: [PATCH 048/126] Update interface --- ego/tools/edisgo_integration.py | 79 ++++++++++++++++-------------- ego/tools/interface.py | 85 ++++++++++++++++----------------- 2 files changed, 83 insertions(+), 81 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 8e694805..11c81877 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -50,10 +50,6 @@ if "READTHEDOCS" not in os.environ: from edisgo.edisgo import import_edisgo_from_files - from edisgo.flex_opt import q_control - from edisgo.io.db import engine - from edisgo.network.results import Results - from edisgo.network.timeseries import TimeSeries from edisgo.tools.logger import setup_logger from edisgo.tools.plots import mv_grid_topology from egoio.db_tables import grid, model_draft @@ -814,8 +810,6 @@ def _run_edisgo(self, mv_grid_id): self._etrago_network, self._pf_post_lopf, self._max_cos_phi_renewable, - engine=engine, - orm=orm, ) # ################### start setting up edisgo object ################## @@ -835,16 +829,14 @@ def _run_edisgo(self, mv_grid_id): # edisgo_path=grid_path # ) from edisgo import EDisGo - edisgo_grid = EDisGo( - ding0_grid=grid_path, legacy_ding0_grids=False - ) + + edisgo_grid = EDisGo(ding0_grid=grid_path, legacy_ding0_grids=False) edisgo_grid.set_timeindex(specs["timeindex"]) - #self._update_edisgo_configs(edisgo_grid) + # self._update_edisgo_configs(edisgo_grid) # set conventional load time series (active and reactive power) edisgo_grid.set_time_series_active_power_predefined( - conventional_loads_ts="oedb", engine=engine, - scenario=scenario + conventional_loads_ts="oedb", engine=engine, scenario=scenario ) edisgo_grid.set_time_series_reactive_power_control( control="fixed_cosphi", @@ -882,7 +874,8 @@ def _run_edisgo(self, mv_grid_id): # requirements overlying grid edisgo_grid.overlying_grid.renewables_curtailment = specs[ - "renewables_curtailment"] + "renewables_curtailment" + ] # check that all generators and conventional loads have time series data # ToDo can caplog be used to check for warnings or does it interfere with @@ -896,7 +889,8 @@ def _run_edisgo(self, mv_grid_id): # requirements overlying grid edisgo_grid.overlying_grid.storage_units_active_power = specs[ - "storage_units_active_power"] + "storage_units_active_power" + ] # ToDo distribute storage capacity to home storage and large storage (right now # only work around to have storage capacity in the grid) @@ -921,7 +915,8 @@ def _run_edisgo(self, mv_grid_id): # thermal storage units # decentral hp_decentral = edisgo_grid.topology.loads_df[ - edisgo_grid.topology.loads_df.sector == "individual_heating"] + edisgo_grid.topology.loads_df.sector == "individual_heating" + ] if hp_decentral.empty and specs["thermal_storage_rural_capacity"] > 0: raise ValueError( "There are thermal storage units for individual heating but no " @@ -929,9 +924,9 @@ def _run_edisgo(self, mv_grid_id): ) if specs["thermal_storage_rural_capacity"] > 0: tes_cap = ( - edisgo_grid.topology.loads_df.loc[hp_decentral.index, "p_set"] * - specs["thermal_storage_rural_capacity"] / - edisgo_grid.topology.loads_df.loc[hp_decentral.index, "p_set"].sum() + edisgo_grid.topology.loads_df.loc[hp_decentral.index, "p_set"] + * specs["thermal_storage_rural_capacity"] + / edisgo_grid.topology.loads_df.loc[hp_decentral.index, "p_set"].sum() ) # ToDo get efficiency from specs edisgo_grid.heat_pump.thermal_storage_units_df = pd.DataFrame( @@ -942,7 +937,8 @@ def _run_edisgo(self, mv_grid_id): ) # district heating hp_dh = edisgo_grid.topology.loads_df[ - edisgo_grid.topology.loads_df.sector == "district_heating"] + edisgo_grid.topology.loads_df.sector == "district_heating" + ] if hp_dh.empty and specs["thermal_storage_central_capacity"] > 0: raise ValueError( "There are thermal storage units for district heating but no " @@ -950,29 +946,35 @@ def _run_edisgo(self, mv_grid_id): ) if specs["thermal_storage_central_capacity"] > 0: tes_cap = ( - edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"] * - specs["thermal_storage_central_capacity"] / - edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"].sum() + edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"] + * specs["thermal_storage_central_capacity"] + / edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"].sum() ) edisgo_grid.heat_pump.thermal_storage_units_df = pd.concat( - [edisgo_grid.heat_pump.thermal_storage_units_df, - pd.DataFrame( - data={ - "capacity": tes_cap, - "efficiency": 0.9, - } - )] + [ + edisgo_grid.heat_pump.thermal_storage_units_df, + pd.DataFrame( + data={ + "capacity": tes_cap, + "efficiency": 0.9, + } + ), + ] ) # requirements overlying grid edisgo_grid.overlying_grid.heat_pump_decentral_active_power = specs[ - "heat_pump_rural_active_power"] + "heat_pump_rural_active_power" + ] edisgo_grid.overlying_grid.heat_pump_central_active_power = specs[ - "heat_pump_central_active_power"] + "heat_pump_central_active_power" + ] edisgo_grid.overlying_grid.geothermal_energy_feedin_district_heating = specs[ - "geothermal_energy_feedin_district_heating"] + "geothermal_energy_feedin_district_heating" + ] edisgo_grid.overlying_grid.solarthermal_energy_feedin_district_heating = specs[ - "solarthermal_energy_feedin_district_heating"] + "solarthermal_energy_feedin_district_heating" + ] # ########################## electromobility ########################## logger.info("Set up electromobility data.") @@ -985,11 +987,13 @@ def _run_edisgo(self, mv_grid_id): edisgo_grid.apply_charging_strategy(strategy="dumb") # get flexibility bands for home and work charging points edisgo_grid.electromobility.get_flexibility_bands( - edisgo_obj=edisgo_grid, use_case=["home", "work"]) + edisgo_obj=edisgo_grid, use_case=["home", "work"] + ) # requirements overlying grid edisgo_grid.overlying_grid.electromobility_active_power = specs[ - "electromobility_active_power"] + "electromobility_active_power" + ] # ToDo Malte add intermediate storage of edisgo grid in case of errors later on edisgo_grid.save( @@ -998,7 +1002,7 @@ def _run_edisgo(self, mv_grid_id): save_timeseries=True, save_results=False, save_electromobility=True, - #save_dsm=True, + # save_dsm=True, save_heatpump=True, save_overlying_grid=True, reduce_memory=True, @@ -1032,7 +1036,8 @@ def _run_edisgo(self, mv_grid_id): # archive_type="zip", # parameters={ # "powerflow_results": ["pfa_p", "pfa_q", "v_res"], - # "grid_expansion_results": ["grid_expansion_costs", "equipment_changes"], + # "grid_expansion_results": ["grid_expansion_costs", "equipment + # _changes"], # }, # ) diff --git a/ego/tools/interface.py b/ego/tools/interface.py index 743a458e..4f7a4b3c 100644 --- a/ego/tools/interface.py +++ b/ego/tools/interface.py @@ -27,14 +27,10 @@ import logging import math -import os import time import pandas as pd -if "READTHEDOCS" not in os.environ: - import ego.mv_clustering.egon_data_io as db_io - logger = logging.getLogger(__name__) @@ -62,6 +58,7 @@ def set_filtered_attribute(etrago_network_obj, component): "bus", "carrier", "p_nom_opt", + "p_nom_min", "p_nom_extendable", "max_hours", ], @@ -82,7 +79,7 @@ def set_filtered_attribute(etrago_network_obj, component): "links": ["p0", "p1"], "generators": ["p", "p_max_pu", "q"], "stores": ["p"], - "storage_units": ["p", "q"], + "storage_units": ["p", "q", "state_of_charge"], "loads": ["p"], } attribute_to_save = attribute_to_save[component] @@ -107,14 +104,7 @@ def set_filtered_attribute(etrago_network_obj, component): logger.info(f"Data selection time {time.perf_counter() - t_start}") -def get_etrago_results_per_bus( - bus_id, - etrago_obj, - pf_post_lopf, - max_cos_phi_ren, - engine=None, - orm=None, -): +def get_etrago_results_per_bus(bus_id, etrago_obj, pf_post_lopf, max_cos_phi_ren): """ Reads eTraGo Results from Database and returns the interface values as a dictionary of corresponding dataframes @@ -160,29 +150,34 @@ def get_etrago_results_per_bus( * 'renewables_potential' Normalised weather dependent feed-in potential of fluctuating generators - per technology and weather cell ID in p.u. at the given bus. + per technology in p.u. at the given bus. Type: pd.DataFrame - Columns: Carrier / Weather Cell ID + Columns: Carrier Unit: pu * 'renewables_curtailment' Normalised curtailment of fluctuating generators per - technology and weather cell ID in p.u. at the given bus. + technology in p.u. at the given bus. Type: pd.DataFrame - Columns: Carrier / Weather Cell ID + Columns: Carrier Unit: pu * 'renewables_dispatch_reactive_power' Normalised reactive power time series of fluctuating generators per - technology and weather cell ID in p.u. at the given bus. + technology in p.u. at the given bus. Type: pd.DataFrame - Columns: Carrier / Weather Cell ID + Columns: Carrier Unit: pu - * 'storage_units_capacity' - Storage unit capacity at the given bus. + * 'storage_units_p_nom' + Storage unit nominal power. Type: float - Unit: MWh + Unit: MW + + * 'storage_units_max_hours' + Storage units maximal discharge with p_nom starting by a soc of 1. + Type: float + Unit: h * 'storage_units_active_power' Active power time series of battery storage units at the given bus. @@ -196,6 +191,12 @@ def get_etrago_results_per_bus( Columns: Carrier Unit: MVar + * 'storage_units_soc' + Reactive power time series of battery storage units at the given bus. + Type: pd.DataFrame + Columns: Carrier + Unit: pu + * 'dsm_active_power' Active power time series of DSM units at the given bus. Type: pd.DataFrame @@ -429,9 +430,7 @@ def renewable_generators(): # Aggregation of p_nom agg_weather_dep_gens_df = ( - weather_dep_gens_df.groupby(["carrier"]) - .agg({"p_nom": "sum"}) - .reset_index() + weather_dep_gens_df.groupby(["carrier"]).agg({"p_nom": "sum"}).reset_index() ) # Initialize dfs @@ -509,12 +508,10 @@ def renewable_generators(): # Renaming columns new_columns = [ - ( - agg_weather_dep_gens_df.at[column, "carrier"] - ) + (agg_weather_dep_gens_df.at[column, "carrier"]) for column in weather_dep_gens_df_pot_p.columns ] - #new_columns = pd.MultiIndex.from_tuples(new_columns) + # new_columns = pd.MultiIndex.from_tuples(new_columns) weather_dep_gens_df_pot_p.columns = new_columns weather_dep_gens_df_dis_p.columns = new_columns weather_dep_gens_df_curt_p.columns = new_columns @@ -548,13 +545,6 @@ def renewable_generators(): def storages(): # Storage # Filter batteries - # ToDo @Malte subtract p_min from p_nom_opt in order to determine additional - # storage capacity from optimisation - hab ich unten schon gemacht, aber p_min - # fehlt noch im storages_df - # ToDo @Malte add 'storage_units_p_nom' (with p_nom_opt - p_min in MW) to - # results dictionary - hab ich schon gemacht, bitte prüfen und docstring anpassen - # ToDo @Malte return max_hours instead of capacity - hab ich schon gemacht, - # bitte prüfen und docstring anpassen min_extended = 0 logger.info(f"Minimum storage of {min_extended} MW") @@ -563,18 +553,14 @@ def storages(): & (etrago_obj.storage_units["bus"] == str(bus_id)) & (etrago_obj.storage_units["p_nom_extendable"]) & (etrago_obj.storage_units["p_nom_opt"] > min_extended) - # & (etrago_obj.storage_units["max_hours"] <= 20.0) ] if not storages_df.empty: # p_nom storages_df_p_nom = ( - storages_df["p_nom_opt"]# - storages_df["p_min"] + storages_df["p_nom_opt"] - storages_df["p_nom_min"] ).values[0] # Capacity - storages_df_max_hours = ( - storages_df["max_hours"] - ).values[0] - + storages_df_max_hours = (storages_df["max_hours"]).values[0] storages_df_p = etrago_obj.storage_units_t["p"][storages_df.index] storages_df_p.columns = storages_df["carrier"] if pf_post_lopf: @@ -588,6 +574,11 @@ def storages(): storages_df_q = pd.DataFrame( 0.0, index=timeseries_index, columns=[storages_df["carrier"]] ) + storages_df_soc = etrago_obj.storage_units_t["state_of_charge"][ + storages_df.index + ] + storages_df_soc.columns = storages_df["carrier"] + else: storages_df_p_nom = 0 storages_df_max_hours = 0 @@ -597,11 +588,14 @@ def storages(): storages_df_q = pd.DataFrame( 0.0, index=timeseries_index, columns=[storages_df["carrier"]] ) - + storages_df_soc = pd.DataFrame( + 0.0, index=timeseries_index, columns=[storages_df["carrier"]] + ) results["storage_units_p_nom"] = storages_df_p_nom results["storage_units_max_hours"] = storages_df_max_hours results["storage_units_active_power"] = storages_df_p results["storage_units_reactive_power"] = storages_df_q + results["storage_units_soc"] = storages_df_soc def dsm(): # DSM @@ -780,14 +774,17 @@ def bev_charger(): results["timeindex"] = timeseries_index # Prefill dict with None result_keys = [ + "timeindex", "dispatchable_generators_active_power", "dispatchable_generators_reactive_power", "renewables_potential", "renewables_curtailment", "renewables_dispatch_reactive_power", - "storage_units_capacity", + "storage_units_p_nom", + "storage_units_max_hours", "storage_units_active_power", "storage_units_reactive_power", + "storage_units_soc", "dsm_active_power", "dsm_reactive_power", "heat_pump_central_active_power", From 357190baf2a9b77ffa798dd1a5a2005a2e3dbadf Mon Sep 17 00:00:00 2001 From: Malte Jahn Date: Thu, 23 Mar 2023 11:33:09 +0100 Subject: [PATCH 049/126] Update loading of config and add missing config values --- .../edisgo_workflow/scenario_setting.json | 17 +++++++++++++++-- ego/tools/utilities.py | 14 +++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/ego/examples/edisgo_workflow/scenario_setting.json b/ego/examples/edisgo_workflow/scenario_setting.json index f5f5994d..5ae1ee0f 100644 --- a/ego/examples/edisgo_workflow/scenario_setting.json +++ b/ego/examples/edisgo_workflow/scenario_setting.json @@ -2,7 +2,7 @@ "eGo": { "eTraGo": true, "eDisGo": true, - "csv_import_eTraGo": true, + "csv_import_eTraGo": false, "csv_import_eDisGo": false, "data_dir": ".../data_in", "results_dir": ".../data_out", @@ -69,7 +69,20 @@ "etrago_load": "grid.egon_etrago_load", "etrago_load_timeseries": "grid.egon_etrago_load_timeseries", "weather_cells": "supply.egon_era5_weather_cells" - } + }, + "database_name": "", + "host": "127.0.0.1", + "port": "59700", + "user": "", + "password": "" + }, + "ssh": { + "enabled": true, + "user": "", + "ip": "", + "port": "", + "local_address": "127.0.0.1", + "local_port": "59700" }, "external_config": "~/.ego/secondary_ego_config.json" } diff --git a/ego/tools/utilities.py b/ego/tools/utilities.py index 404099b1..7c91185e 100644 --- a/ego/tools/utilities.py +++ b/ego/tools/utilities.py @@ -204,6 +204,16 @@ def get_scenario_setting(jsonpath="scenario_setting.json"): # expand directories for key in ["data_dir", "results_dir"]: json_file["eGo"][key] = os.path.expanduser(json_file["eGo"][key]) + # map dirs with "." into data_dir + for key in ["csv_import_eTraGo", "csv_import_eDisGo"]: + if json_file["eGo"][key]: + path = json_file["eGo"][key] + if path[0] == ".": + json_file["eGo"][key] = os.path.join( + json_file["eGo"]["data_dir"], json_file["eGo"][key][2:] + ) + elif path[0] == "~": + json_file["eGo"][key] = os.path.expanduser(json_file["eGo"][key]) # Serializing json json_object = json.dumps(json_file, indent=4) @@ -212,9 +222,7 @@ def get_scenario_setting(jsonpath="scenario_setting.json"): results_dir = os.path.join(json_file["eGo"]["results_dir"]) if not os.path.exists(results_dir): os.makedirs(results_dir) - with open( - os.path.join(results_dir, "config.json"), "w" - ) as outfile: + with open(os.path.join(results_dir, "config.json"), "w") as outfile: outfile.write(json_object) return json_file From 60f97dce47527f10ff6df7c4953d62642d643135 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 31 Mar 2023 13:15:27 +0200 Subject: [PATCH 050/126] Adapt edisgo workflow --- ego/tools/edisgo_integration.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 11c81877..d7d7a787 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -775,7 +775,6 @@ def _run_edisgo(self, mv_grid_id): # ##################### general settings #################### config = self._json_file engine = get_engine(config=config) - orm = register_tables_in_saio(engine, config=config) scenario = config["eTraGo"]["scn_name"] # results directory @@ -824,13 +823,9 @@ def _run_edisgo(self, mv_grid_id): logger.error(msg) raise Exception(msg) - # ToDo change back - # edisgo_grid = import_edisgo_from_files( - # edisgo_path=grid_path - # ) - from edisgo import EDisGo - - edisgo_grid = EDisGo(ding0_grid=grid_path, legacy_ding0_grids=False) + edisgo_grid = import_edisgo_from_files( + edisgo_path=grid_path + ) edisgo_grid.set_timeindex(specs["timeindex"]) # self._update_edisgo_configs(edisgo_grid) @@ -903,9 +898,12 @@ def _run_edisgo(self, mv_grid_id): ) # ################################# DSM ################################## - # logger.info("Set up DSM data.") - # dsm_active_power = specs["dsm_active_power"] - # ToDo: Get DSM potential per load (needs to be added as a function to eDisGo) + logger.info("Set up DSM data.") + # import DSM data + edisgo_grid.import_dsm(scenario=scenario, engine=engine) + + # requirements overlying grid + edisgo_grid.overlying_grid.dsm_active_power = specs["dsm_active_power"] # ####################### district and individual heating ##################### logger.info("Set up heat supply and demand data.") @@ -975,6 +973,7 @@ def _run_edisgo(self, mv_grid_id): edisgo_grid.overlying_grid.solarthermal_energy_feedin_district_heating = specs[ "solarthermal_energy_feedin_district_heating" ] + # ToDo CHP # ########################## electromobility ########################## logger.info("Set up electromobility data.") @@ -1002,7 +1001,7 @@ def _run_edisgo(self, mv_grid_id): save_timeseries=True, save_results=False, save_electromobility=True, - # save_dsm=True, + save_dsm=True, save_heatpump=True, save_overlying_grid=True, reduce_memory=True, From 40f86b6c3a041dd48bf48deca9df9aca1b28c290 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 31 Mar 2023 14:47:21 +0200 Subject: [PATCH 051/126] Fix initialisation of edisgo grid --- ego/tools/edisgo_integration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index d7d7a787..2c1e5b36 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -824,7 +824,7 @@ def _run_edisgo(self, mv_grid_id): raise Exception(msg) edisgo_grid = import_edisgo_from_files( - edisgo_path=grid_path + edisgo_path=grid_path, legacy_ding0_grids=False ) edisgo_grid.set_timeindex(specs["timeindex"]) # self._update_edisgo_configs(edisgo_grid) @@ -973,7 +973,7 @@ def _run_edisgo(self, mv_grid_id): edisgo_grid.overlying_grid.solarthermal_energy_feedin_district_heating = specs[ "solarthermal_energy_feedin_district_heating" ] - # ToDo CHP + # ToDo CHP + resistive heaters # ########################## electromobility ########################## logger.info("Set up electromobility data.") From 11fac872d7a81a0da79d968ec608da8e3be22cb8 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 31 Mar 2023 14:47:32 +0200 Subject: [PATCH 052/126] Fix setting timeindex --- ego/tools/interface.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ego/tools/interface.py b/ego/tools/interface.py index 4f7a4b3c..ec32f49a 100644 --- a/ego/tools/interface.py +++ b/ego/tools/interface.py @@ -770,8 +770,6 @@ def bev_charger(): logger.info("Only active power interface") results = {} - timeseries_index = etrago_obj.snapshots - results["timeindex"] = timeseries_index # Prefill dict with None result_keys = [ "timeindex", @@ -801,6 +799,9 @@ def bev_charger(): for result_key in result_keys: results[result_key] = None + timeseries_index = etrago_obj.snapshots + results["timeindex"] = timeseries_index + # Filter dataframes by bus_id # Generators generators_df = etrago_obj.generators[etrago_obj.generators["bus"] == str(bus_id)] From cf5d603a3e52e0e3b622c88bb5e6a3eff9f9e819 Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 4 Apr 2023 22:21:57 +0200 Subject: [PATCH 053/126] Adapt edisgo workflow --- ego/tools/edisgo_integration.py | 708 ++++++++++++++++++++++++++------ 1 file changed, 575 insertions(+), 133 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 2c1e5b36..c0d2f903 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -722,7 +722,7 @@ def _run_edisgo_pool(self): self._edisgo_grids = set(mv_grids) self._edisgo_grids = parallelizer( mv_grids, - lambda *xs: xs[1]._run_edisgo(xs[0]), + lambda *xs: xs[1].run_edisgo(xs[0]), (self,), self._max_calc_time, workers=no_cpu, @@ -743,7 +743,7 @@ def _run_edisgo_pool(self): mv_grid_id = int(row["the_selected_network_id"]) logger.info("MV grid {}".format(mv_grid_id)) try: - edisgo_grid = self._run_edisgo(mv_grid_id) + edisgo_grid = self.run_edisgo(mv_grid_id) self._edisgo_grids[mv_grid_id] = edisgo_grid except Exception as e: self._edisgo_grids[mv_grid_id] = e @@ -755,7 +755,7 @@ def _run_edisgo_pool(self): self._load_edisgo_results() self._run_finished = True - def _run_edisgo(self, mv_grid_id): + def run_edisgo(self, mv_grid_id): """ Performs a single eDisGo run @@ -802,16 +802,122 @@ def _run_edisgo(self, mv_grid_id): # to the same file logger = logging.getLogger("edisgo.external.ego._run_edisgo") - # ################### get requirements from overlying grid ################## - logger.info(f"MV grid {mv_grid_id}: Calculating interface values.") - specs = get_etrago_results_per_bus( - mv_grid_id, - self._etrago_network, - self._pf_post_lopf, - self._max_cos_phi_renewable, - ) + edisgo_grid = None + + # ################### task: setup grid ################## + if "1_setup_grid" in config["eDisGo"]["tasks"]: + edisgo_grid = self._run_edisgo_task_setup_grid( + mv_grid_id, scenario, engine, logger, config) + if not "2_specs_overlying_grid" in config["eDisGo"]["tasks"]: + edisgo_grid.save( + directory=os.path.join(results_dir, "grid_data"), + save_topology=True, + save_timeseries=True, + save_results=False, + save_electromobility=True, + save_dsm=True, + save_heatpump=True, + save_overlying_grid=False, + reduce_memory=True, + archive=True, + archive_type="zip", + ) + return {edisgo_grid.topology.id: results_dir} + + # ################### task: specs overlying grid ################## + if "2_specs_overlying_grid" in config["eDisGo"]["tasks"]: + if edisgo_grid is None: + grid_path = os.path.join(results_dir, "grid_data.zip") + edisgo_grid = import_edisgo_from_files( + edisgo_path=grid_path, + import_topology=True, + import_timeseries=True, + import_results=False, + import_electromobility=True, + import_heat_pump=True, + import_dsm=True, + import_overlying_grid=False, + from_zip_archive=True + ) + edisgo_grid.legacy_grids = False + edisgo_grid = self._run_edisgo_task_specs_overlying_grid(edisgo_grid, logger) + if not "3_optimisation" in config["eDisGo"]["tasks"]: + edisgo_grid.save( + directory=os.path.join(results_dir, "grid_data"), + save_topology=True, + save_timeseries=True, + save_results=False, + save_electromobility=True, + save_dsm=True, + save_heatpump=True, + save_overlying_grid=True, + reduce_memory=True, + archive=True, + archive_type="zip", + ) + return {edisgo_grid.topology.id: results_dir} + + + # ########################## task: optimisation ########################## + # ToDo Maike Call optimisation + + # ########################## reinforcement ########################## + # edisgo_grid.reinforce() + + # ########################## save results ########################## + self._status_update(mv_grid_id, "end") + # edisgo_grid.save( + # directory=os.path.join(results_dir, "reinforce_data"), + # save_topology=True, + # save_timeseries=True, + # save_results=True, + # save_electromobility=False, + # # save_dsm=True, + # save_heatpump=False, + # save_overlying_grid=False, + # reduce_memory=True, + # archive=True, + # archive_type="zip", + # parameters={ + # "powerflow_results": ["pfa_p", "pfa_q", "v_res"], + # "grid_expansion_results": ["grid_expansion_costs", "equipment + # _changes"], + # }, + # ) + + return {edisgo_grid.topology.id: results_dir} + + def _run_edisgo_task_setup_grid(self, mv_grid_id, scenario, engine, logger, config): + """ + Sets up EDisGo object for future scenario (without specifications from overlying + grid). + + The following data is set up: + + * load time series of conventional loads + * generator park + * home storage units + * DSM data + * heat pumps including heat demand and COP time series per heat pump + * charging points with standing times, etc. as well as charging time series for + uncontrolled charging (done so that public charging points have a charging + time series) and flexibility bands for home and work charging points + + A dummy time index is set that is later on overwritten by the time index used + in eTraGo. + + Parameters + ---------- + mv_grid_id : int + MV grid ID of the ding0 grid. + + Returns + ------- + :class:`edisgo.EDisGo` + + """ + logger.info(f"MV grid {mv_grid_id}: Start task 'setup_grid'.") - # ################### start setting up edisgo object ################## logger.info(f"MV grid {mv_grid_id}: Initialize MV grid.") grid_path = os.path.join( config["eGo"]["data_dir"], @@ -824,12 +930,12 @@ def _run_edisgo(self, mv_grid_id): raise Exception(msg) edisgo_grid = import_edisgo_from_files( - edisgo_path=grid_path, legacy_ding0_grids=False + edisgo_path=grid_path ) - edisgo_grid.set_timeindex(specs["timeindex"]) - # self._update_edisgo_configs(edisgo_grid) + edisgo_grid.legacy_grids = False + edisgo_grid.set_timeindex(pd.date_range("1/1/2011", periods=8760, freq="H")) - # set conventional load time series (active and reactive power) + logger.info("Set up load time series of conventional loads.") edisgo_grid.set_time_series_active_power_predefined( conventional_loads_ts="oedb", engine=engine, scenario=scenario ) @@ -839,14 +945,106 @@ def _run_edisgo(self, mv_grid_id): loads_parametrisation="default", storage_units_parametrisation=None, ) - # ToDo change p_set of conventional loads to peak in time series? + # overwrite p_set of conventional loads as it changes from scenario to scenario + edisgo_grid.topology.loads_df["p_set"] = ( + edisgo_grid.timeseries.loads_active_power.max() + ) - # ########################### generator data ############################# - logger.info("Set up generator data.") - # import generator park of future scenario + logger.info("Set up generator park.") edisgo_grid.import_generators(generator_scenario=scenario, engine=engine) - # set generator time series + logger.info("Set up home storage units.") + edisgo_grid.import_home_batteries(scenario=scenario, engine=engine) + + logger.info("Set up DSM data.") + edisgo_grid.import_dsm(scenario=scenario, engine=engine) + + logger.info("Set up heat supply and demand data.") + edisgo_grid.import_heat_pumps(scenario=scenario, engine=engine) + + logger.info("Set up electromobility data.") + edisgo_grid.import_electromobility( + data_source="oedb", scenario=scenario, engine=engine + ) + # apply charging strategy so that public charging points have a charging + # time series + edisgo_grid.apply_charging_strategy(strategy="dumb") + # get flexibility bands for home and work charging points + edisgo_grid.electromobility.get_flexibility_bands( + edisgo_obj=edisgo_grid, use_case=["home", "work"] + ) + + logger.info("Run integrity check.") + edisgo_grid.check_integrity() + + return edisgo_grid + + def _run_edisgo_task_specs_overlying_grid(self, edisgo_grid, logger): + """ + Gets specifications from overlying grid and integrates them into the EDisGo + object. + + The following data is set up: + + * set generator time series + * set up thermal storage units + * requirements overlying grid on total renewables curtailment, DSM dispatch, + electromobility charging, heat pump dispatch, + + A dummy time index is set that is later on overwritten by the time index used + in eTraGo + + Parameters + ---------- + mv_grid_id : int + MV grid ID of the ding0 grid + + Returns + ------- + :class:`edisgo.EDisGo` + Returns the complete eDisGo container, also including results + + """ + logger.info(f"Start task 'specs_overlying_grid'.") + + logger.info(f"Get specifications from eTraGo.") + specs = get_etrago_results_per_bus( + edisgo_grid.topology.id, + self._etrago_network, + self._pf_post_lopf, + self._max_cos_phi_renewable, + ) + + # overwrite previously set dummy time index if year that was used differs from + # year used in etrago + edisgo_year = edisgo_grid.timeseries.timeindex[0].year + etrago_year = specs["timeindex"][0].year + if edisgo_year != etrago_year: + timeindex_new_full = pd.date_range( + f"1/1/{etrago_year}", periods=8760, freq="H") + # conventional loads + edisgo_grid.timeseries.loads_active_power.index = timeindex_new_full + edisgo_grid.timeseries.loads_reactive_power.index = timeindex_new_full + # DSM + edisgo_grid.dsm.e_max.index = timeindex_new_full + edisgo_grid.dsm.e_min.index = timeindex_new_full + edisgo_grid.dsm.p_max.index = timeindex_new_full + edisgo_grid.dsm.p_min.index = timeindex_new_full + # COP and heat demand + edisgo_grid.heat_pump.cop_df.index = timeindex_new_full + edisgo_grid.heat_pump.heat_demand_df.index = timeindex_new_full + # flexibility bands + edisgo_grid.electromobility.flexibility_bands[ + "upper_power"].index = timeindex_new_full + edisgo_grid.electromobility.flexibility_bands[ + "upper_energy"].index = timeindex_new_full + edisgo_grid.electromobility.flexibility_bands[ + "lower_energy"].index = timeindex_new_full + # TimeSeries.timeindex + edisgo_grid.timeseries.timeindex = specs["timeindex"] + + + logger.info("Set generator time series.") # active power edisgo_grid.set_time_series_active_power_predefined( dispatchable_generators_ts=specs["dispatchable_generators_active_power"], @@ -867,54 +1065,23 @@ def _run_edisgo(self, mv_grid_id): storage_units_parametrisation=None, ) - # requirements overlying grid - edisgo_grid.overlying_grid.renewables_curtailment = specs[ - "renewables_curtailment" - ] - - # check that all generators and conventional loads have time series data - # ToDo can caplog be used to check for warnings or does it interfere with - # logger? - edisgo_grid.check_integrity() - - # ########################## battery storage ########################## - logger.info("Set up storage data.") - # import home storage units - edisgo_grid.import_home_batteries(scenario=scenario, engine=engine) - - # requirements overlying grid - edisgo_grid.overlying_grid.storage_units_active_power = specs[ - "storage_units_active_power" - ] - - # ToDo distribute storage capacity to home storage and large storage (right now - # only work around to have storage capacity in the grid) - edisgo_grid.add_component( - comp_type="storage_unit", - bus=edisgo_grid.topology.mv_grid.station.index[0], - p_nom=specs["storage_units_p_nom"], - max_hours=specs["storage_units_max_hours"], - type="large_storage", - ) - - # ################################# DSM ################################## - logger.info("Set up DSM data.") - # import DSM data - edisgo_grid.import_dsm(scenario=scenario, engine=engine) - - # requirements overlying grid - edisgo_grid.overlying_grid.dsm_active_power = specs["dsm_active_power"] - - # ####################### district and individual heating ##################### - logger.info("Set up heat supply and demand data.") - # import heat pumps - also gets heat demand and COP time series per heat pump - edisgo_grid.import_heat_pumps(scenario=scenario, engine=engine) - - # thermal storage units + # ToDo for now additional optimised storage capacity is ignored as capacities + # are very small and optimisation does not offer storage positioning + # if specs["storage_units_p_nom"] > 0.3: + # logger.info("Set up large battery storage units.") + # edisgo_grid.add_component( + # comp_type="storage_unit", + # bus=edisgo_grid.topology.mv_grid.station.index[0], + # p_nom=specs["storage_units_p_nom"], + # max_hours=specs["storage_units_max_hours"], + # type="large_storage", + # ) + + logger.info("Set up thermal storage units.") # decentral hp_decentral = edisgo_grid.topology.loads_df[ edisgo_grid.topology.loads_df.sector == "individual_heating" - ] + ] if hp_decentral.empty and specs["thermal_storage_rural_capacity"] > 0: raise ValueError( "There are thermal storage units for individual heating but no " @@ -922,9 +1089,10 @@ def _run_edisgo(self, mv_grid_id): ) if specs["thermal_storage_rural_capacity"] > 0: tes_cap = ( - edisgo_grid.topology.loads_df.loc[hp_decentral.index, "p_set"] - * specs["thermal_storage_rural_capacity"] - / edisgo_grid.topology.loads_df.loc[hp_decentral.index, "p_set"].sum() + edisgo_grid.topology.loads_df.loc[hp_decentral.index, "p_set"] + * specs["thermal_storage_rural_capacity"] + / edisgo_grid.topology.loads_df.loc[ + hp_decentral.index, "p_set"].sum() ) # ToDo get efficiency from specs edisgo_grid.heat_pump.thermal_storage_units_df = pd.DataFrame( @@ -936,7 +1104,7 @@ def _run_edisgo(self, mv_grid_id): # district heating hp_dh = edisgo_grid.topology.loads_df[ edisgo_grid.topology.loads_df.sector == "district_heating" - ] + ] if hp_dh.empty and specs["thermal_storage_central_capacity"] > 0: raise ValueError( "There are thermal storage units for district heating but no " @@ -944,10 +1112,11 @@ def _run_edisgo(self, mv_grid_id): ) if specs["thermal_storage_central_capacity"] > 0: tes_cap = ( - edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"] - * specs["thermal_storage_central_capacity"] - / edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"].sum() + edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"] + * specs["thermal_storage_central_capacity"] + / edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"].sum() ) + # ToDo get efficiency from specs edisgo_grid.heat_pump.thermal_storage_units_df = pd.concat( [ edisgo_grid.heat_pump.thermal_storage_units_df, @@ -960,7 +1129,18 @@ def _run_edisgo(self, mv_grid_id): ] ) - # requirements overlying grid + logger.info("Set requirements from overlying grid.") + edisgo_grid.overlying_grid.renewables_curtailment = specs[ + "renewables_curtailment" + ] + # ToDo requirements need to be scaled to capacity of home storage units + edisgo_grid.overlying_grid.storage_units_active_power = specs[ + "storage_units_active_power" + ] + edisgo_grid.overlying_grid.dsm_active_power = specs["dsm_active_power"] + edisgo_grid.overlying_grid.electromobility_active_power = specs[ + "electromobility_active_power" + ] edisgo_grid.overlying_grid.heat_pump_decentral_active_power = specs[ "heat_pump_rural_active_power" ] @@ -975,72 +1155,334 @@ def _run_edisgo(self, mv_grid_id): ] # ToDo CHP + resistive heaters - # ########################## electromobility ########################## - logger.info("Set up electromobility data.") - # import charging points with standing times, etc. - edisgo_grid.import_electromobility( - data_source="oedb", scenario=scenario, engine=engine - ) - # apply charging strategy so that public charging points have a charging - # time series - edisgo_grid.apply_charging_strategy(strategy="dumb") - # get flexibility bands for home and work charging points - edisgo_grid.electromobility.get_flexibility_bands( - edisgo_obj=edisgo_grid, use_case=["home", "work"] - ) + logger.info("Run integrity check.") + edisgo_grid.check_integrity() - # requirements overlying grid - edisgo_grid.overlying_grid.electromobility_active_power = specs[ - "electromobility_active_power" - ] + return edisgo_grid - # ToDo Malte add intermediate storage of edisgo grid in case of errors later on - edisgo_grid.save( - directory=os.path.join(results_dir, "grid_data"), - save_topology=True, - save_timeseries=True, - save_results=False, - save_electromobility=True, - save_dsm=True, - save_heatpump=True, - save_overlying_grid=True, - reduce_memory=True, - archive=True, - archive_type="zip", - ) + def _run_edisgo_task_optimisation(self, edisgo_grid, logger): + """ + Runs the dispatch optimisation. - # ########################## checks ########################## - # ToDo Birgit expand - edisgo_grid.check_integrity() + Parameters + ---------- + mv_grid_id : int + MV grid ID of the ding0 grid - # ########################## optimisation ########################## - # ToDo Maike Call optimisation + Returns + ------- + :class:`edisgo.EDisGo` + Returns the complete eDisGo container, also including results - # ########################## reinforcement ########################## - # edisgo_grid.reinforce() + """ + logger.info(f"Start task 'optimisation'.") + return edisgo_grid - # ########################## save results ########################## - self._status_update(mv_grid_id, "end") - # edisgo_grid.save( - # directory=os.path.join(results_dir, "reinforce_data"), - # save_topology=True, - # save_timeseries=True, - # save_results=True, - # save_electromobility=False, - # # save_dsm=True, - # save_heatpump=False, - # save_overlying_grid=False, - # reduce_memory=True, - # archive=True, - # archive_type="zip", - # parameters={ - # "powerflow_results": ["pfa_p", "pfa_q", "v_res"], - # "grid_expansion_results": ["grid_expansion_costs", "equipment - # _changes"], - # }, - # ) + def _run_edisgo_task_grid_reinforcement(self, edisgo_grid, logger): + """ + Runs the dispatch optimisation. - return {edisgo_grid.topology.id: results_dir} + Parameters + ---------- + mv_grid_id : int + MV grid ID of the ding0 grid + + Returns + ------- + :class:`edisgo.EDisGo` + Returns the complete eDisGo container, also including results + + """ + logger.info(f"Start task 'grid_reinforcement'.") + return edisgo_grid + + # def _run_edisgo(self, mv_grid_id): + # """ + # Performs a single eDisGo run + # + # Parameters + # ---------- + # mv_grid_id : int + # MV grid ID of the ding0 grid + # + # Returns + # ------- + # :class:`edisgo.EDisGo` + # Returns the complete eDisGo container, also including results + # + # """ + # self._status_update(mv_grid_id, "start", show=False) + # + # # ##################### general settings #################### + # config = self._json_file + # engine = get_engine(config=config) + # scenario = config["eTraGo"]["scn_name"] + # + # # results directory + # results_dir = os.path.join( + # config["eGo"]["results_dir"], self._results, str(mv_grid_id) + # ) + # if not os.path.exists(results_dir): + # os.makedirs(results_dir) + # + # # logger + # if self._parallelization: + # stream_level = None + # else: + # stream_level = "debug" + # setup_logger( + # loggers=[ + # # {"name": "root", "file_level": None, "stream_level": None}, + # # {"name": "ego", "file_level": None, "stream_level": None}, + # {"name": "edisgo", "file_level": "debug", "stream_level": stream_level}, + # ], + # file_name=f"run_edisgo_{mv_grid_id}.log", + # log_dir=results_dir, + # ) + # # use edisgo logger in order to have all logging information for one grid go + # # to the same file + # logger = logging.getLogger("edisgo.external.ego._run_edisgo") + # + # # ################### get requirements from overlying grid ################## + # logger.info(f"MV grid {mv_grid_id}: Calculating interface values.") + # specs = get_etrago_results_per_bus( + # mv_grid_id, + # self._etrago_network, + # self._pf_post_lopf, + # self._max_cos_phi_renewable, + # ) + # + # # ################### start setting up edisgo object ################## + # logger.info(f"MV grid {mv_grid_id}: Initialize MV grid.") + # grid_path = os.path.join( + # config["eGo"]["data_dir"], + # config["eDisGo"]["grid_path"], + # str(mv_grid_id), + # ) + # if not os.path.isdir(grid_path): + # msg = f"MV grid {mv_grid_id}: No grid data found." + # logger.error(msg) + # raise Exception(msg) + # + # edisgo_grid = import_edisgo_from_files( + # edisgo_path=grid_path, legacy_ding0_grids=False + # ) + # edisgo_grid.set_timeindex(specs["timeindex"]) + # self._update_edisgo_configs(edisgo_grid) + # + # # set conventional load time series (active and reactive power) + # edisgo_grid.set_time_series_active_power_predefined( + # conventional_loads_ts="oedb", engine=engine, scenario=scenario + # ) + # edisgo_grid.set_time_series_reactive_power_control( + # control="fixed_cosphi", + # generators_parametrisation=None, + # loads_parametrisation="default", + # storage_units_parametrisation=None, + # ) + # # ToDo change p_set of conventional loads to peak in time series? + # + # # ########################### generator data ############################# + # logger.info("Set up generator data.") + # # import generator park of future scenario + # edisgo_grid.import_generators(generator_scenario=scenario, engine=engine) + # + # # set generator time series + # # active power + # edisgo_grid.set_time_series_active_power_predefined( + # dispatchable_generators_ts=specs["dispatchable_generators_active_power"], + # fluctuating_generators_ts=specs["renewables_potential"], + # ) + # # reactive power + # if self._pf_post_lopf: + # # ToDo Use eTraGo time series to set reactive power (scale by nominal power) + # edisgo_grid.set_time_series_manual( + # generators_q=specs["generators_reactive_power"].loc[:, []], + # ) + # pass + # else: + # edisgo_grid.set_time_series_reactive_power_control( + # control="fixed_cosphi", + # generators_parametrisation="default", + # loads_parametrisation=None, + # storage_units_parametrisation=None, + # ) + # + # # requirements overlying grid + # edisgo_grid.overlying_grid.renewables_curtailment = specs[ + # "renewables_curtailment" + # ] + # + # # check that all generators and conventional loads have time series data + # # ToDo can caplog be used to check for warnings or does it interfere with + # # logger? + # edisgo_grid.check_integrity() + # + # # ########################## battery storage ########################## + # logger.info("Set up storage data.") + # # import home storage units + # edisgo_grid.import_home_batteries(scenario=scenario, engine=engine) + # + # # requirements overlying grid + # edisgo_grid.overlying_grid.storage_units_active_power = specs[ + # "storage_units_active_power" + # ] + # + # # ToDo distribute storage capacity to home storage and large storage (right now + # # only work around to have storage capacity in the grid) + # edisgo_grid.add_component( + # comp_type="storage_unit", + # bus=edisgo_grid.topology.mv_grid.station.index[0], + # p_nom=specs["storage_units_p_nom"], + # max_hours=specs["storage_units_max_hours"], + # type="large_storage", + # ) + # + # # ################################# DSM ################################## + # logger.info("Set up DSM data.") + # # import DSM data + # edisgo_grid.import_dsm(scenario=scenario, engine=engine) + # + # # requirements overlying grid + # edisgo_grid.overlying_grid.dsm_active_power = specs["dsm_active_power"] + # + # # ####################### district and individual heating ##################### + # logger.info("Set up heat supply and demand data.") + # # import heat pumps - also gets heat demand and COP time series per heat pump + # edisgo_grid.import_heat_pumps(scenario=scenario, engine=engine) + # + # # thermal storage units + # # decentral + # hp_decentral = edisgo_grid.topology.loads_df[ + # edisgo_grid.topology.loads_df.sector == "individual_heating" + # ] + # if hp_decentral.empty and specs["thermal_storage_rural_capacity"] > 0: + # raise ValueError( + # "There are thermal storage units for individual heating but no " + # "heat pumps." + # ) + # if specs["thermal_storage_rural_capacity"] > 0: + # tes_cap = ( + # edisgo_grid.topology.loads_df.loc[hp_decentral.index, "p_set"] + # * specs["thermal_storage_rural_capacity"] + # / edisgo_grid.topology.loads_df.loc[ + # hp_decentral.index, "p_set"].sum() + # ) + # # ToDo get efficiency from specs + # edisgo_grid.heat_pump.thermal_storage_units_df = pd.DataFrame( + # data={ + # "capacity": tes_cap, + # "efficiency": 0.9, + # } + # ) + # # district heating + # hp_dh = edisgo_grid.topology.loads_df[ + # edisgo_grid.topology.loads_df.sector == "district_heating" + # ] + # if hp_dh.empty and specs["thermal_storage_central_capacity"] > 0: + # raise ValueError( + # "There are thermal storage units for district heating but no " + # "heat pumps." + # ) + # if specs["thermal_storage_central_capacity"] > 0: + # tes_cap = ( + # edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"] + # * specs["thermal_storage_central_capacity"] + # / edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"].sum() + # ) + # edisgo_grid.heat_pump.thermal_storage_units_df = pd.concat( + # [ + # edisgo_grid.heat_pump.thermal_storage_units_df, + # pd.DataFrame( + # data={ + # "capacity": tes_cap, + # "efficiency": 0.9, + # } + # ), + # ] + # ) + # + # # requirements overlying grid + # edisgo_grid.overlying_grid.heat_pump_decentral_active_power = specs[ + # "heat_pump_rural_active_power" + # ] + # edisgo_grid.overlying_grid.heat_pump_central_active_power = specs[ + # "heat_pump_central_active_power" + # ] + # edisgo_grid.overlying_grid.geothermal_energy_feedin_district_heating = specs[ + # "geothermal_energy_feedin_district_heating" + # ] + # edisgo_grid.overlying_grid.solarthermal_energy_feedin_district_heating = specs[ + # "solarthermal_energy_feedin_district_heating" + # ] + # # ToDo CHP + resistive heaters + # + # # ########################## electromobility ########################## + # logger.info("Set up electromobility data.") + # # import charging points with standing times, etc. + # edisgo_grid.import_electromobility( + # data_source="oedb", scenario=scenario, engine=engine + # ) + # # apply charging strategy so that public charging points have a charging + # # time series + # edisgo_grid.apply_charging_strategy(strategy="dumb") + # # get flexibility bands for home and work charging points + # edisgo_grid.electromobility.get_flexibility_bands( + # edisgo_obj=edisgo_grid, use_case=["home", "work"] + # ) + # + # # requirements overlying grid + # edisgo_grid.overlying_grid.electromobility_active_power = specs[ + # "electromobility_active_power" + # ] + # + # # ToDo Malte add intermediate storage of edisgo grid in case of errors later on + # edisgo_grid.save( + # directory=os.path.join(results_dir, "grid_data"), + # save_topology=True, + # save_timeseries=True, + # save_results=False, + # save_electromobility=True, + # save_dsm=True, + # save_heatpump=True, + # save_overlying_grid=True, + # reduce_memory=True, + # archive=True, + # archive_type="zip", + # ) + # + # # ########################## checks ########################## + # # ToDo Birgit expand + # edisgo_grid.check_integrity() + # + # # ########################## optimisation ########################## + # # ToDo Maike Call optimisation + # + # # ########################## reinforcement ########################## + # # edisgo_grid.reinforce() + # + # # ########################## save results ########################## + # self._status_update(mv_grid_id, "end") + # # edisgo_grid.save( + # # directory=os.path.join(results_dir, "reinforce_data"), + # # save_topology=True, + # # save_timeseries=True, + # # save_results=True, + # # save_electromobility=False, + # # # save_dsm=True, + # # save_heatpump=False, + # # save_overlying_grid=False, + # # reduce_memory=True, + # # archive=True, + # # archive_type="zip", + # # parameters={ + # # "powerflow_results": ["pfa_p", "pfa_q", "v_res"], + # # "grid_expansion_results": ["grid_expansion_costs", "equipment + # # _changes"], + # # }, + # # ) + # + # return {edisgo_grid.topology.id: results_dir} def _save_edisgo_results(self): results_dir = os.path.join(self._json_file["eGo"]["results_dir"], self._results) From 0dbaae3228460f867ab850da8a770dfbe9517a3f Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 4 Apr 2023 22:50:20 +0200 Subject: [PATCH 054/126] Only set up central heat store when there are heat pumps --- ego/tools/edisgo_integration.py | 43 +++++++++++++++------------------ 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index c0d2f903..ef984a32 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1105,29 +1105,26 @@ def _run_edisgo_task_specs_overlying_grid(self, edisgo_grid, logger): hp_dh = edisgo_grid.topology.loads_df[ edisgo_grid.topology.loads_df.sector == "district_heating" ] - if hp_dh.empty and specs["thermal_storage_central_capacity"] > 0: - raise ValueError( - "There are thermal storage units for district heating but no " - "heat pumps." - ) - if specs["thermal_storage_central_capacity"] > 0: - tes_cap = ( - edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"] - * specs["thermal_storage_central_capacity"] - / edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"].sum() - ) - # ToDo get efficiency from specs - edisgo_grid.heat_pump.thermal_storage_units_df = pd.concat( - [ - edisgo_grid.heat_pump.thermal_storage_units_df, - pd.DataFrame( - data={ - "capacity": tes_cap, - "efficiency": 0.9, - } - ), - ] - ) + # ToDo check + if not hp_dh.empty: + if specs["thermal_storage_central_capacity"] > 0: + tes_cap = ( + edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"] + * specs["thermal_storage_central_capacity"] + / edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"].sum() + ) + # ToDo get efficiency from specs + edisgo_grid.heat_pump.thermal_storage_units_df = pd.concat( + [ + edisgo_grid.heat_pump.thermal_storage_units_df, + pd.DataFrame( + data={ + "capacity": tes_cap, + "efficiency": 0.9, + } + ), + ] + ) logger.info("Set requirements from overlying grid.") edisgo_grid.overlying_grid.renewables_curtailment = specs[ From aadf6ab3b8509c77a18084638341701ed60fff81 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 6 Apr 2023 09:28:55 +0200 Subject: [PATCH 055/126] Adapt getting cluster attribues for different scenarios --- ego/mv_clustering/egon_data_io.py | 410 +++++++++++++++++++++--------- 1 file changed, 293 insertions(+), 117 deletions(-) diff --git a/ego/mv_clustering/egon_data_io.py b/ego/mv_clustering/egon_data_io.py index a4e5ae07..3badf706 100644 --- a/ego/mv_clustering/egon_data_io.py +++ b/ego/mv_clustering/egon_data_io.py @@ -10,6 +10,20 @@ def func_within(geom_a, geom_b, srid=3035): + """ + Checks if geometry a is completely within geometry b. + + Parameters + ---------- + geom_a : Geometry + Geometry within `geom_b`. + geom_b : Geometry + Geometry containing `geom_a`. + srid : int + SRID geometries are transformed to in order to use the same SRID for both + geometries. + + """ return func.ST_Within( func.ST_Transform( geom_a, @@ -24,163 +38,325 @@ def func_within(geom_a, geom_b, srid=3035): @session_decorator def get_grid_ids(orm=None, session=None): + """ + Gets all MV grid IDs and the area of each grid in m^2. + + Parameters + ----------- + orm : dict + Dictionary with tables to retrieve data from. + + Returns + ------- + pandas.DataFrame + Dataframe with grid ID in index and corresponding area in m^2 in column + "area_m2". + + """ query = session.query( - orm["egon_hvmv_substation"].bus_id.label("bus_id"), - orm["egon_hvmv_substation"].point.label("geom"), + orm["egon_mv_grid_district"].bus_id, + orm["egon_mv_grid_district"].area.label("area_m2"), ) - mv_data = pd.read_sql_query(query.statement, session.bind, index_col="bus_id") - return mv_data + return pd.read_sql_query(query.statement, session.bind, index_col="bus_id") @session_decorator -def get_solar_capacity(orm=None, session=None): - # Get PV open space join weather cell id - query = ( - session.query( - orm["generators_pv"].bus_id, - func.sum(orm["generators_pv"].capacity).label("p_openspace"), +def get_solar_capacity(scenario, grid_ids, orm=None, session=None): + """ + Gets PV capacity (rooftop and ground mounted) in MW per grid in specified scenario. + + Parameters + ----------- + scenario : str + Scenario to obtain data for. Possible options are "status_quo", "eGon2035", + and "eGon100RE". + grid_ids : list(int) + List of grid IDs to obtain data for. + orm : dict + Dictionary with tables to retrieve data from. + + Returns + ------- + pandas.DataFrame + DataFrame with grid ID in index and corresponding PV capacity in MW in column + "pv_capacity_mw". + + """ + # get PV ground mounted capacity per grid + if scenario == "status_quo": + query = ( + session.query( + orm["generators_pv_status_quo"].bus_id, + func.sum(orm["generators_pv_status_quo"].capacity).label("p_openspace"), + ) + .filter( + orm["generators_pv_status_quo"].bus_id.in_(grid_ids), + orm["generators_pv_status_quo"].site_type == "Freifläche", + orm["generators_pv_status_quo"].status == "InBetrieb", + orm["generators_pv_status_quo"].capacity <= 20, + orm["generators_pv_status_quo"].voltage_level.in_([4, 5, 6, 7]), + ) + .group_by( + orm["generators_pv_status_quo"].bus_id, + ) ) - .filter( - orm["generators_pv"].bus_id > 0, - orm["generators_pv"].site_type == "Freifläche", - orm["generators_pv"].status == "InBetrieb", - orm["generators_pv"].voltage_level.in_([4, 5, 6, 7]), + cap_open_space_df = pd.read_sql( + sql=query.statement, con=session.bind, index_col="bus_id" ) - .group_by( - orm["generators_pv"].bus_id, + else: + query = ( + session.query( + orm["generators"].bus_id, + func.sum(orm["generators"].el_capacity).label("p_openspace"), + ) + .filter( + orm["generators"].scenario == scenario, + orm["generators"].bus_id.in_(grid_ids), + orm["generators"].voltage_level >= 4, + orm["generators"].el_capacity <= 20, + orm["generators"].carrier == "solar", + ) + .group_by( + orm["generators"].bus_id, + ) ) - ) - generators_pv_open_space_df = pd.read_sql( - sql=query.statement, con=session.bind, index_col=None - ) - + cap_open_space_df = pd.read_sql( + sql=query.statement, con=session.bind, index_col="bus_id" + ) + # get PV rooftop capacity per grid query = ( session.query( orm["generators_pv_rooftop"].bus_id, func.sum(orm["generators_pv_rooftop"].capacity).label("p_rooftop"), ) .filter( - orm["generators_pv_rooftop"].bus_id > 0, - orm["generators_pv_rooftop"].scenario == "status_quo", + orm["generators_pv_rooftop"].bus_id.in_(grid_ids), + orm["generators_pv_rooftop"].scenario == scenario, + orm["generators_pv_rooftop"].capacity <= 20, orm["generators_pv_rooftop"].voltage_level.in_([4, 5, 6, 7]), ) .group_by( orm["generators_pv_rooftop"].bus_id, ) ) - generators_pv_rooftop_df = pd.read_sql( - sql=query.statement, con=session.bind, index_col=None + cap_rooftop_df = pd.read_sql( + sql=query.statement, con=session.bind, index_col="bus_id" ) - renewable_generators_df = generators_pv_open_space_df.set_index("bus_id").join( - generators_pv_rooftop_df.set_index("bus_id"), how="outer" + return ( + cap_open_space_df.join(cap_rooftop_df, how="outer") + .fillna(value=0) + .sum(axis="columns") + .to_frame("pv_capacity_mw") ) - renewable_generators_df.fillna(value=0, inplace=True) - renewable_generators_df["solar_cap"] = renewable_generators_df.sum(axis="columns") - return renewable_generators_df[["solar_cap"]] @session_decorator -def get_wind_capacity(orm=None, session=None): - # Get generators wind join weather cells - query = ( - session.query( - orm["generators_wind"].bus_id, - func.sum(orm["generators_wind"].capacity).label("wind_capacity"), +def get_wind_capacity(scenario, grid_ids, orm=None, session=None): + """ + Gets wind onshore capacity in MW per grid in specified scenario. + + Parameters + ----------- + scenario : str + Scenario to obtain data for. Possible options are "status_quo", "eGon2035", + and "eGon100RE". + grid_ids : list(int) + List of grid IDs to obtain data for. + orm : dict + Dictionary with tables to retrieve data from. + + Returns + ------- + pandas.DataFrame + DataFrame with grid ID in index and corresponding Wind capacity in MW in + column "wind_capacity_mw". + + """ + if scenario == "status_quo": + query = ( + session.query( + orm["generators_wind_status_quo"].bus_id, + func.sum(orm["generators_wind_status_quo"].capacity).label( + "wind_capacity_mw" + ), + ) + .filter( + orm["generators_wind_status_quo"].bus_id.in_(grid_ids), + orm["generators_wind_status_quo"].site_type == "Windkraft an Land", + orm["generators_wind_status_quo"].status == "InBetrieb", + orm["generators_wind_status_quo"].capacity <= 20, + orm["generators_wind_status_quo"].voltage_level.in_([4, 5, 6, 7]), + ) + .group_by( + orm["generators_wind_status_quo"].bus_id, + ) ) - .filter( - orm["generators_wind"].bus_id > 0, - orm["generators_wind"].site_type == "Windkraft an Land", - orm["generators_wind"].status == "InBetrieb", - orm["generators_wind"].voltage_level.in_([4, 5, 6, 7]), + cap_wind_df = pd.read_sql( + sql=query.statement, con=session.bind, index_col="bus_id" ) - .group_by( - orm["generators_wind"].bus_id, + else: + query = ( + session.query( + orm["generators"].bus_id, + func.sum(orm["generators"].el_capacity).label("wind_capacity_mw"), + ) + .filter( + orm["generators"].scenario == scenario, + orm["generators"].bus_id.in_(grid_ids), + orm["generators"].voltage_level >= 4, + orm["generators"].el_capacity <= 20, + orm["generators"].carrier == "wind_onshore", + ) + .group_by( + orm["generators"].bus_id, + ) ) - ) - generators_wind_df = pd.read_sql( - sql=query.statement, con=session.bind, index_col=None - ) - - renewable_generators_df = generators_wind_df.set_index("bus_id") - renewable_generators_df["wind_cap"] = renewable_generators_df.sum(axis="columns") - return renewable_generators_df[["wind_cap"]] + cap_wind_df = pd.read_sql( + sql=query.statement, con=session.bind, index_col="bus_id" + ) + return cap_wind_df @session_decorator -def get_emob_capacity(orm=None, session=None): - load_timeseries_nested = ( - session.query( - orm["etrago_load_timeseries"].scn_name, - orm["etrago_load_timeseries"].load_id, - orm["etrago_load_timeseries"].temp_id, - orm["etrago_load_timeseries"].p_set, - orm["etrago_load"].bus.label("bus_id"), - ) - .join( - orm["etrago_load_timeseries"], - orm["etrago_load_timeseries"].load_id == orm["etrago_load"].load_id, +def get_electromobility_maximum_load(scenario, grid_ids, orm=None, session=None): + """ + Parameters + ----------- + scenario : str + Scenario to obtain data for. Possible options are "status_quo", "eGon2035", + and "eGon100RE". + grid_ids : list(int) + List of grid IDs to obtain data for. + orm : dict + Dictionary with tables to retrieve data from. + + Returns + ------- + pandas.DataFrame + DataFrame with grid ID in index and corresponding maximum electromobility load + in MW in column "electromobility_max_load_mw". + + """ + if scenario == "status_quo": + return pd.DataFrame(columns=["electromobility_max_load_mw"]) + else: + load_timeseries_nested = ( + session.query( + orm["etrago_load"].bus.label("bus_id"), + orm["etrago_load_timeseries"].p_set, + ) + .join( + orm["etrago_load_timeseries"], + orm["etrago_load_timeseries"].load_id == orm["etrago_load"].load_id, + ) + .filter( + orm["etrago_load"].scn_name == f"{scenario}_lowflex", + orm["etrago_load"].carrier == "land_transport_EV", + orm["etrago_load"].bus.in_(grid_ids), + ) + ).subquery(name="load_timeseries_nested") + load_timeseries_unnested = ( + session.query( + load_timeseries_nested.c.bus_id, + func.unnest(load_timeseries_nested.c.p_set).label("p_set"), + ) + ).subquery(name="load_timeseries_unnested") + load_timeseries_maximal = ( + session.query( + load_timeseries_unnested.c.bus_id, + func.max(load_timeseries_unnested.c.p_set).label("p_set_max"), + ).group_by( + load_timeseries_unnested.c.bus_id, + ) + ).subquery(name="load_timeseries_maximal") + load_p_nom = session.query( + load_timeseries_maximal.c.bus_id, + load_timeseries_maximal.c.p_set_max.label("electromobility_max_load_mw"), ) - .filter( - orm["etrago_load"].scn_name == "eGon2035_lowflex", - orm["etrago_load"].carrier == "land transport EV", + return pd.read_sql( + sql=load_p_nom.statement, con=session.bind, index_col="bus_id" ) - ).subquery(name="load_timeseries_nested") - load_timeseries_unnested = ( - session.query( - load_timeseries_nested.c.bus_id, - load_timeseries_nested.c.scn_name, - load_timeseries_nested.c.load_id, - load_timeseries_nested.c.temp_id, - func.unnest(load_timeseries_nested.c.p_set).label("p_set"), - ) - ).subquery(name="load_timeseries_unnested") - load_timeseries_maximal = ( - session.query( - load_timeseries_unnested.c.bus_id, - load_timeseries_unnested.c.scn_name, - load_timeseries_unnested.c.load_id, - load_timeseries_unnested.c.temp_id, - func.max(load_timeseries_unnested.c.p_set).label("p_set_max"), - ).group_by( - load_timeseries_unnested.c.bus_id, - load_timeseries_unnested.c.scn_name, - load_timeseries_unnested.c.load_id, - load_timeseries_unnested.c.temp_id, - ) - ).subquery(name="load_timeseries_maximal") - load_p_nom = session.query( - load_timeseries_maximal.c.bus_id, - func.sum(load_timeseries_maximal.c.p_set_max).label("emob_cap"), - ).group_by( - load_timeseries_maximal.c.bus_id, - ) - emob_capacity_df = pd.read_sql( - sql=load_p_nom.statement, con=session.bind, index_col=None - ) - emob_capacity_df.set_index("bus_id", inplace=True) - return emob_capacity_df @session_decorator -def get_cummulative_storage_capacity(bus_id_list, orm=None, session=None): - return +def get_heat_pump_capacity(scenario, grid_ids, orm=None, session=None): + """ + Gets heat pump capacity (individual heating and district heating) in MW per grid + in specified scenario. + Parameters + ----------- + scenario : str + Scenario to obtain data for. Possible options are "status_quo", "eGon2035", + and "eGon100RE". + grid_ids : list(int) + List of grid IDs to obtain data for. + orm : dict + Dictionary with tables to retrieve data from. -@session_decorator -def get_weather_id_for_generator(bus_id, orm=None, session=None): - query = ( - session.query( - orm["egon_hvmv_substation"].bus_id, - orm["weather_cells"].w_id, + Returns + ------- + pandas.DataFrame + DataFrame with grid ID in index and corresponding heat pump capacity in MW in + column "heat_pump_capacity_mw". + + """ + if scenario == "status_quo": + return pd.DataFrame(columns=["heat_pump_capacity_mw"]) + else: + # get individual heat pump capacity + query = ( + session.query( + orm["heat_pump_capacity_individual"].mv_grid_id.label("bus_id"), + func.sum(orm["heat_pump_capacity_individual"].capacity).label( + "cap_individual" + ), + ) + .filter( + orm["heat_pump_capacity_individual"].mv_grid_id.in_(grid_ids), + orm["heat_pump_capacity_individual"].carrier == "heat_pump", + orm["heat_pump_capacity_individual"].scenario == scenario, + orm["heat_pump_capacity_individual"].capacity <= 17.5, + ) + .group_by( + orm["heat_pump_capacity_individual"].mv_grid_id, + ) ) - .join( - orm["egon_hvmv_substation"], - func_within(orm["egon_hvmv_substation"].point, orm["weather_cells"].geom), + cap_individual_df = pd.read_sql( + sql=query.statement, con=session.bind, index_col="bus_id" ) - .filter( - orm["egon_hvmv_substation"].bus_id == bus_id, + # get central heat pump capacity + query = ( + session.query( + orm["egon_mv_grid_district"].bus_id, + func.sum(orm["heat_pump_capacity_district_heating"].capacity).label( + "p_set" + ), + ) + .filter( + orm["egon_mv_grid_district"].bus_id.in_(grid_ids), + orm["heat_pump_capacity_district_heating"].scenario == scenario, + orm["heat_pump_capacity_district_heating"].carrier == "heat_pump", + orm["heat_pump_capacity_district_heating"].capacity <= 17.5, + ) + .outerjoin( + orm["egon_mv_grid_district"], + func_within( + orm["heat_pump_capacity_district_heating"].geometry, + orm["egon_mv_grid_district"].geom, + ), + ) + .group_by( + orm["egon_mv_grid_district"].bus_id, + ) + ) + cap_dh_df = pd.read_sql( + sql=query.statement, con=session.bind, index_col="bus_id" ) + return ( + cap_individual_df.join(cap_dh_df, how="outer") + .fillna(value=0) + .sum(axis="columns") + .to_frame("heat_pump_capacity_mw") ) - weather_id = query.all()[0][1] - return weather_id From 2e5a8e783aa66254cab1e9a9566ab54fa4bad973 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 13 Apr 2023 21:10:13 +0200 Subject: [PATCH 056/126] Delete cluster examples --- .../mv_clustering/cluster_setting.json | 88 ------------------- .../mv_clustering/example_clustering.py | 29 ------ .../example_clustering_full_workflow.py | 46 ---------- .../example_database_connection.py | 42 --------- 4 files changed, 205 deletions(-) delete mode 100644 ego/examples/mv_clustering/cluster_setting.json delete mode 100644 ego/examples/mv_clustering/example_clustering.py delete mode 100644 ego/examples/mv_clustering/example_clustering_full_workflow.py delete mode 100644 ego/examples/mv_clustering/example_database_connection.py diff --git a/ego/examples/mv_clustering/cluster_setting.json b/ego/examples/mv_clustering/cluster_setting.json deleted file mode 100644 index b5f6d7ad..00000000 --- a/ego/examples/mv_clustering/cluster_setting.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "eGo": { - "eTraGo": true, - "eDisGo": true, - "csv_import_eTraGo": false, - "csv_import_eDisGo": false, - "random_seed": 42 - }, - "eTraGo": { - "db": "oedb", - "gridversion": "v0.4.5", - "method": "lopf", - "pf_post_lopf": false, - "start_snapshot": 1000, - "end_snapshot" : 1005, - "solver": "gurobi", - "solver_options":{}, - "scn_name": "eGo 100", - "scn_extension": null, - "scn_decommissioning": null, - "lpfile": false, - "csv_export": "results/your_results", - "db_export": false, - "extendable": ["storage", "network"], - "generator_noise": 789456, - "minimize_loading": false, - "ramp_limits": false, - "extra_functionality": null, - "network_clustering_kmeans": 10, - "load_cluster": false, - "network_clustering_ehv": false, - "disaggregation": "uniform", - "snapshot_clustering": false, - "parallelisation": false, - "skip_snapshots": false, - "line_grouping": false, - "branch_capacity_factor": {"HV": 0.5, "eHV" : 0.7}, - "load_shedding": false, - "foreign_lines" :{"carrier": "AC", "capacity": "osmTGmod"}, - "comments": "" - }, - "eDisGo": { - "db": "oedb", - "gridversion": "v0.5.0", - "grid_path": "/home/local/RL-INSTITUT/malte.jahn/git-repos/data/ego/grids", - "choice_mode": "cluster", - "cluster_attributes":["wind_cap", "solar_cap", "emob_cap"], - "only_cluster": false, - "manual_grids": [3280], - "n_clusters": 50, - "parallelization":false, - "max_calc_time": 0.5, - "max_workers":2, - "initial_reinforcement":true, - "max_cos_phi_renewable": 0.9, - "results": "/home/local/RL-INSTITUT/malte.jahn/git-repos/data/ego", - "solver": "glpk", - "timesteps_pfa": "snapshot_analysis" - }, - "enable_external_config":true, - "mv_clustering": { - "database": { - "database_name": "egon-data", - "host": "127.0.0.1", - "port": "port", - "user": "username", - "password": "password", - "tables": { - "egon_mv_grid_district": "grid.egon_mv_grid_district", - "egon_hvmv_substation": "grid.egon_hvmv_substation", - "generators_pv": "supply.egon_power_plants_pv", - "generators_pv_rooftop": "supply.egon_power_plants_pv_roof_building", - "generators_wind": "supply.egon_power_plants_wind", - "etrago_load": "grid.egon_etrago_load", - "etrago_load_timeseries": "grid.egon_etrago_load_timeseries", - "weather_cells": "supply.egon_era5_weather_cells" - }, - "ssh": { - "enabled": true, - "user": "username", - "ip": "ip", - "port": "port", - "local_address": "127.0.0.1", - "local_port": "local_port" - } - } - } -} diff --git a/ego/examples/mv_clustering/example_clustering.py b/ego/examples/mv_clustering/example_clustering.py deleted file mode 100644 index 37e5ed54..00000000 --- a/ego/examples/mv_clustering/example_clustering.py +++ /dev/null @@ -1,29 +0,0 @@ -# flake8: noqa: E402 -import warnings - -warnings.filterwarnings("ignore") - -import os - -from edisgo.tools.logger import setup_logger - -from ego.mv_clustering import cluster_workflow -from ego.tools.utilities import get_scenario_setting - -setup_logger( - loggers=[ - {"name": "root", "file_level": None, "stream_level": "warning"}, - {"name": "ego", "file_level": None, "stream_level": "debug"}, - {"name": "edisgo", "file_level": None, "stream_level": "info"}, - ] -) - -base_path = os.path.join(os.path.expanduser("~"), "git-repos", "data", "ego") - -os.remove(os.path.join(base_path, "grids", "attributes.csv")) - -config = get_scenario_setting(jsonpath="cluster_setting.json") - -cluster_workflow(config=config) - -print("THE END") diff --git a/ego/examples/mv_clustering/example_clustering_full_workflow.py b/ego/examples/mv_clustering/example_clustering_full_workflow.py deleted file mode 100644 index 1c39922f..00000000 --- a/ego/examples/mv_clustering/example_clustering_full_workflow.py +++ /dev/null @@ -1,46 +0,0 @@ -# flake8: noqa: E402 -import warnings - -warnings.filterwarnings("ignore") - -import os -import sys - -from edisgo.tools.logger import setup_logger -from pypsa import Network as PyPSANetwork - -from ego.mv_clustering.database import sshtunnel -from ego.tools.edisgo_integration import EDisGoNetworks -from ego.tools.utilities import get_scenario_setting - -setup_logger( - loggers=[ - {"name": "root", "file_level": None, "stream_level": "warning"}, - {"name": "ego", "file_level": None, "stream_level": "debug"}, - {"name": "edisgo", "file_level": None, "stream_level": "info"}, - ] -) - -if (sys.argv[1] == "--pf_post_lopf") and (sys.argv[2] == "true"): - pf_post_lopf = True -elif (sys.argv[1] == "--pf_post_lopf") and (sys.argv[2] == "false"): - pf_post_lopf = False -else: - pf_post_lopf = False - print(f"Warning: Variable pf_post_lopf is not configured, set it to {False}.") - -base_path = os.path.join(os.path.expanduser("~"), "git-repos", "data", "ego") -config = get_scenario_setting(jsonpath="cluster_setting.json") - -config["eTraGo"]["pf_post_lopf"] = pf_post_lopf - -if pf_post_lopf: - file_name = "etrago_disaggregated_pf_post_lopf_true" -else: - file_name = "etrago_disaggregated_pf_post_lopf_false" - -etrago_network = PyPSANetwork(os.path.join(base_path, file_name)) -with sshtunnel(config=config): - edisgo_networks = EDisGoNetworks(json_file=config, etrago_network=etrago_network) - -print("THE END") diff --git a/ego/examples/mv_clustering/example_database_connection.py b/ego/examples/mv_clustering/example_database_connection.py deleted file mode 100644 index 4710836c..00000000 --- a/ego/examples/mv_clustering/example_database_connection.py +++ /dev/null @@ -1,42 +0,0 @@ -# flake8: noqa: E402 -import warnings - -warnings.filterwarnings("ignore") - -import os - -import pandas as pd - -from edisgo.tools.logger import setup_logger - -import ego.mv_clustering.egon_data_io as db_io - -from ego.mv_clustering.database import get_engine, register_tables_in_saio, sshtunnel -from ego.tools.utilities import get_scenario_setting - -setup_logger( - loggers=[ - {"name": "root", "file_level": None, "stream_level": "warning"}, - {"name": "ego", "file_level": None, "stream_level": "debug"}, - {"name": "edisgo", "file_level": None, "stream_level": "info"}, - ] -) -base_path = os.path.join(os.path.expanduser("~"), "git-repos", "data", "ego") -config = get_scenario_setting(jsonpath="cluster_setting.json") - -with sshtunnel(config=config): - engine = get_engine(config=config) - orm = register_tables_in_saio(engine, config=config) - - grid_ids_df = db_io.get_grid_ids(engine=engine, orm=orm) - solar_capacity_df = db_io.get_solar_capacity(engine=engine, orm=orm) - wind_capacity_df = db_io.get_wind_capacity(engine=engine, orm=orm) - emobility_capacity_df = db_io.get_emob_capacity(engine=engine, orm=orm) - - df = pd.concat( - [grid_ids_df, solar_capacity_df, wind_capacity_df, emobility_capacity_df], - axis="columns", - ) - df.fillna(0, inplace=True) - - print("THE END") From 031d61e7f841785ae7ed849e4e195f419d06c2d7 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 13 Apr 2023 21:11:21 +0200 Subject: [PATCH 057/126] Adapt obtaining heat pump data from database to include resistive heaters --- ego/mv_clustering/egon_data_io.py | 41 ++++++++++++------------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/ego/mv_clustering/egon_data_io.py b/ego/mv_clustering/egon_data_io.py index 3badf706..e4ae60b7 100644 --- a/ego/mv_clustering/egon_data_io.py +++ b/ego/mv_clustering/egon_data_io.py @@ -280,9 +280,9 @@ def get_electromobility_maximum_load(scenario, grid_ids, orm=None, session=None) @session_decorator -def get_heat_pump_capacity(scenario, grid_ids, orm=None, session=None): +def get_pth_capacity(scenario, grid_ids, orm=None, session=None): """ - Gets heat pump capacity (individual heating and district heating) in MW per grid + Gets PtH capacity (individual heating and district heating) in MW per grid in specified scenario. Parameters @@ -298,12 +298,12 @@ def get_heat_pump_capacity(scenario, grid_ids, orm=None, session=None): Returns ------- pandas.DataFrame - DataFrame with grid ID in index and corresponding heat pump capacity in MW in - column "heat_pump_capacity_mw". + DataFrame with grid ID in index and corresponding PtH capacity in MW in + column "pth_capacity_mw". """ if scenario == "status_quo": - return pd.DataFrame(columns=["heat_pump_capacity_mw"]) + return pd.DataFrame(columns=["pth_capacity_mw"]) else: # get individual heat pump capacity query = ( @@ -326,37 +326,28 @@ def get_heat_pump_capacity(scenario, grid_ids, orm=None, session=None): cap_individual_df = pd.read_sql( sql=query.statement, con=session.bind, index_col="bus_id" ) - # get central heat pump capacity + # get central heat pump and resistive heater capacity query = ( session.query( - orm["egon_mv_grid_district"].bus_id, - func.sum(orm["heat_pump_capacity_district_heating"].capacity).label( - "p_set" - ), + orm["pth_capacity_district_heating"].bus0, + func.sum(orm["pth_capacity_district_heating"].p_nom).label("p_set"), ) .filter( - orm["egon_mv_grid_district"].bus_id.in_(grid_ids), - orm["heat_pump_capacity_district_heating"].scenario == scenario, - orm["heat_pump_capacity_district_heating"].carrier == "heat_pump", - orm["heat_pump_capacity_district_heating"].capacity <= 17.5, - ) - .outerjoin( - orm["egon_mv_grid_district"], - func_within( - orm["heat_pump_capacity_district_heating"].geometry, - orm["egon_mv_grid_district"].geom, + orm["pth_capacity_district_heating"].bus0.in_(grid_ids), + orm["pth_capacity_district_heating"].scn_name == scenario, + orm["pth_capacity_district_heating"].carrier.in_( + ["central_heat_pump", "central_resistive_heater"] ), + orm["pth_capacity_district_heating"].p_nom <= 17.5, ) .group_by( - orm["egon_mv_grid_district"].bus_id, + orm["pth_capacity_district_heating"].bus0, ) ) - cap_dh_df = pd.read_sql( - sql=query.statement, con=session.bind, index_col="bus_id" - ) + cap_dh_df = pd.read_sql(sql=query.statement, con=session.bind, index_col="bus0") return ( cap_individual_df.join(cap_dh_df, how="outer") .fillna(value=0) .sum(axis="columns") - .to_frame("heat_pump_capacity_mw") + .to_frame("pth_capacity_mw") ) From cd124c2ba55f7bb75053a1de77b5e331fc49276f Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 13 Apr 2023 21:11:53 +0200 Subject: [PATCH 058/126] Adapt MV clustering --- ego/mv_clustering/mv_clustering.py | 193 ++++++++++++++++++++++------- 1 file changed, 151 insertions(+), 42 deletions(-) diff --git a/ego/mv_clustering/mv_clustering.py b/ego/mv_clustering/mv_clustering.py index 3f5f7bfe..47f5ee7e 100644 --- a/ego/mv_clustering/mv_clustering.py +++ b/ego/mv_clustering/mv_clustering.py @@ -48,51 +48,101 @@ logger = logging.getLogger(__name__) -def cluster_attributes_to_csv(attributes_path, config=None): +def get_cluster_attributes(attributes_path, scenario, config=None): """ - Calculates the attributes to cluster + Determines attributes to cluster MV grids by. + + Considered attributes are PV, wind onshore and PtH capacity, as well as + maximum load of EVs (in case of uncoordinated charging). All attributes are given + in MW as well as in MW per km^2. + + Data is written to csv file and returned. Parameters ---------- - attributes_path : :obj:`str` - Path to attributes csv - - config : :obj:`dict` + attributes_path : str + Path to save attributes csv to, including the file name. + scenario : str + Scenario to determine attributes for. Possible options are "status_quo", + "eGon2035", and "eGon100RE". + config : dict Config dict. - """ + Returns + ------- + pandas.DataFrame + DataFrame with grid ID in index and corresponding attributes in columns: + * "area" : area of MV grid in m^2 + * "pv_capacity_mw" : PV capacity in MW + * "pv_capacity_mw_per_km2" : PV capacity in MW per km^2 + * "wind_capacity_mw" : wind onshore capacity in MW + * "wind_capacity_mw_per_km2" : wind onshore capacity in MW per km^2 + * "electromobility_max_load_mw" : maximum load of EVs (in case of + uncoordinated charging) in MW + * "electromobility_max_load_mw_per_km2" : maximum load of EVs (in case of + uncoordinated charging) in MW per km^2 + * "pth_capacity_mw" : PtH capacity (for individual and district + heating) in MW + * "pth_capacity_mw_per_km2" : PtH capacity (for individual and + district heating) in MW per km^2 + """ + # get attributes from database with sshtunnel(config=config): engine = get_engine(config=config) orm = register_tables_in_saio(engine, config=config) grid_ids_df = db_io.get_grid_ids(engine=engine, orm=orm) - solar_capacity_df = db_io.get_solar_capacity(engine=engine, orm=orm) - wind_capacity_df = db_io.get_wind_capacity(engine=engine, orm=orm) - emob_capacity_df = db_io.get_emob_capacity(engine=engine, orm=orm) - + solar_capacity_df = db_io.get_solar_capacity( + scenario, grid_ids_df.index, orm, engine=engine + ) + wind_capacity_df = db_io.get_wind_capacity( + scenario, grid_ids_df.index, orm, engine=engine + ) + emob_capacity_df = db_io.get_electromobility_maximum_load( + scenario, grid_ids_df.index, orm, engine=engine + ) + pth_capacity_df = db_io.get_pth_capacity( + scenario, grid_ids_df.index, orm, engine=engine + ) df = pd.concat( - [grid_ids_df, solar_capacity_df, wind_capacity_df, emob_capacity_df], + [ + grid_ids_df, + solar_capacity_df, + wind_capacity_df, + emob_capacity_df, + pth_capacity_df, + ], axis="columns", + ).fillna(0) + # calculate relative values + df["pv_capacity_mw_per_km2"] = df["pv_capacity_mw"] / (df["area_m2"] / 1e6) + df["wind_capacity_mw_per_km2"] = df["wind_capacity_mw"] / (df["area_m2"] / 1e6) + df["electromobility_max_load_mw_per_km2"] = df["electromobility_max_load_mw"] / ( + df["area_m2"] / 1e6 ) - df.fillna(0, inplace=True) - + df["pth_capacity_mw_per_km2"] = df["pth_capacity_mw"] / (df["area_m2"] / 1e6) + # write to csv df.to_csv(attributes_path) + return df -def mv_grid_clustering(data_df, working_grids=None, config=None): +def mv_grid_clustering(cluster_attributes_df, working_grids=None, config=None): """ - Clusters the MV grids based on the attributes, for a given number - of MV grids + Clusters the MV grids based on the attributes, for a given number of MV grids. Parameters ---------- + cluster_attributes_df : pandas.DataFrame + Dataframe with data to cluster grids by. Columns contain the attributes to + cluster and index contains the MV grid IDs. + working_grids : pandas.DataFrame + DataFrame with information on whether MV grid can be used for calculations. + Index of the dataframe contains the MV grid ID and boolean value in column + "working" specifies whether respective grid can be used. config : dict Config dict. - working_grids : pandas.DataFrame - DataFrame of working grids, only working grids are used as cluster centers. - data_df : pandas.DataFrame - Attributes to cluster. + Returns ------- pandas.DataFrame @@ -103,18 +153,22 @@ def mv_grid_clustering(data_df, working_grids=None, config=None): n_clusters = config["eDisGo"]["n_clusters"] # Norm attributes - for attribute in data_df: - attribute_max = data_df[attribute].max() - data_df[attribute] = data_df[attribute] / attribute_max + for attribute in cluster_attributes_df: + attribute_max = cluster_attributes_df[attribute].max() + cluster_attributes_df[attribute] = ( + cluster_attributes_df[attribute] / attribute_max + ) # Starting KMeans clustering - logger.info(f"Used Clustering Attributes: {data_df.columns.to_list()}") + logger.info( + f"Used clustering attributes: {cluster_attributes_df.columns.to_list()}" + ) kmeans = KMeans(n_clusters=n_clusters, random_state=random_seed) - data_array = data_df.to_numpy() + data_array = cluster_attributes_df.to_numpy() labels = kmeans.fit_predict(data_array) centroids = kmeans.cluster_centers_ - result_df = pd.DataFrame(index=data_df.index) + result_df = pd.DataFrame(index=cluster_attributes_df.index) result_df["label"] = labels # For each sample, calculate the distance to its assigned centroid. result_df["centroid_distance"] = np.linalg.norm( @@ -134,10 +188,20 @@ def mv_grid_clustering(data_df, working_grids=None, config=None): result_df["working"] & (result_df["label"] == label), "centroid_distance", ].idxmin() + rep_orig = result_df.loc[ + result_df["label"] == label, "centroid_distance" + ].idxmin() result_df.loc[rep, "representative"] = True + result_df.loc[rep, "representative_orig"] = rep_orig except ValueError: failing_labels.append(label) + if len(failing_labels) > 0: + logger.warning( + f"There are {len(failing_labels)} clusters for which no representative " + f"could be determined." + ) + n_grids = result_df.shape[0] df_data = [] columns = [ @@ -145,6 +209,7 @@ def mv_grid_clustering(data_df, working_grids=None, config=None): "n_grids_per_cluster", "relative_representation", "represented_grids", + "representative_orig", ] for label in np.unique(labels): represented_grids = result_df[result_df["label"] == label].index.to_list() @@ -156,41 +221,85 @@ def mv_grid_clustering(data_df, working_grids=None, config=None): ].index.values[0] except IndexError: representative = False + try: + representative_orig = result_df[ + result_df["representative"] & (result_df["label"] == label) + ].representative_orig.values[0] + representative_orig = ( + True if representative == representative_orig else False + ) + except IndexError: + representative_orig = False row = [ representative, n_grids_per_cluster, relative_representation, represented_grids, + representative_orig, ] df_data.append(row) cluster_df = pd.DataFrame(df_data, index=np.unique(labels), columns=columns) cluster_df.index.name = "cluster_id" - return cluster_df + return cluster_df.sort_values("n_grids_per_cluster", ascending=False) def cluster_workflow(config=None): - attributes_path = os.path.join(config["eGo"]["results_dir"], "attributes.csv") - working_grids_path = os.path.join( - config["eGo"]["data_dir"], config["eDisGo"]["grid_path"], "working_grids.csv" - ) + """ + Get cluster attributes per grid if needed and conduct MV grid clustering. + + Parameters + ---------- + config : dict + Config dict from config json. Can be obtained by calling + ego.tools.utilities.get_scenario_setting(jsonpath=config_path). + + Returns + -------- + pandas.DataFrame + DataFrame with clustering results. Columns are "representative" containing + the grid ID of the representative grid, "n_grids_per_cluster" containing the + number of grids that are represented, "relative_representation" containing the + percentage of grids represented, "represented_grids" containing a list of + grid IDs of all represented grids and "representative_orig" containing + information on whether the representative is the actual cluster center (in which + case this value is True) or chosen because the grid in the cluster center is + not a working grid. - if not os.path.isfile(attributes_path): - logger.info("Attributes file is missing, get attributes from egon-data.") - cluster_attributes_to_csv(attributes_path=attributes_path, config=config) + """ + # determine cluster attributes + logger.info("Determine cluster attributes.") + attributes_path = os.path.join( + config["eGo"]["results_dir"], "mv_grid_cluster_attributes.csv" + ) + if not os.path.exists(config["eGo"]["results_dir"]): + os.makedirs(config["eGo"]["results_dir"]) + scenario = config["eTraGo"]["scn_name"] + cluster_attributes_df = get_cluster_attributes( + attributes_path=attributes_path, scenario=scenario, config=config + ) - data_to_cluster = pd.read_csv(attributes_path, index_col=0)[ + # select attributes to cluster by + cluster_attributes_df = cluster_attributes_df[ config["eDisGo"]["cluster_attributes"] ] + working_grids_path = os.path.join( + config["eGo"]["data_dir"], config["eDisGo"]["grid_path"], "working_grids.csv" + ) if os.path.isfile(working_grids_path): working_grids = pd.read_csv(working_grids_path, index_col=0) else: - raise FileNotFoundError("Working_grids aure missing.") - # logger.info("'working_grids.csv' is missing, select representative grids.") - # working_grids = None - - return mv_grid_clustering( - data_to_cluster, working_grids=working_grids, config=config + raise FileNotFoundError( + "working_grids.csv is missing. Cannot conduct MV grid clustering." + ) + # conduct MV grid clustering + cluster_df = mv_grid_clustering( + cluster_attributes_df, working_grids=working_grids, config=config ) + cluster_results_path = os.path.join( + config["eGo"]["results_dir"], "mv_grid_cluster_results.csv" + ) + cluster_df.to_csv(cluster_results_path) + return cluster_df From 17c1d8c5fd34a343ffcd1e14c692b48740abd641 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 13 Apr 2023 21:12:24 +0200 Subject: [PATCH 059/126] Allow specifying own scenario settings path --- ego/tools/utilities.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/ego/tools/utilities.py b/ego/tools/utilities.py index 7c91185e..81fb0414 100644 --- a/ego/tools/utilities.py +++ b/ego/tools/utilities.py @@ -86,7 +86,7 @@ def define_logging(name): return logger -def get_scenario_setting(jsonpath="scenario_setting.json"): +def get_scenario_setting(jsonpath=None): """Get and open json file with scenaio settings of eGo. The settings incluede eGo, eTraGo and eDisGo specific settings of arguments and parameters for a reproducible @@ -103,11 +103,13 @@ def get_scenario_setting(jsonpath="scenario_setting.json"): json_file : dict Dictionary of json file """ - path = os.getcwd() - # add try ego/ - logger.info("Your path is: {}".format(path)) + if jsonpath is None: + path = os.getcwd() + # add try ego/ + logger.info("Your path is: {}".format(path)) + jsonpath = os.path.join(path, "scenario_setting.json") - with open(path + "/" + jsonpath) as f: + with open(jsonpath) as f: json_file = json.load(f) # fix remove result_id From c614173d076e061240a92fc40a5f5240a75d2af6 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 13 Apr 2023 21:17:29 +0200 Subject: [PATCH 060/126] Delete old files --- .../read_config/example_read_setting.py | 22 - .../read_config/external_config_setting.json | 75 - .../no_external_config_setting.json | 88 - ego/examples/tutorials/README.md | 98 -- .../tutorials/edisgo_simple_example.ipynb | 1495 ----------------- .../tutorials/etrago_OpenMod_Zuerich18.ipynb | 706 -------- ego/examples/tutorials/grid_1476.png | Bin 410425 -> 0 bytes ego/examples/tutorials/requirements.yml | 27 - 8 files changed, 2511 deletions(-) delete mode 100644 ego/examples/read_config/example_read_setting.py delete mode 100644 ego/examples/read_config/external_config_setting.json delete mode 100644 ego/examples/read_config/no_external_config_setting.json delete mode 100644 ego/examples/tutorials/README.md delete mode 100644 ego/examples/tutorials/edisgo_simple_example.ipynb delete mode 100644 ego/examples/tutorials/etrago_OpenMod_Zuerich18.ipynb delete mode 100644 ego/examples/tutorials/grid_1476.png delete mode 100644 ego/examples/tutorials/requirements.yml diff --git a/ego/examples/read_config/example_read_setting.py b/ego/examples/read_config/example_read_setting.py deleted file mode 100644 index ae90100e..00000000 --- a/ego/examples/read_config/example_read_setting.py +++ /dev/null @@ -1,22 +0,0 @@ -# flake8: noqa: E402 -import warnings - -warnings.filterwarnings("ignore") - -from edisgo.tools.logger import setup_logger - -from ego.tools.utilities import get_scenario_setting - -setup_logger( - loggers=[ - {"name": "root", "file_level": None, "stream_level": "warning"}, - {"name": "ego", "file_level": None, "stream_level": "debug"}, - {"name": "edisgo", "file_level": None, "stream_level": "info"}, - ] -) - -config = get_scenario_setting(jsonpath="external_config_setting.json") - -config = get_scenario_setting(jsonpath="no_external_config_setting.json") - -print("THE END") diff --git a/ego/examples/read_config/external_config_setting.json b/ego/examples/read_config/external_config_setting.json deleted file mode 100644 index 5a0cc58b..00000000 --- a/ego/examples/read_config/external_config_setting.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "eGo": { - "eTraGo": true, - "eDisGo": true, - "csv_import_eTraGo": false, - "csv_import_eDisGo": false, - "data_dir": ".../data_in", - "results_dir": ".../data_out", - "random_seed": 42 - }, - "eTraGo": { - "db": "oedb", - "gridversion": "v0.4.5", - "method": "lopf", - "pf_post_lopf": false, - "start_snapshot": 1000, - "end_snapshot" : 1005, - "solver": "gurobi", - "solver_options":{}, - "scn_name": "eGo 100", - "scn_extension": null, - "scn_decommissioning": null, - "lpfile": false, - "csv_export": "results/your_results", - "db_export": false, - "extendable": ["storage", "network"], - "generator_noise": 789456, - "minimize_loading": false, - "ramp_limits": false, - "extra_functionality": null, - "network_clustering_kmeans": 10, - "load_cluster": false, - "network_clustering_ehv": false, - "disaggregation": "uniform", - "snapshot_clustering": false, - "parallelisation": false, - "skip_snapshots": false, - "line_grouping": false, - "branch_capacity_factor": {"HV": 0.5, "eHV" : 0.7}, - "load_shedding": false, - "foreign_lines" :{"carrier": "AC", "capacity": "osmTGmod"}, - "comments": "" - }, - "eDisGo": { - "db": "oedb", - "gridversion": "v0.5.0", - "grid_path": "/home/local/RL-INSTITUT/malte.jahn/git-repos/data/ego/grids", - "choice_mode": "cluster", - "cluster_attributes":["wind_cap", "solar_cap", "emob_cap"], - "only_cluster": false, - "manual_grids": [3280], - "n_clusters": 50, - "parallelization":false, - "max_calc_time": 0.5, - "max_workers":2, - "initial_reinforcement":true, - "max_cos_phi_renewable": 0.9, - "results": "/home/local/RL-INSTITUT/malte.jahn/git-repos/data/ego", - "solver": "glpk", - "timesteps_pfa": "snapshot_analysis" - }, - "database": { - "tables": { - "egon_mv_grid_district": "grid.egon_mv_grid_district", - "egon_hvmv_substation": "grid.egon_hvmv_substation", - "generators_pv": "supply.egon_power_plants_pv", - "generators_pv_rooftop": "supply.egon_power_plants_pv_roof_building", - "generators_wind": "supply.egon_power_plants_wind", - "etrago_load": "grid.egon_etrago_load", - "etrago_load_timeseries": "grid.egon_etrago_load_timeseries", - "weather_cells": "supply.egon_era5_weather_cells" - } - }, - "external_config": "~/.ego/secondary_ego_config.json" -} diff --git a/ego/examples/read_config/no_external_config_setting.json b/ego/examples/read_config/no_external_config_setting.json deleted file mode 100644 index e7cf1a9a..00000000 --- a/ego/examples/read_config/no_external_config_setting.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "eGo": { - "eTraGo": true, - "eDisGo": true, - "csv_import_eTraGo": false, - "csv_import_eDisGo": false, - "data_dir": ".../data_in", - "results_dir": ".../data_out", - "random_seed": 42 - }, - "eTraGo": { - "db": "oedb", - "gridversion": "v0.4.5", - "method": "lopf", - "pf_post_lopf": false, - "start_snapshot": 1000, - "end_snapshot" : 1005, - "solver": "gurobi", - "solver_options":{}, - "scn_name": "eGo 100", - "scn_extension": null, - "scn_decommissioning": null, - "lpfile": false, - "csv_export": "results/your_results", - "db_export": false, - "extendable": ["storage", "network"], - "generator_noise": 789456, - "minimize_loading": false, - "ramp_limits": false, - "extra_functionality": null, - "network_clustering_kmeans": 10, - "load_cluster": false, - "network_clustering_ehv": false, - "disaggregation": "uniform", - "snapshot_clustering": false, - "parallelisation": false, - "skip_snapshots": false, - "line_grouping": false, - "branch_capacity_factor": {"HV": 0.5, "eHV" : 0.7}, - "load_shedding": false, - "foreign_lines" :{"carrier": "AC", "capacity": "osmTGmod"}, - "comments": "" - }, - "eDisGo": { - "db": "oedb", - "gridversion": "v0.5.0", - "grid_path": "/home/local/RL-INSTITUT/malte.jahn/git-repos/data/ego/grids", - "choice_mode": "cluster", - "cluster_attributes":["wind_cap", "solar_cap", "emob_cap"], - "only_cluster": false, - "manual_grids": [3280], - "n_clusters": 50, - "parallelization":false, - "max_calc_time": 0.5, - "max_workers":2, - "initial_reinforcement":true, - "max_cos_phi_renewable": 0.9, - "results": "/home/local/RL-INSTITUT/malte.jahn/git-repos/data/ego", - "solver": "glpk", - "timesteps_pfa": "snapshot_analysis" - }, - "database": { - "database_name": "egon-data", - "host": "127.0.0.1", - "port": "port", - "user": "username", - "password": "password", - "tables": { - "egon_mv_grid_district": "grid.egon_mv_grid_district", - "egon_hvmv_substation": "grid.egon_hvmv_substation", - "generators_pv": "supply.egon_power_plants_pv", - "generators_pv_rooftop": "supply.egon_power_plants_pv_roof_building", - "generators_wind": "supply.egon_power_plants_wind", - "etrago_load": "grid.egon_etrago_load", - "etrago_load_timeseries": "grid.egon_etrago_load_timeseries", - "weather_cells": "supply.egon_era5_weather_cells" - } - }, - "ssh": { - "enabled": false, - "user": "username", - "ip": "ip", - "port": "port", - "local_address": "127.0.0.1", - "local_port": "local_port" - }, - "external_config": false -} diff --git a/ego/examples/tutorials/README.md b/ego/examples/tutorials/README.md deleted file mode 100644 index 2d30992a..00000000 --- a/ego/examples/tutorials/README.md +++ /dev/null @@ -1,98 +0,0 @@ -# eGo Tutorials - - -## eDisGo -The python package eDisGo provides a toolbox for analysis and optimization of distribution grids. This software lives in the context of the research project open_eGo. It is closely related to the python project Ding0 as this project is currently the single data source for eDisGo providing synthetic grid data for whole Germany. - - -Learn more about: -* [eDisGo – Optimization of flexibility options and grid expansion for distribution grids based on PyPSA](http://edisgo.readthedocs.io/en/dev/start_page.html) - - -## eTraGo -Optimization of flexibility options for transmission grids based on PyPSA. - -A speciality in this context is that transmission grids are described by the 380, 220 and 110 kV in Germany. Conventionally the 110kV grid is part of the distribution grid. The integration of the transmission and ‘upper’ distribution grid is part of eTraGo. - -The focus of optimization are flexibility options with a special focus on energy storages and grid expansion measures. - - -The python tool eTraGo can be used in several forms like from a terminal as an execution program, by integrated development environments (IDE) like [Spyder](https://anaconda.org/anaconda/spyder), [Jupyter notebooks](http://jupyter.org/install) or many more. - -A general description how you to install and work with eTraGo can be found also [here](http://etrago.readthedocs.io/en/latest/getting_started.html). - - -# Notebook installation - -#### with Anaconda - -Download and install your Python 3.x version of Anaconda [here](https://www.anaconda.com/download/). The full Documentation can be found [on this page.](https://docs.anaconda.com/anaconda/install/) - -We use Anaconda with an own environment in order to reduze problems with Packages and different versions on our system. Learn more about [Anacona environments](https://conda.io/docs/user-guide/tasks/manage-environments.html). Remove your environment with _'conda env remove -n openMod_Zuerich2018'_. - - - - -##### Quick start - steps to do: - -0. Sign-in on [openenergy-platform.org](http://openenergy-platform.org/login/) -1. Install Anacanda -2. Get eGo Repository from github -3. Create environment -4. Activate your environment -5. Install you notebook requirements -6. Make few settings for your notebook -7. Start your notebook and check if the notebook is running - - - -##### Get eGo Repository and install it with an environment -```desktop - -$ git clone -b features/tutorial https://git@github.com/openego/eGo.git -$ cd eGo/ego/examples/tutorials/ -$ conda env create --file requirements.yml -``` - -##### Activate your environment and run your notebooks -```desktop - -$ source activate openMod_Zuerich2018 -$ jupyter notebook -$ source deactivate -``` - -##### fixes and work arounds: - -* Error in function plot_stacked_gen() due to data name changes. Fix error in ../eGo/ego/examples/tutorials/src/etrago/etrago/tools/plot.py and add: 'wind_offshore':'skyblue', 'wind_onshore':'skyblue', instead of 'wind'; restart kernel -plot_stacked_gen(network, resolution="MW") - - -##### API and ego.io settings - -Your API settings will be saved in the folder .egoio in the file config.ini. - - -```desktop -[oedb] -dialect = oedialect -username = -database = oedb -host = openenergy-platform.org -port = 80 -password = -``` - - -### Start you Notebook - -```desktop -$ jupyter notebook -``` - -See for more information [how to run your jupyter notebook](https://jupyter.readthedocs.io/en/latest/running.html#running). - - -

    Note:

    - -The installation is only tested on Ubuntu 16.4. and Windows 10 with [Anaconda](https://www.anaconda.com/download/) diff --git a/ego/examples/tutorials/edisgo_simple_example.ipynb b/ego/examples/tutorials/edisgo_simple_example.ipynb deleted file mode 100644 index 4321ab40..00000000 --- a/ego/examples/tutorials/edisgo_simple_example.ipynb +++ /dev/null @@ -1,1495 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "__copyright__ = \"Reiner Lemoine Institut gGmbH\"\n", - "__license__ = \"GNU Affero General Public License Version 3 (AGPL-3.0)\"\n", - "__url__ = \"https://github.com/openego/eDisGo/blob/master/LICENSE\"\n", - "__author__ = \"gplssm, birgits\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Welcome to the eDisGo example\n", - "\n", - "eDisGo is a **python toolbox for the analysis of distribution networks** (low and medium voltage) that can be used to investigate economically viable **network expansion** scenarios, considering alternative flexibility options such as **storages or redispatch**. \n", - "\n", - "eDisGo is developed in the [open_eGo research project](https://openegoproject.wordpress.com/). It is based on [PyPSA](https://pypsa.org/), a toolbox for simulation and optimization of power networks, and closely related to the [ding0](https://dingo.readthedocs.io/en/dev/) project. ding0 stands for distribution network generator and is a tool to generate synthetic status quo medium and low voltage power distribution networks based on open (or at least accessible) data. It is currently the single data source for eDisGo providing synthetic grid data for whole Germany.\n", - "\n", - "**! eDisGo is work in progress !** Please be aware that some of its features may still be buggy and not yet very sophisticated. We are happy for any bug reports, hints, etc. you may have for us.\n", - "\n", - "### Learn more about eDisGo\n", - "\n", - "* __[eDisGo Source Code](https://github.com/openego/eDisGo)__\n", - "* __[eDisGo Documentation](http://edisgo.readthedocs.io/en/dev/)__\n", - "\n", - "### Table of Contents\n", - "\n", - "* [The eDisGo API](#settings)\n", - "* [The eDisGo data container and grid data structure](#network)\n", - "* [Future generator capacities](#generator_scenario)\n", - "* [Grid reinforcement](#grid_reinforcement)\n", - "* [Evaluate results](#evaluation)\n", - "* [References](#references)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## About the example\n", - "\n", - "This example shows the general usage of eDisGo. Grid expansion costs for an example distribution grid (see image below) are calculated assuming additional renewable energy generators as stated in the open_eGo 'NEP 2035' scenario (based on the scenario framework of the German grid development plan (Netzentwicklungsplan) for the year 2035) and conducting a worst-case analysis. Moreover, the eDisGo network data structure and how to access the results are introduced. At the end of the example grid expansion costs for a different scenario are calculated and compared to the grid expansion costs in the 'NEP 2035' scenario.\n", - "\n", - "\n", - "\n", - "**Let's get started!** First of all we have to import some packages." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Import packages" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/Birgit.Schachler/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/psycopg2/__init__.py:144: UserWarning: The psycopg2 wheel package will be renamed from release 2.8; in order to keep installing from binary please use \"pip install psycopg2-binary\" instead. For details see: .\n", - " \"\"\")\n", - "INFO:keyring.backend:Loading SecretService\n", - "INFO:keyring.backend:Loading kwallet\n", - "INFO:keyring.backend:Loading macOS\n", - "INFO:keyring.backend:Loading windows\n", - "INFO:keyring.backend:Loading Gnome\n", - "INFO:keyring.backend:Loading Google\n", - "INFO:keyring.backend:Loading Windows (alt)\n", - "INFO:keyring.backend:Loading file\n", - "INFO:keyring.backend:Loading keyczar\n", - "INFO:keyring.backend:Loading multi\n", - "INFO:keyring.backend:Loading pyfs\n" - ] - } - ], - "source": [ - "import os\n", - "import sys\n", - "import pandas as pd\n", - "\n", - "from edisgo import EDisGo\n", - "\n", - "import logging\n", - "logging.basicConfig(level=logging.ERROR)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## The eDisGo API \n", - "\n", - "The top-level API for setting up your scenario, invoking grid expansion and flexibility measures, etc. is provided by the **EDisGo class** (see [class documentation](http://edisgo.readthedocs.io/en/dev/api/edisgo.grid.html#edisgo.grid.network.EDisGo) for more information).\n", - "\n", - "In this example we simply want to do a worst-case analysis of a ding0 grid. For this, we only have to provide a grid and set the 'worst_case_analysis' parameter. \n", - "\n", - "#### Specifying the ding0 grid\n", - "\n", - "The ding0 grid is specified through the input parameter 'ding0_grid'. The following assumes you have a file of a ding0 grid named “ding0_grids__6.pkl” in current working directory.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "ding0_grid = os.path.join(sys.path[0], \"ding0_grids_239_DPv0.4.0.pkl\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Specifying worst-case analysis\n", - "\n", - "As worst-case analysis you can either just analyse the feed-in or the load case or do a combined analysis. Choose between the following options:\n", - "\n", - "* **’worst-case-feedin’** \n", - " \n", - " Feed-in and demand for the worst-case scenario \"feed-in case\" are generated. Demand is set to 15% of maximum demand for loads connected to the MV grid and 10% for loads connected to the LV grid. Feed-in of all generators is set to nominal power of the generator, except for PV systems where it is set to 85% of the nominal power.\n", - "\n", - " \n", - "* **’worst-case-load’**\n", - "\n", - " Feed-in and demand for the worst-case scenario \"load case\" are generated. Demand of all loads is set to maximum demand. Feed-in of all generators is set to zero.\n", - "\n", - "\n", - "* **’worst-case’**\n", - " \n", - " Feed-in and demand for the two worst-case scenarios \"feed-in case\" and \"load case\" are generated.\n", - "\n", - "Instead of doing a worst-case analysis you can also provide your own timeseries for demand and feed-in and use those in the network analysis. EDisGo also offers methods to generate load and feed-in time series. Check out the [EDisGo class documentation](http://edisgo.readthedocs.io/en/dev/api/edisgo.grid.html#edisgo.grid.network.EDisGo) for more information." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "worst_case_analysis = 'worst-case-feedin'" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now we are ready to initialize the edisgo API object." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "edisgo = EDisGo(ding0_grid=ding0_grid,\n", - " worst_case_analysis=worst_case_analysis)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## The eDisGo data container and grid data structure \n", - "\n", - "The last line, besides a couple of other things, initialized the [Network class](http://edisgo.readthedocs.io/en/dev/api/edisgo.grid.html#edisgo.grid.network.Network) which serves as an overall data container in eDisGo holding the grid data for the [MV grid](http://edisgo.readthedocs.io/en/dev/api/edisgo.grid.html#edisgo.grid.grids.MVGrid) and [LV grids](http://edisgo.readthedocs.io/en/dev/api/edisgo.grid.html#edisgo.grid.grids.LVGrid), [config data](http://edisgo.readthedocs.io/en/dev/api/edisgo.grid.html#edisgo.grid.network.Config), [results](http://edisgo.readthedocs.io/en/dev/api/edisgo.grid.html#edisgo.grid.network.Results), [timeseries](http://edisgo.readthedocs.io/en/dev/api/edisgo.grid.html#edisgo.grid.network.TimeSeries), etc. It is linked from multiple locations and provides hierarchical access to all data. Network itself can be accessed via the EDisGo API object as follows:\n", - "\n", - "```python\n", - "edisgo.network\n", - "```\n", - "\n", - "As mentioned *Network* holds the MV grid and LV grids. The grid topology is represented by separate undirected graphs for the MV grid and each of the LV grids. Each of these graphs is an eDisGo [Graph](http://edisgo.readthedocs.io/en/dev/_modules/edisgo/grid/grids.html#Graph), which is subclassed from networkx.Graph and extended by extra-functionality. Lines represent edges in the graph. Other equipment is represented by a node. Let's have a look into the graph.\n", - "\n", - "First we take a look at all the **lines**." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{GeneratorFluctuating_839361: {GeneratorFluctuating_878867: {'type': 'line',\n", - " 'line': Line_2390003},\n", - " BranchTee_MVGrid_239_86: {'type': 'line', 'line': Line_2390004}},\n", - " GeneratorFluctuating_839362: {BranchTee_MVGrid_239_87: {'type': 'line',\n", - " 'line': Line_2390005}},\n", - " GeneratorFluctuating_839363: {BranchTee_MVGrid_239_88: {'type': 'line',\n", - " 'line': Line_2390006}},\n", - " GeneratorFluctuating_839364: {LVStation_119612: {'type': 'line',\n", - " 'line': Line_2390007}},\n", - " GeneratorFluctuating_878450: {BranchTee_MVGrid_239_89: {'type': 'line',\n", - " 'line': Line_2390008}},\n", - " GeneratorFluctuating_878583: {BranchTee_MVGrid_239_84: {'type': 'line',\n", - " 'line': Line_2390001}},\n", - " GeneratorFluctuating_878609: {MVStation_239: {'type': 'line',\n", - " 'line': Line_2390009}},\n", - " GeneratorFluctuating_878611: {MVStation_239: {'type': 'line',\n", - " 'line': Line_2390010}},\n", - " GeneratorFluctuating_878614: {MVStation_239: {'type': 'line',\n", - " 'line': Line_2390011}},\n", - " GeneratorFluctuating_878615: {MVStation_239: {'type': 'line',\n", - " 'line': Line_2390012}},\n", - " GeneratorFluctuating_878862: {BranchTee_MVGrid_239_90: {'type': 'line',\n", - " 'line': Line_2390013}},\n", - " GeneratorFluctuating_878863: {BranchTee_MVGrid_239_91: {'type': 'line',\n", - " 'line': Line_2390014}},\n", - " GeneratorFluctuating_878864: {BranchTee_MVGrid_239_92: {'type': 'line',\n", - " 'line': Line_2390015}},\n", - " GeneratorFluctuating_878865: {BranchTee_MVGrid_239_93: {'type': 'line',\n", - " 'line': Line_2390016}},\n", - " GeneratorFluctuating_878866: {BranchTee_MVGrid_239_94: {'type': 'line',\n", - " 'line': Line_2390017}},\n", - " GeneratorFluctuating_878867: {GeneratorFluctuating_839361: {'type': 'line',\n", - " 'line': Line_2390003}},\n", - " GeneratorFluctuating_878875: {MVStation_239: {'type': 'line',\n", - " 'line': Line_2390018}},\n", - " GeneratorFluctuating_878950: {MVStation_239: {'type': 'line',\n", - " 'line': Line_2390019}},\n", - " GeneratorFluctuating_878963: {BranchTee_MVGrid_239_85: {'type': 'line',\n", - " 'line': Line_2390002}},\n", - " BranchTee_MVGrid_239_1: {LVStation_119897: {'type': 'line',\n", - " 'line': Line_2390050},\n", - " MVDisconnectingPoint_1: {'line': Line_2390252, 'type': 'line'}},\n", - " BranchTee_MVGrid_239_2: {MVStation_239: {'type': 'line',\n", - " 'line': Line_2390266},\n", - " MVDisconnectingPoint_2: {'line': Line_2390265, 'type': 'line'}},\n", - " BranchTee_MVGrid_239_3: {LVStation_125269: {'type': 'line',\n", - " 'line': Line_2390181},\n", - " BranchTee_MVGrid_239_88: {'type': 'line', 'line': Line_2390273},\n", - " BranchTee_MVGrid_239_89: {'type': 'line', 'line': Line_2390274}},\n", - " BranchTee_MVGrid_239_4: {LVStation_119904: {'type': 'line',\n", - " 'line': Line_2390055},\n", - " BranchTee_MVGrid_239_18: {'type': 'line', 'line': Line_2390251},\n", - " BranchTee_MVGrid_239_20: {'type': 'line', 'line': Line_2390254}},\n", - " BranchTee_MVGrid_239_5: {LVStation_120736: {'type': 'line',\n", - " 'line': Line_2390071},\n", - " BranchTee_MVGrid_239_6: {'type': 'line', 'line': Line_2390277},\n", - " BranchTee_MVGrid_239_8: {'type': 'line', 'line': Line_2390278}},\n", - " BranchTee_MVGrid_239_6: {LVStation_120411: {'type': 'line',\n", - " 'line': Line_2390064},\n", - " BranchTee_MVGrid_239_25: {'type': 'line', 'line': Line_2390260},\n", - " BranchTee_MVGrid_239_5: {'type': 'line', 'line': Line_2390277}},\n", - " BranchTee_MVGrid_239_7: {LVStation_120470: {'type': 'line',\n", - " 'line': Line_2390065},\n", - " LVStation_419726: {'type': 'line', 'line': Line_2390224},\n", - " BranchTee_MVGrid_239_8: {'type': 'line', 'line': Line_2390285}},\n", - " BranchTee_MVGrid_239_8: {LVStation_419795: {'type': 'line',\n", - " 'line': Line_2390225},\n", - " BranchTee_MVGrid_239_5: {'type': 'line', 'line': Line_2390278},\n", - " BranchTee_MVGrid_239_7: {'type': 'line', 'line': Line_2390285}},\n", - " BranchTee_MVGrid_239_9: {LVStation_120555: {'type': 'line',\n", - " 'line': Line_2390067},\n", - " LVStation_120585: {'type': 'line', 'line': Line_2390069},\n", - " BranchTee_MVGrid_239_10: {'type': 'line', 'line': Line_2390242}},\n", - " BranchTee_MVGrid_239_10: {LVStation_511325: {'type': 'line',\n", - " 'line': Line_2390238},\n", - " BranchTee_MVGrid_239_13: {'type': 'line', 'line': Line_2390241},\n", - " BranchTee_MVGrid_239_9: {'type': 'line', 'line': Line_2390242}},\n", - " BranchTee_MVGrid_239_11: {LVStation_120898: {'type': 'line',\n", - " 'line': Line_2390076},\n", - " LVStation_418244: {'type': 'line', 'line': Line_2390219},\n", - " MVStation_239: {'type': 'line', 'line': Line_2390243}},\n", - " BranchTee_MVGrid_239_12: {LVStation_120943: {'type': 'line',\n", - " 'line': Line_2390080},\n", - " LVStation_419885: {'type': 'line', 'line': Line_2390226},\n", - " BranchTee_MVGrid_239_84: {'type': 'line', 'line': Line_2390244}},\n", - " BranchTee_MVGrid_239_13: {LVStation_121317: {'type': 'line',\n", - " 'line': Line_2390087},\n", - " LVStation_416441: {'type': 'line', 'line': Line_2390204},\n", - " BranchTee_MVGrid_239_10: {'type': 'line', 'line': Line_2390241}},\n", - " BranchTee_MVGrid_239_14: {LVStation_121289: {'type': 'line',\n", - " 'line': Line_2390086},\n", - " LVStation_417530: {'type': 'line', 'line': Line_2390211},\n", - " MVStation_239: {'type': 'line', 'line': Line_2390245}},\n", - " BranchTee_MVGrid_239_15: {LVStation_121776: {'type': 'line',\n", - " 'line': Line_2390096},\n", - " BranchTee_MVGrid_239_29: {'type': 'line', 'line': Line_2390246},\n", - " BranchTee_MVGrid_239_55: {'type': 'line', 'line': Line_2390247}},\n", - " BranchTee_MVGrid_239_16: {LVStation_417276: {'type': 'line',\n", - " 'line': Line_2390210},\n", - " BranchTee_MVGrid_239_19: {'type': 'line', 'line': Line_2390248},\n", - " MVStation_239: {'type': 'line', 'line': Line_2390249}},\n", - " BranchTee_MVGrid_239_17: {LVStation_124110: {'type': 'line',\n", - " 'line': Line_2390148},\n", - " LVStation_416815: {'type': 'line', 'line': Line_2390207},\n", - " LVStation_416983: {'type': 'line', 'line': Line_2390209}},\n", - " BranchTee_MVGrid_239_18: {LVStation_121940: {'type': 'line',\n", - " 'line': Line_2390110},\n", - " BranchTee_MVGrid_239_29: {'type': 'line', 'line': Line_2390250},\n", - " BranchTee_MVGrid_239_4: {'type': 'line', 'line': Line_2390251}},\n", - " BranchTee_MVGrid_239_19: {LVStation_121919: {'type': 'line',\n", - " 'line': Line_2390108},\n", - " LVStation_416589: {'type': 'line', 'line': Line_2390206},\n", - " BranchTee_MVGrid_239_16: {'type': 'line', 'line': Line_2390248}},\n", - " BranchTee_MVGrid_239_20: {LVStation_419079: {'type': 'line',\n", - " 'line': Line_2390223},\n", - " BranchTee_MVGrid_239_35: {'type': 'line', 'line': Line_2390253},\n", - " BranchTee_MVGrid_239_4: {'type': 'line', 'line': Line_2390254}},\n", - " BranchTee_MVGrid_239_21: {LVStation_121879: {'type': 'line',\n", - " 'line': Line_2390099},\n", - " LVStation_122400: {'type': 'line', 'line': Line_2390128},\n", - " BranchTee_MVGrid_239_34: {'type': 'line', 'line': Line_2390255}},\n", - " BranchTee_MVGrid_239_22: {LVStation_122077: {'type': 'line',\n", - " 'line': Line_2390115},\n", - " LVStation_122078: {'type': 'line', 'line': Line_2390117},\n", - " LVStation_418237: {'type': 'line', 'line': Line_2390218}},\n", - " BranchTee_MVGrid_239_23: {LVStation_124085: {'type': 'line',\n", - " 'line': Line_2390144},\n", - " BranchTee_MVGrid_239_43: {'type': 'line', 'line': Line_2390256},\n", - " MVStation_239: {'type': 'line', 'line': Line_2390257}},\n", - " BranchTee_MVGrid_239_24: {LVStation_416449: {'type': 'line',\n", - " 'line': Line_2390205},\n", - " BranchTee_MVGrid_239_70: {'type': 'line', 'line': Line_2390258},\n", - " BranchTee_MVGrid_239_85: {'type': 'line', 'line': Line_2390259}},\n", - " BranchTee_MVGrid_239_25: {LVStation_503036: {'type': 'line',\n", - " 'line': Line_2390237},\n", - " BranchTee_MVGrid_239_6: {'type': 'line', 'line': Line_2390260},\n", - " BranchTee_MVGrid_239_76: {'type': 'line', 'line': Line_2390261}},\n", - " BranchTee_MVGrid_239_26: {LVStation_417550: {'type': 'line',\n", - " 'line': Line_2390213},\n", - " LVStation_417936: {'type': 'line', 'line': Line_2390216},\n", - " BranchTee_MVGrid_239_64: {'type': 'line', 'line': Line_2390262}},\n", - " BranchTee_MVGrid_239_27: {LVStation_417909: {'type': 'line',\n", - " 'line': Line_2390215},\n", - " LVStation_417987: {'type': 'line', 'line': Line_2390217},\n", - " BranchTee_MVGrid_239_78: {'type': 'line', 'line': Line_2390263}},\n", - " BranchTee_MVGrid_239_28: {LVStation_122077: {'type': 'line',\n", - " 'line': Line_2390116},\n", - " LVStation_122426: {'type': 'line', 'line': Line_2390132},\n", - " BranchTee_MVGrid_239_54: {'type': 'line', 'line': Line_2390264}},\n", - " BranchTee_MVGrid_239_29: {LVStation_122480: {'type': 'line',\n", - " 'line': Line_2390133},\n", - " BranchTee_MVGrid_239_15: {'type': 'line', 'line': Line_2390246},\n", - " BranchTee_MVGrid_239_18: {'type': 'line', 'line': Line_2390250}},\n", - " BranchTee_MVGrid_239_30: {BranchTee_MVGrid_239_32: {'type': 'line',\n", - " 'line': Line_2390267},\n", - " BranchTee_MVGrid_239_35: {'type': 'line', 'line': Line_2390268},\n", - " BranchTee_MVGrid_239_91: {'type': 'line', 'line': Line_2390269}},\n", - " BranchTee_MVGrid_239_31: {LVStation_139149: {'type': 'line',\n", - " 'line': Line_2390189},\n", - " LVStation_139186: {'type': 'line', 'line': Line_2390198},\n", - " BranchTee_MVGrid_239_34: {'type': 'line', 'line': Line_2390270}},\n", - " BranchTee_MVGrid_239_32: {LVStation_139104: {'type': 'line',\n", - " 'line': Line_2390184},\n", - " BranchTee_MVGrid_239_30: {'type': 'line', 'line': Line_2390267},\n", - " BranchTee_MVGrid_239_33: {'type': 'line', 'line': Line_2390271}},\n", - " BranchTee_MVGrid_239_33: {LVStation_419885: {'type': 'line',\n", - " 'line': Line_2390227},\n", - " BranchTee_MVGrid_239_32: {'type': 'line', 'line': Line_2390271},\n", - " BranchTee_MVGrid_239_86: {'type': 'line', 'line': Line_2390272}},\n", - " BranchTee_MVGrid_239_34: {LVStation_139150: {'type': 'line',\n", - " 'line': Line_2390191},\n", - " BranchTee_MVGrid_239_21: {'type': 'line', 'line': Line_2390255},\n", - " BranchTee_MVGrid_239_31: {'type': 'line', 'line': Line_2390270}},\n", - " BranchTee_MVGrid_239_35: {LVStation_122231: {'type': 'line',\n", - " 'line': Line_2390127},\n", - " BranchTee_MVGrid_239_20: {'type': 'line', 'line': Line_2390253},\n", - " BranchTee_MVGrid_239_30: {'type': 'line', 'line': Line_2390268}},\n", - " BranchTee_MVGrid_239_36: {LVStation_118322: {'type': 'line',\n", - " 'line': Line_2390020},\n", - " LVStation_118323: {'type': 'line', 'line': Line_2390021},\n", - " LVStation_124111: {'type': 'line', 'line': Line_2390150}},\n", - " BranchTee_MVGrid_239_37: {LVStation_118324: {'type': 'line',\n", - " 'line': Line_2390023},\n", - " LVStation_515314: {'type': 'line', 'line': Line_2390240}},\n", - " BranchTee_MVGrid_239_38: {LVStation_119612: {'type': 'line',\n", - " 'line': Line_2390024},\n", - " LVStation_119613: {'type': 'line', 'line': Line_2390026},\n", - " LVStation_417898: {'type': 'line', 'line': Line_2390214}},\n", - " BranchTee_MVGrid_239_39: {LVStation_119698: {'type': 'line',\n", - " 'line': Line_2390029}},\n", - " BranchTee_MVGrid_239_40: {LVStation_119701: {'type': 'line',\n", - " 'line': Line_2390032},\n", - " LVStation_119704: {'type': 'line', 'line': Line_2390035}},\n", - " BranchTee_MVGrid_239_41: {LVStation_119891: {'type': 'line',\n", - " 'line': Line_2390040},\n", - " LVStation_119894: {'type': 'line', 'line': Line_2390044},\n", - " LVStation_119895: {'type': 'line', 'line': Line_2390046}},\n", - " BranchTee_MVGrid_239_42: {LVStation_119896: {'type': 'line',\n", - " 'line': Line_2390048},\n", - " LVStation_119900: {'type': 'line', 'line': Line_2390052},\n", - " BranchTee_MVGrid_239_43: {'type': 'line', 'line': Line_2390275}},\n", - " BranchTee_MVGrid_239_43: {LVStation_119901: {'type': 'line',\n", - " 'line': Line_2390053},\n", - " BranchTee_MVGrid_239_23: {'type': 'line', 'line': Line_2390256},\n", - " BranchTee_MVGrid_239_42: {'type': 'line', 'line': Line_2390275}},\n", - " BranchTee_MVGrid_239_44: {LVStation_119892: {'type': 'line',\n", - " 'line': Line_2390041},\n", - " LVStation_119893: {'type': 'line', 'line': Line_2390042},\n", - " LVStation_119895: {'type': 'line', 'line': Line_2390047},\n", - " LVStation_119896: {'type': 'line', 'line': Line_2390049},\n", - " BranchTee_MVGrid_239_93: {'type': 'line', 'line': Line_2390276}},\n", - " BranchTee_MVGrid_239_45: {LVStation_119903: {'type': 'line',\n", - " 'line': Line_2390054},\n", - " LVStation_119904: {'type': 'line', 'line': Line_2390056}},\n", - " BranchTee_MVGrid_239_46: {LVStation_120387: {'type': 'line',\n", - " 'line': Line_2390059},\n", - " LVStation_120390: {'type': 'line', 'line': Line_2390063}},\n", - " BranchTee_MVGrid_239_47: {LVStation_120555: {'type': 'line',\n", - " 'line': Line_2390066},\n", - " LVStation_500916: {'type': 'line', 'line': Line_2390235}},\n", - " BranchTee_MVGrid_239_48: {LVStation_120737: {'type': 'line',\n", - " 'line': Line_2390072},\n", - " LVStation_120738: {'type': 'line', 'line': Line_2390073}},\n", - " BranchTee_MVGrid_239_49: {LVStation_120853: {'type': 'line',\n", - " 'line': Line_2390075},\n", - " LVStation_511325: {'type': 'line', 'line': Line_2390239}},\n", - " BranchTee_MVGrid_239_50: {LVStation_120898: {'type': 'line',\n", - " 'line': Line_2390077},\n", - " LVStation_120899: {'type': 'line', 'line': Line_2390078}},\n", - " BranchTee_MVGrid_239_51: {LVStation_120942: {'type': 'line',\n", - " 'line': Line_2390079},\n", - " LVStation_120943: {'type': 'line', 'line': Line_2390081}},\n", - " BranchTee_MVGrid_239_52: {LVStation_121286: {'type': 'line',\n", - " 'line': Line_2390082},\n", - " LVStation_121287: {'type': 'line', 'line': Line_2390083},\n", - " LVStation_121288: {'type': 'line', 'line': Line_2390085}},\n", - " BranchTee_MVGrid_239_53: {LVStation_121317: {'type': 'line',\n", - " 'line': Line_2390088},\n", - " LVStation_121318: {'type': 'line', 'line': Line_2390090}},\n", - " BranchTee_MVGrid_239_54: {LVStation_121742: {'type': 'line',\n", - " 'line': Line_2390093},\n", - " LVStation_121743: {'type': 'line', 'line': Line_2390095},\n", - " BranchTee_MVGrid_239_28: {'type': 'line', 'line': Line_2390264}},\n", - " BranchTee_MVGrid_239_55: {LVStation_121741: {'type': 'line',\n", - " 'line': Line_2390092},\n", - " LVStation_121742: {'type': 'line', 'line': Line_2390094},\n", - " LVStation_122230: {'type': 'line', 'line': Line_2390124},\n", - " LVStation_496409: {'type': 'line', 'line': Line_2390233},\n", - " BranchTee_MVGrid_239_15: {'type': 'line', 'line': Line_2390247}},\n", - " BranchTee_MVGrid_239_56: {LVStation_121879: {'type': 'line',\n", - " 'line': Line_2390100},\n", - " LVStation_121880: {'type': 'line', 'line': Line_2390102}},\n", - " BranchTee_MVGrid_239_57: {LVStation_121915: {'type': 'line',\n", - " 'line': Line_2390105},\n", - " LVStation_121916: {'type': 'line', 'line': Line_2390107},\n", - " LVStation_121919: {'type': 'line', 'line': Line_2390109}},\n", - " BranchTee_MVGrid_239_58: {LVStation_121940: {'type': 'line',\n", - " 'line': Line_2390111},\n", - " LVStation_121941: {'type': 'line', 'line': Line_2390112}},\n", - " BranchTee_MVGrid_239_59: {LVStation_122076: {'type': 'line',\n", - " 'line': Line_2390113},\n", - " LVStation_122078: {'type': 'line', 'line': Line_2390118}},\n", - " BranchTee_MVGrid_239_60: {LVStation_122123: {'type': 'line',\n", - " 'line': Line_2390120},\n", - " LVStation_122124: {'type': 'line', 'line': Line_2390121},\n", - " LVStation_122125: {'type': 'line', 'line': Line_2390123}},\n", - " BranchTee_MVGrid_239_61: {LVStation_122230: {'type': 'line',\n", - " 'line': Line_2390125}},\n", - " BranchTee_MVGrid_239_62: {LVStation_122400: {'type': 'line',\n", - " 'line': Line_2390129},\n", - " LVStation_122401: {'type': 'line', 'line': Line_2390130}},\n", - " BranchTee_MVGrid_239_63: {LVStation_122408: {'type': 'line',\n", - " 'line': Line_2390131},\n", - " LVStation_485974: {'type': 'line', 'line': Line_2390228}},\n", - " BranchTee_MVGrid_239_64: {LVStation_122696: {'type': 'line',\n", - " 'line': Line_2390135},\n", - " BranchTee_MVGrid_239_26: {'type': 'line', 'line': Line_2390262},\n", - " BranchTee_MVGrid_239_66: {'type': 'line', 'line': Line_2390279}},\n", - " BranchTee_MVGrid_239_65: {LVStation_122698: {'type': 'line',\n", - " 'line': Line_2390138},\n", - " LVStation_122699: {'type': 'line', 'line': Line_2390139},\n", - " BranchTee_MVGrid_239_66: {'type': 'line', 'line': Line_2390280}},\n", - " BranchTee_MVGrid_239_66: {LVStation_122697: {'type': 'line',\n", - " 'line': Line_2390136},\n", - " LVStation_418254: {'type': 'line', 'line': Line_2390220},\n", - " BranchTee_MVGrid_239_64: {'type': 'line', 'line': Line_2390279},\n", - " BranchTee_MVGrid_239_65: {'type': 'line', 'line': Line_2390280},\n", - " BranchTee_MVGrid_239_90: {'type': 'line', 'line': Line_2390281}},\n", - " BranchTee_MVGrid_239_67: {LVStation_124010: {'type': 'line',\n", - " 'line': Line_2390141},\n", - " LVStation_124011: {'type': 'line', 'line': Line_2390143}},\n", - " BranchTee_MVGrid_239_68: {LVStation_124085: {'type': 'line',\n", - " 'line': Line_2390145},\n", - " LVStation_124086: {'type': 'line', 'line': Line_2390146}},\n", - " BranchTee_MVGrid_239_69: {LVStation_124109: {'type': 'line',\n", - " 'line': Line_2390147},\n", - " LVStation_124110: {'type': 'line', 'line': Line_2390149},\n", - " LVStation_124111: {'type': 'line', 'line': Line_2390151}},\n", - " BranchTee_MVGrid_239_70: {LVStation_498758: {'type': 'line',\n", - " 'line': Line_2390234},\n", - " BranchTee_MVGrid_239_24: {'type': 'line', 'line': Line_2390258},\n", - " BranchTee_MVGrid_239_71: {'type': 'line', 'line': Line_2390282}},\n", - " BranchTee_MVGrid_239_71: {LVStation_124582: {'type': 'line',\n", - " 'line': Line_2390154},\n", - " LVStation_124583: {'type': 'line', 'line': Line_2390156},\n", - " BranchTee_MVGrid_239_70: {'type': 'line', 'line': Line_2390282}},\n", - " BranchTee_MVGrid_239_72: {LVStation_124911: {'type': 'line',\n", - " 'line': Line_2390160}},\n", - " BranchTee_MVGrid_239_73: {LVStation_125016: {'type': 'line',\n", - " 'line': Line_2390163},\n", - " LVStation_500931: {'type': 'line', 'line': Line_2390236},\n", - " BranchTee_MVGrid_239_74: {'type': 'line', 'line': Line_2390283}},\n", - " BranchTee_MVGrid_239_74: {LVStation_125015: {'type': 'line',\n", - " 'line': Line_2390162},\n", - " LVStation_125017: {'type': 'line', 'line': Line_2390165},\n", - " BranchTee_MVGrid_239_73: {'type': 'line', 'line': Line_2390283}},\n", - " BranchTee_MVGrid_239_75: {LVStation_125210: {'type': 'line',\n", - " 'line': Line_2390166},\n", - " LVStation_125211: {'type': 'line', 'line': Line_2390169},\n", - " MVDisconnectingPoint_4: {'line': Line_2390284, 'type': 'line'}},\n", - " BranchTee_MVGrid_239_76: {LVStation_125214: {'type': 'line',\n", - " 'line': Line_2390172},\n", - " LVStation_125215: {'type': 'line', 'line': Line_2390174},\n", - " BranchTee_MVGrid_239_25: {'type': 'line', 'line': Line_2390261}},\n", - " BranchTee_MVGrid_239_77: {LVStation_124911: {'type': 'line',\n", - " 'line': Line_2390161},\n", - " LVStation_125213: {'type': 'line', 'line': Line_2390171},\n", - " LVStation_125214: {'type': 'line', 'line': Line_2390173}},\n", - " BranchTee_MVGrid_239_78: {LVStation_125267: {'type': 'line',\n", - " 'line': Line_2390178},\n", - " LVStation_125268: {'type': 'line', 'line': Line_2390179},\n", - " BranchTee_MVGrid_239_27: {'type': 'line', 'line': Line_2390263}},\n", - " BranchTee_MVGrid_239_79: {LVStation_125268: {'type': 'line',\n", - " 'line': Line_2390180},\n", - " LVStation_125269: {'type': 'line', 'line': Line_2390182}},\n", - " BranchTee_MVGrid_239_80: {LVStation_139104: {'type': 'line',\n", - " 'line': Line_2390185},\n", - " LVStation_139105: {'type': 'line', 'line': Line_2390186},\n", - " LVStation_139106: {'type': 'line', 'line': Line_2390187}},\n", - " BranchTee_MVGrid_239_81: {LVStation_139150: {'type': 'line',\n", - " 'line': Line_2390192}},\n", - " BranchTee_MVGrid_239_82: {LVStation_139183: {'type': 'line',\n", - " 'line': Line_2390195},\n", - " LVStation_488816: {'type': 'line', 'line': Line_2390231},\n", - " LVStation_490253: {'type': 'line', 'line': Line_2390232}},\n", - " BranchTee_MVGrid_239_83: {LVStation_139186: {'type': 'line',\n", - " 'line': Line_2390199},\n", - " LVStation_139187: {'type': 'line', 'line': Line_2390201},\n", - " MVDisconnectingPoint_5: {'line': Line_2390197, 'type': 'line'}},\n", - " BranchTee_MVGrid_239_84: {GeneratorFluctuating_878583: {'type': 'line',\n", - " 'line': Line_2390001},\n", - " LVStation_120738: {'type': 'line', 'line': Line_2390074},\n", - " BranchTee_MVGrid_239_12: {'type': 'line', 'line': Line_2390244}},\n", - " BranchTee_MVGrid_239_85: {GeneratorFluctuating_878963: {'type': 'line',\n", - " 'line': Line_2390002},\n", - " LVStation_416815: {'type': 'line', 'line': Line_2390208},\n", - " BranchTee_MVGrid_239_24: {'type': 'line', 'line': Line_2390259}},\n", - " BranchTee_MVGrid_239_86: {GeneratorFluctuating_839361: {'type': 'line',\n", - " 'line': Line_2390004},\n", - " LVStation_139107: {'type': 'line', 'line': Line_2390188},\n", - " BranchTee_MVGrid_239_33: {'type': 'line', 'line': Line_2390272}},\n", - " BranchTee_MVGrid_239_87: {GeneratorFluctuating_839362: {'type': 'line',\n", - " 'line': Line_2390005},\n", - " LVStation_119698: {'type': 'line', 'line': Line_2390030},\n", - " LVStation_119889: {'type': 'line', 'line': Line_2390037}},\n", - " BranchTee_MVGrid_239_88: {GeneratorFluctuating_839363: {'type': 'line',\n", - " 'line': Line_2390006},\n", - " LVStation_125210: {'type': 'line', 'line': Line_2390167},\n", - " BranchTee_MVGrid_239_3: {'type': 'line', 'line': Line_2390273}},\n", - " BranchTee_MVGrid_239_89: {GeneratorFluctuating_878450: {'type': 'line',\n", - " 'line': Line_2390008},\n", - " LVStation_418546: {'type': 'line', 'line': Line_2390221},\n", - " BranchTee_MVGrid_239_3: {'type': 'line', 'line': Line_2390274}},\n", - " BranchTee_MVGrid_239_90: {GeneratorFluctuating_878862: {'type': 'line',\n", - " 'line': Line_2390013},\n", - " LVStation_123655: {'type': 'line', 'line': Line_2390140},\n", - " BranchTee_MVGrid_239_66: {'type': 'line', 'line': Line_2390281}},\n", - " BranchTee_MVGrid_239_91: {GeneratorFluctuating_878863: {'type': 'line',\n", - " 'line': Line_2390014},\n", - " LVStation_122520: {'type': 'line', 'line': Line_2390134},\n", - " BranchTee_MVGrid_239_30: {'type': 'line', 'line': Line_2390269}},\n", - " BranchTee_MVGrid_239_92: {GeneratorFluctuating_878864: {'type': 'line',\n", - " 'line': Line_2390015},\n", - " LVStation_119897: {'type': 'line', 'line': Line_2390051},\n", - " MVStation_239: {'type': 'line', 'line': Line_2390286}},\n", - " BranchTee_MVGrid_239_93: {GeneratorFluctuating_878865: {'type': 'line',\n", - " 'line': Line_2390016},\n", - " LVStation_120038: {'type': 'line', 'line': Line_2390057},\n", - " BranchTee_MVGrid_239_44: {'type': 'line', 'line': Line_2390276}},\n", - " BranchTee_MVGrid_239_94: {GeneratorFluctuating_878866: {'type': 'line',\n", - " 'line': Line_2390017},\n", - " LVStation_124910: {'type': 'line', 'line': Line_2390159},\n", - " LVStation_125216: {'type': 'line', 'line': Line_2390175}},\n", - " LVStation_122408: {BranchTee_MVGrid_239_63: {'type': 'line',\n", - " 'line': Line_2390131}},\n", - " LVStation_485974: {BranchTee_MVGrid_239_63: {'type': 'line',\n", - " 'line': Line_2390228},\n", - " MVStation_239: {'type': 'line', 'line': Line_2390229}},\n", - " LVStation_138585: {LVStation_139149: {'type': 'line', 'line': Line_2390183}},\n", - " LVStation_119895: {LVStation_119899: {'type': 'line', 'line': Line_2390045},\n", - " BranchTee_MVGrid_239_41: {'type': 'line', 'line': Line_2390046},\n", - " BranchTee_MVGrid_239_44: {'type': 'line', 'line': Line_2390047}},\n", - " LVStation_119896: {BranchTee_MVGrid_239_42: {'type': 'line',\n", - " 'line': Line_2390048},\n", - " BranchTee_MVGrid_239_44: {'type': 'line', 'line': Line_2390049}},\n", - " LVStation_119889: {LVStation_119891: {'type': 'line', 'line': Line_2390036},\n", - " BranchTee_MVGrid_239_87: {'type': 'line', 'line': Line_2390037}},\n", - " LVStation_119890: {LVStation_119893: {'type': 'line', 'line': Line_2390038},\n", - " LVStation_416175: {'type': 'line', 'line': Line_2390039}},\n", - " LVStation_119891: {LVStation_119889: {'type': 'line', 'line': Line_2390036},\n", - " BranchTee_MVGrid_239_41: {'type': 'line', 'line': Line_2390040}},\n", - " LVStation_119892: {LVStation_119697: {'type': 'line', 'line': Line_2390027},\n", - " BranchTee_MVGrid_239_44: {'type': 'line', 'line': Line_2390041}},\n", - " LVStation_119893: {LVStation_119890: {'type': 'line', 'line': Line_2390038},\n", - " BranchTee_MVGrid_239_44: {'type': 'line', 'line': Line_2390042}},\n", - " LVStation_119894: {LVStation_119898: {'type': 'line', 'line': Line_2390043},\n", - " BranchTee_MVGrid_239_41: {'type': 'line', 'line': Line_2390044}},\n", - " LVStation_119897: {BranchTee_MVGrid_239_1: {'type': 'line',\n", - " 'line': Line_2390050},\n", - " BranchTee_MVGrid_239_92: {'type': 'line', 'line': Line_2390051}},\n", - " LVStation_119898: {LVStation_119894: {'type': 'line', 'line': Line_2390043}},\n", - " LVStation_119899: {LVStation_119895: {'type': 'line', 'line': Line_2390045}},\n", - " LVStation_119900: {BranchTee_MVGrid_239_42: {'type': 'line',\n", - " 'line': Line_2390052}},\n", - " LVStation_119901: {BranchTee_MVGrid_239_43: {'type': 'line',\n", - " 'line': Line_2390053}},\n", - " LVStation_417530: {BranchTee_MVGrid_239_14: {'type': 'line',\n", - " 'line': Line_2390211}},\n", - " LVStation_419885: {BranchTee_MVGrid_239_12: {'type': 'line',\n", - " 'line': Line_2390226},\n", - " BranchTee_MVGrid_239_33: {'type': 'line', 'line': Line_2390227}},\n", - " LVStation_121940: {BranchTee_MVGrid_239_18: {'type': 'line',\n", - " 'line': Line_2390110},\n", - " BranchTee_MVGrid_239_58: {'type': 'line', 'line': Line_2390111}},\n", - " LVStation_121941: {BranchTee_MVGrid_239_58: {'type': 'line',\n", - " 'line': Line_2390112}},\n", - " LVStation_122426: {BranchTee_MVGrid_239_28: {'type': 'line',\n", - " 'line': Line_2390132}},\n", - " LVStation_122480: {BranchTee_MVGrid_239_29: {'type': 'line',\n", - " 'line': Line_2390133}},\n", - " LVStation_418254: {LVStation_124543: {'type': 'line', 'line': Line_2390152},\n", - " BranchTee_MVGrid_239_66: {'type': 'line', 'line': Line_2390220}},\n", - " LVStation_419605: {LVStation_125217: {'type': 'line', 'line': Line_2390176}},\n", - " LVStation_416441: {BranchTee_MVGrid_239_13: {'type': 'line',\n", - " 'line': Line_2390204}},\n", - " LVStation_418546: {BranchTee_MVGrid_239_89: {'type': 'line',\n", - " 'line': Line_2390221}},\n", - " LVStation_416244: {LVStation_119697: {'type': 'line', 'line': Line_2390028}},\n", - " LVStation_417898: {LVStation_417550: {'type': 'line', 'line': Line_2390212},\n", - " BranchTee_MVGrid_239_38: {'type': 'line', 'line': Line_2390214}},\n", - " LVStation_419795: {BranchTee_MVGrid_239_8: {'type': 'line',\n", - " 'line': Line_2390225}},\n", - " LVStation_120737: {LVStation_120736: {'type': 'line', 'line': Line_2390070},\n", - " BranchTee_MVGrid_239_48: {'type': 'line', 'line': Line_2390072}},\n", - " LVStation_120736: {LVStation_120737: {'type': 'line', 'line': Line_2390070},\n", - " BranchTee_MVGrid_239_5: {'type': 'line', 'line': Line_2390071}},\n", - " LVStation_120738: {BranchTee_MVGrid_239_48: {'type': 'line',\n", - " 'line': Line_2390073},\n", - " BranchTee_MVGrid_239_84: {'type': 'line', 'line': Line_2390074}},\n", - " LVStation_120942: {BranchTee_MVGrid_239_51: {'type': 'line',\n", - " 'line': Line_2390079}},\n", - " LVStation_120943: {BranchTee_MVGrid_239_12: {'type': 'line',\n", - " 'line': Line_2390080},\n", - " BranchTee_MVGrid_239_51: {'type': 'line', 'line': Line_2390081}},\n", - " LVStation_122230: {BranchTee_MVGrid_239_55: {'type': 'line',\n", - " 'line': Line_2390124},\n", - " BranchTee_MVGrid_239_61: {'type': 'line', 'line': Line_2390125}},\n", - " LVStation_122231: {LVStation_139192: {'type': 'line', 'line': Line_2390126},\n", - " BranchTee_MVGrid_239_35: {'type': 'line', 'line': Line_2390127}},\n", - " LVStation_418237: {BranchTee_MVGrid_239_22: {'type': 'line',\n", - " 'line': Line_2390218}},\n", - " LVStation_416449: {BranchTee_MVGrid_239_24: {'type': 'line',\n", - " 'line': Line_2390205}},\n", - " LVStation_417550: {LVStation_417898: {'type': 'line', 'line': Line_2390212},\n", - " BranchTee_MVGrid_239_26: {'type': 'line', 'line': Line_2390213}},\n", - " LVStation_139107: {BranchTee_MVGrid_239_86: {'type': 'line',\n", - " 'line': Line_2390188}},\n", - " LVStation_120585: {LVStation_124583: {'type': 'line', 'line': Line_2390068},\n", - " BranchTee_MVGrid_239_9: {'type': 'line', 'line': Line_2390069}},\n", - " LVStation_417276: {BranchTee_MVGrid_239_16: {'type': 'line',\n", - " 'line': Line_2390210}},\n", - " LVStation_122520: {BranchTee_MVGrid_239_91: {'type': 'line',\n", - " 'line': Line_2390134}},\n", - " LVStation_419726: {BranchTee_MVGrid_239_7: {'type': 'line',\n", - " 'line': Line_2390224}},\n", - " LVStation_121776: {BranchTee_MVGrid_239_15: {'type': 'line',\n", - " 'line': Line_2390096}},\n", - " LVStation_419327: {LVStation_124910: {'type': 'line', 'line': Line_2390158}},\n", - " LVStation_417734: {LVStation_139189: {'type': 'line', 'line': Line_2390203}},\n", - " LVStation_125015: {BranchTee_MVGrid_239_74: {'type': 'line',\n", - " 'line': Line_2390162}},\n", - " LVStation_125016: {LVStation_120389: {'type': 'line', 'line': Line_2390062},\n", - " BranchTee_MVGrid_239_73: {'type': 'line', 'line': Line_2390163}},\n", - " LVStation_125017: {LVStation_417909: {'type': 'line', 'line': Line_2390164},\n", - " BranchTee_MVGrid_239_74: {'type': 'line', 'line': Line_2390165}},\n", - " LVStation_500931: {BranchTee_MVGrid_239_73: {'type': 'line',\n", - " 'line': Line_2390236}},\n", - " LVStation_418244: {LVStation_122698: {'type': 'line', 'line': Line_2390137},\n", - " BranchTee_MVGrid_239_11: {'type': 'line', 'line': Line_2390219}},\n", - " LVStation_120411: {BranchTee_MVGrid_239_6: {'type': 'line',\n", - " 'line': Line_2390064}},\n", - " LVStation_121317: {BranchTee_MVGrid_239_13: {'type': 'line',\n", - " 'line': Line_2390087},\n", - " BranchTee_MVGrid_239_53: {'type': 'line', 'line': Line_2390088}},\n", - " LVStation_121318: {LVStation_121918: {'type': 'line', 'line': Line_2390089},\n", - " BranchTee_MVGrid_239_53: {'type': 'line', 'line': Line_2390090}},\n", - " LVStation_416815: {BranchTee_MVGrid_239_17: {'type': 'line',\n", - " 'line': Line_2390207},\n", - " BranchTee_MVGrid_239_85: {'type': 'line', 'line': Line_2390208}},\n", - " LVStation_139104: {BranchTee_MVGrid_239_32: {'type': 'line',\n", - " 'line': Line_2390184},\n", - " BranchTee_MVGrid_239_80: {'type': 'line', 'line': Line_2390185}},\n", - " LVStation_139105: {BranchTee_MVGrid_239_80: {'type': 'line',\n", - " 'line': Line_2390186}},\n", - " LVStation_139106: {BranchTee_MVGrid_239_80: {'type': 'line',\n", - " 'line': Line_2390187}},\n", - " LVStation_139192: {LVStation_122231: {'type': 'line', 'line': Line_2390126}},\n", - " LVStation_119903: {BranchTee_MVGrid_239_45: {'type': 'line',\n", - " 'line': Line_2390054}},\n", - " LVStation_119904: {BranchTee_MVGrid_239_4: {'type': 'line',\n", - " 'line': Line_2390055},\n", - " BranchTee_MVGrid_239_45: {'type': 'line', 'line': Line_2390056}},\n", - " LVStation_418449: {LVStation_121741: {'type': 'line', 'line': Line_2390091}},\n", - " LVStation_419079: {BranchTee_MVGrid_239_20: {'type': 'line',\n", - " 'line': Line_2390223}},\n", - " LVStation_119697: {LVStation_119892: {'type': 'line', 'line': Line_2390027},\n", - " LVStation_416244: {'type': 'line', 'line': Line_2390028}},\n", - " LVStation_119698: {BranchTee_MVGrid_239_39: {'type': 'line',\n", - " 'line': Line_2390029},\n", - " BranchTee_MVGrid_239_87: {'type': 'line', 'line': Line_2390030}},\n", - " LVStation_122076: {BranchTee_MVGrid_239_59: {'type': 'line',\n", - " 'line': Line_2390113},\n", - " MVStation_239: {'type': 'line', 'line': Line_2390114}},\n", - " LVStation_122077: {BranchTee_MVGrid_239_22: {'type': 'line',\n", - " 'line': Line_2390115},\n", - " BranchTee_MVGrid_239_28: {'type': 'line', 'line': Line_2390116}},\n", - " LVStation_122078: {BranchTee_MVGrid_239_22: {'type': 'line',\n", - " 'line': Line_2390117},\n", - " BranchTee_MVGrid_239_59: {'type': 'line', 'line': Line_2390118}},\n", - " LVStation_122124: {LVStation_121878: {'type': 'line', 'line': Line_2390097},\n", - " BranchTee_MVGrid_239_60: {'type': 'line', 'line': Line_2390121}},\n", - " LVStation_122125: {LVStation_490253: {'type': 'line', 'line': Line_2390122},\n", - " BranchTee_MVGrid_239_60: {'type': 'line', 'line': Line_2390123}},\n", - " LVStation_122122: {LVStation_122123: {'type': 'line', 'line': Line_2390119}},\n", - " LVStation_122123: {LVStation_122122: {'type': 'line', 'line': Line_2390119},\n", - " BranchTee_MVGrid_239_60: {'type': 'line', 'line': Line_2390120}},\n", - " LVStation_124543: {LVStation_418254: {'type': 'line', 'line': Line_2390152}},\n", - " LVStation_124911: {BranchTee_MVGrid_239_72: {'type': 'line',\n", - " 'line': Line_2390160},\n", - " BranchTee_MVGrid_239_77: {'type': 'line', 'line': Line_2390161}},\n", - " LVStation_124910: {LVStation_419327: {'type': 'line', 'line': Line_2390158},\n", - " BranchTee_MVGrid_239_94: {'type': 'line', 'line': Line_2390159}},\n", - " LVStation_139183: {LVStation_139184: {'type': 'line', 'line': Line_2390194},\n", - " BranchTee_MVGrid_239_82: {'type': 'line', 'line': Line_2390195}},\n", - " LVStation_139184: {LVStation_139183: {'type': 'line', 'line': Line_2390194},\n", - " LVStation_139185: {'type': 'line', 'line': Line_2390196}},\n", - " LVStation_139185: {LVStation_139184: {'type': 'line', 'line': Line_2390196}},\n", - " LVStation_139186: {BranchTee_MVGrid_239_31: {'type': 'line',\n", - " 'line': Line_2390198},\n", - " BranchTee_MVGrid_239_83: {'type': 'line', 'line': Line_2390199}},\n", - " LVStation_139187: {LVStation_139188: {'type': 'line', 'line': Line_2390200},\n", - " BranchTee_MVGrid_239_83: {'type': 'line', 'line': Line_2390201}},\n", - " LVStation_139188: {LVStation_139187: {'type': 'line', 'line': Line_2390200},\n", - " LVStation_139189: {'type': 'line', 'line': Line_2390202}},\n", - " LVStation_139189: {LVStation_139188: {'type': 'line', 'line': Line_2390202},\n", - " LVStation_417734: {'type': 'line', 'line': Line_2390203}},\n", - " LVStation_488816: {LVStation_490252: {'type': 'line', 'line': Line_2390230},\n", - " BranchTee_MVGrid_239_82: {'type': 'line', 'line': Line_2390231}},\n", - " LVStation_490252: {LVStation_488816: {'type': 'line', 'line': Line_2390230}},\n", - " LVStation_490253: {LVStation_122125: {'type': 'line', 'line': Line_2390122},\n", - " BranchTee_MVGrid_239_82: {'type': 'line', 'line': Line_2390232}},\n", - " LVStation_118322: {BranchTee_MVGrid_239_36: {'type': 'line',\n", - " 'line': Line_2390020}},\n", - " LVStation_118323: {BranchTee_MVGrid_239_36: {'type': 'line',\n", - " 'line': Line_2390021},\n", - " MVDisconnectingPoint_3: {'line': Line_2390022, 'type': 'line'}},\n", - " LVStation_118324: {BranchTee_MVGrid_239_37: {'type': 'line',\n", - " 'line': Line_2390023}},\n", - " LVStation_515314: {LVStation_119702: {'type': 'line', 'line': Line_2390033},\n", - " BranchTee_MVGrid_239_37: {'type': 'line', 'line': Line_2390240}},\n", - " LVStation_120387: {LVStation_120388: {'type': 'line', 'line': Line_2390058},\n", - " BranchTee_MVGrid_239_46: {'type': 'line', 'line': Line_2390059}},\n", - " LVStation_120388: {LVStation_120387: {'type': 'line', 'line': Line_2390058},\n", - " LVStation_121287: {'type': 'line', 'line': Line_2390060}},\n", - " LVStation_120389: {LVStation_120390: {'type': 'line', 'line': Line_2390061},\n", - " LVStation_125016: {'type': 'line', 'line': Line_2390062}},\n", - " LVStation_120390: {LVStation_120389: {'type': 'line', 'line': Line_2390061},\n", - " BranchTee_MVGrid_239_46: {'type': 'line', 'line': Line_2390063}},\n", - " LVStation_120853: {BranchTee_MVGrid_239_49: {'type': 'line',\n", - " 'line': Line_2390075}},\n", - " LVStation_511325: {BranchTee_MVGrid_239_10: {'type': 'line',\n", - " 'line': Line_2390238},\n", - " BranchTee_MVGrid_239_49: {'type': 'line', 'line': Line_2390239}},\n", - " LVStation_120470: {BranchTee_MVGrid_239_7: {'type': 'line',\n", - " 'line': Line_2390065}},\n", - " LVStation_417987: {BranchTee_MVGrid_239_27: {'type': 'line',\n", - " 'line': Line_2390217}},\n", - " LVStation_119612: {GeneratorFluctuating_839364: {'type': 'line',\n", - " 'line': Line_2390007},\n", - " BranchTee_MVGrid_239_38: {'type': 'line', 'line': Line_2390024}},\n", - " LVStation_119613: {LVStation_119703: {'type': 'line', 'line': Line_2390025},\n", - " BranchTee_MVGrid_239_38: {'type': 'line', 'line': Line_2390026}},\n", - " LVStation_119701: {LVStation_119702: {'type': 'line', 'line': Line_2390031},\n", - " BranchTee_MVGrid_239_40: {'type': 'line', 'line': Line_2390032}},\n", - " LVStation_119702: {LVStation_119701: {'type': 'line', 'line': Line_2390031},\n", - " LVStation_515314: {'type': 'line', 'line': Line_2390033}},\n", - " LVStation_119703: {LVStation_119613: {'type': 'line', 'line': Line_2390025},\n", - " LVStation_119704: {'type': 'line', 'line': Line_2390034}},\n", - " LVStation_119704: {LVStation_119703: {'type': 'line', 'line': Line_2390034},\n", - " BranchTee_MVGrid_239_40: {'type': 'line', 'line': Line_2390035}},\n", - " LVStation_120038: {BranchTee_MVGrid_239_93: {'type': 'line',\n", - " 'line': Line_2390057}},\n", - " LVStation_120555: {BranchTee_MVGrid_239_47: {'type': 'line',\n", - " 'line': Line_2390066},\n", - " BranchTee_MVGrid_239_9: {'type': 'line', 'line': Line_2390067}},\n", - " LVStation_500916: {BranchTee_MVGrid_239_47: {'type': 'line',\n", - " 'line': Line_2390235}},\n", - " LVStation_418547: {LVStation_496409: {'type': 'line', 'line': Line_2390222}},\n", - " LVStation_121286: {BranchTee_MVGrid_239_52: {'type': 'line',\n", - " 'line': Line_2390082}},\n", - " LVStation_121287: {LVStation_120388: {'type': 'line', 'line': Line_2390060},\n", - " BranchTee_MVGrid_239_52: {'type': 'line', 'line': Line_2390083}},\n", - " LVStation_121288: {LVStation_121289: {'type': 'line', 'line': Line_2390084},\n", - " BranchTee_MVGrid_239_52: {'type': 'line', 'line': Line_2390085}},\n", - " LVStation_121289: {LVStation_121288: {'type': 'line', 'line': Line_2390084},\n", - " BranchTee_MVGrid_239_14: {'type': 'line', 'line': Line_2390086}},\n", - " LVStation_121741: {LVStation_418449: {'type': 'line', 'line': Line_2390091},\n", - " BranchTee_MVGrid_239_55: {'type': 'line', 'line': Line_2390092}},\n", - " LVStation_121742: {BranchTee_MVGrid_239_54: {'type': 'line',\n", - " 'line': Line_2390093},\n", - " BranchTee_MVGrid_239_55: {'type': 'line', 'line': Line_2390094}},\n", - " LVStation_121743: {BranchTee_MVGrid_239_54: {'type': 'line',\n", - " 'line': Line_2390095}},\n", - " LVStation_496409: {LVStation_418547: {'type': 'line', 'line': Line_2390222},\n", - " BranchTee_MVGrid_239_55: {'type': 'line', 'line': Line_2390233}},\n", - " LVStation_416983: {BranchTee_MVGrid_239_17: {'type': 'line',\n", - " 'line': Line_2390209}},\n", - " LVStation_121878: {LVStation_122124: {'type': 'line', 'line': Line_2390097},\n", - " MVStation_239: {'type': 'line', 'line': Line_2390098}},\n", - " LVStation_121879: {BranchTee_MVGrid_239_21: {'type': 'line',\n", - " 'line': Line_2390099},\n", - " BranchTee_MVGrid_239_56: {'type': 'line', 'line': Line_2390100}},\n", - " LVStation_121880: {LVStation_496410: {'type': 'line', 'line': Line_2390101},\n", - " BranchTee_MVGrid_239_56: {'type': 'line', 'line': Line_2390102},\n", - " MVStation_239: {'type': 'line', 'line': Line_2390103}},\n", - " LVStation_496410: {LVStation_121880: {'type': 'line', 'line': Line_2390101}},\n", - " LVStation_121915: {LVStation_121918: {'type': 'line', 'line': Line_2390104},\n", - " BranchTee_MVGrid_239_57: {'type': 'line', 'line': Line_2390105}},\n", - " LVStation_121916: {LVStation_121917: {'type': 'line', 'line': Line_2390106},\n", - " BranchTee_MVGrid_239_57: {'type': 'line', 'line': Line_2390107}},\n", - " LVStation_121917: {LVStation_121916: {'type': 'line', 'line': Line_2390106}},\n", - " LVStation_121918: {LVStation_121318: {'type': 'line', 'line': Line_2390089},\n", - " LVStation_121915: {'type': 'line', 'line': Line_2390104}},\n", - " LVStation_121919: {BranchTee_MVGrid_239_19: {'type': 'line',\n", - " 'line': Line_2390108},\n", - " BranchTee_MVGrid_239_57: {'type': 'line', 'line': Line_2390109}},\n", - " LVStation_416589: {BranchTee_MVGrid_239_19: {'type': 'line',\n", - " 'line': Line_2390206}},\n", - " LVStation_122400: {BranchTee_MVGrid_239_21: {'type': 'line',\n", - " 'line': Line_2390128},\n", - " BranchTee_MVGrid_239_62: {'type': 'line', 'line': Line_2390129}},\n", - " LVStation_122401: {BranchTee_MVGrid_239_62: {'type': 'line',\n", - " 'line': Line_2390130}},\n", - " LVStation_122696: {BranchTee_MVGrid_239_64: {'type': 'line',\n", - " 'line': Line_2390135}},\n", - " LVStation_122697: {BranchTee_MVGrid_239_66: {'type': 'line',\n", - " 'line': Line_2390136}},\n", - " LVStation_122698: {LVStation_418244: {'type': 'line', 'line': Line_2390137},\n", - " BranchTee_MVGrid_239_65: {'type': 'line', 'line': Line_2390138}},\n", - " LVStation_122699: {BranchTee_MVGrid_239_65: {'type': 'line',\n", - " 'line': Line_2390139}},\n", - " LVStation_123655: {BranchTee_MVGrid_239_90: {'type': 'line',\n", - " 'line': Line_2390140}},\n", - " LVStation_124010: {BranchTee_MVGrid_239_67: {'type': 'line',\n", - " 'line': Line_2390141}},\n", - " LVStation_124011: {LVStation_124109: {'type': 'line', 'line': Line_2390142},\n", - " BranchTee_MVGrid_239_67: {'type': 'line', 'line': Line_2390143}},\n", - " LVStation_124109: {LVStation_124011: {'type': 'line', 'line': Line_2390142},\n", - " BranchTee_MVGrid_239_69: {'type': 'line', 'line': Line_2390147}},\n", - " LVStation_124110: {BranchTee_MVGrid_239_17: {'type': 'line',\n", - " 'line': Line_2390148},\n", - " BranchTee_MVGrid_239_69: {'type': 'line', 'line': Line_2390149}},\n", - " LVStation_124111: {BranchTee_MVGrid_239_36: {'type': 'line',\n", - " 'line': Line_2390150},\n", - " BranchTee_MVGrid_239_69: {'type': 'line', 'line': Line_2390151}},\n", - " LVStation_417936: {LVStation_124902: {'type': 'line', 'line': Line_2390157},\n", - " BranchTee_MVGrid_239_26: {'type': 'line', 'line': Line_2390216}},\n", - " LVStation_124902: {LVStation_417936: {'type': 'line', 'line': Line_2390157}},\n", - " LVStation_416175: {LVStation_119890: {'type': 'line', 'line': Line_2390039}},\n", - " LVStation_125210: {BranchTee_MVGrid_239_75: {'type': 'line',\n", - " 'line': Line_2390166},\n", - " BranchTee_MVGrid_239_88: {'type': 'line', 'line': Line_2390167}},\n", - " LVStation_125211: {LVStation_125212: {'type': 'line', 'line': Line_2390168},\n", - " BranchTee_MVGrid_239_75: {'type': 'line', 'line': Line_2390169}},\n", - " LVStation_125212: {LVStation_125211: {'type': 'line', 'line': Line_2390168}},\n", - " LVStation_125213: {LVStation_125216: {'type': 'line', 'line': Line_2390170},\n", - " BranchTee_MVGrid_239_77: {'type': 'line', 'line': Line_2390171}},\n", - " LVStation_125214: {BranchTee_MVGrid_239_76: {'type': 'line',\n", - " 'line': Line_2390172},\n", - " BranchTee_MVGrid_239_77: {'type': 'line', 'line': Line_2390173}},\n", - " LVStation_125215: {BranchTee_MVGrid_239_76: {'type': 'line',\n", - " 'line': Line_2390174}},\n", - " LVStation_125216: {LVStation_125213: {'type': 'line', 'line': Line_2390170},\n", - " BranchTee_MVGrid_239_94: {'type': 'line', 'line': Line_2390175}},\n", - " LVStation_125217: {LVStation_419605: {'type': 'line', 'line': Line_2390176},\n", - " LVStation_503036: {'type': 'line', 'line': Line_2390177}},\n", - " LVStation_503036: {LVStation_125217: {'type': 'line', 'line': Line_2390177},\n", - " BranchTee_MVGrid_239_25: {'type': 'line', 'line': Line_2390237}},\n", - " LVStation_125269: {BranchTee_MVGrid_239_3: {'type': 'line',\n", - " 'line': Line_2390181},\n", - " BranchTee_MVGrid_239_79: {'type': 'line', 'line': Line_2390182}},\n", - " LVStation_125267: {BranchTee_MVGrid_239_78: {'type': 'line',\n", - " 'line': Line_2390178}},\n", - " LVStation_125268: {BranchTee_MVGrid_239_78: {'type': 'line',\n", - " 'line': Line_2390179},\n", - " BranchTee_MVGrid_239_79: {'type': 'line', 'line': Line_2390180}},\n", - " LVStation_120898: {BranchTee_MVGrid_239_11: {'type': 'line',\n", - " 'line': Line_2390076},\n", - " BranchTee_MVGrid_239_50: {'type': 'line', 'line': Line_2390077}},\n", - " LVStation_120899: {BranchTee_MVGrid_239_50: {'type': 'line',\n", - " 'line': Line_2390078}},\n", - " LVStation_139149: {LVStation_138585: {'type': 'line', 'line': Line_2390183},\n", - " BranchTee_MVGrid_239_31: {'type': 'line', 'line': Line_2390189}},\n", - " LVStation_139150: {LVStation_139151: {'type': 'line', 'line': Line_2390190},\n", - " BranchTee_MVGrid_239_34: {'type': 'line', 'line': Line_2390191},\n", - " BranchTee_MVGrid_239_81: {'type': 'line', 'line': Line_2390192}},\n", - " LVStation_139151: {LVStation_139150: {'type': 'line', 'line': Line_2390190},\n", - " LVStation_139152: {'type': 'line', 'line': Line_2390193}},\n", - " LVStation_139152: {LVStation_139151: {'type': 'line', 'line': Line_2390193}},\n", - " LVStation_417909: {LVStation_125017: {'type': 'line', 'line': Line_2390164},\n", - " BranchTee_MVGrid_239_27: {'type': 'line', 'line': Line_2390215}},\n", - " LVStation_124085: {BranchTee_MVGrid_239_23: {'type': 'line',\n", - " 'line': Line_2390144},\n", - " BranchTee_MVGrid_239_68: {'type': 'line', 'line': Line_2390145}},\n", - " LVStation_124086: {BranchTee_MVGrid_239_68: {'type': 'line',\n", - " 'line': Line_2390146}},\n", - " LVStation_124581: {LVStation_124582: {'type': 'line', 'line': Line_2390153}},\n", - " LVStation_124582: {LVStation_124581: {'type': 'line', 'line': Line_2390153},\n", - " BranchTee_MVGrid_239_71: {'type': 'line', 'line': Line_2390154}},\n", - " LVStation_124583: {LVStation_120585: {'type': 'line', 'line': Line_2390068},\n", - " LVStation_124584: {'type': 'line', 'line': Line_2390155},\n", - " BranchTee_MVGrid_239_71: {'type': 'line', 'line': Line_2390156}},\n", - " LVStation_124584: {LVStation_124583: {'type': 'line', 'line': Line_2390155}},\n", - " LVStation_498758: {BranchTee_MVGrid_239_70: {'type': 'line',\n", - " 'line': Line_2390234}},\n", - " MVStation_239: {GeneratorFluctuating_878609: {'type': 'line',\n", - " 'line': Line_2390009},\n", - " GeneratorFluctuating_878611: {'type': 'line', 'line': Line_2390010},\n", - " GeneratorFluctuating_878614: {'type': 'line', 'line': Line_2390011},\n", - " GeneratorFluctuating_878615: {'type': 'line', 'line': Line_2390012},\n", - " GeneratorFluctuating_878875: {'type': 'line', 'line': Line_2390018},\n", - " GeneratorFluctuating_878950: {'type': 'line', 'line': Line_2390019},\n", - " LVStation_121878: {'type': 'line', 'line': Line_2390098},\n", - " LVStation_121880: {'type': 'line', 'line': Line_2390103},\n", - " LVStation_122076: {'type': 'line', 'line': Line_2390114},\n", - " LVStation_485974: {'type': 'line', 'line': Line_2390229},\n", - " BranchTee_MVGrid_239_11: {'type': 'line', 'line': Line_2390243},\n", - " BranchTee_MVGrid_239_14: {'type': 'line', 'line': Line_2390245},\n", - " BranchTee_MVGrid_239_16: {'type': 'line', 'line': Line_2390249},\n", - " BranchTee_MVGrid_239_23: {'type': 'line', 'line': Line_2390257},\n", - " BranchTee_MVGrid_239_2: {'type': 'line', 'line': Line_2390266},\n", - " BranchTee_MVGrid_239_92: {'type': 'line', 'line': Line_2390286}},\n", - " MVDisconnectingPoint_1: {BranchTee_MVGrid_239_1: {'line': Line_2390252,\n", - " 'type': 'line'}},\n", - " MVDisconnectingPoint_2: {BranchTee_MVGrid_239_2: {'line': Line_2390265,\n", - " 'type': 'line'}},\n", - " MVDisconnectingPoint_3: {LVStation_118323: {'line': Line_2390022,\n", - " 'type': 'line'}},\n", - " MVDisconnectingPoint_4: {BranchTee_MVGrid_239_75: {'line': Line_2390284,\n", - " 'type': 'line'}},\n", - " MVDisconnectingPoint_5: {BranchTee_MVGrid_239_83: {'line': Line_2390197,\n", - " 'type': 'line'}}}" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# get a dictionary of all lines in the mv grid\n", - "edisgo.network.mv_grid.graph.edge" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The dictionary you got should look something like that:\n", - "```python\n", - "{Generator_x: {BranchTee_y: {'type': 'line', 'line': Line_1}},\n", - " BranchTee_y: {\n", - " Generator_x: {'type': 'line', 'line': Line_1},\n", - " BranchTee_z: {'type': 'line', 'line': Line_2}}\n", - "```\n", - "\n", - "That means that Generator_x is connected to BranchTee_y by Line_1 and BranchTee_y is also connected to BranchTee_z by Line_2. Line_1 and Line_2 are [Line](http://edisgo.readthedocs.io/en/dev/api/edisgo.grid.html#edisgo.grid.components.Line) objects containig all important information about the line, such as length, equipment type, and geometry. Accessing this information can for example be done as follows:" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0.3681789122707058" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "edge_dictionary = edisgo.network.mv_grid.graph.edge\n", - "# get random line\n", - "line = edge_dictionary.popitem()[1].popitem()[1]['line']\n", - "# get line length\n", - "line.length" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now let's have a look at all the **nodes**." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[GeneratorFluctuating_839361,\n", - " GeneratorFluctuating_839362,\n", - " GeneratorFluctuating_839363,\n", - " GeneratorFluctuating_839364,\n", - " GeneratorFluctuating_878450,\n", - " GeneratorFluctuating_878583,\n", - " GeneratorFluctuating_878609,\n", - " GeneratorFluctuating_878611,\n", - " GeneratorFluctuating_878614,\n", - " GeneratorFluctuating_878615]" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# get a list of all nodes (stations, generators, loads, branch tees)\n", - "# here, only the first 10 nodes are displayed\n", - "edisgo.network.mv_grid.graph.nodes()[:10]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can also filter for certain kinds of nodes, e.g. generators..." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[GeneratorFluctuating_839361,\n", - " GeneratorFluctuating_839362,\n", - " GeneratorFluctuating_839363,\n", - " GeneratorFluctuating_839364,\n", - " GeneratorFluctuating_878450,\n", - " GeneratorFluctuating_878583,\n", - " GeneratorFluctuating_878609,\n", - " GeneratorFluctuating_878611,\n", - " GeneratorFluctuating_878614,\n", - " GeneratorFluctuating_878615,\n", - " GeneratorFluctuating_878862,\n", - " GeneratorFluctuating_878863,\n", - " GeneratorFluctuating_878864,\n", - " GeneratorFluctuating_878865,\n", - " GeneratorFluctuating_878866,\n", - " GeneratorFluctuating_878867,\n", - " GeneratorFluctuating_878875,\n", - " GeneratorFluctuating_878950,\n", - " GeneratorFluctuating_878963]" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# get a list of all generators in the mv grid\n", - "edisgo.network.mv_grid.graph.nodes_by_attribute('generator')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "... or get a list of all lv grids." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[LVGrid_122408,\n", - " LVGrid_485974,\n", - " LVGrid_138585,\n", - " LVGrid_119895,\n", - " LVGrid_119896,\n", - " LVGrid_119889,\n", - " LVGrid_119890,\n", - " LVGrid_119891,\n", - " LVGrid_119892,\n", - " LVGrid_119893]" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# get a list of all lv grids\n", - "# here, only the first 10 lv grids are displayed\n", - "list(edisgo.network.mv_grid.lv_grids)[:10]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Future generator capacities \n", - "\n", - "In the open_eGo project we developed two future scenarios, the 'NEP 2035' and the 'ego 100' scenario. The 'NEP 2035' scenario closely follows the B2-Scenario 2035 from the German network developement plan (Netzentwicklungsplan NEP) 2015. The share of renewables is 65.8%, electricity demand is assumed to stay the same as in the status quo. The 'ego 100' scenario is based on the e-Highway 2050 scenario X-7 and assumes a share of renewables of 100% and again an equal electricity demand as in the status quo.\n", - "\n", - "As mentioned earlier, ding0 grids represent status quo networks and generator capacities. In order to analyse future scenarios the future generator park has to be imported." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "WARNING:root:Right now only solar and wind generators can be imported from the oedb.\n" - ] - }, - { - "ename": "ProgrammingError", - "evalue": "(psycopg2.ProgrammingError) relation \"model_draft.ego_supply_res_powerplant_nep2035_mview\" does not exist\nLINE 2: FROM model_draft.ego_supply_res_powerplant_nep2035_mview \n ^\n [SQL: 'SELECT model_draft.ego_supply_res_powerplant_nep2035_mview.id, model_draft.ego_supply_res_powerplant_nep2035_mview.subst_id, model_draft.ego_supply_res_powerplant_nep2035_mview.la_id, model_draft.ego_supply_res_powerplant_nep2035_mview.mvlv_subst_id, model_draft.ego_supply_res_powerplant_nep2035_mview.electrical_capacity, model_draft.ego_supply_res_powerplant_nep2035_mview.generation_type, model_draft.ego_supply_res_powerplant_nep2035_mview.generation_subtype, model_draft.ego_supply_res_powerplant_nep2035_mview.voltage_level, ST_AsText(ST_Transform(model_draft.ego_supply_res_powerplant_nep2035_mview.rea_geom_new, %(ST_Transform_1)s)) AS geom, ST_AsText(ST_Transform(model_draft.ego_supply_res_powerplant_nep2035_mview.geom, %(ST_Transform_2)s)) AS geom_em \\nFROM model_draft.ego_supply_res_powerplant_nep2035_mview \\nWHERE model_draft.ego_supply_res_powerplant_nep2035_mview.subst_id = %(subst_id_1)s AND model_draft.ego_supply_res_powerplant_nep2035_mview.generation_type IN (%(generation_type_1)s, %(generation_type_2)s) AND model_draft.ego_supply_res_powerplant_nep2035_mview.voltage_level IN (%(voltage_level_1)s, %(voltage_level_2)s)'] [parameters: {'ST_Transform_1': 4326, 'ST_Transform_2': 4326, 'subst_id_1': 239, 'generation_type_1': 'solar', 'generation_type_2': 'wind', 'voltage_level_1': 4, 'voltage_level_2': 5}] (Background on this error at: http://sqlalche.me/e/f405)", - "output_type": "error", - "traceback": [ - "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m", - "\u001B[0;31mProgrammingError\u001B[0m Traceback (most recent call last)", - "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/base.py\u001B[0m in \u001B[0;36m_execute_context\u001B[0;34m(self, dialect, constructor, statement, parameters, *args)\u001B[0m\n\u001B[1;32m 1192\u001B[0m \u001B[0mparameters\u001B[0m\u001B[0;34m,\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m-> 1193\u001B[0;31m context)\n\u001B[0m\u001B[1;32m 1194\u001B[0m \u001B[0;32mexcept\u001B[0m \u001B[0mBaseException\u001B[0m \u001B[0;32mas\u001B[0m \u001B[0me\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", - "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/default.py\u001B[0m in \u001B[0;36mdo_execute\u001B[0;34m(self, cursor, statement, parameters, context)\u001B[0m\n\u001B[1;32m 506\u001B[0m \u001B[0;32mdef\u001B[0m \u001B[0mdo_execute\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mcursor\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mstatement\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mparameters\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mcontext\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0;32mNone\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m--> 507\u001B[0;31m \u001B[0mcursor\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mexecute\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mstatement\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mparameters\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 508\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n", - "\u001B[0;31mProgrammingError\u001B[0m: relation \"model_draft.ego_supply_res_powerplant_nep2035_mview\" does not exist\nLINE 2: FROM model_draft.ego_supply_res_powerplant_nep2035_mview \n ^\n", - "\nThe above exception was the direct cause of the following exception:\n", - "\u001B[0;31mProgrammingError\u001B[0m Traceback (most recent call last)", - "\u001B[0;32m\u001B[0m in \u001B[0;36m\u001B[0;34m()\u001B[0m\n\u001B[1;32m 1\u001B[0m \u001B[0;31m# Import generators\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 2\u001B[0m \u001B[0mscenario\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0;34m'nep2035'\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m----> 3\u001B[0;31m \u001B[0medisgo\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mimport_generators\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mgenerator_scenario\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0mscenario\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m", - "\u001B[0;32m~/virtualenvs/open_ego_notebook/git_repos/eDisGo/edisgo/grid/network.py\u001B[0m in \u001B[0;36mimport_generators\u001B[0;34m(self, generator_scenario)\u001B[0m\n\u001B[1;32m 326\u001B[0m \u001B[0mdata_source\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0;34m'oedb'\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 327\u001B[0m import_generators(network=self.network,\n\u001B[0;32m--> 328\u001B[0;31m data_source=data_source)\n\u001B[0m\u001B[1;32m 329\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 330\u001B[0m \u001B[0;32mdef\u001B[0m \u001B[0manalyze\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mmode\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0;32mNone\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", - "\u001B[0;32m~/virtualenvs/open_ego_notebook/git_repos/eDisGo/edisgo/data/import_data.py\u001B[0m in \u001B[0;36mimport_generators\u001B[0;34m(network, data_source, file)\u001B[0m\n\u001B[1;32m 984\u001B[0m logging.warning('Right now only solar and wind generators can be '\n\u001B[1;32m 985\u001B[0m 'imported from the oedb.')\n\u001B[0;32m--> 986\u001B[0;31m \u001B[0m_import_genos_from_oedb\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mnetwork\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0mnetwork\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 987\u001B[0m \u001B[0;32melif\u001B[0m \u001B[0mdata_source\u001B[0m \u001B[0;34m==\u001B[0m \u001B[0;34m'pypsa'\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 988\u001B[0m \u001B[0m_import_genos_from_pypsa\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mnetwork\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0mnetwork\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mfile\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0mfile\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", - "\u001B[0;32m~/virtualenvs/open_ego_notebook/git_repos/eDisGo/edisgo/data/import_data.py\u001B[0m in \u001B[0;36m_import_genos_from_oedb\u001B[0;34m(network)\u001B[0m\n\u001B[1;32m 1844\u001B[0m \u001B[0;31m#generators_conv_mv = _import_conv_generators()\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 1845\u001B[0m generators_res_mv, generators_res_lv = _import_res_generators(\n\u001B[0;32m-> 1846\u001B[0;31m types_condition)\n\u001B[0m\u001B[1;32m 1847\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 1848\u001B[0m \u001B[0;31m#generators_mv = generators_conv_mv.append(generators_res_mv)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", - "\u001B[0;32m~/virtualenvs/open_ego_notebook/git_repos/eDisGo/edisgo/data/import_data.py\u001B[0m in \u001B[0;36m_import_res_generators\u001B[0;34m(types_filter)\u001B[0m\n\u001B[1;32m 1089\u001B[0m generators_mv = pd.read_sql_query(generators_mv_sqla.statement,\n\u001B[1;32m 1090\u001B[0m \u001B[0msession\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mbind\u001B[0m\u001B[0;34m,\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m-> 1091\u001B[0;31m index_col='id')\n\u001B[0m\u001B[1;32m 1092\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 1093\u001B[0m \u001B[0;31m# define generators with unknown subtype as 'unknown'\u001B[0m\u001B[0;34m\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", - "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/pandas/io/sql.py\u001B[0m in \u001B[0;36mread_sql_query\u001B[0;34m(sql, con, index_col, coerce_float, params, parse_dates, chunksize)\u001B[0m\n\u001B[1;32m 330\u001B[0m return pandas_sql.read_query(\n\u001B[1;32m 331\u001B[0m \u001B[0msql\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mindex_col\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0mindex_col\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mparams\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0mparams\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mcoerce_float\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0mcoerce_float\u001B[0m\u001B[0;34m,\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m--> 332\u001B[0;31m parse_dates=parse_dates, chunksize=chunksize)\n\u001B[0m\u001B[1;32m 333\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 334\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n", - "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/pandas/io/sql.py\u001B[0m in \u001B[0;36mread_query\u001B[0;34m(self, sql, index_col, coerce_float, parse_dates, params, chunksize)\u001B[0m\n\u001B[1;32m 1085\u001B[0m \u001B[0margs\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0m_convert_params\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0msql\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mparams\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 1086\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m-> 1087\u001B[0;31m \u001B[0mresult\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mexecute\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;34m*\u001B[0m\u001B[0margs\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 1088\u001B[0m \u001B[0mcolumns\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0mresult\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mkeys\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 1089\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n", - "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/pandas/io/sql.py\u001B[0m in \u001B[0;36mexecute\u001B[0;34m(self, *args, **kwargs)\u001B[0m\n\u001B[1;32m 976\u001B[0m \u001B[0;32mdef\u001B[0m \u001B[0mexecute\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0;34m*\u001B[0m\u001B[0margs\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0;34m**\u001B[0m\u001B[0mkwargs\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 977\u001B[0m \u001B[0;34m\"\"\"Simple passthrough to SQLAlchemy connectable\"\"\"\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m--> 978\u001B[0;31m \u001B[0;32mreturn\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mconnectable\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mexecute\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0;34m*\u001B[0m\u001B[0margs\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0;34m**\u001B[0m\u001B[0mkwargs\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 979\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 980\u001B[0m def read_table(self, table_name, index_col=None, coerce_float=True,\n", - "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/base.py\u001B[0m in \u001B[0;36mexecute\u001B[0;34m(self, statement, *multiparams, **params)\u001B[0m\n\u001B[1;32m 2073\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 2074\u001B[0m \u001B[0mconnection\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mcontextual_connect\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mclose_with_result\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0;32mTrue\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m-> 2075\u001B[0;31m \u001B[0;32mreturn\u001B[0m \u001B[0mconnection\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mexecute\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mstatement\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0;34m*\u001B[0m\u001B[0mmultiparams\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0;34m**\u001B[0m\u001B[0mparams\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 2076\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 2077\u001B[0m \u001B[0;32mdef\u001B[0m \u001B[0mscalar\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mstatement\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0;34m*\u001B[0m\u001B[0mmultiparams\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0;34m**\u001B[0m\u001B[0mparams\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", - "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/base.py\u001B[0m in \u001B[0;36mexecute\u001B[0;34m(self, object, *multiparams, **params)\u001B[0m\n\u001B[1;32m 946\u001B[0m \u001B[0;32mraise\u001B[0m \u001B[0mexc\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mObjectNotExecutableError\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mobject\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 947\u001B[0m \u001B[0;32melse\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m--> 948\u001B[0;31m \u001B[0;32mreturn\u001B[0m \u001B[0mmeth\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mmultiparams\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mparams\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 949\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 950\u001B[0m \u001B[0;32mdef\u001B[0m \u001B[0m_execute_function\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mfunc\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mmultiparams\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mparams\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", - "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/sql/elements.py\u001B[0m in \u001B[0;36m_execute_on_connection\u001B[0;34m(self, connection, multiparams, params)\u001B[0m\n\u001B[1;32m 267\u001B[0m \u001B[0;32mdef\u001B[0m \u001B[0m_execute_on_connection\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mconnection\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mmultiparams\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mparams\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 268\u001B[0m \u001B[0;32mif\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0msupports_execution\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m--> 269\u001B[0;31m \u001B[0;32mreturn\u001B[0m \u001B[0mconnection\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m_execute_clauseelement\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mmultiparams\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mparams\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 270\u001B[0m \u001B[0;32melse\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 271\u001B[0m \u001B[0;32mraise\u001B[0m \u001B[0mexc\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mObjectNotExecutableError\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", - "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/base.py\u001B[0m in \u001B[0;36m_execute_clauseelement\u001B[0;34m(self, elem, multiparams, params)\u001B[0m\n\u001B[1;32m 1058\u001B[0m \u001B[0mcompiled_sql\u001B[0m\u001B[0;34m,\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 1059\u001B[0m \u001B[0mdistilled_params\u001B[0m\u001B[0;34m,\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m-> 1060\u001B[0;31m \u001B[0mcompiled_sql\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mdistilled_params\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 1061\u001B[0m )\n\u001B[1;32m 1062\u001B[0m \u001B[0;32mif\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m_has_events\u001B[0m \u001B[0;32mor\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mengine\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m_has_events\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", - "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/base.py\u001B[0m in \u001B[0;36m_execute_context\u001B[0;34m(self, dialect, constructor, statement, parameters, *args)\u001B[0m\n\u001B[1;32m 1198\u001B[0m \u001B[0mparameters\u001B[0m\u001B[0;34m,\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 1199\u001B[0m \u001B[0mcursor\u001B[0m\u001B[0;34m,\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m-> 1200\u001B[0;31m context)\n\u001B[0m\u001B[1;32m 1201\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 1202\u001B[0m \u001B[0;32mif\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m_has_events\u001B[0m \u001B[0;32mor\u001B[0m \u001B[0mself\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mengine\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m_has_events\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", - "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/base.py\u001B[0m in \u001B[0;36m_handle_dbapi_exception\u001B[0;34m(self, e, statement, parameters, cursor, context)\u001B[0m\n\u001B[1;32m 1411\u001B[0m util.raise_from_cause(\n\u001B[1;32m 1412\u001B[0m \u001B[0msqlalchemy_exception\u001B[0m\u001B[0;34m,\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m-> 1413\u001B[0;31m \u001B[0mexc_info\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 1414\u001B[0m )\n\u001B[1;32m 1415\u001B[0m \u001B[0;32melse\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", - "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/util/compat.py\u001B[0m in \u001B[0;36mraise_from_cause\u001B[0;34m(exception, exc_info)\u001B[0m\n\u001B[1;32m 201\u001B[0m \u001B[0mexc_type\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mexc_value\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mexc_tb\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0mexc_info\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 202\u001B[0m \u001B[0mcause\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0mexc_value\u001B[0m \u001B[0;32mif\u001B[0m \u001B[0mexc_value\u001B[0m \u001B[0;32mis\u001B[0m \u001B[0;32mnot\u001B[0m \u001B[0mexception\u001B[0m \u001B[0;32melse\u001B[0m \u001B[0;32mNone\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m--> 203\u001B[0;31m \u001B[0mreraise\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mtype\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mexception\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mexception\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mtb\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0mexc_tb\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mcause\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0mcause\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 204\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 205\u001B[0m \u001B[0;32mif\u001B[0m \u001B[0mpy3k\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", - "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/util/compat.py\u001B[0m in \u001B[0;36mreraise\u001B[0;34m(tp, value, tb, cause)\u001B[0m\n\u001B[1;32m 184\u001B[0m \u001B[0mvalue\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m__cause__\u001B[0m \u001B[0;34m=\u001B[0m \u001B[0mcause\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 185\u001B[0m \u001B[0;32mif\u001B[0m \u001B[0mvalue\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0m__traceback__\u001B[0m \u001B[0;32mis\u001B[0m \u001B[0;32mnot\u001B[0m \u001B[0mtb\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m--> 186\u001B[0;31m \u001B[0;32mraise\u001B[0m \u001B[0mvalue\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mwith_traceback\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mtb\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 187\u001B[0m \u001B[0;32mraise\u001B[0m \u001B[0mvalue\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 188\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n", - "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/base.py\u001B[0m in \u001B[0;36m_execute_context\u001B[0;34m(self, dialect, constructor, statement, parameters, *args)\u001B[0m\n\u001B[1;32m 1191\u001B[0m \u001B[0mstatement\u001B[0m\u001B[0;34m,\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 1192\u001B[0m \u001B[0mparameters\u001B[0m\u001B[0;34m,\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m-> 1193\u001B[0;31m context)\n\u001B[0m\u001B[1;32m 1194\u001B[0m \u001B[0;32mexcept\u001B[0m \u001B[0mBaseException\u001B[0m \u001B[0;32mas\u001B[0m \u001B[0me\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 1195\u001B[0m self._handle_dbapi_exception(\n", - "\u001B[0;32m~/virtualenvs/open_ego_notebook/lib/python3.6/site-packages/sqlalchemy/engine/default.py\u001B[0m in \u001B[0;36mdo_execute\u001B[0;34m(self, cursor, statement, parameters, context)\u001B[0m\n\u001B[1;32m 505\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 506\u001B[0m \u001B[0;32mdef\u001B[0m \u001B[0mdo_execute\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mcursor\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mstatement\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mparameters\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mcontext\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0;32mNone\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0;32m--> 507\u001B[0;31m \u001B[0mcursor\u001B[0m\u001B[0;34m.\u001B[0m\u001B[0mexecute\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mstatement\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mparameters\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n\u001B[0m\u001B[1;32m 508\u001B[0m \u001B[0;34m\u001B[0m\u001B[0m\n\u001B[1;32m 509\u001B[0m \u001B[0;32mdef\u001B[0m \u001B[0mdo_execute_no_params\u001B[0m\u001B[0;34m(\u001B[0m\u001B[0mself\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mcursor\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mstatement\u001B[0m\u001B[0;34m,\u001B[0m \u001B[0mcontext\u001B[0m\u001B[0;34m=\u001B[0m\u001B[0;32mNone\u001B[0m\u001B[0;34m)\u001B[0m\u001B[0;34m:\u001B[0m\u001B[0;34m\u001B[0m\u001B[0m\n", - "\u001B[0;31mProgrammingError\u001B[0m: (psycopg2.ProgrammingError) relation \"model_draft.ego_supply_res_powerplant_nep2035_mview\" does not exist\nLINE 2: FROM model_draft.ego_supply_res_powerplant_nep2035_mview \n ^\n [SQL: 'SELECT model_draft.ego_supply_res_powerplant_nep2035_mview.id, model_draft.ego_supply_res_powerplant_nep2035_mview.subst_id, model_draft.ego_supply_res_powerplant_nep2035_mview.la_id, model_draft.ego_supply_res_powerplant_nep2035_mview.mvlv_subst_id, model_draft.ego_supply_res_powerplant_nep2035_mview.electrical_capacity, model_draft.ego_supply_res_powerplant_nep2035_mview.generation_type, model_draft.ego_supply_res_powerplant_nep2035_mview.generation_subtype, model_draft.ego_supply_res_powerplant_nep2035_mview.voltage_level, ST_AsText(ST_Transform(model_draft.ego_supply_res_powerplant_nep2035_mview.rea_geom_new, %(ST_Transform_1)s)) AS geom, ST_AsText(ST_Transform(model_draft.ego_supply_res_powerplant_nep2035_mview.geom, %(ST_Transform_2)s)) AS geom_em \\nFROM model_draft.ego_supply_res_powerplant_nep2035_mview \\nWHERE model_draft.ego_supply_res_powerplant_nep2035_mview.subst_id = %(subst_id_1)s AND model_draft.ego_supply_res_powerplant_nep2035_mview.generation_type IN (%(generation_type_1)s, %(generation_type_2)s) AND model_draft.ego_supply_res_powerplant_nep2035_mview.voltage_level IN (%(voltage_level_1)s, %(voltage_level_2)s)'] [parameters: {'ST_Transform_1': 4326, 'ST_Transform_2': 4326, 'subst_id_1': 239, 'generation_type_1': 'solar', 'generation_type_2': 'wind', 'voltage_level_1': 4, 'voltage_level_2': 5}] (Background on this error at: http://sqlalche.me/e/f405)" - ] - } - ], - "source": [ - "# Import generators\n", - "scenario = 'nep2035'\n", - "edisgo.import_generators(generator_scenario=scenario)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can have a look at all generators again and compare it to the list of generators created earlier before the import of new generators." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "edisgo.network.mv_grid.graph.nodes_by_attribute('generator')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Grid reinforcement \n", - "\n", - "Now we can finally calculate grid expansion costs.\n", - "\n", - "The grid expansion methodology is based on the distribution grid study of dena [[1]](#[1]) and Baden-Wuerttemberg [[2]](#[2]). For now only a combined analysis of MV and LV grids is possible. The order grid expansion measures are conducted is as follows:\n", - "\n", - "* Reinforce transformers and lines due to over-loading issues\n", - "* Reinforce lines in MV grid due to over-voltage issues\n", - "* Reinforce lines in LV grid due to over-loading issues\n", - "* Reinforce transformers and lines due to over-loading issues\n", - "\n", - "Reinforcement of transformers and lines due to over-loading issues is performed twice, once in the beginning and again after fixing over-voltage problems, because the changed power flows after reinforcing the grid may lead to new over-loading issues. (For further explanation see the [documentation](http://edisgo.readthedocs.io/en/dev/features_in_detail.html#automatic-grid-expansion).)\n", - "\n", - "After each reinforcement step a non-linear power flow analyses is conducted using PyPSA. Let's do a power flow analysis before the reinforcement to see how many over-voltage issues there are." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO:pypsa.pf:Slack bus for sub-network 0 is Bus_MVStation_239\n", - "INFO:pypsa.pf:Performing non-linear load-flow on AC sub-network SubNetwork 0 for snapshots DatetimeIndex(['1970-01-01'], dtype='datetime64[ns]', freq='H')\n", - "INFO:pypsa.pf:Newton-Raphson solved in 3 iterations with error of 0.000001 in 0.519885 seconds\n" - ] - } - ], - "source": [ - "# Do non-linear power flow analysis with PyPSA\n", - "edisgo.analyze()" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Voltage levels for ['GeneratorFluctuating_839361', 'GeneratorFluctuating_839362', 'GeneratorFluctuating_839363', 'GeneratorFluctuating_839364', 'GeneratorFluctuating_878450', 'GeneratorFluctuating_878583', 'GeneratorFluctuating_878609', 'GeneratorFluctuating_878611', 'GeneratorFluctuating_878614', 'GeneratorFluctuating_878615', 'GeneratorFluctuating_878862', 'GeneratorFluctuating_878863', 'GeneratorFluctuating_878864', 'GeneratorFluctuating_878865', 'GeneratorFluctuating_878866', 'GeneratorFluctuating_878867', 'GeneratorFluctuating_878875', 'GeneratorFluctuating_878950', 'GeneratorFluctuating_878963', 'BranchTee_MVGrid_239_1', 'BranchTee_MVGrid_239_2', 'BranchTee_MVGrid_239_3', 'BranchTee_MVGrid_239_4', 'BranchTee_MVGrid_239_5', 'BranchTee_MVGrid_239_6', 'BranchTee_MVGrid_239_7', 'BranchTee_MVGrid_239_8', 'BranchTee_MVGrid_239_9', 'BranchTee_MVGrid_239_10', 'BranchTee_MVGrid_239_11', 'BranchTee_MVGrid_239_12', 'BranchTee_MVGrid_239_13', 'BranchTee_MVGrid_239_14', 'BranchTee_MVGrid_239_15', 'BranchTee_MVGrid_239_16', 'BranchTee_MVGrid_239_17', 'BranchTee_MVGrid_239_18', 'BranchTee_MVGrid_239_19', 'BranchTee_MVGrid_239_20', 'BranchTee_MVGrid_239_21', 'BranchTee_MVGrid_239_22', 'BranchTee_MVGrid_239_23', 'BranchTee_MVGrid_239_24', 'BranchTee_MVGrid_239_25', 'BranchTee_MVGrid_239_26', 'BranchTee_MVGrid_239_27', 'BranchTee_MVGrid_239_28', 'BranchTee_MVGrid_239_29', 'BranchTee_MVGrid_239_30', 'BranchTee_MVGrid_239_31', 'BranchTee_MVGrid_239_32', 'BranchTee_MVGrid_239_33', 'BranchTee_MVGrid_239_34', 'BranchTee_MVGrid_239_35', 'BranchTee_MVGrid_239_36', 'BranchTee_MVGrid_239_37', 'BranchTee_MVGrid_239_38', 'BranchTee_MVGrid_239_39', 'BranchTee_MVGrid_239_40', 'BranchTee_MVGrid_239_41', 'BranchTee_MVGrid_239_42', 'BranchTee_MVGrid_239_43', 'BranchTee_MVGrid_239_44', 'BranchTee_MVGrid_239_45', 'BranchTee_MVGrid_239_46', 'BranchTee_MVGrid_239_47', 'BranchTee_MVGrid_239_48', 'BranchTee_MVGrid_239_49', 'BranchTee_MVGrid_239_50', 'BranchTee_MVGrid_239_51', 'BranchTee_MVGrid_239_52', 'BranchTee_MVGrid_239_53', 'BranchTee_MVGrid_239_54', 'BranchTee_MVGrid_239_55', 'BranchTee_MVGrid_239_56', 'BranchTee_MVGrid_239_57', 'BranchTee_MVGrid_239_58', 'BranchTee_MVGrid_239_59', 'BranchTee_MVGrid_239_60', 'BranchTee_MVGrid_239_61', 'BranchTee_MVGrid_239_62', 'BranchTee_MVGrid_239_63', 'BranchTee_MVGrid_239_64', 'BranchTee_MVGrid_239_65', 'BranchTee_MVGrid_239_66', 'BranchTee_MVGrid_239_67', 'BranchTee_MVGrid_239_68', 'BranchTee_MVGrid_239_69', 'BranchTee_MVGrid_239_70', 'BranchTee_MVGrid_239_71', 'BranchTee_MVGrid_239_72', 'BranchTee_MVGrid_239_73', 'BranchTee_MVGrid_239_74', 'BranchTee_MVGrid_239_75', 'BranchTee_MVGrid_239_76', 'BranchTee_MVGrid_239_77', 'BranchTee_MVGrid_239_78', 'BranchTee_MVGrid_239_79', 'BranchTee_MVGrid_239_80', 'BranchTee_MVGrid_239_81', 'BranchTee_MVGrid_239_82', 'BranchTee_MVGrid_239_83', 'BranchTee_MVGrid_239_84', 'BranchTee_MVGrid_239_85', 'BranchTee_MVGrid_239_86', 'BranchTee_MVGrid_239_87', 'BranchTee_MVGrid_239_88', 'BranchTee_MVGrid_239_89', 'BranchTee_MVGrid_239_90', 'BranchTee_MVGrid_239_91', 'BranchTee_MVGrid_239_92', 'BranchTee_MVGrid_239_93', 'BranchTee_MVGrid_239_94', 'LVStation_122408', 'LVStation_485974', 'LVStation_138585', 'LVStation_119895', 'LVStation_119896', 'LVStation_119889', 'LVStation_119890', 'LVStation_119891', 'LVStation_119892', 'LVStation_119893', 'LVStation_119894', 'LVStation_119897', 'LVStation_119898', 'LVStation_119899', 'LVStation_119900', 'LVStation_119901', 'LVStation_417530', 'LVStation_419885', 'LVStation_121940', 'LVStation_121941', 'LVStation_122426', 'LVStation_122480', 'LVStation_418254', 'LVStation_419605', 'LVStation_416441', 'LVStation_418546', 'LVStation_416244', 'LVStation_417898', 'LVStation_419795', 'LVStation_120737', 'LVStation_120736', 'LVStation_120738', 'LVStation_120942', 'LVStation_120943', 'LVStation_122230', 'LVStation_122231', 'LVStation_418237', 'LVStation_416449', 'LVStation_417550', 'LVStation_139107', 'LVStation_120585', 'LVStation_417276', 'LVStation_122520', 'LVStation_419726', 'LVStation_121776', 'LVStation_419327', 'LVStation_417734', 'LVStation_125015', 'LVStation_125016', 'LVStation_125017', 'LVStation_500931', 'LVStation_418244', 'LVStation_120411', 'LVStation_121317', 'LVStation_121318', 'LVStation_416815', 'LVStation_139104', 'LVStation_139105', 'LVStation_139106', 'LVStation_139192', 'LVStation_119903', 'LVStation_119904', 'LVStation_418449', 'LVStation_419079', 'LVStation_119697', 'LVStation_119698', 'LVStation_122076', 'LVStation_122077', 'LVStation_122078', 'LVStation_122124', 'LVStation_122125', 'LVStation_122122', 'LVStation_122123', 'LVStation_124543', 'LVStation_124911', 'LVStation_124910', 'LVStation_139183', 'LVStation_139184', 'LVStation_139185', 'LVStation_139186', 'LVStation_139187', 'LVStation_139188', 'LVStation_139189', 'LVStation_488816', 'LVStation_490252', 'LVStation_490253', 'LVStation_118322', 'LVStation_118323', 'LVStation_118324', 'LVStation_515314', 'LVStation_120387', 'LVStation_120388', 'LVStation_120389', 'LVStation_120390', 'LVStation_120853', 'LVStation_511325', 'LVStation_120470', 'LVStation_417987', 'LVStation_119612', 'LVStation_119613', 'LVStation_119701', 'LVStation_119702', 'LVStation_119703', 'LVStation_119704', 'LVStation_120038', 'LVStation_120555', 'LVStation_500916', 'LVStation_418547', 'LVStation_121286', 'LVStation_121287', 'LVStation_121288', 'LVStation_121289', 'LVStation_121741', 'LVStation_121742', 'LVStation_121743', 'LVStation_496409', 'LVStation_416983', 'LVStation_121878', 'LVStation_121879', 'LVStation_121880', 'LVStation_496410', 'LVStation_121915', 'LVStation_121916', 'LVStation_121917', 'LVStation_121918', 'LVStation_121919', 'LVStation_416589', 'LVStation_122400', 'LVStation_122401', 'LVStation_122696', 'LVStation_122697', 'LVStation_122698', 'LVStation_122699', 'LVStation_123655', 'LVStation_124010', 'LVStation_124011', 'LVStation_124109', 'LVStation_124110', 'LVStation_124111', 'LVStation_417936', 'LVStation_124902', 'LVStation_416175', 'LVStation_125210', 'LVStation_125211', 'LVStation_125212', 'LVStation_125213', 'LVStation_125214', 'LVStation_125215', 'LVStation_125216', 'LVStation_125217', 'LVStation_503036', 'LVStation_125269', 'LVStation_125267', 'LVStation_125268', 'LVStation_120898', 'LVStation_120899', 'LVStation_139149', 'LVStation_139150', 'LVStation_139151', 'LVStation_139152', 'LVStation_417909', 'LVStation_124085', 'LVStation_124086', 'LVStation_124581', 'LVStation_124582', 'LVStation_124583', 'LVStation_124584', 'LVStation_498758', 'MVStation_239', 'MVDisconnectingPoint_1', 'MVDisconnectingPoint_2', 'MVDisconnectingPoint_3', 'MVDisconnectingPoint_4', 'MVDisconnectingPoint_5'] are not returned from PFA\n" - ] - }, - { - "data": { - "text/html": [ - "
    \n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    1970-01-01
    \n", - "
    " - ], - "text/plain": [ - "Empty DataFrame\n", - "Columns: []\n", - "Index: [1970-01-01 00:00:00]" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# get voltage at each node from power-flow analysis results\n", - "v_mag_pu_pfa = edisgo.network.results.v_res(nodes=edisgo.network.mv_grid.graph.nodes())\n", - "# set maximum allowed voltage deviation to 10%\n", - "max_v_dev = 0.1\n", - "# find all nodes with a node voltage deviation greater the allowed voltage deviation\n", - "v_mag_pu_pfa[(v_mag_pu_pfa > (1 + max_v_dev))] - 1" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Reinforcement is invoked doing the following:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Do grid reinforcement\n", - "edisgo.reinforce()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's check for over-voltage issues again:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# get voltage at each node from power-flow analysis results\n", - "v_mag_pu_pfa = edisgo.network.results.v_res(nodes=edisgo.network.mv_grid.graph.nodes())\n", - "# set maximum allowed voltage deviation to 10%\n", - "max_v_dev = 0.1\n", - "# find all nodes with a node voltage deviation greater the allowed voltage deviation\n", - "v_mag_pu_pfa[(v_mag_pu_pfa > (1 + max_v_dev))] - 1" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Evaluate results \n", - "\n", - "Above we already saw how to access voltage results from the power flow analysis. All results are stored in the [Results](http://edisgo.readthedocs.io/en/dev/api/edisgo.grid.html#edisgo.grid.network.Results) object and can be accessed through\n", - "```python\n", - "edisgo.network.results\n", - "```\n", - "\n", - "All changes in the grid conducted during the grid reinforcement, such as removed and new lines and new transformers, can be viewed as follows:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "edisgo.network.results.equipment_changes" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can also retrieve grid expansion costs through:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "costs = edisgo.network.results.grid_expansion_costs" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you are interested in accumulated costs you could group them like that:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# group costs by type\n", - "costs_grouped = costs.groupby(['type']).sum()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "An overview of the assumptions used to calculate grid expansion costs can be found in the [documentation]( http://edisgo.readthedocs.io/en/dev/features_in_detail.html#grid-expansion-costs)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now let's compare the grid expansion costs for the 'NEP 2035' scenario with grid expansion costs for the 'ego 100' scenario. Therefore, we first have to setup the new scenario and calculate grid expansion costs." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# initialize new EDisGo object with 'ego 100' scenario\n", - "edisgo_ego100 = EDisGo(ding0_grid=ding0_grid,\n", - " worst_case_analysis=worst_case_analysis,\n", - " generator_scenario='ego100')\n", - "# conduct grid reinforcement\n", - "edisgo_ego100.reinforce()\n", - "# get grouped costs\n", - "costs_grouped_ego100 = edisgo_ego100.network.results.grid_expansion_costs.groupby(['type']).sum()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# plot" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## References \n", - "\n", - " [1] A.C. Agricola et al.: dena-Verteilnetzstudie: Ausbau- und Innovationsbedarf der Stromverteilnetze in Deutschland bis 2030. 2012.\n", - "\n", - " [2] C. Rehtanz et al.: Verteilnetzstudie für das Land Baden-Württemberg, ef.Ruhr GmbH, 2017." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/ego/examples/tutorials/etrago_OpenMod_Zuerich18.ipynb b/ego/examples/tutorials/etrago_OpenMod_Zuerich18.ipynb deleted file mode 100644 index 0c7f115f..00000000 --- a/ego/examples/tutorials/etrago_OpenMod_Zuerich18.ipynb +++ /dev/null @@ -1,706 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\"EUF\"\n", - "\"HSF\"\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "__copyright__ = \"Zentrum für nachhaltige Energiesysteme Flensburg\"\n", - "__license__ = \"GNU Affero General Public License Version 3 (AGPL-3.0)\"\n", - "__url__ = \"https://github.com/openego/data_processing/blob/master/LICENSE\"\n", - "__author__ = \"wolfbunke, ulfmueller\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "#
    OpenMod Workshop Zürich 2018
    \n", - "\n", - "## Open source tools for cross-grid-level electricity grid optimization developed in the open_eGo project - Learn-a-thon\n", - "\n", - "****\n", - "\n", - "### Learn more about\n", - "\n", - "\n", - "* __[open_eGo Project Webpage](https://openegoproject.wordpress.com/)__\n", - "* __[eTraGo Tool Documentation](http://etrago.readthedocs.io/en/latest/index.html)__ \n", - "* __[oedb Tutorials](http://oep.iks.cs.ovgu.de/dataedit/)__ How to use the OpenEnergy Database\n", - "* __[OpenMod Forum](https://forum.openmod-initiative.org/t/learn-a-thon-using-tools-for-cross-grid-level-electricity-grid-optimization-developed-in-the-open-ego-project/856)__ " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\"HSF\"\n", - "## Table of Contents \n", - "\n", - "\n", - "* [Getting started with eTraGo](#started)\n", - "* [LOPF Calculation of Germany and neighbours with 10 notes](#d-kmean10)\n", - "* [LOPF Calculation of Schleswig-Holstein](#shcalc)\n", - "* [Using snapshot clustering](#snapshot)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Getting started with eTraGo \n", - "\n", - "\n", - "The python package eTraGo provides a optimization of flexibility options for transmission grids based on PyPSA. A speciality in this context is that transmission grids are described by the 380, 220 and 110 kV in Germany. Conventionally the 110kV grid is part of the distribution grid. The integration of the transmission and ‘upper’ distribution grid is part of eTraGo.\n", - "\n", - "The focus of optimization are flexibility options with a special focus on energy storages and grid expansion measures.\n", - "\n", - "\n", - "\n", - "\n", - "## Installation \n", - "\n", - "Please, find more information on the [README.md](https://github.com/openego/eGo/tree/features/tutorial/ego/examples/tutorials#etrago). \n", - "\n", - "\n", - "## Import eTraGo packages\n", - "\n", - "We are importing the [main function](https://github.com/openego/eTraGo/blob/dev/etrago/appl.py) of eTraGo and its database and plotting functions. \n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "import numpy as np\n", - "import pandas as pd\n", - "from numpy import genfromtxt\n", - "np.random.seed()\n", - "import time\n", - "import oedialect\n", - "\n", - "import os\n", - "\n", - "if not 'READTHEDOCS' in os.environ:\n", - " # Sphinx does not run this code.\n", - " # Do not import internal packages directly \n", - " from etrago.tools.io import NetworkScenario, results_to_oedb\n", - " from etrago.tools.plot import (plot_line_loading, plot_stacked_gen,\n", - " add_coordinates, curtailment, gen_dist,\n", - " storage_distribution,storage_expansion)\n", - " from etrago.tools.utilities import (load_shedding, data_manipulation_sh,\n", - " results_to_csv, parallelisation, pf_post_lopf, \n", - " loading_minimization, calc_line_losses, group_parallel_lines)\n", - " from etrago.cluster.networkclustering import busmap_from_psql, cluster_on_extra_high_voltage, kmean_clustering\n", - " from egoio.tools import db\n", - " from sqlalchemy.orm import sessionmaker\n", - " from etrago.appl import etrago\n", - " \n", - " " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# enable jupyter interactiv plotting\n", - "%matplotlib notebook\n", - "from ipywidgets import *\n", - "import matplotlib.pyplot as plt" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# LOPF Calculation of Germany and neighbours with 30 nodes \n", - "\n", - "\n", - "
    \n", - "[top](#toc)
    \n", - "\n", - "In this section we start our eTraGo calulation with an __Status Quo__ scenario of Germany an its electrical neighbours. For time and performents reasons we are useing the [k-mean](https://de.wikipedia.org/wiki/K-Means-Algorithmus) clustering [functionality of eTraGo](http://etrago.readthedocs.io/en/latest/api/etrago.cluster.html#etrago.cluster.networkclustering.kmean_clustering) and use $k=30$ nodes. For the same reason we choose the time period of __start_snapshot__ and __end_snapshot__ for a day with 24 hours of the scenario year. \n", - "\n", - "\n", - "### Make your calulation settings\n", - "\n", - "A detailed discription of the args python dictionary can be found under . \n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "args_k10 = {# Setup and Configuration:\n", - " 'db': 'oedb', # db session\n", - " 'gridversion': \"v0.2.11\", # None for model_draft or Version number (e.g. v0.2.11) for grid schema\n", - " 'method': 'lopf', # lopf or pf\n", - " 'pf_post_lopf': False, # state whether you want to perform a pf after a lopf simulation\n", - " 'start_snapshot': 4393, # 2.07.\n", - " 'end_snapshot' : 4400,\n", - " 'scn_name': 'NEP 2035', # state which scenario you want to run: Status Quo, NEP 2035, eGo100\n", - " 'solver': 'glpk', # glpk, cplex or gurobi\n", - " # Export options:\n", - " 'lpfile': False, # state if and where you want to save pyomo's lp file: False or /path/tofolder\n", - " 'results': False, # state if and where you want to save results as csv: False or /path/tofolder\n", - " 'export': False, # state if you want to export the results back to the database\n", - " # Settings: \n", - " 'storage_extendable':True, # state if you want storages to be installed at each node if necessary.\n", - " 'generator_noise':True, # state if you want to apply a small generator noise \n", - " 'reproduce_noise': False, # state if you want to use a predefined set of random noise for the given scenario. \n", - " # if so, provide path, e.g. 'noise_values.csv'\n", - " 'minimize_loading':False,\n", - " # Clustering:\n", - " 'k_mean_clustering': 30, # state if you want to perform a k-means clustering on the given network. \n", - " # State False or the value k (e.g. 20).\n", - " 'network_clustering': False, # state if you want to perform a clustering of HV buses to EHV buses.\n", - " # Simplifications:\n", - " 'parallelisation':False, # state if you want to run snapshots parallely.\n", - " 'skip_snapshots':False,\n", - " 'line_grouping': False, # state if you want to group lines running between the same buses.\n", - " 'branch_capacity_factor': 0.7, # globally extend or lower branch capacities\n", - " 'load_shedding':False, # meet the demand at very high cost; for debugging purposes.\n", - " 'comments':None }" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "# Start eTraGo calculation with args setting\n", - "# create network object which incluedes all input and output data\n", - "network = etrago(args_k10)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "# plot generation distripution\n", - "gen_dist(network, techs=None, snapshot=1,n_cols=3,gen_size=0.02)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "# plot stacked sum of nominal power for each generator type and timestep\n", - "#fig.set_size_inches(14,14)\n", - "# fix error in .../eGo/ego/examples/tutorials/src/etrago/etrago/tools/plot.py\n", - "# 'wind_offshore':'skyblue', wind_onshore':'skyblue',\n", - "plot_stacked_gen(network, resolution=\"MW\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "# make a line loading plot\n", - "# basemade einbauen für hintergrund länder\n", - "fig,ax = plt.subplots(1,1)\n", - "fig.set_size_inches(8,8)\n", - "plot_line_loading(network)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "# plot to show extendable storages, if expanded\n", - "storage_expansion(network)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "# get storage sizes in MW\n", - "network.storage_units.p_nom_opt.groupby(network.storage_units.carrier, axis=0).sum()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Marginal price per bus node" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# get snapshot\n", - "now = network.snapshots[2]\n", - "\n", - "fig,ax = plt.subplots(1,1)\n", - "fig.set_size_inches(6,4)\n", - "\n", - "network.plot(ax=ax,line_widths=pd.Series(0.5,network.lines.index))\n", - "plt.hexbin(network.buses.x, network.buses.y, \n", - " gridsize=20,\n", - " C=network.buses_t.marginal_price.loc[now],\n", - " cmap=plt.cm.jet)\n", - "\n", - "#for some reason the colorbar only works with graphs plt.plot\n", - "#and must be attached plt.colorbar\n", - "\n", - "cb = plt.colorbar()\n", - "cb.set_label('Locational Marginal Price (EUR/MWh)') " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Change the scnario \n", - "\n", - "* Set 'scn_name': to 'NEP 2035' and recalculate. \n", - "\n", - "\n", - "\n", - "****\n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# delete eTraGo object\n", - "#del network" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# LOPF Calculation of Schleswig-Holstein \n", - "\n", - "\n", - "
    \n", - "[top](#toc)
    \n", - "\n", - "\n", - "### Case Schleswig-Holstein\n", - "\n", - "The data set of Schleswing-Holstein is an extract of the main data set and works as an island. The power production and flows of the adjacent network areas are neglected. Therefore, the installed capacity and power production is very high.\n", - "\n", - "For our analysis we used serveral plotting options of eTraGo of [etrago.tools.plot](http://etrago.readthedocs.io/en/latest/api/etrago.tools.html#module-etrago.tools.plot).\n", - "\n", - "\n", - "### Make your settings\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "args_sh = {# Setup and Configuration:\n", - " 'db': 'oedb', # db session\n", - " 'gridversion': \"v0.2.11\", # None for model_draft or Version number (e.g. v0.2.11) for grid schema\n", - " 'method': 'lopf', # lopf or pf\n", - " 'pf_post_lopf': False, # state whether you want to perform a pf after a lopf simulation\n", - " 'start_snapshot': 4393, # 6552\n", - " 'end_snapshot' : 4394,\n", - " 'scn_name': 'SH NEP 2035', # state which scenario you want to run: Status Quo, NEP 2035, eGo100\n", - " 'solver': 'glpk', # glpk, cplex or gurobi\n", - " # Export options:\n", - " 'lpfile': False, # state if and where you want to save pyomo's lp file: False or /path/tofolder\n", - " 'results': False, # state if and where you want to save results as csv: False or /path/tofolder\n", - " 'export': False, # state if you want to export the results back to the database\n", - " # Settings: \n", - " 'storage_extendable':False, # state if you want storages to be installed at each node if necessary.\n", - " 'generator_noise':True, # state if you want to apply a small generator noise \n", - " 'reproduce_noise': False, # state if you want to use a predefined set of random noise for the given.\n", - " # scenario. if so, provide path, e.g. 'noise_values.csv'\n", - " 'minimize_loading':False,\n", - " # Clustering:\n", - " 'k_mean_clustering': False, # state if you want to perform a k-means clustering on the given network. \n", - " # State False or the value k (e.g. 20).\n", - " 'network_clustering': False, # state if you want to perform a clustering of HV buses to EHV buses.\n", - " # Simplifications:\n", - " 'parallelisation':False, # state if you want to run snapshots parallely.\n", - " 'skip_snapshots':False,\n", - " 'line_grouping': False, # state if you want to group lines running between the same buses.\n", - " 'branch_capacity_factor': 0.7, # globally extend or lower branch capacities\n", - " 'load_shedding':False, # meet the demand at very high cost; for debugging purposes.\n", - " 'comments':None }" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# Start eTraGo calculation with args setting\n", - "# create network object \n", - "network = etrago(args_sh)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# plot generation distripution\n", - "gen_dist(network, techs=None, snapshot=1,n_cols=3,gen_size=0.02)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# plot stacked sum of nominal power for each generator type and timestep\n", - "plot_stacked_gen(network, resolution=\"MW\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# plots\n", - "# make a line loading plot\n", - "fig,ax = plt.subplots(1,1)\n", - "fig.set_size_inches(12,10)\n", - "plot_line_loading(network)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# plot to show extendable storages\n", - "storage_expansion(network)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# curtailment per carrier / energy source\n", - "curtailment(network, carrier='wind_onshore')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# delete network object\n", - "del network" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Using pf after lopf Calculation of Germany and neighbours with 30 nodes \n", - "\n", - "
    \n", - "[top](#toc)
    \n", - "\n", - "\n", - "In order to compute the grid losses we add an power flow calculation after our liniar opf calculation by setting *pf_post_lopf = True*. \n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "args_lopf_pf = {# Setup and Configuration:\n", - " 'db': 'oedb', # db session\n", - " 'gridversion': \"v0.2.11\", # None for model_draft or Version number (e.g. v0.2.11) for grid schema\n", - " 'method': 'lopf', # lopf or pf\n", - " 'pf_post_lopf': True, # state whether you want to perform a pf after a lopf simulation\n", - " 'start_snapshot': 4393, \n", - " 'end_snapshot' : 4417,\n", - " 'scn_name': 'NEP 2035', # state which scenario you want to run: Status Quo, NEP 2035, eGo100\n", - " 'solver': 'glpk', # glpk, cplex or gurobi\n", - " # Export options:\n", - " 'lpfile': False, # state if and where you want to save pyomo's lp file: False or /path/tofolder\n", - " 'results': False, # state if and where you want to save results as csv: False or /path/tofolder\n", - " 'export': False, # state if you want to export the results back to the database\n", - " # Settings: \n", - " 'storage_extendable':False, # state if you want storages to be installed at each node if necessary.\n", - " 'generator_noise':True, # state if you want to apply a small generator noise \n", - " 'reproduce_noise': False, # state if you want to use a predefined set of random noise for the given.\n", - " # scenario. if so, provide path, e.g. 'noise_values.csv'\n", - " 'minimize_loading':False,\n", - " # Clustering:\n", - " 'k_mean_clustering': 30, # state if you want to perform a k-means clustering on the given network. \n", - " # State False or the value k (e.g. 20).\n", - " 'network_clustering': False, # state if you want to perform a clustering of HV buses to EHV buses.\n", - " # Simplifications:\n", - " 'parallelisation':False, # state if you want to run snapshots parallely.\n", - " 'skip_snapshots':False,\n", - " 'line_grouping': False, # state if you want to group lines running between the same buses.\n", - " 'branch_capacity_factor': 0.7, # globally extend or lower branch capacities\n", - " 'load_shedding':False, # meet the demand at very high cost; for debugging purposes.\n", - " 'comments':None }" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# Start eTraGo calculation with args setting\n", - "# create network object\n", - "network = etrago(args_lopf_pf)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# plot stacked sum of nominal power for each generator type and timestep\n", - "plot_stacked_gen(network, resolution=\"MW\")\n", - "#plt.close()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# plots\n", - "# make a line loading plot\n", - "fig,ax = plt.subplots(1,1)\n", - "fig.set_size_inches(8,8)\n", - "\n", - "plot_line_loading(network)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Calculate grid losses\n", - "\n", - "We are using the function [calc_line_losses(network)](http://etrago.readthedocs.io/en/latest/_modules/etrago/tools/utilities.html#calc_line_losses)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "\n", - "# calcualte total grid losses\n", - "calc_line_losses(network)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Plot line costs" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# Line losses\n", - "# calculate apparent power S = sqrt(p² + q²) [in MW]\n", - "s0_lines = ((network.lines_t.p0**2 + network.lines_t.q0**2).\\\n", - " apply(np.sqrt)) \n", - "\n", - "# calculate current I = S / U [in A]\n", - "i0_lines = np.multiply(s0_lines, 1000000) / np.multiply(network.lines.v_nom, 1000) \n", - "\n", - "# calculate losses per line and timestep network.lines_t.line_losses = I² * R [in MW]\n", - "network.lines_t.losses = np.divide(i0_lines**2 * network.lines.r, 1000000)\n", - "\n", - "# calculate total losses per line [in MW]\n", - "network.lines = network.lines.assign(losses=np.sum(network.lines_t.losses).values)\n", - "\n", - "# prepare plotting\n", - "timestep =1\n", - "cmap = plt.cm.jet\n", - "\n", - "fig,ax = plt.subplots(1,1)\n", - "fig.set_size_inches(6,4)\n", - "\n", - "# do the plotting\n", - "lc= network.plot(line_colors=network.lines.losses, line_cmap=cmap,\n", - " title=\"Line loading\", line_widths=0.55)\n", - "\n", - "cb = plt.colorbar(lc[1])\n", - "cb.set_label('Locational line losses in (EUR/MWh)')\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Plot the reactive power" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# get snapshot\n", - "now = network.snapshots[2]\n", - "#plot the reactive power\n", - "fig,ax = plt.subplots(1,1)\n", - "\n", - "fig.set_size_inches(6,6)\n", - "\n", - "q = network.buses_t.q.sum()#.loc[now]\n", - "\n", - "bus_colors = pd.Series(\"r\",network.buses.index)\n", - "bus_colors[q< 0.] = \"b\"\n", - "\n", - "\n", - "network.plot(bus_sizes=abs(q)*0.005,ax=ax,bus_colors=bus_colors,title=\"Reactive power feed-in (red=+ve, blue=-ve)\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Have a look into the Programm on Github\n", - "\n", - "* " - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.5.5" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/ego/examples/tutorials/grid_1476.png b/ego/examples/tutorials/grid_1476.png deleted file mode 100644 index cbef8115e5939a78c4c5d3b8c821a7236c74dfc9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 410425 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

    Q>j6WqQ!5-ZL1yE2d7He+^@Ls|vh3CXj`S z$R9s)=<3ZE0DuSwY0)+MK!Gp-(E4q>qrL0yryRsr>ErN~l3Wc^p>*tb4gG`~ql$rG zvThRLV9+*FlviF#DWzl>8FM!H3IMmAYEAC6VaPA#w`O4=CIaFy-nzG^%d0Of|HVh| zd;W7LhP`ppWTSBAwb#!-zIN)NYfCFVP-+Y<0pwFP)J$%^4=~wx_(CYZch$e2%~q?t zXGVgxENT~T4c-g82X6&!%3@@ry<)*TK-fW0p1@92tv~d>@B1G<@YAPG96NGo^_ge> z?17U=#F0^%cNJkqg%HN-vbSUNtN@0niik-{g@{qRU31Zz&E)#6;ZTTR)uRvlFFpHd zX;*s+~j9jy|w@EmuhSGqpK4}641tw;!GpRbs5($Pj^C`En1tXmvlY7SK{0gmr_E-;nsf=70}YRJjGWSll` zyMtPP|J99Y4sjW%kd;+C5 zb!!131ThbGlC^7%t()_o{mA{-FOB&UO3~TJZa?yMR~~xf)ucJD%lpDhVWu3cv@8oB zP>N8^5^2Le3nlW^to~}Bg_u<4-sTK3OSunkMHCzu)f8_>?4IU=S(9x{8F zus`z?Kl&X%@WcQ8_ndp<>yLoc>s3);m4%+Gz_MC`t`>wKohK%$tz<&*LWnZYrBuXh zlqkybrcVLjXt(kBiUHUr3a&J3o3`88&p(+3Jvu5}%XLTcgJ+7*d@;E&6I)}+CB`*+ zpcb3pg4-CW!<~*5+gZ8V*aH(ILUc!O1r1STq69JFmJ#YUyeb2Nj6zw6IALZXMF>G6 z6T>e0i8$L~g%2`@&kF%tg0~ta2W zL&}<4Zekrm;7mImMpU#8YI8Zh%0g1`^_jifHh&G{4t{IMuT|GKM(aI|x7WAlmJ=D< zycosyfT`n$eLv_&uY3K0Uwv;++o`U7-fs3bT2;B{bgiD)dE#9!Jp86t;${f|i0B>Hcoq}Q z_HJ@Gpq`OxauJPrU*i=Gs4RW!Prtb5sWWt3%SY3lg1`2B3gE5}+Q&MDXaMX#*hFLIIZ?ZTdk{iYEF25N7vPpb z+g5W!-(w;Xm`s34lOFGbFuUj{mj#$;3LW;`HndIwfDd^6roM8`ynNOE(I;B~AcbR9 zR}yQg%VfDRdCu18P&L|N9ivI=4TMd6RiQGo$UZjbS!vwGTRi~QAS@!X(7;@m3cPcv z+?uDQrftOz=FD*o78pPPFa#)!jRTb~M~R{VstI~fC}0i@Ax)Jwj(~zBW+A0irU+yO zmCo%0`kGo?-x)Lnph9J0LIqGN5cliV3d7~t-1jV10Yk?3PX_5c7HE=S{wEW7^ylOUrk25xldt!*qPJoyVPJU2 zFq&IUYB#CX2+q;wj@|CbP^Ss>vJ%&{>@x@-I-ZWPIIR@C?&19F-jMEu;R`pvZhNh{ zakJgqOz!c(6OvDy-FnNro_XYn>l5*t9Qz4Opjg#tk#i{Im_N`=*0bT!X#c}zKdW@# ziM(UMF!g5%zG9}d7~Qz~JgPX)hgmk@!;6h88{<1yAVF2#w-``JZG(5<3Pc1Huee)M z>cz&M1vrzhiXi|n!dh^u93vWo~zI^nQ+g03C zC_`T4`HQa}|L(u@PoI41QxBhOAfOTw09~G%I4LSB1Iy_Nu^#!dNGVQ1<%03_noGi##&F&HEW=LCP)Smj zMeyFaN}DL{Z;`LkZEuoxAms_ydh+N(VCIn@_VK1r$6b~+7FMckRH20g0N{c6Xj%;} zsat9SsT*71@yo2Mt8)XF>WX=8% z!gqPW0AR>Ye#PF$bGzVYj~mg^fx_meZFg}(m(b5 zv(H`ow%!n&gD0PgpL{9?*h4}@gl4DgEM#*_!?~5bJD+tHv+h#bo*&FDRRCU$+ZW?l z*($IW@a&RiD4w?#m;)@4L_-)DzJ`vG-)_W<{jSe3TfFaMaD2h=?BKo60dGY3rjo;vCm=LW~W<&#R+0;;j%Gx%}Qpng);#5il4 z=*Fd0rKR!JyfqCDjb>K`^}yNJwwj$@rNrTG2mz$zK;Wy| zf1_?vD}WdT3aoD*6yt|Yec)7;RWmfKP4hw`C<(!r2#7*N6J?W*{t63O8TSUf-p1v7 zZ+1s7LKaF%sGyAO?VGPC@n&Q6*>evZSW1TBkQz}ahDzI6a1e!*LdLBn#N7piGQ0b_ z@5C75{QG}<>(TT3L-xJLNBdvQA?%hKrSRzx%zQc&WvJ0M`qItD`t{4*c6$BR{1e~yUFTP%X1&07A>ZfS!&myFp+#A5i;2np6HlFyQ5U^xsm>=4i$dQE(uX=T4VOf)DV;XX0sWU5eFK z2`l5SzT}GW`)O6qN70>D6^B}OSW$cO@MWSXYQdyzBb$_icV}g`p zSBLt#E5ojGmArYSvvGal8PZkcKZd*BKXpEN@ zlf$pE9V)Y_rB3}HmpRv@?|P2_AQT7BL;w<;V=x>%Fr!d4b(AMOCjcnIsCHLyj+K&O z+^|DRDWyu7a)5|IkoQ@Z8EZoz8z-SE5Lqh2RQF`fQK5~4EJ(^mxl1c~;_zRUb->-mdbk{`wmzVV)$Z6ITPq(E$wNvR zWfva%+MTW2!5n?|*=Nq5-|t0V+od+DIXOxJtl@n8%LRZRgM4_7Z&Hg@I~?Ti(6m+_U~%Vbu^ZM@bJ0( z;dA*v_&)fLy(h=(cPV_4D;-Z52lp!F0l?T4V=~ygTC9aS0;XNS#2^ke>|Le|C}#>p z9sp~RRnl6lZ@+!s*ppqwvu#`fV6n4!tG{Q3BIC&S1^^JC{6hI^2a>mTUkCSuzwKT- z0XeY_Scn&4v^IS|7Uq%TkNm&s|NU6^q@B<|Jd2!*EKjSdifw0UJ_f{hJaOh7Pu%~T zzw?Fn{peqO`Pmzr+v)H9oAS=tRRH&$ zm^1Fo$?}Cu_L&!>?|9evqe#&0AOS#VY;==|fi?$lw3$SWVdYwSfA*RPx>|(=j64Et5;uGy7bJ78`vygRFS;{hC*%bbaCJ+2h#v4 zff0p7Wno4SB828qge0@75Y4%|Sc-^6I%qE}j|M$r&a+f%i()4-D@EW&qm^ePtxcW{ zBV)-|+C&UMM3TrCnT{KE^60C)H>@yZTU&Z?Wcig_{@KbT8%A2ulmV>> zIRKDw;lw<|W=P)oLeKcU*Hduz)J1!fR-R1tj*OL5wE#>iS%OP%mq4C^Nkd>%1Ofmn z7veaMqu4tklvPS00x^Eix4+@snU(i^|G$z_eDqTqfi=o7cA8$CY1(lz2n z3ax`Ef=@A*P4Uv z_|Em4QgC$Dojh8dI$r&~|K@rPMuiX}Yere&jF)~qTWWF(Op2B~Y4J2xxPYt$BICI& z1prI6Yj8duCW3v_UK%b&B5DGF)D{>*fd*m?(p3cjN&!*Q@Xl!y5s`_KJRP8Li2cEk zq9{TDL>59;-b*1vAQXT?ppZIBq&CUiYH;Pw)*6EtRE;>M$ne@`8J)A+C9yP0i{MK@ z5nSon06-N|GFg*OtlHtVe5d}kU*cDx25cg|cM?iquH`dIEAV4zcwE6VwysJ^CaxU% zQZzfMq$VH$2*gHgi_#&2o!DiV_+aOzzm!og4l?fdd98z!x&=eob9}FUS?tS^P;<6) z{>b5@FFp6vspE$#QeN&gVJQpj_2l{2?Emw9Uv~GB(E0m|=U=wZUHnhhr8Pvv#JiY) zz`&?{6#_$Ws4X)I1Y#5dBh*B$j!*=6?D&aBGl@m;RjN$V_oIH*OTp*Wh}Z)KkXlF) zJjsa>1~Fp@LdY5u@YzVWy8AV+LUJ}l+YnL&7Q&T;N=QL@icvE5!BW!8Ag&dBI1*OY zOCd&1HO(SJ`*>Fj`>Fodij8N(ttkvAnqaK}CCSSC>NTM8mgS)sV-tJ;B?pu*%q(7r z7$nR(d`Eky!BojBKt!xZl#fZoQeIep)>>JyLX;s8u@_Pa*$N>IVFbLy9Ebu#%{Riw z9$ER&zx}~~{_~&sgO5J*;!C$q9_@bTGuKxU!jF8P`_X?IKJ{69;g4YwIMQkuDhGP^ zc!{85VIV+_lcE5C#~#Z6?SJgPbj7BbY$W46q0F;KQFDS!8m|6~CG`sr#x zk4(4GCM+KvEFT>#9~rD19h^NjdhbIg)viaS27ulmP4n`|%DfaJN1lP;iN_?<8e16O z9rX%N;utBaZL5NFQmg$ljRA>i*5!l<5y3PL{(KqYsSYYQb8RT}}l_d-a9h`~$rb%AcSDKfiiEcW$Y z0`sKqhk#sWgi-{@Qp+eNp~$<-`PxdeGjr`UGY`|EnOH<5@5CK>$zRq86L0%1y4IBQ z5;O%aH_K=-5;KdW{js&B;wKg#1b}`z>UO$NTZOwEk}ibZ)lFDmW37e#3X$<176JF4 zDgof)6+2t-{c1*2L-~1eE#wj3+sP=sDTKV2p){LFz@f zcB9%tQ6~xlrs&>`{1i>ZRRMOK*`YnKZ7q(QD%UTRflw=gVxBTseZ_eStBSX_31XZV zfB;h69^Ra7TDgygrF2|mJG1z}5k2=@xV^iD5W`NU+H;Ho+pz$oqLhKCv1|3XnQbnZ zdr>ST9!%!|0JV2*$~%Wxx8kakeIP=qDYzb^Gm&Mk5Q(@Hs2_j4`^@t<`h)WC z|G*QEJ$(4zr`0=u?~8|j=ogTc`S8DknT)JqT6Cn5-oD+K7zcMWr(T$egrT0cXHRYb z93p~eUW~r>vGJU2$M(iR?A(f#wFP&n5$u*htHMY{I4~x@X@w&{-b|-sd40m#pCJfiP6I9 z_VUpTF-#-f5=1kBb#l^_JhC#M7gZb?%69`g2ah!}!vFv#9^V~PE?tbKZfa7$3q*G; zm+!*a3=XXhIBerWr`alsDw1vC0x%EzLv3`PrE#NOW@+4Pjru*Ew2UzXFWz1YhgMdM zwMr>uuChUUeu<0RHX2M^qqA%yrhq7p9OmBk4sf_}JBP;-V`RG#l`Mx@A*Gbk3MoWy;t|YXT2^|3dF%QID-M=PB zzUNhB8tqhWvma|(d_WV`M)Yi?XGWu4nL+I*z#M$Y&Hm6?|IRR0&{MM4&V2T6j!-jk zola4pX0ziQ3Mqqg9Gp#B%tYR!*2bt3wF@xFO6fF%RBG{4pSkpFzx~+sZb@o)a# z+~>X=f94B~jqULXS(vZh_jfP;;NMjH&hi^yXD>YEAARh62w}v(dG2qpNQ}#Ktz+NM zu-%zncd^%ur|vJQl>~q^FXPDMRX#o8jB+wN5dh>Q$EopF6cVaHOgxqUR~2fzf2N_R zyS3wlc+1S>oIp8*;nVQtj&HTG-HGOej%*ZHRT)Kbg%~%QD5Upcapllx(5tEnt-J5^ zeQ^{G`a4O|XfzwHs>&iKf^>632ozG;I4QG~F#vAey1jnl$eC8toI0kj-l~osZZHr~ zDJA+U2NY;*6ekS9o4&1x3f^p&Xw{VDudJYqtRapqWIK^LbI(zmk3~003BX8vtw-C8eo+Y8D!IdJNn(wko9{ zRLxe$`=FF$*AvQ){i2A%ty}UnUjYyR0IBRgdi->pKU37A&F3ygcZ;ocokvJr+pv$d z^9U=^Qi}cY(#Z#DBVQGAw#;IYTU6?q|q5JGt8o_+p>#RrZT z;5F&4v!DIKs~`R3rAHq+{44+4_o0;k?f1R*fB%(_|HS|F&Bd>NviQF~5&i7yg-VpVCV};P+f2mXF}Jq2x0jX{DFjq< zdvil8C4{8lgcM4l)&p%T!kdT`MT_0dc@y`tlCNCX=Z?31#ie_S5%VK*+OJCDlY3-$jkyoAT>QYB0^)ql@x*$$U+HUVsoy(M*!-6Su6$C z7GE43D%}g|oIHSQWwfF=PP5$k{fWzHG;+>GQ544qE}mM)626=ACpZTAMti*uCs@_4as(_O9ILZkKke%=EGx{5Pn001BWNklssEwd(nQU6a^*((PyW&EfA%j|+f91&>(e*BE_>r+ z*<8mL)v$>3ZfIQ3xbPvGL`)+3hd=TY@B6#o`{}><(&t`%?v3w$^WF=BVbDV~>>OJu zbp$h26X)^(u@G34esQklsw|TXQ(H0`dj$kvmj}mrU7b1rV8kk&TpDI(z*!r!`b|V6 z7r>0yCF6}j)HBToe;>`A;KqxJZ-$$U!%nvaz`_{7A9th53`tyGD$I;zrb19#wKwQKVUF8(bM& zSyrizhE;JoCGAMMG8LjSDn17HU8HBGhY%nzpif5}Tz$c~%rI(T)Tk{HIPL%gPsELZ z0!tIBiiH{*eK^161_#Dg08q*(U_aw7^5GAE_(LE1(89ukQmQD5jg5`>zyJN`&+jkv ze>Gzv>K==i3)(gf0RYHea^>&n&2<2cKyts=TiTT)h+56q^!n-Y;#{XQH+0bJ51O&& z?QOMi0HMsg!bGjwcIeI)Om~j6r^>Yrb@3I;6eze5DyghgwvMMJZu{zBde(yOLv z$yiA>TgxhWPh#7WwBtlMFE5@u_3lsq`5*r9KThV_4G1Xf7Jm_h%AP&<))S{6-)z#+Q|kbL?YQ}_ zM~5Xoa(?Bp55D(LpISYA`s~K5ube&IuFCxQk;dBAV19mSADKoPv~BUucVvI~+RH+~ z%Azl`?TP9E`+la9u+S)8UQdR{xM|<@&fzz_ZKU@Cqr^_I0f3_kUd!aM$%Oss|Mi#O z``*9xnupI{e&woOeZ0}#>&eI4J5(skzM0KkU;pmQWCWC)W^BwEAR z%MD;=MKWcBC?SQQ@q(;&FaVgXWsRNK$kb)7-JULiK;RK*oPtJ-ssu7bK$TPZVAy-| z^PhUt*Swt=ymwVu+BilOx;@W)WovU&NkP@1)$W?OvA*_&b~|dcjBQn;otVQZ-Hf7G z1i}!4l2VyAiCC$=kVVpd(N&S>7El5JD1`IF)kWUkyibXCAn%G{)Mz0cc6 zj=I=NW&TY6Q!2QS-``Qog8^RTM}lLqz>M)J(fq zUq?l{9nY8!Kl|*nzw}GLG<_LD_`nA~@H0R2GyDE99Ax&i@6F2jEH7=`9rQNa-9-RU zTzuu&T22tFxY-2=D@*gEd`wpgc|V>z5&GAvUY}N0?o6rGfo8OKZ!&oHRQahV8!x;p z0YK{LP8Sk&7he)7^*3}Mn?-UZ2vb$%Sb|AV78o@aO!t-T3%~I5AN%?@oyyZR^S2i8 zbns;};1C0sd4Pz$2PV`OlqK(wDAWmEk%6)`-pW-;MTRy8S2BZWE?~W!tC1rb=MD%w zRe7V-!8t%t`w1`u)tHeFv^~Ubf@0TIb3o<2#k^(Sj~aUxzpJW!HS}PTK@+231_sZQ z)R<`3RRCb%XJTvA@x7O)LUjt1hGJI5W(4_(2i4}*TIH&R2%Qj~%eCI6&Rm;9uC$^M zxsejmaP7nIH;h_;>Ed&Tj`x}^TxRlf z)=1g)MSg1G;;;YfKlsu2JqjFJ5w0^Y%yq}}X9gDWIjKkjz@gH}Wsvz(>vikiMhhG{OA<0Aj5Q?}+fXe&-KXmKVSA>tA1QupBQi9jr>PZH$E5 z17~>x07$cU2ch=w<`V25qVY3eoJgv%zht8A(Of9y)Y*I2a%cdqpGl=GFf zph5Mwj{wD`D-cjB?W&R}EG#akX?g@3jF=UJQ5a#|tO1c-yw!P7IPTx<12NYHVd8Pl z?!8BC#>h1@GjElHdy@C}3}g1hJ}ISA2jqVtgdhL$AD^|eV&)4MF6{X#t&-RlrOQf^ zc?k>vg%cdm%x21W=&}GaQnKo^SpW#_W_A%$>I3g~MN@07H47mCkb>7bI`cr8%D-S{1i`?4(E}EVi%LgI z;ISq9^cR2s2j2Z{*+8J~2i^%jVj{E_q&j}2vAua!$wvUCmaZ(hktFq%o`%l)<-TBt zxJ&TxGnFN^)Y7Z1^75v9_;fA9oKC9xpOQtu-~$Dami($O~bHgTD7(2nC254JcNi0tF#e2tr6?Dgj6+ zIX-YADJ6)BUvq6lSNDGdtnGkNU_q_|Z}tYUlAYr~zy)WsCCcN+kALukA54-&DOFWf zzu$lNyWhS4J_9hueDH%GoNa|2LU`|c-+Sklk=08J9VV*R*^E$#Slz=LI})_*<${P5 zG5|Cial1XPL~2)5yRlr)-(LVgAh>eP)M}P}Blz-ev9$`t ztJPqWhwEjpeurEpL{xXRgdzo#blMFc+!J5-=1+b4&%ghBziUuks<&)m3_|!x6M;QN z*xrYQhN#AB;`#Nfj~?$JsZp-b;!l27%s zt&uP+9Y{}D7RImjLN2ovdE(sJ*EEvW(;F8n35Wt3EkF=bG6f1E5Lp`$5iwVevOJ@1 z3<#Q4E5w4aq1!UnfwMS{Id}jRQr7}b=U7T*jdiXfL?I+U343Q2>AhK{I$O00bgHu@7(E?JF_GTbWxB?r*jtC2x(=t)jS^Zpoy* zfZ~yb=80pcnGO4;+*LN3^Um8CDF|0?XK6-JFiJ&Hl%*r9H2@>xu(ypuxUvisgw(-1 z6k=|E@!4meU0GfM4n>|rtx%pVjlxAjNfhb^n2+-*_ zO!MqTe$IP=`V4UfbcLl3RRviMqz5ikU|Kd zk|vLKX0=T9U!lNyk5WoRG>BZJ5&|#@fii9&2Wbo{BW+aw6*?tJjuRg#5i!?H;a)5> zWnx$EhnAM(h22AtQW%2*XOBy8nEpGTv1%Wc8%GR0*oq$QByU+=dDzlIlV7N=eqnU& z$>GMIuHX91_V5O;+;TuD`h%U}uqWmC*NWIU&(4KOr5l|qYoAsj%(%OO5Q3rM^c+g2 z;Hp9@MZx*tg_IP$k`kpyuoMC_LkNNy>MsNk2xa9x9!ui!vt-Y_LXdXyB|$ zXT}t0uGxI+2q{pY zl(J6Sl~SmbLaxtz{a4SF*FK+Jy6A3SE!GE6cnITLK*|zA&imz;*4k#X`Iiz{$6wf( z^OFc3sx`n}*6jg^!;%T>dD5C>I@>!t07s^uQtUnr!Wn&Zt5CHeI6 z@`X$GZn0HJ6WqZ$otD;?K3Efx?#9gyCtq?`xXeYJT{3I5y^vyGB&@RhXXqk@8Q}aKmiE>pngOb-VMt^C%G42_|Dp(@-&50i3pXYY(zGh<~4w7x*KHl<0RESVX6g&J8{1dyN*u=2&y!V&DbY;gAp;Q=NE>E!*c|7w6j~QWCA3071c;L)&r)faf>TBl1wbSsB_&G5 zM8q`6s`>dd(<7zeXSKh+AOO_K%rZInBHaeWh#+b#HH6sdZ*^h-#Q1^L(S>V`E4N{( z1pum6=mkf?dzVM1UHGl%UitFx|KZ1f{--~1YgDBH8**Z=%8ZEfanmd9sgewpt1==H z6G_3u1SGr%3(R4Z7VJlX=30qX6Cp_&3K9YUyz@XDrcL1Y*0Ly6pd$dVIPQp5PmJIL z2pOspq@L!jC+*SW3r7Lq!p75Eeza)Bna14nJ6rt#sLXWg1|K+(t3)MWu5-A|Dp&N{ zbBos6G)hc@ac^tg7+aM^)SC0&X&XyzT%HN308j`_`@Q4Gj}L~! zsH&1?%lkkfltm$+06&n)$TfV6Ck;yOQfHCMBy%fF)EIHz?Lheq0Zi8ZW2e{P1G%bx z-0s0!h4kK`5@Xys>^9(H=84U&N5uLhl=o;(@Ru}DNau28lEBXC+hMkvdgs;}f|OCt zl%NLyyB7ntjfRd629ULB>~qd1!v0Y1^yTKJ>!*#a9ktV!I|I4hliNMH(^pq-7y!6> zm)Hsb-j{cC+JEcZBazxgyIu+FgDR^Zam-w>yO`f>nxqNg*y_rQFT5mhbF-rk$9*lU zMw(R?SVm)R9Uvf3QH?RI8u1ab=C_UwjVq7LQtMEaQk%dm5gv+$+N)1qp<0{t;npk77^tb2&nC!$FJDxHYp8t`&{XjSLMa2vBVKwu+P-J4TMqm^@k znS?aKc|fQWt5&nyU)x-6oe(UvF~F3EA(NB79cfKWN(v%MT`r{(qOEKuL)cKYd8kOA>SOGvPt8D{?Jdyx_FJZUsyM1tVbm3Zg zvriPrmsK%R+6t+aNtB7JVtDzv!7u#Eum0js{v-e#YMSf)w9y&Y2I|;qCC{s+8`)Kb zEQK*PDr>uLtk*7*f@2U;YSUWs0jB9T0Dv5)x4{RYRb7Ar9w%`#Jc04TPD>?7%;K#= zial}t7CRkzWV2Yq31~GAE>d%z)k+uu@WIy}oqz(o6LFKu z9Fzea1pq(@%!82ci>(XZ+@xxn?Q&HLA$=ee<1U+l0)+t6o|nV`kQGBP_cFeLiRNee zayeNJ8!v;>(?dXCt~8Ra5(}+30Nx%9cB=Bma0EKyF)xx)37)~}##Pq|br-@Iu@#sG z-(=|RW(~n3;`PgyPaR8&!bR~SGsQ_URJtq+^s=kw%PfQV|EKM}<0ZSUGSRhm zI`x8En#k;m6>4z~jLH&!pioJd%0LH%wXE z`c)ZD-FUjHpqdRfMpOz8Kw}MB`6Bl(6sKOec<}IU*lc$O0G;>GURwLL-}>YCz3<1E z?3aGpA-5e3H3_g8TM=Q(kGq+49C^lt=VgNy1j-SK+*E^@aam{+2CCQTNq<|=ACLut zAyx=Lo6A@kZpL^5L%bCUuEkr81t!LkeG;^~cC}t@Mv9JgT80S#B2b~SZg|kKoNkGQfwM+TrVz0LzXiJ__d6se^3D5yrSN2Jf1I)4< zoaIso;8O?uK{Ala55f?+Nb*(}S`9w|kU&KeoR|6j^Sr+!j3xxPDEmn<++A7iY$-q? zohka*&qQX75tip@i3p4VF-#O35g>Qt{MItz7jhhL9Rae&8raNX(OS|4F#ZyHCjXML zrUd}3ch}mzwF@g}FJIX>v~S1a%H@oF{2dBAym=PR0j@W=mXbhcPO`@)s8x6aM) zX7x*a!!jB+y$}$Q@B%yN@F28BfzA*z&N=xZ5HLR^B2{{jF9Rz)eeYX)H;Jxsf` z>i&zAuMB#plGLDJ2#z=p)aV+AWvG!)z2H{s@-IjTf&pU5+a;b!JL4EHMX&_pRn1pV zUpe5q>15qHDvAObvnF!{oS`oABg0b05VnIY0E8fr2rWM5)vZUmXK*@ z3=j#+^<=50tAHe#e@@!|5-wfFwg`+o0zzonkZ?4g*HkG0_I<5eKfS)mBJP|7b7-R$-x?Ko8@M>513*!oEW9_ z^+=Fq$Zs{q1Dc+k9}Id+IV0)eX1pd-pb$g#6SAB$t1aUm0AyJ|j^jMf85h!zoHf$( zvosM>g_Q+AJm1}DGY9|~icG}M$YTn!dgGO`Duf~3VQ1mvIr56enq@DFNv}^iG&$KCEOmGVt zon^u!l#Gj#-|DPGr-|V>zttMe(UmJ+xnlzYau$(nn(#2PNuP%i5$9pWImh^mMvs5V z;AZua=O2wLw%0bhXAZvp&K+oZo_4)(+vf8|v6ax5Xg2YZYOOxf=;OdCi~%4}*j^?U zCx7&@9c5shYTC($Y1XsJ36#}-8m+|jEUIQNuKA*TAn<8zx0CDF7gA zJ0?KI!++(%yN2PoHJCR@bIYULKyL8TrKSFrr{40qJyMFz*`~E~02GC?KS+%BckQY@ zarBdS?Emg!qjEqlJf|;=Ht%S4snBcljC;OwqTB@|ATTKHf63@FRFyrijIoYQQYwNm z+VG5;p!7Ufs$}XRW3I@{vxF4js47F$wkj(X9#oiL6_K>ib$QFA#uQWjOJD;80LJDH zjiEsSy?dr-krWz%h4lS{K=k?Ela>odjC1C6!G+lBDFg&-2sX(*VpD&peA*F6cwh=A zl$6qHCCJrzrDEJvFj)cRS85MFb>X%nGpA4Cl{J1~FCY*BD%1{gVt%0q{e+!88YHv9Uw{?NIj=U;Q1tnezp2IIuJSJK5FeU^UYCw}XT&ptd+ zYuDnN?|aP=Z;bdIG3L?9P^E(=up-L1>KL!9lR7jBTCfQ5Uja0?5M6SpvGoApjVP`*yh^<^iDY@!XQfkv0jik>E8k(KhQkHWukC*=JF1608i)im)Y2~(iOsC4Bs&9Qq$-dOjVgvl?t(jOsYoh8G$mQ1 zR;Pl8jha&aGH*Gz#ladTwNg0inA28UrhEbdU99Vi^RO}JScYHP^gZ< zkP$I9ZnVilj!YD3(l=ItZnxF|hKwtz{K5|az+^o+HSMeRZMPWspTHRW-3QN9B3lN{ z%Vy&Xy_3US31B{I)-k{_Gi_6(;{wm)sk>iiWTi02?IypH5*Nv>`TgZuNBL|vwEXz50sDl_6h4TOaI6%2PBZSQFQ8s+m zm>^vclcNg(L~w3MQv(13WQ^RVLpYaFuNMS+CUo44f~LQb?mWM-S_Xa zEc<~UdrusB?QRjM*a0t!OvqNFvHL&%;CG!mb#k6AUjP6g07*naRPq;p{deE++Jmot z{lQcvPD*J}N#0NR{{4r~olYQK;hYy3A!}JTV@+Y5O9uUJxBKwt&%N#KZ$Er!QQ@^n znb_VIJYL-@`HUmEZe1j`v2Jk!@^9bc)^vX*4~>hag8m@+g5MMI4h zEVm9rPeK54TjUU<(-H#at;7c5CXvntl+TCJ+{El!o}caY&*gML)feMx995TFtum@^ z>hU9t+%OmeCV??Z6BI&6vM6#Pq;u9;!x_s9Eu<10SzOK1c4NwSJ;o60%}J|uzu!X~ z){dmdGpni8=tk0Fqfu0Y14X);f8q;&zi{`0DyAhL>UHrUEtDWQ)3itCSO!4aXv6#r z+9y^XP6T`7!kv!jk@e@bGh`gGGB$A_pnL$x0R|QKT!Yjv1j4HD|a7W(P~<5v6!R^^Q1j~*bQQx1<2z96WU_xbL2R=@{zcww+N%n(=rXE|gw0!kD03 zx5oxD;A*T2Ja&Ujkr%z; z$4}S`kA23O-p~BlTLy^_=sob+j~BfQ6F1NN=_jA~z+ZpX zLviTVx%=+fd(Yv$>no>!?*o7Pi~r?KB8bRYvcivYt3gXJ{Z-VgY6c7DaSb27W+)UDp|CTr$=OqwaV`0}8v%WVQRkEa;qPLvpr@K8= zVaeROm@cmuZDUM|={7AsQ;anFsK`Nul=guu5Hf{65g}t?T(w3cDoRs8mSq_i5^<{6 zKMr=H$hvX0mZT}?oC}_(sgTV2Sr^U%fObwh=h9?R2BrvI1RIUj>|&nUXT)Myi30#= z_ma@BA%c+H@(Y*qeGAG+Rv_ia*PndF^x-w#dcJ+`?x~vsuCy~?3CD0AQ~@yojN_<$ zZ8XU3AmKPki3m1dlGPwM{1aO3{}l!S03{YlG8~_!_;Prw)y`qRH}ri$Xd6Vdfaqy_QK`UNA>{#O~|Wj zgKni+iCF;v)3Vlf0|Y=`uDuAGO7Fvd7@ap{Ef#@iXgOI*Jo)8Gi92G>RVFLO+R#`1BzKOknGu$=X;BwK0>Bcj zI$M++XY~B?P%#!J7#FEoHwo>l`ziDL)*W46&AA8YfJ+7dn|d`lb2+4-fYOu)Lw;y^ zouwEq=NyCrGN3#V3IGTo2*Yk`T__)&6`tqFp`|d43S$|fHC7;FocB5#O8MH@Fp7lq zoV96cuFxRgw62wT)^M5&k0k8cw_~H79Dj1z@`joQl-yWnos*2Abs(}8vpPu=9+E)3 zZDOA@=3r(2$@P;DT>MAN1VCBeXN1E>HFCtp96iul5OSpOJDjxO5QnrGUgopqG8*izmCCRD{mzZHC+A zOrARDlXKZ%BW^6hrW#hBcD!JidCknsKK}e?k|sK!7=+h1fzQ=Y#0)`jfy`M80Jq(| zTS&-Y5Y)(uQH`*pyo=4RxaA6!-Yq$&NW@mgq z005rV0~p)pZy%;eM}o>X91|>b)|H#YO-&F2vP16z0W@8HcK@aAMaWQSwst4%GX<;Ae6;#7$| zQ*6UAv@?6+PYDYvcA95NpK1*Sr$B2&3E3lr?{9MZ*9~U1L>%@+Np%f z1->f`XG@nY?G+OMP?h1uY}sY}=E)=8#_4BQPWeG>1arA{a&vMzKx1K(?n>DJgBZrH z5fOG32!ZjLMyHl=NxbDzY!*2^D+;g3jI!&HYdl_ z`-=>tVMd@NhfJJIug)K-z{7KAk5B4?FUj^_ZagBSL@tpmasgau03zxNH#_zzc+HZ6 zsPjHD&X^!)x%8vSIB(lvLizEwC^jzO(rna=L@B^n_{$gb_3mxG_L(=_Q!r$Xs6+{j zwTO83GXAF@`R!*u>OFjVOK)feL@ z?2yf)#gz>jEWnG#Rt9))xK}A4UJx%F448A)NGwz!CnF}P0wyCAsZb#xfJj^Y&LHUr z{>y4gz6=>U@@nNIB=FFD?O9ChC`-KT!yKqMHm7w}1ccAN$Yme(L!Z zh6w>KrLYlm$g6L>rAL@wzH+5en}|Hck^SKJf9D7P^v{3exBu`*fB3uW(gJ|VNe`Ux zaouzu`t<*}X})ohVY>&dK0kcRS554j20hFdbm8jq{NddUkw5c7-tF}#n~k6rC!N;+ z@!p^L;rIUND{ntEj_+HeN6@V&V(029TZv(DA_^7hQEWwoMlhA&@KXqBwlXtFi_llO zHlC72+8uS%mZNV5ky5A59>3-A9nP9Sx|q9=3+YtN2>?iwZfkYTi&Qn9mYY`Uy&h-0 zTYB~kxg6HA%}fmt968Rp(S{4=0NAF&X-Pd`f>~=tTr>R++XT}yJ3z+$FeCs#M$Q&R zfkQiHq4P{KE}0YzL>T2o?)!cz7OL~S*IR=MIOo7?#dXlu`Hll562oOKS;Om}+e8c*V?M&@yJSKC(?nmf8WSuvSaqA-2mMFnS^59ugi z3gB|2OY4MW$f&ISl^kKtj5b_|Au*inrmh$vLkGZ>as=gtBf~ENZ%xzm*6qsFM6_ea zjvF3qiBb~5c75d0xZW=df*=M;0U+jP&B!F#>b7LYjr*Mh73MF6dv93=0HfKZ)Ae2Z zuK*u1F3GtuxhO2I!33Ohw2iR2C85nF05A!{FSkzHA`ai=U6p;Jp{Os4Fi(@DZD!`@ zdcAdk4Mz@1X^m0R^?E&oj7bHEGqaVMAbVylut zOtM|{uW@Efik4%U3Z%B_Al>kTn&eey1Z8okCWd!<^JXIl-M4DU?JX0q7;0PC{f_~?hf`?YVJt<1sp zMlVw&<7F`q8wp;{bl(?otVA*`oL*fzbj$+YQ$YUEkc z?X`CqW+2FNkqO>5y~G*-h%2?(`8{cpJ%2KO?o@o@jGrXz?QiH&OrwUnAu|*ix~)fP z$iLPW#a3@p$)PveAVXse>CC1Agha+y*z$~CR8Ck2K#ogQ4PzoQMWF=e78wu$>-RcV zXdZzpG#J5!GA7X;?lR9W_nY9DA z&v?>-wE&<8jcDQE7ng%W^=>7$7n3Xd;<;D_$_oJ?&j(;RCWMMvaKOay60j7+!jkPf z;r6i|&S!(@_Mi2zYj~~D0j|UGPyy3x9%A8KT zeZfC1wx(&9Y%xTRh&rj!#a1m0t?NL(Jh+f@y_$7YzHt80Ji^M_SA{5y8CqiK;#%>1mJmUxz#?hUwzLH|ImN`Z{I-{4U>UFQf$YZI(u>Hz;(Ja zMUe(C(hLRwld5_qUFoY0sf2bt$BNwaE0w0rY>^_f@wB%C0HCB}zQ_*VYTByoaoOY& z0CLkA-=bbWlwTQ#L;%2oJDo0dC|NVzf(%1;8=KkX4Jo%hmxX=>23;;ynVACTF6|9< zW+jI(W(b4~2v}Tq&aLWBq;`$UjHM&awl9(AEZ9OblQds$EeBy?q+gPaO21`5V94fn zO^K*xtv&JhsheN99{`-mjm|*uezGd0u-01T<&G+qT4`lg4p+GEc-y!A?g#(i5B~VC z|Ks=kTL4&FZ++p(hv%9dlC^f?0IX0RBHpsMcje+qRZR5;`O?agPM*5!RSf`Wck=m# z;gb2MAA4}u{M=o9*XnW)j?_w>it-x0us)9AGUVJ;3clt;Ohl{N8!-SUy^2InQw{*A zWSsGE(gOe@a^!MKoPo4ID6re^^}^Z&w+z6|OfSq#&wY07N{$xM5fOSKX9xhFeLVbz z@7{Cjyf>~>`0%55{Pg=z8_#UMtTMzP)>9>$% zHGqWFa-WkBOpNhc39se1IxD2G;Ipzz(DO$;ag!$^tXPK36ePUi0bfqM6##&!BAf-r z!I87p7UQ|dbzSOSy>+FnBq}M2Zq5{c?70J{&QH{nQ#H=`J$E%e_>psry?&UKX$=5O8(?_y%;lR8&m$R~<|LGbkaD|Y zE;@PnAD`;4&(H6ko}5V2?78QkG{vej1Ev;Ub-)vDYBIrz9OIeqC7&q`;UgKe4nc+kA@K)hy6;=rEEFOCATb@mGMNUyBq zoekH|Yykjdv?V}tL=L$Gqe+F#0|x*E&KlrY{zd?>q&o+I9LnKM--Zd$Qup!+w-yw> zBvim75j9+WdR89o^y{PzCA~{;JG=E8~Yg(GLl-En+|!8e*WQ~{KexXT*qsL^dj$m&!a!WQvIel{{U7*9QRwge_ODMKI3ru#+;I;LAL}|P zr{!waF4YDi2sLYEwnYRxBkC4Zl@5^wEZ-mPe!hPK5hXciyxg`1-2_3m2Lqp1oynC7 z&)UJo2*7RtgV(@9C9W!@8qp`@CH3rzGKmnJ-j;7MupvY1Azyu&<63>_ya>_E79OAbK zClSFKgNT60h0Yx0rVE}=)-J8~E)`FdZSjHc?+IpmE^UfR-?cYq1cDwY*0qGq$fAsj ztX0LGdC)Da;hs=aLEdjBSF$wA*39!~dmBzHa{Dye4K3JbR-OTqL%Dc!1v&$=1|=E- zG=v;Ced?&3t4x$UD{D7}>#QkN8XPhxG7!o-yBTx90wR!g_REL27G|q{HLdiy0pFn# za!#ml{Bgc65+H=lNLxxQnVNXuf%5GNC8iVQQunBYr${vdi?;^s%FQ zdI~y4uU&Na9IQWc^zzLIr^?>#QoA9~^5>3Uyyfr$R{>W6B4!R^frPkke@;ii|6aEu@T?_0OH@-+KEuR;$%t`h}nR`v))8YL$Au z)~Hn*^=h+GZ%))3^=hMDt5#yXVZX!ZVmag+?&#HLAIYA0>^M~Z{hG$s>xVNM zwu2;1Srx=4RArJ^Je17Z2Yb(gD+m}Pu|UvU!(`a}F7TZq3O4T+(0BmJ3zNZeo2387Cl@^pPA0#iewF z6st4c6HF=qAOz+Naalr#zKA=$wvrxqP_UD6^(JKKGf(jRKY#yM{^t81di3bC|KY87 z5OPM8=b7N8ojnO5t#cD~J~PP)naKu>vn&}Hij)j=dhF+a<*}(|^9R589YxyZ6H#=z z(=j+L!I5Q~72uvY`N`LySmS~Afl3FIXklm052?E-fDHvvp2l;sifyw$G>W?y7{2l zv)HQEY5?%v|ND;5JQ_XvsRKvuJwJ}OaxWmOk;`$sRfMq_^m$ad9&hEGo7|1?)-}0O z3KSS?WT0)a^JCN5G3HT0 zTqG8Ah!u4E48$OjGO#WO05A5%eqlFYX1(wTQIovoOSw%U#1Ycq0+G-oTpR;seT785}j5D+25`D)jn-I&^!)+gJ|xxwUo=f2%Xu69qZ!zwsO zmRsZ?JqHXpTIheSw(jCr0HhQXEP!JEH&2Nx}46i{S= zSt#XH$@k@EMVCdMTq;?ta~e|+SNAN=rN6?s?d97X)iZ@%-J zzv-QwZm-quw%VOmySu*8K6U0otJ7(>yPaNde9c6}I1U^2TD?|l)GE!1`ou)tIP+fb z7irNxc=hi;-28)53fdHjddck7xodyI`=TQNp!2@uu{Aj$j~w=xDi?Va(rOJ{lz1xu zkTEESaGoh`7s{B6NR|j-seMkcy&D=QB*#t0ATlAC5dAHYFeC(k^B2!Ne`EV&uzM=t3iU^+-SXT+6Dd&Ky#YYsSUqKW{rUTq>G=JVxFp#XQy0lIBAdXK>DfEj3v_zxD(cIVxX!3ep4^1Fg~X3GmJDdK>`WQoU1fS){|6 z{f=*Jedf{V_!D!zwqL1dFj5M37OWdW3L_;pON4BMw}LUq#0YN%w$+SJn_5u-ppB8r z=ZtZV6k)g5iyb}n%;yi@dY99g9(0{^)>`SI@K|0Xo~r<4F05eUXmbq&?>aHhHBlBFCGm}6Jj-{CQF4M4x6Bvj2Br?dJ#hj z09+`>Bq%Jp4zF>6-mXP&$nWWmSVmK#ii`5fQfGJ!5$Qb528cKq;GR99PRWltgC02z z+_NYxV2^WKU;$iND_jN&yjawwBc9b_RvVb~{eSYYe|+Te*SzYkU;D2=2>{)0FG~mY z#v~%vYt?$Kx@Xto%l;U>{-BJ)?RIy))xL1)>T_34(mO2xWR3Ll^76qwdjLTB)f?(D zj)wHMtO-q#5;-mu0F{-%5<{F+wZ83ap>^aKLWxWvqD3a;m|vEVWHd5S{zjbhmdS6O zfGa6!HyCf91Ay4Cag%JN6$m^vm1e~;M2B2(lk|BImFW^gMu(=_m(}SswOa2 za%mmsTnKBeQmV&X&-So~yS(PafkUZ1f#RfdH$lZKtcq@&|nV5;znyVs#G$D0mhP=G8md%Ynvt6MwSj3X9Ll_V706Ymv0okj;sj) zu%H1pjaSJGb)CsMC0P_$OG0qRSjli2Wwnek=QME1H#G^r^4`IYd42JU{Ng7Mf8{ql zHLhF%OakIK-fBw&=2A`ufIuQcYpKLrkt~#8CBR6zf(!ma_lbk{h*NqwB;<=rWJBG&`$qmkNj zXCQOv3YHtxPLg&7=iD#CkfNMrj5E?XD4#KA0a)t1c2tRKf$~W~YTAHbE)Lc>TM5Ax z4u(9k@nfzz=bDXd=`zGDUC7j7u%;;srV*91Wo^!_+m%E9%`R~OK$ZZ(T8Eq3;KQ+r z<#pUPsaj;5xiJd_85l~ostSB)2ICyzc0C(JPGjv5vy*#cXQqQX=L`sLxoP3(@vFBS zo+D@D+D5Mvf8e7}eBeW0c-378?|b#WtlRg3f=L1yXbk`y$T!AHA&7`0a@4xWg@_-1 zj@T-*Xxv@t*T#IJr{f8i_D zYYyJMXLM~=XV*|8U}V8S4gm7BBV+)GrRK=>W182_+0GDmvlbY0wuGU8h|mvBGCc*IWc*s6w>aDI0mx)uW<( zE9=0T%{K&#!#8~Tvq#_Zoo^x_vTnq=C2K6>fS|R0#ykxKv5gPZ4+4O3bC;9nYV;WC zlg`#b!rAjD-}CNQzyAYI{?AW5`;BkDJz`*OQI?Xq5T57dS%T=|y6(3kAuCTGJM-W_ zT==OU{f1{RuB~0kcFZq$BGiK(+$Ab~fUDYTN&pxXvgR`(6acP5@1~pX%Ui1~BOu@|cjL>!mvPK_=3=gP zc-wNvm#MtSvUkvetni|5Fc7R5%6trKc>=phms?JfhM0~vf}?dX(i150djyx6>wFov z$MS3PwK6kh0)-ciW31O4xnp7XxiinqPB3Gv5d5ugy7kw8_n}|-@qfc5f8o@|M?d!T zpZxjbt82Z_K6>)rSMD)XVa0&Jb4(bcl^UXiRMvZuueP!e{ly~>Japk*-}d(Z@+0pa zR}2{qCFQ@oxlRi4^Y{F?&NGhN!gNWD(D?ung$jYljznycaVEI*Z!BK3IhwMnoQ%Mm z^%?=H3J=!v*0JJBxrbt08aKvobq<99z|qdbO8#W;)LpgkDn+x#RHf=?A~@E2_wP05br9h~M+=ul}7sdh}y|^Yr0Ei--4309a>Dk>@>~_yL7sKTqQ` z7xdM&_QyW{#IODQ+pL3@0IWb$zl9_5bl_D1fN^o@#5oxTI~sF-SqnvEsN3CCN)7^$ zVWEqPoC;j1U5eUES8V0LbmR@?$Ff`pp3i7t62m>g7%S1wuZdsbZ948~(xarFF=mC@ zq@$L#Zy08p2&yptChu@Wx&7uhcYgi%r@F1+sZSrc`>UTHsR99%#wctHZxzBwuY|43 z`$JyH$e1MMLFAAzKEzuQarkWJnuk%}^a{_`ygH}ka(ZQ7Jj)qF#{96l0F}1wBS300 z&PgfWvVATX0I0B{x$vdx6~K`7N!NI|nzaEy2C-YcYB@b8u0$cPv&zZ*(&VJqEs|!u z1>dEzSTFhpY=^kFa4POD-MZHT=!8g7h6S%(Lf`njg35T zViGGCa|;4ShKp;(@`RXPwO2=&jwuS@(&_>kC+EOf(m4w%DOhcCDYmH@y~L-N@1|k~ zz?|JiBGbiS6l$j?C!Tp$pmSH32hQ1J$FF?gL!W!<=!Jc|rw$(qzxFF{d;44O{?K23 z(GO=%Urb+dq>fPQ7q%p6m04K3tB;-zNv}Ngna}n6*}LEI%6sp=^Hr~Ule$r0g;Mb^ zY5CzxwMhLF2V;x+!R9y37y&6j3Zv74t~u({rcgl@N494VoV&TpaD4=;;lqf z_-iiR3s zh|btlC~O0F(x6zEq}X z039$KRjxT;&!tO6`?6kd%sB3BrTxTx31v-t@ZONB_Ut zLmxkM*Bg(+CfZrOdX?PQ%5;5Lf^ppcuVT#l&OW z6vet+Wh$ECai%lpoRASXOJo7iSwq@b=L#(D-Me!&#b?hir?uQuasWw$J^{2{*0O2O zW#DY0*R7BY!L4sVR3pwu{8ne(d3(ZyNF{w94FV?vKyGAR4CRLGCBw^?JlKuM?=@jM zc3TztpB7s+PDpc}8MM*C`odsva-ADXwlrK!d4F$kc&)co+Kvh#979NZ5L6HuFis$t zA2{cD7_BF(v(+th(vjt3-JRCz@i;H`WEH4tPRLTU-vclgi~)`URwIpW0xwCov8M9%+;LJoHImr`a-d|=T$Gf z@Z9&mOHMU|SlUOQ>b^`9)QfwA|FcH!iZ!#oNVABn44OplI!^Dl;fzMWm!uQmcwhy9 zs$XM<5fMioqc1$Mj&`eZZ4Gih#9P@=+U|VOxu1196g6;bVk@!8KFyZyDdmc2Jz2;i*LiU;X&DK=&F-0&E)#>?RNJgAAjz@|K6w8Tgfl~<|ppCbH3S_xarVzFFCnIou zvw{grief3ZZiP{FNAQ&&{+U<;F#cmxOLMUM?UPN(##sN z;=1SmTi(+C^S`aFT&n%auipQy@A-@u(5BWUBb~EN?ZMKf*J?Htiy%Nmlk`NT=B#4? zwkVhoV9dNfIWL=M(yMJ@%5xLbYP)n&Om(Tf+-XmEyyzD;iW8l3@VX*D43%eSa3!VO zcAfMsYw(KF{T>Jj;Bu2CLeKC35UiC#t68^yU~-}z|0yH1dNqxESw9-`wY>O z5&))Set$H7;^Nv$&X%b)Ke6*{cDWDPz~#=mz)~%oV1OZdc_mHJx;znK>>P2)opV+p zFxpJ#CF-1nhtAUIR)0D9t>Q+HUdZgl{GStB5e)!njX*T1%yO&K-dKUjp&iXI>B}~1 zQNtAk+##aX#s_9d5}_R>N~Jxjvm4YK?|kK4jSFHazcT(($iV81OFwH_zfp5mBOjr$ zvdDTPMdaqh;uoHLFlY6NM))f~`?g>DnYUFUiO7n=lQOq1T9{UU^{G$p+O_Ldul%No zIRE6MkEF@-dv;c=b%<`Nk$&vY-~X1c-Z9yXkTFe|t8sxuih`Q|-m&K8WnXi~hHH8)=D04Qk#-k3TJ^Iog=)%`|k?a&*nf3i3=G%O$l zKq?9n5;pw>j5Y@0LjPRgABngdIi~w6avKBk{4(<@5?!0C7mkLq@I>U?$Ycy1649~8 zj(^X4|HBZa72cM|t+VCU$7q9nBt`&0u_H4~wdDLwr#IBvK&)IkfA{S(h|uliExr7v z*R2hD#=B`B(B8Z6-1AotJbm+_>9@Z5CM2POHACl%b?3Z}ai%DwtN{Qa+t7Sf_;tOJ zDV5eH){MMxWo5eJbDKsoM#k8tddS)Hmmc`LhyHB;pM^)11^CNP|F5sO^$tdJfv*67 z6+|v~Mv^ta1UVw3du*$w@5lkTa@r6uXAD!Dy+&ob;Fgv?Wjc*L2>^cB82zTV-<1B) zyVl~|QV2giz)DH$GSlNYJIY}%egV9aB-aKxk(r9old z?{kpZ$W$RB39L@rH}4(I{&}asltnhgw(d{^{~;^ZN^zu zt2?J|1Aq(Ni>J*}dEyAm>(RB4>ovja8kE4Af=iz(grQQ&*2*w$AG^^h9(wq>U-`M4 zt=*_pCbBFa&NdeUULmS)bWEDLsmTdn5qSQ6ufFf8V-rsud-(R7Yl!S`AAI)ySMNEn zyW*Vd57On8-sH@!%C9h{!(nvRFM1cLyF~qzikSJY z2=?q$lll5K%SCeTMLu?P#LHyO*Mlu}Ss8ieM1D2OVSt;BJ&X}KKrDZwk=*?t8|idVx1gkP*RZDSFRS9PaTtwe(~%pZf=l3g+7sv;*BDkVBEX?miZ$G zr{43EA9}-A9GIGptRu+@fSl3b{0l32>MyjcmsyGh9p0%n9$SxR>OoU3wK`A!{U3(2 z_qDs-qQADjeCg`)1_0WW_8-}KsNWXK9O-X^kE+0XTB2{NXhYwe;P1uaOxu!W!=i`Js2Mcl-R0KH7Nx@%aZnc=tDa z*W;sqa;D(QE8ox>i<|*NBm`%elw1m16kJM2FpjsnLUZAd&gygbRJ=3?MlkHVjQZ_^ zQkel?t!yeU--@%TSdWJdQS5&h%3;rN!W?g9$cN`{$4fn!s;mz zR~@;W3bbI*DCQE&Lq05PbIzco+yWpM!@|0=M|RJ3lmDN#HxIMyy6QvM+QXUeP;+&4 zSI>i5k~K@#U?EwSkpbKB2ex^EM;JnY*d`<%2w<>-jj;&{cJdw&KOQ*1dk_*P+t?U` z4Yn+K(qN65pr)Hfeq zn7MIx>UTxEGvSQ@xzG!3md7GQNGl`&4+5h!>H-UewS=%jZ8IWb90|F!vBJE%@PBer z)@2qlYd0;-m+`HwZVLdE?yt!+Cy#YI9XvYC{r+^RT-ZW-F@UJc+7&KjZGCxdeL0wH z&_JTLzuy#mE$wYNgf7!d`AC<dMo!KJQXukg(wP@Zb*7k3D7R zTSe6)1mH5b#y0jcZ@3&nuo<<+xU4x@_^zrC5D{#`U{#KltUVO(<%rRxS^5#nNfj{< zJP^Jw%cwP!*5`s7Yk8&jj=&f@^}^hqBeVUZIAV<1yZ|8)&^gCFuYwb~)Zw7a0}QOQ z5sS_jXIIUNGFtAAOmudTjy7o6Y9?-t)#^`HjE+_^-bekhKk4 zS?)b`yc^dh_U*pzwR?6+US9H`LgHX1YhGACaPZ*4^|jT%`rKph{Fgtpb9Acg713zK zBm%(hrzamd`S|YI%oiT}=CRqiTkf4adHUkS$ViO*+U9jHr6<7i3rC#us!>ICjg_tn z5g`IuYe_TD?_XmDtudPMzCb;{6!6F{Q8A`pc>7Yf$N%iF>tFul(TSP%ov(Wl0DyoY z3^7=ob5jzBem_1YJ)@1_tnceL=&%_JlB)AX^(k!)W9))iAj>%C!VmL2TXSbJDMKlG zS?>k!+*;WIR61%fQ;*u-IQasF2qlZ9Qq~lxg|R>$_ts6X5$?kvOxC+o;Y6I~hqUlr zeO>F7P=?%9Qq@|UoQ0sv!3@W~q&Kp9?7ixmnMYq*aI@xqe-{9NgQB&{ghz~!W1q`w zt?;C?9G&j~A?vWHMgWX4lPeK=OpI|pzim$7BOm$5($doAnY?S)u6MrkomXE+|5!`- zMx^{QzO~T^0iaa<+V0__Hv_=qo#Oz|FLnX|!irhAHoAFP%~yXYikuupRvq$r0XUzg zj`xxOrQmtl3H^GtDI}uJ;U`o%)=g(sZIDIim2D>?ze*8QFFC8Z@IU^E|FPcgeEg#y zc;u^}5<)PrMNtsBN(hHB=4=#%?Wu{{#ktehTyr=IsoOQRny7o6P47DPt%;|eIe*{X zhnAN*t={f??*6X8-&AU2{3;E*yr5Q{y!7ME{N15BtF+3E%FpxiJhoGlds5EI@q_-3 zwbUX5U6nN~N*W6l8gDsmwrk~3q4^8!!h zoo*|GBu*p&fb`;<$8Q{diE-uf!eYs}f`J@g|Ns8KXTEvrru%QGejy~-{G#y4>VCW* z0OSBk|B<0cJ(E6Nm9`Plap~_e$ImQGg-o!B0hgB7Y!oJ$^q|`*rYGxf{oXr&^YEc&X4bW{CBO>x`|dkTXnb5EB~B_*cv4eJQjr&-3h*0OL6T zV4O)O3yUEq0ALIwp`9%O1XMlP`%M?EHKi~7yLXN}yNc~YBIk}o71)}tAuw>sbs>;Z z7zCI!$Y|0EqNF0Np|c?RL%k*ZkW&8Yul}kKn|;eR0m@JR^iRL(O>f%r$KAVkU;fUQ zwU_a&a~Dqm+*p(d0AXC4zpzf_6U$=_4ublWG(tkhZ6j&o5&}4`Qt6Gg7h3h1(M3fJ z-N87Lp8xFXs&m#En+2IKLdNB$mA@fSLYMK5swcX$#AUEqdX{nTUGM$SowwimgFo>7 zB_B0xM|bV?_-F(&D$COIJ!3QgDJA^CtJR7|v|5%50BSXnrWi-;d{@Qwu`ho93-A5u z`@i}$Jn-s!E*}BLMAfBko0qt(%4Qnw1a%!0gWCi>JrE)_kLh2rw@8b0NAJa+SqVTbK1$-FfhlufFi;Q%g#Z ze8)Y9u7v!hqA!G6&Ar=izW%0b_s!CPgK!iasykvlW@JItWU?M^QvW8UYj6((>JScMrVw65| zZZ~bkoarllSgVF|8Nd9_#cs+z{de`h{GHo{ggYNP1_0y?mANYm7RIj35rG8>4eAUsWH!)xY==Lov~ME=+?cTFigw9B zi2l8?KnQ5a^|Z;gYjD*yjUH?)Ne|3!Q;$k62|2Krf~@1|pyv0RVur)tx2) z#)TKvbgxBFc1P2*7vQd=2Lz$cQ>%(|rZ?{mCc|JK3}!HOxdbN2Xws!~#s~$2ZcJPB z1s{6op<~C6-F)-S+fIz3J^0{*SAWB|Z&&lJ>+RmI={f+EB?18F!mm%+WyL)K0744b zJg|cJ0hw+pE0H>%}w|<=?2W!-o97JVTmmPr1l*V9?C~(fGqMMM|%bF3x zItGon?)kNcvtuor4&ST?NCQNaE+=HTT}NW0s4l$~puQ|T=ZILTzUxv;?hvuxgu)g; zoLSAl0TY~Y(q`jR5{dU$L%#vUpgDqc+G+?U$G&y4B_t8ESeEhbt>Gt94PKl@n z0MZWuKuC}BF7keQK}bQm6jA~JA0CBCkp0;rk~#py6i;NMtTSe;^Aaw7^d$2{VsWj_ zPp_dz0PCvRHVq=xbVV$jB}#1EcKNDKJ%|nmqX)g|Z)Lip9RM)KY}&E5}8l>>Xn0H9EV>bwUYPmDGI zV6>?!apZ^`Eu^!yqC1n4>IfWR6H1o@^;c{_Z)rne8M8Tl(G zmS*MnA-?1G@X)=r>nebubME+KCl23qfUMD-b(eM=FyI~}qs|gJvc>}A18Tf=+Bxf} z-?+e4Fx%>6c89^5WA%yA{deE9`;I&J-g$TZ_PhS-ba!R7m)V{YzDet@$b&k6%>y%^ z`rFUUmB%}9T5Wnx^v8TH17ASI825%+aMsZ|v6Q=5 z!T10GmjNPU+>?IHm>iZUhX_h*%G2)DTYvUb&GF1y{DWV+Z*^^Q1K&#EtaFaYQ6+V1 zv_Zx#asU{v&EcV|PjM$E^p5c)+%eM z$79jdF26D2#Wn7S4x#0WQ>JxFbn=7)KsX_uUcInFJ+C?1Fk&<{8bFYe{~(*IN2)#BC*^Q)mh1%tR103=euBSaW-UZGmU2MfA+0!c=f$^-OQL8J{}nN#>RK7Ei%p^cK}3HWd?w(F$@E6qxJfQ z`|r%2KmNy0cB4nXaqKTX`8TIdFa6!4$@jkT_GWY9DnlXtn4HV_aBMGW4w?lRM-fBi7ivJ<7^&Fk4dD68?y}JoX$K7v7ctzZywhRz3v@ zoF6uInQ|e6?aFa5h@ENx#sJA7#NFrSWbDhNU#QrP94j^YRa zPkiyQ@A9d{pfjDX z=Pn5$389Mmi2&fai)Zqh5o82~77NM6q->^XFYp9I?4`xS-}v;c*G6G5L(cu&ul(+J z{o;4|e(jb6>)-hLpVW8H&W59%wbReBj@&nDtOG>rEI3C5RAv~}E7<=A=e2S-L{QO@ ztsU0ckg@8HIIG*$2g3jWAOJ~3K~yS#iHKbIt9c%cXFvWQKL7u`?{%HE;P*aw-!1o@ zZLNA)H)*ZNUOTzrj^nTW!Q;ZC0kq2k@;>OT-%@tKc{LUan9$P~{qwWIzLMn%i~_Ad z6`&lri)klKS8p7j3ZYSFGNTRvI9-x8KybP|Nnpqpg`hcI%wsCp6pQLm2@kq{WeNt- zBoX0&(a-bpK(7Wu%0^?}_Bu{2>rUY+Z|c2CQ0QaeSm+e9@^igUCp-=^4aMo02Bk?>O0q>kX#Qqr$HCY>W31_ z(#+3wz!F{PW&mAau6Oawb%!`dB$cf$cAxe6ZDS)(%`E^xMdc(QAwylJVQs^;AwXL8 zAW8tBEP4%;xm&VeR+V?(gWs)``jb!mm;3IyZR^<*amK8J%p#Csh{%}I8AHd=8SC}* z9xplY$A9*vuRV4_MsVA!cK-JFJ@lDBd1>K7!ISvHg;Q03y35U3jfiF53F48>A@*W# z{uH#P20@~T^7Z-1i~O)LQ`>pQwg8~r z%A1p;+?vfZe5lw&L;wtQ3DQSE5{Tt6N4YVtrabXdr%lKRh*XI(7!VV{1k&exNzXf` zCdJHpKF0!a<8Aj|d(Ayx`}*IovE)=XHyg*Zye(bLtj31NcqDSets>&tx%u;9+o37l zLPQ5=f^>g4sg!zJU$`;eb!`5^#bABaTM5Gb10pXV;+#6?C*6Bqf%Ef^b=t5qPM&#q z{qT<0amj!H0SQnNDCd9(0g3u)XzKrp;9S+yXtT^FB6H4Yl}axr+ITR}c%v)2J+-U- zJWL1*(6 z#62mfP76=9@7#Md^!+SvKW;9-;5pGBLymCiJgMd_Lo&!v2BZpLOoaTt@B9t~a*mKQ zat;y6SzsIy8DYSK(t*!hP9*?v5EiT;449|E3SfkW(IxY~*Uf(5Ew8)Mn%Znv^sQN5 zuCA``pG5$0gP4U2aL$uDH$2(Ew-Nz@AWLBDjS1|C|BY*I zZHTq#ba6nBw(Dj2%<4A|95@=qeb3=X8yKvopM7d^X~{x4zW0FA+2MWr+k8$!-CFp| z&;BpplS-*QyQeNc;VOyd95@2On6cVJ@I6B)j-BY9IB|aG;kPuK%_kmt^1puM)tupq zF2Dr8{I1tO`OL-N{eQoB$9=B{#LID`&T40r@Z%sJ9cEV*=@V-&bwYl9u(!rSU2X-I z0l?NoM`5|gnEXdD4$5BXZKUb?7if(_L~9h|!V4Qi|9ClOfH7fp!Q~ZQIdai;xdZCi zJhf$N3&zkn=mRIE=#|~X?-Pq-)LqMKeCt!BEQ{xkJ$2;JE9ybb_Y(k^5Va1srlYT) z+5ftcTe70_s5#Tew>n~s5qbbriC{5L4#A-ypn_^{LWAj@QSFKK4wu|o%cF$IR+3%T zS|(X0)|n7XYw0I0KHVUZJwg~iPq?%~D?7xj5#m%{o?BlzG&WgxBNv{YAKSTu*Mb;g z0Jylk{Cu=*m;wUiShzDqap1gLIhLM>fPHcOSuH9hsiF(*Soy1Mee(2ryHmQj9A#7M zxU_4wRutwJPd(augZ}cd6F++7ZBZOL>+Y-_;brkCJIh!_$09qBB}Il9B~)ZAN}P3` zrOo&-M69YzX90Jmb#&tsnmRzyHkd--bxF(caipku*9_e);gix%%8w)3+Yz zN)pWH7&*=bM~My?F%E*r5ds1OLc{eOKhx0mFy99NXN#wE#>o-^Rc8wT=W=t`=*V;F zLT&+o3F`znk8__hBYXkqiJ&N_0fa?3gW~&d3i*W&h&ezlDIgyeE=+zXz9dpmOvAM-WQ?JEEKZVI`2k(>ZpWbAr5( zB2Vf-gg)^-Qi3e&v~Tl-&)00w__hkpSM;rC=a#NJyu0!QLx)3Q5dWh zdBvi+zFu4H<`H_`Fjsk*J@dpHZXA!J5h>+p)I@}vua7&Ob7`-v`BG&k=9eZ8>>Ea9 z8zJ6PPd_%k=brfs-zxaoS|cQ&n%ljy+I!!JKK9US?tT6rKL5F|eeSbg`s?ej+b>zX zUUZQ$0IG?AaSwr62aHKhW#SP4b(H&`fBoXon;v}q8}~l(?9)$uYvp?$n2KN=h)wX8 z;e2^zVP+b`fFAqC@%!$0U^LzswW=8(7yhtnfzWZ7^H7FniLvHTS zwkUrGqe6B6aS0&5=BI0`UhH$>4+-OyGi7~>dU^G4xIBq*$r&>7_{z6BUcb1^7^|v( zLo8ut^8^N>4WeA9S`D`PR;5d2N@ESMzL%dA^^D}OF{};D(sO{nnPIW8zDn$?M`2`RmnZC>dogIQkk+qR)|{T*z!_7F&%$m z?y0pSB>>!-&%XKgy=Tv!-?jfXE_iiBMQ}VT4p|Co2@omKYOz(j<}b*WpschN-h+_4 zi+GB4CRrk^b;^`H3Qs@v!VMEBL2@tnnVFvaW~P{gMCx zcPKw~;ndNC*Ko#}G0)^@Q4VCP9SMIBh&V!IM7GLo_;QXlTb(2cQc#exY)OcKU{kwt9$d?Iri|LI zyr7mW%7}AB%aICGwo2<^;BlXb4522b9WA820C+PP{SJRu>hyE@nU!o^NU_>oXRDFl zl)C6(;BDYrrEu1<#J4@oMntvIsh+i*b6cc57;s*_h;#`E-1jT=aRvA#FLY(f!&z!_9NHrJ@*%5(>YHt=g^&lVO>(BwtShTq2SbDOo-GhRW_o})7+ zTV0&1_A>pKWDr@)U)i`5k^?I%E5cvh5uekgJlS1&(h(71d5-*$IGyjESzb+i$vjVv zk2YL46Cigq>QiGoq>!uYt;O=pl#v0B3mz~ojLL-fuf9L0ck3aV0BYSoq+A}g4`w;+I zexTi1o*iM2Kf4-D-Z;8r&t0!>X{|gje&Cf4^sa5!YW*0wFaf#MR_8nry!~B2C0PAm z{p8zz_Cp`Jd6E?xW6uGyA;NUUbk3=j+!Qy}4(twQcf%!pACU=X6Q^e?|Yug}+%y<#J;$r`C0Cxa(1M7N9Sl4CIV+RkH>F zc%<_@6sM~tcXxOw!}|DT-xo)<{d{tYDf_n zzq9!0>M@rWi`O4AQ#!eQpWtFIY60B+;cxznpZ%53{HG7x- zahuQ!b=qZqWQ@80d!K#ev)4TP^vJ{C46eQ7dE)S6}L#(6BZ< zZOXbB^`8}6n47?h9f7o=kNlt<9gD_SZ1-fjxMaIs(*po&?8w1d z`-RooQJ)x~b=3qwhDIAuIr0j^zKut0RPw;TWPq_sjw5PUACAM;nrT9$UuhQ7UOICwYR*y97WMc z%(ld>&S*rbwN+`_>2zjCnvX4CfK$fBdtuED^C#-Pn%-V0vcH;)Q2DXNzdZk-g)}%3VA6t*tLt z9}WEt-xqSTZsq>H*Q9Azn_iTRec(U*(l5OK{lELs4~>Kt0E7&jDK*1q^Ro-8b!{}e zC)$6dab(Ygu80^#NfOr~->AzwVqz1UvmxU}(;DUk z0Kmx}Z`JIGy_64Xi6Igq(%~dIr!;ao2(`GimI)3dY+5j-DX4KFth3{LcaQGg^-6po z?W9jXe;kb>862oZh*%v_gfTECIOCmk*|eyZ03ok&6yxsV5}Tbw#?klYUP^VX6{pW$ zb7*vbeMhN!id>RtpcNp&FeWS!jn!UKw`qqtVMll~kv8S|(S4Ioo}OA7E$8wTaE4Sz z1q_jKE-KwcopwcB>nBEtm=Kjvj&|F$(Y@Sui00G$LX}diGzLlcZ`0^hJ~G)`UrJ6r zH*?)9FAOzYOsN@XWG#?m!UN8SLmffDm}rwK$qK6g0&-POfSDraeqgO-$dzP?^$2ypk!Xa47V<-FD?LOUZSY={5w}~MtEkghx+w@5>IA?MK zV6Fs!n$u79mNf%s^^C0FT)*z`d(T##2UnKn8`ti1WRc0uIRWm6#E=P}s&+4%asmJj z5jQxmRZ&z(9t6&mh#*BYib24GvshLid9k}b!Rt*9hyCDq@e1&sYDgO84a;X8B7s5J znkN(icIUF%&MWRY`fuCvyCI%iuU2D1%EIV^c>q8J7uHx53*WCdrxGJntyUwVm)hs^ z$N~XyL0SXj##qRE)`dYJ*6e(6Wc*u?Nn0w;e58J9k4w%OdUWE#>76@v-mc9u$DTwT zxG2;UoJr#J4-Y@#oVz%;@VZT};<<+KZyh^S-@U!>bI#~8m0rY{ zfGcd?3oM!1hQPYwpmuOiM12tt$)F zOV}?2_g5?;g|)#M_WvhGVGcQ}cz|d-cg^@9Npve)EvzkBvPLH~Ffi>Ue zHr^jDz0esE{-nQyVMDv1OR2S808V7dtzEp}-F@Hp&$n7f#|vPk)-hr*@7kdqoonf{ zW1w|6V71b&x*z~jRCUAyvc};6Sk|vxdttvTNA&=<${9s{otJIQlfiiEEka&hnza@f zA4;2FbJMx6e*W6$ADemo50%yl(xovPqgn;S@WJP@VVzL6tcH3VJpfp;J+K-o79xT( zt|B^bD5e8A6waDFPXk56lGog&Qlhx%WjP3WunX*1vFLZi{ zag&#-5G=f`5f&g^0X1`pUDneDZOGMPt7EL)zjJiYSU(t%mXG?SF4s!%>$>Q)gI=1J zR$`2UO-Ihc#91ez#1aT0SM(ZE2d1O$G0hmOo)X5|UN?=x@W}r7{Kda}{)sd9-E{}R zs5Nm_7+qRkeeVZ<<9l9v=lA}z*U#?UG1_QuIYTbcl-}&j#FrmG9Yr|O94(8Y$tP-PfyGCmEpt7G^#jX2Bm)~Q~53A^ngWi^HjOUfg-q{?JXNzPiUy`WYJDQtmKDKZ!J zTF2$fx}BBkRd8VB$|oSiKd${jWe%w4imuCBIxTts4)#M22F~@P0JIvU2bB`5bqtMi zEM}KXHD5T8Hi1i?;h+EeN8bI89~=H!XJMtk*Uz_BJzYkSYi^|-%0D}D3>ko#^t>0+ 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%0ACk?Iwf9r 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

    R% z?>+AYkpO|6qYz8QqjJ}Ft@SOu)jPj@e(X;^cHhQIm6W{i=O4TC9Y;z{?%}Y1=zwdyU=yF|iwf=mk-cFKPT)A0o9C*f#BBRNd}qEjI7ckPtnibILPVJ1AV9 zXVG{oeC7Ch4$$dkLr)f@9^}(IrQrNvr@z*Ikrd&khjy&PTWRvi;mPX7-d*$eJ-qk@ zM;2V+yaE814S~=(i=>fAf*27Qz=<n=A+{ zyRY8)b07Pu-}|*cyzAY!D?;5aX4Gs`>eG{ZO)UU`e6X%U1pwlPb32F4R?^;xzVUb0 z-0?uL_ljOnX8;fQkaj;IzwCJ+tXxc_ohGOa?jlNLQJ?Xt5?t-DKgekNr2w;&}F) zLKaJfkpZA!1ON-D*?W&f5i&Y}Ac%aR{rA%oKp;gZAc$M&=PhqOap)9kExh&dFYo#6AKjUyx;c^l#Q*lKJ=ZJ) zs8KPP^NN(whCBfdLfP+w-rVku(?=%G9GTg7-7@-IOcep0bKm{k6UPponBRNnFMs^k zbdZs^-WEuNIsicMmXewZ!g^BQd0@w}BP&DX%&V`kD@*yvoo;t>V%&QU!(iOU%Y_Ysz_WnRFmOXICW_j2Mg0VBc?F=vT3h1=V`{zjT#amBt$!vlcpQB@lUXf7$mh!ub`N2@|46~>{+ zRxcrYheU(C--VpKpV60y7QzlTy~Kms?C#$AVRe4Fds>Jt;jK}Oh zdB;*b1zr($@n(tT`gLzF3?z&KH571HJ! zd?a=p#=6KMsa!T2ct%2WOo~dqE2~%&vIrqyr{AoNW7h2@8k}cMslut8E=FX41JZ}` zcA*A+=x`DRh4oRu%qgU#DAj3i;F|TRFeMCPVJ=MJjl_`jXo$L>D<{sa27>{gLJ~k} zN<82pVeY7DiZzbIa+-3sDBp-;oXM08fslmAS9H~4PfH=LBJn?fw{nq#2$6`Bh=Ne` zo9^y>D&fJ9p8dw|xBd8Y;(3Iy_>smaA_5RQUt%le-vEG!Tof1wviZV?O`RLhmoOdg z@zZ~J`{Q4}5&*_$d%y6puZ&IgN;si>^+i##zOiyZUBk9?8P1Vue&5>ZBNN9DPrmiR zLQL$f|ITM0edG(TOfLL%HvHIE{$}do&$ti&(nHtWbrFI@aMQb3ha%##6qI@Uhu-?1 z{^ftY;>JBAFpG>Jv5o+M%h|T18F(&*LPP|W@c^I$1`rj%C5i%|iM~J0WXDgNITD^s2aI=Cwsn7ZJ6$BKJoU^uT zH+4(Lv^-eyOTMwA*-5jBESyLx)T9pBbDiFFHIaa&5F)zzsw*D)^4H#a&l{`N8d0BF zlp^QZ7-NgVT8D@wnz1lji#AQXg*_sqc@f0n$XM7p5hSlMA>^D3!z_^$Gd}a)d$vQw zu+l>CXO~tkuC46fyE~~9N*C;O93pxrAYKHOVi6Hw7zw)hf~`+4CNpyx32rQBD?RZk z4>ve1SvI@EiR~1&?QXuLOa?eza8TWQZg|c!2_OoJwE%>O;+=>>iCvzf2^a}b0iw9V zd8ZH@ODziD_{MiX^zFa>`-e_`=r?|P^1zN#7`{Xy0l*~5SPv?Pz5(~Z8fSs z(Lcc=C@9LLJbbeK-J4o>9%&!$L3=y;`6|4%DDpaz70yLAE0AADWB>rXQo?o%Z$(6x z7tkMicAy!XwfpbA`L`eYgbouEhOQXCs_3qtuCK@nB8ruFf8?oacQg+@Iezd+u)cxq z4b$G3JN*0{tmpscU%b4OE_+aA3PBat8~y6pKo9`EcX0V^Q0_|6D-Nu80MK^57E~2?rXI}kMDZ>hn_c;iuW?| zTi+^zMkS0yC1hnj&Xe)=q%Eav=%PGvD6x!Xvu?V9(Vn z|L9+RGp-D=gkohx>LNuqTz5NfLbO6XL*sM1+92!gQ?I1Y+2@y^{o-Go=x+R6n0#Vm zbwtLrxqj$x@44nppV69Fcr2aQEc7#cq#_z<1)FU+MLix_x0cU5T%~?S^u#q_uw6 zJ05uc;7bSg&LNWXqKFXD+M=w&f~|xkZ1sjO%Y2>I1Q0NbfR1A4tU?520bx*x;5qZo zdUj^>ZrA4H|ik^y`$6(sE zUm+3|$6eRym@ppGCsPd9+ck}atBXN7M1;&9#f2jTJP0c$Fcc1NXM_rEa0>DTmxstn0^H%_IZYTiE z&gg(>I|R?-3NmGS1KmbS&O2|-*cclwKMG&yJ@?)R@BhRf{>eZ6<&VC$!liB+U74_l~{grP-pZ{|B(AUCuz5{4qpG;KxM}?%BXk>4-CEg@lD&9>5 z(*qa`ivIRpf*O2*q?A_dJqD2gD;$K<1m7JDht_ZsSG`iC=qxYI+3G7nj(`}4m_Z0o zAd*%lO{c<%)MZP?*Hj%XZWL=e(VXWMT~kT$%J3OaIeM?c*n6+zn2Ul+RaT_)_Uue$ zo8!LRG6b5ri#+faMJ(Q?q+kg&RNjlP9m-d+UGs-g_YJy@xGZTmbKU$YOFPYcBRZ-l~C6!QEG?Dc%JnVg(lC_!~|AprN9G# zA`uCU0YvuRIxj<&X2dMI#YkS_bYej@y1=`Q6u?1aS0{29E*1>3YE3G<2xP<;Q7(TP{=*_J+oEw}v zZ%>y}N0%b4JYgJiH^)Xmh%8XJ;phOziU3f&w}9Sz4=$AyAZ5{rwS+a0fU`rCu#D@0 zfw_I*`A?lcd-mLp9SZ9D(xn?+SuG1xtG%dxdDfhZRCff4U@;Z0%EBwmCLC;$%` zqyi%1r(M%gvcGyDwL|N2)JoF6*8!A1DXwUS69JQzq78$#J2jpwWzahxs8OP*utU+w z=tioF&!;dOh+`076W}ltBY81I|9x^<@cQ2t_uScU*16rKSDx89F?&!4L2mKWfB2>+ zzViB%$=5k60x407NwZe0G?NykSZR(c_WlnZy6VB(5cC@eRH zRHblX-zu58FMZ|Q>2t}sm7fR`b<>+p-13&=d#*mG1H;V64$i*t-5n48`sWzc9+G0kc3UeKul22%u7k1O(d?xR>VE&4n9jVlf2b z@|+65N-PKj$n3y`JuGw*x#8gIpnCIdcc;bh89ck!N3H2SJC;s-^$$Mv>mPh`qrJ8o zgiIZqR)$FmdHHGCwH>hn2vIX+-@a<{%1&M^AysNogRmT0&Abuy7Qgn;pH0kMGc~ho zVthP~qpPkw@WK=S?fUmz4FF0}VXacS>?H)yT4&iv4}p{}rvW4cB2u6T9kWt8FNzQx zp&|{WKm_8XL4y5Z->Y=T-towoerF?UIGZ&p^+{lM-g)+*2$VwRExEFi=hq+PRjgQD z1AvWA`^xF@P=`4H2@8&tCD52|(X9ttWoGJ9RW$$rOt_@qZF?T3Q&W9NsS${z2u8#M zL|6=|QB@`TC5nfh&&UQeVXyatS6+;@S!?1x+Ci><JD}9(UR&*RQKCE-6xMM{mEV7(ygYJ zw}0p3zy0ywd~R?s#q^S;NI(hu96@P?Ss~m%!G{+4!o-=0D|g+ZmHc1-s{4VDjIFGM zfA@*IKJqUfE?>q(NF*%Vm;j}=76GhlSfgfWnmVE(XCaxh_Dr+h z3dA{8iIfypLKBGLEbsMN3J@r60@8lkaTL$Dd3~04T$J+?sn{A1)&dd{PWO)M2~-#C*7CUvkAsdy=Y6U*tN9opnZ=LAurn^%f4kX54HMtjDC3LXRblx14;~ zvkF31U5$wkNN0tQ0%jZN*inJzd+WCX0KDF+Q!BaujlEBOvGMXVGk3q|;QC7Z=O4fS z)Jqcpu>Xd|`+xY5*13rY&;=m^SZ#pFYr`;D)vcz|tQ3#*nYMvPTeH$AZE9WlWsR3R z$gbm1Y<@)$ixU>E2lZ()Z}Y`p`IYvc{mC!)z4^e;zI4?+f0I-#0Eo5T69ByJN1r|T z*p5Lr{LbHBb>9b{2k(#wQ~)GQC@kK2jFP+GdCRB&{nvl(|M_RD{v;xL>qtWhD3=LV z01)qKlM(T{C|G$T&N@VLA{a!HdGQWFoOhr@H|#1hm=PqE=6bK!X|IhL9}TlgKTTJ91BhmKFR-olCnp!CB%(_N ztcWT|n(&$}v3fx1XxW}J5`aUGc|ToWqQUwgcRn8!dC|_<*{suFrwD|n8)@LzUU}wo zPxLSd>$6kA8*Ul9aQ@uf{6dKYY%NJjV4ajMvc3so0AX*Hi9rOI9ed~55CN~NxNE5D zaDtgQ7eZ8^RyWqx+N%q@$BJTz7*mwy{QwFtDtCG2ib4qlait7y@4dBF5gB8gbF1yN zM$MvxAgI#GWdf_`A@|Osrt&!gK*ta3aLFy4Nat;rx%GABI{nU~U6YB5EIE%}c$3E> z3^vCgVr$97mzdO>MnpA?)9F2CkVb>GPk!y&yRH~}^2no+qEN>=UauEG(Uk3-?YaK4 zUG2kwD}=Gx8Ukg?VJp(0#N3%hqy(dmXD@ZRe?#@QXWLJR@&GU!&Kfj|Jt(#87R!;E z7lRRX0R#Yeks$#WI|!lZtWX$1g6y~DLdv`-lGs8>mHo5Y=#J0E2cLb#?MXfP?PD`5 zimFLW3S#OPp(nNr8v4azWARv2qp=C|kAAZF|NeO9;G;X9{pRi)s)}(yHC9|K?Dzc@c9RR`~@Nb?|}4W z{8mLEzyVOgB7$^DIs5zYTa{9yMC~@bRX}u<#Ksus>)4p0*d_R8pL3?8T z`9k%V$b%37jNUMW(+ev%z3KF`-`e%9|8d=$A3PGq_7dLeM+TEh>H2shn1B~gKRL5= ziikvO!gA%h4R3}2X}oo7Ra-bv1`$OWQY4PFBF{nyEP`yoJJcbyCZ1nf{PZWj@W1}b z&-TaE%bt-C8a3%}Q~;|=@7TG|+QLHCG3uPl{H_TIVw*JXsNDpERityT`>rwxxHtK5(9QEqL{J;{J}-)rnxeJVO67Sjh|7z^tCEF_Apt;E zt>(*v`ML3n()PmJ!>puK_<}c@;a0yRJkfW=HV{aQ~?H7+F*d;tYR zk*g}$G*d=18Ko3w17LTwn%op%jLBrAf9u;%{LBCI11FWNqemQw^dSPmqg>s@TTLR% zeX531`ne)p;EOWtCLw87k`K{N!!=s@1jCcRsNGnXg8Rr<#B9@q4yH?wMzH{?p(6lK1x2cq>2= z2Z3;bxP8|!5{CDYksr^~uF`>sXKxX}6+^Dcm2dO_y6b%BLZGspiP zJk9c2ygp{1iQ3{IR}zBtrzO=2`tp#+3TNnfwMd@?-qt$S`b zFgG)oM7kuJ1STlyr~tss%p8Q_SgWOtNr&n3>P8$VrK-+W21{#=L`X_&Ey5z6NPB?I8p?w_yhQr=kXZ^y;GHSJNVScLFL`1KA?y@*auuv+DHA|jnfl`gQ zYP1HW8DKQyBU)R8VOV%Rb$E53+8G>Z9GI(JQ|A|ZvbPl+OV0zd2tc>sZYG88w>u~E z#l!ym8CqJY<^w(SVR$LbM1ae%0MRr;1_gi=5Mfdi08~bhz~-E%7+1iAAhj7k)vEFn zFTAkQ?!EJE@AKBSWpIg@_^QBmTuMrgA{;LdMG&$1DDcX+wKiglTooJX>h<+oSJEZN z8IUU1BSNe)!pI;f>xQF9E10sJa|Y!AKx~dljr$!i;U$f_Z7)JDvQp3v06>!?clP6;AbfZEMr!P3Mn1GoKEEYBQhN+3v0PdqQ5>- znVrDqTJLN|Jdj=!$31xTjuj=2rg5(3>rI-SFf;S5Jqg7LRfw?QY&@Qi3S(_&NFEVI zY03Ivf~`wJQpWp~+(=L_8v+p^Zap%I4LA`~N-~lvG9mrkmk-X(?7sj0`{8xgts(&E ziZqC-L>fo~fC;MrKqNo=qw630f2Y6w_ty>k0RYVI?v(=!0GKT@p*Elta4FJCfCx&7 z%TaA(Vk#wr4pthkZu%eE1{PxJI5!>y0ir6+>k+YE^rk8;+8AP#0F>rkqcp|j-0+?U zk9_TKuld&hxaOW8I-<3I^+~-06Er8nh5d8K51u-3^F9E8q$)*LB44A!bSyB6~Q+V1x+kQx$q z8nK_4s3(|H9uQYoD8B@10ifRCjcABwmufr+bhc;9-{=tVgShUA%=Oe)LHUWf7mnR?q#smxd+W-5Xe5T)xs*Mx?{^s}Y zdhE;l9{S5$Z+YvvxSGRjHWfr(y-q(2e7`qr=ioCt3@_w*OmW1D$k{xq)Gw3P1GoU4 zVfV>v<7=;tuR3Utq#<@;<0{@SVcMun*TS(Z%Ql1$+4ThlK3zJIy__rFAMU?UoX^GH zQoU=aigfUbEj{9@b+82lmzWZ{_?n|(tjfJI?3E<1}4ZXCDBA z!9E~J0uTWb5NWO=DUu*9QVz@1Z-*@}p-_Y!vO}Wn@Iu)lLzYR9;tEj=k`Oq6vCjaQ zeP4QCYOlB6B{R?QkNfIX&0q$!!#DazzgO?(a`R-~lV5)4JA@zq*&q1L|NOVlzvFg4 z*fv=!dap8&s1Q9Gs|qZTVO~^wnTmiLRYOiwJa7ljCg;+a`Py5h5l%PFjqCAdqDjJO zlvr`;qi4i}j0_5~LhBT0>)Lj^*K(G;_b-;?dtCQIZl5dapooN*WoLUb1b|(x8h4)> zcZ|TRpX^Rwef{G8&bMMw5l^$0uCLj1OKB&wzz%8Uz{5b1Kt%W5zS>{AJe^+7ySB~! zRb0Px`NnOxtu=@kA{t}rx&{CRV4}LN<>0i}8XH2OX5Wgmy1KHxwY|8s=v)%K*CzIy zx$(*nQ2~)yi%E?nLZa~4)6WA}X=?htW5<`5s;Zb4V*p4~^rpm2q;k{gBuUc5rN$vuN{1wnJ6H zB&ZN$B=J#^K=r7ZHQ8JYNXyc+&?G@bl?X8+sR%U5iYSz76A}f0RqhG2E|xd?wT`Rm z@5=rVc(BFrgT0RR$`wm0&9oRZ3#0gUP#oIwn+bIf$TfaXc^`H=mm`OrUX{ zLojSn(HdL$;#2?8CqMtYkN)s~_=%&(3B)w1C#g-#P|h%cM)i*9%&9x~_O@8|YKg7x zRH6dH5C77)pL^`&Xg~SW|Kow5`G=3c2@_CN3SJQBXKS;@AjVKkWL(FPwsMz`c3K17 z0j!imZ1;O5l=GY}&9mLb@ngfs-qn8RyX-rPYBVu^rFASAkh+|VH@0MVuwOmUJzH$; zQ`an*g#yA}*z@%?p}Z=qQ^_62Xyv8J(}k^6L48eZ8Wo7(16X6rT9szoaez1)uyewe zjS0n6xM+=5scTVTJRQaMjI{eUYiqN>5;tN(Z~8t@<%hm+^ap=Fb2(SqQR@m_Hj}fI z0P)Bzh>dKkLIMB3FGeO@5`7I|9RPe;*4FeBGeII?RP0E1OmCKgM!;@fe&k19_>14a z|It6X|9wC7sM?+fh(Lvn4b65w%AE7TL{T*7woOcj$r#%Y3s-k;oH=s_0Gg9`0I2J_flyUN zR2z^PjRAltk%<%lVnl#E&$Cvxd22JvvR<#(c$6X%qBv(lS=1^~KudppXK!3Wniw?h z)XCfWOG6uubBt;%6A>}GVYKSU%WUg*y8{4TZ6s!Z7^Oj1o3(XD4+ue^aa~ze2RCj# zbl=jKzy7;5rq-B#*I(bNarM0AH+o)icGQk8fvSEBHcX??ks2Krv!E>`K$LO}b7_JY z!DRrzNIg2fw=d_9_5Rtv{8yj&vCQUTBp8#i`tQrD9Thz-2^q9ykq_Izw_6= z_Qt+pacFS7H{Y&$@zP7(ZVv|vM_jtRQ~lU4KK8GE)q|^?X7Fwyd6LEqc0`t6cJn4HpGaS1MiP-x?WFeAZJBjXyMpWHoDotodVfYQIXA5 zYtCCTofuAiqL=Bl`f0KPLEyh*L@t=HBh>y?w;wDlIc!ax0~{rTGZk=q}7DvT?s zX5zapRbL@Hm`i&g006bRQcN&O)1ZCq5z$S(q2&Y!Rbh@y34%t^XjBA{)R$l!COIIY zV$?XrX~G!dp5X;+ZLMGo`C^o0tq^g+tlq?%vsOw(aENGptPFnmC!YVxU*0*~&A#}D z_kZl~JOKcEo9!1LKi%o>-}mtAz=%;X?|kA{e&k>Lm;d@de)t{$Kx4ITl+E7Q)mI!uU&lVhEno4U%GRZ7nY8> zS5sX2}5n&8<9@}FWoH8e0zj+l_LO{=A<|IUBW@8i}#u?qR zS;A%kcntxLA;bz1c9U`Y&W_#Vn>TKXNRp)8ZreIHE$ZpCnoOqK+uOUNwAZFnCznNK zf9x(e!<32hzIK!u)r#G|Ld*hwelf1f5*QBeg(Kz6ZDu8fkwBAP%&U$| z2%uJUJjRs{XvC~N|2-mV46|gHF;dow5FX$A%9-@e^PPLQMmM9;j_oPvtCMG=6qvOR z>-7~N9((|FDMSyU25TBiL&YZ9k`Ez{*&MQ^Pl(!pFa8mLPTz8&5b3a zqWHpZ+vWCdJw^f)Y`Wk4;N2g5`CDJU^ZDq(fvuBijR~k2unK9Sbq$bgmiynh z_~^Ob*)y_?HnC~>m~??nX5I#fQ*vwq{@op!% zv32!TX`f6oHyb2lQ6dl%5wZbaqH*&PBoH7*LYM|>9e$612oW06u0(PvBEBoT{e@|@ zNdSPVP)%fKhg=2|Pl?0aQp;eqcihNMj%XXm#roXZKFuwY_<@x4W53ii^wLYvZl8&O%TjP&97VT4r(yF-XdFCd1))vo)Idfg`&Foi5~8 z{dFZ25=+*Qi5dqwJk9I-VYis>^t1IF*KT&&os?<9(~{!DorjI0MYC%PF-U?|krkA% z6YPcF12^QBz${^4v&5}%f3Ul2t?OnH68ft@fBeMC*|#~%I`X1A8oP4z`bWRM@yfM) zb+rIUPrEahCztdfdK%fqy)8V5*QTruhWhEsf;ptJ8!$cUZ&zF2#cw597W(Y0{AoVt~DzqjQLn@SLgfrw4g=swt(lrYhGUIUjQqQ)439{#}>KL307 z))jyDH{N%&FBe}}ZYX#FAl3APpMLM}{L?>Z)LSG(@?Lz6_DBMSBio`k@LL7I3;2Kt zT36VlCjf%+5Yv`2qs~5n!H;(R**L1yvJSqukiL z+3n^aP+^&Aip(k!LIBm~Y^upW2mm337$X4cfqVoYT)TeRTI-emV^qFY>A zyz9b+7^6#)=xZ{j9jeElxVX8WH!kPSsQ>Iso9}vHId6>sshso?GXM|~1qdM+wngDp z4S-WsP&Ld7V2vrtvYCD%1UW#f6tJD8MJPfd47N$l4aN|{YPx*Kx#?G*e(Lu3ynCjy zp$e+=%oI}9#L4_=YgL015=syXhe8=L+QYnqOsb+GHh&sQSTNn&d-r_y@xS+9>It?N~}8|%n&)G!c0tBFQwR2DI5=`jizu#*6YhQmY~nk(#=m|D$`Y>y1c&IC(~ zDMKeT>zbD1+DwL{YY&wlTUlK-#>5aK9<-SdLli)a3Nb*#E1H4e40WlsN0;J}lpLrk z5{}yT-~QE4{O*7MhrjkuK67-EMv5(J)v<00?B>bE(cTnsAzK>gKsuroz%F>KTb!3+ zd~9X!@BPMX{o}jue)#g~3!C#(vII>9 z3N=X1t7HFqG2S1iDP=qRdnQW=7`<G3E?@PAcD3r1yXVfH1Av$JE>C3tZ13JDrcdwt{d4*4ak{Ij_vYu0wN?Oh zus>Q_Is*WU<>C#06A3l=MaI*ePOfxuw14u%*;h;2Ekk)kneJ`TADgs%HU94JRpNaY zMxXlrt=mtJi)op1dVBj0Bs7Sp-0=@~-uKYLdu?}t@~-Lhi48N4!{|+g9Cz#39DOhu zHf$IX*)Y0<35kf9P$Ll%QQs^YFm2mWOIf-;neHWtyS1^=&653E(pJy7tkF?jrqww| zhqD|2*szIUjOp|yANkZ%jq#khd;6z<{VSjP%o6}`>+*ssBOx(y+H$1brOy7=01<7c z558jOngWrKh>j)%W5Yz$)JO$!%K`vNK$o)N=J#X_A~w*KAq4T0XiLd3F?V$!Xv zB?;6ltj&$hGOYl{w3(eThKWLmA;uV^51#1IRvYCJRb@on*xfLj%0Y>0iBV<%Lhkx2 z*}}u;iyM!B>EaX5A}GZQ%IV?QF%uKc?adft3;>vx1s|J{MM9J3$mTGa8-iiJF-~tw zV~!3h#gsR=G_AN52hm|;*x)Ou#^k4<(X@M*cWk#$X`7fR&6-gLfLV>!AR^}pxC&F8 zPluUdbPcbj*a{d*9PdD=O}mFSvBwrv@f+XxMqSs(;(CiZjinKqBw0Y05fe1I?Hgdb zNifxFBLWl5?3rh&4c=he2>s>$>dEC-o_f7`*BnE$@wdNq*9V?_=j}zi=VvuCed^~Q?e&Ke@%R3R4~Ago zjj8>}kLuFw7WMY}+S!)P0jX28twe)8#j};*C8s3=R|ThcYChN=rsv2+pbb){|eEPA7Rf za}7^trx!^c>b2h2@3vz7++a8g@yw|^WAO83VSatDOm>K*DiS>aHV_^HstU197?&11 z+j|pZ%qe%e4S83REMEeEcCs+sn{d0GL!R4Ko7?nVEBI=%x?sPLkQ_P${ObN!#xRO8 zB60Kqh*cD9B6Wo>iHZ-|N0ei2N)i%(EJ=1xoIGJ9Oha_DL|7#e^=bJ^yckgdL480r zp_oLCFs(J#f9n^XdCw;1*1&)>}Kh*BpJS==cBlBY*fm|Jz^w zFMck@s4-{-fE5}CF@UiV^sV&-1e8OwC5I;Hc(zm|5JOZ(#Ha)kA_BPdbN}NX{_rn- z92A5N#L)bF<{?%k7&qh3hB_2uFcyr{o;i{9&mVvI0>ZZk-^w9>_Qmb<=WRJI*;egV z7jd@xkF%)INX!lJT1=|9cnQyjTKV`nS^)D34L#^61%5fN)`jAfddhP1D0o5e4L zX_6sPt-^>TDkRpAGEOt&N`NBD5LA*hQ%O#pKDEEUUyfH0;9d70d+f;efp_rZPhMBK2TY1&V^tv=i2^R0MbRiU8Fps@W{J_wp{#X5LCYy78O&qP zq9DK>?&PJdy-9xK>A!q_^?`K(C0{YL0@)^x#9L`Wu;d!%Xo;) zKk2bt~x;FLRgF1>v%J>KQ+cQ32}z`z-0 z2__9HeB2#pV$kNDNf;^wBqUBUL@anLBI>bi`bZ?PYi!kNx2v*hXDwz%!Zb^bwZfD^ zf{5r#s0*s6K!#ggNHT79iD+?kvU+N)z9OdeGkZ3P0I+d+k&v*7K_*^YU1;}OS6{pW zDu_T?PD#$zQIa93hCp?p!Bg~FO)&=a6(T@2Mz(BN0ht*T=8g|)gVvF%l1A|L;MVs3 z-t^9UPZALVvb9=Q%}G5OMq^P0t0~kavjLl$tc`5grC3%kPhWhwcnJW!@oQ(^HQpTs zn;>D5I3L2*YtNSDO`;flJ(l4x?8g`tz*vh!A`y@#(+Q=hphy@Y3Pe>k)^f9P76E|a zXfPTN0YGHJX3TEGWdkA~gA<{4t0N+yj;Q55$B@UcL~w%eHiQ*~E}|LikI6VT#ixN0N`dSN_2--T|r+F9Kgll9V|z5i^HS)zvim3cx4MHSk4~K$8#LRZ)pCimFH> zaS6;UMOhoc#5mJN1tJO|_z*&r5JJNFvFr>WvYl~M*}5jP1^}RVoOy4Ah-7UuOn^Ws zqX2=Db%@jqXmeNBw#sttfrZv;y7U)+_NDj#;6tF$%Ue5T$;LqRSWhWyQY$8tC3mv$EKYi88l{wnKI1^RJgwPdSca)tmOTbPXD3) zdvk3OQ;hY2LrP>F_`31>o4c49lNR`@sg#KkQ4QJ%^Li1{bXp(((MSIBcm8@_qk>wt zO&1oNMO~TC=)URCYcxKlwH!CcD8>`eUx_XDRcUmqQ`1U|x-ITGT5j=@J#qK-pZW2Z z0N{&%d~f;665SXSxkAfJUuyNa?M7?XI*T33+7XkiecQtFOBXK>hXV{=s)Di>xs+T= zSuTVey&7?~L6~*w;0f5F707e0y^X>R)RWi>LP=m~7BAydE*;yvd!~Jw-A1j?B57Go zCzIjL$*b3@E6?0|={uv{OVhGUGF52i%j@Nh=SQzRwf+3b`~p^QSnJ**cEDE?W78)0 z+|e!Jn=aH~SQl27Mw4Mb>BA8XeB<)(7J~d9*J`@<%JgM8&=ZhwpeCS#NKt3aX!eI` zx?E1{{r%15rKNg08CIc9T9p+d-h?Z|;Z-$`(iUcEY=Zeh0Du?+(1B7taf}g38+GlQ zSGo^MlLsLJKq#i4_&bmMU;pe+f9;=qMnizIfEdK3P?wOlkWtuRBLL-$A_5r|RZFBA ze1I52;JKfIh#De540R#CZXzZ=|A$}u^soM~`Z}^j#25jPLR|r|_*%-Tde7YjKoS55 zMF<{|VvOo|RP7@hRrSGd)|)2)jNup|G0qsbwsz*~)mN;&)<3?w$0Yz*vN^Y#hhU5W z^)9oO^Xj!xnX93~h>1%YCRX#KFcGr9bf_x7J#C7oD-4WV3wzO0g5B z1t3z|Qjtb(ZVpnIZy{XNi;goe5|SsU!y5-knLZ- z^vc=ZV%ke=zdzk6Kd|xxU)%hG*BOXE15e1rvOt%-K~I2fEGFR6gUBm@Py;7$BwyD3 z@4xG>{`?!0;UsHi&D^X*4FKwkJ}o8o@DQbE`~AXLb`m4l_SnRIzo`~Rx_3w-o@*H& zh{HiI`>CJZ_`;XY-@HZ7U+=y1kp!?ZL1OUa5`(`BKM|E*h+Tec$Q31g87G z==PSIhc;U(00SA+LMN(@=hlJ%`*GCR4owHVF){*YIvK{8HU*;SNUJf_it6CX1-uWA=wN*}-wkcj!h_(D zw+lm>t)W_`S%Soq;qFo8N48n?{~tK5Eov>e#Tm@ROvW?-5UCK;ERCWy#+W?IeCR40Sb3M8+DjjzAlzh?x-3SaaL?EdbcKvWT%J z@dRp|BbWA%uN*(S_WE;IxVuOorrjlLxz!;;HUxD+AX*m|#IPYIHcS?0Z+mMRdqWxz_CrnWH-rNN4|+vG`uT`d1cp>7ex*O(qb^GM#KPE+hkC;R+x!dRlKi3WIP@Z z2SY?0Po|S;Syt6}T9uWrt9o~^6{W0;fyxRqnPf=D8%M)BYz-zO4b3xb@Yd#$!c1op ztuahwj3ER7QB|yb$PFSI5hOw+Q4LWr;RcFnir5(%&wvG&w_&Fepc}faLTdPUy zSN`EIeg1d9IG>w|;1YFCxo%?Dk4|BWb_(Tw;r^Y1)Y&L(oupYqeqh6nU4XDzZxq!Y(b6;J%R;cZ zb;H>NeMP>&P(nFkhzCYu31mK?8`ZUA{l&IF3^)B7n9MG1hx%<-N!E%$TCKbe@Mbdh z+dI5(c`cZQ`Dk1_VO+z0M;^Towp=A5DI ze0ssEo9CVsk>G=hiiku}(dfN`)|KR~007r7_LZHG6`)xgLj2?}eC*%-)?cWoBFC~& zQ4tl<7(|T|AavP#7e4Y(_Reg-j6SNfO^qQ$3EmI)r7A^LZKg;OSwVvjs`~rC@y9># zlaGjq+B8naes4RLQvrmm6VWJJi4sFpz*%cUlp@AU6S-N$POa^h6w}wAeBsL1{`!uS zo5TnJUDncj6*SC8la7YbzQ0|4=JgWhQQwpD`KLWB4mh$h=Q*}@M)U*stPfNs;WaM%Q{VyqV#oL z8*8GkTaK(j0C6W;o(nPwkpW#U%eKo(g?)~N#T=$d2vu2*y`M&j4V#r{c4`3#Q6$C~ zMFAb2oPcv0BzEv3qH7Vd~j_$)?P3bA@y|(Q50hEja5WngK@!! z*)WNQa;z>_MTw$p_jV_DorXu=v;4rFd53WDLQ(Bk@4fHLUFTQsyPF@nzu125MNw$( zjUhJncKER|#V8U*M4H+)_@gx*{xz!9rN1(U91iadA&iG%I+40K{8D7<%Pl_u0HYN- z(y`ncynIjp!8B&`0ws!&3B}~-j1UnaD=}(}hphtDqMEn1g6iYYC;a#{`~l!nuUlWw-;eX&0p@0NpmPThO%*tt`k6Cp(L5X(udYpJUUtjMBJbzEVG zbt_g>mt0pl)?F$)5vI{zrKSN<6ukEeDk31w!w(W-jH56_fM<3e598e%VP~6%7ss#p zBoUjbN!29ca;eMU1K2c{Mf2!FG1YR4O#LI8^nb(JO6(-oZ~JFe(qOC+dGkuzND%zL zzgCh}0BFwB*)U}-B4U@21Qeeogc`@Y+lY;dA`JEi*Kd9k01UBFX?Bi?s2WjuIa@wF zoo}Lgm@t&hD|$R$`2F8_Z|U#RZE-4RU@Ld{aEub7GQ4yPIO%paK{XaKh^Z)ELkd3}ip0RhV) zwR^VJ1~P~3Y~IEi5Rq%v-I_&l-SApU))Enc&~)>|@5>kZxk^+CNM^OytH?%CW4J-^ zmzT~iEv;U9{WZV69ei=H7r|b;zrc8=ouAD69q1-uF~fy~yUwcC-@gT63sd4|=*L9L)##ryYi2)HcdS82EY_HcR5N5K5fEYyp36QeFn1U@?n5Bly zeZB63cKR_*%RDvARl8%d9965v#+rB`??dD88D?v(1vHaF7Q2|GEHZ zXRyp&cOGwDy)xb{i6WTR>|Fi;A7o|{nY(6)h_PbRYL3Ve(HJwiwS8)FnXA;=^rcJE zTK)8o{rKM(9|$RL(>Tym_QE=7~4BoKS<* zqoNALz^x9aIa@cMDUHo+$N&HuSRWV)7oOaC>^&` zpdn+E#?x$0GKyG!<`@3%pZ<$KZ|+sqFMsu~L?to37>XDwKy+z$qLn(P2AlTPtm6pA z+M_idB`3^y3&t!R+XjHG8$BXoLkWO~2q<2s<1k2)B<1#fx82jh<&If@>FR5{!{JKO zeaG6_4z!S(oq=hF2@s620SRnU%&wXcN}htN;kg%utR9f!dw@@nCH{?<#;CZ)>qt0LREsgQ*%^24!Q~HvL>2wQzj^TSzdkn^IsoV{ zl%M*=Z#M@!&9U`NAyDMwKlg)Q_`NRz0JVAzgIJN7P=K0P?;5IS#?OpeOe=lp8%okh z&UP1?AIyL}KovD<9=$i#{L%mVXFvL@pN!Lqijc9?TVZ2o%7ioNenmtKD5C^6r!kJr zN~Yb6T%v#w10q6_X^dEhuJ)>0iz+lLNJ1)zAw(oXPyn;gKX?DV@9!@xbQda{UUBKw zG{5D;xGQ~A^vnG;p*A;iJS1}_qePxed|3)&Dy-u$DWp_YQElk1Q3R1f2*jqY3l%{k zafvESLn9m)Mx#;fLtTX!yfucitT|30an`i}L1OS_frubM<5E=|V~R<315hqsd2M@V zqp_0(_G+U<u3-Xhec6T(=n>nbrof>Gaak1 zRCK0GiP$KgH6LeM1dV+40rwt!6{?b*i!ricm6?cIQ=^o@bTUoa6eauA@3^8%p zToM5R2Vw8bsWTXKblIETm7*v`Bd8)7HW`})k%&##w3~cIs){zjCWANgyq!&rvo44n z3UbD{c6 z|H`mt=YkKMw<1}xPRa>HkARBE%wam_P=vuQNYofG?^2ShY86X!L`~*z_`xjZ z0C4+#8=wA_$JWp8LG-Gof!pUvH~_#HzWd$hpMUJ7yB<7+E(HZph$28V=U^q2AcQ&R z1CM5~fJ2SMP!x#p?Jquk`#rbiEeB*2K}FQp3ea$IKsAiVSQs(22B3bbYQWezCk6mC z1c(vYx@gYk7h(WFB(k*+)@UcOyVEKmiXpHupc?8rf+pE&F^+&eq%dQP>PW(){E;H$g_L?yU0YVJTDqxsH@W@tF3BZSdh*{9QDb0Zn2lE@i~uCER27x6Axm>}U~(Z=*5)Au6=h?Z zq$bTyPDAuny~v%GN$XNLGoVU@U>E_VuEbc49vWh%!TZjxO}eC{`wmpTgPf$E8tmu@N_zfbBi z`ik;yLu@)~PI#1q6@nrHGA1t86}fcI!D_q|*{iUh&uk2EHu`h6AA*>qQ3wYl@wEWF zUTv@Q3ME+`uLNr)#tDsIo;-Vd>n>n@W$?VGTI7&oH49>Hq&Quw0kxV&O-+y~PG+*7 z017&ky>znlu@8OdxBjbt{@?tgPd$Cnq`l*BQW~*o&+gv>gxr&_%3zHK-()sIo5ZpJ z;|_DWC^Y=Sd1IDN4u~KI+<4urp6LnOUI^R!8!HRP_p04tF}%Bd$Mv0C#-wR~;TKPS z;HFIWWOJhBg3;B*+cHO6m)w&j?bDawt{?^bB>pp>G zXUcPG2GeQ5#@y3eG%l&DV*lnir|IH=_L4e8L5o60>k0r>MeABcsJARg=$fJr-ogO2 z@pL5E>dk&@8S5(gqulL9?$b zCqS%q^~C5qU)cWqbLEp09M*O1oa?mOiwkGU>U#4&Tr|2{&xi_$=nTHQ^Zs}D9=@Y> z=Ym>Gxq}ks8p8k}B3YJ;%=Y_DR51}5&OvEfjJ>Y~w%A_p z^-7oSw%UU<-%pYfvBUmgN6eTB?6#|)?qAI0!|xCYZA{a;!?)hooBkA zc-Pve?&-h(0$e!d%SsN%OI4}@*nk3djPBKk&mUcco_j7g$ z%@2I3(I+KTd2ael*DZW>`Qvkv000=n5Cg`Vts`qWZzJNDKKJ-Xe(Xcm+4)&5B61`e z)HJ9tbM|B#`_0)dBQv2kFdV3|29#*BHWCkaM_0f7*qs-c*fj_v05G#EL{Vp0LM@>J z08x!1vC&jVto%p?5e>3^;<^2=K5akn!5@3)J05!Yq4(W!*C$_iW!G984kE8qwPjLf zOcDfusL>d<)-vc!nz|UPim_HjAciIr6C*NW;@sxeR#}vK(1s9#v(_aE8tWS+up@as zno(mSV;CeV=$X@Zw%T2SV-ijj(}nS{TbIX*X{W9On_=48N|N2}{q5(k%PTwS=FX%Q z3?nd+v(5+4D#l2}K%^oEuY6U3!q}}O+e8}n+J4QhWt>9;nT47SBZ9G};gC|ABi3`g zmDucTmtF@U#wc>ziQcvAyQ(6wf+&PMw_DR{bJjs$p!hf8tr#k9_2zi%?DtvgDBh~u zTbQ~A{hUQZrP+)q-2loU>TBa1wfZK>ns-vfxEJ>TfT6x8z3rpPJMSExZVD{j@(e@6J2k@2%Xnc(R-hU%L8= z8hPM>^I`)4)HZGwFScN{60C)Yf+w3r*K+x4XR#P>u?8gMEQh?!qaiU)Al zDE0?A76L^q;>hEnSnZqF)`V?BZ;JqY_ca)MlcmR2meVvj`n^QY#?|E@`l&`wCSjAZ zNo~?$o4$p@8vp>T+Y>X)k+31%QF|d)Rg7^ggNpl1U7mYz; zUT`?_hp$sr8HVZe)91dn_m%&9@Wq#=FAn?w0Hi9Gj!yt!xSLHzL`34f)CGd73TN5* z&OIx=Gd*{#n=Ezml^^=akNw$i{UsnvHC0izHVSZ(1nUwE$ zadByhLTJdGjdR){p$!TKN2t@*S|*Yh>)Hneh&mh%X1n9LEI$AM19W0oxwh}WdFlF> zx6HBf+zEUBa&Umc_J~XdEN$0&Wk7H}YI!XqVH~7?!JYG0*E& z?qg6@r6dhyfpr&-LF^Yzw*pZluA|m{m!I_!FwX^KzE&du|1jL ztqrR(X#*0L;}}DOw_>c(rbo3-W(cn+b3@k^DMpkWqr}Y7Pc|%oT8t17f`?cOkje7k zBO4Y4)X;dDTh%527_It?-0r%QorP4=8GtLHwAC}kL-2{N`Tm2XhGu$AWJ>m&_m$T|^0VoFm{Z7OQA`y`kwxuu+1E!R`2Og32PFRvUsK9C`yS+q+Z zTKI4stHdP12Wl;?Wd9FmZyIb_mYs#IwWl-O`OQ<#W93lQHFS;LEp^KpK(+v38;6DD z5FoG}AepeSjUzx1GJ!2yf<_SpNJtLL#%&8v;ue!obO$(6uNzC;vKM+hi&jGqkGKGf!e1xa#ZmQ_82Oaehl zX^B^Y<)&%|Mpa}cWALNMUSk4{IAoN1h0%kRzTnW!Bu&MfA)tS}K)c)60~Ntu9|(IKGot_)C7 zGE`yYg_1!ynH;-%Ie+k_KmO7G`akjnGLbZ}ICj|aFdd6pz)A^MR9eW@oU5J(sT4~k zfL)$fYw#)=a_m5RD(r+-kL?g6jA~7RSP7YX@0FBNnW)w@LEtMo`Yz3bnGwZyubf`~ z>}Oth!CL6{dX*BkcjdzT@%|v$WjGNt3syzp-@lYxzZG1+QKotBy&vVpm0=-vr_nxh zK+39ON(y0&X?NP~b_=|NT@fa@&!cS*j8;-s1P@=u=!wyY4oyu;BV{0^R8j^(KupFM zfkO z1ps9MqkcH(Ul;cV0%e<89cZu)WF=EwNuAXr9;M7&txyh}I*dz?UfQ^)q^#Jrm134PvS451n*GbfcgMXT^#_#9XZD>6o}4()L2R1YI{4s_D1rJJK2p# z8jl@`P6{up!m#}_3MJTAmjDd6-@Ewf!}DcX64Cd5=Hv&L))1ns*B{&aR7v(43iN(f zPE}dEs-G?yPUiMRGGa25_s+;DW@knK(7QF$65+&84;Tenvhy>ubB);|9c_-*huMe) zpNk$n(mJwfZ-k>}l6hHpueJ9|VQfTPayfZefQ{)a7o8TzQ)0F$+r&gbJ9fvB(*cIf zyGjLRk(ahO5*~Y@_1PyIPbPMJEIx5EJhecole(+P=8BI+TxVNgR2R&YJL%9TR(j;z zD?;g6FeuN09)LM|RAY&4XdPm0M>7|_kp8b0BC5(wv^1=wL2}<+O}Ii{a631-}z_%_H6CkqFI`hOIJF< z49zqsi$UGA=cA=cQg-3JuG!sL5%l5bzf&7j4_DV zmO&wx_~4oF`~h=#SuGx{9h(hS0tl3nL{wF*3aObv0uL3g;ZwVdx6v) z*qNEdFbt9;Z8uxiGXO|sjvP6Dd$nNZ3S_X>U%fGS`E4_M?gtJ$`Te-~h2Q$-CK1<- zI?)U|Mrs81&U>$vs-RdR5>h(v%d(uLXq6%&s4nV00}rjWl+yW0(%(6!q?A%tKL?12 zBJv@CN;%7H=A5g{yYiyQMCl~3iqUac;4qe}g&Vb}KHMCy`tlBySCa1LxIk3x_DrME zDvKIZ3n6r6St;cMvy?(BRjW0GP={)_7dIEr#g8?M2{Vy*ot=OI41n7kCYV~B84f~Q zu^PQ4VibbO2SM=Ip+%b%Qc208Eozg|XBKf2bU@=?HDT@>^!;<83AxC?1i*k{xPJ^0 z6)6LY#?ru$!Yat+3R6h;#R|UAOgK`uaE@eRl2# zPKmQKdglHEFpM!B1A}vp(>o@b)RGbTzwy&gf9d~wBOBfbb3t)Y>(I=sJa(q`th^oTehdr})#LRiAJxZet(!$UpO2eT=s?Az zZo0kE+urI6D{l2Je9(VqJAn7~V7sXz$LvaA5;<{7XqgKyJaFOBGCT4T3BgJ9;(?Xx z!#n}c1OO!oU`F;@2>?JLX;=T8ytPFlb>Pa(gbf6!H$y7Jp!qRsgi=S|=J%IueaChn zI@lfW?EM!_2>BS}CR#UAay8qUQw!cx5Qce{o4_E(LY85m0HCIDE%PlcxDuBV@<{E( zn!BC)^u8|)1o6z`X#IBUl|MLf^YUyqHt9&`X^J(CnbEQ5VkFW+bHpc( zAK$#xJ9qBvX1`0oT%>@}bMh^~SD5R_s&O*n`+*f4*t^-4+suKQ`N$Fi}yn08oq72bWs+HV2QMi>3E{RkS=Y^KjHx zqQT@5F)NCW_YS}WLA44?(){qD$Bemf@zNh`%I7Q>5GZ(XizPY@N-gLK-X(+>a+ou! zqL`g-3`TY?mgK!-hbV}g_g*S-JXjoaekos_d6{!c#l!VfjV!w+{x$d-$Z zFqZe$Ro||*69Cv2iw8>p$O~NG&}#PM4>M6?K8U3d?`^*%RWubn^GGKiyV1_Lo7#YK z>EyVR6U(56%^5!xns~9kpeam~;o{=Lj^Eq?00vC9g8F#l&Z3kx1m|5u%vNRqeq^`q zl^Z6G7dTv}Nfhd#Z(3-Ym2y1lVZo(yz!2!Y5lPcbDQV2ZclhCOPFP=-PP}H4=Yqq} z21m{u@c?l9%G9f0KKkCvE7vZ}uU>C=H)`2f6G2hH&;O(PQ_V*gg*q+glVsyB|E0h9 z%fIq#N``0gAm^#7H9#t%0}vo|B|l!}eJ2uavQW^?_)ae!5ANMtJ@NDzAg|&Y090^z z#cDGv{mH`@-VqpISIeino&<0F-&=oF`^$tw0b6 zQlRA05DB%bb}WQ8n!T?fOXgllfT*NU=+i{3t>3Eb2u_|jes{BfWO-k1Db`PbsAY2_q&#T-Dqqc8F*c*HQ@#d0?fI zwNCrgXP#?3^1_Ap6A?y$TvE|&v|7y$3L%KR_pNrTS`c~f1Y#J40#V4I)oQHF?A*NZ zuIPBF_VgZcH+>{z5CZP2N*49n&2)8a{K|6Y#;q+ON@pc76L~3>RMG>zQ{Jr0=tQ(s zO$Jd-3~fXhW0@lRs0{@X8KH|&(-v!$0u>h?@S$zjbOj06Ie-5Pr5B7AjZ=(GL%F6-tXT2&3ohZX)`lb zZ*qMsr8v|$_xXj-KG8luJw0`@bF3XUq#&bBo~Fsp_^>>FQbb`~j(aO6Xn_#OY_r4{ zN$yu~>PtIjgX}`YtrPt^RMb6yeqhf=N6t(?y)ylX@Z^xa^XOUbBLMu-zzQE-w?<0G zX74k;vZOSj%snECAZ~y3E20300kLCLAM`04l&F=c3m#Z(?XEtAfF?Cp2LPd~qq<@ox$@pDfki(v-<9NBXxSKfT>rAN;_ z(kgECCWs;L;n&E*6F}mj@L-7}hwTVj!N23f)jw4g|AF)SEJg3w73{sJH30y7 z^8C`ulnIzw2syMuhx@mBzP&gcZj-kHZCT_9u{I_dXIE}Bi^ z`!EiP2oNXPHX+Kg#9hcmDTTmXOe)GyYo%0at+(ElHW-pGZBdu(9ONmZ3?j2>R_@%s z*Jwyx?@3=1Qc7VM7`!j?LaR#jIxt2j<0J?}@+8I7RD1oYX8)ac{*W9b&K!RF>4~3c z1pQS{R0HqXSrJ6!JSYR!f&> zi7LQdCqw`cLa=2520)a`=NT%!=Q%T3P$Jxg8|iR-zV%dEj0;~BBJbu~W1kvlHM^Fy zN^Bsaz&IHpvblBZ=HY{_nW^%F5C7;>=l?T-4}=jNHdVYd=WhSTq91-kV9|OQUSHti5!k@o*ebOA*z*^CAezd7;eGspI{@!kz8D`tY3}efA)e)LJoD6&4~4n-$Z(EUnfC z0kl#wp4r^G6^G-e&Kq+6!uvfcav7K^>H$h$3*+C_I@AqWE^@<=a_FMWUWu{X2pYqV9>stKi;8I@)sC>u+y$%K{HsStvNK!L2o zs<#hN1uCUrsi{a*tvSiCPdtNZ@}vljILW;2{p#G(`StDf^2na!ACU4!4_q?;y;GK@^TVi9tJ8j>ZH*>df z2LM3gHSS7fy!SN~KGS&4x^+Jootu8_TkGEh0IwyKsS=^|WQeVn(zBO{{6ISEyN#_v zYVuCVk2xMu1j^^`Jhwp@BI4B48~`O=WSF3o9+eUbjPk^Y z#Dc2$Idc?XN3DT22uJ!7o0+xM9fMj(C?bi01A-C6$MJQrxu92qJ@aayUN+j z6K~+na&t*7ukpR=3!QZc;PModx&K92Jyz8jQNxJ)xqXF;B_ozSURG+DQX)~v%osCK zL8^$TXO1qs@$T=}>vIdUCjr1(kJ11lLh(o#MJi0Q<_Bpn&s-HKCiIFujX{8l<~%Ob z`Pm-8;>Mji0Q7CYAsV^NQ}Sua8`c6?W=5BH8tt0aM*?Gl_;$ambX1R)3`^ErlYI5s z2S*N{%+ma@K5@%kNs?hb>bY`(d=$sQxVIg~^)xTSS~QZ~Ql^Pdq|<%u4qTVZyi#05 zMrJ;w4lOj60+L#W5?6-ujp3N)K2Xk6Sa>fHfPIl*5c7TMtnW6qY7>|F z$V9D={tBf@HZqv@5B;3k+q`0ap)E#Cp1cd|GyBo%0z-j`Au0Qk&P$!~wT zdEs){8>mie|DEv{e(cZwuV4I?U;KN2(@0FREta)1Ediovsu9a(XE@a>yRhr6dEuKM zKKlIGupR;c3WJs6HV?P?&fMBYey3qf;HHExNX(18hRLED7&4oDdQMDu_8>tFxZ=MPC2=Hq|` zS;5j28VM`21MhuVmbpkb+!_Eguq;HY~_VMYR`tn4W*?X_GA^>Az z?<^uP5em7nIoRmt-P8|JyRa^svua5zEDzM10AO@@=ff+Vr3KU(PQ_4}#{+tIW^ z1qKn|WPu@SP0s8t>T=&Bw)-HUlw4*2j8gA)X}_yM1l}X4y)WD@w@x-swfgO`9!Vhu z%UopWk;#G5dmjb>^d7tgsX)jx@o8;(zNkydo_tHx2GnEXFl{uk5rt8OFAE_x;-}9a zzI63p+_`ox8Ro=;qi3F;nxnE|o#ibSAi3lT8S#PV*EiwSVXnjXuD@>K^Z0;+2}gkpvKLe_e8D9yAI)#u~sTnQA0MWic29>SYKMb{a(@N zutqXMpdnvI0ksQ_oq>}qirc1_U8dlj0Agak|y$4nH)QCSp-=-iwM-Y>~X84H0;4gLKr zC3yyvm^%^5hhG5qH@VBh{mjz|B6trky%GQL7xrt?=I3S__1cG*u03+=b%v+ zTA)M#SPoaZ#ZF}mC#8JtD{uXUzx88P|B=Zj15_D)<%)%99 z0x4yT@xl1eqFHizqQhoomRGgQ>7~{3hCDMJo8`7uN=g8>`A~-~061~%tUY|}wb#Be z*IrL$u6-;VD>W%OBb=$Xr{BB$mBWV~3F8iVn`~aG*Uvolzc=1!)!I@;B8-J~SU3PM)`wF^FJAe|_e*N!ZDLaL}&!d2f*=4OY}A7|0)dl#v6z62LKy;3TJ3<^E7C{AinM2mszj)D zHVBPCBmgBFL}kOYhct3ZD2WwYYckU`GQfleuM`m*Wi4r^$fTos z*mehll`B5o8-+?lN$g^Sz)+Ko;gGt4JKuWzLjPqiq=*AAp-9JrV!T~ueH}D{+Vn@4 zr6{B=9`+^t*tQTX;_m#e)T}Gn^GQt$ATx(iH0%$Gybw|vW72Vl;C7z%oEw#8q7Zr( zpi0F80zheA&*hzx2z$o@PldnkckxD3?wa zwk^@pMtkR_6z}};o5xNrfVW(vVDrGsW@mXiSlM*_VL9?Nsn;q5GZ0FQW$Rq?(ND<} z3!<|+y!J<%|7M+bx^CxAar>Q}Z@xPE)*p4he7m@1sqi+x=k9&rZ)|lhGjkPK38-Io z0pQf(!{2;$3IK%E^{GQ;*6(!M+jnHjyZr7;05#qro{c+QtEC2@+2qK00nc|NC#_PL?AfSawWFBORG>CbNuk zA%p<={s+4D$QPC$dt5$wxPNr~+UUiN;Z-371IC@1-k>+Kn>*>^ka`MKXG#Q=zI_w8PsNLuw_*a@BIDs zTAX_!z?Y?z=$)6wR5EN}g32+7P$_{vOVH(230bw@zxUF6wQF~0@2rl8iBwXvR-gc& z9dsh7eYN|Q1d~0vFVumE>L9fYBG6`EkInh3#nl(Pw|4RyvU}xJ=Xgkw5uxF**X>uu zCIAow0W((xSBT9rUo6tVmGh4uZNK}qufO`*yYJn+b9Xeb2)>v|t%LWJjX+8nM2u<= z7p{_@a|b32`g=EPQp&n+EQU+4>-D{Nq|z!bpzhQ_*kMR16pp=+f}|pVrG*2`oQ}h? zG-*0SM4M+)8Ep%Rq{Ap5bP=G;#{EH-7fym|hjWM35;Iqo9TmFboxfvmzu5blhe=Ej zsLa%SCpFa=mI)y<@g&hoWT6(DIMCsK>JFwtoC-6GiEQ-WtVmr^xf5KJ(C8^`R!e zJK8{7wkBH6m1Zy_I6Kjb_Lbf*xgx1G^p03}&N=6lT-LhmObHWFNjr4~d!-`+F;9ky zGo>RTb+z&TXk*@g=REUx3IN}FD}L|*Zgyr`otdj^1*1Z#yv=k&mjed?bycrXjlHu) z{)fNu((`}jbA*z0$SMFK51Jzx*jdq-g|G#oFAK1?Alogwt|*+#F7@Aez4-8Qa-|O$ zp?JUh#%ujoKN!8g=61UIP9@w+LRWTC#<4+NCErRe-x|EWRc>D>uM`db+zJE%{_W9Cfud$=0I){euMIA375&ku zk2FIgzEWz}0y#yQJGHW{;b3f#R>>4?2|E6Y=tuC4YJ6Nzh-l#cf1Hd`bG-dD~; z3{2iSB4T1DCRieUz%FY+`-nJOCu+m{4vyXw;;E5J7nIN~yWI*;cE?%$4%lo(Ygt(laO# zG2_C*)aTBtQ#04sE?)r#XC3IkjtA^513*wCPxm9j&N(V`Z(VzNs1yOf&b?MrCU5j! zyO*sV4vtic+SQQ(i73ZHz)+ME&2OUD`rq0!p_!eTYt~zh`b-=*gCH)mq~7k7u5?mr zlmb8~JsggNlt#-U&xhk|d%HI+4z9T6`QdaGSSF{l4ghk$?+g%{m4mH_Z3g6wAdQGB zm`rIexbz_K{($;jV*>CnH@--OS2Ee6Xi6D0+MNRzZ2@`ib_No_b|+V(QV; zr_Ww^FWc$8IX~B6rrGJB7>AA8{NBYfa9I>8s2@DIa`~-yudTm+cYG_cV*u!v2>>{v z!TQ~>5i=8!bMyYK!v^H^$?jV-@pkl=Nv{g0Kn>IddJ?c#(Ju^ zZI$)PARq)aE^{;?02GDoUt87r&T}W55ymM2G9aN>M8t@K5hWlI#Zl;-Ct_oQJa^5e zmQb&Q&ImwCv>uqCD*QwN0#HPN5VTu6Wvozcce5}4)35yFU;WyT|G8(JwZ|@XfAIQ? zw{E_0j#hBe(Rm)(n@=Ny1MqEOJ5p&=$V^EE_Ljggac+~g8(Sv2Dk!LfQX1I>W&nUzLzlf2=G$xKy=LC*yZ%PGes=cUsqplj zwL2gI85seknwUFT3nrLg#Cuy>@`+F*)(l=r1I*UiEKRFkIOnXVKx`6h#NK%* z|2^**)C(`lbPdE%9Cz547kPb(9XD}#_T**<0KBqqFAtx${qb{i|!Wc)L|sbF=Zw zH^uopsf81w)!n)C0X7ykPUXE4$7j4rsjOom$d+EO#skB znyq}UXdgPh*gZH?i>b2mfe(agN%!44@{)In0{D1%E;0Mk}|Lyb7>*VzhuE?c1 zBC1eAP$-;#JX&r{&s-hfyD?t3&VeNnDKL^StimfhC!`=xlF8+n3_@>9xjSm04Uv~h zO*&KIt#O^xQ9W4BX-Wv}v}1Mk^0?P*PB;76PNeFU^bPd0twJ zh!?LHt2>8Gn4xfcH_cT-4NO3u^Ng8f7?IkC1&9e477mZrSLZkGP6-prK{tw80yB?( z8p2Fd0;N>QmaD~bMdSizF?1qqP~?)V=wlykdYI;eAgcIfQfY0BD@$ACDu}faTI=DU zr*tTEptadZ*V?ij)u!i4>IDhs2|Fi)z*%=7JP?Wp>gc3coRv3r*a8Y5Cv=-~a^q3Z zsFM=HpuA7A>fZqq5D!YOgl$g7>CR5C*ByB88_oLkOecuj)yC)BwC_@6U`EaCFz-)32UuK00#4O}JP2k<3ibJ6q09&u2x+%*&CebX|pjq@XhY4ruS* zHP&K(_GAtK|MGWQYnzpu1^nv&HJRdn@hAU_U;96PefJ}ZnN%$8$N|91|NNaF`AdJo zmjwbM6H6tu_RdT8XfZG?@}6P;4yeL8>Dlw~+KIF6r=Mv*_iXd|$7+uXft4QEWb!$O zt^fcKs#0bGgzN5(a3y(C9!SV1o1b2Y7CT}JjC#*szu15C`Op4|w>~J!qF|TCV8?c! zhjw6Pg#pT}4*;hQoF-2U9st;tHW~DiG7iG9)=2w3AQC1@*6-LXrEDZjxDa*j^xj+D zxiL!8_06lB-D}(V2aCs-KX`Y|b9-4YtQPA5YvHX+$3iQQN*Dt=tP(Gk9@0c5-gzZO zg}5;>I131nj8RILjRla)vOT<&iNQNmV{$vaMjCn%j(|{#FajM49irAjyPc+`m&z7S z32p>QV0I!s^bCI2c^3eP*_LIM=Mq8~qa{u&T`aYx%8h=7ehQ^&n6r5RZBfQYn}7V#_EE|BXn6R9UPOT)HD^Pthm>c6ohx!xZxTus#??)$>VpO1{hgWMtzpe} zhXzaUNhlRSzf)%-~U5@_WJs47}rD?@2dSua863G-H(o)IJ>z% zJF~d3rxzt$X-7RC_rQCM>Z;x%z=!ZwM4Vqv0ATZOQz%u&Q5uSo;ejrDrc9~a!&|FM z_TEe8qcq2`AgJNJX93PTR6a?1aolvS1mtdZZ;ftbn1;1F2pGM`BXpJF2bugMe6wlXXrNW6KB_5>YKAGR@ zN*?zddqN>}tObJ%QDEg&ODJ?+Qj)ve0ty$!(e!lZ{*R+jHtos!w-XTfM83g;t)r!nGPO2HXQY5re)gH{AN-T%D{sWFzg<4{ zsWOhEGiT0RxbXhDb7ujCFB4`03^z75udUzyo+F=Tp@@iykQq@2GIk8c)Bu2zb?>R7 z(hB0hL=C8S8l5GxT%CF?x%qDLJ_rHK>=}Svp0cxGLIwdu75{GHN8p_XDG6rO^pu*K zQd0o1U2I>?F5hw++29S*O+pvPVH{>@ZlN(5nkv949WgnHa_44m>QKukL!m28)^>#QNDEUkO^vgf@Km3Q&({7NCzkXru^p~!sfBpx5<6ifbfBDs) zcxmo=CdfxYl?|8{?e=2B_6y09cyicQ3!XwSJZO zNHv;LN?-=1yr)-Q{>sxnor<;tbG}RltRt`mX@xhB zah`$(qM`t$X*YZ6oU4qQ6fqH@Ey<|a+DhMT_lN6BS`sQ^V{&jNUE}UNFNaa>uD@79 zd5dm|QgBf&HV>ULGfZJ=y(jAL?4WZ!v55{)AQLk)NJGPf+&GJhd&S*pGvmlbtca*4 z>+dF`N0+8<6r!0 zHwgHz{M1kWFF*ea=gyr4fKgIhzw}1ZTmAKa{>sn({l7VG6{FEzmRbdM^n|7c0BJF< zb_+^G4XoAm^oHF*=Y`Q{>t|A*jQu!w*&yGzR^9<2_EU$}q7J}YP*+u!OUv0dOg3E9@byIiH&T-`h-~ylB89u0YG3(Sz4h2SB#hDXWEzN z8FCrcsv2rQ35Z;#Xkwj+j-OjM+u5L(K@R{>pd#=}AcIGam}gYb-^@=fF5FueXlGpIyz`@kZBe^j+?GO6ya{ zT2cxCwAt81P3NLGE>BIvD>v3=k1nDG2n`~#x1ddR&zqv@QGWB)n~$Aa$+Dq$=FY90 z79P3xi_cjl&VBi<-pS<;PoMrz^Q{m2_*=jHd$H5)cJukC9}|x>dOqt>2KIqk)fwG7 z2@6NV`Guf;A-}nXy~({25^M?Dpi=DIlYl&JUcS{V(i-%gp9APd(UdWo z}-WTsZjaU;5gg`;mhHP=$CQkeM!D+hJdR{+a07b@|q4b9#0W7{Hin-eOea zbc|8`Vd`%67t#EI9sn@I&U$mOpv*50-yUP(XEm?} zfDQ)JjYUYg@C@D+SrLWJk=q0W0%D)7mum;~VH=d*i8!t!@Pat9ovah0wIyl<0MHr& zgjytQQH-}X)Oek`D8#@GLko`SRJ2Boye}}ypZ(dN-TTiQ zZ@lr=TW|ivkN@YDoexUX^$?{njX;Jf8K&(mr^`O37UwV7}v$DaajVi+3fIiisg}J0_yTg3z zb5F#-@w>H+E#V-4;;|$Q;&Z_T z*M(ZxWc`snRf%8L#$k1@&Ne5Csbxk+?R>M{TKLT3 z6KESgxpDf~^c!zqpF4PbD26Nr2$g9z*83=uVT8&cFytAyQbKl zidPY@Prd3>xjwG%s@Lv^hz(vZ>Qz7iMIeY5LL?9p64F+aRHgc=Q>X9G+5PHf_x@P> zoT^l+5_t2kslq6TF5knU^dl5^;^IGb_$jdc;Z}R^qT87X9ohC zeC6-I{;vP)f!u~R=;(at)(>=ETl(=k?-9&ST_}IyGY_9Sa*hk$(c6|v{ij09O&!jf zN~DuM(N+{bCblTDHQgkPrdsUQT7u9r5=XRPDd$ET#@NExTy*x_EyJ*>#8&DDh&u}M zG^c_PM=Qa!*OkrncP3Nw2bWjl!Jak>xuxaB_F}d_tNT5t*WrDx81URd-_4+>6&*Ww zc30npYd1t&I>X^!Y|rveC)cP^LInb(Qe@UB%2*=J`LV^(E!}DAN(d1`eEhHf)>XRwhVUrl24H@@{K!+uqsk!Mddk ztszP~8qg?BC~;j0Km-Q>N(d+!c`SYFq&S9QFz9S3I^9|8&X9$&UTBy&v>{~_EKt`q zVLegF1${w6(nh+XFz{S&Ihayt9qtjz4bgx=(Hcsb=XLPzq5Mb}7CjVP_%#TC(IzE| zVwcxh%(un5S!IeKu_|G%_w1twH*{~j>#n=thv<0#i9mM0C4^_aptf4bb-&@pPNqF> zE<2fyt8GA;W35S5v{P0VB|p%Sw2O}Uq$RzRzVR4mFEtNaup^mmh@?uImfIkd1DW6Uw^ZrtF%@5dkd*vCKdj@`$?V2As=o){|)_Qidr)#k!xC>pDbw7>;5*=(i}Mg@)(fr_+Z1^|pI8wV`S%A+Q_ zK4^u;fa4J_3rZp-K)v+Npa2Gd)T>5tMCJmobSC8!vg0Jo?+q;i5<+rCPzkJQM?tFU zWKCO7`+N}3Hz2e7=4?kR!53NQrkmgL)RSM`F<9v+7C>pVNQh@qUbB&Y>Z|2*r~2R& z?7DfXcS9L~GNR|t%^m2m|NiL9_usp-nDvz_mY3%Tdv@NE-39>j;VL3Jo&x|P#}C(@ z-Iv*>RinMVbkhyDZn=NcO0|CQ+ee;w_-Wya8}He_`@jysme|syUake<4mO-;g|>Xh z-14#nAYj27N-0=!QS==sXw5in)`E_uv$xz>5*Q}26NNQGNMEjKEnQtLx3!h5<~kSKC|p93Ni z(r7f>+Z@UrlSBac;a~gQiPLv|IQOdv?k~BfM)b#*>fNkq)F3)8vOL-)Sfaqs=BS?HFy#Zj&Je(v6 z8e{y-nnpXObFD!y2lK=P4%C+UB5>%!zAmS;l9YiE%G{cq1wvh3h|n}u(-*GRTChT; zK51Oj=?*3sAs}$h7$X)tFss7~_lg~m>i|e>ypSxUnN2z)1?=psWma^Z&@>bfL_^$5n;;y&$b`?GOdM3u#9$48)hccTD+|q;8q)>= z03_P5sP1^rxZ~<1CV%rcoh#AG;{4>?lD3W3cv!(NSZB%td!tpjW(Ub zJ>8r8ynF`MsEgIcg5&vj-TS`NGwqFLx!zo9hGkG0#|ObluOFZ}3IK$f7BZN~#_}x7 z>L;Lw58#|7-6@6bhPK1&2gP`0Tsx-%G6n0w`lWh1~6X%>X&RJUOeA#A;=dSU0LB4t3bHjB8^7 zi4i^^G1{cpt!tnL04ocb7O`DxP07XEAf3B7G6=@?Qhw|D6OnE(XDTyxaz0v$%K=sa z13R@eR;{l#!djpLa0=WhATj`4VDqc6uoR6YI#3n~Xoyw1H^+*t6>)PdFP5j|()sm= z!fIImAx2rFS*z(_oNd%D~f5wDH<9&PD17Tf{=#5Mtd z0k?NNn%v@VYoiytE%xO1ABvAEP)I2NAf?DT1PzO52}J}j3J6Ub#t{$*jpGJbs~I^J zoJcua7R;&OgfS-4!HB<|BQLOGzcaL8=T_`NDejOa&v|;1PXHE=X z+e?tdLDdi>Zq5ic-I&-`+O>9oHV#&uOgjJ|fQx3dw6z*7TWb-lz`C`DqttP^WP4Yz zi2ycMBAcn}c+kynvv3-#fQTt&lRKbwt4fnZfIu>Zlw$`7R!Lw2)__zn%EmEp2M{di zD5(%i<_2W%Sv!0vJbkNY)?^s~jFllU0oq`ySQEOT|1UU3c@kH5QS|-)X?Y-GA zbhc9f&@p!k(5R@FPPP$}XaGXis0Bc}mHg({&wl6w2P|M(VD}tSuCzpy&m26r{{wpf zVSr<&n>|*o%lY8u-j3m+Ek=S?e)p`{7 z5B9X($+Zefh0cDL6Nef`7H+=w{PiG5|m%*6LP{htNG(Iq_oO@?36>&zl;l zwwiiTYnsI1x_~4lVuG=t4KWB-Q)4YC@CXuJ4J%4&Ka*LIQx+`YC?Nt%OQcw0nX0Y= zz;d!uWW80n6q&m3`#`X%>N!zpV71G*12k*20z?jI;<^#u$?~yAQg^5r%x+i>XY#pX zK37~=Jev*YUA^SH13L6d?Bdezhv^*v$Q83e5N5J(K*DeB+R467E5Ylav9B`>P(YGC zA>F(<7gy|>1B3zd{rWRj2WWF(VZ71o@2C=R0l@W(T8A4;nJ9@IP7@FgyehWJxIw7M z=G&8aQHl=2T+Y0L-yzr5^6TS{caFdC-TsL)`LBKHjt&3$m0fMLzqjYh-&x+jXHy~P z0YFYn&Qms)j_abqGKYQSd^>lvDFxr_c}+&wiC@) zE(&x40OM;ErNp2@;e!PmN5)7%rlLA{IfP4`Gt%l%63yZKbTqRww`)KQ4tDIDpIkT_ zo-1@_V|u(uhnu<>B)n(?A|uM(Vj+LsmKz4!d*{QM@vx=|3g#xDrdg4Ic!xDK;%B`~p1oG6Uj{OpztTMQb9YX%6z zTx6|XSe)(c8T4E~S@A=vtkMVu5R5jIQg8p3yKfjZ+RRTbeB;q4&b%_VY2$`_@4bE7 z&RvJAhkz@oHFvYH#@jPNgHoC_11T#7-e-(eT0i=o<8QfZcUs3+T6>ggrMh9@#LB6v zvvp|uMr&+9b0040uKla3lU;NOUc5dt4w`XwsP5-@84W4=N{JY<=k7!~!XiyYIPG_fr3UPhA zX6TZ3t7Gd{wE$pcjOh{^006oN$^fu3pIi5RQG#O}0rjy_2iMcWp@e|6pbV%M<7hos zL_}*5qdMW-=R(@Vg3a(O6~YkHNGeKX+F1LvUg#G+ImZ(QSHzMHmx;>(ATmLFl-q85 zvql3VpT~Z@eJ@0HIu}=xV8{mLZ|+YMr#NGc+&?2>@i7r<-d;zcr0ZOBq;82$6r z(Yro;;?I8lrqxCF!sJ_Df-mmc_oksO`{u^JpY9=!72^&f*lb3{P39l~^wR8%u9t}G zB5PWipbDD{UZ!Uk-;{G(FFK>*Bq+Q2o<65Ge%Rwf#zM?3n zj%S6-C{aO^Xla3`;GHpsGODGdj-QhA&ooYVmO8iZ-oM?LJy1VVI~2s-ZSAuvjFVq_ee&GENOzalHJ2=DiU`zNJGFFtQ~OS9wD3y^z--8DbOli{qo}1( z*z7D00n&`yx~e(m3&OQBv(2U9!iH)&ItMkaH3h4KCUadavII%$acfni2`3a)P-pGE z+E|t=8f_>c20bGqG{z9Nu5FqWy|FEF>j|z;@X6gKrOm21T$t(qUmv|YXuO^ersJue z4VCG0rEfn!I=gUY!(fcWaSB_;s|V7+YE5I}#M!Z9&rgrdHml+9{>tw+n~f8dhkJ{~ zVp4D%VxTMR$;3F1Z7!PvK_^`uGgbRr`t29r&87wr1;=k z3qPTxr>}JP&%ZBAGgDKuk9_^9&wS?JlVY^@j%#+@IBM6luE7GOv{`FTz4*eu?OH*? ziBBwk|M_!&@X-gND9&ZGO7MEUp3CKwQq2k$y7DFpkx~Pkai%>@foQF{0SJ>*M-En| z5@g}F0|&MYkBE#j8B9=04LG2sc(o|MVT!T1v>L|LTKJJ$tsN zyYOFs_p870OZRBk?WyyoTu000C^gwmR6B*tW1AHw=@&z4G3F=%_U zedijdq)0Z9K2YjZ?Ue${<(xv<@vV%Iy8s}Pn!|fzWR8siEFlcQ+WJy~B~?TqB%94d zI)p~67j~xB=x2^=a$qi3Df!`ID*!Ogq*R>$Kwq?4U7|Vts_{A`t=At0IkgD@lt?B+ zou-NbL4{33VpW3NvDRv%0bndl`ZEhkcQTq~!Z$iujgw5q*D|okDN7d`go#&SzOLg% z(SoGeKDVoncOjs%>Xt(~k^3`i;@M{@d;0neQ%6u?-zEMcYr0 zhdXb%763$6B$XfqMGC~Z>($OejW7a$yeNQ`sAG_ImkY+Nu?CUs&F`NMrUzBDR_QO~qTL z8`DK+lji4*VcnT-N%*RS0tBUE;iZ>ryZMfMDYH2Lq!5{YQta&?ad}21P0C#a;2XL34C9*t?qR#5>6 z#sVqz=w==LY|7+KFM^FD%Na*NpW9P!~znJh~|bKx>R}bkN@nmdv4wJN_n|SIasi3vbLwxZ0cs{8h=~+ z?%B#@D>q;ByG_AFCXGs@bP^3k%62~V!M0^lyD)#@*fS^o`A@%WjUC)Nu=A#ErLLmZ zdU=wsi@X zee&4Bhu-s{vGZph`@8>hvTvLaT!EFCgu-<)+#B}$7iP=HV!_QZ3Yn}grLL>d zAG!P8QKTF}jI}pkzv;&P!vL_jRR7dxU%c;u57(+QH|<#9)Fq;wP;8xPN^ED2Tv;H+ zPHh1h(^?~8DU}rfI{V9rAQM`i%CxoDS{m#|0H*)|(%4iaM5~qwm^EP(Q|<^yKr8$k zW3{CmD8`V0$gMP%ca(Of-K%Yo%e%Q|t$Doi5&(daoBSP9>I_f@MgyT!BvBxVbdx~=aTLB=68Q&8)%q=oxd%Yd$M`z>l1-0_v&z?H^Z1=*V`1bd2zHR^f#^Pjq z(FXvnjbsDq(u@+)j4by;YZPUU(JD2`swH(znXNAsgllEPakGGs$_O~ZiNIJO0>vhO zbD!7${qj?mSgkZMT8B;Kg8I4jhQHLNm{MxI3{na-tK*Pm@@Yqt1&S&%+7QadgA0H- z;*J2NnF#xM-;AB@hHZq5g~=*68$4008$U0@+i$)3%(=1cqgxMj-R2Tu;|Q6bs-IHs zSpb8KUMaR}8ExIVWBtF)%lzws13<$hO&w*ZJL>iy49`jKUDWPd>v;uO*QF;Kpe41p zp7?M9j%QY8FyDzrTBIZiEuo7WP2jBTKGjxOj85J_t!tZA0RY$DHu3)d`+Kdt&(b7_ zk^4zx0Ql(&SIH|y&W@%+*8$NuGsrJ3b{ zEqzUCY77Dbcv*sA z?0V+Tw3aEu%=ME(dr(>S7bc4B?KLo3Y6F0rfK9?Qm;xYBhHLK+!Yp|eD^!~#rkn#n zQ`a&yKa$%)k*%tgsc6avHxC_cPJ7X9t3<2SsG6t#*3#&i#uyk401E6R3A+p)HM1q! zHNx|#8b}!9Za3ux866jK9WH7*8zPLbQpbW=$#Rz__djTCRdG|RkckX;G7vRYqS)2j z2QD42BQHSSNU`NLBlUnWV5;27YLyJ-H=eQ8wAPUN+dvvv5v{fWu%cj9&eh`btZa@t zn|j>#*${J-E1EU7Lh!1H1S0YMywR}?=cEQrUE!4gV5>8nNlaIJVdkf2Kk;hl25hn-WA?CtG6e0FdMfLq*nB&6k{E72Fnm%0+iH zGkUy!+y-?m9Q5;PJJ$9a+xP$gAOJ~3K~%+t9G#l$X{%(5gUy7BLlmBh0?-oOEIQO0 zt?QLBeI{06uhZAzcCtoez#G!WFxas$e{gB}(TziUy|$4mxOo#UEiMi8_vW&BkO|}p zAY~Yry==>VQ=v=Yat4T2Fx8j4jV22__>xw zRM-f|PfpFAotii|FB564?d)qG+T45noA-4Ob*4lpt0WS&Q=Ba?JWsIiz5nR|04zH; zr_C$}ra&ug2_clZah6MJn6wb1;pa|6Z)Y)-v10%k*Qqp#y(s3NoK!}TA?2&qb~Drd$A0^ofB0lgkOVw9TzP(YQ`^Ro`>d3cV~gMW#*6>) z*|WPx{kPwHeU=3O^u_1C^qCKPo|nmFq?8DtwUts*LaCCpQiWJik?E-nJ^ILZzIXUZ zFYoR1YL`XEJGi|R>~c%+~*(F@a$Q*;U9Boz+?AcPP~jkTkjvVZzlpLo-KBSf?lO}nX;%?Jf+-LYV;wdi;% zXrz8Tt@b=5>zRIMf333YFHVXb`v3^li9|pk7KICd05pjch9*@=0075zY!b9WSsX^u zEGEgiUvE>_&c{bc!vhpKH>V;wTc7Om2b7ioKp3BIP8WC~edeu7v?X&qo`%?hUDWes z>x^bM6)bV$RRHi={u&jJV`GN25CG1(1|lKO5NEv;A{{?ad`!X$p<+G#EEIC(TUo>Ms* zbS<6lb^4df<9*$Gk2Q~3w5g=U#dI~ziE3qexwyQJMqBb%n+)1aql_sNQjL*zY4G#^s&V*t` z>Rb|pg*Gb#q*To>Z`|MSGO@gJL1~To4Uc^L@q6#sK7o)nfa)+tN|7+L=Eace2{dd} z8Q<~zQ9X9Nd+bD4^6Fwlt{80IzA&lIXZEqi>YxYJMW4@FFYpsk>cmNPYaXnsW%k_CYSJP%SjBS#bD7MxT z=1|J4OekTyM)H)gIF8wxGPmP6VHhF;BFW}lrn#mj(eZ|EessRlzNzOT-fEE$0$jBG z#~9OE6G~uB++biJI{Pv#uXNks5V~1ca2q=kt#E~Q{CQ*}iye((ly%^IDA73UJmCIFz6=8D9-i0hkc=T1Y8luYqWhFcmkLb9wHjWvErWUMA;G?y{UnqhPj$C3j(d3YWFk z7aw*DDc-7;2830GePQ$F-0gu0j#gjVmERWxbz_SzgV*UVW3SGs(=_(t{|O-4U;UZo zU;Kaj9)BkD+>ySKEeBpYKfZJC^(>rm+zcRqmjSTgT=h3p60+C0G$MdYWU}_kA;w&j zA0finz6Ws>A3Stq+tzFUzUeRi_g`4L@YRQ3 znVzZq#6vfpI6Y^r-CoSR>pky}7>?D?m6Fc&*lLvp2LKVd16lzhXz3DTWrT#m z8Z4k?OoWGs)@TGkFAG|&bsMzuq~&D3(++s9SBGGtKISsNp40%K5|_`GPoP$oP|BF+ zd6d%BBdFW&-BjBBd}A>-dav8&SnCJ#r;^1v$6i{5kCk*N0SyQi%|*E2^bP2q{&w76^|giIR!bS_7isj0>L0_?IFep6jlzR64q@kms%kR-~zhVa$0F!_XKX?t?64)>5%dc6Lg49bQ6ZyjG7&V_8@X|9^Jxq~zW^>No5OUwGO3y+7~z=4bmh4{x}_h2l18T9LUzgmwI499EszxHB+Htg0x{ z0oK^&Y{t(iUF{XUGAmCuPoyjsLWwa(AUTk|USvCL764#8zHqvJiV~_V5UWibFfW_d z&5bdjWooE{GOoIDZDqwObUc>#Ist%^{Qf;}e*VxSvu(p$ilxz!E&u*V?IZuAV`-TJ zz@{M;Cs?VGAVh0HDp)Dgl`_3B#RV<=)$d|eX66n3)^3T2K95WZR7XPAKX9E@A(XX zOiZuz_vDm{k%|_~o$l5crTJ4|8+qcZ*EH*`Jj-!lXmoMo!1U&Qvzta&2sK<8ZKM$n zXu>Vg6tyL9d2r0A#;5=K=2wmu{_GQ*KlrN?$cbeDwB>`4g4DbA-f-VvzWdN~U%O>5 zo;)8fPl3YT~F7m%=NU=c~RKz?Qj!i9Pd>5L_IqO zaNp$l&amltnV4jXpc3L200|IcWlX&+0Dwx?bs3Za6U8nk*s4pTO<|c!Muz1(o!&uj z%S3c~A)HMxilZ119LJfB7lAwJ2~x)cYk)ADMDsA4Ad{@L-)oma?MQOkHs)pSa{E-N z0SKh>rI(f>t{PjDpiH^@Ix#Mr^{bVt2@MzA%V){elR!cVF+@t4`jWotu%epLl5!WU zbu#S$mWel}c$rHMyPf46!vfG(#9LE9ryM1N`49j~ZZ{!}dL23}6Q>Jod^LAMqcy#{ zGc2Jjs4qEQ{x^T_!b88bW4X-!_MiKI^XC^|yQP~Yl{wvZrE@|;uSW9al<1_YqDnTC z0{{lvCd)(4;6gYLHer;`CuNWNXlq#K20GYs4 zC1aqqwLr6EDOnj^`+6?Z>xqt_K3)g=sn2ix{Fl3o0pEk)`nCEyZ(RT(xG*Y8Rx5P1 zLaQ};@R0L;nN&2Aq6c+}ZQyH1-G zt&~v;bSk%Y5pPAxqeg9Rwl;lcc6w}fd3pgE_gp%>WAm1g4Zrcg&mE%Et?6E)sv2#S zu$qNolLJ6<13$K@qc9y6&(?-7P&4Y6%PYbYB*ze`(TY(Y2_{-{N@At*!kK7JIjkw8 z!h1j1JbZAdw(33dgwQ=Oe%=g+aoG;Jh92)_R4(YM^WlMn$wv_()c zP>zU4zrW?*|9)?^oC5$t?G1Ona_y~S@4xZJ=awHut8Eg~2#Mz?ik<$3Zhv^WdVV#U zSB$3oqBq}nc5#J&{a>y-_Tte0`TX^7fA8}s(IOB|*82K1Fv#U{dvCb^+_3GH=$t@Z5X7FrzJFJbHn+KU7{NS$AU8#7ETz(hquyPA*xs1 zrykvL&%2HQ05AbY>!?Zne9Dwc|7fEIIzAu~LKtfi%-X|i>?KRxW~1qPZoj|DK@R|= zi3D{L6RAtE1sezG_y7QiHi=MpY4c9GIkml`yIdWg2q(1Fm@R-xfV~Qlh?R8RQj355 z`gp6+G6~9VuIsfQT{a5Eyh;Kl-nb-H?8w*ZjbiCyY_&!wVHp4%zkR(|+VR`p(76Br zETBP7Y%5*r7j0u|@oIQ0V@x6t8e7>QR)HuY^IV?PRn$~Aa}~A#Wg?{wU+ox#lcCJn zvkTt+*6P20z3|zuls@pI<=(E#GdO})V=7>Og`FT`v$nv6kAzyI)AukUvDR1_JI=-H zz$qbed3SynGZha-9fd>kp{yN?PpX*qIW2v{5^KAmaMQ`=sn379Z+5(G+rFvYH!O8@ zhB3roAw-#TyUKByQ3{%j=C*b44jDJlTCEc%{9QXXzxeXhpMUY5gD-Ia80a(a`_b>c z<1IUE&7k8d(2f)LcFFWK_g@qK-9NY0o8`gx0$Q~?fOAF6 zEXSi@+Il6(PV0H?mpQK@6s>mh(^;@Y~<<6HorrU*31{@y+}0 zJV*D;?K0UW)-}%n5CEClhMzjK{7XM|_bW4Vyn6@{31efYx*z@A{`0SN0RSRgd)@e( zfBf0*Es5962GI&BQY(?f1OPMuD!s97*9)`Xy}t86bz&AAYJjM)>EsG`Kk#z1#=iT= zwNHFwr{gv6`O)KO0TM$MqM!;)8p{fWLb=YYwP~ZHpn|va?yjOzN=kXtb(_EN$lwj09{`91=%1#fB+$cgV@@(y^D3F zu$a+uwlQrO2LQ_S4R@aX&ewK7`OR&&-FM8(C;-sLMw)o$VhIZX(9MF;fPj=yYhbN{ zlE?&;h^{?8lu@OWPjl8SVlg7SRt?CQ(P$Y!%i8Fd^7 z5Uq@nUkrN67{*1z2FppcYOn4)km^GHAb~YK49XlC)w!E{{p|=Nh^UlCP+EpyEqAk5 z;ASDdp>sj&IFZ6yY;EJYv+>;3@K&vLXsnbAdD;#D3Fxe#D=}nQL%Ho>R4|5vi!1amKi%<1ADMgYmf23KVtd5}0C%!l#^Q=s&&^q*RT8XYvXsz* zZdzajNzp20Ltxn(_>EKXJSX`XBxq=uw% z{K%#akiVqyj<0_wf98U7_JR||nDx!4{%Cq+PuQq7S6=$= zV297@nRwsuhD_(ATm}FloJw42ciNgkc>coV^zoTz$4^0^v((EF7mL&x%-(R%zV_aB z%7~2ubp3Q%LB`fvYYc)7RP;n}`1a20d~}YR$rirJSjzdW&HHI0>lNyCYrq2WNMnGj zxnyy2w%Fa_8nN^Fq)C?N_v9)=9uk9uqf=Bm0#hHa)vwKz9X)vao#CNpXHLA>|F3^} z+b{gyS6(U1jna;%jvgEt;sjYEh~uE%*Hh9anOu;CEyc0pJ^%5AT^CODTMLYvYi~RM z(1VRFn}R^~uB^^&@7{AfI&7=~3WTE7R#R-}auT zS#`BHI-(COKue1Lb00U?A; z5^E*lln|zr5{_eyaRgJwAY~yLD)?kRnCtj}Gpu~hS?-ct=!*O$kQVxU= z%Bw*Y38qWi19zYN-XlAsz>w8T2fw=gNdxQVGz2W zO9)A{IYm}fkio*ZxQE!;cnz@?GWaTQ0|NJ!WfV!@Vm zUs$V=P_V`rMF?Xi1W=OZ2A!c!(S5pkvP*RJioQSo^X%6iaRK1Y1IhRl9iODrGh}*Z z^RwUD3;?B~&1S5GP%z>NPAO0Vj9E%yafwV$cLKmbU-RRCINRG(t%%BC*RG}I)!D`6 z{3I6|OEc%^#!pTD^HP$bUOt!EKC*e&w$c4JZQnPv#{x7|FkZV*@qqxLN#uyUtHZwB zMnF6o&aSFi8#d7KD4|+w$?#j%mjva~C_;!y6mp*Ga0ja%063mc)lP zVhn2jCN3Alp`&*5;ixqf%+}kyB8N0 zuGzS8RVc+d;H{` zZ|$H-6gFuJ!P}^gR46UUg4S(TQ_eguEw7tomCHs|HyNN@V;rXmW3jXS#B-+6m|-Jc z2kEo`)4>IbwA&r#Oh_pKKtrY(+e$Sw3LL%ROyy*^-|JE@FACMTk}Q?mo%Sv-FQdBf z3Te$Y)ZwKo^g>(nrn^o*_2~BReRJoncOT1@fB*{Ml{S-J#VF5L+)PXA!_& z(KZn+uD`7*vXWvqbnFw@I$=%nR>~xj#@47)0@(~3q3gCAw@=0k{3^(58pW=93@S|$)f6v`N!z7BCDt0G zjG1WbZTD1{R=)B4$sfC=;0Vr9s3e$&sa|i05;{Fw9~^my=XuW`KXT_?cbs0C3!3K_ z8?)`A#N(s{&fabBp|7qomE)DuKwPgx4LTiD@lpq7yPb{>Qf%6wz@7EL3IL#)mVqZakx&3F-A>O5 z5J^2)woZ}T$`yY_tN67P)Ju+fJvFc&{l&%i{QQRJj`;uaz3jcWU+&7qJdy-j_*VfL zuHM1`0NlwYQTK}VB z_^}6rMw2{$P+XXxN<*{WwCEDTv_@?}Y19g=h0bk9fAr1a{5U6sAmIhtGpo*IL}~lB z(Q2bv5DZ<^v6Fsd+E_$yCeKxpKBkV=Ku0<{>?k*!E0~SYy0E6OD)xQFz%M`AxIB z*-x`)f{9a&Nwmgxr0 zxaI$&>`kL>%dYaUIaj-z)7^91*LU@*@1#;+QVApkq6|WU5Dk;KgJH)ICp5zY9B^e^0x+)y@3a=bn4+efK^| zYrG%toO{mMd#}CMT;H7Yo8P?N$Ub#_=Up8qjN&OF`P^VsWaFW<;VtideP{D8{^a-G z@!HppJ^j0He(1teJ!zH;m9!E|Ai;k6#kG?UK2%M_&Y;m92ku;$^hUbx{s%(vmj^Ey zk&y@F02v5RDg?{8GUGh-leCk&j47S@ApiiA00fQsnxxs9oBVP%I>e`}(ITk15C}Ms zx6?ITr}%_moH2!sW<1yM8^Idp0pnc*#{eJzlc-~=irMsfU<`>t%`WGHoJAD@`Oeb3 z)NlIA%dbEF=-F7ddoFk7aE)DG)+ak{^|h})4^gXeD%_~9W>7M*a4oy4yccPwS8Pm^ zOas$TWn(?N^-}M7+Dj=DRXNJ2vsk|MiPygPnY+L6>3iS*JW7q%yAOJ~3K~(vBuf6=WQ+MD0bqNF36Z!&?Q}FXLeX{)~$tFkXz(E+uy!3Lf zb|~dx?64`JseEShQg>OktpqwxIi&!gn2VT^D)qKJDo8T14nhOK!}k{7^}gXBex~(5 z|Jw39-<-D^`?jbwrttueiZST8ZL3V`Bd=HL^uPy#H*usxu$HBO901<9CjQCKwL^dh z?{mNYi+!yQ?oW+0y#KwE%{9+PQXN9u7*@@~RjpR1&I)o7b;`vJfBD4md;jPYPkr~d zeJgn9h2&6D2#0F_GDbiCLz_SPbH|_hT%R`nvwqNY*Qc} zQ)*GhV%IxSDlT0P;*bgATmd)$R)!J~K*|--u|Nv>svmjAL1|!!Q3@1LNU|1~822>* zH0LXnnyL~etvVt`jL4BEA1Z?e8Zm3PR-@i9E#CA>(tcL<-*G(SS@70tf&e@2=DJ*G zy8VIgea716(;s`_soyy>e)#U*tDQuR&fGQsz{96M{wed~=37s3#QO8eQkG?z9fc5r=e`Pn2*JCmKn_R2)y|p&d3TkHvz#8j=kER7s zT=Hg@stk1s0E9|$g7TnkJ-9v)IpgvUvet(V^0alcX8?_tu)iZLA|^>Z9_RbN0Jg|% zHA2Rb3C1OP3jm#kWdJ&P@gZ-ELyi)hGkI{8w}rO_FfC-vg%T=e$ft(&&>=82um>mS zrQ5cZaV5Y$kGOP_AY5@MYXr5*0g*n1OO~7 zUF@Fv@GF1~RN$p@0AYRo@9f#=+BM+Ur`C00;y~wgdnI%m+Kc zIcm)H`t;E+cU+vXVAw69h`3K_ z$9pHzrJ^V{HfNZscXZh6=x6=^Q^(AI^^qO`{LUYCE?qeYoTbKMaHPr8NvFyWk4x+x zhzp?s$a(9W*^AE-nXLNppI%tsV67(o`Y#UJtveakB9s8Y7+O;T00KM6@MZkuiL=L+ zPinp~a=^KkhE-IYIJUCB-2+4d@Tl%ksZ7B=d|v?oY4hFZ&wudA5B{x_$B#|_nxG~K zp22jhsE1_!(21tqBEkknXbmwj&zhI)PT|T?QGMd+)+fHmuUwkTH;?C|7(p}CoF)0V z0RZg|Dc->#5|VAtmmmtRwA&k0<`AVKT@q?Ru!d}|P+4t1+5y4ecsvnli8ns?rAO2G_cFFOhe=wU zK7AcM)mq(ux%!#UzWzs_yZpz_7Xd>VahnPoaROXvFUKnB^#^5v+l%cRn1a5dHF?qo9chRZ?f1up5VceFo^St( z(tg-WpMU0I;VZHwgaBM3<5M4$LIAu#L%#r`S&}0;u&q3I#Cq^>zUHPWmJyVqsHR&(TwE0D+=&_zEKu-+^0rc9xG|s zX&x)zsDLp^rNam~2{Dm|pIp#`asJ@(voEi%RYkuYow#@5ZTHO6 zt?gItDxWty{pHsAA6SHI_OsRaMz2V-H+0`N=1=w(kBxVg^ub|OD|m2#$`f}UIUQe^ zYn8_nSRe`%e5*aTv3g4gS)Y$|q1-IG=h_e|*j=t4`FnqH?egNF7yrS3`1T+A@sE=! zP#w(o3MsCY!^{pFvSr<_aY2+)JbHJ2?bavWbYCM7wbWcASz_z^=`n)C$UyBKa|yQc zjK~;)GPJq?Fv%u`fPzPnhTt2W=()AEm%j8$2o##9yLY9L)>IWjM5Hx9aNeSdS!>Z& zJGRRimn`v4&G6!1Wx6KbM25m0D$^%LLrSD*glw?Fj*Hz((m`vMng$SMDuc`ExMQTXA3_DE`biZj~ux3 z74AR5D2nTIMj25#MxAn@%d+Hz+cB&e6qkGO(0C4Gw zHq|If_HDFFqH~$8kEmqgU|=AU{Lr1@rF`oq&zh(n)g4>J&t4(3ed-I(>+4sVoWs(* z>koNR*8Lly$V8C|0BAMC_rICW%Vn@t#^6fWxtjF>0EPBMQ05hZ6kf>|0K`eR8mtoq z0+3_N=R#j4-Lli>U|ipa;a$t`+}zmUO~D06DMJOl-gv$N5l^ck5ZY!ZUpzM~;~va} zrj!jq@&JKpVK|F8UleocaIu-qNhy)B(YT!J&JzH5i&ED+oE5CqkjOdnW#F1zfiiA) zvmf}8&;8qf^}(0^`ow>J`q9U~{Ur#VoH<}&8HR1MrdTAn^ucXb8x3qk7&RL4Jr@)J z5E18WIX>>ob>Yd);@_5n!a%G5fHTDdITkUPv;(6M5CFtP=d?cb$@}gF0B{Bv6M;E$ zKt6B)Wm)PYg!uXEwS7NFw#N@)sNf(JzK4upc41?5fozK^`}g{R-Vv)TCK zr|x~?$>#xpTm@X9N~p?EC^9+j*OMb>Se@Xp6>xcf>uu9NamI(kVW-oXKK}yM`S^jC zvM&b;p_Z}zF&UU{p@Gpw8EGjdqSa98A^D5qFsgZ%w9W%&vCk4PbZQXe7g~bzRlV%$L zWYjW6U&Tj8QHda+WOO)sx_zKiJo3A(F(Br&k-@3`7snXJaeVgdS!>F+%0KXqi)&k> z7cZ~!crKM*bDC@3mL=n2mZ18+$4BpeYySB!C%^T(2f_O{FpeL1EmeqP7sXyjPHS4d zm1LuXw`^(A|K0B$e$SH@pi_=FrEYjzaFtL9hiSo}bFQqa;3-XF06^sGHcbO!4EY?g z>kS%h+olT{2(ZT7+Soe1yuis5?29}Qh00Wv98)v|3g^!l09fA=?)sPv0g$)?WGFOv8-Sm^`}((j;Q2p&>cKzy z?YEx4Z~fS*9TbXeL8fFnEyhD0H;KTxa;?0|P#9nN;0iHb(Pz|DkxK~Y{LS?-*C}}m ze46SZ7aSNTgM!Fb{QB<$l;6Ny69WvxdP_vY3w7A>Wj4D$E8zl{>GK?CIBL*3Y z3gQVj7MK9EsA6!I$lz>>m~jAbz`!}~DwK+xsR9E)Ewabn^ZMtXy7#kx{NP)^RL+6V>@TsS+0q6C1>ySceJV{BQJu)9DbmMr7a4R_N4 zm$qa&1%MDjp5=`w0*1giFz%cK03pNyk|jzlqPDKKi+&O4=-Pn;L(DKMrz3>W+Zm*d zB#I7aWwtxbt<7yqrhqCXwzPCYB!`-`7syLhY{ zb<)`o0Un4L{F5KK_EZ1MU6)?xX%dXi1 z#^UAc(ZUh|VDM(QJyL#7%*h}CC?9UYSwTXCrY2<;g5+t%Ps>~Z78<$v?fAE{lU%FY$iACSvPTSq<*RQvlX^#dC6wkeI>3#2geA^bb z+ykFB-e{a<|fxo6L8tu=q+=fCrB{pg>byZd_0wIB*SX%RtY^p~E?f*k`g zYrR7$bR%}J-g9iRnMgz!W^T1t4jOZdcO4s#^OTFg>&g_e#;R0-@iaD&c!rZCY+dCE zDk-Zxj$m*@AZxyKQR&6fJs7SDZOiB?OKIF3I^WRiV#XE zpgnF@UFRu;fEgx6%_as!GafSHVJ77I^=k)$x3jjjs!W#Uow?Sbq2kF`?09ae{X+Hn z*yjzN96uU+0sxPg^l91OX)mm_7glEBj7}HI@n+OGn0DVg5PmaUF~n5QWz|lqC-ljB zlNU$SDKIYT1HrRIEB)F#+517chRTiPJX67wQBoU1BUmaMF>s<5cAEU~Jve%Wr8 z_umWW&RQk+gFXet#wcEDBJ$bYsM9#cfT2i(tF~RQC0dH9q9~+FW@GcSyGvn*m&(_h zsiYC6vepI9DzyC06?QYAq#zK68hMwicy&pvSV;&gDYgN zE1e0rUD)24cxiPUdd7Aw2fH9IY`9z9_A(NsX*$j}E9M$wcjEXl?{MU{kPASRN&*0m z_I4~CX=03V#u(~RJEA2Hniy4O!MT7yWtkOaVXck3-J7>=EzYmxFa|~tsB!^4Tj-=~ zy_|wa8F6G%B?R!dfB5PD>sLQ8=*9o%FMP)nPk!k;zyE~@wdV|jBX940*E^>tK!Ly( zD5N01nwPH^B>%pnvOlzMFd=U>upsv5PcL;mFXx2sACko zHt`euxBG;bWidB5=bRIQ&qRI+om+04^0W>B6Fx7Z(h9wst~9A+4Zz7(wXlnK-dbDF z8>-0mw`PlkzP@c0Dh;lxaZQ#L*4S?MPC>mY->DU>AByj}Vb+ezuP;p?QX!x_czv^d z;h-6nx^Yy;EdcOG%K#oaBHk(a9@nQUUGCK#`gWNzi)PeZ5NN5z-}kn=*0%H2TQ{D* zyn5f==jJ<&rA{17W-!L6Q_rn)4g_n>cT8qM60;VB;Kp#Kv%$8Kp&c_Fb3_ghCJBTV z0I;2G8`P20dsHZ8a7usb?$?MG?vcG0%j)X?xf5RpIO#>#L8P`wl*< zTQ;+wc~`AfYpdnzx?FoSInP*y425IY_;ZW=oK}L10Jd-@6G@vW5QQo6mSG4s)ROOK zPny@>P?xS~_?A1=q#eAiI!br?*HpUbOpeG@G6w)98S?}2$1bdofSYFHv5VwD3=2;kI(t)KXpAN#FedH)-i7N7q3 z+lL#AfBPSNHVBxsISC#@;7oXi$Pjb_M7A8a(i8*=!KbyGl5=xJdgt>TxA0VE~FuY7=h$cAXbH_ zfRrZ2Ss@Ga8jZ%DVeziwGS<+A(Y4zBuc0dd5EoOH>y+b# zIssRK&{D}-Ezj6WaDnXwnt>_b5Z8)vWy?{&H?)QA%-yMnpfkDJ)yx;b&#DvZ8-|wY z@z(8$DK2AM3=aMLWPCGh0b3cEQ4eTyJEFw9J46)@&tI*~Cy(57^1~l^>wG7@wJ~^Y zHOsJTd51AkmX&kfTBntYVx2Y%O;E2dLL(+(bLRi#y zA)uvlX=$l6851H(+5q64*;f->>wO^tz^P*v04`sA5&w3X-9yY;Yk zrFian@yuVDFB(>kA;0O~KppZtkK!00n30=uw25dqDghvpyf)V;RE?(?L9<=_@Xvhm zv3FkvfM@>l{ICDYciMsEHOG;05CV!ZOeYEv<7O~r#7RmC6d-uArLV?T$$%i--X(aq z>jF0bKtM$y1abxr<;l1SzS}(zBY*IrMWKOaWcKeoM67$Su6CReUBWBV$b$fB?JI>baNM}Q*V#d&(;D? z%*SuLf91jw6F!q6_qK@Qgiu;X!Lxi=RnBgi ztKz_pA!f1)$V4rAJ2vA;ck`-XrXorjblgxLY5mG;R{(%P&4ZHTC50(J%+)L?=)vxbqIq!Y&ECc|^bBi-Y zdSoVxgd-}(gAW??S=BvTvL#q_-T{DfZf7C1cD$|_nJV}O;{_sGYx81fEUNhkshnd> zx+;hLwslvY9T0#Jk2O!6Zk#lLm+|#C!fV^uGvr;cFTiup+rPT*uh?L<)?A1)r%wwm zx^j8?Q-iGVfucweFt8mM`@I&NzhtaTA2Xr~w32jzg8zpQkeOK9pGw-o5|B&mlrK($9YJ z+~k{5)6|x6LEbq%DN&_rcbNyrJ+VMYfSeo5hHjAieO2im!nN`e5!HQV$XS$8t-I!} zsmclh2*2++Cbus`;+&5O!T`bVfr+ngn5xQb-P~z6+ezzA`YEm&a}nuuo`O5Hs_GjV z%~K1mJcuF>=w=}R2dgNL!XWZJq$AQmhmA-!7$`TRk#jwK&sldcplxEOi6>4jj7fsBni+}@aSNWrI(oHVnM0_-UETf**y z@kT_0jx0Kzg>DSpMl{!mTd`Vbwyg?25|kvpWYZKg1U=dpNIlIK9OukC@Q{x;Ofi`1 zA?yc(h=@Xp0097!h!JLjLBp-Y*{M#aJ;*E}H`euwgK0(vo-toHvp;*%0Km<4Y3Yu% zFl63U!CUW)GkNgFR|9gB=jWJSdMtV1lsxtw?=L%TB7%>9=14WmysFlE{g-dteE!wT zgS@D-3P}`YGc|!!?QQ&;3uI`}yk=feI`R2Pm4TTOP1!#onB?kOPsE9kAsc%@Cb-a@ zrVoC(k#Kel09e#?)`S@{e2{w!%5faI;<%g>j2B{z47uPOfN_Ql>X#u9XPiRE-?j_@ z03ZNKL_t(&HJjJ3T|beWUtlM!bpSv_Fs60QCM3Kl{;t z@F}I;#;w*b{ZHTXwXdE*=XqfP0JuaMK?r1W0Pq}zClG2{d5uyNg^IW#?xm(G2QG=B z%?eYeR=ooiL=vn0u7b0!EJ^?XCcqhMZ8@HpDg*>%&N<+U%q}Qlb)Uz{IwD&O(oZs* zE+3b9v6p4xq5|PilCwYrj4|@6(I#(?o!F|O`e#3Rzo{$)2d+XHg%GlFR!f}$ml(+9 z!!Sjf$c*BI7!U!VqEW@yN;g~^zhbJsCx4^=%Cr5?@7VSFA(Ch595qsCH9{*b7w2q(JCR*S91G@3yp7gIWUEX?Eo5A&7^@vdF2ZUq zrZ|dQiC&s-B}#C{1S1>vCZr)65f2gfiUk?r&hX|GNFn2liC2o(hi+)gftC&HO3%a~ z3cp{0>D#$ zw%~3n{M#9gpS^tb#cMZiY;BK=5-3d5wR}H|*7Zv&Ldbyzehy*^A^0$|gV)Qiyr?f3 z&4P*`ZD-5Mdf!~l2iOxxN@`OS*REY&UtbR{Y>)GiySZg=_1zW#%oaOVp{%y6bGsD~ zxhpxa6*1?mqdK2RW6mATnsI;>WgPc4W>BmV4XKDU4;R+5wT*9TeYe3)OD z1O(JlOF^~-E&v&i8RId`EVU5A-X%|@rGO}qEzwyJ zvg~h``FP3}7!Uf^T9mrJt%N+w%V14S>19#EB*?1T1=^FMEL2h0xQaUEN(-d`5r97U z{a@jNRh>3Ix~rU=HeUE3T@b*OR8#3|s70U7QcAndKiTeB}Mo9w+F!nzy0 zSlxJf`216&uYR?-@?v#;&Gq}?pr<)=x*XVkZYDrrN48G5Lref_p{(0NQO1!}hos-M z5~-qyhLX|9Ouj`FOgWs*2mycuBkYzSb9+QG5>(oKw?nze6FOoxu%aXvml`CIi-0{ERaoT1z*d4PU;-ZwL$z-*fUk zkH71smtR_f(=l%VKnS$e%N#0^w17M?2Cj;en8!_&noLf`Kq+gV61$kf$Wi+u(fRG>* zeu{XMR(oQhjB|22Z6dM>OfJV!lInQoVU{9<09=wO006v2L_$`_a2aDVN?jC54=6O) zO0Y=`H*KHXo7v!j@wtW3WAA<)0G|HXBh%ilHCd{Td)5I-A%u?fUb$EBj-yeWdGZ`t zoK!40j$>gnVT6*7vr2KtnH3DQiU6tHBZHz)djl%`3#;t+_r`gHdnVo-z=P3Wb#h-mxA*qSIHqfc@=kp|0W!EpNDGO>aGgzAm*Kd zM{kQN+s?B=mXB>UzO?=FYi8|Ieskb6?|cX$gy6l;-O&b-c?kP#Q(J`1BR+1*Q6O*1 zgPMW6gAT0<+Zy_X_MKq9qA^dp#yV&6;B5_8XFnih?Cp_I`am(+9HazTyF{K7aeKgL zC}?tdDMrqja?1h&Lwl^dl9r+j_ zvfFpBC~8bAwWl+D_XT&~J&p)||Bnu${#dDaH~k%|Q9vSB)hMYdjdh2F3kgM0=Es|- zhzVl6qH^9ZUySbCHtU--I2~Qcu}pSCZ@BJho-ik%UayY?9k~!tF%iuPZ~Pc%v{C8? z+M?5V?bem0#?py+@oKpdfSN4+Q1@-$+ITQ!krBWv@4Z)*L!q3r0N|{CKm0SFeBjO30pPhW zUHpZg_`Z+*`*&mmA(BSiY)#ZlrMBR7q@%-RGd)LO!p8}B0Rfn``$|z8910KRZBIMm zv;=?=po%bRh@>@BxD7f^Yrrev#SjneRPTWNv@B7em zjKR&DCtiGJ8EzXa5CSm9#Eie9B?AEA0;}77$FZ75iW!Fkc`9cKfhC=p9xn_T2hPD; zanNTXzq^sAX`|KJ7ydfLZdR4qUh6fJ=G>X?0f={CP;lN>^0um;y<`FMJy0A?A38?%V)?5Nz+Y%*R|NN2y{Rnv$QQEB$?OgMC<}SmHBSt=XP7SNMrcQTvLtMVT~9DJ{m=NddE2hIo~7*I2cMx%i-9{0xtq|}{QM#)_L@8UTF0Iy!r zhorprO~)aVHHE9QoU_0AeFXsg-@o7ftLF|*JF8ayv!KmoHNEQyNv(@04v?- z;>C-JYTTW?Nihuo!TXSp%i$1(mX#rAcwBP^Xi_8^9GOB$|95`;>F@ckzdXM*wwC?b zf4c7%{_#gY`MZw|`Yi|^Y{dhhPJl}QB3sU8;c(_Q`beXxOl2mru)D*mP!t6mu2u#B zzyj@g46_mZsST~Cs0IzSqTtLp;2N8%vQ;%3(YIw$mDS?P;NiEu0RW%;4{yC4gn5R- z9yD1fBXA}#+>@x6dIF-MmQ0*=ZP0UuS!VJQh$hCA2?}968au&wn1+U>sLWI}C(FhM zq%#ga*3P-CpUp3IqqauIknua{4cclfj#eE73c;JWxg3JE)ltxIay?8i9__}+A`UcZ z)CvF;oJ!_Gurg{*BM$+%M;*l-ksBiN&W8zX4Z(-L86SF_iUt@2-!Gs=L|p1Hle4nb zICxk4tpFfy6U6|4;O*^<2p?dag}L^fm=eww&XnzDGfC2ljx;)m>?oVg7#5C~UgVa! zfC>}MA_52m3`ywF)7O3F0YEdQP$q0wCY31ws4zTw^35mpV!gTo`3NQxl2iFo`P!!2 z2C0z?XEJ2u0*g2nr44}^ji#w8L>@ben-aB`lny3mKU08SUE7QScTpLHmWtDB$ zZ+`g6@-1(&mBD}XvnMup_JOZfQi~^Zl0$NMwmh{#h(%3=YqD-Jt50>fsrp>Oc6v`Ai#F`lH+5O<^=HyR&=L-OE>&Ei4 ze|a|m&2AD!Q95I0AGZ*~L<)fRwf{gB+cLM-flxFH{tyYk2uXqgqT{R%v!W{{M_+dd z-@UHctkT&tccsZbkLsXp<(zky=hzHQk4+%JZCQ)pkLr8{0KruuxVjog>ZXvf$#;(W zFq7yjKYN6Dp`CAS23L7o*4v%b%>xG7p$33cqEpj%xtIV~&bf!+_GiR~Cfl^*aZoNl z0%h;+InLya%h~8BA}hw*w_pD4fx@kUTj<=$u;!ypM5dw!05G#-Ai6y>GaXVl%IuJ& zT<)avFdCP6Vd|~2%R8C{%>qXPpsJZwwgi9(^`hu5%Lxrja`n){n*_)};Z*m&8l29v zmI$;0gp4~|m@0FDe1OvWYr{9beCxBXZGEl&z)ayB{mf5{q=fAr|H*%OoMy;# z%boT#)P-zP@r_KyNpqZ!@G$3KkmUd{uUfUQgMg8UeKAy8Zunb_XuTQY_2LF@zEa&1 zY8Xc`1s{nh!4?r+?+@-??1JkNH0u6r_47*<04OBv)tzeqa5`DqsYVicVe+!d&$LdS zX`HBwCYn5+*DU}LQXt|jU*Bu;1Ehxy$`TsxlyADzetPaUW$=7okiG z2LK?AH6jY<`R)RdQBo@+p1HVr=Hey*eEMS#1F_j$H)pIAd)C-{pXU>N%0oCX1=iS_ zA%iO3>$+5FV$Q*A{P9o|6JIvN*#x5YJ?qCeOfjfbI7M+e) z)O3yG-oGLo;7-xyE0=Dq-C*Jn##Mv4shM#0U}wPkWu4}(vE-r62ued>Nz21Qnlrg6 zM@e&W`r}eC8OAxZ$4-r%vbPZ{f-{Fg=7{^?ED=e;>)WMppN~vHc`-4{r`4YRixSaF zay(KR1p@%am5f@9t9G)ykQ|dDzAwFJ0a6CA!jh9rb|fVj`!Fc8f-xRNQKQ-7oIm$c z&zOpW4YyH)19HQfgaQEWI#U5aAh^1=To_jcUO`+zRFJNCyW;JVwK9q;v0&7)Pfl4m zZ)BIEMa$&a=95lM09?h>yy%5doPhI3XaGDvAU@o zg@g7zD=X*hNwok738v@t-IuFZJPU*n6s*yW@hNKQ2#2}R(MG<_Sgxbkn(Caoo0G7h zHnS{cMk+z3h$3BQcu+=UD^PfCd%YHMo!8Ca-TCH;7xJ6PMDSLs)R_VRPGW10)CnUl z6wSqLTbIcsjbt<&5D^!=tCmjhn%7AM6c;G96dV9M&3LJ;S#Tf}2a1vel?G?PI}X*i zJo(ih`tLr@7_8oy`^?84w${#~NU3BRMA%U^fyvE?+f(n%yYf&;)8K7z zhH-^6em4d0T_sc^qjvB|#0MBe3%c6gJ2Tx*$nQHnWa2>X$YP?S=5dPac4O1Q7|W?D z##_aBJ9t}WJHb02e6w*lXq}UxSDGsCS9w2556R0RVx8u$1zQg_(EYJrS~|Q4%@jjq z{Pteiv2eyznNyoXU(H@D1!oK7LL3ITtSQPO6CBmsQp$Q^H=9i%#Qk@-H1pla1bdK=AwtQiGAspv zb0?}g?RM#nB*=DfK2;ZkaL$EN05B>n2ROdiRDuB);0%>x5DK^eE+ogh9_zjRiRS^}Q-Ao_+I8*rr}dO0 z&k?-y3JC$XuSjycTuRALtz&@2Z~MO-SfRqaMheDL~X zwgjON5vMDfagj9G4E91Mgb#kULSYyAUATYs-bZc%z@L5cfrIyTW-!KD<>bUHkh5-I zi~^CXgjrJ`41-r%1?NyrH~;_uLIDLL>W_*7q;`Q3c+QxTOo-{l_KV(netms#qnM%F ze-pM9k?W*ejMib&;`AV-^0r`H98grE;Jw>Jc|tc3upDn5b^rhbnu+o-WrNvYQ|K+t zq!$SwFx82=btw@NG(7+RIeYUU+0v^%@SJbGOD>g}wY_@#){9!LZf%G~EHW|z49tLI zJT@~pFdm3uiD9rcHi5$y25iFPnIU9DfMI4L41=+;nZX7_fWTlQkU(m+q;Ax$Uf%v{ zugdjq-*)DYo0XNdy>99J@ggeozRY{`e)oK5`JLZyt&(r`-L$kSYQ~Ko<1$LST&S`v z27}{u2@1Z<_HDM4IpR|5_>+;viE`ab+DK4hz-fILu4${ctcC4)&aLS}RUQayF;N2t>iz!n?{&w!n-7N*%%Z zB?ADjyzWo(Y(2s@vWEn?*`JP?F)mFxaMfWA{cr!kkcs%lr55jNEklL4Z!&d}Z_ixd8IPnucoY z=F*PoBja(}1c12FThrZCHTrHB0D+u$jthBF-$-R!=vbvK-R{lcr4HpFH$q3fRIp${ zD2$plasXi3B>aZI_CnGy-r+}o=S$RFkChSB)qpAb7y$(rN(vw5w>oQGNz9=a*3T0^ zLoA>f;9zae3JW+x7ffrxfI`-T13F2k;4P`AfuVOG=fV`mw3v&@oNHnG7f#Qw?yo zh|)zk&2(2)gTrS&>U4_TBb5iC8^>ZCgXuChpJhw$<5xbQpM2lK+uynP%+-Z!jdbyt zJA3y#zjAfue0{cM)-SXDGV23Ely;-E>viwcAh9+Tx2m&g4TxYc_MOvkS7p}cT%Cjy z%-ho9;jcjytjT7Cs)(E&WB`Sw?ObQj9FHfq%u3iDmmB%$mMI=k`S9{2c{{uLZ2I&y zerZ*#w3DuY@@EdofL1$gEhRA)+%#5VjI^lgY8o=7Wg5s8~%qe%c_Dpsv?BJCnT1b#J%0LVFt;@Eqd z=qxlNnYOr&k#Wsb-S#1(y+OXQzNOQ~V&@<$5^CyOxr7kXX8HBs`ey*}{KuYnaBp=M zUo&6tKw!fm(9Lsn;E{2do7qE#Ae0dpAn-tkjdH<&`D`znRuYb0JHP<~1X=welbX*0 z1@ZtuwgAAI?hgP00#h*^j6tJLz#0mNU;&zIIA3{e>&6rJiQspB?WrO!2=F+X3P5HxPTC*q46C4Y<5=>vDaI`NO;avjYdH3ozJo?5SRenUIcSZ z^e@o20suhVT#>pltGAMOKqPhZcxQtGm(jc=FHtagzZMiY>%-~jJSaP%S9+{FPk=mG zzS6$dOqU)Zl?nj+{oS%EgEP()QQC>pZj^Rr$tIHbkCZS_@tIr-dn4CL__0_vcx#H0 zj$3c2W;hrQi6{i`ovDj)^Y1!aRE~BhGzo1&nN221+9=~u7C-Dt-boi?6$z=Gki#$@ z(WLMC!)hOhqNX={(`9OgW!cYt<+6SjTp$94XHNPiEUv2u$W_`{rckyh-w)gcYBm}G z)LZP$UzCbkDn4HBVB`&JJpOpi*mCBe>dfY6i(9fSoTsBtf^IC@DzoKuhr52&{rHcJ z7{b5$_Y42_KTfj^L|o}C1jeLn)T^U7TMIV>$cmgls)y_03RbN;Gc zX=1b}(ktl&0En3G21!B^(e83cGZGUQAZ(OAiYhL2=_aq6&uvt9gb*8_Yti|-EoQAX#yWE(RPx9H0E36%wAKgWQH+4BEwj8h9In?~DbDOPwGSaU zXAeCfviikCy+55m@I*AN)H4A9Rb@^J_E zPawie&tKcPzlw+q83iAzBW2});5a&QL>F$pHe$T@)=#$_smYc^6j01|m$bdWYD7&4 zAI{aBW{aZJ z6H{+hW=`Nrp#2^T+x_8#2M=tKHPU2b`$%<#3w3U z8jZ!}m8Zv(d~qB_1)3sH0m)XTG8L;{TEF-E5B~Ip@%_(d8;6(6 zn1~W(a8b1P!jb;+ARn|v^43m|F$P>mk)nWI-isounOj?JU+LC3=s>@ z2IHdBX-&Nbc<~M%%9pyNygH|bJ+DOG528*1n zDr2leg@cfq;l}E~Q{90t&miY3R9RMHIqL2gIZB_5jc^dlS0`BRw04o$~xTb=@&-R-+x>&R~5 zT~%a#efCJjym7l9vdxff#?4N4T8BxfSVs}(*hm}B8ta4BYL&MaFphz%gFCp)n$Pe{ zH`&DnX#3zTgaSOaWTPk3b4`9g#Hp3s-P$b6H>;5@h*2q{I0Fh1hNY#Yb|b!YVKvK7 zP8A=|T67paad9dN{b1t__LzAv?}A!LKrD*Z-Tjwl3(UIUy!~7?_-wv6WA^*O|8dgm zlC|(dKem?T)33O&)bpGR9w8x_)1 zSDNP&EI0i1kSs+3c^I(uM zYPf7L0)X3-jlMlxLaNy@qy0r{8#v-y*P6>+VlR%j#%8yXD8oj`n6)OLX{512xda^m zkj(|unh=F29w@UEf?saV9u};hd=bS!Ax_Kh`H|ndcy$i|{^)n#_3wW9TiAi?nCeB> z<|@>Qlps)y_q?Cfi02MIC;v{Bss86z`lmhV3J)w#@ zLey+e$7BH*KfqBT3u!ZyW2tl$9pZ52iUNU=k0M={O+Wp~r}K%@k$d9V2iyl12oY5? zO_)Fc;VE#0Io{RU7-LZ!&0b`z4#*Fm2%+Ox07`XOWMI4wdxpkiE~ zIhk^EoOhas4Vy3Cw+3f~N`*?jEe?sVh$uLIpfxtL$=)4#qDwwAmwRu5?w-+PADz;5 zp#v(N>7S3h5KFmc4RJz<>6oF2&#H(Ye9Q5TB&&iToPn~vtDG$nnR;Y(V)|(mY?Y-+ z(omtHLdqtXELSJoyOgTPCf-wJDr7<{m6s;7;@C&kJgRNfSK0CvTq{@`_!99&hjQEQ zzFytCYaYH{-7R?4?evo54GN$$W0JY55yd5uOPeihG)V{+e7kd;#hmfu=?Eit^`fcQ z@Wb_K+k`08uw`%Z;H|fXt;XJ#LM6c)3f|j7p!o6&?|<8cuj=aMJA>N*uvKiVk6zo# zhMtKcxaaa|FLa?De*8y9h;a9z{L!D94na1esGX*P3m{tPEG#Xa-`d(Mivj@3vKZ_S zj!1P875SK-sM{>7P6&YlmE7^I{H9*K&$j!)E%ZbZ#f!}wT{cAx1BzSy$>W{7ARh-@ z0syGFR=pE(Lvdm{!lVg#6^K5u_xbr%#N&i#RYNDADDGtk-9=-vR%1zN*n^{B@5hFGr+0m3`*l_=B*>Q3IE`?o(6#T zyziByQL+G@Pay?XG9>VP3Ut+4A3{q5bnJ4EA1CZb70)~=lZs0POu!U05vt12Q9e9W zF$C|(DLQCpN$pVe=OC_Mq;K`EayGA_0$j#i#=)E5s^ASWG2hN3@+R*$dY4b)%nyzL za-=(FW~q&}Qwp7oOIKwF*AP@VbMA}C=WZMABgZZQ0KlQBPMbZS*)i}h0qpO)mYfbd zT9Z@oLPm^BsT);(I#rE#rsje|K&jK&&mtJ|a?8oC3r7-U2sTZlG))1JF;-Q^nQ~`P zx;a*aV-s8)g*=GD7+3pde-yII>il(mxy_r%xy#-xOBs_8DdkO%iTjMGI)K^);F~)~ zDl?}}Hb**KTC)If_dXAyZ&9_D(qhcsfz61 z{NV3>-M4*)G5XXeuKwIle0{zZ@v?xbqR|*-gn&roY6UF-06+k1tgOR_P_wqzA0DGl zAcVj~UKr9Taf6c*hXr^?&eXhCM+$&KgEK(Dk~TSmLc`pBvBu7B6GrJFCn11w4ia5E z`WgN9r$2Ue^ZtU;{(ay4LN(4R?;OV|RTV(MQxaP&WQD2s7LJkF}|c+42n zS_=ko9LH(%ezEmx_R`(#we``haBLxQmWLt_Tig}C+%aZw=C~A5 z@U{r93S{&`BUabaW<{{i>nfG^S1+8qyV0Mi48Ym2gU7B_0C4}IU?O^STkm8b6Up>K z=epY4XPzv%_0fwfBCfyhm2$9sK=biW;6(k%SLJ0T9Tc6l_g#fnQ@bhNlE)1I}QN^$J z@7G_=xU^M<06mt}>H{;mHcm4Ef~tN-hV{;NCd7rj&!7a{-#n#Yqmj!|LbNZ|p?6b}fA)|o=cxYn>b zXjPqcGC0gaQQ1JWwQuqw%lS$kl0YDpq7bU8@`Qi_06d2><>QpbEp^}T9O3ZxK4Edf zq6SMFEKOBwwrIGO;|e+?A5nx^fOP6l;;_({K9q z-c3RV3klu4n4|9O#qVlfYpbQv`18HRW$%2qakD9sDY6Ior#AMkbjB&}@#tFJ973Id zwp{^s<=EL$$F1P4MlKGX&?l11 zi_tkfDv#}AF;~wlUVn1oa*gc|i2cGg+iit4$q1*T`2+(B z0)zxEl!eNXttv7_O@0UfT*@R#<_n{&(S^D0sFBl9nhL}7u)W^iUT*=wJHF}xc`K9@ zLU2#BhE+|KE$V4_N6YS9EaENyS}B6@zO2d(xVs9M7;={|yS~?Kwz*Wt+UzLxVG3R= zYk6N)=Ewj7<1$WB3*iGwJ>LQQf_T3;ZcjOv;i0GNSM z3vWtIiy$21Z?0c8;{gx>g{qu**^yDR=2t#)6t@`OSO}!HrkWQtKW@nQjL&?O2SquP zMkfnrfdM#Ia0b=O8FB`c#=I=c#ieDHwsWyo7^teM+v(PVYm6}mUJ0*Rz7k)*#=qn` zf6pcUw$=FQ{_v3TfPk`)55suq$3s7kK;DQh)o%&a2;P{yuhUNR{7TgBRD)49?sE-{ zMO?;QM5oe3D7hfI5u1Xxv+3gW=@dVyiAc)+|s{i4;?ktI{}TfgXR? z!|(avZzYMTO7VaHZ|{HYh3P|0MDuNQ0I-r<$*f=mzl*E(P+9C%?OlU6Rd~+h3Zzh@q z08mNOl}80?a+hrm!8WRFACSqY1$YF)HZwY<1$CyaDAx~fJVE3GjmECqZ9H0)K*g=I zxL>(YCuy#TsBpa5Y;JCBbi2K>D1IGIfD-@6yQvn*FFjFUvCJ;5Xc1^{)inJzt9 z&Atnnhroa_WCRo_#4z(FV9FY>fdIfH#!H>J*X3Ff&`Cy*d6RrC;;NhWOWq(M(j>{O zD2*$Fsz?AT)dn_VjQ zRnDBFqZ5fc0#@PLRyqb)o+$>t}6zX0DFymcl& zHe_|uyeqw{(uIqUIL4JBUT!cU&L(_0Vj|&g4D~+dt1!B|4G_|E?RY6g-*P5H1Qkx_ z6~rKky;+KGgiwS>h0;n>8t7tNhGWe~#7Ouo%aSzR*w|>c zrtsMP{r%O|RU%N7rUjdGKxE4?m$8ruAk$G}G~U`8uJ2E_G3$qHK>1i&Cyg=VO*h#L zlZOvp|J?n%w@*DE-IDowVFtF^v*nH{cTb6|6}zI;?oPfBh#dr}^@%77wGq4Ws+&}qqQShcF=MCPNVi3F~Z=K2N%!bPt009|MNE(Z2cdgUB7`T4T z53kEE01(kwyGU+{Jn0)_(lCQFOZ!gWf{If`w^6-5L@vZV%qNJQCQb zY$MF`f+K>r(Bu%NfSyr4bO%{yo9PGc$WDxa6i(EFk*TPek9PpTR$1_t%jntVw>F~| zSYK5Y5fR|d#21BGXvG)K_13ycC&Gw%Ja7{nh(pjER~NUx_ka17r{2Eq9Q%!5dC$N4 z`ELvm#c9fPQVUX1m66G*7>9gF<%EKB;0Unpq!Lk<<<43F0%Sx`tfL7Y(O0%d*bGwUYh@l=RKo;oB-uEH^-2VK^`Ym2K zivabz+=*UTy+k+KY%zn;2Hmq{KqxpZA2q{URW?YQOCXt(0RT|NlEXaVOAS7Q1K>H? zYRcOH002c^RAmJKbti4U^YI1vR$ESV`UV{#TaGA%M{o}UfE_ysgbVu`nk77>+-Tl>OL&UhA6iJmV0Nxr&y3t{08XPc@qrG*=PYSblhCjDn8ymI>Q*3 zk78mKYF1-KsptE*B?1u(M8&2lAE3}?tTMiu~D&6Sn* z+DdbIIZ0QOcqLZtkS!{<*hrhPO5^zYmGjpwoz4{v3>udcwIFp*MoUUHr7MoZ2^dpk zE-gm4*KrcC(on+A${j}9Q|JW5VGDBMe0C#c3idi-! z;if&4y9}aQxaoX%MAbkfV3fS77#!v+P-J9 zu{9M0kHG^1BjACM=<>OZwM%^>`0OX1N`=ynxB1FNxUqnXnc2ICuWi%Al8RbE2LM27 z2tLeV*pWaFM?>0I6w6_#mAC<59zW{6H`dZz+Zm;vO40&=tC%fxGw+ zkH@1t%gVBf<0u5rqvY=1K2fmcXnqf4;q)@G)aiWegR_6$$e2bT2-U${xtU(BR!gyL zNw!{k@XlSkeE!8hf9a6F7)3w{0L91@xfqYcXdJ!kZ8-pZ^y86Ex--&9sZ$j_N|Jrv zf;9kePA+b{opXv`VscH28>$tN{I#FH@_pa*wr~En^WXo27q_<5;!5#jAO2(U*lSt8 z@Rm$QD6o4W?nRQwhCnq_S_rh+?_2U)#r@YOuNKu{(;h(-RNmamH$FCgy;@ruJQmBH z0e|sswm$&?#^MOE4oKsWq;@&y4bGX4w9?Z+xD0GN%a-`(<`69Cw;bZTSoU1hB{C@fI;-!c~O;9Mkd0TIxjVghHsJlLeW+rFT^}5;3)`#ZS-Hr8~;pR^N?t`6;k=vcv0`$gLx-H(3YTR+Jdz5de5hkx|jUj5t! za2C9Eru0=_qpwiK6oRb|<4{DDO3W0q>)8R0hgwIhqhWMV?+!#hj7?=YI*vswJ;z4e zP8!YG0a)3L{0v^m7cY`?!5Uzk5n!1M<7`nFUkR?h{QWNjz{mf~TjTOZORkhyJb>4C zVBPc6p9N1?=D95nu$x5e(lbk)E4kf2*S+boGK3IHL&y#rfCOX%vj_aM$eguqsgnyb)kdF|vv z^8WV&0Q}L%m7P;jWL!Q%(&CuOs2NOwfwYCq?DambpNrZpCI93XuYdfv7rn!NUv553 z7CPp~Km5lRu8doJMaQxlJC&l)I3ja*wHT&*`)0E#WeBd+m4XW5WqdVXKPR+ViR~4Y<2X;2^p8! z02vo5DYHJ2f1-6!N`)u){Ccp;S>)1KX96PtaArPDMh;*1-@Nc0-}f6jGTWP-pZm#g z{C|G#1Cy+kG+R1J>xi{JF?TTR4*);`LI5rgohf5eIqRv+Du6`DD*)hug5)*7_3Sc< zVI3_h8;D^^VV-h3a2B^ZI*OSwgv=$W(ZUdc)K#qB`IWCXn$>6!{U1MlV>@i*G$tT+ zww@B_OzK$1XE5&!@oZ>wxCYAzphzE!q&tcGC6E?QJ4!zM(=gnEQ~ z&9{e*OC12vY0ngWK~)?E(obe}fc7N?KB>b)qEKd|!M$<5Uye5ln-ONQhd28e1Hf8L zxmY;M8%AlT$_I(Y(!6=!s!x^$LU3iaUuOH@@|}FJ<&H#`E2-72Qj&9yA^5>?m^RYk zV9;u}+O4MaVPlx@j;1*QgD_82eB#>=ffHqAyWL)JZ_*J1lT0m$WG&J?>2hz!4gfe) zl3hI4-60_|V^_PKTet6IdqufjxxzOtti&s6yDp!1> zc(7u}iabXyRlMhibhQV8R>ty{A9xDG(%*aKVJDt86)vmu9NAjBcI0xHOr!umm<{|$ zDQBe1j#sY^rhwLEp5#XhzK;Co%6s+s2pKt3uJUv*mt|Q*qr}1EFBnoj=I=p8Ar_zx|`Xc=hH606hP(tN--xeCwzGJ;NIFTL-gBa2;t>F$e`hqfUZzR1`n~n3$g&;~)Ca zhh~2~_uO;OKmVz}{vF?5|Jg4`k4LmyC1t6d) z28U?MIWvQ09!1Q-q0XWJ*`?Sg}HRM?_)1xgtw-AJWEHzn6B8%$(_$v5I0IQSex z!3!?;LejwEG<+30lM59iLSZO6LulZbaiL5xU`$lSC8x= zWh(wgJ;~{%&V3+{n6CoY3;>WKCbo=Ei4rlXbeeAWcg|f|sa1i=J74BJ0Mt5>96tU@ zwZ6#(hwWYdtN&lzY|wIdKt%PluC2y(;3GtYOfaG9={_!_nvxtDCkhlCXL3TtuGxF; z4{rXW|Gh~B@BWI1fAb%F`m4Y8&Nu(HTdQlu6?T4QrZtHeh(0yE#SWZEgyJc+e5u*q z*Lj&)M%)!-yddEv;)n=>e^5SbQ8`A|AfIu~00Eg~GI_O6qtajK_@wdyHebKLvV1X) zngF0sMogcN)*kdMNwR>abGD9YiZThTzAmmN}AKiU1 z2%-QhasdFikj_@Ah)1D{@hAop(L2Mq;9PoJMlw0ySkfr^)d-2`>|E1YOGHyIi42?p z#u?*Dliu;ApJPJZd3Cuej3o-?AaZq7m z*4QGO=x7dWNCA)mGVl%<1I5@FLVR?rk;qhrF@|$0fru!5QzvVTwK*m}3`t7ni8w>4 z19vqV_dXtkXoL3} z6g6T{jX823#`u+JX&j22?3!pkt5&#YxoY;JRS|11$wsIkf->EUutb-;VPYqS%U#t> zihZlo06UOzcm##x9?Qb(dTE)}A+ZBIX&O zDF8BO9R%KMv>Pf0fDu(~rX7R_#qRTi*Msy)EO)BO)o5izydaXUB<~t0n1&D;AUKnA zp%|Cm83x#m7gm$r-N~kOR66I2Qb>91w+%joK;noX`!tha9IoHI{mhp?7!MlT8|{a8 zyU%^}Mv|Jf%Y8%uBIF#1$lLm8#+c;1?y3<%2tIhv>buEUaIPpyA(<$Fi6er5)>b>5E;VM z+rpw-O?XqHR!mE#y3uNzE&cKD|JmBP3$<^3R8620wU*=dsf>Ium2a_9AAdw$HKr4M z*wvn4`poaD^1T^F7882b+H$!iBlJQPg`xCgJRq}hwo*~^)SR6ObrN{sOG|K8k&!5P zQ;v~w#^rn?Zb{l57t;yHYOAcw`t`RjHu>)0Q4|EG7?s&R7h0$!ZuHI~$&RJ?s`Hmm z`W4BsC9^4EK?vo+2wx%tlzk(*ob1iS--z&rZ#~JQQxU$5iCKG8D**tC!<(;l&Kq)6 zEm`iEYR|;cQq<+G)?h*m0JgWcCzHcF;=RxJOxle>(rCMU&r`!*%K^Z-m2l~TFADa< zKi2g@&Z`NWG#_w%gMZ`?qks1Ejqm&5!u$X7<-hoC%isUOh2Q#j*L;Adp4t1MzyC=k zed03jDG5Hf;Y*WyLw95WNG9G9U3drUJps)LNgl0erY9lq@b)|6r>?Q(CptIZe*W!G zgtxG)wZo!a)r?&wtI-bE%aiQ|Ggd-np=u= z%5VR^{{jGm{p8R7(>1cCDGOWVWD3+NAQKRfv6DlqaS{u0Bo-l{O}f{E6%fh{#>N;_ zG3pe#DD&JAOlQdeTE0V)hOlR;iabwwGJ)YH-7D~L?Hvzu2#TWGiuDQrtTmS=>~zQK zP42xpSu1cRM<$NRy-)xYjGd7?my?~Sab(bo3mshLtq&C@T`9u|=BoI%%4!bmQ%9Q^ z6Tn08W_~(LrSZ7hY|K>=>d}72c<^4R_@t6mZI@%SRf!d4>M%HXYs!g^n^LD$KA_;G z;6meQ&$W6-X$Ea+O)g|Cb^2x~TmgXSgs>j~0Hm7dW#3@a7y^L8@mM$jh!vK0Xw@>a zJ2CT*H~ql!yAwFCt&qob#(2$eF&JgH?r*>2sjEUpl`G&horQs=i35&sE;>~r8+gQk zIFC61f&fcVCod*Z)G%Z%*$9+ma%2bR^lH6Sdc96J zbcVyx-QwYcrd+fW$Muk+B|9#wd=j7E$^&OqxkX~;(=yE|CVgMDT20M#7yhlnF%j1ByRXD>C?;yq{HCOTCC@-$zP9*NfA3qq<=a2;6(4xXTI;J}l%`Yc)S<{O9t=bs zVhOa?+>>vj$bxM$>B8|`kOG(mrwoPhv~-IQX^lcu=5-{NEWX9t%H@t^gmM>{2)a=X z`#NnrytnqtKl9b~$FzGD%18@3blHqcDIVE|2LLdYAt0?aRkR%*t|S+Hb7hG81>^t# zLg_|}yhWV=01_@x@0H1mDpr=6oC6Dl4&Z41dcwC7O5M-xMkLQuXirBikDi*hCDAD$ zR_{s*Rz$~Hg{plMEuOx-%E^vSdnb}^343;gF%l{wqPorUXul^Qa3=Dq(u};fh&WR!)X9RaCQ>)LO|h|U<18?3 zoh><+&Q?N2QF;pH!lMlF*p^V6vqdU{3)7*SiJAIV0EoEDt6?QO+zyn01?gLoZ4?Tv z?@Eg1YE2$ zJ8!#buNn6qZn>&&8Al31w&Xnb3UVJJ_vGV*$gW*+|LwOHzx1o||M^R4t4aUzUoUQM zJt}`fgx*s1_Q&m$H_B(EeOYtSPoIF<q0zUSo^&VS2)|A(y( z03cAHDxY@ADF6}U0w@R%P9Oj%tKxQ+SIs#$LBT@*Wz|?AN@yP49b9l$9&@Y&)xbW| zv38H?`XPcHjf5$i=ae@Sm@D<3RIW~%%ct(-LPp09ZZulM{hiiA_Yu-8r&^cgyCq5K zPI6r%a_@`@5+yjANK*ANk5^V3Y@D4-hc~`nmd!kvxkL%Y65&#+d|O z=^DTfe4oi+g=(Q(Hts&42Ri^noeBUB!A%3<5P%6_992TLL}UmU=}uYw9_jZ&wJSv- zrTqL0*S%wDv-t16=Re$hc65?N*i?l=1TIjgS*A3xZ? z^V}_f4!Z|as`kr+FZEvO$^4n@)|AS3>-S#IU+wqlpa1vc|Kh(|ZCm^Yzft~=|MmXb z%DJ}ngxz=BM>o~VOL`-~bqOfu_ptzcP3?TEJheUD9rF-+0I(tQ4RPaM9IqM!U3X~K zrj7tedLYo3>~gs_<@gh~C&9g0z4MH{c{{&@-Vp#Q)%7%mG%s$vbZt0(>)_}6a;2A> zm*q~LZ8Gf)O70^@8y&IwSWw%r$D-=MaDqBm&&@qhBY zH@@-lzx)@!aeUDK!~gS*H@|!LKmMnGwz=^jfROTB^OO)wqDJ5a>BxLVi3tJ}KHQ&q z&vcO@NHtHa31F$upfdnqfEEHb4%H77vNXoEZFh&SZBB2#bjV4;`P|CQ6aZOBj2@Ih zWwo*pJ^LDU-YuxepiU@k2*|Iv$(gx<3J`3Yk`hrONPwy59-%ZGTR?$Awv&-AdV(63hT zZTTk>f~Bb_$3l2JSO|-BY*D5Zo;FegKtyqQex+^Sw`UsQnOL(MX?pZ&laob+irR7# zTxqhwrx&69+(dUJTk@|N+V;9Wva3f*XUiH|S0>g)^ejqu+Q^l%Ih&<)pxO-M%>nQB z;C2!33~8rtdJ-5AfcpIbBF1>dppQ~W$*mU!0G3Z5*WJ1O{Pk-$yMJ%oxc1iD@0`5@ zkwvp*)<1Ojf7ZO0fCi%fe17|}e-aaa;l=6)fAI4^{y#qYUw`l6g~SQ~L$t<+K8X~) z1|gOTy%gI-VVKW}hAc9p7uVp99Ary40Yu@dkr1-t>0h)Dei0`g_0Q{H^h3GRj%}#! z`@MH2KMh(ErVE|%0T!>X-hI*T4o`QFxCHvjj2QCXlUgeXDxRAnH_<)v55M1`Dl-gy9qC8A`E(Ytx}Oc4N__v3MCl!};6V�@ zRwo@eTHF3cv30+G@@}*DL%;WCI@$MQAymg1`=ma=Or8Ti6QS)brc5!!+C>g6Py(SA z(D$ltJ1i%ff?drhs_B&5vJ<_&^X#Ml=70MAU;mwVg@CUw=b@!Ang| zZ=|v2jGLb#q!2#MwNA02HFmKnOHBY70_j!!>0r>TJ2m_$hrZk!86Mm}N!V^y}$!D1fuaFQ9x8SS{A80JM+q&?^%en64q*9&S-)FLK>lR@0`sn12k=@8n-0U2EG}YYy|+;4NL+* zjVEz*{G@H0Izbf~Vz{+?eWA}L1ionRzL-7xTK+;s%_F}L0IPCParwaSRq#Ymxw|#F z@@<*o-_CE{uxp(T*5cJ19Frs9;HqwF->JaVafp9?}GwvOSTPjnoi(+1mEnjc0D%%})>j&@!!>@aTYl@c#7P z0RZ&#VJ`dcee|>F>lE6%t1pnNBQc-=CP4XU+CH~B()zh2Mw*&QD|MKs47pko{X`_1 z6Q7g;iUM&onyL_TrbqPn1M!K;heD{Xp^pH!`ZxB_u`+?v586k^)R=te+sUKyocI$g zsLxV&8Ym2`lm=yIb)qtW2$YmkG_Rb{76<^%gmey4AtIxQ)#Km&Cx7x^{5ZKeIfYK6k2wa+`jkDUs_H%zJ4jaYGaYI?maivVxa5{G0yj5-&763JhIW@S!od1#8Yu9+YqS}tuG338#P=t6Jva{=1B zpcI%=H;b^vT6>j;3e%Y-LC)K)JV}m|=W;KE>io55s86f%_{8)?wqp9%it5S5V#=3P zAEpaAd}AO_C*+&Y%hCGGm}{5((oF{Ez06lAx?p$eAOX%gw<(b|+BpvZ$PoaPl0+0@ zB%-$Uksu`u34MqF5Zdwbta3NU)440+wAvHe^uY? z(FTqSldA&tim)ScsAfRAP6ejuuSeeifC2FhdqaZwY4hGFPG0W4LV7iV=(G81@#<>6 zeOZ!?fx5<}&}wbtTCAeUSIn@N|E(95weBa6DJDwc)n{HEPfrdH-;p}&XKNewIsnXI z^8iOJ9aYVic|dW+Ei52~1WMK#V>AeW5Eu4PLdq=9md4yl0_!YCR@=iBR)kqTN#_=% z4CV84Ap)zjzw-MZ$C0#scOIk|oU7+!H8eS-ptZJ{MMO{rn1B*6aP&5_vuZcY9Ikph zw?wx!l$yF00nxLs9{sa_{`O%bGKqRJBy zfwrs@w8X*Xd9IY2K~|VRYBaeq#w<}+5z%IuF`YbJ909=e^Z_f= zx^q`LmJtzD|KewNlFV6u3sFi4IgL%42=V|>9n?}|zJ7%k>a25g>G5NxWr_g+*kX0L z2{1ob*#ZWuWTtXa9@o-f|GG|oj-zF0POI|dFdN#z_PW$o={&eezM*2v-3xq4%`xdr z6r0kG&(f%yQ299AxMhyY&r8z(JfU=!ei{5}2(BSb`88Czlu~f;a#-V9oX;|2N>$zV zirE=vfFjq!ewOE%02W99NX%7LccrV4`a*Kwv6z^T&%@p_yCwjcCsGzY3A;l1arMDR z_qwu6Neppzt~S;BuM}TANuwv>7y)yg4}^U$94nK@Z~+S9FR((iDPm|2+`*gi=v=1~ z;6=T@s|Snab6sSV;)KtiI>g}WmgB!ZeJcSWzh7y z!A;L$HdJ(zQzlH&fmZsfFJTo%fAaH`;>POM8?U`yr{gX=zj6DUS$B}iaA+aLM>TKu z|u8e3Y?2q?iQ_;L^{CU0q z|L%Q%A{c^$=pZ@{PUuW2c_szW0prgOLd?3Kh-b=v+AdT}FdsnlKqRw%J{Zcp4=D1& z|L#Be^MCd~{O{LqpAzBs{`6%~Mp7(##rYP&%;?Zq(;WczGGM|+ovETI!_*0xBQU{b zf2w&1#5~W1?JFGtK-woqqt*^rA^HyIkFg!v%rHiYL5n1ijL|+RsYDcf>YxX>(WfQ( z8{<=#h;<+n(n1LrltFE~mNe=Nl*PKTTIf#$w#<+7!vB$Z*K+0KUc)zT+|=42pj<+$ zNnD$bH&N}k%_nG4XGxi`NUxlL0rlWw#Y)Mi(AwUnglMTWY1%)$6-`K#?SMXm=pv%k z7=mxaMpDD$X*eD=mB%J3)&tDeiA6{tlo1HTWQk_S{Qhf$+rOFJ{X5xh!7!;0n@~No`js(1 znet2~t@D}%047={#Ux)zl@;6TVd{yJkXk*zD|a{fu%^Qn0Im&hK6889xqV7Oq1se4 zEXoZ3#Qe_TXlM>iwli1Y0WumE(ykhKl+gA#oLRPlUR+pZ2*8=6U&*GErm9Y4wwOjloCxxf=U61*j2zx)d-?z-#`jo(*SjbozoWp z5uwh`dNOnOgr&=msI#4C({KOd9|OSqZ(mPQqRBY4v&7BRmEN10FgmOf~)2bx+qX*@Hw`XSES#v@>xm)h|}q$EXxo= zOvz}|H53pr&u*8KourOXra%67e{gubKfjRdPQ5-~u?k?04Y^5fZAMd*CStx^#D=Tm zIv*Bzi8IO+V|0s&xsWrq6;td0(n?=pl%lgiXSRCb=O!C;WniX;Am?-u1&pyCovhs0 zG?(yh+V)+2s&dHJRk5k0IoB>zxrmPJiqysI1g`aEo%pId;jbA&suVx1jY||=N)=_0 zZPAr|b7~Q&^eB7V#riGMD@hN_phk5HzM<%Rb4q^P%T2$RG1J$-{&lTYjO3b$ev10v ziUxsj?9Wg{S*|0m@=+(0MauTB?QDPe(Y@}s$Z1&()1tYdXRZ0ji?$*tlM}@ho$5?p zF9o7_u6QvcV$=LAJUN7uCp2b&UavbW-H|&j<^hQl#SqpWTuRfpb^J>j;dfOka ztK!!;er2GC67)^^+9^Gr%+sro##P75$3V$<+6yQp2*xx^<>u?JqSe)R?^6@3vD{|1 z-;eDC0AAers-k(%Q7R6NV`X6NwUzw!tyen@K*7L>i?qdS#vM#R8epb+Owl)Wvpi8r zDaFt&c)VX9{hN3G??2r8;s5vK$KRRWdwcQ#0GifUr$_GeiJzW!k3eiH(ml0#i+v3U zXU36IgAizYEVambpbVOvFiU-(001BWNkl?)kMZk_Y98(_H-n%B#$5QiDt1dRmXM4Y-+S}KmtwQW<^ z%L|O_IB$p`HBkEIZ@z;F@BeJ~@L>)BL{wE3GXtDyfyK5FI^&3;4zy}`du0KW62ur2 z0|X_2IR)QV6-Q43qC_vCscLTh;>k}jr9^zmQfv|BwB)6tpIbAO&OBrza=m zyv0obXuS76!dxmeaO8pI!wOJ{Vg-qm0IP5|;bEfS{7iTh+4jOikA2ml~@YXHdYbY>#pYM>Z=sHYR2Qzw|ogA&@JGVEK3UfL|r?M#PInUqp& z>XZ`NV$r?`A#@j7>G;MsJ_djvedpDVX$nNiyI7tk-!4~q>{}2LQe@`3s$Dq&Ex2ib z$ooJCsjyq%a*j3Mf??3q&X-Y>Gh zQMg=5TucxlgkW8f$s#wC$s~HeGwh|5nzltDivFOp;zHzRI+4QcY~QdZV@`BB>E~5W zr#6jsDit+?Lt7qo`jX2ZK>^&9vNa(sN@c=%MmeR3fLA>SkxfuP!6Ev@J%{CttI>H{}2TfLZgbJ0w9}moGk{ z4;!8W01AbO3s=MoOgS^Kelq{nT3^0&HyoDbUw+64F?bLrv?Tys>)*@-=(!mx$B>B1 zYV3Rqn6%mjCHqnX%)A1vwOMY(_JE&M0DvYl)q<58xh5( z0!m#|#pDHwr{`8c`h|bL$q+)<_TJv!JvljO zChrP85+W^Yh4^-Avf%{|No?3n$qvLKziZoHR0q^uxl4(aDaGKF$ti^^TY)LGYVHDp zLNfE^S&&qg14V9PXoS%EbBOE-X+D#Qyd0ZDbv*T#z+ivJSj-`Aie@1+k z_U;wVxJ$tV0&NFh}* zke!~a&M@nP)EavFqlxLjPdei?Z-eTrn{EjyJA?oL03y3O%je^JfahP??+siG;_V;Z zKKmRYL_XxU0stc76la#MLSPEk=q}IVa0p(aS`zE(5`>T_l=Q)CnFOXx2PJeQA+%rw zieLQNLjahJ@?X4pWAUND05w1vc)t?X&Kyq}T9^FK}TO4c4=lQA*KerY}JUS6%6AePw9dzp}}2Wgx=liaH$s zjdli>9AR75?bAid7m*y*hok0b+#Z*1RJX@h}PpV8NiuUkJD z3=~CFinZbM4|yOzlzjjoh667@vUA;KsrkGFkE>5n%H{2a0`Y>ndE#wilv6t>Ob>O- zso{9wPNTpsv)q>b!6jXb%TnCS{*^fnx57=Ettr!!Dzm)}{giEuT{G_V><_cQ_f@qz z_~hLWUF#$O#Xvg03T1ko#pK=LvuCq%M5*M)+V=IGo6rZJ?QO>EsU*=E>vk{et}+5h z37wah)I$_!m)Us4S$wzU50Ev3Z_LW1nU#2LRCmf>3jJ3C#LTF}2k! zS*LJDmx>;wMq6|sgzi02(5qkh1OR^c7tf!2U#Ul{!xclx(Gbd+cl;{*jiO$L(?<}a}qy2~VhtTgm^yyUYP2vLpXr_m@SQSfI zfD=3!kE*OWO;?~$5oPQs!&l9Sqpz2LZj04=?teweBg!k~Wlc70<}VAbB1+oylqrBU zN%1sutt-P8D{XcBIbWPxDJ4|x-yGaksBWET1A$qMoC060+&Qry`{uB$4@T`tDGU}{ ztG(@wY@=^ii+r<}Zw&K|UTx`PJ* z#rW*X3(J33HC59#w;;=0Jj@V=99t>%>#InZVMmj>2?y)r?#pmV_Ga!%!nCE!)?Xw6to2t%uH*$Hp`jB@p0VT zIJ*i`Sq>#p+T7SY99N5vcrg9=!Sv(zM?e43!FL0No&+zOZpI9>h|W&7zO z{5A+>^UZZ%o=bM^w31q>!;$;9KU83&E0yi>Cu)`Os0Axl zA<-0_1QetIp(`-YeNET8g~zo41qK`Ebah5xRZ8l$9ri5;2Y`Swx~6Gbkg6*!z4M+s ze^Y+`3y+l|W_at{H!W;cLYEM&2$L?r=#X%Jw zR&BvtCPaX~1?|!V_Xr69Y+4zf+#DRNXP7?@)7lpYbcIIlZyvrN;e5YQV8%F(QN`J}x6lgB@L@95n` z2>@1A@y+74M%kp-*b;Vq$iWdYb(3U*Zs;^4R)7?3mj`#?M7>q&7bY>KJBlr zcw1)04N;U=q51#-g<7bMFvUgBN@g-tb2x#b)msB;H4KZie;oewr(rq<0EzNuahIzx z0Dn>+{A~2mcOSkLRtCzb`u(Gu_Lhv^t86>WM2n=&wS*Z0v~K`{c4uwVj=G+LpH0%G z;uHn45Mnlf3XX}|wq3wNBSBqNAT>x0+Oluz)1zi`ws+J<_vSc%4^adtvffY*Af>Kw zQ^rau(PT5qtW=ReP@L&hMK7cfLV#54nk@G(eDN^={N(%3&0M*nFQ;XU5h!t|4MJ2` z^-Sg3<`O|0Eu~}*Ahi{iMYomndu6T+_NhyVhGfXQ%l4*v4 zQKpk~l}ZaB!^&&1yh2~}botQ_e)OOJ)BjBOggm9J=&#lr*Y)+x+V$4l&?~-dSBLU& zGL;7>gYDsZzL}~i(CSLO1)x;__>pT*sPK}~xb7E4l;hGLPW>YvCrRW7NBcK-?=b~t@ZR3TpPrmNyM0T{H_;Bph+ttAQ2=oI45fl|A*G4Fd78W%0Hn}T^4j(gg;t^t z41u-zv;}|(f}^@k(=twKKN+Xmr^Kiv7MWP>%RwP~R#=TLu+UZrfq+LRr};*<)*A|q zh$6aDT-6m}iYP=k-DoUvYLSNxfV^51| z;L|cxb*RfUE=L&3&D}eDQ@4_9WbUTdLYk~tXjB|Z60Gsl0-PN@4kKkw(V47N}97T9dGLXx5*DYnxyU-{0d8 zo`hb^wr;HC>y3tSm?-C1DPV}iVzsYCJlOwmeXu#yeQnp9$;j&lP)MaYhHjjTMnOoB z2=rOurUeN^=7h=sC1rB~0Kgp_1VbZ~k#jkH1_qRZsnfnfWk5<$Mkxs?0wv^xLLlPc z$)3tLRm-tJrARKmcC9JO1vU zyyTtA`{CJ__Yu(ur2&%^Nnir0j6tbTE@ovYr2tAORd+*zkSGb%VSN*PYykMR zSH1!OoANb^9fE90C2M{lh$1(0CObi2fDd&NO%#n*xiuMMEYl$-qh#JpkMhA5TneAU z5BDE~7<}>iOOu)wWLl{Oc>@ZfuT%6-^8tHWban$5^b9El-%f-yN?R$sz}*1!qT$*+ll@RXC!KVZjv`F1yZB=7mQh#mo|hxE z&X=jbUHG=F6Ax8(=;{L@6;%c5HPy?`pABZB6ahHJpiTBPDX|UNU(+iUH4`cw`M{BZ z0dPh1hpKOiHSX-J&xc(dRF6-4yK6C40J&6xwSrgf_+F0RdxyV$&zqsSwfpkg7Y^$E zkRi5pq4SO}lvanjJ%0D&?|t>f-xPqg>Fowvz&bkfZcz)OvAn<%*kfvLx(vmmr{K{h1pS9dN(k;V$%mskR#E1&>xq}`>0PL_0T^-GomMguBRh5*c zn{pyb-B&5J*@o2vEi<9m6)M!n<@Sx$T=GFM06>h#ZSm3QK;*~eB-6UQ-g|U6E3`US z9T=4p&Cuq)^g^f(7;yPCT@Is*Ty6}+jV#N*^o3tx;uNFp>qhQR%%dqk@N!i8gT?Ec zditG54~*#F?%y!luqwuG29I!QA}Oa7lqtFtc_pF?08(f#H{d#<J~OelqnMoPZ59Av zvi^#&)JDK?#36fwJNYef(C#y@%N-wk{p6lHE304Px;;3Y;Zu4VOT6i#ZcJ%~AAOKOp? zPZdDnyJ z;HbQKZ^_kbU;YRHe)8SBz|_$MX1`?uA^8&Q0Zn%HZdrsWfP)Z%4*`H@YkPJ~MG+e7RC2YDodrgH*(7rcg zrx{^u8)LW$y<%!UA@VD9h1RMRVq<+9fB^{rl+HQDUuvf0y*oO30w7n+9pzS6#q-)FJYIkZ?e*s`nrnoHpIy+jOg-gOn z0ML0L#?ZFau~bIe{sJL{C@yp``49-iL*X% z%2v*f2Zij1D5PrZaa*0V)k*M`wnew=*S0T&5}yOI1_0@*`%t6aF+db27S0VvG zP={os4nraeF@RLvpnP^p*ZX3{q7mtnJyMz8$NkB1X%V>LI6OMcVvXlLwkl&SX^|Y*;GK_Cw^w;Ds^|{suN3QHOD%XQv{tJ{{6g|=3W0so73Li^;DuXT z*Vi`JSNbOJc~Ga*`E$exQbd!ttxHR6#!H?qkTM!GaBN#`azFrz6oQnRfoBkI6d<#l z{J04t_M2;u>*+=#n_<3P(xYJ``~52rFnyNPH1ep{*Jel;sk0Q?Pw}X@5DNJDyU#Bd z^N6C?P0xuNKB?3vm^6viSeL;tg0^-TLqKL91*MZ=Z?i6azjxM;)3yk{x{^nrZU3^1 zOb6mY6xk(ZMF7yI$4tvEHQTFd|HSEQs+;n32^k)1<&0JQ!ja`T66?*?mHrsjc%&WyZItQk?X4V9Ir7P`;ato6=hGM z_o;4tOvz>z0Pa3>_w?ZLt!vM>_IeWzj>?Bh+F@T4*JXqQYNz7F#L>y5yyj0^il?#K z?G0ZoZsq1o(4Zc*51Yehx367x;}9r(TV}=CH)^X>czQ^wV`ySZ43bbWN@@B+>W~`A z9Nd(dJBX~1dg;7<8sfYF}I06B7Z|0BjQ11FP>Pxk-@6Wrich8Q_e?g0ASYAp(L)ayGfvl%eMxw zka-1=%+wKN=ef+Wl5MTE2B|?P4(=Q)#}v^yg$(nd1u>x#M5)bFmNGoq_6IBNbez5O*3IGePcx%tSyITZHPQKCgR&_l zK$J$$><<8#XQZ!USYXHI)1vEjDy3AGLG++4Ggnn@t?3#~DLG*L&42XP5B~gx`yXt+ z`@_vwzOoM}l2ER0iaY}p-TJ}NqtpP1S)W?R%o;%9y1EN=b)?gA^of|;rUiM;kGAr)Rn$Gk z3-kJ}YRlzD9(SeE4o*M%_!p1%?rURW2vR8o5JCi*I4Oz^Eq#9!is@?n4Rns&3#Jfr zTb&9aZL#{)MlB+pUs|PrjhD*Y$sDIaYFxV^R~&micLI4D zfWhP|SBPn$Sd?keSC?6Er=HiAM9pzYQh^zqPfZ(QBt$$trI37CITOQS9TRa(M9fNS z0F+W{t=r?n0Yn~6A+B6o{ra%~k`TLGlA-_rb>mN~rfix=?dT*{3=j<6lSlh*1}Gl8 z>9-%g`*V8u*5o>5V*mgk07*naROJ5UFDRtBKwN&BJR>tz_0);}X&t$D8+tcHu_N5g#UN+p9t-B^4m3C6pmrV}I+*`isx< zqtoygKmEeS>znyDNS%%8AbW!Tl-X4y?^U)J5TDWpgOJFe%M z;GR%34=bUvm32Vm&_Z+|6>1C2Rtq77l&R=D06;WKE0D92ImYPy%%&S)=Aqs3bWvyQ zD)0OZeWeV5gB}0@lrZB>r2va56F`W(ZfIP#heYM19U*p&R55!T5dpDv!9%1Jfk{arrJN~m zb+r_e@b$8s-_t!^l$vq~30;x^&|kgIl!#Jf;n+O$>~qs9m}0L!i5aXsYw4L{OS>1< zbXbeEG1+ml$I(1;`d(}Ax%^(6-*fr>)|Ntl=YIT?z3C73!$;#(Mj?bEici~{&kiQ} zu01MsCY2RJEnZK_i#gj&%dEg_7S`_Ee%{)=sY@aX-ZOwwD$B#^v|kLj=sdB~WCNnB z_tc0&DnrTpcG}jbS#Rw#OKhpL3+dHn0)8317eBp!dfl$F;?s7#qc;blr*I*Pc=b`C zP|QG=a?PT$j=s5oJwil*=b*1rXDQS~+_rH%ZJIW;E}~R6vjAv}Vqj)2I6WNu)&&5_ ztc@|GlvY>Qo*bWKMZSCO+I3@I?7_1JS4k7zDN6;ujlNBZls?-(wLacII{v}&`v>Lz zqsgOh9lTZNEqFL+Pw9O9NGV)iP7C{P#sFdo8R!(tIkbLb|e+~fm-{1Q97dyZ# zfE6-O;?NSx7$PEecq{-$lLKHA@3ZO9jkpXyxDc}j3nxwiP$*+`cJ|<~uQ>*zdwdSS za8*1^)1y?s8^$L-KdRNl>n_i53WujHFg$+z$YZ4pjap+Y0Pu>8FPkLS`p2v3IEKCm z8_M6X;cn*c+U9Q7+_m)$Ilj3AuienAL;baDTG`1vM6kFp~UDjh3(md_cDaCqn zp!6@}iG6;Qi70&eOXOA|%|g%gLKqMwRTRsahW+$MBc?@BZb% zFOm?DfoM>m;0t^6r$M#mKPHM*+f2OR;)d>BfhYN#A*5LrI$(yX_LFHlnO00u+X%EN zl!(n!1su%RUJ_q^Uazd2ymS9Gcy2x0L}eqA2%)PArP3c!8!bIYxde80A5p7nwW*sD zND#}!YF0Og`Lv(z>XEl54P*(kmI%ERsLF#!D0zxpx&&t_K4s551ZRS1-3GA=}&T=xPb zAR!qkFgi$ap=rymE=Q-7Vh3dflotk8?3>V(XbL6F|4-R_HCd7+XJYP0hPpCUY1P$g zdZtG}4F&^3Ep}=RA~UX5AY){P=PIgcs++=~ZMhpwImAFZ|&G+JkVIW!c=|ek~#~ z5(TI}kWY!q{yfCnd2i#f*qeF}00(hV^sf`Z5Rl`=mpppgE#HRRZ;N6#nmgI(+r7cp zI2_>fzcq*d=-KBr`n3y}qPNX7D@TAB&nJVFBA3~PV(2pitw*2O(eXh{(c9b4=kho_ z&3eD|@iC+gL>Oct5hlD?WE3jDte$1t8KqE-Zkc9o!quJP%cBF&uC_3Zz9w1yDvB=v zuR`5sTyLU0pvhi)_|on&E|CeD^%0P9$+$vfF_;U;0hJFYhtDU|GY+C_W~vyee3fr*~Q|y zIBy5Ao$K$72fsePy;FTd$y;QbGva)30FVTiN&$f2?6==}_j~v)e&KO_74?mvX`5wW zh7VbJQ|9ORU0i;p;{Jt^Ra>?`1b`6IY|&a5gb+;V`Eu6U<6K`;eRWDDxN`8Faf&G~ z3y%EToA2-BW8kt)sh&?yeC=^Bz`m^CVm%pyV+YuW0`6&{Bg4osFieOnbg26Rf(>nL z>se5QWezh7>zc3?7Roi(P5(Xdio z5uK)OrXfM4bTu&3(nMn&Q@$Z#NV@H<_a(OH@nojvPgpKJy<> z>_`83*-k=dnq9|WPr;;oO|m2gcV*5pPziNPi6Ae$5JG30{8m9^LeCKmQdCQG9FxOXR+Q3s>fxCDldK|j}{^I ztNJH-Lc^`{(zCO~Z5`suMV3;C!K`m#INt6RXBmOa&a}rELXK6PLpdy+|JAI zZtuL^yO!r~Gb}>8BvW_l@lwiek0GYVW><`oKfl($vUn1Lt$m_=hwCA-E6U=C>?;~N zTo^JNG|SndwkZ+k91wxP#p6t_9E(+@&GGAq(Y7&Y;Q5Yx_5QcIPW_H5)@jgT;@F<3}A$TcH~pLMlH+o# zZMQW5z^pu-J7bKI5TXa6Ao>u)N*xhaPBSsa)58{d#bG5927REE+G87=1yG`P0YY)k z^CD-Aq0ETr;kSQ2+-Lycum0@aPW?8eMBc9Sw}Okd4GD>H03dH;Z~|dH8%-&NHBBP5 zF3w5N%qHHnX>rsw#29B;riBz;eP6}7LdM6i;Tm*n(gKH!2$l<$K{o44g zd!qipP99H>Y6=%hV~k6pl%nH8AzpFheTFxcf62|y0{+s}EEZqLFG`&WetmX4Y|YWj z12J*=d{Hxz9X0|k^^LgD!L>11Yvwv1oE97y7aK)l^i`MkZW;A}mpkGV7Woyi5GMm) zRii-0q!v1;utsUU5V#pvTx2Qw;OZ`pFfLPy;UqW-Rd!#oF4(9dR;}w~E#boKdy0$X zoKXs0`HTymQcNjLr_&fyUX%%z5PeGrI1 z0uiN@L=?P71f5&&PO9FSfJ;yQ`X~m~H~#RuFk|23!Ktce-S;jbKRR@MeEsXVaw7Sa_9HmuG zw|*GTI?x5hK;8%;ic6*&7aXL|MzWIQRcXk;f*019LpZSC`}=~8%2 zDFoMM#YLeImGxd#2E9CranCpBj`LT_TLA!H0nDr+j`U!ldh7+BCZ<{lChuDS$a`CejB#O_$(g)=3Y*#JpWV3k znJK4a|QrX$zoXGMnRJn0-(xSv{{y25OVEAS5f7iEO!TAh2T!-vnk=hzFcSV7Yg6psczxQ7~>J3av@OS_IKBTCt z-YVo#0s=-M*8;m1{|Y2SU5&LfAmD=Mim%Eh(Xal_B_xi+To;uP0@l=1S)x<$He0i& z{gOrQT`s>}m2daed)M?guW{L5IPat`Kl|iqRY77OAp}IySr&sU)WAABX|0RUI0V@z zAa9p3*emTi?&ZOSECp9>_kDeKBvo(wHXY<}=NZd>uuLzS?5L3)I-PMTb$P+hb4Uj994!gzy}eyyjQ1XxoSAcqwyvFZ zG@dJo15*DAOZ#BYz2W>S;pf*3Qy*WsuOq)0@JbI)IjT8R+%%ic>>eE9O{m&em; zHd`p&mofnWXZ*OGX-u8{Pj^%ZD20^VBntM#hmQ*tV-R`}yy>Q^6hk-l?vz`|=A+LiPad@gZ3@e9F8OuNf~bfo`U?qa4~6+JGt-93TZU%PvfiYtGfvg+iI3EopSclvP=g2mz8fA(bTMGaT0YV6af)YYUs$sTqYE)i$sL>YV5g9iMcDPOjkdJ zftp2laS%Shq7md5_0hCGw9poE%Je5LxHsQ?I27BZ1Y(bYqO}%L&YM>0Y?KM5R0_6I zsWT0vbOje10D!0`I1`Y%EsJj=QgVH^0&uBIT@&N_^jP&r*i zj$&MWb&D6c)XjTMS-_cjSK&4F#55DrOp5;Y<&RD5ED9x24tamPH8NnDmz}cQ>xvas zRrdN7bbMz30O(Yo875;)+qSDB(Mk+~8sic$Yb*d*Ym8|ClJQS!|AXFO*0!kN{pZKv zJloh9TjNfjbk1-&Y*Pr{`%WBBDdoL?_no($X){_q1KlIBw?AXDR9UZC?5}I6b1onB zHom_x{*7w*ej)Dm@0R^<3f3dDfZ!%^zO+8X2mr@+{^{(+^Z8zD?YwoC_xYE3Nu=mf z^m+d}03s9O5+2L@XjktEZt{EGmUF9+htfB4S5Yk~KG7MqqV05FDSuP|usbSd?h7=OX4^F!+fuId!;f{-Xh;%0eV z35*0O#+0IQ`&#M6a-P7>_;gy_Ia0UCR(V!Q6m4=(pFEW+mr6<{8DlwQ*^=21rIe@E z*^J@FXz<00gLP27RN3Z`3C;kZ=xux3TrkT*CIfqPBHyoQnXUp{#JD*19hTsW~xkvz2_jMDTIZ4f2GYP(BC#dp{f4IIo_0}CiX#VLL0Nl*Rnn?h- zQ|$Z~8}EL9`~55*420^j4kN)0(;va~t!%UzA(dp7pQw66i5n0-07+n3S)r}hQ4^*$ zwl&b_IL<*MNQSI};{Znv4r|{8)FsLskb_4+8AE^*xX22b)QtdukPo#+7B~iuUB|`^ zxoD{KKHzyzmDO3phjAf8mp1Cw~_jWd=-;S$2ZlsPffvyTcwJU{-dZ*HlmO zbh=G3D@u`ic zB%>2Cnq-#AwG=FwB?JfE51qgba;db=GDHMK6tb&4kgb6+R5|KWKq_?x0N0cdYsHF7 zc`IS&cgFI)?a}+4+O`5Qpn;1C0ZNo(eXF%(+<~DuH47sjRLMgaDx29My-bf+iyKwtlnp_r;yB%dIWx9^~zS)1x+zs2mAEM4_21vT`hC zw%q-!-hDEE{`vm?{9^6p!BAKdWq+&o#vJuB_;K2XY=mzoXpya*w9@1>O8zsOZj zI=PO{L8f#$t;0d1*5LtMX#ZN8!`X#A3Ch7W(Hlx3MURYm-*BNAmtBqwfKpdib*|~M z*ys|eBH0V8$4+addwQ{0nu9N?HbiIQiJai+$f|MhM^)eDdBfNefF)uBiHqP>exR$5 z6kil&@6Ctr8OFrmZJiAXA@7XvgALa6ziVtdw&2la{2_a&P zh$w{2^6S%VvHJMRzgDP$162l0WMF^%uCyu@U)r*DS0dh|79iZN82W8Pnf;&Z{X zYHhkjfDB+PU_+%*9%AxG0W5$A2mlTg<@s>M;a1w@vTfvl{I}l#0H8#+S(PsVymM|bgWxCyN|D+*r;u?(*HAs9 zcFu~yDn*SRPOMfK0p~jM+=c|nlRGO6u52RkU^oku<&vQZLiOuXQ zs{sH2&?Hu@nba`>h&qMUQZG?Mv$$T-4yic)I3?H1FwHRp{Gkf^EmOsR5y>R zJCluf)%N{68^tYc?qzs8Pj`FzW=;bU_V@shGYZKpKl|)cE{|jK-h0Lv5h-X9E-vAv zR5?sD?m3=Y)Tyjzze1zA!}CXPgethqkO{^mV}f1f%6$Q-3jmbT1)Nz(aa-?5Iv0z} zGS|b(q}NH_#9#=LxU`EzfN43O@9!Tx1x!TLx|xg5Hiuut1ipTlzs%<=P?A%nWkis3C@PYo<0%GEK}|w!U;h+Mql(c{hFc@ z7b+`;raty(j5{A(dx{CUR^;!A?Cst+e)qxm-n;%CpWJ1fz}2VkO^W`)@}CR6&a@Vs zVX};+EYm4Lrg(}$2s!BWB^I)Oz4d%jqcLq+Ra|f%owpv-`$8CkJtEQq3#E}U>mrv* zWI4sOwmWr!*e&keK;Ztwcgr->>;!ID$^yK3X^w6eJGaUm;F5Vqz>ZA#X>+(a$lsGy zsp^Mfb94DFK*bUv0IKY?zqDdQT{ReIs*q9&&;V-XfC2@;VznG6qWZ8-4M7EvfpRf$ z23LmgI0742nOaJ0u{R9C1-+%YsVxq}g?Ce*C!mJXI^8TPJP@v2( z@1x96Wxxd+Ze!lB8xv!M=mGJR(3 z)gi540mkyOT&an#@$XNEbFOW#N>(~SZ8~XhoQTLkup5CrEC!GKWc}@aUDaRe6;kJ} z+72oFQ~ekK1h7%g=H+5X-RX%N3|rl$3G>Yn+^xhw(n!XwQr)TGZf3{A<64dogXKc; zW;O!5#|Hp7Ecnfvw}iAhf2Oq#A%qZWe|1xSJRFudy#O_3OkAlBbzv}EC8rnuex+vB zm2yoNUXM@B{vs}`YzzR%gg@D(0~EoK|HPW6BE@Tr$Q^Yznc$G;;|~ zoW6pqQ}q65IX~PzRfFw1S6Ml>?YXvOR*o4H?efUBGa?cyUw<)1RVDOr*V47ou!`Zu z(MMOF^6BV0{#<5PPVlFkw3H}B9E`8^d%a9^0BDRCf-_8lQNPNT7V3}*rHit3)=c8$ zF#DxXmJ89=*Yn;~Yjr!n#|#>4@;n0oS;??QB|s)FEKOLygpkbRQ*F$3jSH@r8p`Si zM<0E@d0L4K12mq*aYR^GgLI^8{{@UPNdLJd%jSBN2d*E2b%&KwK{XyZ``%27vE;|Fhvn<6I7M zFaQ7`07*naR2=`0fA$^;4FF^ot3)p=0Dvy7|4?RRJ40DU-U2c>|6X{>%50G3`D#ke zHTXnC$<_eCWCj4KBk86{(f``_egOa<{_y6xy&%3`d>sJ!>1w;n-xq*01OmpG&h!d* z5iAG^0B@AzvQ(c<_lW7Vw&5@BsCobz@p3Ht2WkF^dkFv!#poTe(Zh>bgBGOR3ziKR zF^@5*SJ-5pjvRwAQ16HKD5{bbTOuEEu7z(`PlMD|v`dptwa3|&cmn{4{^}ZNM_3R= zp_t#P)N`?sY-5Zu&GndUV?P1_C1|^Njoma|Ua(75nC5kQO8}7K)lf&N%gf4&ms?~+ z>B!rc=Ga35fY0oqrs9sgaoVbsS??78KOPUA@WEcF@RDtgJDIDZEDCk*f1O`mFOsFh zJTDkyob!4<^5$D}dq;4E42e>qi(bTDgpAH+2+r8)(K5ukdD^sdfK@j(csuB2l3@Z& z!3tFhRWdHW_TGEIH!sJy%QY4*R9gh1&MhJ>HJ_e*E}vIPJ@fFZyD*nnk& zj4_tk5IN(Kh#?YD7q(7^1@exF*1JGN)YMJ)D`V-qfABE?{KH?qS06?47PB6tuxep3 zj;bKn`nFjfADmqlU68kVo^y6)G;rYxNPpt`l{2uRnH)xtGGbS{Yn$4JNZz6<9)4|i z=jQS+|Lk2lu3-kS1Y9Ca0VaSm><)N2U!H^6L{LU5r*ML>1P}lfKo4j@a3P*C4uHO0 z>YyH5(`IF4$mlC$uyir%UZk(*w^O1In`Z$M0NfV+D?iT}%hIucxTJm)b$Tr}2mne9 z<+jK-_!<9akoh%?Wmb)Q>8l{UuijC&oD{muM_W}{T+7o$Z6jilX5{YmdPKOJ5`=^R zu#Cy4Xm;CW}AKe({eO2DLcKhy~H>DhL zHVVOW$wzt6X*SHx?t+r?P%n>)_GvW%MKKW(gb;n42tKmTgwT5L6>FDl|AWa-p3Hyr zcsXA`tF5iAm%C^B<)=faoKn2Zu<9agjNUXC?|#MO*Q@x!7hWVxm1jS$cI`9m^}iD03g>4RDeiG5fi2uV5>oO7Iu z#f3yf^fWGv~yW8hhN*^j@!u;E|FLm7hK3f=D)r1&b#G}9@8RHSDFff z0U}TI(t_RL?{94t<;2#zjP6M%I04fI>JktjI3;DK1i7YDnD*X>5MXU)CjedMJkePK z3xEL1#UOa(*m_6KMUIi5KgJwTWk4y*^Yt{abBO7F{_Q{drOtvi;{W_V{!3yx03@aY zic>vrR5=K#dqarfR6-lMgm~7can6MhAO>pYouI9g4B|@knhRNqp+x2-ONamv8M2j% z6$+OB$$$UkvyX56=bwHDN=HR>qK&14o}~eyL0Xbz3LAuuxorUaNiZD|R1jla@p0Ei zr|SyH5aKs_+n=~epzOw#iFvOg@iq*hmgBxuW z@$$@h`o*i_4?jm$#2mgH7TVbvz2*ZBF;w z=Z&}i{HCNTY`bc+q~L5bRarkP$FBzr@K-q60zd~jA`}T#V0pt!CpRwSXk2OlWWYA^ zYh%{m%y#;EBxJrmi*T8Dy>9?ujE$}j8)_$C93Jc*)lKYjeCPb$?&0d-(R%tout^y= zhWvgGn;6w>c5F?XVnl|{dCgNHLI#US z#FQuzlAWsk0F4wktT73m143C1jImAYVoI_n{hAjnSH;Eo_*xG+cY6~`QK~DP5Rvhr z?mryd{{Hx#-yJ-Bm~T@gV{Af*3{^~kqVB1?*9Z4A!uZ9Q?vLfe%nlIxz(*i9IO}EE zMo(`_QIc8uHXyNe(dInQx~&C{$|o`ot(QI~&N}ZI5u!yrfhGeHt*8?QC`M$AcV2`i zS1ivn)@cu>SPj}g{;z)o06+V|oqzqmeIEd04BmMr^{I|b$BQ(4JlEu>&a5| zj#2=rAVmnaZ4ENjwZ%{Za0yEANXkb5$hDA4b?yd2U4QSJe+2;l*Z=;Ti+TXajPqP7 z5E`PVj9A99s4+)S00Kyp1$!3Zf&-#t+hl8qp3IVhCF2=Zo#@w1Q6Bo*D+vG!bp`z; zoIS}`1j=*%pa5LRPQ>K4-rV9>r>h_O!ymer^EELJ&al9xYvdoQ7%|s+m0( zaj?CLf6d$Wc#Y4i%USp`OIKvCZ{H&_XBI-`H%Iatcl%PZly9WOtkJ#xkt%;NbuS6R zkAL>}t+}|Jt_Nk?nKSc5sT0KFLZ9+(8`myuy|CvN7q1ef4J>z{UkLUrXHUxXxN^_R zhUNYFVq$z+S~#5By*WKSTx&S4hw1YvE*Dv+Ap!tWiY(8lC$@Gr%%Sx@w5?e#YD8Gf zm#upe@k8mdif&21dXqV_LhF8S(3+r>b_9h0=dC*5`H?~nOg37otz}VBo@Zjv%SVGe z&s|*`NLzV6ErbP{|3CSCi*)S${w!|ONSfA8Zj4&bUGuM)~RJ>RStnR;?T zndO@H$37JzlNgWUoXg^?*h-rDi+J!eU5rl6!8ijVOtP0(Loi2Z&!k$7O?BO9C8XAE zvy3r1XQY(T7{-{gSuc)ah(wXI_p1J4zIahDmocFav{<_7!kEeV+4uYm!3Kj^=5`jB zN7L8TE@JRf7rnvuyBlx3U*0$^1saA&)0!oiW;|{1H^##Uo6Q@gzTwq_!_mXz@q_*G z?SpHZyW4~M-qyj}*V1>#;_KN#c1aL_HJoG)$xwB;D>+x#^3xu??@@LJX-Cu~flJIPCbL`m?HlrLA4e3dO!lyoyG3>l z?(D-0SStiw8MAQxIc8jSS;I}a@n(L7JMlqkUc|+5Z2!aTmw#`cKaGnB1OPDwoym&T z!W~4+jECnh4~d`aZiRG3O;%S`SS!F2rMmqUtY5Ag)Fqn$Kq(+ZmpA|*A|+{>VY|E! zBpK3ZxLM?-i4z;n^#qXd6oXX7dbj;lLCRqAQd-WNwfO!j8P*s)&L8voB`PU|)+{Sn zQFGV8U>*kz^^)2I+3SsO%w~(=t!roN)#S_d1YK@+2~qS-k!RzLAu`;$yE#3cg%HOZ zL(Z8jo=U9ybib6F>)$O@K9*(0`V4wN6s6u$fzBJ60suqGNGXy4;CT83EyD-^fUaL` zS9worz-)z;LBI!tyF~HWsz-}j=&HL=yU&zxN0L{?q^cH6G$B$vwH(+Al84uHB?-ll1(B{Y944L-}Aq8~{>GJL&d*dLCvi zydEcj$Sy9y7!R&NfK#FS(=j9afjj8(wL%v2WIuEVe{G(8<&pkyKUBhK@X6T?;kN1j<+whdI$tkb+b@r-^1ptv*YczO4{xTd5kui6S<^bKa zm+X5;p*f0f;X}%@o)8&@4WVk8_094S0B#lABYEJx{d~cn)(b9IV7}k_?sq?aw0p(w z0aVt%vNDjO55DDG6}`=Naqisj_waEWD&5wzSWj5^O5V<~zdiqT2ICC4&X{a&V}2I_N$1p8X) zEX$K~D25F9W8F?6alL9m3#BN1ZYOo>#7 z`jlp zqXh#OeSJM4Ick|w8m37sGPuK#Ul(@{?OsDmNVFaxuU>1fQ|Kr~N-12`x*>oM-Jzq{ zi5BydzMiA_GEEoCFQ|SDFXpfWfWcsJ>YW=MwZ~E9n5nB9^wGplkhB9QZIs3S;&tk6 z0078>H@02b(xvFLax7ILRH2KJkU0^#cJAt<7;Gl_C=U#QlI+;VxkwCQ|6q4EpJaS1 zGvglJGOZzHQh(9NgGG30+_{&ZtpRJe5HY0&c5Sxn^-wM+ksk$wZ zS5Z_+4n(0*WJ-kKl~Rl`!B1UxR&N!T#R@*nW!C{eceA?J?~*W>98$CVjEOypA(-rV z`bxI$sZ=3$Jl(6x5ADy!D3>H?9VF)3cx{ex!j;^qKiFlKq6 z^5J>P7GgHM@JYcjMk1!#!Dzu^osMcc)Jhb&8sT*Su=4nj9RpH~fpdvSLS)nCv!G@U zYRBePsVCD(e^3pF!Pw{4S|PQ}@=>40BiPttg8@~8R188f2pfI6yP?Lpu&tRNOcGt) z_Kd+^MkNElox$z9;?w~XxfIhy3k-XO-r4Bo8)#x<8E=!E261HY(PZ*TQ^))e^+N0Z zlDP?aO!D2Ezj1f`b`&fKb{bMw7bFrA5CD&Q`<6@k>CIjLim&a_Rl$|C!cNn!{7W`=fXn4k<5UxD@11i z&WIQ%65~X)0Xws}3y4+3UQays(?i(30}lzvs)jLk!FBu&IU_4jyJN9pjjfxyZ0d zoGu0hvdd<0Yj9pSi%OpCCB@a%|4}maaaWSl`5-IDSvfuxemxz!9P7dL$wpZQhR$M)Ynk-QVoJS-FSI7gp`g>9J3%sKF_a8pe(HnaI*wmrqGSL`~Zo zYrA`ih>6I$(6p{@R+CWPyLJl0O3_E(GOncVaha_(j=;E=f&W~@M2~NXl$W_04PHqJ z&3L@oGo!wa-MzT#O+R0SM`WDkNVM9flvR;!C;NF;jgp%a5kTbhQ%;Yku2_b)d(EOZ zZ$G^AgP&fFOXXYz*AgY5WSU9GmR?8QahY8Jt6h`&7yD_W*jThLBdvvON@jTgfLXQm zD%{@sKt#q;Oi-|EeRo%yQB5;L3&t?V!HC^R5Fwk)7RQ2fm1V*Em?G2arT_3r{Mj?} z>BJsgzqWN_XJM&MZ;8;%j}zB`NWlRm zM3mSLH){YmJQzna?_-gxQpqCQxp~$HlNn|eovd9#yR1!E1$0UQuw5<)5QPLvvbD%K zPZSxlM1X*WMUw#&s0!x;($M**-KK=(*Y7TV?fahrz`y%9zqXv`0079UsuV(E-n$%L zG4Cyp53RL;jG_lhZPV6qAwf$XZ;Add7K;STQ1cW%J)jqiF>nda&x8C=|MX8!fBDgm ze)N+c|L}kQpZ--0j#6M;h!Zmt8Q#ZRybs z%yVGQ)i<(X$+(5HoYc(fYs0s0-MST1(1Tp@Q+;K5yj)aT#^6>D1W~jovWxFSFVhp_ zOL-PvT{|aA!J8DlE`~zoLS`|zR{{GYg=gs{Zf<35@*ys9m=&egJ>y?47p+tyOqf&4 zW5`v(g$lk)xHtkTo@>_AFchIr5dnz-=P{RI*yA^CN>XQ1s$t$&r&9t#3Ycf5rcr{} z&^}1-ol{H)mak3~7(<|yvj$iffXF$Q7X?EE0AqYg5JHM6Nx@gL?SLs*poCnUv$Ube zxb$I`@Tp|;EN)cAX4V(ABgH)cQ}T5UbQ`gaX(-~AK2$N-3q@FqM^fHGkkG*=T%&cH zMR9oi_~&L%A|L2#4mUW<3laN*r{K8Cd;QU(ZUlrPzak@JTsTuB<4RXovZc9D-Zfk( zJNG6e#`;X_i9KxC!EQa-%CDu+#$dczWaY+J>296v8ZiXVMHZYhOogrIgsnU~O+OTxO3Oc|4y?=O!M`=hL`fhnZw*Sd3E4h<#3RGs{bgPF;Q;kp&d< zCYX;N>y(KA!EoG0?8~#E|CkMgvpDd_wmnf9q#sT-kIF{+Hr7lw(u&W6=2p| zN+Fmg5%8>p7&;dKp%g;^Q=?E698;PBq14P8(uHGC*H%eEoJESj1tbOxn1S5%NM}(j z02u&rB%!n0503ufuik2Fb+p(2)^C4-oOd`X06>J8DEPp|ni>FOm;#pyxq#>i$T7Di zc6Ky|bP{^*b3a^6ZQc3dW*iVF$M*Eio}F+N)E z{@u}sZE}HO8N%CQG>|9Bi_o|A#&`Q~-3{5TuyGr&cV>CYv88oW|KX4wZDqBPs`jgg zzGR_+yxSu(-o-K|SaerR_pJ||zKLljrk<$0CseMo-a1L)HH@i8^h~JyChVxZAJte% z(JZD%lcjA(gN@^qoD?ckMRqcsTq)QpxGKcBWIF}h&c!%Wt6gu5x3@O0UHGtHL5?uT zjS+vCS`I~X96vI4&)CpfUpHnlYp3%T02o8gG0StM6hrLzxQwwFqm&W=TkByZI6^6U zwgyz_2A8?@Pl12JOe*Q7VrRX6M0T}4+S-W05HO;2S;qh_k;DK1IR)>dZ-c9uP`SP{ zYp33~qrvrG@7;Z;fBXI6{en-4+|ql-)hXWd!TtL`{P`#6cWsGc@P;T!mA&RBBI7`~ zn9P=@$NM{aQ0xrK#j+Ar#^LDG!=FS-QunfIEB-3}kI|bL49DobtEysSb2Lz+3OA0@ z697<9Y_kUdVC1o`@~vxIMJ@+odr_O##YBXN8-r4m=JVzQmz)wK2E9tW7^$YzXXoMp zrxYH==0h|2@zLiW@V!tk)Uw?f3@L@2L7)3zUogf3$o(bJ<+?oWRDZ#d{9%YJtJ;-l${)-T@}JV*g%hm&AD0C3JZXCPLf zR>s%`(EyfM(h)i`6%hq{ssJlGAZ`le4KUu(w03Sx0buXtz*^VDqoQb5C5RM(3zS)8 ziXlL7ovt94k|&RpP%5Crbs?3;4q03W<>c^R8>Yu}o&|Sn%6n>>|y;cWtZC zk-G-t`d|JJKLmiE|K~d&{`gkc89RBZFvf)EW#NU+WPak6Ae54VcoIJDx3NturNkLK ztCK7(4vX$dI=e&{ZUHiWwh}pydZ~)hJH2ZF;3?G<7NPmg>Vde_wgv#HE5<}=@Ey3n zF&*8h?|gUX-Ot1RJhlJ;=8)7`G$h0eYafvbyS$N8@{)P8nQtSf#YhH$P- zUaHN)-zw-Dm(K$JtZsHW7etAOQlc1BOq61D)^@Vz-h0M5B0A?7W8O#a30gsJsvyWI5=eM>2Dp}5Gn%#@TdQHTE@vg=Da@?bo

    %1UdL~g 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

    ToS2x1#vrlp!35Wd1wMg>hK2|k8q>eK zfZ7Ib*8;WweR*^pWc$7p-}9l1^1s;33l9BS!rKF$EReeW-re1$c3xZ4C4BF zeOTZ@E#gU&r|!PlIp0wTBTT_eTI*6}kKW}9m$R3LSFWaoXyZraOmcsqM-1R0&b2?# zn#vbE1ypMfc>mr|BvBU+%)=vKe=61e#GoB?_h0|UayS3Z=ogdyD(-)Mhu>pEkxx*W zwPq8$n&XI(!ZUN0;tqqMvBsqIyBJOcQ3wLK1jUNKAXq>2J0vGFHg|~?)Wsx_P7&YysAd0I{+TFsO`1y0c)9tls3Jiop$IM#Br26`9EyH~ z&Ey*9Q!X;WSlT|k5hfj`nK#}Gx`Om7Zui`kH zIa9azh3O(M90qcF6Cc%vm^Pw%&M>dXa1V&y4ugs0dQ^Kpldy|7wK-dUlne>+qM+-LpyF6ePJ&cr$f#{VHvta zS8Ry_&3deQ7%UEW$4W{m^hMmHz4~<>L6s1V#OcyboK!sYVt+=X6QM)B0Z+Ix% zT%m-v6)`gf0hXfPraK1WHF{EcpTd{e^p}S9GzG&mz8VG&$C3e*Q7ztTo6C_nf_-=aXFEQ7KaV0?z2Vx;i+TKvA{8;q&kB{q2>%?_8SQ zdi(7?G4uzm0#eb}ZwDojwWb4b81+Qx5Y`V?+XwLc+`OAvTqKE+dG_oX07iT@Qd$LZ zFi_we)8i#Pi4$KKU3}uwR2NL9b7mjV5tfm^UPp!Egnud#o$K${IADB{(+@&hmgo}%#<$} zTTUv}PXv=rq|Y1t>1LnmZNAY!CuHT zcXLL$N8<yt-0TVtc0M=&l+ zHU}d8e|Cka=+u7dinzK{LgWJQm;r_w{tN~Lf(Vg2~r2Z6gpPC8R``nm63Maq~#pJ9^BWw-{+TS<4E zlAQe8Pqki>NX{o^b8ZV}S>=9KsWV?W?>QX~Mlf-s~&6_H3#;<7H_jm>0p0aqbvL#JTC{f4hYILO-4l^Y-Q=U{)yn{%rv5 z)IH!`F1)M-sfx~LY~Z0i+uGasfcNtLA&rjRMfp>|T=@eE-^BV6_#p&KLA7@Q2#Tvp}go?&|%r#2u6VOT-neX4I}L zFlT|Fekh={%5(E0)sGn)XK%19TamLHTf{t*Pn&_(=dP+y|U$)4=-%RE9p^ zlDMojw6w&5kQY1xsRGWMz*FS%<0aD;*wnt;FY)CZzSq~!e7li^lQ_!Sh!6VJ2Z2^x zUBQ0o`{kl-8zC7AHbuMLLLH#5;64LDGr$E!fKYWgT=u2b1&+M0HrCdIjfaIv%B^r6 zFGv?Kn1jo4@s$OPK^~9jhQ>Kgxn`vhKoP+4FA9c!(EEo$0$X4h9RRq%=4rFJ4!|Y7 zZ^qz)x$;uMnRU6}%F^Cl_`7II=k*S9%47{C!<|?{6X66{dRpH!6=v*sSyTwrKQQRY zDWh|jS$i>+5xjQW?{d{1T$gR^58Vu95L3Th#MFB9IeE@ts5M+tCG>Z-Z{Xpl=kblF zq`87_GJ%>4^K|X?OTIZ@X0-FzbMka?lMg;ud30z{fXd&@G4<>0atZ3M?idwWb?y22 zE=dW<4!n>al%5O33) zN&01%y2YoXRFS^mUsp?ngUjB>V)Lwv}=t1Vh=t(+s)!ElUH-TvPYuq3*I z+$-iHxS9<3AU%`gzLu4#Du89q7HD1f$V!`=1x&>Z_4M@Kzfb04hhG+rCJsOgvNM*& zQgjFMBF2Cu5+qpc0|WnX^MAmUq77Ij&=9=#_;3ZBOe{++=O=6atL-{tHbH

    okeg;!B@VD?L(j)f7 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 g4P&)PW>#sYdSy<9;5X0oQ>wsMy5(4{S&;+5i9m diff --git a/ego/examples/tutorials/requirements.yml b/ego/examples/tutorials/requirements.yml deleted file mode 100644 index 7fe086c7..00000000 --- a/ego/examples/tutorials/requirements.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: openMod_Zuerich2018 - -channels: - - conda-forge - - anaconda - -dependencies: - - python=3.5 - - notebook # jupyter notebook - - numpy=1.14.3 - - pandas=0.20.3 - - pyyaml=3.12 - - requests - - sqlalchemy - - Rtree=0.8.3 - - scikit-learn - - scipy - - ipywidgets - - pip: - - ding0==0.1.4 - - "--editable=git+git@github.com:openego/eGo.git@dev#egg=eGo --process-dependency-links" - - "--editable=git+git@github.com:openego/oedialect.git@master#egg=oedialect" - - "--editable=git+git@github.com:openego/ego.io.git@v0.4.0#egg=egoio" - - "--editable=git+git@github.com:openego/PyPSA.git@dev#egg=pypsa" - - "--editable=git+git@github.com:openego/eTraGo.git@0.5.1#egg=etrago" # eTraGo==0.5.1 - - "--editable=git+git@github.com:openego/tsam.git@master#egg=tsam" - - "--editable=git+git@github.com:openego/eDisGo.git@dev#egg=edisgo" From 3d6d5118dbc8f170b6229015147192b0d7cd2df6 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 13 Apr 2023 21:17:49 +0200 Subject: [PATCH 061/126] Adapt scenario settings --- .../edisgo_workflow/scenario_setting.json | 19 ++++----- ego/scenario_setting.json | 40 ++++++++++++++++--- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/ego/examples/edisgo_workflow/scenario_setting.json b/ego/examples/edisgo_workflow/scenario_setting.json index 5ae1ee0f..2e2ebbb1 100644 --- a/ego/examples/edisgo_workflow/scenario_setting.json +++ b/ego/examples/edisgo_workflow/scenario_setting.json @@ -45,30 +45,31 @@ "db": "oedb", "gridversion": "v0.5.0", "grid_path": "grids", - "choice_mode": "manual", - "cluster_attributes":["wind_cap", "solar_cap", "emob_cap"], + "choice_mode": "cluster", + "cluster_attributes":["pv_capacity_mw_per_km2", "wind_capacity_mw_per_km2", "electromobility_max_load_mw_per_km2", "pth_capacity_mw_per_km2"], "only_cluster": false, - "manual_grids": [33532], + "manual_grids": [32355], "n_clusters": 50, "parallelization":false, "max_calc_time": 0.5, "max_workers":1, - "initial_reinforcement":true, "max_cos_phi_renewable": 0.9, "results": "results_edisgo", "solver": "glpk", - "timesteps_pfa": "snapshot_analysis" + "timesteps_pfa": "snapshot_analysis", + "tasks": ["2_specs_overlying_grid"] }, "database": { "tables": { "egon_mv_grid_district": "grid.egon_mv_grid_district", - "egon_hvmv_substation": "grid.egon_hvmv_substation", - "generators_pv": "supply.egon_power_plants_pv", + "generators_pv_status_quo": "supply.egon_power_plants_pv", "generators_pv_rooftop": "supply.egon_power_plants_pv_roof_building", - "generators_wind": "supply.egon_power_plants_wind", + "generators_wind_status_quo": "supply.egon_power_plants_wind", + "generators": "supply.egon_power_plants", "etrago_load": "grid.egon_etrago_load", "etrago_load_timeseries": "grid.egon_etrago_load_timeseries", - "weather_cells": "supply.egon_era5_weather_cells" + "heat_pump_capacity_individual": "supply.egon_individual_heating", + "pth_capacity_district_heating": "grid.egon_etrago_link" }, "database_name": "", "host": "127.0.0.1", diff --git a/ego/scenario_setting.json b/ego/scenario_setting.json index d5ebde61..496063b0 100644 --- a/ego/scenario_setting.json +++ b/ego/scenario_setting.json @@ -3,7 +3,10 @@ "eTraGo": true, "eDisGo": true, "csv_import_eTraGo": false, - "csv_import_eDisGo": false + "csv_import_eDisGo": false, + "data_dir": ".../data_in", + "results_dir": ".../data_out", + "random_seed": 42 }, "eTraGo": { "db": "oedb", @@ -46,14 +49,41 @@ "cluster_attributes":["farthest_node", "wind_cap", "solar_cap", "extended_storage"], "only_cluster": false, "manual_grids": [], - "no_grids": 2, + "n_clusters": 2, "parallelization":true, "max_calc_time": 0.5, "max_workers":2, - "initial_reinforcement":true, "max_cos_phi_renewable": 0.9, "results": "results/another_result", "solver": "gurobi", - "timesteps_pfa": "snapshot_analysis" - } + "timesteps_pfa": "snapshot_analysis", + "tasks": ["2_specs_overlying_grid"] + }, + "database": { + "tables": { + "egon_mv_grid_district": "grid.egon_mv_grid_district", + "generators_pv_status_quo": "supply.egon_power_plants_pv", + "generators_pv_rooftop": "supply.egon_power_plants_pv_roof_building", + "generators_wind_status_quo": "supply.egon_power_plants_wind", + "generators": "supply.egon_power_plants", + "etrago_load": "grid.egon_etrago_load", + "etrago_load_timeseries": "grid.egon_etrago_load_timeseries", + "heat_pump_capacity_individual": "supply.egon_individual_heating", + "pth_capacity_district_heating": "grid.egon_etrago_link" + }, + "database_name": "", + "host": "127.0.0.1", + "port": "59700", + "user": "", + "password": "" + }, + "ssh": { + "enabled": true, + "user": "", + "ip": "", + "port": "", + "local_address": "127.0.0.1", + "local_port": "59700" + }, + "external_config": "~/.ego/secondary_ego_config.json" } From 81d87dc0121958ef6e92d1470943dab5191cd287 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 13 Apr 2023 21:25:26 +0200 Subject: [PATCH 062/126] Bug fix remove initial reinforcement --- ego/tools/edisgo_integration.py | 359 +++----------------------------- 1 file changed, 34 insertions(+), 325 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index ef984a32..00d790ef 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -18,7 +18,7 @@ # File description """ -This file is part of the the eGo toolbox. +This file is part of the eGo toolbox. It contains the class definition for multiple eDisGo networks. """ __copyright__ = ( @@ -56,7 +56,7 @@ from egoio.tools import db from ego.mv_clustering import cluster_workflow - from ego.mv_clustering.database import get_engine, register_tables_in_saio + from ego.mv_clustering.database import get_engine from ego.tools.economics import edisgo_grid_investment from ego.tools.interface import ETraGoMinimalData, get_etrago_results_per_bus @@ -524,7 +524,6 @@ def _set_scenario_settings(self): self._grid_path = self._edisgo_args["grid_path"] self._choice_mode = self._edisgo_args["choice_mode"] self._parallelization = self._edisgo_args["parallelization"] - self._initial_reinforcement = self._edisgo_args["initial_reinforcement"] self._cluster_attributes = self._edisgo_args["cluster_attributes"] self._only_cluster = self._edisgo_args["only_cluster"] self._max_workers = self._edisgo_args["max_workers"] @@ -533,10 +532,6 @@ def _set_scenario_settings(self): self._max_calc_time = self._edisgo_args["max_calc_time"] # Some basic checks - if not self._initial_reinforcement: - raise NotImplementedError( - "Skipping the initial reinforcement is not yet implemented" - ) if self._only_cluster: logger.warning("\n\nThis eDisGo run only returns cluster results\n\n") @@ -807,8 +802,9 @@ def run_edisgo(self, mv_grid_id): # ################### task: setup grid ################## if "1_setup_grid" in config["eDisGo"]["tasks"]: edisgo_grid = self._run_edisgo_task_setup_grid( - mv_grid_id, scenario, engine, logger, config) - if not "2_specs_overlying_grid" in config["eDisGo"]["tasks"]: + mv_grid_id, scenario, engine, logger, config + ) + if "2_specs_overlying_grid" not in config["eDisGo"]["tasks"]: edisgo_grid.save( directory=os.path.join(results_dir, "grid_data"), save_topology=True, @@ -837,11 +833,13 @@ def run_edisgo(self, mv_grid_id): import_heat_pump=True, import_dsm=True, import_overlying_grid=False, - from_zip_archive=True + from_zip_archive=True, ) edisgo_grid.legacy_grids = False - edisgo_grid = self._run_edisgo_task_specs_overlying_grid(edisgo_grid, logger) - if not "3_optimisation" in config["eDisGo"]["tasks"]: + edisgo_grid = self._run_edisgo_task_specs_overlying_grid( + edisgo_grid, logger + ) + if "3_optimisation" not in config["eDisGo"]["tasks"]: edisgo_grid.save( directory=os.path.join(results_dir, "grid_data"), save_topology=True, @@ -857,7 +855,6 @@ def run_edisgo(self, mv_grid_id): ) return {edisgo_grid.topology.id: results_dir} - # ########################## task: optimisation ########################## # ToDo Maike Call optimisation @@ -929,9 +926,7 @@ def _run_edisgo_task_setup_grid(self, mv_grid_id, scenario, engine, logger, conf logger.error(msg) raise Exception(msg) - edisgo_grid = import_edisgo_from_files( - edisgo_path=grid_path - ) + edisgo_grid = import_edisgo_from_files(edisgo_path=grid_path) edisgo_grid.legacy_grids = False edisgo_grid.set_timeindex(pd.date_range("1/1/2011", periods=8760, freq="H")) @@ -946,9 +941,9 @@ def _run_edisgo_task_setup_grid(self, mv_grid_id, scenario, engine, logger, conf storage_units_parametrisation=None, ) # overwrite p_set of conventional loads as it changes from scenario to scenario - edisgo_grid.topology.loads_df["p_set"] = ( - edisgo_grid.timeseries.loads_active_power.max() - ) + edisgo_grid.topology.loads_df[ + "p_set" + ] = edisgo_grid.timeseries.loads_active_power.max() logger.info("Set up generator park.") edisgo_grid.import_generators(generator_scenario=scenario, engine=engine) @@ -1005,9 +1000,9 @@ def _run_edisgo_task_specs_overlying_grid(self, edisgo_grid, logger): Returns the complete eDisGo container, also including results """ - logger.info(f"Start task 'specs_overlying_grid'.") + logger.info("Start task 'specs_overlying_grid'.") - logger.info(f"Get specifications from eTraGo.") + logger.info("Get specifications from eTraGo.") specs = get_etrago_results_per_bus( edisgo_grid.topology.id, self._etrago_network, @@ -1021,7 +1016,8 @@ def _run_edisgo_task_specs_overlying_grid(self, edisgo_grid, logger): etrago_year = specs["timeindex"][0].year if edisgo_year != etrago_year: timeindex_new_full = pd.date_range( - f"1/1/{etrago_year}", periods=8760, freq="H") + f"1/1/{etrago_year}", periods=8760, freq="H" + ) # conventional loads edisgo_grid.timeseries.loads_active_power.index = timeindex_new_full edisgo_grid.timeseries.loads_reactive_power.index = timeindex_new_full @@ -1035,15 +1031,17 @@ def _run_edisgo_task_specs_overlying_grid(self, edisgo_grid, logger): edisgo_grid.heat_pump.heat_demand_df.index = timeindex_new_full # flexibility bands edisgo_grid.electromobility.flexibility_bands[ - "upper_power"].index = timeindex_new_full + "upper_power" + ].index = timeindex_new_full edisgo_grid.electromobility.flexibility_bands[ - "upper_energy"].index = timeindex_new_full + "upper_energy" + ].index = timeindex_new_full edisgo_grid.electromobility.flexibility_bands[ - "lower_energy"].index = timeindex_new_full + "lower_energy" + ].index = timeindex_new_full # TimeSeries.timeindex edisgo_grid.timeseries.timeindex = specs["timeindex"] - logger.info("Set generator time series.") # active power edisgo_grid.set_time_series_active_power_predefined( @@ -1081,7 +1079,7 @@ def _run_edisgo_task_specs_overlying_grid(self, edisgo_grid, logger): # decentral hp_decentral = edisgo_grid.topology.loads_df[ edisgo_grid.topology.loads_df.sector == "individual_heating" - ] + ] if hp_decentral.empty and specs["thermal_storage_rural_capacity"] > 0: raise ValueError( "There are thermal storage units for individual heating but no " @@ -1089,10 +1087,9 @@ def _run_edisgo_task_specs_overlying_grid(self, edisgo_grid, logger): ) if specs["thermal_storage_rural_capacity"] > 0: tes_cap = ( - edisgo_grid.topology.loads_df.loc[hp_decentral.index, "p_set"] - * specs["thermal_storage_rural_capacity"] - / edisgo_grid.topology.loads_df.loc[ - hp_decentral.index, "p_set"].sum() + edisgo_grid.topology.loads_df.loc[hp_decentral.index, "p_set"] + * specs["thermal_storage_rural_capacity"] + / edisgo_grid.topology.loads_df.loc[hp_decentral.index, "p_set"].sum() ) # ToDo get efficiency from specs edisgo_grid.heat_pump.thermal_storage_units_df = pd.DataFrame( @@ -1104,14 +1101,14 @@ def _run_edisgo_task_specs_overlying_grid(self, edisgo_grid, logger): # district heating hp_dh = edisgo_grid.topology.loads_df[ edisgo_grid.topology.loads_df.sector == "district_heating" - ] + ] # ToDo check if not hp_dh.empty: if specs["thermal_storage_central_capacity"] > 0: tes_cap = ( - edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"] - * specs["thermal_storage_central_capacity"] - / edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"].sum() + edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"] + * specs["thermal_storage_central_capacity"] + / edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"].sum() ) # ToDo get efficiency from specs edisgo_grid.heat_pump.thermal_storage_units_df = pd.concat( @@ -1172,7 +1169,7 @@ def _run_edisgo_task_optimisation(self, edisgo_grid, logger): Returns the complete eDisGo container, also including results """ - logger.info(f"Start task 'optimisation'.") + logger.info("Start task 'optimisation'.") return edisgo_grid def _run_edisgo_task_grid_reinforcement(self, edisgo_grid, logger): @@ -1190,297 +1187,9 @@ def _run_edisgo_task_grid_reinforcement(self, edisgo_grid, logger): Returns the complete eDisGo container, also including results """ - logger.info(f"Start task 'grid_reinforcement'.") + logger.info("Start task 'grid_reinforcement'.") return edisgo_grid - # def _run_edisgo(self, mv_grid_id): - # """ - # Performs a single eDisGo run - # - # Parameters - # ---------- - # mv_grid_id : int - # MV grid ID of the ding0 grid - # - # Returns - # ------- - # :class:`edisgo.EDisGo` - # Returns the complete eDisGo container, also including results - # - # """ - # self._status_update(mv_grid_id, "start", show=False) - # - # # ##################### general settings #################### - # config = self._json_file - # engine = get_engine(config=config) - # scenario = config["eTraGo"]["scn_name"] - # - # # results directory - # results_dir = os.path.join( - # config["eGo"]["results_dir"], self._results, str(mv_grid_id) - # ) - # if not os.path.exists(results_dir): - # os.makedirs(results_dir) - # - # # logger - # if self._parallelization: - # stream_level = None - # else: - # stream_level = "debug" - # setup_logger( - # loggers=[ - # # {"name": "root", "file_level": None, "stream_level": None}, - # # {"name": "ego", "file_level": None, "stream_level": None}, - # {"name": "edisgo", "file_level": "debug", "stream_level": stream_level}, - # ], - # file_name=f"run_edisgo_{mv_grid_id}.log", - # log_dir=results_dir, - # ) - # # use edisgo logger in order to have all logging information for one grid go - # # to the same file - # logger = logging.getLogger("edisgo.external.ego._run_edisgo") - # - # # ################### get requirements from overlying grid ################## - # logger.info(f"MV grid {mv_grid_id}: Calculating interface values.") - # specs = get_etrago_results_per_bus( - # mv_grid_id, - # self._etrago_network, - # self._pf_post_lopf, - # self._max_cos_phi_renewable, - # ) - # - # # ################### start setting up edisgo object ################## - # logger.info(f"MV grid {mv_grid_id}: Initialize MV grid.") - # grid_path = os.path.join( - # config["eGo"]["data_dir"], - # config["eDisGo"]["grid_path"], - # str(mv_grid_id), - # ) - # if not os.path.isdir(grid_path): - # msg = f"MV grid {mv_grid_id}: No grid data found." - # logger.error(msg) - # raise Exception(msg) - # - # edisgo_grid = import_edisgo_from_files( - # edisgo_path=grid_path, legacy_ding0_grids=False - # ) - # edisgo_grid.set_timeindex(specs["timeindex"]) - # self._update_edisgo_configs(edisgo_grid) - # - # # set conventional load time series (active and reactive power) - # edisgo_grid.set_time_series_active_power_predefined( - # conventional_loads_ts="oedb", engine=engine, scenario=scenario - # ) - # edisgo_grid.set_time_series_reactive_power_control( - # control="fixed_cosphi", - # generators_parametrisation=None, - # loads_parametrisation="default", - # storage_units_parametrisation=None, - # ) - # # ToDo change p_set of conventional loads to peak in time series? - # - # # ########################### generator data ############################# - # logger.info("Set up generator data.") - # # import generator park of future scenario - # edisgo_grid.import_generators(generator_scenario=scenario, engine=engine) - # - # # set generator time series - # # active power - # edisgo_grid.set_time_series_active_power_predefined( - # dispatchable_generators_ts=specs["dispatchable_generators_active_power"], - # fluctuating_generators_ts=specs["renewables_potential"], - # ) - # # reactive power - # if self._pf_post_lopf: - # # ToDo Use eTraGo time series to set reactive power (scale by nominal power) - # edisgo_grid.set_time_series_manual( - # generators_q=specs["generators_reactive_power"].loc[:, []], - # ) - # pass - # else: - # edisgo_grid.set_time_series_reactive_power_control( - # control="fixed_cosphi", - # generators_parametrisation="default", - # loads_parametrisation=None, - # storage_units_parametrisation=None, - # ) - # - # # requirements overlying grid - # edisgo_grid.overlying_grid.renewables_curtailment = specs[ - # "renewables_curtailment" - # ] - # - # # check that all generators and conventional loads have time series data - # # ToDo can caplog be used to check for warnings or does it interfere with - # # logger? - # edisgo_grid.check_integrity() - # - # # ########################## battery storage ########################## - # logger.info("Set up storage data.") - # # import home storage units - # edisgo_grid.import_home_batteries(scenario=scenario, engine=engine) - # - # # requirements overlying grid - # edisgo_grid.overlying_grid.storage_units_active_power = specs[ - # "storage_units_active_power" - # ] - # - # # ToDo distribute storage capacity to home storage and large storage (right now - # # only work around to have storage capacity in the grid) - # edisgo_grid.add_component( - # comp_type="storage_unit", - # bus=edisgo_grid.topology.mv_grid.station.index[0], - # p_nom=specs["storage_units_p_nom"], - # max_hours=specs["storage_units_max_hours"], - # type="large_storage", - # ) - # - # # ################################# DSM ################################## - # logger.info("Set up DSM data.") - # # import DSM data - # edisgo_grid.import_dsm(scenario=scenario, engine=engine) - # - # # requirements overlying grid - # edisgo_grid.overlying_grid.dsm_active_power = specs["dsm_active_power"] - # - # # ####################### district and individual heating ##################### - # logger.info("Set up heat supply and demand data.") - # # import heat pumps - also gets heat demand and COP time series per heat pump - # edisgo_grid.import_heat_pumps(scenario=scenario, engine=engine) - # - # # thermal storage units - # # decentral - # hp_decentral = edisgo_grid.topology.loads_df[ - # edisgo_grid.topology.loads_df.sector == "individual_heating" - # ] - # if hp_decentral.empty and specs["thermal_storage_rural_capacity"] > 0: - # raise ValueError( - # "There are thermal storage units for individual heating but no " - # "heat pumps." - # ) - # if specs["thermal_storage_rural_capacity"] > 0: - # tes_cap = ( - # edisgo_grid.topology.loads_df.loc[hp_decentral.index, "p_set"] - # * specs["thermal_storage_rural_capacity"] - # / edisgo_grid.topology.loads_df.loc[ - # hp_decentral.index, "p_set"].sum() - # ) - # # ToDo get efficiency from specs - # edisgo_grid.heat_pump.thermal_storage_units_df = pd.DataFrame( - # data={ - # "capacity": tes_cap, - # "efficiency": 0.9, - # } - # ) - # # district heating - # hp_dh = edisgo_grid.topology.loads_df[ - # edisgo_grid.topology.loads_df.sector == "district_heating" - # ] - # if hp_dh.empty and specs["thermal_storage_central_capacity"] > 0: - # raise ValueError( - # "There are thermal storage units for district heating but no " - # "heat pumps." - # ) - # if specs["thermal_storage_central_capacity"] > 0: - # tes_cap = ( - # edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"] - # * specs["thermal_storage_central_capacity"] - # / edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"].sum() - # ) - # edisgo_grid.heat_pump.thermal_storage_units_df = pd.concat( - # [ - # edisgo_grid.heat_pump.thermal_storage_units_df, - # pd.DataFrame( - # data={ - # "capacity": tes_cap, - # "efficiency": 0.9, - # } - # ), - # ] - # ) - # - # # requirements overlying grid - # edisgo_grid.overlying_grid.heat_pump_decentral_active_power = specs[ - # "heat_pump_rural_active_power" - # ] - # edisgo_grid.overlying_grid.heat_pump_central_active_power = specs[ - # "heat_pump_central_active_power" - # ] - # edisgo_grid.overlying_grid.geothermal_energy_feedin_district_heating = specs[ - # "geothermal_energy_feedin_district_heating" - # ] - # edisgo_grid.overlying_grid.solarthermal_energy_feedin_district_heating = specs[ - # "solarthermal_energy_feedin_district_heating" - # ] - # # ToDo CHP + resistive heaters - # - # # ########################## electromobility ########################## - # logger.info("Set up electromobility data.") - # # import charging points with standing times, etc. - # edisgo_grid.import_electromobility( - # data_source="oedb", scenario=scenario, engine=engine - # ) - # # apply charging strategy so that public charging points have a charging - # # time series - # edisgo_grid.apply_charging_strategy(strategy="dumb") - # # get flexibility bands for home and work charging points - # edisgo_grid.electromobility.get_flexibility_bands( - # edisgo_obj=edisgo_grid, use_case=["home", "work"] - # ) - # - # # requirements overlying grid - # edisgo_grid.overlying_grid.electromobility_active_power = specs[ - # "electromobility_active_power" - # ] - # - # # ToDo Malte add intermediate storage of edisgo grid in case of errors later on - # edisgo_grid.save( - # directory=os.path.join(results_dir, "grid_data"), - # save_topology=True, - # save_timeseries=True, - # save_results=False, - # save_electromobility=True, - # save_dsm=True, - # save_heatpump=True, - # save_overlying_grid=True, - # reduce_memory=True, - # archive=True, - # archive_type="zip", - # ) - # - # # ########################## checks ########################## - # # ToDo Birgit expand - # edisgo_grid.check_integrity() - # - # # ########################## optimisation ########################## - # # ToDo Maike Call optimisation - # - # # ########################## reinforcement ########################## - # # edisgo_grid.reinforce() - # - # # ########################## save results ########################## - # self._status_update(mv_grid_id, "end") - # # edisgo_grid.save( - # # directory=os.path.join(results_dir, "reinforce_data"), - # # save_topology=True, - # # save_timeseries=True, - # # save_results=True, - # # save_electromobility=False, - # # # save_dsm=True, - # # save_heatpump=False, - # # save_overlying_grid=False, - # # reduce_memory=True, - # # archive=True, - # # archive_type="zip", - # # parameters={ - # # "powerflow_results": ["pfa_p", "pfa_q", "v_res"], - # # "grid_expansion_results": ["grid_expansion_costs", "equipment - # # _changes"], - # # }, - # # ) - # - # return {edisgo_grid.topology.id: results_dir} - def _save_edisgo_results(self): results_dir = os.path.join(self._json_file["eGo"]["results_dir"], self._results) if not os.path.exists(results_dir): From c4d546583a61aa71873d0fad083698d8659f5b69 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 13 Apr 2023 21:31:12 +0200 Subject: [PATCH 063/126] Adapt scenario settings --- ego/examples/edisgo_workflow/scenario_setting.json | 2 +- ego/scenario_setting.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ego/examples/edisgo_workflow/scenario_setting.json b/ego/examples/edisgo_workflow/scenario_setting.json index 2e2ebbb1..90b883d1 100644 --- a/ego/examples/edisgo_workflow/scenario_setting.json +++ b/ego/examples/edisgo_workflow/scenario_setting.json @@ -57,7 +57,7 @@ "results": "results_edisgo", "solver": "glpk", "timesteps_pfa": "snapshot_analysis", - "tasks": ["2_specs_overlying_grid"] + "tasks": ["1_setup_grid"] }, "database": { "tables": { diff --git a/ego/scenario_setting.json b/ego/scenario_setting.json index 496063b0..1595a6f0 100644 --- a/ego/scenario_setting.json +++ b/ego/scenario_setting.json @@ -57,7 +57,7 @@ "results": "results/another_result", "solver": "gurobi", "timesteps_pfa": "snapshot_analysis", - "tasks": ["2_specs_overlying_grid"] + "tasks": ["1_setup_grid"] }, "database": { "tables": { From dc487f08670914ee3d134030d356b079fa34110d Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 14 Apr 2023 10:47:20 +0200 Subject: [PATCH 064/126] Change limits --- ego/mv_clustering/egon_data_io.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ego/mv_clustering/egon_data_io.py b/ego/mv_clustering/egon_data_io.py index e4ae60b7..f4cfcff6 100644 --- a/ego/mv_clustering/egon_data_io.py +++ b/ego/mv_clustering/egon_data_io.py @@ -317,7 +317,7 @@ def get_pth_capacity(scenario, grid_ids, orm=None, session=None): orm["heat_pump_capacity_individual"].mv_grid_id.in_(grid_ids), orm["heat_pump_capacity_individual"].carrier == "heat_pump", orm["heat_pump_capacity_individual"].scenario == scenario, - orm["heat_pump_capacity_individual"].capacity <= 17.5, + orm["heat_pump_capacity_individual"].capacity <= 20.0, ) .group_by( orm["heat_pump_capacity_individual"].mv_grid_id, @@ -338,7 +338,7 @@ def get_pth_capacity(scenario, grid_ids, orm=None, session=None): orm["pth_capacity_district_heating"].carrier.in_( ["central_heat_pump", "central_resistive_heater"] ), - orm["pth_capacity_district_heating"].p_nom <= 17.5, + orm["pth_capacity_district_heating"].p_nom <= 20.0, ) .group_by( orm["pth_capacity_district_heating"].bus0, From 519a226a389569c754b912a48b8365ea0c44ceba Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 14 Apr 2023 10:47:40 +0200 Subject: [PATCH 065/126] Change integrity checks --- ego/tools/edisgo_integration.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 00d790ef..873aaf1a 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -970,7 +970,11 @@ def _run_edisgo_task_setup_grid(self, mv_grid_id, scenario, engine, logger, conf ) logger.info("Run integrity check.") - edisgo_grid.check_integrity() + logger.info("Run integrity checks.") + edisgo_grid.topology.check_integrity() + edisgo_grid.electromobility.check_integrity() + edisgo_grid.heat_pump.check_integrity() + edisgo_grid.dsm.check_integrity() return edisgo_grid From d142c6519b862980d45f554cf652ce51557e8a74 Mon Sep 17 00:00:00 2001 From: birgits Date: Sun, 23 Apr 2023 12:10:26 +0200 Subject: [PATCH 066/126] Move setting up engine to task setup_grid --- ego/tools/edisgo_integration.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 873aaf1a..ae9a3e70 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -769,7 +769,6 @@ def run_edisgo(self, mv_grid_id): # ##################### general settings #################### config = self._json_file - engine = get_engine(config=config) scenario = config["eTraGo"]["scn_name"] # results directory @@ -802,7 +801,7 @@ def run_edisgo(self, mv_grid_id): # ################### task: setup grid ################## if "1_setup_grid" in config["eDisGo"]["tasks"]: edisgo_grid = self._run_edisgo_task_setup_grid( - mv_grid_id, scenario, engine, logger, config + mv_grid_id, scenario, logger, config ) if "2_specs_overlying_grid" not in config["eDisGo"]["tasks"]: edisgo_grid.save( @@ -884,7 +883,7 @@ def run_edisgo(self, mv_grid_id): return {edisgo_grid.topology.id: results_dir} - def _run_edisgo_task_setup_grid(self, mv_grid_id, scenario, engine, logger, config): + def _run_edisgo_task_setup_grid(self, mv_grid_id, scenario, logger, config): """ Sets up EDisGo object for future scenario (without specifications from overlying grid). @@ -915,6 +914,8 @@ def _run_edisgo_task_setup_grid(self, mv_grid_id, scenario, engine, logger, conf """ logger.info(f"MV grid {mv_grid_id}: Start task 'setup_grid'.") + engine = get_engine(config=config) + logger.info(f"MV grid {mv_grid_id}: Initialize MV grid.") grid_path = os.path.join( config["eGo"]["data_dir"], From fde3e13cccff094b04fd335bbe6b009ccd8ddbaf Mon Sep 17 00:00:00 2001 From: birgits Date: Sun, 23 Apr 2023 12:10:43 +0200 Subject: [PATCH 067/126] Overwrite configs --- ego/tools/edisgo_integration.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index ae9a3e70..8829b8ea 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -50,6 +50,7 @@ if "READTHEDOCS" not in os.environ: from edisgo.edisgo import import_edisgo_from_files + from edisgo.tools.config import Config from edisgo.tools.logger import setup_logger from edisgo.tools.plots import mv_grid_topology from egoio.db_tables import grid, model_draft @@ -929,6 +930,8 @@ def _run_edisgo_task_setup_grid(self, mv_grid_id, scenario, logger, config): edisgo_grid = import_edisgo_from_files(edisgo_path=grid_path) edisgo_grid.legacy_grids = False + # overwrite configs + edisgo_grid.config = Config() edisgo_grid.set_timeindex(pd.date_range("1/1/2011", periods=8760, freq="H")) logger.info("Set up load time series of conventional loads.") From 39ebd05ba5dc5bb279b101bd3d90b96c3325f65b Mon Sep 17 00:00:00 2001 From: birgits Date: Sun, 23 Apr 2023 12:10:55 +0200 Subject: [PATCH 068/126] Remove logging message --- ego/tools/edisgo_integration.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 8829b8ea..beae8f11 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -973,7 +973,6 @@ def _run_edisgo_task_setup_grid(self, mv_grid_id, scenario, logger, config): edisgo_obj=edisgo_grid, use_case=["home", "work"] ) - logger.info("Run integrity check.") logger.info("Run integrity checks.") edisgo_grid.topology.check_integrity() edisgo_grid.electromobility.check_integrity() From 5df47763d25ab3288225ff27603a2b00cf2a4839 Mon Sep 17 00:00:00 2001 From: birgits Date: Sun, 23 Apr 2023 13:27:42 +0200 Subject: [PATCH 069/126] Bug fix --- ego/tools/edisgo_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index beae8f11..28727d19 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -931,7 +931,7 @@ def _run_edisgo_task_setup_grid(self, mv_grid_id, scenario, logger, config): edisgo_grid = import_edisgo_from_files(edisgo_path=grid_path) edisgo_grid.legacy_grids = False # overwrite configs - edisgo_grid.config = Config() + edisgo_grid._config = Config() edisgo_grid.set_timeindex(pd.date_range("1/1/2011", periods=8760, freq="H")) logger.info("Set up load time series of conventional loads.") From 2ab096c4716c7539a586f1e34767d306d8535996 Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 24 Apr 2023 16:26:58 +0200 Subject: [PATCH 070/126] Save to different zip to keep original grid --- ego/tools/edisgo_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 28727d19..e7df37ab 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -841,7 +841,7 @@ def run_edisgo(self, mv_grid_id): ) if "3_optimisation" not in config["eDisGo"]["tasks"]: edisgo_grid.save( - directory=os.path.join(results_dir, "grid_data"), + directory=os.path.join(results_dir, "grid_data_overlying_grid"), save_topology=True, save_timeseries=True, save_results=False, From cc2113d65772ae974e8c6258658e9e8bf5f85bba Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 24 Apr 2023 16:27:34 +0200 Subject: [PATCH 071/126] Change CHP carrier names --- ego/tools/edisgo_integration.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index e7df37ab..8a23a533 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1051,6 +1051,20 @@ def _run_edisgo_task_specs_overlying_grid(self, edisgo_grid, logger): logger.info("Set generator time series.") # active power + # rename CHP carrier to match with carrier names in overlying grid + generators_df = edisgo_grid.topology.generators_df + if "p_nom_th" in generators_df.columns: + gens_gas_chp = generators_df[ + (generators_df["type"].isin(["gas", "gas extended"])) + & (generators_df["p_nom_th"] > 0) + ] + generators_df.loc[gens_gas_chp.index, "type"] = "gas_CHP" + gens_biomass_chp = generators_df[ + (generators_df["type"].isin(["biomass"])) + & (generators_df["p_nom_th"] > 0) + ] + generators_df.loc[gens_biomass_chp.index, "type"] = "biomass_CHP" + edisgo_grid.set_time_series_active_power_predefined( dispatchable_generators_ts=specs["dispatchable_generators_active_power"], fluctuating_generators_ts=specs["renewables_potential"], From 82600d70973c270cbb544c5c77c381247dec856c Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 24 Apr 2023 16:28:47 +0200 Subject: [PATCH 072/126] Add scaling and check of curtailment --- ego/tools/edisgo_integration.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 8a23a533..5134ad1a 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1145,9 +1145,6 @@ def _run_edisgo_task_specs_overlying_grid(self, edisgo_grid, logger): ) logger.info("Set requirements from overlying grid.") - edisgo_grid.overlying_grid.renewables_curtailment = specs[ - "renewables_curtailment" - ] # ToDo requirements need to be scaled to capacity of home storage units edisgo_grid.overlying_grid.storage_units_active_power = specs[ "storage_units_active_power" @@ -1168,6 +1165,29 @@ def _run_edisgo_task_specs_overlying_grid(self, edisgo_grid, logger): edisgo_grid.overlying_grid.solarthermal_energy_feedin_district_heating = specs[ "solarthermal_energy_feedin_district_heating" ] + + # curtailment + # scale curtailment by ratio of nominal power in eDisGo and eTraGo + for carrier in specs["renewables_curtailment"].columns: + p_nom_total = specs["renewables_p_nom"][carrier] + p_nom_mv_lv = edisgo_grid.topology.generators_df[ + edisgo_grid.topology.generators_df["type"] == carrier + ].p_nom.sum() + specs["renewables_curtailment"][carrier] *= p_nom_mv_lv / p_nom_total + # check that curtailment does not exceed feed-in + vres_gens = edisgo_grid.topology.generators_df[ + edisgo_grid.topology.generators_df["type"].isin( + specs["renewables_curtailment"].columns + ) + ].index + pot_vres_gens = edisgo_grid.timeseries.generators_active_power.loc[ + :, vres_gens + ].sum(axis=1) + total_curtailment = specs["renewables_curtailment"].sum(axis=1) + if (total_curtailment > pot_vres_gens).any(): + logger.warning("Curtailment exceeds feed-in!") + edisgo_grid.overlying_grid.renewables_curtailment = total_curtailment + # ToDo CHP + resistive heaters logger.info("Run integrity check.") From 4b150fdfe0c29832c9aaabae3b1b9713bb093422 Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 24 Apr 2023 17:08:58 +0200 Subject: [PATCH 073/126] Also make oil and others CHP gas_CHP --- ego/tools/edisgo_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 5134ad1a..a8b4a9ab 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1055,7 +1055,7 @@ def _run_edisgo_task_specs_overlying_grid(self, edisgo_grid, logger): generators_df = edisgo_grid.topology.generators_df if "p_nom_th" in generators_df.columns: gens_gas_chp = generators_df[ - (generators_df["type"].isin(["gas", "gas extended"])) + (generators_df["type"].isin(["gas", "gas extended", "oil", "others"])) & (generators_df["p_nom_th"] > 0) ] generators_df.loc[gens_gas_chp.index, "type"] = "gas_CHP" From 9a6a5264cfa63399f9434ff07ceaf2f0b4299663 Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 24 Apr 2023 18:09:16 +0200 Subject: [PATCH 074/126] Adapt docstring --- ego/tools/interface.py | 99 ++++++++++++++++-------------------------- 1 file changed, 37 insertions(+), 62 deletions(-) diff --git a/ego/tools/interface.py b/ego/tools/interface.py index ec32f49a..07fe4ad7 100644 --- a/ego/tools/interface.py +++ b/ego/tools/interface.py @@ -111,10 +111,6 @@ def get_etrago_results_per_bus(bus_id, etrago_obj, pf_post_lopf, max_cos_phi_ren Parameters ---------- - engine: - Engine of the database. - orm: - Object relational model dict. bus_id : int ID of the corresponding HV bus etrago_obj: :class:`etrago.tools.io.NetworkScenario` @@ -127,7 +123,7 @@ def get_etrago_results_per_bus(bus_id, etrago_obj, pf_post_lopf, max_cos_phi_ren Returns ------- - :obj:`dict` of :pandas:`pandas.DataFrame` + dict(str: :pandas:`pandas.DataFrame`) Dataframes used as eDisGo inputs. * 'timeindex' @@ -135,14 +131,14 @@ def get_etrago_results_per_bus(bus_id, etrago_obj, pf_post_lopf, max_cos_phi_ren Type: pd.Datetimeindex * 'dispatchable_generators_active_power' - Normalised dispatch of active power of dispatchable generators per + Normalised active power dispatch of dispatchable generators per technology in p.u. at the given bus. Type: pd.DataFrame Columns: Carrier Unit: pu * 'dispatchable_generators_reactive_power' - Normalised dispatch of reactive power of dispatchable generators per + Normalised reactive power dispatch of dispatchable generators per technology in p.u. at the given bus. Type: pd.DataFrame Columns: Carrier @@ -150,17 +146,20 @@ def get_etrago_results_per_bus(bus_id, etrago_obj, pf_post_lopf, max_cos_phi_ren * 'renewables_potential' Normalised weather dependent feed-in potential of fluctuating generators - per technology in p.u. at the given bus. + per technology (solar / wind) in p.u. at the given bus. Type: pd.DataFrame Columns: Carrier Unit: pu * 'renewables_curtailment' - Normalised curtailment of fluctuating generators per - technology in p.u. at the given bus. + Curtailment of fluctuating generators per + technology (solar / wind) in MW at the given bus. This curtailment can also + include curtailment of plants at the HV side of the HV/MV station and + therefore needs to be scaled using the quotient of installed power at the + MV side and installed power at the HV side. Type: pd.DataFrame Columns: Carrier - Unit: pu + Unit: MW * 'renewables_dispatch_reactive_power' Normalised reactive power time series of fluctuating generators per @@ -169,102 +168,78 @@ def get_etrago_results_per_bus(bus_id, etrago_obj, pf_post_lopf, max_cos_phi_ren Columns: Carrier Unit: pu + * 'renewables_p_nom' + Installed capacity of fluctuating generators per + technology (solar / wind) at the given bus. + Type: pd.Series + Unit: MW + * 'storage_units_p_nom' Storage unit nominal power. Type: float Unit: MW * 'storage_units_max_hours' - Storage units maximal discharge with p_nom starting by a soc of 1. + Storage units maximal discharge hours when discharged with p_nom starting + at a SoC of 1. Type: float Unit: h * 'storage_units_active_power' Active power time series of battery storage units at the given bus. - Type: pd.DataFrame - Columns: Carrier + Type: pd.Series Unit: MW * 'storage_units_reactive_power' Reactive power time series of battery storage units at the given bus. - Type: pd.DataFrame - Columns: Carrier + Type: pd.Series Unit: MVar * 'storage_units_soc' - Reactive power time series of battery storage units at the given bus. - Type: pd.DataFrame - Columns: Carrier + State of charge in p.u. of battery storage units at the given bus. + Type: pd.Series Unit: pu * 'dsm_active_power' Active power time series of DSM units at the given bus. - Type: pd.DataFrame - Columns: Carrier + Type: pd.Series Unit: MW - * 'dsm_reactive_power' - Reactive power time series of DSM units at the given bus. - Type: pd.DataFrame - Columns: Carrier - Unit: MVar - - * 'heat_pump_central_active_power' - Active power time series of central heat units at the given bus. - Type: pd.DataFrame - Columns: Carrier + * 'heat_pump_active_power' + Active power time series of PtH units at the given bus. + Type: pd.Series Unit: MW - * 'heat_pump_central_reactive_power' - Reactive power time series of central heat units at the given bus. - Type: pd.DataFrame - Columns: Carrier + * 'heat_pump_reactive_power' + Reactive power time series of PtH units at the given bus. + Type: pd.Series Unit: MVar * 'thermal_storage_central_capacity' Capacity of the storage at the bus where the central heat units feed in. - Type: float + Type: pd.DataFrame + Columns: Area ID Unit: MWh - * 'geothermal_energy_feedin_district_heating' + * 'feedin_district_heating' Geothermal feedin time series at the heat bus. Type: pd.DataFrame - Columns: Carrier - Unit: MW - - * 'solarthermal_energy_feedin_district_heating' - Solarthermal feedin time series at the heat bus. - Type: pd.DataFrame - Columns: Carrier - Unit: MW - - * 'heat_pump_rural_active_power' - Active power time series of rural heat pump units at the given bus. - Type: pd.DataFrame - Columns: Carrier + Columns: Area ID Unit: MW - * 'heat_pump_rural_reactive_power' - Reactive power time series of rural heat pump units at the given bus. - Type: pd.DataFrame - Columns: Carrier - Unit: MVar - * 'thermal_storage_rural_capacity' Capacity of the storage at the bus where the rural heat units feed in. Type: float Unit: MWh * 'electromobility_active_power' - Active power time series of electromobility units at the given bus. - Type: pd.DataFrame - Columns: Carrier + Active power charging time series at the given bus. + Type: pd.Series Unit: MW * 'electromobility_reactive_power' - Reactive power time series of electromobility units at the given bus. - Type: pd.DataFrame - Columns: Carrier + Reactive power charging time series at the given bus. + Type: pd.Series Unit: MVar """ From 6fbbafae2b7bb372c179a4962492fb3937e60430 Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 24 Apr 2023 18:11:33 +0200 Subject: [PATCH 075/126] Adapt interface functions --- ego/tools/interface.py | 406 +++++++++++++---------------------------- 1 file changed, 131 insertions(+), 275 deletions(-) diff --git a/ego/tools/interface.py b/ego/tools/interface.py index 07fe4ad7..7e184d1f 100644 --- a/ego/tools/interface.py +++ b/ego/tools/interface.py @@ -243,143 +243,101 @@ def get_etrago_results_per_bus(bus_id, etrago_obj, pf_post_lopf, max_cos_phi_ren Unit: MVar """ - # Defining inner functions def dispatchable_gens(): - # Dispatchable generators dispatchable_gens_df_p = pd.DataFrame(index=timeseries_index) dispatchable_gens_df_q = pd.DataFrame(index=timeseries_index) dispatchable_gens_carriers = [ - # "CH4", - # "CH4_NG", - # "CH4_biogas", - "biomass", - "central_biomass_CHP", - # "central_biomass_CHP_heat", - # "coal", - # "geo_thermal", - "industrial_biomass_CHP", - # "lignite", - # "nuclear", - # "oil", - "others", - "reservoir", - "run_of_river", - # "solar", - # "solar_rooftop", - # "solar_thermal_collector", - # "wind_offshore", - # "wind_onshore", + _ + for _ in generators_df["carrier"].unique() + if "solar" not in _ and "wind" not in _ ] # Filter generators_df for selected carriers. dispatchable_gens_df = generators_df[ generators_df["carrier"].isin(dispatchable_gens_carriers) ] - for carrier in dispatchable_gens_carriers: - if not dispatchable_gens_df[ + # Rename CHP carrier to match with carrier names in eDisGo + gens_gas_chp = dispatchable_gens_df[ + dispatchable_gens_df.carrier.isin(["central_gas_CHP", "industrial_gas_CHP"]) + ] + dispatchable_gens_df.loc[gens_gas_chp.index, "carrier"] = "gas_CHP" + gens_biomass_chp = dispatchable_gens_df[ + dispatchable_gens_df.carrier.isin( + ["central_biomass_CHP", "industrial_biomass_CHP"] + ) + ] + dispatchable_gens_df.loc[gens_biomass_chp.index, "carrier"] = "biomass_CHP" + for carrier in dispatchable_gens_df.carrier.unique(): + p_nom = dispatchable_gens_df.loc[ + dispatchable_gens_df["carrier"] == carrier, "p_nom" + ].sum() + columns_to_aggregate = dispatchable_gens_df[ dispatchable_gens_df["carrier"] == carrier - ].empty: - p_nom = dispatchable_gens_df.loc[ - dispatchable_gens_df["carrier"] == carrier, "p_nom" - ].sum() - columns_to_aggregate = dispatchable_gens_df[ - dispatchable_gens_df["carrier"] == carrier - ].index - - dispatchable_gens_df_p[carrier] = ( - etrago_obj.generators_t["p"][columns_to_aggregate].sum( + ].index + dispatchable_gens_df_p[carrier] = ( + etrago_obj.generators_t["p"][columns_to_aggregate].sum(axis="columns") + / p_nom + ) + if pf_post_lopf: + dispatchable_gens_df_q[carrier] = ( + etrago_obj.generators_t["q"][columns_to_aggregate].sum( axis="columns" ) / p_nom ) - if pf_post_lopf: - dispatchable_gens_df_q[carrier] = ( - etrago_obj.generators_t["q"][columns_to_aggregate].sum( + else: + dispatchable_gens_df_q[carrier] = pd.Series( + data=0, index=timeseries_index, dtype=float + ) + + # Add CHP to conventional generators (only needed in case pf_post_lopf is False, + # otherwise it is already included above) + if pf_post_lopf is False: + chp_df = links_df[ + links_df["carrier"].isin( + [ + "central_gas_CHP", + "industrial_gas_CHP", + "central_biomass_CHP", + "industrial_biomass_CHP", + ] + ) + ] + if not chp_df.empty: + # Rename CHP carrier to match with carrier names in eDisGo + gens_gas_chp = chp_df[ + chp_df.carrier.isin(["central_gas_CHP", "industrial_gas_CHP"]) + ] + chp_df.loc[gens_gas_chp.index, "carrier"] = "gas_CHP" + gens_biomass_chp = chp_df[ + chp_df.carrier.isin( + ["central_biomass_CHP", "industrial_biomass_CHP"] + ) + ] + chp_df.loc[gens_biomass_chp.index, "carrier"] = "biomass_CHP" + + for carrier in chp_df.carrier.unique(): + p_nom = chp_df.loc[chp_df["carrier"] == carrier, "p_nom"].sum() + columns_to_aggregate = chp_df[chp_df["carrier"] == carrier].index + dispatchable_gens_df_p[carrier] = ( + etrago_obj.links_t["p1"][columns_to_aggregate].sum( axis="columns" ) / p_nom ) - else: dispatchable_gens_df_q[carrier] = pd.Series( data=0, index=timeseries_index, dtype=float ) - else: - dispatchable_gens_df_p[carrier] = pd.Series( - data=0, index=timeseries_index, dtype=float - ) - dispatchable_gens_df_q[carrier] = pd.Series( - data=0, index=timeseries_index, dtype=float - ) - - # Add CHP to conventional generators - if pf_post_lopf: - chp_df = generators_df[generators_df["carrier"] == "central_gas_CHP"] - else: - chp_df = links_df[links_df["carrier"] == "central_gas_CHP"] - if not chp_df.empty: - p_nom = chp_df["p_nom"].sum() - if pf_post_lopf: - dispatchable_gens_df_p["central_gas_CHP"] = ( - etrago_obj.generators_t["p"][chp_df.index].sum(axis="columns") - / p_nom - ) - dispatchable_gens_df_q["central_gas_CHP"] = ( - etrago_obj.generators_t["q"][chp_df.index].sum(axis="columns") - / p_nom - ) - else: - dispatchable_gens_df_p["central_gas_CHP"] = ( - etrago_obj.links_t["p1"][chp_df.index].sum(axis="columns") / p_nom - ) - dispatchable_gens_df_q["central_gas_CHP"] = pd.Series( - data=0, index=timeseries_index, dtype=float - ) - else: - dispatchable_gens_df_p["central_gas_CHP"] = pd.Series( - data=0, index=timeseries_index, dtype=float - ) - dispatchable_gens_df_q["central_gas_CHP"] = pd.Series( - data=0, index=timeseries_index, dtype=float - ) + if (dispatchable_gens_df_p < -1e-3).any().any(): + logger.warning("Dispatchable generator feed-in values smaller -1 kW.") results["dispatchable_generators_active_power"] = dispatchable_gens_df_p results["dispatchable_generators_reactive_power"] = dispatchable_gens_df_q def renewable_generators(): - """ - # Renewables - weather_dependent = weather_dep - generators = gens - weather_id = w_id - aggregated = agg - potential = pot - dispatch = dis - curtailment = curt - """ - - weather_dep_gens = [ - # "CH4", - # "CH4_NG", - # "CH4_biogas", - # "biomass", - # "central_biomass_CHP", - # "central_biomass_CHP_heat", - # "coal", - # "geo_thermal", - # "industrial_biomass_CHP", - # "lignite", - # "nuclear", - # "oil", - # "others", - # "reservoir", - # "run_of_river", - "solar", - "solar_rooftop", - # "solar_thermal_collector", - # "wind_offshore", - "wind_onshore", - ] + + weather_dep_gens = ["solar", "solar_rooftop", "wind_onshore"] renaming_carrier_dict = { "solar": ["solar", "solar_rooftop"], "wind": ["wind_onshore"], @@ -388,14 +346,6 @@ def renewable_generators(): generators_df.carrier.isin(weather_dep_gens) ] - # ToDo @Malte please check - # # Add weather ids - # for gens_index in weather_dep_gens_df.index: - # weather_id = db_io.get_weather_id_for_generator( - # bus_id, engine=engine, orm=orm - # ) - # weather_dep_gens_df.loc[gens_index, "w_id"] = str(weather_id) - # Rename carrier to aggregate to carriers for new_carrier_name, item in renaming_carrier_dict.items(): for carrier in item: @@ -409,163 +359,112 @@ def renewable_generators(): ) # Initialize dfs + # potential weather_dep_gens_df_pot_p = pd.DataFrame( 0.0, index=timeseries_index, - columns=agg_weather_dep_gens_df.index, - ) - weather_dep_gens_df_dis_p = pd.DataFrame( - 0.0, - index=timeseries_index, - columns=agg_weather_dep_gens_df.index, + columns=agg_weather_dep_gens_df.carrier.unique(), ) + # reactive power weather_dep_gens_df_dis_q = pd.DataFrame( 0.0, index=timeseries_index, - columns=agg_weather_dep_gens_df.index, + columns=agg_weather_dep_gens_df.carrier.unique(), ) + # curtailment weather_dep_gens_df_curt_p = pd.DataFrame( 0.0, index=timeseries_index, - columns=agg_weather_dep_gens_df.index, + columns=agg_weather_dep_gens_df.carrier.unique(), ) for index, carrier, p_nom in weather_dep_gens_df[ ["carrier", "p_nom"] ].itertuples(): + # get index in aggregated dataframe to determine total installed capacity + # of the respective carrier agg_idx = agg_weather_dep_gens_df[ agg_weather_dep_gens_df["carrier"] == carrier ].index.values[0] p_nom_agg = agg_weather_dep_gens_df.loc[agg_idx, "p_nom"] p_series = etrago_obj.generators_t["p"][index] - p_normed_series = p_series / p_nom_agg - p_max_pu_series = etrago_obj.generators_t["p_max_pu"][index] p_max_pu_normed_series = p_max_pu_series * p_nom / p_nom_agg if pf_post_lopf: q_series = etrago_obj.generators_t["q"][index] + # If set limit maximum reactive power + if max_cos_phi_ren: + logger.info( + "Applying Q limit (max cos(phi)={})".format(max_cos_phi_ren) + ) + phi = math.acos(max_cos_phi_ren) + for timestep in timeseries_index: + p = p_series[timestep] + q = q_series[timestep] + q_max = p * math.tan(phi) + q_min = -p * math.tan(phi) + if q > q_max: + q = q_max + elif q < q_min: + q = q_min + q_series[timestep] = q + q_normed_series = q_series / p_nom_agg else: - q_series = pd.Series(0.0, index=timeseries_index) + q_normed_series = pd.Series(0.0, index=timeseries_index) - # If set limit maximum reactive power - if max_cos_phi_ren: - logger.info( - "Applying Q limit (max cos(phi)={})".format(max_cos_phi_ren) - ) - phi = math.acos(max_cos_phi_ren) - for timestep in timeseries_index: - p = p_series[timestep] - q = q_series[timestep] - q_max = p * math.tan(phi) - q_min = -p * math.tan(phi) - if q > q_max: - q = q_max - elif q < q_min: - q = q_min - q_series[timestep] = q - - q_normed_series = q_series / p_nom_agg - - weather_dep_gens_df_dis_p[agg_idx] = ( - weather_dep_gens_df_dis_p[agg_idx] + p_normed_series - ) - weather_dep_gens_df_pot_p[agg_idx] = ( - weather_dep_gens_df_pot_p[agg_idx] + p_max_pu_normed_series - ) - weather_dep_gens_df_dis_q[agg_idx] = ( - weather_dep_gens_df_dis_q[agg_idx] + q_normed_series - ) - weather_dep_gens_df_curt_p[agg_idx] = weather_dep_gens_df_curt_p[ - agg_idx - ] + (p_max_pu_series * p_nom - p_series) - - # Renaming columns - new_columns = [ - (agg_weather_dep_gens_df.at[column, "carrier"]) - for column in weather_dep_gens_df_pot_p.columns - ] - # new_columns = pd.MultiIndex.from_tuples(new_columns) - weather_dep_gens_df_pot_p.columns = new_columns - weather_dep_gens_df_dis_p.columns = new_columns - weather_dep_gens_df_curt_p.columns = new_columns - weather_dep_gens_df_dis_q.columns = new_columns - - # Add zero for empty carriers - for carrier in renaming_carrier_dict.keys(): - if carrier not in weather_dep_gens_df_pot_p.columns: - empty_df = pd.DataFrame( - 0.0, - index=timeseries_index, - columns=[carrier], - ) - weather_dep_gens_df_pot_p = pd.concat( - [weather_dep_gens_df_pot_p, empty_df.copy()], axis="columns" - ) - weather_dep_gens_df_dis_p = pd.concat( - [weather_dep_gens_df_dis_p, empty_df.copy()], axis="columns" - ) - weather_dep_gens_df_curt_p = pd.concat( - [weather_dep_gens_df_curt_p, empty_df.copy()], axis="columns" - ) - weather_dep_gens_df_dis_q = pd.concat( - [weather_dep_gens_df_dis_q, empty_df.copy()], axis="columns" - ) + weather_dep_gens_df_pot_p[carrier] += p_max_pu_normed_series + weather_dep_gens_df_dis_q[carrier] += q_normed_series + weather_dep_gens_df_curt_p[carrier] += p_max_pu_series * p_nom - p_series + if (weather_dep_gens_df_curt_p.min() < -1e-3).any(): + logger.warning("Curtailment values smaller -1 kW.") + # ToDo remove once fixed + weather_dep_gens_df_curt_p[weather_dep_gens_df_curt_p < 0] = 0 results["renewables_potential"] = weather_dep_gens_df_pot_p results["renewables_curtailment"] = weather_dep_gens_df_curt_p results["renewables_dispatch_reactive_power"] = weather_dep_gens_df_dis_q + results["renewables_p_nom"] = agg_weather_dep_gens_df.set_index("carrier").p_nom def storages(): # Storage # Filter batteries - min_extended = 0 - logger.info(f"Minimum storage of {min_extended} MW") - storages_df = etrago_obj.storage_units.loc[ (etrago_obj.storage_units["carrier"] == "battery") & (etrago_obj.storage_units["bus"] == str(bus_id)) - & (etrago_obj.storage_units["p_nom_extendable"]) - & (etrago_obj.storage_units["p_nom_opt"] > min_extended) + # & (etrago_obj.storage_units["p_nom_extendable"]) + # & (etrago_obj.storage_units["p_nom_opt"] > 0) ] if not storages_df.empty: - # p_nom - storages_df_p_nom = ( - storages_df["p_nom_opt"] - storages_df["p_nom_min"] - ).values[0] + # p_nom - p_nom_opt can always be used, if extendable is True or False + storages_df_p_nom = storages_df["p_nom_opt"].sum() # Capacity storages_df_max_hours = (storages_df["max_hours"]).values[0] - storages_df_p = etrago_obj.storage_units_t["p"][storages_df.index] - storages_df_p.columns = storages_df["carrier"] + storages_cap = storages_df_p_nom * storages_df_max_hours + # p and q + storages_df_p = etrago_obj.storage_units_t["p"][storages_df.index].sum( + axis=1 + ) if pf_post_lopf: - # ToDo: No q timeseries? - # storages_df_q = etrago_obj.storage_units_t["q"][storages_df.index] - # storages_df_q.columns = storages_df["carrier"] - storages_df_q = pd.DataFrame( - 0.0, index=timeseries_index, columns=[storages_df["carrier"]] + storages_df_q = etrago_obj.storage_units_t["q"][storages_df.index].sum( + axis=1 ) else: - storages_df_q = pd.DataFrame( - 0.0, index=timeseries_index, columns=[storages_df["carrier"]] + storages_df_q = pd.Series(0.0, index=timeseries_index) + storages_df_soc = ( + etrago_obj.storage_units_t["state_of_charge"][storages_df.index].sum( + axis=1 ) - storages_df_soc = etrago_obj.storage_units_t["state_of_charge"][ - storages_df.index - ] - storages_df_soc.columns = storages_df["carrier"] + / storages_cap + ) else: storages_df_p_nom = 0 storages_df_max_hours = 0 - storages_df_p = pd.DataFrame( - 0.0, index=timeseries_index, columns=[storages_df["carrier"]] - ) - storages_df_q = pd.DataFrame( - 0.0, index=timeseries_index, columns=[storages_df["carrier"]] - ) - storages_df_soc = pd.DataFrame( - 0.0, index=timeseries_index, columns=[storages_df["carrier"]] - ) + storages_df_p = pd.Series(0.0, index=timeseries_index) + storages_df_q = pd.Series(0.0, index=timeseries_index) + storages_df_soc = pd.Series(0.0, index=timeseries_index) results["storage_units_p_nom"] = storages_df_p_nom results["storage_units_max_hours"] = storages_df_max_hours results["storage_units_active_power"] = storages_df_p @@ -578,20 +477,10 @@ def dsm(): (links_df["carrier"] == "dsm") & (links_df["bus0"] == str(bus_id)) ] if not dsm_df.empty: - dsm_df_p = etrago_obj.links_t["p0"][dsm_df.index] - dsm_df_p.columns = dsm_df["carrier"] - dsm_df_q = pd.DataFrame( - 0.0, index=timeseries_index, columns=[dsm_df["carrier"]] - ) + dsm_df_p = etrago_obj.links_t["p0"][dsm_df.index].sum(axis=1) else: - dsm_df_p = pd.DataFrame( - 0.0, index=timeseries_index, columns=[dsm_df["carrier"]] - ) - dsm_df_q = pd.DataFrame( - 0.0, index=timeseries_index, columns=[dsm_df["carrier"]] - ) + dsm_df_p = pd.Series(0.0, index=timeseries_index) results["dsm_active_power"] = dsm_df_p - results["dsm_reactive_power"] = dsm_df_q def central_heat(): # Heat @@ -716,21 +605,16 @@ def rural_heat(): def bev_charger(): # BEV charger bev_charger_df = links_df.loc[ - (links_df["carrier"] == "BEV charger") & (links_df["bus0"] == str(bus_id)) + (links_df["carrier"] == "BEV_charger") & (links_df["bus0"] == str(bus_id)) ] if not bev_charger_df.empty: - bev_charger_df_p = etrago_obj.links_t["p0"][bev_charger_df.index] - bev_charger_df_p.columns = bev_charger_df["carrier"] - bev_charger_df_q = pd.DataFrame( - 0.0, index=timeseries_index, columns=[bev_charger_df["carrier"]] + bev_charger_df_p = etrago_obj.links_t["p0"][bev_charger_df.index].sum( + axis=1 ) + bev_charger_df_q = pd.Series(0.0, index=timeseries_index) else: - bev_charger_df_p = pd.DataFrame( - 0.0, index=timeseries_index, columns=[bev_charger_df["carrier"]] - ) - bev_charger_df_q = pd.DataFrame( - 0.0, index=timeseries_index, columns=[bev_charger_df["carrier"]] - ) + bev_charger_df_p = pd.Series(0.0, index=timeseries_index) + bev_charger_df_q = pd.Series(0.0, index=timeseries_index) results["electromobility_active_power"] = bev_charger_df_p results["electromobility_reactive_power"] = bev_charger_df_q @@ -745,34 +629,6 @@ def bev_charger(): logger.info("Only active power interface") results = {} - # Prefill dict with None - result_keys = [ - "timeindex", - "dispatchable_generators_active_power", - "dispatchable_generators_reactive_power", - "renewables_potential", - "renewables_curtailment", - "renewables_dispatch_reactive_power", - "storage_units_p_nom", - "storage_units_max_hours", - "storage_units_active_power", - "storage_units_reactive_power", - "storage_units_soc", - "dsm_active_power", - "dsm_reactive_power", - "heat_pump_central_active_power", - "heat_pump_central_reactive_power", - "thermal_storage_central_capacity", - "geothermal_energy_feedin_district_heating", - "solarthermal_energy_feedin_district_heating", - "heat_pump_rural_active_power", - "heat_pump_rural_reactive_power", - "thermal_storage_rural_capacity", - "electromobility_active_power", - "electromobility_reactive_power", - ] - for result_key in result_keys: - results[result_key] = None timeseries_index = etrago_obj.snapshots results["timeindex"] = timeseries_index @@ -791,8 +647,8 @@ def bev_charger(): renewable_generators() storages() dsm() - central_heat() - rural_heat() + # central_heat() + # rural_heat() bev_charger() logger.info(f"Overall time: {time.perf_counter() - t_start}") From dba981f84a6ecbec85716f6450060819898cdfc2 Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 24 Apr 2023 18:47:32 +0200 Subject: [PATCH 076/126] Adapt rural_heat interface --- ego/tools/interface.py | 61 ++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/ego/tools/interface.py b/ego/tools/interface.py index 7e184d1f..4545d569 100644 --- a/ego/tools/interface.py +++ b/ego/tools/interface.py @@ -205,16 +205,41 @@ def get_etrago_results_per_bus(bus_id, etrago_obj, pf_post_lopf, max_cos_phi_ren Type: pd.Series Unit: MW - * 'heat_pump_active_power' + * 'heat_pump_rural_active_power' Active power time series of PtH units at the given bus. Type: pd.Series Unit: MW - * 'heat_pump_reactive_power' + * 'heat_pump_rural_reactive_power' Reactive power time series of PtH units at the given bus. Type: pd.Series Unit: MVar + * 'heat_pump_rural_p_nom' + Nominal power of all rural PtH units at the given bus. + Type: float + Unit: MWh + + * 'thermal_storage_rural_capacity' + Capacity of the storage at the bus where the rural heat units feed in. + Type: float + Unit: MWh + + * 'heat_pump_central_active_power' + Active power time series of PtH units at the given bus. + Type: pd.Series + Unit: MW + + * 'heat_pump_central_reactive_power' + Reactive power time series of PtH units at the given bus. + Type: pd.Series + Unit: MVar + + * 'heat_pump_central_p_nom' + Nominal power of all rural PtH units at the given bus. + Type: float + Unit: MWh + * 'thermal_storage_central_capacity' Capacity of the storage at the bus where the central heat units feed in. Type: pd.DataFrame @@ -227,11 +252,6 @@ def get_etrago_results_per_bus(bus_id, etrago_obj, pf_post_lopf, max_cos_phi_ren Columns: Area ID Unit: MW - * 'thermal_storage_rural_capacity' - Capacity of the storage at the bus where the rural heat units feed in. - Type: float - Unit: MWh - * 'electromobility_active_power' Active power charging time series at the given bus. Type: pd.Series @@ -563,8 +583,8 @@ def central_heat(): ] = solarthermal_feedin_df_p def rural_heat(): - # Rural heat - # Power2Heat + + # ToDo (low priority) add resistive heaters (they only exist in eGon100RE) rural_heat_carriers = ["rural_heat_pump"] rural_heat_df = links_df.loc[ links_df["carrier"].isin(rural_heat_carriers) @@ -572,12 +592,10 @@ def rural_heat(): ] if not rural_heat_df.empty: # Timeseries - rural_heat_df_p = etrago_obj.links_t["p0"][rural_heat_df.index] - rural_heat_df_p.columns = rural_heat_df["carrier"] - rural_heat_df_q = pd.DataFrame( - 0.0, index=timeseries_index, columns=[rural_heat_df["carrier"]] - ) - + rural_heat_df_p = etrago_obj.links_t["p0"][rural_heat_df.index].sum(axis=1) + rural_heat_df_q = pd.Series(0.0, index=timeseries_index) + # p_nom + rural_heat_p_nom = rural_heat_df.p_nom.sum() # Stores rural_heat_bus = rural_heat_df["bus1"].values[0] rural_heat_store_bus = etrago_obj.links.loc[ @@ -589,17 +607,14 @@ def rural_heat(): "e_nom_opt", ].values[0] else: - column_names = rural_heat_df["carrier"].to_list() - rural_heat_df_p = pd.DataFrame( - 0.0, index=timeseries_index, columns=column_names - ) - rural_heat_df_q = pd.DataFrame( - 0.0, index=timeseries_index, columns=column_names - ) + rural_heat_df_p = pd.Series(0.0, index=timeseries_index) + rural_heat_df_q = pd.Series(0.0, index=timeseries_index) rural_heat_store_capacity = 0 + rural_heat_p_nom = 0 results["heat_pump_rural_active_power"] = rural_heat_df_p results["heat_pump_rural_reactive_power"] = rural_heat_df_q + results["heat_pump_rural_p_nom"] = rural_heat_p_nom results["thermal_storage_rural_capacity"] = rural_heat_store_capacity def bev_charger(): @@ -648,7 +663,7 @@ def bev_charger(): storages() dsm() # central_heat() - # rural_heat() + rural_heat() bev_charger() logger.info(f"Overall time: {time.perf_counter() - t_start}") From 0cd237a8383330ab38a213147d0bb0e260e5376f Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 24 Apr 2023 18:48:51 +0200 Subject: [PATCH 077/126] Adapt overlying grid task --- ego/tools/edisgo_integration.py | 117 ++++++++++++++++++-------------- 1 file changed, 67 insertions(+), 50 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index a8b4a9ab..134af705 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1071,7 +1071,8 @@ def _run_edisgo_task_specs_overlying_grid(self, edisgo_grid, logger): ) # reactive power if self._pf_post_lopf: - # ToDo Use eTraGo time series to set reactive power (scale by nominal power) + # ToDo (low priority) Use eTraGo time series to set reactive power + # (scale by nominal power) edisgo_grid.set_time_series_manual( generators_q=specs["generators_reactive_power"].loc[:, []], ) @@ -1084,8 +1085,9 @@ def _run_edisgo_task_specs_overlying_grid(self, edisgo_grid, logger): storage_units_parametrisation=None, ) - # ToDo for now additional optimised storage capacity is ignored as capacities - # are very small and optimisation does not offer storage positioning + # ToDo (medium priority) for now additional optimised storage capacity is + # ignored as capacities are very small and optimisation does not offer storage + # positioning # if specs["storage_units_p_nom"] > 0.3: # logger.info("Set up large battery storage units.") # edisgo_grid.add_component( @@ -1102,11 +1104,11 @@ def _run_edisgo_task_specs_overlying_grid(self, edisgo_grid, logger): edisgo_grid.topology.loads_df.sector == "individual_heating" ] if hp_decentral.empty and specs["thermal_storage_rural_capacity"] > 0: - raise ValueError( + logger.warning( "There are thermal storage units for individual heating but no " "heat pumps." ) - if specs["thermal_storage_rural_capacity"] > 0: + if not hp_decentral.empty and specs["thermal_storage_rural_capacity"] > 0: tes_cap = ( edisgo_grid.topology.loads_df.loc[hp_decentral.index, "p_set"] * specs["thermal_storage_rural_capacity"] @@ -1119,52 +1121,33 @@ def _run_edisgo_task_specs_overlying_grid(self, edisgo_grid, logger): "efficiency": 0.9, } ) - # district heating - hp_dh = edisgo_grid.topology.loads_df[ - edisgo_grid.topology.loads_df.sector == "district_heating" - ] - # ToDo check - if not hp_dh.empty: - if specs["thermal_storage_central_capacity"] > 0: - tes_cap = ( - edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"] - * specs["thermal_storage_central_capacity"] - / edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"].sum() - ) - # ToDo get efficiency from specs - edisgo_grid.heat_pump.thermal_storage_units_df = pd.concat( - [ - edisgo_grid.heat_pump.thermal_storage_units_df, - pd.DataFrame( - data={ - "capacity": tes_cap, - "efficiency": 0.9, - } - ), - ] - ) + # # district heating + # hp_dh = edisgo_grid.topology.loads_df[ + # edisgo_grid.topology.loads_df.sector == "district_heating" + # ] + # # ToDo check + # if not hp_dh.empty: + # # ToDo map area ID + # if specs["thermal_storage_central_capacity"] > 0: + # tes_cap = ( + # edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"] + # * specs["thermal_storage_central_capacity"] + # / edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"].sum() + # ) + # # ToDo get efficiency from specs + # edisgo_grid.heat_pump.thermal_storage_units_df = pd.concat( + # [ + # edisgo_grid.heat_pump.thermal_storage_units_df, + # pd.DataFrame( + # data={ + # "capacity": tes_cap, + # "efficiency": 0.9, + # } + # ), + # ] + # ) logger.info("Set requirements from overlying grid.") - # ToDo requirements need to be scaled to capacity of home storage units - edisgo_grid.overlying_grid.storage_units_active_power = specs[ - "storage_units_active_power" - ] - edisgo_grid.overlying_grid.dsm_active_power = specs["dsm_active_power"] - edisgo_grid.overlying_grid.electromobility_active_power = specs[ - "electromobility_active_power" - ] - edisgo_grid.overlying_grid.heat_pump_decentral_active_power = specs[ - "heat_pump_rural_active_power" - ] - edisgo_grid.overlying_grid.heat_pump_central_active_power = specs[ - "heat_pump_central_active_power" - ] - edisgo_grid.overlying_grid.geothermal_energy_feedin_district_heating = specs[ - "geothermal_energy_feedin_district_heating" - ] - edisgo_grid.overlying_grid.solarthermal_energy_feedin_district_heating = specs[ - "solarthermal_energy_feedin_district_heating" - ] # curtailment # scale curtailment by ratio of nominal power in eDisGo and eTraGo @@ -1188,7 +1171,41 @@ def _run_edisgo_task_specs_overlying_grid(self, edisgo_grid, logger): logger.warning("Curtailment exceeds feed-in!") edisgo_grid.overlying_grid.renewables_curtailment = total_curtailment - # ToDo CHP + resistive heaters + # battery storage + # scale storage time series by ratio of nominal power in eDisGo and eTraGo + p_nom_total = specs["storage_units_p_nom"] + p_nom_mv_lv = edisgo_grid.topology.storage_units_df.p_nom.sum() + edisgo_grid.overlying_grid.storage_units_active_power = ( + specs["storage_units_active_power"] * p_nom_mv_lv / p_nom_total + ) + edisgo_grid.overlying_grid.storage_units_soc = specs["storage_units_soc"] + + # DSM + edisgo_grid.overlying_grid.dsm_active_power = specs["dsm_active_power"] + + # BEV + edisgo_grid.overlying_grid.electromobility_active_power = specs[ + "electromobility_active_power" + ] + + # PtH + # scale heat pump time series by ratio of nominal power in eDisGo and eTraGo + p_nom_total = specs["heat_pump_rural_p_nom"] + p_nom_mv_lv = edisgo_grid.topology.loads_df[ + edisgo_grid.topology.loads_df.sector.isin( + ["individual_heating", "individual_heating_resistive_heater"] + ) + ].p_set.sum() + edisgo_grid.overlying_grid.heat_pump_decentral_active_power = ( + specs["heat_pump_rural_active_power"] * p_nom_mv_lv / p_nom_total + ) + # # ToDo scale by p_nom + # edisgo_grid.overlying_grid.heat_pump_central_active_power = specs[ + # "heat_pump_central_active_power" + # ] + # edisgo_grid.overlying_grid.feedin_district_heating = specs[ + # "feedin_district_heating" + # ] logger.info("Run integrity check.") edisgo_grid.check_integrity() From 752f1379e54c3406376d53d86c8e92207c7e6f4a Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 25 Apr 2023 10:13:45 +0200 Subject: [PATCH 078/126] Rename water generator type --- ego/tools/edisgo_integration.py | 10 ++++++---- ego/tools/interface.py | 10 +++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 134af705..39e7e878 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1054,16 +1054,18 @@ def _run_edisgo_task_specs_overlying_grid(self, edisgo_grid, logger): # rename CHP carrier to match with carrier names in overlying grid generators_df = edisgo_grid.topology.generators_df if "p_nom_th" in generators_df.columns: - gens_gas_chp = generators_df[ + gens_rename = generators_df[ (generators_df["type"].isin(["gas", "gas extended", "oil", "others"])) & (generators_df["p_nom_th"] > 0) ] - generators_df.loc[gens_gas_chp.index, "type"] = "gas_CHP" - gens_biomass_chp = generators_df[ + generators_df.loc[gens_rename.index, "type"] = "gas_CHP" + gens_rename = generators_df[ (generators_df["type"].isin(["biomass"])) & (generators_df["p_nom_th"] > 0) ] - generators_df.loc[gens_biomass_chp.index, "type"] = "biomass_CHP" + generators_df.loc[gens_rename.index, "type"] = "biomass_CHP" + gens_rename = generators_df[generators_df["type"].isin(["water"])] + generators_df.loc[gens_rename.index, "type"] = "run_of_river" edisgo_grid.set_time_series_active_power_predefined( dispatchable_generators_ts=specs["dispatchable_generators_active_power"], diff --git a/ego/tools/interface.py b/ego/tools/interface.py index 4545d569..6eea6910 100644 --- a/ego/tools/interface.py +++ b/ego/tools/interface.py @@ -277,17 +277,17 @@ def dispatchable_gens(): dispatchable_gens_df = generators_df[ generators_df["carrier"].isin(dispatchable_gens_carriers) ] - # Rename CHP carrier to match with carrier names in eDisGo - gens_gas_chp = dispatchable_gens_df[ + # Rename carriers to match with carrier names in eDisGo + gens = dispatchable_gens_df[ dispatchable_gens_df.carrier.isin(["central_gas_CHP", "industrial_gas_CHP"]) ] - dispatchable_gens_df.loc[gens_gas_chp.index, "carrier"] = "gas_CHP" - gens_biomass_chp = dispatchable_gens_df[ + dispatchable_gens_df.loc[gens.index, "carrier"] = "gas_CHP" + gens = dispatchable_gens_df[ dispatchable_gens_df.carrier.isin( ["central_biomass_CHP", "industrial_biomass_CHP"] ) ] - dispatchable_gens_df.loc[gens_biomass_chp.index, "carrier"] = "biomass_CHP" + dispatchable_gens_df.loc[gens.index, "carrier"] = "biomass_CHP" for carrier in dispatchable_gens_df.carrier.unique(): p_nom = dispatchable_gens_df.loc[ dispatchable_gens_df["carrier"] == carrier, "p_nom" From 0448197aba171180789b99145549802e8fa4f079 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 26 Apr 2023 19:43:12 +0200 Subject: [PATCH 079/126] Add new database tables to config --- ego/examples/edisgo_workflow/scenario_setting.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ego/examples/edisgo_workflow/scenario_setting.json b/ego/examples/edisgo_workflow/scenario_setting.json index 90b883d1..73880ec6 100644 --- a/ego/examples/edisgo_workflow/scenario_setting.json +++ b/ego/examples/edisgo_workflow/scenario_setting.json @@ -69,7 +69,9 @@ "etrago_load": "grid.egon_etrago_load", "etrago_load_timeseries": "grid.egon_etrago_load_timeseries", "heat_pump_capacity_individual": "supply.egon_individual_heating", - "pth_capacity_district_heating": "grid.egon_etrago_link" + "pth_capacity_district_heating": "grid.egon_etrago_link", + "district_heating_areas": "demand.egon_district_heating_areas", + "etrago_bus": "grid.egon_etrago_bus" }, "database_name": "", "host": "127.0.0.1", From 1c85de99aa6d2f37c93e4bfa78ef2738330ccb45 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 26 Apr 2023 19:45:30 +0200 Subject: [PATCH 080/126] Adapt eTraGo interface --- ego/tools/edisgo_integration.py | 172 +++++++++++++++------ ego/tools/interface.py | 260 ++++++++++++++++++++------------ 2 files changed, 296 insertions(+), 136 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 39e7e878..e5166357 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -48,16 +48,15 @@ from sqlalchemy.orm import scoped_session, sessionmaker if "READTHEDOCS" not in os.environ: - from edisgo.edisgo import import_edisgo_from_files from edisgo.tools.config import Config from edisgo.tools.logger import setup_logger from edisgo.tools.plots import mv_grid_topology from egoio.db_tables import grid, model_draft from egoio.tools import db + from sqlalchemy import func - from ego.mv_clustering import cluster_workflow - from ego.mv_clustering.database import get_engine + from ego.mv_clustering import cluster_workflow, database from ego.tools.economics import edisgo_grid_investment from ego.tools.interface import ETraGoMinimalData, get_etrago_results_per_bus @@ -771,6 +770,7 @@ def run_edisgo(self, mv_grid_id): # ##################### general settings #################### config = self._json_file scenario = config["eTraGo"]["scn_name"] + engine = database.get_engine(config=config) # results directory results_dir = os.path.join( @@ -802,7 +802,7 @@ def run_edisgo(self, mv_grid_id): # ################### task: setup grid ################## if "1_setup_grid" in config["eDisGo"]["tasks"]: edisgo_grid = self._run_edisgo_task_setup_grid( - mv_grid_id, scenario, logger, config + mv_grid_id, scenario, logger, config, engine ) if "2_specs_overlying_grid" not in config["eDisGo"]["tasks"]: edisgo_grid.save( @@ -837,7 +837,7 @@ def run_edisgo(self, mv_grid_id): ) edisgo_grid.legacy_grids = False edisgo_grid = self._run_edisgo_task_specs_overlying_grid( - edisgo_grid, logger + edisgo_grid, scenario, logger, config, engine ) if "3_optimisation" not in config["eDisGo"]["tasks"]: edisgo_grid.save( @@ -884,7 +884,7 @@ def run_edisgo(self, mv_grid_id): return {edisgo_grid.topology.id: results_dir} - def _run_edisgo_task_setup_grid(self, mv_grid_id, scenario, logger, config): + def _run_edisgo_task_setup_grid(self, mv_grid_id, scenario, logger, config, engine): """ Sets up EDisGo object for future scenario (without specifications from overlying grid). @@ -915,8 +915,6 @@ def _run_edisgo_task_setup_grid(self, mv_grid_id, scenario, logger, config): """ logger.info(f"MV grid {mv_grid_id}: Start task 'setup_grid'.") - engine = get_engine(config=config) - logger.info(f"MV grid {mv_grid_id}: Initialize MV grid.") grid_path = os.path.join( config["eGo"]["data_dir"], @@ -981,7 +979,9 @@ def _run_edisgo_task_setup_grid(self, mv_grid_id, scenario, logger, config): return edisgo_grid - def _run_edisgo_task_specs_overlying_grid(self, edisgo_grid, logger): + def _run_edisgo_task_specs_overlying_grid( + self, edisgo_grid, scenario, logger, config, engine + ): """ Gets specifications from overlying grid and integrates them into the EDisGo object. @@ -1111,43 +1111,114 @@ def _run_edisgo_task_specs_overlying_grid(self, edisgo_grid, logger): "heat pumps." ) if not hp_decentral.empty and specs["thermal_storage_rural_capacity"] > 0: + # distribute thermal storage capacity to all heat pump depending on + # heat pump size tes_cap = ( edisgo_grid.topology.loads_df.loc[hp_decentral.index, "p_set"] * specs["thermal_storage_rural_capacity"] / edisgo_grid.topology.loads_df.loc[hp_decentral.index, "p_set"].sum() ) - # ToDo get efficiency from specs edisgo_grid.heat_pump.thermal_storage_units_df = pd.DataFrame( data={ "capacity": tes_cap, - "efficiency": 0.9, + "efficiency": specs["thermal_storage_rural_efficiency"], } ) - # # district heating - # hp_dh = edisgo_grid.topology.loads_df[ - # edisgo_grid.topology.loads_df.sector == "district_heating" - # ] - # # ToDo check - # if not hp_dh.empty: - # # ToDo map area ID - # if specs["thermal_storage_central_capacity"] > 0: - # tes_cap = ( - # edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"] - # * specs["thermal_storage_central_capacity"] - # / edisgo_grid.topology.loads_df.loc[hp_dh.index, "p_set"].sum() - # ) - # # ToDo get efficiency from specs - # edisgo_grid.heat_pump.thermal_storage_units_df = pd.concat( - # [ - # edisgo_grid.heat_pump.thermal_storage_units_df, - # pd.DataFrame( - # data={ - # "capacity": tes_cap, - # "efficiency": 0.9, - # } - # ), - # ] - # ) + # district heating + hp_dh = edisgo_grid.topology.loads_df[ + edisgo_grid.topology.loads_df.sector.isin( + ["district_heating", "district_heating_resistive_heater"] + ) + ] + # check if there are as many district heating systems in eTraGo as in eDisGo + if len(hp_dh.area_id.unique()) != len(specs["feedin_district_heating"].columns): + logger.warning( + f"There are {len(hp_dh.area_id.unique())} district heating " + f"systems in eDisGo and " + f"{len(specs['feedin_district_heating'].columns)} in eTraGo." + ) + # check that installed PtH capacity is equal in eTraGo as in eDisGo + if abs(hp_dh.p_set.sum() - specs["heat_pump_central_p_nom"]) > 1e-3: + logger.warning( + f"Installed capacity of PtH units in district heating differs between " + f"eTraGo ({specs['heat_pump_central_p_nom']} MW) and eDisGo " + f"({hp_dh.p_set.sum()} MW)." + ) + + if not specs["feedin_district_heating"].empty: + + # map district heating ID to heat bus ID from eTraGo + orm = database.register_tables_in_saio(engine, config=config) + heat_buses = [int(_) for _ in specs["feedin_district_heating"].columns] + with database.session_scope(engine) as session: + # get srid of etrago_bus table + query = session.query(func.ST_SRID(orm["etrago_bus"].geom)).limit(1) + srid_etrago_bus = query.all()[0] + # get district heating ID corresponding to heat bus ID by geo join + query = ( + session.query( + orm["etrago_bus"].bus_id.label("heat_bus_id"), + orm["district_heating_areas"].id.label("district_heating_id"), + ) + .filter( + orm["etrago_bus"].scn_name == scenario, + orm["district_heating_areas"].scenario == scenario, + orm["etrago_bus"].bus_id.in_(heat_buses), + ) + .outerjoin( # join to obtain district heating ID + orm["district_heating_areas"], + func.ST_Transform( + func.ST_Centroid( + orm["district_heating_areas"].geom_polygon + ), + srid_etrago_bus, + ) + == orm["etrago_bus"].geom, + ) + ) + mapping_heat_bus_dh_id = pd.read_sql( + query.statement, + engine, + index_col="district_heating_id", + ) + hp_dh = pd.merge( + hp_dh, + mapping_heat_bus_dh_id, + left_on="district_heating_id", + right_index=True, + ) + + for heat_bus in heat_buses: + if str(heat_bus) in specs["thermal_storage_central_capacity"].index: + if specs["thermal_storage_central_capacity"].at[str(heat_bus)] > 0: + # get PtH unit name to allocate thermal storage unit to + comp_name = hp_dh[hp_dh.heat_bus_id == heat_bus].index[0] + edisgo_grid.heat_pump.thermal_storage_units_df = pd.concat( + [ + edisgo_grid.heat_pump.thermal_storage_units_df, + pd.DataFrame( + data={ + "capacity": specs[ + "thermal_storage_central_capacity" + ].at[str(heat_bus)], + "efficiency": specs[ + "thermal_storage_central_efficiency" + ], + }, + index=[comp_name], + ), + ] + ) + # overwrite column name of SoC dataframe to be district heating + # ID + specs["thermal_storage_central_soc"].rename( + columns={ + str(heat_bus): hp_dh.at[ + comp_name, "district_heating_id" + ] + }, + inplace=True, + ) logger.info("Set requirements from overlying grid.") @@ -1201,13 +1272,28 @@ def _run_edisgo_task_specs_overlying_grid(self, edisgo_grid, logger): edisgo_grid.overlying_grid.heat_pump_decentral_active_power = ( specs["heat_pump_rural_active_power"] * p_nom_mv_lv / p_nom_total ) - # # ToDo scale by p_nom - # edisgo_grid.overlying_grid.heat_pump_central_active_power = specs[ - # "heat_pump_central_active_power" - # ] - # edisgo_grid.overlying_grid.feedin_district_heating = specs[ - # "feedin_district_heating" - # ] + p_nom_total = specs["heat_pump_central_p_nom"] + p_nom_mv_lv = edisgo_grid.topology.loads_df[ + edisgo_grid.topology.loads_df.sector.isin( + ["district_heating", "district_heating_resistive_heater"] + ) + ].p_set.sum() + edisgo_grid.overlying_grid.heat_pump_central_active_power = ( + specs["heat_pump_central_active_power"] * p_nom_mv_lv / p_nom_total + ) + + # Other feed-in into district heating + edisgo_grid.overlying_grid.feedin_district_heating = specs[ + "feedin_district_heating" + ] + + # Thermal storage units SoC + edisgo_grid.overlying_grid.thermal_storage_units_central_soc = specs[ + "thermal_storage_rural_soc" + ] + edisgo_grid.overlying_grid.thermal_storage_units_central_soc = specs[ + "thermal_storage_central_soc" + ] logger.info("Run integrity check.") edisgo_grid.check_integrity() diff --git a/ego/tools/interface.py b/ego/tools/interface.py index 6eea6910..42ed03cb 100644 --- a/ego/tools/interface.py +++ b/ego/tools/interface.py @@ -51,7 +51,14 @@ def set_filtered_attribute(etrago_network_obj, component): # filter components columns_to_save = { - "links": ["bus0", "bus1", "carrier", "p_nom", "p_nom_opt"], + "links": [ + "bus0", + "bus1", + "carrier", + "p_nom", + "p_nom_opt", + "efficiency", + ], "generators": ["bus", "carrier", "p_nom", "p_nom_opt"], "stores": ["bus", "carrier", "e_nom", "e_nom_opt"], "storage_units": [ @@ -78,7 +85,7 @@ def set_filtered_attribute(etrago_network_obj, component): attribute_to_save = { "links": ["p0", "p1"], "generators": ["p", "p_max_pu", "q"], - "stores": ["p"], + "stores": ["p", "e"], "storage_units": ["p", "q", "state_of_charge"], "loads": ["p"], } @@ -171,7 +178,7 @@ def get_etrago_results_per_bus(bus_id, etrago_obj, pf_post_lopf, max_cos_phi_ren * 'renewables_p_nom' Installed capacity of fluctuating generators per technology (solar / wind) at the given bus. - Type: pd.Series + Type: pd.Series with carrier in index Unit: MW * 'storage_units_p_nom' @@ -206,50 +213,74 @@ def get_etrago_results_per_bus(bus_id, etrago_obj, pf_post_lopf, max_cos_phi_ren Unit: MW * 'heat_pump_rural_active_power' - Active power time series of PtH units at the given bus. + Active power time series of PtH units for individual heating at the given + bus. Type: pd.Series Unit: MW * 'heat_pump_rural_reactive_power' - Reactive power time series of PtH units at the given bus. + Reactive power time series of PtH units for individual heating at the given + bus. Type: pd.Series Unit: MVar * 'heat_pump_rural_p_nom' - Nominal power of all rural PtH units at the given bus. + Nominal power of all PtH units for individual heating at the given bus. Type: float - Unit: MWh + Unit: MW * 'thermal_storage_rural_capacity' - Capacity of the storage at the bus where the rural heat units feed in. + Capacity of thermal storage units in individual heating. Type: float Unit: MWh + * 'thermal_storage_rural_efficiency' + Charging and discharging efficiency of thermal storage units in individual + heating. + Type: float + Unit: p.u. + + * 'thermal_storage_rural_soc' + SoC of central thermal storage units. + Type: pd.Series + Unit: p.u. + * 'heat_pump_central_active_power' - Active power time series of PtH units at the given bus. + Active power time series of central PtH units at the given bus. Type: pd.Series Unit: MW * 'heat_pump_central_reactive_power' - Reactive power time series of PtH units at the given bus. + Reactive power time series of central PtH units at the given bus. Type: pd.Series Unit: MVar * 'heat_pump_central_p_nom' - Nominal power of all rural PtH units at the given bus. + Nominal power of all central PtH units at the given bus. Type: float - Unit: MWh + Unit: MW * 'thermal_storage_central_capacity' - Capacity of the storage at the bus where the central heat units feed in. - Type: pd.DataFrame - Columns: Area ID + Capacity of central thermal storage units. + Type: pd.Series with eTraGo heat bus ID in index Unit: MWh + * 'thermal_storage_central_efficiency' + Charging and discharging efficiency of central thermal storage units. + Type: float + Unit: p.u. + + * 'thermal_storage_central_soc' + SoC of central thermal storage units. + Type: pd.DataFrame + Columns: eTraGo heat bus ID + Unit: p.u. + * 'feedin_district_heating' - Geothermal feedin time series at the heat bus. + Time series of other thermal feed-in from e.g. gas boilers or geothermal + units at the heat bus. Type: pd.DataFrame - Columns: Area ID + Columns: eTraGo heat bus ID Unit: MW * 'electromobility_active_power' @@ -288,6 +319,8 @@ def dispatchable_gens(): ) ] dispatchable_gens_df.loc[gens.index, "carrier"] = "biomass_CHP" + gens = dispatchable_gens_df[dispatchable_gens_df.carrier.isin(["reservoir"])] + dispatchable_gens_df.loc[gens.index, "carrier"] = "run_of_river" for carrier in dispatchable_gens_df.carrier.unique(): p_nom = dispatchable_gens_df.loc[ dispatchable_gens_df["carrier"] == carrier, "p_nom" @@ -440,21 +473,17 @@ def renewable_generators(): if (weather_dep_gens_df_curt_p.min() < -1e-3).any(): logger.warning("Curtailment values smaller -1 kW.") - # ToDo remove once fixed - weather_dep_gens_df_curt_p[weather_dep_gens_df_curt_p < 0] = 0 + results["renewables_potential"] = weather_dep_gens_df_pot_p results["renewables_curtailment"] = weather_dep_gens_df_curt_p results["renewables_dispatch_reactive_power"] = weather_dep_gens_df_dis_q results["renewables_p_nom"] = agg_weather_dep_gens_df.set_index("carrier").p_nom def storages(): - # Storage # Filter batteries storages_df = etrago_obj.storage_units.loc[ (etrago_obj.storage_units["carrier"] == "battery") & (etrago_obj.storage_units["bus"] == str(bus_id)) - # & (etrago_obj.storage_units["p_nom_extendable"]) - # & (etrago_obj.storage_units["p_nom_opt"] > 0) ] if not storages_df.empty: # p_nom - p_nom_opt can always be used, if extendable is True or False @@ -503,84 +532,116 @@ def dsm(): results["dsm_active_power"] = dsm_df_p def central_heat(): - # Heat - # Central heat - # Power2Heat + central_heat_carriers = ["central_heat_pump", "central_resistive_heater"] central_heat_df = links_df.loc[ - links_df["carrier"].isin(central_heat_carriers) + (links_df["carrier"].isin(central_heat_carriers)) & (links_df["bus0"] == str(bus_id)) + & (links_df["p_nom"] <= 20) ] if not central_heat_df.empty: # Timeseries - central_heat_df_p = etrago_obj.links_t["p0"][central_heat_df.index] - central_heat_df_p.columns = central_heat_df["carrier"] - central_heat_df_q = pd.DataFrame( - 0.0, index=timeseries_index, columns=[central_heat_df["carrier"]] + central_heat_df_p = etrago_obj.links_t["p0"][central_heat_df.index].sum( + axis=1 ) + central_heat_df_q = pd.Series(0.0, index=timeseries_index) - # Stores - central_heat_bus = central_heat_df["bus1"].values[0] - central_heat_store_bus = etrago_obj.links.loc[ - etrago_obj.links["bus0"] == central_heat_bus, "bus1" - ].values[0] - central_heat_store_capacity = etrago_obj.stores.loc[ - (etrago_obj.stores["carrier"] == "central_heat_store") - & (etrago_obj.stores["bus"] == central_heat_store_bus), - "e_nom_opt", - ].values[0] - - # Feedin - geothermal_feedin_df = etrago_obj.generators[ - (etrago_obj.generators["carrier"] == "geo_thermal") - & (etrago_obj.generators["bus"] == central_heat_bus) - ] - if not geothermal_feedin_df.empty: - geothermal_feedin_df_p = etrago_obj.generators_t["p"][ - geothermal_feedin_df.index - ] - geothermal_feedin_df_p.columns = geothermal_feedin_df["carrier"] - else: - geothermal_feedin_df_p = pd.DataFrame( - 0.0, index=timeseries_index, columns=["geo_thermal"] - ) + # Nominal power of PtH units + p_nom = central_heat_df.p_nom.sum() - solarthermal_feedin_df = etrago_obj.generators[ - (etrago_obj.generators["carrier"] == "solar_thermal_collector") - & (etrago_obj.generators["bus"] == central_heat_bus) + # Stores + central_heat_buses = central_heat_df["bus1"].unique() + # find all heat stores connected to heat buses + central_heat_store_links_df = etrago_obj.links.loc[ + etrago_obj.links["bus0"].isin(central_heat_buses) ] - if not solarthermal_feedin_df.empty: - solarthermal_feedin_df_p = etrago_obj.generators_t["p"][ - solarthermal_feedin_df.index - ] - solarthermal_feedin_df_p.columns = solarthermal_feedin_df["carrier"] - else: - solarthermal_feedin_df_p = pd.DataFrame( - 0.0, index=timeseries_index, columns=["solar_thermal_collector"] + central_heat_store_df = etrago_obj.stores.loc[ + (etrago_obj.stores["carrier"] == "central_heat_store") + & ( + etrago_obj.stores["bus"].isin( + central_heat_store_links_df.bus1.values + ) ) - else: - column_names = central_heat_df["carrier"].to_list() - central_heat_df_p = pd.DataFrame( - 0.0, index=timeseries_index, columns=column_names - ) - central_heat_df_q = pd.DataFrame( - 0.0, index=timeseries_index, columns=column_names + ].reset_index(names="store_name") + central_heat_store_merge_links_df = pd.merge( + central_heat_store_links_df, + central_heat_store_df, + left_on="bus1", + right_on="bus", ) - central_heat_store_capacity = 0 - geothermal_feedin_df_p = pd.DataFrame( - 0.0, index=timeseries_index, columns=["geo_thermal"] + # capacity + central_heat_store_capacity = central_heat_store_merge_links_df.set_index( + "bus0" + ).e_nom_opt + # efficiency + central_heat_store_efficiency = ( + central_heat_store_links_df.efficiency.values[0] ) - solarthermal_feedin_df_p = pd.DataFrame( - 0.0, index=timeseries_index, columns=["solar_thermal_collector"] + # SoC + soc_ts = etrago_obj.stores_t["e"][ + central_heat_store_df.store_name.values + ].rename( + columns=central_heat_store_merge_links_df.set_index("store_name").bus0 ) - # ToDo: Overlying grid no resistive heater + soc_ts = soc_ts / central_heat_store_capacity + + # Other feed-in + dh_feedin_df = pd.DataFrame() + for heat_bus in central_heat_buses: + # get feed-in from generators + heat_gens = etrago_obj.generators[ + (etrago_obj.generators["bus"] == heat_bus) + & (etrago_obj.generators["carrier"] != "load shedding") + ] + if not heat_gens.empty: + feedin_df_gens = etrago_obj.generators_t["p"][heat_gens.index].sum( + axis=1 + ) + else: + feedin_df_gens = pd.Series(0.0, index=timeseries_index) + # get feed-in from links + # get all links feeding into heat bus (except heat store) + heat_links_all = etrago_obj.links[ + (etrago_obj.links["bus1"] == heat_bus) + & ( + etrago_obj.links["carrier"].isin( + [ + "central_gas_boiler", + "central_gas_CHP_heat", + "central_heat_pump", + "central_resistive_heater", + ] + ) + ) + ] + # filter out PtH units that are already considered in PtH dispatch + # above + heat_links = heat_links_all.drop( + index=central_heat_df.index, errors="ignore" + ) + if not heat_links.empty: + feedin_df_links = etrago_obj.links_t["p1"][heat_links.index].sum( + axis=1 + ) + else: + feedin_df_links = pd.Series(0.0, index=timeseries_index) + dh_feedin_df[heat_bus] = feedin_df_gens + feedin_df_links + else: + central_heat_df_p = pd.Series(0.0, index=timeseries_index) + central_heat_df_q = pd.Series(0.0, index=timeseries_index) + p_nom = 0 + central_heat_store_capacity = pd.Series() + central_heat_store_efficiency = 0 + soc_ts = pd.DataFrame() + dh_feedin_df = pd.DataFrame() + results["heat_pump_central_active_power"] = central_heat_df_p results["heat_pump_central_reactive_power"] = central_heat_df_q + results["heat_pump_central_p_nom"] = p_nom results["thermal_storage_central_capacity"] = central_heat_store_capacity - results["geothermal_energy_feedin_district_heating"] = geothermal_feedin_df_p - results[ - "solarthermal_energy_feedin_district_heating" - ] = solarthermal_feedin_df_p + results["thermal_storage_central_efficiency"] = central_heat_store_efficiency + results["thermal_storage_central_soc"] = soc_ts + results["feedin_district_heating"] = dh_feedin_df def rural_heat(): @@ -596,26 +657,39 @@ def rural_heat(): rural_heat_df_q = pd.Series(0.0, index=timeseries_index) # p_nom rural_heat_p_nom = rural_heat_df.p_nom.sum() - # Stores + # Store + # capacity rural_heat_bus = rural_heat_df["bus1"].values[0] - rural_heat_store_bus = etrago_obj.links.loc[ - etrago_obj.links["bus0"] == rural_heat_bus, "bus1" - ].values[0] - rural_heat_store_capacity = etrago_obj.stores.loc[ + rural_heat_store_link_df = etrago_obj.links.loc[ + etrago_obj.links["bus0"] == rural_heat_bus + ] + rural_heat_store_df = etrago_obj.stores.loc[ (etrago_obj.stores["carrier"] == "rural_heat_store") - & (etrago_obj.stores["bus"] == rural_heat_store_bus), - "e_nom_opt", - ].values[0] + & (etrago_obj.stores["bus"] == rural_heat_store_link_df.bus1.values[0]) + ] + rural_heat_store_capacity = rural_heat_store_df.e_nom_opt.values[0] + # efficiency + heat_store_efficiency = rural_heat_store_link_df.efficiency.values[0] + # SoC + if rural_heat_store_capacity > 0: + soc_ts = etrago_obj.stores_t["e"][rural_heat_store_df.index[0]] + soc_ts = soc_ts / rural_heat_store_capacity + else: + soc_ts = pd.Series(0.0, index=timeseries_index) else: rural_heat_df_p = pd.Series(0.0, index=timeseries_index) rural_heat_df_q = pd.Series(0.0, index=timeseries_index) - rural_heat_store_capacity = 0 rural_heat_p_nom = 0 + rural_heat_store_capacity = 0 + heat_store_efficiency = 0 + soc_ts = pd.Series(0.0, index=timeseries_index) results["heat_pump_rural_active_power"] = rural_heat_df_p results["heat_pump_rural_reactive_power"] = rural_heat_df_q results["heat_pump_rural_p_nom"] = rural_heat_p_nom results["thermal_storage_rural_capacity"] = rural_heat_store_capacity + results["thermal_storage_rural_efficiency"] = heat_store_efficiency + results["thermal_storage_rural_soc"] = soc_ts def bev_charger(): # BEV charger @@ -662,7 +736,7 @@ def bev_charger(): renewable_generators() storages() dsm() - # central_heat() + central_heat() rural_heat() bev_charger() logger.info(f"Overall time: {time.perf_counter() - t_start}") From 24a5c2c5b7e9f905773303dece072f88ed7bdc9d Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 27 Apr 2023 10:35:09 +0200 Subject: [PATCH 081/126] Bug fix --- ego/tools/edisgo_integration.py | 177 +++++++++++++++++--------------- 1 file changed, 96 insertions(+), 81 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index e5166357..ecdbf7b3 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1131,94 +1131,109 @@ def _run_edisgo_task_specs_overlying_grid( ) ] # check if there are as many district heating systems in eTraGo as in eDisGo - if len(hp_dh.area_id.unique()) != len(specs["feedin_district_heating"].columns): - logger.warning( - f"There are {len(hp_dh.area_id.unique())} district heating " - f"systems in eDisGo and " - f"{len(specs['feedin_district_heating'].columns)} in eTraGo." - ) - # check that installed PtH capacity is equal in eTraGo as in eDisGo - if abs(hp_dh.p_set.sum() - specs["heat_pump_central_p_nom"]) > 1e-3: - logger.warning( - f"Installed capacity of PtH units in district heating differs between " - f"eTraGo ({specs['heat_pump_central_p_nom']} MW) and eDisGo " - f"({hp_dh.p_set.sum()} MW)." - ) + if hp_dh.empty: + if len(specs["feedin_district_heating"].columns) != 0: + logger.warning( + f"There are {len(hp_dh.area_id.unique())} district heating " + f"systems in eDisGo and " + f"{len(specs['feedin_district_heating'].columns)} in eTraGo." + ) + else: + if len(hp_dh.area_id.unique()) != len( + specs["feedin_district_heating"].columns + ): + logger.warning( + f"There are {len(hp_dh.area_id.unique())} district heating " + f"systems in eDisGo and " + f"{len(specs['feedin_district_heating'].columns)} in eTraGo." + ) + # check that installed PtH capacity is equal in eTraGo as in eDisGo + if abs(hp_dh.p_set.sum() - specs["heat_pump_central_p_nom"]) > 1e-3: + logger.warning( + f"Installed capacity of PtH units in district heating differs " + f"between eTraGo ({specs['heat_pump_central_p_nom']} MW) and " + f"eDisGo ({hp_dh.p_set.sum()} MW)." + ) - if not specs["feedin_district_heating"].empty: - - # map district heating ID to heat bus ID from eTraGo - orm = database.register_tables_in_saio(engine, config=config) - heat_buses = [int(_) for _ in specs["feedin_district_heating"].columns] - with database.session_scope(engine) as session: - # get srid of etrago_bus table - query = session.query(func.ST_SRID(orm["etrago_bus"].geom)).limit(1) - srid_etrago_bus = query.all()[0] - # get district heating ID corresponding to heat bus ID by geo join - query = ( - session.query( - orm["etrago_bus"].bus_id.label("heat_bus_id"), - orm["district_heating_areas"].id.label("district_heating_id"), - ) - .filter( - orm["etrago_bus"].scn_name == scenario, - orm["district_heating_areas"].scenario == scenario, - orm["etrago_bus"].bus_id.in_(heat_buses), - ) - .outerjoin( # join to obtain district heating ID - orm["district_heating_areas"], - func.ST_Transform( - func.ST_Centroid( - orm["district_heating_areas"].geom_polygon + if not specs["feedin_district_heating"].empty: + + # map district heating ID to heat bus ID from eTraGo + orm = database.register_tables_in_saio(engine, config=config) + heat_buses = [int(_) for _ in specs["feedin_district_heating"].columns] + with database.session_scope(engine) as session: + # get srid of etrago_bus table + query = session.query(func.ST_SRID(orm["etrago_bus"].geom)).limit(1) + srid_etrago_bus = query.all()[0] + # get district heating ID corresponding to heat bus ID by geo join + query = ( + session.query( + orm["etrago_bus"].bus_id.label("heat_bus_id"), + orm["district_heating_areas"].id.label( + "district_heating_id" ), - srid_etrago_bus, ) - == orm["etrago_bus"].geom, + .filter( + orm["etrago_bus"].scn_name == scenario, + orm["district_heating_areas"].scenario == scenario, + orm["etrago_bus"].bus_id.in_(heat_buses), + ) + .outerjoin( # join to obtain district heating ID + orm["district_heating_areas"], + func.ST_Transform( + func.ST_Centroid( + orm["district_heating_areas"].geom_polygon + ), + srid_etrago_bus, + ) + == orm["etrago_bus"].geom, + ) ) + mapping_heat_bus_dh_id = pd.read_sql( + query.statement, + engine, + index_col="district_heating_id", + ) + hp_dh = pd.merge( + hp_dh, + mapping_heat_bus_dh_id, + left_on="district_heating_id", + right_index=True, ) - mapping_heat_bus_dh_id = pd.read_sql( - query.statement, - engine, - index_col="district_heating_id", - ) - hp_dh = pd.merge( - hp_dh, - mapping_heat_bus_dh_id, - left_on="district_heating_id", - right_index=True, - ) - for heat_bus in heat_buses: - if str(heat_bus) in specs["thermal_storage_central_capacity"].index: - if specs["thermal_storage_central_capacity"].at[str(heat_bus)] > 0: - # get PtH unit name to allocate thermal storage unit to - comp_name = hp_dh[hp_dh.heat_bus_id == heat_bus].index[0] - edisgo_grid.heat_pump.thermal_storage_units_df = pd.concat( - [ - edisgo_grid.heat_pump.thermal_storage_units_df, - pd.DataFrame( - data={ - "capacity": specs[ - "thermal_storage_central_capacity" - ].at[str(heat_bus)], - "efficiency": specs[ - "thermal_storage_central_efficiency" - ], - }, - index=[comp_name], - ), - ] - ) - # overwrite column name of SoC dataframe to be district heating - # ID - specs["thermal_storage_central_soc"].rename( - columns={ - str(heat_bus): hp_dh.at[ - comp_name, "district_heating_id" + for heat_bus in heat_buses: + if str(heat_bus) in specs["thermal_storage_central_capacity"].index: + if ( + specs["thermal_storage_central_capacity"].at[str(heat_bus)] + > 0 + ): + # get PtH unit name to allocate thermal storage unit to + comp_name = hp_dh[hp_dh.heat_bus_id == heat_bus].index[0] + edisgo_grid.heat_pump.thermal_storage_units_df = pd.concat( + [ + edisgo_grid.heat_pump.thermal_storage_units_df, + pd.DataFrame( + data={ + "capacity": specs[ + "thermal_storage_central_capacity" + ].at[str(heat_bus)], + "efficiency": specs[ + "thermal_storage_central_efficiency" + ], + }, + index=[comp_name], + ), ] - }, - inplace=True, - ) + ) + # overwrite column name of SoC dataframe to be district + # heating ID + specs["thermal_storage_central_soc"].rename( + columns={ + str(heat_bus): hp_dh.at[ + comp_name, "district_heating_id" + ] + }, + inplace=True, + ) logger.info("Set requirements from overlying grid.") From a625210279a96fe862f38301fc648b601b077949 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 28 Apr 2023 14:58:35 +0200 Subject: [PATCH 082/126] Add option to drop non-converging time steps --- ego/tools/edisgo_integration.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index ecdbf7b3..e3a00e85 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1017,10 +1017,28 @@ def _run_edisgo_task_specs_overlying_grid( self._max_cos_phi_renewable, ) + # exclude non-converging time steps + try: + convergence = pd.read_csv( + os.path.join(config["eGo"]["csv_import_eTraGo"], "pf_solution.csv"), + index_col=0, + parse_dates=True, + ) + ts_not_converged = convergence[~convergence.converged].index + snapshots = specs["timeindex"].drop(ts_not_converged) + except FileNotFoundError: + logger.info( + "No info on converged time steps, wherefore it is assumed that all " + "converged." + ) + snapshots = specs["timeindex"] + except Exception: + raise + # overwrite previously set dummy time index if year that was used differs from # year used in etrago edisgo_year = edisgo_grid.timeseries.timeindex[0].year - etrago_year = specs["timeindex"][0].year + etrago_year = snapshots[0].year if edisgo_year != etrago_year: timeindex_new_full = pd.date_range( f"1/1/{etrago_year}", periods=8760, freq="H" @@ -1047,7 +1065,7 @@ def _run_edisgo_task_specs_overlying_grid( "lower_energy" ].index = timeindex_new_full # TimeSeries.timeindex - edisgo_grid.timeseries.timeindex = specs["timeindex"] + edisgo_grid.timeseries.timeindex = snapshots logger.info("Set generator time series.") # active power From 6bea3c030c0e2b7217b72feb4d6f8db5079c4442 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 28 Apr 2023 14:59:30 +0200 Subject: [PATCH 083/126] Distribute thermal storage for individual heating --- ego/tools/edisgo_integration.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index e3a00e85..a2a46e37 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1129,12 +1129,21 @@ def _run_edisgo_task_specs_overlying_grid( "heat pumps." ) if not hp_decentral.empty and specs["thermal_storage_rural_capacity"] > 0: - # distribute thermal storage capacity to all heat pump depending on - # heat pump size - tes_cap = ( + tes_cap_min_cumsum = ( edisgo_grid.topology.loads_df.loc[hp_decentral.index, "p_set"] + .sort_index() + .cumsum() + ) + hps_selected = tes_cap_min_cumsum[ + tes_cap_min_cumsum <= specs["thermal_storage_rural_capacity"] + ].index + + # distribute thermal storage capacity to all selected heat pumps depending + # on heat pump size + tes_cap = ( + edisgo_grid.topology.loads_df.loc[hps_selected, "p_set"] * specs["thermal_storage_rural_capacity"] - / edisgo_grid.topology.loads_df.loc[hp_decentral.index, "p_set"].sum() + / edisgo_grid.topology.loads_df.loc[hps_selected, "p_set"].sum() ) edisgo_grid.heat_pump.thermal_storage_units_df = pd.DataFrame( data={ From 92401a668fad15e97c604a824c2112ee2f3967f3 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 28 Apr 2023 15:11:28 +0200 Subject: [PATCH 084/126] Set column names to be district heating ID --- ego/tools/edisgo_integration.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index a2a46e37..3384b174 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1261,6 +1261,14 @@ def _run_edisgo_task_specs_overlying_grid( }, inplace=True, ) + specs["feedin_district_heating"].rename( + columns={ + str(heat_bus): hp_dh.at[ + comp_name, "district_heating_id" + ] + }, + inplace=True, + ) logger.info("Set requirements from overlying grid.") From abd64460a41535ea0acb9908d031dc2827868205 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 28 Apr 2023 15:34:33 +0200 Subject: [PATCH 085/126] Correct numerical errors for curtailment --- ego/tools/edisgo_integration.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 3384b174..fa116cad 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1290,8 +1290,18 @@ def _run_edisgo_task_specs_overlying_grid( :, vres_gens ].sum(axis=1) total_curtailment = specs["renewables_curtailment"].sum(axis=1) - if (total_curtailment > pot_vres_gens).any(): - logger.warning("Curtailment exceeds feed-in!") + diff = pot_vres_gens - total_curtailment + if (diff < 0).any(): + # if curtailment is much larger than feed-in, throw an error + if (diff < -1e-3).any(): + raise ValueError("Curtailment exceeds feed-in!") + # if curtailment is only slightly larger than feed-in, this is due to + # numerical errors and therefore corrected + else: + ts_neg_curtailment = diff[(diff < 0)].index + total_curtailment.loc[ts_neg_curtailment] += diff.loc[ + ts_neg_curtailment + ] edisgo_grid.overlying_grid.renewables_curtailment = total_curtailment # battery storage From e0d3c9a1969714156228a4d49f2827a45ed3f6ec Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 28 Apr 2023 15:59:52 +0200 Subject: [PATCH 086/126] Rename conventional to others --- ego/tools/edisgo_integration.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index fa116cad..c913ea30 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1069,7 +1069,7 @@ def _run_edisgo_task_specs_overlying_grid( logger.info("Set generator time series.") # active power - # rename CHP carrier to match with carrier names in overlying grid + # rename carrier to match with carrier names in overlying grid generators_df = edisgo_grid.topology.generators_df if "p_nom_th" in generators_df.columns: gens_rename = generators_df[ @@ -1084,6 +1084,8 @@ def _run_edisgo_task_specs_overlying_grid( generators_df.loc[gens_rename.index, "type"] = "biomass_CHP" gens_rename = generators_df[generators_df["type"].isin(["water"])] generators_df.loc[gens_rename.index, "type"] = "run_of_river" + gens_rename = generators_df[generators_df["type"].isin(["conventional"])] + generators_df.loc[gens_rename.index, "type"] = "others" edisgo_grid.set_time_series_active_power_predefined( dispatchable_generators_ts=specs["dispatchable_generators_active_power"], From e0659bf686cfdd02288fc63d32fe6d18dac34cd6 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 28 Apr 2023 16:01:38 +0200 Subject: [PATCH 087/126] Make values positive --- ego/tools/interface.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ego/tools/interface.py b/ego/tools/interface.py index 42ed03cb..15a14a58 100644 --- a/ego/tools/interface.py +++ b/ego/tools/interface.py @@ -373,7 +373,7 @@ def dispatchable_gens(): for carrier in chp_df.carrier.unique(): p_nom = chp_df.loc[chp_df["carrier"] == carrier, "p_nom"].sum() columns_to_aggregate = chp_df[chp_df["carrier"] == carrier].index - dispatchable_gens_df_p[carrier] = ( + dispatchable_gens_df_p[carrier] = abs( etrago_obj.links_t["p1"][columns_to_aggregate].sum( axis="columns" ) @@ -620,8 +620,8 @@ def central_heat(): index=central_heat_df.index, errors="ignore" ) if not heat_links.empty: - feedin_df_links = etrago_obj.links_t["p1"][heat_links.index].sum( - axis=1 + feedin_df_links = abs( + etrago_obj.links_t["p1"][heat_links.index].sum(axis=1) ) else: feedin_df_links = pd.Series(0.0, index=timeseries_index) From 0ca1205958f46a5dfbf407f56ac0f0eef930be9a Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 28 Apr 2023 17:53:15 +0200 Subject: [PATCH 088/126] Fix setting type for CHP --- ego/tools/edisgo_integration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index c913ea30..fe05c3ec 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1074,12 +1074,12 @@ def _run_edisgo_task_specs_overlying_grid( if "p_nom_th" in generators_df.columns: gens_rename = generators_df[ (generators_df["type"].isin(["gas", "gas extended", "oil", "others"])) - & (generators_df["p_nom_th"] > 0) + & (~generators_df["p_nom_th"].isna()) ] generators_df.loc[gens_rename.index, "type"] = "gas_CHP" gens_rename = generators_df[ (generators_df["type"].isin(["biomass"])) - & (generators_df["p_nom_th"] > 0) + & (generators_df["p_nom_th"].isna()) ] generators_df.loc[gens_rename.index, "type"] = "biomass_CHP" gens_rename = generators_df[generators_df["type"].isin(["water"])] From 2c0681674cc9f3aa7d1458ba646afc3f46cd7e7d Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 28 Apr 2023 17:57:11 +0200 Subject: [PATCH 089/126] Only check for converged time steps --- ego/tools/edisgo_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index fe05c3ec..c0002240 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1291,7 +1291,7 @@ def _run_edisgo_task_specs_overlying_grid( pot_vres_gens = edisgo_grid.timeseries.generators_active_power.loc[ :, vres_gens ].sum(axis=1) - total_curtailment = specs["renewables_curtailment"].sum(axis=1) + total_curtailment = specs["renewables_curtailment"].loc[snapshots].sum(axis=1) diff = pot_vres_gens - total_curtailment if (diff < 0).any(): # if curtailment is much larger than feed-in, throw an error From 83c8c5909094a9849f9d1abe5244be01889c17c3 Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 2 May 2023 14:43:16 +0200 Subject: [PATCH 090/126] Bug fix --- ego/tools/edisgo_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index c0002240..55d9b82d 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1079,7 +1079,7 @@ def _run_edisgo_task_specs_overlying_grid( generators_df.loc[gens_rename.index, "type"] = "gas_CHP" gens_rename = generators_df[ (generators_df["type"].isin(["biomass"])) - & (generators_df["p_nom_th"].isna()) + & (~generators_df["p_nom_th"].isna()) ] generators_df.loc[gens_rename.index, "type"] = "biomass_CHP" gens_rename = generators_df[generators_df["type"].isin(["water"])] From ff4bea5f09e404d8c260d9b63b9309e987fe9e09 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 3 May 2023 14:09:40 +0200 Subject: [PATCH 091/126] Add task temporal complexity reduction --- ego/tools/edisgo_integration.py | 228 +++++++++++++++++++++++++++++++- 1 file changed, 227 insertions(+), 1 deletion(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 55d9b82d..0d38399c 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -49,9 +49,13 @@ if "READTHEDOCS" not in os.environ: from edisgo.edisgo import import_edisgo_from_files + from edisgo.network.overlying_grid import distribute_overlying_grid_requirements from edisgo.tools.config import Config from edisgo.tools.logger import setup_logger from edisgo.tools.plots import mv_grid_topology + from edisgo.tools.temporal_complexity_reduction import ( + get_most_critical_time_intervals, + ) from egoio.db_tables import grid, model_draft from egoio.tools import db from sqlalchemy import func @@ -839,7 +843,7 @@ def run_edisgo(self, mv_grid_id): edisgo_grid = self._run_edisgo_task_specs_overlying_grid( edisgo_grid, scenario, logger, config, engine ) - if "3_optimisation" not in config["eDisGo"]["tasks"]: + if "3_temporal_complexity_reduction" not in config["eDisGo"]["tasks"]: edisgo_grid.save( directory=os.path.join(results_dir, "grid_data_overlying_grid"), save_topology=True, @@ -855,6 +859,28 @@ def run_edisgo(self, mv_grid_id): ) return {edisgo_grid.topology.id: results_dir} + # ################### task: temporal complexity reduction ################## + if "3_temporal_complexity_reduction" in config["eDisGo"]["tasks"]: + if edisgo_grid is None: + grid_path = os.path.join(results_dir, "grid_data_overlying_grid.zip") + edisgo_grid = import_edisgo_from_files( + edisgo_path=grid_path, + import_topology=True, + import_timeseries=True, + import_results=False, + import_electromobility=True, + import_heat_pump=True, + import_dsm=True, + import_overlying_grid=True, + from_zip_archive=True, + ) + edisgo_grid.legacy_grids = False + time_intervals = self._run_edisgo_task_temporal_complexity_reduction( + edisgo_grid, logger, config + ) + if "4_optimisation" not in config["eDisGo"]["tasks"]: + return {edisgo_grid.topology.id: results_dir} + # ########################## task: optimisation ########################## # ToDo Maike Call optimisation @@ -1362,6 +1388,206 @@ def _run_edisgo_task_specs_overlying_grid( return edisgo_grid + def _run_edisgo_task_temporal_complexity_reduction( + self, edisgo_grid, logger, config + ): + """ + Runs the temporal complexity reduction. + + Parameters + ---------- + mv_grid_id : int + MV grid ID of the ding0 grid + + Returns + ------- + :class:`edisgo.EDisGo` + Returns the complete eDisGo container, also including results + + """ + logger.info("Start task 'temporal complexity reduction'.") + + # get non-converging time steps + try: + convergence = pd.read_csv( + os.path.join(config["eGo"]["csv_import_eTraGo"], "pf_solution.csv"), + index_col=0, + parse_dates=True, + ) + ts_not_converged = convergence[~convergence.converged].index + except FileNotFoundError: + logger.info( + "No info on converged time steps, wherefore it is assumed that all " + "converged." + ) + ts_not_converged = [] + except Exception: + raise + + # set time series data at time steps with non-convergence issues to zero + if len(ts_not_converged) > 0: + logger.info( + f"{len(ts_not_converged)} time steps did not converge in overlying " + f"grid. Time series data at time steps with non-convergence issues is " + f"set to zero." + ) + # set data in TimeSeries object to zero + attributes = edisgo_grid.timeseries._attributes + for attr in attributes: + ts = getattr(edisgo_grid.timeseries, attr) + if not ts.empty: + ts.loc[ts_not_converged, :] = 0 + setattr(edisgo_grid.timeseries, attr, ts) + # set data in OverlyingGrid object to zero + attributes = edisgo_grid.overlying_grid._attributes + for attr in attributes: + ts = getattr(edisgo_grid.overlying_grid, attr) + if not ts.empty and "soc" not in attr: + if isinstance(ts, pd.Series): + ts.loc[ts_not_converged] = 0 + else: + ts.loc[ts_not_converged, :] = 0 + setattr(edisgo_grid.overlying_grid, attr, ts) + + # distribute overlying grid data + logger.info("Distribute overlying grid data.") + edisgo_grid = distribute_overlying_grid_requirements(edisgo_grid) + + # get critical time intervals + results_dir = os.path.join( + config["eGo"]["results_dir"], self._results, str(edisgo_grid.topology.id) + ) + # # ToDo temporary! + # edisgo_grid.timeseries.timeindex = edisgo_grid.timeseries.timeindex[ + # 168:168+200] + time_intervals = get_most_critical_time_intervals( + edisgo_grid, + percentage=1.0, + time_steps_per_time_interval=168, + time_step_day_start=4, + save_steps=True, + path=results_dir, + use_troubleshooting_mode=True, + overloading_factor=0.95, + voltage_deviation_factor=0.95, + ) + + # drop time intervals with non-converging time steps + if len(ts_not_converged) > 0: + + # check overloading time intervals + for ti in time_intervals.index: + # check if there is one time step in time interval that did not converge + non_converged_ts_in_ti = [ + _ + for _ in ts_not_converged + if _ in time_intervals.at[ti, "time_steps_overloading"] + ] + if len(non_converged_ts_in_ti) > 0: + # if any time step did not converge, set time steps to None + time_intervals.at[ti, "time_steps_overloading"] = None + + # check voltage issues time intervals + for ti in time_intervals.index: + # check if there is one time step in time interval that did not converge + non_converged_ts_in_ti = [ + _ + for _ in ts_not_converged + if _ in time_intervals.at[ti, "time_steps_voltage_issues"] + ] + if len(non_converged_ts_in_ti) > 0: + # if any time step did not converge, set time steps to None + time_intervals.at[ti, "time_steps_voltage_issues"] = None + + # select time intervals + if not time_intervals.loc[:, "time_steps_overloading"].dropna().empty: + tmp = time_intervals.loc[:, "time_steps_overloading"].dropna() + time_interval_1 = tmp.iloc[0] + time_interval_1_ind = tmp.index[0] + else: + time_interval_1 = pd.Index([]) + time_interval_1_ind = None + if not time_intervals.loc[:, "time_steps_voltage_issues"].dropna().empty: + tmp = time_intervals.loc[:, "time_steps_voltage_issues"].dropna() + time_interval_2 = tmp.iloc[0] + time_interval_2_ind = tmp.index[0] + else: + time_interval_2 = pd.Index([]) + time_interval_2_ind = None + + # check if time intervals overlap + overlap = [_ for _ in time_interval_1 if _ in time_interval_2] + if len(overlap) > 0: + logger.info( + "Selected time intervals overlap. Trying to find another " + "time interval in voltage_issues intervals." + ) + # check if time interval without overlap can be found + for ti in time_intervals.loc[:, "time_steps_voltage_issues"].dropna().index: + overlap = [ + _ + for _ in time_interval_1 + if _ in time_intervals.at[ti, "time_steps_voltage_issues"] + ] + if len(overlap) == 0: + time_interval_2 = time_intervals.at[ti, "time_steps_voltage_issues"] + time_interval_2_ind = ti + break + overlap = [_ for _ in time_interval_1 if _ in time_interval_2] + if len(overlap) > 0: + logger.info( + "Selected time intervals overlap. Trying to find another " + "time interval in overloading intervals." + ) + # check if time interval without overlap can be found + for ti in time_intervals.loc[:, "time_steps_overloading"].dropna().index: + overlap = [ + _ + for _ in time_interval_2 + if _ in time_intervals.at[ti, "time_steps_overloading"] + ] + if len(overlap) == 0: + time_interval_1 = time_intervals.at[ti, "time_steps_overloading"] + time_interval_1_ind = ti + break + + overlap = [_ for _ in time_interval_1 if _ in time_interval_2] + if len(overlap) > 0: + logger.info( + "Overlap of selected time intervals cannot be avoided. " + "Time intervals are therefore concatenated." + ) + time_interval_1 = ( + time_interval_1.append(time_interval_2).unique().sort_values() + ) + time_interval_2 = None + + # save to csv + percentage = pd.Series() + percentage["time_interval_1"] = ( + None + if time_interval_1_ind is None + else time_intervals.at[ + time_interval_1_ind, "percentage_max_overloaded_components" + ] + ) + percentage["time_interval_2"] = ( + None + if time_interval_2_ind is None + else time_intervals.at[ + time_interval_2_ind, "percentage_buses_max_voltage_deviation" + ] + ) + pd.DataFrame( + { + "time_steps": [time_interval_1, time_interval_2], + "percentage": percentage, + }, + index=["time_interval_1", "time_interval_2"], + ).to_csv(os.path.join(results_dir, "selected_time_intervals.csv")) + + return time_interval_1, time_interval_2 + def _run_edisgo_task_optimisation(self, edisgo_grid, logger): """ Runs the dispatch optimisation. From cfdb7bff69a7cb13d38c912f09ad9e3cfa39333f Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 4 May 2023 20:25:36 +0200 Subject: [PATCH 092/126] Add optimization task --- ego/tools/edisgo_integration.py | 144 ++++++++++++++++++++++++++++++-- 1 file changed, 139 insertions(+), 5 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 0d38399c..0b525850 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -34,6 +34,7 @@ import os import pickle +from copy import deepcopy from datetime import datetime from datetime import timedelta as td from time import localtime, sleep, strftime @@ -56,6 +57,10 @@ from edisgo.tools.temporal_complexity_reduction import ( get_most_critical_time_intervals, ) + from edisgo.tools.tools import ( + aggregate_district_heating_components, + get_sample_using_time, + ) from egoio.db_tables import grid, model_draft from egoio.tools import db from sqlalchemy import func @@ -64,6 +69,7 @@ from ego.tools.economics import edisgo_grid_investment from ego.tools.interface import ETraGoMinimalData, get_etrago_results_per_bus + # Logging logger = logging.getLogger(__name__) @@ -802,6 +808,7 @@ def run_edisgo(self, mv_grid_id): logger = logging.getLogger("edisgo.external.ego._run_edisgo") edisgo_grid = None + time_intervals = None # ################### task: setup grid ################## if "1_setup_grid" in config["eDisGo"]["tasks"]: @@ -882,7 +889,52 @@ def run_edisgo(self, mv_grid_id): return {edisgo_grid.topology.id: results_dir} # ########################## task: optimisation ########################## - # ToDo Maike Call optimisation + if "4_optimisation" in config["eDisGo"]["tasks"]: + if edisgo_grid is None: + grid_path = os.path.join(results_dir, "grid_data_overlying_grid.zip") + edisgo_grid = import_edisgo_from_files( + edisgo_path=grid_path, + import_topology=True, + import_timeseries=True, + import_results=False, + import_electromobility=True, + import_heat_pump=True, + import_dsm=True, + import_overlying_grid=True, + from_zip_archive=True, + ) + edisgo_grid.legacy_grids = False + if time_intervals is None: + time_intervals = pd.read_csv( + os.path.join(results_dir, "selected_time_intervals.csv"), + index_col=0, + ) + for ti in time_intervals.index: + time_steps = time_intervals.at[ti, "time_steps"] + if time_steps is not None: + time_intervals.at[ti, "time_steps"] = pd.date_range( + start=time_steps.split("'")[1], + periods=int(time_steps.split("=")[-2].split(",")[0]), + freq="H", + ) + edisgo_grid = self._run_edisgo_task_optimisation( + edisgo_grid, logger, time_intervals, results_dir + ) + if "5_grid_reinforcement" not in config["eDisGo"]["tasks"]: + edisgo_grid.save( + directory=os.path.join(results_dir, "grid_data_optimisation"), + save_topology=True, + save_timeseries=True, + save_results=False, + save_electromobility=False, + save_dsm=False, + save_heatpump=False, + save_overlying_grid=False, + reduce_memory=True, + archive=True, + archive_type="zip", + ) + return {edisgo_grid.topology.id: results_dir} # ########################## reinforcement ########################## # edisgo_grid.reinforce() @@ -1457,9 +1509,6 @@ def _run_edisgo_task_temporal_complexity_reduction( results_dir = os.path.join( config["eGo"]["results_dir"], self._results, str(edisgo_grid.topology.id) ) - # # ToDo temporary! - # edisgo_grid.timeseries.timeindex = edisgo_grid.timeseries.timeindex[ - # 168:168+200] time_intervals = get_most_critical_time_intervals( edisgo_grid, percentage=1.0, @@ -1588,7 +1637,9 @@ def _run_edisgo_task_temporal_complexity_reduction( return time_interval_1, time_interval_2 - def _run_edisgo_task_optimisation(self, edisgo_grid, logger): + def _run_edisgo_task_optimisation( + self, edisgo_grid, logger, time_intervals, results_dir, reduction_factor=0.3 + ): """ Runs the dispatch optimisation. @@ -1604,6 +1655,89 @@ def _run_edisgo_task_optimisation(self, edisgo_grid, logger): """ logger.info("Start task 'optimisation'.") + + edisgo_grid._config = Config() # ToDo needed? + aggregate_district_heating_components(edisgo_grid) + + timeindex = pd.Index([]) + for ti in time_intervals.index: + time_steps = time_intervals.at[ti, "time_steps"] + if time_steps is None: + continue + else: + timeindex = timeindex.append(pd.Index(time_steps)) + # copy edisgo object + edisgo_copy = deepcopy(edisgo_grid) + # temporal complexity reduction + get_sample_using_time( + edisgo_copy, start_date=time_steps[0], periods=len(time_steps) + ) + + # spatial complexity reduction + edisgo_copy.spatial_complexity_reduction( + mode="kmeansdijkstra", + cluster_area="feeder", + reduction_factor=reduction_factor, + reduction_factor_not_focused=False, + ) + + # OPF + psa_net = edisgo_copy.to_pypsa() + flexible_cps = psa_net.loads.loc[ + psa_net.loads.index.str.contains("home") + | (psa_net.loads.index.str.contains("work")) + ].index.values + flexible_hps = edisgo_copy.heat_pump.heat_demand_df.columns.values + flexible_loads = edisgo_copy.dsm.p_max.columns + flexible_storage_units = ( + edisgo_copy.topology.storage_units_df.index.values + ) + + edisgo_copy.pm_optimize( + flexible_cps=flexible_cps, + flexible_hps=flexible_hps, + flexible_loads=flexible_loads, + flexible_storage_units=flexible_storage_units, + s_base=1, + opf_version=4, + silence_moi=False, + method="soc", + ) + + # save OPF results + edisgo_copy.save( + directory=os.path.join(results_dir, f"opf_results_{ti}"), + save_topology=True, + save_timeseries=False, + save_results=False, + save_opf_results=True, + reduce_memory=True, + archive=True, + archive_type="zip", + ) + + # write flexibility dispatch results to spatially unreduced edisgo + # object + edisgo_grid.timeseries._loads_active_power.loc[ + time_steps, : + ] = edisgo_copy.timeseries.loads_active_power + edisgo_grid.timeseries._loads_reactive_power.loc[ + time_steps, : + ] = edisgo_copy.timeseries.loads_reactive_power + edisgo_grid.timeseries._generators_active_power.loc[ + time_steps, : + ] = edisgo_copy.timeseries.generators_active_power + edisgo_grid.timeseries._generators_reactive_power.loc[ + time_steps, : + ] = edisgo_copy.timeseries.generators_reactive_power + edisgo_grid.timeseries._storage_units_active_power.loc[ + time_steps, : + ] = edisgo_copy.timeseries.storage_units_active_power + edisgo_grid.timeseries._storage_units_reactive_power.loc[ + time_steps, : + ] = edisgo_copy.timeseries.storage_units_reactive_power + + edisgo_grid.timeseries.timeindex = timeindex return edisgo_grid def _run_edisgo_task_grid_reinforcement(self, edisgo_grid, logger): From fb39d3d36ca9cf8319efb357538468a15c30cd22 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 5 May 2023 13:02:55 +0200 Subject: [PATCH 093/126] Always save grid data --- ego/tools/edisgo_integration.py | 78 ++++++++++++++++----------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 0b525850..96f814c1 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -815,20 +815,20 @@ def run_edisgo(self, mv_grid_id): edisgo_grid = self._run_edisgo_task_setup_grid( mv_grid_id, scenario, logger, config, engine ) + edisgo_grid.save( + directory=os.path.join(results_dir, "grid_data"), + save_topology=True, + save_timeseries=True, + save_results=False, + save_electromobility=True, + save_dsm=True, + save_heatpump=True, + save_overlying_grid=False, + reduce_memory=True, + archive=True, + archive_type="zip", + ) if "2_specs_overlying_grid" not in config["eDisGo"]["tasks"]: - edisgo_grid.save( - directory=os.path.join(results_dir, "grid_data"), - save_topology=True, - save_timeseries=True, - save_results=False, - save_electromobility=True, - save_dsm=True, - save_heatpump=True, - save_overlying_grid=False, - reduce_memory=True, - archive=True, - archive_type="zip", - ) return {edisgo_grid.topology.id: results_dir} # ################### task: specs overlying grid ################## @@ -850,20 +850,20 @@ def run_edisgo(self, mv_grid_id): edisgo_grid = self._run_edisgo_task_specs_overlying_grid( edisgo_grid, scenario, logger, config, engine ) + edisgo_grid.save( + directory=os.path.join(results_dir, "grid_data_overlying_grid"), + save_topology=True, + save_timeseries=True, + save_results=False, + save_electromobility=True, + save_dsm=True, + save_heatpump=True, + save_overlying_grid=True, + reduce_memory=True, + archive=True, + archive_type="zip", + ) if "3_temporal_complexity_reduction" not in config["eDisGo"]["tasks"]: - edisgo_grid.save( - directory=os.path.join(results_dir, "grid_data_overlying_grid"), - save_topology=True, - save_timeseries=True, - save_results=False, - save_electromobility=True, - save_dsm=True, - save_heatpump=True, - save_overlying_grid=True, - reduce_memory=True, - archive=True, - archive_type="zip", - ) return {edisgo_grid.topology.id: results_dir} # ################### task: temporal complexity reduction ################## @@ -920,20 +920,20 @@ def run_edisgo(self, mv_grid_id): edisgo_grid = self._run_edisgo_task_optimisation( edisgo_grid, logger, time_intervals, results_dir ) + edisgo_grid.save( + directory=os.path.join(results_dir, "grid_data_optimisation"), + save_topology=True, + save_timeseries=True, + save_results=False, + save_electromobility=False, + save_dsm=False, + save_heatpump=False, + save_overlying_grid=False, + reduce_memory=True, + archive=True, + archive_type="zip", + ) if "5_grid_reinforcement" not in config["eDisGo"]["tasks"]: - edisgo_grid.save( - directory=os.path.join(results_dir, "grid_data_optimisation"), - save_topology=True, - save_timeseries=True, - save_results=False, - save_electromobility=False, - save_dsm=False, - save_heatpump=False, - save_overlying_grid=False, - reduce_memory=True, - archive=True, - archive_type="zip", - ) return {edisgo_grid.topology.id: results_dir} # ########################## reinforcement ########################## From 477dfc9ec3904b3db4e46c9f3a1e6464166ea6f3 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 5 May 2023 13:03:26 +0200 Subject: [PATCH 094/126] Bug fix initialise storage time series --- ego/tools/edisgo_integration.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 96f814c1..2d1e2c2d 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1656,7 +1656,20 @@ def _run_edisgo_task_optimisation( """ logger.info("Start task 'optimisation'.") - edisgo_grid._config = Config() # ToDo needed? + # initialise storage time series in case they don't yet exist + try: + edisgo_grid.timeseries._storage_units_active_power + except AttributeError: + edisgo_grid.timeseries.storage_units_active_power = pd.DataFrame( + index=edisgo_grid.timeseries.timeindex + ) + try: + edisgo_grid.timeseries._storage_units_reactive_power + except AttributeError: + edisgo_grid.timeseries.storage_units_active_power = pd.DataFrame( + index=edisgo_grid.timeseries.timeindex + ) + aggregate_district_heating_components(edisgo_grid) timeindex = pd.Index([]) From f831d761d7dd9d712dbe3622c23694155eac7f79 Mon Sep 17 00:00:00 2001 From: birgits Date: Sat, 6 May 2023 12:59:54 +0200 Subject: [PATCH 095/126] Bug fix set time series for all time steps --- ego/tools/edisgo_integration.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 2d1e2c2d..0464e874 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1094,8 +1094,9 @@ def _run_edisgo_task_specs_overlying_grid( self._pf_post_lopf, self._max_cos_phi_renewable, ) + snapshots = specs["timeindex"] - # exclude non-converging time steps + # get time steps that don't converge in overlying grid try: convergence = pd.read_csv( os.path.join(config["eGo"]["csv_import_eTraGo"], "pf_solution.csv"), @@ -1103,13 +1104,12 @@ def _run_edisgo_task_specs_overlying_grid( parse_dates=True, ) ts_not_converged = convergence[~convergence.converged].index - snapshots = specs["timeindex"].drop(ts_not_converged) except FileNotFoundError: logger.info( "No info on converged time steps, wherefore it is assumed that all " "converged." ) - snapshots = specs["timeindex"] + ts_not_converged = pd.Index([]) except Exception: raise @@ -1360,16 +1360,19 @@ def _run_edisgo_task_specs_overlying_grid( edisgo_grid.topology.generators_df["type"] == carrier ].p_nom.sum() specs["renewables_curtailment"][carrier] *= p_nom_mv_lv / p_nom_total - # check that curtailment does not exceed feed-in + # check that curtailment does not exceed feed-in (for all converged time steps) vres_gens = edisgo_grid.topology.generators_df[ edisgo_grid.topology.generators_df["type"].isin( specs["renewables_curtailment"].columns ) ].index + ts_converged = snapshots.drop(ts_not_converged) pot_vres_gens = edisgo_grid.timeseries.generators_active_power.loc[ - :, vres_gens + ts_converged, vres_gens ].sum(axis=1) - total_curtailment = specs["renewables_curtailment"].loc[snapshots].sum(axis=1) + total_curtailment = ( + specs["renewables_curtailment"].loc[ts_converged].sum(axis=1) + ) diff = pot_vres_gens - total_curtailment if (diff < 0).any(): # if curtailment is much larger than feed-in, throw an error From baa25d044536a9c2ec64ecb9c28483dee650e8c9 Mon Sep 17 00:00:00 2001 From: birgits Date: Sat, 6 May 2023 13:16:26 +0200 Subject: [PATCH 096/126] Bug fix --- ego/tools/edisgo_integration.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 0464e874..093f99e0 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1366,13 +1366,12 @@ def _run_edisgo_task_specs_overlying_grid( specs["renewables_curtailment"].columns ) ].index - ts_converged = snapshots.drop(ts_not_converged) pot_vres_gens = edisgo_grid.timeseries.generators_active_power.loc[ - ts_converged, vres_gens + :, vres_gens ].sum(axis=1) - total_curtailment = ( - specs["renewables_curtailment"].loc[ts_converged].sum(axis=1) - ) + pot_vres_gens.loc[ts_not_converged] = 0.0 + total_curtailment = specs["renewables_curtailment"].loc[:].sum(axis=1) + total_curtailment.loc[ts_not_converged] = 0.0 diff = pot_vres_gens - total_curtailment if (diff < 0).any(): # if curtailment is much larger than feed-in, throw an error From 3b89ea37620efda49789f4330467fdeb64749ca4 Mon Sep 17 00:00:00 2001 From: birgits Date: Sat, 6 May 2023 17:50:05 +0200 Subject: [PATCH 097/126] Fix setting storage time series --- ego/tools/edisgo_integration.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 093f99e0..587a6fc4 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1658,20 +1658,6 @@ def _run_edisgo_task_optimisation( """ logger.info("Start task 'optimisation'.") - # initialise storage time series in case they don't yet exist - try: - edisgo_grid.timeseries._storage_units_active_power - except AttributeError: - edisgo_grid.timeseries.storage_units_active_power = pd.DataFrame( - index=edisgo_grid.timeseries.timeindex - ) - try: - edisgo_grid.timeseries._storage_units_reactive_power - except AttributeError: - edisgo_grid.timeseries.storage_units_active_power = pd.DataFrame( - index=edisgo_grid.timeseries.timeindex - ) - aggregate_district_heating_components(edisgo_grid) timeindex = pd.Index([]) @@ -1745,11 +1731,26 @@ def _run_edisgo_task_optimisation( edisgo_grid.timeseries._generators_reactive_power.loc[ time_steps, : ] = edisgo_copy.timeseries.generators_reactive_power + + try: + edisgo_grid.timeseries._storage_units_active_power + except AttributeError: + edisgo_grid.timeseries.storage_units_active_power = pd.DataFrame( + index=edisgo_grid.timeseries.timeindex + ) edisgo_grid.timeseries._storage_units_active_power.loc[ - time_steps, : + time_steps, + edisgo_copy.timeseries.storage_units_active_power.columns, ] = edisgo_copy.timeseries.storage_units_active_power + try: + edisgo_grid.timeseries._storage_units_reactive_power + except AttributeError: + edisgo_grid.timeseries.storage_units_reactive_power = pd.DataFrame( + index=edisgo_grid.timeseries.timeindex + ) edisgo_grid.timeseries._storage_units_reactive_power.loc[ - time_steps, : + time_steps, + edisgo_copy.timeseries.storage_units_reactive_power.columns, ] = edisgo_copy.timeseries.storage_units_reactive_power edisgo_grid.timeseries.timeindex = timeindex From 27fbad6145c72d4dfce160a6df1db3766612261c Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 9 May 2023 20:04:08 +0200 Subject: [PATCH 098/126] Adapt to changes in edisgo --- ego/tools/edisgo_integration.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 587a6fc4..5041dcf7 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1658,7 +1658,15 @@ def _run_edisgo_task_optimisation( """ logger.info("Start task 'optimisation'.") - aggregate_district_heating_components(edisgo_grid) + # aggregate PtH units in same district heating network and subtract feed-in + # from other heat sources from heat demand in district heating network + aggregate_district_heating_components( + edisgo_grid, + feedin_district_heating=edisgo_grid.overlying_grid.feedin_district_heating, + ) + # apply operating strategy so that inflexible heat pumps (without heat + # storage units) have a time series + edisgo_grid.apply_heat_pump_operating_strategy() timeindex = pd.Index([]) for ti in time_intervals.index: From 3feb24cb331c723ba932e7b2f90a1a55bd7d78dd Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 10 May 2023 08:32:56 +0200 Subject: [PATCH 099/126] Move carrier renaming to function --- ego/tools/edisgo_integration.py | 26 +++++++------------------- ego/tools/interface.py | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 5041dcf7..6c209a9b 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -67,7 +67,11 @@ from ego.mv_clustering import cluster_workflow, database from ego.tools.economics import edisgo_grid_investment - from ego.tools.interface import ETraGoMinimalData, get_etrago_results_per_bus + from ego.tools.interface import ( + ETraGoMinimalData, + get_etrago_results_per_bus, + rename_generator_carriers_edisgo, + ) # Logging @@ -1146,25 +1150,9 @@ def _run_edisgo_task_specs_overlying_grid( edisgo_grid.timeseries.timeindex = snapshots logger.info("Set generator time series.") - # active power # rename carrier to match with carrier names in overlying grid - generators_df = edisgo_grid.topology.generators_df - if "p_nom_th" in generators_df.columns: - gens_rename = generators_df[ - (generators_df["type"].isin(["gas", "gas extended", "oil", "others"])) - & (~generators_df["p_nom_th"].isna()) - ] - generators_df.loc[gens_rename.index, "type"] = "gas_CHP" - gens_rename = generators_df[ - (generators_df["type"].isin(["biomass"])) - & (~generators_df["p_nom_th"].isna()) - ] - generators_df.loc[gens_rename.index, "type"] = "biomass_CHP" - gens_rename = generators_df[generators_df["type"].isin(["water"])] - generators_df.loc[gens_rename.index, "type"] = "run_of_river" - gens_rename = generators_df[generators_df["type"].isin(["conventional"])] - generators_df.loc[gens_rename.index, "type"] = "others" - + rename_generator_carriers_edisgo(edisgo_grid) + # active power edisgo_grid.set_time_series_active_power_predefined( dispatchable_generators_ts=specs["dispatchable_generators_active_power"], fluctuating_generators_ts=specs["renewables_potential"], diff --git a/ego/tools/interface.py b/ego/tools/interface.py index 15a14a58..81ffb2d5 100644 --- a/ego/tools/interface.py +++ b/ego/tools/interface.py @@ -742,3 +742,26 @@ def bev_charger(): logger.info(f"Overall time: {time.perf_counter() - t_start}") return results + + +def rename_generator_carriers_edisgo(edisgo_grid): + """ + Helper function to rename carriers so that they match carrier names in eTraGo. + + """ + generators_df = edisgo_grid.topology.generators_df + if "p_nom_th" in generators_df.columns: + gens_rename = generators_df[ + (generators_df["type"].isin(["gas", "gas extended", "oil", "others"])) + & (~generators_df["p_nom_th"].isna()) + ] + generators_df.loc[gens_rename.index, "type"] = "gas_CHP" + gens_rename = generators_df[ + (generators_df["type"].isin(["biomass"])) + & (~generators_df["p_nom_th"].isna()) + ] + generators_df.loc[gens_rename.index, "type"] = "biomass_CHP" + gens_rename = generators_df[generators_df["type"].isin(["water"])] + generators_df.loc[gens_rename.index, "type"] = "run_of_river" + gens_rename = generators_df[generators_df["type"].isin(["conventional"])] + generators_df.loc[gens_rename.index, "type"] = "others" From e08520d08420eb2c5c51812cb247df32ea85a0d2 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 10 May 2023 08:58:01 +0200 Subject: [PATCH 100/126] Add grid reinforcement task --- ego/tools/edisgo_integration.py | 249 +++++++++++++++++++++++++++++++- 1 file changed, 246 insertions(+), 3 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 6c209a9b..a6cc3b4a 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -44,12 +44,14 @@ import dill import multiprocess as mp2 +import numpy as np import pandas as pd from sqlalchemy.orm import scoped_session, sessionmaker if "READTHEDOCS" not in os.environ: from edisgo.edisgo import import_edisgo_from_files + from edisgo.flex_opt.reinforce_grid import enhanced_reinforce_grid from edisgo.network.overlying_grid import distribute_overlying_grid_requirements from edisgo.tools.config import Config from edisgo.tools.logger import setup_logger @@ -941,7 +943,52 @@ def run_edisgo(self, mv_grid_id): return {edisgo_grid.topology.id: results_dir} # ########################## reinforcement ########################## - # edisgo_grid.reinforce() + if "5_grid_reinforcement" in config["eDisGo"]["tasks"]: + if edisgo_grid is None: + grid_path = os.path.join(results_dir, "grid_data_optimisation.zip") + edisgo_grid = import_edisgo_from_files( + edisgo_path=grid_path, + import_topology=True, + import_timeseries=True, + import_results=False, + import_electromobility=False, + import_heat_pump=False, + import_dsm=False, + import_overlying_grid=False, + from_zip_archive=True, + ) + edisgo_grid.legacy_grids = False + if time_intervals is None: + time_intervals = pd.read_csv( + os.path.join(results_dir, "selected_time_intervals.csv"), + index_col=0, + ) + for ti in time_intervals.index: + time_steps = time_intervals.at[ti, "time_steps"] + if time_steps is not None: + time_intervals.at[ti, "time_steps"] = pd.date_range( + start=time_steps.split("'")[1], + periods=int(time_steps.split("=")[-2].split(",")[0]), + freq="H", + ) + edisgo_grid = self._run_edisgo_task_grid_reinforcement( + edisgo_grid, logger, time_intervals + ) + edisgo_grid.save( + directory=os.path.join( + results_dir, f"grid_data_reinforcement_{scenario}" + ), + save_topology=True, + save_timeseries=True, + save_results=True, + save_electromobility=False, + save_dsm=False, + save_heatpump=False, + save_overlying_grid=False, + reduce_memory=True, + archive=True, + archive_type="zip", + ) # ########################## save results ########################## self._status_update(mv_grid_id, "end") @@ -1752,9 +1799,189 @@ def _run_edisgo_task_optimisation( edisgo_grid.timeseries.timeindex = timeindex return edisgo_grid - def _run_edisgo_task_grid_reinforcement(self, edisgo_grid, logger): + def _run_edisgo_task_specs_overlying_grid_low_flex( + self, edisgo_grid, scenario, logger, config, engine + ): """ - Runs the dispatch optimisation. + Sets up grid for low-flex scenario variation. + + Parameters + ---------- + mv_grid_id : int + MV grid ID of the ding0 grid + + Returns + ------- + :class:`edisgo.EDisGo` + Returns the complete eDisGo container, also including results + + """ + logger.info("Start task 'specs_overlying_grid_low_flex'.") + + logger.info("Get specifications from eTraGo.") + specs = get_etrago_results_per_bus( + edisgo_grid.topology.id, + self._etrago_network, + self._pf_post_lopf, + self._max_cos_phi_renewable, + ) + snapshots = specs["timeindex"] + + # overwrite previously set dummy time index if year that was used differs from + # year used in etrago + edisgo_year = edisgo_grid.timeseries.timeindex[0].year + etrago_year = snapshots[0].year + if edisgo_year != etrago_year: + timeindex_new_full = pd.date_range( + f"1/1/{etrago_year}", periods=8760, freq="H" + ) + # conventional loads + edisgo_grid.timeseries.loads_active_power.index = timeindex_new_full + edisgo_grid.timeseries.loads_reactive_power.index = timeindex_new_full + # COP and heat demand + edisgo_grid.heat_pump.cop_df.index = timeindex_new_full + edisgo_grid.heat_pump.heat_demand_df.index = timeindex_new_full + # TimeSeries.timeindex + edisgo_grid.timeseries.timeindex = snapshots + + # set generator time series + # rename carrier to match with carrier names in overlying grid + rename_generator_carriers_edisgo(edisgo_grid) + # active power + # ToDo Abregelung? + edisgo_grid.set_time_series_active_power_predefined( + dispatchable_generators_ts=specs["dispatchable_generators_active_power"], + fluctuating_generators_ts=specs["renewables_potential"], + ) + # reactive power + edisgo_grid.set_time_series_reactive_power_control( + control="fixed_cosphi", + generators_parametrisation="default", + loads_parametrisation=None, + storage_units_parametrisation=None, + ) + + # set heat pump time series + logger.info("Set heat pump time series.") + + # individual heat pumps - uncontrolled operation + hp_decentral = edisgo_grid.topology.loads_df[ + edisgo_grid.topology.loads_df.sector == "individual_heating" + ] + if not hp_decentral.empty: + edisgo_grid.apply_heat_pump_operating_strategy( + heat_pump_names=hp_decentral.index + ) + # district heating heat pumps from overlying grid + hp_dh = edisgo_grid.topology.loads_df[ + edisgo_grid.topology.loads_df.sector.isin( + ["district_heating", "district_heating_resistive_heater"] + ) + ] + if len(hp_dh) > 0: + if specs["feedin_district_heating"].empty: + raise ValueError( + "There are PtH units in district heating but no time series " + "from overlying grid." + ) + + # map district heating ID to heat bus ID from eTraGo + orm = database.register_tables_in_saio(engine, config=config) + heat_buses = [int(_) for _ in specs["feedin_district_heating"].columns] + with database.session_scope(engine) as session: + # get srid of etrago_bus table + query = session.query(func.ST_SRID(orm["etrago_bus"].geom)).limit(1) + srid_etrago_bus = query.all()[0] + # get district heating ID corresponding to heat bus ID by geo join + query = ( + session.query( + orm["etrago_bus"].bus_id.label("heat_bus_id"), + orm["district_heating_areas"].id.label("district_heating_id"), + ) + .filter( + orm["etrago_bus"].scn_name == scenario, + orm["district_heating_areas"].scenario == scenario, + orm["etrago_bus"].bus_id.in_(heat_buses), + ) + .outerjoin( # join to obtain district heating ID + orm["district_heating_areas"], + func.ST_Transform( + func.ST_Centroid( + orm["district_heating_areas"].geom_polygon + ), + srid_etrago_bus, + ) + == orm["etrago_bus"].geom, + ) + ) + mapping_heat_bus_dh_id = pd.read_sql( + query.statement, + engine, + index_col="district_heating_id", + ) + hp_dh = pd.merge( + hp_dh, + mapping_heat_bus_dh_id, + left_on="district_heating_id", + right_index=True, + ) + + for heat_bus in heat_buses: + if str(heat_bus) in specs["thermal_storage_central_capacity"].index: + if specs["thermal_storage_central_capacity"].at[str(heat_bus)] > 0: + # get PtH unit name to allocate thermal storage unit to + comp_name = hp_dh[hp_dh.heat_bus_id == heat_bus].index[0] + edisgo_grid.heat_pump.thermal_storage_units_df = pd.concat( + [ + edisgo_grid.heat_pump.thermal_storage_units_df, + pd.DataFrame( + data={ + "capacity": specs[ + "thermal_storage_central_capacity" + ].at[str(heat_bus)], + "efficiency": specs[ + "thermal_storage_central_efficiency" + ], + }, + index=[comp_name], + ), + ] + ) + specs["feedin_district_heating"].rename( + columns={ + str(heat_bus): hp_dh.at[ + comp_name, "district_heating_id" + ] + }, + inplace=True, + ) + # reactive power + edisgo_grid.set_time_series_reactive_power_control( + control="fixed_cosphi", + generators_parametrisation=None, + loads_parametrisation="default", + storage_units_parametrisation=None, + ) + + # delete storage units + logger.info("Delete battery storage units.") + for stor in edisgo_grid.topology.storage_units_df.index: + edisgo_grid.remove_component( + comp_type="storage_unit", comp_name=stor, drop_ts=False + ) + # delete DSM and flexibility bands + edisgo_grid.dsm = edisgo_grid.dsm.__class__() + edisgo_grid.electromobility.flexibility_bands = { + "upper_power": pd.DataFrame(), + "lower_energy": pd.DataFrame(), + "upper_energy": pd.DataFrame(), + } + + return edisgo_grid + + def _run_edisgo_task_grid_reinforcement(self, edisgo_grid, logger, time_intervals): + """ + Runs the grid reinforcement. Parameters ---------- @@ -1768,6 +1995,22 @@ def _run_edisgo_task_grid_reinforcement(self, edisgo_grid, logger): """ logger.info("Start task 'grid_reinforcement'.") + + # overwrite configs with new configs + edisgo_grid._config = Config() + + # set timeindex to given time_intervals + timesteps = np.concatenate( + [cols.time_steps for _, cols in time_intervals.iterrows()] + ) + edisgo_grid.timeseries.timeindex = pd.Index(timesteps) + + enhanced_reinforce_grid( + edisgo_grid, + activate_cost_results_disturbing_mode=True, + separate_lv_grids=True, + separation_threshold=2, + ) return edisgo_grid def _save_edisgo_results(self): From 86454c6631a07a053d55fc22ca8b8404a4276d39 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 10 May 2023 09:08:35 +0200 Subject: [PATCH 101/126] Use same setup function for full and low flex scenario --- ego/tools/edisgo_integration.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index a6cc3b4a..f390e5e7 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -818,8 +818,15 @@ def run_edisgo(self, mv_grid_id): # ################### task: setup grid ################## if "1_setup_grid" in config["eDisGo"]["tasks"]: + # data is always imported for the full flex scenario, wherefore in case + # a low-flex scenario is given, the lowflex-extension is dropped for the + # data import + if scenario.split("_")[-1] == "lowflex": + scn = scenario.split("_")[0] + else: + scn = scenario edisgo_grid = self._run_edisgo_task_setup_grid( - mv_grid_id, scenario, logger, config, engine + mv_grid_id, scn, logger, config, engine ) edisgo_grid.save( directory=os.path.join(results_dir, "grid_data"), From 55223003fb00d013cd9f45ba3dc34dbfb1ccde86 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 10 May 2023 09:35:44 +0200 Subject: [PATCH 102/126] Adapt edisgo workflow for low flex scenario --- ego/tools/edisgo_integration.py | 92 ++++++++++++++++++++------------- 1 file changed, 57 insertions(+), 35 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index f390e5e7..8e2e9c4b 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -860,11 +860,19 @@ def run_edisgo(self, mv_grid_id): from_zip_archive=True, ) edisgo_grid.legacy_grids = False - edisgo_grid = self._run_edisgo_task_specs_overlying_grid( - edisgo_grid, scenario, logger, config, engine - ) + # function that is called depends on scenario + zip_name = "grid_data_overlying_grid" + if scenario in ["eGon2035", "eGon100RE"]: + edisgo_grid = self._run_edisgo_task_specs_overlying_grid( + edisgo_grid, scenario, logger, config, engine + ) + else: + edisgo_grid = self._run_edisgo_task_specs_overlying_grid_low_flex( + edisgo_grid, scenario, logger, config, engine + ) + zip_name += "_lowflex" edisgo_grid.save( - directory=os.path.join(results_dir, "grid_data_overlying_grid"), + directory=os.path.join(results_dir, zip_name), save_topology=True, save_timeseries=True, save_results=False, @@ -876,13 +884,16 @@ def run_edisgo(self, mv_grid_id): archive=True, archive_type="zip", ) - if "3_temporal_complexity_reduction" not in config["eDisGo"]["tasks"]: - return {edisgo_grid.topology.id: results_dir} # ################### task: temporal complexity reduction ################## + # task temporal complexity reduction is optional if "3_temporal_complexity_reduction" in config["eDisGo"]["tasks"]: if edisgo_grid is None: - grid_path = os.path.join(results_dir, "grid_data_overlying_grid.zip") + if scenario in ["eGon2035", "eGon100RE"]: + zip_name = "grid_data_overlying_grid.zip" + else: + zip_name = "grid_data_overlying_grid_lowflex.zip" + grid_path = os.path.join(results_dir, zip_name) edisgo_grid = import_edisgo_from_files( edisgo_path=grid_path, import_topology=True, @@ -898,8 +909,41 @@ def run_edisgo(self, mv_grid_id): time_intervals = self._run_edisgo_task_temporal_complexity_reduction( edisgo_grid, logger, config ) - if "4_optimisation" not in config["eDisGo"]["tasks"]: + + # determine whether work flow ends here or continues, and if it continues + # whether time intervals need to be loaded + # for full flex scenarios the optimisation would be the next step + if scenario in ["eGon2035", "eGon100RE"]: + if "4_optimisation" in config["eDisGo"]["tasks"]: + if time_intervals is None: + load_time_intervals = True + else: + load_time_intervals = False + else: return {edisgo_grid.topology.id: results_dir} + # for low flex scenarios the grid reinforcement would be the next step + else: + if "5_grid_reinforcement" in config["eDisGo"]["tasks"]: + if time_intervals is None: + load_time_intervals = True + else: + load_time_intervals = False + else: + return {edisgo_grid.topology.id: results_dir} + if load_time_intervals is True: + # load time intervals + time_intervals = pd.read_csv( + os.path.join(results_dir, "selected_time_intervals.csv"), + index_col=0, + ) + for ti in time_intervals.index: + time_steps = time_intervals.at[ti, "time_steps"] + if time_steps is not None: + time_intervals.at[ti, "time_steps"] = pd.date_range( + start=time_steps.split("'")[1], + periods=int(time_steps.split("=")[-2].split(",")[0]), + freq="H", + ) # ########################## task: optimisation ########################## if "4_optimisation" in config["eDisGo"]["tasks"]: @@ -917,19 +961,6 @@ def run_edisgo(self, mv_grid_id): from_zip_archive=True, ) edisgo_grid.legacy_grids = False - if time_intervals is None: - time_intervals = pd.read_csv( - os.path.join(results_dir, "selected_time_intervals.csv"), - index_col=0, - ) - for ti in time_intervals.index: - time_steps = time_intervals.at[ti, "time_steps"] - if time_steps is not None: - time_intervals.at[ti, "time_steps"] = pd.date_range( - start=time_steps.split("'")[1], - periods=int(time_steps.split("=")[-2].split(",")[0]), - freq="H", - ) edisgo_grid = self._run_edisgo_task_optimisation( edisgo_grid, logger, time_intervals, results_dir ) @@ -952,7 +983,11 @@ def run_edisgo(self, mv_grid_id): # ########################## reinforcement ########################## if "5_grid_reinforcement" in config["eDisGo"]["tasks"]: if edisgo_grid is None: - grid_path = os.path.join(results_dir, "grid_data_optimisation.zip") + if scenario in ["eGon2035", "eGon100RE"]: + zip_name = "grid_data_optimisation.zip" + else: + zip_name = "grid_data_overlying_grid_lowflex.zip" + grid_path = os.path.join(results_dir, zip_name) edisgo_grid = import_edisgo_from_files( edisgo_path=grid_path, import_topology=True, @@ -965,19 +1000,6 @@ def run_edisgo(self, mv_grid_id): from_zip_archive=True, ) edisgo_grid.legacy_grids = False - if time_intervals is None: - time_intervals = pd.read_csv( - os.path.join(results_dir, "selected_time_intervals.csv"), - index_col=0, - ) - for ti in time_intervals.index: - time_steps = time_intervals.at[ti, "time_steps"] - if time_steps is not None: - time_intervals.at[ti, "time_steps"] = pd.date_range( - start=time_steps.split("'")[1], - periods=int(time_steps.split("=")[-2].split(",")[0]), - freq="H", - ) edisgo_grid = self._run_edisgo_task_grid_reinforcement( edisgo_grid, logger, time_intervals ) From d64b0b451681d646ab42d34a20221af9cdd843a8 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 10 May 2023 11:19:11 +0200 Subject: [PATCH 103/126] Adapt interface for low flex scenario --- ego/tools/interface.py | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/ego/tools/interface.py b/ego/tools/interface.py index 81ffb2d5..2e0999d6 100644 --- a/ego/tools/interface.py +++ b/ego/tools/interface.py @@ -521,6 +521,7 @@ def storages(): results["storage_units_soc"] = storages_df_soc def dsm(): + # not needed in eDisGo in low flex scenario (dsm_df will be empty in that case) # DSM dsm_df = links_df.loc[ (links_df["carrier"] == "dsm") & (links_df["bus0"] == str(bus_id)) @@ -644,7 +645,7 @@ def central_heat(): results["feedin_district_heating"] = dh_feedin_df def rural_heat(): - + # not needed in eDisGo in low flex scenario, but obtained anyway # ToDo (low priority) add resistive heaters (they only exist in eGon100RE) rural_heat_carriers = ["rural_heat_pump"] rural_heat_df = links_df.loc[ @@ -663,19 +664,27 @@ def rural_heat(): rural_heat_store_link_df = etrago_obj.links.loc[ etrago_obj.links["bus0"] == rural_heat_bus ] - rural_heat_store_df = etrago_obj.stores.loc[ - (etrago_obj.stores["carrier"] == "rural_heat_store") - & (etrago_obj.stores["bus"] == rural_heat_store_link_df.bus1.values[0]) - ] - rural_heat_store_capacity = rural_heat_store_df.e_nom_opt.values[0] - # efficiency - heat_store_efficiency = rural_heat_store_link_df.efficiency.values[0] - # SoC - if rural_heat_store_capacity > 0: - soc_ts = etrago_obj.stores_t["e"][rural_heat_store_df.index[0]] - soc_ts = soc_ts / rural_heat_store_capacity - else: + if rural_heat_store_link_df.empty: + rural_heat_store_capacity = 0 + heat_store_efficiency = 0 soc_ts = pd.Series(0.0, index=timeseries_index) + else: + rural_heat_store_df = etrago_obj.stores.loc[ + (etrago_obj.stores["carrier"] == "rural_heat_store") + & ( + etrago_obj.stores["bus"] + == rural_heat_store_link_df.bus1.values[0] + ) + ] + rural_heat_store_capacity = rural_heat_store_df.e_nom_opt.values[0] + # efficiency + heat_store_efficiency = rural_heat_store_link_df.efficiency.values[0] + # SoC + if rural_heat_store_capacity > 0: + soc_ts = etrago_obj.stores_t["e"][rural_heat_store_df.index[0]] + soc_ts = soc_ts / rural_heat_store_capacity + else: + soc_ts = pd.Series(0.0, index=timeseries_index) else: rural_heat_df_p = pd.Series(0.0, index=timeseries_index) rural_heat_df_q = pd.Series(0.0, index=timeseries_index) @@ -692,6 +701,8 @@ def rural_heat(): results["thermal_storage_rural_soc"] = soc_ts def bev_charger(): + # not needed in eDisGo in low flex scenario (bev_charger_df will be empty in + # that case) # BEV charger bev_charger_df = links_df.loc[ (links_df["carrier"] == "BEV_charger") & (links_df["bus0"] == str(bus_id)) From 0e0361549fb998c078680173e58c29309722b958 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 10 May 2023 14:22:31 +0200 Subject: [PATCH 104/126] Bug fix in case no storage was build --- ego/tools/interface.py | 61 +++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/ego/tools/interface.py b/ego/tools/interface.py index 2e0999d6..e679c655 100644 --- a/ego/tools/interface.py +++ b/ego/tools/interface.py @@ -556,35 +556,42 @@ def central_heat(): central_heat_store_links_df = etrago_obj.links.loc[ etrago_obj.links["bus0"].isin(central_heat_buses) ] - central_heat_store_df = etrago_obj.stores.loc[ - (etrago_obj.stores["carrier"] == "central_heat_store") - & ( - etrago_obj.stores["bus"].isin( - central_heat_store_links_df.bus1.values + if central_heat_store_links_df.empty: + central_heat_store_capacity = pd.Series() + central_heat_store_efficiency = 0 + soc_ts = pd.DataFrame() + else: + central_heat_store_df = etrago_obj.stores.loc[ + (etrago_obj.stores["carrier"] == "central_heat_store") + & ( + etrago_obj.stores["bus"].isin( + central_heat_store_links_df.bus1.values + ) ) + ].reset_index(names="store_name") + central_heat_store_merge_links_df = pd.merge( + central_heat_store_links_df, + central_heat_store_df, + left_on="bus1", + right_on="bus", ) - ].reset_index(names="store_name") - central_heat_store_merge_links_df = pd.merge( - central_heat_store_links_df, - central_heat_store_df, - left_on="bus1", - right_on="bus", - ) - # capacity - central_heat_store_capacity = central_heat_store_merge_links_df.set_index( - "bus0" - ).e_nom_opt - # efficiency - central_heat_store_efficiency = ( - central_heat_store_links_df.efficiency.values[0] - ) - # SoC - soc_ts = etrago_obj.stores_t["e"][ - central_heat_store_df.store_name.values - ].rename( - columns=central_heat_store_merge_links_df.set_index("store_name").bus0 - ) - soc_ts = soc_ts / central_heat_store_capacity + # capacity + central_heat_store_capacity = ( + central_heat_store_merge_links_df.set_index("bus0").e_nom_opt + ) + # efficiency + central_heat_store_efficiency = ( + central_heat_store_links_df.efficiency.values[0] + ) + # SoC + soc_ts = etrago_obj.stores_t["e"][ + central_heat_store_df.store_name.values + ].rename( + columns=central_heat_store_merge_links_df.set_index( + "store_name" + ).bus0 + ) + soc_ts = soc_ts / central_heat_store_capacity # Other feed-in dh_feedin_df = pd.DataFrame() From 7259b9bc5b1f3bdd52f12027047304634065e357 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 10 May 2023 14:23:11 +0200 Subject: [PATCH 105/126] Move mapping of etrago heat bus ID to district heating ID to separate function --- ego/tools/interface.py | 62 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/ego/tools/interface.py b/ego/tools/interface.py index e679c655..ae409294 100644 --- a/ego/tools/interface.py +++ b/ego/tools/interface.py @@ -27,10 +27,16 @@ import logging import math +import os import time import pandas as pd +if "READTHEDOCS" not in os.environ: + from sqlalchemy import func + + from ego.mv_clustering import database + logger = logging.getLogger(__name__) @@ -783,3 +789,59 @@ def rename_generator_carriers_edisgo(edisgo_grid): generators_df.loc[gens_rename.index, "type"] = "run_of_river" gens_rename = generators_df[generators_df["type"].isin(["conventional"])] generators_df.loc[gens_rename.index, "type"] = "others" + + +def map_etrago_heat_bus_to_district_heating_id(specs, scenario, config, engine): + """ + Helper function to rename heat bus ID from eTraGo to district heating ID used + in eDisGo for specifications from overlying grid on district heating feed-in, + as well as district heating storage SoC and capacity. + + """ + # map district heating ID to heat bus ID from eTraGo + orm = database.register_tables_in_saio(engine, config=config) + heat_buses = [int(_) for _ in specs["feedin_district_heating"].columns] + with database.session_scope(engine) as session: + # get srid of etrago_bus table + query = session.query(func.ST_SRID(orm["etrago_bus"].geom)).limit(1) + srid_etrago_bus = query.all()[0] + # get district heating ID corresponding to heat bus ID by geo join + query = ( + session.query( + orm["etrago_bus"].bus_id.label("heat_bus_id"), + orm["district_heating_areas"].id.label("district_heating_id"), + ) + .filter( + orm["etrago_bus"].scn_name == scenario, + orm["district_heating_areas"].scenario == scenario, + orm["etrago_bus"].bus_id.in_(heat_buses), + ) + .outerjoin( # join to obtain district heating ID + orm["district_heating_areas"], + func.ST_Transform( + func.ST_Centroid(orm["district_heating_areas"].geom_polygon), + srid_etrago_bus, + ) + == orm["etrago_bus"].geom, + ) + ) + mapping_heat_bus_dh_id = pd.read_sql( + query.statement, + engine, + index_col="heat_bus_id", + ) + # convert heat bus ID to string + mapping_heat_bus_dh_id.index = mapping_heat_bus_dh_id.index.map(str) + # rename heat bus to district heating ID + specs["feedin_district_heating"].rename( + columns=mapping_heat_bus_dh_id.district_heating_id, + inplace=True, + ) + specs["thermal_storage_central_soc"].rename( + columns=mapping_heat_bus_dh_id.district_heating_id, + inplace=True, + ) + specs["thermal_storage_central_capacity"].rename( + index=mapping_heat_bus_dh_id.district_heating_id, + inplace=True, + ) From a6cee9cd89f5e9fe0ade8dcdc78cdb26de7723c2 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 10 May 2023 14:26:29 +0200 Subject: [PATCH 106/126] Handle low flex and full flex in one function --- ego/tools/edisgo_integration.py | 290 ++++---------------------------- 1 file changed, 32 insertions(+), 258 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 8e2e9c4b..8d29cd3a 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -65,13 +65,13 @@ ) from egoio.db_tables import grid, model_draft from egoio.tools import db - from sqlalchemy import func from ego.mv_clustering import cluster_workflow, database from ego.tools.economics import edisgo_grid_investment from ego.tools.interface import ( ETraGoMinimalData, get_etrago_results_per_bus, + map_etrago_heat_bus_to_district_heating_id, rename_generator_carriers_edisgo, ) @@ -860,16 +860,11 @@ def run_edisgo(self, mv_grid_id): from_zip_archive=True, ) edisgo_grid.legacy_grids = False - # function that is called depends on scenario + edisgo_grid = self._run_edisgo_task_specs_overlying_grid( + edisgo_grid, scenario, logger, config, engine + ) zip_name = "grid_data_overlying_grid" - if scenario in ["eGon2035", "eGon100RE"]: - edisgo_grid = self._run_edisgo_task_specs_overlying_grid( - edisgo_grid, scenario, logger, config, engine - ) - else: - edisgo_grid = self._run_edisgo_task_specs_overlying_grid_low_flex( - edisgo_grid, scenario, logger, config, engine - ) + if scenario in ["eGon2035_lowflex", "eGon100RE_lowflex"]: zip_name += "_lowflex" edisgo_grid.save( directory=os.path.join(results_dir, zip_name), @@ -1329,56 +1324,19 @@ def _run_edisgo_task_specs_overlying_grid( if not specs["feedin_district_heating"].empty: # map district heating ID to heat bus ID from eTraGo - orm = database.register_tables_in_saio(engine, config=config) - heat_buses = [int(_) for _ in specs["feedin_district_heating"].columns] - with database.session_scope(engine) as session: - # get srid of etrago_bus table - query = session.query(func.ST_SRID(orm["etrago_bus"].geom)).limit(1) - srid_etrago_bus = query.all()[0] - # get district heating ID corresponding to heat bus ID by geo join - query = ( - session.query( - orm["etrago_bus"].bus_id.label("heat_bus_id"), - orm["district_heating_areas"].id.label( - "district_heating_id" - ), - ) - .filter( - orm["etrago_bus"].scn_name == scenario, - orm["district_heating_areas"].scenario == scenario, - orm["etrago_bus"].bus_id.in_(heat_buses), - ) - .outerjoin( # join to obtain district heating ID - orm["district_heating_areas"], - func.ST_Transform( - func.ST_Centroid( - orm["district_heating_areas"].geom_polygon - ), - srid_etrago_bus, - ) - == orm["etrago_bus"].geom, - ) - ) - mapping_heat_bus_dh_id = pd.read_sql( - query.statement, - engine, - index_col="district_heating_id", - ) - hp_dh = pd.merge( - hp_dh, - mapping_heat_bus_dh_id, - left_on="district_heating_id", - right_index=True, - ) + if scenario.split("_")[-1] == "lowflex": + scn = scenario.split("_")[0] + else: + scn = scenario + map_etrago_heat_bus_to_district_heating_id(specs, scn, config, engine) - for heat_bus in heat_buses: - if str(heat_bus) in specs["thermal_storage_central_capacity"].index: - if ( - specs["thermal_storage_central_capacity"].at[str(heat_bus)] - > 0 - ): + for dh_id in hp_dh.district_heating_id.unique(): + if dh_id in specs["thermal_storage_central_capacity"].index: + if specs["thermal_storage_central_capacity"].at[dh_id] > 0: # get PtH unit name to allocate thermal storage unit to - comp_name = hp_dh[hp_dh.heat_bus_id == heat_bus].index[0] + comp_name = hp_dh[hp_dh.district_heating_id == dh_id].index[ + 0 + ] edisgo_grid.heat_pump.thermal_storage_units_df = pd.concat( [ edisgo_grid.heat_pump.thermal_storage_units_df, @@ -1386,7 +1344,7 @@ def _run_edisgo_task_specs_overlying_grid( data={ "capacity": specs[ "thermal_storage_central_capacity" - ].at[str(heat_bus)], + ].at[dh_id], "efficiency": specs[ "thermal_storage_central_efficiency" ], @@ -1395,26 +1353,10 @@ def _run_edisgo_task_specs_overlying_grid( ), ] ) - # overwrite column name of SoC dataframe to be district - # heating ID - specs["thermal_storage_central_soc"].rename( - columns={ - str(heat_bus): hp_dh.at[ - comp_name, "district_heating_id" - ] - }, - inplace=True, - ) - specs["feedin_district_heating"].rename( - columns={ - str(heat_bus): hp_dh.at[ - comp_name, "district_heating_id" - ] - }, - inplace=True, - ) logger.info("Set requirements from overlying grid.") + # all time series from overlying grid are also kept for low flex scenarios + # in order to afterwards check difference in dispatch between eTraGo and eDisGo # curtailment # scale curtailment by ratio of nominal power in eDisGo and eTraGo @@ -1494,13 +1436,23 @@ def _run_edisgo_task_specs_overlying_grid( ] # Thermal storage units SoC - edisgo_grid.overlying_grid.thermal_storage_units_central_soc = specs[ + edisgo_grid.overlying_grid.thermal_storage_units_decentral_soc = specs[ "thermal_storage_rural_soc" ] edisgo_grid.overlying_grid.thermal_storage_units_central_soc = specs[ "thermal_storage_central_soc" ] + # Delete some flex data in case of low flex scenario + if scenario in ["eGon2035_lowflex", "eGon100RE_flex"]: + # delete DSM and flexibility bands to save disk space + edisgo_grid.dsm = edisgo_grid.dsm.__class__() + edisgo_grid.electromobility.flexibility_bands = { + "upper_power": pd.DataFrame(), + "lower_energy": pd.DataFrame(), + "upper_energy": pd.DataFrame(), + } + logger.info("Run integrity check.") edisgo_grid.check_integrity() @@ -1760,6 +1712,8 @@ def _run_edisgo_task_optimisation( psa_net.loads.index.str.contains("home") | (psa_net.loads.index.str.contains("work")) ].index.values + # ToDo nur die mit Wärmespeicher und für anderen operating + # strategy flexible_hps = edisgo_copy.heat_pump.heat_demand_df.columns.values flexible_loads = edisgo_copy.dsm.p_max.columns flexible_storage_units = ( @@ -1828,186 +1782,6 @@ def _run_edisgo_task_optimisation( edisgo_grid.timeseries.timeindex = timeindex return edisgo_grid - def _run_edisgo_task_specs_overlying_grid_low_flex( - self, edisgo_grid, scenario, logger, config, engine - ): - """ - Sets up grid for low-flex scenario variation. - - Parameters - ---------- - mv_grid_id : int - MV grid ID of the ding0 grid - - Returns - ------- - :class:`edisgo.EDisGo` - Returns the complete eDisGo container, also including results - - """ - logger.info("Start task 'specs_overlying_grid_low_flex'.") - - logger.info("Get specifications from eTraGo.") - specs = get_etrago_results_per_bus( - edisgo_grid.topology.id, - self._etrago_network, - self._pf_post_lopf, - self._max_cos_phi_renewable, - ) - snapshots = specs["timeindex"] - - # overwrite previously set dummy time index if year that was used differs from - # year used in etrago - edisgo_year = edisgo_grid.timeseries.timeindex[0].year - etrago_year = snapshots[0].year - if edisgo_year != etrago_year: - timeindex_new_full = pd.date_range( - f"1/1/{etrago_year}", periods=8760, freq="H" - ) - # conventional loads - edisgo_grid.timeseries.loads_active_power.index = timeindex_new_full - edisgo_grid.timeseries.loads_reactive_power.index = timeindex_new_full - # COP and heat demand - edisgo_grid.heat_pump.cop_df.index = timeindex_new_full - edisgo_grid.heat_pump.heat_demand_df.index = timeindex_new_full - # TimeSeries.timeindex - edisgo_grid.timeseries.timeindex = snapshots - - # set generator time series - # rename carrier to match with carrier names in overlying grid - rename_generator_carriers_edisgo(edisgo_grid) - # active power - # ToDo Abregelung? - edisgo_grid.set_time_series_active_power_predefined( - dispatchable_generators_ts=specs["dispatchable_generators_active_power"], - fluctuating_generators_ts=specs["renewables_potential"], - ) - # reactive power - edisgo_grid.set_time_series_reactive_power_control( - control="fixed_cosphi", - generators_parametrisation="default", - loads_parametrisation=None, - storage_units_parametrisation=None, - ) - - # set heat pump time series - logger.info("Set heat pump time series.") - - # individual heat pumps - uncontrolled operation - hp_decentral = edisgo_grid.topology.loads_df[ - edisgo_grid.topology.loads_df.sector == "individual_heating" - ] - if not hp_decentral.empty: - edisgo_grid.apply_heat_pump_operating_strategy( - heat_pump_names=hp_decentral.index - ) - # district heating heat pumps from overlying grid - hp_dh = edisgo_grid.topology.loads_df[ - edisgo_grid.topology.loads_df.sector.isin( - ["district_heating", "district_heating_resistive_heater"] - ) - ] - if len(hp_dh) > 0: - if specs["feedin_district_heating"].empty: - raise ValueError( - "There are PtH units in district heating but no time series " - "from overlying grid." - ) - - # map district heating ID to heat bus ID from eTraGo - orm = database.register_tables_in_saio(engine, config=config) - heat_buses = [int(_) for _ in specs["feedin_district_heating"].columns] - with database.session_scope(engine) as session: - # get srid of etrago_bus table - query = session.query(func.ST_SRID(orm["etrago_bus"].geom)).limit(1) - srid_etrago_bus = query.all()[0] - # get district heating ID corresponding to heat bus ID by geo join - query = ( - session.query( - orm["etrago_bus"].bus_id.label("heat_bus_id"), - orm["district_heating_areas"].id.label("district_heating_id"), - ) - .filter( - orm["etrago_bus"].scn_name == scenario, - orm["district_heating_areas"].scenario == scenario, - orm["etrago_bus"].bus_id.in_(heat_buses), - ) - .outerjoin( # join to obtain district heating ID - orm["district_heating_areas"], - func.ST_Transform( - func.ST_Centroid( - orm["district_heating_areas"].geom_polygon - ), - srid_etrago_bus, - ) - == orm["etrago_bus"].geom, - ) - ) - mapping_heat_bus_dh_id = pd.read_sql( - query.statement, - engine, - index_col="district_heating_id", - ) - hp_dh = pd.merge( - hp_dh, - mapping_heat_bus_dh_id, - left_on="district_heating_id", - right_index=True, - ) - - for heat_bus in heat_buses: - if str(heat_bus) in specs["thermal_storage_central_capacity"].index: - if specs["thermal_storage_central_capacity"].at[str(heat_bus)] > 0: - # get PtH unit name to allocate thermal storage unit to - comp_name = hp_dh[hp_dh.heat_bus_id == heat_bus].index[0] - edisgo_grid.heat_pump.thermal_storage_units_df = pd.concat( - [ - edisgo_grid.heat_pump.thermal_storage_units_df, - pd.DataFrame( - data={ - "capacity": specs[ - "thermal_storage_central_capacity" - ].at[str(heat_bus)], - "efficiency": specs[ - "thermal_storage_central_efficiency" - ], - }, - index=[comp_name], - ), - ] - ) - specs["feedin_district_heating"].rename( - columns={ - str(heat_bus): hp_dh.at[ - comp_name, "district_heating_id" - ] - }, - inplace=True, - ) - # reactive power - edisgo_grid.set_time_series_reactive_power_control( - control="fixed_cosphi", - generators_parametrisation=None, - loads_parametrisation="default", - storage_units_parametrisation=None, - ) - - # delete storage units - logger.info("Delete battery storage units.") - for stor in edisgo_grid.topology.storage_units_df.index: - edisgo_grid.remove_component( - comp_type="storage_unit", comp_name=stor, drop_ts=False - ) - # delete DSM and flexibility bands - edisgo_grid.dsm = edisgo_grid.dsm.__class__() - edisgo_grid.electromobility.flexibility_bands = { - "upper_power": pd.DataFrame(), - "lower_energy": pd.DataFrame(), - "upper_energy": pd.DataFrame(), - } - - return edisgo_grid - def _run_edisgo_task_grid_reinforcement(self, edisgo_grid, logger, time_intervals): """ Runs the grid reinforcement. From 735cdeb47fa4e9cf63557ae5bdfcc353fb6b63ac Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 10 May 2023 17:32:48 +0200 Subject: [PATCH 107/126] Use same optimisation task for full and low flex --- ego/tools/edisgo_integration.py | 134 +++++++++++++++++--------------- 1 file changed, 70 insertions(+), 64 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 8d29cd3a..a8a65a3a 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -907,43 +907,17 @@ def run_edisgo(self, mv_grid_id): # determine whether work flow ends here or continues, and if it continues # whether time intervals need to be loaded - # for full flex scenarios the optimisation would be the next step - if scenario in ["eGon2035", "eGon100RE"]: - if "4_optimisation" in config["eDisGo"]["tasks"]: - if time_intervals is None: - load_time_intervals = True - else: - load_time_intervals = False - else: - return {edisgo_grid.topology.id: results_dir} - # for low flex scenarios the grid reinforcement would be the next step - else: - if "5_grid_reinforcement" in config["eDisGo"]["tasks"]: - if time_intervals is None: - load_time_intervals = True - else: - load_time_intervals = False - else: - return {edisgo_grid.topology.id: results_dir} - if load_time_intervals is True: - # load time intervals - time_intervals = pd.read_csv( - os.path.join(results_dir, "selected_time_intervals.csv"), - index_col=0, - ) - for ti in time_intervals.index: - time_steps = time_intervals.at[ti, "time_steps"] - if time_steps is not None: - time_intervals.at[ti, "time_steps"] = pd.date_range( - start=time_steps.split("'")[1], - periods=int(time_steps.split("=")[-2].split(",")[0]), - freq="H", - ) + if "4_optimisation" not in config["eDisGo"]["tasks"]: + return {edisgo_grid.topology.id: results_dir} # ########################## task: optimisation ########################## if "4_optimisation" in config["eDisGo"]["tasks"]: if edisgo_grid is None: - grid_path = os.path.join(results_dir, "grid_data_overlying_grid.zip") + if scenario in ["eGon2035", "eGon100RE"]: + zip_name = "grid_data_overlying_grid.zip" + else: + zip_name = "grid_data_overlying_grid_lowflex.zip" + grid_path = os.path.join(results_dir, zip_name) edisgo_grid = import_edisgo_from_files( edisgo_path=grid_path, import_topology=True, @@ -956,11 +930,28 @@ def run_edisgo(self, mv_grid_id): from_zip_archive=True, ) edisgo_grid.legacy_grids = False + if time_intervals is None: + # load time intervals + time_intervals = pd.read_csv( + os.path.join(results_dir, "selected_time_intervals.csv"), + index_col=0, + ) + for ti in time_intervals.index: + time_steps = time_intervals.at[ti, "time_steps"] + if time_steps is not None: + time_intervals.at[ti, "time_steps"] = pd.date_range( + start=time_steps.split("'")[1], + periods=int(time_steps.split("=")[-2].split(",")[0]), + freq="H", + ) edisgo_grid = self._run_edisgo_task_optimisation( - edisgo_grid, logger, time_intervals, results_dir + edisgo_grid, scenario, logger, time_intervals, results_dir ) + zip_name = "grid_data_optimisation" + if scenario in ["eGon2035_lowflex", "eGon100RE_lowflex"]: + zip_name += "_lowflex" edisgo_grid.save( - directory=os.path.join(results_dir, "grid_data_optimisation"), + directory=os.path.join(results_dir, zip_name), save_topology=True, save_timeseries=True, save_results=False, @@ -981,7 +972,7 @@ def run_edisgo(self, mv_grid_id): if scenario in ["eGon2035", "eGon100RE"]: zip_name = "grid_data_optimisation.zip" else: - zip_name = "grid_data_overlying_grid_lowflex.zip" + zip_name = "grid_data_optimisation_lowflex.zip" grid_path = os.path.join(results_dir, zip_name) edisgo_grid = import_edisgo_from_files( edisgo_path=grid_path, @@ -1014,26 +1005,7 @@ def run_edisgo(self, mv_grid_id): archive_type="zip", ) - # ########################## save results ########################## self._status_update(mv_grid_id, "end") - # edisgo_grid.save( - # directory=os.path.join(results_dir, "reinforce_data"), - # save_topology=True, - # save_timeseries=True, - # save_results=True, - # save_electromobility=False, - # # save_dsm=True, - # save_heatpump=False, - # save_overlying_grid=False, - # reduce_memory=True, - # archive=True, - # archive_type="zip", - # parameters={ - # "powerflow_results": ["pfa_p", "pfa_q", "v_res"], - # "grid_expansion_results": ["grid_expansion_costs", "equipment - # _changes"], - # }, - # ) return {edisgo_grid.topology.id: results_dir} @@ -1656,7 +1628,13 @@ def _run_edisgo_task_temporal_complexity_reduction( return time_interval_1, time_interval_2 def _run_edisgo_task_optimisation( - self, edisgo_grid, logger, time_intervals, results_dir, reduction_factor=0.3 + self, + edisgo_grid, + scenario, + logger, + time_intervals, + results_dir, + reduction_factor=0.3, ): """ Runs the dispatch optimisation. @@ -1707,15 +1685,23 @@ def _run_edisgo_task_optimisation( ) # OPF + # flexibilities in full flex: DSM, decentral and central PtH units, + # curtailment, EVs, storage units + # flexibilities in low flex: curtailment, storage units psa_net = edisgo_copy.to_pypsa() - flexible_cps = psa_net.loads.loc[ - psa_net.loads.index.str.contains("home") - | (psa_net.loads.index.str.contains("work")) - ].index.values - # ToDo nur die mit Wärmespeicher und für anderen operating - # strategy - flexible_hps = edisgo_copy.heat_pump.heat_demand_df.columns.values - flexible_loads = edisgo_copy.dsm.p_max.columns + if scenario in ["eGon2035", "eGon100RE"]: + flexible_loads = edisgo_copy.dsm.p_max.columns + flexible_hps = ( + edisgo_copy.heat_pump.thermal_storage_units_df.index.values + ) + flexible_cps = psa_net.loads.loc[ + psa_net.loads.index.str.contains("home") + | (psa_net.loads.index.str.contains("work")) + ].index.values + else: + flexible_loads = [] + flexible_hps = [] + flexible_cps = [] flexible_storage_units = ( edisgo_copy.topology.storage_units_df.index.values ) @@ -1779,6 +1765,26 @@ def _run_edisgo_task_optimisation( edisgo_copy.timeseries.storage_units_reactive_power.columns, ] = edisgo_copy.timeseries.storage_units_reactive_power + # write OPF results back + edisgo_grid.opf_results.overlying_grid = pd.concat( + [ + edisgo_grid.opf_results.overlying_grid, + edisgo_copy.opf_results.overlying_grid, + ] + ) + edisgo_grid.opf_results.battery_storage_t.p = pd.concat( + [ + edisgo_grid.opf_results.battery_storage_t.p, + edisgo_copy.opf_results.battery_storage_t.p, + ] + ) + edisgo_grid.opf_results.battery_storage_t.e = pd.concat( + [ + edisgo_grid.opf_results.battery_storage_t.e, + edisgo_copy.opf_results.battery_storage_t.e, + ] + ) + edisgo_grid.timeseries.timeindex = timeindex return edisgo_grid From f9cd82711e423e8d8ece5f839d3e8faaf0844f15 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 10 May 2023 18:04:18 +0200 Subject: [PATCH 108/126] Time intervals not needed anymore in grid reinforcement --- ego/tools/edisgo_integration.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index a8a65a3a..aee6009c 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -44,7 +44,6 @@ import dill import multiprocess as mp2 -import numpy as np import pandas as pd from sqlalchemy.orm import scoped_session, sessionmaker @@ -986,9 +985,7 @@ def run_edisgo(self, mv_grid_id): from_zip_archive=True, ) edisgo_grid.legacy_grids = False - edisgo_grid = self._run_edisgo_task_grid_reinforcement( - edisgo_grid, logger, time_intervals - ) + edisgo_grid = self._run_edisgo_task_grid_reinforcement(edisgo_grid, logger) edisgo_grid.save( directory=os.path.join( results_dir, f"grid_data_reinforcement_{scenario}" @@ -1788,7 +1785,7 @@ def _run_edisgo_task_optimisation( edisgo_grid.timeseries.timeindex = timeindex return edisgo_grid - def _run_edisgo_task_grid_reinforcement(self, edisgo_grid, logger, time_intervals): + def _run_edisgo_task_grid_reinforcement(self, edisgo_grid, logger): """ Runs the grid reinforcement. @@ -1808,12 +1805,6 @@ def _run_edisgo_task_grid_reinforcement(self, edisgo_grid, logger, time_interval # overwrite configs with new configs edisgo_grid._config = Config() - # set timeindex to given time_intervals - timesteps = np.concatenate( - [cols.time_steps for _, cols in time_intervals.iterrows()] - ) - edisgo_grid.timeseries.timeindex = pd.Index(timesteps) - enhanced_reinforce_grid( edisgo_grid, activate_cost_results_disturbing_mode=True, From 36abdd17f9d1b77960026a3289f5bde68e4069f5 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 10 May 2023 21:32:13 +0200 Subject: [PATCH 109/126] Fixes in optimisation task --- ego/tools/edisgo_integration.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index aee6009c..e3c1ef49 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1688,17 +1688,18 @@ def _run_edisgo_task_optimisation( psa_net = edisgo_copy.to_pypsa() if scenario in ["eGon2035", "eGon100RE"]: flexible_loads = edisgo_copy.dsm.p_max.columns - flexible_hps = ( - edisgo_copy.heat_pump.thermal_storage_units_df.index.values - ) + # flexible_hps = ( + # edisgo_copy.heat_pump.thermal_storage_units_df.index.values + # ) flexible_cps = psa_net.loads.loc[ psa_net.loads.index.str.contains("home") | (psa_net.loads.index.str.contains("work")) ].index.values else: flexible_loads = [] - flexible_hps = [] + # flexible_hps = [] flexible_cps = [] + flexible_hps = edisgo_copy.heat_pump.heat_demand_df.columns.values flexible_storage_units = ( edisgo_copy.topology.storage_units_df.index.values ) @@ -1715,8 +1716,11 @@ def _run_edisgo_task_optimisation( ) # save OPF results + zip_name = f"opf_results_{ti}" + if scenario in ["eGon2035_lowflex", "eGon100RE_lowflex"]: + zip_name += "_lowflex" edisgo_copy.save( - directory=os.path.join(results_dir, f"opf_results_{ti}"), + directory=os.path.join(results_dir, zip_name), save_topology=True, save_timeseries=False, save_results=False, From 14d4e71ab2ad003a92197bf702e44a5e87f0e017 Mon Sep 17 00:00:00 2001 From: birgits Date: Wed, 10 May 2023 22:28:13 +0200 Subject: [PATCH 110/126] Add saving of opf results --- ego/tools/edisgo_integration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index e3c1ef49..2b6a2a7d 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -954,6 +954,7 @@ def run_edisgo(self, mv_grid_id): save_topology=True, save_timeseries=True, save_results=False, + save_opf_results=True, save_electromobility=False, save_dsm=False, save_heatpump=False, From 89dacb16976de33bb4ff84b597974bf6c60cc4c8 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 11 May 2023 12:53:54 +0200 Subject: [PATCH 111/126] Fix save correct edisgo object --- ego/tools/edisgo_integration.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 2b6a2a7d..3506bd06 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1810,11 +1810,12 @@ def _run_edisgo_task_grid_reinforcement(self, edisgo_grid, logger): # overwrite configs with new configs edisgo_grid._config = Config() - enhanced_reinforce_grid( + edisgo_grid = enhanced_reinforce_grid( edisgo_grid, activate_cost_results_disturbing_mode=True, separate_lv_grids=True, separation_threshold=2, + copy_grid=False, ) return edisgo_grid From f49383c03cec537fc0d00b8b5b5d5b00d5f98d9f Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 11 May 2023 21:28:24 +0200 Subject: [PATCH 112/126] Fix bug district heating ID as string of integer --- ego/tools/edisgo_integration.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 3506bd06..dba11418 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1650,6 +1650,20 @@ def _run_edisgo_task_optimisation( """ logger.info("Start task 'optimisation'.") + # prepare district heating data + # make sure district heating ID is string of integer not float + columns_rename = [ + str(int(float(_))) + for _ in edisgo_grid.overlying_grid.feedin_district_heating.columns + ] + if len(columns_rename) > 0: + edisgo_grid.overlying_grid.feedin_district_heating.columns = columns_rename + cols = edisgo_grid.overlying_grid.thermal_storage_units_central_soc.columns + columns_rename = [str(int(float(_))) for _ in cols] + if len(columns_rename) > 0: + edisgo_grid.overlying_grid.thermal_storage_units_central_soc.columns = ( + columns_rename + ) # aggregate PtH units in same district heating network and subtract feed-in # from other heat sources from heat demand in district heating network aggregate_district_heating_components( From 18323c00eec304be7279dd0dc31f2ffca0719b5e Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 12 May 2023 15:56:31 +0200 Subject: [PATCH 113/126] Add reinforcement of LV feeders with large voltage issues to grid reinforcement --- ego/tools/edisgo_integration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index dba11418..3ec6e559 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1829,6 +1829,7 @@ def _run_edisgo_task_grid_reinforcement(self, edisgo_grid, logger): activate_cost_results_disturbing_mode=True, separate_lv_grids=True, separation_threshold=2, + reinforce_lv_feeders_large_voltage_issues=True, copy_grid=False, ) return edisgo_grid From 0adbd69496ce4a7472b4dbd60fd3d894a832170f Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 25 May 2023 18:13:59 +0200 Subject: [PATCH 114/126] Remove capacity limitation --- ego/mv_clustering/egon_data_io.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ego/mv_clustering/egon_data_io.py b/ego/mv_clustering/egon_data_io.py index f4cfcff6..a98e7e69 100644 --- a/ego/mv_clustering/egon_data_io.py +++ b/ego/mv_clustering/egon_data_io.py @@ -317,7 +317,6 @@ def get_pth_capacity(scenario, grid_ids, orm=None, session=None): orm["heat_pump_capacity_individual"].mv_grid_id.in_(grid_ids), orm["heat_pump_capacity_individual"].carrier == "heat_pump", orm["heat_pump_capacity_individual"].scenario == scenario, - orm["heat_pump_capacity_individual"].capacity <= 20.0, ) .group_by( orm["heat_pump_capacity_individual"].mv_grid_id, From b468188a7418ca305c08c56a39cb269511b57b47 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 25 May 2023 18:20:07 +0200 Subject: [PATCH 115/126] Add expansion as clustering attribute --- ego/mv_clustering/mv_clustering.py | 90 ++++++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 4 deletions(-) diff --git a/ego/mv_clustering/mv_clustering.py b/ego/mv_clustering/mv_clustering.py index 47f5ee7e..1efd20ce 100644 --- a/ego/mv_clustering/mv_clustering.py +++ b/ego/mv_clustering/mv_clustering.py @@ -75,16 +75,33 @@ def get_cluster_attributes(attributes_path, scenario, config=None): * "area" : area of MV grid in m^2 * "pv_capacity_mw" : PV capacity in MW * "pv_capacity_mw_per_km2" : PV capacity in MW per km^2 + * "pv_capacity_expansion_mw" : PV expansion from status quo to given + scenario in MW + * "pv_capacity_expansion_mw_per_km2" : PV expansion from status quo to given + scenario in MW per km^2 * "wind_capacity_mw" : wind onshore capacity in MW * "wind_capacity_mw_per_km2" : wind onshore capacity in MW per km^2 + * "wind_capacity_expansion_mw" : wind onshore expansion from status quo to given + scenario in MW + * "wind_capacity_expansion_mw_per_km2" : wind onshore expansion from status quo + to given scenario in MW per km^2 * "electromobility_max_load_mw" : maximum load of EVs (in case of - uncoordinated charging) in MW + uncoordinated charging) in MW * "electromobility_max_load_mw_per_km2" : maximum load of EVs (in case of - uncoordinated charging) in MW per km^2 + uncoordinated charging) in MW per km^2 + * "electromobility_max_load_expansion_mw" : increase in maximum load of EVs + from status quo to given scenario (in case of uncoordinated charging) in MW + * "electromobility_max_load_expansion_mw_per_km2" : increase in maximum load of + EVs from status quo to given scenario (in case of uncoordinated charging) + in MW per km^2 * "pth_capacity_mw" : PtH capacity (for individual and district - heating) in MW + heating) in MW * "pth_capacity_mw_per_km2" : PtH capacity (for individual and - district heating) in MW per km^2 + district heating) in MW per km^2 + * "pth_capacity_expansion_mw" : increase in PtH capacity (for individual and + district heating) from status quo to given scenario in MW + * "pth_capacity_expansion_mw_per_km2" : increase in PtH capacity (for individual + and district heating) from status quo to given scenario in MW per km^2 """ # get attributes from database @@ -96,15 +113,40 @@ def get_cluster_attributes(attributes_path, scenario, config=None): solar_capacity_df = db_io.get_solar_capacity( scenario, grid_ids_df.index, orm, engine=engine ) + if scenario == "status_quo": + solar_capacity_sq_df = solar_capacity_df + else: + solar_capacity_sq_df = db_io.get_solar_capacity( + "status_quo", grid_ids_df.index, orm, engine=engine + ) wind_capacity_df = db_io.get_wind_capacity( scenario, grid_ids_df.index, orm, engine=engine ) + if scenario == "status_quo": + wind_capacity_sq_df = wind_capacity_df + else: + wind_capacity_sq_df = db_io.get_wind_capacity( + "status_quo", grid_ids_df.index, orm, engine=engine + ) emob_capacity_df = db_io.get_electromobility_maximum_load( scenario, grid_ids_df.index, orm, engine=engine ) + if scenario == "status_quo": + emob_capacity_sq_df = emob_capacity_df + else: + emob_capacity_sq_df = db_io.get_electromobility_maximum_load( + "status_quo", grid_ids_df.index, orm, engine=engine + ) pth_capacity_df = db_io.get_pth_capacity( scenario, grid_ids_df.index, orm, engine=engine ) + if scenario == "status_quo": + pth_capacity_sq_df = pth_capacity_df + else: + pth_capacity_sq_df = db_io.get_pth_capacity( + "status_quo", grid_ids_df.index, orm, engine=engine + ) + emob_rename_col = "electromobility_max_load_expansion_mw" df = pd.concat( [ grid_ids_df, @@ -112,9 +154,36 @@ def get_cluster_attributes(attributes_path, scenario, config=None): wind_capacity_df, emob_capacity_df, pth_capacity_df, + solar_capacity_sq_df.rename( + columns={"pv_capacity_mw": "pv_capacity_expansion_mw"} + ), + wind_capacity_sq_df.rename( + columns={"wind_capacity_mw": "wind_capacity_expansion_mw"} + ), + emob_capacity_sq_df.rename( + columns={"electromobility_max_load_mw": emob_rename_col} + ), + pth_capacity_sq_df.rename( + columns={"pth_capacity_mw": "pth_capacity_expansion_mw"} + ), ], axis="columns", ).fillna(0) + + # calculate expansion values + df["pv_capacity_expansion_mw"] = ( + df["pv_capacity_mw"] - df["pv_capacity_expansion_mw"] + ) + df["wind_capacity_expansion_mw"] = ( + df["wind_capacity_mw"] - df["wind_capacity_expansion_mw"] + ) + df["electromobility_max_load_expansion_mw"] = ( + df["electromobility_max_load_mw"] - df["electromobility_max_load_expansion_mw"] + ) + df["pth_capacity_expansion_mw"] = ( + df["pth_capacity_mw"] - df["pth_capacity_expansion_mw"] + ) + # calculate relative values df["pv_capacity_mw_per_km2"] = df["pv_capacity_mw"] / (df["area_m2"] / 1e6) df["wind_capacity_mw_per_km2"] = df["wind_capacity_mw"] / (df["area_m2"] / 1e6) @@ -122,6 +191,19 @@ def get_cluster_attributes(attributes_path, scenario, config=None): df["area_m2"] / 1e6 ) df["pth_capacity_mw_per_km2"] = df["pth_capacity_mw"] / (df["area_m2"] / 1e6) + df["pv_capacity_expansion_mw_per_km2"] = df["pv_capacity_expansion_mw"] / ( + df["area_m2"] / 1e6 + ) + df["wind_capacity_expansion_mw_per_km2"] = df["wind_capacity_expansion_mw"] / ( + df["area_m2"] / 1e6 + ) + df["electromobility_max_load_expansion_mw_per_km2"] = df[ + "electromobility_max_load_expansion_mw" + ] / (df["area_m2"] / 1e6) + df["pth_capacity_expansion_mw_per_km2"] = df["pth_capacity_expansion_mw"] / ( + df["area_m2"] / 1e6 + ) + # write to csv df.to_csv(attributes_path) return df From b47baecd124478f159317994a247a6933c11a7fc Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 25 May 2023 18:20:36 +0200 Subject: [PATCH 116/126] Add saving results class --- ego/tools/edisgo_integration.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 3ec6e559..472b967c 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -831,7 +831,7 @@ def run_edisgo(self, mv_grid_id): directory=os.path.join(results_dir, "grid_data"), save_topology=True, save_timeseries=True, - save_results=False, + save_results=True, save_electromobility=True, save_dsm=True, save_heatpump=True, @@ -839,6 +839,7 @@ def run_edisgo(self, mv_grid_id): reduce_memory=True, archive=True, archive_type="zip", + parameters={"grid_expansion_results": ["equipment_changes"]}, ) if "2_specs_overlying_grid" not in config["eDisGo"]["tasks"]: return {edisgo_grid.topology.id: results_dir} @@ -851,7 +852,7 @@ def run_edisgo(self, mv_grid_id): edisgo_path=grid_path, import_topology=True, import_timeseries=True, - import_results=False, + import_results=True, import_electromobility=True, import_heat_pump=True, import_dsm=True, @@ -869,7 +870,7 @@ def run_edisgo(self, mv_grid_id): directory=os.path.join(results_dir, zip_name), save_topology=True, save_timeseries=True, - save_results=False, + save_results=True, save_electromobility=True, save_dsm=True, save_heatpump=True, @@ -877,6 +878,7 @@ def run_edisgo(self, mv_grid_id): reduce_memory=True, archive=True, archive_type="zip", + parameters={"grid_expansion_results": ["equipment_changes"]}, ) # ################### task: temporal complexity reduction ################## @@ -892,7 +894,7 @@ def run_edisgo(self, mv_grid_id): edisgo_path=grid_path, import_topology=True, import_timeseries=True, - import_results=False, + import_results=True, import_electromobility=True, import_heat_pump=True, import_dsm=True, @@ -921,7 +923,7 @@ def run_edisgo(self, mv_grid_id): edisgo_path=grid_path, import_topology=True, import_timeseries=True, - import_results=False, + import_results=True, import_electromobility=True, import_heat_pump=True, import_dsm=True, @@ -953,7 +955,7 @@ def run_edisgo(self, mv_grid_id): directory=os.path.join(results_dir, zip_name), save_topology=True, save_timeseries=True, - save_results=False, + save_results=True, save_opf_results=True, save_electromobility=False, save_dsm=False, @@ -962,6 +964,7 @@ def run_edisgo(self, mv_grid_id): reduce_memory=True, archive=True, archive_type="zip", + parameters={"grid_expansion_results": ["equipment_changes"]}, ) if "5_grid_reinforcement" not in config["eDisGo"]["tasks"]: return {edisgo_grid.topology.id: results_dir} @@ -978,7 +981,7 @@ def run_edisgo(self, mv_grid_id): edisgo_path=grid_path, import_topology=True, import_timeseries=True, - import_results=False, + import_results=True, import_electromobility=False, import_heat_pump=False, import_dsm=False, @@ -1829,7 +1832,6 @@ def _run_edisgo_task_grid_reinforcement(self, edisgo_grid, logger): activate_cost_results_disturbing_mode=True, separate_lv_grids=True, separation_threshold=2, - reinforce_lv_feeders_large_voltage_issues=True, copy_grid=False, ) return edisgo_grid From 35ed7cb56cb89047e52ff369314ef46835b6bad7 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 25 May 2023 19:09:16 +0200 Subject: [PATCH 117/126] Add docstrings and delete old functions --- ego/tools/edisgo_integration.py | 157 ++++++++++---------------------- 1 file changed, 46 insertions(+), 111 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 472b967c..2c3940a6 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -62,7 +62,6 @@ aggregate_district_heating_components, get_sample_using_time, ) - from egoio.db_tables import grid, model_draft from egoio.tools import db from ego.mv_clustering import cluster_workflow, database @@ -192,60 +191,6 @@ def grid_investment_costs(self): """ return self._grid_investment_costs - def get_mv_grid_from_bus_id(self, bus_id): - """ - Queries the MV grid ID for a given eTraGo bus - - Parameters - ---------- - bus_id : int - eTraGo bus ID - - Returns - ------- - int - MV grid (ding0) ID - - """ - - conn = db.connection(section=self._db_section) - session_factory = sessionmaker(bind=conn) - Session = scoped_session(session_factory) - session = Session() - - mv_grid_id = self._get_mv_grid_from_bus_id(session, bus_id) - - Session.remove() - - return mv_grid_id - - def get_bus_id_from_mv_grid(self, subst_id): - """ - Queries the eTraGo bus ID for given MV grid (ding0) ID - - Parameters - ---------- - subst_id : int - MV grid (ding0) ID - - Returns - ------- - int - eTraGo bus ID - - """ - - conn = db.connection(section=self._db_section) - session_factory = sessionmaker(bind=conn) - Session = scoped_session(session_factory) - session = Session() - - bus_id = self._get_bus_id_from_mv_grid(session, subst_id) - - Session.remove() - - return bus_id - def plot_storage_integration(self, mv_grid_id, **kwargs): """ Plots storage position in MV grid of integrated storages. @@ -605,11 +550,10 @@ def _identify_extended_storages(self): logger.info("Identifying extended storage") for mv_grid in all_mv_grids: - bus_id = self._get_bus_id_from_mv_grid(session, mv_grid) min_extended = 0.3 stor_p_nom = self._etrago_network.storage_units.loc[ - (self._etrago_network.storage_units["bus"] == str(bus_id)) + (self._etrago_network.storage_units["bus"] == str(mv_grid)) & ( self._etrago_network.storage_units["p_nom_extendable"] == True # noqa: E712 @@ -643,11 +587,8 @@ def _check_available_mv_grids(self): """ mv_grids = [] for file in os.listdir(self._grid_path): - if file.endswith(".pkl"): - mv_grids.append( - int(file.replace("ding0_grids__", "").replace(".pkl", "")) - ) - + if os.path.isdir(os.path.join(self._grid_path, file)): + mv_grids.append(int(file)) return mv_grids def _set_grid_choice(self): @@ -1033,6 +974,14 @@ def _run_edisgo_task_setup_grid(self, mv_grid_id, scenario, logger, config, engi ---------- mv_grid_id : int MV grid ID of the ding0 grid. + scenario : str + Name of scenario to import data for. Possible options are "eGon2035" + and "eGon100RE". + logger : logger handler + config : dict + Dictionary with configuration data. + engine : :sqlalchemy:`sqlalchemy.Engine` + Database engine. Returns ------- @@ -1124,8 +1073,16 @@ def _run_edisgo_task_specs_overlying_grid( Parameters ---------- - mv_grid_id : int - MV grid ID of the ding0 grid + edisgo_grid : :class:`edisgo.EDisGo` + EDisGo object. + scenario : str + Name of scenario to import data for. Possible options are "eGon2035" + and "eGon100RE". + logger : logger handler + config : dict + Dictionary with configuration data. + engine : :sqlalchemy:`sqlalchemy.Engine` + Database engine. Returns ------- @@ -1435,17 +1392,21 @@ def _run_edisgo_task_temporal_complexity_reduction( self, edisgo_grid, logger, config ): """ - Runs the temporal complexity reduction. + Runs the temporal complexity reduction to select most critical time periods. Parameters ---------- - mv_grid_id : int - MV grid ID of the ding0 grid + edisgo_grid : :class:`edisgo.EDisGo` + EDisGo object. + logger : logger handler + config : dict + Dictionary with configuration data. + engine : :sqlalchemy:`sqlalchemy.Engine` + Database engine. Returns ------- :class:`edisgo.EDisGo` - Returns the complete eDisGo container, also including results """ logger.info("Start task 'temporal complexity reduction'.") @@ -1642,13 +1603,24 @@ def _run_edisgo_task_optimisation( Parameters ---------- - mv_grid_id : int - MV grid ID of the ding0 grid + edisgo_grid : :class:`edisgo.EDisGo` + EDisGo object. + scenario : str + Name of scenario to define flexible components. Possible options are + "eGon2035", "eGon2035_lowflex", "eGon100RE", and "eGon100RE_lowflex". + logger : logger handler + time_intervals : pd.DataFrame + Dataframe with information on time intervals to consider in the optimisation + in column "time_steps". + results_dir : str + Directory where to store OPF results. + reduction_factor : float + Reduction factor to use in spatial complexity reduction. Per default this + is set to 0.3. Returns ------- :class:`edisgo.EDisGo` - Returns the complete eDisGo container, also including results """ logger.info("Start task 'optimisation'.") @@ -1813,13 +1785,13 @@ def _run_edisgo_task_grid_reinforcement(self, edisgo_grid, logger): Parameters ---------- - mv_grid_id : int - MV grid ID of the ding0 grid + edisgo_grid : :class:`edisgo.EDisGo` + EDisGo object. + logger : logger handler Returns ------- :class:`edisgo.EDisGo` - Returns the complete eDisGo container, also including results """ logger.info("Start task 'grid_reinforcement'.") @@ -1891,43 +1863,6 @@ def _load_edisgo_results(self): logger.warning("MV grid {} could not be loaded".format(mv_grid_id)) - def _get_mv_grid_from_bus_id(self, session, bus_id): - """ - Queries the MV grid ID for a given eTraGo bus - - Parameters - ---------- - bus_id : int - eTraGo bus ID - - Returns - ------- - int - MV grid (ding0) ID - - """ - - if self._versioned is True: - ormclass_hvmv_subst = grid.__getattribute__("EgoDpHvmvSubstation") - subst_id = ( - session.query(ormclass_hvmv_subst.subst_id) - .filter( - ormclass_hvmv_subst.otg_id == bus_id, - ormclass_hvmv_subst.version == self._grid_version, - ) - .scalar() - ) - - if self._versioned is False: - ormclass_hvmv_subst = model_draft.__getattribute__("EgoGridHvmvSubstation") - subst_id = ( - session.query(ormclass_hvmv_subst.subst_id) - .filter(ormclass_hvmv_subst.otg_id == bus_id) - .scalar() - ) - - return subst_id - class _ETraGoData: """ From 5a3d09ef3db2b167e19d7e63f14d99009f8f7505 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 25 May 2023 19:21:43 +0200 Subject: [PATCH 118/126] Use MV grid ID directly --- ego/tools/io.py | 61 +++++++++++++++---------------------------------- 1 file changed, 19 insertions(+), 42 deletions(-) diff --git a/ego/tools/io.py b/ego/tools/io.py index a6c0b06c..e45f9e43 100644 --- a/ego/tools/io.py +++ b/ego/tools/io.py @@ -20,45 +20,28 @@ """This file contains the eGo main class as well as input & output functions of eGo in order to build the eGo application container. """ -import json import logging import os -import sys -logger = logging.getLogger("ego") -import json - -import numpy as np import pandas as pd -if not "READTHEDOCS" in os.environ: +if "READTHEDOCS" not in os.environ: import re from importlib import import_module - import oedialect - import pyproj as proj import pypsa - from egoio.db_tables import grid, model_draft from egoio.db_tables.model_draft import EgoGridPfHvSource as Source from egoio.db_tables.model_draft import EgoGridPfHvTempResolution as TempResolution - from egoio.db_tables.model_draft import RenpassGisParameterRegion from egoio.tools import db from etrago import Etrago from etrago.appl import run_etrago - - from geoalchemy2 import * - from shapely.geometry import MultiPolygon, Point, Polygon - from sqlalchemy import MetaData, and_, create_engine, func + from etrago.tools.io import load_config_file + from sqlalchemy import and_ from sqlalchemy.orm import sessionmaker - from ego.tools.economics import ( - etrago_convert_overnight_cost, - etrago_grid_investment, - etrago_operating_costs, - get_generator_investment, - ) + from ego.tools.economics import etrago_convert_overnight_cost from ego.tools.edisgo_integration import EDisGoNetworks from ego.tools.plots import ( igeoplot, @@ -69,13 +52,9 @@ plot_storage_use, power_price_plot, ) - from ego.tools.results import create_etrago_results - from ego.tools.storages import etrago_storages, etrago_storages_investment - from ego.tools.utilities import ( - fix_leading_separator, - get_scenario_setting, - get_time_steps, - ) + from ego.tools.utilities import get_scenario_setting + +logger = logging.getLogger("ego") __copyright__ = "Europa-Universität Flensburg, " "Centre for Sustainable Energy Systems" __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" @@ -119,7 +98,7 @@ def __init__(self, *args, **kwargs): Session = sessionmaker(bind=conn) self.session = Session() logger.info("Connected to Database") - except: + except: # noqa: E722 logger.error("Failed connection to Database", exc_info=True) # get scn_name @@ -145,7 +124,7 @@ def __init__(self, *args, **kwargs): logger.info("eTraGo section started") - if self.json_file["eGo"]["result_id"] != None: + if self.json_file["eGo"]["result_id"] is not None: # Delete arguments from scenario_setting logger.info("Remove given eTraGo settings from scenario_setting") @@ -188,7 +167,7 @@ def __init__(self, *args, **kwargs): logger.info("Create eTraGo network from oedb result") self._etrago_network = etrago_from_oedb(self.session, self.json_file) - if self.json_file["eTraGo"]["disaggregation"] != False: + if self.json_file["eTraGo"]["disaggregation"] is not False: self._etrago_disaggregated_network = self._etrago_network else: logger.warning("No disaggregated network found in DB") @@ -197,18 +176,18 @@ def __init__(self, *args, **kwargs): # create eTraGo NetworkScenario if self.json_file["eGo"]["eTraGo"] is True: - if self.json_file["eGo"].get("csv_import_eTraGo") != False: + if self.json_file["eGo"].get("csv_import_eTraGo") is not False: logger.info("Import eTraGo network from csv files") self.etrago = Etrago( - csv_folder_name = self.json_file['eGo'].get('csv_import_eTraGo')) + csv_folder_name=self.json_file["eGo"].get("csv_import_eTraGo") + ) else: logger.info("Create eTraGo network calcualted by eGo") - run_etrago(args= self.json_file['eTraGo'], - json_path=None) + run_etrago(args=self.json_file["eTraGo"], json_path=None) class eDisGoResults(eTraGoResults): @@ -385,7 +364,7 @@ def _integrate_mv_storage_investment(self): costs_df = costs_df.append(new_storage_row) self._total_investment_costs = costs_df - except: + except: # noqa: E722 logger.info("Something went wrong with the MV storage distribution.") def _calculate_all_extended_storages(self): @@ -395,7 +374,7 @@ def _calculate_all_extended_storages(self): etrago_network = self._etrago_disaggregated_network stor_df = etrago_network.storage_units.loc[ - (etrago_network.storage_units["p_nom_extendable"] == True) + (etrago_network.storage_units["p_nom_extendable"] is True) ] stor_df = stor_df[["bus", "p_nom_opt"]] @@ -412,7 +391,7 @@ def _calculate_mv_storage(self): min_extended = 0.3 stor_df = etrago_network.storage_units.loc[ - (etrago_network.storage_units["p_nom_extendable"] == True) + (etrago_network.storage_units["p_nom_extendable"] is True) & (etrago_network.storage_units["p_nom_opt"] > min_extended) & (etrago_network.storage_units["max_hours"] <= 20.0) ] @@ -422,11 +401,9 @@ def _calculate_mv_storage(self): integrated_storage = 0.0 # Storage integrated in MV grids for idx, row in stor_df.iterrows(): - bus_id = row["bus"] + mv_grid_id = row["bus"] p_nom_opt = row["p_nom_opt"] - mv_grid_id = self.edisgo.get_mv_grid_from_bus_id(bus_id) - if not mv_grid_id: continue @@ -647,7 +624,7 @@ def series_results(name, column, session, result_id, ormclass): Parameters ---------- - session: : sqlalchemy: `sqlalchemy.orm.session.Session < orm/session_basics.html >` + session: : sqlalchemy: `sqlalchemy.orm.session.Session` SQLAlchemy session to the OEDB """ From 17fa2736b505553e2c04000a4994d35828904162 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 25 May 2023 19:52:46 +0200 Subject: [PATCH 119/126] Adapt config --- ego/examples/edisgo_workflow/edisgo_mwe.py | 6 ++---- .../edisgo_workflow/scenario_setting.json | 3 --- ego/mv_clustering/mv_clustering.py | 10 +++++----- ego/scenario_setting.json | 7 ++----- ego/tools/edisgo_integration.py | 18 ++++++------------ ego/tools/utilities.py | 16 +--------------- 6 files changed, 16 insertions(+), 44 deletions(-) diff --git a/ego/examples/edisgo_workflow/edisgo_mwe.py b/ego/examples/edisgo_workflow/edisgo_mwe.py index 86fc2c3a..8298a180 100644 --- a/ego/examples/edisgo_workflow/edisgo_mwe.py +++ b/ego/examples/edisgo_workflow/edisgo_mwe.py @@ -6,7 +6,7 @@ from ego.tools.utilities import get_scenario_setting config = get_scenario_setting() -results_dir = config["eGo"]["results_dir"] +results_dir = config["eDisGo"]["results"] setup_logger( loggers=[ {"name": "root", "file_level": "warning", "stream_level": "warning"}, @@ -17,9 +17,7 @@ log_dir=results_dir, ) -etrago_network = PyPSANetwork( - config["eGo"]["csv_import_eTraGo"] -) +etrago_network = PyPSANetwork(config["eGo"]["csv_import_eTraGo"]) with sshtunnel(config=config): edisgo_networks = EDisGoNetworks(json_file=config, etrago_network=etrago_network) diff --git a/ego/examples/edisgo_workflow/scenario_setting.json b/ego/examples/edisgo_workflow/scenario_setting.json index 73880ec6..13362130 100644 --- a/ego/examples/edisgo_workflow/scenario_setting.json +++ b/ego/examples/edisgo_workflow/scenario_setting.json @@ -4,8 +4,6 @@ "eDisGo": true, "csv_import_eTraGo": false, "csv_import_eDisGo": false, - "data_dir": ".../data_in", - "results_dir": ".../data_out", "random_seed": 42 }, "eTraGo": { @@ -56,7 +54,6 @@ "max_cos_phi_renewable": 0.9, "results": "results_edisgo", "solver": "glpk", - "timesteps_pfa": "snapshot_analysis", "tasks": ["1_setup_grid"] }, "database": { diff --git a/ego/mv_clustering/mv_clustering.py b/ego/mv_clustering/mv_clustering.py index 1efd20ce..0e93570b 100644 --- a/ego/mv_clustering/mv_clustering.py +++ b/ego/mv_clustering/mv_clustering.py @@ -354,10 +354,10 @@ def cluster_workflow(config=None): # determine cluster attributes logger.info("Determine cluster attributes.") attributes_path = os.path.join( - config["eGo"]["results_dir"], "mv_grid_cluster_attributes.csv" + config["eDisGo"]["results"], "mv_grid_cluster_attributes.csv" ) - if not os.path.exists(config["eGo"]["results_dir"]): - os.makedirs(config["eGo"]["results_dir"]) + if not os.path.exists(config["eDisGo"]["results"]): + os.makedirs(config["eDisGo"]["results"]) scenario = config["eTraGo"]["scn_name"] cluster_attributes_df = get_cluster_attributes( attributes_path=attributes_path, scenario=scenario, config=config @@ -368,7 +368,7 @@ def cluster_workflow(config=None): config["eDisGo"]["cluster_attributes"] ] working_grids_path = os.path.join( - config["eGo"]["data_dir"], config["eDisGo"]["grid_path"], "working_grids.csv" + config["eDisGo"]["grid_path"], "working_grids.csv" ) if os.path.isfile(working_grids_path): working_grids = pd.read_csv(working_grids_path, index_col=0) @@ -381,7 +381,7 @@ def cluster_workflow(config=None): cluster_attributes_df, working_grids=working_grids, config=config ) cluster_results_path = os.path.join( - config["eGo"]["results_dir"], "mv_grid_cluster_results.csv" + config["eDisGo"]["results"], "mv_grid_cluster_results_new.csv" ) cluster_df.to_csv(cluster_results_path) return cluster_df diff --git a/ego/scenario_setting.json b/ego/scenario_setting.json index c2495a09..6e4dcbfa 100644 --- a/ego/scenario_setting.json +++ b/ego/scenario_setting.json @@ -4,8 +4,6 @@ "eDisGo": true, "csv_import_eTraGo": false, "csv_import_eDisGo": false, - "data_dir": ".../data_in", - "results_dir": ".../data_out", "random_seed": 42 }, "eTraGo": { @@ -132,7 +130,7 @@ "gridversion": "v0.4.5", "grid_path": "/path/to_your/.dingo/grids", "choice_mode": "cluster", - "cluster_attributes":["farthest_node", "wind_cap", "solar_cap", "extended_storage"], + "cluster_attributes":["pv_capacity_expansion_mw_per_km2", "wind_capacity_expansion_mw_per_km2", "electromobility_max_load_expansion_mw_per_km2", "pth_capacity_expansion_mw_per_km2"], "only_cluster": false, "manual_grids": [], "n_clusters": 2, @@ -142,8 +140,7 @@ "max_cos_phi_renewable": 0.9, "results": "results/another_result", "solver": "gurobi", - "timesteps_pfa": "snapshot_analysis", - "tasks": ["1_setup_grid"] + "tasks": ["1_setup_grid", "2_specs_overlying_grid", "3_temporal_complexity_reduction", "4_optimisation", "5_grid_reinforcement"] }, "database": { "tables": { diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 2c3940a6..680594ad 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -294,7 +294,7 @@ def _init_status(self): * 'end_time' - end time of calculation """ - self._status_dir = os.path.join(self._json_file["eGo"]["results_dir"], "status") + self._status_dir = os.path.join(self._json_file["eDisGo"]["results"], "status") if not os.path.exists(self._status_dir): os.makedirs(self._status_dir) @@ -479,7 +479,6 @@ def _set_scenario_settings(self): # TODO: Integrate into a for-loop self._db_section = self._edisgo_args["db"] self._grid_version = self._edisgo_args["gridversion"] - self._timesteps_pfa = self._edisgo_args["timesteps_pfa"] self._solver = self._edisgo_args["solver"] self._grid_path = self._edisgo_args["grid_path"] self._choice_mode = self._edisgo_args["choice_mode"] @@ -656,7 +655,7 @@ def _run_edisgo_pool(self): """ parallelization = self._parallelization - results_dir = os.path.join(self._json_file["eGo"]["results_dir"], self._results) + results_dir = self._results if not os.path.exists(results_dir): os.makedirs(results_dir) @@ -729,9 +728,7 @@ def run_edisgo(self, mv_grid_id): engine = database.get_engine(config=config) # results directory - results_dir = os.path.join( - config["eGo"]["results_dir"], self._results, str(mv_grid_id) - ) + results_dir = os.path.join(self._results, str(mv_grid_id)) if not os.path.exists(results_dir): os.makedirs(results_dir) @@ -992,7 +989,6 @@ def _run_edisgo_task_setup_grid(self, mv_grid_id, scenario, logger, config, engi logger.info(f"MV grid {mv_grid_id}: Initialize MV grid.") grid_path = os.path.join( - config["eGo"]["data_dir"], config["eDisGo"]["grid_path"], str(mv_grid_id), ) @@ -1458,9 +1454,7 @@ def _run_edisgo_task_temporal_complexity_reduction( edisgo_grid = distribute_overlying_grid_requirements(edisgo_grid) # get critical time intervals - results_dir = os.path.join( - config["eGo"]["results_dir"], self._results, str(edisgo_grid.topology.id) - ) + results_dir = os.path.join(self._results, str(edisgo_grid.topology.id)) time_intervals = get_most_critical_time_intervals( edisgo_grid, percentage=1.0, @@ -1809,7 +1803,7 @@ def _run_edisgo_task_grid_reinforcement(self, edisgo_grid, logger): return edisgo_grid def _save_edisgo_results(self): - results_dir = os.path.join(self._json_file["eGo"]["results_dir"], self._results) + results_dir = self._results if not os.path.exists(results_dir): os.makedirs(results_dir) @@ -1829,7 +1823,7 @@ def _load_edisgo_results(self): """ # Load the grid choice from CSV - results_dir = os.path.join(self._json_file["eGo"]["results_dir"], self._results) + results_dir = self._results self._grid_choice = pd.read_csv( os.path.join(results_dir, "grid_choice.csv"), index_col=0 ) diff --git a/ego/tools/utilities.py b/ego/tools/utilities.py index 81fb0414..289ba5a8 100644 --- a/ego/tools/utilities.py +++ b/ego/tools/utilities.py @@ -203,25 +203,11 @@ def get_scenario_setting(jsonpath=None): else: logger.info("Don't load external config.") - # expand directories - for key in ["data_dir", "results_dir"]: - json_file["eGo"][key] = os.path.expanduser(json_file["eGo"][key]) - # map dirs with "." into data_dir - for key in ["csv_import_eTraGo", "csv_import_eDisGo"]: - if json_file["eGo"][key]: - path = json_file["eGo"][key] - if path[0] == ".": - json_file["eGo"][key] = os.path.join( - json_file["eGo"]["data_dir"], json_file["eGo"][key][2:] - ) - elif path[0] == "~": - json_file["eGo"][key] = os.path.expanduser(json_file["eGo"][key]) - # Serializing json json_object = json.dumps(json_file, indent=4) # Writing to sample.json - results_dir = os.path.join(json_file["eGo"]["results_dir"]) + results_dir = os.path.join(json_file["eDisGo"]["results"]) if not os.path.exists(results_dir): os.makedirs(results_dir) with open(os.path.join(results_dir, "config.json"), "w") as outfile: From 554ccf2cd36829d1bff280d43e23e69b48c6dd98 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 25 May 2023 22:29:02 +0200 Subject: [PATCH 120/126] Adapt function name --- ego/tools/edisgo_integration.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 680594ad..84d028ab 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -60,7 +60,7 @@ ) from edisgo.tools.tools import ( aggregate_district_heating_components, - get_sample_using_time, + reduce_timeseries_data_to_given_timeindex, ) from egoio.tools import db @@ -1653,9 +1653,7 @@ def _run_edisgo_task_optimisation( # copy edisgo object edisgo_copy = deepcopy(edisgo_grid) # temporal complexity reduction - get_sample_using_time( - edisgo_copy, start_date=time_steps[0], periods=len(time_steps) - ) + reduce_timeseries_data_to_given_timeindex(edisgo_copy, time_steps) # spatial complexity reduction edisgo_copy.spatial_complexity_reduction( From 605b65b17347892419e7718fac7d9d7614f04b3c Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 26 May 2023 18:59:59 +0200 Subject: [PATCH 121/126] Adapt test --- tests/data/etrago_test_network_1/links.csv | 36 +++---- .../storage_units-state_of_charge.csv | 4 + tests/data/etrago_test_network_1/stores-e.csv | 4 + .../dispatchable_generators_active_power.csv | 8 +- ...dispatchable_generators_reactive_power.csv | 8 +- .../feedin_district_heating.csv | 4 + .../heat_central_active_power.csv | 4 - .../heat_central_reactive_power.csv | 4 - .../heat_pump_central_active_power.csv | 4 + .../heat_pump_central_reactive_power.csv | 4 + .../renewables_curtailment.csv | 10 +- .../renewables_dispatch_reactive_power.csv | 10 +- .../renewables_p_nom.csv | 3 + .../renewables_potential.csv | 10 +- .../storage_units_soc.csv | 4 + ...ng.csv => thermal_storage_central_soc.csv} | 2 +- ...ting.csv => thermal_storage_rural_soc.csv} | 2 +- tests/tools/test_interface.py | 101 ++++++------------ 18 files changed, 98 insertions(+), 124 deletions(-) create mode 100644 tests/data/etrago_test_network_1/storage_units-state_of_charge.csv create mode 100644 tests/data/etrago_test_network_1/stores-e.csv create mode 100644 tests/data/interface_results_reference_data/feedin_district_heating.csv delete mode 100644 tests/data/interface_results_reference_data/heat_central_active_power.csv delete mode 100644 tests/data/interface_results_reference_data/heat_central_reactive_power.csv create mode 100644 tests/data/interface_results_reference_data/heat_pump_central_active_power.csv create mode 100644 tests/data/interface_results_reference_data/heat_pump_central_reactive_power.csv create mode 100644 tests/data/interface_results_reference_data/renewables_p_nom.csv create mode 100644 tests/data/interface_results_reference_data/storage_units_soc.csv rename tests/data/interface_results_reference_data/{geothermal_energy_feedin_district_heating.csv => thermal_storage_central_soc.csv} (77%) rename tests/data/interface_results_reference_data/{solarthermal_energy_feedin_district_heating.csv => thermal_storage_rural_soc.csv} (68%) diff --git a/tests/data/etrago_test_network_1/links.csv b/tests/data/etrago_test_network_1/links.csv index 368aca62..8e835b47 100644 --- a/tests/data/etrago_test_network_1/links.csv +++ b/tests/data/etrago_test_network_1/links.csv @@ -1,18 +1,18 @@ -name,bus0,bus1,carrier,p_nom_opt -0,1,0,central_gas_CHP,1.0 -1,10,10,central_gas_CHP,10.0 -2,0,3,dsm,1.0 -3,10,10,dsm,10.0 -4,0,4,central_heat_pump,1.0 -5,10,10,central_heat_pump,10.0 -6,0,4,central_resistive_heater,1.0 -7,10,10,central_resistive_heater,10.0 -8,4,5,central_heat_store_charger,1.0 -9,10,10,central_heat_store_charger,10.0 -10,0,6,rural_heat_pump,1.0 -11,10,10,rural_heat_pump,10.0 -12,6,7,rural_heat_store_charger,1.0 -13,10,10,rural_heat_store_charger,10.0 -14,0,3,BEV charger,1.0 -15,10,10,BEV charger,10.0 -16,10,10,junk,100.0 +name,bus0,bus1,carrier,p_nom,efficiency +0,1,0,central_gas_CHP,1.0,1.0 +1,10,10,central_gas_CHP,10.0,1.0 +2,0,3,dsm,1.0,1.0 +3,10,10,dsm,10.0,1.0 +4,0,4,central_heat_pump,1.0,1.0 +5,10,10,central_heat_pump,10.0,1.0 +6,0,4,central_resistive_heater,1.0,1.0 +7,10,10,central_resistive_heater,10.0,1.0 +8,4,5,central_heat_store_charger,1.0,0.84 +9,10,10,central_heat_store_charger,10.0,0.84 +10,0,6,rural_heat_pump,1.0,1.0 +11,10,10,rural_heat_pump,10.0,1.0 +12,6,7,rural_heat_store_charger,1.0,0.8 +13,10,10,rural_heat_store_charger,10.0,0.8 +14,0,3,BEV_charger,1.0,1.0 +15,10,10,BEV_charger,10.0,1.0 +16,10,10,junk,100.0,1.0 diff --git a/tests/data/etrago_test_network_1/storage_units-state_of_charge.csv b/tests/data/etrago_test_network_1/storage_units-state_of_charge.csv new file mode 100644 index 00000000..506a427a --- /dev/null +++ b/tests/data/etrago_test_network_1/storage_units-state_of_charge.csv @@ -0,0 +1,4 @@ +,0 battery,10 battery,10 junk +0,0.0,0.0,0.0 +1,0.5,0.5,0.5 +2,1.0,1.0,1.0 diff --git a/tests/data/etrago_test_network_1/stores-e.csv b/tests/data/etrago_test_network_1/stores-e.csv new file mode 100644 index 00000000..2fac2f91 --- /dev/null +++ b/tests/data/etrago_test_network_1/stores-e.csv @@ -0,0 +1,4 @@ +,5 central_heat_store,10 central_heat_store,7 rural_heat_store,10 rural_heat_store +0,0.0,0.0,0.0,0.0 +1,0.5,0.5,0.5,0.5 +2,1.0,1.0,1.0,1.0 diff --git a/tests/data/interface_results_reference_data/dispatchable_generators_active_power.csv b/tests/data/interface_results_reference_data/dispatchable_generators_active_power.csv index 0ed50dc0..45530ad1 100644 --- a/tests/data/interface_results_reference_data/dispatchable_generators_active_power.csv +++ b/tests/data/interface_results_reference_data/dispatchable_generators_active_power.csv @@ -1,4 +1,4 @@ -snapshot,biomass,central_biomass_CHP,run_of_river,gas,other_non_renewable,reservoir,central_gas_CHP -2011-01-01 00:00:00,0.0,0.0,0.0,0.0,0.0,0.0,0.0 -2011-01-01 12:00:00,0.5,0.5,0.5,0.5,0.5,0.5,0.5 -2011-01-02 00:00:00,1.0,1.0,1.0,1.0,1.0,1.0,1.0 +snapshot,biomass,biomass_CHP,run_of_river,gas,other_non_renewable,junk +2011-01-01 00:00:00,0.0,0.0,0.0,0.0,0.0,0.0 +2011-01-01 12:00:00,0.5,0.5,0.5,0.5,0.5,0.005 +2011-01-02 00:00:00,1.0,1.0,1.0,1.0,1.0,0.01 diff --git a/tests/data/interface_results_reference_data/dispatchable_generators_reactive_power.csv b/tests/data/interface_results_reference_data/dispatchable_generators_reactive_power.csv index 915fb77e..fc3eef2c 100644 --- a/tests/data/interface_results_reference_data/dispatchable_generators_reactive_power.csv +++ b/tests/data/interface_results_reference_data/dispatchable_generators_reactive_power.csv @@ -1,4 +1,4 @@ -snapshot,biomass,central_biomass_CHP,run_of_river,gas,other_non_renewable,reservoir,central_gas_CHP -2011-01-01 00:00:00,0.0,0.0,0.0,0.0,0.0,0.0,0.0 -2011-01-01 12:00:00,0.5,0.5,0.5,0.5,0.5,0.5,0.0 -2011-01-02 00:00:00,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,0.0 +snapshot,biomass,biomass_CHP,run_of_river,gas,other_non_renewable,junk +2011-01-01 00:00:00,0.0,0.0,0.0,0.0,0.0,0.0 +2011-01-01 12:00:00,0.5,0.5,0.5,0.5,0.5,0.005 +2011-01-02 00:00:00,-1.0,-1.0,-1.0,-1.0,-1.0,-0.01 diff --git a/tests/data/interface_results_reference_data/feedin_district_heating.csv b/tests/data/interface_results_reference_data/feedin_district_heating.csv new file mode 100644 index 00000000..49cb0fbf --- /dev/null +++ b/tests/data/interface_results_reference_data/feedin_district_heating.csv @@ -0,0 +1,4 @@ +snapshot,4 +2011-01-01 00:00:00,0.0 +2011-01-01 12:00:00,1.0 +2011-01-02 00:00:00,2.0 diff --git a/tests/data/interface_results_reference_data/heat_central_active_power.csv b/tests/data/interface_results_reference_data/heat_central_active_power.csv deleted file mode 100644 index 80ab6dd6..00000000 --- a/tests/data/interface_results_reference_data/heat_central_active_power.csv +++ /dev/null @@ -1,4 +0,0 @@ -snapshot,central_heat_pump,central_resistive_heater -2011-01-01 00:00:00,0.0,0.0 -2011-01-01 12:00:00,0.5,0.5 -2011-01-02 00:00:00,1.0,1.0 diff --git a/tests/data/interface_results_reference_data/heat_central_reactive_power.csv b/tests/data/interface_results_reference_data/heat_central_reactive_power.csv deleted file mode 100644 index 9ac30ecb..00000000 --- a/tests/data/interface_results_reference_data/heat_central_reactive_power.csv +++ /dev/null @@ -1,4 +0,0 @@ -snapshot,central_heat_pump,central_resistive_heater -2011-01-01 00:00:00,0.0,0.0 -2011-01-01 12:00:00,0.0,0.0 -2011-01-02 00:00:00,0.0,0.0 diff --git a/tests/data/interface_results_reference_data/heat_pump_central_active_power.csv b/tests/data/interface_results_reference_data/heat_pump_central_active_power.csv new file mode 100644 index 00000000..afdd9dd7 --- /dev/null +++ b/tests/data/interface_results_reference_data/heat_pump_central_active_power.csv @@ -0,0 +1,4 @@ +snapshot,central_heat_pump +2011-01-01 00:00:00,0.0 +2011-01-01 12:00:00,1.0 +2011-01-02 00:00:00,2.0 diff --git a/tests/data/interface_results_reference_data/heat_pump_central_reactive_power.csv b/tests/data/interface_results_reference_data/heat_pump_central_reactive_power.csv new file mode 100644 index 00000000..25ef1425 --- /dev/null +++ b/tests/data/interface_results_reference_data/heat_pump_central_reactive_power.csv @@ -0,0 +1,4 @@ +snapshot,central_heat_pump +2011-01-01 00:00:00,0.0 +2011-01-01 12:00:00,0.0 +2011-01-02 00:00:00,0.0 diff --git a/tests/data/interface_results_reference_data/renewables_curtailment.csv b/tests/data/interface_results_reference_data/renewables_curtailment.csv index 2e091ab1..85a8dab8 100644 --- a/tests/data/interface_results_reference_data/renewables_curtailment.csv +++ b/tests/data/interface_results_reference_data/renewables_curtailment.csv @@ -1,6 +1,4 @@ -,solar,solar,wind_onshore,wind_onshore -,0,1,0,1 -snapshot,,,, -2011-01-01 00:00:00,0.5,0.5,0.5,0.5 -2011-01-01 12:00:00,0.25,0.7,0.25,0.7 -2011-01-02 00:00:00,0.0,0.9,0.0,0.9 +snapshot,solar,wind +2011-01-01 00:00:00,11.0,5.5 +2011-01-01 12:00:00,14.5,7.25 +2011-01-02 00:00:00,18.0,9.0 diff --git a/tests/data/interface_results_reference_data/renewables_dispatch_reactive_power.csv b/tests/data/interface_results_reference_data/renewables_dispatch_reactive_power.csv index 6bbad759..36db45a2 100644 --- a/tests/data/interface_results_reference_data/renewables_dispatch_reactive_power.csv +++ b/tests/data/interface_results_reference_data/renewables_dispatch_reactive_power.csv @@ -1,6 +1,4 @@ -,solar,solar,wind_onshore,wind_onshore -,0,1,0,1 -snapshot,,,, -2011-01-01 00:00:00,0.0,0.0,0.0,0.0 -2011-01-01 12:00:00,0.5,0.05,0.5,0.05 -2011-01-02 00:00:00,-1.0,-0.1,-1.0,-0.1 +snapshot,solar,wind +2011-01-01 00:00:00,0.0,0.0 +2011-01-01 12:00:00,0.09091,0.09091 +2011-01-02 00:00:00,-0.18182,-0.18182 diff --git a/tests/data/interface_results_reference_data/renewables_p_nom.csv b/tests/data/interface_results_reference_data/renewables_p_nom.csv new file mode 100644 index 00000000..242760ec --- /dev/null +++ b/tests/data/interface_results_reference_data/renewables_p_nom.csv @@ -0,0 +1,3 @@ +carrier,p_nom +solar,22.0 +wind,11.0 diff --git a/tests/data/interface_results_reference_data/renewables_potential.csv b/tests/data/interface_results_reference_data/renewables_potential.csv index 5aa79156..1bc313f4 100644 --- a/tests/data/interface_results_reference_data/renewables_potential.csv +++ b/tests/data/interface_results_reference_data/renewables_potential.csv @@ -1,6 +1,4 @@ -,solar,solar,wind_onshore,wind_onshore -,0,1,0,1 -snapshot,,,, -2011-01-01 00:00:00,0.5,0.5,0.5,0.5 -2011-01-01 12:00:00,0.75,0.75,0.75,0.75 -2011-01-02 00:00:00,1.0,1.0,1.0,1.0 +snapshot,solar,wind +2011-01-01 00:00:00,0.5,0.5 +2011-01-01 12:00:00,0.75,0.75 +2011-01-02 00:00:00,1.0,1.0 diff --git a/tests/data/interface_results_reference_data/storage_units_soc.csv b/tests/data/interface_results_reference_data/storage_units_soc.csv new file mode 100644 index 00000000..e9f7ec39 --- /dev/null +++ b/tests/data/interface_results_reference_data/storage_units_soc.csv @@ -0,0 +1,4 @@ +snapshot,battery +2011-01-01 00:00:00,0.0 +2011-01-01 12:00:00,0.05 +2011-01-02 00:00:00,0.1 diff --git a/tests/data/interface_results_reference_data/geothermal_energy_feedin_district_heating.csv b/tests/data/interface_results_reference_data/thermal_storage_central_soc.csv similarity index 77% rename from tests/data/interface_results_reference_data/geothermal_energy_feedin_district_heating.csv rename to tests/data/interface_results_reference_data/thermal_storage_central_soc.csv index 27d65dcf..2e622412 100644 --- a/tests/data/interface_results_reference_data/geothermal_energy_feedin_district_heating.csv +++ b/tests/data/interface_results_reference_data/thermal_storage_central_soc.csv @@ -1,4 +1,4 @@ -snapshot,geo_thermal +snapshot,4 2011-01-01 00:00:00,0.0 2011-01-01 12:00:00,0.5 2011-01-02 00:00:00,1.0 diff --git a/tests/data/interface_results_reference_data/solarthermal_energy_feedin_district_heating.csv b/tests/data/interface_results_reference_data/thermal_storage_rural_soc.csv similarity index 68% rename from tests/data/interface_results_reference_data/solarthermal_energy_feedin_district_heating.csv rename to tests/data/interface_results_reference_data/thermal_storage_rural_soc.csv index d4403e1e..4827dda7 100644 --- a/tests/data/interface_results_reference_data/solarthermal_energy_feedin_district_heating.csv +++ b/tests/data/interface_results_reference_data/thermal_storage_rural_soc.csv @@ -1,4 +1,4 @@ -snapshot,solar_thermal_collector +snapshot, 2011-01-01 00:00:00,0.0 2011-01-01 12:00:00,0.5 2011-01-02 00:00:00,1.0 diff --git a/tests/tools/test_interface.py b/tests/tools/test_interface.py index fd270fae..cade5931 100644 --- a/tests/tools/test_interface.py +++ b/tests/tools/test_interface.py @@ -14,17 +14,6 @@ random.seed(42) -def mock_get_weather_id_for_generator(grid_version, session, generator_index, scn_name): - generator_number = int(generator_index.split("_")[-1]) - - if generator_number in [0]: - weather_id = 0 - elif generator_number in [1]: - weather_id = 1 - - return weather_id - - class TestSpecs: @classmethod def setup_class(cls): @@ -34,27 +23,18 @@ def test_class_etrago_minimal_data(self): etrago_network = ETraGoMinimalData(self.etrago_network) assert "p_min_pu" not in etrago_network.generators_t - def test_get_etrago_results_per_bus(self, monkeypatch): - monkeypatch.setattr( - "ego.tools.interface.get_weather_id_for_generator", - mock_get_weather_id_for_generator, - ) + def test_get_etrago_results_per_bus(self): - session = None bus_id = 0 etrago_network = ETraGoMinimalData(self.etrago_network) - grid_version = None - scn_name = "ego 100" pf_post_lopf = True max_cos_phi_renewable = False etrago_results_per_bus = get_etrago_results_per_bus( bus_id, etrago_network, - grid_version, - scn_name, pf_post_lopf, - session=session, + max_cos_phi_renewable, ) for key, value in etrago_results_per_bus.items(): @@ -72,56 +52,55 @@ def test_get_etrago_results_per_bus(self, monkeypatch): name="snapshot", ), ) - elif key == "storage_units_capacity": + elif key == "storage_units_p_nom": + assert value == 1.0 + elif key == "storage_units_max_hours": assert value == 10.0 elif key == "thermal_storage_central_capacity": - assert value == 1.0 + pd.testing.assert_series_equal( + value, pd.Series(index=["4"], data=[1.0]), check_names=False + ) elif key == "thermal_storage_rural_capacity": assert value == 1.0 + elif key == "heat_pump_rural_p_nom": + assert value == 1.0 + elif key == "heat_pump_central_p_nom": + assert value == 2.0 + elif key == "thermal_storage_rural_efficiency": + assert value == 0.8 + elif key == "thermal_storage_central_efficiency": + assert value == 0.84 else: - assert type(value) is pd.DataFrame path_reference_df = os.path.join( pytest.interface_results_reference_data_path, f"{key}.csv" ) - # value.to_csv(path_reference_df) - - if key in [ - "renewables_potential", - "renewables_curtailment", - "renewables_dispatch_reactive_power", - ]: + if isinstance(value, pd.DataFrame): reference_df = pd.read_csv( - path_reference_df, index_col=0, header=[0, 1], parse_dates=True + path_reference_df, index_col=0, parse_dates=True + ) + pd.testing.assert_frame_equal( + value, reference_df, check_index_type=False, check_names=False ) else: - reference_df = pd.read_csv( + reference_s = pd.read_csv( path_reference_df, index_col=0, parse_dates=True + ).iloc[:, 0] + pd.testing.assert_series_equal( + value, reference_s, check_index_type=False, check_names=False ) - pd.testing.assert_frame_equal( - value, reference_df, check_index_type=False, check_names=False - ) - def test_get_etrago_results_per_bus_no_non_linear_pf(self, monkeypatch): - monkeypatch.setattr( - "ego.tools.interface.get_weather_id_for_generator", - mock_get_weather_id_for_generator, - ) + def test_get_etrago_results_per_bus_no_non_linear_pf(self): - session = None bus_id = 0 etrago_network = ETraGoMinimalData(self.etrago_network) - grid_version = None - scn_name = "ego 100" pf_post_lopf = False max_cos_phi_renewable = False etrago_results_per_bus = get_etrago_results_per_bus( bus_id, etrago_network, - grid_version, - scn_name, pf_post_lopf, - session=session, + max_cos_phi_renewable, ) none_results = [ @@ -140,27 +119,18 @@ def test_get_etrago_results_per_bus_no_non_linear_pf(self, monkeypatch): assert len(none_results) == 0 - def test_get_etrago_results_per_bus_empty(self, monkeypatch): - monkeypatch.setattr( - "ego.tools.interface.get_weather_id_for_generator", - mock_get_weather_id_for_generator, - ) + def test_get_etrago_results_per_bus_empty(self): - session = None bus_id = 11 etrago_network = ETraGoMinimalData(self.etrago_network) - grid_version = None - scn_name = "ego 100" pf_post_lopf = True max_cos_phi_renewable = False etrago_results_per_bus = get_etrago_results_per_bus( bus_id, etrago_network, - grid_version, - scn_name, pf_post_lopf, - session=session, + max_cos_phi_renewable, ) none_results = [ @@ -192,27 +162,18 @@ def test_get_etrago_results_per_bus_empty(self, monkeypatch): assert len(none_results) == 0 - def test_get_etrago_results_per_bus_with_set_max_cosphi(self, monkeypatch): - monkeypatch.setattr( - "ego.tools.interface.get_weather_id_for_generator", - mock_get_weather_id_for_generator, - ) + def test_get_etrago_results_per_bus_with_set_max_cosphi(self): - session = None bus_id = 0 etrago_network = ETraGoMinimalData(self.etrago_network) - grid_version = None - scn_name = "ego 100" pf_post_lopf = True max_cos_phi_renewable = 0.9 etrago_results_per_bus = get_etrago_results_per_bus( bus_id, etrago_network, - grid_version, - scn_name, pf_post_lopf, - session=session, + max_cos_phi_renewable, ) for key, value in etrago_results_per_bus.items(): From 30280b0caef7f1974ae7d9c26a4ff6974787645e Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 26 May 2023 20:56:56 +0200 Subject: [PATCH 122/126] Adapt tests --- tests/conftest.py | 4 - ...les_dispatch_reactive_power_max_cosphi.csv | 4 + .../dispatchable_generators_active_power.csv | 4 - ...dispatchable_generators_reactive_power.csv | 4 - .../dsm_active_power.csv | 4 - .../dsm_reactive_power.csv | 4 - .../electromobility_active_power.csv | 4 - .../electromobility_reactive_power.csv | 4 - ...thermal_energy_feedin_district_heating.csv | 4 - .../heat_central_active_power.csv | 4 - .../heat_central_reactive_power.csv | 4 - .../heat_pump_rural_active_power.csv | 4 - .../heat_pump_rural_reactive_power.csv | 4 - .../renewables_curtailment.csv | 6 - .../renewables_dispatch_reactive_power.csv | 6 - .../renewables_potential.csv | 6 - ...thermal_energy_feedin_district_heating.csv | 4 - .../storage_units_active_power.csv | 4 - .../storage_units_reactive_power.csv | 4 - tests/tools/test_interface.py | 142 ++++++------------ 20 files changed, 54 insertions(+), 170 deletions(-) create mode 100644 tests/data/interface_results_reference_data/renewables_dispatch_reactive_power_max_cosphi.csv delete mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/dispatchable_generators_active_power.csv delete mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/dispatchable_generators_reactive_power.csv delete mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/dsm_active_power.csv delete mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/dsm_reactive_power.csv delete mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/electromobility_active_power.csv delete mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/electromobility_reactive_power.csv delete mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/geothermal_energy_feedin_district_heating.csv delete mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/heat_central_active_power.csv delete mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/heat_central_reactive_power.csv delete mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/heat_pump_rural_active_power.csv delete mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/heat_pump_rural_reactive_power.csv delete mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/renewables_curtailment.csv delete mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/renewables_dispatch_reactive_power.csv delete mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/renewables_potential.csv delete mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/solarthermal_energy_feedin_district_heating.csv delete mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/storage_units_active_power.csv delete mode 100644 tests/data/interface_results_reference_data_set_max_cos_phi/storage_units_reactive_power.csv diff --git a/tests/conftest.py b/tests/conftest.py index b9f3e64f..d5a556f8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,10 +11,6 @@ def pytest_configure(config): os.path.realpath(os.path.dirname(__file__)), "data/interface_results_reference_data", ) - pytest.interface_results_reference_data_set_max_cos_phi_path = os.path.join( - os.path.realpath(os.path.dirname(__file__)), - "data/interface_results_reference_data_set_max_cos_phi", - ) config.addinivalue_line("markers", "slow: mark test as slow to run") diff --git a/tests/data/interface_results_reference_data/renewables_dispatch_reactive_power_max_cosphi.csv b/tests/data/interface_results_reference_data/renewables_dispatch_reactive_power_max_cosphi.csv new file mode 100644 index 00000000..b004a539 --- /dev/null +++ b/tests/data/interface_results_reference_data/renewables_dispatch_reactive_power_max_cosphi.csv @@ -0,0 +1,4 @@ +snapshot,solar,wind +2011-01-01 00:00:00,0.0,0.0 +2011-01-01 12:00:00,0.04403,0.04403 +2011-01-02 00:00:00,-0.08806,-0.08806 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/dispatchable_generators_active_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/dispatchable_generators_active_power.csv deleted file mode 100644 index 0ed50dc0..00000000 --- a/tests/data/interface_results_reference_data_set_max_cos_phi/dispatchable_generators_active_power.csv +++ /dev/null @@ -1,4 +0,0 @@ -snapshot,biomass,central_biomass_CHP,run_of_river,gas,other_non_renewable,reservoir,central_gas_CHP -2011-01-01 00:00:00,0.0,0.0,0.0,0.0,0.0,0.0,0.0 -2011-01-01 12:00:00,0.5,0.5,0.5,0.5,0.5,0.5,0.5 -2011-01-02 00:00:00,1.0,1.0,1.0,1.0,1.0,1.0,1.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/dispatchable_generators_reactive_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/dispatchable_generators_reactive_power.csv deleted file mode 100644 index 915fb77e..00000000 --- a/tests/data/interface_results_reference_data_set_max_cos_phi/dispatchable_generators_reactive_power.csv +++ /dev/null @@ -1,4 +0,0 @@ -snapshot,biomass,central_biomass_CHP,run_of_river,gas,other_non_renewable,reservoir,central_gas_CHP -2011-01-01 00:00:00,0.0,0.0,0.0,0.0,0.0,0.0,0.0 -2011-01-01 12:00:00,0.5,0.5,0.5,0.5,0.5,0.5,0.0 -2011-01-02 00:00:00,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,0.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/dsm_active_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/dsm_active_power.csv deleted file mode 100644 index 9a217ea7..00000000 --- a/tests/data/interface_results_reference_data_set_max_cos_phi/dsm_active_power.csv +++ /dev/null @@ -1,4 +0,0 @@ -snapshot,dsm -2011-01-01 00:00:00,0.0 -2011-01-01 12:00:00,0.5 -2011-01-02 00:00:00,1.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/dsm_reactive_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/dsm_reactive_power.csv deleted file mode 100644 index f90539eb..00000000 --- a/tests/data/interface_results_reference_data_set_max_cos_phi/dsm_reactive_power.csv +++ /dev/null @@ -1,4 +0,0 @@ -snapshot,dsm -2011-01-01 00:00:00,0.0 -2011-01-01 12:00:00,0.0 -2011-01-02 00:00:00,0.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/electromobility_active_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/electromobility_active_power.csv deleted file mode 100644 index 4e95b784..00000000 --- a/tests/data/interface_results_reference_data_set_max_cos_phi/electromobility_active_power.csv +++ /dev/null @@ -1,4 +0,0 @@ -snapshot,BEV charger -2011-01-01 00:00:00,0.0 -2011-01-01 12:00:00,0.5 -2011-01-02 00:00:00,1.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/electromobility_reactive_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/electromobility_reactive_power.csv deleted file mode 100644 index cd21048c..00000000 --- a/tests/data/interface_results_reference_data_set_max_cos_phi/electromobility_reactive_power.csv +++ /dev/null @@ -1,4 +0,0 @@ -snapshot,BEV charger -2011-01-01 00:00:00,0.0 -2011-01-01 12:00:00,0.0 -2011-01-02 00:00:00,0.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/geothermal_energy_feedin_district_heating.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/geothermal_energy_feedin_district_heating.csv deleted file mode 100644 index 27d65dcf..00000000 --- a/tests/data/interface_results_reference_data_set_max_cos_phi/geothermal_energy_feedin_district_heating.csv +++ /dev/null @@ -1,4 +0,0 @@ -snapshot,geo_thermal -2011-01-01 00:00:00,0.0 -2011-01-01 12:00:00,0.5 -2011-01-02 00:00:00,1.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/heat_central_active_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/heat_central_active_power.csv deleted file mode 100644 index 80ab6dd6..00000000 --- a/tests/data/interface_results_reference_data_set_max_cos_phi/heat_central_active_power.csv +++ /dev/null @@ -1,4 +0,0 @@ -snapshot,central_heat_pump,central_resistive_heater -2011-01-01 00:00:00,0.0,0.0 -2011-01-01 12:00:00,0.5,0.5 -2011-01-02 00:00:00,1.0,1.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/heat_central_reactive_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/heat_central_reactive_power.csv deleted file mode 100644 index 9ac30ecb..00000000 --- a/tests/data/interface_results_reference_data_set_max_cos_phi/heat_central_reactive_power.csv +++ /dev/null @@ -1,4 +0,0 @@ -snapshot,central_heat_pump,central_resistive_heater -2011-01-01 00:00:00,0.0,0.0 -2011-01-01 12:00:00,0.0,0.0 -2011-01-02 00:00:00,0.0,0.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/heat_pump_rural_active_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/heat_pump_rural_active_power.csv deleted file mode 100644 index 13e33170..00000000 --- a/tests/data/interface_results_reference_data_set_max_cos_phi/heat_pump_rural_active_power.csv +++ /dev/null @@ -1,4 +0,0 @@ -snapshot,rural_heat_pump -2011-01-01 00:00:00,0.0 -2011-01-01 12:00:00,0.5 -2011-01-02 00:00:00,1.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/heat_pump_rural_reactive_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/heat_pump_rural_reactive_power.csv deleted file mode 100644 index b7845c2e..00000000 --- a/tests/data/interface_results_reference_data_set_max_cos_phi/heat_pump_rural_reactive_power.csv +++ /dev/null @@ -1,4 +0,0 @@ -snapshot,rural_heat_pump -2011-01-01 00:00:00,0.0 -2011-01-01 12:00:00,0.0 -2011-01-02 00:00:00,0.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/renewables_curtailment.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/renewables_curtailment.csv deleted file mode 100644 index 2e091ab1..00000000 --- a/tests/data/interface_results_reference_data_set_max_cos_phi/renewables_curtailment.csv +++ /dev/null @@ -1,6 +0,0 @@ -,solar,solar,wind_onshore,wind_onshore -,0,1,0,1 -snapshot,,,, -2011-01-01 00:00:00,0.5,0.5,0.5,0.5 -2011-01-01 12:00:00,0.25,0.7,0.25,0.7 -2011-01-02 00:00:00,0.0,0.9,0.0,0.9 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/renewables_dispatch_reactive_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/renewables_dispatch_reactive_power.csv deleted file mode 100644 index 064b1ce2..00000000 --- a/tests/data/interface_results_reference_data_set_max_cos_phi/renewables_dispatch_reactive_power.csv +++ /dev/null @@ -1,6 +0,0 @@ -,solar,solar,wind_onshore,wind_onshore -,0,1,0,1 -snapshot,,,, -2011-01-01 00:00:00,0.0,0.0,0.0,0.0 -2011-01-01 12:00:00,0.24216105241892627,0.024216105241892626,0.24216105241892627,0.024216105241892626 -2011-01-02 00:00:00,-0.48432210483785254,-0.04843221048378525,-0.48432210483785254,-0.04843221048378525 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/renewables_potential.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/renewables_potential.csv deleted file mode 100644 index 5aa79156..00000000 --- a/tests/data/interface_results_reference_data_set_max_cos_phi/renewables_potential.csv +++ /dev/null @@ -1,6 +0,0 @@ -,solar,solar,wind_onshore,wind_onshore -,0,1,0,1 -snapshot,,,, -2011-01-01 00:00:00,0.5,0.5,0.5,0.5 -2011-01-01 12:00:00,0.75,0.75,0.75,0.75 -2011-01-02 00:00:00,1.0,1.0,1.0,1.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/solarthermal_energy_feedin_district_heating.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/solarthermal_energy_feedin_district_heating.csv deleted file mode 100644 index d4403e1e..00000000 --- a/tests/data/interface_results_reference_data_set_max_cos_phi/solarthermal_energy_feedin_district_heating.csv +++ /dev/null @@ -1,4 +0,0 @@ -snapshot,solar_thermal_collector -2011-01-01 00:00:00,0.0 -2011-01-01 12:00:00,0.5 -2011-01-02 00:00:00,1.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/storage_units_active_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/storage_units_active_power.csv deleted file mode 100644 index afc27bff..00000000 --- a/tests/data/interface_results_reference_data_set_max_cos_phi/storage_units_active_power.csv +++ /dev/null @@ -1,4 +0,0 @@ -snapshot,battery -2011-01-01 00:00:00,0.0 -2011-01-01 12:00:00,0.5 -2011-01-02 00:00:00,1.0 diff --git a/tests/data/interface_results_reference_data_set_max_cos_phi/storage_units_reactive_power.csv b/tests/data/interface_results_reference_data_set_max_cos_phi/storage_units_reactive_power.csv deleted file mode 100644 index afc27bff..00000000 --- a/tests/data/interface_results_reference_data_set_max_cos_phi/storage_units_reactive_power.csv +++ /dev/null @@ -1,4 +0,0 @@ -snapshot,battery -2011-01-01 00:00:00,0.0 -2011-01-01 12:00:00,0.5 -2011-01-02 00:00:00,1.0 diff --git a/tests/tools/test_interface.py b/tests/tools/test_interface.py index cade5931..29f676cd 100644 --- a/tests/tools/test_interface.py +++ b/tests/tools/test_interface.py @@ -89,36 +89,6 @@ def test_get_etrago_results_per_bus(self): value, reference_s, check_index_type=False, check_names=False ) - def test_get_etrago_results_per_bus_no_non_linear_pf(self): - - bus_id = 0 - etrago_network = ETraGoMinimalData(self.etrago_network) - pf_post_lopf = False - max_cos_phi_renewable = False - - etrago_results_per_bus = get_etrago_results_per_bus( - bus_id, - etrago_network, - pf_post_lopf, - max_cos_phi_renewable, - ) - - none_results = [ - "dispatchable_generators_reactive_power", - "renewables_dispatch_reactive_power", - "storage_units_reactive_power", - "dsm_reactive_power", - "heat_central_reactive_power", - "heat_pump_rural_reactive_power", - "electromobility_reactive_power", - ] - - for key, value in etrago_results_per_bus.items(): - if value is None: - none_results.remove(key) - - assert len(none_results) == 0 - def test_get_etrago_results_per_bus_empty(self): bus_id = 11 @@ -133,34 +103,52 @@ def test_get_etrago_results_per_bus_empty(self): max_cos_phi_renewable, ) - none_results = [ - "dispatchable_generators_active_power", - "dispatchable_generators_reactive_power", - "renewables_potential", - "renewables_curtailment", - "renewables_dispatch_reactive_power", - "storage_units_capacity", + float_results = [ + "storage_units_p_nom", + "storage_units_max_hours", + "heat_pump_rural_p_nom", + "heat_pump_central_p_nom", + "thermal_storage_rural_capacity", + "thermal_storage_rural_efficiency", + "thermal_storage_central_efficiency", + ] + series_results = [ + "renewables_p_nom", "storage_units_active_power", "storage_units_reactive_power", + "storage_units_soc", "dsm_active_power", - "dsm_reactive_power", + "heat_pump_rural_active_power", + "heat_pump_rural_reactive_power", + "thermal_storage_rural_soc", "heat_central_active_power", "heat_central_reactive_power", "thermal_storage_central_capacity", - "geothermal_energy_feedin_district_heating", - "solarthermal_energy_feedin_district_heating", - "heat_pump_rural_active_power", - "heat_pump_rural_reactive_power", - "thermal_storage_rural_capacity", "electromobility_active_power", "electromobility_reactive_power", ] + dataframes_results = [ + "dispatchable_generators_active_power", + "dispatchable_generators_reactive_power", + "renewables_potential", + "renewables_curtailment", + "renewables_dispatch_reactive_power", + "thermal_storage_central_soc", + "feedin_district_heating", + ] # for key, value in etrago_results_per_bus.items(): - if value is None: - none_results.remove(key) - - assert len(none_results) == 0 + if key in float_results: + if value == 0.0: + float_results.remove(key) + elif key in series_results: + if value.empty: + series_results.remove(key) + elif key in dataframes_results: + if len(value.columns) == 0: + dataframes_results.remove(key) + + assert len(float_results) == 0 def test_get_etrago_results_per_bus_with_set_max_cosphi(self): @@ -175,48 +163,18 @@ def test_get_etrago_results_per_bus_with_set_max_cosphi(self): pf_post_lopf, max_cos_phi_renewable, ) - - for key, value in etrago_results_per_bus.items(): - logger.info(f"Check Result: {key}") - if key == "timeindex": - assert type(value) is pd.DatetimeIndex - pd.testing.assert_index_equal( - value, - pd.DatetimeIndex( - data=[ - "2011-01-01 00:00:00", - "2011-01-01 12:00:00", - "2011-01-02 00:00:00", - ], - name="snapshot", - ), - ) - elif key == "storage_units_capacity": - assert value == 10.0 - elif key == "thermal_storage_central_capacity": - assert value == 1.0 - elif key == "thermal_storage_rural_capacity": - assert value == 1.0 - else: - assert type(value) is pd.DataFrame - path_reference_df = os.path.join( - pytest.interface_results_reference_data_set_max_cos_phi_path, - f"{key}.csv", - ) - # value.to_csv(path_reference_df) - - if key in [ - "renewables_potential", - "renewables_curtailment", - "renewables_dispatch_reactive_power", - ]: - reference_df = pd.read_csv( - path_reference_df, index_col=0, header=[0, 1], parse_dates=True - ) - else: - reference_df = pd.read_csv( - path_reference_df, index_col=0, parse_dates=True - ) - pd.testing.assert_frame_equal( - value, reference_df, check_index_type=False, check_names=False - ) + renewables_dispatch_reactive_power = etrago_results_per_bus[ + "renewables_dispatch_reactive_power" + ] + path_reference_df = os.path.join( + pytest.interface_results_reference_data_path, + "renewables_dispatch_reactive_power_max_cosphi.csv", + ) + reference_df = pd.read_csv(path_reference_df, index_col=0, parse_dates=True) + pd.testing.assert_frame_equal( + renewables_dispatch_reactive_power, + reference_df, + check_index_type=False, + check_names=False, + atol=1e-4, + ) From 384c5a5242a63cdeeddaa0633aa8903b545fd98c Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 26 May 2023 21:00:16 +0200 Subject: [PATCH 123/126] Remove example --- ego/examples/edisgo_workflow/edisgo_mwe.py | 25 ------ .../edisgo_workflow/scenario_setting.json | 88 ------------------- 2 files changed, 113 deletions(-) delete mode 100644 ego/examples/edisgo_workflow/edisgo_mwe.py delete mode 100644 ego/examples/edisgo_workflow/scenario_setting.json diff --git a/ego/examples/edisgo_workflow/edisgo_mwe.py b/ego/examples/edisgo_workflow/edisgo_mwe.py deleted file mode 100644 index 8298a180..00000000 --- a/ego/examples/edisgo_workflow/edisgo_mwe.py +++ /dev/null @@ -1,25 +0,0 @@ -from edisgo.tools.logger import setup_logger -from pypsa import Network as PyPSANetwork - -from ego.mv_clustering.database import sshtunnel -from ego.tools.edisgo_integration import EDisGoNetworks -from ego.tools.utilities import get_scenario_setting - -config = get_scenario_setting() -results_dir = config["eDisGo"]["results"] -setup_logger( - loggers=[ - {"name": "root", "file_level": "warning", "stream_level": "warning"}, - {"name": "ego", "file_level": "debug", "stream_level": "debug"}, - {"name": "edisgo", "file_level": None, "stream_level": None}, - ], - file_name="ego.log", - log_dir=results_dir, -) - -etrago_network = PyPSANetwork(config["eGo"]["csv_import_eTraGo"]) - -with sshtunnel(config=config): - edisgo_networks = EDisGoNetworks(json_file=config, etrago_network=etrago_network) - -print("THE END") diff --git a/ego/examples/edisgo_workflow/scenario_setting.json b/ego/examples/edisgo_workflow/scenario_setting.json deleted file mode 100644 index 13362130..00000000 --- a/ego/examples/edisgo_workflow/scenario_setting.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "eGo": { - "eTraGo": true, - "eDisGo": true, - "csv_import_eTraGo": false, - "csv_import_eDisGo": false, - "random_seed": 42 - }, - "eTraGo": { - "db": "oedb", - "gridversion": "v0.4.5", - "method": "lopf", - "pf_post_lopf": false, - "start_snapshot": 1000, - "end_snapshot" : 1005, - "solver": "gurobi", - "solver_options":{}, - "scn_name": "eGon2035", - "scn_extension": null, - "scn_decommissioning": null, - "lpfile": false, - "csv_export": "results/your_results", - "db_export": false, - "extendable": ["storage", "network"], - "generator_noise": 789456, - "minimize_loading": false, - "ramp_limits": false, - "extra_functionality": null, - "network_clustering_kmeans": 10, - "load_cluster": false, - "network_clustering_ehv": false, - "disaggregation": "uniform", - "snapshot_clustering": false, - "parallelisation": false, - "skip_snapshots": false, - "line_grouping": false, - "branch_capacity_factor": {"HV": 0.5, "eHV" : 0.7}, - "load_shedding": false, - "foreign_lines" :{"carrier": "AC", "capacity": "osmTGmod"}, - "comments": "" - }, - "eDisGo": { - "db": "oedb", - "gridversion": "v0.5.0", - "grid_path": "grids", - "choice_mode": "cluster", - "cluster_attributes":["pv_capacity_mw_per_km2", "wind_capacity_mw_per_km2", "electromobility_max_load_mw_per_km2", "pth_capacity_mw_per_km2"], - "only_cluster": false, - "manual_grids": [32355], - "n_clusters": 50, - "parallelization":false, - "max_calc_time": 0.5, - "max_workers":1, - "max_cos_phi_renewable": 0.9, - "results": "results_edisgo", - "solver": "glpk", - "tasks": ["1_setup_grid"] - }, - "database": { - "tables": { - "egon_mv_grid_district": "grid.egon_mv_grid_district", - "generators_pv_status_quo": "supply.egon_power_plants_pv", - "generators_pv_rooftop": "supply.egon_power_plants_pv_roof_building", - "generators_wind_status_quo": "supply.egon_power_plants_wind", - "generators": "supply.egon_power_plants", - "etrago_load": "grid.egon_etrago_load", - "etrago_load_timeseries": "grid.egon_etrago_load_timeseries", - "heat_pump_capacity_individual": "supply.egon_individual_heating", - "pth_capacity_district_heating": "grid.egon_etrago_link", - "district_heating_areas": "demand.egon_district_heating_areas", - "etrago_bus": "grid.egon_etrago_bus" - }, - "database_name": "", - "host": "127.0.0.1", - "port": "59700", - "user": "", - "password": "" - }, - "ssh": { - "enabled": true, - "user": "", - "ip": "", - "port": "", - "local_address": "127.0.0.1", - "local_port": "59700" - }, - "external_config": "~/.ego/secondary_ego_config.json" -} From 64efce895713aea2d50b1d7c13a5297f4b845cb7 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 26 May 2023 21:23:19 +0200 Subject: [PATCH 124/126] Move database tables from config to database file --- ego/mv_clustering/database.py | 14 ++++++++++++-- ego/mv_clustering/mv_clustering.py | 2 +- ego/scenario_setting.json | 11 ----------- ego/tools/edisgo_integration.py | 2 +- ego/tools/interface.py | 4 ++-- 5 files changed, 16 insertions(+), 17 deletions(-) diff --git a/ego/mv_clustering/database.py b/ego/mv_clustering/database.py index 5dc424be..92cec635 100644 --- a/ego/mv_clustering/database.py +++ b/ego/mv_clustering/database.py @@ -89,8 +89,18 @@ def wrapper(*args, **kwargs): return wrapper -def register_tables_in_saio(engine, config=None): - db_tables = config["database"]["tables"] +def register_tables_in_saio(engine): + db_tables = { + "egon_mv_grid_district": "grid.egon_mv_grid_district", + "generators_pv_status_quo": "supply.egon_power_plants_pv", + "generators_pv_rooftop": "supply.egon_power_plants_pv_roof_building", + "generators_wind_status_quo": "supply.egon_power_plants_wind", + "generators": "supply.egon_power_plants", + "etrago_load": "grid.egon_etrago_load", + "etrago_load_timeseries": "grid.egon_etrago_load_timeseries", + "heat_pump_capacity_individual": "supply.egon_individual_heating", + "pth_capacity_district_heating": "grid.egon_etrago_link", + } orm = {} for name, table_str in db_tables.items(): diff --git a/ego/mv_clustering/mv_clustering.py b/ego/mv_clustering/mv_clustering.py index 0e93570b..18cd63a0 100644 --- a/ego/mv_clustering/mv_clustering.py +++ b/ego/mv_clustering/mv_clustering.py @@ -107,7 +107,7 @@ def get_cluster_attributes(attributes_path, scenario, config=None): # get attributes from database with sshtunnel(config=config): engine = get_engine(config=config) - orm = register_tables_in_saio(engine, config=config) + orm = register_tables_in_saio(engine) grid_ids_df = db_io.get_grid_ids(engine=engine, orm=orm) solar_capacity_df = db_io.get_solar_capacity( diff --git a/ego/scenario_setting.json b/ego/scenario_setting.json index 6e4dcbfa..426d0302 100644 --- a/ego/scenario_setting.json +++ b/ego/scenario_setting.json @@ -143,17 +143,6 @@ "tasks": ["1_setup_grid", "2_specs_overlying_grid", "3_temporal_complexity_reduction", "4_optimisation", "5_grid_reinforcement"] }, "database": { - "tables": { - "egon_mv_grid_district": "grid.egon_mv_grid_district", - "generators_pv_status_quo": "supply.egon_power_plants_pv", - "generators_pv_rooftop": "supply.egon_power_plants_pv_roof_building", - "generators_wind_status_quo": "supply.egon_power_plants_wind", - "generators": "supply.egon_power_plants", - "etrago_load": "grid.egon_etrago_load", - "etrago_load_timeseries": "grid.egon_etrago_load_timeseries", - "heat_pump_capacity_individual": "supply.egon_individual_heating", - "pth_capacity_district_heating": "grid.egon_etrago_link" - }, "database_name": "", "host": "127.0.0.1", "port": "59700", diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index 84d028ab..a1443171 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -1254,7 +1254,7 @@ def _run_edisgo_task_specs_overlying_grid( scn = scenario.split("_")[0] else: scn = scenario - map_etrago_heat_bus_to_district_heating_id(specs, scn, config, engine) + map_etrago_heat_bus_to_district_heating_id(specs, scn, engine) for dh_id in hp_dh.district_heating_id.unique(): if dh_id in specs["thermal_storage_central_capacity"].index: diff --git a/ego/tools/interface.py b/ego/tools/interface.py index ae409294..e5781087 100644 --- a/ego/tools/interface.py +++ b/ego/tools/interface.py @@ -791,7 +791,7 @@ def rename_generator_carriers_edisgo(edisgo_grid): generators_df.loc[gens_rename.index, "type"] = "others" -def map_etrago_heat_bus_to_district_heating_id(specs, scenario, config, engine): +def map_etrago_heat_bus_to_district_heating_id(specs, scenario, engine): """ Helper function to rename heat bus ID from eTraGo to district heating ID used in eDisGo for specifications from overlying grid on district heating feed-in, @@ -799,7 +799,7 @@ def map_etrago_heat_bus_to_district_heating_id(specs, scenario, config, engine): """ # map district heating ID to heat bus ID from eTraGo - orm = database.register_tables_in_saio(engine, config=config) + orm = database.register_tables_in_saio(engine) heat_buses = [int(_) for _ in specs["feedin_district_heating"].columns] with database.session_scope(engine) as session: # get srid of etrago_bus table From 34cb190f88c62e806fff77e7a4ceada60202043e Mon Sep 17 00:00:00 2001 From: birgits Date: Sat, 27 May 2023 21:15:50 +0200 Subject: [PATCH 125/126] Remove costs --- ego/data/__init__.py | 6 ---- ego/data/investment_costs.csv | 18 ---------- .../investment_costs_of_grid_ measures.csv | 34 ------------------- 3 files changed, 58 deletions(-) delete mode 100644 ego/data/__init__.py delete mode 100644 ego/data/investment_costs.csv delete mode 100644 ego/data/investment_costs_of_grid_ measures.csv diff --git a/ego/data/__init__.py b/ego/data/__init__.py deleted file mode 100644 index c28ee935..00000000 --- a/ego/data/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -""" - -__copyright__ = "Europa-Universität Flensburg, Centre for Sustainable Energy Systems" -__license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" -__author__ = "wolf_bunke" diff --git a/ego/data/investment_costs.csv b/ego/data/investment_costs.csv deleted file mode 100644 index 3793547b..00000000 --- a/ego/data/investment_costs.csv +++ /dev/null @@ -1,18 +0,0 @@ -carriers,Status Quo,NEP 2035,eGo 100,source -gas,1384,1322,1280, https://www.diw.de/documents/publikationen/73/diw_01.c.424566.de/diw_datadoc_2013-068.pdf page 75 -lignite,2862,2718,2620, https://www.diw.de/documents/publikationen/73/diw_01.c.424566.de/diw_datadoc_2013-068.pdf page 75 -waste,1800,1800,1800, https://www.diw.de/documents/publikationen/73/diw_01.c.424566.de/diw_datadoc_2013-068.pdf page 75 -oil,400,400,400, https://www.diw.de/documents/publikationen/73/diw_01.c.424566.de/diw_datadoc_2013-068.pdf page 75 -uranium,6000,6000,6000, https://www.diw.de/documents/publikationen/73/diw_01.c.424566.de/diw_datadoc_2013-068.pdf page 75 -biomass,2424,2141,1951, https://www.diw.de/documents/publikationen/73/diw_01.c.424566.de/diw_datadoc_2013-068.pdf page 75 -eeg_gas,2424,2141,1951, https://www.diw.de/documents/publikationen/73/diw_01.c.424566.de/diw_datadoc_2013-068.pdf page 75 -coal,1800,1800,1800, https://www.diw.de/documents/publikationen/73/diw_01.c.424566.de/diw_datadoc_2013-068.pdf page 75 -run_of_river,3000,3000,3000, https://www.diw.de/documents/publikationen/73/diw_01.c.424566.de/diw_datadoc_2013-068.pdf page 75 -reservoir,2000,2000,2000, https://www.diw.de/documents/publikationen/73/diw_01.c.424566.de/diw_datadoc_2013-068.pdf page 75 -pumped_storage,2000,2000,2000, https://www.diw.de/documents/publikationen/73/diw_01.c.424566.de/diw_datadoc_2013-068.pdf page 75 -solar,950,555,425, https://www.diw.de/documents/publikationen/73/diw_01.c.424566.de/diw_datadoc_2013-068.pdf page 75 -wind_onshore,1269,1154,1075, https://www.diw.de/documents/publikationen/73/diw_01.c.424566.de/diw_datadoc_2013-068.pdf page 75 -geothermal,3982,3216,2740, https://www.diw.de/documents/publikationen/73/diw_01.c.424566.de/diw_datadoc_2013-068.pdf page 75 -other_non_renewable,3982,3216,2740, https://www.diw.de/documents/publikationen/73/diw_01.c.424566.de/diw_datadoc_2013-068.pdf page 75 -extendable_storage,100000,100000,100000, https://www.diw.de/documents/publikationen/73/diw_01.c.424566.de/diw_datadoc_2013-068.pdf page 75 -wind_offshore,2868,2396,2093, https://www.diw.de/documents/publikationen/73/diw_01.c.424566.de/diw_datadoc_2013-068.pdf page 75 diff --git a/ego/data/investment_costs_of_grid_ measures.csv b/ego/data/investment_costs_of_grid_ measures.csv deleted file mode 100644 index 825aecb4..00000000 --- a/ego/data/investment_costs_of_grid_ measures.csv +++ /dev/null @@ -1,34 +0,0 @@ -id,Spannungsebene,Anlage/Anlagenteil,Maßnahme," -Investionskosten ",Einheit,Bemerkung,Literatur,Source -1,220,AC-Freileitungen,"220-kV-Stromkreisauflage/ -Umbeseilung","0,15",Mio. €/km,auf Bestandsleitung pro Stromkreis,NEP 2025,https://www.netzentwicklungsplan.de/sites/default/files/paragraphs-files/kostenschaetzungen_nep_2025_1_entwurf.pdf -2,380,AC-Freileitungen,"380-kV-Stromkreisauflage/ -Umbeseilung","0,20",Mio. €/km,auf Bestandsleitung pro Stromkreis,NEP 2025,https://www.netzentwicklungsplan.de/sites/default/files/paragraphs-files/kostenschaetzungen_nep_2025_1_entwurf.pdf -3,380,AC-Freileitungen,"380-kV-Neubau in bestehender -Trasse Doppelleitung","1,6",Mio. €/km,inkl. Rückbau der bestehenden Trasse,NEP 2025,https://www.netzentwicklungsplan.de/sites/default/files/paragraphs-files/kostenschaetzungen_nep_2025_1_entwurf.pdf -4,380,AC-Freileitungen,380-kV-Neubau in Doppelleitung,"1,5",Mio. €/km,"Neubautrasse, Hochstrom",NEP 2025,https://www.netzentwicklungsplan.de/sites/default/files/paragraphs-files/kostenschaetzungen_nep_2025_1_entwurf.pdf -5,,DC-Freileitungen,Neubau DC-Freileitung*,"1,5",Mio. €/km,Neubautrasse mit bis zu 4 GW,NEP 2025,https://www.netzentwicklungsplan.de/sites/default/files/paragraphs-files/kostenschaetzungen_nep_2025_1_entwurf.pdf -6,,DC-Freileitungen,Umstellung Freileitung AC → DC,"0,20",Mio. €/km,"AC-Bestandsleitung, Stromkreisauflage DC -(Nachbeseilung), Kosten pro Stromkreis",NEP 2025,https://www.netzentwicklungsplan.de/sites/default/files/paragraphs-files/kostenschaetzungen_nep_2025_1_entwurf.pdf -7,,DC-Erdkabel,Neubau DC-Erdkabel,"4,00",Mio. €/km,"Neubautrasse mit 2 GW bei durchschnittlichen -Gegebenheiten",NEP 2025,https://www.netzentwicklungsplan.de/sites/default/files/paragraphs-files/kostenschaetzungen_nep_2025_1_entwurf.pdf -8,,DC-Erdkabel,Neubau DC-Erdkabel,"8,00",Mio. €/km,"Neubautrasse mit 2 x 2 GW bei durchschnittlichen -Gegebenheiten",NEP 2025,https://www.netzentwicklungsplan.de/sites/default/files/paragraphs-files/kostenschaetzungen_nep_2025_1_entwurf.pdf -9,380,AC-Stationen,380-kV-Schaltfeld,"4,00",Mio. €/SF,inkl. anlagenanteiliger Infrastruktur,NEP 2025,https://www.netzentwicklungsplan.de/sites/default/files/paragraphs-files/kostenschaetzungen_nep_2025_1_entwurf.pdf -10,,DC-Stationen,DC-Konverterstation,"0,20",Mio. €/MW,"pro Konverterstation inkl. Kosten des/der -AC-Anschluss-SF, Kosten für VSC-Umrichter, -die ausschließlich zum Einsatz kommen -sollen (vorher Kosten für LCC-Umrichter)",NEP 2025,https://www.netzentwicklungsplan.de/sites/default/files/paragraphs-files/kostenschaetzungen_nep_2025_1_entwurf.pdf -11,380,"Kompensations -anlagen",380-kV-MSCDN,"1,4",Mio. €/Stück,100 Mvar schaltbarere Kondensator (ohne SF),NEP 2025,https://www.netzentwicklungsplan.de/sites/default/files/paragraphs-files/kostenschaetzungen_nep_2025_1_entwurf.pdf -12,380,"Kompensations -anlagen",380-kV-SVC,"4,0",Mio. €/Stück,"100 Mvar regelbare Kompensation: -inkl. Anpasstransformator (ohne SF)",NEP 2025,https://www.netzentwicklungsplan.de/sites/default/files/paragraphs-files/kostenschaetzungen_nep_2025_1_entwurf.pdf -13,380,"Kompensations -anlagen",380-kV-Kompensationsspule,"1,5",Mio. €/Stück,100 Mvar Drosselspule (ohne SF),NEP 2025,https://www.netzentwicklungsplan.de/sites/default/files/paragraphs-files/kostenschaetzungen_nep_2025_1_entwurf.pdf -14,380/110,380/110-kV-Transformatoren,300 MVA,"5,2",Mio. €/Stück,"inkl. 110-kV-Schaltfeld und Kabelableitung -(ohne 380-kV-Schaltfeld)",NEP 2025,https://www.netzentwicklungsplan.de/sites/default/files/paragraphs-files/kostenschaetzungen_nep_2025_1_entwurf.pdf -15,380/110,"380/220-kV- -Transformatoren",600 MVA,"8,5",Mio. €/Stück,inkl. Nebenanlagen (ohne 380-kV-Schaltfeld),NEP 2025,https://www.netzentwicklungsplan.de/sites/default/files/paragraphs-files/kostenschaetzungen_nep_2025_1_entwurf.pdf -16,110,"AC-Freileitungen",110-kV-Stromkreisauflage/Umbeseilung,"0,06",Mio. €/km,auf Bestandsleitung pro Stromkreis, dena Verteilnetzstudie 2030 S.146 ,https://shop.dena.de/sortiment/detail/produkt/dena-verteilnetzstudie-ausbau-und-innovationsbedarf-der-stromverteilnetze-in-deutschland-bis-2030/ -17,110,"AC-Freileitungen",110-kV-Neubau in bestehender Trasse Doppelleitung,"0,52",Mio. €/km,inkl. Rückbau der bestehenden Trasse,dena Verteilnetzstudie 2030 S.146 ,https://shop.dena.de/sortiment/detail/produkt/dena-verteilnetzstudie-ausbau-und-innovationsbedarf-der-stromverteilnetze-in-deutschland-bis-2030/ From 566bd66589376b5c1d347379cb7dacd59a428192 Mon Sep 17 00:00:00 2001 From: birgits Date: Sat, 27 May 2023 21:22:36 +0200 Subject: [PATCH 126/126] Update dependencies and scenario_settings --- doc/api/modules.rst | 12 +++++------- doc/conf.py | 31 +++++++++++++++++-------------- ego/scenario_setting.json | 5 ++--- ego/tools/edisgo_integration.py | 26 -------------------------- requirements.txt | 2 -- setup.py | 7 ++----- 6 files changed, 26 insertions(+), 57 deletions(-) diff --git a/doc/api/modules.rst b/doc/api/modules.rst index 7a416ba0..ed06c8c5 100644 --- a/doc/api/modules.rst +++ b/doc/api/modules.rst @@ -49,22 +49,20 @@ The file can be found on This section of :json:object:`scenario_setting.json` contains all input parameters for the eDisGo tool and the clustering of MV grids. - :property string db: Name of your database (e.g.``''oedb''``). eDisGo queries generator data from this database. Please note that this parameters is automatically overwritten in eDisGo's configuration files. - :property string gridversion: ``null`` or *open_eGo* dataset version (e.g. ``''v0.4.5''``). If ``null``, *open_eGo*'s model_draft is used. Please note that this parameters is automatically overwritten in eDisGo's configuration files. + :property string gridversion: This parameter is currently not used. :property string grid_path: Path to the MV grid files (created by `ding0 `_) (e.g. ``''data/MV_grids/20180713110719''``) :property string choice_mode: Mode that eGo uses to chose MV grids out of the files in **grid_path** (e.g. ``''manual''``, ``''cluster''`` or ``''all''``). If ``''manual''`` is chosen, the parameter **manual_grids** must contain a list of the desired grids. If ``''cluster''`` is chosen, **no_grids** must specify the desired number of clusters and **cluster_attributes** must specify the applied cluster attributes. If ``''all''`` is chosen, all MV grids from **grid_path** are calculated. - :property list cluster_attributes: List of strings containing the desired cluster attributes. Available attributes are: ``''farthest_node''``, ``''wind_cap''``, ``''solar_cap''`` and ``''extended_storage''``, thus an exemplary list looks like ``["farthest_node", "wind_cap", "solar_cap", "extended_storage"]``. ``''farthest_node''`` represents the longest path within each grid, ``''wind_cap''`` the installed wind capacity within each grid, ``''solar_cap''`` the installed solar capacity within each grid and ``''extended_storage''`` the installed storage units (as calculated by eTraGo). Please note that ``''extended_storage''`` is only available in combination with eTraGo datasets that optimized storage extension. Otherwise this attribute is ignored. + :property list cluster_attributes: List of strings containing the desired cluster attributes. Available attributes are all attributes returned from :py:func:`~ego.mv_clustering.mv_clustering.get_cluster_attributes. :property bool only_cluster: If ``true``, eGo only identifies cluster results, but performs no eDisGo run. Please note that for **only_cluster** an eTraGo run or dataset must be provided. - :property list manual_grids: List of MV grid ID's (*open_eGo* HV/MV substation ID's) is case of **choice_mode** = ``''manual''`` (e.g. ``[1718,1719]``). Ohterwise this parameter is ignored. - :property int no_grids: Number of MV grid clusters (from all grids in **grid_path**, a specified number of representative clusters is calculated) in case of **choice_mode** = ``''cluster''``. Otherwise this parameter is ignored. + :property list manual_grids: List of MV grid ID's in case of **choice_mode** = ``''manual''`` (e.g. ``[1718,1719]``). Ohterwise this parameter is ignored. + :property int n_clusters: Number of MV grid clusters (from all grids in **grid_path**, a specified number of representative clusters is calculated) in case of **choice_mode** = ``''cluster''``. Otherwise this parameter is ignored. :property bool parallelization: If ``false``, eDisgo is used in a consecutive way (this may take very long time). In order to increase the performance of MV grid simulations, ``true`` allows the parallel calculation of MV grids. If **parallelization** = ``true``, **max_calc_time** and **max_workers** must be specified. :property float max_calc_time: Maximum calculation time in hours for eDisGo simulations. The calculation is terminated after this time and all costs are extrapolated based on the unfinished simulation. Please note that this parameter is only used if **parallelization** = ``true``. :property ing max_workers: Number of workers (cpus) that are allocated to the simulation. If the given value exceeds the number of available workers, it is reduced to the number of available workers. Please note that this parameter is only used if **parallelization** = ``true``. - :property bool initial_reinforcement: This parameter must be set ``true``. :property float max_cos_phi_renewable: Maximum power factor for wind and solar generators in MV grids (e.g. ``0.9``). If the reactive power (as calculated by eTraGo) exceeds this power factor, the reactive power is reduced in order to reach the power factor conditions. :property string solver: Solver eDisGo uses to optimize the curtailment and storage integration (e.g. ``''gurobi''``). - :property string timesteps_pfa: Method eDisGo uses for the storage integration (e.g. ``''snapshot_analysis''``). :property string results: Path to folder where eDisGo's results will be saved. + :property list tasks: List of string defining the tasks to run. The eDisGo calculation for each MV grid can be devided into separate tasks which is helpful in case one tasks fails and calculations do not need to started in the beginning. The following tasks exist: ``''1_setup_grid''``, ``''2_specs_overlying_grid''``, ``''3_temporal_complexity_reduction''``, ``''4_optimisation''``, ``''5_grid_reinforcement''``. diff --git a/doc/conf.py b/doc/conf.py index 0f2d2351..3a6bd854 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -6,7 +6,11 @@ The documentation is available on RTD: https://openego.readthedocs.io""" -__copyright__ = "Flensburg University of Applied Sciences, Europa-Universität Flensburg, Centre for Sustainable Energy Systems, DLR-Institute for Networked Energy Systems" +__copyright__ = ( + "Flensburg University of Applied Sciences, Europa-Universität " + "Flensburg, Centre for Sustainable Energy Systems, DLR-Institute " + "for Networked Energy Systems" +) __license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)" __author__ = "wolf_bunke" @@ -26,11 +30,12 @@ # serve to show the default. import os -import shlex import sys from unittest.mock import MagicMock +import sphinx_rtd_theme + # from mock import Mock as MagicMock # If extensions (or modules to document with autodoc) are in another directory, @@ -56,9 +61,7 @@ "sphinx.ext.imgmath", "sphinx.ext.viewcode", "sphinx.ext.autosummary", - # 'sphinxcontrib.napoleon',#enable Napoleon interpreter of docstrings Sphinx v<=1.2 "sphinx.ext.napoleon", # enable Napoleon Sphinx v>1.3 - # 'sphinx_paramlinks',#to have links to the types of the parameters of the functions "numpydoc", "sphinxcontrib.httpdomain", # for restfull API "sphinxcontrib.autohttp.flask", @@ -95,6 +98,7 @@ "shapely": ("http://toblerity.org/shapely/manual.html#%s", "Shapely object"), } + # test oedb implementation def rstjinja(app, docname, source): """ @@ -117,7 +121,6 @@ def setup(app): # oep_url= 'http://oep.iks.cs.ovgu.de/' # get data from oedb test -# power_class = requests.get(oep_url+'/api/v0/schema/model_draft/tables/ego_power_class/rows/', ).json() # import json # path = os.getcwd() @@ -173,7 +176,7 @@ def setup(app): # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: @@ -213,8 +216,10 @@ def setup(app): todo_include_todos = True -# Fix import error of modules which depend on C modules (mock out the imports for these modules) -# see http://read-the-docs.readthedocs.io/en/latest/faq.html#i-get-import-errors-on-libraries-that-depend-on-c-modules +# Fix import error of modules which depend on C modules (mock out the imports for +# these modules) +# see http://read-the-docs.readthedocs.io/en/latest/faq.html#i-get-import- +# errors-on-libraries-that-depend-on-c-modules if "READTHEDOCS" in os.environ: @@ -246,8 +251,6 @@ def __getattr__(cls, name): # a list of builtin themes. # html_theme = 'alabaster' -import sphinx_rtd_theme - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] html_theme = "sphinx_rtd_theme" @@ -347,13 +350,13 @@ def __getattr__(cls, name): latex_elements = { # The paper size ('letterpaper' or 'a4paper'). - #'papersize': 'letterpaper', + # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). - #'pointsize': '10pt', + # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. - #'preamble': '', + # 'preamble': '', # Latex figure (float) alignment - #'figure_align': 'htbp', + # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples diff --git a/ego/scenario_setting.json b/ego/scenario_setting.json index 426d0302..768fea97 100644 --- a/ego/scenario_setting.json +++ b/ego/scenario_setting.json @@ -126,8 +126,6 @@ "comments": null }, "eDisGo": { - "db": "oedb", - "gridversion": "v0.4.5", "grid_path": "/path/to_your/.dingo/grids", "choice_mode": "cluster", "cluster_attributes":["pv_capacity_expansion_mw_per_km2", "wind_capacity_expansion_mw_per_km2", "electromobility_max_load_expansion_mw_per_km2", "pth_capacity_expansion_mw_per_km2"], @@ -140,7 +138,8 @@ "max_cos_phi_renewable": 0.9, "results": "results/another_result", "solver": "gurobi", - "tasks": ["1_setup_grid", "2_specs_overlying_grid", "3_temporal_complexity_reduction", "4_optimisation", "5_grid_reinforcement"] + "tasks": ["1_setup_grid", "2_specs_overlying_grid", "3_temporal_complexity_reduction", "4_optimisation", "5_grid_reinforcement"], + "gridversion": "v0.4.5" }, "database": { "database_name": "", diff --git a/ego/tools/edisgo_integration.py b/ego/tools/edisgo_integration.py index a1443171..f30b9cf6 100644 --- a/ego/tools/edisgo_integration.py +++ b/ego/tools/edisgo_integration.py @@ -46,8 +46,6 @@ import multiprocess as mp2 import pandas as pd -from sqlalchemy.orm import scoped_session, sessionmaker - if "READTHEDOCS" not in os.environ: from edisgo.edisgo import import_edisgo_from_files from edisgo.flex_opt.reinforce_grid import enhanced_reinforce_grid @@ -62,7 +60,6 @@ aggregate_district_heating_components, reduce_timeseries_data_to_given_timeindex, ) - from egoio.tools import db from ego.mv_clustering import cluster_workflow, database from ego.tools.economics import edisgo_grid_investment @@ -378,21 +375,6 @@ def _update_edisgo_configs(self, edisgo_grid): self._suppress_log = False # Only in the first run warnings and # info get thrown - # Database section - ego_db = self._db_section - edisgo_db = edisgo_grid.network.config["db_connection"]["section"] - - if not ego_db == edisgo_db: - if not self._suppress_log: - logger.warning( - ( - "eDisGo database configuration (db: '{}') " - + "will be overwritten with database configuration " - + "from eGo's scenario settings (db: '{}')" - ).format(edisgo_db, ego_db) - ) - edisgo_grid.network.config["db_connection"]["section"] = ego_db - # Versioned ego_gridversion = self._grid_version if ego_gridversion is None: @@ -477,7 +459,6 @@ def _set_scenario_settings(self): # Reading all eDisGo settings # TODO: Integrate into a for-loop - self._db_section = self._edisgo_args["db"] self._grid_version = self._edisgo_args["gridversion"] self._solver = self._edisgo_args["solver"] self._grid_path = self._edisgo_args["grid_path"] @@ -538,11 +519,6 @@ def _cluster_mv_grids(self): def _identify_extended_storages(self): - conn = db.connection(section=self._db_section) - session_factory = sessionmaker(bind=conn) - Session = scoped_session(session_factory) - session = Session() - all_mv_grids = self._check_available_mv_grids() storages = pd.DataFrame(index=all_mv_grids, columns=["storage_p_nom"]) @@ -570,8 +546,6 @@ def _identify_extended_storages(self): storages.at[mv_grid, "storage_p_nom"] = stor_p_nom - Session.remove() - return storages def _check_available_mv_grids(self): diff --git a/requirements.txt b/requirements.txt index 98d8ba1d..4e199d06 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,8 +2,6 @@ # Using single requirments for docs, see: # https://github.com/rtfd/readthedocs.org/issues/2070 sphinx_rtd_theme -pandas >=0.20.3, <=0.20.3 -pypsa >= 0.11.0, <= 0.11.0 numpy numpydoc sphinxcontrib-httpdomain diff --git a/setup.py b/setup.py index 8d84b9cb..4a984284 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ def read(fname): "flake8", ] -doc_req = [] +doc_req = ["numpydoc", "sphinxcontrib.httpdomain", "sphinx-jsondomain"] full_req = list(set(dev_req + doc_req)) @@ -55,9 +55,6 @@ def read(fname): install_requires=req, extras_require=extras, package_data={ - "ego": [os.path.join("tools", "*.csv")] - + [os.path.join("tools", "*.json")] - + [os.path.join("", "*.json")], - "ego.data": ["*.csv"], + "ego": [os.path.join("tools", "*.json")] + [os.path.join("", "*.json")], }, )