From 5d38f5917369a0ae28070909a303fd785e2b0dd4 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 25 May 2023 22:26:51 +0200 Subject: [PATCH 1/7] Add file with home storage operation strategy --- edisgo/flex_opt/battery_storage_operation.py | 147 +++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 edisgo/flex_opt/battery_storage_operation.py diff --git a/edisgo/flex_opt/battery_storage_operation.py b/edisgo/flex_opt/battery_storage_operation.py new file mode 100644 index 000000000..3630e26e5 --- /dev/null +++ b/edisgo/flex_opt/battery_storage_operation.py @@ -0,0 +1,147 @@ +from copy import deepcopy + +import pandas as pd + + +def battery_storage_reference_operation( + df, + init_storage_charge, + storage_max, + charger_power, + time_base, + efficiency_charge=0.9, + efficiency_discharge=0.9, +): + """ + Reference operation of storage system where it directly charges + Todo: Find original source + + Parameters + ----------- + df : :pandas:`pandas.DataFrame` + Timeseries of house demand - PV generation + init_storage_charge : float + Initial state of energy of storage device + storage_max : float + Maximum energy level of storage device + charger_power : float + Nominal charging power of storage device + time_base : float + Timestep of inserted timeseries + efficiency_charge: float + Efficiency of storage system in case of charging + efficiency_discharge: float + Efficiency of storage system in case of discharging + + Returns + --------- + :pandas:`pandas.DataFrame` + Dataframe with storage operation timeseries + + """ + # Battery model handles generation positive, demand negative + lst_storage_power = [] + lst_storage_charge = [] + storage_charge = init_storage_charge + + for i, d in df.iterrows(): + # If the house would feed electricity into the grid, charge the storage first. + # No electricity exchange with grid as long as charger power is not exceeded + if (d.house_demand > 0) & (storage_charge < storage_max): + # Check if energy produced exceeds charger power + if d.house_demand < charger_power: + storage_charge = storage_charge + ( + d.house_demand * efficiency_charge * time_base + ) + storage_power = -d.house_demand + # If it does, feed the rest to the grid + else: + storage_charge = storage_charge + ( + charger_power * efficiency_charge * time_base + ) + storage_power = -charger_power + + # If the storage would be overcharged, feed the 'rest' to the grid + if storage_charge > storage_max: + storage_power = storage_power + (storage_charge - storage_max) / ( + efficiency_charge * time_base + ) + storage_charge = storage_max + + # If the house needs electricity from the grid, discharge the storage first. + # In this case d.house_demand is negative! + # No electricity exchange with grid as long as demand does not exceed charger + # power + elif (d.house_demand < 0) & (storage_charge > 0): + # Check if energy demand exceeds charger power + if d.house_demand / efficiency_discharge < (charger_power * -1): + storage_charge = storage_charge - (charger_power * time_base) + storage_power = charger_power * efficiency_discharge + + else: + storage_charge = storage_charge + ( + d.house_demand / efficiency_discharge * time_base + ) + storage_power = -d.house_demand + + # If the storage would be undercharged, take the 'rest' from the grid + if storage_charge < 0: + # since storage_charge is negative in this case it can be taken as + # demand + storage_power = ( + storage_power + storage_charge * efficiency_discharge / time_base + ) + storage_charge = 0 + + # If the storage is full or empty, the demand is not affected + # elif(storage_charge == 0) | (storage_charge == storage_max): + else: + storage_power = 0 + lst_storage_power.append(storage_power) + lst_storage_charge.append(storage_charge) + df["storage_power"] = lst_storage_power + df["storage_charge"] = lst_storage_charge + + return df.round(6) + + +def create_storage_data(edisgo_obj): + storage_units = edisgo_obj.topology.storage_units_df + soc_df = pd.DataFrame(index=edisgo_obj.timeseries.timeindex) + # one storage per roof mounted solar generator + for row in storage_units.iterrows(): + building_id = row[1]["building_id"] + pv_gen = edisgo_obj.topology.generators_df.loc[ + edisgo_obj.topology.generators_df.building_id == building_id + ].index[0] + pv_feedin = edisgo_obj.timeseries.generators_active_power[pv_gen] + loads = edisgo_obj.topology.loads_df.loc[ + edisgo_obj.topology.loads_df.building_id == building_id + ].index + if len(loads) == 0: + pass + else: + house_demand = deepcopy( + edisgo_obj.timeseries.loads_active_power[loads].sum(axis=1) + ) + storage_ts = battery_storage_reference_operation( + pd.DataFrame(columns=["house_demand"], data=pv_feedin - house_demand), + 0, + row[1].p_nom, + row[1].p_nom, + 1, + ) + # Add storage ts to storage_units_active_power dataframe + edisgo_obj.set_time_series_manual( + storage_units_p=pd.DataFrame( + columns=[row[0]], + index=storage_ts.index, + data=storage_ts.storage_power.values, + ) + ) + + soc_df = pd.concat([soc_df, storage_ts.storage_charge], axis=1) + + soc_df.columns = edisgo_obj.topology.storage_units_df.index + edisgo_obj.overlying_grid.storage_units_soc = soc_df + edisgo_obj.set_time_series_reactive_power_control() From ffb87dd7e0d283cbc3a4a9ea5472596ae375a868 Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 30 Oct 2023 11:58:46 +0100 Subject: [PATCH 2/7] Update storage operation functions --- edisgo/flex_opt/battery_storage_operation.py | 222 ++++++++++++------- 1 file changed, 146 insertions(+), 76 deletions(-) diff --git a/edisgo/flex_opt/battery_storage_operation.py b/edisgo/flex_opt/battery_storage_operation.py index 3630e26e5..3cbcfc9a6 100644 --- a/edisgo/flex_opt/battery_storage_operation.py +++ b/edisgo/flex_opt/battery_storage_operation.py @@ -1,116 +1,149 @@ from copy import deepcopy +import logging import pandas as pd +logger = logging.getLogger(__name__) -def battery_storage_reference_operation( + +def reference_operation( df, - init_storage_charge, - storage_max, - charger_power, - time_base, + soe_init, + soe_max, + storage_p_nom, + freq, efficiency_charge=0.9, efficiency_discharge=0.9, ): """ - Reference operation of storage system where it directly charges - Todo: Find original source + Reference operation of storage system where it directly charges when PV feed-in is + higher than electricity demand of the building. + + Battery model handles generation positive, demand negative Parameters ----------- df : :pandas:`pandas.DataFrame` - Timeseries of house demand - PV generation - init_storage_charge : float - Initial state of energy of storage device - storage_max : float - Maximum energy level of storage device - charger_power : float - Nominal charging power of storage device - time_base : float - Timestep of inserted timeseries - efficiency_charge: float - Efficiency of storage system in case of charging - efficiency_discharge: float - Efficiency of storage system in case of discharging + Dataframe with time index and the buildings residual electricity demand + (PV generation minus electricity demand) in column "feedin_minus_demand". + soe_init : float + Initial state of energy of storage device in MWh. + soe_max : float + Maximum energy level of storage device in MWh. + storage_p_nom : float + Nominal charging power of storage device in MW. + freq : float + Frequency of provided time series. Set to one, in case of hourly time series or + 0.5 in case of half-hourly time series. + efficiency_charge : float + Efficiency of storage system in case of charging. + efficiency_discharge : float + Efficiency of storage system in case of discharging. Returns --------- :pandas:`pandas.DataFrame` - Dataframe with storage operation timeseries + Dataframe provided through parameter `df` extended by columns "storage_power", + holding the charging (negative values) and discharging (positive values) power + of the storage unit in MW, and "storage_soe" holding the storage unit's state of + energy in MWh. """ - # Battery model handles generation positive, demand negative lst_storage_power = [] - lst_storage_charge = [] - storage_charge = init_storage_charge + lst_storage_soe = [] + storage_soe = soe_init for i, d in df.iterrows(): # If the house would feed electricity into the grid, charge the storage first. - # No electricity exchange with grid as long as charger power is not exceeded - if (d.house_demand > 0) & (storage_charge < storage_max): + # No electricity exchange with grid as long as charger power is not exceeded. + if (d.feedin_minus_demand > 0) & (storage_soe < soe_max): # Check if energy produced exceeds charger power - if d.house_demand < charger_power: - storage_charge = storage_charge + ( - d.house_demand * efficiency_charge * time_base - ) - storage_power = -d.house_demand + if d.feedin_minus_demand < storage_p_nom: + storage_power = -d.feedin_minus_demand # If it does, feed the rest to the grid else: - storage_charge = storage_charge + ( - charger_power * efficiency_charge * time_base - ) - storage_power = -charger_power - - # If the storage would be overcharged, feed the 'rest' to the grid - if storage_charge > storage_max: - storage_power = storage_power + (storage_charge - storage_max) / ( - efficiency_charge * time_base + storage_power = -storage_p_nom + storage_soe = storage_soe + ( + -storage_power * efficiency_charge * freq + ) + # If the storage is overcharged, feed the 'rest' to the grid + if storage_soe > soe_max: + storage_power = storage_power + (storage_soe - soe_max) / ( + efficiency_charge * freq ) - storage_charge = storage_max + storage_soe = soe_max # If the house needs electricity from the grid, discharge the storage first. - # In this case d.house_demand is negative! - # No electricity exchange with grid as long as demand does not exceed charger + # In this case d.feedin_minus_demand is negative! + # No electricity exchange with grid as long as demand does not exceed charging # power - elif (d.house_demand < 0) & (storage_charge > 0): + elif (d.feedin_minus_demand < 0) & (storage_soe > 0): # Check if energy demand exceeds charger power - if d.house_demand / efficiency_discharge < (charger_power * -1): - storage_charge = storage_charge - (charger_power * time_base) - storage_power = charger_power * efficiency_discharge - + if d.feedin_minus_demand / efficiency_discharge < (storage_p_nom * -1): + storage_soe = storage_soe - (storage_p_nom * freq) + storage_power = storage_p_nom * efficiency_discharge else: - storage_charge = storage_charge + ( - d.house_demand / efficiency_discharge * time_base + storage_charge = storage_soe + ( + d.feedin_minus_demand / efficiency_discharge * freq ) - storage_power = -d.house_demand - - # If the storage would be undercharged, take the 'rest' from the grid - if storage_charge < 0: + storage_power = -d.feedin_minus_demand + # If the storage is undercharged, take the 'rest' from the grid + if storage_soe < 0: # since storage_charge is negative in this case it can be taken as # demand storage_power = ( - storage_power + storage_charge * efficiency_discharge / time_base + storage_power + storage_soe * efficiency_discharge / freq ) storage_charge = 0 # If the storage is full or empty, the demand is not affected - # elif(storage_charge == 0) | (storage_charge == storage_max): else: storage_power = 0 lst_storage_power.append(storage_power) - lst_storage_charge.append(storage_charge) + lst_storage_soe.append(storage_soe) + df["storage_power"] = lst_storage_power - df["storage_charge"] = lst_storage_charge + df["storage_soe"] = lst_storage_soe return df.round(6) -def create_storage_data(edisgo_obj): +def create_storage_data(edisgo_obj, soe_init=0.0, freq=1): + """ + Matches storage units to PV plants and building electricity demand using the + building ID and applies reference storage operation. + The storage units active power time series are written to timeseries.loads_active_power. + Reactive power is as well set with default values. + State of energy time series is returned. + + In case there is no electricity load, the storage operation is set to zero. + + Parameters + ---------- + edisgo_obj : :class:`~.EDisGo` + EDisGo object to obtain storage units and PV feed-in and electricity demand + in same building from. + soe_init : float + Initial state of energy of storage device in MWh. Default: 0 MWh. + freq : float + Frequency of provided time series. Set to one, in case of hourly time series or + 0.5 in case of half-hourly time series. Default: 1. + + Returns + -------- + :pandas:`pandas.DataFrame` + Dataframe with time index and state of energy in MWh of each storage in columns. + Column names correspond to storage name as in topology.storage_units_df. + + """ + # ToDo add automatic determination of freq + # ToDo allow setting efficiency through storage_units_df + # ToDo allow specifying storage units for which to apply reference strategy storage_units = edisgo_obj.topology.storage_units_df soc_df = pd.DataFrame(index=edisgo_obj.timeseries.timeindex) # one storage per roof mounted solar generator - for row in storage_units.iterrows(): - building_id = row[1]["building_id"] + for idx, row in storage_units.iterrows(): + building_id = row["building_id"] pv_gen = edisgo_obj.topology.generators_df.loc[ edisgo_obj.topology.generators_df.building_id == building_id ].index[0] @@ -119,29 +152,66 @@ def create_storage_data(edisgo_obj): edisgo_obj.topology.loads_df.building_id == building_id ].index if len(loads) == 0: - pass - else: - house_demand = deepcopy( - edisgo_obj.timeseries.loads_active_power[loads].sum(axis=1) + logger.info( + f"Storage unit {idx} in building {building_id} has not load. " + f"Storage operation is therefore set to zero." ) - storage_ts = battery_storage_reference_operation( - pd.DataFrame(columns=["house_demand"], data=pv_feedin - house_demand), - 0, - row[1].p_nom, - row[1].p_nom, - 1, + edisgo_obj.set_time_series_manual( + storage_units_p=pd.DataFrame( + columns=[idx], + index=soc_df.index, + data=0, + ) ) - # Add storage ts to storage_units_active_power dataframe + else: + house_demand = edisgo_obj.timeseries.loads_active_power[loads].sum(axis=1) + storage_ts = reference_operation( + df=pd.DataFrame(columns=["feedin_minus_demand"], data=pv_feedin - house_demand), + soe_init=soe_init, + soe_max=row.p_nom * row.max_hours, + storage_p_nom=row.p_nom, + freq=freq, + ) + # import matplotlib + # from matplotlib import pyplot as plt + # matplotlib.use('TkAgg', force=True) + # storage_ts.plot() + # plt.show() + # Add storage time series to storage_units_active_power dataframe edisgo_obj.set_time_series_manual( storage_units_p=pd.DataFrame( - columns=[row[0]], + columns=[idx], index=storage_ts.index, data=storage_ts.storage_power.values, ) ) - - soc_df = pd.concat([soc_df, storage_ts.storage_charge], axis=1) + soc_df = pd.concat([soc_df, storage_ts.storage_soe], axis=1) soc_df.columns = edisgo_obj.topology.storage_units_df.index - edisgo_obj.overlying_grid.storage_units_soc = soc_df edisgo_obj.set_time_series_reactive_power_control() + return soc_df + + +if __name__ == "__main__": + import os + from edisgo.edisgo import import_edisgo_from_files + + mv_grid = 33128 + results_dir_base = "/home/birgit/virtualenvs/wp_flex/git_repos/394_wp_flex/results" + results_dir = os.path.join(results_dir_base, str(mv_grid)) + + zip_name = f"grid_data_wp_flex_No-flex.zip" + grid_path = os.path.join(results_dir, zip_name) + edisgo_grid = import_edisgo_from_files( + edisgo_path=grid_path, + import_topology=True, + import_timeseries=True, + import_results=False, + import_electromobility=False, + import_heat_pump=False, + import_dsm=False, + import_overlying_grid=False, + from_zip_archive=True, + ) + edisgo_grid.legacy_grids = False + create_storage_data(edisgo_obj=edisgo_grid) \ No newline at end of file From 444cb010d49f78d1e5ce23efbb8a286801bc08c5 Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 30 Oct 2023 11:58:59 +0100 Subject: [PATCH 3/7] Restrict sci-kit to fix failing tests --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 72bbdfbe1..680d01cd5 100644 --- a/setup.py +++ b/setup.py @@ -55,7 +55,7 @@ def read(fname): "pypsa >= 0.17.0, <= 0.20.1", "pyyaml", "saio", - "scikit-learn", + "scikit-learn <= 1.1.1", "shapely >= 1.7.0", "sqlalchemy < 1.4.0", "sshtunnel", From d2093ce4d5eb458b0aea5dfca9588a3b6c67b980 Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 30 Oct 2023 15:11:12 +0100 Subject: [PATCH 4/7] Add build information to rtd yml --- .readthedocs.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.readthedocs.yml b/.readthedocs.yml index 86c001e07..7a2ef9c2e 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -21,3 +21,9 @@ python: version: "3.8" install: - requirements: rtd_requirements.txt + +# Set the version of Python +build: + os: ubuntu-20.04 + tools: + python: "3.10" \ No newline at end of file From a42c685de3b4fb558a8f77a119f4aae0af313ab2 Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 30 Oct 2023 15:11:44 +0100 Subject: [PATCH 5/7] Bug fix beautiful soup not needed anymore as github returns json instead of html --- examples/electromobility_example.ipynb | 22 +++++++++++----------- rtd_requirements.txt | 1 - setup.py | 1 - 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/examples/electromobility_example.ipynb b/examples/electromobility_example.ipynb index c2170bf82..70738b6ab 100644 --- a/examples/electromobility_example.ipynb +++ b/examples/electromobility_example.ipynb @@ -43,7 +43,7 @@ "outputs": [], "source": [ "import os\n", - "\n", + "import json\n", "import geopandas as gpd\n", "import pandas as pd\n", "import requests\n", @@ -53,7 +53,6 @@ "\n", "from copy import deepcopy\n", "from pathlib import Path\n", - "from bs4 import BeautifulSoup\n", "\n", "from edisgo.edisgo import EDisGo\n", "from edisgo.tools.logger import setup_logger\n", @@ -415,11 +414,6 @@ "source": [ "# Download SimBEV data\n", "\n", - "def listFD(url, ext=\"\"):\n", - " page = requests.get(url).text\n", - " soup = BeautifulSoup(page, \"html.parser\")\n", - " return [node.get(\"href\").split(\"/\")[-1] for node in soup.find_all(\"a\") if node.get(\"href\").endswith(ext)]\n", - "\n", "def download_simbev_example_data():\n", "\n", " raw_url = (\"https://raw.githubusercontent.com/openego/eDisGo/dev/\" +\n", @@ -435,7 +429,9 @@ " # download files\n", " url = (f\"https://github.com/openego/eDisGo/tree/dev/\" +\n", " f\"tests/data/simbev_example_scenario/{ags}/\")\n", - " filenames = [f for f in listFD(url, \"csv\")]\n", + " page = requests.get(url).text\n", + " items = json.loads(page)[\"payload\"][\"tree\"][\"items\"]\n", + " filenames = [f[\"name\"] for f in items if \"csv\" in f[\"name\"]]\n", "\n", " for file in filenames:\n", " req = requests.get(f\"{raw_url}/{ags}/{file}\")\n", @@ -473,7 +469,9 @@ " # download files\n", " url = (\"https://github.com/openego/eDisGo/tree/dev/\" +\n", " \"tests/data/tracbev_example_scenario/\")\n", - " filenames = [f for f in listFD(url, \"gpkg\")]\n", + " page = requests.get(url).text\n", + " items = json.loads(page)[\"payload\"][\"tree\"][\"items\"]\n", + " filenames = [f[\"name\"] for f in items if \"gpkg\" in f[\"name\"]]\n", "\n", " for file in filenames:\n", " req = requests.get(\n", @@ -493,7 +491,9 @@ "cell_type": "code", "execution_count": null, "id": "1d65e6d6", - "metadata": {}, + "metadata": { + "scrolled": true + }, "outputs": [], "source": [ "edisgo.import_electromobility(\n", @@ -776,7 +776,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.5" + "version": "3.8.18" }, "toc": { "base_numbering": 1, diff --git a/rtd_requirements.txt b/rtd_requirements.txt index 966400840..560dc8213 100644 --- a/rtd_requirements.txt +++ b/rtd_requirements.txt @@ -1,4 +1,3 @@ -beautifulsoup4 dash < 2.9.0 demandlib docutils == 0.16.0 diff --git a/setup.py b/setup.py index 680d01cd5..967ce07dd 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,6 @@ def read(fname): requirements = [ - "beautifulsoup4", "contextily", "dash < 2.9.0", "demandlib", From 8774ce529843cf11f071337e594109e25c452f57 Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 30 Oct 2023 15:15:03 +0100 Subject: [PATCH 6/7] Fix rtd configs --- .readthedocs.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 7a2ef9c2e..f6d04600f 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -18,12 +18,11 @@ formats: all # Optionally set the version of Python and requirements required to build your docs python: - version: "3.8" install: - requirements: rtd_requirements.txt # Set the version of Python build: - os: ubuntu-20.04 + os: ubuntu-22.04 tools: - python: "3.10" \ No newline at end of file + python: "3.8" \ No newline at end of file From 8f7536852b1f641ec2b5e2711309888000d909dc Mon Sep 17 00:00:00 2001 From: birgits Date: Mon, 30 Oct 2023 15:34:17 +0100 Subject: [PATCH 7/7] Bug fix autoapi fails for versions lower 3.0.0 --- rtd_requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rtd_requirements.txt b/rtd_requirements.txt index 560dc8213..7592090d5 100644 --- a/rtd_requirements.txt +++ b/rtd_requirements.txt @@ -19,7 +19,7 @@ scikit-learn sphinx >= 4.3.0, < 5.1.0 sphinx_rtd_theme >=0.5.2 sphinx-autodoc-typehints -sphinx-autoapi +sphinx-autoapi >= 3.0.0 sshtunnel urllib3 < 2.0.0 workalendar diff --git a/setup.py b/setup.py index 967ce07dd..9c97a90f8 100644 --- a/setup.py +++ b/setup.py @@ -74,7 +74,7 @@ def read(fname): "sphinx >= 4.3.0, < 5.1.0", "sphinx_rtd_theme >=0.5.2", "sphinx-autodoc-typehints", - "sphinx-autoapi", + "sphinx-autoapi >= 3.0.0", ] extras = {"dev": dev_requirements}