From 3e0b88fc1e6df1dedffc0d63186890ca618e4ffc Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Wed, 5 Feb 2025 10:21:00 +0100 Subject: [PATCH 01/43] Catch missing home battery efficiency within function --- edisgo/flex_opt/battery_storage_operation.py | 19 +++++++++++++++++++ edisgo/io/storage_import.py | 8 +++++++- examples/edisgo_simple_example.ipynb | 2 +- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/edisgo/flex_opt/battery_storage_operation.py b/edisgo/flex_opt/battery_storage_operation.py index 64447a8fd..d63acc89f 100644 --- a/edisgo/flex_opt/battery_storage_operation.py +++ b/edisgo/flex_opt/battery_storage_operation.py @@ -164,6 +164,25 @@ def apply_reference_operation( if storage_units_names is None: storage_units_names = edisgo_obj.topology.storage_units_df.index + if edisgo_obj.topology.storage_units_df.efficiency_store.isna().all(): + logger.warning( + "The efficiency of storage units charge is not specified in the " + "storage_units_df. By default, it is set to 95%. To change this behavior, " + "first set the 'efficiency_store' parameter in topology.storage_units_df." + ) + + edisgo_obj.topology.storage_units_df["efficiency_store"] = 0.95 + + if edisgo_obj.topology.storage_units_df.efficiency_dispatch.isna().all(): + logger.warning( + "The efficiency of storage units discharge is not specified in the " + "storage_units_df. By default, it is set to 95%. To change this behavior, " + "first set the 'efficiency_dispatch' parameter in " + "topology.storage_units_df." + ) + + edisgo_obj.topology.storage_units_df["efficiency_dispatch"] = 0.95 + storage_units = edisgo_obj.topology.storage_units_df.loc[storage_units_names] soe_df = pd.DataFrame(index=edisgo_obj.timeseries.timeindex) diff --git a/edisgo/io/storage_import.py b/edisgo/io/storage_import.py index 2ba1716cf..e5031ed24 100644 --- a/edisgo/io/storage_import.py +++ b/edisgo/io/storage_import.py @@ -73,7 +73,13 @@ def home_batteries_oedb( ) batteries_df = pd.read_sql(sql=query.statement, con=engine, index_col=None) - return _home_batteries_grid_integration(edisgo_obj, batteries_df) + names = _home_batteries_grid_integration(edisgo_obj, batteries_df) + + edisgo_obj.topology.storage_units_df.building_id = ( + edisgo_obj.topology.storage_units_df.building_id.astype(int) + ) + + return names def _home_batteries_grid_integration(edisgo_obj, batteries_df): diff --git a/examples/edisgo_simple_example.ipynb b/examples/edisgo_simple_example.ipynb index c7ee79ce1..31b493ead 100644 --- a/examples/edisgo_simple_example.ipynb +++ b/examples/edisgo_simple_example.ipynb @@ -892,7 +892,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.5" + "version": "3.11.0" }, "toc": { "base_numbering": 1, From ae803f2288d3289a025cc6354e0282a2d29d2cda Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Thu, 6 Feb 2025 12:53:24 +0100 Subject: [PATCH 02/43] only set efficiency for relevant storage units --- edisgo/flex_opt/battery_storage_operation.py | 24 ++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/edisgo/flex_opt/battery_storage_operation.py b/edisgo/flex_opt/battery_storage_operation.py index d63acc89f..086dec1f8 100644 --- a/edisgo/flex_opt/battery_storage_operation.py +++ b/edisgo/flex_opt/battery_storage_operation.py @@ -164,16 +164,30 @@ def apply_reference_operation( if storage_units_names is None: storage_units_names = edisgo_obj.topology.storage_units_df.index - if edisgo_obj.topology.storage_units_df.efficiency_store.isna().all(): + if ( + edisgo_obj.topology.storage_units_df.loc[ + storage_units_names, "efficiency_store" + ] + .isna() + .all() + ): logger.warning( "The efficiency of storage units charge is not specified in the " "storage_units_df. By default, it is set to 95%. To change this behavior, " "first set the 'efficiency_store' parameter in topology.storage_units_df." ) - edisgo_obj.topology.storage_units_df["efficiency_store"] = 0.95 + edisgo_obj.topology.storage_units_df.loc[ + storage_units_names, "efficiency_store" + ] = 0.95 - if edisgo_obj.topology.storage_units_df.efficiency_dispatch.isna().all(): + if ( + edisgo_obj.topology.storage_units_df.loc[ + storage_units_names, "efficiency_dispatch" + ] + .isna() + .all() + ): logger.warning( "The efficiency of storage units discharge is not specified in the " "storage_units_df. By default, it is set to 95%. To change this behavior, " @@ -181,7 +195,9 @@ def apply_reference_operation( "topology.storage_units_df." ) - edisgo_obj.topology.storage_units_df["efficiency_dispatch"] = 0.95 + edisgo_obj.topology.storage_units_df.loc[ + storage_units_names, "efficiency_dispatch" + ] = 0.95 storage_units = edisgo_obj.topology.storage_units_df.loc[storage_units_names] soe_df = pd.DataFrame(index=edisgo_obj.timeseries.timeindex) From 12926707ae427eb1bdb5bf6ad58ff0ef37a62f8c Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Thu, 6 Feb 2025 13:44:06 +0100 Subject: [PATCH 03/43] allow to use egon-data configuration files without setting up an ssh tunnel internally --- edisgo/io/db.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/edisgo/io/db.py b/edisgo/io/db.py index e920dee47..a2fc1ac8c 100644 --- a/edisgo/io/db.py +++ b/edisgo/io/db.py @@ -168,7 +168,7 @@ def engine(path: Path | str = None, ssh: bool = False) -> Engine: """ - if not ssh: + if path is None: database_url = "toep.iks.cs.ovgu.de" return create_engine( "postgresql+oedialect://:@" f"{database_url}", @@ -176,7 +176,8 @@ def engine(path: Path | str = None, ssh: bool = False) -> Engine: ) cred = credentials(path=path) - local_port = ssh_tunnel(cred) + + local_port = ssh_tunnel(cred) if ssh else int(cred["--database-port"]) return create_engine( f"postgresql+psycopg2://{cred['POSTGRES_USER']}:" From 6abd429098806f90450c05f40ce05c19f5bc787a Mon Sep 17 00:00:00 2001 From: Jonas Danke Date: Thu, 6 Feb 2025 14:23:50 +0100 Subject: [PATCH 04/43] Detect TOEP Database and Enable Translated Table Names - Check if the database host contains 'toep' by examining engine.url.host. - If connected to a TOEP database, apply logic to translate table names accordingly. --- edisgo/tools/config.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/edisgo/tools/config.py b/edisgo/tools/config.py index 0341299f7..b6cdf8de8 100644 --- a/edisgo/tools/config.py +++ b/edisgo/tools/config.py @@ -228,14 +228,22 @@ def import_tables_from_oep( list of sqlalchemy.Table A list of SQLAlchemy Table objects corresponding to the imported tables. """ - schema = self.db_schema_mapping.get(schema_name) - saio.register_schema(schema, engine) - tables = [] - for table in table_names: - table = self.db_table_mapping.get(table) - module_name = f"saio.{schema}" - tables.append(importlib.import_module(module_name).__getattr__(table)) - return tables + if "toep" in engine.url.host: + schema = self.db_schema_mapping.get(schema_name) + saio.register_schema(schema, engine) + tables = [] + for table in table_names: + table = self.db_table_mapping.get(table) + module_name = f"saio.{schema}" + tables.append(importlib.import_module(module_name).__getattr__(table)) + return tables + else: + saio.register_schema(schema_name, engine) + tables = [] + for table in table_names: + module_name = f"saio.{schema}" + tables.append(importlib.import_module(module_name).__getattr__(table)) + return tables def from_cfg(self, config_path=None): """ From 45e43f755d22e4278af51118fdaa35c36a371935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Schl=C3=B6sser?= Date: Tue, 11 Feb 2025 10:24:59 +0100 Subject: [PATCH 05/43] setup workshop jupyter notebook --- edisgo/config/config_opf_julia_default.cfg | 2 +- edisgo/edisgo.py | 9 ++- edisgo/tools/plots.py | 29 ++++---- examples/edisgo_simple_example.ipynb | 2 +- examples/electromobility_example.ipynb | 86 +++++++++++----------- setup.py | 2 + 6 files changed, 69 insertions(+), 61 deletions(-) diff --git a/edisgo/config/config_opf_julia_default.cfg b/edisgo/config/config_opf_julia_default.cfg index 5a24d842e..e0c1eacee 100644 --- a/edisgo/config/config_opf_julia_default.cfg +++ b/edisgo/config/config_opf_julia_default.cfg @@ -11,4 +11,4 @@ [julia_dir] -julia_bin = julia-1.1.0/bin +julia_bin = julia/bin diff --git a/edisgo/edisgo.py b/edisgo/edisgo.py index 4fda72aa2..000253831 100755 --- a/edisgo/edisgo.py +++ b/edisgo/edisgo.py @@ -1846,9 +1846,11 @@ def _aggregate_time_series(attribute, groups, naming): [ pd.DataFrame( { - naming.format("_".join(k)) - if isinstance(k, tuple) - else naming.format(k): getattr(self.timeseries, attribute) + ( + naming.format("_".join(k)) + if isinstance(k, tuple) + else naming.format(k) + ): getattr(self.timeseries, attribute) .loc[:, v] .sum(axis=1) } @@ -2408,6 +2410,7 @@ def plot_mv_grid_topology(self, technologies=False, **kwargs): xlim=kwargs.get("xlim", None), ylim=kwargs.get("ylim", None), title=kwargs.get("title", ""), + **kwargs, ) def plot_mv_voltages(self, **kwargs): diff --git a/edisgo/tools/plots.py b/edisgo/tools/plots.py index 3a1207788..f0b56ce01 100644 --- a/edisgo/tools/plots.py +++ b/edisgo/tools/plots.py @@ -391,7 +391,7 @@ def get_color_and_size(connected_components, colors_dict, sizes_dict): else: return colors_dict["else"], sizes_dict["else"] - def nodes_by_technology(buses, edisgo_obj): + def nodes_by_technology(buses, edisgo_obj, sizes_dict=None): bus_sizes = {} bus_colors = {} colors_dict = { @@ -405,17 +405,18 @@ def nodes_by_technology(buses, edisgo_obj): "DisconnectingPoint": "0.75", "else": "orange", } - sizes_dict = { - "BranchTee": 10000, - "GeneratorFluctuating": 100000, - "Generator": 100000, - "Load": 100000, - "LVStation": 50000, - "MVStation": 120000, - "Storage": 100000, - "DisconnectingPoint": 75000, - "else": 200000, - } + if sizes_dict is None: + sizes_dict = { + "BranchTee": 10000, + "GeneratorFluctuating": 100000, + "Generator": 100000, + "Load": 100000, + "LVStation": 50000, + "MVStation": 120000, + "Storage": 100000, + "DisconnectingPoint": 75000, + "else": 200000, + } for bus in buses: connected_components = ( edisgo_obj.topology.get_connected_components_from_bus(bus) @@ -583,7 +584,9 @@ def nodes_by_costs(buses, grid_expansion_costs, edisgo_obj): # bus colors and sizes if node_color == "technology": - bus_sizes, bus_colors = nodes_by_technology(pypsa_plot.buses.index, edisgo_obj) + bus_sizes, bus_colors = nodes_by_technology( + pypsa_plot.buses.index, edisgo_obj, kwargs.get("sizes_dict", None) + ) bus_cmap = None elif node_color == "voltage": bus_sizes, bus_colors = nodes_by_voltage( diff --git a/examples/edisgo_simple_example.ipynb b/examples/edisgo_simple_example.ipynb index c7ee79ce1..2292a1973 100644 --- a/examples/edisgo_simple_example.ipynb +++ b/examples/edisgo_simple_example.ipynb @@ -892,7 +892,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.5" + "version": "3.10.16" }, "toc": { "base_numbering": 1, diff --git a/examples/electromobility_example.ipynb b/examples/electromobility_example.ipynb index 9ab632b71..259437467 100644 --- a/examples/electromobility_example.ipynb +++ b/examples/electromobility_example.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "e9100083", + "id": "0", "metadata": {}, "source": [ "# Electromobility example\n", @@ -17,7 +17,7 @@ }, { "cell_type": "markdown", - "id": "c74c4450", + "id": "1", "metadata": {}, "source": [ "## Installation and setup\n", @@ -27,7 +27,7 @@ }, { "cell_type": "markdown", - "id": "ecefffc4", + "id": "2", "metadata": {}, "source": [ "### Import packages" @@ -36,7 +36,7 @@ { "cell_type": "code", "execution_count": null, - "id": "6898e8bd", + "id": "3", "metadata": { "tags": [] }, @@ -65,7 +65,7 @@ { "cell_type": "code", "execution_count": null, - "id": "6b5c46ca", + "id": "4", "metadata": { "tags": [] }, @@ -77,7 +77,7 @@ }, { "cell_type": "markdown", - "id": "488bfb8c", + "id": "5", "metadata": {}, "source": [ "### Set up logger" @@ -86,7 +86,7 @@ { "cell_type": "code", "execution_count": null, - "id": "e3b60c43", + "id": "6", "metadata": { "tags": [] }, @@ -104,7 +104,7 @@ }, { "cell_type": "markdown", - "id": "fd735589", + "id": "7", "metadata": {}, "source": [ "### Download example grid" @@ -113,7 +113,7 @@ { "cell_type": "code", "execution_count": null, - "id": "afe44b3f", + "id": "8", "metadata": { "tags": [] }, @@ -155,7 +155,7 @@ }, { "cell_type": "markdown", - "id": "abddc320", + "id": "9", "metadata": {}, "source": [ "### Set up edisgo object" @@ -164,7 +164,7 @@ { "cell_type": "code", "execution_count": null, - "id": "b8a406ae", + "id": "10", "metadata": { "tags": [] }, @@ -191,7 +191,7 @@ { "cell_type": "code", "execution_count": null, - "id": "716fa083-0409-46a4-a55c-07cac583e387", + "id": "11", "metadata": { "tags": [] }, @@ -213,7 +213,7 @@ }, { "cell_type": "markdown", - "id": "4269ad12", + "id": "12", "metadata": {}, "source": [ "## Prerequisite data\n", @@ -225,7 +225,7 @@ }, { "cell_type": "markdown", - "id": "0ba78c69", + "id": "13", "metadata": {}, "source": [ "### Download 'Verwaltungsgebiete' data\n", @@ -235,7 +235,7 @@ }, { "cell_type": "markdown", - "id": "ccb74f72", + "id": "14", "metadata": {}, "source": [ "```python\n", @@ -265,7 +265,7 @@ { "cell_type": "code", "execution_count": null, - "id": "3fdf5534", + "id": "15", "metadata": {}, "outputs": [], "source": [ @@ -304,7 +304,7 @@ }, { "cell_type": "markdown", - "id": "b2e81602", + "id": "16", "metadata": {}, "source": [ "### Check which 'Verwaltungsgebiete' intersect MV grid" @@ -313,7 +313,7 @@ { "cell_type": "code", "execution_count": null, - "id": "d6bdc1f4", + "id": "17", "metadata": {}, "outputs": [], "source": [ @@ -330,7 +330,7 @@ { "cell_type": "code", "execution_count": null, - "id": "38e067dd", + "id": "18", "metadata": {}, "outputs": [], "source": [ @@ -345,7 +345,7 @@ }, { "cell_type": "markdown", - "id": "e2082ea8-3be5-4e69-8b3b-26023bedc71b", + "id": "19", "metadata": {}, "source": [ "As most municipalities only intersect the grid district at its border, only the electromobility data for one municipality needs to be generated." @@ -354,7 +354,7 @@ { "cell_type": "code", "execution_count": null, - "id": "0d4e721d-6be2-4e41-b6d0-349f9bbc2f5b", + "id": "20", "metadata": {}, "outputs": [], "source": [ @@ -369,7 +369,7 @@ }, { "cell_type": "markdown", - "id": "bfc8a701", + "id": "21", "metadata": {}, "source": [ "## Add electromobility to EDisGo object\n", @@ -410,7 +410,7 @@ { "cell_type": "code", "execution_count": null, - "id": "c8f2e17e", + "id": "22", "metadata": {}, "outputs": [], "source": [ @@ -453,7 +453,7 @@ { "cell_type": "code", "execution_count": null, - "id": "8421b212", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -490,7 +490,7 @@ { "cell_type": "code", "execution_count": null, - "id": "1d65e6d6", + "id": "24", "metadata": {}, "outputs": [], "source": [ @@ -503,7 +503,7 @@ }, { "cell_type": "markdown", - "id": "ae9955f1", + "id": "25", "metadata": {}, "source": [ "### eDisGo electromobility data structure \n", @@ -526,7 +526,7 @@ { "cell_type": "code", "execution_count": null, - "id": "0e859c1e-6aba-4457-92f5-59b1a4b4ddae", + "id": "26", "metadata": {}, "outputs": [], "source": [ @@ -537,7 +537,7 @@ { "cell_type": "code", "execution_count": null, - "id": "964916d6-82fc-47fb-8ff4-d28173113128", + "id": "27", "metadata": {}, "outputs": [], "source": [ @@ -548,7 +548,7 @@ { "cell_type": "code", "execution_count": null, - "id": "db648528-06dd-40cf-9fc0-4137280f21cb", + "id": "28", "metadata": {}, "outputs": [], "source": [ @@ -559,7 +559,7 @@ { "cell_type": "code", "execution_count": null, - "id": "f6663f9f-2481-403d-b1d8-c0cf364d3eba", + "id": "29", "metadata": {}, "outputs": [], "source": [ @@ -570,7 +570,7 @@ { "cell_type": "code", "execution_count": null, - "id": "c71977c0-e4e0-443e-afa1-ed632c30c54b", + "id": "30", "metadata": {}, "outputs": [], "source": [ @@ -580,7 +580,7 @@ { "cell_type": "code", "execution_count": null, - "id": "1b156984-4431-4312-a617-a23441e0d153", + "id": "31", "metadata": {}, "outputs": [], "source": [ @@ -625,7 +625,7 @@ }, { "cell_type": "markdown", - "id": "b82b9f8f", + "id": "32", "metadata": {}, "source": [ "## Applying different charging strategies\n", @@ -635,7 +635,7 @@ }, { "cell_type": "markdown", - "id": "0cc6707b", + "id": "33", "metadata": {}, "source": [ "The eDisGo tool currently offers three different charging strategies: `dumb`, `reduced` and `residual`.\n", @@ -656,7 +656,7 @@ { "cell_type": "code", "execution_count": null, - "id": "18455dcc-0db7-4ade-9003-6c183552a12b", + "id": "34", "metadata": {}, "outputs": [], "source": [ @@ -668,7 +668,7 @@ { "cell_type": "code", "execution_count": null, - "id": "685108f9-f15b-459e-8f22-2d99c678fb1c", + "id": "35", "metadata": {}, "outputs": [], "source": [ @@ -679,7 +679,7 @@ { "cell_type": "code", "execution_count": null, - "id": "b56ebbd4", + "id": "36", "metadata": {}, "outputs": [], "source": [ @@ -695,7 +695,7 @@ }, { "cell_type": "markdown", - "id": "b9cd3434", + "id": "37", "metadata": {}, "source": [ "To change the charging strategy from the default `dumb` to one of the other strategies, the `strategy` parameter has to be set accordingly:" @@ -704,7 +704,7 @@ { "cell_type": "code", "execution_count": null, - "id": "a15eece2-951e-4749-9ab4-eaf3c22b0077", + "id": "38", "metadata": {}, "outputs": [], "source": [ @@ -715,7 +715,7 @@ { "cell_type": "code", "execution_count": null, - "id": "2b61d2e2", + "id": "39", "metadata": {}, "outputs": [], "source": [ @@ -725,7 +725,7 @@ }, { "cell_type": "markdown", - "id": "3bd366aa-ea6e-4d1f-a66b-fee6bcaf3f4f", + "id": "40", "metadata": {}, "source": [ "**Plot charging time series for different charging strategies**" @@ -734,7 +734,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20d98ca8", + "id": "41", "metadata": { "tags": [] }, @@ -774,7 +774,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.18" + "version": "3.10.16" }, "toc": { "base_numbering": 1, diff --git a/setup.py b/setup.py index bbbe8dadb..4887bc135 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ """Setup""" + import os import sys @@ -40,6 +41,7 @@ def read(fname): "geopandas >= 0.12.0, < 1.1.0", "geopy >= 2.0.0, < 2.5.0", "jupyterlab < 4.4.0", + "jupyter-black", "jupyter_dash < 0.5.0", "matplotlib >= 3.3.0, < 3.11.0", "multiprocess < 0.71.0", From 427944c0183e053bc3f72bfb4c97a22741cdf8ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Schl=C3=B6sser?= Date: Tue, 11 Feb 2025 10:29:46 +0100 Subject: [PATCH 06/43] Jupyter notebook LoMa Workshop --- examples/Workshop_LoMa.ipynb | 581 +++++++++++++++++++++++++++++++++++ 1 file changed, 581 insertions(+) create mode 100644 examples/Workshop_LoMa.ipynb diff --git a/examples/Workshop_LoMa.ipynb b/examples/Workshop_LoMa.ipynb new file mode 100644 index 000000000..bde03132c --- /dev/null +++ b/examples/Workshop_LoMa.ipynb @@ -0,0 +1,581 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# LoMa EDisGo-Workshop 13.2.2025" + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "Contents:" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "Import Packages" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext jupyter_black" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import requests\n", + "import sys\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import networkx as nx\n", + "import pandas as pd\n", + "\n", + "from edisgo import EDisGo\n", + "from edisgo.tools.logger import setup_logger\n", + "from numpy.random import default_rng" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "# Nur wegen der Übersicht. Normalerweise nicht zu empfehlen\n", + "import warnings\n", + "\n", + "warnings.filterwarnings(\"ignore\")" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## Load grid Data from ding0" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "Currently, synthetic grid data generated with the python project ding0 is the only supported data source for distribution grid data. ding0 provides the grid topology data in the form of csv files, with separate files for buses, lines, loads, generators, etc. You can retrieve ding0 data from Zenodo (make sure you choose latest data) or check out the Ding0 documentation on how to generate grids yourself. A ding0 example grid can be viewed here. It is possible to provide your own grid data if it is in the same format as the ding0 grid data.\n", + "\n", + "This example works with any ding0 grid data. If you don't have grid data yet, you can execute the following to download the example grid data mentioned above." + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "The ding0 grid you want to use in your analysis is specified through the input parameter 'ding0_grid' of the EDisGo class. The following assumes you want to use the ding0 example grid downloaded above. To use a different ding0 grid, just change the path below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "ding0_grid = os.path.join(os.path.expanduser(\"~\"), \".edisgo\", \"husum_grids\", \"35725\")\n", + "edisgo = EDisGo(ding0_grid=ding0_grid, legacy_ding0_grids=False)" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## Plot grid topology (MV)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "sizes_dict = {\n", + " \"BranchTee\": 10000,\n", + " \"GeneratorFluctuating\": 100000,\n", + " \"Generator\": 100000,\n", + " \"Load\": 100000,\n", + " \"LVStation\": 50000,\n", + " \"MVStation\": 120000,\n", + " \"Storage\": 100000,\n", + " \"DisconnectingPoint\": 75000,\n", + " \"else\": 200000,\n", + "}\n", + "\n", + "sizes_dict = {k: v / 10 for k, v in sizes_dict.items()}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.plot_mv_grid_topology(technologies=True, sizes_dict=sizes_dict)" + ] + }, + { + "cell_type": "markdown", + "id": "13", + "metadata": {}, + "source": [ + "red: nodes with substation secondary side\n", + "light blue: nodes distribution substations's primary side\n", + "green: nodes with fluctuating generators\n", + "black: nodes with conventional generators\n", + "grey: disconnecting points\n", + "dark blue: branch trees" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "Geladenes Netz muss auf den aktuellen Stand gestzt werden --> reinforce" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "worst case analysis ... " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.set_time_series_worst_case_analysis()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.reinforce()" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "## Adapt network (Husum)" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "### Basic components addition and removal" + ] + }, + { + "cell_type": "markdown", + "id": "20", + "metadata": {}, + "source": [ + "To see how a loaded network can be adapted later on, we add a solar plant to a random bus. Then we remove it again. \n", + "\n", + "Components can also be added according to their geolocation with the function integrate_component_based_on_geolocation()." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.topology.generators_df" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "Add a generator with the function add_component or add_generator." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "rng = default_rng(1)\n", + "rnd_bus = rng.choice(edisgo.topology.buses_df.index, size=1)[0]\n", + "generator_type = \"solar\"\n", + "\n", + "new_generator = edisgo.add_component(\n", + " comp_type=\"generator\", p_nom=0.01, bus=rnd_bus, generator_type=generator_type\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "24", + "metadata": {}, + "source": [ + "Mit Generator zeigen, mit Last nachmachen" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.topology.generators_df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.remove_component(comp_type=\"generator\", comp_name=new_generator)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.topology.generators_df" + ] + }, + { + "cell_type": "markdown", + "id": "28", + "metadata": {}, + "source": [ + "### Add flexible components to grid (Heat pumps)" + ] + }, + { + "cell_type": "markdown", + "id": "29", + "metadata": {}, + "source": [ + "Add heat pumps with the function import_heat_pumps()" + ] + }, + { + "cell_type": "markdown", + "id": "30", + "metadata": {}, + "source": [ + "Engine der eigentlichen Datenbank muss verwendet werden" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": {}, + "outputs": [], + "source": [ + "scenario = \"eGon2035\"\n", + "from edisgo.io.db import engine as toep_engine" + ] + }, + { + "cell_type": "markdown", + "id": "32", + "metadata": {}, + "source": [ + "ToDo @ Kilian: Lokale Engine" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.import_heat_pumps(scenario = scenario, engine = toep_engine())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.topology.loads_df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "35", + "metadata": {}, + "source": [ + "Batteries: import_home_batteries()\n", + "\n", + "Demand Side Management: import_dsm()\n", + "\n", + "Charging parks and stations for EV: import_electromobility() --> See electromobility example" + ] + }, + { + "cell_type": "markdown", + "id": "36", + "metadata": {}, + "source": [ + "## Create timeseries for all intergrated components" + ] + }, + { + "cell_type": "markdown", + "id": "37", + "metadata": {}, + "source": [ + "Create timeseries for the four worst cases MV load case, LV load case, MV feed-in case, LV feed-in case with the function set_time_series_worst_case_analysis():" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.set_time_series_worst_case_analysis()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.timeseries.timeindex_worst_cases" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.timeseries.loads_active_power.loc[edisgo.timeseries.timeindex_worst_cases[\"load_case_mv\"]]" + ] + }, + { + "cell_type": "markdown", + "id": "41", + "metadata": {}, + "source": [ + "## Main features" + ] + }, + { + "cell_type": "markdown", + "id": "42", + "metadata": {}, + "source": [ + "Execute a power flow analysis to determine line overloads and voltage deviations for the MV load case timeseries with the function analyze():" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.analyze(timesteps = edisgo.timeseries.timeindex_worst_cases[\"load_case_mv\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.plot_mv_line_loading(\n", + " node_color=\"voltage_deviation\", timestep = edisgo.timeseries.timeindex_worst_cases[\"load_case_mv\"]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.histogram_voltage(binwidth=0.005)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.histogram_relative_line_load(binwidth=0.2, voltage_level=\"mv\")" + ] + }, + { + "cell_type": "markdown", + "id": "47", + "metadata": {}, + "source": [ + "Reinfoce the grid with the function reinforce(): (Use mode 'mv' for shorter runtime)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.reinforce(mode = 'mv', timestep = edisgo.timeseries.timeindex_worst_cases[\"load_case_mv\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.plot_mv_line_loading(\n", + " node_color=\"voltage_deviation\", timestep=edisgo.timeseries.timeindex_worst_cases[\"load_case_mv\"]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "50", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.histogram_voltage(binwidth=0.005)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.histogram_relative_line_load(binwidth=0.2, voltage_level=\"mv\")" + ] + }, + { + "cell_type": "markdown", + "id": "52", + "metadata": {}, + "source": [ + "## Results" + ] + }, + { + "cell_type": "markdown", + "id": "53", + "metadata": {}, + "source": [ + "Display the resulting equipment changes and their corresponding costs with the results class:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.results.equipment_changes.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.results.grid_expansion_costs.head()" + ] + } + ], + "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.10.16" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 6b58f58e38a39bbe8fbe538e2456f4054a8d4b90 Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Tue, 11 Feb 2025 13:35:51 +0100 Subject: [PATCH 07/43] bugfix wrong schema name in heat pump import --- edisgo/io/heat_pump_import.py | 2 +- edisgo/tools/config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/edisgo/io/heat_pump_import.py b/edisgo/io/heat_pump_import.py index 4bf862485..da6d26cc7 100644 --- a/edisgo/io/heat_pump_import.py +++ b/edisgo/io/heat_pump_import.py @@ -327,7 +327,7 @@ def _get_individual_heat_pump_capacity(): "boundaries", ) egon_etrago_bus, egon_etrago_link = config.import_tables_from_oep( - engine, ["egon_etrago_bus", "egon_etrago_link"], "supply" + engine, ["egon_etrago_bus", "egon_etrago_link"], "grid" ) building_ids = edisgo_object.topology.loads_df.building_id.unique() diff --git a/edisgo/tools/config.py b/edisgo/tools/config.py index b6cdf8de8..7b42fdf5c 100644 --- a/edisgo/tools/config.py +++ b/edisgo/tools/config.py @@ -241,7 +241,7 @@ def import_tables_from_oep( saio.register_schema(schema_name, engine) tables = [] for table in table_names: - module_name = f"saio.{schema}" + module_name = f"saio.{schema_name}" tables.append(importlib.import_module(module_name).__getattr__(table)) return tables From e2d80ab5da4bd62fc4115b53960e572d73f1eaf8 Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Tue, 11 Feb 2025 14:52:41 +0100 Subject: [PATCH 08/43] adapt notebook to use hetzner db --- edisgo/flex_opt/reinforce_grid.py | 3 +- examples/Workshop_LoMa.ipynb | 248 +++++++++++++++++++++++------- 2 files changed, 195 insertions(+), 56 deletions(-) diff --git a/edisgo/flex_opt/reinforce_grid.py b/edisgo/flex_opt/reinforce_grid.py index e56eb58b4..6da2449f5 100644 --- a/edisgo/flex_opt/reinforce_grid.py +++ b/edisgo/flex_opt/reinforce_grid.py @@ -519,7 +519,8 @@ def reinforce_grid( ) raise exceptions.MaximumIterationError( "Over-voltage issues for the following nodes in LV grids " - f"could not be solved: {crit_nodes}" + f"could not be solved within {max_while_iterations} iterations: " + f"{crit_nodes}" ) else: logger.info( diff --git a/examples/Workshop_LoMa.ipynb b/examples/Workshop_LoMa.ipynb index bde03132c..2b11fbd62 100644 --- a/examples/Workshop_LoMa.ipynb +++ b/examples/Workshop_LoMa.ipynb @@ -49,9 +49,13 @@ "import networkx as nx\n", "import pandas as pd\n", "\n", + "from copy import deepcopy\n", + "from numpy.random import default_rng\n", + "from pathlib import Path\n", + "\n", "from edisgo import EDisGo\n", - "from edisgo.tools.logger import setup_logger\n", - "from numpy.random import default_rng" + "from edisgo.io.db import engine\n", + "from edisgo.tools.logger import setup_logger" ] }, { @@ -100,7 +104,9 @@ "metadata": {}, "outputs": [], "source": [ - "ding0_grid = os.path.join(os.path.expanduser(\"~\"), \".edisgo\", \"husum_grids\", \"35725\")\n", + "ding0_grid = os.path.join(\n", + " os.path.expanduser(\"~\"), \".ding0\", \"run_2025-02-04-09-34-25\", \"35725\"\n", + ")\n", "edisgo = EDisGo(ding0_grid=ding0_grid, legacy_ding0_grids=False)" ] }, @@ -194,16 +200,52 @@ ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "id": "18", "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.topology.generators_df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "20", + "metadata": {}, + "source": [ + "### TODO: Hier etwas die eDisGo Struktur zeigen und ein paar Statistiken wie folgende" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.topology.generators_df[[\"p_nom\", \"type\"]].groupby(\"type\").sum()" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, "source": [ "## Adapt network (Husum)" ] }, { "cell_type": "markdown", - "id": "19", + "id": "23", "metadata": {}, "source": [ "### Basic components addition and removal" @@ -211,7 +253,7 @@ }, { "cell_type": "markdown", - "id": "20", + "id": "24", "metadata": {}, "source": [ "To see how a loaded network can be adapted later on, we add a solar plant to a random bus. Then we remove it again. \n", @@ -222,7 +264,7 @@ { "cell_type": "code", "execution_count": null, - "id": "21", + "id": "25", "metadata": {}, "outputs": [], "source": [ @@ -231,7 +273,7 @@ }, { "cell_type": "markdown", - "id": "22", + "id": "26", "metadata": {}, "source": [ "Add a generator with the function add_component or add_generator." @@ -240,7 +282,7 @@ { "cell_type": "code", "execution_count": null, - "id": "23", + "id": "27", "metadata": {}, "outputs": [], "source": [ @@ -255,7 +297,7 @@ }, { "cell_type": "markdown", - "id": "24", + "id": "28", "metadata": {}, "source": [ "Mit Generator zeigen, mit Last nachmachen" @@ -264,7 +306,7 @@ { "cell_type": "code", "execution_count": null, - "id": "25", + "id": "29", "metadata": {}, "outputs": [], "source": [ @@ -274,7 +316,7 @@ { "cell_type": "code", "execution_count": null, - "id": "26", + "id": "30", "metadata": {}, "outputs": [], "source": [ @@ -284,7 +326,7 @@ { "cell_type": "code", "execution_count": null, - "id": "27", + "id": "31", "metadata": {}, "outputs": [], "source": [ @@ -293,7 +335,7 @@ }, { "cell_type": "markdown", - "id": "28", + "id": "32", "metadata": {}, "source": [ "### Add flexible components to grid (Heat pumps)" @@ -301,7 +343,7 @@ }, { "cell_type": "markdown", - "id": "29", + "id": "33", "metadata": {}, "source": [ "Add heat pumps with the function import_heat_pumps()" @@ -309,7 +351,7 @@ }, { "cell_type": "markdown", - "id": "30", + "id": "34", "metadata": {}, "source": [ "Engine der eigentlichen Datenbank muss verwendet werden" @@ -318,45 +360,102 @@ { "cell_type": "code", "execution_count": null, - "id": "31", + "id": "35", "metadata": {}, "outputs": [], "source": [ - "scenario = \"eGon2035\"\n", - "from edisgo.io.db import engine as toep_engine" + "scenario = \"eGon2035\"" ] }, { - "cell_type": "markdown", - "id": "32", + "cell_type": "code", + "execution_count": null, + "id": "36", "metadata": {}, + "outputs": [], "source": [ - "ToDo @ Kilian: Lokale Engine" + "conf_path = (\n", + " Path.home()\n", + " / \"Documents\"\n", + " / \"data\"\n", + " / \"egon-data-hetzner\"\n", + " / \"egon-data.configuration.yaml\"\n", + ")\n", + "\n", + "db_engine = engine(path=conf_path, ssh=True)" ] }, { "cell_type": "code", "execution_count": null, - "id": "33", + "id": "37", "metadata": {}, "outputs": [], "source": [ - "edisgo.import_heat_pumps(scenario = scenario, engine = toep_engine())" + "edisgo_orig = deepcopy(edisgo)" ] }, { "cell_type": "code", "execution_count": null, - "id": "34", + "id": "38", + "metadata": {}, + "outputs": [], + "source": [ + "# Retry if running into \"Connection reset by peer\" error\n", + "edisgo = deepcopy(edisgo_orig)\n", + "\n", + "edisgo.import_generators(generator_scenario=scenario)\n", + "edisgo.import_home_batteries(scenario=scenario, engine=db_engine)\n", + "edisgo.import_heat_pumps(scenario=scenario, engine=db_engine)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39", + "metadata": {}, + "outputs": [], + "source": [ + "# This takes too long for the workshop, but needs to be mentioned\n", + "# edisgo_obj.import_dsm(scenario=scenario, engine=db_engine)\n", + "# edisgo_obj.import_electromobility(\n", + "# data_source=\"oedb\", scenario=scenario, engine=db_engine\n", + "# )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40", "metadata": {}, "outputs": [], "source": [ - "edisgo.topology.loads_df.head()" + "edisgo.topology.generators_df[[\"p_nom\", \"type\"]].groupby(\"type\").sum()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41", + "metadata": {}, + "outputs": [], + "source": [ + "# TODO Moritz: Show statistics before and after import\n", + "edisgo.topology.generators_df.head()" ] }, { "cell_type": "markdown", - "id": "35", + "id": "42", + "metadata": {}, + "source": [ + "edisgo.import_heat_pumps(scenario = scenario, engine = toep_engine())" + ] + }, + { + "cell_type": "markdown", + "id": "43", "metadata": {}, "source": [ "Batteries: import_home_batteries()\n", @@ -368,7 +467,7 @@ }, { "cell_type": "markdown", - "id": "36", + "id": "44", "metadata": {}, "source": [ "## Create timeseries for all intergrated components" @@ -376,7 +475,7 @@ }, { "cell_type": "markdown", - "id": "37", + "id": "45", "metadata": {}, "source": [ "Create timeseries for the four worst cases MV load case, LV load case, MV feed-in case, LV feed-in case with the function set_time_series_worst_case_analysis():" @@ -385,7 +484,7 @@ { "cell_type": "code", "execution_count": null, - "id": "38", + "id": "46", "metadata": {}, "outputs": [], "source": [ @@ -395,26 +494,29 @@ { "cell_type": "code", "execution_count": null, - "id": "39", + "id": "47", "metadata": {}, "outputs": [], "source": [ + "# TODO: Erklärung zu der Unterscheidung der vier timesteps\n", "edisgo.timeseries.timeindex_worst_cases" ] }, { "cell_type": "code", "execution_count": null, - "id": "40", + "id": "48", "metadata": {}, "outputs": [], "source": [ - "edisgo.timeseries.loads_active_power.loc[edisgo.timeseries.timeindex_worst_cases[\"load_case_mv\"]]" + "edisgo.timeseries.loads_active_power.loc[\n", + " edisgo.timeseries.timeindex_worst_cases[\"load_case_mv\"]\n", + "]" ] }, { "cell_type": "markdown", - "id": "41", + "id": "49", "metadata": {}, "source": [ "## Main features" @@ -422,7 +524,7 @@ }, { "cell_type": "markdown", - "id": "42", + "id": "50", "metadata": {}, "source": [ "Execute a power flow analysis to determine line overloads and voltage deviations for the MV load case timeseries with the function analyze():" @@ -431,29 +533,30 @@ { "cell_type": "code", "execution_count": null, - "id": "43", + "id": "51", "metadata": {}, "outputs": [], "source": [ - "edisgo.analyze(timesteps = edisgo.timeseries.timeindex_worst_cases[\"load_case_mv\"])" + "edisgo.analyze(timesteps=edisgo.timeseries.timeindex_worst_cases[\"load_case_mv\"])" ] }, { "cell_type": "code", "execution_count": null, - "id": "44", + "id": "52", "metadata": {}, "outputs": [], "source": [ "edisgo.plot_mv_line_loading(\n", - " node_color=\"voltage_deviation\", timestep = edisgo.timeseries.timeindex_worst_cases[\"load_case_mv\"]\n", + " node_color=\"voltage_deviation\",\n", + " timestep=edisgo.timeseries.timeindex_worst_cases[\"load_case_mv\"],\n", ")" ] }, { "cell_type": "code", "execution_count": null, - "id": "45", + "id": "53", "metadata": {}, "outputs": [], "source": [ @@ -463,16 +566,16 @@ { "cell_type": "code", "execution_count": null, - "id": "46", + "id": "54", "metadata": {}, "outputs": [], "source": [ - "edisgo.histogram_relative_line_load(binwidth=0.2, voltage_level=\"mv\")" + "edisgo.histogram_relative_line_load(binwidth=0.1, voltage_level=\"mv\")" ] }, { "cell_type": "markdown", - "id": "47", + "id": "55", "metadata": {}, "source": [ "Reinfoce the grid with the function reinforce(): (Use mode 'mv' for shorter runtime)" @@ -481,29 +584,30 @@ { "cell_type": "code", "execution_count": null, - "id": "48", + "id": "56", "metadata": {}, "outputs": [], "source": [ - "edisgo.reinforce(mode = 'mv', timestep = edisgo.timeseries.timeindex_worst_cases[\"load_case_mv\"])" + "edisgo.reinforce(mode=\"mv\")" ] }, { "cell_type": "code", "execution_count": null, - "id": "49", + "id": "57", "metadata": {}, "outputs": [], "source": [ "edisgo.plot_mv_line_loading(\n", - " node_color=\"voltage_deviation\", timestep=edisgo.timeseries.timeindex_worst_cases[\"load_case_mv\"]\n", + " node_color=\"voltage_deviation\",\n", + " timestep=edisgo.timeseries.timeindex_worst_cases[\"load_case_mv\"],\n", ")" ] }, { "cell_type": "code", "execution_count": null, - "id": "50", + "id": "58", "metadata": {}, "outputs": [], "source": [ @@ -513,16 +617,16 @@ { "cell_type": "code", "execution_count": null, - "id": "51", + "id": "59", "metadata": {}, "outputs": [], "source": [ - "edisgo.histogram_relative_line_load(binwidth=0.2, voltage_level=\"mv\")" + "edisgo.histogram_relative_line_load(binwidth=0.1, voltage_level=\"mv\")" ] }, { "cell_type": "markdown", - "id": "52", + "id": "60", "metadata": {}, "source": [ "## Results" @@ -530,7 +634,7 @@ }, { "cell_type": "markdown", - "id": "53", + "id": "61", "metadata": {}, "source": [ "Display the resulting equipment changes and their corresponding costs with the results class:" @@ -539,22 +643,56 @@ { "cell_type": "code", "execution_count": null, - "id": "54", + "id": "62", "metadata": {}, "outputs": [], "source": [ "edisgo.results.equipment_changes.head()" ] }, + { + "cell_type": "markdown", + "id": "63", + "metadata": {}, + "source": [ + "### TODO: Die initialen Netzausbaumaßnahmen am Anfang müssen hier abgezogen werden" + ] + }, { "cell_type": "code", "execution_count": null, - "id": "55", + "id": "64", "metadata": {}, "outputs": [], "source": [ "edisgo.results.grid_expansion_costs.head()" ] + }, + { + "cell_type": "markdown", + "id": "65", + "metadata": {}, + "source": [ + "### TODO: Visualisierung der Ergebnisse" + ] + }, + { + "cell_type": "markdown", + "id": "66", + "metadata": {}, + "source": [ + "### TODO: Alternative Wege für Zeitreihen, wie von der DB\n", + "\n", + "* show time resolution" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -573,7 +711,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.16" + "version": "3.11.0" } }, "nbformat": 4, From 4998b8d729817609df03af4f7ce9e11ed0387d3f Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Wed, 12 Feb 2025 11:34:28 +0100 Subject: [PATCH 09/43] fix wrong variable name --- edisgo/tools/config.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/edisgo/tools/config.py b/edisgo/tools/config.py index b6cdf8de8..84346a9be 100644 --- a/edisgo/tools/config.py +++ b/edisgo/tools/config.py @@ -228,22 +228,22 @@ def import_tables_from_oep( list of sqlalchemy.Table A list of SQLAlchemy Table objects corresponding to the imported tables. """ + tables = [] + if "toep" in engine.url.host: schema = self.db_schema_mapping.get(schema_name) saio.register_schema(schema, engine) - tables = [] for table in table_names: table = self.db_table_mapping.get(table) module_name = f"saio.{schema}" tables.append(importlib.import_module(module_name).__getattr__(table)) - return tables else: saio.register_schema(schema_name, engine) - tables = [] for table in table_names: - module_name = f"saio.{schema}" + module_name = f"saio.{schema_name}" tables.append(importlib.import_module(module_name).__getattr__(table)) - return tables + + return tables def from_cfg(self, config_path=None): """ @@ -303,21 +303,26 @@ def from_cfg(self, config_path=None): config_dict["demandlib"]["day_start"] = datetime.datetime.strptime( config_dict["demandlib"]["day_start"], "%H:%M" ) + config_dict["demandlib"]["day_start"] = datetime.time( config_dict["demandlib"]["day_start"].hour, config_dict["demandlib"]["day_start"].minute, ) + config_dict["demandlib"]["day_end"] = datetime.datetime.strptime( config_dict["demandlib"]["day_end"], "%H:%M" ) + config_dict["demandlib"]["day_end"] = datetime.time( config_dict["demandlib"]["day_end"].hour, config_dict["demandlib"]["day_end"].minute, ) + ( config_dict["db_tables_dict"], config_dict["db_schema_dict"], ) = self.get_database_alias_dictionaries() + return config_dict def to_json(self, directory, filename=None): From 0029d0111fbbb904256e25564ce8514529702567 Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Wed, 12 Feb 2025 14:48:47 +0100 Subject: [PATCH 10/43] make it possible to setup edisgo instance without connecting to oedb --- edisgo/edisgo.py | 84 +++++++++++++++-------------------- edisgo/io/heat_pump_import.py | 2 +- edisgo/tools/config.py | 10 ++++- 3 files changed, 46 insertions(+), 50 deletions(-) diff --git a/edisgo/edisgo.py b/edisgo/edisgo.py index 4fda72aa2..469f9972d 100755 --- a/edisgo/edisgo.py +++ b/edisgo/edisgo.py @@ -71,6 +71,11 @@ class EDisGo: ---------- ding0_grid : :obj:`str` Path to directory containing csv files of network to be loaded. + engine : :sqlalchemy:`sqlalchemy.Engine` or None + Database engine for connecting to the `OpenEnergy DataBase OEDB + `_ or other eGon-data + databases. Defaults to the OEDB engine. Can be set to None if no scenario is to + be loaded. generator_scenario : None or :obj:`str`, optional If None, the generator park of the imported grid is kept as is. Otherwise defines which scenario of future generator park to use @@ -158,8 +163,10 @@ class EDisGo: """ def __init__(self, **kwargs): + # Set database engine for future scenarios + self.engine: Engine | None = kwargs.pop("engine", toep_engine()) # load configuration - self._config = Config(**kwargs) + self._config = Config(engine=self.engine, **kwargs) # instantiate topology object and load grid data self.topology = Topology(config=self.config) @@ -418,12 +425,9 @@ def set_time_series_active_power_predefined( Technology- and weather cell-specific hourly feed-in time series are obtained from the `OpenEnergy DataBase - `_. See - :func:`edisgo.io.timeseries_import.feedin_oedb` for more information. - - This option requires that the parameter `engine` is provided in case - new ding0 grids with geo-referenced LV grids are used. For further - settings, the parameter `timeindex` can also be provided. + `_ or other eGon-data + databases. See :func:`edisgo.io.timeseries_import.feedin_oedb` for more + information. * :pandas:`pandas.DataFrame` @@ -536,9 +540,6 @@ def set_time_series_active_power_predefined( Other Parameters ------------------ - engine : :sqlalchemy:`sqlalchemy.Engine` - Database engine. This parameter is only required in case - `conventional_loads_ts` or `fluctuating_generators_ts` is 'oedb'. scenario : str Scenario for which to retrieve demand data. Possible options are 'eGon2035' and 'eGon100RE'. This parameter is only required in case @@ -569,7 +570,7 @@ def set_time_series_active_power_predefined( self, fluctuating_generators_ts, fluctuating_generators_names, - engine=kwargs.get("engine", toep_engine()), + engine=self.engine, timeindex=kwargs.get("timeindex", None), ) if dispatchable_generators_ts is not None: @@ -584,7 +585,7 @@ def set_time_series_active_power_predefined( loads_ts_df = timeseries_import.electricity_demand_oedb( edisgo_obj=self, scenario=kwargs.get("scenario"), - engine=kwargs.get("engine", toep_engine()), + engine=self.engine, timeindex=kwargs.get("timeindex", None), load_names=conventional_loads_names, ) @@ -971,9 +972,7 @@ def import_generators(self, generator_scenario=None, **kwargs): Other Parameters ---------------- kwargs : - In case you are using new ding0 grids, where the LV is geo-referenced, a - database engine needs to be provided through keyword argument `engine`. - In case you are using old ding0 grids, where the LV is not geo-referenced, + If you are using old ding0 grids, where the LV is not geo-referenced, you can check :func:`edisgo.io.generators_import.oedb_legacy` for possible keyword arguments. @@ -985,7 +984,7 @@ def import_generators(self, generator_scenario=None, **kwargs): else: generators_import.oedb( edisgo_object=self, - engine=kwargs.get("engine", toep_engine()), + engine=self.engine, scenario=generator_scenario, ) @@ -1846,9 +1845,11 @@ def _aggregate_time_series(attribute, groups, naming): [ pd.DataFrame( { - naming.format("_".join(k)) - if isinstance(k, tuple) - else naming.format(k): getattr(self.timeseries, attribute) + ( + naming.format("_".join(k)) + if isinstance(k, tuple) + else naming.format(k) + ): getattr(self.timeseries, attribute) .loc[:, v] .sum(axis=1) } @@ -1917,9 +1918,8 @@ def _aggregate_time_series(attribute, groups, naming): def import_electromobility( self, - data_source: str, + data_source: str = "oedb", scenario: str = None, - engine: Engine = None, charging_processes_dir: PurePath | str = None, potential_charging_points_dir: PurePath | str = None, import_electromobility_data_kwds=None, @@ -1961,10 +1961,8 @@ def import_electromobility( * "oedb" Electromobility data is obtained from the `OpenEnergy DataBase - `_. - - This option requires that the parameters `scenario` and `engine` are - provided. + `_ or other eGon-data + databases depending on the provided Engine. * "directory" @@ -1974,9 +1972,6 @@ def import_electromobility( scenario : str Scenario for which to retrieve electromobility data in case `data_source` is set to "oedb". Possible options are "eGon2035" and "eGon100RE". - engine : :sqlalchemy:`sqlalchemy.Engine` - Database engine. Needs to be provided in case `data_source` is set to - "oedb". charging_processes_dir : str or pathlib.PurePath Directory holding data on charging processes (standing times, charging demand, etc. per vehicle), including metadata, from SimBEV. @@ -2038,7 +2033,7 @@ def import_electromobility( import_electromobility_from_oedb( self, scenario=scenario, - engine=engine, + engine=self.engine, **import_electromobility_data_kwds, ) elif data_source == "directory": @@ -2131,10 +2126,11 @@ def apply_charging_strategy(self, strategy="dumb", **kwargs): """ charging_strategy(self, strategy=strategy, **kwargs) - def import_heat_pumps(self, scenario, engine, timeindex=None, import_types=None): + def import_heat_pumps(self, scenario, timeindex=None, import_types=None): """ - Gets heat pump data for specified scenario from oedb and integrates the heat - pumps into the grid. + Gets heat pump data for specified scenario from the OEDB or other eGon-data + databases depending on the provided Engine and integrates the heat pumps into + the grid. Besides heat pump capacity the heat pump's COP and heat demand to be served are as well retrieved. @@ -2189,8 +2185,6 @@ def import_heat_pumps(self, scenario, engine, timeindex=None, import_types=None) scenario : str Scenario for which to retrieve heat pump data. Possible options are 'eGon2035' and 'eGon100RE'. - engine : :sqlalchemy:`sqlalchemy.Engine` - Database engine. timeindex : :pandas:`pandas.DatetimeIndex` or None Specifies time steps for which to set COP and heat demand data. Leap years can currently not be handled. In case the given @@ -2231,7 +2225,7 @@ def import_heat_pumps(self, scenario, engine, timeindex=None, import_types=None) year = tools.get_year_based_on_scenario(scenario) return self.import_heat_pumps( scenario, - engine, + self.engine, timeindex=pd.date_range(f"1/1/{year}", periods=8760, freq="H"), import_types=import_types, ) @@ -2239,7 +2233,7 @@ def import_heat_pumps(self, scenario, engine, timeindex=None, import_types=None) integrated_heat_pumps = import_heat_pumps_oedb( edisgo_object=self, scenario=scenario, - engine=engine, + engine=self.engine, import_types=import_types, ) if len(integrated_heat_pumps) > 0: @@ -2247,7 +2241,7 @@ def import_heat_pumps(self, scenario, engine, timeindex=None, import_types=None) self, "oedb", heat_pump_names=integrated_heat_pumps, - engine=engine, + engine=self.engine, scenario=scenario, timeindex=timeindex, ) @@ -2255,7 +2249,7 @@ def import_heat_pumps(self, scenario, engine, timeindex=None, import_types=None) self, "oedb", heat_pump_names=integrated_heat_pumps, - engine=engine, + engine=self.engine, timeindex=timeindex, ) @@ -2303,7 +2297,7 @@ def apply_heat_pump_operating_strategy( """ hp_operating_strategy(self, strategy=strategy, heat_pump_names=heat_pump_names) - def import_dsm(self, scenario: str, engine: Engine, timeindex=None): + def import_dsm(self, scenario: str, timeindex=None): """ Gets industrial and CTS DSM profiles from the `OpenEnergy DataBase `_. @@ -2322,8 +2316,6 @@ def import_dsm(self, scenario: str, engine: Engine, timeindex=None): scenario : str Scenario for which to retrieve DSM data. Possible options are 'eGon2035' and 'eGon100RE'. - engine : :sqlalchemy:`sqlalchemy.Engine` - Database engine. timeindex : :pandas:`pandas.DatetimeIndex` or None Specifies time steps for which to get data. Leap years can currently not be handled. In case the given timeindex contains a leap year, the data will be @@ -2336,7 +2328,7 @@ def import_dsm(self, scenario: str, engine: Engine, timeindex=None): """ dsm_profiles = dsm_import.oedb( - edisgo_obj=self, scenario=scenario, engine=engine, timeindex=timeindex + edisgo_obj=self, scenario=scenario, engine=self.engine, timeindex=timeindex ) self.dsm.p_min = dsm_profiles["p_min"] self.dsm.p_max = dsm_profiles["p_max"] @@ -2346,7 +2338,6 @@ def import_dsm(self, scenario: str, engine: Engine, timeindex=None): def import_home_batteries( self, scenario: str, - engine: Engine, ): """ Gets home battery data for specified scenario and integrates the batteries into @@ -2357,7 +2348,8 @@ def import_home_batteries( between two scenarios: 'eGon2035' and 'eGon100RE'. The data is retrieved from the - `open energy platform `_. + `open energy platform `_ or other eGon-data + databases depending on the given Engine. The batteries are integrated into the grid (added to :attr:`~.network.topology.Topology.storage_units_df`) based on their building @@ -2374,14 +2366,12 @@ def import_home_batteries( scenario : str Scenario for which to retrieve home battery data. Possible options are 'eGon2035' and 'eGon100RE'. - engine : :sqlalchemy:`sqlalchemy.Engine` - Database engine. """ home_batteries_oedb( edisgo_obj=self, scenario=scenario, - engine=engine, + engine=self.engine, ) def plot_mv_grid_topology(self, technologies=False, **kwargs): diff --git a/edisgo/io/heat_pump_import.py b/edisgo/io/heat_pump_import.py index 4bf862485..da6d26cc7 100644 --- a/edisgo/io/heat_pump_import.py +++ b/edisgo/io/heat_pump_import.py @@ -327,7 +327,7 @@ def _get_individual_heat_pump_capacity(): "boundaries", ) egon_etrago_bus, egon_etrago_link = config.import_tables_from_oep( - engine, ["egon_etrago_bus", "egon_etrago_link"], "supply" + engine, ["egon_etrago_bus", "egon_etrago_link"], "grid" ) building_ids = edisgo_object.topology.loads_df.building_id.unique() diff --git a/edisgo/tools/config.py b/edisgo/tools/config.py index 84346a9be..adccd816a 100644 --- a/edisgo/tools/config.py +++ b/edisgo/tools/config.py @@ -130,6 +130,8 @@ class Config: """ def __init__(self, **kwargs): + self._engine = kwargs.get("engine", None) + if not kwargs.get("from_json", False): self._data = self.from_cfg(kwargs.get("config_path", "default")) else: @@ -164,13 +166,17 @@ def _set_db_mappings(self) -> None: """ Sets the database table and schema mappings by retrieving alias dictionaries. """ - name_mapping, schema_mapping = self.get_database_alias_dictionaries() + if self._engine is not None and "toep.iks.cs.ovgu.de" in self._engine.url.host: + name_mapping, schema_mapping = self.get_database_alias_dictionaries() + else: + name_mapping = schema_mapping = {} + self.db_table_mapping = name_mapping self.db_schema_mapping = schema_mapping def get_database_alias_dictionaries(self) -> tuple[dict[str, str], dict[str, str]]: """ - Retrieves the database alias dictionaries for table and schema mappings. + Retrieves the OEP database alias dictionaries for table and schema mappings. Returns ------- From 1b4057a85bf87e3d913409da942d9b0fbfc3d9dd Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Wed, 12 Feb 2025 15:04:29 +0100 Subject: [PATCH 11/43] only get table and schema mapping from db if loading data from (t)oep --- edisgo/tools/config.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/edisgo/tools/config.py b/edisgo/tools/config.py index adccd816a..1b1a681d0 100644 --- a/edisgo/tools/config.py +++ b/edisgo/tools/config.py @@ -324,10 +324,12 @@ def from_cfg(self, config_path=None): config_dict["demandlib"]["day_end"].minute, ) - ( - config_dict["db_tables_dict"], - config_dict["db_schema_dict"], - ) = self.get_database_alias_dictionaries() + if self._engine is not None and "toep.iks.cs.ovgu.de" in self._engine.url.host: + config_dict["db_tables_dict"], config_dict["db_schema_dict"] = ( + self.get_database_alias_dictionaries() + ) + else: + config_dict["db_tables_dict"] = config_dict["db_schema_dict"] = {} return config_dict From a323903927233319d674b87a95db8730ea742fb4 Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Wed, 12 Feb 2025 15:18:50 +0100 Subject: [PATCH 12/43] adapt whatsnew --- doc/whatsnew/v0-3-0.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/whatsnew/v0-3-0.rst b/doc/whatsnew/v0-3-0.rst index 1335a26c7..95ab687b0 100644 --- a/doc/whatsnew/v0-3-0.rst +++ b/doc/whatsnew/v0-3-0.rst @@ -28,4 +28,6 @@ Changes * Move function to assign feeder to Topology class and add methods to the Grid class to get information on the feeders `#360 `_ * Added a storage operation strategy where the storage is charged when PV feed-in is higher than electricity demand of the household and discharged when electricity demand exceeds PV generation `#386 `_ * Added an estimation of the voltage deviation over a cable when selecting a suitable cable to connect a new component `#411 `_ -* Added clipping of heat pump electrical power at its maximum value #428 +* Added clipping of heat pump electrical power at its maximum value `#428 `_ +* Made OEP database call optional in get_database_alias_dictionaries, allowing setup without OEP when using an alternative eGon-data database. `#451 `_ +* Fixed database import issues by addressing table naming assumptions and added support for external SSH tunneling in eGon-data configurations. `#451 `_ From 8f126a3bf3ddbfd9753159be59e51a454d91c7a0 Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Wed, 12 Feb 2025 15:53:20 +0100 Subject: [PATCH 13/43] use provided engine in config.py --- edisgo/tools/config.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/edisgo/tools/config.py b/edisgo/tools/config.py index 1b1a681d0..6cf74c77c 100644 --- a/edisgo/tools/config.py +++ b/edisgo/tools/config.py @@ -187,20 +187,16 @@ def get_database_alias_dictionaries(self) -> tuple[dict[str, str], dict[str, str - schema_mapping: A dictionary mapping source schema names to target schema names. """ - OEP_CONNECTION = "postgresql+oedialect://:@{platform}" - platform = "toep.iks.cs.ovgu.de" - conn_str = OEP_CONNECTION.format(platform=platform) - engine = sa.create_engine(conn_str) dictionary_schema_name = ( "model_draft" # Replace with the actual schema name if needed ) dictionary_module_name = f"saio.{dictionary_schema_name}" - register_schema(dictionary_schema_name, engine) + register_schema(dictionary_schema_name, self._engine) dictionary_table_name = "edut_00" dictionary_table = importlib.import_module(dictionary_module_name).__getattr__( dictionary_table_name ) - with session_scope_egon_data(engine) as session: + with session_scope_egon_data(self._engine) as session: query = session.query(dictionary_table) dictionary_entries = query.all() name_mapping = { From 3bdb727fb47a4607e012004696a81d46b200caf4 Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Mon, 17 Feb 2025 12:01:44 +0100 Subject: [PATCH 14/43] Fix missing timeindex in set_time_series_active_power_predefined - Automatically set `EDisGo.TimeSeries.timeindex` to the default year of the database if it is empty. # - Added a logger warning to inform users about the default behavior and recommend setting the `timeindex` explicitly using `EDisGo.set_timeindex()`. # On branch feature/#456-feature-set-default-timeindex-in-edisgoset_time_series_active_power_predefined --- doc/whatsnew/v0-3-0.rst | 1 + edisgo/edisgo.py | 29 +++++++++++++++++++++-------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/doc/whatsnew/v0-3-0.rst b/doc/whatsnew/v0-3-0.rst index 1335a26c7..cc0cae1fa 100644 --- a/doc/whatsnew/v0-3-0.rst +++ b/doc/whatsnew/v0-3-0.rst @@ -29,3 +29,4 @@ Changes * Added a storage operation strategy where the storage is charged when PV feed-in is higher than electricity demand of the household and discharged when electricity demand exceeds PV generation `#386 `_ * Added an estimation of the voltage deviation over a cable when selecting a suitable cable to connect a new component `#411 `_ * Added clipping of heat pump electrical power at its maximum value #428 +* Loading predefined time series now automatically sets the timeindex to the default year of the database if it is empty. `#457 `_ diff --git a/edisgo/edisgo.py b/edisgo/edisgo.py index 4fda72aa2..07ca92d0a 100755 --- a/edisgo/edisgo.py +++ b/edisgo/edisgo.py @@ -41,6 +41,7 @@ ) from edisgo.io.heat_pump_import import oedb as import_heat_pumps_oedb from edisgo.io.storage_import import home_batteries_oedb +from edisgo.io.timeseries_import import _timeindex_helper_func from edisgo.network import timeseries from edisgo.network.dsm import DSM from edisgo.network.electromobility import Electromobility @@ -558,12 +559,22 @@ def set_time_series_active_power_predefined( """ if self.timeseries.timeindex.empty: logger.warning( - "When setting time series using predefined profiles it is better to " - "set a time index as all data in TimeSeries class is indexed by the" - "time index. You can set the time index upon initialisation of " - "the EDisGo object by providing the input parameter 'timeindex' or by " - "using the function EDisGo.set_timeindex()." + "The EDisGo.TimeSeries.timeindex is empty. By default, this function " + "will set the timeindex to the default year of the provided database " + "connection or, if specified, to the given timeindex. To ensure " + "expected behavior, consider setting the timeindex explicitly before " + "running this function using EDisGo.set_timeindex()." ) + + timeindex = kwargs.get("timeindex", None) + + if timeindex is None: + timeindex, _ = _timeindex_helper_func( + self, timeindex, allow_leap_year=True + ) + + self.set_timeindex(timeindex) + if fluctuating_generators_ts is not None: self.timeseries.predefined_fluctuating_generators_by_technology( self, @@ -1846,9 +1857,11 @@ def _aggregate_time_series(attribute, groups, naming): [ pd.DataFrame( { - naming.format("_".join(k)) - if isinstance(k, tuple) - else naming.format(k): getattr(self.timeseries, attribute) + ( + naming.format("_".join(k)) + if isinstance(k, tuple) + else naming.format(k) + ): getattr(self.timeseries, attribute) .loc[:, v] .sum(axis=1) } From bb1f3b4a77a124a4f7a5bb011724050d85ea8bec Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Mon, 17 Feb 2025 16:02:57 +0100 Subject: [PATCH 15/43] Overwrite timeindex if given timeindex differs from existing timeindex in set_time_series_active_power_predefined --- edisgo/edisgo.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/edisgo/edisgo.py b/edisgo/edisgo.py index 07ca92d0a..80ed8c35c 100755 --- a/edisgo/edisgo.py +++ b/edisgo/edisgo.py @@ -557,22 +557,38 @@ def set_time_series_active_power_predefined( is indexed using a default year and set for the whole year. """ - if self.timeseries.timeindex.empty: + + timeindex = kwargs.get("timeindex", None) + set_timeindex = False + + if (timeindex is not None) and not timeindex.equals(self.timeseries.timeindex): + logger.warning( + "The given timeindex is different from the EDisGo.TimeSeries.timeindex." + " Therefore the EDisGo.TimeSeries.timeindex will be overwritten by the " + "given timeindex." + ) + + set_timeindex = True + + elif self.timeseries.timeindex.empty: logger.warning( "The EDisGo.TimeSeries.timeindex is empty. By default, this function " "will set the timeindex to the default year of the provided database " - "connection or, if specified, to the given timeindex. To ensure " - "expected behavior, consider setting the timeindex explicitly before " - "running this function using EDisGo.set_timeindex()." + "connection. To ensure expected behavior, consider setting the " + "timeindex explicitly before running this function using " + "EDisGo.set_timeindex()." ) - timeindex = kwargs.get("timeindex", None) + set_timeindex = True + if set_timeindex: if timeindex is None: timeindex, _ = _timeindex_helper_func( self, timeindex, allow_leap_year=True ) + logger.warning(f"Setting EDisGo.TimeSeries.timeindex to {timeindex}.") + self.set_timeindex(timeindex) if fluctuating_generators_ts is not None: From aa075cc6aa9382ff37ce3f085fa9e594b5752858 Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Mon, 17 Feb 2025 16:40:47 +0100 Subject: [PATCH 16/43] add logging and correct timeindex variable handling --- edisgo/edisgo.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/edisgo/edisgo.py b/edisgo/edisgo.py index 80ed8c35c..8f98e4796 100755 --- a/edisgo/edisgo.py +++ b/edisgo/edisgo.py @@ -591,13 +591,17 @@ def set_time_series_active_power_predefined( self.set_timeindex(timeindex) + logger.info( + f"Trying to set predefined timeseries for {self.timeseries.timeindex}" + ) + if fluctuating_generators_ts is not None: self.timeseries.predefined_fluctuating_generators_by_technology( self, fluctuating_generators_ts, fluctuating_generators_names, engine=kwargs.get("engine", toep_engine()), - timeindex=kwargs.get("timeindex", None), + timeindex=timeindex, ) if dispatchable_generators_ts is not None: self.timeseries.predefined_dispatchable_generators_by_technology( @@ -612,7 +616,7 @@ def set_time_series_active_power_predefined( edisgo_obj=self, scenario=kwargs.get("scenario"), engine=kwargs.get("engine", toep_engine()), - timeindex=kwargs.get("timeindex", None), + timeindex=timeindex, load_names=conventional_loads_names, ) # concat new time series with existing ones and drop any duplicate From f4253bf3aa8c1e35246020cb1db31010e582717a Mon Sep 17 00:00:00 2001 From: Jonas Danke Date: Tue, 18 Feb 2025 09:02:36 +0100 Subject: [PATCH 17/43] Refactor test cases for improved readability and consistency --- tests/test_edisgo.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_edisgo.py b/tests/test_edisgo.py index a4d20407a..f6c67283f 100755 --- a/tests/test_edisgo.py +++ b/tests/test_edisgo.py @@ -934,9 +934,9 @@ def test_aggregate_components(self): # ##### test without any aggregation - self.edisgo.topology._loads_df.at[ - "Load_residential_LVGrid_1_4", "bus" - ] = "Bus_BranchTee_LVGrid_1_10" + self.edisgo.topology._loads_df.at["Load_residential_LVGrid_1_4", "bus"] = ( + "Bus_BranchTee_LVGrid_1_10" + ) # save original values number_gens_before = len(self.edisgo.topology.generators_df) @@ -1054,9 +1054,9 @@ def test_aggregate_components(self): ) # manipulate grid so that more than one load of the same sector is # connected at the same bus - self.edisgo.topology._loads_df.at[ - "Load_residential_LVGrid_1_4", "bus" - ] = "Bus_BranchTee_LVGrid_1_10" + self.edisgo.topology._loads_df.at["Load_residential_LVGrid_1_4", "bus"] = ( + "Bus_BranchTee_LVGrid_1_10" + ) # save original values (only loads, as generators did not change) loads_p_set_before = self.edisgo.topology.loads_df.p_set.sum() @@ -1136,9 +1136,9 @@ def test_aggregate_components(self): # manipulate grid so that two generators of different types are # connected at the same bus - self.edisgo.topology._generators_df.at[ - "GeneratorFluctuating_13", "type" - ] = "misc" + self.edisgo.topology._generators_df.at["GeneratorFluctuating_13", "type"] = ( + "misc" + ) # save original values (values of loads were changed in previous aggregation) loads_p_set_before = self.edisgo.topology.loads_df.p_set.sum() From fa7ff54fb69a296d86dcfa083fdb36f040795fa4 Mon Sep 17 00:00:00 2001 From: Jonas Danke Date: Tue, 18 Feb 2025 09:02:47 +0100 Subject: [PATCH 18/43] updated tests with new warning text --- tests/test_edisgo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_edisgo.py b/tests/test_edisgo.py index f6c67283f..cc6f346d4 100755 --- a/tests/test_edisgo.py +++ b/tests/test_edisgo.py @@ -207,7 +207,7 @@ def test_set_time_series_active_power_predefined(self, caplog): # check warning self.edisgo.set_time_series_active_power_predefined() assert ( - "When setting time series using predefined profiles it is better" + "The EDisGo.TimeSeries.timeindex is empty. By default, this function" in caplog.text ) From 2fca51c57f53ef2789fa72258cccbbb239704441 Mon Sep 17 00:00:00 2001 From: Kilian Helfenbein Date: Wed, 19 Feb 2025 15:26:55 +0100 Subject: [PATCH 19/43] Refactor object copying in EDisGo class - Replaced direct use of copy.deepcopy with custom copy method in two places. - Added a new `copy` method to the `EDisGo` class, supporting both shallow and deep copies while excluding the SQLAlchemy engine from being copied. - Ensured proper restoration of the SQLAlchemy engine after copying. --- edisgo/edisgo.py | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/edisgo/edisgo.py b/edisgo/edisgo.py index 469f9972d..77e6ba0a7 100755 --- a/edisgo/edisgo.py +++ b/edisgo/edisgo.py @@ -1349,7 +1349,7 @@ def reinforce( """ if copy_grid: - edisgo_obj = copy.deepcopy(self) + edisgo_obj = self.copy() else: edisgo_obj = self @@ -3121,7 +3121,7 @@ def spatial_complexity_reduction( """ if copy_edisgo is True: - edisgo_obj = copy.deepcopy(self) + edisgo_obj = self.copy() else: edisgo_obj = self busmap_df, linemap_df = spatial_complexity_reduction( @@ -3335,6 +3335,44 @@ def resample_timeseries( self.heat_pump.resample_timeseries(method=method, freq=freq) self.overlying_grid.resample(method=method, freq=freq) + def copy(self, deep=True): + """ + Returns a copy of the object, with an option for a deep copy. + + The SQLAlchemy engine is excluded from the copying process and restored + afterward. + + Parameters + ---------- + deep : bool + If True, performs a deep copy; otherwise, performs a shallow copy. + + Returns + --------- + :class:`~.EDisGo` + Copied EDisGo object. + + """ + tmp_engine = ( + getattr(self, "engine", None) + if isinstance(getattr(self, "engine", None), Engine) + else None + ) + + if tmp_engine: + logging.info("Temporarily removing the SQLAlchemy engine before copying.") + self.engine = self.config._engine = None + + cpy = copy.deepcopy(self) if deep else copy.copy(self) + + if tmp_engine: + logging.info("Restoring the SQLAlchemy engine after copying.") + self.engine = self.config._engine = cpy.engine = cpy.config._engine = ( + tmp_engine + ) + + return cpy + def import_edisgo_from_pickle(filename, path=""): """ From 567770dab7ef569ef99afffce001fe41cd361580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Schl=C3=B6sser?= Date: Tue, 18 Feb 2025 09:33:30 +0100 Subject: [PATCH 20/43] Jupyter Notebook for LoMa Workshop --- examples/Workshop_LoMa.ipynb | 485 +++++++++++++++++++++++------------ 1 file changed, 320 insertions(+), 165 deletions(-) diff --git a/examples/Workshop_LoMa.ipynb b/examples/Workshop_LoMa.ipynb index 2b11fbd62..0d303da08 100644 --- a/examples/Workshop_LoMa.ipynb +++ b/examples/Workshop_LoMa.ipynb @@ -13,21 +13,18 @@ "id": "1", "metadata": {}, "source": [ - "Contents:" - ] - }, - { - "cell_type": "markdown", - "id": "2", - "metadata": {}, - "source": [ - "Import Packages" + "Contents:\n", + "1. Topology Setup\n", + "2. Worst Case Time Series Creation\n", + "3. Grid Investigation\n", + "4. Results\n", + "5. Additional Time Series\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "3", + "id": "2", "metadata": {}, "outputs": [], "source": [ @@ -37,7 +34,7 @@ { "cell_type": "code", "execution_count": null, - "id": "4", + "id": "3", "metadata": {}, "outputs": [], "source": [ @@ -61,7 +58,7 @@ { "cell_type": "code", "execution_count": null, - "id": "5", + "id": "4", "metadata": {}, "outputs": [], "source": [ @@ -71,12 +68,20 @@ "warnings.filterwarnings(\"ignore\")" ] }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "## 1 Topology Setup" + ] + }, { "cell_type": "markdown", "id": "6", "metadata": {}, "source": [ - "## Load grid Data from ding0" + "In this section we load all components into a newly created edisgo object. This includes the lines, buses, transformers, switches, generators, loads, heat pumps and battery storages." ] }, { @@ -84,9 +89,7 @@ "id": "7", "metadata": {}, "source": [ - "Currently, synthetic grid data generated with the python project ding0 is the only supported data source for distribution grid data. ding0 provides the grid topology data in the form of csv files, with separate files for buses, lines, loads, generators, etc. You can retrieve ding0 data from Zenodo (make sure you choose latest data) or check out the Ding0 documentation on how to generate grids yourself. A ding0 example grid can be viewed here. It is possible to provide your own grid data if it is in the same format as the ding0 grid data.\n", - "\n", - "This example works with any ding0 grid data. If you don't have grid data yet, you can execute the following to download the example grid data mentioned above." + "### Standard components" ] }, { @@ -94,7 +97,7 @@ "id": "8", "metadata": {}, "source": [ - "The ding0 grid you want to use in your analysis is specified through the input parameter 'ding0_grid' of the EDisGo class. The following assumes you want to use the ding0 example grid downloaded above. To use a different ding0 grid, just change the path below." + "Set up a new edisgo object:" ] }, { @@ -104,10 +107,11 @@ "metadata": {}, "outputs": [], "source": [ - "ding0_grid = os.path.join(\n", - " os.path.expanduser(\"~\"), \".ding0\", \"run_2025-02-04-09-34-25\", \"35725\"\n", - ")\n", - "edisgo = EDisGo(ding0_grid=ding0_grid, legacy_ding0_grids=False)" + "conf_path = Path.home() / \"Downloads\" / \"egon-data.configuration.yaml\"\n", + "db_engine = engine(path=conf_path, ssh=True)\n", + "ding0_grid = Path.home() / \".edisgo\" / \"husum_grids\" / \"35725\"\n", + "\n", + "edisgo = EDisGo(ding0_grid=ding0_grid, legacy_ding0_grids=False, engine=db_engine)" ] }, { @@ -115,13 +119,64 @@ "id": "10", "metadata": {}, "source": [ - "## Plot grid topology (MV)" + "The ding0 grids are not up to date and their capacity is not sufficient for the connected loads and generators. To update the imported grids they need to be extended first with the function ```reinforce()```." + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "Grids are reinforced for their worst case scenarios. The corresponding time series are created with ```set_time_series_worst_case_analysis()```. " ] }, { "cell_type": "code", "execution_count": null, - "id": "11", + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.set_time_series_worst_case_analysis()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.reinforce()" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "### Plot grid topology (MV)" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "The topology can be visualized with the ```plot_mv_grid_topology()```. For ```technologies=True``` the buses sizes and colors are determined to the type and size of the technologies connected to it. \n", + "\n", + "- red: nodes with substation secondary side\n", + "- light blue: nodes distribution substations's primary side\n", + "- green: nodes with fluctuating generators\n", + "- black: nodes with conventional generators\n", + "- grey: disconnecting points\n", + "- dark blue: branch trees" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", "metadata": {}, "outputs": [], "source": [ @@ -143,7 +198,7 @@ { "cell_type": "code", "execution_count": null, - "id": "12", + "id": "17", "metadata": {}, "outputs": [], "source": [ @@ -152,100 +207,97 @@ }, { "cell_type": "markdown", - "id": "13", - "metadata": {}, - "source": [ - "red: nodes with substation secondary side\n", - "light blue: nodes distribution substations's primary side\n", - "green: nodes with fluctuating generators\n", - "black: nodes with conventional generators\n", - "grey: disconnecting points\n", - "dark blue: branch trees" - ] - }, - { - "cell_type": "markdown", - "id": "14", + "id": "18", "metadata": {}, "source": [ - "Geladenes Netz muss auf den aktuellen Stand gestzt werden --> reinforce" + "### Topology-Module Data Structure" ] }, { "cell_type": "markdown", - "id": "15", + "id": "19", "metadata": {}, "source": [ - "worst case analysis ... " + "Let's get familiar with the topology module:" ] }, { "cell_type": "code", "execution_count": null, - "id": "16", + "id": "20", "metadata": {}, "outputs": [], "source": [ - "edisgo.set_time_series_worst_case_analysis()" + "edisgo.topology.generators_df[[\"p_nom\", \"type\"]].groupby(\"type\").sum()" ] }, { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "21", "metadata": {}, "outputs": [], "source": [ - "edisgo.reinforce()" + "edisgo.topology.loads_df[[\"p_set\", \"type\"]].groupby(\"type\").sum()" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "18", + "cell_type": "markdown", + "id": "22", "metadata": {}, - "outputs": [], - "source": [] + "source": [ + "Number of LV grids in the MV grid" + ] }, { "cell_type": "code", "execution_count": null, - "id": "19", + "id": "23", "metadata": {}, "outputs": [], "source": [ - "edisgo.topology.generators_df.head()" + "len(list(edisgo.topology.mv_grid.lv_grids))" ] }, { "cell_type": "markdown", - "id": "20", + "id": "24", "metadata": {}, "source": [ - "### TODO: Hier etwas die eDisGo Struktur zeigen und ein paar Statistiken wie folgende" + "Total number of lines:" ] }, { "cell_type": "code", "execution_count": null, - "id": "21", + "id": "25", "metadata": {}, "outputs": [], "source": [ - "edisgo.topology.generators_df[[\"p_nom\", \"type\"]].groupby(\"type\").sum()" + "len(edisgo.topology.lines_df.index)" ] }, { "cell_type": "markdown", - "id": "22", + "id": "26", + "metadata": {}, + "source": [ + "Number of lines in one of the low voltage grids." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", "metadata": {}, + "outputs": [], "source": [ - "## Adapt network (Husum)" + "len(edisgo.topology.grids[5].lines_df.index)" ] }, { "cell_type": "markdown", - "id": "23", + "id": "28", "metadata": {}, "source": [ "### Basic components addition and removal" @@ -253,10 +305,10 @@ }, { "cell_type": "markdown", - "id": "24", + "id": "29", "metadata": {}, "source": [ - "To see how a loaded network can be adapted later on, we add a solar plant to a random bus. Then we remove it again. \n", + "To see how a loaded network can be adapted later on, we add a solar plant to a random bus.\n", "\n", "Components can also be added according to their geolocation with the function integrate_component_based_on_geolocation()." ] @@ -264,7 +316,7 @@ { "cell_type": "code", "execution_count": null, - "id": "25", + "id": "30", "metadata": {}, "outputs": [], "source": [ @@ -273,16 +325,16 @@ }, { "cell_type": "markdown", - "id": "26", + "id": "31", "metadata": {}, "source": [ - "Add a generator with the function add_component or add_generator." + "Add a generator with the function ```add_component()``` or ```add_generator()```. " ] }, { "cell_type": "code", "execution_count": null, - "id": "27", + "id": "32", "metadata": {}, "outputs": [], "source": [ @@ -295,110 +347,136 @@ ")" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "33", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.topology.generators_df" + ] + }, { "cell_type": "markdown", - "id": "28", + "id": "34", "metadata": {}, "source": [ - "Mit Generator zeigen, mit Last nachmachen" + "We can also add a heat pump:" ] }, { "cell_type": "code", "execution_count": null, - "id": "29", + "id": "35", "metadata": {}, "outputs": [], "source": [ - "edisgo.topology.generators_df" + "edisgo.topology.loads_df" ] }, { "cell_type": "code", "execution_count": null, - "id": "30", + "id": "36", "metadata": {}, "outputs": [], "source": [ - "edisgo.remove_component(comp_type=\"generator\", comp_name=new_generator)" + "new_load = edisgo.add_component(\n", + " comp_type=\"load\", p_set=0.01, bus=rnd_bus, type=\"heat_pump\"\n", + ")" ] }, { "cell_type": "code", "execution_count": null, - "id": "31", + "id": "37", "metadata": {}, "outputs": [], "source": [ - "edisgo.topology.generators_df" + "edisgo.topology.loads_df" ] }, { "cell_type": "markdown", - "id": "32", + "id": "38", "metadata": {}, "source": [ - "### Add flexible components to grid (Heat pumps)" + "Single components can be removed with ```remove_component()```" ] }, { - "cell_type": "markdown", - "id": "33", + "cell_type": "code", + "execution_count": null, + "id": "39", "metadata": {}, + "outputs": [], "source": [ - "Add heat pumps with the function import_heat_pumps()" + "edisgo.remove_component(comp_type=\"generator\", comp_name=new_generator)\n", + "edisgo.remove_component(comp_type=\"load\", comp_name=new_load)" ] }, { - "cell_type": "markdown", - "id": "34", + "cell_type": "code", + "execution_count": null, + "id": "40", "metadata": {}, + "outputs": [], "source": [ - "Engine der eigentlichen Datenbank muss verwendet werden" + "edisgo.topology.generators_df" ] }, { "cell_type": "code", "execution_count": null, - "id": "35", + "id": "41", "metadata": {}, "outputs": [], "source": [ - "scenario = \"eGon2035\"" + "edisgo.topology.loads_df" + ] + }, + { + "cell_type": "markdown", + "id": "42", + "metadata": {}, + "source": [ + "### Add flexible components to grid " + ] + }, + { + "cell_type": "markdown", + "id": "43", + "metadata": {}, + "source": [ + "For realistic future grids we also add further components like additional generators, home batteries, (charging points) and heat pumps. The components are added according to the scenario \"eGon2035\" and the data from the oedb." ] }, { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "44", "metadata": {}, "outputs": [], "source": [ - "conf_path = (\n", - " Path.home()\n", - " / \"Documents\"\n", - " / \"data\"\n", - " / \"egon-data-hetzner\"\n", - " / \"egon-data.configuration.yaml\"\n", - ")\n", - "\n", - "db_engine = engine(path=conf_path, ssh=True)" + "scenario = \"eGon2035\"" ] }, { "cell_type": "code", "execution_count": null, - "id": "37", + "id": "45", "metadata": {}, "outputs": [], "source": [ + "# copy the edisgo object for later comparisons\n", "edisgo_orig = deepcopy(edisgo)" ] }, { "cell_type": "code", "execution_count": null, - "id": "38", + "id": "46", "metadata": {}, "outputs": [], "source": [ @@ -406,28 +484,28 @@ "edisgo = deepcopy(edisgo_orig)\n", "\n", "edisgo.import_generators(generator_scenario=scenario)\n", - "edisgo.import_home_batteries(scenario=scenario, engine=db_engine)\n", - "edisgo.import_heat_pumps(scenario=scenario, engine=db_engine)" + "edisgo.import_home_batteries(scenario=scenario)\n", + "edisgo.import_heat_pumps(scenario=scenario)" ] }, { "cell_type": "code", "execution_count": null, - "id": "39", + "id": "47", "metadata": {}, "outputs": [], "source": [ "# This takes too long for the workshop, but needs to be mentioned\n", - "# edisgo_obj.import_dsm(scenario=scenario, engine=db_engine)\n", + "# edisgo_obj.import_dsm(scenario=scenario)\n", "# edisgo_obj.import_electromobility(\n", - "# data_source=\"oedb\", scenario=scenario, engine=db_engine\n", - "# )" + "# data_source=\"oedb\", scenario=scenario\n", + "#)" ] }, { "cell_type": "code", "execution_count": null, - "id": "40", + "id": "48", "metadata": {}, "outputs": [], "source": [ @@ -437,75 +515,121 @@ { "cell_type": "code", "execution_count": null, - "id": "41", + "id": "49", "metadata": {}, "outputs": [], "source": [ - "# TODO Moritz: Show statistics before and after import\n", - "edisgo.topology.generators_df.head()" + "edisgo_orig.topology.generators_df[[\"p_nom\", \"type\"]].groupby(\"type\").sum()" ] }, { - "cell_type": "markdown", - "id": "42", + "cell_type": "code", + "execution_count": null, + "id": "50", "metadata": {}, + "outputs": [], "source": [ - "edisgo.import_heat_pumps(scenario = scenario, engine = toep_engine())" + "edisgo.topology.loads_df[[\"p_set\", \"type\"]].groupby(\"type\").sum()" ] }, { - "cell_type": "markdown", - "id": "43", + "cell_type": "code", + "execution_count": null, + "id": "51", "metadata": {}, + "outputs": [], "source": [ - "Batteries: import_home_batteries()\n", - "\n", - "Demand Side Management: import_dsm()\n", - "\n", - "Charging parks and stations for EV: import_electromobility() --> See electromobility example" + "edisgo_orig.topology.loads_df[[\"p_set\", \"type\"]].groupby(\"type\").sum()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.topology.storage_units_df[\"p_nom\"].sum()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo_orig.topology.storage_units_df[\"p_nom\"].sum()" ] }, { "cell_type": "markdown", - "id": "44", + "id": "54", "metadata": {}, "source": [ - "## Create timeseries for all intergrated components" + "## 2 Worst Case Time Series Creation" ] }, { "cell_type": "markdown", - "id": "45", + "id": "55", "metadata": {}, "source": [ - "Create timeseries for the four worst cases MV load case, LV load case, MV feed-in case, LV feed-in case with the function set_time_series_worst_case_analysis():" + "Create timeseries for the four worst cases MV load case, LV load case, MV feed-in case, LV feed-in case with the function set_time_series_worst_case_analysis().\n", + "\n", + "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:\n", + "\n", + "* **’feed-in_case’** \n", + " \n", + " Feed-in and demand for the worst-case scenario \"reverse power flow\" are generated (e.g. conventional electricity demand is set to 15% of maximum demand for loads connected to the MV grid and 10% for loads connected to the LV grid and 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)\n", + "\n", + " \n", + "* **’load_case’**\n", + "\n", + " Feed-in and demand for the worst-case scenario \"heavy load flow\" are generated (e.g. demand of all conventional loads is by default set to maximum demand and feed-in of all generators is set to zero)\n", + "\n", + "\n", + "* **[’feed-in_case’, ’load_case’]**\n", + "\n", + " Both cases are set up.\n", + " \n", + "By default both cases are set up.\n", + "\n", + "Feed-in and demand in the two worst-cases are defined in the [config file 'config_timeseries.cfg'](https://edisgo.readthedocs.io/en/latest/configs.html#config-timeseries) and can be changed by setting different values in the config file. " ] }, { "cell_type": "code", "execution_count": null, - "id": "46", + "id": "56", "metadata": {}, "outputs": [], "source": [ "edisgo.set_time_series_worst_case_analysis()" ] }, + { + "cell_type": "markdown", + "id": "57", + "metadata": {}, + "source": [ + "The function creates time series for four time steps since both worst cases are defined seperately for the LV and the MV grid with individual simultanerity factors." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "47", + "id": "58", "metadata": {}, "outputs": [], "source": [ - "# TODO: Erklärung zu der Unterscheidung der vier timesteps\n", "edisgo.timeseries.timeindex_worst_cases" ] }, { "cell_type": "code", "execution_count": null, - "id": "48", + "id": "59", "metadata": {}, "outputs": [], "source": [ @@ -516,15 +640,15 @@ }, { "cell_type": "markdown", - "id": "49", + "id": "60", "metadata": {}, "source": [ - "## Main features" + "## 3 Grid Investigation" ] }, { "cell_type": "markdown", - "id": "50", + "id": "61", "metadata": {}, "source": [ "Execute a power flow analysis to determine line overloads and voltage deviations for the MV load case timeseries with the function analyze():" @@ -533,17 +657,25 @@ { "cell_type": "code", "execution_count": null, - "id": "51", + "id": "62", "metadata": {}, "outputs": [], "source": [ "edisgo.analyze(timesteps=edisgo.timeseries.timeindex_worst_cases[\"load_case_mv\"])" ] }, + { + "cell_type": "markdown", + "id": "63", + "metadata": {}, + "source": [ + "A geoplot wtih the bus and line colrs based on the voltage deviations and line loadings repectively can be created with ```plot_mv_line_loading()```." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "52", + "id": "64", "metadata": {}, "outputs": [], "source": [ @@ -553,10 +685,18 @@ ")" ] }, + { + "cell_type": "markdown", + "id": "65", + "metadata": {}, + "source": [ + "For a better overview of the voltage deviations and line loads in the entire grid, edisgo provides histrogram plots." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "53", + "id": "66", "metadata": {}, "outputs": [], "source": [ @@ -566,35 +706,43 @@ { "cell_type": "code", "execution_count": null, - "id": "54", + "id": "67", "metadata": {}, "outputs": [], "source": [ - "edisgo.histogram_relative_line_load(binwidth=0.1, voltage_level=\"mv\")" + "edisgo.histogram_relative_line_load(binwidth=0.1)" ] }, { "cell_type": "markdown", - "id": "55", + "id": "68", "metadata": {}, "source": [ - "Reinfoce the grid with the function reinforce(): (Use mode 'mv' for shorter runtime)" + "## 4 Results" + ] + }, + { + "cell_type": "markdown", + "id": "69", + "metadata": {}, + "source": [ + "Now we reinforce the fully equipped grid. For a shorter runtime, only the MV grid is considered by setting ```mode = \"mv\"```." ] }, { "cell_type": "code", "execution_count": null, - "id": "56", + "id": "70", "metadata": {}, "outputs": [], "source": [ - "edisgo.reinforce(mode=\"mv\")" + "edisgo.reinforce()" ] }, { "cell_type": "code", "execution_count": null, - "id": "57", + "id": "71", "metadata": {}, "outputs": [], "source": [ @@ -607,91 +755,98 @@ { "cell_type": "code", "execution_count": null, - "id": "58", + "id": "72", "metadata": {}, "outputs": [], "source": [ - "edisgo.histogram_voltage(binwidth=0.005)" + "# edisgo.analyze() " ] }, { "cell_type": "code", "execution_count": null, - "id": "59", + "id": "73", "metadata": {}, "outputs": [], "source": [ - "edisgo.histogram_relative_line_load(binwidth=0.1, voltage_level=\"mv\")" + "edisgo.histogram_voltage(binwidth=0.005)" ] }, { - "cell_type": "markdown", - "id": "60", + "cell_type": "code", + "execution_count": null, + "id": "74", "metadata": {}, + "outputs": [], "source": [ - "## Results" + "edisgo.histogram_relative_line_load(binwidth=0.1)" ] }, { "cell_type": "markdown", - "id": "61", + "id": "75", "metadata": {}, "source": [ - "Display the resulting equipment changes and their corresponding costs with the results class:" + "The module ```results```holds the outpts of the reinforcement" ] }, { "cell_type": "code", "execution_count": null, - "id": "62", + "id": "76", "metadata": {}, "outputs": [], "source": [ - "edisgo.results.equipment_changes.head()" - ] - }, - { - "cell_type": "markdown", - "id": "63", - "metadata": {}, - "source": [ - "### TODO: Die initialen Netzausbaumaßnahmen am Anfang müssen hier abgezogen werden" + "# The equipment changes of the reinforcement after the grid setup have to be dropped\n", + "edisgo.results.equipment_changes[len(edisgo_orig.results.equipment_changes) :].head()" ] }, { "cell_type": "code", "execution_count": null, - "id": "64", + "id": "77", "metadata": {}, "outputs": [], "source": [ - "edisgo.results.grid_expansion_costs.head()" + "edisgo.results.grid_expansion_costs[\n", + " len(edisgo_orig.results.grid_expansion_costs) :\n", + "].head()" ] }, { "cell_type": "markdown", - "id": "65", + "id": "78", "metadata": {}, "source": [ "### TODO: Visualisierung der Ergebnisse" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "79", + "metadata": {}, + "outputs": [], + "source": [ + "# edisgo_costs = deepcopy(edisgo)\n", + "# edisgo_costs.results.grid_expansion_costs = edisgo.results.grid_expansion_costs[len(edisgo_orig.results.grid_expansion_costs) :]\n", + "# edisgo_costs.plot_mv_grid_expansion_costs()" + ] + }, { "cell_type": "markdown", - "id": "66", + "id": "80", "metadata": {}, "source": [ - "### TODO: Alternative Wege für Zeitreihen, wie von der DB\n", + "## 5 Additional Time Series\n", "\n", "* show time resolution" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "67", + "cell_type": "markdown", + "id": "81", "metadata": {}, - "outputs": [], "source": [] } ], @@ -711,7 +866,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.0" + "version": "3.10.16" } }, "nbformat": 4, From 426562c46c478c9e01762768b9caf4e4066fbdcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Schl=C3=B6sser?= Date: Tue, 18 Feb 2025 09:33:49 +0100 Subject: [PATCH 21/43] Jupyter Notebook for LoMa Workshop --- examples/plot_example.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/plot_example.ipynb b/examples/plot_example.ipynb index 4c71ecc1d..696c2a5a0 100644 --- a/examples/plot_example.ipynb +++ b/examples/plot_example.ipynb @@ -411,7 +411,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.16" + "version": "3.10.16" }, "toc": { "base_numbering": 1, From dfcf353da1591277d847aa371ed4677f6f47572b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Schl=C3=B6sser?= Date: Tue, 25 Feb 2025 16:11:04 +0100 Subject: [PATCH 22/43] Pre-final Jupyter Notebook for LoMa Workshop Part 2 --- examples/Workshop_LoMa.ipynb | 460 +++++++++++++++++++++++++++-------- 1 file changed, 354 insertions(+), 106 deletions(-) diff --git a/examples/Workshop_LoMa.ipynb b/examples/Workshop_LoMa.ipynb index 0d303da08..6333d8bc0 100644 --- a/examples/Workshop_LoMa.ipynb +++ b/examples/Workshop_LoMa.ipynb @@ -52,7 +52,8 @@ "\n", "from edisgo import EDisGo\n", "from edisgo.io.db import engine\n", - "from edisgo.tools.logger import setup_logger" + "from edisgo.tools.logger import setup_logger\n", + "from edisgo.flex_opt.battery_storage_operation import apply_reference_operation" ] }, { @@ -62,7 +63,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Nur wegen der Übersicht. Normalerweise nicht zu empfehlen\n", + "# to make the notebook clearer. not recommendable\n", "import warnings\n", "\n", "warnings.filterwarnings(\"ignore\")" @@ -180,6 +181,7 @@ "metadata": {}, "outputs": [], "source": [ + "# adjust node sizes to make plot clearer\n", "sizes_dict = {\n", " \"BranchTee\": 10000,\n", " \"GeneratorFluctuating\": 100000,\n", @@ -228,6 +230,7 @@ "metadata": {}, "outputs": [], "source": [ + "# generator types\n", "edisgo.topology.generators_df[[\"p_nom\", \"type\"]].groupby(\"type\").sum()" ] }, @@ -238,15 +241,19 @@ "metadata": {}, "outputs": [], "source": [ + "# load types\n", "edisgo.topology.loads_df[[\"p_set\", \"type\"]].groupby(\"type\").sum()" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "id": "22", "metadata": {}, + "outputs": [], "source": [ - "Number of LV grids in the MV grid" + "# load sectors\n", + "edisgo.topology.loads_df[[\"p_set\", \"sector\"]].groupby(\"sector\").sum()" ] }, { @@ -256,6 +263,7 @@ "metadata": {}, "outputs": [], "source": [ + "# amount of lv grids inside the mv grid\n", "len(list(edisgo.topology.mv_grid.lv_grids))" ] }, @@ -274,30 +282,24 @@ "metadata": {}, "outputs": [], "source": [ - "len(edisgo.topology.lines_df.index)" - ] - }, - { - "cell_type": "markdown", - "id": "26", - "metadata": {}, - "source": [ - "Number of lines in one of the low voltage grids." + "# overall amount of lines\n", + "len(edisgo.topology.lines_df)" ] }, { "cell_type": "code", "execution_count": null, - "id": "27", + "id": "26", "metadata": {}, "outputs": [], "source": [ + "# amount of lines in one of the lv grids\n", "len(edisgo.topology.grids[5].lines_df.index)" ] }, { "cell_type": "markdown", - "id": "28", + "id": "27", "metadata": {}, "source": [ "### Basic components addition and removal" @@ -305,18 +307,18 @@ }, { "cell_type": "markdown", - "id": "29", + "id": "28", "metadata": {}, "source": [ "To see how a loaded network can be adapted later on, we add a solar plant to a random bus.\n", "\n", - "Components can also be added according to their geolocation with the function integrate_component_based_on_geolocation()." + "Components can also be added according to their geolocation with the function ```integrate_component_based_on_geolocation()```." ] }, { "cell_type": "code", "execution_count": null, - "id": "30", + "id": "29", "metadata": {}, "outputs": [], "source": [ @@ -325,7 +327,7 @@ }, { "cell_type": "markdown", - "id": "31", + "id": "30", "metadata": {}, "source": [ "Add a generator with the function ```add_component()``` or ```add_generator()```. " @@ -334,10 +336,11 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "31", "metadata": {}, "outputs": [], "source": [ + "# determine a random bus\n", "rng = default_rng(1)\n", "rnd_bus = rng.choice(edisgo.topology.buses_df.index, size=1)[0]\n", "generator_type = \"solar\"\n", @@ -350,7 +353,7 @@ { "cell_type": "code", "execution_count": null, - "id": "33", + "id": "32", "metadata": {}, "outputs": [], "source": [ @@ -359,32 +362,39 @@ }, { "cell_type": "markdown", - "id": "34", + "id": "33", "metadata": {}, "source": [ - "We can also add a heat pump:" + "Single components can be removed with ```remove_component()```" ] }, { "cell_type": "code", "execution_count": null, - "id": "35", + "id": "34", "metadata": {}, "outputs": [], "source": [ - "edisgo.topology.loads_df" + "edisgo.remove_component(comp_type=\"generator\", comp_name=new_generator)" ] }, { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "35", "metadata": {}, "outputs": [], "source": [ - "new_load = edisgo.add_component(\n", - " comp_type=\"load\", p_set=0.01, bus=rnd_bus, type=\"heat_pump\"\n", - ")" + "edisgo.topology.generators_df" + ] + }, + { + "cell_type": "markdown", + "id": "36", + "metadata": {}, + "source": [ + "### Task: \n", + "Add and remobve a 'heat_pump' with the function ```add_component()``` and the function ```remove_component()```." ] }, { @@ -398,11 +408,15 @@ ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "id": "38", "metadata": {}, + "outputs": [], "source": [ - "Single components can be removed with ```remove_component()```" + "new_load = edisgo.add_component(\n", + " comp_type=\"load\", p_set=0.01, bus=rnd_bus, type=\"heat_pump\"\n", + ")" ] }, { @@ -412,8 +426,7 @@ "metadata": {}, "outputs": [], "source": [ - "edisgo.remove_component(comp_type=\"generator\", comp_name=new_generator)\n", - "edisgo.remove_component(comp_type=\"load\", comp_name=new_load)" + "edisgo.topology.loads_df" ] }, { @@ -423,7 +436,7 @@ "metadata": {}, "outputs": [], "source": [ - "edisgo.topology.generators_df" + "edisgo.remove_component(comp_type=\"load\", comp_name=new_load)" ] }, { @@ -470,7 +483,7 @@ "outputs": [], "source": [ "# copy the edisgo object for later comparisons\n", - "edisgo_orig = deepcopy(edisgo)" + "edisgo_orig = edisgo.copy()" ] }, { @@ -479,9 +492,21 @@ "id": "46", "metadata": {}, "outputs": [], + "source": [ + "# set timeindex to ensure that correct time series for COP and heat pump heat demand are downloaded\n", + "timeindex = pd.date_range(f\"1/1/{2011} 12:00\", periods=4, freq=\"H\")\n", + "edisgo.set_timeindex(timeindex=timeindex)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47", + "metadata": {}, + "outputs": [], "source": [ "# Retry if running into \"Connection reset by peer\" error\n", - "edisgo = deepcopy(edisgo_orig)\n", + "# edisgo = deepcopy(edisgo_orig)\n", "\n", "edisgo.import_generators(generator_scenario=scenario)\n", "edisgo.import_home_batteries(scenario=scenario)\n", @@ -491,80 +516,118 @@ { "cell_type": "code", "execution_count": null, - "id": "47", + "id": "48", "metadata": {}, "outputs": [], "source": [ - "# This takes too long for the workshop, but needs to be mentioned\n", + "# This takes too long for the workshop\n", "# edisgo_obj.import_dsm(scenario=scenario)\n", "# edisgo_obj.import_electromobility(\n", "# data_source=\"oedb\", scenario=scenario\n", - "#)" + "# )" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "48", + "cell_type": "markdown", + "id": "49", "metadata": {}, - "outputs": [], "source": [ - "edisgo.topology.generators_df[[\"p_nom\", \"type\"]].groupby(\"type\").sum()" + "## Task:\n", + "Determine the differnet generator types that were installed before and that are installed in the grid now." ] }, { "cell_type": "code", "execution_count": null, - "id": "49", + "id": "50", "metadata": {}, "outputs": [], "source": [ - "edisgo_orig.topology.generators_df[[\"p_nom\", \"type\"]].groupby(\"type\").sum()" + "set(edisgo.topology.generators_df[\"type\"])" ] }, { "cell_type": "code", "execution_count": null, - "id": "50", + "id": "51", "metadata": {}, "outputs": [], "source": [ - "edisgo.topology.loads_df[[\"p_set\", \"type\"]].groupby(\"type\").sum()" + "set(edisgo_orig.topology.generators_df[\"type\"])" + ] + }, + { + "cell_type": "markdown", + "id": "52", + "metadata": {}, + "source": [ + "## Task:\n", + "Determine the added solar energy power." ] }, { "cell_type": "code", "execution_count": null, - "id": "51", + "id": "53", "metadata": {}, "outputs": [], "source": [ - "edisgo_orig.topology.loads_df[[\"p_set\", \"type\"]].groupby(\"type\").sum()" + "solar_power_new = edisgo.topology.generators_df[\n", + " edisgo.topology.generators_df[\"type\"] == \"solar\"\n", + "][\"p_nom\"].sum()\n", + "solar_power_old = edisgo_orig.topology.generators_df[\n", + " edisgo_orig.topology.generators_df[\"type\"] == \"solar\"\n", + "][\"p_nom\"].sum()\n", + "\n", + "solar_power_new - solar_power_old" + ] + }, + { + "cell_type": "markdown", + "id": "54", + "metadata": {}, + "source": [ + "## Task:\n", + "Determine the amount of storage units added to the grid with a nominal power (p_nom) larger than 0.01." ] }, { "cell_type": "code", "execution_count": null, - "id": "52", + "id": "55", "metadata": {}, "outputs": [], "source": [ - "edisgo.topology.storage_units_df[\"p_nom\"].sum()" + "sum(edisgo.topology.storage_units_df[\"p_nom\"] > 0.01)" + ] + }, + { + "cell_type": "markdown", + "id": "56", + "metadata": {}, + "source": [ + "## Task:\n", + "Determine the amount heat pumps connected to the MV level." ] }, { "cell_type": "code", "execution_count": null, - "id": "53", + "id": "57", "metadata": {}, "outputs": [], "source": [ - "edisgo_orig.topology.storage_units_df[\"p_nom\"].sum()" + "len(\n", + " edisgo.topology.loads_df[\n", + " (edisgo.topology.loads_df[\"type\"] == \"heat_pump\")\n", + " & (edisgo.topology.loads_df[\"voltage_level\"] == \"mv\")\n", + " ]\n", + ")" ] }, { "cell_type": "markdown", - "id": "54", + "id": "58", "metadata": {}, "source": [ "## 2 Worst Case Time Series Creation" @@ -572,7 +635,7 @@ }, { "cell_type": "markdown", - "id": "55", + "id": "59", "metadata": {}, "source": [ "Create timeseries for the four worst cases MV load case, LV load case, MV feed-in case, LV feed-in case with the function set_time_series_worst_case_analysis().\n", @@ -601,7 +664,7 @@ { "cell_type": "code", "execution_count": null, - "id": "56", + "id": "60", "metadata": {}, "outputs": [], "source": [ @@ -610,7 +673,7 @@ }, { "cell_type": "markdown", - "id": "57", + "id": "61", "metadata": {}, "source": [ "The function creates time series for four time steps since both worst cases are defined seperately for the LV and the MV grid with individual simultanerity factors." @@ -619,7 +682,7 @@ { "cell_type": "code", "execution_count": null, - "id": "58", + "id": "62", "metadata": {}, "outputs": [], "source": [ @@ -629,10 +692,11 @@ { "cell_type": "code", "execution_count": null, - "id": "59", + "id": "63", "metadata": {}, "outputs": [], "source": [ + "# indexing with worst case timeindex\n", "edisgo.timeseries.loads_active_power.loc[\n", " edisgo.timeseries.timeindex_worst_cases[\"load_case_mv\"]\n", "]" @@ -640,7 +704,7 @@ }, { "cell_type": "markdown", - "id": "60", + "id": "64", "metadata": {}, "source": [ "## 3 Grid Investigation" @@ -648,34 +712,35 @@ }, { "cell_type": "markdown", - "id": "61", + "id": "65", "metadata": {}, "source": [ - "Execute a power flow analysis to determine line overloads and voltage deviations for the MV load case timeseries with the function analyze():" + "Execute a power flow analysis to determine line overloads and voltage deviations for the MV load case timeseries with the function ```analyze()```:" ] }, { "cell_type": "code", "execution_count": null, - "id": "62", + "id": "66", "metadata": {}, "outputs": [], "source": [ + "# power flow analysis\n", "edisgo.analyze(timesteps=edisgo.timeseries.timeindex_worst_cases[\"load_case_mv\"])" ] }, { "cell_type": "markdown", - "id": "63", + "id": "67", "metadata": {}, "source": [ - "A geoplot wtih the bus and line colrs based on the voltage deviations and line loadings repectively can be created with ```plot_mv_line_loading()```." + "A geoplot with the bus and line colors based on the voltage deviations and line loadings repectively can be created with ```plot_mv_line_loading()```." ] }, { "cell_type": "code", "execution_count": null, - "id": "64", + "id": "68", "metadata": {}, "outputs": [], "source": [ @@ -687,7 +752,7 @@ }, { "cell_type": "markdown", - "id": "65", + "id": "69", "metadata": {}, "source": [ "For a better overview of the voltage deviations and line loads in the entire grid, edisgo provides histrogram plots." @@ -696,7 +761,7 @@ { "cell_type": "code", "execution_count": null, - "id": "66", + "id": "70", "metadata": {}, "outputs": [], "source": [ @@ -706,7 +771,7 @@ { "cell_type": "code", "execution_count": null, - "id": "67", + "id": "71", "metadata": {}, "outputs": [], "source": [ @@ -715,34 +780,29 @@ }, { "cell_type": "markdown", - "id": "68", + "id": "72", "metadata": {}, "source": [ "## 4 Results" ] }, - { - "cell_type": "markdown", - "id": "69", - "metadata": {}, - "source": [ - "Now we reinforce the fully equipped grid. For a shorter runtime, only the MV grid is considered by setting ```mode = \"mv\"```." - ] - }, { "cell_type": "code", "execution_count": null, - "id": "70", + "id": "73", "metadata": {}, "outputs": [], "source": [ - "edisgo.reinforce()" + "# Reinforce the grid\n", + "# mode = \"mvlv\" for a shorter run time. However, grid reinforcement should generally be conducted in mode=\"lv\" (default)\n", + "# since the majority of the reinforcement costs is caused in the lv grid part\n", + "edisgo.reinforce(mode=\"mvlv\")" ] }, { "cell_type": "code", "execution_count": null, - "id": "71", + "id": "74", "metadata": {}, "outputs": [], "source": [ @@ -755,17 +815,18 @@ { "cell_type": "code", "execution_count": null, - "id": "72", + "id": "75", "metadata": {}, "outputs": [], "source": [ - "# edisgo.analyze() " + "# power flow analysis to retrieve all bus voltages and line flows\n", + "edisgo.analyze(timesteps=edisgo.timeseries.timeindex_worst_cases[\"load_case_mv\"])" ] }, { "cell_type": "code", "execution_count": null, - "id": "73", + "id": "76", "metadata": {}, "outputs": [], "source": [ @@ -775,7 +836,7 @@ { "cell_type": "code", "execution_count": null, - "id": "74", + "id": "77", "metadata": {}, "outputs": [], "source": [ @@ -784,16 +845,16 @@ }, { "cell_type": "markdown", - "id": "75", + "id": "78", "metadata": {}, "source": [ - "The module ```results```holds the outpts of the reinforcement" + "The module ```results```holds the outputs of the reinforcement" ] }, { "cell_type": "code", "execution_count": null, - "id": "76", + "id": "79", "metadata": {}, "outputs": [], "source": [ @@ -801,53 +862,240 @@ "edisgo.results.equipment_changes[len(edisgo_orig.results.equipment_changes) :].head()" ] }, + { + "cell_type": "markdown", + "id": "80", + "metadata": {}, + "source": [ + "## Task:\n", + "Determine the total costs for the grid reinforcement. The costs for each added component are stored in the data frame ```edisgo.results.grid_expansion_costs```." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "77", + "id": "81", "metadata": {}, "outputs": [], "source": [ - "edisgo.results.grid_expansion_costs[\n", - " len(edisgo_orig.results.grid_expansion_costs) :\n", - "].head()" + "edisgo.results.grid_expansion_costs[len(edisgo_orig.results.grid_expansion_costs) :][\n", + " \"total_costs\"\n", + "].sum()" ] }, { "cell_type": "markdown", - "id": "78", + "id": "82", "metadata": {}, "source": [ - "### TODO: Visualisierung der Ergebnisse" + "## 5 Additional Time Series\n", + "\n", + "Besides setting worst case scenarios and the corresponding time series, component time series can also be set with the function ```predefined()```. Either standard profiles for different component types are loaded from a data base or type- (for generators) and sectorwise (for loads) time series can be determined manually and passed to the function. \n", + "\n", + "The function ```set_time_series_manual()``` can be used to set individual time series for components. " ] }, { "cell_type": "code", "execution_count": null, - "id": "79", + "id": "83", "metadata": {}, "outputs": [], "source": [ - "# edisgo_costs = deepcopy(edisgo)\n", - "# edisgo_costs.results.grid_expansion_costs = edisgo.results.grid_expansion_costs[len(edisgo_orig.results.grid_expansion_costs) :]\n", - "# edisgo_costs.plot_mv_grid_expansion_costs()" + "# determine interval time series are set for\n", + "# timeindex has to be set again to desired time interval because it was overwritten by set_time_series_worst_case()\n", + "timeindex = pd.date_range(f\"1/1/{2011} 12:00\", periods=4, freq=\"H\")\n", + "edisgo.set_timeindex(timeindex=timeindex)" ] }, { - "cell_type": "markdown", - "id": "80", + "cell_type": "code", + "execution_count": null, + "id": "84", "metadata": {}, + "outputs": [], "source": [ - "## 5 Additional Time Series\n", + "# check which load sectors are included in the Husum grid\n", + "set(edisgo.topology.loads_df[\"sector\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "85", + "metadata": {}, + "outputs": [], + "source": [ + "# constant load for all time steps for all load types\n", + "timeseries_load = pd.DataFrame(\n", + " {\n", + " \"industrial\": [0.0001] * len(timeindex),\n", + " \"cts\": [0.0002] * len(timeindex),\n", + " \"residential\": [0.0002] * len(timeindex),\n", + " \"district_heating_resistive_heater\": [0.0002] * len(timeindex),\n", + " \"individual_heating\": [0.0002] * len(timeindex),\n", + " },\n", + " index=timeindex,\n", + ")\n", "\n", - "* show time resolution" + "# annual_consumption of loads is not set in Husum data set\n", + "edisgo.topology.loads_df[\"annual_consumption\"] = 700 * edisgo.topology.loads_df[\"p_set\"]" ] }, { - "cell_type": "markdown", - "id": "81", + "cell_type": "code", + "execution_count": null, + "id": "86", "metadata": {}, - "source": [] + "outputs": [], + "source": [ + "# check which generator types are included into the grid\n", + "set(edisgo.topology.generators_df[\"type\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "87", + "metadata": {}, + "outputs": [], + "source": [ + "# constant feed-in for dispatchable generators\n", + "timeseries_generation_dispatchable = pd.DataFrame(\n", + " {\n", + " \"biomass\": [1] * len(timeindex),\n", + " \"gas\": [1] * len(timeindex),\n", + " \"other\": [1] * len(timeindex),\n", + " },\n", + " index=timeindex,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "88", + "metadata": {}, + "outputs": [], + "source": [ + "# determine fluctuating generators, for which generator-type time series are loaded from a data base\n", + "fluctuating_generators = edisgo.topology.generators_df[\n", + " edisgo.topology.generators_df[\"type\"].isin([\"solar\", \"wind\"])\n", + "].index" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "89", + "metadata": {}, + "outputs": [], + "source": [ + "# set active power time series for loads and generators\n", + "edisgo.set_time_series_active_power_predefined(\n", + " fluctuating_generators=fluctuating_generators,\n", + " fluctuating_generators_ts=\"oedb\",\n", + " scenario=scenario,\n", + " timeindex=edisgo.timeseries.timeindex,\n", + " conventional_loads_ts=timeseries_load,\n", + " dispatchable_generators_ts=timeseries_generation_dispatchable,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.timeseries.generators_active_power" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "91", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.timeseries.loads_active_power" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "92", + "metadata": {}, + "outputs": [], + "source": [ + "# set heat pump time series \n", + "# set_time_series_active_power_predefined does not consider heat demand\n", + "edisgo.apply_heat_pump_operating_strategy()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "93", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.timeseries.loads_active_power" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94", + "metadata": {}, + "outputs": [], + "source": [ + "# set battery storage time series (not inluded in set_time_series_active_power_predefined())\n", + "apply_reference_operation(edisgo)\n", + "# returns soe" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.timeseries.storage_units_active_power" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "96", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.timeseries.generators_reactive_power" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "97", + "metadata": {}, + "outputs": [], + "source": [ + "# set reactive power time series\n", + "edisgo.set_time_series_reactive_power_control()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "98", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.timeseries.generators_reactive_power" + ] } ], "metadata": { From 3eab63131abb2e6903c3c02c6c49c9dc4bd2d23e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Schl=C3=B6sser?= Date: Tue, 25 Feb 2025 16:13:33 +0100 Subject: [PATCH 23/43] Workshop date modified --- examples/Workshop_LoMa.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/Workshop_LoMa.ipynb b/examples/Workshop_LoMa.ipynb index 6333d8bc0..cd817caf4 100644 --- a/examples/Workshop_LoMa.ipynb +++ b/examples/Workshop_LoMa.ipynb @@ -5,7 +5,7 @@ "id": "0", "metadata": {}, "source": [ - "# LoMa EDisGo-Workshop 13.2.2025" + "# LoMa EDisGo-Workshop 27.2.2025" ] }, { From f32abbccf07f48010be81b9071af0f62b20eb086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Schl=C3=B6sser?= Date: Tue, 25 Feb 2025 16:15:34 +0100 Subject: [PATCH 24/43] Small changes --- examples/Workshop_LoMa.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/Workshop_LoMa.ipynb b/examples/Workshop_LoMa.ipynb index cd817caf4..41827ecd2 100644 --- a/examples/Workshop_LoMa.ipynb +++ b/examples/Workshop_LoMa.ipynb @@ -795,7 +795,7 @@ "source": [ "# Reinforce the grid\n", "# mode = \"mvlv\" for a shorter run time. However, grid reinforcement should generally be conducted in mode=\"lv\" (default)\n", - "# since the majority of the reinforcement costs is caused in the lv grid part\n", + "# since the majority of the reinforcement costs is caused in the lv grid part, especially for high load grids (much EV charging demand and low PV capacity)\n", "edisgo.reinforce(mode=\"mvlv\")" ] }, From 3da99f97ae5233067fadbc5afb4fd259ed11093e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Schl=C3=B6sser?= Date: Wed, 26 Feb 2025 14:56:06 +0100 Subject: [PATCH 25/43] Workshop notebooks with and w/o solutions for 27th Feb --- examples/Workshop_LoMa.ipynb | 178 ++-- examples/Workshop_LoMa_solutions.ipynb | 1132 ++++++++++++++++++++++++ 2 files changed, 1211 insertions(+), 99 deletions(-) create mode 100644 examples/Workshop_LoMa_solutions.ipynb diff --git a/examples/Workshop_LoMa.ipynb b/examples/Workshop_LoMa.ipynb index 41827ecd2..f73cdf2e2 100644 --- a/examples/Workshop_LoMa.ipynb +++ b/examples/Workshop_LoMa.ipynb @@ -53,7 +53,8 @@ "from edisgo import EDisGo\n", "from edisgo.io.db import engine\n", "from edisgo.tools.logger import setup_logger\n", - "from edisgo.flex_opt.battery_storage_operation import apply_reference_operation" + "from edisgo.flex_opt.battery_storage_operation import apply_reference_operation\n", + "from edisgo.network.results import Results" ] }, { @@ -394,7 +395,7 @@ "metadata": {}, "source": [ "### Task: \n", - "Add and remobve a 'heat_pump' with the function ```add_component()``` and the function ```remove_component()```." + "Add and remove a 'heat_pump' with the function ```add_component()``` and the function ```remove_component()```." ] }, { @@ -413,11 +414,7 @@ "id": "38", "metadata": {}, "outputs": [], - "source": [ - "new_load = edisgo.add_component(\n", - " comp_type=\"load\", p_set=0.01, bus=rnd_bus, type=\"heat_pump\"\n", - ")" - ] + "source": [] }, { "cell_type": "code", @@ -435,9 +432,7 @@ "id": "40", "metadata": {}, "outputs": [], - "source": [ - "edisgo.remove_component(comp_type=\"load\", comp_name=new_load)" - ] + "source": [] }, { "cell_type": "code", @@ -492,6 +487,17 @@ "id": "46", "metadata": {}, "outputs": [], + "source": [ + "# clear initial reinfocement results from results module\n", + "edisgo.results = Results(edisgo)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47", + "metadata": {}, + "outputs": [], "source": [ "# set timeindex to ensure that correct time series for COP and heat pump heat demand are downloaded\n", "timeindex = pd.date_range(f\"1/1/{2011} 12:00\", periods=4, freq=\"H\")\n", @@ -501,12 +507,11 @@ { "cell_type": "code", "execution_count": null, - "id": "47", + "id": "48", "metadata": {}, "outputs": [], "source": [ "# Retry if running into \"Connection reset by peer\" error\n", - "# edisgo = deepcopy(edisgo_orig)\n", "\n", "edisgo.import_generators(generator_scenario=scenario)\n", "edisgo.import_home_batteries(scenario=scenario)\n", @@ -516,7 +521,7 @@ { "cell_type": "code", "execution_count": null, - "id": "48", + "id": "49", "metadata": {}, "outputs": [], "source": [ @@ -529,7 +534,7 @@ }, { "cell_type": "markdown", - "id": "49", + "id": "50", "metadata": {}, "source": [ "## Task:\n", @@ -539,26 +544,22 @@ { "cell_type": "code", "execution_count": null, - "id": "50", + "id": "51", "metadata": {}, "outputs": [], - "source": [ - "set(edisgo.topology.generators_df[\"type\"])" - ] + "source": [] }, { "cell_type": "code", "execution_count": null, - "id": "51", + "id": "52", "metadata": {}, "outputs": [], - "source": [ - "set(edisgo_orig.topology.generators_df[\"type\"])" - ] + "source": [] }, { "cell_type": "markdown", - "id": "52", + "id": "53", "metadata": {}, "source": [ "## Task:\n", @@ -568,23 +569,14 @@ { "cell_type": "code", "execution_count": null, - "id": "53", + "id": "54", "metadata": {}, "outputs": [], - "source": [ - "solar_power_new = edisgo.topology.generators_df[\n", - " edisgo.topology.generators_df[\"type\"] == \"solar\"\n", - "][\"p_nom\"].sum()\n", - "solar_power_old = edisgo_orig.topology.generators_df[\n", - " edisgo_orig.topology.generators_df[\"type\"] == \"solar\"\n", - "][\"p_nom\"].sum()\n", - "\n", - "solar_power_new - solar_power_old" - ] + "source": [] }, { "cell_type": "markdown", - "id": "54", + "id": "55", "metadata": {}, "source": [ "## Task:\n", @@ -594,40 +586,31 @@ { "cell_type": "code", "execution_count": null, - "id": "55", + "id": "56", "metadata": {}, "outputs": [], - "source": [ - "sum(edisgo.topology.storage_units_df[\"p_nom\"] > 0.01)" - ] + "source": [] }, { "cell_type": "markdown", - "id": "56", + "id": "57", "metadata": {}, "source": [ "## Task:\n", - "Determine the amount heat pumps connected to the MV level." + "Determine the buses of the heat pumps whose application ('sector') is not inidividual_heating." ] }, { "cell_type": "code", "execution_count": null, - "id": "57", + "id": "58", "metadata": {}, "outputs": [], - "source": [ - "len(\n", - " edisgo.topology.loads_df[\n", - " (edisgo.topology.loads_df[\"type\"] == \"heat_pump\")\n", - " & (edisgo.topology.loads_df[\"voltage_level\"] == \"mv\")\n", - " ]\n", - ")" - ] + "source": [] }, { "cell_type": "markdown", - "id": "58", + "id": "59", "metadata": {}, "source": [ "## 2 Worst Case Time Series Creation" @@ -635,7 +618,7 @@ }, { "cell_type": "markdown", - "id": "59", + "id": "60", "metadata": {}, "source": [ "Create timeseries for the four worst cases MV load case, LV load case, MV feed-in case, LV feed-in case with the function set_time_series_worst_case_analysis().\n", @@ -664,7 +647,7 @@ { "cell_type": "code", "execution_count": null, - "id": "60", + "id": "61", "metadata": {}, "outputs": [], "source": [ @@ -673,7 +656,7 @@ }, { "cell_type": "markdown", - "id": "61", + "id": "62", "metadata": {}, "source": [ "The function creates time series for four time steps since both worst cases are defined seperately for the LV and the MV grid with individual simultanerity factors." @@ -682,7 +665,7 @@ { "cell_type": "code", "execution_count": null, - "id": "62", + "id": "63", "metadata": {}, "outputs": [], "source": [ @@ -692,7 +675,7 @@ { "cell_type": "code", "execution_count": null, - "id": "63", + "id": "64", "metadata": {}, "outputs": [], "source": [ @@ -704,7 +687,7 @@ }, { "cell_type": "markdown", - "id": "64", + "id": "65", "metadata": {}, "source": [ "## 3 Grid Investigation" @@ -712,7 +695,7 @@ }, { "cell_type": "markdown", - "id": "65", + "id": "66", "metadata": {}, "source": [ "Execute a power flow analysis to determine line overloads and voltage deviations for the MV load case timeseries with the function ```analyze()```:" @@ -721,7 +704,7 @@ { "cell_type": "code", "execution_count": null, - "id": "66", + "id": "67", "metadata": {}, "outputs": [], "source": [ @@ -731,7 +714,7 @@ }, { "cell_type": "markdown", - "id": "67", + "id": "68", "metadata": {}, "source": [ "A geoplot with the bus and line colors based on the voltage deviations and line loadings repectively can be created with ```plot_mv_line_loading()```." @@ -740,7 +723,7 @@ { "cell_type": "code", "execution_count": null, - "id": "68", + "id": "69", "metadata": {}, "outputs": [], "source": [ @@ -752,7 +735,7 @@ }, { "cell_type": "markdown", - "id": "69", + "id": "70", "metadata": {}, "source": [ "For a better overview of the voltage deviations and line loads in the entire grid, edisgo provides histrogram plots." @@ -761,7 +744,7 @@ { "cell_type": "code", "execution_count": null, - "id": "70", + "id": "71", "metadata": {}, "outputs": [], "source": [ @@ -771,7 +754,7 @@ { "cell_type": "code", "execution_count": null, - "id": "71", + "id": "72", "metadata": {}, "outputs": [], "source": [ @@ -780,7 +763,7 @@ }, { "cell_type": "markdown", - "id": "72", + "id": "73", "metadata": {}, "source": [ "## 4 Results" @@ -789,20 +772,22 @@ { "cell_type": "code", "execution_count": null, - "id": "73", + "id": "74", "metadata": {}, "outputs": [], "source": [ "# Reinforce the grid\n", "# mode = \"mvlv\" for a shorter run time. However, grid reinforcement should generally be conducted in mode=\"lv\" (default)\n", "# since the majority of the reinforcement costs is caused in the lv grid part, especially for high load grids (much EV charging demand and low PV capacity)\n", + "# The lv mode is currently not applicable for the Husum grid. The newly added generators are concentrated in one LV grid. The reinforcement\n", + "# very long or cannot be resolved. This issue will be fixed soon. A possible workarounf for running the reinfocrement anyway is to remove the generators for the overloaded LV grid.\n", "edisgo.reinforce(mode=\"mvlv\")" ] }, { "cell_type": "code", "execution_count": null, - "id": "74", + "id": "75", "metadata": {}, "outputs": [], "source": [ @@ -815,7 +800,7 @@ { "cell_type": "code", "execution_count": null, - "id": "75", + "id": "76", "metadata": {}, "outputs": [], "source": [ @@ -826,7 +811,7 @@ { "cell_type": "code", "execution_count": null, - "id": "76", + "id": "77", "metadata": {}, "outputs": [], "source": [ @@ -836,7 +821,7 @@ { "cell_type": "code", "execution_count": null, - "id": "77", + "id": "78", "metadata": {}, "outputs": [], "source": [ @@ -845,7 +830,7 @@ }, { "cell_type": "markdown", - "id": "78", + "id": "79", "metadata": {}, "source": [ "The module ```results```holds the outputs of the reinforcement" @@ -854,17 +839,16 @@ { "cell_type": "code", "execution_count": null, - "id": "79", + "id": "80", "metadata": {}, "outputs": [], "source": [ - "# The equipment changes of the reinforcement after the grid setup have to be dropped\n", - "edisgo.results.equipment_changes[len(edisgo_orig.results.equipment_changes) :].head()" + "edisgo_orig.results.equipment_changes" ] }, { "cell_type": "markdown", - "id": "80", + "id": "81", "metadata": {}, "source": [ "## Task:\n", @@ -874,18 +858,14 @@ { "cell_type": "code", "execution_count": null, - "id": "81", + "id": "82", "metadata": {}, "outputs": [], - "source": [ - "edisgo.results.grid_expansion_costs[len(edisgo_orig.results.grid_expansion_costs) :][\n", - " \"total_costs\"\n", - "].sum()" - ] + "source": [] }, { "cell_type": "markdown", - "id": "82", + "id": "83", "metadata": {}, "source": [ "## 5 Additional Time Series\n", @@ -898,7 +878,7 @@ { "cell_type": "code", "execution_count": null, - "id": "83", + "id": "84", "metadata": {}, "outputs": [], "source": [ @@ -911,7 +891,7 @@ { "cell_type": "code", "execution_count": null, - "id": "84", + "id": "85", "metadata": {}, "outputs": [], "source": [ @@ -922,7 +902,7 @@ { "cell_type": "code", "execution_count": null, - "id": "85", + "id": "86", "metadata": {}, "outputs": [], "source": [ @@ -945,7 +925,7 @@ { "cell_type": "code", "execution_count": null, - "id": "86", + "id": "87", "metadata": {}, "outputs": [], "source": [ @@ -956,7 +936,7 @@ { "cell_type": "code", "execution_count": null, - "id": "87", + "id": "88", "metadata": {}, "outputs": [], "source": [ @@ -974,7 +954,7 @@ { "cell_type": "code", "execution_count": null, - "id": "88", + "id": "89", "metadata": {}, "outputs": [], "source": [ @@ -987,7 +967,7 @@ { "cell_type": "code", "execution_count": null, - "id": "89", + "id": "90", "metadata": {}, "outputs": [], "source": [ @@ -1005,7 +985,7 @@ { "cell_type": "code", "execution_count": null, - "id": "90", + "id": "91", "metadata": {}, "outputs": [], "source": [ @@ -1015,7 +995,7 @@ { "cell_type": "code", "execution_count": null, - "id": "91", + "id": "92", "metadata": {}, "outputs": [], "source": [ @@ -1025,11 +1005,11 @@ { "cell_type": "code", "execution_count": null, - "id": "92", + "id": "93", "metadata": {}, "outputs": [], "source": [ - "# set heat pump time series \n", + "# set heat pump time series\n", "# set_time_series_active_power_predefined does not consider heat demand\n", "edisgo.apply_heat_pump_operating_strategy()" ] @@ -1037,7 +1017,7 @@ { "cell_type": "code", "execution_count": null, - "id": "93", + "id": "94", "metadata": {}, "outputs": [], "source": [ @@ -1047,7 +1027,7 @@ { "cell_type": "code", "execution_count": null, - "id": "94", + "id": "95", "metadata": {}, "outputs": [], "source": [ @@ -1059,7 +1039,7 @@ { "cell_type": "code", "execution_count": null, - "id": "95", + "id": "96", "metadata": {}, "outputs": [], "source": [ @@ -1069,7 +1049,7 @@ { "cell_type": "code", "execution_count": null, - "id": "96", + "id": "97", "metadata": {}, "outputs": [], "source": [ @@ -1079,7 +1059,7 @@ { "cell_type": "code", "execution_count": null, - "id": "97", + "id": "98", "metadata": {}, "outputs": [], "source": [ @@ -1090,7 +1070,7 @@ { "cell_type": "code", "execution_count": null, - "id": "98", + "id": "99", "metadata": {}, "outputs": [], "source": [ diff --git a/examples/Workshop_LoMa_solutions.ipynb b/examples/Workshop_LoMa_solutions.ipynb new file mode 100644 index 000000000..8ccadb9fc --- /dev/null +++ b/examples/Workshop_LoMa_solutions.ipynb @@ -0,0 +1,1132 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# LoMa EDisGo-Workshop 27.2.2025" + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "Contents:\n", + "1. Topology Setup\n", + "2. Worst Case Time Series Creation\n", + "3. Grid Investigation\n", + "4. Results\n", + "5. Additional Time Series\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext jupyter_black" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import requests\n", + "import sys\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import networkx as nx\n", + "import pandas as pd\n", + "\n", + "from copy import deepcopy\n", + "from numpy.random import default_rng\n", + "from pathlib import Path\n", + "\n", + "from edisgo import EDisGo\n", + "from edisgo.io.db import engine\n", + "from edisgo.tools.logger import setup_logger\n", + "from edisgo.flex_opt.battery_storage_operation import apply_reference_operation\n", + "from edisgo.network.results import Results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "# to make the notebook clearer. not recommendable\n", + "import warnings\n", + "\n", + "warnings.filterwarnings(\"ignore\")" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "## 1 Topology Setup" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "In this section we load all components into a newly created edisgo object. This includes the lines, buses, transformers, switches, generators, loads, heat pumps and battery storages." + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "### Standard components" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "Set up a new edisgo object:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "conf_path = Path.home() / \"Downloads\" / \"egon-data.configuration.yaml\"\n", + "db_engine = engine(path=conf_path, ssh=True)\n", + "ding0_grid = Path.home() / \".edisgo\" / \"husum_grids\" / \"35725\"\n", + "\n", + "edisgo = EDisGo(ding0_grid=ding0_grid, legacy_ding0_grids=False, engine=db_engine)" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "The ding0 grids are not up to date and their capacity is not sufficient for the connected loads and generators. To update the imported grids they need to be extended first with the function ```reinforce()```." + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "Grids are reinforced for their worst case scenarios. The corresponding time series are created with ```set_time_series_worst_case_analysis()```. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.set_time_series_worst_case_analysis()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.reinforce()" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "### Plot grid topology (MV)" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "The topology can be visualized with the ```plot_mv_grid_topology()```. For ```technologies=True``` the buses sizes and colors are determined to the type and size of the technologies connected to it. \n", + "\n", + "- red: nodes with substation secondary side\n", + "- light blue: nodes distribution substations's primary side\n", + "- green: nodes with fluctuating generators\n", + "- black: nodes with conventional generators\n", + "- grey: disconnecting points\n", + "- dark blue: branch trees" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "# adjust node sizes to make plot clearer\n", + "sizes_dict = {\n", + " \"BranchTee\": 10000,\n", + " \"GeneratorFluctuating\": 100000,\n", + " \"Generator\": 100000,\n", + " \"Load\": 100000,\n", + " \"LVStation\": 50000,\n", + " \"MVStation\": 120000,\n", + " \"Storage\": 100000,\n", + " \"DisconnectingPoint\": 75000,\n", + " \"else\": 200000,\n", + "}\n", + "\n", + "sizes_dict = {k: v / 10 for k, v in sizes_dict.items()}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.plot_mv_grid_topology(technologies=True, sizes_dict=sizes_dict)" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "### Topology-Module Data Structure" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "Let's get familiar with the topology module:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "# generator types\n", + "edisgo.topology.generators_df[[\"p_nom\", \"type\"]].groupby(\"type\").sum()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "# load types\n", + "edisgo.topology.loads_df[[\"p_set\", \"type\"]].groupby(\"type\").sum()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22", + "metadata": {}, + "outputs": [], + "source": [ + "# load sectors\n", + "edisgo.topology.loads_df[[\"p_set\", \"sector\"]].groupby(\"sector\").sum()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "# amount of lv grids inside the mv grid\n", + "len(list(edisgo.topology.mv_grid.lv_grids))" + ] + }, + { + "cell_type": "markdown", + "id": "24", + "metadata": {}, + "source": [ + "Total number of lines:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": {}, + "outputs": [], + "source": [ + "# overall amount of lines\n", + "len(edisgo.topology.lines_df)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "# amount of lines in one of the lv grids\n", + "len(edisgo.topology.grids[5].lines_df.index)" + ] + }, + { + "cell_type": "markdown", + "id": "27", + "metadata": {}, + "source": [ + "### Basic components addition and removal" + ] + }, + { + "cell_type": "markdown", + "id": "28", + "metadata": {}, + "source": [ + "To see how a loaded network can be adapted later on, we add a solar plant to a random bus.\n", + "\n", + "Components can also be added according to their geolocation with the function ```integrate_component_based_on_geolocation()```." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.topology.generators_df" + ] + }, + { + "cell_type": "markdown", + "id": "30", + "metadata": {}, + "source": [ + "Add a generator with the function ```add_component()``` or ```add_generator()```. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": {}, + "outputs": [], + "source": [ + "# determine a random bus\n", + "rng = default_rng(1)\n", + "rnd_bus = rng.choice(edisgo.topology.buses_df.index, size=1)[0]\n", + "generator_type = \"solar\"\n", + "\n", + "new_generator = edisgo.add_component(\n", + " comp_type=\"generator\", p_nom=0.01, bus=rnd_bus, generator_type=generator_type\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.topology.generators_df" + ] + }, + { + "cell_type": "markdown", + "id": "33", + "metadata": {}, + "source": [ + "Single components can be removed with ```remove_component()```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.remove_component(comp_type=\"generator\", comp_name=new_generator)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.topology.generators_df" + ] + }, + { + "cell_type": "markdown", + "id": "36", + "metadata": {}, + "source": [ + "### Task: \n", + "Add and remove a 'heat_pump' with the function ```add_component()``` and the function ```remove_component()```." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.topology.loads_df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38", + "metadata": {}, + "outputs": [], + "source": [ + "new_load = edisgo.add_component(\n", + " comp_type=\"load\", p_set=0.01, bus=rnd_bus, type=\"heat_pump\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.topology.loads_df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.remove_component(comp_type=\"load\", comp_name=new_load)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.topology.loads_df" + ] + }, + { + "cell_type": "markdown", + "id": "42", + "metadata": {}, + "source": [ + "### Add flexible components to grid " + ] + }, + { + "cell_type": "markdown", + "id": "43", + "metadata": {}, + "source": [ + "For realistic future grids we also add further components like additional generators, home batteries, (charging points) and heat pumps. The components are added according to the scenario \"eGon2035\" and the data from the oedb." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44", + "metadata": {}, + "outputs": [], + "source": [ + "scenario = \"eGon2035\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45", + "metadata": {}, + "outputs": [], + "source": [ + "# copy the edisgo object for later comparisons\n", + "edisgo_orig = edisgo.copy()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46", + "metadata": {}, + "outputs": [], + "source": [ + "# clear initial reinfocement results from results module\n", + "edisgo.results = Results(edisgo)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47", + "metadata": {}, + "outputs": [], + "source": [ + "# set timeindex to ensure that correct time series for COP and heat pump heat demand are downloaded\n", + "timeindex = pd.date_range(f\"1/1/{2011} 12:00\", periods=4, freq=\"H\")\n", + "edisgo.set_timeindex(timeindex=timeindex)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48", + "metadata": {}, + "outputs": [], + "source": [ + "# Retry if running into \"Connection reset by peer\" error\n", + "\n", + "edisgo.import_generators(generator_scenario=scenario)\n", + "edisgo.import_home_batteries(scenario=scenario)\n", + "edisgo.import_heat_pumps(scenario=scenario)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49", + "metadata": {}, + "outputs": [], + "source": [ + "# This takes too long for the workshop\n", + "# edisgo_obj.import_dsm(scenario=scenario)\n", + "# edisgo_obj.import_electromobility(\n", + "# data_source=\"oedb\", scenario=scenario\n", + "# )" + ] + }, + { + "cell_type": "markdown", + "id": "50", + "metadata": {}, + "source": [ + "## Task:\n", + "Determine the differnet generator types that were installed before and that are installed in the grid now." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51", + "metadata": {}, + "outputs": [], + "source": [ + "set(edisgo.topology.generators_df[\"type\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52", + "metadata": {}, + "outputs": [], + "source": [ + "set(edisgo_orig.topology.generators_df[\"type\"])" + ] + }, + { + "cell_type": "markdown", + "id": "53", + "metadata": {}, + "source": [ + "## Task:\n", + "Determine the added solar energy power." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54", + "metadata": {}, + "outputs": [], + "source": [ + "solar_power_new = edisgo.topology.generators_df[\n", + " edisgo.topology.generators_df[\"type\"] == \"solar\"\n", + "][\"p_nom\"].sum()\n", + "\n", + "solar_power_old = edisgo_orig.topology.generators_df[\n", + " edisgo_orig.topology.generators_df[\"type\"] == \"solar\"\n", + "][\"p_nom\"].sum()\n", + "\n", + "solar_power_new - solar_power_old" + ] + }, + { + "cell_type": "markdown", + "id": "55", + "metadata": {}, + "source": [ + "## Task:\n", + "Determine the amount of storage units added to the grid with a nominal power (p_nom) larger than 0.01." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "56", + "metadata": {}, + "outputs": [], + "source": [ + "sum(edisgo.topology.storage_units_df[\"p_nom\"] > 0.01)" + ] + }, + { + "cell_type": "markdown", + "id": "57", + "metadata": {}, + "source": [ + "## Task:\n", + "Determine the buses of the heat pumps whose application ('sector') is not inidividual_heating." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "58", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.topology.loads_df.loc[\n", + " (edisgo.topology.loads_df[\"type\"] == \"heat_pump\")\n", + " & (edisgo.topology.loads_df[\"sector\"] != \"individual_heating\"),\n", + " \"bus\",\n", + "]" + ] + }, + { + "cell_type": "markdown", + "id": "59", + "metadata": {}, + "source": [ + "## 2 Worst Case Time Series Creation" + ] + }, + { + "cell_type": "markdown", + "id": "60", + "metadata": {}, + "source": [ + "Create timeseries for the four worst cases MV load case, LV load case, MV feed-in case, LV feed-in case with the function set_time_series_worst_case_analysis().\n", + "\n", + "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:\n", + "\n", + "* **’feed-in_case’** \n", + " \n", + " Feed-in and demand for the worst-case scenario \"reverse power flow\" are generated (e.g. conventional electricity demand is set to 15% of maximum demand for loads connected to the MV grid and 10% for loads connected to the LV grid and 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)\n", + "\n", + " \n", + "* **’load_case’**\n", + "\n", + " Feed-in and demand for the worst-case scenario \"heavy load flow\" are generated (e.g. demand of all conventional loads is by default set to maximum demand and feed-in of all generators is set to zero)\n", + "\n", + "\n", + "* **[’feed-in_case’, ’load_case’]**\n", + "\n", + " Both cases are set up.\n", + " \n", + "By default both cases are set up.\n", + "\n", + "Feed-in and demand in the two worst-cases are defined in the [config file 'config_timeseries.cfg'](https://edisgo.readthedocs.io/en/latest/configs.html#config-timeseries) and can be changed by setting different values in the config file. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "61", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.set_time_series_worst_case_analysis()" + ] + }, + { + "cell_type": "markdown", + "id": "62", + "metadata": {}, + "source": [ + "The function creates time series for four time steps since both worst cases are defined seperately for the LV and the MV grid with individual simultanerity factors." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "63", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.timeseries.timeindex_worst_cases" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64", + "metadata": {}, + "outputs": [], + "source": [ + "# indexing with worst case timeindex\n", + "edisgo.timeseries.loads_active_power.loc[\n", + " edisgo.timeseries.timeindex_worst_cases[\"load_case_mv\"]\n", + "]" + ] + }, + { + "cell_type": "markdown", + "id": "65", + "metadata": {}, + "source": [ + "## 3 Grid Investigation" + ] + }, + { + "cell_type": "markdown", + "id": "66", + "metadata": {}, + "source": [ + "Execute a power flow analysis to determine line overloads and voltage deviations for the MV load case timeseries with the function ```analyze()```:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67", + "metadata": {}, + "outputs": [], + "source": [ + "# power flow analysis\n", + "edisgo.analyze(timesteps=edisgo.timeseries.timeindex_worst_cases[\"load_case_mv\"])" + ] + }, + { + "cell_type": "markdown", + "id": "68", + "metadata": {}, + "source": [ + "A geoplot with the bus and line colors based on the voltage deviations and line loadings repectively can be created with ```plot_mv_line_loading()```." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.plot_mv_line_loading(\n", + " node_color=\"voltage_deviation\",\n", + " timestep=edisgo.timeseries.timeindex_worst_cases[\"load_case_mv\"],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "70", + "metadata": {}, + "source": [ + "For a better overview of the voltage deviations and line loads in the entire grid, edisgo provides histrogram plots." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "71", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.histogram_voltage(binwidth=0.005)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.histogram_relative_line_load(binwidth=0.1)" + ] + }, + { + "cell_type": "markdown", + "id": "73", + "metadata": {}, + "source": [ + "## 4 Results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74", + "metadata": {}, + "outputs": [], + "source": [ + "# Reinforce the grid\n", + "# mode = \"mvlv\" for a shorter run time. However, grid reinforcement should generally be conducted in mode=\"lv\" (default)\n", + "# since the majority of the reinforcement costs is caused in the lv grid part, especially for high load grids (much EV charging demand and low PV capacity)\n", + "# The lv mode is currently not applicable for the Husum grid. The newly added generators are concentrated in one LV grid. The reinforcement\n", + "# very long or cannot be resolved. This issue will be fixed soon. A possible workarounf for running the reinfocrement anyway is to remove the generators for the overloaded LV grid.\n", + "edisgo.reinforce(mode=\"mvlv\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "75", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.plot_mv_line_loading(\n", + " node_color=\"voltage_deviation\",\n", + " timestep=edisgo.timeseries.timeindex_worst_cases[\"load_case_mv\"],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "76", + "metadata": {}, + "outputs": [], + "source": [ + "# power flow analysis to retrieve all bus voltages and line flows\n", + "edisgo.analyze(timesteps=edisgo.timeseries.timeindex_worst_cases[\"load_case_mv\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "77", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.histogram_voltage(binwidth=0.005)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "78", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.histogram_relative_line_load(binwidth=0.1)" + ] + }, + { + "cell_type": "markdown", + "id": "79", + "metadata": {}, + "source": [ + "The module ```results```holds the outputs of the reinforcement" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "80", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo_orig.results.equipment_changes" + ] + }, + { + "cell_type": "markdown", + "id": "81", + "metadata": {}, + "source": [ + "## Task:\n", + "Determine the total costs for the grid reinforcement. The costs for each added component are stored in the data frame ```edisgo.results.grid_expansion_costs```." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "82", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.results.grid_expansion_costs[\"total_costs\"].sum()" + ] + }, + { + "cell_type": "markdown", + "id": "83", + "metadata": {}, + "source": [ + "## 5 Additional Time Series\n", + "\n", + "Besides setting worst case scenarios and the corresponding time series, component time series can also be set with the function ```predefined()```. Either standard profiles for different component types are loaded from a data base or type- (for generators) and sectorwise (for loads) time series can be determined manually and passed to the function. \n", + "\n", + "The function ```set_time_series_manual()``` can be used to set individual time series for components. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "84", + "metadata": {}, + "outputs": [], + "source": [ + "# determine interval time series are set for\n", + "# timeindex has to be set again to desired time interval because it was overwritten by set_time_series_worst_case()\n", + "timeindex = pd.date_range(f\"1/1/{2011} 12:00\", periods=4, freq=\"H\")\n", + "edisgo.set_timeindex(timeindex=timeindex)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "85", + "metadata": {}, + "outputs": [], + "source": [ + "# check which load sectors are included in the Husum grid\n", + "set(edisgo.topology.loads_df[\"sector\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "86", + "metadata": {}, + "outputs": [], + "source": [ + "# constant load for all time steps for all load types\n", + "timeseries_load = pd.DataFrame(\n", + " {\n", + " \"industrial\": [0.0001] * len(timeindex),\n", + " \"cts\": [0.0002] * len(timeindex),\n", + " \"residential\": [0.0002] * len(timeindex),\n", + " \"district_heating_resistive_heater\": [0.0002] * len(timeindex),\n", + " \"individual_heating\": [0.0002] * len(timeindex),\n", + " },\n", + " index=timeindex,\n", + ")\n", + "\n", + "# annual_consumption of loads is not set in Husum data set\n", + "edisgo.topology.loads_df[\"annual_consumption\"] = 700 * edisgo.topology.loads_df[\"p_set\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "87", + "metadata": {}, + "outputs": [], + "source": [ + "# check which generator types are included into the grid\n", + "set(edisgo.topology.generators_df[\"type\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "88", + "metadata": {}, + "outputs": [], + "source": [ + "# constant feed-in for dispatchable generators\n", + "timeseries_generation_dispatchable = pd.DataFrame(\n", + " {\n", + " \"biomass\": [1] * len(timeindex),\n", + " \"gas\": [1] * len(timeindex),\n", + " \"other\": [1] * len(timeindex),\n", + " },\n", + " index=timeindex,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "89", + "metadata": {}, + "outputs": [], + "source": [ + "# determine fluctuating generators, for which generator-type time series are loaded from a data base\n", + "fluctuating_generators = edisgo.topology.generators_df[\n", + " edisgo.topology.generators_df[\"type\"].isin([\"solar\", \"wind\"])\n", + "].index" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90", + "metadata": {}, + "outputs": [], + "source": [ + "# set active power time series for loads and generators\n", + "edisgo.set_time_series_active_power_predefined(\n", + " fluctuating_generators=fluctuating_generators,\n", + " fluctuating_generators_ts=\"oedb\",\n", + " scenario=scenario,\n", + " timeindex=edisgo.timeseries.timeindex,\n", + " conventional_loads_ts=timeseries_load,\n", + " dispatchable_generators_ts=timeseries_generation_dispatchable,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "91", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.timeseries.generators_active_power" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "92", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.timeseries.loads_active_power" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "93", + "metadata": {}, + "outputs": [], + "source": [ + "# set heat pump time series\n", + "# set_time_series_active_power_predefined does not consider heat demand\n", + "edisgo.apply_heat_pump_operating_strategy()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.timeseries.loads_active_power" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95", + "metadata": {}, + "outputs": [], + "source": [ + "# set battery storage time series (not inluded in set_time_series_active_power_predefined())\n", + "apply_reference_operation(edisgo)\n", + "# returns soe" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "96", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.timeseries.storage_units_active_power" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "97", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.timeseries.generators_reactive_power" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "98", + "metadata": {}, + "outputs": [], + "source": [ + "# set reactive power time series\n", + "edisgo.set_time_series_reactive_power_control()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "99", + "metadata": {}, + "outputs": [], + "source": [ + "edisgo.timeseries.generators_reactive_power" + ] + } + ], + "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.10.16" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 76e5e9d018fea694a80433caa526ae7a9186065a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Schl=C3=B6sser?= Date: Wed, 26 Feb 2025 17:00:01 +0100 Subject: [PATCH 26/43] Update Workshop Notebooks --- examples/Workshop_LoMa.ipynb | 261 +++++++++++++++---------- examples/Workshop_LoMa_solutions.ipynb | 97 +++++++-- 2 files changed, 234 insertions(+), 124 deletions(-) diff --git a/examples/Workshop_LoMa.ipynb b/examples/Workshop_LoMa.ipynb index f73cdf2e2..3c6467a0a 100644 --- a/examples/Workshop_LoMa.ipynb +++ b/examples/Workshop_LoMa.ipynb @@ -102,31 +102,43 @@ "Set up a new edisgo object:" ] }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "conf_path and ding0_grid need to be set according to local storage location." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "9", + "id": "10", "metadata": {}, "outputs": [], "source": [ "conf_path = Path.home() / \"Downloads\" / \"egon-data.configuration.yaml\"\n", + "assert conf_path.is_file()\n", + "\n", "db_engine = engine(path=conf_path, ssh=True)\n", + "\n", "ding0_grid = Path.home() / \".edisgo\" / \"husum_grids\" / \"35725\"\n", + "assert ding0_grid.is_dir()\n", "\n", "edisgo = EDisGo(ding0_grid=ding0_grid, legacy_ding0_grids=False, engine=db_engine)" ] }, { "cell_type": "markdown", - "id": "10", + "id": "11", "metadata": {}, "source": [ - "The ding0 grids are not up to date and their capacity is not sufficient for the connected loads and generators. To update the imported grids they need to be extended first with the function ```reinforce()```." + "ding0 and edisgo use different assumptions for the grid design and extension, respectively. This may cause that edisgo detects voltage deviations and line overloads. To avoid this the edisgo assumptions should be transferred to the ding0 grid by applying ```reinforce()``` after the grid import." ] }, { "cell_type": "markdown", - "id": "11", + "id": "12", "metadata": {}, "source": [ "Grids are reinforced for their worst case scenarios. The corresponding time series are created with ```set_time_series_worst_case_analysis()```. " @@ -135,7 +147,7 @@ { "cell_type": "code", "execution_count": null, - "id": "12", + "id": "13", "metadata": {}, "outputs": [], "source": [ @@ -145,7 +157,7 @@ { "cell_type": "code", "execution_count": null, - "id": "13", + "id": "14", "metadata": {}, "outputs": [], "source": [ @@ -154,7 +166,7 @@ }, { "cell_type": "markdown", - "id": "14", + "id": "15", "metadata": {}, "source": [ "### Plot grid topology (MV)" @@ -162,7 +174,7 @@ }, { "cell_type": "markdown", - "id": "15", + "id": "16", "metadata": {}, "source": [ "The topology can be visualized with the ```plot_mv_grid_topology()```. For ```technologies=True``` the buses sizes and colors are determined to the type and size of the technologies connected to it. \n", @@ -178,7 +190,7 @@ { "cell_type": "code", "execution_count": null, - "id": "16", + "id": "17", "metadata": {}, "outputs": [], "source": [ @@ -201,7 +213,7 @@ { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "18", "metadata": {}, "outputs": [], "source": [ @@ -210,7 +222,7 @@ }, { "cell_type": "markdown", - "id": "18", + "id": "19", "metadata": {}, "source": [ "### Topology-Module Data Structure" @@ -218,7 +230,7 @@ }, { "cell_type": "markdown", - "id": "19", + "id": "20", "metadata": {}, "source": [ "Let's get familiar with the topology module:" @@ -227,7 +239,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -238,7 +250,7 @@ { "cell_type": "code", "execution_count": null, - "id": "21", + "id": "22", "metadata": {}, "outputs": [], "source": [ @@ -249,7 +261,7 @@ { "cell_type": "code", "execution_count": null, - "id": "22", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -260,7 +272,7 @@ { "cell_type": "code", "execution_count": null, - "id": "23", + "id": "24", "metadata": {}, "outputs": [], "source": [ @@ -270,7 +282,7 @@ }, { "cell_type": "markdown", - "id": "24", + "id": "25", "metadata": {}, "source": [ "Total number of lines:" @@ -279,7 +291,7 @@ { "cell_type": "code", "execution_count": null, - "id": "25", + "id": "26", "metadata": {}, "outputs": [], "source": [ @@ -290,7 +302,7 @@ { "cell_type": "code", "execution_count": null, - "id": "26", + "id": "27", "metadata": {}, "outputs": [], "source": [ @@ -300,7 +312,7 @@ }, { "cell_type": "markdown", - "id": "27", + "id": "28", "metadata": {}, "source": [ "### Basic components addition and removal" @@ -308,7 +320,7 @@ }, { "cell_type": "markdown", - "id": "28", + "id": "29", "metadata": {}, "source": [ "To see how a loaded network can be adapted later on, we add a solar plant to a random bus.\n", @@ -319,7 +331,7 @@ { "cell_type": "code", "execution_count": null, - "id": "29", + "id": "30", "metadata": {}, "outputs": [], "source": [ @@ -328,7 +340,7 @@ }, { "cell_type": "markdown", - "id": "30", + "id": "31", "metadata": {}, "source": [ "Add a generator with the function ```add_component()``` or ```add_generator()```. " @@ -337,7 +349,7 @@ { "cell_type": "code", "execution_count": null, - "id": "31", + "id": "32", "metadata": {}, "outputs": [], "source": [ @@ -354,7 +366,7 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "33", "metadata": {}, "outputs": [], "source": [ @@ -363,7 +375,7 @@ }, { "cell_type": "markdown", - "id": "33", + "id": "34", "metadata": {}, "source": [ "Single components can be removed with ```remove_component()```" @@ -372,7 +384,7 @@ { "cell_type": "code", "execution_count": null, - "id": "34", + "id": "35", "metadata": {}, "outputs": [], "source": [ @@ -382,7 +394,7 @@ { "cell_type": "code", "execution_count": null, - "id": "35", + "id": "36", "metadata": {}, "outputs": [], "source": [ @@ -391,7 +403,7 @@ }, { "cell_type": "markdown", - "id": "36", + "id": "37", "metadata": {}, "source": [ "### Task: \n", @@ -401,7 +413,7 @@ { "cell_type": "code", "execution_count": null, - "id": "37", + "id": "38", "metadata": {}, "outputs": [], "source": [ @@ -411,7 +423,7 @@ { "cell_type": "code", "execution_count": null, - "id": "38", + "id": "39", "metadata": {}, "outputs": [], "source": [] @@ -419,7 +431,7 @@ { "cell_type": "code", "execution_count": null, - "id": "39", + "id": "40", "metadata": {}, "outputs": [], "source": [ @@ -429,7 +441,7 @@ { "cell_type": "code", "execution_count": null, - "id": "40", + "id": "41", "metadata": {}, "outputs": [], "source": [] @@ -437,7 +449,7 @@ { "cell_type": "code", "execution_count": null, - "id": "41", + "id": "42", "metadata": {}, "outputs": [], "source": [ @@ -446,7 +458,7 @@ }, { "cell_type": "markdown", - "id": "42", + "id": "43", "metadata": {}, "source": [ "### Add flexible components to grid " @@ -454,7 +466,7 @@ }, { "cell_type": "markdown", - "id": "43", + "id": "44", "metadata": {}, "source": [ "For realistic future grids we also add further components like additional generators, home batteries, (charging points) and heat pumps. The components are added according to the scenario \"eGon2035\" and the data from the oedb." @@ -463,7 +475,7 @@ { "cell_type": "code", "execution_count": null, - "id": "44", + "id": "45", "metadata": {}, "outputs": [], "source": [ @@ -473,7 +485,7 @@ { "cell_type": "code", "execution_count": null, - "id": "45", + "id": "46", "metadata": {}, "outputs": [], "source": [ @@ -484,7 +496,7 @@ { "cell_type": "code", "execution_count": null, - "id": "46", + "id": "47", "metadata": {}, "outputs": [], "source": [ @@ -495,19 +507,19 @@ { "cell_type": "code", "execution_count": null, - "id": "47", + "id": "48", "metadata": {}, "outputs": [], "source": [ "# set timeindex to ensure that correct time series for COP and heat pump heat demand are downloaded\n", - "timeindex = pd.date_range(f\"1/1/{2011} 12:00\", periods=4, freq=\"H\")\n", + "timeindex = pd.date_range(f\"1/1/{2011} 8:00\", periods=12, freq=\"H\")\n", "edisgo.set_timeindex(timeindex=timeindex)" ] }, { "cell_type": "code", "execution_count": null, - "id": "48", + "id": "49", "metadata": {}, "outputs": [], "source": [ @@ -521,7 +533,7 @@ { "cell_type": "code", "execution_count": null, - "id": "49", + "id": "50", "metadata": {}, "outputs": [], "source": [ @@ -534,7 +546,7 @@ }, { "cell_type": "markdown", - "id": "50", + "id": "51", "metadata": {}, "source": [ "## Task:\n", @@ -544,7 +556,7 @@ { "cell_type": "code", "execution_count": null, - "id": "51", + "id": "52", "metadata": {}, "outputs": [], "source": [] @@ -552,14 +564,14 @@ { "cell_type": "code", "execution_count": null, - "id": "52", + "id": "53", "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", - "id": "53", + "id": "54", "metadata": {}, "source": [ "## Task:\n", @@ -569,14 +581,14 @@ { "cell_type": "code", "execution_count": null, - "id": "54", + "id": "55", "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", - "id": "55", + "id": "56", "metadata": {}, "source": [ "## Task:\n", @@ -586,14 +598,14 @@ { "cell_type": "code", "execution_count": null, - "id": "56", + "id": "57", "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", - "id": "57", + "id": "58", "metadata": {}, "source": [ "## Task:\n", @@ -603,14 +615,14 @@ { "cell_type": "code", "execution_count": null, - "id": "58", + "id": "59", "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", - "id": "59", + "id": "60", "metadata": {}, "source": [ "## 2 Worst Case Time Series Creation" @@ -618,7 +630,7 @@ }, { "cell_type": "markdown", - "id": "60", + "id": "61", "metadata": {}, "source": [ "Create timeseries for the four worst cases MV load case, LV load case, MV feed-in case, LV feed-in case with the function set_time_series_worst_case_analysis().\n", @@ -647,7 +659,7 @@ { "cell_type": "code", "execution_count": null, - "id": "61", + "id": "62", "metadata": {}, "outputs": [], "source": [ @@ -656,7 +668,7 @@ }, { "cell_type": "markdown", - "id": "62", + "id": "63", "metadata": {}, "source": [ "The function creates time series for four time steps since both worst cases are defined seperately for the LV and the MV grid with individual simultanerity factors." @@ -665,7 +677,7 @@ { "cell_type": "code", "execution_count": null, - "id": "63", + "id": "64", "metadata": {}, "outputs": [], "source": [ @@ -675,7 +687,7 @@ { "cell_type": "code", "execution_count": null, - "id": "64", + "id": "65", "metadata": {}, "outputs": [], "source": [ @@ -687,7 +699,7 @@ }, { "cell_type": "markdown", - "id": "65", + "id": "66", "metadata": {}, "source": [ "## 3 Grid Investigation" @@ -695,7 +707,7 @@ }, { "cell_type": "markdown", - "id": "66", + "id": "67", "metadata": {}, "source": [ "Execute a power flow analysis to determine line overloads and voltage deviations for the MV load case timeseries with the function ```analyze()```:" @@ -704,7 +716,7 @@ { "cell_type": "code", "execution_count": null, - "id": "67", + "id": "68", "metadata": {}, "outputs": [], "source": [ @@ -714,7 +726,7 @@ }, { "cell_type": "markdown", - "id": "68", + "id": "69", "metadata": {}, "source": [ "A geoplot with the bus and line colors based on the voltage deviations and line loadings repectively can be created with ```plot_mv_line_loading()```." @@ -723,7 +735,7 @@ { "cell_type": "code", "execution_count": null, - "id": "69", + "id": "70", "metadata": {}, "outputs": [], "source": [ @@ -735,7 +747,7 @@ }, { "cell_type": "markdown", - "id": "70", + "id": "71", "metadata": {}, "source": [ "For a better overview of the voltage deviations and line loads in the entire grid, edisgo provides histrogram plots." @@ -744,7 +756,7 @@ { "cell_type": "code", "execution_count": null, - "id": "71", + "id": "72", "metadata": {}, "outputs": [], "source": [ @@ -754,7 +766,7 @@ { "cell_type": "code", "execution_count": null, - "id": "72", + "id": "73", "metadata": {}, "outputs": [], "source": [ @@ -763,7 +775,7 @@ }, { "cell_type": "markdown", - "id": "73", + "id": "74", "metadata": {}, "source": [ "## 4 Results" @@ -772,7 +784,7 @@ { "cell_type": "code", "execution_count": null, - "id": "74", + "id": "75", "metadata": {}, "outputs": [], "source": [ @@ -787,7 +799,7 @@ { "cell_type": "code", "execution_count": null, - "id": "75", + "id": "76", "metadata": {}, "outputs": [], "source": [ @@ -800,7 +812,7 @@ { "cell_type": "code", "execution_count": null, - "id": "76", + "id": "77", "metadata": {}, "outputs": [], "source": [ @@ -811,7 +823,7 @@ { "cell_type": "code", "execution_count": null, - "id": "77", + "id": "78", "metadata": {}, "outputs": [], "source": [ @@ -821,7 +833,7 @@ { "cell_type": "code", "execution_count": null, - "id": "78", + "id": "79", "metadata": {}, "outputs": [], "source": [ @@ -830,7 +842,7 @@ }, { "cell_type": "markdown", - "id": "79", + "id": "80", "metadata": {}, "source": [ "The module ```results```holds the outputs of the reinforcement" @@ -839,7 +851,7 @@ { "cell_type": "code", "execution_count": null, - "id": "80", + "id": "81", "metadata": {}, "outputs": [], "source": [ @@ -848,7 +860,7 @@ }, { "cell_type": "markdown", - "id": "81", + "id": "82", "metadata": {}, "source": [ "## Task:\n", @@ -858,14 +870,14 @@ { "cell_type": "code", "execution_count": null, - "id": "82", + "id": "83", "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", - "id": "83", + "id": "84", "metadata": {}, "source": [ "## 5 Additional Time Series\n", @@ -878,20 +890,20 @@ { "cell_type": "code", "execution_count": null, - "id": "84", + "id": "85", "metadata": {}, "outputs": [], "source": [ "# determine interval time series are set for\n", "# timeindex has to be set again to desired time interval because it was overwritten by set_time_series_worst_case()\n", - "timeindex = pd.date_range(f\"1/1/{2011} 12:00\", periods=4, freq=\"H\")\n", + "timeindex = pd.date_range(f\"1/1/{2011} 8:00\", periods=12, freq=\"H\")\n", "edisgo.set_timeindex(timeindex=timeindex)" ] }, { "cell_type": "code", "execution_count": null, - "id": "85", + "id": "86", "metadata": {}, "outputs": [], "source": [ @@ -902,11 +914,11 @@ { "cell_type": "code", "execution_count": null, - "id": "86", + "id": "87", "metadata": {}, "outputs": [], "source": [ - "# constant load for all time steps for all load types\n", + "# constant load for all time steps for all load sectors\n", "timeseries_load = pd.DataFrame(\n", " {\n", " \"industrial\": [0.0001] * len(timeindex),\n", @@ -925,7 +937,7 @@ { "cell_type": "code", "execution_count": null, - "id": "87", + "id": "88", "metadata": {}, "outputs": [], "source": [ @@ -936,7 +948,7 @@ { "cell_type": "code", "execution_count": null, - "id": "88", + "id": "89", "metadata": {}, "outputs": [], "source": [ @@ -954,7 +966,7 @@ { "cell_type": "code", "execution_count": null, - "id": "89", + "id": "90", "metadata": {}, "outputs": [], "source": [ @@ -967,7 +979,7 @@ { "cell_type": "code", "execution_count": null, - "id": "90", + "id": "91", "metadata": {}, "outputs": [], "source": [ @@ -982,30 +994,60 @@ ")" ] }, + { + "cell_type": "markdown", + "id": "92", + "metadata": {}, + "source": [ + "## Task\n", + "Plot the time series for three solar generators and gas power plants in individual plots." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "91", + "id": "93", "metadata": {}, "outputs": [], - "source": [ - "edisgo.timeseries.generators_active_power" - ] + "source": [] }, { "cell_type": "code", "execution_count": null, - "id": "92", + "id": "94", "metadata": {}, "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "95", + "metadata": {}, "source": [ - "edisgo.timeseries.loads_active_power" + "## Task\n", + "Plot the time series of three conventional loads and of three heat pumps in individual plots." ] }, { "cell_type": "code", "execution_count": null, - "id": "93", + "id": "96", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "97", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "98", "metadata": {}, "outputs": [], "source": [ @@ -1017,21 +1059,24 @@ { "cell_type": "code", "execution_count": null, - "id": "94", + "id": "99", "metadata": {}, "outputs": [], "source": [ - "edisgo.timeseries.loads_active_power" + "timeseries_heat_pumps = edisgo.timeseries.loads_active_power.loc[\n", + " :, edisgo.topology.loads_df[\"type\"] == \"heat_pump\"\n", + "]\n", + "timeseries_heat_pumps.iloc[:, :4].plot()" ] }, { "cell_type": "code", "execution_count": null, - "id": "95", + "id": "100", "metadata": {}, "outputs": [], "source": [ - "# set battery storage time series (not inluded in set_time_series_active_power_predefined())\n", + "# set battery storage time series (not included in set_time_series_active_power_predefined())\n", "apply_reference_operation(edisgo)\n", "# returns soe" ] @@ -1039,27 +1084,27 @@ { "cell_type": "code", "execution_count": null, - "id": "96", + "id": "101", "metadata": {}, "outputs": [], "source": [ - "edisgo.timeseries.storage_units_active_power" + "edisgo.timeseries.storage_units_active_power.head()" ] }, { "cell_type": "code", "execution_count": null, - "id": "97", + "id": "102", "metadata": {}, "outputs": [], "source": [ - "edisgo.timeseries.generators_reactive_power" + "edisgo.timeseries.storage_units_active_power.iloc[:, :4].plot()" ] }, { "cell_type": "code", "execution_count": null, - "id": "98", + "id": "103", "metadata": {}, "outputs": [], "source": [ @@ -1070,12 +1115,20 @@ { "cell_type": "code", "execution_count": null, - "id": "99", + "id": "104", "metadata": {}, "outputs": [], "source": [ - "edisgo.timeseries.generators_reactive_power" + "edisgo.timeseries.generators_reactive_power.head()" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "105", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/examples/Workshop_LoMa_solutions.ipynb b/examples/Workshop_LoMa_solutions.ipynb index 8ccadb9fc..bfd619095 100644 --- a/examples/Workshop_LoMa_solutions.ipynb +++ b/examples/Workshop_LoMa_solutions.ipynb @@ -110,8 +110,12 @@ "outputs": [], "source": [ "conf_path = Path.home() / \"Downloads\" / \"egon-data.configuration.yaml\"\n", + "assert conf_path.is_file()\n", + "\n", "db_engine = engine(path=conf_path, ssh=True)\n", + "\n", "ding0_grid = Path.home() / \".edisgo\" / \"husum_grids\" / \"35725\"\n", + "assert ding0_grid.is_dir()\n", "\n", "edisgo = EDisGo(ding0_grid=ding0_grid, legacy_ding0_grids=False, engine=db_engine)" ] @@ -121,7 +125,7 @@ "id": "10", "metadata": {}, "source": [ - "The ding0 grids are not up to date and their capacity is not sufficient for the connected loads and generators. To update the imported grids they need to be extended first with the function ```reinforce()```." + "ding0 and edisgo use different assumptions for the grid design and extension, respectively. This may cause that edisgo detects voltage deviations and line overloads. To avoid this the edisgo assumptions should be transferred to the ding0 grid by applying ```reinforce()``` after the grid import." ] }, { @@ -506,7 +510,7 @@ "outputs": [], "source": [ "# set timeindex to ensure that correct time series for COP and heat pump heat demand are downloaded\n", - "timeindex = pd.date_range(f\"1/1/{2011} 12:00\", periods=4, freq=\"H\")\n", + "timeindex = pd.date_range(f\"1/1/{2011} 8:00\", periods=12, freq=\"H\")\n", "edisgo.set_timeindex(timeindex=timeindex)" ] }, @@ -914,7 +918,7 @@ "source": [ "# determine interval time series are set for\n", "# timeindex has to be set again to desired time interval because it was overwritten by set_time_series_worst_case()\n", - "timeindex = pd.date_range(f\"1/1/{2011} 12:00\", periods=4, freq=\"H\")\n", + "timeindex = pd.date_range(f\"1/1/{2011} 8:00\", periods=12, freq=\"H\")\n", "edisgo.set_timeindex(timeindex=timeindex)" ] }, @@ -1013,13 +1017,12 @@ ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "id": "91", "metadata": {}, - "outputs": [], "source": [ - "edisgo.timeseries.generators_active_power" + "## Task\n", + "Plot the time series for three solar generators and gas power plants in individual plots." ] }, { @@ -1029,7 +1032,10 @@ "metadata": {}, "outputs": [], "source": [ - "edisgo.timeseries.loads_active_power" + "timeseries_solar_generators = edisgo.timeseries.generators_active_power.loc[\n", + " :, edisgo.topology.generators_df[\"type\"] == \"solar\"\n", + "]\n", + "timeseries_solar_generators.iloc[:, :5].plot()" ] }, { @@ -1038,6 +1044,54 @@ "id": "93", "metadata": {}, "outputs": [], + "source": [ + "timeseries_gas_generators = edisgo.timeseries.generators_active_power.loc[\n", + " :, edisgo.topology.generators_df[\"type\"] == \"gas\"\n", + "]\n", + "timeseries_gas_generators.iloc[:, :5].plot()" + ] + }, + { + "cell_type": "markdown", + "id": "94", + "metadata": {}, + "source": [ + "## Task\n", + "Plot the time series of three conventional loads and of three heat pumps in individual plots." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95", + "metadata": {}, + "outputs": [], + "source": [ + "timeseries_heat_pumps = edisgo.timeseries.loads_active_power.loc[\n", + " :, edisgo.topology.loads_df[\"type\"] == \"conventional_load\"\n", + "]\n", + "timeseries_heat_pumps.iloc[:, :4].plot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "96", + "metadata": {}, + "outputs": [], + "source": [ + "timeseries_heat_pumps = edisgo.timeseries.loads_active_power.loc[\n", + " :, edisgo.topology.loads_df[\"type\"] == \"heat_pump\"\n", + "]\n", + "timeseries_heat_pumps.iloc[:, :4].plot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "97", + "metadata": {}, + "outputs": [], "source": [ "# set heat pump time series\n", "# set_time_series_active_power_predefined does not consider heat demand\n", @@ -1047,49 +1101,52 @@ { "cell_type": "code", "execution_count": null, - "id": "94", + "id": "98", "metadata": {}, "outputs": [], "source": [ - "edisgo.timeseries.loads_active_power" + "timeseries_heat_pumps = edisgo.timeseries.loads_active_power.loc[\n", + " :, edisgo.topology.loads_df[\"type\"] == \"heat_pump\"\n", + "]\n", + "timeseries_heat_pumps.iloc[:, :4].plot()" ] }, { "cell_type": "code", "execution_count": null, - "id": "95", + "id": "99", "metadata": {}, "outputs": [], "source": [ - "# set battery storage time series (not inluded in set_time_series_active_power_predefined())\n", - "apply_reference_operation(edisgo)\n", - "# returns soe" + "edisgo.timeseries.loads_active_power" ] }, { "cell_type": "code", "execution_count": null, - "id": "96", + "id": "100", "metadata": {}, "outputs": [], "source": [ - "edisgo.timeseries.storage_units_active_power" + "# set battery storage time series (not inluded in set_time_series_active_power_predefined())\n", + "apply_reference_operation(edisgo)\n", + "# returns soe" ] }, { "cell_type": "code", "execution_count": null, - "id": "97", + "id": "101", "metadata": {}, "outputs": [], "source": [ - "edisgo.timeseries.generators_reactive_power" + "edisgo.timeseries.storage_units_active_power.iloc[:, :4].plot()" ] }, { "cell_type": "code", "execution_count": null, - "id": "98", + "id": "102", "metadata": {}, "outputs": [], "source": [ @@ -1100,7 +1157,7 @@ { "cell_type": "code", "execution_count": null, - "id": "99", + "id": "103", "metadata": {}, "outputs": [], "source": [ From e136f25757ba143187300b5be349751ccec6f502 Mon Sep 17 00:00:00 2001 From: Jonas Danke Date: Mon, 3 Nov 2025 11:05:17 +0100 Subject: [PATCH 27/43] fix: rename default schema from 'dataset' to 'data' in Config class --- edisgo/tools/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edisgo/tools/config.py b/edisgo/tools/config.py index b4748bf44..bf0f56015 100644 --- a/edisgo/tools/config.py +++ b/edisgo/tools/config.py @@ -200,7 +200,7 @@ def get_database_alias_dictionaries(self) -> tuple[dict[str, str], dict[str, str entry.source_name: entry.target_name for entry in dictionary_entries } schema_mapping = { - entry.source_schema: getattr(entry, "target_schema", "dataset") + entry.source_schema: getattr(entry, "target_schema", "data") for entry in dictionary_entries } From 2d4a4347139fff4ac7ccb2d9c8a744464819a245 Mon Sep 17 00:00:00 2001 From: joda9 Date: Mon, 3 Nov 2025 18:13:56 +0100 Subject: [PATCH 28/43] comment out loading of switches dataframe in import_ding0_grid function to allow networks without switches --- edisgo/io/ding0_import.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/edisgo/io/ding0_import.py b/edisgo/io/ding0_import.py index 326553111..ffdf740df 100644 --- a/edisgo/io/ding0_import.py +++ b/edisgo/io/ding0_import.py @@ -116,9 +116,9 @@ def sort_hvmv_transformer_buses(transformers_df): columns={"r": "r_pu", "x": "x_pu"} ) ) - edisgo_obj.topology.switches_df = pd.read_csv( - os.path.join(path, "switches.csv"), index_col=[0] - ) + # edisgo_obj.topology.switches_df = pd.read_csv( + # os.path.join(path, "switches.csv"), index_col=[0] + # ) edisgo_obj.topology.grid_district = { "population": grid.mv_grid_district_population, From 3df5d9dcecddd8c79f5b0bd9c08fddee02d272bb Mon Sep 17 00:00:00 2001 From: joda9 Date: Mon, 3 Nov 2025 18:14:22 +0100 Subject: [PATCH 29/43] feat: add active_power_p_max_pu method to scale generator time series by nominal power --- edisgo/network/timeseries.py | 75 ++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/edisgo/network/timeseries.py b/edisgo/network/timeseries.py index 6cf4a7b47..47c537e43 100644 --- a/edisgo/network/timeseries.py +++ b/edisgo/network/timeseries.py @@ -14,6 +14,7 @@ from edisgo.io import timeseries_import from edisgo.tools.tools import assign_voltage_level_to_component, resample +from edisgo.io.db import engine as egon_engine if TYPE_CHECKING: from edisgo import EDisGo @@ -1250,6 +1251,10 @@ def predefined_fluctuating_generators_by_technology( are used. """ + if not engine: + engine = egon_engine() + + # in case time series from oedb are used, retrieve oedb time series if isinstance(ts_generators, str) and ts_generators == "oedb": if edisgo_object.legacy_grids is True: @@ -1520,6 +1525,76 @@ def predefined_charging_points_by_use_case( ).T self.add_component_time_series("loads_active_power", ts_scaled) + def active_power_p_max_pu( + self, edisgo_object, ts_generators_p_max_pu, generator_names=None + ): + """ + Set active power feed-in time series for generators using p_max_pu time series. + + This function reads generator-specific p_max_pu time series (normalized to + nominal capacity) and scales them by the nominal power (p_nom) of each + generator to obtain absolute active power time series. + + Parameters + ---------- + edisgo_object : :class:`~.EDisGo` + ts_generators_p_max_pu : :pandas:`pandas.DataFrame` + DataFrame with generator-specific p_max_pu time series normalized to + a nominal capacity of 1. Each column represents a specific generator + and should match the generator names in the network. + Index needs to be a :pandas:`pandas.DatetimeIndex`. + Column names should correspond to generator names in + :attr:`~.network.topology.Topology.generators_df`. + generator_names : list(str), optional + Defines for which generators to set p_max_pu time series. If None, + all generators for which p_max_pu time series are provided in + `ts_generators_p_max_pu` are used. Default: None. + + Notes + ----- + This function is useful when you have generator-specific capacity factors + or availability profiles that differ from technology-wide profiles. + + """ + if not isinstance(ts_generators_p_max_pu, pd.DataFrame): + raise ValueError( + "Parameter 'ts_generators_p_max_pu' must be a pandas DataFrame." + ) + elif ts_generators_p_max_pu.empty: + logger.warning("Provided time series dataframe is empty.") + return + + # set generator_names if None + if generator_names is None: + generator_names = ts_generators_p_max_pu.columns.tolist() + + generator_names = self._check_if_components_exist( + edisgo_object, generator_names, "generators" + ) + + # Filter to only include generators that have time series provided + generators_with_ts = [ + gen for gen in generator_names if gen in ts_generators_p_max_pu.columns + ] + + if not generators_with_ts: + logger.warning( + "None of the specified generators have time series in " + "ts_generators_p_max_pu." + ) + return + + generators_df = edisgo_object.topology.generators_df.loc[generators_with_ts, :] + + # scale time series by nominal power + ts_scaled = generators_df.apply( + lambda x: ts_generators_p_max_pu[x.name] * x.p_nom, + axis=1, + ).T + + if not ts_scaled.empty: + self.add_component_time_series("generators_active_power", ts_scaled) + def fixed_cosphi( self, edisgo_object, From c3bdf26ecc6540fccff733f0e92344111022ff00 Mon Sep 17 00:00:00 2001 From: joda9 Date: Mon, 3 Nov 2025 18:14:45 +0100 Subject: [PATCH 30/43] fix: initialize engine in get_weather_cells_intersecting_with_grid_district function if None --- edisgo/tools/tools.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/edisgo/tools/tools.py b/edisgo/tools/tools.py index 66353c55d..cf94fbc7d 100644 --- a/edisgo/tools/tools.py +++ b/edisgo/tools/tools.py @@ -12,6 +12,8 @@ import pandas as pd from sqlalchemy.engine.base import Engine +from edisgo.io.db import engine as egon_engine + from edisgo.flex_opt import exceptions, q_control from edisgo.io.db import session_scope_egon_data, sql_grid_geom, sql_intersects @@ -729,6 +731,8 @@ def get_weather_cells_intersecting_with_grid_district( Set with weather cell IDs. """ + if engine is None: + engine = egon_engine() # Download geometries of weather cells sql_geom = sql_grid_geom(edisgo_obj) srid = edisgo_obj.topology.grid_district["srid"] From 4ea9d8ba0697ad66223555b9fc9ca5ae5377ff04 Mon Sep 17 00:00:00 2001 From: Jonas Danke Date: Wed, 5 Nov 2025 12:54:45 +0100 Subject: [PATCH 31/43] fix: update engine initialization to use toep egon_engine --- edisgo/edisgo.py | 3 ++- edisgo/io/electromobility_import.py | 14 +++++++++----- edisgo/tools/config.py | 6 ++++++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/edisgo/edisgo.py b/edisgo/edisgo.py index 402a34c8e..1e9ca3239 100755 --- a/edisgo/edisgo.py +++ b/edisgo/edisgo.py @@ -165,7 +165,7 @@ class EDisGo: def __init__(self, **kwargs): # Set database engine for future scenarios - self.engine: Engine | None = kwargs.pop("engine", toep_engine()) + self.engine: Engine | None = kwargs.pop("engine", egon_engine()) # load configuration self._config = Config(engine=self.engine, **kwargs) @@ -558,6 +558,7 @@ def set_time_series_active_power_predefined( is indexed using a default year and set for the whole year. """ + timeindex = kwargs.get("timeindex", None) engine = kwargs["engine"] if "engine" in kwargs else egon_engine() if self.timeseries.timeindex.empty: logger.warning( diff --git a/edisgo/io/electromobility_import.py b/edisgo/io/electromobility_import.py index 83dc68af4..7fcc0a2e0 100644 --- a/edisgo/io/electromobility_import.py +++ b/edisgo/io/electromobility_import.py @@ -15,6 +15,7 @@ from sklearn import preprocessing from sqlalchemy.engine.base import Engine +from edisgo.io.db import engine as egon_engine from edisgo.io.db import get_srid_of_db_table, session_scope_egon_data from edisgo.tools.config import Config @@ -1077,11 +1078,11 @@ def distribute_public_charging_demand(edisgo_obj, **kwargs): idx, "charging_point_id" ] = charging_point_id - available_charging_points_df.loc[ - charging_point_id - ] = edisgo_obj.electromobility.charging_processes_df.loc[ - idx, available_charging_points_df.columns - ].tolist() + available_charging_points_df.loc[charging_point_id] = ( + edisgo_obj.electromobility.charging_processes_df.loc[ + idx, available_charging_points_df.columns + ].tolist() + ) designated_charging_point_capacity_df.at[ charging_park_id, "designated_charging_point_capacity" @@ -1312,6 +1313,9 @@ def charging_processes_from_oedb( more information. """ + if not engine: + engine = egon_engine() + config = Config() egon_ev_mv_grid_district, egon_ev_trip = config.import_tables_from_oep( engine, ["egon_ev_mv_grid_district", "egon_ev_trip"], "demand" diff --git a/edisgo/tools/config.py b/edisgo/tools/config.py index 1eae03a9d..db5685474 100644 --- a/edisgo/tools/config.py +++ b/edisgo/tools/config.py @@ -39,6 +39,7 @@ import edisgo from edisgo.io.db import engine as Engine +from edisgo.io.db import engine as egon_engine from edisgo.io.db import session_scope_egon_data logger = logging.getLogger(__name__) @@ -172,6 +173,9 @@ def _ensure_db_mappings_loaded(self) -> None: return name_mapping, schema_mapping = self.get_database_alias_dictionaries() + self.db_table_mapping = name_mapping + self.db_schema_mapping = schema_mapping + def _set_db_mappings(self) -> None: """ Sets the database table and schema mappings by retrieving alias dictionaries. @@ -279,6 +283,8 @@ def import_tables_from_oep( list of sqlalchemy.Table A list of SQLAlchemy Table objects corresponding to the imported tables. """ + if engine is None: + engine = egon_engine() if "toep" in str(engine.url): self._ensure_db_mappings_loaded() schema = self.db_schema_mapping.get(schema_name) From 0a838e215f1e5f26bf9d617c8ecf069639311cc4 Mon Sep 17 00:00:00 2001 From: joda9 Date: Tue, 14 Oct 2025 14:37:59 +0200 Subject: [PATCH 32/43] update timeindex handling in EDisGo class and adjust database schema name in Config class --- edisgo/edisgo.py | 5 ++++- edisgo/tools/config.py | 19 ++----------------- setup.py | 2 ++ 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/edisgo/edisgo.py b/edisgo/edisgo.py index 1e9ca3239..c452dbc6e 100755 --- a/edisgo/edisgo.py +++ b/edisgo/edisgo.py @@ -560,7 +560,7 @@ def set_time_series_active_power_predefined( """ timeindex = kwargs.get("timeindex", None) engine = kwargs["engine"] if "engine" in kwargs else egon_engine() - if self.timeseries.timeindex.empty: + if timeindex is not None and not self.timeseries.timeindex.empty: logger.warning( "The given timeindex is different from the EDisGo.TimeSeries.timeindex." " Therefore the EDisGo.TimeSeries.timeindex will be overwritten by the " @@ -580,6 +580,9 @@ def set_time_series_active_power_predefined( set_timeindex = True + else: + set_timeindex = False + if set_timeindex: if timeindex is None: timeindex, _ = _timeindex_helper_func( diff --git a/edisgo/tools/config.py b/edisgo/tools/config.py index db5685474..a2466eedf 100644 --- a/edisgo/tools/config.py +++ b/edisgo/tools/config.py @@ -39,7 +39,6 @@ import edisgo from edisgo.io.db import engine as Engine -from edisgo.io.db import engine as egon_engine from edisgo.io.db import session_scope_egon_data logger = logging.getLogger(__name__) @@ -176,18 +175,6 @@ def _ensure_db_mappings_loaded(self) -> None: self.db_table_mapping = name_mapping self.db_schema_mapping = schema_mapping - def _set_db_mappings(self) -> None: - """ - Sets the database table and schema mappings by retrieving alias dictionaries. - """ - if self._engine is not None and "toep.iks.cs.ovgu.de" in self._engine.url.host: - name_mapping, schema_mapping = self.get_database_alias_dictionaries() - else: - name_mapping = schema_mapping = {} - - self.db_table_mapping = name_mapping - self.db_schema_mapping = schema_mapping - def get_database_alias_dictionaries(self) -> tuple[dict[str, str], dict[str, str]]: """ Retrieves the OEP database alias dictionaries for table and schema mappings. @@ -202,7 +189,7 @@ def get_database_alias_dictionaries(self) -> tuple[dict[str, str], dict[str, str names. """ engine = Engine() - dictionary_schema_name = "data" + dictionary_schema_name = "dataset" dictionary_table = self._get_module_attr( self._get_saio_module(dictionary_schema_name, engine), "edut_00", @@ -215,7 +202,7 @@ def get_database_alias_dictionaries(self) -> tuple[dict[str, str], dict[str, str entry.source_name: entry.target_name for entry in dictionary_entries } schema_mapping = { - entry.source_schema: getattr(entry, "target_schema", "data") + entry.source_schema: getattr(entry, "target_schema", "dataset") for entry in dictionary_entries } @@ -283,8 +270,6 @@ def import_tables_from_oep( list of sqlalchemy.Table A list of SQLAlchemy Table objects corresponding to the imported tables. """ - if engine is None: - engine = egon_engine() if "toep" in str(engine.url): self._ensure_db_mappings_loaded() schema = self.db_schema_mapping.get(schema_name) diff --git a/setup.py b/setup.py index 715d0869a..618527ebd 100644 --- a/setup.py +++ b/setup.py @@ -61,6 +61,8 @@ def read(fname): "shapely >= 1.7.0, < 2.1.0", "sqlalchemy < 1.4.0", "sshtunnel < 0.5.0", + # sshtunnel 0.4.0 incompatible with paramiko >= 3.5.0 (DSSKey removed) + "paramiko < 3.5.0", "urllib3 < 2.6.0", "workalendar < 17.1.0", "astroid == 3.3.11", From 82bf68b4f1cf8ac362bdb30fd542083ebd714d17 Mon Sep 17 00:00:00 2001 From: joda9 Date: Thu, 11 Dec 2025 13:26:24 +0100 Subject: [PATCH 33/43] =?UTF-8?q?feat:=20Implement=20=C2=A714a=20EnWG=20cu?= =?UTF-8?q?rtailment=20for=20heat=20pumps=20with=20virtual=20generators?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added support for §14a curtailment in EDisGo class and related functions. - Introduced new parameters and documentation for curtailment settings in `edisgo.py` and `powermodels_io.py`. - Created virtual generators for heat pumps in the PowerModels dictionary to simulate curtailment. - Implemented constraints for curtailment in the Julia OPF module, including binary coupling and minimum net load constraints. - Updated power balance constraints to account for the impact of virtual generators on net load. - Enhanced objective functions to minimize curtailment support. - Added time budget constraints for daily and total usage of curtailment support. - Included debug logging for better traceability of curtailment operations. --- edisgo/edisgo.py | 14 ++ edisgo/io/powermodels_io.py | 187 +++++++++++++++++- edisgo/opf/eDisGo_OPF.jl/src/core/base.jl | 8 + .../src/core/constraint_hp_14a.jl | 151 ++++++++++++++ .../src/core/constraint_template.jl | 3 +- .../opf/eDisGo_OPF.jl/src/core/objective.jl | 14 ++ .../opf/eDisGo_OPF.jl/src/core/variables.jl | 31 +++ edisgo/opf/eDisGo_OPF.jl/src/eDisGo_OPF.jl | 1 + edisgo/opf/eDisGo_OPF.jl/src/form/bf.jl | 9 +- edisgo/opf/eDisGo_OPF.jl/src/prob/opf_bf.jl | 53 +++++ edisgo/opf/powermodels_opf.py | 7 + 11 files changed, 471 insertions(+), 7 deletions(-) create mode 100644 edisgo/opf/eDisGo_OPF.jl/src/core/constraint_hp_14a.jl diff --git a/edisgo/edisgo.py b/edisgo/edisgo.py index e9aa0a005..aad8c2d8b 100755 --- a/edisgo/edisgo.py +++ b/edisgo/edisgo.py @@ -828,6 +828,7 @@ def to_powermodels( flexible_loads=None, flexible_storage_units=None, opf_version=1, + curtailment_14a=False, ): """ Convert eDisGo representation of the network topology and timeseries to @@ -854,6 +855,11 @@ def to_powermodels( Version of optimization models to choose from. Must be one of [1, 2, 3, 4]. For more information see :func:`edisgo.opf.powermodels_opf.pm_optimize`. Default: 1. + curtailment_14a : bool + If True, enables §14a EnWG curtailment for heat pumps with virtual + generators. Heat pumps can be curtailed down to 4.2 kW with time budget + constraints. + Default: False. Returns ------- @@ -870,6 +876,7 @@ def to_powermodels( flexible_loads=flexible_loads, flexible_storage_units=flexible_storage_units, opf_version=opf_version, + curtailment_14a=curtailment_14a, ) def pm_optimize( @@ -886,6 +893,7 @@ def pm_optimize( save_heat_storage=True, save_slack_gen=True, save_slacks=True, + curtailment_14a=False, ): """ Run OPF in julia subprocess and write results of OPF back to edisgo object. @@ -933,6 +941,11 @@ def pm_optimize( hence there will be no logging coming from julia subprocess in python process. Default: False. + curtailment_14a : bool + If True, enables §14a EnWG curtailment for heat pumps with virtual + generators. Heat pumps can be curtailed down to 4.2 kW with time budget + constraints. + Default: False. """ return powermodels_opf.pm_optimize( self, @@ -945,6 +958,7 @@ def pm_optimize( method=method, warm_start=warm_start, silence_moi=silence_moi, + curtailment_14a=curtailment_14a, ) def to_graph(self): diff --git a/edisgo/io/powermodels_io.py b/edisgo/io/powermodels_io.py index 541e01ffc..91b9e9f4e 100644 --- a/edisgo/io/powermodels_io.py +++ b/edisgo/io/powermodels_io.py @@ -31,6 +31,7 @@ def to_powermodels( flexible_loads=None, flexible_storage_units=None, opf_version=1, + curtailment_14a=None, ): """ Convert eDisGo representation of the network topology and timeseries to @@ -58,6 +59,12 @@ def to_powermodels( Version of optimization models to choose from. Must be one of [1, 2, 3, 4]. For more information see :func:`edisgo.opf.powermodels_opf.pm_optimize`. Default: 1. + curtailment_14a : dict or None + Dictionary with §14a EnWG curtailment settings. Keys: + - 'max_power_mw': float, minimum power in MW (default 0.0042) + - 'components': list, heat pump names to apply curtailment to (empty = all) + - 'max_hours_per_day': float, maximum curtailment hours per day (default 2.0) + Default: None (no §14a curtailment). Returns ------- @@ -90,6 +97,7 @@ def to_powermodels( if (flex not in opf_flex) & (len(loads) != 0): logger.info("{} will be optimized.".format(text)) opf_flex.append(flex) + hv_flex_dict = dict() # Sorts buses such that bus0 is always the upstream bus edisgo_object.topology.sort_buses() @@ -118,6 +126,16 @@ def to_powermodels( ) # length of timesteps in hours pm["baseMVA"] = s_base pm["source_version"] = 2 + + # Add §14a curtailment flexibility if enabled (must be BEFORE pm["flexibilities"] assignment) + if curtailment_14a is not None and curtailment_14a is not False: + all_hps = edisgo_object.topology.loads_df[ + edisgo_object.topology.loads_df['type'] == 'heat_pump' + ] + if len(all_hps) > 0: + logger.info("§14a heat pump curtailment will be optimized.") + opf_flex.append("hp_14a") + pm["flexibilities"] = opf_flex logger.info("Transforming busses into PowerModels dictionary format.") _build_bus(psa_net, edisgo_object, pm, flexible_storage_units) @@ -141,15 +159,43 @@ def to_powermodels( s_base, flexible_cps, ) - if len(flexible_hps) > 0: + + # Get all heat pumps for §14a support (needed before flexible_hps check) + all_hps = edisgo_object.topology.loads_df[ + edisgo_object.topology.loads_df['type'] == 'heat_pump' + ].index.to_numpy() + + # Determine which heat pumps need to be in PowerModels dict + # Either flexible_hps OR §14a curtailment requires heatpump dict + hps_for_pm = flexible_hps if len(flexible_hps) > 0 else [] + if curtailment_14a is not None and curtailment_14a is not False and len(all_hps) > 0: + # For §14a, we need ALL heat pumps in the PM dict + hps_for_pm = all_hps + + if len(hps_for_pm) > 0: logger.info("Transforming heatpumps into PowerModels dictionary format.") - _build_heatpump(psa_net, pm, edisgo_object, s_base, flexible_hps) + _build_heatpump(psa_net, pm, edisgo_object, s_base, hps_for_pm) logger.info( "Transforming heat storage units into PowerModels dictionary format." ) _build_heat_storage( - psa_net, pm, edisgo_object, s_base, flexible_hps, opf_version + psa_net, pm, edisgo_object, s_base, hps_for_pm, opf_version ) + + # Create virtual generators for §14a curtailment if enabled + # This applies to ALL heat pumps, not just flexible ones + if curtailment_14a is not None and curtailment_14a is not False: + # Convert bool to dict if needed + if curtailment_14a is True: + curtailment_14a_config = {} + else: + curtailment_14a_config = curtailment_14a + + if len(all_hps) > 0: + logger.info(f"Creating virtual generators for §14a heat pump support ({len(all_hps)} heat pumps).") + _build_gen_hp_14a_support( + psa_net, pm, edisgo_object, s_base, all_hps, curtailment_14a_config + ) if len(flexible_loads) > 0: logger.info("Transforming DSM loads into PowerModels dictionary format.") flexible_loads = _build_dsm(edisgo_object, psa_net, pm, s_base, flexible_loads) @@ -271,17 +317,43 @@ def from_powermodels( "cp": ["electromobility", "pcp"], "storage": ["storage", "pf"], "dsm": ["dsm", "pdsm"], + "hp_14a": ["gen_hp_14a", "p"], # §14a virtual generators (uses "p" not "php14a") } timesteps = pd.Series([int(k) for k in pm["nw"].keys()]).sort_values().values logger.info("Writing OPF results to eDisGo object.") + + print("\n" + "="*80) + print("🔍 PYTHON DEBUG: Processing OPF Results") + print("="*80) + print(f"Flexibilities in results: {pm_results['nw']['1']['flexibilities']}") + print("="*80 + "\n") + # write active power OPF results to edisgo object for flexibility in pm_results["nw"]["1"]["flexibilities"]: + print(f" → Processing flexibility: {flexibility}") flex, variable = flex_dicts[flexibility] + print(f" flex={flex}, variable={variable}") + + # Check if flex exists in network + if flex not in pm["nw"]["1"]: + print(f" ⚠ WARNING: '{flex}' not found in pm['nw']['1']!") + continue + names = [ pm["nw"]["1"][flex][flex_comp]["name"] for flex_comp in list(pm["nw"]["1"][flex].keys()) ] + print(f" Found {len(names)} components: {names[:5]}...") # Show first 5 + + # Check if results exist in pm_results + if flex not in pm_results["nw"]["1"]: + print(f" ⚠ WARNING: '{flex}' not found in pm_results['nw']['1']!") + print(f" Available keys in pm_results['nw']['1']: {list(pm_results['nw']['1'].keys())}") + continue + + print(f" ✓ '{flex}' found in pm_results, extracting data...") + # replace storage power values by branch power values of virtual branch to # account for losses if flex == "storage": @@ -299,17 +371,27 @@ def from_powermodels( else: data = [ [ - pm["nw"][str(t)][flex][flex_comp][variable] * s_base + pm_results["nw"][str(t)][flex][flex_comp][variable] * s_base for flex_comp in list(pm["nw"]["1"][flex].keys()) ] for t in timesteps ] + + print(f" ✓ Extracted {len(data)} timesteps x {len(data[0])} components") results = pd.DataFrame(index=timesteps, columns=names, data=data) if (flex == "gen_nd") & (pm["nw"]["1"]["opf_version"] in [3, 4]): edisgo_object.timeseries._generators_active_power.loc[:, names] = ( edisgo_object.timeseries.generators_active_power.loc[:, names].values - results[names].values ) + elif flex == "gen_hp_14a": + # §14a virtual generators: write as positive generation + print(f" → Writing {len(names)} gen_hp_14a generators to generators_active_power") + print(f" → Sample values: {results.iloc[0, :3].to_dict()}") + edisgo_object.timeseries._generators_active_power.loc[:, names] = results[ + names + ].values + print(f" ✓ Written successfully!") elif flex in ["heatpumps", "electromobility"]: edisgo_object.timeseries._loads_active_power.loc[:, names] = results[ names @@ -523,6 +605,7 @@ def _init_pm(): "heat_storage": dict(), "dsm": dict(), "HV_requirements": dict(), + "gen_hp_14a": dict(), # Virtual generators for §14a heat pump support "baseMVA": 1, "source_version": 2, "shunt": dict(), @@ -537,6 +620,7 @@ def _init_pm(): "heatpumps": dict(), "dsm": dict(), "HV_requirements": dict(), + "gen_hp_14a": dict(), # Timeseries for virtual HP support generators "num_steps": int, }, } @@ -1236,6 +1320,95 @@ def _build_heatpump(psa_net, pm, edisgo_obj, s_base, flexible_hps): } +def _build_gen_hp_14a_support(psa_net, pm, edisgo_obj, s_base, flexible_hps, curtailment_14a): + """ + Build virtual generator dictionary for §14a heat pump support and add it to + PowerModels dictionary 'pm'. + + Creates one virtual generator per heat pump at the same bus. The generator + can reduce the net electrical load to simulate §14a curtailment. + + Parameters + ---------- + psa_net : :pypsa:`PyPSA.Network` + :pypsa:`PyPSA.Network` representation of network. + pm : dict + (PowerModels) dictionary. + edisgo_obj : :class:`~.EDisGo` + s_base : int + Base value of apparent power for per unit system. + flexible_hps : :numpy:`numpy.ndarray` or list + Array containing all heat pumps that allow for flexible operation. + curtailment_14a : dict + Dictionary with §14a EnWG curtailment settings. + + """ + # Extract curtailment settings + p_min_14a = curtailment_14a.get("max_power_mw", 0.0042) # MW + max_hours_per_day = curtailment_14a.get("max_hours_per_day", 2.0) # hours + specific_components = curtailment_14a.get("components", []) + + # Filter heat pumps if specific components are defined + if len(specific_components) > 0: + hps_14a = np.intersect1d(flexible_hps, specific_components) + else: + hps_14a = flexible_hps + + if len(hps_14a) == 0: + logger.warning("No heat pumps selected for §14a curtailment.") + return + + heat_df = psa_net.loads.loc[hps_14a] + hp_p_nom = edisgo_obj.topology.loads_df.p_set[hps_14a] + + # Filter out heat pumps with nominal power <= §14a minimum + # These cannot be curtailed to the minimum and would make constraints infeasible + hps_eligible = [hp for hp in hps_14a if hp_p_nom[hp] > p_min_14a] + + if len(hps_eligible) < len(hps_14a): + excluded_hps = set(hps_14a) - set(hps_eligible) + logger.warning( + f"Excluded {len(excluded_hps)} heat pump(s) from §14a curtailment due to " + f"nominal power <= {p_min_14a*1000:.1f} kW: {excluded_hps}" + ) + + if len(hps_eligible) == 0: + logger.warning("No heat pumps eligible for §14a curtailment after filtering by minimum power.") + return + + # Create mapping from HP name to its index in pm["heatpumps"] + # The heatpumps dict is built by iterating over flexible_hps with indices 1, 2, 3, ... + hp_name_to_index = {hp_name: i + 1 for i, hp_name in enumerate(flexible_hps)} + + for hp_i, hp_name in enumerate(hps_eligible): + # Bus of the heat pump + idx_bus = _mapping(psa_net, edisgo_obj, heat_df.bus[hp_name]) + + # Nominal power of HP + p_nominal = hp_p_nom[hp_name] # MW + + # Maximum support = difference between nominal and §14a limit + # This is how much the load can be virtually reduced + p_max_support = p_nominal - p_min_14a # Now guaranteed > 0 + + pm["gen_hp_14a"][str(hp_i + 1)] = { + "name": f"hp_14a_support_{hp_name}", + "gen_bus": idx_bus, + "pmin": 0.0, + "pmax": p_max_support / s_base, + "qmin": 0.0, + "qmax": 0.0, + "pf": 1.0, + "sign": 1, + "gen_status": 1, + "hp_name": hp_name, # Reference to heat pump + "hp_index": hp_name_to_index[hp_name], # Correct index in heatpumps dict + "p_min_14a": p_min_14a / s_base, # §14a minimum power + "max_hours_per_day": max_hours_per_day, # Time budget + "index": hp_i + 1, + } + + def _build_heat_storage(psa_net, pm, edisgo_obj, s_base, flexible_hps, opf_version): """ Build heat storage dictionary and add it to PowerModels dictionary 'pm'. @@ -1639,6 +1812,7 @@ def _build_timeseries( "load", "electromobility", "heatpumps", + "gen_hp_14a", "dsm", "HV_requirements", ]: @@ -1891,6 +2065,11 @@ def _build_component_timeseries( "pd": p_set[comp].values.tolist(), "cop": cop[comp].values.tolist(), } + elif kind == "gen_hp_14a": + # Timeseries for virtual §14a support generators (currently just bounds) + # The power bounds are static (defined in gen_hp_14a dict) + # No time-varying timeseries needed for now + pass elif kind == "dsm": if len(flexible_loads) > 0: p_set = (edisgo_obj.dsm.p_max[flexible_loads] / s_base).round(20) diff --git a/edisgo/opf/eDisGo_OPF.jl/src/core/base.jl b/edisgo/opf/eDisGo_OPF.jl/src/core/base.jl index 3704f4c98..b37bd220b 100644 --- a/edisgo/opf/eDisGo_OPF.jl/src/core/base.jl +++ b/edisgo/opf/eDisGo_OPF.jl/src/core/base.jl @@ -139,6 +139,14 @@ function ref_add_core!(ref::Dict{Symbol,Any}) end nw_ref[:bus_hps] = bus_hps + bus_gen_hp_14a = Dict((i, Int[]) for (i,bus) in nw_ref[:bus]) + if haskey(nw_ref, :gen_hp_14a) + for (i,gen) in nw_ref[:gen_hp_14a] + push!(bus_gen_hp_14a[gen["gen_bus"]], i) + end + end + nw_ref[:bus_gen_hp_14a] = bus_gen_hp_14a + bus_gens_nd = Dict((i, Int[]) for (i,bus) in nw_ref[:bus]) for (i,gen) in nw_ref[:gen_nd] push!(bus_gens_nd[gen["gen_bus"]], i) diff --git a/edisgo/opf/eDisGo_OPF.jl/src/core/constraint_hp_14a.jl b/edisgo/opf/eDisGo_OPF.jl/src/core/constraint_hp_14a.jl new file mode 100644 index 000000000..ae2f62663 --- /dev/null +++ b/edisgo/opf/eDisGo_OPF.jl/src/core/constraint_hp_14a.jl @@ -0,0 +1,151 @@ +""" +Constraints for §14a EnWG heat pump curtailment using virtual generators. + +This file implements §14a curtailment by modeling virtual generators at each +heat pump bus. The virtual generator can reduce the net electrical load, +simulating the effect of curtailment while maintaining a minimum power level. +""" + +""" + constraint_hp_14a_binary_coupling(pm, i, nw) + +Couples binary variable with power variable for §14a support generator. +When binary variable is 0, power must be 0. When binary is 1, power can be between 0 and pmax. +This ensures time budget tracking works correctly. + +# Arguments +- `pm::AbstractBFModelEdisgo`: PowerModels model +- `i::Int`: Generator index +- `nw::Int`: Network (timestep) index +""" +function constraint_hp_14a_binary_coupling(pm::AbstractBFModelEdisgo, i::Int, nw::Int=nw_id_default) + gen_hp14a = PowerModels.ref(pm, nw, :gen_hp_14a, i) + p_hp14a = PowerModels.var(pm, nw, :p_hp14a, i) + z_hp14a = PowerModels.var(pm, nw, :z_hp14a, i) + + # p ≤ pmax × z (if z=0 then p=0, if z=1 then p can be 0..pmax) + JuMP.@constraint(pm.model, p_hp14a <= gen_hp14a["pmax"] * z_hp14a) +end + + +""" + constraint_hp_14a_min_net_load(pm, i, nw) + +Ensures that the net electrical load (heat pump load - virtual generator support) +stays above the §14a minimum power level (typically 4.2 kW = 0.0042 MW). + +# Arguments +- `pm::AbstractBFModelEdisgo`: PowerModels model +- `i::Int`: Virtual generator index +- `nw::Int`: Network (timestep) index +""" +function constraint_hp_14a_min_net_load(pm::AbstractBFModelEdisgo, i::Int, nw::Int=nw_id_default) + gen_hp14a = PowerModels.ref(pm, nw, :gen_hp_14a, i) + hp_idx = gen_hp14a["hp_index"] + hp = PowerModels.ref(pm, nw, :heatpumps, hp_idx) + + # Electrical power demand of heat pump (thermal demand / COP) + p_hp_load = hp["pd"] / hp["cop"] + + # Virtual generator support + p_hp14a = PowerModels.var(pm, nw, :p_hp14a, i) + + # §14a minimum power (per unit) + p_min_14a = gen_hp14a["p_min_14a"] + + # Maximum support capacity (matches Python field name "pmax") + p_max_support = gen_hp14a["pmax"] + + # Net load must stay ≥ minimum net load allowed + # The minimum is the LOWER of: current load or §14a limit + # This handles cases where HP draws less than 4.2 kW (e.g., 3 kW due to low thermal demand) + # p_hp_load - p_hp14a ≥ min(p_hp_load, p_min_14a) + # + # Special cases: + # - If p_max_support ≈ 0 (HP too small), force virtual gen to zero + # - If HP is off (p_hp_load ≈ 0), no support needed + if p_max_support < 1e-6 + # Heat pump too small for §14a curtailment, disable virtual generator + JuMP.@constraint(pm.model, p_hp14a == 0.0) + elseif p_hp_load > 1e-6 + # Normal case: enforce minimum net load + # Net load cannot go below current load or §14a minimum, whichever is lower + p_min_net = min(p_hp_load, p_min_14a) + JuMP.@constraint(pm.model, p_hp_load - p_hp14a >= p_min_net) + else + # Heat pump is off, no support needed + JuMP.@constraint(pm.model, p_hp14a == 0.0) + end +end + + +""" + constraint_hp_14a_time_budget_daily(pm, day_start, day_end, i) + +Limits the usage of §14a support generator to a maximum number of hours per day. +This is implemented by counting the number of timesteps where the binary variable is 1. + +# Arguments +- `pm::AbstractBFModelEdisgo`: PowerModels model +- `day_start::Int`: First timestep of the day +- `day_end::Int`: Last timestep of the day +- `i::Int`: Virtual generator index +""" +function constraint_hp_14a_time_budget_daily(pm::AbstractBFModelEdisgo, day_start::Int, day_end::Int, i::Int) + # Get time step duration in hours + if haskey(PowerModels.ref(pm, day_start), :time_elapsed) + time_elapsed = PowerModels.ref(pm, day_start, :time_elapsed) + else + Memento.warn(_LOGGER, "network data should specify time_elapsed, using 1.0 as default") + time_elapsed = 1.0 + end + + gen_hp14a = PowerModels.ref(pm, day_start, :gen_hp_14a, i) + max_hours = gen_hp14a["max_hours_per_day"] + + # Collect binary variables for all timesteps of the day + z_hp14a_day = [PowerModels.var(pm, t, :z_hp14a, i) for t in day_start:day_end] + + # Maximum number of active timesteps + max_active_steps = max_hours / time_elapsed + + # Sum of binary variables must not exceed budget + JuMP.@constraint(pm.model, sum(z_hp14a_day) <= max_active_steps) +end + + +""" + constraint_hp_14a_time_budget_total(pm, i, nws) + +Alternative to daily budget: Limits total usage over entire optimization horizon. +Can be used instead of daily budget for simpler formulation. + +# Arguments +- `pm::AbstractBFModelEdisgo`: PowerModels model +- `i::Int`: Virtual generator index +- `nws`: Network IDs (all timesteps) +""" +function constraint_hp_14a_time_budget_total(pm::AbstractBFModelEdisgo, i::Int, nws) + # Get time step duration + if haskey(PowerModels.ref(pm, first(nws)), :time_elapsed) + time_elapsed = PowerModels.ref(pm, first(nws), :time_elapsed) + else + time_elapsed = 1.0 + end + + gen_hp14a = PowerModels.ref(pm, first(nws), :gen_hp_14a, i) + max_hours_per_day = gen_hp14a["max_hours_per_day"] + + # Calculate total hours available (number of days × hours per day) + num_timesteps = length(nws) + num_days = ceil(num_timesteps * time_elapsed / 24.0) + total_max_hours = max_hours_per_day * num_days + + # Collect all binary variables + z_hp14a_all = [PowerModels.var(pm, t, :z_hp14a, i) for t in nws] + + # Total active timesteps + max_active_steps = total_max_hours / time_elapsed + + JuMP.@constraint(pm.model, sum(z_hp14a_all) <= max_active_steps) +end diff --git a/edisgo/opf/eDisGo_OPF.jl/src/core/constraint_template.jl b/edisgo/opf/eDisGo_OPF.jl/src/core/constraint_template.jl index f0f05b6ae..1c6966bbf 100644 --- a/edisgo/opf/eDisGo_OPF.jl/src/core/constraint_template.jl +++ b/edisgo/opf/eDisGo_OPF.jl/src/core/constraint_template.jl @@ -11,6 +11,7 @@ function constraint_power_balance_bf(pm::AbstractBFModelEdisgo, i::Int; nw::Int= bus_dsm = PowerModels.ref(pm, nw, :bus_dsm, i) bus_hps = PowerModels.ref(pm, nw, :bus_hps, i) bus_cps = PowerModels.ref(pm, nw, :bus_cps, i) + bus_gen_hp_14a = get(PowerModels.ref(pm, nw), :bus_gen_hp_14a, Dict())[i] = get(get(PowerModels.ref(pm, nw), :bus_gen_hp_14a, Dict()), i, []) branch_r = Dict(k => PowerModels.ref(pm, nw, :branch, k, "br_r") for k in bus_lines_to) @@ -34,7 +35,7 @@ function constraint_power_balance_bf(pm::AbstractBFModelEdisgo, i::Int; nw::Int= bus_gen_d_pf = Dict(k => tan(acos(PowerModels.ref(pm, nw, :gen, k, "pf")))*PowerModels.ref(pm, nw, :gen, k, "sign") for k in bus_gens) bus_loads_pf = Dict(k => tan(acos(PowerModels.ref(pm, nw, :load, k, "pf")))*PowerModels.ref(pm, nw, :load, k, "sign") for k in bus_loads) - constraint_power_balance(pm, nw, i, bus_gens, bus_gens_nd, bus_gens_slack, bus_loads, bus_arcs_to, bus_arcs_from, bus_lines_to, bus_storage, bus_pg, bus_qg, bus_pg_nd, bus_qg_nd, bus_pd, bus_qd, branch_r, branch_x, bus_dsm, bus_hps, bus_cps, bus_storage_pf, bus_dsm_pf, bus_hps_pf, bus_cps_pf, bus_gen_nd_pf, bus_gen_d_pf, bus_loads_pf, branch_strg_pf) + constraint_power_balance(pm, nw, i, bus_gens, bus_gens_nd, bus_gens_slack, bus_loads, bus_arcs_to, bus_arcs_from, bus_lines_to, bus_storage, bus_pg, bus_qg, bus_pg_nd, bus_qg_nd, bus_pd, bus_qd, branch_r, branch_x, bus_dsm, bus_hps, bus_cps, bus_gen_hp_14a, bus_storage_pf, bus_dsm_pf, bus_hps_pf, bus_cps_pf, bus_gen_nd_pf, bus_gen_d_pf, bus_loads_pf, branch_strg_pf) end "" diff --git a/edisgo/opf/eDisGo_OPF.jl/src/core/objective.jl b/edisgo/opf/eDisGo_OPF.jl/src/core/objective.jl index 5a8470750..94d8e3902 100644 --- a/edisgo/opf/eDisGo_OPF.jl/src/core/objective.jl +++ b/edisgo/opf/eDisGo_OPF.jl/src/core/objective.jl @@ -28,7 +28,13 @@ function objective_min_losses_slacks(pm::AbstractBFModelEdisgo) phps = Dict(n => PowerModels.var(pm, n, :phps) for n in nws) phps2 = Dict(n => PowerModels.var(pm, n, :phps2) for n in nws) phss = Dict(n => PowerModels.var(pm, n, :phss) for n in nws) + + # §14a virtual generators + p_hp14a = Dict(n => get(PowerModels.var(pm, n), :p_hp14a, Dict()) for n in nws) + factor_slacks = 0.6 + factor_14a = 0.5 # Weight for §14a curtailment (between slacks and losses) + return JuMP.@objective(pm.model, Min, (1-factor_slacks) * sum(sum(ccm[n][b] * r[n][b] for (b,i,j) in PowerModels.ref(pm, n, :arcs_from) ) for n in nws) # minimize line losses incl. storage losses + factor_slacks * sum(sum(pgc[n][i] for i in keys(PowerModels.ref(pm,1 , :gen_nd))) for n in nws) # minimize non-dispatchable curtailment @@ -37,6 +43,7 @@ function objective_min_losses_slacks(pm::AbstractBFModelEdisgo) + factor_slacks * sum(sum(pcps[n][i] for i in keys(PowerModels.ref(pm,1 , :electromobility))) for n in nws) # minimize cp load sheddin + factor_slacks * sum(sum(phps[n][i] for i in keys(PowerModels.ref(pm,1 , :heatpumps))) for n in nws) # minimize hp load shedding + 1e4 * sum(sum(phss[n][i] + phps2[n][i] for i in keys(PowerModels.ref(pm, 1 , :heatpumps))) for n in nws) + + factor_14a * sum(sum(p_hp14a[n][i] for i in keys(p_hp14a[n])) for n in nws) # minimize §14a curtailment support ) end @@ -48,10 +55,17 @@ function objective_min_line_loading_max(pm::AbstractBFModelEdisgo) l = Dict(n => Dict(i => get(branch, "length", 1.0) for (i,branch) in PowerModels.ref(pm, n, :branch)) for n in nws) c = Dict(n => Dict(i => get(branch, "cost", 1.0) for (i,branch) in PowerModels.ref(pm, n, :branch)) for n in nws) storage = Dict(i => get(branch, "storage", 1.0) for (i,branch) in PowerModels.ref(pm, 1, :branch)) + + # §14a virtual generators + p_hp14a = Dict(n => get(PowerModels.var(pm, n), :p_hp14a, Dict()) for n in nws) + factor_ll = 0.1 + factor_14a = 0.05 # Small penalty for §14a usage in line loading optimization + return JuMP.@objective(pm.model, Min, (1-factor_ll) * sum(sum(ccm[n][b] * r[n][b] for (b,i,j) in PowerModels.ref(pm, n, :arcs_from)) for n in nws) # minimize line losses + factor_ll * sum((ll[(b,i,j)]-1) * c[1][b] * l[1][b] for (b,i,j) in PowerModels.ref(pm, 1, :arcs_from) if storage[b] == 0) # minimize max line loading + + factor_14a * sum(sum(p_hp14a[n][i] for i in keys(p_hp14a[n])) for n in nws) # minimize §14a curtailment support ) end diff --git a/edisgo/opf/eDisGo_OPF.jl/src/core/variables.jl b/edisgo/opf/eDisGo_OPF.jl/src/core/variables.jl index 5aec7f71c..d4f172094 100644 --- a/edisgo/opf/eDisGo_OPF.jl/src/core/variables.jl +++ b/edisgo/opf/eDisGo_OPF.jl/src/core/variables.jl @@ -395,6 +395,37 @@ function variable_cp_energy(pm::AbstractPowerModel; nw::Int=nw_id_default, bound report && PowerModels.sol_component_value(pm, nw, :electromobility, :cpe, PowerModels.ids(pm, nw, :electromobility), cpe) end +"§14a virtual generator continuous variables for curtailment support power" +function variable_gen_hp_14a_power(pm::AbstractPowerModel; nw::Int=nw_id_default, bounded::Bool=true, report::Bool=true) + p_hp14a = PowerModels.var(pm, nw)[:p_hp14a] = JuMP.@variable(pm.model, + [i in PowerModels.ids(pm, nw, :gen_hp_14a)], + base_name="$(nw)_p_hp14a", + lower_bound = 0.0 + ) + + if bounded + for (i, gen) in PowerModels.ref(pm, nw, :gen_hp_14a) + JuMP.set_upper_bound(p_hp14a[i], gen["pmax"]) + end + end + + if report + println(" 🔍 JULIA: Reporting gen_hp_14a power for nw=$nw, ids=$(PowerModels.ids(pm, nw, :gen_hp_14a))") + PowerModels.sol_component_value(pm, nw, :gen_hp_14a, :p, PowerModels.ids(pm, nw, :gen_hp_14a), p_hp14a) + end +end + +"§14a virtual generator binary variables for time budget tracking" +function variable_gen_hp_14a_binary(pm::AbstractPowerModel; nw::Int=nw_id_default, report::Bool=true) + z_hp14a = PowerModels.var(pm, nw)[:z_hp14a] = JuMP.@variable(pm.model, + [i in PowerModels.ids(pm, nw, :gen_hp_14a)], + base_name="$(nw)_z_hp14a", + binary = true + ) + + report && PowerModels.sol_component_value(pm, nw, :gen_hp_14a, :z, PowerModels.ids(pm, nw, :gen_hp_14a), z_hp14a) +end + "slack variables for grid restrictions" function variable_slack_grid_restrictions(pm::AbstractBFModelEdisgo; kwargs...) eDisGo_OPF.variable_hp_slack(pm; kwargs...) diff --git a/edisgo/opf/eDisGo_OPF.jl/src/eDisGo_OPF.jl b/edisgo/opf/eDisGo_OPF.jl/src/eDisGo_OPF.jl index 33df80742..ba208f462 100644 --- a/edisgo/opf/eDisGo_OPF.jl/src/eDisGo_OPF.jl +++ b/edisgo/opf/eDisGo_OPF.jl/src/eDisGo_OPF.jl @@ -18,6 +18,7 @@ include("core/types.jl") include("core/base.jl") include("core/constraint.jl") include("core/constraint_template.jl") +include("core/constraint_hp_14a.jl") include("core/data.jl") include("core/objective.jl") include("core/solution.jl") diff --git a/edisgo/opf/eDisGo_OPF.jl/src/form/bf.jl b/edisgo/opf/eDisGo_OPF.jl/src/form/bf.jl index 1245fe7bf..b9ca87b3a 100644 --- a/edisgo/opf/eDisGo_OPF.jl/src/form/bf.jl +++ b/edisgo/opf/eDisGo_OPF.jl/src/form/bf.jl @@ -135,7 +135,7 @@ function constraint_max_line_loading(pm::AbstractNCBFModelEdisgo, n::Int) end -function constraint_power_balance(pm::AbstractBFModelEdisgo, n::Int, i, bus_gens, bus_gens_nd, bus_gens_slack, bus_loads, bus_arcs_to, bus_arcs_from, bus_lines_to, bus_storage, bus_pg, bus_qg, bus_pg_nd, bus_qg_nd, bus_pd, bus_qd, branch_r, branch_x, bus_dsm, bus_hps, bus_cps, bus_storage_pf, bus_dsm_pf, bus_hps_pf, bus_cps_pf, bus_gen_nd_pf, bus_gen_d_pf, bus_loads_pf, branch_strg_pf) +function constraint_power_balance(pm::AbstractBFModelEdisgo, n::Int, i, bus_gens, bus_gens_nd, bus_gens_slack, bus_loads, bus_arcs_to, bus_arcs_from, bus_lines_to, bus_storage, bus_pg, bus_qg, bus_pg_nd, bus_qg_nd, bus_pd, bus_qd, branch_r, branch_x, bus_dsm, bus_hps, bus_cps, bus_gen_hp_14a, bus_storage_pf, bus_dsm_pf, bus_hps_pf, bus_cps_pf, bus_gen_nd_pf, bus_gen_d_pf, bus_loads_pf, branch_strg_pf) pt = get(PowerModels.var(pm, n), :p, Dict()); PowerModels._check_var_keys(pt, bus_arcs_to, "active power", "branch") qt = get(PowerModels.var(pm, n), :q, Dict()); PowerModels._check_var_keys(qt, bus_arcs_to, "reactive power", "branch") pf = get(PowerModels.var(pm, n), :p, Dict()); PowerModels._check_var_keys(pf, bus_arcs_from, "active power", "branch") @@ -147,6 +147,7 @@ function constraint_power_balance(pm::AbstractBFModelEdisgo, n::Int, i, bus_gens pdsm = get(PowerModels.var(pm, n), :pdsm, Dict()); PowerModels._check_var_keys(pdsm, bus_dsm, "active power", "dsm") php = get(PowerModels.var(pm, n), :php, Dict()); PowerModels._check_var_keys(php, bus_hps, "active power", "heatpumps") pcp = get(PowerModels.var(pm, n), :pcp, Dict()); PowerModels._check_var_keys(pcp, bus_cps, "active power", "electromobility") + p_hp14a = get(PowerModels.var(pm, n), :p_hp14a, Dict()) # §14a virtual generators if PowerModels.ref(pm, 1, :opf_version) in(2, 4) # Eq. (3.3iii), (3.4iii) pgens = get(PowerModels.var(pm, n), :pgens, Dict()); PowerModels._check_var_keys(pgens, bus_gens, "active power slack", "curtailment") @@ -171,6 +172,7 @@ function constraint_power_balance(pm::AbstractBFModelEdisgo, n::Int, i, bus_gens + sum(pdsm[dsm] for dsm in bus_dsm) + sum(php[hp] - phps[hp] for hp in bus_hps) + sum(pcp[cp] - pcps[cp] for cp in bus_cps) + - sum(p_hp14a[g] for g in bus_gen_hp_14a) # Virtual generators reduce net load ) cstr_q = JuMP.@constraint(pm.model, sum(qt[a] for a in bus_arcs_to) @@ -187,7 +189,8 @@ function constraint_power_balance(pm::AbstractBFModelEdisgo, n::Int, i, bus_gens + sum(pgens[g] * bus_gen_d_pf[g] for g in bus_gens) + sum(pdsm[dsm] * bus_dsm_pf[dsm] for dsm in bus_dsm) + sum((php[hp] - phps[hp]) * bus_hps_pf[hp] for hp in bus_hps) - + sum((pcp[cp] - pcps[cp]) * bus_cps_pf[cp] for cp in bus_cps) + + sum((pcp[cp] - pcps[cp]) * bus_cps_pf[cp] for hp in bus_cps) + # §14a generators have pf=1, q=0 ) else # Eq. (3.3ii), (3.4ii) cstr_p = JuMP.@constraint(pm.model, @@ -203,6 +206,7 @@ function constraint_power_balance(pm::AbstractBFModelEdisgo, n::Int, i, bus_gens + sum(pdsm[dsm] for dsm in bus_dsm) + sum(php[hp] for hp in bus_hps) + sum(pcp[cp] for cp in bus_cps) + - sum(p_hp14a[g] for g in bus_gen_hp_14a) # Virtual generators reduce net load ) cstr_q = JuMP.@constraint(pm.model, sum(qt[a] for a in bus_arcs_to) @@ -217,6 +221,7 @@ function constraint_power_balance(pm::AbstractBFModelEdisgo, n::Int, i, bus_gens + sum(pdsm[dsm] * bus_dsm_pf[dsm] for dsm in bus_dsm) + sum(php[hp] * bus_hps_pf[hp] for hp in bus_hps) + sum(pcp[cp] * bus_cps_pf[cp] for cp in bus_cps) + # §14a generators have pf=1, q=0 ) end diff --git a/edisgo/opf/eDisGo_OPF.jl/src/prob/opf_bf.jl b/edisgo/opf/eDisGo_OPF.jl/src/prob/opf_bf.jl index aeaea8d65..fd5360895 100644 --- a/edisgo/opf/eDisGo_OPF.jl/src/prob/opf_bf.jl +++ b/edisgo/opf/eDisGo_OPF.jl/src/prob/opf_bf.jl @@ -30,6 +30,12 @@ function build_mn_opf_bf_flex(pm::AbstractBFModelEdisgo) eDisGo_OPF.variable_cp_power(pm, nw=n) # Eq. (3.27), (3.28) eDisGo_OPF.variable_dsm_storage_power(pm, nw=n) # Eq. (3.34), (3.35) eDisGo_OPF.variable_slack_gen(pm, nw=n) # keine Bounds für Slack Generator + + # §14a EnWG virtual generators for heat pump support + if haskey(PowerModels.ref(pm, n), :gen_hp_14a) && !isempty(PowerModels.ref(pm, n, :gen_hp_14a)) + eDisGo_OPF.variable_gen_hp_14a_power(pm, nw=n) + eDisGo_OPF.variable_gen_hp_14a_binary(pm, nw=n) + end if PowerModels.ref(pm, 1, :opf_version) in(3, 4) # Nicht Teil der MA eDisGo_OPF.variable_slack_HV_requirements(pm, nw=n) @@ -57,6 +63,14 @@ function build_mn_opf_bf_flex(pm::AbstractBFModelEdisgo) for i in PowerModels.ids(pm, :heatpumps, nw=n) eDisGo_OPF.constraint_hp_operation(pm, i, n) # Eq. (3.19) end + + # §14a EnWG constraints for virtual generators + if haskey(PowerModels.ref(pm, n), :gen_hp_14a) && !isempty(PowerModels.ref(pm, n, :gen_hp_14a)) + for i in PowerModels.ids(pm, :gen_hp_14a, nw=n) + eDisGo_OPF.constraint_hp_14a_binary_coupling(pm, i, n) + eDisGo_OPF.constraint_hp_14a_min_net_load(pm, i, n) + end + end end @@ -91,6 +105,45 @@ function build_mn_opf_bf_flex(pm::AbstractBFModelEdisgo) n_1 = n_2 end + # §14a EnWG daily time budget constraints + if haskey(PowerModels.ref(pm, 1), :gen_hp_14a) && !isempty(PowerModels.ref(pm, 1, :gen_hp_14a)) + println("\n" * "="^80) + println("🔍 JULIA DEBUG: §14a Generators") + println("="^80) + + gen_hp_14a_dict = PowerModels.ref(pm, 1, :gen_hp_14a) + println("Number of gen_hp_14a entries: ", length(gen_hp_14a_dict)) + + # Show first 5 generators + count = 0 + for (idx, gen) in gen_hp_14a_dict + count += 1 + if count <= 5 + println(" [$idx]: hp_name=$(get(gen, "hp_name", "N/A")), hp_index=$(get(gen, "hp_index", "N/A")), pmax=$(get(gen, "pmax", "N/A"))") + end + end + println("="^80 * "\n") + + # Determine timesteps per day based on time_elapsed (in hours) + n_first = network_ids[1] + time_elapsed = PowerModels.ref(pm, n_first, :time_elapsed) + timesteps_per_day = Int(round(24.0 / time_elapsed)) + + # Group network_ids into days + for day_start_idx in 1:timesteps_per_day:length(network_ids) + day_end_idx = min(day_start_idx + timesteps_per_day - 1, length(network_ids)) + day_network_ids = network_ids[day_start_idx:day_end_idx] + + # Apply daily time budget constraint for each §14a generator + for i in PowerModels.ids(pm, :gen_hp_14a, nw=network_ids[1]) + # Call with correct argument order: (pm, day_start, day_end, i) + eDisGo_OPF.constraint_hp_14a_time_budget_daily(pm, day_network_ids[1], day_network_ids[end], i) + end + end + else + println("\n⚠ JULIA DEBUG: No gen_hp_14a found or empty!\n") + end + # OBJECTIVE FUNCTION if PowerModels.ref(pm, 1, :opf_version) == 1 #eDisGo_OPF.objective_min_losses(pm) diff --git a/edisgo/opf/powermodels_opf.py b/edisgo/opf/powermodels_opf.py index 85da160a8..b4e62fb2a 100644 --- a/edisgo/opf/powermodels_opf.py +++ b/edisgo/opf/powermodels_opf.py @@ -26,6 +26,7 @@ def pm_optimize( method: str = "soc", warm_start: bool = False, silence_moi: bool = False, + curtailment_14a: bool = False, ) -> None: """ Run OPF for edisgo object in julia subprocess and write results of OPF to edisgo @@ -93,6 +94,11 @@ def pm_optimize( hence there will be no logging coming from julia subprocess in python process. Default: False. + curtailment_14a : bool + If True, enables §14a EnWG curtailment for heat pumps with virtual + generators. Heat pumps can be curtailed down to 4.2 kW with time budget + constraints. + Default: False. save_heat_storage : bool Indicates whether to save results of heat storage variables from the optimization to eDisGo object. @@ -118,6 +124,7 @@ def pm_optimize( flexible_loads=flexible_loads, flexible_storage_units=flexible_storage_units, opf_version=opf_version, + curtailment_14a=curtailment_14a, ) def _convert(o): From 51b73aae19916aa5759ccde5d7421de44e673c7c Mon Sep 17 00:00:00 2001 From: joda9 Date: Thu, 11 Dec 2025 15:44:26 +0100 Subject: [PATCH 34/43] =?UTF-8?q?feat:=20Add=20=C2=A714a=20support=20for?= =?UTF-8?q?=20charging=20point=20curtailment=20using=20virtual=20generator?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- edisgo/io/powermodels_io.py | 142 ++++++++++++++++ .../src/core/constraint_cp_14a.jl | 151 ++++++++++++++++++ .../opf/eDisGo_OPF.jl/src/core/objective.jl | 12 +- 3 files changed, 301 insertions(+), 4 deletions(-) create mode 100644 edisgo/opf/eDisGo_OPF.jl/src/core/constraint_cp_14a.jl diff --git a/edisgo/io/powermodels_io.py b/edisgo/io/powermodels_io.py index 91b9e9f4e..66178d855 100644 --- a/edisgo/io/powermodels_io.py +++ b/edisgo/io/powermodels_io.py @@ -135,6 +135,12 @@ def to_powermodels( if len(all_hps) > 0: logger.info("§14a heat pump curtailment will be optimized.") opf_flex.append("hp_14a") + + # Check for charging points + all_cps = edisgo_object.topology.charging_points_df + if len(all_cps) > 0: + logger.info("§14a charging point curtailment will be optimized.") + opf_flex.append("cp_14a") pm["flexibilities"] = opf_flex logger.info("Transforming busses into PowerModels dictionary format.") @@ -196,6 +202,16 @@ def to_powermodels( _build_gen_hp_14a_support( psa_net, pm, edisgo_object, s_base, all_hps, curtailment_14a_config ) + + # Build §14a support for charging points + # Use ALL charging points for §14a, not just flexible ones + all_cps = edisgo_object.topology.charging_points_df.index.to_numpy() + if len(all_cps) > 0: + logger.info(f"Creating virtual generators for §14a charging point support ({len(all_cps)} CPs).") + _build_gen_cp_14a_support( + psa_net, pm, edisgo_object, s_base, all_cps, curtailment_14a_config + ) + if len(flexible_loads) > 0: logger.info("Transforming DSM loads into PowerModels dictionary format.") flexible_loads = _build_dsm(edisgo_object, psa_net, pm, s_base, flexible_loads) @@ -318,6 +334,7 @@ def from_powermodels( "storage": ["storage", "pf"], "dsm": ["dsm", "pdsm"], "hp_14a": ["gen_hp_14a", "p"], # §14a virtual generators (uses "p" not "php14a") + "cp_14a": ["gen_cp_14a", "p"], # §14a virtual generators for charging points } timesteps = pd.Series([int(k) for k in pm["nw"].keys()]).sort_values().values @@ -392,6 +409,15 @@ def from_powermodels( names ].values print(f" ✓ Written successfully!") + + elif flex == "gen_cp_14a": + # §14a virtual generators for CPs: write as positive generation + print(f" → Writing {len(names)} gen_cp_14a generators to generators_active_power") + print(f" → Sample values: {results.iloc[0, :3].to_dict()}") + edisgo_object.timeseries._generators_active_power.loc[:, names] = results[ + names + ].values + print(f" ✓ Written successfully!") elif flex in ["heatpumps", "electromobility"]: edisgo_object.timeseries._loads_active_power.loc[:, names] = results[ names @@ -606,6 +632,7 @@ def _init_pm(): "dsm": dict(), "HV_requirements": dict(), "gen_hp_14a": dict(), # Virtual generators for §14a heat pump support + "gen_cp_14a": dict(), # Virtual generators for §14a charging point support "baseMVA": 1, "source_version": 2, "shunt": dict(), @@ -621,6 +648,7 @@ def _init_pm(): "dsm": dict(), "HV_requirements": dict(), "gen_hp_14a": dict(), # Timeseries for virtual HP support generators + "gen_cp_14a": dict(), # Timeseries for virtual CP support generators "num_steps": int, }, } @@ -1409,6 +1437,120 @@ def _build_gen_hp_14a_support(psa_net, pm, edisgo_obj, s_base, flexible_hps, cur } +def _build_gen_cp_14a_support(psa_net, pm, edisgo_obj, s_base, all_cps, curtailment_14a): + """ + Build virtual generator dictionary for §14a charging point support and add it to + PowerModels dictionary 'pm'. + + Creates one virtual generator per charging point at the same bus. The generator + can reduce the net electrical load to simulate §14a curtailment. + + Parameters + ---------- + psa_net : :pypsa:`PyPSA.Network` + :pypsa:`PyPSA.Network` representation of network. + pm : dict + (PowerModels) dictionary. + edisgo_obj : :class:`~.EDisGo` + s_base : int + Base value of apparent power for per unit system. + all_cps : :numpy:`numpy.ndarray` or list + Array containing all charging points in the grid (for §14a curtailment). + curtailment_14a : dict + Dictionary with §14a EnWG curtailment settings. + + """ + # Extract curtailment settings + p_min_14a = curtailment_14a.get("max_power_mw", 0.0042) # MW (same as HPs) + max_hours_per_day = curtailment_14a.get("max_hours_per_day", 2.0) # hours + specific_components = curtailment_14a.get("components", []) + + # Filter charging points if specific components are defined + if len(specific_components) > 0: + cps_14a = np.intersect1d(all_cps, specific_components) + else: + cps_14a = all_cps + + if len(cps_14a) == 0: + logger.warning("No charging points selected for §14a curtailment.") + return + + cp_df = edisgo_obj.topology.charging_points_df.loc[cps_14a] + cp_p_nom = cp_df.p_set # Nominal charging power in MW + + # Filter out CPs with nominal power <= §14a minimum + # These cannot be curtailed to the minimum and would make constraints infeasible + cps_eligible = [cp for cp in cps_14a if cp_p_nom[cp] > p_min_14a] + + if len(cps_eligible) < len(cps_14a): + excluded_cps = set(cps_14a) - set(cps_eligible) + logger.warning( + f"Excluded {len(excluded_cps)} charging point(s) from §14a curtailment due to " + f"nominal power <= {p_min_14a*1000:.1f} kW: {excluded_cps}" + ) + + if len(cps_eligible) == 0: + logger.warning("No charging points eligible for §14a curtailment after filtering by minimum power.") + return + + # NOTE: §14a curtailment can only work for CPs that are in pm["electromobility"] + # These are the flexible CPs. Check which eligible CPs are actually flexible. + flexible_cp_indices = list(pm["electromobility"].keys()) if "electromobility" in pm and len(pm["electromobility"]) > 0 else [] + + if len(flexible_cp_indices) == 0: + logger.warning("No flexible charging points in PowerModels dict - §14a curtailment cannot be applied to CPs.") + logger.warning("CPs must be flexible (have electromobility optimization) to support §14a curtailment.") + return + + # Get the CP names from electromobility dict to create proper index mapping + flexible_cp_names = [pm["electromobility"][idx]["name"] for idx in flexible_cp_indices] + cp_name_to_index = {cp_name: int(idx) for idx, cp_dict in pm["electromobility"].items() + if (cp_name := cp_dict["name"])} + + # Only use CPs that are both eligible for §14a AND flexible + cps_final = [cp for cp in cps_eligible if cp in cp_name_to_index] + + if len(cps_final) < len(cps_eligible): + non_flexible = set(cps_eligible) - set(cps_final) + logger.warning( + f"{len(non_flexible)} eligible CP(s) are not flexible and cannot use §14a: {non_flexible}" + ) + + if len(cps_final) == 0: + logger.warning("No flexible CPs available for §14a curtailment.") + return + + logger.info(f"Creating §14a support for {len(cps_final)} flexible charging points.") + + for cp_i, cp_name in enumerate(cps_final): + # Bus of the charging point + idx_bus = _mapping(psa_net, edisgo_obj, cp_df.loc[cp_name, 'bus']) + + # Nominal power of CP + p_nominal = cp_p_nom[cp_name] # MW + + # Maximum support = difference between nominal and §14a limit + # This is how much the load can be virtually reduced + p_max_support = p_nominal - p_min_14a # Now guaranteed > 0 + + pm["gen_cp_14a"][str(cp_i + 1)] = { + "name": f"cp_14a_support_{cp_name}", + "gen_bus": idx_bus, + "pmin": 0.0, + "pmax": p_max_support / s_base, + "qmin": 0.0, + "qmax": 0.0, + "pf": 1.0, + "sign": 1, + "gen_status": 1, + "cp_name": cp_name, # Reference to charging point + "cp_index": cp_name_to_index[cp_name], # Correct index in electromobility dict + "p_min_14a": p_min_14a / s_base, # §14a minimum power + "max_hours_per_day": max_hours_per_day, # Time budget + "index": cp_i + 1, + } + + def _build_heat_storage(psa_net, pm, edisgo_obj, s_base, flexible_hps, opf_version): """ Build heat storage dictionary and add it to PowerModels dictionary 'pm'. diff --git a/edisgo/opf/eDisGo_OPF.jl/src/core/constraint_cp_14a.jl b/edisgo/opf/eDisGo_OPF.jl/src/core/constraint_cp_14a.jl new file mode 100644 index 000000000..21bd186b7 --- /dev/null +++ b/edisgo/opf/eDisGo_OPF.jl/src/core/constraint_cp_14a.jl @@ -0,0 +1,151 @@ +""" +Constraints for §14a EnWG charging point curtailment using virtual generators. + +This file implements §14a curtailment by modeling virtual generators at each +charging point bus. The virtual generator can reduce the net electrical load, +simulating the effect of curtailment while maintaining a minimum power level. +""" + +""" + constraint_cp_14a_binary_coupling(pm, i, nw) + +Couples binary variable with power variable for §14a support generator. +When binary variable is 0, power must be 0. When binary is 1, power can be between 0 and pmax. +This ensures time budget tracking works correctly. + +# Arguments +- `pm::AbstractBFModelEdisgo`: PowerModels model +- `i::Int`: Generator index +- `nw::Int`: Network (timestep) index +""" +function constraint_cp_14a_binary_coupling(pm::AbstractBFModelEdisgo, i::Int, nw::Int=nw_id_default) + gen_hp14a = PowerModels.ref(pm, nw, :gen_cp_14a, i) + p_hp14a = PowerModels.var(pm, nw, :p_hp14a, i) + z_hp14a = PowerModels.var(pm, nw, :z_hp14a, i) + + # p ≤ pmax × z (if z=0 then p=0, if z=1 then p can be 0..pmax) + JuMP.@constraint(pm.model, p_hp14a <= gen_hp14a["pmax"] * z_hp14a) +end + + +""" + constraint_cp_14a_min_net_load(pm, i, nw) + +Ensures that the net electrical load (charging point load - virtual generator support) +stays above the §14a minimum power level (typically 4.2 kW = 0.0042 MW). + +# Arguments +- `pm::AbstractBFModelEdisgo`: PowerModels model +- `i::Int`: Virtual generator index +- `nw::Int`: Network (timestep) index +""" +function constraint_cp_14a_min_net_load(pm::AbstractBFModelEdisgo, i::Int, nw::Int=nw_id_default) + gen_cp14a = PowerModels.ref(pm, nw, :gen_cp_14a, i) + cp_idx = gen_cp14a["cp_index"] + cp = PowerModels.ref(pm, nw, :electromobility, cp_idx) + + # Electrical power demand of charging point (direct charging power, no COP needed) + p_cp_load = cp["pcp"] # Charging power variable + + # Virtual generator support + p_cp14a = PowerModels.var(pm, nw, :p_cp14a, i) + + # §14a minimum power (per unit) + p_min_14a = gen_cp14a["p_min_14a"] + + # Maximum support capacity (matches Python field name "pmax") + p_max_support = gen_cp14a["pmax"] + + # Net load must stay ≥ minimum net load allowed + # The minimum is the LOWER of: current load or §14a limit + # This handles cases where CP draws less than 4.2 kW (e.g., 3 kW due to low charging demand) + # p_hp_load - p_hp14a ≥ min(p_hp_load, p_min_14a) + # + # Special cases: + # - If p_max_support ≈ 0 (CP too small), force virtual gen to zero + # - If CP is off (p_hp_load ≈ 0), no support needed + if p_max_support < 1e-6 + # Charging point too small for §14a curtailment, disable virtual generator + JuMP.@constraint(pm.model, p_hp14a == 0.0) + elseif p_hp_load > 1e-6 + # Normal case: enforce minimum net load + # Net load cannot go below current load or §14a minimum, whichever is lower + p_min_net = min(p_hp_load, p_min_14a) + JuMP.@constraint(pm.model, p_hp_load - p_hp14a >= p_min_net) + else + # Charging point is off, no support needed + JuMP.@constraint(pm.model, p_hp14a == 0.0) + end +end + + +""" + constraint_cp_14a_time_budget_daily(pm, day_start, day_end, i) + +Limits the usage of §14a support generator to a maximum number of hours per day. +This is implemented by counting the number of timesteps where the binary variable is 1. + +# Arguments +- `pm::AbstractBFModelEdisgo`: PowerModels model +- `day_start::Int`: First timestep of the day +- `day_end::Int`: Last timestep of the day +- `i::Int`: Virtual generator index +""" +function constraint_cp_14a_time_budget_daily(pm::AbstractBFModelEdisgo, day_start::Int, day_end::Int, i::Int) + # Get time step duration in hours + if haskey(PowerModels.ref(pm, day_start), :time_elapsed) + time_elapsed = PowerModels.ref(pm, day_start, :time_elapsed) + else + Memento.warn(_LOGGER, "network data should specify time_elapsed, using 1.0 as default") + time_elapsed = 1.0 + end + + gen_hp14a = PowerModels.ref(pm, day_start, :gen_cp_14a, i) + max_hours = gen_hp14a["max_hours_per_day"] + + # Collect binary variables for all timesteps of the day + z_hp14a_day = [PowerModels.var(pm, t, :z_hp14a, i) for t in day_start:day_end] + + # Maximum number of active timesteps + max_active_steps = max_hours / time_elapsed + + # Sum of binary variables must not exceed budget + JuMP.@constraint(pm.model, sum(z_hp14a_day) <= max_active_steps) +end + + +""" + constraint_cp_14a_time_budget_total(pm, i, nws) + +Alternative to daily budget: Limits total usage over entire optimization horizon. +Can be used instead of daily budget for simpler formulation. + +# Arguments +- `pm::AbstractBFModelEdisgo`: PowerModels model +- `i::Int`: Virtual generator index +- `nws`: Network IDs (all timesteps) +""" +function constraint_cp_14a_time_budget_total(pm::AbstractBFModelEdisgo, i::Int, nws) + # Get time step duration + if haskey(PowerModels.ref(pm, first(nws)), :time_elapsed) + time_elapsed = PowerModels.ref(pm, first(nws), :time_elapsed) + else + time_elapsed = 1.0 + end + + gen_hp14a = PowerModels.ref(pm, first(nws), :gen_cp_14a, i) + max_hours_per_day = gen_hp14a["max_hours_per_day"] + + # Calculate total hours available (number of days × hours per day) + num_timesteps = length(nws) + num_days = ceil(num_timesteps * time_elapsed / 24.0) + total_max_hours = max_hours_per_day * num_days + + # Collect all binary variables + z_hp14a_all = [PowerModels.var(pm, t, :z_hp14a, i) for t in nws] + + # Total active timesteps + max_active_steps = total_max_hours / time_elapsed + + JuMP.@constraint(pm.model, sum(z_hp14a_all) <= max_active_steps) +end diff --git a/edisgo/opf/eDisGo_OPF.jl/src/core/objective.jl b/edisgo/opf/eDisGo_OPF.jl/src/core/objective.jl index 94d8e3902..17d180539 100644 --- a/edisgo/opf/eDisGo_OPF.jl/src/core/objective.jl +++ b/edisgo/opf/eDisGo_OPF.jl/src/core/objective.jl @@ -29,8 +29,9 @@ function objective_min_losses_slacks(pm::AbstractBFModelEdisgo) phps2 = Dict(n => PowerModels.var(pm, n, :phps2) for n in nws) phss = Dict(n => PowerModels.var(pm, n, :phss) for n in nws) - # §14a virtual generators + # §14a virtual generators for HPs and CPs p_hp14a = Dict(n => get(PowerModels.var(pm, n), :p_hp14a, Dict()) for n in nws) + p_cp14a = Dict(n => get(PowerModels.var(pm, n), :p_cp14a, Dict()) for n in nws) factor_slacks = 0.6 factor_14a = 0.5 # Weight for §14a curtailment (between slacks and losses) @@ -43,7 +44,8 @@ function objective_min_losses_slacks(pm::AbstractBFModelEdisgo) + factor_slacks * sum(sum(pcps[n][i] for i in keys(PowerModels.ref(pm,1 , :electromobility))) for n in nws) # minimize cp load sheddin + factor_slacks * sum(sum(phps[n][i] for i in keys(PowerModels.ref(pm,1 , :heatpumps))) for n in nws) # minimize hp load shedding + 1e4 * sum(sum(phss[n][i] + phps2[n][i] for i in keys(PowerModels.ref(pm, 1 , :heatpumps))) for n in nws) - + factor_14a * sum(sum(p_hp14a[n][i] for i in keys(p_hp14a[n])) for n in nws) # minimize §14a curtailment support + + factor_14a * sum(sum(p_hp14a[n][i] for i in keys(p_hp14a[n])) for n in nws) # minimize §14a HP curtailment support + + factor_14a * sum(sum(p_cp14a[n][i] for i in keys(p_cp14a[n])) for n in nws) # minimize §14a CP curtailment support ) end @@ -56,8 +58,9 @@ function objective_min_line_loading_max(pm::AbstractBFModelEdisgo) c = Dict(n => Dict(i => get(branch, "cost", 1.0) for (i,branch) in PowerModels.ref(pm, n, :branch)) for n in nws) storage = Dict(i => get(branch, "storage", 1.0) for (i,branch) in PowerModels.ref(pm, 1, :branch)) - # §14a virtual generators + # §14a virtual generators for HPs and CPs p_hp14a = Dict(n => get(PowerModels.var(pm, n), :p_hp14a, Dict()) for n in nws) + p_cp14a = Dict(n => get(PowerModels.var(pm, n), :p_cp14a, Dict()) for n in nws) factor_ll = 0.1 factor_14a = 0.05 # Small penalty for §14a usage in line loading optimization @@ -65,7 +68,8 @@ function objective_min_line_loading_max(pm::AbstractBFModelEdisgo) return JuMP.@objective(pm.model, Min, (1-factor_ll) * sum(sum(ccm[n][b] * r[n][b] for (b,i,j) in PowerModels.ref(pm, n, :arcs_from)) for n in nws) # minimize line losses + factor_ll * sum((ll[(b,i,j)]-1) * c[1][b] * l[1][b] for (b,i,j) in PowerModels.ref(pm, 1, :arcs_from) if storage[b] == 0) # minimize max line loading - + factor_14a * sum(sum(p_hp14a[n][i] for i in keys(p_hp14a[n])) for n in nws) # minimize §14a curtailment support + + factor_14a * sum(sum(p_hp14a[n][i] for i in keys(p_hp14a[n])) for n in nws) # minimize §14a HP curtailment support + + factor_14a * sum(sum(p_cp14a[n][i] for i in keys(p_cp14a[n])) for n in nws) # minimize §14a CP curtailment support ) end From 230a4a26cc396f83e3e8e50f1e4ac5055ebd1307 Mon Sep 17 00:00:00 2001 From: joda9 Date: Thu, 11 Dec 2025 15:59:46 +0100 Subject: [PATCH 35/43] =?UTF-8?q?feat:=20Implement=20=C2=A714a=20virtual?= =?UTF-8?q?=20generator=20support=20for=20charging=20points=20with=20power?= =?UTF-8?q?=20and=20binary=20variables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/core/constraint_cp_14a.jl | 36 ++++++------- .../opf/eDisGo_OPF.jl/src/core/variables.jl | 31 +++++++++++ edisgo/opf/eDisGo_OPF.jl/src/eDisGo_OPF.jl | 1 + edisgo/opf/eDisGo_OPF.jl/src/prob/opf_bf.jl | 53 +++++++++++++++++++ 4 files changed, 103 insertions(+), 18 deletions(-) diff --git a/edisgo/opf/eDisGo_OPF.jl/src/core/constraint_cp_14a.jl b/edisgo/opf/eDisGo_OPF.jl/src/core/constraint_cp_14a.jl index 21bd186b7..822879733 100644 --- a/edisgo/opf/eDisGo_OPF.jl/src/core/constraint_cp_14a.jl +++ b/edisgo/opf/eDisGo_OPF.jl/src/core/constraint_cp_14a.jl @@ -19,12 +19,12 @@ This ensures time budget tracking works correctly. - `nw::Int`: Network (timestep) index """ function constraint_cp_14a_binary_coupling(pm::AbstractBFModelEdisgo, i::Int, nw::Int=nw_id_default) - gen_hp14a = PowerModels.ref(pm, nw, :gen_cp_14a, i) - p_hp14a = PowerModels.var(pm, nw, :p_hp14a, i) - z_hp14a = PowerModels.var(pm, nw, :z_hp14a, i) + gen_cp14a = PowerModels.ref(pm, nw, :gen_cp_14a, i) + p_cp14a = PowerModels.var(pm, nw, :p_cp14a, i) + z_cp14a = PowerModels.var(pm, nw, :z_cp14a, i) # p ≤ pmax × z (if z=0 then p=0, if z=1 then p can be 0..pmax) - JuMP.@constraint(pm.model, p_hp14a <= gen_hp14a["pmax"] * z_hp14a) + JuMP.@constraint(pm.model, p_cp14a <= gen_cp14a["pmax"] * z_cp14a) end @@ -59,22 +59,22 @@ function constraint_cp_14a_min_net_load(pm::AbstractBFModelEdisgo, i::Int, nw::I # Net load must stay ≥ minimum net load allowed # The minimum is the LOWER of: current load or §14a limit # This handles cases where CP draws less than 4.2 kW (e.g., 3 kW due to low charging demand) - # p_hp_load - p_hp14a ≥ min(p_hp_load, p_min_14a) + # p_cp_load - p_cp14a ≥ min(p_cp_load, p_min_14a) # # Special cases: # - If p_max_support ≈ 0 (CP too small), force virtual gen to zero # - If CP is off (p_hp_load ≈ 0), no support needed if p_max_support < 1e-6 # Charging point too small for §14a curtailment, disable virtual generator - JuMP.@constraint(pm.model, p_hp14a == 0.0) - elseif p_hp_load > 1e-6 + JuMP.@constraint(pm.model, p_cp14a == 0.0) + elseif p_cp_load > 1e-6 # Normal case: enforce minimum net load # Net load cannot go below current load or §14a minimum, whichever is lower - p_min_net = min(p_hp_load, p_min_14a) - JuMP.@constraint(pm.model, p_hp_load - p_hp14a >= p_min_net) + p_min_net = min(p_cp_load, p_min_14a) + JuMP.@constraint(pm.model, p_cp_load - p_cp14a >= p_min_net) else # Charging point is off, no support needed - JuMP.@constraint(pm.model, p_hp14a == 0.0) + JuMP.@constraint(pm.model, p_cp14a == 0.0) end end @@ -100,17 +100,17 @@ function constraint_cp_14a_time_budget_daily(pm::AbstractBFModelEdisgo, day_star time_elapsed = 1.0 end - gen_hp14a = PowerModels.ref(pm, day_start, :gen_cp_14a, i) - max_hours = gen_hp14a["max_hours_per_day"] + gen_cp14a = PowerModels.ref(pm, day_start, :gen_cp_14a, i) + max_hours = gen_cp14a["max_hours_per_day"] # Collect binary variables for all timesteps of the day - z_hp14a_day = [PowerModels.var(pm, t, :z_hp14a, i) for t in day_start:day_end] + z_cp14a_day = [PowerModels.var(pm, t, :z_cp14a, i) for t in day_start:day_end] # Maximum number of active timesteps max_active_steps = max_hours / time_elapsed # Sum of binary variables must not exceed budget - JuMP.@constraint(pm.model, sum(z_hp14a_day) <= max_active_steps) + JuMP.@constraint(pm.model, sum(z_cp14a_day) <= max_active_steps) end @@ -133,8 +133,8 @@ function constraint_cp_14a_time_budget_total(pm::AbstractBFModelEdisgo, i::Int, time_elapsed = 1.0 end - gen_hp14a = PowerModels.ref(pm, first(nws), :gen_cp_14a, i) - max_hours_per_day = gen_hp14a["max_hours_per_day"] + gen_cp14a = PowerModels.ref(pm, first(nws), :gen_cp_14a, i) + max_hours_per_day = gen_cp14a["max_hours_per_day"] # Calculate total hours available (number of days × hours per day) num_timesteps = length(nws) @@ -142,10 +142,10 @@ function constraint_cp_14a_time_budget_total(pm::AbstractBFModelEdisgo, i::Int, total_max_hours = max_hours_per_day * num_days # Collect all binary variables - z_hp14a_all = [PowerModels.var(pm, t, :z_hp14a, i) for t in nws] + z_cp14a_all = [PowerModels.var(pm, t, :z_cp14a, i) for t in nws] # Total active timesteps max_active_steps = total_max_hours / time_elapsed - JuMP.@constraint(pm.model, sum(z_hp14a_all) <= max_active_steps) + JuMP.@constraint(pm.model, sum(z_cp14a_all) <= max_active_steps) end diff --git a/edisgo/opf/eDisGo_OPF.jl/src/core/variables.jl b/edisgo/opf/eDisGo_OPF.jl/src/core/variables.jl index d4f172094..ce0a270dd 100644 --- a/edisgo/opf/eDisGo_OPF.jl/src/core/variables.jl +++ b/edisgo/opf/eDisGo_OPF.jl/src/core/variables.jl @@ -426,6 +426,37 @@ function variable_gen_hp_14a_binary(pm::AbstractPowerModel; nw::Int=nw_id_defaul report && PowerModels.sol_component_value(pm, nw, :gen_hp_14a, :z, PowerModels.ids(pm, nw, :gen_hp_14a), z_hp14a) end +"§14a virtual generator power variables for charging points" +function variable_gen_cp_14a_power(pm::AbstractPowerModel; nw::Int=nw_id_default, bounded::Bool=true, report::Bool=true) + p_cp14a = PowerModels.var(pm, nw)[:p_cp14a] = JuMP.@variable(pm.model, + [i in PowerModels.ids(pm, nw, :gen_cp_14a)], + base_name="$(nw)_p_cp14a", + lower_bound = 0.0 + ) + + if bounded + for (i, gen) in PowerModels.ref(pm, nw, :gen_cp_14a) + JuMP.set_upper_bound(p_cp14a[i], gen["pmax"]) + end + end + + if report + println(" 🔍 JULIA: Reporting gen_cp_14a power for nw=$nw, ids=$(PowerModels.ids(pm, nw, :gen_cp_14a))") + PowerModels.sol_component_value(pm, nw, :gen_cp_14a, :p, PowerModels.ids(pm, nw, :gen_cp_14a), p_cp14a) + end +end + +"§14a virtual generator binary variables for charging point time budget tracking" +function variable_gen_cp_14a_binary(pm::AbstractPowerModel; nw::Int=nw_id_default, report::Bool=true) + z_cp14a = PowerModels.var(pm, nw)[:z_cp14a] = JuMP.@variable(pm.model, + [i in PowerModels.ids(pm, nw, :gen_cp_14a)], + base_name="$(nw)_z_cp14a", + binary = true + ) + + report && PowerModels.sol_component_value(pm, nw, :gen_cp_14a, :z, PowerModels.ids(pm, nw, :gen_cp_14a), z_cp14a) +end + "slack variables for grid restrictions" function variable_slack_grid_restrictions(pm::AbstractBFModelEdisgo; kwargs...) eDisGo_OPF.variable_hp_slack(pm; kwargs...) diff --git a/edisgo/opf/eDisGo_OPF.jl/src/eDisGo_OPF.jl b/edisgo/opf/eDisGo_OPF.jl/src/eDisGo_OPF.jl index ba208f462..2ba82c687 100644 --- a/edisgo/opf/eDisGo_OPF.jl/src/eDisGo_OPF.jl +++ b/edisgo/opf/eDisGo_OPF.jl/src/eDisGo_OPF.jl @@ -19,6 +19,7 @@ include("core/base.jl") include("core/constraint.jl") include("core/constraint_template.jl") include("core/constraint_hp_14a.jl") +include("core/constraint_cp_14a.jl") include("core/data.jl") include("core/objective.jl") include("core/solution.jl") diff --git a/edisgo/opf/eDisGo_OPF.jl/src/prob/opf_bf.jl b/edisgo/opf/eDisGo_OPF.jl/src/prob/opf_bf.jl index fd5360895..c46f1c8cb 100644 --- a/edisgo/opf/eDisGo_OPF.jl/src/prob/opf_bf.jl +++ b/edisgo/opf/eDisGo_OPF.jl/src/prob/opf_bf.jl @@ -37,6 +37,12 @@ function build_mn_opf_bf_flex(pm::AbstractBFModelEdisgo) eDisGo_OPF.variable_gen_hp_14a_binary(pm, nw=n) end + # §14a EnWG virtual generators for charging point support + if haskey(PowerModels.ref(pm, n), :gen_cp_14a) && !isempty(PowerModels.ref(pm, n, :gen_cp_14a)) + eDisGo_OPF.variable_gen_cp_14a_power(pm, nw=n) + eDisGo_OPF.variable_gen_cp_14a_binary(pm, nw=n) + end + if PowerModels.ref(pm, 1, :opf_version) in(3, 4) # Nicht Teil der MA eDisGo_OPF.variable_slack_HV_requirements(pm, nw=n) if PowerModels.ref(pm, 1, :opf_version) in(3) @@ -72,6 +78,14 @@ function build_mn_opf_bf_flex(pm::AbstractBFModelEdisgo) end end + # §14a EnWG constraints for charging point virtual generators + if haskey(PowerModels.ref(pm, n), :gen_cp_14a) && !isempty(PowerModels.ref(pm, n, :gen_cp_14a)) + for i in PowerModels.ids(pm, :gen_cp_14a, nw=n) + eDisGo_OPF.constraint_cp_14a_binary_coupling(pm, i, n) + eDisGo_OPF.constraint_cp_14a_min_net_load(pm, i, n) + end + end + end # CONSTRAINTS @@ -144,6 +158,45 @@ function build_mn_opf_bf_flex(pm::AbstractBFModelEdisgo) println("\n⚠ JULIA DEBUG: No gen_hp_14a found or empty!\n") end + # §14a EnWG daily time budget constraints for charging points + if haskey(PowerModels.ref(pm, 1), :gen_cp_14a) && !isempty(PowerModels.ref(pm, 1, :gen_cp_14a)) + println("\n" * "="^80) + println("🔍 JULIA DEBUG: §14a Charging Point Generators") + println("="^80) + + gen_cp_14a_dict = PowerModels.ref(pm, 1, :gen_cp_14a) + println("Number of gen_cp_14a entries: ", length(gen_cp_14a_dict)) + + # Show first 5 generators + count = 0 + for (idx, gen) in gen_cp_14a_dict + count += 1 + if count <= 5 + println(" [$idx]: cp_name=$(get(gen, "cp_name", "N/A")), cp_index=$(get(gen, "cp_index", "N/A")), pmax=$(get(gen, "pmax", "N/A"))") + end + end + println("="^80 * "\n") + + # Determine timesteps per day based on time_elapsed (in hours) + n_first = network_ids[1] + time_elapsed = PowerModels.ref(pm, n_first, :time_elapsed) + timesteps_per_day = Int(round(24.0 / time_elapsed)) + + # Group network_ids into days + for day_start_idx in 1:timesteps_per_day:length(network_ids) + day_end_idx = min(day_start_idx + timesteps_per_day - 1, length(network_ids)) + day_network_ids = network_ids[day_start_idx:day_end_idx] + + # Apply daily time budget constraint for each §14a generator + for i in PowerModels.ids(pm, :gen_cp_14a, nw=network_ids[1]) + # Call with correct argument order: (pm, day_start, day_end, i) + eDisGo_OPF.constraint_cp_14a_time_budget_daily(pm, day_network_ids[1], day_network_ids[end], i) + end + end + else + println("\n⚠ JULIA DEBUG: No gen_cp_14a found or empty!\n") + end + # OBJECTIVE FUNCTION if PowerModels.ref(pm, 1, :opf_version) == 1 #eDisGo_OPF.objective_min_losses(pm) From c8ee3eb835417cf680bfed277378579dd9e849c3 Mon Sep 17 00:00:00 2001 From: joda9 Date: Thu, 11 Dec 2025 17:00:49 +0100 Subject: [PATCH 36/43] =?UTF-8?q?feat:=20Enhance=20=C2=A714a=20support=20f?= =?UTF-8?q?or=20charging=20points=20with=20virtual=20generator=20integrati?= =?UTF-8?q?on=20and=20improved=20load=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- edisgo/io/powermodels_io.py | 43 +++++++------------ .../src/core/constraint_cp_14a.jl | 32 +++++++++++--- 2 files changed, 43 insertions(+), 32 deletions(-) diff --git a/edisgo/io/powermodels_io.py b/edisgo/io/powermodels_io.py index 66178d855..733104275 100644 --- a/edisgo/io/powermodels_io.py +++ b/edisgo/io/powermodels_io.py @@ -1465,6 +1465,13 @@ def _build_gen_cp_14a_support(psa_net, pm, edisgo_obj, s_base, all_cps, curtailm max_hours_per_day = curtailment_14a.get("max_hours_per_day", 2.0) # hours specific_components = curtailment_14a.get("components", []) + # DEBUG: Print all CPs in topology + all_cps_in_topology = edisgo_obj.topology.charging_points_df + logger.info(f"DEBUG: Found {len(all_cps_in_topology)} charging points in topology.charging_points_df") + logger.info(f"DEBUG: all_cps parameter has {len(all_cps)} entries") + if len(all_cps_in_topology) > 0: + logger.info(f"DEBUG: Sample CP names: {list(all_cps_in_topology.index[:3])}") + # Filter charging points if specific components are defined if len(specific_components) > 0: cps_14a = np.intersect1d(all_cps, specific_components) @@ -1476,6 +1483,7 @@ def _build_gen_cp_14a_support(psa_net, pm, edisgo_obj, s_base, all_cps, curtailm return cp_df = edisgo_obj.topology.charging_points_df.loc[cps_14a] + logger.info(f"DEBUG: After filtering, cp_df has {len(cp_df)} charging points") cp_p_nom = cp_df.p_set # Nominal charging power in MW # Filter out CPs with nominal power <= §14a minimum @@ -1493,34 +1501,15 @@ def _build_gen_cp_14a_support(psa_net, pm, edisgo_obj, s_base, all_cps, curtailm logger.warning("No charging points eligible for §14a curtailment after filtering by minimum power.") return - # NOTE: §14a curtailment can only work for CPs that are in pm["electromobility"] - # These are the flexible CPs. Check which eligible CPs are actually flexible. - flexible_cp_indices = list(pm["electromobility"].keys()) if "electromobility" in pm and len(pm["electromobility"]) > 0 else [] - - if len(flexible_cp_indices) == 0: - logger.warning("No flexible charging points in PowerModels dict - §14a curtailment cannot be applied to CPs.") - logger.warning("CPs must be flexible (have electromobility optimization) to support §14a curtailment.") - return - - # Get the CP names from electromobility dict to create proper index mapping - flexible_cp_names = [pm["electromobility"][idx]["name"] for idx in flexible_cp_indices] - cp_name_to_index = {cp_name: int(idx) for idx, cp_dict in pm["electromobility"].items() - if (cp_name := cp_dict["name"])} - - # Only use CPs that are both eligible for §14a AND flexible - cps_final = [cp for cp in cps_eligible if cp in cp_name_to_index] + # §14a curtailment works for ALL CPs (like HPs), not just flexible ones + # The virtual generator reduces the load, independent of flexibility optimization + cps_final = cps_eligible - if len(cps_final) < len(cps_eligible): - non_flexible = set(cps_eligible) - set(cps_final) - logger.warning( - f"{len(non_flexible)} eligible CP(s) are not flexible and cannot use §14a: {non_flexible}" - ) - - if len(cps_final) == 0: - logger.warning("No flexible CPs available for §14a curtailment.") - return + logger.info(f"Creating §14a support for {len(cps_final)} charging points.") - logger.info(f"Creating §14a support for {len(cps_final)} flexible charging points.") + # Create a simple index mapping for CPs (needed by Julia constraints) + # For non-flexible CPs, we create a sequential index starting from 1 + cp_name_to_index = {cp_name: idx + 1 for idx, cp_name in enumerate(cps_final)} for cp_i, cp_name in enumerate(cps_final): # Bus of the charging point @@ -1544,7 +1533,7 @@ def _build_gen_cp_14a_support(psa_net, pm, edisgo_obj, s_base, all_cps, curtailm "sign": 1, "gen_status": 1, "cp_name": cp_name, # Reference to charging point - "cp_index": cp_name_to_index[cp_name], # Correct index in electromobility dict + "cp_index": cp_name_to_index[cp_name], # Sequential index for Julia "p_min_14a": p_min_14a / s_base, # §14a minimum power "max_hours_per_day": max_hours_per_day, # Time budget "index": cp_i + 1, diff --git a/edisgo/opf/eDisGo_OPF.jl/src/core/constraint_cp_14a.jl b/edisgo/opf/eDisGo_OPF.jl/src/core/constraint_cp_14a.jl index 822879733..03c9afd9b 100644 --- a/edisgo/opf/eDisGo_OPF.jl/src/core/constraint_cp_14a.jl +++ b/edisgo/opf/eDisGo_OPF.jl/src/core/constraint_cp_14a.jl @@ -42,10 +42,32 @@ stays above the §14a minimum power level (typically 4.2 kW = 0.0042 MW). function constraint_cp_14a_min_net_load(pm::AbstractBFModelEdisgo, i::Int, nw::Int=nw_id_default) gen_cp14a = PowerModels.ref(pm, nw, :gen_cp_14a, i) cp_idx = gen_cp14a["cp_index"] - cp = PowerModels.ref(pm, nw, :electromobility, cp_idx) - # Electrical power demand of charging point (direct charging power, no COP needed) - p_cp_load = cp["pcp"] # Charging power variable + # Check if CP is flexible (in electromobility dict) or simple load + p_cp_load = nothing + if haskey(PowerModels.ref(pm, nw), :electromobility) && haskey(PowerModels.ref(pm, nw, :electromobility), cp_idx) + # Flexible CP: use electromobility variable + cp = PowerModels.ref(pm, nw, :electromobility, cp_idx) + p_cp_load = cp["pcp"] # Charging power variable (optimization variable) + else + # Non-flexible CP: use fixed load timeseries + # Find the load by CP name + cp_name = gen_cp14a["cp_name"] + # Search for load with matching name + load_found = false + for (load_id, load) in PowerModels.ref(pm, nw, :load) + if haskey(load, "name") && load["name"] == cp_name + p_cp_load = load["pd"] # Fixed load value (parameter, not variable) + load_found = true + break + end + end + + if !load_found + @warn "Could not find load for charging point $(cp_name), skipping constraint" + return + end + end # Virtual generator support p_cp14a = PowerModels.var(pm, nw, :p_cp14a, i) @@ -63,7 +85,7 @@ function constraint_cp_14a_min_net_load(pm::AbstractBFModelEdisgo, i::Int, nw::I # # Special cases: # - If p_max_support ≈ 0 (CP too small), force virtual gen to zero - # - If CP is off (p_hp_load ≈ 0), no support needed + # - If CP is off (p_cp_load ≈ 0), no support needed if p_max_support < 1e-6 # Charging point too small for §14a curtailment, disable virtual generator JuMP.@constraint(pm.model, p_cp14a == 0.0) @@ -71,7 +93,7 @@ function constraint_cp_14a_min_net_load(pm::AbstractBFModelEdisgo, i::Int, nw::I # Normal case: enforce minimum net load # Net load cannot go below current load or §14a minimum, whichever is lower p_min_net = min(p_cp_load, p_min_14a) - JuMP.@constraint(pm.model, p_cp_load - p_cp14a >= p_min_net) + JuMP.@constraint(pm.model, p_cp14a <= p_cp_load - p_min_net) else # Charging point is off, no support needed JuMP.@constraint(pm.model, p_cp14a == 0.0) From a99219e7f3eb2a8fb49f7b3075539a2f29696ddb Mon Sep 17 00:00:00 2001 From: joda9 <66819219+joda9@users.noreply.github.com> Date: Fri, 12 Dec 2025 09:11:36 +0100 Subject: [PATCH 37/43] Add multiple some comments --- edisgo/opf/eDisGo_OPF.jl/src/core/objective.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/edisgo/opf/eDisGo_OPF.jl/src/core/objective.jl b/edisgo/opf/eDisGo_OPF.jl/src/core/objective.jl index 17d180539..68f774f23 100644 --- a/edisgo/opf/eDisGo_OPF.jl/src/core/objective.jl +++ b/edisgo/opf/eDisGo_OPF.jl/src/core/objective.jl @@ -1,3 +1,4 @@ +# OPF Version 1: Minimize line losses and maximal line loading function objective_min_losses(pm::AbstractBFModelEdisgo) nws = PowerModels.nw_ids(pm) ccm = Dict(n => PowerModels.var(pm, n, :ccm) for n in nws) @@ -17,6 +18,7 @@ function objective_min_losses(pm::AbstractBFModelEdisgo) ) end +# OPF Version 2: Minimize line losses and grid related slacks function objective_min_losses_slacks(pm::AbstractBFModelEdisgo) nws = PowerModels.nw_ids(pm) ccm = Dict(n => PowerModels.var(pm, n, :ccm) for n in nws) @@ -49,6 +51,7 @@ function objective_min_losses_slacks(pm::AbstractBFModelEdisgo) ) end +# OPF Version 3: Minimize line losses, maximal line loading and HV slacks function objective_min_line_loading_max(pm::AbstractBFModelEdisgo) nws = PowerModels.nw_ids(pm) ccm = Dict(n => PowerModels.var(pm, n, :ccm) for n in nws) @@ -74,7 +77,7 @@ function objective_min_line_loading_max(pm::AbstractBFModelEdisgo) end -# OPF with overlying grid +# OPF Version 4: Minimize line losses, HV slacks and grid related slacks (with overlying grid) function objective_min_losses_slacks_OG(pm::AbstractBFModelEdisgo) nws = PowerModels.nw_ids(pm) ccm = Dict(n => PowerModels.var(pm, n, :ccm) for n in nws) @@ -106,6 +109,7 @@ function objective_min_losses_slacks_OG(pm::AbstractBFModelEdisgo) ) end +# OPF Version 3 (alternative): Minimize line losses, maximal line loading and HV slacks (with overlying grid) function objective_min_line_loading_max_OG(pm::AbstractBFModelEdisgo) nws = PowerModels.nw_ids(pm) ccm = Dict(n => PowerModels.var(pm, n, :ccm) for n in nws) From d3069a41fd275bdaa1d010af96643a44273c4e3c Mon Sep 17 00:00:00 2001 From: Jonas Danke Date: Fri, 12 Dec 2025 16:45:14 +0100 Subject: [PATCH 38/43] =?UTF-8?q?Add=20quick=20start=20guide=20for=20?= =?UTF-8?q?=C2=A714a=20EnWG=20curtailment=20analysis=20with=20heat=20pumps?= =?UTF-8?q?=20and=20charging=20points?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Optimization14A_quickstart.md | 145 ++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 Optimization14A_quickstart.md diff --git a/Optimization14A_quickstart.md b/Optimization14A_quickstart.md new file mode 100644 index 000000000..bd6cd56c4 --- /dev/null +++ b/Optimization14A_quickstart.md @@ -0,0 +1,145 @@ +# §14a EnWG Curtailment Analysis - Quick Start Guide + +## What does `analyze_14a.py` do? + +This script performs a complete §14a curtailment analysis for heat pumps and charging points in a distribution grid: + +1. **Loads a grid** (ding0 format) +2. **Adds heat pumps** (50 units, 11-20 kW, realistic size distribution) +3. **Adds charging points** (30 units, 3.7-50 kW, home/work/fast charging) +4. **Generates realistic winter timeseries** (configurable days, default: 7 days) +5. **Runs OPF with §14a curtailment** (opf_version=3) +6. **Analyzes curtailment results** (statistics, daily/monthly aggregation) +7. **Creates visualizations** (3 plots: timeseries, HP profiles, CP profiles) +8. **Exports CSV data** (summary, curtailment data, totals per HP/CP) + +**Output:** Results folder with plots and CSV files for detailed analysis. + +--- + +## Setup & Run (Complete Workflow) + +### Create Project Directory + +```bash +# Create and navigate to project directory +mkdir -p ~/projects/edisgo_14a +cd ~/projects/edisgo_14a +``` + +### Clone eDisGo Repository + +```bash +# Clone the repository +git clone https://github.com/openego/eDisGo.git +cd eDisGo + +# Checkout the §14a feature branch +git checkout project/411_LoMa_14aOptimization_with_virtual_generators +``` + +### Setup Python Environment + +```bash +# Create Python 3.11 virtual environment +python3.11 -m venv .venv + +# Activate virtual environment +source .venv/bin/activate # Linux/Mac +# OR +.venv\Scripts\activate # Windows + +# Upgrade pip +pip install --upgrade pip setuptools wheel +``` + +### Install eDisGo + +```bash +# Install eDisGo in development mode +python -m pip install -e .[dev] # install eDisGo from source +``` + +The script uses grid data from the `30879` folder: + +```bash +# Navigate to workspace root +cd ~/projects/edisgo_14a + +# Grid folder should be at: +# ~/projects/edisgo_14a/30879/ +# with files: buses.csv, lines.csv, generators.csv, etc. + +# Verify grid data exists +ls -lh 30879/ +``` + +### Run the Analysis + +```bash +# Navigate to script location +cd ~/projects/edisgo_14a + +# Run the analysis +python analyze_14a.py +``` + +**Expected runtime:** 5-15 minutes (depending on hardware) + +--- + +## Configuration + +Edit these variables in `analyze_14a.py` (around line 1015): + +```python +# Grid configuration +GRID_PATH = "./30879" # Path to ding0 grid folder +SCENARIO = "eGon2035" # Scenario name + +# Simulation parameters +NUM_DAYS = 7 # Number of days to simulate (7, 30, 365) +NUM_HEAT_PUMPS = 50 # Number of heat pumps to add +NUM_CHARGING_POINTS = 30 # Number of charging points to add + +# Output +OUTPUT_DIR = "./" # Where to save results +``` + +--- + +## Output Files + +After running, you'll find a results folder: + +``` +results_7d_HP50_CP30_14a/ +├── summary_statistics.csv # Overall statistics +├── curtailment_timeseries.csv # Hourly curtailment per HP/CP +├── curtailment_daily.csv # Daily aggregation +├── curtailment_monthly.csv # Monthly aggregation +├── hp_curtailment_total.csv # Total curtailment per HP +├── cp_curtailment_total.csv # Total curtailment per CP +├── curtailment_timeseries.png # Time series plot +├── detailed_hp_profiles.png # Detailed HP profile +└── detailed_cp_profiles.png # Detailed CP profile (if curtailed) +``` + +--- + +## Using Your Own Grid + +### Option 1: Use Another ding0 Grid + +```python +# In analyze_14a.py, change GRID_PATH: +GRID_PATH = "/path/to/your/ding0/grid/folder" + +# Grid folder must contain: +# - buses.csv +# - lines.csv +# - generators.csv +# - loads.csv +# - transformers.csv +# - etc. +``` From 58a24e729f06b0c74ac10c027e56f8ff7e4be824 Mon Sep 17 00:00:00 2001 From: Jonas Danke Date: Fri, 12 Dec 2025 16:46:22 +0100 Subject: [PATCH 39/43] add example for using 14a optimization --- examples/example_analyze_14a.py | 1059 +++++++++++++++++++++++++++++++ 1 file changed, 1059 insertions(+) create mode 100644 examples/example_analyze_14a.py diff --git a/examples/example_analyze_14a.py b/examples/example_analyze_14a.py new file mode 100644 index 000000000..be377d68e --- /dev/null +++ b/examples/example_analyze_14a.py @@ -0,0 +1,1059 @@ +""" +§14a EnWG Heat Pump Curtailment Analysis - Monthly Simulation + +This script performs a monthly optimization with §14a heat pump curtailment +and generates comprehensive analysis plots and statistics. + +Usage: + python analyze_14a_full_year.py --grid_path --scenario eGon2035 --num_days 30 +""" + +import os +import sys +import argparse +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +from datetime import datetime +from pathlib import Path + +from edisgo import EDisGo +from edisgo.io.db import engine as egon_engine + + +def add_charging_points_manually(edisgo, num_cps=30, seed=42): + """ + Add charging points manually to the grid with realistic size distribution. + + Parameters + ---------- + edisgo : EDisGo + EDisGo object + num_cps : int + Number of charging points to add (default: 30) + seed : int + Random seed for reproducibility (default: 42) + + Returns + ------- + EDisGo + EDisGo object with added charging points + """ + print(f"\n3a. Adding {num_cps} charging points manually...") + + np.random.seed(seed + 100) # Different seed than HPs + + # Get random LV buses (different from HP buses if possible) + lv_buses = edisgo.topology.buses_df[ + edisgo.topology.buses_df.v_nom < 1.0 # LV buses + ] + + if len(lv_buses) < num_cps: + print(f" ⚠ Warning: Only {len(lv_buses)} LV buses available, using all") + num_cps = len(lv_buses) + + selected_buses = lv_buses.sample(n=num_cps, random_state=seed + 100) + + # Realistic distribution based on typical EV charging points: + # - 50% home charging (3.7-11 kW, typically 11 kW) + # - 30% work/public charging (11-22 kW) + # - 20% fast charging (22-50 kW, but curtailed to grid limits) + num_home = int(num_cps * 0.5) + num_work = int(num_cps * 0.3) + num_fast = num_cps - num_home - num_work + + cp_data = [] + cp_names = [] + + # Home charging points (3.7-11 kW) + for i in range(num_home): + p_set = np.random.uniform(0.0037, 0.011) # 3.7-11 kW + cp_data.append({ + 'bus': selected_buses.index[i], + 'p_set': p_set, + 'type': 'charging_point', + 'sector': 'home' + }) + cp_names.append(f'CP_home_{i+1}') + + # Work/public charging points (11-22 kW) + for i in range(num_work): + p_set = np.random.uniform(0.011, 0.022) # 11-22 kW + cp_data.append({ + 'bus': selected_buses.index[num_home + i], + 'p_set': p_set, + 'type': 'charging_point', + 'sector': 'work' + }) + cp_names.append(f'CP_work_{i+1}') + + # Fast charging points (22-50 kW) + for i in range(num_fast): + p_set = np.random.uniform(0.022, 0.050) # 22-50 kW + cp_data.append({ + 'bus': selected_buses.index[num_home + num_work + i], + 'p_set': p_set, + 'type': 'charging_point', + 'sector': 'public' + }) + cp_names.append(f'CP_fast_{i+1}') + + # Add to topology + cp_df = pd.DataFrame(cp_data, index=cp_names) + edisgo.topology.loads_df = pd.concat([edisgo.topology.loads_df, cp_df]) + + print(f" ✓ Added {len(cp_names)} charging points:") + print(f" - {num_home} home (3.7-11 kW)") + print(f" - {num_work} work/public (11-22 kW)") + print(f" - {num_fast} fast charging (22-50 kW)") + print(f" - §14a eligible (>4.2 kW): {len(cp_df[cp_df['p_set'] > 0.0042])}") + + return edisgo + + +def add_heat_pumps_manually(edisgo, num_hps=50, seed=42): + """ + Add heat pumps manually to the grid with realistic size distribution. + + Parameters + ---------- + edisgo : EDisGo + EDisGo object + num_hps : int + Number of heat pumps to add (default: 50) + seed : int + Random seed for reproducibility (default: 42) + + Returns + ------- + EDisGo + EDisGo object with added heat pumps + """ + print(f"\n3. Adding {num_hps} heat pumps manually...") + + np.random.seed(seed) + + # Get random LV buses + lv_buses = edisgo.topology.buses_df[ + edisgo.topology.buses_df.v_nom < 1.0 # LV buses + ] + + if len(lv_buses) < num_hps: + print(f" ⚠ Warning: Only {len(lv_buses)} LV buses available, using all") + num_hps = len(lv_buses) + + selected_buses = lv_buses.sample(n=num_hps, random_state=seed) + + # Realistic distribution based on German residential heat pumps: + # - 60% large (11-20 kW) - typical for older/larger houses + # - 30% medium (5-11 kW) - typical for modern houses + # - 10% small (3-5 kW) - typical for well-insulated new buildings + num_large = int(num_hps * 0.6) + num_medium = int(num_hps * 0.3) + num_small = num_hps - num_large - num_medium + + hp_data = [] + hp_names = [] + + # Large heat pumps (11-20 kW) + for i in range(num_large): + p_set = np.random.uniform(0.011, 0.020) # 11-20 kW + hp_data.append({ + 'bus': selected_buses.index[i], + 'p_set': p_set, + 'type': 'heat_pump', + 'sector': 'residential' + }) + hp_names.append(f'HP_large_{i+1}') + + # Medium heat pumps (5-11 kW) + for i in range(num_medium): + p_set = np.random.uniform(0.005, 0.011) # 5-11 kW + hp_data.append({ + 'bus': selected_buses.index[num_large + i], + 'p_set': p_set, + 'type': 'heat_pump', + 'sector': 'residential' + }) + hp_names.append(f'HP_medium_{i+1}') + + # Small heat pumps (3-5 kW) + for i in range(num_small): + p_set = np.random.uniform(0.003, 0.005) # 3-5 kW + hp_data.append({ + 'bus': selected_buses.index[num_large + num_medium + i], + 'p_set': p_set, + 'type': 'heat_pump', + 'sector': 'residential' + }) + hp_names.append(f'HP_small_{i+1}') + + # Add to topology + hp_df = pd.DataFrame(hp_data, index=hp_names) + edisgo.topology.loads_df = pd.concat([edisgo.topology.loads_df, hp_df]) + + print(f" ✓ Added {len(hp_names)} heat pumps:") + print(f" - {num_large} large (11-20 kW)") + print(f" - {num_medium} medium (5-11 kW)") + print(f" - {num_small} small (3-5 kW)") + print(f" - §14a eligible (>4.2 kW): {num_large + num_medium}") + + return edisgo + + +def create_hp_timeseries(edisgo, scenario="eGon2035", num_days=30): + """ + Create realistic heat demand and COP time series for heat pumps. + + Parameters + ---------- + edisgo : EDisGo + EDisGo object with heat pumps + scenario : str + Scenario name for time index + num_days : int + Number of days to simulate (default: 30 for one month) + + Returns + ------- + EDisGo + EDisGo object with HP time series + """ + print(f"\n4. Creating heat pump time series for {num_days} days...") + + # Get heat pumps + heat_pumps = edisgo.topology.loads_df[edisgo.topology.loads_df['type'] == 'heat_pump'] + + if len(heat_pumps) == 0: + print(" ⚠ Warning: No heat pumps found") + return edisgo + + # Create time index (hourly for specified days) + num_timesteps = num_days * 24 + timeindex = pd.date_range('2035-01-15', periods=num_timesteps, freq='h') # Mid-winter + edisgo.timeseries.timeindex = timeindex + + print(f" Creating time series for {len(heat_pumps)} heat pumps...") + print(f" Timeindex: {len(timeindex)} timesteps (hourly, {num_days} days)") + + # Create realistic heat demand profile for winter month + hours = np.arange(len(timeindex)) + hour_of_day = timeindex.hour.values + day_of_week = timeindex.dayofweek.values # 0=Monday, 6=Sunday + + # Winter season - high base load (mid-January) + seasonal_factor = 0.9 # High demand in winter + + # Daily pattern (higher demand morning and evening) + daily_factor = 0.7 + 0.3 * ( + np.exp(-((hour_of_day - 7) ** 2) / 8) + # Morning peak + np.exp(-((hour_of_day - 19) ** 2) / 8) # Evening peak + ) + + # Weekend pattern (slightly different - later morning, more evening) + weekend_mask = day_of_week >= 5 + daily_factor[weekend_mask] = 0.6 + 0.4 * ( + np.exp(-((hour_of_day[weekend_mask] - 9) ** 2) / 10) + # Later morning + np.exp(-((hour_of_day[weekend_mask] - 20) ** 2) / 10) # Evening peak + ) + + # Combine patterns + base_profile = seasonal_factor * daily_factor + + # Create COP profile (winter - lower COP due to cold temperatures) + # Typical air-source heat pump COP in winter: 2.5-3.5 + cop_profile = 3.0 + np.random.normal(0, 0.2, len(timeindex)) + cop_profile = np.clip(cop_profile, 2.5, 3.5) + + # Create individual profiles for each HP + heat_demand_data = {} + cop_data = {} + + for hp_name in heat_pumps.index: + p_set = heat_pumps.loc[hp_name, 'p_set'] + + # Heat demand: base profile scaled by nominal power with random variation + # Assume average COP of 3.5, so thermal = electrical * 3.5 + base_thermal = p_set * 3.5 + + # Add individual variation (±20%) + individual_factor = 0.8 + 0.4 * np.random.random(len(timeindex)) + heat_demand = base_profile * base_thermal * individual_factor + + # Add some random noise + heat_demand += np.random.normal(0, 0.001, len(timeindex)) + heat_demand = np.maximum(heat_demand, 0) # No negative demand + + heat_demand_data[hp_name] = heat_demand + + # Individual COP with small variation + individual_cop = cop_profile + np.random.normal(0, 0.1, len(timeindex)) + individual_cop = np.clip(individual_cop, 2.5, 4.5) + cop_data[hp_name] = individual_cop + + # Set data + edisgo.heat_pump.heat_demand_df = pd.DataFrame(heat_demand_data, index=timeindex) + edisgo.heat_pump.cop_df = pd.DataFrame(cop_data, index=timeindex) + + print(f" ✓ Created time series:") + print(f" Heat demand range: {edisgo.heat_pump.heat_demand_df.min().min():.6f} - {edisgo.heat_pump.heat_demand_df.max().max():.6f} MW") + print(f" COP range: {edisgo.heat_pump.cop_df.min().min():.2f} - {edisgo.heat_pump.cop_df.max().max():.2f}") + + return edisgo + + +def create_cp_timeseries(edisgo, scenario="eGon2035", num_days=30): + """ + Create realistic charging demand time series for charging points. + + Parameters + ---------- + edisgo : EDisGo + EDisGo object with charging points + scenario : str + Scenario name for time index + num_days : int + Number of days to simulate (default: 30 for one month) + + Returns + ------- + EDisGo + EDisGo object with CP time series + """ + print(f"\n4a. Creating charging point time series for {num_days} days...") + + # Get charging points + charging_points = edisgo.topology.loads_df[edisgo.topology.loads_df['type'] == 'charging_point'] + + if len(charging_points) == 0: + print(" ⚠ Warning: No charging points found") + return edisgo + + # Use same time index as heat pumps + timeindex = edisgo.timeseries.timeindex + + print(f" Creating time series for {len(charging_points)} charging points...") + print(f" Timeindex: {len(timeindex)} timesteps (hourly, {num_days} days)") + + # Create realistic charging profiles based on use case + hours = np.arange(len(timeindex)) + hour_of_day = timeindex.hour.values + day_of_week = timeindex.dayofweek.values # 0=Monday, 6=Sunday + + cp_load_data = {} + + for cp_name in charging_points.index: + p_set = charging_points.loc[cp_name, 'p_set'] + sector = charging_points.loc[cp_name, 'sector'] + + # Different profiles based on sector + if sector == 'home': + # Home charging: evening/night (18:00-07:00), higher on weekends + peak_hours = ((hour_of_day >= 18) | (hour_of_day <= 7)) + base_profile = np.where(peak_hours, 0.7, 0.1) + # Higher usage on weekends + weekend_mask = day_of_week >= 5 + base_profile[weekend_mask] *= 1.3 + + elif sector == 'work': + # Work charging: daytime on weekdays (08:00-17:00) + work_hours = (hour_of_day >= 8) & (hour_of_day <= 17) + weekday_mask = day_of_week < 5 + base_profile = np.where(work_hours & weekday_mask, 0.6, 0.05) + + else: # public/fast charging + # Public charging: distributed throughout day, peaks at noon and evening + base_profile = 0.3 + 0.4 * ( + np.exp(-((hour_of_day - 12) ** 2) / 12) + # Noon peak + np.exp(-((hour_of_day - 18) ** 2) / 12) # Evening peak + ) + + # Add randomness and individual variation + random_factor = 0.7 + 0.6 * np.random.random(len(timeindex)) + cp_load = base_profile * p_set * random_factor + + # Add some random noise + cp_load += np.random.normal(0, p_set * 0.05, len(timeindex)) + cp_load = np.maximum(cp_load, 0) # No negative load + cp_load = np.minimum(cp_load, p_set) # Cap at nominal power + + cp_load_data[cp_name] = cp_load + + # Add CP loads to timeseries (they will be added to loads_active_power) + cp_load_df = pd.DataFrame(cp_load_data, index=timeindex) + + # Store for later use + if not hasattr(edisgo, 'charging_point_loads'): + edisgo.charging_point_loads = cp_load_df + else: + edisgo.charging_point_loads = cp_load_df + + print(f" ✓ Created time series:") + print(f" Load range: {cp_load_df.min().min():.6f} - {cp_load_df.max().max():.6f} MW") + print(f" Average load: {cp_load_df.mean().mean():.6f} MW") + + return edisgo + + +def setup_edisgo(grid_path, scenario="eGon2035", num_hps=50, num_cps=30, num_days=30): + """ + Load grid and setup components for time series analysis. + + Parameters + ---------- + grid_path : str + Path to ding0 grid folder + scenario : str + Scenario name (default: eGon2035) + num_hps : int + Number of heat pumps to add (default: 50) + num_cps : int + Number of charging points to add (default: 30) + num_days : int + Number of days to simulate (default: 30) + + Returns + ------- + EDisGo + Initialized EDisGo object with time series + """ + print(f"\n{'='*80}") + print(f"🔧 Setting up EDisGo Grid") + print(f"{'='*80}") + print(f"Grid path: {grid_path}") + print(f"Scenario: {scenario}") + + # Load grid + print("\n1. Loading ding0 grid...") + edisgo = EDisGo(ding0_grid=grid_path, legacy_ding0_grids=False) + + # Import generators + print("2. Importing generators...") + edisgo.import_generators(generator_scenario=scenario) + + # Add heat pumps manually + edisgo = add_heat_pumps_manually(edisgo, num_hps=num_hps) + + # Add charging points manually + edisgo = add_charging_points_manually(edisgo, num_cps=num_cps) + + # Create HP time series (this sets the timeindex) + edisgo = create_hp_timeseries(edisgo, scenario=scenario, num_days=num_days) + + # Create CP time series + edisgo = create_cp_timeseries(edisgo, scenario=scenario, num_days=num_days) + + # Store HP timeindex + hp_timeindex = edisgo.timeseries.timeindex + num_timesteps = len(hp_timeindex) + + # Set time series for other components (generators, loads) + print("\n5. Setting time series for generators and loads...") + + # Create simple time series for generators (use nominal power) + generators = edisgo.topology.generators_df + if len(generators) > 0: + # Active power: use p_nom for all timesteps + gen_active = pd.DataFrame( + data=generators['p_nom'].values.reshape(1, -1).repeat(num_timesteps, axis=0), + columns=generators.index, + index=hp_timeindex + ) + edisgo.timeseries._generators_active_power = gen_active + + # Reactive power: zeros + gen_reactive = pd.DataFrame( + data=0.0, + columns=generators.index, + index=hp_timeindex + ) + edisgo.timeseries._generators_reactive_power = gen_reactive + print(f" ✓ Created time series for {len(generators)} generators") + + # Create simple time series for other loads (use nominal power) + other_loads = edisgo.topology.loads_df[edisgo.topology.loads_df['type'] != 'heat_pump'] + if len(other_loads) > 0: + # Active power: use p_set for all timesteps + load_active = pd.DataFrame( + data=other_loads['p_set'].values.reshape(1, -1).repeat(num_timesteps, axis=0), + columns=other_loads.index, + index=hp_timeindex + ) + edisgo.timeseries._loads_active_power = load_active + + # Reactive power: zeros + load_reactive = pd.DataFrame( + data=0.0, + columns=other_loads.index, + index=hp_timeindex + ) + edisgo.timeseries._loads_reactive_power = load_reactive + print(f" ✓ Created time series for {len(other_loads)} other loads") + + # Calculate HP loads from heat demand and COP + print("6. Calculating heat pump electrical loads...") + heat_pumps = edisgo.topology.loads_df[edisgo.topology.loads_df['type'] == 'heat_pump'] + + # Initialize loads_active_power with existing data if any, or create empty + if not hasattr(edisgo.timeseries, '_loads_active_power') or edisgo.timeseries._loads_active_power is None: + edisgo.timeseries._loads_active_power = pd.DataFrame(index=hp_timeindex) + + if not hasattr(edisgo.timeseries, '_loads_reactive_power') or edisgo.timeseries._loads_reactive_power is None: + edisgo.timeseries._loads_reactive_power = pd.DataFrame(index=hp_timeindex) + + # Add HP electrical loads + for hp_name in heat_pumps.index: + hp_load = edisgo.heat_pump.heat_demand_df[hp_name] / edisgo.heat_pump.cop_df[hp_name] + edisgo.timeseries._loads_active_power[hp_name] = hp_load.values + edisgo.timeseries._loads_reactive_power[hp_name] = 0.0 + + print(f" ✓ Calculated electrical loads for {len(heat_pumps)} heat pumps") + + # Add charging point loads + if hasattr(edisgo, 'charging_point_loads'): + charging_points = edisgo.topology.loads_df[edisgo.topology.loads_df['type'] == 'charging_point'] + for cp_name in charging_points.index: + edisgo.timeseries._loads_active_power[cp_name] = edisgo.charging_point_loads[cp_name].values + edisgo.timeseries._loads_reactive_power[cp_name] = 0.0 + print(f" ✓ Added loads for {len(charging_points)} charging points") + + # Initial analysis + print("7. Running initial power flow analysis...") + edisgo.analyze() + + print("\n✓ Grid setup complete!") + print(f" Timeindex: {len(edisgo.timeseries.timeindex)} timesteps") + print(f" Start: {edisgo.timeseries.timeindex[0]}") + print(f" End: {edisgo.timeseries.timeindex[-1]}") + + # Heat pump statistics + print(f"\n Heat pumps: {len(heat_pumps)}") + print(f" Power range: {heat_pumps['p_set'].min()*1000:.1f} - {heat_pumps['p_set'].max()*1000:.1f} kW") + print(f" §14a eligible (>4.2 kW): {len(heat_pumps[heat_pumps['p_set'] > 0.0042])}") + + # Charging point statistics + charging_points = edisgo.topology.loads_df[edisgo.topology.loads_df['type'] == 'charging_point'] + if len(charging_points) > 0: + print(f"\n Charging points: {len(charging_points)}") + print(f" Power range: {charging_points['p_set'].min()*1000:.1f} - {charging_points['p_set'].max()*1000:.1f} kW") + print(f" §14a eligible (>4.2 kW): {len(charging_points[charging_points['p_set'] > 0.0042])}") + + # DEBUG: Verify charging_points_df property works + cp_via_property = edisgo.topology.charging_points_df + print(f" DEBUG: topology.charging_points_df returns {len(cp_via_property)} CPs") + if len(cp_via_property) != len(charging_points): + print(f" ⚠️ WARNING: Mismatch between direct query and property!") + + return edisgo + + +def run_optimization_14a(edisgo): + """ + Run optimization with §14a curtailment enabled. + + Uses opf_version=3 which minimizes line losses, maximal line loading, and HV slacks. + + Parameters + ---------- + edisgo : EDisGo + EDisGo object with time series + + Returns + ------- + EDisGo + EDisGo object with optimization results + """ + print(f"\n{'='*80}") + print(f"⚡ Running OPF with §14a Curtailment") + print(f"{'='*80}") + print(f"\nUsing OPF version 3:") + print(f" - Minimize line losses") + print(f" - Minimize maximal line loading") + print(f" - Minimize HV slacks") + print(f" - §14a curtailment enabled for heat pumps and charging points") + + start_time = datetime.now() + + # Run optimization + edisgo.pm_optimize(opf_version=2, curtailment_14a=True) + + duration = (datetime.now() - start_time).total_seconds() + + print(f"\n✓ Optimization complete!") + print(f" Duration: {duration:.1f} seconds ({duration/60:.1f} minutes)") + + return edisgo + + +def analyze_curtailment_results(edisgo, output_dir="results_14a"): + """ + Analyze §14a curtailment results and generate statistics. + + Parameters + ---------- + edisgo : EDisGo + EDisGo object with optimization results + output_dir : str + Directory to save results + + Returns + ------- + dict + Dictionary with analysis results + """ + print(f"\n{'='*80}") + print(f"📊 Analyzing §14a Curtailment Results") + print(f"{'='*80}") + + # Create output directory + Path(output_dir).mkdir(parents=True, exist_ok=True) + + # Get curtailment data for both heat pumps and charging points + hp_gen_cols = [col for col in edisgo.timeseries.generators_active_power.columns + if 'hp_14a_support' in col] + cp_gen_cols = [col for col in edisgo.timeseries.generators_active_power.columns + if 'cp_14a_support' in col or 'charging_point_14a_support' in col] + + all_gen_cols = hp_gen_cols + cp_gen_cols + + if len(all_gen_cols) == 0: + print("⚠ WARNING: No §14a virtual generators found in results!") + return {} + + curtailment = edisgo.timeseries.generators_active_power[all_gen_cols] + + # Get heat pump and charging point load data + heat_pumps = edisgo.topology.loads_df[edisgo.topology.loads_df['type'] == 'heat_pump'] + charging_points = edisgo.topology.loads_df[edisgo.topology.loads_df['type'] == 'charging_point'] + + all_flexible_loads = pd.concat([heat_pumps, charging_points]) + flexible_loads = edisgo.timeseries.loads_active_power[all_flexible_loads.index] + + # Separate for detailed analysis + hp_loads = edisgo.timeseries.loads_active_power[heat_pumps.index] if len(heat_pumps) > 0 else pd.DataFrame() + cp_loads = edisgo.timeseries.loads_active_power[charging_points.index] if len(charging_points) > 0 else pd.DataFrame() + + # Calculate statistics + total_curtailment = curtailment.sum().sum() + total_flexible_load = flexible_loads.sum().sum() + total_hp_load = hp_loads.sum().sum() if len(hp_loads) > 0 else 0 + total_cp_load = cp_loads.sum().sum() if len(cp_loads) > 0 else 0 + curtailment_percentage = (total_curtailment / total_flexible_load * 100) if total_flexible_load > 0 else 0 + + flexible_curtailment_total = curtailment.sum() + curtailed_units = flexible_curtailment_total[flexible_curtailment_total > 0] + + # Separate HP and CP curtailment + hp_curtailment_total = curtailment[hp_gen_cols].sum() if len(hp_gen_cols) > 0 else pd.Series() + cp_curtailment_total = curtailment[cp_gen_cols].sum() if len(cp_gen_cols) > 0 else pd.Series() + curtailed_hps = hp_curtailment_total[hp_curtailment_total > 0] if len(hp_curtailment_total) > 0 else pd.Series() + curtailed_cps = cp_curtailment_total[cp_curtailment_total > 0] if len(cp_curtailment_total) > 0 else pd.Series() + + # Time series statistics + curtailment_per_timestep = curtailment.sum(axis=1) + max_curtailment_timestep = curtailment_per_timestep.idxmax() + max_curtailment_value = curtailment_per_timestep.max() + + # Daily statistics + curtailment_daily = curtailment_per_timestep.resample('D').sum() + + # Monthly statistics + curtailment_monthly = curtailment_per_timestep.resample('M').sum() + + results = { + 'total_curtailment_MWh': total_curtailment, + 'total_flexible_load_MWh': total_flexible_load, + 'total_hp_load_MWh': total_hp_load, + 'total_cp_load_MWh': total_cp_load, + 'curtailment_percentage': curtailment_percentage, + 'num_virtual_gens': len(all_gen_cols), + 'num_hp_gens': len(hp_gen_cols), + 'num_cp_gens': len(cp_gen_cols), + 'num_curtailed_hps': len(curtailed_hps), + 'num_curtailed_cps': len(curtailed_cps), + 'max_curtailment_MW': curtailment.max().max(), + 'max_curtailment_timestep': max_curtailment_timestep, + 'max_curtailment_value_MW': max_curtailment_value, + 'curtailment_per_timestep': curtailment_per_timestep, + 'curtailment_daily': curtailment_daily, + 'curtailment_monthly': curtailment_monthly, + 'curtailment_data': curtailment, + 'hp_curtailment_data': curtailment[hp_gen_cols] if len(hp_gen_cols) > 0 else pd.DataFrame(), + 'cp_curtailment_data': curtailment[cp_gen_cols] if len(cp_gen_cols) > 0 else pd.DataFrame(), + 'hp_loads': hp_loads, + 'cp_loads': cp_loads, + 'flexible_loads': flexible_loads, + 'hp_curtailment_total': hp_curtailment_total, + 'cp_curtailment_total': cp_curtailment_total, + 'curtailed_hps': curtailed_hps, + 'curtailed_cps': curtailed_cps + } + + # Print summary + print(f"\n📈 Summary Statistics:") + print(f" Virtual generators: {len(all_gen_cols)} (HPs: {len(hp_gen_cols)}, CPs: {len(cp_gen_cols)})") + print(f" Heat pumps curtailed: {len(curtailed_hps)} / {len(heat_pumps)}") + print(f" Charging points curtailed: {len(curtailed_cps)} / {len(charging_points)}") + print(f" Total curtailment: {total_curtailment:.2f} MWh") + print(f" Total flexible load: {total_flexible_load:.2f} MWh (HP: {total_hp_load:.2f}, CP: {total_cp_load:.2f})") + print(f" Curtailment ratio: {curtailment_percentage:.2f}%") + print(f" Max curtailment: {curtailment.max().max():.4f} MW") + print(f" Max total curtailment (timestep): {max_curtailment_value:.4f} MW at {max_curtailment_timestep}") + + if len(curtailed_hps) > 0: + print(f"\n Top 5 curtailed heat pumps:") + for i, (hp, value) in enumerate(curtailed_hps.sort_values(ascending=False).head().items(), 1): + hp_name = hp.replace('hp_14a_support_', '') + print(f" {i}. {hp_name}: {value:.4f} MWh") + + if len(curtailed_cps) > 0: + print(f"\n Top 5 curtailed charging points:") + for i, (cp, value) in enumerate(curtailed_cps.sort_values(ascending=False).head().items(), 1): + cp_name = cp.replace('cp_14a_support_', '').replace('charging_point_14a_support_', '') + print(f" {i}. {cp_name}: {value:.4f} MWh") + + # Save statistics to CSV + stats_df = pd.DataFrame({ + 'Metric': [ + 'Total Curtailment (MWh)', + 'Total Flexible Load (MWh)', + 'Total HP Load (MWh)', + 'Total CP Load (MWh)', + 'Curtailment Percentage (%)', + 'Virtual Generators (Total)', + 'Virtual Generators (HPs)', + 'Virtual Generators (CPs)', + 'Curtailed HPs', + 'Curtailed CPs', + 'Max Curtailment (MW)', + 'Max Total Curtailment (MW)' + ], + 'Value': [ + f"{total_curtailment:.2f}", + f"{total_flexible_load:.2f}", + f"{total_hp_load:.2f}", + f"{total_cp_load:.2f}", + f"{curtailment_percentage:.2f}", + len(all_gen_cols), + len(hp_gen_cols), + len(cp_gen_cols), + len(curtailed_hps), + len(curtailed_cps), + f"{curtailment.max().max():.4f}", + f"{max_curtailment_value:.4f}" + ] + }) + stats_df.to_csv(f"{output_dir}/summary_statistics.csv", index=False) + print(f"\n✓ Summary statistics saved to {output_dir}/summary_statistics.csv") + + # Save detailed curtailment data + curtailment.to_csv(f"{output_dir}/curtailment_timeseries.csv") + curtailment_daily.to_csv(f"{output_dir}/curtailment_daily.csv") + curtailment_monthly.to_csv(f"{output_dir}/curtailment_monthly.csv") + if len(hp_curtailment_total) > 0: + hp_curtailment_total.to_csv(f"{output_dir}/hp_curtailment_total.csv") + if len(cp_curtailment_total) > 0: + cp_curtailment_total.to_csv(f"{output_dir}/cp_curtailment_total.csv") + + print(f"✓ Detailed data saved to {output_dir}/") + + return results + + +def create_plots(results, output_dir="results_14a"): + """ + Create comprehensive visualization plots. + + Parameters + ---------- + results : dict + Results dictionary from analyze_curtailment_results + output_dir : str + Directory to save plots + """ + print(f"\n{'='*80}") + print(f"📊 Creating Visualization Plots") + print(f"{'='*80}") + + curtailment = results['curtailment_data'] + hp_loads = results['hp_loads'] + cp_loads = results.get('cp_loads', pd.DataFrame()) + curtailment_per_timestep = results['curtailment_per_timestep'] + curtailment_daily = results['curtailment_daily'] + curtailment_monthly = results['curtailment_monthly'] + hp_curtailment_total = results['hp_curtailment_total'] + cp_curtailment_total = results.get('cp_curtailment_total', pd.Series()) + curtailed_hps = results['curtailed_hps'] + curtailed_cps = results.get('curtailed_cps', pd.Series()) + + # Plot 1: Time series curtailment + num_days = len(curtailment_per_timestep) // 24 + print(f"1. Creating {num_days}-day curtailment plot...") + fig, ax = plt.subplots(figsize=(16, 5)) + ax.plot(curtailment_per_timestep.index, curtailment_per_timestep.values, + 'r-', linewidth=1.5, alpha=0.7, marker='o', markersize=3) + ax.set_xlabel('Time', fontsize=12) + ax.set_ylabel('Total Curtailment (MW)', fontsize=12) + ax.set_title(f'§14a Heat Pump & Charging Point Curtailment - {num_days} Days', fontsize=14, fontweight='bold') + ax.grid(True, alpha=0.3) + plt.tight_layout() + plt.savefig(f"{output_dir}/01_curtailment_timeseries.png", dpi=300, bbox_inches='tight') + plt.close() + + # Plot 2: Daily curtailment + print("2. Creating daily curtailment plot...") + fig, ax = plt.subplots(figsize=(16, 5)) + ax.bar(curtailment_daily.index, curtailment_daily.values, width=1, color='red', alpha=0.7) + ax.set_xlabel('Date', fontsize=12) + ax.set_ylabel('Daily Curtailment (MWh)', fontsize=12) + ax.set_title('§14a Daily Curtailment', fontsize=14, fontweight='bold') + ax.grid(True, alpha=0.3, axis='y') + plt.tight_layout() + plt.savefig(f"{output_dir}/02_curtailment_daily.png", dpi=300, bbox_inches='tight') + plt.close() + + # Plot 3: Monthly curtailment + print("3. Creating monthly curtailment plot...") + fig, ax = plt.subplots(figsize=(12, 6)) + months = [d.strftime('%b %Y') for d in curtailment_monthly.index] + ax.bar(months, curtailment_monthly.values, color='red', alpha=0.7) + ax.set_xlabel('Month', fontsize=12) + ax.set_ylabel('Monthly Curtailment (MWh)', fontsize=12) + ax.set_title('§14a Monthly Curtailment', fontsize=14, fontweight='bold') + ax.grid(True, alpha=0.3, axis='y') + plt.xticks(rotation=45, ha='right') + plt.tight_layout() + plt.savefig(f"{output_dir}/03_curtailment_monthly.png", dpi=300, bbox_inches='tight') + plt.close() + + # Plot 4: Top 10 curtailed units (HPs and CPs combined) + print("4. Creating top curtailed units plot...") + fig, ax = plt.subplots(figsize=(12, 6)) + all_curtailed_list = [] + if len(curtailed_hps) > 0: + all_curtailed_list.append(curtailed_hps) + if len(curtailed_cps) > 0: + all_curtailed_list.append(curtailed_cps) + + if len(all_curtailed_list) > 0: + all_curtailed = pd.concat(all_curtailed_list) + top10 = all_curtailed.sort_values(ascending=False).head(10) + unit_names = [name.replace('hp_14a_support_', 'HP: ').replace('cp_14a_support_', 'CP: ').replace('charging_point_14a_support_', 'CP: ') for name in top10.index] + colors = ['blue' if 'HP:' in name else 'green' for name in unit_names] + ax.barh(unit_names, top10.values, color=colors, alpha=0.7) + else: + ax.text(0.5, 0.5, 'No curtailed units found', ha='center', va='center', transform=ax.transAxes) + + ax.set_xlabel('Total Curtailment (MWh)', fontsize=12) + ax.set_ylabel('Unit', fontsize=12) + ax.set_title('Top 10 Curtailed Units (Heat Pumps & Charging Points)', fontsize=14, fontweight='bold') + ax.grid(True, alpha=0.3, axis='x') + ax.invert_yaxis() + plt.tight_layout() + plt.savefig(f"{output_dir}/04_top10_curtailed_hps.png", dpi=300, bbox_inches='tight') + plt.close() + + # Plot 5: Curtailment distribution (histogram) + print("5. Creating curtailment distribution plot...") + fig, ax = plt.subplots(figsize=(10, 6)) + curtailment_nonzero = curtailment_per_timestep[curtailment_per_timestep > 0] + ax.hist(curtailment_nonzero.values, bins=50, color='red', alpha=0.7, edgecolor='black') + ax.set_xlabel('Curtailment (MW)', fontsize=12) + ax.set_ylabel('Frequency', fontsize=12) + ax.set_title('Distribution of Non-Zero Curtailment Events', fontsize=14, fontweight='bold') + ax.grid(True, alpha=0.3, axis='y') + plt.tight_layout() + plt.savefig(f"{output_dir}/05_curtailment_distribution.png", dpi=300, bbox_inches='tight') + plt.close() + + # Plot 6: Detailed view of most curtailed HP + print("6. Creating detailed HP profile plot...") + most_curtailed = hp_curtailment_total.idxmax() + hp_original_name = most_curtailed.replace('hp_14a_support_', '') + + if hp_original_name in hp_loads.columns: + fig, axes = plt.subplots(2, 1, figsize=(16, 10)) + + # Full year + original_load = hp_loads[hp_original_name] + curtailment_power = curtailment[most_curtailed] + net_load = original_load - curtailment_power + + ax1 = axes[0] + ax1.plot(original_load.index, original_load.values, 'b-', linewidth=0.5, + label='Original Load', alpha=0.7) + ax1.plot(net_load.index, net_load.values, 'g-', linewidth=0.5, + label='Net Load (after curtailment)', alpha=0.7) + ax1.axhline(y=0.0042, color='orange', linestyle='--', linewidth=1, + label='§14a Minimum (4.2 kW)') + ax1.set_xlabel('Time', fontsize=12) + ax1.set_ylabel('Power (MW)', fontsize=12) + ax1.set_title(f'{hp_original_name} - Full Year Profile', fontsize=14, fontweight='bold') + ax1.grid(True, alpha=0.3) + ax1.legend() + + # Sample week (first week with curtailment) + curtailment_weeks = curtailment_power.resample('W').sum() + first_curtailment_week = curtailment_weeks[curtailment_weeks > 0].index[0] + week_start = first_curtailment_week + week_end = week_start + pd.Timedelta(days=7) + + ax2 = axes[1] + week_mask = (original_load.index >= week_start) & (original_load.index < week_end) + ax2.plot(original_load.index[week_mask], original_load.values[week_mask], + 'b-', marker='o', linewidth=2, label='Original Load', markersize=3) + ax2.plot(net_load.index[week_mask], net_load.values[week_mask], + 'g-', marker='s', linewidth=2, label='Net Load', markersize=3) + ax2.fill_between(original_load.index[week_mask], + net_load.values[week_mask], + original_load.values[week_mask], + alpha=0.3, color='red', label='Curtailed Power') + ax2.axhline(y=0.0042, color='orange', linestyle='--', linewidth=2, + label='§14a Minimum (4.2 kW)') + ax2.set_xlabel('Time', fontsize=12) + ax2.set_ylabel('Power (MW)', fontsize=12) + ax2.set_title(f'{hp_original_name} - Sample Week with Curtailment', + fontsize=14, fontweight='bold') + ax2.grid(True, alpha=0.3) + ax2.legend() + + plt.tight_layout() + plt.savefig(f"{output_dir}/detailed_hp_profiles.png", dpi=300, bbox_inches='tight') + plt.close() + + # Plot 3: Charging Point Analysis (if CPs were curtailed) + if 'curtailed_cps' in results and len(results['curtailed_cps']) > 0: + print("3. Creating detailed charging point profile plot...") + + cp_curtailment = results['cp_curtailment_data'] + cp_loads = results['cp_loads'] + curtailed_cps = results['curtailed_cps'] + + # Most curtailed CP detail + most_curtailed_cp = curtailed_cps.idxmax() + cp_original_name = most_curtailed_cp.replace('cp_14a_support_', '') + + if cp_original_name in cp_loads.columns: + fig, axes = plt.subplots(2, 1, figsize=(16, 10)) + + original_load = cp_loads[cp_original_name] + curtailment_power = cp_curtailment[most_curtailed_cp] + net_load = original_load - curtailment_power + + # Full period + ax1 = axes[0] + ax1.plot(original_load.index, original_load.values, 'b-', linewidth=0.5, + label='Original Load', alpha=0.7) + ax1.plot(net_load.index, net_load.values, 'g-', linewidth=0.5, + label='Net Load (after curtailment)', alpha=0.7) + ax1.axhline(y=0.0042, color='orange', linestyle='--', linewidth=1, + label='§14a Minimum (4.2 kW)') + ax1.set_xlabel('Time', fontsize=12) + ax1.set_ylabel('Power (MW)', fontsize=12) + ax1.set_title(f'{cp_original_name} - Full Period Profile', fontsize=14, fontweight='bold') + ax1.grid(True, alpha=0.3) + ax1.legend() + + # Sample week with curtailment + curtailment_weeks = curtailment_power.resample('W').sum() + first_curtailment_week = curtailment_weeks[curtailment_weeks > 0].index[0] if any(curtailment_weeks > 0) else curtailment_weeks.index[0] + week_start = first_curtailment_week + week_end = week_start + pd.Timedelta(days=7) + + ax2 = axes[1] + week_mask = (original_load.index >= week_start) & (original_load.index < week_end) + ax2.plot(original_load.index[week_mask], original_load.values[week_mask], + 'b-', marker='o', linewidth=2, label='Original Load', markersize=3) + ax2.plot(net_load.index[week_mask], net_load.values[week_mask], + 'g-', marker='s', linewidth=2, label='Net Load', markersize=3) + ax2.fill_between(original_load.index[week_mask], + net_load.values[week_mask], + original_load.values[week_mask], + alpha=0.3, color='orange', label='Curtailed Power') + ax2.axhline(y=0.0042, color='orange', linestyle='--', linewidth=2, + label='§14a Minimum (4.2 kW)') + ax2.set_xlabel('Time', fontsize=12) + ax2.set_ylabel('Power (MW)', fontsize=12) + ax2.set_title(f'{cp_original_name} - Sample Week', fontsize=14, fontweight='bold') + ax2.grid(True, alpha=0.3) + ax2.legend() + + plt.tight_layout() + plt.savefig(f"{output_dir}/detailed_cp_profiles.png", dpi=300, bbox_inches='tight') + plt.close() + + print(f"\n✓ All plots saved to {output_dir}/") + + +def main(): + # ============================================================================ + # CONFIGURATION - Edit these values directly + # ============================================================================ + + # Grid configuration + GRID_PATH = "../../30879" + SCENARIO = "eGon2035" + + # Simulation parameters + NUM_DAYS = 7 # Number of days to simulate (e.g., 7, 30, 365) + NUM_HEAT_PUMPS = 50 # Number of heat pumps to add + NUM_CHARGING_POINTS = 30 # Number of charging points to add + + # Output + OUTPUT_DIR = "./" + + # ============================================================================ + # END CONFIGURATION + # ============================================================================ + # Create directory name from configuration parameters + output_dir = f"{OUTPUT_DIR}/results_{NUM_DAYS}d_HP{NUM_HEAT_PUMPS}_CP{NUM_CHARGING_POINTS}_14a" + print(f"\n{'#'*80}") + print(f"# §14a EnWG Heat Pump Curtailment Analysis - {NUM_DAYS} Days") + print(f"{'#'*80}") + print(f"\nStarted at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + + try: + # Setup grid and load data + edisgo = setup_edisgo( + GRID_PATH, + scenario=SCENARIO, + num_hps=NUM_HEAT_PUMPS, + num_cps=NUM_CHARGING_POINTS, + num_days=NUM_DAYS + ) + + # Run optimization with §14a + edisgo = run_optimization_14a(edisgo) + + # Analyze results + results = analyze_curtailment_results(edisgo, output_dir=output_dir) + + if results: + # Create plots + create_plots(results, output_dir=output_dir) + + print(f"\n{'='*80}") + print(f"✓ Analysis Complete!") + print(f"{'='*80}") + print(f"\nResults saved to: {output_dir}/") + print(f" - summary_statistics.csv") + print(f" - curtailment_timeseries.csv") + print(f" - curtailment_daily.csv") + print(f" - curtailment_monthly.csv") + print(f" - hp_curtailment_total.csv") + print(f" - cp_curtailment_total.csv") + print(f" - curtailment_timeseries.png") + print(f" - detailed_hp_profiles.png") + print(f" - detailed_cp_profiles.png (if CPs curtailed)") + + print(f"\nCompleted at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + + except Exception as e: + print(f"\n❌ ERROR: {str(e)}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() From a0473e4a5ee5f2e9e16d74524c1da80281289c6f Mon Sep 17 00:00:00 2001 From: joda9 Date: Tue, 6 Jan 2026 14:57:19 +0100 Subject: [PATCH 40/43] add german optimization documentation --- doc/optimization_de.rst | 3007 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 3007 insertions(+) create mode 100644 doc/optimization_de.rst diff --git a/doc/optimization_de.rst b/doc/optimization_de.rst new file mode 100644 index 000000000..1573f9a36 --- /dev/null +++ b/doc/optimization_de.rst @@ -0,0 +1,3007 @@ +Julia-Optimierung in eDisGo mit PowerModels +======================================================================= + +Inhaltsverzeichnis +------------------ + +1. `Überblick <#überblick>`__ +2. `Notation und Meta-Variablen <#notation-und-meta-variablen>`__ +3. `Alle Julia-Variablen + (Tabellarisch) <#alle-julia-variablen-tabellarisch>`__ +4. `Zeitliche Einordnung der + Optimierung <#zeitliche-einordnung-der-optimierung>`__ +5. `Die analyze-Funktion <#die-analyze-funktion>`__ +6. `Die reinforce-Funktion <#die-reinforce-funktion>`__ +7. `Die §14a EnWG Optimierung <#die-14a-enwg-optimierung>`__ +8. `Zeitreihen-Nutzung <#zeitreihen-nutzung>`__ +9. `Dateipfade und Referenzen <#dateipfade-und-referenzen>`__ + +-------------- + +Überblick +--------- + +Die Julia-Optimierung in eDisGo verwendet **PowerModels.jl** zur Lösung +von Optimal Power Flow (OPF) Problemen. Der Workflow erfolgt über eine +Python-Julia-Schnittstelle: + +- **Python (eDisGo)**: Netzmodellierung, Zeitreihen, + Ergebnisverarbeitung +- **Julia (PowerModels)**: Mathematische Optimierung, Solver-Interface +- **Kommunikation**: JSON über stdin/stdout + +**Optimierungsziele:** - Minimierung von Netzverlusten - Einhaltung von +Spannungs- und Stromgrenzen - Flexibilitätsnutzung (Speicher, +Wärmepumpen, E-Autos, DSM) - Optional: §14a EnWG Abregelung mit +Zeitbudget-Constraints + +-------------- + +Notation und Meta-Variablen +--------------------------- + +Bevor wir die konkreten Optimierungsvariablen betrachten, hier eine +Übersicht über die **allgemeinen Variablen und Notation**, die im +Julia-Code verwendet werden: + +Meta-Variablen (nicht Teil des Optimierungsproblems) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++----------------+-------+-----------------------+--------------------+ +| Variable | Typ | Beschreibung | Verwendung | ++================+=======+=======================+====================+ +| ``pm`` | `` | PowerModels-Objekt | Enthält das | +| | Abstr | | gesamte | +| | actPo | | O | +| | werMo | | ptimierungsproblem | +| | del`` | | (Netz, Variablen, | +| | | | Constraints) | ++----------------+-------+-----------------------+--------------------+ +| ``nw`` oder | `` | Network-ID | Identifiziert | +| ``n`` | Int`` | (Zeitschritt-Index) | einen Zeitschritt | +| | | | im | +| | | | Mu | +| | | | lti-Period-Problem | +| | | | (0, 1, 2, …, T-1) | ++----------------+-------+-----------------------+--------------------+ +| ``nw_ids(pm)`` | ``Ar | Alle Network-IDs | Gibt alle | +| | ray{I | | Z | +| | nt}`` | | eitschritt-Indizes | +| | | | zurück, z.B. | +| | | | ``[0, | +| | | | 1, 2, ..., 8759]`` | +| | | | für 8760h | ++----------------+-------+-----------------------+--------------------+ +| ` | ``D | Referenzdaten für | Zugriff auf | +| `ref(pm, nw)`` | ict`` | Zeitschritt | Netzdaten eines | +| | | | bestimmten | +| | | | Zeitschritts | ++----------------+-------+-----------------------+--------------------+ +| ` | ``D | Variablen-Dictionary | Zugriff auf | +| `var(pm, nw)`` | ict`` | | Opt | +| | | | imierungsvariablen | +| | | | eines Zeitschritts | ++----------------+-------+-----------------------+--------------------+ +| ``model`` oder | ``Ju | Ju | Das | +| ``pm.model`` | MP.Mo | MP-Optimierungsmodell | zugrundeliegende | +| | del`` | | mathematische | +| | | | Optimierungsmodell | ++----------------+-------+-----------------------+--------------------+ + +Index-Variablen +~~~~~~~~~~~~~~~ + ++---------------+----------------+---------------------+---------------+ +| Variable | Bedeutung | Beschreibung | Beispiel | ++===============+================+=====================+===============+ +| ``i``, ``j`` | Bus-Index | Identifiziert | ``i=1`` = Bus | +| | | Knoten im Netzwerk | “Bus_MV_123” | ++---------------+----------------+---------------------+---------------+ +| ``l`` | Branch-Index | Identifiziert | ``l=5`` = | +| | (Lin | Leitungen und | Leitung | +| | e/Transformer) | Transformatoren | “Line_LV_456” | ++---------------+----------------+---------------------+---------------+ +| ``g`` | G | Identifiziert | ``g=3`` = | +| | enerator-Index | Generatoren (PV, | “PV_001” | +| | | Wind, BHKW, Slack) | | ++---------------+----------------+---------------------+---------------+ +| ``s`` | Storage-Index | Identifiziert | ``s=1`` = | +| | | Batteriespeicher | “Storage_1” | ++---------------+----------------+---------------------+---------------+ +| ``h`` | Heat | Identifiziert | ``h=2`` = | +| | Pump-Index | Wärmepumpen | “HP_LV_789” | ++---------------+----------------+---------------------+---------------+ +| ``c`` | Charging | Identifiziert | ``c=4`` = | +| | Point-Index | Ladepunkte für | “CP_LV_101” | +| | | E-Autos | | ++---------------+----------------+---------------------+---------------+ +| ``d`` | DSM-Index | Identifiziert | ``d=1`` = | +| | | DSM-Lasten | “DSM_Load_1” | ++---------------+----------------+---------------------+---------------+ +| ``t`` oder | Zei | Zeitpunkt im | ``t=0`` = | +| ``n`` | tschritt-Index | O | 2035-01-01 | +| | | ptimierungshorizont | 00:00, | +| | | | ``t=1`` = | +| | | | 01:00, … | ++---------------+----------------+---------------------+---------------+ + +PowerModels-Funktionen +~~~~~~~~~~~~~~~~~~~~~~ + ++--------------+--------------------+--------------------+--------------+ +| Funktion | Rückgabewert | Beschreibung | Beispiel | ++==============+====================+====================+==============+ +| ``ids(pm, : | ``Array{Int}`` | Gibt alle Bus-IDs | ``[1, 2, 3, | +| bus, nw=n)`` | | für Zeitschritt n | ..., 150]`` | +| | | zurück | | ++--------------+--------------------+--------------------+--------------+ +| `` | ``Array{Int}`` | Gibt alle | ``[1, 2, 3, | +| ids(pm, :bra | | Branch-IDs | ..., 200]`` | +| nch, nw=n)`` | | (Leitungen/Trafos) | | +| | | zurück | | ++--------------+--------------------+--------------------+--------------+ +| ``ids(pm, : | ``Array{Int}`` | Gibt alle | ``[1, 2, 3 | +| gen, nw=n)`` | | Generator-IDs | , ..., 50]`` | +| | | zurück | | ++--------------+--------------------+--------------------+--------------+ +| ``i | ``Array{Int}`` | Gibt alle | ` | +| ds(pm, :stor | | Storage-IDs zurück | `[1, 2, 3]`` | +| age, nw=n)`` | | | | ++--------------+--------------------+--------------------+--------------+ +| ``ref(pm, nw | ``Dict`` | Gibt Daten für Bus | ``{"vmin": | +| , :bus, i)`` | | i in Zeitschritt | 0.9, "vmax": | +| | | nw | 1.1, ...}`` | ++--------------+--------------------+--------------------+--------------+ +| ``r | ``Dict`` | Gibt Daten für | `` | +| ef(pm, nw, : | | Branch l in | {"rate_a": 0 | +| branch, l)`` | | Zeitschritt nw | .5, "br_r": | +| | | | 0.01, ...}`` | ++--------------+--------------------+--------------------+--------------+ +| ``var(pm, | ``JuMP.Variable`` | Gibt | JuMP-Var | +| nw, :p, l)`` | | Wir | iable-Objekt | +| | | kleistungsvariable | | +| | | für Branch l | | +| | | zurück | | ++--------------+--------------------+--------------------+--------------+ +| ``var(pm, | ``JuMP.Variable`` | Gibt | JuMP-Var | +| nw, :w, i)`` | | Spannungsvariable | iable-Objekt | +| | | für Bus i zurück | | ++--------------+--------------------+--------------------+--------------+ + +Typische Code-Muster +~~~~~~~~~~~~~~~~~~~~ + +1. Iteration über alle Zeitschritte +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: julia + + for n in nw_ids(pm) + # Code für Zeitschritt n + println("Verarbeite Zeitschritt $n") + end + +2. Iteration über alle Busse in einem Zeitschritt +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: julia + + for i in ids(pm, :bus, nw=n) + # Code für Bus i in Zeitschritt n + bus_data = ref(pm, n, :bus, i) + println("Bus $i: Vmin = $(bus_data["vmin"])") + end + +3. Zugriff auf Variablen +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: julia + + # Variable abrufen + w_i = var(pm, n, :w, i) # Spannungsvariable für Bus i, Zeitschritt n + + # Variable in Constraint verwenden + JuMP.@constraint(pm.model, w_i >= 0.9^2) # Untere Spannungsgrenze + +4. Variable erstellen und im Dictionary speichern +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: julia + + # Variablen-Dictionary für Zeitschritt n initialisieren + var(pm, n)[:p_hp14a] = JuMP.@variable( + pm.model, + [h in ids(pm, :gen_hp_14a, nw=n)], + base_name = "p_hp14a_$(n)", + lower_bound = 0.0 + ) + + # Später darauf zugreifen + for h in ids(pm, :gen_hp_14a, nw=n) + p_hp14a_h = var(pm, n, :p_hp14a, h) + end + +Multi-Network-Struktur +~~~~~~~~~~~~~~~~~~~~~~ + +PowerModels verwendet eine **Multi-Network-Struktur** für zeitabhängige +Optimierung: + +:: + + pm (PowerModel) + ├─ nw["0"] (Zeitschritt 0: 2035-01-01 00:00) + │ ├─ :bus → {1: {...}, 2: {...}, ...} ← Alle 150 Busse + │ ├─ :branch → {1: {...}, 2: {...}, ...} ← Alle 200 Leitungen/Trafos + │ ├─ :gen → {1: {...}, 2: {...}, ...} ← Alle 50 Generatoren + │ ├─ :load → {1: {...}, 2: {...}, ...} ← Alle 120 Lasten + │ └─ :storage → {1: {...}, 2: {...}, ...} ← Alle 5 Speicher + │ + ├─ nw["1"] (Zeitschritt 1: 2035-01-01 01:00) + │ ├─ :bus → {1: {...}, 2: {...}, ...} ← WIEDER alle 150 Busse + │ ├─ :branch → {1: {...}, 2: {...}, ...} ← WIEDER alle 200 Leitungen + │ └─ ... ← usw. + │ + ├─ nw["2"] (Zeitschritt 2: 2035-01-01 02:00) + │ ├─ :bus → {1: {...}, 2: {...}, ...} ← WIEDER alle 150 Busse + │ └─ ... + │ + ├─ ... (8757 weitere Zeitschritte) + │ + └─ nw["8759"] (Zeitschritt 8759: 2035-12-31 23:00) + └─ Komplettes Netz nochmal + +**WICHTIG: Das Netz existiert T-mal!** + +Für einen Optimierungshorizont von **8760 Stunden** (1 Jahr) bedeutet +das: - Das gesamte Netz wird **8760-mal dupliziert** - Jeder Zeitschritt +hat seine eigene vollständige Netz-Kopie - Alle Busse, Leitungen, +Trafos, Generatoren, Lasten existieren **8760-mal** - Jeder Zeitschritt +hat **eigene Optimierungsvariablen** + +**Was unterscheidet die Zeitschritte?** + ++--------+----------------------+--------------------------------------+ +| Aspekt | Zeitschritte | Unterschiedlich pro Zeitschritt | ++========+======================+======================================+ +| **Net | Identisch | Gleiche Busse, Leitungen, Trafos | +| ztopol | | | +| ogie** | | | ++--------+----------------------+--------------------------------------+ +| **Net | Identisch | Gleiche Widerstände, Kapazitäten | +| zparam | | | +| eter** | | | ++--------+----------------------+--------------------------------------+ +| ** | Unterschiedlich | Generator-Einspeisung, Lasten, COP | +| Zeitre | | | +| ihen-W | | | +| erte** | | | ++--------+----------------------+--------------------------------------+ +| * | Unterschiedlich | Spannungen, Leistungsflüsse, | +| *Varia | | Speicher-Leistung | +| blen** | | | ++--------+----------------------+--------------------------------------+ +| **Sp | Gekoppelt | SOC[t+1] hängt von SOC[t] ab | +| eicher | | | +| -SOC** | | | ++--------+----------------------+--------------------------------------+ + +**Beispiel: Wirkleistungsvariable p[l,i,j]** + +Für eine Leitung ``l=5`` zwischen Bus ``i=10`` und ``j=11``: - +``var(pm, 0, :p)[(5,10,11)]`` = Wirkleistung in Zeitschritt 0 (00:00 +Uhr) - ``var(pm, 1, :p)[(5,10,11)]`` = Wirkleistung in Zeitschritt 1 +(01:00 Uhr) - ``var(pm, 2, :p)[(5,10,11)]`` = Wirkleistung in +Zeitschritt 2 (02:00 Uhr) - … - ``var(pm, 8759, :p)[(5,10,11)]`` = +Wirkleistung in Zeitschritt 8759 (23:00 Uhr) + +→ **8760 verschiedene Variablen** für dieselbe Leitung! + +**Optimierungsproblem-Größe:** + +Für ein Netz mit: - 150 Busse - 200 Leitungen/Trafos - 50 Generatoren - +5 Batteriespeicher - 20 Wärmepumpen - 10 Ladepunkte - 8760 Zeitschritte +(1 Jahr, 1h-Auflösung) + +**Anzahl Variablen (grob):** - Spannungen: 150 Busse × 8760 Zeitschritte += **1,314,000 Variablen** - Leitungsflüsse: 200 × 2 (p,q) × 8760 = +**3,504,000 Variablen** - Generatoren: 50 × 2 (p,q) × 8760 = **876,000 +Variablen** - Speicher: 5 × 2 (Leistung + SOC) × 8760 = **87,600 +Variablen** - … + +→ **Mehrere Millionen Variablen** für Jahressimulation! + +**Warum dieser Ansatz?** + +**Vorteile:** - Erlaubt zeitgekoppelte Optimierung (Speicher, +Wärmepumpen) - PowerModels-Syntax bleibt einfach (jeder Zeitschritt wie +Einzelproblem) - Flexible Zeitreihen (unterschiedliche Werte pro +Zeitschritt) + +**Nachteile:** - Sehr großes Optimierungsproblem (Millionen Variablen) - +Hoher Speicherbedarf - Lange Lösungszeiten (Minuten bis Stunden) + +**Inter-Zeitschritt Constraints:** + +Bestimmte Constraints koppeln die Zeitschritte: + +.. code:: julia + + # Speicher-Energiekopplung + for n in 0:8758 # Alle Zeitschritte außer letzter + for s in storage_ids + # SOC in t+1 hängt von SOC in t und Leistung in t ab + @constraint(pm.model, + var(pm, n+1, :se, s) == + var(pm, n, :se, s) + var(pm, n, :ps, s) × Δt × η + ) + end + end + +→ Diese Constraints verbinden die sonst unabhängigen Zeitschritte! + +**Zusammenfassung:** - Jeder Zeitschritt hat eine **eigene vollständige +Kopie** des Netzes - Zeitreihen-Werte (Lasten, Einspeisung) +unterscheiden sich zwischen Zeitschritten - Variablen existieren **pro +Zeitschritt** (8760-mal für jede physikalische Variable!) - +Inter-zeitschritt Constraints (Speicher-SOC, Wärmespeicher) koppeln die +Zeitschritte - **Für 8760 Zeitschritte:** Das Netz existiert 8760-mal → +Millionen von Variablen + +-------------- + +Alle Julia-Variablen (Tabellarisch) +----------------------------------- + +Netz-Variablen (Grid Variables) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++------------------+---------------+------------+--------------------+ +| Variable | Dimension | Einheit | Beschreibung | ++==================+===============+============+====================+ +| ``p[l,i,j]`` | ℝ | MW | Wirkleistungsfluss | +| | | | auf Leitung/Trafo | +| | | | von Bus i zu Bus j | ++------------------+---------------+------------+--------------------+ +| ``q[l,i,j]`` | ℝ | MVAr | B | +| | | | lindleistungsfluss | +| | | | auf Leitung/Trafo | +| | | | von Bus i zu Bus j | ++------------------+---------------+------------+--------------------+ +| ``w[i]`` | ℝ⁺ | p.u.² | Quadrierte | +| | | | Spannungsamplitude | +| | | | an Bus i | ++------------------+---------------+------------+--------------------+ +| ``ccm[l,i,j]`` | ℝ⁺ | kA² | Quadrierte | +| | | | Stromstärke auf | +| | | | Leitung/Trafo | ++------------------+---------------+------------+--------------------+ +| ``ll[l,i,j]`` | [0,1] | - | Leitungsauslastung | +| | | | (nur OPF Version 1 | +| | | | & 3) | ++------------------+---------------+------------+--------------------+ + +**Hinweise:** - ``l`` = Leitungs-/Trafo-ID - ``i,j`` = Bus-IDs +(from_bus, to_bus) - Quadrierte Variablen vermeiden nichtkonvexe +Wurzelfunktionen + +-------------- + +Generator-Variablen (Generation Variables) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++---------------+-----------------+-------------+---------------------+ +| Variable | Dimension | Einheit | Beschreibung | ++===============+=================+=============+=====================+ +| ``pg[g]`` | ℝ | MW | Wirkl | +| | | | eistungseinspeisung | +| | | | Generator g | ++---------------+-----------------+-------------+---------------------+ +| ``qg[g]`` | ℝ | MVAr | Blindl | +| | | | eistungseinspeisung | +| | | | Generator g | ++---------------+-----------------+-------------+---------------------+ +| ``pgc[g]`` | ℝ⁺ | MW | Abregelung | +| | | | nicht-regelbarer | +| | | | Generatoren | +| | | | (Curtailment) | ++---------------+-----------------+-------------+---------------------+ +| ``pgs`` | ℝ | MW | Slack-Generator | +| | | | Wirkleistung | +| | | | (Netzanschluss) | ++---------------+-----------------+-------------+---------------------+ +| ``qgs`` | ℝ | MVAr | Slack-Generator | +| | | | Blindleistung | ++---------------+-----------------+-------------+---------------------+ + +**Hinweise:** - Slack-Generator repräsentiert Übertragungsnetz-Anschluss +- Curtailment nur für EE-Anlagen (PV, Wind) + +-------------- + +Batteriespeicher-Variablen (Battery Storage Variables) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++---------------+-----------------+-------------+---------------------+ +| Variable | Dimension | Einheit | Beschreibung | ++===============+=================+=============+=====================+ +| ``ps[s,t]`` | ℝ | MW | Wirkleistung | +| | | | Batteriespeicher s | +| | | | zum Zeitpunkt t (+ | +| | | | = Entladung, - = | +| | | | Ladung) | ++---------------+-----------------+-------------+---------------------+ +| ``qs[s,t]`` | ℝ | MVAr | Blindleistung | +| | | | Batteriespeicher s | ++---------------+-----------------+-------------+---------------------+ +| ``se[s,t]`` | ℝ⁺ | MWh | Energieinhalt | +| | | | (State of Energy) | +| | | | Batteriespeicher s | ++---------------+-----------------+-------------+---------------------+ + +**Constraints:** - SOC-Kopplung zwischen Zeitschritten: +``se[t+1] = se[t] + ps[t] × Δt × η`` - Kapazitätsgrenzen: +``se_min ≤ se[t] ≤ se_max`` - Leistungsgrenzen: +``ps_min ≤ ps[t] ≤ ps_max`` + +-------------- + +Wärmepumpen-Variablen (Heat Pump Variables) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++---------------+-----------------+-------------+---------------------+ +| Variable | Dimension | Einheit | Beschreibung | ++===============+=================+=============+=====================+ +| ``php[h,t]`` | ℝ⁺ | MW | Elektrische | +| | | | Leistungsaufnahme | +| | | | Wärmepumpe h | ++---------------+-----------------+-------------+---------------------+ +| ``qhp[h,t]`` | ℝ | MVAr | Blindleistung | +| | | | Wärmepumpe h | ++---------------+-----------------+-------------+---------------------+ +| ``phs[h,t]`` | ℝ | MW | Leistung | +| | | | Wärmespeicher h (+ | +| | | | = Beladung, - = | +| | | | Entladung) | ++---------------+-----------------+-------------+---------------------+ +| ``hse[h,t]`` | ℝ⁺ | MWh | Energieinhalt | +| | | | Wärmespeicher h | ++---------------+-----------------+-------------+---------------------+ + +**Hinweise:** - Wärmepumpen mit thermischem Speicher können zeitlich +verschoben werden - Wärmebedarf muss über Optimierungshorizont gedeckt +werden - COP (Coefficient of Performance) verknüpft elektrische und +thermische Leistung + +-------------- + +Ladepunkt-Variablen (Charging Point / EV Variables) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ++----------------+----------------+-------------+---------------------+ +| Variable | Dimension | Einheit | Beschreibung | ++================+================+=============+=====================+ +| ``pcp[c,t]`` | ℝ⁺ | MW | Ladeleistung | +| | | | Ladepunkt c zum | +| | | | Zeitpunkt t | ++----------------+----------------+-------------+---------------------+ +| ``qcp[c,t]`` | ℝ | MVAr | Blindleistung | +| | | | Ladepunkt c | ++----------------+----------------+-------------+---------------------+ +| ``cpe[c,t]`` | ℝ⁺ | MWh | Energieinhalt | +| | | | Fahrzeugbatterie am | +| | | | Ladepunkt c | ++----------------+----------------+-------------+---------------------+ + +**Constraints:** - Energiekopplung: +``cpe[t+1] = cpe[t] + pcp[t] × Δt × η`` - Kapazität: +``cpe_min ≤ cpe[t] ≤ cpe_max`` - Ladeleistung: ``0 ≤ pcp[t] ≤ pcp_max`` + +-------------- + +DSM-Variablen (Demand Side Management Variables) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +============= ========= ======= ===================================== +Variable Dimension Einheit Beschreibung +============= ========= ======= ===================================== +``pdsm[d,t]`` ℝ⁺ MW Verschiebbare Last d zum Zeitpunkt t +``qdsm[d,t]`` ℝ MVAr Blindleistung DSM-Last d +``dsme[d,t]`` ℝ⁺ MWh Virtueller Energieinhalt DSM-Speicher +============= ========= ======= ===================================== + +**Hinweise:** - DSM modelliert verschiebbare Lasten (z.B. +Industrieprozesse) - Gesamtenergie über Horizont bleibt konstant + +-------------- + +Slack-Variablen für Netzrestriktionen (Slack Variables) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Nur in **OPF Version 2 & 4** (mit Netzrestriktionen): + ++----------------+-----------+---------+-------------------------------------------+ +| Variable | Dimension | Einheit | Beschreibung | ++================+===========+=========+===========================================+ +| ``phps[h,t]`` | ℝ⁺ | MW | Slack für Wärmepumpen-Restriktion | ++----------------+-----------+---------+-------------------------------------------+ +| ``phps2[h,t]`` | ℝ⁺ | MW | Slack für Wärmepumpen-Betriebsrestriktion | ++----------------+-----------+---------+-------------------------------------------+ +| ``phss[h,t]`` | ℝ⁺ | MW | Slack für Wärmespeicher-Restriktion | ++----------------+-----------+---------+-------------------------------------------+ +| ``pds[d,t]`` | ℝ⁺ | MW | Lastabwurf (Load Shedding) | ++----------------+-----------+---------+-------------------------------------------+ +| ``pgens[g,t]`` | ℝ⁺ | MW | Slack für Generator-Abregelung | ++----------------+-----------+---------+-------------------------------------------+ +| ``pcps[c,t]`` | ℝ⁺ | MW | Slack für Ladepunkt-Restriktion | ++----------------+-----------+---------+-------------------------------------------+ +| ``phvs[t]`` | ℝ⁺ | MW | Slack für Hochspannungs-Anforderungen | ++----------------+-----------+---------+-------------------------------------------+ + +**Zweck:** - Gewährleisten Lösbarkeit des Optimierungsproblems - Hohe +Kosten im Zielfunktional → werden minimiert - Zeigen an, wo +Netzrestriktionen nicht eingehalten werden können + +-------------- + +§14a EnWG Variablen (NEU) +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Nur wenn ``curtailment_14a=True``: + +Wärmepumpen §14a +^^^^^^^^^^^^^^^^ + ++----------------+----------------+-------------+---------------------+ +| Variable | Dimension | Einheit | Beschreibung | ++================+================+=============+=====================+ +| `` | ℝ⁺ | MW | Virtueller | +| p_hp14a[h,t]`` | | | Generator für | +| | | | WP-Abregelung (0 | +| | | | bis pmax) | ++----------------+----------------+-------------+---------------------+ +| `` | Binär | {0,1} | - | +| z_hp14a[h,t]`` | | | | ++----------------+----------------+-------------+---------------------+ + +Ladepunkte §14a +^^^^^^^^^^^^^^^ + ++----------------+----------------+-------------+---------------------+ +| Variable | Dimension | Einheit | Beschreibung | ++================+================+=============+=====================+ +| `` | ℝ⁺ | MW | Virtueller | +| p_cp14a[c,t]`` | | | Generator für | +| | | | CP-Abregelung (0 | +| | | | bis pmax) | ++----------------+----------------+-------------+---------------------+ +| `` | Binär | {0,1} | - | +| z_cp14a[c,t]`` | | | | ++----------------+----------------+-------------+---------------------+ + +**Wichtige Parameter:** - ``pmax = P_nominal - P_min_14a`` (maximale +Abregelleistung) - ``P_min_14a = 0.0042 MW`` (4.2 kW Mindestleistung +gemäß §14a) - ``max_hours_per_day`` (z.B. 2h/Tag Zeitbudget) + +**Funktionsweise:** - Virtueller Generator “erzeugt” Leistung am +WP/CP-Bus - Effekt: Nettolast = Original-Last - p_hp14a - Simuliert +Abregelung ohne komplexe Lastanpassung + +-------------- + +Zeitliche Einordnung der Optimierung +------------------------------------ + +Gesamter Workflow +~~~~~~~~~~~~~~~~~ + +**WICHTIGER HINWEIS zum Workflow:** - **Reinforce VOR der Optimierung:** +Nur optional und sinnvoll für das Basisnetz (z.B. ohne +Wärmepumpen/E-Autos). Wenn man vor der Optimierung bereits das komplette +Netz ausbaut, gibt es keine Überlastungen mehr und die Optimierung zur +Flexibilitätsnutzung macht keinen Sinn. - **Reinforce NACH der +Optimierung:** In der Regel erforderlich! Die Optimierung nutzt +Flexibilität um Netzausbau zu minimieren, kann aber nicht alle Probleme +lösen. Verbleibende Überlastungen und Spannungsverletzungen müssen durch +konventionellen Netzausbau behoben werden. + +**Typischer Anwendungsfall:** 1. Basisnetz laden (z.B. Ist-Zustand ohne +neue Wärmepumpen) 2. Optional: reinforce auf Basisnetz 3. (Neue) +Komponenten hinzufügen (Wärmepumpen, E-Autos für Zukunftsszenario) 4. +Optimierung durchführen → nutzt Flexibilität statt Netzausbau 5. +**Zwingend:** reinforce mit optimierten Zeitreihen → behebt verbleibende +Probleme + +:: + + ┌─────────────────────────────────────────────────────────────────────┐ + │ 1. INITIALISIERUNG │ + ├─────────────────────────────────────────────────────────────────────┤ + │ - Netzladung (ding0-Netz oder Datenbank) │ + │ - Import Zeitreihen (Generatoren, Lasten ohne neue Komponenten) │ + │ - Konfiguration Optimierungsparameter │ + └─────────────────────────────────────────────────────────────────────┘ + ↓ + ┌─────────────────────────────────────────────────────────────────────┐ + │ 2. BASISNETZ-VERSTÄRKUNG (optional) │ + ├─────────────────────────────────────────────────────────────────────┤ + │ edisgo.reinforce() │ + │ - Verstärkung des Basisnetzes (OHNE neue WP/CP) │ + │ - Sinnvoll als Referenzszenario │ + │ - Erstellt Ausgangsbasis für Szenariovergleich │ + │ │ + │ WICHTIG: Dies ist NICHT der Hauptverstärkungsschritt! │ + └─────────────────────────────────────────────────────────────────────┘ + ↓ + ┌─────────────────────────────────────────────────────────────────────┐ + │ 3. NEUE KOMPONENTEN HINZUFÜGEN │ + ├─────────────────────────────────────────────────────────────────────┤ + │ - Wärmepumpen hinzufügen (mit thermischen Speichern) │ + │ - E-Auto Ladepunkte hinzufügen (mit Flexibilitätsbändern) │ + │ - Batteriespeicher hinzufügen │ + │ - Zeitreihen für neue Komponenten setzen │ + │ │ + │ → Netz ist jetzt wahrscheinlich überlastet │ + │ → KEIN reinforce an dieser Stelle! │ + └─────────────────────────────────────────────────────────────────────┘ + ↓ + ┌─────────────────────────────────────────────────────────────────────┐ + │ 4. JULIA-OPTIMIERUNG │ + ├─────────────────────────────────────────────────────────────────────┤ + │ edisgo.pm_optimize(opf_version=2, curtailment_14a=True) │ + │ │ + │ ZIEL: Flexibilität nutzen um Netzausbau zu VERMEIDEN │ + │ - Batteriespeicher optimal laden/entladen │ + │ - Wärmepumpen zeitlich verschieben (thermischer Speicher) │ + │ - E-Auto-Ladung optimieren (innerhalb Flexibilitätsband) │ + │ - §14a Abregelung bei Engpässen (max. 2h/Tag) │ + │ │ + │ 4.1 PYTHON → POWERMODELS KONVERTIERUNG │ + │ ├─ to_powermodels(): Netz → PowerModels-Dictionary │ + │ ├─ Zeitreihen für alle Komponenten │ + │ ├─ Falls 14a: Virtuelle Generatoren für WP/CP erstellen │ + │ └─ Serialisierung zu JSON │ + │ │ + │ 4.2 PYTHON → JULIA KOMMUNIKATION │ + │ ├─ Starte Julia-Subprozess: julia Main.jl [args] │ + │ ├─ Übergabe JSON via stdin │ + │ └─ Args: grid_name, results_path, method (soc/nc), etc. │ + │ │ + │ 4.3 JULIA-OPTIMIERUNG │ + │ ├─ Parse JSON → PowerModels Multinetwork │ + │ ├─ Solver-Auswahl: Gurobi (SOC) oder IPOPT (NC) │ + │ ├─ build_mn_opf_bf_flex(): │ + │ │ ├─ Variablen erstellen (alle aus Tabellen oben) │ + │ │ ├─ Constraints pro Zeitschritt: │ + │ │ │ ├─ Leistungsbilanz an Knoten │ + │ │ │ ├─ Spannungsfallgleichungen │ + │ │ │ ├─ Stromgleichungen │ + │ │ │ ├─ Speicher-/WP-/CP-Zustandsgleichungen │ + │ │ │ ├─ §14a Binär-Kopplung (falls aktiviert) │ + │ │ │ └─ §14a Mindest-Nettolast (falls aktiviert) │ + │ │ ├─ Inter-Zeitschritt Constraints: │ + │ │ │ ├─ Energiekopplung Speicher/WP/CP │ + │ │ │ └─ §14a Tages-Zeitbudget (falls aktiviert) │ + │ │ └─ Zielfunktion setzen (versionsabhängig) │ + │ ├─ Optimierung lösen │ + │ ├─ Ergebnisse zu JSON serialisieren │ + │ └─ Output via stdout │ + │ │ + │ 4.4 JULIA → PYTHON KOMMUNIKATION │ + │ ├─ Python liest stdout zeilenweise │ + │ ├─ Erfasse JSON-Ergebnis (beginnt mit {"name") │ + │ └─ Parse JSON zu Dictionary │ + │ │ + │ 4.5 POWERMODELS → EDISGO KONVERTIERUNG │ + │ ├─ from_powermodels(): Extrahiere optimierte Zeitreihen │ + │ ├─ Schreibe zu edisgo.timeseries: │ + │ │ ├─ generators_active_power, generators_reactive_power │ + │ │ ├─ storage_units_active_power (optimiert) │ + │ │ ├─ heat_pump_loads (zeitlich verschoben) │ + │ │ ├─ charging_point_loads (optimiert) │ + │ │ └─ §14a Abregelung als virtuelle Generatoren: │ + │ │ ├─ hp_14a_support_{name} │ + │ │ └─ cp_14a_support_{name} │ + │ └─ Abregelung = Virtuelle Generator-Leistung │ + │ │ + │ ERGEBNIS: Optimierte Zeitreihen mit minimiertem Netzausbaubedarf │ + │ Aber: Evtl. verbleibende Überlastungen (Slacks > 0) │ + └─────────────────────────────────────────────────────────────────────┘ + ↓ + ┌─────────────────────────────────────────────────────────────────────┐ + │ 5. NETZAUSBAU MIT OPTIMIERTEN ZEITREIHEN (ZWINGEND!) │ + ├─────────────────────────────────────────────────────────────────────┤ + │ edisgo.reinforce() │ + │ │ + │ WICHTIG: Dieser Schritt ist ZWINGEND erforderlich! │ + │ │ + │ Warum? │ + │ - Optimierung nutzt Flexibilität, kann aber nicht alle Probleme │ + │ lösen (z.B. Netzrestriktionen, zu geringe Flexibilität) │ + │ - Slack-Variablen > 0 zeigen verbleibende Verletzungen │ + │ - Verbleibende Überlastungen müssen durch Netzausbau behoben │ + │ werden │ + │ │ + │ Ablauf: │ + │ - Iterative Verstärkungsmaßnahmen │ + │ - Leitungsausbau, Trafoausbau │ + │ - Berechnung Netzausbaukosten │ + │ │ + │ ERGEBNIS: Netzausbaukosten NACH Flexibilitätsnutzung │ + │ (deutlich geringer als ohne Optimierung!) │ + └─────────────────────────────────────────────────────────────────────┘ + ↓ + ┌─────────────────────────────────────────────────────────────────────┐ + │ 6. AUSWERTUNG │ + ├─────────────────────────────────────────────────────────────────────┤ + │ - Analyse optimierter Zeitreihen │ + │ - Berechnung §14a Statistiken (Abregelenergie, Zeitbudget-Nutzung) │ + │ - Vergleich Netzausbaukosten (mit vs. ohne Optimierung) │ + │ - Flexibilitätsnutzung analysieren │ + │ - Visualisierung, Export │ + └─────────────────────────────────────────────────────────────────────┘ + +-------------- + +Workflow-Varianten im Vergleich +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Die folgende Tabelle zeigt die wichtigsten Workflow-Varianten und deren +Anwendungsfälle: + ++--------------+--------------+------------------------+--------------+ +| Workflow | Schritte | Wann sinnvoll? | Ergebnis | ++==============+==============+========================+==============+ +| **A: Nur | 1. Netz | - Keine Flexibilitäten | Hohe | +| Netzausbau | laden2. | vorhanden- Schnelle | Ne | +| (ohne | Komponenten | konservative Planung- | tzausbaukost | +| Op | hinzufügen3. | Referenzszenario | enFlexibilit | +| timierung)** | ``r | | ätspotenzial | +| | einforce()`` | | ungenutzt | ++--------------+--------------+------------------------+--------------+ +| **B: Mit | 1. Netz | - Flexibilitäten | Minimale | +| Optimierung | laden2. | vorhanden (Speicher, | Netzausbauko | +| ( | Optional: | WP, CP)- §14a-Nutzung | stenOptimale | +| EMPFOHLEN)** | ``r | gewünscht- Minimierung | Flexibilität | +| | einforce()`` | Netzausbaukosten | snutzungBetr | +| | auf | | iebssicheres | +| | Basisnetz3. | | Netz | +| | Komponenten | | | +| | hinzufügen4. | | | +| | ``pm_opti | | | +| | mize()``\ 5. | | | +| | * | | | +| | *Zwingend:** | | | +| | ``r | | | +| | einforce()`` | | | ++--------------+--------------+------------------------+--------------+ +| **C: | 1. Netz | - Kostenvergleich | Kostent | +| Basisn | laden | mit/ohne neue | ransparenzAt | +| etz-Referenz | ( | Komponenten- Analyse | tributierung | +| + | Basisnetz)2. | Zusatzkosten durch | auf neue | +| O | ``r | WP/CP- Bewertung | Ko | +| ptimierung** | einforce()`` | §14a-Nutzen | mponentenQua | +| | → Kosten₁3. | | ntifizierung | +| | Neue | | Flexibi | +| | Komponenten | | litätsnutzen | +| | hinzufügen4. | | | +| | ``pm_opti | | | +| | mize()``\ 5. | | | +| | ``r | | | +| | einforce()`` | | | +| | → Kosten₂6. | | | +| | Vergleich: | | | +| | Kosten₂ - | | | +| | Kosten₁ | | | ++--------------+--------------+------------------------+--------------+ +| **D: Mehrere | 1. Netz | - Bewertung | V | +| Optimierung | laden + | verschiedener | ollständiger | +| sszenarien** | Komponenten | Flexibilitätsoptionen- | S | +| | h | Kosten-Nutzen-Analyse | zenariovergl | +| | inzufügen2a. | §14a- | eichOptimale | +| | ``r | Sensitivitätsanalyse | Strategiew | +| | einforce()`` | | ahlFundierte | +| | → | | Entscheidu | +| | Referenz2b. | | ngsgrundlage | +| | `` | | | +| | pm_optimize( | | | +| | 14a=False)`` | | | +| | + | | | +| | ``reinfo | | | +| | rce()``\ 2c. | | | +| | ` | | | +| | `pm_optimize | | | +| | (14a=True)`` | | | +| | + | | | +| | ``reinf | | | +| | orce()``\ 3. | | | +| | Vergleich | | | ++--------------+--------------+------------------------+--------------+ + +**Wichtige Erkenntnisse:** + +1. **Reinforce VOR Optimierung macht NUR Sinn für:** + + - Basisnetz ohne neue Komponenten (Referenzszenario) + - Dokumentation des Ausgangszustands + - Status quo soll ermittelt werden + - **NICHT nach Hinzufügen der (neuen) Komponenten, deren + Flexibilitätseinsatz untersucht werden soll** → Würde + Flexibilitätspotenzial zunichtemachen + +2. **Reinforce NACH Optimierung ist ZWINGEND:** + + - Optimierung reduziert Netzausbau, löst aber nicht alle Probleme + - Slack-Variablen zeigen verbleibende Verletzungen + - Ohne finales ``reinforce()`` ist das Netz **NICHT betriebssicher** + +3. **Beispielhafte Kostenreduktion:** + + - Ohne Optimierung: 100% Netzausbaukosten (Referenz) + - Mit Optimierung ohne §14a: 60-80% der Referenzkosten + - Mit Optimierung mit §14a: 40-60% der Referenzkosten + - Abhängig von: Flexibilitätsgrad, Netzstruktur, Lastprofile + +**Beispiel-Code für Workflow B (empfohlen):** + +.. code:: python + + # Workflow B: Mit Optimierung (BESTE PRAXIS) + + # 1. Netz laden + edisgo = EDisGo(ding0_grid="path/to/grid") + + # Zeitreihen laden etc. + + # 2. Optional: Basisnetz verstärken (für Vergleich) + # edisgo.reinforce() # Nur wenn Referenzkosten gewünscht oder auf status quo ausgebaut werden soll + + # 3. Neue Komponenten für Zukunftsszenario hinzufügen + edisgo.add_heat_pumps( + scenario="eGon2035", + with_thermal_storage=True # Flexibilität! + ) + edisgo.add_charging_points( + scenario="eGon2035" + ) + + # 4. Optimierung durchführen + edisgo.pm_optimize( + opf_version=2, # Mit Netzrestriktionen + curtailment_14a=True, # §14a-Abregelung nutzen + max_hours_per_day=2.0, # 2h/Tag Zeitbudget + solver="gurobi" + ) + + # 5. ZWINGEND: Netzausbau für verbleibende Probleme + edisgo.reinforce() + + # 6. Ergebnisse analysieren + costs = edisgo.results.grid_expansion_costs + curtailment = edisgo.timeseries.generators_active_power[ + [c for c in edisgo.timeseries.generators_active_power.columns + if '14a_support' in c] + ] + + print(f"Netzausbaukosten (nach Optimierung): {costs:,.0f} €") + print(f"§14a-Abregelung gesamt: {curtailment.sum().sum():.2f} MWh") + +-------------- + +Detaillierter Zeitablauf der Julia-Optimierung +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Phase 1: Problemaufbau (build_mn_opf_bf_flex) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Für jeden Zeitschritt n im Optimierungshorizont:** + +.. code:: julia + + for n in nw_ids(pm) + # 1. VARIABLEN ERSTELLEN + PowerModels.variable_bus_voltage(pm, nw=n) # w[i] + PowerModels.variable_gen_power(pm, nw=n) # pg, qg + PowerModels.variable_branch_power(pm, nw=n) # p, q + eDisGo_OPF.variable_branch_current(pm, nw=n) # ccm + + # Flexibilitäten + eDisGo_OPF.variable_storage_power(pm, nw=n) # ps + eDisGo_OPF.variable_heat_pump_power(pm, nw=n) # php + eDisGo_OPF.variable_heat_storage_power(pm, nw=n) # phs + eDisGo_OPF.variable_charging_point_power(pm, nw=n) # pcp + + # Falls OPF Version 1 oder 3: Line Loading + if opf_version in [1, 3] + eDisGo_OPF.variable_line_loading(pm, nw=n) # ll + end + + # Falls OPF Version 2 oder 4: Slack-Variablen + if opf_version in [2, 4] + eDisGo_OPF.variable_slack_heatpumps(pm, nw=n) # phps, phps2 + eDisGo_OPF.variable_slack_heat_storage(pm, nw=n) # phss + eDisGo_OPF.variable_slack_loads(pm, nw=n) # pds + eDisGo_OPF.variable_slack_gens(pm, nw=n) # pgens + eDisGo_OPF.variable_slack_cps(pm, nw=n) # pcps + end + + # Falls §14a aktiviert: Virtuelle Generatoren + Binärvariablen + if curtailment_14a + eDisGo_OPF.variable_gen_hp_14a_power(pm, nw=n) # p_hp14a + eDisGo_OPF.variable_gen_hp_14a_binary(pm, nw=n) # z_hp14a + eDisGo_OPF.variable_gen_cp_14a_power(pm, nw=n) # p_cp14a + eDisGo_OPF.variable_gen_cp_14a_binary(pm, nw=n) # z_cp14a + end + + # 2. CONSTRAINTS PRO ZEITSCHRITT + for i in ids(pm, :bus, nw=n) + constraint_power_balance(pm, i, n) # Eq 3.3, 3.4 + end + + for l in ids(pm, :branch, nw=n) + constraint_voltage_drop(pm, l, n) # Eq 3.5 + constraint_current_limit(pm, l, n) # Eq 3.6 + if opf_version in [1, 3] + constraint_line_loading(pm, l, n) # ll definition + end + end + + for s in ids(pm, :storage, nw=n) + constraint_storage_state(pm, s, n) # Eq 3.9 + constraint_storage_complementarity(pm, s, n) # Eq 3.10 + end + + for h in ids(pm, :heat_pump, nw=n) + constraint_heat_pump_operation(pm, h, n) # Eq 3.19 + constraint_heat_storage_state(pm, h, n) # Eq 3.22 + constraint_heat_storage_complementarity(pm, h, n)# Eq 3.23 + end + + for c in ids(pm, :charging_point, nw=n) + constraint_cp_state(pm, c, n) # Eq 3.25 + constraint_cp_complementarity(pm, c, n) # Eq 3.26 + end + + for d in ids(pm, :dsm, nw=n) + constraint_dsm_state(pm, d, n) # Eq 3.32 + constraint_dsm_complementarity(pm, d, n) # Eq 3.33 + end + + # §14a Constraints pro Zeitschritt + if curtailment_14a + for h in ids(pm, :gen_hp_14a, nw=n) + constraint_hp_14a_binary_coupling(pm, h, n) # p_hp14a ≤ pmax × z + constraint_hp_14a_min_net_load(pm, h, n) # Nettolast ≥ min(Last, 4.2kW) + end + for c in ids(pm, :gen_cp_14a, nw=n) + constraint_cp_14a_binary_coupling(pm, c, n) + constraint_cp_14a_min_net_load(pm, c, n) + end + end + end + +Phase 2: Inter-Zeitschritt Constraints +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: julia + + # Speicher-Energiekopplung zwischen Zeitschritten + for s in ids(pm, :storage) + for t in 1:(T-1) + se[t+1] == se[t] + ps[t] × Δt × η + end + end + + # Wärmespeicher-Kopplung + for h in ids(pm, :heat_pump) + for t in 1:(T-1) + hse[t+1] == hse[t] + phs[t] × Δt × η + end + end + + # EV-Batterie-Kopplung + for c in ids(pm, :charging_point) + for t in 1:(T-1) + cpe[t+1] == cpe[t] + pcp[t] × Δt × η + end + end + + # §14a Tages-Zeitbudget + if curtailment_14a + # Gruppiere Zeitschritte in 24h-Tage + day_groups = group_timesteps_by_day(timesteps) + + for day in day_groups + for h in ids(pm, :gen_hp_14a) + sum(z_hp14a[h,t] for t in day) ≤ max_hours_per_day / Δt + end + for c in ids(pm, :gen_cp_14a) + sum(z_cp14a[c,t] for t in day) ≤ max_hours_per_day / Δt + end + end + end + +Phase 3: Zielfunktion +^^^^^^^^^^^^^^^^^^^^^ + +**OPF Version 1** (geliftete Restriktionen, ohne Slacks): + +.. code:: julia + + minimize: 0.9 × sum(Verluste) + 0.1 × max(ll) + 0.05 × sum(p_hp14a) + 0.05 × sum(p_cp14a) + +**OPF Version 2** (mit Netzrestriktionen, mit Slacks): + +.. code:: julia + + minimize: 0.4 × sum(Verluste) + 0.6 × sum(Slacks) + 0.5 × sum(p_hp14a) + 0.5 × sum(p_cp14a) + +**OPF Version 3** (mit HV-Anforderungen, geliftete Restriktionen): + +.. code:: julia + + minimize: 0.9 × sum(Verluste) + 0.1 × max(ll) + 50 × sum(phvs) + 0.05 × sum(p_hp14a) + 0.05 × sum(p_cp14a) + +**OPF Version 4** (mit HV-Anforderungen und Restriktionen): + +.. code:: julia + + minimize: 0.4 × sum(Verluste) + 0.6 × sum(Slacks) + 50 × sum(phvs) + 0.5 × sum(p_hp14a) + 0.5 × sum(p_cp14a) + +**Wichtig:** - §14a-Terme haben moderate Gewichte → Abregelung wird +genutzt, aber minimiert - Slack-Variablen haben hohe implizite Kosten → +nur wenn unvermeidbar - HV-Slack hat sehr hohes Gewicht → Einhaltung +prioritär + +Phase 4: Lösen +^^^^^^^^^^^^^^ + +.. code:: julia + + # Solver-Auswahl + if method == "soc" + solver = Gurobi.Optimizer + # SOC-Relaxation: ccm-Constraints als Second-Order-Cone + elseif method == "nc" + solver = Ipopt.Optimizer + # Non-Convex: ccm-Constraints als quadratische Gleichungen + end + + # Optimierung durchführen + result = optimize_model!(pm, solver) + + # Optional: Warm-Start NC mit SOC-Lösung + if warm_start + result_soc = optimize_model!(pm, Gurobi.Optimizer) + initialize_from_soc!(pm, result_soc) + result = optimize_model!(pm, Ipopt.Optimizer) + end + +-------------- + +Die analyze-Funktion +-------------------- + +Funktionsdefinition +~~~~~~~~~~~~~~~~~~~ + +**Datei:** ``edisgo/edisgo.py`` (Zeile ~1038) + +**Signatur:** + +.. code:: python + + def analyze( + self, + mode: str | None = None, + timesteps: pd.DatetimeIndex | None = None, + troubleshooting_mode: str | None = None, + scale_timeseries: float | None = None, + **kwargs + ) -> None + +Was macht analyze? +~~~~~~~~~~~~~~~~~~ + +Die ``analyze``-Funktion führt eine **statische, nicht-lineare +Leistungsflussberechnung** (Power Flow Analysis, PFA) mit PyPSA durch. +Sie berechnet: + +1. **Spannungen** an allen Knoten (``v_res``) +2. **Ströme** auf allen Leitungen und Trafos (``i_res``) +3. **Wirkleistungsflüsse** auf Betriebsmitteln (``pfa_p``) +4. **Blindleistungsflüsse** auf Betriebsmitteln (``pfa_q``) + +Die Ergebnisse werden in ``edisgo.results`` gespeichert. + +Parameter +~~~~~~~~~ + ++--------------------+------------------+-----------------------------+ +| Parameter | Default | Beschreibung | ++====================+==================+=============================+ +| ``mode`` | str \| None | Analyseebene: ``'mv'`` | +| | | (MS-Netz), ``'mvlv'`` (MS | +| | | mit NS an Sekundärseite), | +| | | ``'lv'`` (einzelnes | +| | | NS-Netz), ``None`` | +| | | (gesamtes Netz) | ++--------------------+------------------+-----------------------------+ +| ``timesteps`` | DatetimeIndex \| | Zeitschritte für Analyse. | +| | None | ``None`` = alle in | +| | | ``timeseries.timeindex`` | ++--------------------+------------------+-----------------------------+ +| ``trou | str \| None | ``'lpf'`` = Linear PF | +| bleshooting_mode`` | | seeding, ``'iteration'`` = | +| | | schrittweise Lasterhöhung | ++--------------------+------------------+-----------------------------+ +| `` | float \| None | Skalierungsfaktor für | +| scale_timeseries`` | | Zeitreihen (z.B. 0.5 für | +| | | 50% Last) | ++--------------------+------------------+-----------------------------+ + +Zeitreihen-Nutzung +~~~~~~~~~~~~~~~~~~ + +``analyze`` verwendet **alle** Zeitreihen aus ``edisgo.timeseries``: + +Generatoren +^^^^^^^^^^^ + +- **Quelle:** ``edisgo.timeseries.generators_active_power`` +- **Quelle:** ``edisgo.timeseries.generators_reactive_power`` +- **Inhalt:** Einspeisung aller Generatoren (PV, Wind, BHKW, etc.) in + MW/MVAr +- **Zeitauflösung:** Typisch 1h oder 15min +- **Herkunft:** Datenbank (eGon), WorstCase-Profil, oder optimierte + Zeitreihen + +Lasten +^^^^^^ + +- **Quelle:** ``edisgo.timeseries.loads_active_power`` +- **Quelle:** ``edisgo.timeseries.loads_reactive_power`` +- **Inhalt:** Haushaltslast, Gewerbe, Industrie in MW/MVAr +- **Zeitauflösung:** Typisch 1h oder 15min +- **Herkunft:** Datenbank, Standardlastprofile, oder gemessene Daten + +Speicher +^^^^^^^^ + +- **Quelle:** ``edisgo.timeseries.storage_units_active_power`` +- **Quelle:** ``edisgo.timeseries.storage_units_reactive_power`` +- **Inhalt:** Batteriespeicher Ladung/Entladung in MW/MVAr +- **Zeitauflösung:** Wie Zeitreihenindex +- **Herkunft:** Optimierung oder vorgegebene Fahrpläne + +Wärmepumpen +^^^^^^^^^^^ + +- **Quelle:** Indirekt aus ``heat_demand_df`` und ``cop_df`` +- **Berechnung:** ``P_el = heat_demand / COP`` +- **Zeitauflösung:** Wie Zeitreihenindex +- **Herkunft:** Wärmebedarfsprofile (z.B. BDEW), COP-Profile + (temperaturabhängig) +- **Nach Optimierung:** Aus optimierten Zeitreihen + ``timeseries.heat_pumps_active_power`` + +Ladepunkte (E-Autos) +^^^^^^^^^^^^^^^^^^^^ + +- **Quelle:** ``edisgo.timeseries.charging_points_active_power`` +- **Zeitauflösung:** Wie Zeitreihenindex +- **Herkunft:** Ladeprofile (z.B. SimBEV), Flexibilitätsbänder, oder + Optimierung + +Prozessablauf +~~~~~~~~~~~~~ + +.. code:: python + + # 1. Zeitschritte bestimmen + if timesteps is None: + timesteps = self.timeseries.timeindex + else: + timesteps = pd.DatetimeIndex(timesteps) + + # 2. In PyPSA-Netzwerk konvertieren + pypsa_network = self.to_pypsa( + mode=mode, + timesteps=timesteps + ) + + # 3. Optional: Zeitreihen skalieren + if scale_timeseries is not None: + pypsa_network.loads_t.p_set *= scale_timeseries + pypsa_network.generators_t.p_set *= scale_timeseries + # ... weitere Zeitreihen skalieren + + # 4. Leistungsflussberechnung durchführen + pypsa_network.pf( + timesteps, + use_seed=(troubleshooting_mode == 'lpf') + ) + + # 5. Konvergenz prüfen + converged_ts = timesteps[pypsa_network.converged] + not_converged_ts = timesteps[~pypsa_network.converged] + + if len(not_converged_ts) > 0: + logger.warning(f"Power flow did not converge for {len(not_converged_ts)} timesteps") + + # 6. Ergebnisse verarbeiten + pypsa_io.process_pfa_results( + edisgo_obj=self, + pypsa_network=pypsa_network, + timesteps=timesteps + ) + + # 7. Ergebnisse in edisgo.results speichern + # self.results.v_res -> Spannungen an Knoten + # self.results.i_res -> Ströme auf Leitungen + # self.results.pfa_p -> Wirkleistungsflüsse + # self.results.pfa_q -> Blindleistungsflüsse + +Wann wird analyze aufgerufen? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +1. **Manuell vom Benutzer:** + + .. code:: python + + edisgo.analyze() # Analysiere gesamtes Netz, alle Zeitschritte + +2. **Von reinforce-Funktion:** + + - Initial: Identifiziere Netzprobleme + - Nach jeder Verstärkung: Prüfe ob Probleme gelöst + - Iterativ bis keine Verletzungen mehr + +3. **Nach Optimierung (optional):** + + .. code:: python + + edisgo.pm_optimize(...) + edisgo.analyze() # Analysiere mit optimierten Zeitreihen + +4. **Bei Worst-Case-Analyse:** + + .. code:: python + + # Nur zwei kritische Zeitpunkte + worst_case_ts = edisgo.get_worst_case_timesteps() + edisgo.analyze(timesteps=worst_case_ts) + +Troubleshooting-Modi +~~~~~~~~~~~~~~~~~~~~ + +Linear Power Flow Seeding (``'lpf'``) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- Problem: Nicht-linearer PF konvergiert nicht +- Lösung: Starte mit linearer PF-Lösung (Winkel) als Startwert +- Nutzen: Stabilisiert Konvergenz bei schwierigen Netzen + +Iteration (``'iteration'``) +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- Problem: Konvergenz bei hoher Last nicht möglich +- Lösung: Beginne mit 10% Last, erhöhe schrittweise bis 100% +- Nutzen: Findet Lösung bei extremen Betriebspunkten + +Ausgabe +~~~~~~~ + +**Erfolgreiche Analyse:** + +:: + + Info: Power flow analysis completed for 8760 timesteps + Info: 8760 timesteps converged, 0 did not converge + +**Konvergenzprobleme:** + +:: + + Warning: Power flow did not converge for 15 timesteps + Warning: Non-converged timesteps: ['2035-01-15 18:00', '2035-07-21 12:00', ...] + +-------------- + +Die reinforce-Funktion +---------------------- + +.. _funktionsdefinition-1: + +Funktionsdefinition +~~~~~~~~~~~~~~~~~~~ + +**Datei:** ``edisgo/edisgo.py`` (Zeile ~1243) **Implementierung:** +``edisgo/flex_opt/reinforce_grid.py`` (Zeile ~25) + +**Signatur:** + +.. code:: python + + def reinforce( + self, + timesteps_pfa: str | pd.DatetimeIndex | None = None, + reduced_analysis: bool = False, + max_while_iterations: int = 20, + split_voltage_band: bool = True, + mode: str | None = None, + without_generator_import: bool = False, + n_minus_one: bool = False, + **kwargs + ) -> None + +Was macht reinforce? +~~~~~~~~~~~~~~~~~~~~ + +Die ``reinforce``-Funktion **identifiziert Netzprobleme** (Überlastung, +Spannungsverletzungen) und **führt Verstärkungsmaßnahmen** durch: + +1. **Leitungsverstärkung:** Parallele Leitungen oder Ersatz durch + größeren Querschnitt +2. **Transformatorverstärkung:** Paralleltrafos oder größere Leistung +3. **Spannungsebenen-Trennung:** LV-Netze bei Bedarf aufteilen +4. **Kostenberechnung:** Netzausbaukosten (€) + +.. _parameter-1: + +Parameter +~~~~~~~~~ + ++----------------+--------+----------------+--------------------------+ +| Parameter | Typ | Default | Beschreibung | ++================+========+================+==========================+ +| ``t | `` | ``'snapsh | ``'snapshot_analysis'`` | +| imesteps_pfa`` | str \| | ot_analysis'`` | = 2 | +| | Datet | | Worst-Case-Zeitschritte, | +| | imeInd | | ``DatetimeIndex`` = | +| | ex \| | | benutzerdefiniert, | +| | None`` | | ``None`` = alle | +| | | | Zeitschritte | ++----------------+--------+----------------+--------------------------+ +| ``redu | `` | ``False`` | Nutzt nur die | +| ced_analysis`` | bool`` | | kritischsten | +| | | | Zeitschritte (höchste | +| | | | Überlast oder | +| | | | Spannungsabweichung) | ++----------------+--------+----------------+--------------------------+ +| ``max_whil | ` | ``20`` | Maximale Anzahl der | +| e_iterations`` | `int`` | | Verstärkungsiterationen | ++----------------+--------+----------------+--------------------------+ +| ``split_ | `` | ``True`` | Getrennte | +| voltage_band`` | bool`` | | Spannungsbänder für | +| | | | NS/MS (z.B. NS ±3 %, MS | +| | | | ±7 %) | ++----------------+--------+----------------+--------------------------+ +| ``mode`` | ``s | ``None`` | Netzebene: ``'mv'``, | +| | tr \| | | ``'mvlv'``, ``'lv'`` | +| | None`` | | oder ``None`` (= | +| | | | automatisch) | ++----------------+--------+----------------+--------------------------+ +| ``without_gene | `` | ``False`` | Ignoriert | +| rator_import`` | bool`` | | Generatoreinspeisung | +| | | | (nur für | +| | | | Planungsanalysen | +| | | | sinnvoll) | ++----------------+--------+----------------+--------------------------+ +| ` | `` | ``False`` | Berücksichtigt das | +| `n_minus_one`` | bool`` | | (n-1)-Kriterium | ++----------------+--------+----------------+--------------------------+ + +.. _zeitreihen-nutzung-1: + +Zeitreihen-Nutzung +~~~~~~~~~~~~~~~~~~ + +Nutzt **dieselben Zeitreihen wie analyze**: + +- ``generators_active_power``, ``generators_reactive_power`` +- ``loads_active_power``, ``loads_reactive_power`` +- ``storage_units_active_power``, ``storage_units_reactive_power`` +- Wärmepumpen-Lasten (aus heat_demand/COP) +- Ladepunkt-Lasten + +**Zeitreihen-Auswahl:** + +Option 1: Snapshot Analysis (``timesteps_pfa='snapshot_analysis'``) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: python + + # Nur 2 kritische Zeitpunkte + ts1 = timestep_max_residual_load # Max. Residuallast (hohe Last, wenig Einspeisung) + ts2 = timestep_min_residual_load # Min. Residuallast (niedrige Last, viel Einspeisung) + timesteps = [ts1, ts2] + +**Vorteil:** Sehr schnell (nur 2 PFA statt 8760) **Nachteil:** Kann +seltene Probleme übersehen + +Option 2: Reduzierte Analyse (``reduced_analysis=True``) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: python + + # 1. Initiale PFA mit allen Zeitschritten + edisgo.analyze(timesteps=all_timesteps) + + # 2. Identifiziere kritischste Zeitschritte + critical_timesteps = get_most_critical_timesteps( + overloading_factor=1.0, # Nur Zeitschritte mit Überlast + voltage_deviation=0.03 # Nur Zeitschritte mit >3% Spannungsabweichung + ) + + # 3. Verstärkung nur auf Basis dieser Zeitschritte + timesteps = critical_timesteps # z.B. 50 statt 8760 + +**Vorteil:** Deutlich schneller als volle Analyse, genauer als Snapshot +**Nachteil:** Initial-PFA mit allen Zeitschritten nötig + +Option 3: Alle Zeitschritte (``timesteps_pfa=None``) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: python + + timesteps = edisgo.timeseries.timeindex # z.B. alle 8760h eines Jahres + +**Vorteil:** Maximale Genauigkeit **Nachteil:** Sehr rechenintensiv +(viele PFA) + +Option 4: Custom (``timesteps_pfa=custom_datetimeindex``) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: python + + # Z.B. nur Wintermonate + timesteps = pd.date_range('2035-01-01', '2035-03-31', freq='H') + +reinforce Algorithmus +~~~~~~~~~~~~~~~~~~~~~ + +Schritt 1: Überlastungen beseitigen +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: python + + iteration = 0 + while has_overloading() and iteration < max_while_iterations: + iteration += 1 + + # 1.1 HV/MV-Station prüfen + if hv_mv_station_max_overload() > 0: + reinforce_hv_mv_station() + + # 1.2 MV/LV-Stationen prüfen + for station in mv_lv_stations: + if station.max_overload > 0: + reinforce_mv_lv_station(station) + + # 1.3 MV-Leitungen prüfen + for line in mv_lines: + if line.max_relative_overload > 0: + reinforce_line(line) + + # 1.4 LV-Leitungen prüfen + for line in lv_lines: + if line.max_relative_overload > 0: + reinforce_line(line) + + # 1.5 Erneute Analyse + edisgo.analyze(timesteps=timesteps) + + # 1.6 Konvergenz prüfen + if not has_overloading(): + break + +**Verstärkungsmaßnahmen:** - **Parallelleitungen:** Identischer Typ +parallel schalten - **Leitungsersatz:** Größerer Querschnitt (z.B. +150mm² → 240mm²) - **Paralleltrafos:** Identischer Trafo parallel - +**Trafoersatz:** Größere Leistung (z.B. 630kVA → 1000kVA) + +Schritt 2: MV-Spannungsprobleme lösen +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: python + + iteration = 0 + while has_voltage_issues_mv() and iteration < max_while_iterations: + iteration += 1 + + # Identifiziere kritische Leitungen + critical_lines = get_lines_voltage_issues(voltage_level='mv') + + for line in critical_lines: + reinforce_line(line) + + # Erneute Analyse + edisgo.analyze(timesteps=timesteps) + +Schritt 3: MV/LV-Stations-Spannungsprobleme +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: python + + for station in mv_lv_stations: + if has_voltage_issues_at_secondary_side(station): + # Trafo-Kapazität erhöhen + reinforce_mv_lv_station(station) + + edisgo.analyze(timesteps=timesteps) + +Schritt 4: LV-Spannungsprobleme lösen +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: python + + for lv_grid in lv_grids: + while has_voltage_issues(lv_grid) and iteration < max_while_iterations: + iteration += 1 + + # Kritische Leitungen verstärken + critical_lines = get_lines_voltage_issues( + grid=lv_grid, + voltage_level='lv' + ) + + for line in critical_lines: + reinforce_line(line) + + edisgo.analyze(timesteps=timesteps, mode='lv') + +Schritt 5: Finale Überprüfung +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: python + + # Prüfe ob Spannungsverstärkungen neue Überlastungen verursacht haben + edisgo.analyze(timesteps=timesteps) + + if has_overloading(): + # Zurück zu Schritt 1 + goto_step_1() + +Schritt 6: Kostenberechnung +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: python + + # Berechne Netzausbaukosten + costs = calculate_grid_expansion_costs(edisgo) + + # Kosten pro Komponente + line_costs = costs['lines'] # € + trafo_costs = costs['transformers'] # € + total_costs = costs['total'] # € + + # Speichere in edisgo.results + edisgo.results.grid_expansion_costs = costs + +Verstärkungslogik +~~~~~~~~~~~~~~~~~ + +Leitungsverstärkung +^^^^^^^^^^^^^^^^^^^ + +.. code:: python + + def reinforce_line(line): + # 1. Berechne benötigte Kapazität + required_capacity = line.s_nom * (1 + max_relative_overload) + + # 2. Option A: Parallelleitungen + num_parallel = ceil(required_capacity / line.s_nom) + cost_parallel = num_parallel * line_cost(line.type) + + # 3. Option B: Größerer Querschnitt + new_type = get_next_larger_type(line.type) + if new_type is not None: + cost_replacement = line_cost(new_type) + else: + cost_replacement = inf + + # 4. Wähle günstigere Option + if cost_parallel < cost_replacement: + add_parallel_lines(line, num_parallel - 1) + else: + replace_line(line, new_type) + +Transformatorverstärkung +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: python + + def reinforce_transformer(trafo): + # 1. Berechne benötigte Leistung + required_power = trafo.s_nom * (1 + max_relative_overload) + + # 2. Option A: Paralleltrafos + num_parallel = ceil(required_power / trafo.s_nom) + cost_parallel = num_parallel * trafo_cost(trafo.type) + + # 3. Option B: Größerer Trafo + new_type = get_next_larger_trafo(trafo.s_nom) + cost_replacement = trafo_cost(new_type) + + # 4. Wähle günstigere Option + if cost_parallel < cost_replacement: + add_parallel_trafos(trafo, num_parallel - 1) + else: + replace_trafo(trafo, new_type) + +.. _ausgabe-1: + +Ausgabe +~~~~~~~ + +**Erfolgreiche Verstärkung:** + +:: + + Info: ==> Checking stations. + Info: MV station is not overloaded. + Info: All MV/LV stations are within allowed load range. + Info: ==> Checking lines. + Info: Reinforcing 15 overloaded MV lines. + Info: Reinforcing 42 overloaded LV lines. + Info: ==> Voltage issues in MV grid. + Info: Reinforcing 8 lines due to voltage issues. + Info: ==> Voltage issues in LV grids. + Info: Reinforcing 23 lines in LV grids. + Info: Grid reinforcement finished. Total costs: 145,320 € + +**Iterations-Limit erreicht:** + +:: + + Warning: Maximum number of iterations (20) reached. Grid issues may remain. + +Wann wird reinforce aufgerufen? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +1. **Manuell vom Benutzer:** + + .. code:: python + + edisgo.reinforce() + +2. **Basisnetz-Verstärkung VOR neuen Komponenten (optional):** + + .. code:: python + + # Laden eines Basisnetzes (z.B. Ist-Zustand 2024) + edisgo = EDisGo(ding0_grid="path/to/grid") + edisgo.import_generators(scenario="status_quo") + edisgo.import_loads(scenario="status_quo") + + # Optional: Verstärkung des Basisnetzes (als Referenz) + edisgo.reinforce() # Kosten für Basisnetz dokumentieren + + # DANN: Neue Komponenten für Zukunftsszenario hinzufügen + edisgo.add_heat_pumps(scenario="2035_high") + edisgo.add_charging_points(scenario="2035_high") + + # WICHTIG: KEIN reinforce an dieser Stelle! + # Stattdessen: Optimierung nutzen → siehe Punkt 5 + + **Sinnvoll für:** Referenzszenario, Vergleich Netzausbaukosten + mit/ohne Flexibilität + +3. **Nach Szenario-Simulation OHNE Optimierung:** + + .. code:: python + + # Wenn man KEINE Optimierung nutzen möchte (rein konventioneller Netzausbau) + edisgo.add_charging_points(scenario='high_ev') + edisgo.reinforce() # Verstärke Netz für neue Last (ohne Flexibilität) + + **Nachteil:** Hohe Netzausbaukosten, Flexibilitätspotenzial wird + nicht genutzt + +4. **Nach Optimierung (ZWINGEND ERFORDERLICH!):** + + .. code:: python + + # Korrekte Reihenfolge: + # 1. Neue Komponenten hinzufügen (WP, CP, Speicher) + edisgo.add_heat_pumps(...) + edisgo.add_charging_points(...) + + # 2. Optimierung durchführen (nutzt Flexibilität) + edisgo.pm_optimize(opf_version=2, curtailment_14a=True) + + # 3. ZWINGEND: Netzausbau für verbleibende Probleme + edisgo.reinforce() # Behebt Überlastungen die Optimierung nicht lösen konnte + + # Ergebnis: Minimierte Netzausbaukosten durch Flexibilitätsnutzung + + **Warum zwingend?** + + - Optimierung minimiert Netzausbau, kann aber nicht alle Probleme + lösen + - Slack-Variablen > 0 zeigen verbleibende + Netzrestriktionsverletzungen + - Verbleibende Überlastungen müssen durch konventionellen Ausbau + behoben werden + - **Ohne diesen Schritt:** Netz ist NICHT betriebssicher! + +5. **Iterativer Workflow für mehrere Szenarien:** + + .. code:: python + + # Vergleich verschiedener Flexibilitätsszenarien + + # Szenario 1: Ohne Optimierung (Referenz) + edisgo_ref = edisgo.copy() + edisgo_ref.reinforce() + costs_ref = edisgo_ref.results.grid_expansion_costs + + # Szenario 2: Mit Optimierung aber ohne §14a + edisgo_opt = edisgo.copy() + edisgo_opt.pm_optimize(opf_version=2, curtailment_14a=False) + edisgo_opt.reinforce() + costs_opt = edisgo_opt.results.grid_expansion_costs + + # Szenario 3: Mit Optimierung und §14a + edisgo_14a = edisgo.copy() + edisgo_14a.pm_optimize(opf_version=2, curtailment_14a=True) + edisgo_14a.reinforce() + costs_14a = edisgo_14a.results.grid_expansion_costs + + # Vergleich + print(f"Ohne Optimierung: {costs_ref:,.0f} €") + print(f"Mit Optimierung: {costs_opt:,.0f} € (-{100*(1-costs_opt/costs_ref):.1f}%)") + print(f"Mit §14a: {costs_14a:,.0f} € (-{100*(1-costs_14a/costs_ref):.1f}%)") + +-------------- + +Die §14a EnWG Optimierung +------------------------- + +Was ist §14a EnWG? +~~~~~~~~~~~~~~~~~~ + +**Gesetzesgrundlage:** § 14a Energiewirtschaftsgesetz (EnWG) + +**Inhalt:** Netzbetreiber dürfen **steuerbare Verbrauchseinrichtungen** +(Wärmepumpen, Ladeeinrichtungen für E-Autos) bei Netzengpässen +**abregelnd** bis auf eine **Mindestleistung** (4,2 kW). + +**Bedingungen:** - Maximales **Zeitbudget**: Typisch 2 Stunden pro Tag - +**Mindestleistung**: 4,2 kW (0,0042 MW) muss gewährleistet bleiben - +**Vergütung**: Reduziertes Netzentgelt für Kunden + +**Ziel:** Netzausbau reduzieren durch gezielte Spitzenlast-Kappung + +Wie unterscheidet sich §14a von Standard-Optimierung? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Standard-Optimierung (OHNE §14a): +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- **Wärmepumpen mit thermischem Speicher:** Zeitliche Lastverschiebung +- **E-Autos:** Ladesteuerung innerhalb Flexibilitätsband +- **Inflexible WP/CP:** Können NICHT abgeregelt werden + +§14a-Optimierung (MIT §14a): +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- **ALLE Wärmepumpen > 4,2 kW:** Können bis auf 4,2 kW abgeregelt + werden +- **ALLE Ladepunkte > 4,2 kW:** Können bis auf 4,2 kW abgeregelt werden +- **Auch ohne Speicher:** Abregelung möglich +- **Zeitbudget-Constraints:** Max. 2h/Tag Abregelung +- **Binäre Entscheidung:** Abregelung aktiv JA/NEIN + +Mathematische Modellierung +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Virtuelle Generatoren +^^^^^^^^^^^^^^^^^^^^^ + +§14a-Abregelung wird durch **virtuelle Generatoren** modelliert: + +:: + + Nettolast_WP = Original_Last_WP - p_hp14a + + Beispiel: + - WP-Last: 8 kW + - Abregelung: 3,8 kW (virtueller Generator erzeugt 3,8 kW) + - Nettolast: 8 - 3,8 = 4,2 kW (Mindestlast) + +**Vorteile dieser Modellierung:** - Keine Änderung der Last-Zeitreihen +nötig - Kompatibel mit PowerModels-Struktur - Einfache Implementierung +in Optimierungsproblem + +Variablen (pro Wärmepumpe h, Zeitschritt t) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: julia + + # Kontinuierliche Variable: Abregelleistung + @variable(model, 0 <= p_hp14a[h,t] <= pmax[h]) + + # Binäre Variable: Abregelung aktiv? + @variable(model, z_hp14a[h,t], Bin) + +**Parameter:** - ``pmax[h] = P_nominal[h] - P_min_14a`` - +``P_nominal[h]``: Nennleistung Wärmepumpe h (z.B. 8 kW) - +``P_min_14a = 4,2 kW``: Gesetzliche Mindestlast - +``pmax[h] = 8 - 4,2 = 3,8 kW``: Maximale Abregelleistung + +- ``max_hours_per_day = 2``: Zeitbudget in Stunden pro Tag + +Constraints +^^^^^^^^^^^ + +1. Binäre Kopplung (Binary Coupling) +'''''''''''''''''''''''''''''''''''' + +.. code:: julia + + @constraint(model, p_hp14a[h,t] <= pmax[h] × z_hp14a[h,t]) + +**Bedeutung:** - Wenn ``z_hp14a[h,t] = 0`` (keine Abregelung): +``p_hp14a[h,t] = 0`` - Wenn ``z_hp14a[h,t] = 1`` (Abregelung aktiv): +``0 ≤ p_hp14a[h,t] ≤ pmax[h]`` + +**Zweck:** Verhindert “Teilabregelung” ohne binäre Aktivierung + +2. Mindest-Nettolast (Minimum Net Load) +''''''''''''''''''''''''''''''''''''''' + +.. code:: julia + + @constraint(model, + p_hp_load[h,t] - p_hp14a[h,t] >= min(p_hp_load[h,t], p_min_14a) + ) + +**Bedeutung:** - Nettolast muss mindestens so groß wie aktuelle Last +ODER 4,2 kW sein - Verhindert “negative Last” (Generator größer als +Last) + +**Spezialfälle:** + +**Fall A: WP ist aus** (``p_hp_load[h,t] < 1e-6 MW``): + +.. code:: julia + + @constraint(model, p_hp14a[h,t] == 0) + +Abregelung macht keinen Sinn, wenn WP eh aus ist. + +**Fall B: WP zu klein** (``pmax[h] < 1e-6 MW``): + +.. code:: julia + + @constraint(model, p_hp14a[h,t] == 0) + +WP-Nennleistung < 4,2 kW → keine Abregelung möglich. + +**Fall C: Normalbetrieb** (``p_hp_load[h,t] >= p_min_14a``): + +.. code:: julia + + @constraint(model, p_hp_load[h,t] - p_hp14a[h,t] >= p_min_14a) + +Nettolast muss mindestens 4,2 kW bleiben. + +3. Tages-Zeitbudget (Daily Time Budget) +''''''''''''''''''''''''''''''''''''''' + +.. code:: julia + + for day in days + @constraint(model, + sum(z_hp14a[h,t] for t in timesteps_in_day(day)) + <= max_hours_per_day / time_elapsed_per_timestep + ) + end + +**Beispiel:** - Zeitauflösung: 1h - Zeitbudget: 2h/Tag - Constraint: +``sum(z_hp14a[h,t] for t in 0..23) <= 2`` + +**Beispiel mit 15min-Auflösung:** - Zeitauflösung: 0,25h - Zeitbudget: +2h/Tag - Constraint: +``sum(z_hp14a[h,t] for t in 0..95) <= 2 / 0.25 = 8`` + +**Alternative:** Total Budget Constraint (über gesamten Horizont): + +.. code:: julia + + @constraint(model, + sum(z_hp14a[h,t] for t in all_timesteps) + <= max_hours_total / time_elapsed_per_timestep + ) + +Zielfunktions-Integration +^^^^^^^^^^^^^^^^^^^^^^^^^ + +§14a-Variablen werden mit **moderaten Gewichten** ins Zielfunktional +aufgenommen: + +**OPF Version 2 (mit §14a):** + +.. code:: julia + + minimize: + 0.4 × sum(line_losses[t] for t in timesteps) + + 0.6 × sum(all_slacks[t] for t in timesteps) + + 0.5 × sum(p_hp14a[h,t] for h,t) + + 0.5 × sum(p_cp14a[c,t] for c,t) + +**Interpretation der Gewichte:** - ``0.4`` für Verluste: Basiskosten +Netzbetrieb - ``0.6`` für Slacks: Hohe Priorität Netzrestriktionen +einhalten - ``0.5`` für §14a: Moderate Kosten → Abregelung wird genutzt, +aber minimiert - Präferenz: Erst andere Flexibilitäten (Speicher, +zeitliche Verschiebung) - Wenn andere Flexibilitäten nicht ausreichen: +§14a als “letzte Reserve” + +**OPF Version 1 (mit §14a):** + +.. code:: julia + + minimize: + 0.9 × sum(line_losses[t] for t in timesteps) + + 0.1 × max(line_loading[l,t] for l,t) + + 0.05 × sum(p_hp14a[h,t] for h,t) + + 0.05 × sum(p_cp14a[c,t] for c,t) + +**Niedrigeres Gewicht (0.05):** §14a wird bevorzugt gegenüber hoher +Leitungsauslastung. + +Implementation Details +~~~~~~~~~~~~~~~~~~~~~~ + +Datei-Struktur +^^^^^^^^^^^^^^ + +**Python-Seite:** - **Datei:** ``edisgo/io/powermodels_io.py`` - +**Funktionen:** - ``_build_gen_hp_14a_support()``: Erstellt virtuelle +Generatoren für WP - ``_build_gen_cp_14a_support()``: Erstellt virtuelle +Generatoren für CP - ``to_powermodels(..., curtailment_14a=True)``: Ruft +obige Funktionen auf + +**Julia-Seite:** - **Variablen:** +``edisgo/opf/eDisGo_OPF.jl/src/core/variables.jl`` - +``variable_gen_hp_14a_power()`` - ``variable_gen_hp_14a_binary()`` - +``variable_gen_cp_14a_power()`` - ``variable_gen_cp_14a_binary()`` + +- **Constraints:** + ``edisgo/opf/eDisGo_OPF.jl/src/core/constraint_hp_14a.jl`` + + - ``constraint_hp_14a_binary_coupling()`` + - ``constraint_hp_14a_min_net_load()`` + +- **Constraints:** + ``edisgo/opf/eDisGo_OPF.jl/src/core/constraint_cp_14a.jl`` + + - ``constraint_cp_14a_binary_coupling()`` + - ``constraint_cp_14a_min_net_load()`` + +- **Problemdefinition:** + ``edisgo/opf/eDisGo_OPF.jl/src/prob/opf_bf.jl`` + + - Integration in ``build_mn_opf_bf_flex()`` + +Python: Virtuelle Generatoren erstellen +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: python + + def _build_gen_hp_14a_support( + edisgo_obj, + pm_dict, + timesteps, + max_hours_per_day=2.0 + ): + """ + Erstellt virtuelle Generatoren für §14a-Abregelung von Wärmepumpen. + """ + # 1. Alle Wärmepumpen identifizieren + heat_pumps = edisgo_obj.topology.loads_df[ + edisgo_obj.topology.loads_df.type == "heat_pump" + ] + + gen_hp_14a_dict = {} + + for idx, (hp_name, hp_row) in enumerate(heat_pumps.iterrows()): + # 2. Parameter berechnen + p_nominal = hp_row["p_set"] # Nennleistung in MW + p_min_14a = 0.0042 # 4.2 kW + pmax = p_nominal - p_min_14a + + # Nur wenn WP groß genug (> 4.2 kW) + if pmax > 1e-6: + # 3. Virtuellen Generator definieren + gen_hp_14a_dict[idx] = { + "name": f"hp_14a_support_{hp_name}", + "gen_bus": hp_row["bus"], # Gleicher Bus wie WP + "pmax": pmax, # Maximale Abregelleistung + "pmin": 0.0, + "qmax": 0.0, # Nur Wirkleistung + "qmin": 0.0, + "max_hours_per_day": max_hours_per_day, + "p_min_14a": p_min_14a, + "hp_name": hp_name, # Referenz zur Original-WP + "index": idx, + "source_id": f"gen_hp_14a_{idx}" + } + + # 4. Zeitreihen erstellen (zunächst Nullen, Optimierung setzt Werte) + gen_hp_14a_p = pd.DataFrame( + 0.0, + index=timesteps, + columns=[f"gen_hp_14a_{i}" for i in gen_hp_14a_dict.keys()] + ) + + gen_hp_14a_q = gen_hp_14a_p.copy() + + # 5. In PowerModels-Dictionary einfügen + for n, ts in enumerate(timesteps): + pm_dict["nw"][str(n)]["gen_hp_14a"] = gen_hp_14a_dict + + return pm_dict + +**Analoger Code für Ladepunkte (``_build_gen_cp_14a_support``).** + +Julia: Variablen definieren +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: julia + + function variable_gen_hp_14a_power(pm::AbstractPowerModel; nw::Int=nw_id_default) + """ + Erstellt kontinuierliche Variable für §14a WP-Abregelung. + """ + gen_hp_14a = PowerModels.ref(pm, nw, :gen_hp_14a) + + # Variable p_hp14a[i] für jeden virtuellen Generator i + PowerModels.var(pm, nw)[:p_hp14a] = JuMP.@variable( + pm.model, + [i in keys(gen_hp_14a)], + base_name = "p_hp14a_$(nw)", + lower_bound = 0.0, + upper_bound = gen_hp_14a[i]["pmax"], + start = 0.0 + ) + end + + function variable_gen_hp_14a_binary(pm::AbstractPowerModel; nw::Int=nw_id_default) + """ + Erstellt binäre Variable für §14a WP-Abregelung. + """ + gen_hp_14a = PowerModels.ref(pm, nw, :gen_hp_14a) + + # Binärvariable z_hp14a[i] + PowerModels.var(pm, nw)[:z_hp14a] = JuMP.@variable( + pm.model, + [i in keys(gen_hp_14a)], + base_name = "z_hp14a_$(nw)", + binary = true, + start = 0 + ) + end + +Julia: Constraints implementieren +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: julia + + function constraint_hp_14a_binary_coupling( + pm::AbstractPowerModel, + i::Int, + nw::Int=nw_id_default + ) + """ + Binäre Kopplung: p_hp14a <= pmax × z_hp14a + """ + p_hp14a = PowerModels.var(pm, nw, :p_hp14a, i) + z_hp14a = PowerModels.var(pm, nw, :z_hp14a, i) + + gen_hp_14a = PowerModels.ref(pm, nw, :gen_hp_14a, i) + pmax = gen_hp_14a["pmax"] + + # Constraint + JuMP.@constraint(pm.model, p_hp14a <= pmax * z_hp14a) + end + + function constraint_hp_14a_min_net_load( + pm::AbstractPowerModel, + i::Int, + nw::Int=nw_id_default + ) + """ + Mindest-Nettolast: Last - p_hp14a >= min(Last, 4.2kW) + """ + p_hp14a = PowerModels.var(pm, nw, :p_hp14a, i) + gen_hp_14a = PowerModels.ref(pm, nw, :gen_hp_14a, i) + + # Finde zugehörige WP-Last + hp_name = gen_hp_14a["hp_name"] + hp_bus = gen_hp_14a["gen_bus"] + p_hp_load = get_hp_load_at_bus(pm, hp_bus, hp_name, nw) + + pmax = gen_hp_14a["pmax"] + p_min_14a = gen_hp_14a["p_min_14a"] + + # Spezialfälle + if pmax < 1e-6 + # WP zu klein + JuMP.@constraint(pm.model, p_hp14a == 0.0) + elseif p_hp_load < 1e-6 + # WP ist aus + JuMP.@constraint(pm.model, p_hp14a == 0.0) + else + # Normalbetrieb: Nettolast >= min(Last, Mindestlast) + min_net_load = min(p_hp_load, p_min_14a) + JuMP.@constraint(pm.model, p_hp_load - p_hp14a >= min_net_load) + end + end + +Julia: Zeitbudget-Constraint +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: julia + + function constraint_hp_14a_time_budget_daily( + pm::AbstractPowerModel, + gen_hp_14a_ids, + timesteps_per_day, + max_hours_per_day, + time_elapsed + ) + """ + Tages-Zeitbudget: Σ(z_hp14a über Tag) <= max_hours / Δt + """ + # Gruppiere Zeitschritte nach Tagen + day_groups = group_timesteps_by_day(timesteps_per_day) + + for (day_idx, day_timesteps) in enumerate(day_groups) + for i in gen_hp_14a_ids + # Summe binärer Variablen über Tag + z_sum = sum( + PowerModels.var(pm, nw, :z_hp14a, i) + for nw in day_timesteps + ) + + # Max. erlaubte Zeitschritte + max_timesteps = max_hours_per_day / time_elapsed + + # Constraint + JuMP.@constraint(pm.model, z_sum <= max_timesteps) + end + end + end + +**Hilfsfunktion:** + +.. code:: julia + + function group_timesteps_by_day(timesteps) + """ + Gruppiert Zeitschritte in 24h-Tage. + + Beispiel: + - Input: [2035-01-01 00:00, 2035-01-01 01:00, ..., 2035-01-02 00:00, ...] + - Output: [[0..23], [24..47], [48..71], ...] + """ + days = [] + current_day = [] + current_date = Date(timesteps[0]) + + for (idx, ts) in enumerate(timesteps) + ts_date = Date(ts) + + if ts_date == current_date + push!(current_day, idx) + else + push!(days, current_day) + current_day = [idx] + current_date = ts_date + end + end + + # Letzter Tag + if !isempty(current_day) + push!(days, current_day) + end + + return days + end + +Integration in Optimierungsproblem +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Datei:** ``edisgo/opf/eDisGo_OPF.jl/src/prob/opf_bf.jl`` + +.. code:: julia + + function build_mn_opf_bf_flex(pm::AbstractPowerModel; kwargs...) + # ... Standard-Variablen ... + + # §14a Variablen (falls aktiviert) + for n in PowerModels.nw_ids(pm) + if haskey(PowerModels.ref(pm, n), :gen_hp_14a) && + !isempty(PowerModels.ref(pm, n, :gen_hp_14a)) + + # Variablen erstellen + variable_gen_hp_14a_power(pm, nw=n) + variable_gen_hp_14a_binary(pm, nw=n) + end + + if haskey(PowerModels.ref(pm, n), :gen_cp_14a) && + !isempty(PowerModels.ref(pm, n, :gen_cp_14a)) + + variable_gen_cp_14a_power(pm, nw=n) + variable_gen_cp_14a_binary(pm, nw=n) + end + end + + # ... Standard-Constraints ... + + # §14a Constraints pro Zeitschritt + for n in PowerModels.nw_ids(pm) + # WP §14a + for i in PowerModels.ids(pm, :gen_hp_14a, nw=n) + constraint_hp_14a_binary_coupling(pm, i, n) + constraint_hp_14a_min_net_load(pm, i, n) + end + + # CP §14a + for i in PowerModels.ids(pm, :gen_cp_14a, nw=n) + constraint_cp_14a_binary_coupling(pm, i, n) + constraint_cp_14a_min_net_load(pm, i, n) + end + end + + # §14a Zeitbudget-Constraints + if haskey(PowerModels.ref(pm, first(nw_ids)), :gen_hp_14a) + gen_hp_14a_ids = PowerModels.ids(pm, :gen_hp_14a, nw=first(nw_ids)) + constraint_hp_14a_time_budget_daily( + pm, + gen_hp_14a_ids, + timesteps_per_day, + max_hours_per_day, + time_elapsed + ) + end + + # Analog für CP + + # ... Zielfunktion (mit §14a-Termen) ... + end + +Ergebnisinterpretation +~~~~~~~~~~~~~~~~~~~~~~ + +Nach der Optimierung sind die §14a-Abregelungen in den Zeitreihen +enthalten: + +.. code:: python + + # Optimierung durchführen + edisgo.pm_optimize( + opf_version=2, + curtailment_14a=True, + max_hours_per_day=2.0 + ) + + # §14a-Abregelungen extrahieren + hp_14a_gens = [ + col for col in edisgo.timeseries.generators_active_power.columns + if 'hp_14a_support' in col + ] + + curtailment_hp = edisgo.timeseries.generators_active_power[hp_14a_gens] + + # Beispiel: WP "HP_1234" + hp_support_gen = "hp_14a_support_HP_1234" + curtailment_ts = curtailment_hp[hp_support_gen] # MW + + # Original-Last + hp_load_ts = edisgo.timeseries.loads_active_power["HP_1234"] # MW + + # Nettolast (nach Abregelung) + net_load_ts = hp_load_ts - curtailment_ts + + # Statistiken + total_curtailment_mwh = curtailment_ts.sum() # MWh (bei 1h-Auflösung) + total_load_mwh = hp_load_ts.sum() + curtailment_percentage = (total_curtailment_mwh / total_load_mwh) * 100 + + # Zeitbudget-Nutzung + hours_curtailed = (curtailment_ts > 0).sum() # Anzahl Stunden mit Abregelung + days_in_horizon = len(curtailment_ts) / 24 + avg_hours_per_day = hours_curtailed / days_in_horizon + + print(f"Abregelung HP_1234:") + print(f" Total: {total_curtailment_mwh:.2f} MWh ({curtailment_percentage:.1f}%)") + print(f" Stunden mit Abregelung: {hours_curtailed}") + print(f" Ø pro Tag: {avg_hours_per_day:.2f}h (Limit: 2h)") + +**Beispiel-Ausgabe:** + +:: + + Abregelung HP_1234: + Total: 15.32 MWh (3.2%) + Stunden mit Abregelung: 487 + Ø pro Tag: 1.33h (Limit: 2h) + +Beispiel-Workflow mit §14a +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + # 1. Netz laden + edisgo = EDisGo(ding0_grid="path/to/grid") + + # 2. Wärmepumpen hinzufügen (alle > 4.2 kW können abgeregelt werden) + edisgo.add_component( + comp_type="heat_pump", + comp_name="HP_001", + bus="Bus_LV_123", + p_set=0.008, # 8 kW Nennleistung + ... + ) + + # 3. Zeitreihen setzen + edisgo.set_time_series_manual( + generators_p=..., + loads_p=..., + heat_pump_cop_df=..., + heat_demand_df=... + ) + + # 4. Optimierung MIT §14a + edisgo.pm_optimize( + opf_version=2, # Mit Netzrestriktionen + Slacks + curtailment_14a=True, # §14a aktivieren + max_hours_per_day=2.0, # Zeitbudget 2h/Tag + curtailment_14a_total_hours=None, # Optional: Gesamtbudget statt täglich + solver="gurobi", # Binäre Variablen → MILP-Solver nötig + warm_start=False # Kein Warm-Start bei binären Variablen + ) + + # 5. ZWINGEND: Netzausbau für verbleibende Probleme + edisgo.reinforce() # Behebt Überlastungen die Optimierung nicht lösen konnte + + # 6. Ergebnisse analysieren + # §14a Abregelung ist nun in edisgo.timeseries.generators_active_power + # als virtuelle Generatoren "hp_14a_support_..." und "cp_14a_support_..." + + # Netzausbaukosten (nach Flexibilitätsnutzung) + costs = edisgo.results.grid_expansion_costs + print(f"Netzausbaukosten: {costs:,.0f} €") + + # §14a Statistiken + hp_14a_curtailment = edisgo.timeseries.generators_active_power[ + [c for c in edisgo.timeseries.generators_active_power.columns + if 'hp_14a_support' in c] + ] + cp_14a_curtailment = edisgo.timeseries.generators_active_power[ + [c for c in edisgo.timeseries.generators_active_power.columns + if 'cp_14a_support' in c] + ] + + total_curtailment = hp_14a_curtailment.sum().sum() + cp_14a_curtailment.sum().sum() + print(f"§14a Abregelung gesamt: {total_curtailment:.2f} MWh") + +-------------- + +.. _zeitreihen-nutzung-2: + +Zeitreihen-Nutzung +------------------ + +Überblick Zeitreihen-Datenstruktur +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Alle Zeitreihen werden in ``edisgo.timeseries`` gespeichert: + +.. code:: python + + edisgo.timeseries + ├── timeindex: pd.DatetimeIndex # z.B. 8760 Stunden eines Jahres + ├── generators_active_power: pd.DataFrame # MW, Spalten = Generator-Namen + ├── generators_reactive_power: pd.DataFrame # MVAr + ├── loads_active_power: pd.DataFrame # MW, Spalten = Last-Namen + ├── loads_reactive_power: pd.DataFrame # MVAr + ├── storage_units_active_power: pd.DataFrame # MW (+ = Entladung, - = Ladung) + ├── storage_units_reactive_power: pd.DataFrame # MVAr + └── ... weitere Komponenten + +Zeitreihen-Quellen +~~~~~~~~~~~~~~~~~~ + +1. Datenbank-Import (eGon-Datenbank) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: python + + edisgo.import_generators( + generator_scenario="eGon2035", + engine=db_engine + ) + +**Inhalt:** - PV-Anlagen: Zeitreihen aus Wetterdaten (Globalstrahlung) - +Windkraftanlagen: Zeitreihen aus Windgeschwindigkeiten - BHKW: +Wärmegeführter Betrieb oder Grundlast + +**Auflösung:** Typisch 1h für ein Jahr (8760 Zeitschritte) + +2. Worst-Case-Profile +^^^^^^^^^^^^^^^^^^^^^ + +.. code:: python + + edisgo.set_time_series_worst_case( + cases=["feed-in_case", "load_case"] + ) + +**feed-in_case:** - PV: Maximale Einstrahlung (z.B. Mittag im Sommer) - +Wind: Maximaler Wind - Lasten: Minimale Last + +**load_case:** - Generatoren: Minimale Einspeisung - Lasten: Maximale +Last (z.B. Winterabend) + +**Nutzung:** Schnelle Netzplanung ohne vollständige Zeitreihen + +3. Manuelle Zeitreihen +^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: python + + # Eigene Zeitreihen erstellen + timesteps = pd.date_range("2035-01-01", periods=8760, freq="H") + gen_p = pd.DataFrame({ + "PV_001": pv_timeseries, + "Wind_002": wind_timeseries + }, index=timesteps) + + edisgo.set_time_series_manual( + generators_p=gen_p, + loads_p=load_p, + ... + ) + +4. Optimierte Zeitreihen (nach pm_optimize) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Nach ``edisgo.pm_optimize()`` werden Zeitreihen überschrieben mit +optimierten Werten: + +.. code:: python + + # VOR Optimierung: Original-Lasten + hp_load_original = edisgo.timeseries.loads_active_power["HP_123"] + + # Optimierung durchführen + edisgo.pm_optimize(opf_version=2) + + # NACH Optimierung: Optimierte Lasten + hp_load_optimized = edisgo.timeseries.loads_active_power["HP_123"] + + # Unterschied: Zeitliche Verschiebung durch thermischen Speicher + # oder Abregelung durch §14a + +**Zusätzlich:** Virtuelle Generatoren für §14a: + +.. code:: python + + # NEU nach Optimierung mit curtailment_14a=True + curtailment = edisgo.timeseries.generators_active_power["hp_14a_support_HP_123"] + +Zeitreihen in analyze +~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + edisgo.analyze(timesteps=None) + +**Verwendete Zeitreihen:** 1. ``generators_active_power``, +``generators_reactive_power`` - Für alle PV, Wind, BHKW, etc. - +Einspeisung ins Netz + +2. ``loads_active_power``, ``loads_reactive_power`` + + - Für alle Haushalte, Gewerbe, Industrie + - Entnahme aus dem Netz + +3. ``storage_units_active_power``, ``storage_units_reactive_power`` + + - Batteriespeicher Ladung/Entladung + - Positiv = Entladung (wie Generator) + - Negativ = Ladung (wie Last) + +4. **Wärmepumpen** (speziell): + + - NICHT direkt aus ``loads_active_power`` + - Berechnung: ``P_el(t) = heat_demand(t) / COP(t)`` + - ``heat_demand_df``: Wärmebedarf in MW_th + - ``cop_df``: Coefficient of Performance (temperaturabhängig) + +5. **Ladepunkte**: + + - ``charging_points_active_power`` (falls vorhanden) + - Oder aus Flexibilitätsbändern berechnet + +**Ablauf:** + +.. code:: python + + def to_pypsa(self, mode=None, timesteps=None): + # 1. Zeitreihen extrahieren + gen_p = self.timeseries.generators_active_power.loc[timesteps] + load_p = self.timeseries.loads_active_power.loc[timesteps] + + # 2. Wärmepumpen elektrische Last berechnen + hp_loads = [] + for hp_name in heat_pumps: + heat_demand = self.heat_pump.heat_demand_df.loc[timesteps, hp_name] + cop = self.heat_pump.cop_df.loc[timesteps, hp_name] + hp_load_p = heat_demand / cop + hp_loads.append(hp_load_p) + + # 3. Zu PyPSA-Zeitreihen konvertieren + pypsa.loads_t.p_set = load_p + pypsa.generators_t.p_set = gen_p + pypsa.storage_units_t.p_set = storage_p + + return pypsa + +Zeitreihen in reinforce +~~~~~~~~~~~~~~~~~~~~~~~ + +``reinforce`` nutzt dieselben Zeitreihen wie ``analyze``, aber mit +verschiedenen Auswahlmodi: + +Modus 1: Snapshot Analysis +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: python + + edisgo.reinforce(timesteps_pfa="snapshot_analysis") + +**Zeitreihen:** - Nur 2 Zeitschritte: 1. **Max. Residuallast:** +``t_max = argmax(load - generation)`` 2. **Min. Residuallast:** +``t_min = argmin(load - generation)`` + +**Berechnung:** + +.. code:: python + + residual_load = ( + edisgo.timeseries.loads_active_power.sum(axis=1) + - edisgo.timeseries.generators_active_power.sum(axis=1) + ) + + t_max = residual_load.idxmax() # Kritischer Zeitpunkt für Überlastung + t_min = residual_load.idxmin() # Kritischer Zeitpunkt für Rückspeisung + + timesteps = pd.DatetimeIndex([t_max, t_min]) + +**Vorteil:** Sehr schnell (nur 2 Power Flow Analysen) **Risiko:** +Übersieht seltene Extremereignisse + +Modus 2: Reduzierte Analyse +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: python + + edisgo.reinforce(reduced_analysis=True) + +**Ablauf:** 1. Initiale PFA mit allen Zeitschritten 2. Identifiziere +kritische Zeitschritte: - Überlastung > 0% auf irgendeiner Komponente - +Spannungsabweichung > 3% an irgendeinem Knoten 3. Wähle nur diese +Zeitschritte für Verstärkung + +**Zeitreihen:** + +.. code:: python + + # 1. Initiale Analyse + edisgo.analyze(timesteps=edisgo.timeseries.timeindex) + + # 2. Kritische Zeitschritte identifizieren + overloaded_ts = edisgo.results.i_res[ + (edisgo.results.i_res / s_nom).max(axis=1) > 1.0 + ].index + + voltage_issues_ts = edisgo.results.v_res[ + ((edisgo.results.v_res < 0.97) | (edisgo.results.v_res > 1.03)).any(axis=1) + ].index + + critical_ts = overloaded_ts.union(voltage_issues_ts).unique() + + # 3. Nur diese Zeitschritte für Verstärkung + timesteps = critical_ts # z.B. 50 statt 8760 + +**Vorteil:** Viel schneller als volle Analyse, genauer als Snapshot +**Nachteil:** Initial-PFA mit allen Zeitschritten notwendig + +Modus 3: Alle Zeitschritte +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: python + + edisgo.reinforce(timesteps_pfa=None) + +**Zeitreihen:** - Alle Zeitschritte in ``edisgo.timeseries.timeindex`` - +Typisch 8760 Stunden für ein Jahr + +**Vorteil:** Höchste Genauigkeit **Nachteil:** Sehr rechenintensiv + +Modus 4: Custom +^^^^^^^^^^^^^^^ + +.. code:: python + + # Nur Wintermonate + winter_ts = edisgo.timeseries.timeindex[ + edisgo.timeseries.timeindex.month.isin([11, 12, 1, 2]) + ] + + edisgo.reinforce(timesteps_pfa=winter_ts) + +Zeitreihen in pm_optimize +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Input-Zeitreihen +^^^^^^^^^^^^^^^^ + +.. code:: python + + edisgo.pm_optimize( + opf_version=2, + curtailment_14a=True + ) + +**Verwendete Zeitreihen (vor Optimierung):** + +1. **Generatoren:** + + - ``generators_active_power``: Einspeise-Zeitreihen + - ``generators_reactive_power``: Blindleistung (oder aus cos φ + berechnet) + +2. **Inflexible Lasten:** + + - ``loads_active_power``: Haushalte, Gewerbe ohne Flexibilität + - ``loads_reactive_power``: Blindleistung + +3. **Batteriespeicher:** + + - Initial State of Charge (SOC) + - Kapazität, Ladeleistung, Entladeleistung + - **Keine Input-Zeitreihen** (werden optimiert) + +4. **Wärmepumpen:** + + - ``heat_demand_df``: Wärmebedarf in MW_th + - ``cop_df``: COP-Zeitreihen + - Flexibilitätsband (falls flexible WP): + + - ``p_min``, ``p_max`` pro Zeitschritt + - Thermischer Speicher: Kapazität, Anfangs-SOC + +5. **Ladepunkte:** + + - Flexibilitätsbänder: + + - ``p_min``, ``p_max`` pro Zeitschritt + - ``energy_min``, ``energy_max`` (SOC-Grenzen) + + - Ladeeffizienz + +6. **§14a-Komponenten (falls aktiviert):** + + - Alle WP/CP > 4,2 kW werden identifiziert + - Virtuelle Generatoren mit ``pmax = P_nom - 4,2kW`` erstellt + +Optimierungs-Prozess +^^^^^^^^^^^^^^^^^^^^ + +.. code:: python + + # Python → Julia Datenübergabe + pm_dict = to_powermodels( + edisgo_obj, + timesteps=timesteps, + curtailment_14a=True + ) + + # pm_dict enthält: + { + "multinetwork": true, + "nw": { + "0": { # Zeitschritt 0 + "bus": {...}, + "load": {...}, # Inflexible Lasten (P, Q vorgegeben) + "gen": {...}, # Generatoren (P, Q vorgegeben) + "storage": {...}, # Batterie (SOC optimiert) + "heat_pump": {...}, # WP (P optimiert, Wärmebedarf vorgegeben) + "charging_point": {...}, # CP (P optimiert, Flexband vorgegeben) + "gen_hp_14a": {...}, # §14a virtuelle Generatoren (P optimiert) + "gen_cp_14a": {...} + }, + "1": {...}, # Zeitschritt 1 + ... + } + } + +**Julia-Optimierung:** - Löst für alle Zeitschritte gleichzeitig +(Multi-Period OPF) - Bestimmt optimale Fahrpläne für: - Batteriespeicher +(Ladung/Entladung) - Wärmepumpen (elektrische Leistung) - Wärmespeicher +(Beladung/Entladung) - Ladepunkte (Ladeleistung) - §14a-Abregelung +(Curtailment) + +Output-Zeitreihen +^^^^^^^^^^^^^^^^^ + +.. code:: python + + # Julia → Python Ergebnisrückgabe + result = pm_optimize(...) + + # from_powermodels schreibt optimierte Zeitreihen zurück + from_powermodels(edisgo_obj, result) + +**Aktualisierte Zeitreihen:** + +1. **Generatoren:** + + - ``generators_active_power``: + + - Original-Generatoren (ggf. mit Curtailment) + - **NEU:** Virtuelle §14a-Generatoren (``hp_14a_support_...``, + ``cp_14a_support_...``) + + - ``generators_reactive_power``: Optimierte Blindleistung + +2. **Speicher:** + + - ``storage_units_active_power``: Optimierte Ladung/Entladung + - ``storage_units_reactive_power``: Optimierte Blindleistung + +3. **Wärmepumpen:** + + - ``loads_active_power`` (WP-Spalten): Optimierte elektrische + Leistung + - Berücksichtigt thermischen Speicher (zeitliche Verschiebung) + +4. **Ladepunkte:** + + - ``loads_active_power`` (CP-Spalten): Optimierte Ladeleistung + - Innerhalb Flexibilitätsbänder + +5. **§14a-Abregelung (NEU):** + + - Als virtuelle Generatoren in ``generators_active_power``: + - Spalten: ``hp_14a_support_{hp_name}``, + ``cp_14a_support_{cp_name}`` + - Werte: Abregelleistung in MW + - **Nettolast = Original-Last - Abregelung** + +**Beispiel:** + +.. code:: python + + # VOR Optimierung + print(edisgo.timeseries.generators_active_power.columns) + # ['PV_001', 'Wind_002', 'PV_003', ...] + + print(edisgo.timeseries.loads_active_power.columns) + # ['Load_HH_001', 'HP_LV_123', 'CP_LV_456', ...] + + # Optimierung MIT §14a + edisgo.pm_optimize(opf_version=2, curtailment_14a=True) + + # NACH Optimierung + print(edisgo.timeseries.generators_active_power.columns) + # ['PV_001', 'Wind_002', 'PV_003', ..., + # 'hp_14a_support_HP_LV_123', # NEU! + # 'cp_14a_support_CP_LV_456'] # NEU! + + # WP-Last optimiert (zeitlich verschoben durch thermischen Speicher) + hp_load_opt = edisgo.timeseries.loads_active_power["HP_LV_123"] + + # §14a-Abregelung + hp_curtailment = edisgo.timeseries.generators_active_power["hp_14a_support_HP_LV_123"] + + # Effektive Nettolast + hp_net_load = hp_load_opt - hp_curtailment + +Zeitreihen-Auflösung +~~~~~~~~~~~~~~~~~~~~ + +Die Zeitauflösung beeinflusst Optimierung und Genauigkeit: + ++------------+----------------------+----------------------+-----------+ +| Auflösung | Zeitschritte/Jahr | Optimierungsgröße | Use Case | ++============+======================+======================+===========+ +| 1 Stunde | 8760 | Groß (langsam) | Jahressi | +| | | | mulation, | +| | | | det | +| | | | aillierte | +| | | | Planung | ++------------+----------------------+----------------------+-----------+ +| 15 Minuten | 35040 | Sehr groß | Intr | +| | | | aday-Flex | +| | | | ibilität, | +| | | | genaue | +| | | | Ne | +| | | | tzanalyse | ++------------+----------------------+----------------------+-----------+ +| 1 Tag | 365 | Klein (schnell) | Grobe | +| | | | Planung, | +| | | | Se | +| | | | nsitivitä | +| | | | tsstudien | ++------------+----------------------+----------------------+-----------+ +| Worst-Case | 2-10 | Sehr klein | Schnelle | +| | | | Net | +| | | | zplanung, | +| | | | Screening | ++------------+----------------------+----------------------+-----------+ + +**Wichtig für §14a:** - Zeitbudget skaliert mit Auflösung: - +1h-Auflösung: ``max_timesteps_per_day = 2h / 1h = 2`` - 15min-Auflösung: +``max_timesteps_per_day = 2h / 0.25h = 8`` + +Zeitreihen-Persistenz +~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + # Speichern + edisgo.save(directory="results/grid_001") + + # Lädt: + # - Netztopologie (Leitungen, Trafos, Busse) + # - Zeitreihen (alle DataFrames) + # - Results (PFA-Ergebnisse) + # - Optimierungsergebnisse (§14a-Curtailment) + + # Laden + edisgo = EDisGo(directory="results/grid_001") + + # Zeitreihen direkt verfügbar + timesteps = edisgo.timeseries.timeindex + gen_p = edisgo.timeseries.generators_active_power + +-------------- + +Dateipfade und Referenzen +------------------------- + +Python-Dateien +~~~~~~~~~~~~~~ + ++-----------------+---------------+------------------------------------+ +| Datei | Pfad | Beschreibung | ++=================+===============+====================================+ +| **EDisGo | ``edisg | ``analyze()``, ``reinforce()``, | +| Hauptklasse** | o/edisgo.py`` | ``pm_optimize()`` Methoden | ++-----------------+---------------+------------------------------------+ +| **PowerModels | ``edi | ``to_powermodels()``, | +| I/O** | sgo/io/powerm | ``from_powermodels()``, | +| | odels_io.py`` | §14a-Generatoren | ++-----------------+---------------+------------------------------------+ +| **PowerModels | ``edisg | Julia-Subprozess, | +| OPF** | o/opf/powermo | JSON-Kommunikation | +| | dels_opf.py`` | | ++-----------------+---------------+------------------------------------+ +| **Reinforce | ``edisgo/fl | Verstärkungsalgorithmus | +| I | ex_opt/reinfo | | +| mplementation** | rce_grid.py`` | | ++-----------------+---------------+------------------------------------+ +| **Reinforce | `` | Leitungs-/Trafo-Verstärkung | +| Measures** | edisgo/flex_o | | +| | pt/reinforce_ | | +| | measures.py`` | | ++-----------------+---------------+------------------------------------+ +| **Timeseries** | ``edisg | Zeitreihen-Verwaltung | +| | o/edisgo.py`` | | +| | (Klasse | | +| | `` | | +| | TimeSeries``) | | ++-----------------+---------------+------------------------------------+ + +Julia-Dateien +~~~~~~~~~~~~~ + ++-----------------+---------------+------------------------------------+ +| Datei | Pfad | Beschreibung | ++=================+===============+====================================+ +| **Main Entry** | ``edisgo/o | Solver-Setup, JSON I/O | +| | pf/eDisGo_OPF | | +| | .jl/Main.jl`` | | ++-----------------+---------------+------------------------------------+ +| **OPF Problem** | ``edisgo | ``build_mn_opf_bf_flex()`` | +| | /opf/eDisGo_O | | +| | PF.jl/src/pro | | +| | b/opf_bf.jl`` | | ++-----------------+---------------+------------------------------------+ +| **Variables** | ``edisgo/op | Alle Variablendefinitionen | +| | f/eDisGo_OPF. | | +| | jl/src/core/v | | +| | ariables.jl`` | | ++-----------------+---------------+------------------------------------+ +| **Constraints** | ``edisgo/opf | Standard-Constraints | +| | /eDisGo_OPF.j | | +| | l/src/core/co | | +| | nstraint.jl`` | | ++-----------------+---------------+------------------------------------+ +| **§14a HP | ``edis | WP-§14a-Constraints | +| Constraints** | go/opf/eDisGo | | +| | _OPF.jl/src/c | | +| | ore/constrain | | +| | t_hp_14a.jl`` | | ++-----------------+---------------+------------------------------------+ +| **§14a CP | ``edis | CP-§14a-Constraints | +| Constraints** | go/opf/eDisGo | | +| | _OPF.jl/src/c | | +| | ore/constrain | | +| | t_cp_14a.jl`` | | ++-----------------+---------------+------------------------------------+ +| **Objective** | ``edisgo/op | Zielfunktionen (4 Versionen) | +| | f/eDisGo_OPF. | | +| | jl/src/core/o | | +| | bjective.jl`` | | ++-----------------+---------------+------------------------------------+ +| **Branch Flow** | ``ed | Branch-Flow-Formulierung | +| | isgo/opf/eDis | | +| | Go_OPF.jl/src | | +| | /form/bf.jl`` | | ++-----------------+---------------+------------------------------------+ +| **Data | ``edis | Datenstrukturen | +| Handling** | go/opf/eDisGo | | +| | _OPF.jl/src/c | | +| | ore/data.jl`` | | ++-----------------+---------------+------------------------------------+ +| **Solution | ``edisgo/o | Ergebnis-Extraktion | +| Processing** | pf/eDisGo_OPF | | +| | .jl/src/core/ | | +| | solution.jl`` | | ++-----------------+---------------+------------------------------------+ + +Beispiele +~~~~~~~~~ + ++-----------------+---------------+------------------------------------+ +| Datei | Pfad | Beschreibung | ++=================+===============+====================================+ +| **§14a | ``example | Beispiel für §14a-Optimierung und | +| Analyse** | s/example_ana | Auswertung | +| | lyze_14a.py`` | | ++-----------------+---------------+------------------------------------+ +| **Standard | ``exam | Standard-OPF ohne §14a | +| Optimierung** | ples/example_ | | +| | optimize.py`` | | ++-----------------+---------------+------------------------------------+ +| ** | ``examp | Netzausbau-Beispiel | +| Reinforcement** | les/example_r | | +| | einforce.py`` | | ++-----------------+---------------+------------------------------------+ + +Konfigurationsdateien +~~~~~~~~~~~~~~~~~~~~~ + ++-----------------+---------------+------------------------------------+ +| Datei | Pfad | Beschreibung | ++=================+===============+====================================+ +| **Config** | ``edisgo/c | Default-Einstellungen | +| | onfig/config_ | (Spannungsgrenzen, | +| | default.cfg`` | Kostenparameter) | ++-----------------+---------------+------------------------------------+ +| **Equipment | ``edisgo/c | Leitungs-/Trafo-Typen mit techn. | +| Data** | onfig/equipme | Daten | +| | nt_data.csv`` | | ++-----------------+---------------+------------------------------------+ + +-------------- \ No newline at end of file From 030d10aa957ecc28130fcc19e9b5bdc93eb324e0 Mon Sep 17 00:00:00 2001 From: joda9 Date: Tue, 6 Jan 2026 15:03:18 +0100 Subject: [PATCH 41/43] add: include optimization documentation in the table of contents --- doc/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/index.rst b/doc/index.rst index 444e35f04..ccffab368 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -65,6 +65,7 @@ Contents quickstart usage_details features_in_detail + optimization_de dev_notes definitions_and_units configs From 637a0a88a751cc73878a7daac582c9f6d109ab67 Mon Sep 17 00:00:00 2001 From: joda9 Date: Tue, 6 Jan 2026 15:14:02 +0100 Subject: [PATCH 42/43] small fixes --- doc/optimization_de.rst | 52 ++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/doc/optimization_de.rst b/doc/optimization_de.rst index 1573f9a36..7b77bd5e0 100644 --- a/doc/optimization_de.rst +++ b/doc/optimization_de.rst @@ -303,10 +303,10 @@ Für ein Netz mit: - 150 Busse - 200 Leitungen/Trafos - 50 Generatoren - 5 Batteriespeicher - 20 Wärmepumpen - 10 Ladepunkte - 8760 Zeitschritte (1 Jahr, 1h-Auflösung) -**Anzahl Variablen (grob):** - Spannungen: 150 Busse × 8760 Zeitschritte -= **1,314,000 Variablen** - Leitungsflüsse: 200 × 2 (p,q) × 8760 = -**3,504,000 Variablen** - Generatoren: 50 × 2 (p,q) × 8760 = **876,000 -Variablen** - Speicher: 5 × 2 (Leistung + SOC) × 8760 = **87,600 +**Anzahl Variablen (grob):** - Spannungen: 150 Busse x 8760 Zeitschritte += **1,314,000 Variablen** - Leitungsflüsse: 200 x 2 (p,q) x 8760 = +**3,504,000 Variablen** - Generatoren: 50 x 2 (p,q) x 8760 = **876,000 +Variablen** - Speicher: 5 x 2 (Leistung + SOC) x 8760 = **87,600 Variablen** - … → **Mehrere Millionen Variablen** für Jahressimulation! @@ -333,7 +333,7 @@ Bestimmte Constraints koppeln die Zeitschritte: # SOC in t+1 hängt von SOC in t und Leistung in t ab @constraint(pm.model, var(pm, n+1, :se, s) == - var(pm, n, :se, s) + var(pm, n, :ps, s) × Δt × η + var(pm, n, :se, s) + var(pm, n, :ps, s) x Δt x η ) end end @@ -440,7 +440,7 @@ Batteriespeicher-Variablen (Battery Storage Variables) +---------------+-----------------+-------------+---------------------+ **Constraints:** - SOC-Kopplung zwischen Zeitschritten: -``se[t+1] = se[t] + ps[t] × Δt × η`` - Kapazitätsgrenzen: +``se[t+1] = se[t] + ps[t] x Δt x η`` - Kapazitätsgrenzen: ``se_min ≤ se[t] ≤ se_max`` - Leistungsgrenzen: ``ps_min ≤ ps[t] ≤ ps_max`` @@ -494,7 +494,7 @@ Ladepunkt-Variablen (Charging Point / EV Variables) +----------------+----------------+-------------+---------------------+ **Constraints:** - Energiekopplung: -``cpe[t+1] = cpe[t] + pcp[t] × Δt × η`` - Kapazität: +``cpe[t+1] = cpe[t] + pcp[t] x Δt x η`` - Kapazität: ``cpe_min ≤ cpe[t] ≤ cpe_max`` - Ladeleistung: ``0 ≤ pcp[t] ≤ pcp_max`` -------------- @@ -970,7 +970,7 @@ Phase 1: Problemaufbau (build_mn_opf_bf_flex) # §14a Constraints pro Zeitschritt if curtailment_14a for h in ids(pm, :gen_hp_14a, nw=n) - constraint_hp_14a_binary_coupling(pm, h, n) # p_hp14a ≤ pmax × z + constraint_hp_14a_binary_coupling(pm, h, n) # p_hp14a ≤ pmax x z constraint_hp_14a_min_net_load(pm, h, n) # Nettolast ≥ min(Last, 4.2kW) end for c in ids(pm, :gen_cp_14a, nw=n) @@ -988,21 +988,21 @@ Phase 2: Inter-Zeitschritt Constraints # Speicher-Energiekopplung zwischen Zeitschritten for s in ids(pm, :storage) for t in 1:(T-1) - se[t+1] == se[t] + ps[t] × Δt × η + se[t+1] == se[t] + ps[t] x Δt x η end end # Wärmespeicher-Kopplung for h in ids(pm, :heat_pump) for t in 1:(T-1) - hse[t+1] == hse[t] + phs[t] × Δt × η + hse[t+1] == hse[t] + phs[t] x Δt x η end end # EV-Batterie-Kopplung for c in ids(pm, :charging_point) for t in 1:(T-1) - cpe[t+1] == cpe[t] + pcp[t] × Δt × η + cpe[t+1] == cpe[t] + pcp[t] x Δt x η end end @@ -1028,25 +1028,25 @@ Phase 3: Zielfunktion .. code:: julia - minimize: 0.9 × sum(Verluste) + 0.1 × max(ll) + 0.05 × sum(p_hp14a) + 0.05 × sum(p_cp14a) + minimize: 0.9 x sum(Verluste) + 0.1 x max(ll) + 0.05 x sum(p_hp14a) + 0.05 x sum(p_cp14a) **OPF Version 2** (mit Netzrestriktionen, mit Slacks): .. code:: julia - minimize: 0.4 × sum(Verluste) + 0.6 × sum(Slacks) + 0.5 × sum(p_hp14a) + 0.5 × sum(p_cp14a) + minimize: 0.4 x sum(Verluste) + 0.6 x sum(Slacks) + 0.5 x sum(p_hp14a) + 0.5 x sum(p_cp14a) **OPF Version 3** (mit HV-Anforderungen, geliftete Restriktionen): .. code:: julia - minimize: 0.9 × sum(Verluste) + 0.1 × max(ll) + 50 × sum(phvs) + 0.05 × sum(p_hp14a) + 0.05 × sum(p_cp14a) + minimize: 0.9 x sum(Verluste) + 0.1 x max(ll) + 50 x sum(phvs) + 0.05 x sum(p_hp14a) + 0.05 x sum(p_cp14a) **OPF Version 4** (mit HV-Anforderungen und Restriktionen): .. code:: julia - minimize: 0.4 × sum(Verluste) + 0.6 × sum(Slacks) + 50 × sum(phvs) + 0.5 × sum(p_hp14a) + 0.5 × sum(p_cp14a) + minimize: 0.4 x sum(Verluste) + 0.6 x sum(Slacks) + 50 x sum(phvs) + 0.5 x sum(p_hp14a) + 0.5 x sum(p_cp14a) **Wichtig:** - §14a-Terme haben moderate Gewichte → Abregelung wird genutzt, aber minimiert - Slack-Variablen haben hohe implizite Kosten → @@ -1785,7 +1785,7 @@ Standard-Optimierung (OHNE §14a): - **E-Autos:** Ladesteuerung innerhalb Flexibilitätsband - **Inflexible WP/CP:** Können NICHT abgeregelt werden -§14a-Optimierung (MIT §14a): +§14a-Optimierung: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - **ALLE Wärmepumpen > 4,2 kW:** Können bis auf 4,2 kW abgeregelt @@ -1842,7 +1842,7 @@ Constraints .. code:: julia - @constraint(model, p_hp14a[h,t] <= pmax[h] × z_hp14a[h,t]) + @constraint(model, p_hp14a[h,t] <= pmax[h] x z_hp14a[h,t]) **Bedeutung:** - Wenn ``z_hp14a[h,t] = 0`` (keine Abregelung): ``p_hp14a[h,t] = 0`` - Wenn ``z_hp14a[h,t] = 1`` (Abregelung aktiv): @@ -1928,10 +1928,10 @@ aufgenommen: .. code:: julia minimize: - 0.4 × sum(line_losses[t] for t in timesteps) - + 0.6 × sum(all_slacks[t] for t in timesteps) - + 0.5 × sum(p_hp14a[h,t] for h,t) - + 0.5 × sum(p_cp14a[c,t] for c,t) + 0.4 x sum(line_losses[t] for t in timesteps) + + 0.6 x sum(all_slacks[t] for t in timesteps) + + 0.5 x sum(p_hp14a[h,t] for h,t) + + 0.5 x sum(p_cp14a[c,t] for c,t) **Interpretation der Gewichte:** - ``0.4`` für Verluste: Basiskosten Netzbetrieb - ``0.6`` für Slacks: Hohe Priorität Netzrestriktionen @@ -1945,10 +1945,10 @@ zeitliche Verschiebung) - Wenn andere Flexibilitäten nicht ausreichen: .. code:: julia minimize: - 0.9 × sum(line_losses[t] for t in timesteps) - + 0.1 × max(line_loading[l,t] for l,t) - + 0.05 × sum(p_hp14a[h,t] for h,t) - + 0.05 × sum(p_cp14a[c,t] for c,t) + 0.9 x sum(line_losses[t] for t in timesteps) + + 0.1 x max(line_loading[l,t] for l,t) + + 0.05 x sum(p_hp14a[h,t] for h,t) + + 0.05 x sum(p_cp14a[c,t] for c,t) **Niedrigeres Gewicht (0.05):** §14a wird bevorzugt gegenüber hoher Leitungsauslastung. @@ -2097,7 +2097,7 @@ Julia: Constraints implementieren nw::Int=nw_id_default ) """ - Binäre Kopplung: p_hp14a <= pmax × z_hp14a + Binäre Kopplung: p_hp14a <= pmax x z_hp14a """ p_hp14a = PowerModels.var(pm, nw, :p_hp14a, i) z_hp14a = PowerModels.var(pm, nw, :z_hp14a, i) From d2c8b355ef9d3c264b27198119ae64838539b934 Mon Sep 17 00:00:00 2001 From: joda9 Date: Tue, 6 Jan 2026 15:35:55 +0100 Subject: [PATCH 43/43] small fixes --- doc/optimization_de.rst | 357 ++++++++++++++++++++-------------------- 1 file changed, 176 insertions(+), 181 deletions(-) diff --git a/doc/optimization_de.rst b/doc/optimization_de.rst index 7b77bd5e0..5a48d076e 100644 --- a/doc/optimization_de.rst +++ b/doc/optimization_de.rst @@ -128,45 +128,42 @@ Index-Variablen PowerModels-Funktionen ~~~~~~~~~~~~~~~~~~~~~~ -+--------------+--------------------+--------------------+--------------+ -| Funktion | Rückgabewert | Beschreibung | Beispiel | -+==============+====================+====================+==============+ -| ``ids(pm, : | ``Array{Int}`` | Gibt alle Bus-IDs | ``[1, 2, 3, | -| bus, nw=n)`` | | für Zeitschritt n | ..., 150]`` | -| | | zurück | | -+--------------+--------------------+--------------------+--------------+ -| `` | ``Array{Int}`` | Gibt alle | ``[1, 2, 3, | -| ids(pm, :bra | | Branch-IDs | ..., 200]`` | -| nch, nw=n)`` | | (Leitungen/Trafos) | | -| | | zurück | | -+--------------+--------------------+--------------------+--------------+ -| ``ids(pm, : | ``Array{Int}`` | Gibt alle | ``[1, 2, 3 | -| gen, nw=n)`` | | Generator-IDs | , ..., 50]`` | -| | | zurück | | -+--------------+--------------------+--------------------+--------------+ -| ``i | ``Array{Int}`` | Gibt alle | ` | -| ds(pm, :stor | | Storage-IDs zurück | `[1, 2, 3]`` | -| age, nw=n)`` | | | | -+--------------+--------------------+--------------------+--------------+ -| ``ref(pm, nw | ``Dict`` | Gibt Daten für Bus | ``{"vmin": | -| , :bus, i)`` | | i in Zeitschritt | 0.9, "vmax": | -| | | nw | 1.1, ...}`` | -+--------------+--------------------+--------------------+--------------+ -| ``r | ``Dict`` | Gibt Daten für | `` | -| ef(pm, nw, : | | Branch l in | {"rate_a": 0 | -| branch, l)`` | | Zeitschritt nw | .5, "br_r": | -| | | | 0.01, ...}`` | -+--------------+--------------------+--------------------+--------------+ -| ``var(pm, | ``JuMP.Variable`` | Gibt | JuMP-Var | -| nw, :p, l)`` | | Wir | iable-Objekt | -| | | kleistungsvariable | | -| | | für Branch l | | -| | | zurück | | -+--------------+--------------------+--------------------+--------------+ -| ``var(pm, | ``JuMP.Variable`` | Gibt | JuMP-Var | -| nw, :w, i)`` | | Spannungsvariable | iable-Objekt | -| | | für Bus i zurück | | -+--------------+--------------------+--------------------+--------------+ ++---------------------------+--------------------+--------------------+----------------------+ +| Funktion | Rückgabewert | Beschreibung | Beispiel | ++===========================+====================+====================+======================+ +| ``ids(pm, :bus, nw=n)`` | ``Array{Int}`` | Gibt alle Bus-IDs | ``[1, 2, 3, ...]`` | +| | | für Zeitschritt n | | +| | | zurück | | ++---------------------------+--------------------+--------------------+----------------------+ +| ``ids(pm, :branch,`` | ``Array{Int}`` | Gibt alle | ``[1, 2, 3, ...]`` | +| ``nw=n)`` | | Branch-IDs | | +| | | (Leitungen/Trafos) | | +| | | zurück | | ++---------------------------+--------------------+--------------------+----------------------+ +| ``ids(pm, :gen, nw=n)`` | ``Array{Int}`` | Gibt alle | ``[1, 2, 3, ...]`` | +| | | Generator-IDs | | +| | | zurück | | ++---------------------------+--------------------+--------------------+----------------------+ +| ``ids(pm, :storage,`` | ``Array{Int}`` | Gibt alle | ``[1, 2, 3]`` | +| ``nw=n)`` | | Storage-IDs zurück | | ++---------------------------+--------------------+--------------------+----------------------+ +| ``ref(pm, nw, :bus, i)`` | ``Dict`` | Gibt Daten für Bus | ``{"vmin": 0.9,`` | +| | | i in Zeitschritt | ``"vmax": 1.1}`` | +| | | nw | | ++---------------------------+--------------------+--------------------+----------------------+ +| ``ref(pm, nw, :branch,`` | ``Dict`` | Gibt Daten für | ``{"rate_a": 0.5,`` | +| ``l)`` | | Branch l in | ``"br_r": 0.01}`` | +| | | Zeitschritt nw | | ++---------------------------+--------------------+--------------------+----------------------+ +| ``var(pm, nw, :p, l)`` | ``JuMP.Variable`` | Gibt | JuMP-Variable-Objekt | +| | | Wirkleistungs- | | +| | | variable für | | +| | | Branch l zurück | | ++---------------------------+--------------------+--------------------+----------------------+ +| ``var(pm, nw, :w, i)`` | ``JuMP.Variable`` | Gibt Spannungs- | JuMP-Variable-Objekt | +| | | variable für | | +| | | Bus i zurück | | ++---------------------------+--------------------+--------------------+----------------------+ Typische Code-Muster ~~~~~~~~~~~~~~~~~~~~ @@ -253,11 +250,12 @@ Optimierung: **WICHTIG: Das Netz existiert T-mal!** -Für einen Optimierungshorizont von **8760 Stunden** (1 Jahr) bedeutet -das: - Das gesamte Netz wird **8760-mal dupliziert** - Jeder Zeitschritt -hat seine eigene vollständige Netz-Kopie - Alle Busse, Leitungen, -Trafos, Generatoren, Lasten existieren **8760-mal** - Jeder Zeitschritt -hat **eigene Optimierungsvariablen** +Für einen Optimierungshorizont von **8760 Stunden** (1 Jahr) bedeutet das: + +- Das gesamte Netz wird **8760-mal dupliziert** +- Jeder Zeitschritt hat seine eigene vollständige Netz-Kopie +- Alle Busse, Leitungen, Trafos, Generatoren, Lasten existieren **8760-mal** +- Jeder Zeitschritt hat **eigene Optimierungsvariablen** **Was unterscheidet die Zeitschritte?** @@ -288,26 +286,35 @@ hat **eigene Optimierungsvariablen** **Beispiel: Wirkleistungsvariable p[l,i,j]** -Für eine Leitung ``l=5`` zwischen Bus ``i=10`` und ``j=11``: - -``var(pm, 0, :p)[(5,10,11)]`` = Wirkleistung in Zeitschritt 0 (00:00 -Uhr) - ``var(pm, 1, :p)[(5,10,11)]`` = Wirkleistung in Zeitschritt 1 -(01:00 Uhr) - ``var(pm, 2, :p)[(5,10,11)]`` = Wirkleistung in -Zeitschritt 2 (02:00 Uhr) - … - ``var(pm, 8759, :p)[(5,10,11)]`` = -Wirkleistung in Zeitschritt 8759 (23:00 Uhr) +Für eine Leitung ``l=5`` zwischen Bus ``i=10`` und ``j=11``: + +- ``var(pm, 0, :p)[(5,10,11)]`` = Wirkleistung in Zeitschritt 0 (00:00 Uhr) +- ``var(pm, 1, :p)[(5,10,11)]`` = Wirkleistung in Zeitschritt 1 (01:00 Uhr) +- ``var(pm, 2, :p)[(5,10,11)]`` = Wirkleistung in Zeitschritt 2 (02:00 Uhr) +- … +- ``var(pm, 8759, :p)[(5,10,11)]`` = Wirkleistung in Zeitschritt 8759 (23:00 Uhr) → **8760 verschiedene Variablen** für dieselbe Leitung! **Optimierungsproblem-Größe:** -Für ein Netz mit: - 150 Busse - 200 Leitungen/Trafos - 50 Generatoren - -5 Batteriespeicher - 20 Wärmepumpen - 10 Ladepunkte - 8760 Zeitschritte -(1 Jahr, 1h-Auflösung) +Für ein Netz mit: + +- 150 Busse +- 200 Leitungen/Trafos +- 50 Generatoren +- 5 Batteriespeicher +- 20 Wärmepumpen +- 10 Ladepunkte +- 8760 Zeitschritte (1 Jahr, 1h-Auflösung) -**Anzahl Variablen (grob):** - Spannungen: 150 Busse x 8760 Zeitschritte -= **1,314,000 Variablen** - Leitungsflüsse: 200 x 2 (p,q) x 8760 = -**3,504,000 Variablen** - Generatoren: 50 x 2 (p,q) x 8760 = **876,000 -Variablen** - Speicher: 5 x 2 (Leistung + SOC) x 8760 = **87,600 -Variablen** - … +**Anzahl Variablen (grob):** + +- Spannungen: 150 Busse x 8760 Zeitschritte = **1,314,000 Variablen** +- Leitungsflüsse: 200 x 2 (p,q) x 8760 = **3,504,000 Variablen** +- Generatoren: 50 x 2 (p,q) x 8760 = **876,000 Variablen** +- Speicher: 5 x 2 (Leistung + SOC) x 8760 = **87,600 Variablen** +- … → **Mehrere Millionen Variablen** für Jahressimulation! @@ -709,11 +716,11 @@ Probleme └─────────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────────┐ - │ 5. NETZAUSBAU MIT OPTIMIERTEN ZEITREIHEN (ZWINGEND!) │ + │ 5. NETZAUSBAU MIT OPTIMIERTEN ZEITREIHEN │ ├─────────────────────────────────────────────────────────────────────┤ │ edisgo.reinforce() │ │ │ - │ WICHTIG: Dieser Schritt ist ZWINGEND erforderlich! │ + │ WICHTIG: Dieser Schritt ist in der Regel erforderlich! │ │ │ │ Warum? │ │ - Optimierung nutzt Flexibilität, kann aber nicht alle Probleme │ @@ -749,78 +756,72 @@ Workflow-Varianten im Vergleich Die folgende Tabelle zeigt die wichtigsten Workflow-Varianten und deren Anwendungsfälle: -+--------------+--------------+------------------------+--------------+ -| Workflow | Schritte | Wann sinnvoll? | Ergebnis | -+==============+==============+========================+==============+ -| **A: Nur | 1. Netz | - Keine Flexibilitäten | Hohe | -| Netzausbau | laden2. | vorhanden- Schnelle | Ne | -| (ohne | Komponenten | konservative Planung- | tzausbaukost | -| Op | hinzufügen3. | Referenzszenario | enFlexibilit | -| timierung)** | ``r | | ätspotenzial | -| | einforce()`` | | ungenutzt | -+--------------+--------------+------------------------+--------------+ -| **B: Mit | 1. Netz | - Flexibilitäten | Minimale | -| Optimierung | laden2. | vorhanden (Speicher, | Netzausbauko | -| ( | Optional: | WP, CP)- §14a-Nutzung | stenOptimale | -| EMPFOHLEN)** | ``r | gewünscht- Minimierung | Flexibilität | -| | einforce()`` | Netzausbaukosten | snutzungBetr | -| | auf | | iebssicheres | -| | Basisnetz3. | | Netz | -| | Komponenten | | | -| | hinzufügen4. | | | -| | ``pm_opti | | | -| | mize()``\ 5. | | | -| | * | | | -| | *Zwingend:** | | | -| | ``r | | | -| | einforce()`` | | | -+--------------+--------------+------------------------+--------------+ -| **C: | 1. Netz | - Kostenvergleich | Kostent | -| Basisn | laden | mit/ohne neue | ransparenzAt | -| etz-Referenz | ( | Komponenten- Analyse | tributierung | -| + | Basisnetz)2. | Zusatzkosten durch | auf neue | -| O | ``r | WP/CP- Bewertung | Ko | -| ptimierung** | einforce()`` | §14a-Nutzen | mponentenQua | -| | → Kosten₁3. | | ntifizierung | -| | Neue | | Flexibi | -| | Komponenten | | litätsnutzen | -| | hinzufügen4. | | | -| | ``pm_opti | | | -| | mize()``\ 5. | | | -| | ``r | | | -| | einforce()`` | | | -| | → Kosten₂6. | | | -| | Vergleich: | | | -| | Kosten₂ - | | | -| | Kosten₁ | | | -+--------------+--------------+------------------------+--------------+ -| **D: Mehrere | 1. Netz | - Bewertung | V | -| Optimierung | laden + | verschiedener | ollständiger | -| sszenarien** | Komponenten | Flexibilitätsoptionen- | S | -| | h | Kosten-Nutzen-Analyse | zenariovergl | -| | inzufügen2a. | §14a- | eichOptimale | -| | ``r | Sensitivitätsanalyse | Strategiew | -| | einforce()`` | | ahlFundierte | -| | → | | Entscheidu | -| | Referenz2b. | | ngsgrundlage | -| | `` | | | -| | pm_optimize( | | | -| | 14a=False)`` | | | -| | + | | | -| | ``reinfo | | | -| | rce()``\ 2c. | | | -| | ` | | | -| | `pm_optimize | | | -| | (14a=True)`` | | | -| | + | | | -| | ``reinf | | | -| | orce()``\ 3. | | | -| | Vergleich | | | -+--------------+--------------+------------------------+--------------+ +.. list-table:: + :header-rows: 1 + :widths: 20 25 30 25 + + * - Workflow + - Schritte + - Wann sinnvoll? + - Ergebnis + * - **A: Nur Netzausbau (ohne Optimierung)** + - 1. Netz laden + + 2. Komponenten hinzufügen + + 3. ``reinforce()`` + - - Keine Flexibilitäten vorhanden + - Schnelle konservative Planung + - Referenzszenario + - Hohe Netzausbaukosten, Flexibilitätspotenzial ungenutzt + * - **B: Mit Optimierung (EMPFOHLEN)** + - 1. Netz laden + + 2. Optional: ``reinforce()`` auf Basisnetz + + 3. Komponenten hinzufügen + + 4. ``pm_optimize()`` + + 5. **Zwingend:** ``reinforce()`` + - - Flexibilitäten vorhanden (Speicher, WP, CP) + - §14a-Nutzung gewünscht + - Minimierung Netzausbaukosten + - Minimale Netzausbaukosten, optimale Flexibilitätsnutzung, betriebssicheres Netz + * - **C: Basisnetz-Referenz + Optimierung** + - 1. Netz laden (Basisnetz) + + 2. ``reinforce()`` → Kosten₁ + + 3. Neue Komponenten hinzufügen + + 4. ``pm_optimize()`` + + 5. ``reinforce()`` → Kosten₂ + + 6. Vergleich: Kosten₂ - Kosten₁ + - - Kostenvergleich mit/ohne neue Komponenten + - Analyse Zusatzkosten durch WP/CP + - Bewertung §14a-Nutzen + - Kostentransparenz, Attributierung auf neue Komponenten, Quantifizierung Flexibilitätsnutzen + * - **D: Mehrere Optimierungsszenarien** + - 1. Netz laden + Komponenten hinzufügen + + 2a. ``reinforce()`` → Referenz + + 2b. ``pm_optimize(14a=False)`` + ``reinforce()`` + + 2c. ``pm_optimize(14a=True)`` + ``reinforce()`` + + 3. Vergleich + - - Bewertung verschiedener Flexibilitätsoptionen + - Kosten-Nutzen-Analyse §14a + - Sensitivitätsanalyse + - Vollständiger Szenariovergleich, optimale Strategiewahl, fundierte Entscheidungsgrundlage **Wichtige Erkenntnisse:** -1. **Reinforce VOR Optimierung macht NUR Sinn für:** +1. **Reinforce vor Optimierung macht nur Sinn für:** - Basisnetz ohne neue Komponenten (Referenzszenario) - Dokumentation des Ausgangszustands @@ -829,11 +830,10 @@ Anwendungsfälle: Flexibilitätseinsatz untersucht werden soll** → Würde Flexibilitätspotenzial zunichtemachen -2. **Reinforce NACH Optimierung ist ZWINGEND:** +2. **Reinforce nach Optimierung ist in der Regel sinnvoll:** - Optimierung reduziert Netzausbau, löst aber nicht alle Probleme - Slack-Variablen zeigen verbleibende Verletzungen - - Ohne finales ``reinforce()`` ist das Netz **NICHT betriebssicher** 3. **Beispielhafte Kostenreduktion:** @@ -1352,45 +1352,42 @@ Spannungsverletzungen) und **führt Verstärkungsmaßnahmen** durch: Parameter ~~~~~~~~~ -+----------------+--------+----------------+--------------------------+ -| Parameter | Typ | Default | Beschreibung | -+================+========+================+==========================+ -| ``t | `` | ``'snapsh | ``'snapshot_analysis'`` | -| imesteps_pfa`` | str \| | ot_analysis'`` | = 2 | -| | Datet | | Worst-Case-Zeitschritte, | -| | imeInd | | ``DatetimeIndex`` = | -| | ex \| | | benutzerdefiniert, | -| | None`` | | ``None`` = alle | -| | | | Zeitschritte | -+----------------+--------+----------------+--------------------------+ -| ``redu | `` | ``False`` | Nutzt nur die | -| ced_analysis`` | bool`` | | kritischsten | -| | | | Zeitschritte (höchste | -| | | | Überlast oder | -| | | | Spannungsabweichung) | -+----------------+--------+----------------+--------------------------+ -| ``max_whil | ` | ``20`` | Maximale Anzahl der | -| e_iterations`` | `int`` | | Verstärkungsiterationen | -+----------------+--------+----------------+--------------------------+ -| ``split_ | `` | ``True`` | Getrennte | -| voltage_band`` | bool`` | | Spannungsbänder für | -| | | | NS/MS (z.B. NS ±3 %, MS | -| | | | ±7 %) | -+----------------+--------+----------------+--------------------------+ -| ``mode`` | ``s | ``None`` | Netzebene: ``'mv'``, | -| | tr \| | | ``'mvlv'``, ``'lv'`` | -| | None`` | | oder ``None`` (= | -| | | | automatisch) | -+----------------+--------+----------------+--------------------------+ -| ``without_gene | `` | ``False`` | Ignoriert | -| rator_import`` | bool`` | | Generatoreinspeisung | -| | | | (nur für | -| | | | Planungsanalysen | -| | | | sinnvoll) | -+----------------+--------+----------------+--------------------------+ -| ` | `` | ``False`` | Berücksichtigt das | -| `n_minus_one`` | bool`` | | (n-1)-Kriterium | -+----------------+--------+----------------+--------------------------+ +.. list-table:: + :header-rows: 1 + :widths: 25 20 20 35 + + * - Parameter + - Typ + - Default + - Beschreibung + * - ``timesteps_pfa`` + - ``str | DatetimeIndex | None`` + - ``'snapshot_analysis'`` + - ``'snapshot_analysis'`` = 2 Worst-Case-Zeitschritte, ``DatetimeIndex`` = benutzerdefiniert, ``None`` = alle Zeitschritte + * - ``reduced_analysis`` + - ``bool`` + - ``False`` + - Nutzt nur die kritischsten Zeitschritte (höchste Überlast oder Spannungsabweichung) + * - ``max_while_iterations`` + - ``int`` + - ``20`` + - Maximale Anzahl der Verstärkungsiterationen + * - ``split_voltage_band`` + - ``bool`` + - ``True`` + - Getrennte Spannungsbänder für NS/MS (z.B. NS ±3 %, MS ±7 %) + * - ``mode`` + - ``str | None`` + - ``None`` + - Netzebene: ``'mv'``, ``'mvlv'``, ``'lv'`` oder ``None`` (= automatisch) + * - ``without_generator_import`` + - ``bool`` + - ``False`` + - Ignoriert Generatoreinspeisung (nur für Planungsanalysen sinnvoll) + * - ``n_minus_one`` + - ``bool`` + - ``False`` + - Berücksichtigt das (n-1)-Kriterium .. _zeitreihen-nutzung-1: @@ -1741,13 +1738,13 @@ Wann wird reinforce aufgerufen? # Szenario 2: Mit Optimierung aber ohne §14a edisgo_opt = edisgo.copy() edisgo_opt.pm_optimize(opf_version=2, curtailment_14a=False) - edisgo_opt.reinforce() + edisgo_opt.reinforce() costs_opt = edisgo_opt.results.grid_expansion_costs # Szenario 3: Mit Optimierung und §14a edisgo_14a = edisgo.copy() edisgo_14a.pm_optimize(opf_version=2, curtailment_14a=True) - edisgo_14a.reinforce() + edisgo_14a.reinforce() costs_14a = edisgo_14a.results.grid_expansion_costs # Vergleich @@ -1848,7 +1845,6 @@ Constraints ``p_hp14a[h,t] = 0`` - Wenn ``z_hp14a[h,t] = 1`` (Abregelung aktiv): ``0 ≤ p_hp14a[h,t] ≤ pmax[h]`` -**Zweck:** Verhindert “Teilabregelung” ohne binäre Aktivierung 2. Mindest-Nettolast (Minimum Net Load) ''''''''''''''''''''''''''''''''''''''' @@ -2896,13 +2892,14 @@ Python-Dateien | OPF** | o/opf/powermo | JSON-Kommunikation | | | dels_opf.py`` | | +-----------------+---------------+------------------------------------+ -| **Reinforce | ``edisgo/fl | Verstärkungsalgorithmus | -| I | ex_opt/reinfo | | -| mplementation** | rce_grid.py`` | | +| **Reinforce | ``edisgo/ | Verstärkungsalgorithmus | +| I | flex_opt/ | | +| mplementation** | reinforce_ | | +| | grid.py`` | | +-----------------+---------------+------------------------------------+ -| **Reinforce | `` | Leitungs-/Trafo-Verstärkung | -| Measures** | edisgo/flex_o | | -| | pt/reinforce_ | | +| **Reinforce | ``edisgo/ | Leitungs-/Trafo-Verstärkung | +| Measures** | flex_opt/ | | +| | reinforce_ | | | | measures.py`` | | +-----------------+---------------+------------------------------------+ | **Timeseries** | ``edisg | Zeitreihen-Verwaltung | @@ -3003,5 +3000,3 @@ Konfigurationsdateien | Data** | onfig/equipme | Daten | | | nt_data.csv`` | | +-----------------+---------------+------------------------------------+ - --------------- \ No newline at end of file