Skip to content

Commit

Permalink
Merge pull request #386 from openego/features/home_storage_operation
Browse files Browse the repository at this point in the history
Add home storage operation strategy
  • Loading branch information
birgits authored Dec 19, 2023
2 parents fce09be + abdfb22 commit 2e5179b
Show file tree
Hide file tree
Showing 9 changed files with 515 additions and 19 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.9"
1 change: 1 addition & 0 deletions doc/whatsnew/v0-3-0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ Changes
* Added option to run reinforcement with reduced number of time steps `#379 <https://github.com/openego/eDisGo/pull/379>`_
* Added optimization methods to determine dispatch of flexibilities that lead to minimal network expansion costs. `#376 <https://github.com/openego/eDisGo/pull/376>`_
* Added a new reinforcement method that separate lv grids when the overloading is very high `#380 <https://github.com/openego/eDisGo/pull/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 <https://github.com/openego/eDisGo/pull/386>`_
255 changes: 255 additions & 0 deletions edisgo/flex_opt/battery_storage_operation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
import logging
import math

import pandas as pd

logger = logging.getLogger(__name__)


def _reference_operation(
df,
soe_init,
soe_max,
storage_p_nom,
freq,
efficiency_store,
efficiency_dispatch,
):
"""
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.
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_store : float
Efficiency of storage system in case of charging.
efficiency_dispatch : 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 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
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_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_store * 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.0) & (storage_soe > 0.0):
# Check if energy demand exceeds charger power
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_dispatch
else:
storage_soe = storage_soe + (
d.feedin_minus_demand / efficiency_dispatch * freq
)
storage_power = -d.feedin_minus_demand
# If the storage is undercharged, take the 'rest' from the grid
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_soe = 0.0

# If the storage is full or empty, the demand is not affected
else:
storage_power = 0.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 apply_reference_operation(
edisgo_obj, storage_units_names=None, soe_init=0.0, freq=1
):
"""
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
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
: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`.
"""
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
loads = edisgo_obj.topology.loads_df.loc[
edisgo_obj.topology.loads_df.building_id == building_id
].index
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=[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)
# 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=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,
)
# add storage time series to storage_units_active_power dataframe
edisgo_obj.set_time_series_manual(
storage_units_p=pd.DataFrame(
columns=[stor_name],
index=storage_ts.index,
data=storage_ts.storage_power.values,
)
)
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
17 changes: 16 additions & 1 deletion edisgo/network/topology.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,14 @@
"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",
Expand Down Expand Up @@ -367,6 +374,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<DataFrame>`
Expand Down
5 changes: 5 additions & 0 deletions edisgo/tools/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,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]
Expand Down
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
Loading

0 comments on commit 2e5179b

Please sign in to comment.