Electromobility example

This example shows you the first steps how to integrate electromobility into eDisGo using data from SimBEV and TracBEV. SimBEV provides data on standing times, charging demand, etc. per vehicle, whereas TracBEV provides potential charging point locations.

Learn more about eDisGo

Installation and setup

This notebook requires a working installation of eDisGo. Checkout the eDisGo documentation on how to install eDisGo for more information.

Import packages

[ ]:
import os

from copy import deepcopy

import geopandas as gpd
import matplotlib.pyplot as plt
import pandas as pd
import requests

from edisgo.edisgo import EDisGo
from edisgo.tools.logger import setup_logger

# Ensure plots are displayed inline in the notebook
%matplotlib inline
[ ]:
# interactive matplotlib
%matplotlib notebook

Set up logger

[ ]:
# set up logger that streams edisgo logging messages with level info and above
# and other logging messages with level error and above to stdout
setup_logger(
    loggers=[
        {"name": "root", "file_level": None, "stream_level": "error"},
        {"name": "edisgo", "file_level": None, "stream_level": "info"},
    ]
)

Download example grid

[ ]:
def download_ding0_example_grid():

    # create directories to save ding0 example grid into
    ding0_example_grid_path = os.path.join(
        os.path.expanduser("~"), ".edisgo", "ding0_test_network"
    )
    os.makedirs(ding0_example_grid_path, exist_ok=True)

    # download files
    filenames = [
        "buses",
        "generators",
        "lines",
        "loads",
        "network",
        "switches",
        "transformers",
        "transformers_hvmv",
    ]

    for file in filenames:
        req = requests.get(
            f"https://raw.githubusercontent.com/openego/eDisGo/dev/tests/data/ding0_test_network_2/{file}.csv"
        )
        filename = os.path.join(ding0_example_grid_path, f"{file}.csv")
        with open(filename, "wb") as fout:
            fout.write(req.content)


download_ding0_example_grid()

Set up edisgo object

[ ]:
ding0_grid = os.path.join(os.path.expanduser("~"), ".edisgo", "ding0_test_network")
edisgo = EDisGo(ding0_grid=ding0_grid)

# set up time series
timeindex = pd.date_range("1/1/2011", periods=24 * 7, freq="H")
edisgo.set_timeindex(timeindex)
edisgo.set_time_series_active_power_predefined(
    fluctuating_generators_ts="oedb",
    dispatchable_generators_ts=pd.DataFrame(data=1, columns=["other"], index=timeindex),
    conventional_loads_ts="demandlib",
)
edisgo.set_time_series_reactive_power_control()

# resample time series to have a temporal resolution of 15 minutes, which is the same
# as the electromobility time series
edisgo.resample_timeseries()
[ ]:
# plot feed-in, demand and residual load

fig, ax = plt.subplots(figsize=(8, 6))

edisgo.timeseries.generators_active_power.sum(axis=1).plot.line(ax=ax)
edisgo.timeseries.loads_active_power.sum(axis=1).plot.line(ax=ax)
edisgo.timeseries.residual_load.plot.line(ax=ax)

ax.legend(["Feed-in", "Demand", "Residual load"])
ax.set_ylabel("Power in MW")

plt.show()

Prerequisite data

Currently, eDisGo only provides an automated process to obtain electromobility data from SimBEV and TracBEV.

Since SimBEV and TracBEV generate data on municipality level, it is necessary to determine which municipalities lie within or intersect the network area. Therefore, municipality geodata is necessary. The download and how to find the municipalities that intersect the chosen MV grid district is shown in the following.

Download ‘Verwaltungsgebiete’ data

The following code shows you how to download the original municipality geodata. To make the example faster we will however skip the download and use a reduced version of the dataset containing only the municipalities that intersect with the grid district used in this example. The reduced dataset is loaded in the next cell.

vg250_path = os.path.join(
        os.path.expanduser("~"), ".edisgo", "vg250"
    )
target = Path(vg250_path, "vg250_01-01.geo84.shape.ebenen/vg250_ebenen_0101/VG250_GEM.shp")

