diff --git a/examples/edisgo_documentation_examples.ipynb b/examples/edisgo_documentation_examples.ipynb new file mode 100644 index 000000000..113953251 --- /dev/null +++ b/examples/edisgo_documentation_examples.ipynb @@ -0,0 +1,640 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "f5954919", + "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, khelfen\"" + ] + }, + { + "cell_type": "markdown", + "id": "872814eb", + "metadata": {}, + "source": [ + "# A minimum working example\n", + "\n", + "Following you find short examples on how to use eDisGo to set up a network and time series information for loads and generators in the network and afterwards conduct a power flow analysis and determine possible grid expansion needs and costs. Further details are provided in Usage details. Further examples can be found in the examples directory.\n", + "\n", + "All following examples assume you have a ding0 grid topology (directory containing csv files, defining the grid topology) in a directory “ding0_example_grid” in the directory from where you run your example. If you do not have an example grid, you can download one here.\n", + "\n", + "Aside from grid topology data you may eventually need a dataset on future installation of power plants. You may therefore use the scenarios developed in the open_eGo project that are available in the OpenEnergy DataBase (oedb) hosted on the OpenEnergy Platform (OEP). eDisGo provides an interface to the oedb using the package ego.io. ego.io gives you a python SQL-Alchemy representations of the oedb and access to it by using the oedialect, an SQL-Alchemy dialect used by the OEP.\n", + "\n", + "You can run a worst-case scenario as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "295e6256", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import pandas as pd\n", + "from edisgo import EDisGo" + ] + }, + { + "cell_type": "markdown", + "id": "8f0c2ed1", + "metadata": {}, + "source": [ + "### Download example grid" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "889a3eec", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "\n", + "def download_ding0_example_grid():\n", + "\n", + " # create directories to save ding0 example grid into\n", + " ding0_example_grid_path = os.path.join(\n", + " os.path.expanduser(\"~\"), \".edisgo\", \"ding0_test_network\"\n", + " )\n", + " os.makedirs(ding0_example_grid_path, exist_ok=True)\n", + "\n", + " # download files\n", + " filenames = [\n", + " \"buses\",\n", + " \"generators\",\n", + " \"lines\",\n", + " \"loads\",\n", + " \"network\",\n", + " \"switches\",\n", + " \"transformers\",\n", + " \"transformers_hvmv\",\n", + " ]\n", + "\n", + " for file in filenames:\n", + " req = requests.get(\n", + " \"https://raw.githubusercontent.com/openego/eDisGo/features/%23261-emob-tests/tests/data/ding0_test_network_2/{}.csv\".format(\n", + " file\n", + " )\n", + " )\n", + " filename = os.path.join(ding0_example_grid_path, \"{}.csv\".format(file))\n", + " with open(filename, \"wb\") as fout:\n", + " fout.write(req.content)\n", + "\n", + "\n", + "download_ding0_example_grid()\n", + "ding0_grid = os.path.join(os.path.expanduser(\"~\"), \".edisgo\", \"ding0_test_network\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "347a0ec5", + "metadata": {}, + "outputs": [], + "source": [ + "# Set up the EDisGo object - the EDisGo object provides the top-level API for\n", + "# invocation of data import, power flow analysis, network reinforcement,\n", + "# flexibility measures, etc..\n", + "edisgo_obj = EDisGo(ding0_grid=ding0_grid)\n", + "\n", + "# Import scenario for future generator park from the oedb\n", + "edisgo_obj.import_generators(generator_scenario=\"nep2035\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70f22ee9", + "metadata": {}, + "outputs": [], + "source": [ + "# Set up feed-in and load time series (here for a worst case analysis)\n", + "edisgo_obj.set_time_series_worst_case_analysis()\n", + "\n", + "# Conduct power flow analysis (non-linear power flow using PyPSA)\n", + "edisgo_obj.analyze()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b7c29df2", + "metadata": {}, + "outputs": [], + "source": [ + "# Do grid reinforcement\n", + "edisgo_obj.reinforce()\n", + "\n", + "# Determine costs for each line/transformer that was reinforced\n", + "costs = edisgo_obj.results.grid_expansion_costs" + ] + }, + { + "cell_type": "markdown", + "id": "ccdaa6a0", + "metadata": {}, + "source": [ + "Instead of conducting a worst-case analysis you can also provide specific time series:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce514ff0", + "metadata": {}, + "outputs": [], + "source": [ + "# Set up the EDisGo object with generator park scenario NEP2035\n", + "edisgo_obj = EDisGo(\n", + " ding0_grid=ding0_grid,\n", + " generator_scenario=\"nep2035\"\n", + ")\n", + "\n", + "# Set up your own time series by load sector and generator type (these are dummy\n", + "# time series!)\n", + "timeindex = pd.date_range(\"1/1/2011\", periods=4, freq=\"H\")\n", + "# load time series (scaled by annual demand)\n", + "timeseries_load = pd.DataFrame(\n", + " {\"residential\": [0.0001] * len(timeindex),\n", + " \"retail\": [0.0002] * len(timeindex),\n", + " \"industrial\": [0.00015] * len(timeindex),\n", + " \"agricultural\": [0.00005] * len(timeindex)\n", + " },\n", + " index=timeindex)\n", + "# feed-in time series of fluctuating generators (scaled by nominal power)\n", + "timeseries_generation_fluctuating = pd.DataFrame(\n", + " {\"solar\": [0.2] * len(timeindex),\n", + " \"wind\": [0.3] * len(timeindex)\n", + " },\n", + " index=timeindex)\n", + "# feed-in time series of dispatchable generators (scaled by nominal power)\n", + "timeseries_generation_dispatchable = pd.DataFrame(\n", + " {\"biomass\": [1] * len(timeindex),\n", + " \"coal\": [1] * len(timeindex),\n", + " \"other\": [1] * len(timeindex)\n", + " },\n", + " index=timeindex)\n", + "\n", + "# Before you can set the time series to the edisgo_obj you need to set the time\n", + "# index (this could also be done upon initialisation of the edisgo_obj) - the time\n", + "# index specifies which time steps to consider in power flow analysis\n", + "edisgo_obj.set_timeindex(timeindex)\n", + "\n", + "# Now you can set the active power time series of loads and generators in the grid\n", + "edisgo_obj.set_time_series_active_power_predefined(\n", + " conventional_loads_ts=timeseries_load,\n", + " fluctuating_generators_ts=timeseries_generation_fluctuating,\n", + " dispatchable_generators_ts=timeseries_generation_dispatchable\n", + ")\n", + "\n", + "# Before you can now run a power flow analysis and determine grid expansion needs,\n", + "# reactive power time series of the loads and generators also need to be set. If you\n", + "# simply want to use default configurations, you can do the following.\n", + "edisgo_obj.set_time_series_reactive_power_control()\n", + "\n", + "# Now you are ready to determine grid expansion needs\n", + "edisgo_obj.reinforce()\n", + "\n", + "# Determine cost for each line/transformer that was reinforced\n", + "costs = edisgo_obj.results.grid_expansion_costs" + ] + }, + { + "cell_type": "markdown", + "id": "a71118bb", + "metadata": {}, + "source": [ + "Time series for loads and fluctuating generators can also be automatically generated using the provided API for the oemof demandlib and the OpenEnergy DataBase:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1c3c996d", + "metadata": {}, + "outputs": [], + "source": [ + "# Set up the EDisGo object with generator park scenario NEP2035 and time index\n", + "timeindex = pd.date_range(\"1/1/2011\", periods=4, freq=\"H\")\n", + "edisgo_obj = EDisGo(\n", + " ding0_grid=ding0_grid,\n", + " generator_scenario=\"nep2035\",\n", + " timeindex=timeindex\n", + ")\n", + "\n", + "# Set up your own time series by load sector and generator type (these are dummy\n", + "# time series!)\n", + "# Set up active power time series of loads and generators in the grid using prede-\n", + "# fined profiles per load sector and technology type\n", + "# (There are currently no predefined profiles for dispatchable generators, wherefore\n", + "# their feed-in profiles need to be provided)\n", + "timeseries_generation_dispatchable = pd.DataFrame(\n", + " {\"biomass\": [1] * len(timeindex),\n", + " \"coal\": [1] * len(timeindex),\n", + " \"other\": [1] * len(timeindex)\n", + " },\n", + " index=timeindex\n", + ")\n", + "edisgo_obj.set_time_series_active_power_predefined(\n", + " conventional_loads_ts=\"demandlib\",\n", + " fluctuating_generators_ts=\"oedb\",\n", + " dispatchable_generators_ts=timeseries_generation_dispatchable\n", + ")\n", + "\n", + "# Before you can now run a power flow analysis and determine grid expansion needs,\n", + "# reactive power time series of the loads and generators also need to be set. Here,\n", + "# default configurations are again used.\n", + "edisgo_obj.set_time_series_reactive_power_control()\n", + "\n", + "# Do grid reinforcement\n", + "edisgo_obj.reinforce()\n", + "\n", + "# Determine cost for each line/transformer that was reinforced\n", + "costs = edisgo_obj.results.grid_expansion_costs\n" + ] + }, + { + "cell_type": "markdown", + "id": "6777c87d", + "metadata": {}, + "source": [ + "# Usage details\n", + "## The fundamental data structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a2818c53", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo = edisgo_obj" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ca2061d", + "metadata": {}, + "outputs": [], + "source": [ + "# Access Topology grid data container object\n", + "edisgo.topology\n", + "\n", + "# Access TimeSeries data container object\n", + "edisgo.timeseries\n", + "\n", + "# Access Electromobility data container object\n", + "edisgo.electromobility\n", + "\n", + "# Access Results data container object\n", + "edisgo.results\n", + "\n", + "# Access configuration data container object\n", + "edisgo.config\n", + "\n", + "# Access all buses in MV grid and underlying LV grids\n", + "edisgo.topology.buses_df\n", + "\n", + "# Access all lines in MV grid and underlying LV grids\n", + "edisgo.topology.lines_df\n", + "\n", + "# Access all MV/LV transformers\n", + "edisgo.topology.transformers_df\n", + "\n", + "# Access all HV/MV transformers\n", + "edisgo.topology.transformers_hvmv_df\n", + "\n", + "# Access all switches in MV grid and underlying LV grids\n", + "edisgo.topology.switches_df\n", + "\n", + "# Access all generators in MV grid and underlying LV grids\n", + "edisgo.topology.generators_df\n", + "\n", + "# Access all loads in MV grid and underlying LV grids\n", + "edisgo.topology.loads_df\n", + "\n", + "# Access all storage units in MV grid and underlying LV grids\n", + "edisgo.topology.storage_units_df\n", + "\n", + "# Access MV grid\n", + "edisgo.topology.mv_grid\n", + "\n", + "# Access all buses in MV grid\n", + "edisgo.topology.mv_grid.buses_df\n", + "\n", + "# Access all generators in MV grid\n", + "edisgo.topology.mv_grid.generators_df\n", + "\n", + "# Get list of all underlying LV grids\n", + "# (Note that MVGrid.lv_grids returns a generator object that must first be\n", + "# converted to a list in order to view the LVGrid objects)\n", + "list(edisgo.topology.mv_grid.lv_grids)\n", + "# the following yields the same\n", + "list(edisgo.topology.lv_grids)\n", + "\n", + "# Get single LV grid by providing its ID (e.g. 1) or name (e.g. \"LVGrid_1\")\n", + "lv_grid = edisgo.topology.get_lv_grid(\"LVGrid_402945\")\n", + "\n", + "# Access all buses in that LV grid\n", + "lv_grid.buses_df\n", + "\n", + "# Access all loads in that LV grid\n", + "lv_grid.loads_df\n", + "\n", + "# Get all switch disconnectors in MV grid as Switch objects\n", + "# (Note that objects are returned as a python generator object that must\n", + "# first be converted to a list in order to view the Switch objects)\n", + "list(edisgo.topology.mv_grid.switch_disconnectors)\n", + "\n", + "# Get all generators in LV grid as Generator objects\n", + "list(lv_grid.generators)\n", + "\n", + "# Get graph representation of whole topology\n", + "edisgo.to_graph()\n", + "\n", + "# Get graph representation for MV grid\n", + "edisgo.topology.mv_grid.graph\n", + "\n", + "# Get graph representation for LV grid\n", + "lv_grid.graph" + ] + }, + { + "cell_type": "markdown", + "id": "1b07cef8", + "metadata": {}, + "source": [ + "## Component time series" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38c914c2", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.set_time_series_manual()\n", + "edisgo.set_time_series_worst_case_analysis()\n", + "edisgo.timeseries.timeindex_worst_cases\n", + "edisgo.set_time_series_active_power_predefined()\n", + "#edisgo.apply_charging_strategy()\n", + "edisgo.set_time_series_reactive_power_control()" + ] + }, + { + "cell_type": "markdown", + "id": "23a23240", + "metadata": {}, + "source": [ + "## Identifying grid issues" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b4ed0e9b", + "metadata": {}, + "outputs": [], + "source": [ + "# Do non-linear power flow analysis for MV and LV grid\n", + "edisgo.analyze()" + ] + }, + { + "cell_type": "markdown", + "id": "badf27fc", + "metadata": {}, + "source": [ + "## Grid expansion" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f724c0a9", + "metadata": {}, + "outputs": [], + "source": [ + "# Reinforce grid due to overloading and overvoltage issues\n", + "edisgo.reinforce()\n", + "\n", + "# Get costs of grid expansion\n", + "costs = edisgo.results.grid_expansion_costs" + ] + }, + { + "cell_type": "markdown", + "id": "1a1f4524", + "metadata": {}, + "source": [ + "## Electromobility" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b64f638c", + "metadata": {}, + "outputs": [], + "source": [ + "# ToDo" + ] + }, + { + "cell_type": "markdown", + "id": "5e6a3807", + "metadata": {}, + "source": [ + "## Battery storage systems" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "382689ce", + "metadata": {}, + "outputs": [], + "source": [ + "# Set up EDisGo object\n", + "edisgo = EDisGo(ding0_grid=ding0_grid)\n", + "\n", + "# Get random bus to connect storage to\n", + "random_bus = edisgo.topology.buses_df.index[3]\n", + "# Add storage instance\n", + "edisgo.add_component(\n", + " comp_type=\"storage_unit\",\n", + " add_ts=False,\n", + " bus=random_bus,\n", + " p_nom=4\n", + ")\n", + "\n", + "# Set up worst case time series for loads, generators and storage unit\n", + "edisgo.set_time_series_worst_case_analysis()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "91d0f566", + "metadata": {}, + "outputs": [], + "source": [ + "# Set up the EDisGo object\n", + "timeindex = pd.date_range(\"1/1/2011\", periods=4, freq=\"H\")\n", + "edisgo = EDisGo(\n", + " ding0_grid=ding0_grid,\n", + " generator_scenario=\"ego100\",\n", + " timeindex=timeindex\n", + ")\n", + "\n", + "# Add time series for loads and generators\n", + "timeseries_generation_dispatchable = pd.DataFrame(\n", + " {\"biomass\": [1] * len(timeindex),\n", + " \"coal\": [1] * len(timeindex),\n", + " \"other\": [1] * len(timeindex)\n", + " },\n", + " index=timeindex\n", + ")\n", + "edisgo.set_time_series_active_power_predefined(\n", + " conventional_loads_ts=\"demandlib\",\n", + " fluctuating_generators_ts=\"oedb\",\n", + " dispatchable_generators_ts=timeseries_generation_dispatchable\n", + ")\n", + "edisgo.set_time_series_reactive_power_control()\n", + "\n", + "# Add storage unit to random bus with time series\n", + "edisgo.add_component(\n", + " comp_type=\"storage_unit\",\n", + " bus=edisgo.topology.buses_df.index[3],\n", + " p_nom=4,\n", + " ts_active_power=pd.Series(\n", + " [-3.4, 2.5, -3.4, 2.5],\n", + " index=edisgo.timeseries.timeindex),\n", + " ts_reactive_power=pd.Series(\n", + " [0., 0., 0., 0.],\n", + " index=edisgo.timeseries.timeindex)\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e7bad16f", + "metadata": {}, + "outputs": [], + "source": [ + "# DOES NOT WORK\n", + "#random_bus = edisgo.topology.buses_df.index[3:13]\n", + "#edisgo.perform_mp_opf(\n", + "# timesteps=period,\n", + "# scenario=\"storage\",\n", + "# storage_units=True,\n", + "# storage_buses=busnames,\n", + "# total_storage_capacity=10.0,\n", + "# results_path=results_path)" + ] + }, + { + "cell_type": "markdown", + "id": "8f517e50", + "metadata": {}, + "source": [ + "## Curtailment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5541d52", + "metadata": {}, + "outputs": [], + "source": [ + "# DOES NOT WORK\n", + "#edisgo.perform_mp_opf(\n", + "# timesteps=period,\n", + "# scenario='curtailment',\n", + "# results_path=results_path,\n", + "# curtailment_requirement=True,\n", + "# curtailment_requirement_series=[10, 20, 15, 0])" + ] + }, + { + "cell_type": "markdown", + "id": "7900a177", + "metadata": {}, + "source": [ + "## Plots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36a76ba4", + "metadata": {}, + "outputs": [], + "source": [ + "# plot MV grid topology on a map\n", + "edisgo_obj.plot_mv_grid_topology()\n", + "\n", + "# plot grid expansion costs for lines in the MV grid and stations on a map\n", + "edisgo_obj.plot_mv_grid_expansion_costs()\n", + "\n", + "# plot voltage histogram\n", + "edisgo_obj.histogram_voltage()\n", + "\n", + "# ToDo: add Plotly plot option" + ] + }, + { + "cell_type": "markdown", + "id": "e238f91e", + "metadata": {}, + "source": [ + "## Results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "deb9c4b9", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.results\n", + "#edisgo.results.save('./')" + ] + } + ], + "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 +} diff --git a/tests/test_examples.py b/tests/test_examples.py index 487913f58..03c5dbfcb 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1,6 +1,9 @@ import logging import os +import subprocess +import tempfile +import nbformat import pytest import pytest_notebook @@ -12,6 +15,40 @@ def setup_class(self): os.path.dirname(os.path.dirname(__file__)), "examples" ) + def _notebook_run(self, path): + """ + Execute a notebook via nbconvert and collect output. + Returns (parsed nb object, execution errors) + """ + dirname, __ = os.path.split(path) + os.chdir(dirname) + with tempfile.NamedTemporaryFile(suffix=".ipynb") as fout: + args = [ + "jupyter", + "nbconvert", + path, + "--output", + fout.name, + "--to", + "notebook", + "--execute", + "--ExecutePreprocessor.timeout=90", + ] + subprocess.check_call(args) + + fout.seek(0) + nb = nbformat.read(fout, nbformat.current_nbformat) + + errors = [ + output + for cell in nb.cells + if "outputs" in cell + for output in cell["outputs"] + if output.output_type == "error" + ] + + return nb, errors + @pytest.mark.slow def test_plot_example_ipynb(self): path = os.path.join(self.examples_dir_path, "plot_example.ipynb") @@ -57,3 +94,13 @@ def teardown_class(cls): logger = logging.getLogger("edisgo") logger.handlers.clear() logger.propagate = True + + # @pytest.mark.slow + def test_edisgo_documentation_examples_ipynb(self): + examples_dir_path = os.path.join( + os.path.dirname(os.path.dirname(__file__)), "examples" + ) + nb, errors = self._notebook_run( + os.path.join(examples_dir_path, "edisgo_documentation_examples.ipynb") + ) + assert errors == []