Skip to content

Commit

Permalink
Merge branch 'dev' into wp_flex_changed_reinforcement_2
Browse files Browse the repository at this point in the history
  • Loading branch information
birgits committed Dec 20, 2023
2 parents f3eab58 + 5f0be84 commit 762dce3
Show file tree
Hide file tree
Showing 39 changed files with 574 additions and 362 deletions.
2 changes: 1 addition & 1 deletion .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ python:
build:
os: ubuntu-22.04
tools:
python: "3.8"
python: "3.9"
4 changes: 3 additions & 1 deletion doc/whatsnew/v0-3-0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,7 @@ Changes
* Added method to aggregate LV grid buses to station bus secondary side `#353 <https://github.com/openego/eDisGo/pull/353>`_
* Adapted codebase to work with pandas 2.0 `#373 <https://github.com/openego/eDisGo/pull/373>`_
* 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 optimization method 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>`_
* Move function to assign feeder to Topology class and add methods to the Grid class to get information on the feeders `#360 <https://github.com/openego/eDisGo/pull/360>`_
* Added a storage operation strategy where the storage is charged when PV feed-in is higher than electricity demand of the household and discharged when electricity demand exceeds PV generation `#386 <https://github.com/openego/eDisGo/pull/386>`_
181 changes: 122 additions & 59 deletions edisgo/flex_opt/battery_storage_operation.py
Original file line number Diff line number Diff line change
@@ -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
-----------
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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

Expand All @@ -76,21 +77,19 @@ 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
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_power = storage_power + storage_soe * efficiency_dispatch / freq
storage_soe = 0.0

# If the storage is full or empty, the demand is not affected
Expand All @@ -105,22 +104,42 @@ 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
Expand All @@ -131,62 +150,106 @@ def create_storage_data(edisgo_obj, soe_init=0.0, freq=1):
--------
: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.
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)
soe_df = pd.concat(
[soe_df, storage_ts.storage_soe.to_frame(stor_name)], axis=1
)

soc_df.columns = edisgo_obj.topology.storage_units_df.index
edisgo_obj.set_time_series_reactive_power_control()
return soc_df
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
4 changes: 2 additions & 2 deletions edisgo/flex_opt/check_tech_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,7 @@ def stations_allowed_load(edisgo_obj, grids=None):
"""
if grids is None:
grids = list(edisgo_obj.topology.lv_grids) + [edisgo_obj.topology.mv_grid]
grids = edisgo_obj.topology.grids

allowed_loading = pd.DataFrame()
for grid in grids:
Expand Down Expand Up @@ -695,7 +695,7 @@ def stations_relative_load(edisgo_obj, grids=None):
"""
if grids is None:
grids = list(edisgo_obj.topology.lv_grids) + [edisgo_obj.topology.mv_grid]
grids = edisgo_obj.topology.grids

# get allowed loading
allowed_loading = stations_allowed_load(edisgo_obj, grids)
Expand Down
1 change: 0 additions & 1 deletion edisgo/flex_opt/curtailment.py
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,6 @@ class CurtailmentControl:
def __init__(
self, edisgo, methodology, curtailment_timeseries, mode=None, **kwargs
):

raise NotImplementedError

logging.info("Start curtailment methodology {}.".format(methodology))
Expand Down
3 changes: 0 additions & 3 deletions edisgo/flex_opt/storage_positioning.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,6 @@ def _estimate_new_number_of_lines(critical_lines_feeder):
)

if battery_node:

# add to output lists
if debug:
feeder_repr.append(repr(feeder))
Expand All @@ -435,7 +434,6 @@ def _estimate_new_number_of_lines(critical_lines_feeder):
# if p_storage is greater than or equal to the minimum storage
# power required, do storage integration
if p_storage >= p_storage_min:

# third step: integrate storage

share = p_storage / storage_nominal_power
Expand Down Expand Up @@ -472,7 +470,6 @@ def _estimate_new_number_of_lines(critical_lines_feeder):
# reinforcement costs or number of issues

if check_costs_reduction == "each_feeder":

# calculate new network expansion costs

grid_expansion_results_new = edisgo.reinforce(
Expand Down
1 change: 0 additions & 1 deletion edisgo/io/electromobility_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,6 @@ def get_weights_df(edisgo_obj, potential_charging_park_indices, **kwargs):
"""

def _get_lv_grid_weights():

"""
DataFrame containing technical data of LV grids.
Expand Down
3 changes: 0 additions & 3 deletions edisgo/io/generators_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,6 @@ def _validate_sample_geno_location():
)

if oedb_data_source == "model_draft":

# import ORMs
orm_conv_generators = model_draft.__getattribute__(orm_conv_generators_name)
orm_re_generators = model_draft.__getattribute__(orm_re_generators_name)
Expand All @@ -325,7 +324,6 @@ def _validate_sample_geno_location():
orm_re_generators_version = 1 == 1

elif oedb_data_source == "versioned":

data_version = edisgo_object.config["versioned"]["version"]

# import ORMs
Expand Down Expand Up @@ -1359,7 +1357,6 @@ def _integrate_new_power_plant(edisgo_object, comp_data):
# status quo and future scenario
existing_gens_with_source = gens_df[~gens_df.source_id.isna()]
if len(existing_gens_with_source) > 0:

# join dataframes at source ID
existing_gens_with_source.index.name = "gen_name"
power_plants_gdf.index.name = "gen_index_new"
Expand Down
4 changes: 3 additions & 1 deletion edisgo/io/heat_pump_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,9 @@ def _get_individual_heat_pump_capacity():
checking.
"""
query = session.query(egon_individual_heating.capacity,).filter(
query = session.query(
egon_individual_heating.capacity,
).filter(
egon_individual_heating.scenario == scenario,
egon_individual_heating.carrier == "heat_pump",
egon_individual_heating.mv_grid_id == edisgo_object.topology.id,
Expand Down
Loading

0 comments on commit 762dce3

Please sign in to comment.