if not target.is_file():
    vg250_path.mkdir(parents=True, exist_ok=True)

    filename = os.path.join(vg250_path, "vg250.geo84")

    url = "https://daten.gdz.bkg.bund.de/produkte/vg/vg250_ebenen_0101/2020/vg250_01-01.geo84.shape.ebenen.zip"
    req = requests.get(url)

    with open(filename, "wb") as fout:
        fout.write(req.content)

    with zipfile.ZipFile(filename, "r") as zip_ref:
        zip_ref.extractall(vg250_path)

vg250 = gpd.read_file(target)
[ ]:
def download_vg250_data():

    # create directory to save vg250 data into
    vg250_path = os.path.join(os.path.expanduser("~"), ".edisgo", "vg250")
    os.makedirs(vg250_path, exist_ok=True)

    # download files
    filenames = [
        "vg250.cpg",
        "vg250.dbf",
        "vg250.prj",
        "vg250.shp",
        "vg250.shx",
    ]

    for file in filenames:
        req = requests.get(
            f"https://raw.githubusercontent.com/openego/eDisGo/dev/examples/data/{file}"
        )
        filename = os.path.join(vg250_path, f"{file}")
        with open(filename, "wb") as fout:
            fout.write(req.content)

    return vg250_path


vg250_path = download_vg250_data()
vg250 = gpd.read_file(os.path.join(vg250_path, "vg250.shp"))

Check which ‘Verwaltungsgebiete’ intersect MV grid

[ ]:
mv_grid_gdf = gpd.GeoDataFrame(
    pd.DataFrame(data={"geometry": [edisgo.topology.grid_district["geom"]]}),
    crs=f"EPSG:{edisgo.topology.grid_district['srid']}",
)

intersect_gdf = mv_grid_gdf.sjoin(vg250)
print("Intersecting AGS")
intersect_gdf.AGS.to_list()
[ ]:
# plot MV grid district (black line) and intersecting AGS (blue shapes)
fig, ax = plt.subplots(figsize=(5, 8))

vg250.loc[vg250.AGS.isin(intersect_gdf.AGS)].plot(ax=ax)
mv_grid_gdf.boundary.plot(ax=ax, color="black")

plt.show()

As most municipalities only intersect the grid district at its border, only the electromobility data for one municipality needs to be generated.

[ ]:
# plot MV grid district (black line) and mainly intersecting AGS (blue shape)
fig, ax = plt.subplots(figsize=(5, 5))

vg250.loc[vg250.AGS == "05334032"].plot(ax=ax)
mv_grid_gdf.boundary.plot(ax=ax, color="black")

plt.show()

Add electromobility to EDisGo object

Electromobility data

So far, adding electromobility data to an EDisGo object requires electromobility data from SimBEV (required version: 3083c5a) and TracBEV (required version: 14d864c) to be pre-generated. The data is currently not created automatically.

If you don’t have SimBEV and TracBEV data yet, you can use the data provided for this example for the ding0 grid downloaded above.

In order to import the electromobility data of the grid that you downloaded above and integrate charging points into the grid, you can use the function EDisGo.import_electromobility. Besides loading the electromobility data, the function also allocates the charging demand from SimBEV to charging sites from TracBEV and integrates the charging parks into the grid. This is further explained in the following.

Allocation of charging demand

After electromobility data is loaded, the charging demand from SimBEV is allocated to potential charging parks from TracBEV. The allocation of the charging processes to the charging infrastructure is carried out with the help of the weighting factor of the potential charging parks determined by TracBEV. This involves a random and weighted selection of one charging park per charging process. In the case of private charging infrastructure, a separate charging point is set up for each EV. All charging processes of the respective EV and charging use case are assigned to this charging point.

For the public charging infrastructure, the allocation is made explicitly per charging process. For each charging process it is determined whether a suitable charging point is already available. For this purpose it is checked whether the charging point is occupied by another EV in the corresponding period and whether it can provide the corresponding charging capacity. If no suitable charging point is available, a charging point is determined randomly and weighted in the same way as for private charging.

Integration of charging parks

