In [ ]:
__copyright__ = "Reiner Lemoine Institut gGmbH"
__license__ = "GNU Affero General Public License Version 3 (AGPL-3.0)"
__url__ = "https://github.com/openego/eDisGo/blob/master/LICENSE"
__author__ = "gplssm, birgits"
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.
eDisGo is developed in the open_eGo research project. It is based on PyPSA, a toolbox for simulation and optimization of power networks, and closely related to the ding0 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.
! eDisGo is work in progress ! We are therefore happy for any bug reports, hints, etc. you may have for us.
This notebook requires a working installation of eDisGo. Install eDisGo with
pip install eDisGo
In order to run this jupyter notebook install jupyter with
pip install jupyter
All plots plotting the MV grid topology can optionally be plotted on a map. You therefore need to install the python package contextily with
pip install contextily
Contextily requires a bunch of system packages you may have to install as well. See its documentation for more information.
This example shows the general usage of eDisGo. Grid expansion costs for an example distribution grid are calculated assuming renewable and conventional power plant capacities as stated in the scenario framework of the German Grid Development Plan 2015 (Netzentwicklungsplan) for the year 2035 (scenario B2).
In [ ]:
import os
import sys
import pandas as pd
from edisgo import EDisGo
In this section we start setting up the eDisGo calculation. The EDisGo class provides the top-level API for invocation of data import, grid reinforcement, flexibility measures, etc. (see class documentation for more information).
If you want to set up a scenario to do a worst-case analysis of a ding0 grid you simply have to provide a grid and set the 'worst_case_analysis' parameter.
The ding0 grid is specified through the input parameter 'ding0_grid'. The following assumes you have a file of a ding0 grid named “ding0_grid_example.pkl” in the current working directory.
In [ ]:
ding0_grid = os.path.join(sys.path[0], "ding0_grid_example.pkl")
If you don't have a ding0 grid yet, you can either download the ding0 dataset from zenodo (make sure to use the latest data set) or create a grid yourself by uncommenting and running the following lines:
In [ ]:
# from egoio.tools import db
# from sqlalchemy.orm import sessionmaker
# from ding0.core import NetworkDing0
# from ding0.tools.results import save_nd_to_pickle
# # choose MV Grid District to import and name under which to save it
# mv_grid_districts = [460]
# ding0_grid = 'ding0_grid_example.pkl'
# # database connection
# engine = db.connection(readonly=True)
# session = sessionmaker(bind=engine)()
# # instantiate new ding0 network object
# nd = NetworkDing0(name='network')
# # run DING0 on selected MV Grid District
# nd.run_ding0(session=session,
# mv_grid_districts_no=mv_grid_districts)
# # export grid to file (pickle)
# save_nd_to_pickle(nd, filename=ding0_grid)
In conventional grid expansion planning worst-cases, the heavy load flow and the reverse power flow, are used to determine grid expansion needs. eDisGo allows you to analyze these cases separately or together. Choose between the following options:
’worst-case-feedin’
Feed-in and demand for the worst-case scenario "reverse power flow" are generated. Demand is by default 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 the nominal power of the generator, except for PV systems where it is by default set to 85% of the nominal power.
’worst-case-load’
Feed-in and demand for the worst-case scenario "heavy load flow" are generated. Demand of all loads is by default set to maximum demand; feed-in of all generators is set to zero.
’worst-case’
Feed-in and demand for the two worst-case scenarios "reverse power flow" and "heavy load flow" are generated.
Feed-in and demand in the two worst-cases are defined in the config file 'config_timeseries.cfg' and can be changed by setting different values in the config file.
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 for more information.
In [ ]:
worst_case_analysis = 'worst-case'
Now we are ready to initialize the edisgo object.
In [ ]:
edisgo = EDisGo(ding0_grid='ding0_grid_example.pkl',
worst_case_analysis=worst_case_analysis)
The last line invoked a bunch of things. First of all it initialized the Network class which serves as an overall data container in eDisGo holding the grid data for the MV grid and LV grids, config data, results, timeseries, etc. It is linked from multiple locations and provides hierarchical access to all data. Network itself can be accessed via the EDisGo object as follows:
edisgo.network
The network topology is represented by separate undirected graphs for the MV grid and each of the LV grids. The Graph 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.
First we take a look at all the lines in the MV grid.
In [ ]:
# get a dictionary of all lines in the mv grid
edisgo.network.mv_grid.graph.edges
The dictionary you got should look something like that:
{Generator_x: {BranchTee_y: {'type': 'line', 'line': Line_1}},
BranchTee_y: {
Generator_x: {'type': 'line', 'line': Line_1},
BranchTee_z: {'type': 'line', 'line': Line_2}}
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 objects containig all important information about the line, such as length, equipment type, and geometry. Accessing line information can for example be done as follows:
In [ ]:
# get a list of all lines in the MV grid
lines = list(edisgo.network.mv_grid.graph.lines())
# choose first line
line = lines[0]
# get line length
print('line length: {} km'.format(line['line'].length))
# get adjacent nodes
print('adjacent nodes: {}'.format(line['adj_nodes']))
Now let's have a look at all the nodes in the MV grid.
In [ ]:
# get a list of all nodes (stations, generators, loads, branch tees)
# here, only the first 10 nodes are displayed
list(edisgo.network.mv_grid.graph.nodes())[:10]
You can also filter for certain kinds of nodes, e.g. generators...
In [ ]:
# get a list of all generators in the mv grid
edisgo.network.mv_grid.graph.nodes_by_attribute('generator')
... or get a list of all lv grids.
In [ ]:
# get a list of all lv grids
# here, only the first 10 lv grids are displayed
list(edisgo.network.mv_grid.lv_grids)[:10]
The graphs can also be plotted. As the MV grid is georeferenced edisgo provides a function to plot it on a map. The LV grids are not georeferenced and can only be plotted using the networkx draw() function.
In [ ]:
# plot MV grid on a map
edisgo.plot_mv_grid_topology()
In [ ]:
# draw graph of one of the LV grids
import networkx as nx
lv_grid = list(edisgo.network.mv_grid.lv_grids)[5]
nx.draw(lv_grid.graph)
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 and assumes a share of renewables of 100% and again an equal electricity demand as in the status quo.
As mentioned earlier, ding0 grids represent status quo networks with status quo generator capacities. In order to analyse future scenarios future generators have to be imported into the network.
In [ ]:
# Import generators
scenario = 'nep2035'
edisgo.import_generators(generator_scenario=scenario)
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.
In [ ]:
# get first ten generators
edisgo.network.mv_grid.generators[:10]
Now we can finally calculate grid expansion costs.
The grid expansion methodology is based on the distribution grid study of dena [1] and Baden-Wuerttemberg [2]. The order grid expansion measures are conducted is as follows:
Reinforcement of transformers and lines due to overloading issues is performed twice, once in the beginning and again after fixing voltage problems, as the changed power flows after reinforcing the grid may lead to new overloading issues. (For further explanation see the documentation.)
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.
In [ ]:
# Do non-linear power flow analysis with PyPSA
edisgo.analyze()
Let's check voltages and line loadings before the reinforcement.
In [ ]:
# get voltages in grid
v_mag_pu_pfa = edisgo.network.results.v_res()
v_mag_pu_pfa
In [ ]:
edisgo.histogram_voltage(x_limits=(0.85, 1.10), binwidth=0.005)
In [ ]:
edisgo.plot_mv_line_loading()
Reinforcement is invoked doing the following:
In [ ]:
# Do grid reinforcement
edisgo.reinforce()
Let's check voltages and line loadings again:
In [ ]:
edisgo.histogram_voltage(x_limits=(0.85, 1.10), binwidth=0.005)
In [ ]:
edisgo.plot_mv_line_loading()
Above we already saw how to access voltage results from the power flow analysis. All results are stored in the Results object and can be accessed through
edisgo.network.results
All changes such as removed and new lines and new transformers can be viewed as follows:
In [ ]:
edisgo.network.results.equipment_changes
You can also retrieve grid expansion costs through:
In [ ]:
costs = edisgo.network.results.grid_expansion_costs
costs
In [ ]:
# group costs by type
costs_grouped_nep = costs.groupby(['voltage_level']).sum()
costs_grouped_nep.loc[:, ['total_costs']]
An overview of the assumptions used to calculate grid expansion costs can be found in the documentation.
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.
In [ ]:
# initialize new EDisGo object with 'ego 100' scenario
edisgo_ego100 = EDisGo(ding0_grid=ding0_grid,
worst_case_analysis=worst_case_analysis,
generator_scenario='ego100')
# conduct grid reinforcement
edisgo_ego100.reinforce()
In [ ]:
# get grouped costs
costs_grouped_ego100 = edisgo_ego100.network.results.grid_expansion_costs.groupby(['voltage_level']).sum()
costs_grouped_ego100.loc[:, ['total_costs']]
In [ ]:
# compare expansion costs for both scenarios in a plot
import matplotlib.pyplot as plt
# set up dataframe to plot
costs_df = costs_grouped_nep.loc[:, ['total_costs']].join(costs_grouped_ego100.loc[:, ['total_costs']], rsuffix='_ego100', lsuffix='_nep2035').rename(
columns={'total_costs_ego100': 'ego100',
'total_costs_nep2035': 'NEP2035'}).T
# plot
costs_df.plot(kind='bar', stacked=True)
plt.xticks(rotation=0)
plt.ylabel('Grid reinforcement costs in k€');
In [ ]: