Skip to content

Commit

Permalink
Merge pull request #1054 from lensum/feature/MultipleOutputsOffsetCon…
Browse files Browse the repository at this point in the history
…verter

Multiple Outputs for OffsetConverter
  • Loading branch information
p-snft committed May 16, 2024
2 parents 0986292 + 595676a commit e5fb98c
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 95 deletions.
43 changes: 23 additions & 20 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -682,40 +682,42 @@ The `Link` allows to model connections between two busses, e.g. modeling the tra
OffsetConverter (component)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
The `OffsetConverter` object makes it possible to create a Converter with different efficiencies in part load condition.
For this object it is necessary to define the inflow as a nonconvex flow and to set a minimum load.
The `OffsetConverter` object makes it possible to create a Converter with efficiencies depending on the part load condition.
For this object it is necessary to define the outflow as a nonconvex flow and to set a minimum load.
The following example illustrates how to define an OffsetConverter for given information for the output:
.. code-block:: python
eta_min = 0.5 # efficiency at minimal operation point
eta_max = 0.8 # efficiency at nominal operation point
P_out_min = 20 # absolute minimal output power
P_out_max = 100 # absolute nominal output power
# calculate limits of input power flow
P_in_min = P_out_min / eta_min
P_in_max = P_out_max / eta_max
eta_min = 0.5 # efficiency at minimal operation point
eta_max = 0.8 # efficiency at nominal operation point
P_out_min = 20 # absolute minimal output power
P_out_max = 100 # absolute nominal output power
l_max = P_out_max/P_out_max # upper part load limit
l_min = P_out_min/P_out_max # lower part load limit
# calculate coefficients of input-output line equation
c1 = (P_out_max-P_out_min)/(P_in_max-P_in_min)
c0 = P_out_max - c1*P_in_max
c1 = (l_max-l_min)/(l_max/eta_max - l_min/eta_min)
c0 = l_min * (1-c1/eta_min)
# define OffsetConverter
solph.components.OffsetConverter(
label='boiler',
inputs={bfuel: solph.flows.Flow(
nominal_value=P_in_max,
max=1,
min=P_in_min/P_in_max,
nonconvex=solph.NonConvex())},
outputs={bth: solph.flows.Flow()},
coefficients = [c0, c1])
inputs={bfuel: solph.flows.Flow()},
outputs={
bth: solph.flows.Flow(
nominal_value=P_out_max,
max=l_max,
min=l_min,
nonconvex=solph.NonConvex()
),
},
coefficients = {bth: (c0, c1)},
)
This example represents a boiler, which is supplied by fuel and generates heat.
It is assumed that the nominal thermal power of the boiler (output power) is 100 (kW) and the efficiency at nominal power is 80 %.
The boiler cannot operate under 20 % of nominal power, in this case 20 (kW) and the efficiency at that part load is 50 %.
Note that the nonconvex flow has to be defined for the input flow.
Note that the nonconvex flow has to be defined for the output flow.
By using the OffsetConverter a linear relation of in- and output power with a power dependent efficiency is generated.
The following figures illustrate the relations:
Expand Down Expand Up @@ -746,6 +748,7 @@ which results in a nonlinear relation:
:align: center
The parameters :math:`C_{0}` and :math:`C_{1}` can be given by scalars or by series in order to define a different efficiency equation for every timestep.
It is also possible to define multiple outputs.
.. note:: See the :py:class:`~oemof.solph.components._offset_converter.OffsetConverter` class for all parameters and the mathematical background.
Expand Down
8 changes: 8 additions & 0 deletions docs/whatsnew/v0-5-3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ v0.5.3
API changes
###########

* `coefficient` of `OffsetConverter` expects a dictionary similar to
`Converter`s `conversion_factors`

New features
############

* `OffsetConverter` can now handle multiple outputs

Documentation
#############

Expand All @@ -19,10 +24,13 @@ Bug fixes
Other changes
#############

* Unified (usage) documentation for `OffsetConverter`

Known issues
############

Contributors
############

* Lennart Schürmann
* Richard Keil
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,14 @@
__copyright__ = "oemof developer group"
__license__ = "MIT"

import numpy as np
import os
import pandas as pd
import time
from datetime import datetime, timedelta
from datetime import datetime
from datetime import timedelta

import numpy as np
import pandas as pd

from oemof import solph

try:
Expand Down Expand Up @@ -155,10 +158,10 @@ def offset_converter_example():