After the allocation of charging demand to specific charging sites, all potential charging parks with charging demand allocated to them are integrated into the grid. This is realised the following way:

  • If power rating is <= 0.3 MVA, the charging point is integrated into the LV grid, otherwise it is integrated into the MV grid.

  • Integration into LV grid:

    • The considered charging point is integrated into the LV grid whose distribution substation is closest (this is currently done this way because the LV grids are not georeferenced but only the MV grid including the MV-LV substations).

    • If power rating is > 0.1 MVA, the charging point is directly connected to the distribution substation.

    • If power rating is <= 0.1 MVA, the type of connection depends on the charging point use case:

      • Use Case home: Charging point is connected to a random household load in the identified LV grid.

      • Use Case work: Charging point is connected to a random commercial, industrial or agricultural consumer.

      • Use Case public: Charging point is connected to a random grid connection point in the identified LV grid.

  • Integration into MV grid:

    • If the power rating of the charging point is > 4.5 MVA, it is directly connected to the HV-MV station.

    • If the power rating of the charging point is <= 4.5 MVA, it is connected to the nearest grid connection point or cable. If a cable is selected, the line is cut at the point closest to the charging station and a new branch tee is added to which the charging station is connected.

[ ]:
# Download SimBEV data


def download_simbev_example_data():

    raw_url = (
        "https://raw.githubusercontent.com/openego/eDisGo/dev/"
        + "tests/data/simbev_example_scenario/"
    )
    ags = "5334032"

    # create directories to save data to
    simbev_example_data_path = os.path.join(
        os.path.expanduser("~"), ".edisgo", "simbev_example_data"
    )
    os.makedirs(simbev_example_data_path, exist_ok=True)
    os.makedirs(os.path.join(simbev_example_data_path, ags), exist_ok=True)
    # download files
    url = (
        "https://github.com/openego/eDisGo/tree/dev/"
        + f"tests/data/simbev_example_scenario/{ags}/"
    )
    page = requests.get(url).text
    filenames = [_ for _ in page.split('"') if ".csv" in _ and "/" not in _]

    for file in filenames:
        req = requests.get(f"{raw_url}/{ags}/{file}")
        filename = os.path.join(simbev_example_data_path, ags, f"{file}")
        with open(filename, "wb") as fout:
            fout.write(req.content)

    req = requests.get(f"{raw_url}/metadata_simbev_run.json")
    filename = os.path.join(simbev_example_data_path, "metadata_simbev_run.json")
    with open(filename, "wb") as fout:
        fout.write(req.content)

    return simbev_example_data_path


simbev_example_data_path = download_simbev_example_data()
[ ]:
# Download TracBEV data


def download_tracbev_example_data():

    # create directories to save data to
    tracbev_example_data_path = os.path.join(
        os.path.expanduser("~"), ".edisgo", "tracbev_example_data"
    )
    os.makedirs(tracbev_example_data_path, exist_ok=True)

    # download files
    url = (
        "https://github.com/openego/eDisGo/tree/dev/"
        + "tests/data/tracbev_example_scenario/"
    )
    page = requests.get(url).text
    filenames = [_ for _ in page.split('"') if ".gpkg" in _ and "/" not in _]

    for file in filenames:
        req = requests.get(
            "https://raw.githubusercontent.com/openego/eDisGo/dev/"
            + f"tests/data/tracbev_example_scenario/{file}"
        )
        filename = os.path.join(tracbev_example_data_path, f"{file}")
        with open(filename, "wb") as fout:
            fout.write(req.content)

    return tracbev_example_data_path


tracbev_example_data_path = download_tracbev_example_data()
[ ]:
edisgo.import_electromobility(
    data_source="directory",
    charging_processes_dir=simbev_example_data_path,
    potential_charging_points_dir=tracbev_example_data_path,
)

eDisGo electromobility data structure

All data coming from SimBEV and TracBEV is stored in the Electromobility object that can be accessed through the EDisGo object as follows:

edisgo.electromobility

Integrated charging parks can also be found in the Topology object:

edisgo.topology.loads_df[edisgo.topology.loads_df.type == "charging_point"]

Data stored in the Electromobility object is shown in the following.

[ ]:
# SimBEV charging processes data
edisgo.electromobility.charging_processes_df.head()
[ ]:
# SimBEV configuration data
edisgo.electromobility.simbev_config_df
[ ]:
# TracBEV potential charging point data
edisgo.electromobility.potential_charging_parks_gdf.head()
[ ]:
# Charging parks that got integrated into the network
edisgo.electromobility.integrated_charging_parks_df.head()
[ ]:
edisgo.topology.loads_df[edisgo.topology.loads_df.type == "charging_point"].head()
[ ]:
# plotting the grid district and all potential charging parks

