diff --git a/.gitignore b/.gitignore index 3c1523de2..3996726fc 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,9 @@ eDisGo.egg-info/ /edisgo/examples/Exemplary_PyPSA_line_results.csv # exclude directories -/edisgo/examples/edisgo2pypsa_export/* \ No newline at end of file +/edisgo/examples/edisgo2pypsa_export/* +/edisgo/examples/data/ + +# exclude check scripts +/edisgo/examples/compare_graphs.py +/edisgo/examples/compare_pypsa_network.py \ No newline at end of file diff --git a/doc/api.rst b/doc/api.rst index c9ca8fcfd..dc8ad4b52 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -1,5 +1,7 @@ +.. _api: + API -~~~ +=== .. make doc-string generated documentation appear here diff --git a/doc/data_sources.rst b/doc/data_sources.rst new file mode 100644 index 000000000..e55ebaed0 --- /dev/null +++ b/doc/data_sources.rst @@ -0,0 +1,9 @@ +.. _data-sources: + +Data sources +============ + + +Input parameter +--------------- + diff --git a/doc/dev_notes.rst b/doc/dev_notes.rst index 8b00857d6..8d61fb582 100644 --- a/doc/dev_notes.rst +++ b/doc/dev_notes.rst @@ -1,3 +1,5 @@ +.. _dev-notes: + Notes to developers =================== diff --git a/doc/features_in_detail.rst b/doc/features_in_detail.rst new file mode 100644 index 000000000..2c6ea2f3c --- /dev/null +++ b/doc/features_in_detail.rst @@ -0,0 +1,10 @@ +.. _features-in-detail: + +Features in detail +================== + +Data import +----------- + +Power flow analysis +------------------- diff --git a/doc/index.rst b/doc/index.rst index 64a702e89..32343e9f1 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -11,6 +11,11 @@ eDisGo -- Optimization of flexibility options and grid expansion for distributio .. toctree:: :maxdepth: 2 + start_page + quickstart + usage_details + features_in_detail + data_sources dev_notes definitions_and_units api diff --git a/doc/quickstart.rst b/doc/quickstart.rst new file mode 100644 index 000000000..1b05408af --- /dev/null +++ b/doc/quickstart.rst @@ -0,0 +1,4 @@ +.. _quickstart: + +Quickstart +========== \ No newline at end of file diff --git a/doc/start_page.rst b/doc/start_page.rst new file mode 100644 index 000000000..e944f0f82 --- /dev/null +++ b/doc/start_page.rst @@ -0,0 +1,46 @@ +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. + +The toolbox currently includes + +* Data import from data sources of the open_eGo project +* Power flow analysis for grid issue identification (enabled by `PyPSA `_) +* Automatic grid reinforced solving overloading and overvoltage issues + +Features to be included + +* Battery storage integration +* Grid operation optimized curtailment +* Cluster based analyses + +See :ref:`quickstart` for the first steps. A deeper guide is provided in :ref:`usage-details`. +We explain in detail how things are done in :ref:`features-in-detail`. +:ref:`data-sources` details on how to import and suitable available data sources. +For those of you who want to contribute see :ref:`dev-notes` and the +:ref:`api` reference. + + +LICENSE +------- + +Copyright (C) 2017 Reiner Lemoine Institut gGmbH + +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 General Public License along with +this program. If not, see https://www.gnu.org/licenses/. \ No newline at end of file diff --git a/doc/usage_details.rst b/doc/usage_details.rst new file mode 100644 index 000000000..d9363bb89 --- /dev/null +++ b/doc/usage_details.rst @@ -0,0 +1,22 @@ +.. _usage-details: + +Usage details +============= + +The fundamental data structure +------------------------------ + +Identify grid issues +-------------------- + +Grid extension +-------------- + +.. Battery storages +.. ---------------- + +.. Curtailment +.. ----------- + +Retrieve results +---------------- diff --git a/edisgo/config/config_costs_default.cfg b/edisgo/config/config_costs_default.cfg index 149c5c8d9..8b7ebe625 100644 --- a/edisgo/config/config_costs_default.cfg +++ b/edisgo/config/config_costs_default.cfg @@ -8,50 +8,19 @@ # the key refers to column 'name' in equipment files -[costs_lv_cables] - -NAYY 4x1x300 = 0 -NAYY 4x1x240 = 0 -NAYY 4x1x185 = 0 -NAYY 4x1x150 = 0 -NAYY 4x1x120 = 0 -NAYY 4x1x95 = 0 -NAYY 4x1x50 = 0 -NAYY 4x1x35 = 0 - -[costs_mv_cables] - -NA2XS2Y 3x1x185 RM/25 = 0 -NA2XS2Y 3x1x240 RM/25 = 0 -NA2XS2Y 3x1x300 RM/25 = 0 -NA2XS2Y 3x1x400 RM/35 = 0 -NA2XS2Y 3x1x500 RM/35 = 0 -NA2XS2Y 3x1x150 RE/25 = 0 -NA2XS2Y 3x1x240 = 0 -NA2XS(FL)2Y 3x1x300 RM/25 = 0 -NA2XS(FL)2Y 3x1x400 RM/35 = 0 -NA2XS(FL)2Y 3x1x500 RM/35 = 0 - -[costs_mv_overhead_lines] - -48-AL1/8-ST1A = 0 -94-AL1/15-ST1A = 0 -122-AL1/20-ST1A = 0 - -[costs_lv_transformers] - -50 kVA = 0 -100 kVA = 0 -160 kVA = 0 -250 kVA = 0 -400 kVA = 0 -630 kVA = 0 -800 kVA = 0 -1000 kVA = 0 - -[costs_mv_transformers] - -20 MVA = 0 -32 MVA = 0 -40 MVA = 0 -63 MVA = 0 +[costs_cables] + +# costs in kEUR/km, source: DENA Verteilnetzstudie +# costs depend on population density according to DENA Verteilnetzstudie +# here "rural" corresponds to a population density of <= 500 people/km² +# and "urban" corresponds to a population density of > 500 people/km² +lv rural = 60 +lv urban = 100 +mv rural = 80 +mv urban = 140 + +[costs_transformers] + +# costs in kEUR, source: DENA Verteilnetzstudie +lv = 10 +mv = 1000 diff --git a/edisgo/config/config_flexopt_default.cfg b/edisgo/config/config_flexopt_default.cfg index 8e3bcc881..8ca0cf13b 100644 --- a/edisgo/config/config_flexopt_default.cfg +++ b/edisgo/config/config_flexopt_default.cfg @@ -61,5 +61,5 @@ lv_feedin_case_max_v_deviation = 0.03 # ============ load_factor_hv_mv_transformer = 1.0 load_factor_mv_lv_transformer = 1.0 -load_factor_mv_line = 0.6 +load_factor_mv_line = 1.0 load_factor_lv_line = 1.0 diff --git a/edisgo/data/import_data.py b/edisgo/data/import_data.py index 7e7f29827..9d291cb0b 100644 --- a/edisgo/data/import_data.py +++ b/edisgo/data/import_data.py @@ -20,7 +20,6 @@ if not 'READTHEDOCS' in os.environ: from ding0.tools.results import load_nd_from_pickle from ding0.core.network.stations import LVStationDing0 - from ding0.core.network.grids import CircuitBreakerDing0 from ding0.core.structure.regions import LVLoadAreaCentreDing0 import logging @@ -75,9 +74,6 @@ def import_from_ding0(file, network): ding0_mv_grid = ding0_nd._mv_grid_districts[0].mv_grid - # Make sure circuit breakers (respectively the rings) are closed - ding0_mv_grid.close_circuit_breakers() - # Import medium-voltage grid data network.mv_grid = _build_mv_grid(ding0_mv_grid, network) @@ -271,6 +267,15 @@ def _build_mv_grid(ding0_grid, network): v_level=_.v_level) for _ in ding0_grid.generators()} grid.graph.add_nodes_from(generators.values(), type='generator') + # Create list of diconnection point instances and add these to grid's graph + disconnecting_points = {_: MVDisconnectingPoint(id=_.id_db, + geom=_.geo_data, + state=_.status, + grid=grid) + for _ in ding0_grid._circuit_breakers} + grid.graph.add_nodes_from(disconnecting_points.values(), + type='disconnection_point') + # Create list of branch tee instances and add these to grid's graph branch_tees = {_: BranchTee(id=_.id_db, geom=_.geo_data, @@ -322,6 +327,7 @@ def _build_mv_grid(ding0_grid, network): # Merge node above defined above to a single dict nodes = {**loads, **generators, + **disconnecting_points, **branch_tees, **stations, **{ding0_grid.station(): mv_station}} @@ -556,7 +562,7 @@ def _attach_aggregated(network, grid, aggregated, ding0_grid): # connect generator to MV station line = Line(id='line_aggr_generator_vlevel_{v_level}_' - '{subtype}'.format( + '{type}_{subtype}'.format( v_level=v_level, subtype=subtype), type=aggr_line_type, @@ -649,6 +655,12 @@ def _validate_ding0_mv_grid_import(grid, ding0_grid): data_integrity['branch_tee']['edisgo'] = len( grid.graph.nodes_by_attribute('branch_tee')) + # Check number of disconnecting points + data_integrity['disconnection_point']['ding0'] = len( + ding0_grid._circuit_breakers) + data_integrity['disconnection_point']['edisgo'] = len( + grid.graph.nodes_by_attribute('disconnection_point')) + # Check number of MV transformers data_integrity['mv_transformer']['ding0'] = len( list(ding0_grid.station().transformers())) diff --git a/edisgo/examples/example.py b/edisgo/examples/example.py index dd0251642..289db8360 100644 --- a/edisgo/examples/example.py +++ b/edisgo/examples/example.py @@ -1,108 +1,68 @@ from edisgo.grid.network import Network, Scenario, TimeSeries -from edisgo.flex_opt import reinforce_grid import os -import pickle +import sys +import pandas as pd import logging logging.basicConfig(filename='example.log', format='%(asctime)s %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p', - level=logging.INFO) + level=logging.DEBUG) logger = logging.getLogger('edisgo') logger.setLevel(logging.DEBUG) -timeseries = TimeSeries() -scenario = Scenario(timeseries=timeseries, - power_flow='worst-case') - -import_network = True - -if import_network: - network = Network.import_from_ding0( - os.path.join('data', 'ding0_grids_example_mvgd265_new-genos-dp-v0.3.pkl'), - id='Test grid', - scenario=scenario - ) - network.import_generators() - # Do non-linear power flow analysis with PyPSA - #network.analyze() - #network.pypsa.export_to_csv_folder('data/pypsa_export') - #network.pypsa = None - #pickle.dump(network, open('test_network.pkl', 'wb')) -else: - network = None #pickle.load(open('test_network.pkl', 'rb')) - -# from pypsa import Network as PyPSANetwork -# pypsa_network = PyPSANetwork(csv_folder_name='data/pypsa_export_80_stations') -# # q unterscheidet sich -# b1 = pypsa_network.transformers_t['q0'] -# b2 = network.pypsa.transformers_t['q0'] -# b3 = b1 - b2 -# b1 = pypsa_network.loads_t['q_set'] -# b2 = network.pypsa.loads_t['q_set'] - -# # Print LV station secondary side voltage levels returned by PFA -# print(network.results.v_res( -# network.mv_grid.graph.nodes_by_attribute('lv_station'), 'lv')) - -# Print LV station apparent power returned by PFA -# lv_transformers = [transformer for station in -# network.mv_grid.graph.nodes_by_attribute('lv_station') for -# transformer in station.transformers] -# print(network.results.s_res(lv_transformers)) - -# Print voltage levels for entire LV grid -# for attr in ['lv_station', 'load', 'generator', 'branch_tee']: -# objs = [] -# for lv_grid in network.mv_grid.lv_grids: -# objs.extend(lv_grid.graph.nodes_by_attribute(attr)) -# print("\n\n\n{}\n".format(attr)) -# print(network.results.v_res( -# objs, 'lv')) - -# Print voltage level of all nodes -# print(network.results.pfa_v_mag_pu) - -# Print current (line loading) at MV lines -# print(network.results.i_res([_['line'] for _ in network.mv_grid.graph.graph_edges()])) - -# Print apparent power at lines -# print(network.results.s_res([_['line'] for _ in network.mv_grid.graph.graph_edges()])) - -# Print number of buses, generators, load and lines to study problem size -# print('buses: ', network.pypsa.buses.shape) -# print('generators: ', network.pypsa.generators.shape) -# print('loads: ', network.pypsa.loads.shape) -# print('lines: ', network.pypsa.lines.shape) - -# Print voltage levels for all lines -# print(network.results.s_res()) - -# # MV generators -# gens = network.mv_grid.graph.nodes_by_attribute('generator') -# print('Generators in MV grid incl. aggregated generators from MV and LV') -# print('Type\tSubtype\tCapacity in kW') -# for gen in gens: -# print("{type}\t{sub}\t{capacity}".format( -# type=gen.type, sub=gen.subtype, capacity=gen.nominal_capacity)) -# -# # Load located in aggregated LAs -# print('\n\nAggregated load in LA adds up to\n') -# if network.mv_grid.graph.nodes_by_attribute('load'): -# [print('\t{0}: {1} MWh'.format( -# _, -# network.mv_grid.graph.nodes_by_attribute('load')[0].consumption[_] / 1e3)) -# for _ in ['retail', 'industrial', 'agricultural', 'residential']] +# import pickle +# import_network = True +# if import_network: +# network = Network.import_from_ding0( +# os.path.join('data', 'ding0_grids__76.pkl'), +# id='Test grid', +# scenario=scenario +# ) +# # Import generators +# network.import_generators() +# # Do non-linear power flow analysis with PyPSA +# network.analyze() +# #network.pypsa.export_to_csv_folder('data/pypsa_export') +# #network.pypsa = None +# #pickle.dump(network, open('test_network.pkl', 'wb')) # else: -# print("O MWh") - -#reinforce_grid.reinforce_grid(network) -#print(network.results.grid_expansion_costs) - -# liste aller lv grids -# [_ for _ in network.mv_grid.lv_grids] - -# nx.draw_spectral(list(network.mv_grid.lv_grids)[0].graph) - -# ToDo: Möglichkeit MV und LV getrennt zu rechnen - +# network = pickle.load(open('test_results_neu.pkl', 'rb')) + +if __name__ == '__main__': + grids = [] + for file in os.listdir(os.path.join(sys.path[0], "data")): + if file.endswith(".pkl"): + grids.append(file) + + timeseries = TimeSeries() + scenario = Scenario(timeseries=timeseries, + power_flow='worst-case') + costs = pd.DataFrame() + faulty_grids = {'grid': [], 'msg': []} + for dingo_grid in grids: + logging.info('Grid expansion for {}'.format(dingo_grid)) + network = Network.import_from_ding0( + os.path.join('data', dingo_grid), + id='Test grid', + scenario=scenario) + try: + # Do non-linear power flow analysis with PyPSA + network.analyze() + # Do grid reinforcement + network.reinforce() + costs_grouped = network.results.grid_expansion_costs.groupby( + ['type']).sum() + costs = costs.append( + pd.DataFrame(costs_grouped.values, + columns=costs_grouped.columns, + index=[[network.id] * len(costs_grouped), + costs_grouped.index])) + logging.info('SUCCESS!') + except Exception as e: + faulty_grids['grid'].append(network.id) + faulty_grids['msg'].append(e) + logging.info('Something went wrong.') + + pd.DataFrame(faulty_grids).to_csv('faulty_grids.csv', index_label='grid') + costs.to_csv('costs.csv') diff --git a/edisgo/flex_opt/costs.py b/edisgo/flex_opt/costs.py index b7b4b0c9e..5e0f6231d 100644 --- a/edisgo/flex_opt/costs.py +++ b/edisgo/flex_opt/costs.py @@ -1,11 +1,118 @@ -def grid_expansion_costs(results): +import sys +import pandas as pd +import pyproj +from functools import partial +from shapely.ops import transform + +from edisgo.grid.components import Transformer, Line +from edisgo.grid.grids import LVGrid, MVGrid + + +def grid_expansion_costs(network): """ - Calculates grid expansion costs + Calculates grid expansion costs for each reinforced transformer and line + in kEUR. Attributes ---------- - results : :class:`~.grid.network.Results` - Results object holding equipment changes. + network : :class:`~.grid.network.Network` + + Returns + ------- + `pandas.DataFrame` + DataFrame containing type and costs plus in the case of lines the + line length and number of parallel lines of each reinforced + transformer and line. The DataFrame has the following columns: + + type: String + Transformer size or cable name + + total_costs: float + Costs of equipment in kEUR. For lines the line length and number of + parallel lines is already included in the total costs. + + quantity: int + For transformers quantity is always one, for lines it specifies the + number of parallel lines. + + line_length: float + Length of line or in case of parallel lines all lines in km. + + Notes + ------- + Total grid expansion costs can be obtained through + self.grid_expansion_costs.total_costs.sum(). """ - raise NotImplementedError + def _get_transformer_costs(transformer): + if isinstance(transformer.grid, LVGrid): + return float(network.config['costs_transformers']['lv']) + elif isinstance(transformer.grid, MVGrid): + return float(network.config['costs_transformers']['mv']) + + def _get_line_costs(line): + # get voltage level + if isinstance(line.grid, LVGrid): + voltage_level = 'lv' + elif isinstance(line.grid, MVGrid): + voltage_level = 'mv' + else: + print("Voltage level for line must be lv or mv.") + sys.exit() + # get population density in people/km^2 + # transform area to calculate area in km^2 + projection = partial( + pyproj.transform, + pyproj.Proj(init='epsg:{}'.format(network.config['geo']['srid'])), + pyproj.Proj(init='epsg:3035')) + sqm2sqkm = 1e6 + population_density = (line.grid.grid_district['population'] / + (transform(projection, + line.grid.grid_district['geom']).area / + sqm2sqkm)) + if population_density <= 500: + population_density = 'rural' + else: + population_density = 'urban' + return (float(network.config['costs_cables']['{} {}'.format( + voltage_level, population_density)])) + + costs = pd.DataFrame() + + # costs for transformers + transformers = network.results.equipment_changes[ + network.results.equipment_changes['equipment'].apply( + isinstance, args=(Transformer,))] + added_transformers = transformers[transformers['change'] == 'added'] + removed_transformers = transformers[transformers['change'] == 'removed'] + # check if any of the added transformers were later removed + added_removed_transformers = added_transformers.loc[ + added_transformers['equipment'].isin( + removed_transformers['equipment'])] + added_transformers = added_transformers[ + ~added_transformers['equipment'].isin( + added_removed_transformers.equipment)] + # calculate costs for each transformer + for t in added_transformers['equipment']: + costs = costs.append(pd.DataFrame( + {'type': t.type.name, + 'total_costs': _get_transformer_costs(t), + 'quantity': 1}, + index=[repr(t)])) + + # costs for lines + # get changed lines + lines = network.results.equipment_changes.loc[ + network.results.equipment_changes.index[ + network.results.equipment_changes.reset_index()['index'].apply( + isinstance, args=(Line,))]] + # calculate costs for each reinforced line + for l in list(lines.index.unique()): + costs = costs.append(pd.DataFrame( + {'type': l.type.name, + 'total_costs': _get_line_costs(l) * l.length * l.quantity, + 'length': l.length * l.quantity, + 'quantity': l.quantity}, + index=[repr(l)])) + + return costs diff --git a/edisgo/flex_opt/reinforce_grid.py b/edisgo/flex_opt/reinforce_grid.py index 85bc81d14..ee14612f1 100644 --- a/edisgo/flex_opt/reinforce_grid.py +++ b/edisgo/flex_opt/reinforce_grid.py @@ -7,22 +7,21 @@ logger = logging.getLogger('edisgo') -def reinforce_grid(network, while_counter_max=10): - """Evaluates grid reinforcement needs and performs measures. This function +def reinforce_grid(network, max_while_iterations=10): + """ Evaluates grid reinforcement needs and performs measures. This function is the parent function for all grid reinforcements. Parameters ---------- network : :class:`~.grid.network.Network` - while_counter_max : int - Maximum number of iterations when solving overvoltage problems, to - prevent infinite grid expansion. + max_while_iterations : int + Maximum number of times each while loop is conducted. Notes ----- Vorgehen laut BW-Studie: - * getrennte oder kombinierte Betrachtung von NS und MS muss noch entschieden - werden, BW-Studie führt getrennte Betrachtung durch + * getrennte oder kombinierte Betrachtung von NS und MS muss noch + entschieden werden, BW-Studie führt getrennte Betrachtung durch * Reihenfolge der Behebung von Grenzwertverletzungen: ** Trafo ** Spannung @@ -33,8 +32,8 @@ def reinforce_grid(network, while_counter_max=10): BM ersetzt * Spannungsbandverletztung ** Strangauftrennung nach 2/3 der Distanz - ** danach eventuell weitere Strangauftrennung wenn sinnvoll, sonst parallele - BM + ** danach eventuell weitere Strangauftrennung wenn sinnvoll, sonst + parallele BM Sonstiges: * nur Rückspeisefall @@ -82,47 +81,56 @@ def _add_transformer_changes_to_equipment_changes(mode): # REINFORCE OVERLOADED TRANSFORMERS AND LINES iteration_step = 1 - # # ToDo: check overloading of HV/MV Trafo? - # logger.debug('==> Check MV/LV station load.') - # overloaded_stations = checks.mv_lv_station_load(network) - # logger.debug('==> Check line load.') - # crit_lines_lv = checks.lv_line_load(network) - # crit_lines_mv = checks.mv_line_load(network) - # crit_lines = {**crit_lines_lv, **crit_lines_mv} - # - # while_counter = 0 - # while ((overloaded_stations or crit_lines) and - # while_counter < while_counter_max): - # - # if overloaded_stations: - # # reinforce substations - # transformer_changes = \ - # reinforce_measures.extend_distribution_substation( - # network, overloaded_stations) - # # write added and removed transformers to results.equipment_changes - # _add_transformer_changes_to_equipment_changes('added') - # _add_transformer_changes_to_equipment_changes('removed') - # - # if crit_lines: - # # reinforce lines - # lines_changes = reinforce_measures.reinforce_branches_overloading( - # network, crit_lines) - # # write changed lines to results.equipment_changes - # _add_lines_changes_to_equipment_changes() - # - # # run power flow analysis again and check if all over-loading - # # problems were solved - # logger.debug('==> Run power flow analysis.') - # network.analyze() - # logger.debug('==> Recheck MV/LV station load.') - # overloaded_stations = checks.mv_lv_station_load(network) - # logger.debug('==> Recheck line load.') - # crit_lines_lv = checks.lv_line_load(network) - # crit_lines_mv = checks.mv_line_load(network) - # crit_lines = {**crit_lines_lv, **crit_lines_mv} - # - # iteration_step += 1 - # while_counter += 1 + # ToDo: check overloading of HV/MV Trafo? + logger.debug('==> Check MV/LV station load.') + overloaded_stations = checks.mv_lv_station_load(network) + logger.debug('==> Check line load.') + crit_lines_lv = checks.lv_line_load(network) + crit_lines_mv = checks.mv_line_load(network) + crit_lines = {**crit_lines_lv, **crit_lines_mv} + + while_counter = 0 + while ((overloaded_stations or crit_lines) and + while_counter < max_while_iterations): + + if overloaded_stations: + # reinforce substations + transformer_changes = \ + reinforce_measures.extend_distribution_substation( + network, overloaded_stations) + # write added and removed transformers to results.equipment_changes + _add_transformer_changes_to_equipment_changes('added') + _add_transformer_changes_to_equipment_changes('removed') + + if crit_lines: + # reinforce lines + lines_changes = reinforce_measures.reinforce_branches_overloading( + network, crit_lines) + # write changed lines to results.equipment_changes + _add_lines_changes_to_equipment_changes() + + # run power flow analysis again and check if all over-loading + # problems were solved + logger.debug('==> Run power flow analysis.') + network.analyze() + logger.debug('==> Recheck MV/LV station load.') + overloaded_stations = checks.mv_lv_station_load(network) + logger.debug('==> Recheck line load.') + crit_lines_lv = checks.lv_line_load(network) + crit_lines_mv = checks.mv_line_load(network) + crit_lines = {**crit_lines_lv, **crit_lines_mv} + + iteration_step += 1 + while_counter += 1 + + # check if all load problems were solved after maximum number of + # iterations allowed + if (while_counter == max_while_iterations and + (crit_lines or overloaded_stations)): + logger.error("==> Load issues were not solved.") + sys.exit() + else: + logger.debug('==> All load issues in MV grid are solved.') # # dump network # import pickle @@ -137,54 +145,54 @@ def _add_transformer_changes_to_equipment_changes(mode): # REINFORCE BRANCHES DUE TO VOLTAGE ISSUES iteration_step += 1 - # # solve voltage problems in MV grid - # logger.debug('==> Check voltage in MV grid.') - # crit_nodes = checks.mv_voltage_deviation(network) - # - # while_counter = 0 - # while crit_nodes and while_counter < while_counter_max: - # - # # ToDo: get crit_nodes as objects instead of string - # # for now iterate through grid to find node for repr - # crit_nodes_objects = pd.Series() - # for node in network.mv_grid.graph.nodes(): - # if repr(node) in crit_nodes[network.mv_grid].index: - # crit_nodes_objects = pd.concat( - # [crit_nodes_objects, - # pd.Series(crit_nodes[network.mv_grid].loc[repr(node)], - # index=[node])]) - # crit_nodes_objects.sort_values(ascending=False, inplace=True) - # - # # reinforce lines - # lines_changes = reinforce_measures.reinforce_branches_overvoltage( - # network, network.mv_grid, crit_nodes_objects) - # # write changed lines to results.equipment_changes - # _add_lines_changes_to_equipment_changes() - # - # # run power flow analysis again and check if all over-voltage - # # problems were solved - # logger.debug('==> Run power flow analysis.') - # network.analyze() - # logger.debug('==> Recheck voltage in MV grid.') - # crit_nodes = checks.mv_voltage_deviation(network) - # - # iteration_step += 1 - # while_counter += 1 - # - # # check if all voltage problems were solved after maximum number of - # # iterations allowed - # if while_counter == while_counter_max and crit_nodes: - # logger.error("==> Voltage issues in MV grid were not solved.") - # sys.exit() - # else: - # logger.debug('==> All voltage issues in MV grid are solved.') + # solve voltage problems in MV grid + logger.debug('==> Check voltage in MV grid.') + crit_nodes = checks.mv_voltage_deviation(network) + + while_counter = 0 + while crit_nodes and while_counter < max_while_iterations: + + # ToDo: get crit_nodes as objects instead of string + # for now iterate through grid to find node for repr + crit_nodes_objects = pd.Series() + for node in network.mv_grid.graph.nodes(): + if repr(node) in crit_nodes[network.mv_grid].index: + crit_nodes_objects = pd.concat( + [crit_nodes_objects, + pd.Series(crit_nodes[network.mv_grid].loc[repr(node)], + index=[node])]) + crit_nodes_objects.sort_values(ascending=False, inplace=True) + + # reinforce lines + lines_changes = reinforce_measures.reinforce_branches_overvoltage( + network, network.mv_grid, crit_nodes_objects) + # write changed lines to results.equipment_changes + _add_lines_changes_to_equipment_changes() + + # run power flow analysis again and check if all over-voltage + # problems were solved + logger.debug('==> Run power flow analysis.') + network.analyze() + logger.debug('==> Recheck voltage in MV grid.') + crit_nodes = checks.mv_voltage_deviation(network) + + iteration_step += 1 + while_counter += 1 + + # check if all voltage problems were solved after maximum number of + # iterations allowed + if while_counter == max_while_iterations and crit_nodes: + logger.error("==> Voltage issues in MV grid were not solved.") + sys.exit() + else: + logger.debug('==> All voltage issues in MV grid are solved.') # solve voltage problems in LV grids logger.debug('==> Check voltage in LV grids.') crit_nodes = checks.lv_voltage_deviation(network) while_counter = 0 - while crit_nodes and while_counter < while_counter_max: + while crit_nodes and while_counter < max_while_iterations: # for every grid in crit_nodes do reinforcement for grid in crit_nodes: @@ -214,7 +222,7 @@ def _add_transformer_changes_to_equipment_changes(mode): # check if all voltage problems were solved after maximum number of # iterations allowed - if while_counter == while_counter_max and crit_nodes: + if while_counter == max_while_iterations and crit_nodes: logger.error("==> Voltage issues in LV grids were not solved.") sys.exit() else: @@ -230,7 +238,7 @@ def _add_transformer_changes_to_equipment_changes(mode): while_counter = 0 while ((overloaded_stations or crit_lines) and - while_counter < while_counter_max): + while_counter < max_while_iterations): if overloaded_stations: # reinforce substations diff --git a/edisgo/flex_opt/reinforce_measures.py b/edisgo/flex_opt/reinforce_measures.py index cef4b16f6..94a39bd5b 100644 --- a/edisgo/flex_opt/reinforce_measures.py +++ b/edisgo/flex_opt/reinforce_measures.py @@ -1,18 +1,14 @@ -import os import copy import math -import pandas as pd +import sys import networkx as nx from networkx.algorithms.shortest_paths.weighted import _dijkstra as \ dijkstra_shortest_path_length -if not 'READTHEDOCS' in os.environ: - import ding0 -from edisgo.grid.components import Transformer + +from edisgo.grid.components import Transformer, BranchTee, Generator, Load from edisgo.grid.grids import LVGrid import logging - -# package_path = ding0.__path__[0] logger = logging.getLogger('edisgo') @@ -44,8 +40,8 @@ def extend_distribution_substation(network, critical_stations): except KeyError: print('Standard MV/LV transformer is not in equipment list.') - load_factor_mv_lv_transformer = float(network.config['grid_expansion'][ - 'load_factor_mv_lv_transformer']) + load_factor_mv_lv_transformer = \ + network.scenario.parameters.load_factor_mv_lv_transformer transformers_changes = {'added': {}, 'removed': {}} for station in critical_stations: @@ -141,6 +137,9 @@ def reinforce_branches_overvoltage(network, grid, crit_nodes): farthest away from the station and install new standard line 2. Install parallel standard line + In LV grids only lines outside buildings are reinforced; loads and + generators in buildings cannot be directly connected to the MV/LV station. + References ---------- .. [1] "Verteilnetzstudie für das Land Baden-Württemberg" @@ -150,9 +149,6 @@ def reinforce_branches_overvoltage(network, grid, crit_nodes): """ - # ToDo: gilt Methodik auch für die MS? - # ToDo: in MV muss neue line zu node_2_3 zu rep_main_line und main_line_reinforced hinzugefügt werden - # load standard line data if isinstance(grid, LVGrid): try: @@ -178,10 +174,12 @@ def reinforce_branches_overvoltage(network, grid, crit_nodes): for i in range(len(crit_nodes)): path = nx.shortest_path(grid.graph, grid.station, crit_nodes.index[i]) - - # ToDo: Remove + # stop execution if voltage issue occurs at station's secondary side if len(path) == 1: - break + logging.error("Voltage issues of station need to be solved at " + + "secondary side.") + sys.exit() + # check if representative of line is already in list # main_line_reinforced, if it is the main line the critical node is # connected to has already been reinforced in this iteration step @@ -198,6 +196,32 @@ def reinforce_branches_overvoltage(network, grid, crit_nodes): node_2_3 = next(j for j in path if path_length[j] >= path_length[ crit_nodes.index[i]] * 2 / 3) + # if LVGrid: check if node_2_3 is outside of a house + # and if not find next BranchTee outside the house + if isinstance(grid, LVGrid): + if isinstance(node_2_3, BranchTee): + if node_2_3.in_building: + # ToDo more generic (new function) + try: + node_2_3 = path[path.index(node_2_3) - 1] + except IndexError: + print('BranchTee outside of building is not in ' + + 'path.') + elif (isinstance(node_2_3, Generator) or + isinstance(node_2_3, Load)): + pred_node = path[path.index(node_2_3) - 1] + if isinstance(pred_node, BranchTee): + if pred_node.in_building: + # ToDo more generic (new function) + try: + node_2_3 = path[path.index(node_2_3) - 2] + except IndexError: + print('BranchTee outside of building is ' + + 'not in path.') + else: + logging.error("Not implemented for {}.".format( + str(type(node_2_3)))) + # if node_2_3 is a representative (meaning it is already directly # connected to the station), line cannot be disconnected and must # therefore be reinforced @@ -239,6 +263,7 @@ def reinforce_branches_overvoltage(network, grid, crit_nodes): crit_line.length = path_length[node_2_3] crit_line.type = standard_line.copy() crit_line.kind = 'cable' + crit_line.quantity = 1 lines_changes[crit_line] = 1 # add node_2_3 to representatives list to not further reinforce # this part off the grid in this iteration step @@ -252,8 +277,8 @@ def reinforce_branches_overvoltage(network, grid, crit_nodes): 'has already been reinforced.') if main_line_reinforced: - logger.debug('==> {} branche(s) was/were reinforced.'.format( - str(len(main_line_reinforced))) + 'due to over-voltage issues.') + logger.debug('==> {} branche(s) was/were reinforced '.format( + str(len(lines_changes))) + 'due to over-voltage issues.') return lines_changes @@ -329,7 +354,7 @@ def reinforce_branches_overloading(network, crit_lines): crit_line.kind = 'cable' if crit_lines: - logger.debug('==> {} branche(s) was/were reinforced.'.format( + logger.debug('==> {} branche(s) was/were reinforced '.format( str(len(crit_lines))) + 'due to over-loading issues.') return lines_changes diff --git a/edisgo/grid/components.py b/edisgo/grid/components.py index 9761ee51d..8bafff999 100644 --- a/edisgo/grid/components.py +++ b/edisgo/grid/components.py @@ -155,11 +155,13 @@ def timeseries(self): 'agricultural': 0.0025} if isinstance(self.grid, MVGrid): - q_factor = tan(acos(self.grid.network.scenario.pfac_mv_load)) + q_factor = tan(acos( + self.grid.network.scenario.parameters.pfac_mv_load)) power_scaling = float(self.grid.network.config['scenario'][ 'scale_factor_mv_load']) elif isinstance(self.grid, LVGrid): - q_factor = tan(acos(self.grid.network.scenario.pfac_lv_load)) + q_factor = tan(acos( + self.grid.network.scenario.parameters.pfac_lv_load)) power_scaling = float(self.grid.network.config['scenario'][ 'scale_factor_lv_load']) @@ -217,7 +219,7 @@ def consumption(self, cons_dict): def __repr__(self): return '_'.join(['Load', - list(self.consumption.keys())[0], + sorted(list(self.consumption.keys()))[0], repr(self.grid), str(self.id)]) @@ -263,9 +265,11 @@ def timeseries(self): """ if self._timeseries is None: if isinstance(self.grid, MVGrid): - q_factor = tan(acos(self.grid.network.scenario.pfac_mv_gen)) + q_factor = tan(acos( + self.grid.network.scenario.parameters.pfac_mv_gen)) elif isinstance(self.grid, LVGrid): - q_factor = tan(acos(self.grid.network.scenario.pfac_lv_gen)) + q_factor = tan(acos( + self.grid.network.scenario.parameters.pfac_lv_gen)) timeseries = self.grid.network.scenario.timeseries.generation timeseries['q'] = ( diff --git a/edisgo/grid/grids.py b/edisgo/grid/grids.py index 8c8cf3149..320af3b8b 100644 --- a/edisgo/grid/grids.py +++ b/edisgo/grid/grids.py @@ -73,6 +73,11 @@ def id(self): def network(self): return self._network + @property + def grid_district(self): + """Provide access to the grid_district""" + return self._grid_district + def __repr__(self): return '_'.join([self.__class__.__name__, str(self._id)]) @@ -189,7 +194,7 @@ def nodes_by_attribute(self, attr_val, attr='type'): return nodes def lines_by_attribute(self, attr_val=None, attr='type'): - """Returns a generator for iterating over Graph's lines by attribute value. + """ Returns a generator for iterating over Graph's lines by attribute value. Get all lines that share the same attribute. By default, the attr 'type' is used to specify the lines' type (line, agg_line, etc.). @@ -237,7 +242,7 @@ def lines_by_attribute(self, attr_val=None, attr='type'): of the in-going tuple (which is defined by the needs of networkX). If this changes, the code will break. - Adapted from `Ding0 `_. """ @@ -262,7 +267,7 @@ def lines_by_attribute(self, attr_val=None, attr='type'): yield {'adj_nodes': line[0], 'line': line[1]} def lines(self): - """Returns a generator for iterating over Graph's lines + """ Returns a generator for iterating over Graph's lines Returns ------- diff --git a/edisgo/grid/network.py b/edisgo/grid/network.py index bf7cba894..cb40807e5 100644 --- a/edisgo/grid/network.py +++ b/edisgo/grid/network.py @@ -1,12 +1,12 @@ +from os import path +import pandas as pd +from math import sqrt + import edisgo -from edisgo.tools import config +from edisgo.tools import config, pypsa_io from edisgo.data.import_data import import_from_ding0, import_generators from edisgo.flex_opt.costs import grid_expansion_costs - -from os import path -import pandas as pd -from edisgo.tools import pypsa_io -from math import sqrt, acos, tan +from edisgo.flex_opt.reinforce_grid import reinforce_grid class Network: @@ -180,7 +180,8 @@ def import_from_ding0(cls, file, **kwargs): network = cls(**kwargs) # call the importer - import_from_ding0(file=file, network=network) + import_from_ding0(file=file, + network=network) return network @@ -253,14 +254,11 @@ def analyze(self, mode=None): else: raise ValueError("Power flow analysis did not converge.") - - def reinforce(self): - """Reinforces the grid - - TBD - - """ - raise NotImplementedError + def reinforce(self, **kwargs): + """Reinforces the grid and calculates grid expansion costs""" + reinforce_grid( + self, max_while_iterations=kwargs.get('max_while_iterations', 10)) + self.results.grid_expansion_costs = grid_expansion_costs(self) @property def id(self): @@ -370,6 +368,17 @@ class Scenario: To specify the time range for a power flow analysis provide the start and end time as 2-tuple of :obj:`datetime` + Optional Parameters + -------------------- + pfac_mv_gen : :obj:`float` + Power factor for medium voltage generators + pfac_mv_load : :obj:`float` + Power factor for medium voltage loads + pfac_lv_gen : :obj:`float` + Power factor for low voltage generators + pfac_lv_load : :obj:`float` + Power factor for low voltage loads + Attributes ---------- _name : :obj:`str` @@ -381,14 +390,9 @@ class Scenario: _etrago_specs : :class:`~.grid.grids.ETraGoSpecs` Specifications which are to be fulfilled at transition point (HV-MV substation) - _pfac_mv_gen : :obj:`float` - Power factor for medium voltage generators - _pfac_mv_load : :obj:`float` - Power factor for medium voltage loads - _pfac_lv_gen : :obj:`float` - Power factor for low voltage generators - _pfac_lv_load : :obj:`float` - Power factor for low voltage loads + _parameters : :class:`~.grid.network.Parameters` + Parameters for power flow analysis and grid expansion. + """ def __init__(self, power_flow, **kwargs): @@ -396,10 +400,7 @@ def __init__(self, power_flow, **kwargs): self._network = kwargs.get('network', None) self._timeseries = kwargs.get('timeseries', None) self._etrago_specs = kwargs.get('etrago_specs', None) - self._pfac_mv_gen = kwargs.get('pfac_mv_gen', None) - self._pfac_mv_load = kwargs.get('pfac_mv_load', None) - self._pfac_lv_gen = kwargs.get('pfac_lv_gen', None) - self._pfac_lv_load = kwargs.get('pfac_lv_load', None) + self._parameters = Parameters(self, **kwargs) if isinstance(power_flow, str): if power_flow != 'worst-case': @@ -419,48 +420,173 @@ def __init__(self, power_flow, **kwargs): def timeseries(self): return self._timeseries + @property + def parameters(self): + return self._parameters + + def __repr__(self): + return 'Scenario ' + self._name + + +class Parameters: + """ + Contains model parameters for power flow analysis and grid expansion. + + Attributes + ---------- + _pfac_mv_gen : :obj:`float` + Power factor for medium voltage generators + _pfac_mv_load : :obj:`float` + Power factor for medium voltage loads + _pfac_lv_gen : :obj:`float` + Power factor for low voltage generators + _pfac_lv_load : :obj:`float` + Power factor for low voltage loads + _hv_mv_trafo_offset : :obj:`float` + Offset at substation + _hv_mv_trafo_control_deviation : :obj:`float` + Voltage control deviation at substation + _load_factor_hv_mv_transformer : :obj:`float` + Allowed load of transformers at substation, retrieved from config + files depending on analyzed case (feed-in or load). + _load_factor_mv_lv_transformer : :obj:`float` + Allowed load of transformers at distribution substation, retrieved from + config files depending on analyzed case (feed-in or load). + _load_factor_mv_line : :obj:`float` + Allowed load of MV line, retrieved from config files depending on + analyzed case (feed-in or load). + _load_factor_lv_line : :obj:`float` + Allowed load of LV line, retrieved from config files depending on + analyzed case (feed-in or load). + _mv_max_v_deviation : :obj:`float` + Allowed voltage deviation in MV grid, retrieved from config files + depending on analyzed case (feed-in or load). + _lv_max_v_deviation : :obj:`float` + Allowed voltage deviation in LV grid, retrieved from config files + depending on analyzed case (feed-in or load). + + """ + + def __init__(self, scenario_class, **kwargs): + self._scenario = scenario_class + self._pfac_mv_gen = kwargs.get('pfac_mv_gen', None) + self._pfac_mv_load = kwargs.get('pfac_mv_load', None) + self._pfac_lv_gen = kwargs.get('pfac_lv_gen', None) + self._pfac_lv_load = kwargs.get('pfac_lv_load', None) + self._hv_mv_transformer_offset = None + self._hv_mv_transformer_control_deviation = None + self._load_factor_hv_mv_transformer = None + self._load_factor_mv_lv_transformer = None + self._load_factor_mv_line = None + self._load_factor_lv_line = None + self._mv_max_v_deviation = None + self._lv_max_v_deviation = None + + @property + def scenario(self): + return self._scenario + @property def pfac_mv_gen(self): if not self._pfac_mv_gen: self._pfac_mv_gen = float( - self.network.config['scenario']['pfac_mv_gen']) - + self.scenario.network.config['scenario']['pfac_mv_gen']) return self._pfac_mv_gen @property def pfac_mv_load(self): if not self._pfac_mv_load: self._pfac_mv_load = float( - self.network.config['scenario']['pfac_mv_load']) - + self.scenario.network.config['scenario']['pfac_mv_load']) return self._pfac_mv_load @property def pfac_lv_gen(self): if not self._pfac_lv_gen: self._pfac_lv_gen = float( - self.network.config['scenario']['pfac_lv_gen']) - + self.scenario.network.config['scenario']['pfac_lv_gen']) return self._pfac_lv_gen @property def pfac_lv_load(self): if not self._pfac_lv_load: self._pfac_lv_load = float( - self.network.config['scenario']['pfac_lv_load']) - + self.scenario.network.config['scenario']['pfac_lv_load']) return self._pfac_lv_load @property - def network(self): - return self._network + def hv_mv_transformer_offset(self): + if not self._hv_mv_transformer_offset: + self._hv_mv_transformer_offset = float( + self.scenario.network.config['grid_expansion'][ + 'hv_mv_trafo_offset']) + return self._hv_mv_transformer_offset - @network.setter - def network(self, network): - self._network = network + @property + def hv_mv_transformer_control_deviation(self): + if not self._hv_mv_transformer_control_deviation: + self._hv_mv_transformer_control_deviation = float( + self.scenario.network.config['grid_expansion'][ + 'hv_mv_trafo_control_deviation']) + return self._hv_mv_transformer_control_deviation - def __repr__(self): - return 'Scenario ' + self._name + @property + # ToDo: for now only feed-in case is considered + def load_factor_hv_mv_transformer(self): + if not self._load_factor_hv_mv_transformer: + self._load_factor_hv_mv_transformer = float( + self.scenario.network.config['grid_expansion'][ + 'load_factor_hv_mv_transformer']) + return self._load_factor_hv_mv_transformer + + @property + # ToDo: for now only feed-in case is considered + def load_factor_mv_lv_transformer(self): + if not self._load_factor_mv_lv_transformer: + self._load_factor_mv_lv_transformer = float( + self.scenario.network.config['grid_expansion'][ + 'load_factor_mv_lv_transformer']) + return self._load_factor_mv_lv_transformer + + @property + # ToDo: for now only feed-in case is considered + def load_factor_mv_line(self): + if not self._load_factor_mv_line: + self._load_factor_mv_line = float( + self.scenario.network.config['grid_expansion'][ + 'load_factor_mv_line']) + return self._load_factor_mv_line + + @property + # ToDo: for now only feed-in case is considered + def load_factor_lv_line(self): + if not self._load_factor_lv_line: + self._load_factor_lv_line = float( + self.scenario.network.config['grid_expansion'][ + 'load_factor_lv_line']) + return self._load_factor_lv_line + + @property + # ToDo: for now only voltage deviation for the combined calculation of MV + # and LV is considered (load and feed-in case for seperate consideration + # of MV and LV needs to be implemented) + def mv_max_v_deviation(self): + if not self._mv_max_v_deviation: + self._mv_max_v_deviation = float( + self.scenario.network.config['grid_expansion'][ + 'mv_lv_max_v_deviation']) + return self._mv_max_v_deviation + + @property + # ToDo: for now only voltage deviation for the combined calculation of MV + # and LV is considered (load and feed-in case for seperate consideration + # of MV and LV needs to be implemented) + def lv_max_v_deviation(self): + if not self._lv_max_v_deviation: + self._lv_max_v_deviation = float( + self.scenario.network.config['grid_expansion'][ + 'mv_lv_max_v_deviation']) + return self._lv_max_v_deviation class TimeSeries: @@ -662,9 +788,6 @@ class Results: A stack that details the history of measures to increase grid's hosting capacity. The last item refers to the latest measure. The key `original` refers to the state of the grid topology as it was initially imported. - grid_expansion_costs: float - Total costs of grid expansion measures in `equipment_changes`. - ToDo: add unit """ # TODO: maybe add setter to alter list of measures @@ -681,19 +804,18 @@ def __init__(self): @property def pfa_p(self): """ - Active power results from power flow analysis + Active power results from power flow analysis in kW. Holds power flow analysis results for active power for the last iteration step. Index of the DataFrame is a DatetimeIndex indicating the time period the power flow analysis was conducted for; columns of the DataFrame are the edges as well as stations of the grid topology. - ToDo: add unit Parameters ---------- pypsa: `pandas.DataFrame` - Results time series of active power P from the + Results time series of active power P in kW from the `PyPSA network `_ Provide this if you want to set values. For retrieval of data do not @@ -713,19 +835,18 @@ def pfa_p(self, pypsa): @property def pfa_q(self): """ - Reactive power results from power flow analysis + Reactive power results from power flow analysis in kvar. Holds power flow analysis results for reactive power for the last iteration step. Index of the DataFrame is a DatetimeIndex indicating the time period the power flow analysis was conducted for; columns of the DataFrame are the edges as well as stations of the grid topology. - ToDo: add unit Parameters ---------- pypsa: `pandas.DataFrame` - Results time series of reactive power Q from the + Results time series of reactive power Q in kvar from the `PyPSA network `_ Provide this if you want to set values. For retrieval of data do not @@ -757,11 +878,11 @@ def pfa_v_mag_pu(self): Parameters ---------- pypsa: `pandas.DataFrame` - Results time series of voltage deviation from the + Results time series of voltage deviation in p.u. from the `PyPSA network `_ - Provide this if you want to set values. For retrieval of data do not - pass an argument + Provide this if you want to set values. For retrieval of data do + not pass an argument Returns ------- @@ -775,6 +896,38 @@ def pfa_v_mag_pu(self): def pfa_v_mag_pu(self, pypsa): self._pfa_v_mag_pu = pypsa + @property + def i_res(self): + """ + Current results from power flow analysis in A. + + Holds power flow analysis results for current for the last + iteration step. Index of the DataFrame is a DatetimeIndex indicating + the time period the power flow analysis was conducted for; columns + of the DataFrame are the edges as well as stations of the grid + topology. + ToDo: add unit + + Parameters + ---------- + pypsa: `pandas.DataFrame` + Results time series of current in A from the + `PyPSA network `_ + + Provide this if you want to set values. For retrieval of data do + not pass an argument + + Returns + ------- + :pandas:`pandas.DataFrame` + Current results from power flow analysis + """ + return self._i_res + + @i_res.setter + def i_res(self, pypsa): + self._i_res = pypsa + @property def equipment_changes(self): """ @@ -823,21 +976,48 @@ def equipment_changes(self, changes): @property def grid_expansion_costs(self): """ - Holds grid expansion costs in MEUR due to grid expansion measures - tracked in self.equipment_changes. + Holds grid expansion costs in kEUR due to grid expansion measures + tracked in self.equipment_changes and calculated in + edisgo.flex_opt.costs.grid_expansion_costs() Parameters ---------- - total_costs: float - Provide this if you want to set grid_expansion_costs. For - retrieval of costs do not pass an argument. + total_costs : :pandas:`pandas.DataFrame` + + DataFrame containing type and costs plus in the case of lines the + line length and number of parallel lines of each reinforced + transformer and line. Provide this if you want to set + grid_expansion_costs. For retrieval of costs do not pass an + argument. + + The DataFrame has the following columns: + + type: String + Transformer size or cable name + + total_costs: float + Costs of equipment in kEUR. For lines the line length and + number of parallel lines is already included in the total + costs. + + quantity: int + For transformers quantity is always one, for lines it specifies + the number of parallel lines. + + line_length: float + Length of line or in case of parallel lines all lines in km. Returns ------- - float + :pandas:`pandas.DataFrame` + Costs of each reinforced equipment in kEUR. + + Notes + ------- + Total grid expansion costs can be obtained through + costs.total_costs.sum(). + """ - if not self._grid_expansion_costs: - grid_expansion_costs(self) return self._grid_expansion_costs @grid_expansion_costs.setter @@ -846,9 +1026,9 @@ def grid_expansion_costs(self, total_costs): def s_res(self, components=None): """ - Get resulting apparent power at line(s) and transformer(s) + Get resulting apparent power in kVA at line(s) and transformer(s). - The apparent power at a line (or transformer) determines from the + The apparent power at a line (or transformer) is determined from the maximum values of active power P and reactive power Q. .. math:: @@ -857,12 +1037,13 @@ def s_res(self, components=None): Parameters ---------- - components : :class:`~.grid.components.Line` or :class:`~.grid.components.Transformer` + components : :class:`~.grid.components.Line` or + :class:`~.grid.components.Transformer` Could be a list of instances of these classes Line or Transformers objects of grid topology. If not provided - (respectively None) - defaults to return `s_res` of all lines and transformers in the grid. + (respectively None) defaults to return `s_res` of all lines and + transformers in the grid. Returns ------- @@ -876,7 +1057,8 @@ def s_res(self, components=None): labels_not_included = [] labels = [repr(l) for l in components] for label in labels: - if label in list(self.pfa_p.columns) and label in list(self.pfa_q.columns): + if (label in list(self.pfa_p.columns) and + label in list(self.pfa_q.columns)): labels_included.append(label) else: labels_not_included.append(label) @@ -887,7 +1069,7 @@ def s_res(self, components=None): labels_included = self.pfa_p.columns s_res = ((self.pfa_p[labels_included] ** 2 + self.pfa_q[ - labels_included] ** 2) * 1e3).applymap(sqrt) + labels_included] ** 2)).applymap(sqrt) return s_res diff --git a/edisgo/tools/pypsa_io.py b/edisgo/tools/pypsa_io.py index df3c5737b..66d0620d1 100644 --- a/edisgo/tools/pypsa_io.py +++ b/edisgo/tools/pypsa_io.py @@ -212,6 +212,27 @@ def mv_to_pypsa(network): * 'Line' * 'BranchTee' * 'Tranformer' + + + .. warning:: + + PyPSA takes resistance R and reactance X in p.u. The conversion from + values in ohm to pu notation is performed by following equations + + .. math:: + + r_{p.u.} = R_{\Omega} / Z_{B} + + x_{p.u.} = X_{\Omega} / Z_{B} + + with + + Z_{B} = V_B / S_B + + I'm quite sure, but its not 100 % clear if the base voltage V_B is + chosen correctly. We take the primary side voltage of transformer as + the transformers base voltage. See + `#54 `_ for discussion. """ generators = network.mv_grid.graph.nodes_by_attribute('generator') @@ -323,15 +344,18 @@ def mv_to_pypsa(network): bus['name'].append(bus1_name) bus['v_nom'].append(lv_st.transformers[0].voltage_op) + v_base = lv_st.mv_grid.voltage_nom # we choose voltage of transformers' primary side + for tr in lv_st.transformers: + z_base = v_base ** 2 / tr.type.S_nom transformer['name'].append( '_'.join([repr(lv_st), 'transformer', str(transformer_count)])) transformer['bus0'].append(bus0_name) transformer['bus1'].append(bus1_name) transformer['type'].append("") transformer['model'].append('pi') - transformer['r'].append(tr.type.R) - transformer['x'].append(tr.type.X) + transformer['r'].append(tr.type.R / z_base) + transformer['x'].append(tr.type.X / z_base) transformer['s_nom'].append(tr.type.S_nom / 1e3) transformer['tap_ratio'].append(1) @@ -545,7 +569,6 @@ def add_aggregated_lv_components(network, components): loads[lv_grid].setdefault(sector, 0) loads[lv_grid][sector] += val - # define dict for DataFrame creation of aggr. generation and load generator = {'name': [], 'bus': [], @@ -686,7 +709,23 @@ def _pypsa_generator_timeseries(network, mode=None): def _pypsa_bus_timeseries(network, buses, mode=None): - """Timeseries in PyPSA compatible format for generator instances + """Timeseries in PyPSA compatible format for bus instances + + Set all buses except for the slack bus to voltage of 1 pu (it is assumed + this setting is entirely ingnored during solving the power flow problem). + This slack bus is set to an operational voltage which is typically greater + than nominal voltage plus a control deviation. + The control deviation is always added positively to the operational voltage. + For example, the operational voltage (offset) is set to 1.025 pu plus the + control deviation of 0.015 pu. This adds up to a set voltage of the slack + bus of 1.04 pu. + + .. warning:: + + Voltage settings for the slack bus defined by this function assume the + feedin case (reverse power flow case) as the worst-case for the power + system. Thus, the set point for the slack is always greater 1. + Parameters ---------- @@ -706,8 +745,21 @@ def _pypsa_bus_timeseries(network, buses, mode=None): Time series table in PyPSA format """ - v_set_dict = {bus: 1 for bus in buses} + # get slack bus label + slack_bus = '_'.join( + ['Bus', network.mv_grid.station.__repr__(side='mv')]) + # set all buses (except slack bus) to nominal voltage + v_set_dict = {bus: 1 for bus in buses if bus != slack_bus} + + # Set slack bus to operational voltage (includes offset and control + # deviation + slack_voltage_pu = 1 + \ + network.scenario.parameters.hv_mv_transformer_offset + \ + network.scenario.parameters.hv_mv_transformer_control_deviation + v_set_dict.update({slack_bus: slack_voltage_pu}) + + # Convert to PyPSA compatible dataframe v_set_df = pd.DataFrame(v_set_dict, index=network.scenario.timeseries.timeindex) @@ -939,7 +991,6 @@ def _check_integrity_of_pypsa(pypsa_network): if duplicate_v_mag_set: raise ValueError("{labels} have duplicate entry in buses_t".format( labels=duplicate_v_mag_set)) - def process_pfa_results(network, pypsa): @@ -1108,14 +1159,18 @@ def update_pypsa(network): 's_nom': [], 'tap_ratio': []} + for idx, row in added_transformers.iterrows(): + v_base = idx.mv_grid.voltage_nom # we choose voltage of transformers' primary side + z_base = v_base ** 2 / row['equipment'].type.S_nom + transformer['bus0'].append('_'.join(['Bus', idx.__repr__(side='mv')])) transformer['bus1'].append('_'.join(['Bus', idx.__repr__(side='lv')])) transformer['name'].append(repr(row['equipment'])) transformer['type'].append("") transformer['model'].append('pi') - transformer['r'].append(row['equipment'].type.R) - transformer['x'].append(row['equipment'].type.X) + transformer['r'].append(row['equipment'].type.R / z_base) + transformer['x'].append(row['equipment'].type.X / z_base) transformer['s_nom'].append(row['equipment'].type.S_nom / 1e3) transformer['tap_ratio'].append(1) @@ -1125,22 +1180,12 @@ def update_pypsa(network): network.pypsa.import_components_from_dataframe( pd.DataFrame(transformer).set_index('name'), 'Transformer') - # Step 2: Update lines lines = equipment_changes.loc[equipment_changes.index[ equipment_changes.reset_index()['index'].apply( isinstance, args=(Line,))]] changed_lines = lines[lines['change'] == 'changed'] - line = {'name': [], - 'bus0': [], - 'bus1': [], - 'type': [], - 'x': [], - 'r': [], - 's_nom': [], - 'length': []} - lv_stations = network.mv_grid.graph.nodes_by_attribute('lv_station') omega = 2 * pi * 50 @@ -1154,6 +1199,7 @@ def update_pypsa(network): network.pypsa.lines.loc[repr(idx), 's_nom'] = ( sqrt(3) * idx.type['I_max_th'] * idx.type[ 'U_n'] * idx.quantity / 1e3) + network.pypsa.lines.loc[repr(idx), 'length'] = idx.length # Update buses line is connected to adj_nodes = idx.grid.graph.nodes_from_line(idx)