# Calculate the two polynomial coefficients, i.e. the y-intersection and the
# slope of the linear equation.
c1 = (max_load / max_efficiency - min_load / min_efficiency) / (
max_load - min_load
c1 = (max_load - min_load) / (
max_load / max_efficiency - min_load / min_efficiency
)
c0 = min_load * (1 / min_efficiency - c1)
c0 = min_load * (1 - c1 / min_efficiency)

epc_diesel_genset = 84.80 # currency/kW/year
variable_cost_diesel_genset = 0.045 # currency/kWh
Expand Down Expand Up @@ -277,7 +280,7 @@ def offset_converter_example():
# The higher the MipGap or ratioGap, the faster the solver would converge,
# but the less accurate the results would be.
solver_option = {"gurobi": {"MipGap": "0.02"}, "cbc": {"ratioGap": "0.02"}}
solver = "cbc"
solver = "gurobi"

model = solph.Model(energy_system)
model.solve(
Expand Down
131 changes: 96 additions & 35 deletions src/oemof/solph/components/_offset_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,39 @@


class OffsetConverter(Node):
"""An object with one input and one output and two coefficients to model
part load behaviour.
"""An object with one input and multiple outputs and two coefficients
per output to model part load behaviour.
The output must contain a NonConvex object.
Parameters
----------
coefficients : tuple, (:math:`C_0(t)`, :math:`C_1(t)`)
Tuple containing the first two polynomial coefficients
i.e. the y-intersection and slope of a linear equation.
coefficients : dict of tuples, (:math:`C_0(t)`, :math:`C_1(t)`)
Dict of tuples containing the respective output bus as key and
as value a tuple with the parameters :math:`C_0(t)` and :math:`C_1(t)`.
Here, :math:`C_1(t)` represents the slope of a linear equation and
:math:`C_0(t)` is the y-intercept devided by the `nominal_value` of the
output flow (this is for internal purposes).
The tuple values can either be a scalar or a sequence with length
of time horizon for simulation.
Notes
-----
**C_1 and C_0 can be calculated as follows:**
.. _OffsetConverterCoefficients-equations:
.. math::
C_1 = (l_{max}-l_{min})/(l_{max}/\\eta_{max}-l_{min}/\\eta_{min})
C_0 = l_{min} \\cdot (1-C_1/\\eta_{min})
Where :math:`l_{max}` and :math:`l_{min}` are the maximum and minimum
partload share (e.g. 1.0 and 0.3) and :math:`\\eta_{max}` and
:math:`\\eta_{min}` are the efficiencies/conversion factors at these
partloads.
The sets, variables, constraints and objective parts are created
* :py:class:`~oemof.solph.components._offset_converter.OffsetConverterBlock`
Expand All @@ -52,13 +72,20 @@ class OffsetConverter(Node):
>>> from oemof import solph
>>> bel = solph.buses.Bus(label='bel')
>>> bth = solph.buses.Bus(label='bth')
>>> l_max = 1
>>> l_min = 0.5
>>> eta_max = 0.5
>>> eta_min = 0.3
>>> c1 = (l_max-l_min)/(l_max/eta_max-l_min/eta_min)
>>> c0 = l_min*(1-c1/eta_min)
>>> ostf = solph.components.OffsetConverter(
... label='ostf',
... inputs={bel: solph.flows.Flow()},
... outputs={bth: solph.flows.Flow(
... nominal_value=60, min=0.5, max=1.0,
... nominal_value=60, min=l_min, max=l_max,
... nonconvex=solph.NonConvex())},
... coefficients=(20, 0.5))
... coefficients={bth: (c0, c1)}
... )
>>> type(ostf)
<class 'oemof.solph.components._offset_converter.OffsetConverter'>
""" # noqa: E501
Expand All @@ -81,10 +108,42 @@ def __init__(
)

if coefficients is not None:
self.coefficients = tuple([sequence(i) for i in coefficients])
if len(self.coefficients) != 2:
raise ValueError(
"Two coefficients or coefficient series have to be given."
self.coefficients = dict()
if isinstance(coefficients, tuple):
# TODO: add the correct version in the message
msg = (
"Passing a tuple to the keyword `coefficients` will be"
" deprecated in a later version. Please use a dict to"
" specify the corresponding output flow. The first output"
" flow will be assumed as target by default."
)
warn(msg, DeprecationWarning)
if len(coefficients) != 2:
raise ValueError(
"Two coefficients or coefficient series have to be"
" given."
)
self.coefficients.update(
{
[k for k in self.outputs.keys()][0]: tuple(
[sequence(i) for i in coefficients]
)
}
)
elif isinstance(coefficients, dict):
for k, v in coefficients.items():
if len(v) != 2:
raise ValueError(
"Two coefficients or coefficient series have to be"
" given."
)
self.coefficients.update(
{k: (sequence(v[0]), sequence(v[1]))}
)
else:
raise TypeError(
"`coefficiencts` needs to be either dict or tuple"
" (deprecated)."
)

# `OffsetConverter` always needs the `NonConvex` attribute, but the
Expand Down Expand Up @@ -113,10 +172,9 @@ def __init__(
+ "output flow!"
)

if len(self.inputs) > 1 or len(self.outputs) > 1:
if len(self.inputs) > 1:
raise ValueError(
"Component `OffsetConverter` must not have "
+ "more than 1 input and 1 output!"
"Component `OffsetConverter` must not have more than 1 input!"
)

def constraint_group(self):
Expand Down Expand Up @@ -161,30 +219,31 @@ class OffsetConverterBlock(ScalarBlock):
.. math::
&
P_{in}(p, t) = C_1(t) \cdot P_{out}(p, t) + C_0(t) \cdot P_max(p) \cdot Y(t) \\
P_{out}(p, t) = P_{in}(p, t) \cdot C_1(t) + P_nom(p) \cdot Y(t) \cdot C_0(t) \\
The symbols used are defined as follows (with Variables (V) and Parameters (P)):
+--------------------+------------------------+------+--------------------------------------------+
| symbol | attribute | type | explanation |
+====================+========================+======+============================================+
| :math:`P_{out}(t)` | `flow[n,o,p,t]` | V | Outflow of converter |
+--------------------+------------------------+------+--------------------------------------------+
| :math:`P_{in}(t)` | `flow[i,n,p,t]` | V | Inflow of converter |
+--------------------+------------------------+------+--------------------------------------------+
| :math:`Y(t)` | | V | Binary status variable of nonconvex inflow |
+--------------------+------------------------+------+--------------------------------------------+
| :math:`P_{max}(t)` | | V | Maximum Outflow of converter |
+--------------------+------------------------+------+--------------------------------------------+
| :math:`C_1(t)` | `coefficients[1][n,t]` | P | Linear coefficient 1 (slope) |
+--------------------+------------------------+------+--------------------------------------------+
| :math:`C_0(t)` | `coefficients[0][n,t]` | P | Linear coefficient 0 (y-intersection) |
+--------------------+------------------------+------+--------------------------------------------+
Note that :math:`P_{max}(t) \cdot Y(t)` is merged into one variable,
+--------------------+---------------------------+------+--------------------------------------------------+
| symbol | attribute | type | explanation |
+====================+===========================+======+==================================================+
| :math:`P_{out}(t)` | `flow[n,o,p,t]` | V | Outflow of converter |
+--------------------+---------------------------+------+--------------------------------------------------+
| :math:`P_{in}(t)` | `flow[i,n,p,t]` | V | Inflow of converter |
+--------------------+---------------------------+------+--------------------------------------------------+
| :math:`Y(t)` | | V | Binary status variable of nonconvex outflow |
+--------------------+---------------------------+------+--------------------------------------------------+
| :math:`P_{nom}(t)` | | V | Nominal value (max. capacity) of the outflow |
+--------------------+---------------------------+------+--------------------------------------------------+
| :math:`C_1(t)` | `coefficients[o][1][n,t]` | P | Linear coefficient 1 (slope) |
+--------------------+---------------------------+------+--------------------------------------------------+
| :math:`C_0(t)` | `coefficients[o][0][n,t]` | P | Linear coefficient 0 (y-intersection)/P_{nom}(t) |
+--------------------+---------------------------+------+--------------------------------------------------+
Note that :math:`P_{nom}(t) \cdot Y(t)` is merged into one variable,
called `status_nominal[n, o, p, t]`.
""" # noqa: E501

CONSTRAINT_GROUP = True

def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -231,7 +290,9 @@ def _relation_rule(block):
for i in in_flows[n]:
expr = 0
expr += -m.flow[n, o, p, t]
expr += m.flow[i, n, p, t] * n.coefficients[1][t]
expr += (
m.flow[i, n, p, t] * n.coefficients[o][1][t]
)
# `Y(t)` in the last term of the constraint
# (":math:`C_0(t) \cdot Y(t)`") is different for
# different cases. If both `Investment` and
Expand All @@ -248,7 +309,7 @@ def _relation_rule(block):
m.InvestNonConvexFlowBlock.status_nominal[
n, o, t
]
* n.coefficients[0][t]
* n.coefficients[o][0][t]
)
# `KeyError` occurs when more than one
# `OffsetConverter` is defined, and in some of
Expand All @@ -265,7 +326,7 @@ def _relation_rule(block):
m.NonConvexFlowBlock.status_nominal[
n, o, t
]
* n.coefficients[0][t]
* n.coefficients[o][0][t]
)
block.relation.add((n, i, o, p, t), (expr == 0))

Expand Down
4 changes: 2 additions & 2 deletions tests/constraint_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1368,7 +1368,7 @@ def test_offsetconverter_nonconvex(self):
min=0.2,
)
},
coefficients=[2.5, 0.5],
coefficients={b_el: (2.5, 0.5)},
)
self.energysystem.add(b_diesel, b_el, diesel_genset)

Expand All @@ -1393,7 +1393,7 @@ def test_offsetconverter_nonconvex_investment(self):
),
)
},
coefficients=[2.5, 0.5],
coefficients={b_el: (2.5, 0.5)},
)
self.energysystem.add(b_diesel, b_el, diesel_genset)

Expand Down
2 changes: 1 addition & 1 deletion tests/multi_period_constraint_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1529,7 +1529,7 @@ def test_offsetconverter(self):
nominal_value=100, min=0.32, nonconvex=solph.NonConvex()
)
},
coefficients=[-17, 0.9],
coefficients={bth: (-17, 0.9)},
)
self.energysystem.add(bgas, bth, otrf)
self.compare_lp_files("offsetconverter_multi_period.lp")
Expand Down

0 comments on commit e5fb98c

Please sign in to comment.