From 5d38f5917369a0ae28070909a303fd785e2b0dd4 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 25 May 2023 22:26:51 +0200 Subject: [PATCH 01/19] 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 00000000..3630e26e --- /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 02/19] 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 3630e26e..3cbcfc9a 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 03/19] 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 72bbdfbe..680d01cd 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 04/19] Add build information to rtd yml --- .readthedocs.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.readthedocs.yml b/.readthedocs.yml index 86c001e0..7a2ef9c2 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 05/19] 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 c2170bf8..70738b6a 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 96640084..560dc821 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 680d01cd..967ce07d 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 06/19] Fix rtd configs --- .readthedocs.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 7a2ef9c2..f6d04600 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 07/19] 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 560dc821..7592090d 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 967ce07d..9c97a90f 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} From 8f81acab1d3381b9177b60235c95b4a8154afdc4 Mon Sep 17 00:00:00 2001 From: birgits Date: Tue, 31 Oct 2023 17:02:48 +0100 Subject: [PATCH 08/19] Bug fix use float and forgotten parameter names changes --- edisgo/flex_opt/battery_storage_operation.py | 53 ++++++-------------- 1 file changed, 14 insertions(+), 39 deletions(-) diff --git a/edisgo/flex_opt/battery_storage_operation.py b/edisgo/flex_opt/battery_storage_operation.py index 3cbcfc9a..4f6498fd 100644 --- a/edisgo/flex_opt/battery_storage_operation.py +++ b/edisgo/flex_opt/battery_storage_operation.py @@ -1,4 +1,3 @@ -from copy import deepcopy import logging import pandas as pd @@ -56,16 +55,14 @@ def reference_operation( 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.feedin_minus_demand > 0) & (storage_soe < soe_max): + if (d.feedin_minus_demand > 0.0) & (storage_soe < soe_max): # Check if energy produced exceeds charger power 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_power = -storage_p_nom - storage_soe = storage_soe + ( - -storage_power * efficiency_charge * freq - ) + 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) / ( @@ -77,28 +74,28 @@ def reference_operation( # 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.feedin_minus_demand < 0) & (storage_soe > 0): + elif (d.feedin_minus_demand < 0.0) & (storage_soe > 0.0): # Check if energy demand exceeds charger power 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_soe + ( + storage_soe = storage_soe + ( d.feedin_minus_demand / efficiency_discharge * freq ) 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 + if storage_soe < 0.0: + # since storage_soe is negative in this case it can be taken as # demand storage_power = ( storage_power + storage_soe * efficiency_discharge / freq ) - storage_charge = 0 + storage_soe = 0.0 # If the storage is full or empty, the demand is not affected else: - storage_power = 0 + storage_power = 0.0 lst_storage_power.append(storage_power) lst_storage_soe.append(storage_soe) @@ -112,7 +109,8 @@ 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. + 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. @@ -160,13 +158,15 @@ def create_storage_data(edisgo_obj, soe_init=0.0, freq=1): storage_units_p=pd.DataFrame( columns=[idx], index=soc_df.index, - data=0, + data=0.0, ) ) 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), + 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, @@ -190,28 +190,3 @@ def create_storage_data(edisgo_obj, soe_init=0.0, freq=1): soc_df.columns = edisgo_obj.topology.storage_units_df.index 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 24338273f9dd4bd1f5fb63ed58120a0d740944a3 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 7 Dec 2023 17:26:58 -0800 Subject: [PATCH 09/19] Bug fix nan values as string cannot be detected by isna() --- edisgo/tools/tools.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/edisgo/tools/tools.py b/edisgo/tools/tools.py index fccbf019..10035b16 100644 --- a/edisgo/tools/tools.py +++ b/edisgo/tools/tools.py @@ -319,6 +319,11 @@ def _assign_to_lines(lines): lambda _: edisgo_obj.topology.buses_df.at[_.bus1, mode], axis=1 ) + # assign np.nan values to new columns, so that missing values can be found through + # isna() + edisgo_obj.topology.lines_df[mode] = np.nan + edisgo_obj.topology.buses_df[mode] = np.nan + if mode == "mv_feeder": graph = edisgo_obj.topology.mv_grid.graph station = edisgo_obj.topology.mv_grid.station.index[0] From a860ee17d92e1c8776adea8be16178755599aeca Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 7 Dec 2023 17:27:19 -0800 Subject: [PATCH 10/19] Version conflicts - try without version limits --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 9c97a90f..e28c1469 100644 --- a/setup.py +++ b/setup.py @@ -71,10 +71,10 @@ def read(fname): "pytest", "pytest-notebook", "pyupgrade", - "sphinx >= 4.3.0, < 5.1.0", + "sphinx", "sphinx_rtd_theme >=0.5.2", "sphinx-autodoc-typehints", - "sphinx-autoapi >= 3.0.0", + "sphinx-autoapi", ] extras = {"dev": dev_requirements} From 75bafd7a0265f72ca4f443afbd1334dec099d383 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 7 Dec 2023 17:29:24 -0800 Subject: [PATCH 11/19] Version conflicts - try removing limits --- rtd_requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rtd_requirements.txt b/rtd_requirements.txt index 7592090d..62493181 100644 --- a/rtd_requirements.txt +++ b/rtd_requirements.txt @@ -16,10 +16,10 @@ pypsa >=0.17.0, <=0.20.1 pyyaml saio scikit-learn -sphinx >= 4.3.0, < 5.1.0 +sphinx sphinx_rtd_theme >=0.5.2 sphinx-autodoc-typehints -sphinx-autoapi >= 3.0.0 +sphinx-autoapi sshtunnel urllib3 < 2.0.0 workalendar From 82f4b2816e7c7f4471ac29a495f46ed9f4be4742 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 7 Dec 2023 17:36:35 -0800 Subject: [PATCH 12/19] Tru sphinx with python 3.9 --- .readthedocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index f6d04600..2702d5ab 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -25,4 +25,4 @@ python: build: os: ubuntu-22.04 tools: - python: "3.8" \ No newline at end of file + python: "3.9" \ No newline at end of file From 2b202507aae8aba3f5414b33617401754174e52a Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 7 Dec 2023 18:03:00 -0800 Subject: [PATCH 13/19] Try sphinx autoapi 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 62493181..9022feb7 100644 --- a/rtd_requirements.txt +++ b/rtd_requirements.txt @@ -19,7 +19,7 @@ scikit-learn sphinx 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 e28c1469..fd29db75 100644 --- a/setup.py +++ b/setup.py @@ -74,7 +74,7 @@ def read(fname): "sphinx", "sphinx_rtd_theme >=0.5.2", "sphinx-autodoc-typehints", - "sphinx-autoapi", + "sphinx-autoapi == 3.0.0", ] extras = {"dev": dev_requirements} From f31f15da779215db316bd4e930feeb603cb42aa2 Mon Sep 17 00:00:00 2001 From: birgits Date: Thu, 7 Dec 2023 18:06:35 -0800 Subject: [PATCH 14/19] Try taking out docutils --- rtd_requirements.txt | 3 +-- setup.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/rtd_requirements.txt b/rtd_requirements.txt index 9022feb7..dd3c393d 100644 --- a/rtd_requirements.txt +++ b/rtd_requirements.txt @@ -1,6 +1,5 @@ dash < 2.9.0 demandlib -docutils == 0.16.0 egoio >= 0.4.7 geopy >= 2.0.0 jupyter_dash @@ -19,7 +18,7 @@ scikit-learn sphinx sphinx_rtd_theme >=0.5.2 sphinx-autodoc-typehints -sphinx-autoapi == 3.0.0 +sphinx-autoapi sshtunnel urllib3 < 2.0.0 workalendar diff --git a/setup.py b/setup.py index fd29db75..e28c1469 100644 --- a/setup.py +++ b/setup.py @@ -74,7 +74,7 @@ def read(fname): "sphinx", "sphinx_rtd_theme >=0.5.2", "sphinx-autodoc-typehints", - "sphinx-autoapi == 3.0.0", + "sphinx-autoapi", ] extras = {"dev": dev_requirements} From fac800008bafa888e99b0ed6e65298f6a483bab1 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 15 Dec 2023 17:02:30 -0800 Subject: [PATCH 15/19] Clean up functions --- edisgo/flex_opt/battery_storage_operation.py | 175 +++++++++++++------ 1 file changed, 117 insertions(+), 58 deletions(-) diff --git a/edisgo/flex_opt/battery_storage_operation.py b/edisgo/flex_opt/battery_storage_operation.py index 4f6498fd..c9399430 100644 --- a/edisgo/flex_opt/battery_storage_operation.py +++ b/edisgo/flex_opt/battery_storage_operation.py @@ -1,24 +1,25 @@ import logging +import math import pandas as pd logger = logging.getLogger(__name__) -def reference_operation( +def _reference_operation( df, soe_init, soe_max, storage_p_nom, freq, - efficiency_charge=0.9, - efficiency_discharge=0.9, + efficiency_store, + efficiency_dispatch, ): """ - Reference operation of storage system where it directly charges when PV feed-in is - higher than electricity demand of the building. + Reference operation of storage system where it is directly charged when PV feed-in + is higher than electricity demand of the building. - Battery model handles generation positive, demand negative + Battery model handles generation positive, demand negative. Parameters ----------- @@ -34,9 +35,9 @@ def reference_operation( 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_store : float Efficiency of storage system in case of charging. - efficiency_discharge : float + efficiency_dispatch : float Efficiency of storage system in case of discharging. Returns @@ -53,7 +54,7 @@ def reference_operation( storage_soe = soe_init for i, d in df.iterrows(): - # If the house would feed electricity into the grid, charge the storage first. + # If the house were to feed electricity into the grid, charge the storage first. # No electricity exchange with grid as long as charger power is not exceeded. if (d.feedin_minus_demand > 0.0) & (storage_soe < soe_max): # Check if energy produced exceeds charger power @@ -62,11 +63,11 @@ def reference_operation( # If it does, feed the rest to the grid else: storage_power = -storage_p_nom - storage_soe = storage_soe + (-storage_power * efficiency_charge * freq) + storage_soe = storage_soe + (-storage_power * efficiency_store * 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 + efficiency_store * freq ) storage_soe = soe_max @@ -76,12 +77,12 @@ def reference_operation( # power elif (d.feedin_minus_demand < 0.0) & (storage_soe > 0.0): # Check if energy demand exceeds charger power - if d.feedin_minus_demand / efficiency_discharge < (storage_p_nom * -1): + if d.feedin_minus_demand / efficiency_dispatch < (storage_p_nom * -1): storage_soe = storage_soe - (storage_p_nom * freq) - storage_power = storage_p_nom * efficiency_discharge + storage_power = storage_p_nom * efficiency_dispatch else: storage_soe = storage_soe + ( - d.feedin_minus_demand / efficiency_discharge * freq + d.feedin_minus_demand / efficiency_dispatch * freq ) storage_power = -d.feedin_minus_demand # If the storage is undercharged, take the 'rest' from the grid @@ -89,7 +90,7 @@ def reference_operation( # since storage_soe is negative in this case it can be taken as # demand storage_power = ( - storage_power + storage_soe * efficiency_discharge / freq + storage_power + storage_soe * efficiency_dispatch / freq ) storage_soe = 0.0 @@ -105,22 +106,40 @@ def reference_operation( return df.round(6) -def create_storage_data(edisgo_obj, soe_init=0.0, freq=1): +def apply_reference_operation(edisgo_obj, storage_units_names=None, 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. + Applies reference storage operation to specified home storage units. + + In the reference storage operation, the home storage system is directly charged when + PV feed-in is higher than electricity demand of the building until the storage + is fully charged. The storage is directly discharged, in case electricity demand + of the building is higher than the PV feed-in, until it is fully discharged. + The battery model handles generation positive and demand negative. + + To determine the PV feed-in and electricity demand of the building that the home + storage is located in (including demand from heat pumps + and electric vehicles), this function matches the storage units to PV plants and + building electricity demand using the building ID. + In case there is no electricity load or no PV system, the storage operation is set + to zero. + + The resulting storage units' active power time series are written to + :attr:`~.network.timeseries.TimeSeries.loads_active_power`. + Further, reactive power time series are set up using function + :attr:`~.edisgo.EDisGo.set_time_series_reactive_power_control` with default values. + The state of energy time series that are calculated within this function are not + written anywhere, but are returned by this function. Parameters ---------- edisgo_obj : :class:`~.EDisGo` EDisGo object to obtain storage units and PV feed-in and electricity demand in same building from. + storage_units_names : list(str) or None + Names of storage units as in + :attr:`~.network.topology.Topology.storage_units_df` to set time for. If None, + time series are set for all storage units in + :attr:`~.network.topology.Topology.storage_units_df`. soe_init : float Initial state of energy of storage device in MWh. Default: 0 MWh. freq : float @@ -131,62 +150,102 @@ def create_storage_data(edisgo_obj, soe_init=0.0, freq=1): -------- :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. + Column names correspond to storage name as in + :attr:`~.network.topology.Topology.storage_units_df`. + + Notes + ------ + This function requires that the storage parameters `building_id`, + `efficiency_store`, `efficiency_dispatch` and `max_hours` are set in + :attr:`~.network.topology.Topology.storage_units_df` for all storage units + specified in parameter `storage_units_names`. """ - # 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 idx, row in storage_units.iterrows(): - building_id = row["building_id"] - pv_gen = edisgo_obj.topology.generators_df.loc[ + if storage_units_names is None: + storage_units_names = edisgo_obj.topology.storage_units_df.index + + storage_units = edisgo_obj.topology.storage_units_df.loc[storage_units_names] + soe_df = pd.DataFrame(index=edisgo_obj.timeseries.timeindex) + + for stor_name, stor_data in storage_units.iterrows(): + # get corresponding PV systems and electric loads + building_id = stor_data["building_id"] + pv_gens = 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] + ].index loads = edisgo_obj.topology.loads_df.loc[ edisgo_obj.topology.loads_df.building_id == building_id ].index - if len(loads) == 0: - logger.info( - f"Storage unit {idx} in building {building_id} has not load. " - f"Storage operation is therefore set to zero." - ) + if len(loads) == 0 or len(pv_gens) == 0: + if len(loads) == 0: + logger.warning( + f"Storage unit {stor_name} in building {building_id} has no load. " + f"Storage operation is therefore set to zero." + ) + if len(pv_gens) == 0: + logger.warning( + f"Storage unit {stor_name} in building {building_id} has no PV " + f"system. Storage operation is therefore set to zero." + ) edisgo_obj.set_time_series_manual( storage_units_p=pd.DataFrame( - columns=[idx], - index=soc_df.index, + columns=[stor_name], + index=soe_df.index, data=0.0, ) ) else: + # check storage values + if math.isnan(stor_data.max_hours) is True: + raise ValueError( + f"Parameter max_hours for storage unit {stor_name} is not a " + f"number. It needs to be set in Topology.storage_units_df." + ) + if math.isnan(stor_data.efficiency_store) is True: + raise ValueError( + f"Parameter efficiency_store for storage unit {stor_name} is not a " + f"number. It needs to be set in Topology.storage_units_df." + ) + if math.isnan(stor_data.efficiency_dispatch) is True: + raise ValueError( + f"Parameter efficiency_dispatch for storage unit {stor_name} is " + f"not a number. It needs to be set in Topology.storage_units_df." + ) + pv_feedin = edisgo_obj.timeseries.generators_active_power[pv_gens].sum(axis=1) house_demand = edisgo_obj.timeseries.loads_active_power[loads].sum(axis=1) - storage_ts = reference_operation( + # apply operation strategy + 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, + soe_max=stor_data.p_nom * stor_data.max_hours, + storage_p_nom=stor_data.p_nom, freq=freq, + efficiency_store=stor_data.efficiency_store, + efficiency_dispatch=stor_data.efficiency_dispatch, ) - # 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 + # add storage time series to storage_units_active_power dataframe edisgo_obj.set_time_series_manual( storage_units_p=pd.DataFrame( - columns=[idx], + columns=[stor_name], index=storage_ts.index, data=storage_ts.storage_power.values, ) ) - soc_df = pd.concat([soc_df, storage_ts.storage_soe], axis=1) - - soc_df.columns = edisgo_obj.topology.storage_units_df.index - edisgo_obj.set_time_series_reactive_power_control() - return soc_df + soe_df = pd.concat([soe_df, storage_ts.storage_soe.to_frame(stor_name)], axis=1) + + edisgo_obj.set_time_series_reactive_power_control( + generators_parametrisation=None, + loads_parametrisation=None, + storage_units_parametrisation=pd.DataFrame( + { + "components": [storage_units_names], + "mode": ["default"], + "power_factor": ["default"], + }, + index=[1], + ), + ) + + return soe_df From 661ac4be6a254e83b8cec7fbbbf542d1e3f3a876 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 15 Dec 2023 17:02:41 -0800 Subject: [PATCH 16/19] Add storage parameters --- edisgo/network/topology.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/edisgo/network/topology.py b/edisgo/network/topology.py index d8db32e9..1805cf60 100755 --- a/edisgo/network/topology.py +++ b/edisgo/network/topology.py @@ -51,7 +51,7 @@ "subtype", "source_id", ], - "storage_units_df": ["bus", "control", "p_nom", "max_hours"], + "storage_units_df": ["bus", "control", "p_nom", "max_hours", "efficiency_store", "efficiency_dispatch"], "transformers_df": ["bus0", "bus1", "x_pu", "r_pu", "s_nom", "type_info"], "lines_df": [ "bus0", @@ -368,6 +368,14 @@ def storage_units_df(self): Maximum state of charge capacity in terms of hours at full output capacity p_nom. + efficiency_store : float + Efficiency of storage system in case of charging. So far only used in + :func:`~.edisgo.flex_opt.battery_storage_operation.apply_reference_operation.` + + efficiency_dispatch : float + Efficiency of storage system in case of discharging. So far only used in + :func:`~.edisgo.flex_opt.battery_storage_operation.apply_reference_operation.` + Returns -------- :pandas:`pandas.DataFrame` From 4198082bd45a746a2964ca98156c05cfce4f7d32 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 15 Dec 2023 17:03:52 -0800 Subject: [PATCH 17/19] Add tests --- .../test_battery_storage_operation.py | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 tests/flex_opt/test_battery_storage_operation.py diff --git a/tests/flex_opt/test_battery_storage_operation.py b/tests/flex_opt/test_battery_storage_operation.py new file mode 100644 index 00000000..2268d7bc --- /dev/null +++ b/tests/flex_opt/test_battery_storage_operation.py @@ -0,0 +1,218 @@ +import numpy as np +import pandas as pd +import pytest + +from edisgo import EDisGo +from edisgo.flex_opt.battery_storage_operation import apply_reference_operation + + +class TestStorageOperation: + @classmethod + def setup_class(self): + self.timeindex = pd.date_range("1/1/2011 12:00", periods=5, freq="H") + self.edisgo = EDisGo( + ding0_grid=pytest.ding0_test_network_path, timeindex=self.timeindex + ) + self.edisgo.topology.storage_units_df = pd.DataFrame( + data={ + "bus": [ + "Bus_BranchTee_LVGrid_2_4", + "Bus_BranchTee_LVGrid_2_4", + "Bus_BranchTee_LVGrid_2_4", + "Bus_BranchTee_LVGrid_2_4", + "Bus_BranchTee_LVGrid_2_4", + ], + "control": ["PQ", "PQ", "PQ", "PQ", "PQ"], + "p_nom": [0.2, 2.0, 0.4, 0.5, 0.6], + "max_hours": [6, 6, 1, 6, 6], + "efficiency_store": [0.9, 1.0, 0.9, 1.0, 0.8], + "efficiency_dispatch": [0.9, 1.0, 0.9, 1.0, 0.8], + "building_id": [1, 2, 3, 4, 5], + }, + index=["stor1", "stor2", "stor3", "stor4", "stor5"], + ) + # set building IDs + self.edisgo.topology.loads_df.at[ + "Load_residential_LVGrid_8_2", "building_id" + ] = 2 + self.edisgo.topology.loads_df.at[ + "Load_residential_LVGrid_8_3", "building_id" + ] = 2 + self.edisgo.topology.generators_df.at[ + "GeneratorFluctuating_25", "building_id" + ] = 2 + self.edisgo.topology.generators_df.at[ + "GeneratorFluctuating_26", "building_id" + ] = 2 + self.edisgo.topology.loads_df.at[ + "Load_residential_LVGrid_3_2", "building_id" + ] = 3.0 + self.edisgo.topology.generators_df.at[ + "GeneratorFluctuating_17", "building_id" + ] = 3.0 + self.edisgo.topology.loads_df.at[ + "Load_residential_LVGrid_1_6", "building_id" + ] = 4 + self.edisgo.topology.loads_df.at[ + "Load_residential_LVGrid_1_4", "building_id" + ] = 5.0 + self.edisgo.topology.generators_df.at[ + "GeneratorFluctuating_27", "building_id" + ] = 5.0 + # set time series + self.edisgo.timeseries.loads_active_power = pd.DataFrame( + data={ + "Load_residential_LVGrid_8_2": [0.5, 1.0, 1.5, 0.0, 0.5], + "Load_residential_LVGrid_8_3": [0.5, 1.0, 1.5, 0.0, 0.5], + "Load_residential_LVGrid_3_2": [0.5, 0.0, 1.0, 0.5, 0.5], + "Load_residential_LVGrid_1_4": [0.0, 1.0, 1.5, 0.0, 0.5], + }, + index=self.timeindex, + ) + self.edisgo.timeseries.generators_active_power = pd.DataFrame( + data={ + "GeneratorFluctuating_25": [1.5, 3.0, 4.5, 0.0, 0.0], + "GeneratorFluctuating_26": [0.5, 1.0, 1.5, 0.0, 0.5], + "GeneratorFluctuating_17": [0.0, 1.0, 1.5, 1.0, 0.0], + "GeneratorFluctuating_27": [0.5, 0.0, 0.5, 0.0, 0.0], + }, + index=self.timeindex, + ) + + def test_operating_strategy(self): + # test without load (stor1) + # test with several loads and several PV systems (stor2) + # test with one load and one PV system (stor3) + # test without PV system (stor4) + # test with one value not numeric (stor5) + + # test with providing storage name + apply_reference_operation(edisgo_obj=self.edisgo, storage_units_names=["stor1"]) + + check_ts = pd.DataFrame( + data={ + "stor1": [0.0, 0.0, 0.0, 0.0, 0.0], + }, + index=self.timeindex, + ) + pd.testing.assert_frame_equal( + self.edisgo.timeseries.storage_units_active_power, + check_ts, + ) + pd.testing.assert_frame_equal( + self.edisgo.timeseries.storage_units_reactive_power, + check_ts, + ) + + # test without providing storage names + soe_df = apply_reference_operation(edisgo_obj=self.edisgo) + + assert soe_df.shape == (5, 3) + assert self.edisgo.timeseries.storage_units_active_power.shape == (5, 5) + assert self.edisgo.timeseries.storage_units_reactive_power.shape == (5, 5) + + # check stor2 + s = "stor2" + check_ts = pd.DataFrame( + data={ + s: [-1.0, -2.0, -2.0, 0.0, 0.5], + }, + index=self.timeindex, + ) + pd.testing.assert_frame_equal( + self.edisgo.timeseries.storage_units_active_power.loc[:, [s]], + check_ts, + ) + pd.testing.assert_frame_equal( + self.edisgo.timeseries.storage_units_reactive_power.loc[:, [s]], + check_ts * -np.tan(np.arccos(0.95)), + ) + check_ts = pd.DataFrame( + data={ + s: [1.0, 3.0, 5.0, 5.0, 4.5], + }, + index=self.timeindex, + ) + pd.testing.assert_frame_equal( + soe_df.loc[:, [s]], + check_ts, + ) + + # check stor3 + s = "stor3" + check_ts = pd.DataFrame( + data={ + s: [0.0, -0.4, -0.044444, 0.0, 0.36], + }, + index=self.timeindex, + ) + pd.testing.assert_frame_equal( + self.edisgo.timeseries.storage_units_active_power.loc[:, [s]], + check_ts, + ) + pd.testing.assert_frame_equal( + self.edisgo.timeseries.storage_units_reactive_power.loc[:, [s]], + check_ts * -np.tan(np.arccos(0.95)), + ) + check_ts = pd.DataFrame( + data={ + s: [0.0, 0.36, 0.4, 0.4, 0.0], + }, + index=self.timeindex, + ) + pd.testing.assert_frame_equal( + soe_df.loc[:, [s]], + check_ts, + ) + + # check stor4 - all zeros + s = "stor4" + check_ts = pd.DataFrame( + data={ + s: [0.0, 0.0, 0.0, 0.0, 0.0], + }, + index=self.timeindex, + ) + pd.testing.assert_frame_equal( + self.edisgo.timeseries.storage_units_active_power.loc[:, [s]], + check_ts, + ) + pd.testing.assert_frame_equal( + self.edisgo.timeseries.storage_units_reactive_power.loc[:, [s]], + check_ts, + ) + # check stor5 + s = "stor5" + check_ts = pd.DataFrame( + data={ + s: [-0.5, 0.32, 0.0, 0.0, 0.0], + }, + index=self.timeindex, + ) + pd.testing.assert_frame_equal( + self.edisgo.timeseries.storage_units_active_power.loc[:, [s]], + check_ts, + ) + pd.testing.assert_frame_equal( + self.edisgo.timeseries.storage_units_reactive_power.loc[:, [s]], + check_ts * -np.tan(np.arccos(0.95)), + ) + check_ts = pd.DataFrame( + data={ + s: [0.4, 0.0, 0.0, 0.0, 0.0], + }, + index=self.timeindex, + ) + pd.testing.assert_frame_equal( + soe_df.loc[:, [s]], + check_ts, + ) + + # test error raising + self.edisgo.topology.storage_units_df.at["stor5", "max_hours"] = np.nan + msg = ( + "Parameter max_hours for storage unit stor5 is not a number. It needs " + "to be set in Topology.storage_units_df." + ) + with pytest.raises(ValueError, match=msg): + apply_reference_operation(self.edisgo) From 18118019f91378658644983093bba989022ab5ad Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 15 Dec 2023 17:08:47 -0800 Subject: [PATCH 18/19] Pre-commit hooks --- edisgo/flex_opt/battery_storage_operation.py | 16 ++++++++++------ edisgo/network/topology.py | 18 ++++++++---------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/edisgo/flex_opt/battery_storage_operation.py b/edisgo/flex_opt/battery_storage_operation.py index c9399430..64447a8f 100644 --- a/edisgo/flex_opt/battery_storage_operation.py +++ b/edisgo/flex_opt/battery_storage_operation.py @@ -89,9 +89,7 @@ def _reference_operation( if storage_soe < 0.0: # since storage_soe is negative in this case it can be taken as # demand - storage_power = ( - storage_power + storage_soe * efficiency_dispatch / freq - ) + storage_power = storage_power + storage_soe * efficiency_dispatch / freq storage_soe = 0.0 # If the storage is full or empty, the demand is not affected @@ -106,7 +104,9 @@ def _reference_operation( return df.round(6) -def apply_reference_operation(edisgo_obj, storage_units_names=None, soe_init=0.0, freq=1): +def apply_reference_operation( + edisgo_obj, storage_units_names=None, soe_init=0.0, freq=1 +): """ Applies reference storage operation to specified home storage units. @@ -211,7 +211,9 @@ def apply_reference_operation(edisgo_obj, storage_units_names=None, soe_init=0.0 f"Parameter efficiency_dispatch for storage unit {stor_name} is " f"not a number. It needs to be set in Topology.storage_units_df." ) - pv_feedin = edisgo_obj.timeseries.generators_active_power[pv_gens].sum(axis=1) + pv_feedin = edisgo_obj.timeseries.generators_active_power[pv_gens].sum( + axis=1 + ) house_demand = edisgo_obj.timeseries.loads_active_power[loads].sum(axis=1) # apply operation strategy storage_ts = _reference_operation( @@ -233,7 +235,9 @@ def apply_reference_operation(edisgo_obj, storage_units_names=None, soe_init=0.0 data=storage_ts.storage_power.values, ) ) - soe_df = pd.concat([soe_df, storage_ts.storage_soe.to_frame(stor_name)], axis=1) + soe_df = pd.concat( + [soe_df, storage_ts.storage_soe.to_frame(stor_name)], axis=1 + ) edisgo_obj.set_time_series_reactive_power_control( generators_parametrisation=None, diff --git a/edisgo/network/topology.py b/edisgo/network/topology.py index 1805cf60..bf1e70a5 100755 --- a/edisgo/network/topology.py +++ b/edisgo/network/topology.py @@ -51,7 +51,14 @@ "subtype", "source_id", ], - "storage_units_df": ["bus", "control", "p_nom", "max_hours", "efficiency_store", "efficiency_dispatch"], + "storage_units_df": [ + "bus", + "control", + "p_nom", + "max_hours", + "efficiency_store", + "efficiency_dispatch", + ], "transformers_df": ["bus0", "bus1", "x_pu", "r_pu", "s_nom", "type_info"], "lines_df": [ "bus0", @@ -88,7 +95,6 @@ class Topology: """ def __init__(self, **kwargs): - # load technical data of equipment self._equipment_data = self._load_equipment_data(kwargs.get("config", None)) @@ -1901,7 +1907,6 @@ def connect_to_mv(self, edisgo_object, comp_data, comp_type="generator"): # ===== voltage level 4: component is connected to MV station ===== if voltage_level == 4: - # add line line_length = geo.calc_geo_dist_vincenty( grid_topology=self, @@ -1931,7 +1936,6 @@ def connect_to_mv(self, edisgo_object, comp_data, comp_type="generator"): ) elif voltage_level == 5: - # get branches within the predefined `connection_buffer_radius` lines = geo.calc_geo_lines_in_buffer( grid_topology=self, @@ -2131,13 +2135,11 @@ def _choose_random_substation_id(): logger.error(f"Component type {comp_type} is not a valid option.") if mvlv_subst_id is not None and not np.isnan(mvlv_subst_id): - # if substation ID (= LV grid ID) is given and it matches an # existing LV grid ID (i.e. it is no aggregated LV grid), set grid # to connect component to specified grid (in case the component # has no geometry it is connected to the grid's station) if int(mvlv_subst_id) in self._lv_grid_ids: - # get LV grid lv_grid = self.get_lv_grid(int(mvlv_subst_id)) @@ -2190,7 +2192,6 @@ def _choose_random_substation_id(): # v_level 7 -> connect in LV grid elif voltage_level == 7: - # get valid buses to connect new component to lv_loads = lv_grid.loads_df if comp_type == "generator" or comp_type == "storage_unit": @@ -2278,7 +2279,6 @@ def _choose_random_substation_id(): lv_conn_target = None while len(lv_buses_rnd) > 0 and lv_conn_target is None: - lv_bus = lv_buses_rnd.pop() # determine number of components of the same type at LV bus @@ -2487,7 +2487,6 @@ def _connect_mv_bus_to_target_object( # MV line is nearest connection point => split old line into 2 segments # (delete old line and create 2 new ones) if isinstance(target_obj["shp"], LineString): - line_data = self.lines_df.loc[target_obj["repr"], :] # if line that is split is connected to switch, the line name needs @@ -2616,7 +2615,6 @@ def _connect_mv_bus_to_target_object( # bus ist nearest connection point else: - # add new branch for satellite (station to station) line_length = geo.calc_geo_dist_vincenty( grid_topology=self, From abdfb22b6262df2a7c72f18b1a74006b7f1e5ae6 Mon Sep 17 00:00:00 2001 From: birgits Date: Fri, 15 Dec 2023 17:23:50 -0800 Subject: [PATCH 19/19] Add changes to whatsnew --- doc/whatsnew/v0-3-0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/whatsnew/v0-3-0.rst b/doc/whatsnew/v0-3-0.rst index 0165a76f..2261c835 100644 --- a/doc/whatsnew/v0-3-0.rst +++ b/doc/whatsnew/v0-3-0.rst @@ -23,3 +23,4 @@ Changes * Adapted codebase to work with pandas 2.0 `#373 `_ * Added option to run reinforcement with reduced number of time steps `#379 `_ * Added a new reinforcement method that separate lv grids when the overloading is very high `#380 `_ +* 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 `_