Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add storage_costs to GenericStorage #962

Merged
merged 8 commits into from
Aug 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions examples/storage_costs/storage_costs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# -*- coding: utf-8 -*-

"""
General description
-------------------
Example that shows the parameter `storage_costs` of `GenericStorage`.
A battery is used to make profit from fluctuating electricity prices.
For a battery without storage costs, it is beneficial to be empty
the end of the time horizon of the optimisation. For a battery that
assumes the average revenue, energy is kept at the end.


Installation requirements
-------------------------
This example requires oemof.solph (v0.5.x), install by:

pip install oemof.solph[examples]


License
-------
`MIT license <https://github.com/oemof/oemof-solph/blob/dev/LICENSE>`_
"""

import numpy as np
import pandas as pd
from matplotlib import pyplot as plt

from oemof import solph


def storage_costs_example():
# create an energy system
idx = pd.date_range("1/1/2023", periods=13, freq="H")
es = solph.EnergySystem(timeindex=idx, infer_last_interval=False)

# power bus
bel = solph.Bus(label="bel")
es.add(bel)

es.add(
solph.components.Source(
label="source_el",
outputs={bel: solph.Flow()},
)
)

es.add(
solph.components.Sink(
label="sink_el",
inputs={bel: solph.Flow()},
)
)

electricity_price = np.array(
[
0.38,
0.31,
0.32,
0.33,
0.37,
0.32,
0.33,
0.34,
0.39,
0.38,
0.37,
0.35,
0.35,
]
)

# Electric Storage 1
# Costs are designed in a way that storing energy is benificial until the
# last four time steps but emptying it is not a good option.
battery1 = solph.components.GenericStorage(
label="battery 1",
nominal_storage_capacity=10,
inputs={
bel: solph.Flow(
nominal_value=1,
variable_costs=electricity_price,
)
},
outputs={
bel: solph.Flow(
nominal_value=1,
variable_costs=-electricity_price,
)
},
initial_storage_level=0.5,
balanced=False,
)
es.add(battery1)

# storages that balance our fluctuating costs
# Electric Storage 2
battery2 = solph.components.GenericStorage(
label="battery 2",
nominal_storage_capacity=10,
inputs={
bel: solph.Flow(
nominal_value=1,
variable_costs=electricity_price,
)
},
outputs={
bel: solph.Flow(
nominal_value=1,
variable_costs=-electricity_price,
)
},
storage_costs=12 * [0] + [-np.mean(electricity_price)],
initial_storage_level=0.5,
balanced=False,
)
es.add(battery2)

# create an optimization problem and solve it
model = solph.Model(es)

# solve model
model.solve(solver="cbc")

# create result object
results = solph.processing.results(model)

plt.plot(
results[(battery1, None)]["sequences"],
label="content w/o storage costs",
)
plt.plot(
results[(battery2, None)]["sequences"],
label="content w/ storage revenue",
)
plt.legend()
plt.grid()

plt.show()


if __name__ == "__main__":
storage_costs_example()
29 changes: 27 additions & 2 deletions src/oemof/solph/components/_generic_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ class GenericStorage(network.Component):
investment variable instead of to the nominal_storage_capacity. The
nominal_storage_capacity should not be set (or set to None) if an
investment object is used.
storage_costs : numeric (iterable or scalar), :math:`c_{storage}(t)`
Cost (per energy) for having energy in the storage.
lifetime_inflow : int, :math:`n_{in}`
Determine the lifetime of an inflow; only applicable for multi-period
models which can invest in storage capacity and have an
Expand Down Expand Up @@ -177,6 +179,7 @@ def __init__(
inflow_conversion_factor=1,
outflow_conversion_factor=1,
fixed_costs=0,
storage_costs=None,
lifetime_inflow=None,
lifetime_outflow=None,
custom_attributes=None,
Expand Down Expand Up @@ -230,6 +233,7 @@ def __init__(
self.max_storage_level = solph_sequence(max_storage_level)
self.min_storage_level = solph_sequence(min_storage_level)
self.fixed_costs = solph_sequence(fixed_costs)
self.storage_costs = solph_sequence(storage_costs)
self.investment = investment
self.invest_relation_input_output = invest_relation_input_output
self.invest_relation_input_capacity = invest_relation_input_capacity
Expand Down Expand Up @@ -415,13 +419,19 @@ class GenericStorageBlock(ScalarBlock):
:math:`\delta(t)` and
timeincrement
:math:`\tau(t)`
:math:`c_{storage}(t)` costs of having `storage_costs`
energy stored
=========================== ======================= =========

**The following parts of the objective function are created:**

*Standard model*

Nothing added to the objective function.
* :attr: `storage_costs` not 0

..math::
\sum_{t \in \textrm{TIMESTEPS}} c_{storage}(t) \cdot E(t)


*Multi-period model*

Expand Down Expand Up @@ -596,7 +606,22 @@ def _objective_expression(self):
* ((1 + m.discount_rate) ** -p)
)
self.fixed_costs = Expression(expr=fixed_costs)
return self.fixed_costs

storage_costs = 0

for n in self.STORAGES:
if n.storage_costs[0] is not None:
storage_costs += (
self.storage_content[n, 0] * n.storage_costs[0]
)
for t in m.TIMESTEPS:
storage_costs += (
self.storage_content[n, t + 1] * n.storage_costs[t + 1]
)

self.storage_costs = Expression(expr=storage_costs)

return self.fixed_costs + self.storage_costs


class GenericInvestmentStorageBlock(ScalarBlock):
Expand Down
1 change: 1 addition & 0 deletions tests/constraint_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ def test_storage(self):
},
nominal_storage_capacity=1e5,
loss_rate=0.13,
storage_costs=0.1,
inflow_conversion_factor=0.97,
outflow_conversion_factor=0.86,
initial_storage_level=0.4,
Expand Down
5 changes: 5 additions & 0 deletions tests/lp_files/storage.lp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

min
objective:
+4000.0 ONE_VAR_CONSTANT
+0.1 GenericStorageBlock_storage_content(storage_no_invest_1)
+0.1 GenericStorageBlock_storage_content(storage_no_invest_2)
+0.1 GenericStorageBlock_storage_content(storage_no_invest_3)
Comment on lines +6 to +8
Copy link
Contributor

@nailend nailend Aug 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently there is no period index for the storage block. Maybe this should be fixed for the sake of consistency? Not sure if this needs to be addressed in a separate issue though?!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the storage content is given in absolute values, only the limit (which might change from period to period) needs to have the index. I will leave it as it is.

+56 flow(electricityBus_storage_no_invest_0_0)
+56 flow(electricityBus_storage_no_invest_0_1)
+56 flow(electricityBus_storage_no_invest_0_2)
Expand Down Expand Up @@ -51,6 +55,7 @@ c_e_GenericStorageBlock_balanced_cstr(storage_no_invest)_:
= 40000.0

bounds
+1 <= ONE_VAR_CONSTANT <= 1
0 <= flow(electricityBus_storage_no_invest_0_0) <= 16667
0 <= flow(electricityBus_storage_no_invest_0_1) <= 16667
0 <= flow(electricityBus_storage_no_invest_0_2) <= 16667
Expand Down
Loading