Skip to content

Commit

Permalink
Merge branch 'features/home_storage_operation' into wp_flex_changed_r…
Browse files Browse the repository at this point in the history
…einforcement_2
  • Loading branch information
birgits committed Oct 30, 2023
2 parents 25c5df5 + 8f75368 commit 3fed9d1
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 17 deletions.
7 changes: 6 additions & 1 deletion .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +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-22.04
tools:
python: "3.8"
217 changes: 217 additions & 0 deletions edisgo/flex_opt/battery_storage_operation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
from copy import deepcopy
import logging

import pandas as pd

logger = logging.getLogger(__name__)


def reference_operation(
df,
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 when PV feed-in is
higher than electricity demand of the building.
Battery model handles generation positive, demand negative
Parameters
-----------
df : :pandas:`pandas.DataFrame<DataFrame>`
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>`
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.
"""
lst_storage_power = []
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.feedin_minus_demand > 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
)
# 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_soe = soe_max

# If the house needs electricity from the grid, discharge the storage first.
# 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):
# 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 + (
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
# demand
storage_power = (
storage_power + storage_soe * efficiency_discharge / freq
)
storage_charge = 0

# If the storage is full or empty, the demand is not affected
else:
storage_power = 0
lst_storage_power.append(storage_power)
lst_storage_soe.append(storage_soe)

df["storage_power"] = lst_storage_power
df["storage_soe"] = lst_storage_soe

return df.round(6)


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>`
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 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]
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:
logger.info(
f"Storage unit {idx} in building {building_id} has not load. "
f"Storage operation is therefore set to zero."
)
edisgo_obj.set_time_series_manual(
storage_units_p=pd.DataFrame(
columns=[idx],
index=soc_df.index,
data=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),
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=[idx],
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


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)
22 changes: 11 additions & 11 deletions examples/electromobility_example.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -493,7 +491,9 @@
"cell_type": "code",
"execution_count": null,
"id": "1d65e6d6",
"metadata": {},
"metadata": {
"scrolled": true
},
"outputs": [],
"source": [
"edisgo.import_electromobility(\n",
Expand Down Expand Up @@ -776,7 +776,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.5"
"version": "3.8.18"
},
"toc": {
"base_numbering": 1,
Expand Down
3 changes: 1 addition & 2 deletions rtd_requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
beautifulsoup4
dash < 2.9.0
demandlib
docutils == 0.16.0
Expand All @@ -20,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
5 changes: 2 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ def read(fname):


requirements = [
"beautifulsoup4",
"contextily",
"dash < 2.9.0",
"demandlib",
Expand All @@ -55,7 +54,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",
Expand All @@ -75,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}
Expand Down

0 comments on commit 3fed9d1

Please sign in to comment.