fig, ax = plt.subplots(figsize=(11, 11))

mv_grid_gdf.boundary.plot(ax=ax, color="black")

# plot potential charging parks
edisgo.electromobility.potential_charging_parks_gdf.plot(ax=ax, alpha=0.3)

# plot integrated charging parks
edisgo.electromobility.potential_charging_parks_gdf.loc[
    edisgo.electromobility.integrated_charging_parks_df.index
].plot(ax=ax, color="green", markersize=50)

# plot charging parks with charging demand but outside of the grid district
# and therefore not integrated
charging_parks_with_charging_demand = (
    edisgo.electromobility.charging_processes_df.charging_park_id.unique()
)
charging_parks_not_integrated = list(
    set(charging_parks_with_charging_demand)
    - set(edisgo.electromobility.integrated_charging_parks_df.index)
)

edisgo.electromobility.potential_charging_parks_gdf.loc[
    charging_parks_not_integrated
].plot(ax=ax, color="red", markersize=50)

ax.legend(
    [
        "Grid district",
        "Potential charging parks",
        "Integrated charging parks",
        "Charging parks with charging demand not integrated",
    ]
)

plt.show()

Applying different charging strategies

The EDisGo.import_electromobility() function does not yield charging time series per charging point but only charging processes taking place at each charging point. The actual charging time series are determined through applying a charging strategy using the function EDisGo.apply_charging_strategy.

The eDisGo tool currently offers three different charging strategies: dumb, reduced and residual. The aim of the charging strategies ‘reduced’ and ‘residual’ is to generate the most grid-friendly charging behavior possible without restricting the convenience for end users. Therefore, the boundary condition of all charging strategies is that the charging requirement of each charging process must be fully covered. This means that charging processes can only be flexibilised if the EV can be fully charged while it is stationary. Furthermore, only private charging processes can be used as a flexibility, since the fulfillment of the service is the priority for public charging processes.

  • dumb: In this charging strategy the cars are charged directly after arrival with the maximum possible charging capacity.

  • reduced: This is a preventive charging strategy. The cars are charged directly after arrival with the minimum possible charging power. The minimum possible charging power is determined by the parking time and the parameter minimum_charging_capacity_factor.

  • residual: This is an active charging strategy. The cars are charged when the residual load in the MV grid is lowest (high generation and low consumption). Charging processes with a low flexibility are given priority.

In the following all three charging strategies are applied. To show their differences, three EDisGo objects are used.

[ ]:
# copy edisgo object to have three objects to apply charging strategies on
edisgo2 = deepcopy(edisgo)
edisgo3 = deepcopy(edisgo)
[ ]:
# apply default charging strategy "dumb"
edisgo.apply_charging_strategy()
[ ]:
# conduct grid analysis
# to keep the calculation time low in this example, only time steps with maximum and
# minimum residual load are analysed
residual_load = edisgo.timeseries.residual_load
worst_case_time_steps = pd.DatetimeIndex(
    [residual_load.idxmin(), residual_load.idxmax()]
)
edisgo.analyze(timesteps=worst_case_time_steps);

To change the charging strategy from the default dumb to one of the other strategies, the strategy parameter has to be set accordingly:

[ ]:
edisgo2.apply_charging_strategy(strategy="reduced")
edisgo2.analyze(timesteps=worst_case_time_steps);
[ ]:
edisgo3.apply_charging_strategy(strategy="residual")
edisgo3.analyze(timesteps=worst_case_time_steps);

Plot charging time series for different charging strategies

[ ]:
fig, ax = plt.subplots(1, 1, figsize=(9, 5))

edisgo.timeseries.charging_points_active_power(edisgo).sum(axis=1).plot.line(
    ax=ax, color="blue", legend=True, label="dumb"
)
edisgo2.timeseries.charging_points_active_power(edisgo2).sum(axis=1).plot.line(
    ax=ax, color="red", legend=True, label="reduced"
)
edisgo3.timeseries.charging_points_active_power(edisgo3).sum(axis=1).plot.line(
    ax=ax, color="cyan", legend=True, label="residual"
)

plt.tight_layout()

plt.show()