1
|
|
|
# -*- coding: utf-8 -*- |
2
|
|
|
|
3
|
|
|
""" |
4
|
|
|
solph version of oemof.network.Edge |
5
|
|
|
|
6
|
|
|
SPDX-FileCopyrightText: Uwe Krien <[email protected]> |
7
|
|
|
SPDX-FileCopyrightText: Simon Hilpert |
8
|
|
|
SPDX-FileCopyrightText: Cord Kaldemeyer |
9
|
|
|
SPDX-FileCopyrightText: Stephan Günther |
10
|
|
|
SPDX-FileCopyrightText: Birgit Schachler |
11
|
|
|
SPDX-FileCopyrightText: jnnr |
12
|
|
|
SPDX-FileCopyrightText: jmloenneberga |
13
|
|
|
SPDX-FileCopyrightText: Pierre-François Duc |
14
|
|
|
SPDX-FileCopyrightText: Saeed Sayadi |
15
|
|
|
SPDX-FileCopyrightText: Johannes Kochems |
16
|
|
|
|
17
|
|
|
SPDX-License-Identifier: MIT |
18
|
|
|
|
19
|
|
|
""" |
20
|
|
|
import math |
21
|
|
|
import numbers |
22
|
|
|
from collections.abc import Iterable |
23
|
|
|
from warnings import warn |
24
|
|
|
|
25
|
|
|
import numpy as np |
26
|
|
|
from oemof.network import Edge |
27
|
|
|
from oemof.tools import debugging |
28
|
|
|
|
29
|
|
|
from oemof.solph._options import Investment |
30
|
|
|
from oemof.solph._plumbing import sequence |
31
|
|
|
|
32
|
|
|
|
33
|
|
|
class Flow(Edge): |
34
|
|
|
r"""Defines a flow between two nodes. |
35
|
|
|
|
36
|
|
|
Keyword arguments are used to set the attributes of this flow. Parameters |
37
|
|
|
which are handled specially are noted below. |
38
|
|
|
For the case where a parameter can be either a scalar or an iterable, a |
39
|
|
|
scalar value will be converted to a sequence containing the scalar value at |
40
|
|
|
every index. This sequence is then stored under the parameter's key. |
41
|
|
|
|
42
|
|
|
Parameters |
43
|
|
|
---------- |
44
|
|
|
nominal_capacity : numeric, :math:`P_{nom}` or |
45
|
|
|
:class:`Investment <oemof.solph.options.Investment>` |
46
|
|
|
The nominal calacity of the flow, either fixed or as an investement |
47
|
|
|
optimisation. If this value is set, the corresponding optimization |
48
|
|
|
variable of the flow object will be bounded by this value |
49
|
|
|
multiplied by min(lower bound)/max(upper bound). |
50
|
|
|
variable_costs : numeric (iterable or scalar), default: 0, :math:`c` |
51
|
|
|
The costs associated with one unit of the flow per hour. The |
52
|
|
|
costs for each timestep (:math:`P_t \cdot c \cdot \delta(t)`) |
53
|
|
|
will be added to the objective expression of the optimization problem. |
54
|
|
|
max : numeric (iterable or scalar), :math:`f_{max}` |
55
|
|
|
Normed maximum value of the flow. The flow absolute maximum will be |
56
|
|
|
calculated by multiplying :attr:`nominal_capacity` with :attr:`max` |
57
|
|
|
min : numeric (iterable or scalar), :math:`f_{min}` |
58
|
|
|
Normed minimum value of the flow (see :attr:`max`). |
59
|
|
|
fix : numeric (iterable or scalar), :math:`f_{fix}` |
60
|
|
|
Normed fixed value for the flow variable. Will be multiplied with the |
61
|
|
|
:attr:`nominal_capacity` to get the absolute value. |
62
|
|
|
positive_gradient_limit : numeric (iterable, scalar or None) |
63
|
|
|
the normed *upper bound* on the positive difference |
64
|
|
|
(`flow[t-1] < flow[t]`) of two consecutive flow values. |
65
|
|
|
negative_gradient_limit : numeric (iterable, scalar or None) |
66
|
|
|
the normed *upper bound* on the negative difference |
67
|
|
|
(`flow[t-1] > flow[t]`) of two consecutive flow values. |
68
|
|
|
full_load_time_max : numeric, :math:`t_{full\_load,max}` |
69
|
|
|
Maximum energy transported by the flow expressed as the time (in |
70
|
|
|
hours) that the flow would have to run at nominal capacity |
71
|
|
|
(`nominal_capacity`). |
72
|
|
|
full_load_time_min : numeric, :math:`t_{full\_load,min}` |
73
|
|
|
Minimum energy transported by the flow expressed as the time (in |
74
|
|
|
hours) that the flow would have to run at nominal capacity |
75
|
|
|
(`nominal_capacity`). |
76
|
|
|
integer : boolean |
77
|
|
|
Set True to bound the flow values to integers. |
78
|
|
|
nonconvex : :class:`NonConvex <oemof.solph.options.NonConvex>` |
79
|
|
|
If a nonconvex flow object is added here, the flow constraints will |
80
|
|
|
be altered significantly as the mathematical model for the flow |
81
|
|
|
will be different, i.e. constraint etc. from |
82
|
|
|
:class:`~oemof.solph.flows._non_convex_flow_block.NonConvexFlowBlock` |
83
|
|
|
will be used instead of |
84
|
|
|
:class:`~oemof.solph.flows._simple_flow_block.SimpleFlowBlock`. |
85
|
|
|
fixed_costs : numeric (iterable or scalar), :math:`c_{fixed}` |
86
|
|
|
The fixed costs associated with a flow. |
87
|
|
|
Note: These are only applicable for a multi-period model |
88
|
|
|
and given on a yearly basis. |
89
|
|
|
lifetime : int, :math:`l` |
90
|
|
|
The lifetime of a flow (usually given in years); |
91
|
|
|
once it reaches its lifetime (considering also |
92
|
|
|
an initial age), the flow is forced to 0. |
93
|
|
|
Note: Only applicable for a multi-period model. |
94
|
|
|
age : int, :math:`a` |
95
|
|
|
The initial age of a flow (usually given in years); |
96
|
|
|
once it reaches its lifetime (considering also |
97
|
|
|
an initial age), the flow is forced to 0. |
98
|
|
|
Note: Only applicable for a multi-period model. |
99
|
|
|
|
100
|
|
|
Notes |
101
|
|
|
----- |
102
|
|
|
See :py:class:`~oemof.solph.flows._simple_flow.SimpleFlowBlock` |
103
|
|
|
for the variables, constraints and objective parts, that are created for |
104
|
|
|
a Flow object. |
105
|
|
|
|
106
|
|
|
Examples |
107
|
|
|
-------- |
108
|
|
|
Creating a fixed flow object: |
109
|
|
|
|
110
|
|
|
>>> f = Flow(nominal_capacity=2, fix=[10, 4, 4], variable_costs=5) |
111
|
|
|
>>> f.variable_costs[2] |
112
|
|
|
5 |
113
|
|
|
>>> f.fix[2] |
114
|
|
|
np.int64(4) |
115
|
|
|
|
116
|
|
|
Creating a flow object with time-depended lower and upper bounds: |
117
|
|
|
|
118
|
|
|
>>> f1 = Flow(min=[0.2, 0.3], max=0.99, nominal_capacity=100) |
119
|
|
|
>>> f1.max[1] |
120
|
|
|
0.99 |
121
|
|
|
""" # noqa: E501 |
122
|
|
|
|
123
|
|
|
def __init__( |
124
|
|
|
self, |
125
|
|
|
nominal_capacity=None, |
126
|
|
|
# --- BEGIN: To be removed for versions >= v0.7 --- |
127
|
|
|
nominal_value=None, |
128
|
|
|
# --- END --- |
129
|
|
|
variable_costs=0, |
130
|
|
|
min=None, |
131
|
|
|
max=None, |
132
|
|
|
fix=None, |
133
|
|
|
positive_gradient_limit=None, |
134
|
|
|
negative_gradient_limit=None, |
135
|
|
|
full_load_time_max=None, |
136
|
|
|
full_load_time_min=None, |
137
|
|
|
integer=False, |
138
|
|
|
bidirectional=False, |
139
|
|
|
nonconvex=None, |
140
|
|
|
lifetime=None, |
141
|
|
|
age=None, |
142
|
|
|
fixed_costs=None, |
143
|
|
|
custom_attributes=None, |
144
|
|
|
): |
145
|
|
|
# TODO: Check if we can inherit from pyomo.core.base.var _VarData |
146
|
|
|
# then we need to create the var object with |
147
|
|
|
# pyomo.core.base.IndexedVarWithDomain before any SimpleFlowBlock |
148
|
|
|
# is created. E.g. create the variable in the energy system and |
149
|
|
|
# populate with information afterwards when creating objects. |
150
|
|
|
|
151
|
|
|
# --- BEGIN: The following code can be removed for versions >= v0.7 --- |
152
|
|
|
if nominal_value is not None: |
153
|
|
|
msg = ( |
154
|
|
|
"For backward compatibility," |
155
|
|
|
+ " the option nominal_value overwrites the option" |
156
|
|
|
+ " nominal_capacity." |
157
|
|
|
+ " Both options cannot be set at the same time." |
158
|
|
|
) |
159
|
|
|
if nominal_capacity is not None: |
160
|
|
|
raise AttributeError(msg) |
161
|
|
|
else: |
162
|
|
|
warn(msg, FutureWarning) |
163
|
|
|
nominal_capacity = nominal_value |
164
|
|
|
# --- END --- |
165
|
|
|
|
166
|
|
|
super().__init__() |
167
|
|
|
|
168
|
|
|
if custom_attributes is not None: |
169
|
|
|
for attribute, value in custom_attributes.items(): |
170
|
|
|
setattr(self, attribute, value) |
171
|
|
|
|
172
|
|
|
self.nominal_capacity = None |
173
|
|
|
self.investment = None |
174
|
|
|
|
175
|
|
|
infinite_error_msg = ( |
176
|
|
|
"{} must be a finite value. Passing an infinite " |
177
|
|
|
"value is not allowed." |
178
|
|
|
) |
179
|
|
|
if isinstance(nominal_capacity, numbers.Real): |
180
|
|
|
if not math.isfinite(nominal_capacity): |
181
|
|
|
raise ValueError(infinite_error_msg.format("nominal_capacity")) |
182
|
|
|
self.nominal_capacity = nominal_capacity |
183
|
|
|
elif isinstance(nominal_capacity, Investment): |
184
|
|
|
self.investment = nominal_capacity |
185
|
|
|
|
186
|
|
|
if fixed_costs is not None: |
187
|
|
|
msg = ( |
188
|
|
|
"Be aware that the fixed costs attribute is only\n" |
189
|
|
|
"meant to be used for multi-period models to depict " |
190
|
|
|
"fixed costs that occur on a yearly basis.\n" |
191
|
|
|
"If you wish to set up a multi-period model, explicitly " |
192
|
|
|
"set the `periods` attribute of your energy system.\n" |
193
|
|
|
"It has been decided to remove the `fixed_costs` " |
194
|
|
|
"attribute with v0.2 for regular uses.\n" |
195
|
|
|
"If you specify `fixed_costs` for a regular model, " |
196
|
|
|
"this will simply be silently ignored." |
197
|
|
|
) |
198
|
|
|
warn(msg, debugging.SuspiciousUsageWarning) |
199
|
|
|
|
200
|
|
|
self.fixed_costs = sequence(fixed_costs) |
201
|
|
|
self.positive_gradient_limit = sequence(positive_gradient_limit) |
202
|
|
|
self.negative_gradient_limit = sequence(negative_gradient_limit) |
203
|
|
|
|
204
|
|
|
self.full_load_time_max = full_load_time_max |
205
|
|
|
self.full_load_time_min = full_load_time_min |
206
|
|
|
self.integer = integer |
207
|
|
|
self.nonconvex = nonconvex |
208
|
|
|
self.bidirectional = bidirectional |
209
|
|
|
self.lifetime = lifetime |
210
|
|
|
self.age = age |
211
|
|
|
|
212
|
|
|
# It is not allowed to define min or max if fix is defined. |
213
|
|
|
if fix is not None and (min is not None or max is not None): |
214
|
|
|
raise AttributeError( |
215
|
|
|
"It is not allowed to define `min`/`max` if `fix` is defined." |
216
|
|
|
) |
217
|
|
|
|
218
|
|
|
need_nominal_value = [ |
219
|
|
|
"fix", |
220
|
|
|
"full_load_time_max", |
221
|
|
|
"full_load_time_min", |
222
|
|
|
"min", |
223
|
|
|
"max", |
224
|
|
|
] |
225
|
|
|
sequences = ["fix", "variable_costs", "min", "max"] |
226
|
|
|
if self.investment is None and self.nominal_capacity is None: |
227
|
|
|
for attr in need_nominal_value: |
228
|
|
|
if isinstance(eval(attr), Iterable): |
229
|
|
|
the_attr = eval(attr)[0] |
230
|
|
|
else: |
231
|
|
|
the_attr = eval(attr) |
232
|
|
|
if the_attr is not None: |
233
|
|
|
raise AttributeError( |
234
|
|
|
f"If {attr} is set in a flow (except InvestmentFlow), " |
235
|
|
|
"nominal_value must be set as well.\n" |
236
|
|
|
"Otherwise, it won't have any effect." |
237
|
|
|
) |
238
|
|
|
# minimum will be set even without nominal limit |
239
|
|
|
|
240
|
|
|
# maximum and minimum (absolute values) should be always set, |
241
|
|
|
# as nominal_value or invest might be defined later |
242
|
|
|
if max is None: |
243
|
|
|
max = 1 |
244
|
|
|
if min is None: |
245
|
|
|
if bidirectional: |
246
|
|
|
min = -1 |
247
|
|
|
else: |
248
|
|
|
min = 0 |
249
|
|
|
|
250
|
|
|
for attr in sequences: |
251
|
|
|
setattr(self, attr, sequence(eval(attr))) |
252
|
|
|
|
253
|
|
|
if self.nominal_capacity is not None and not math.isfinite( |
254
|
|
|
self.max[0] |
255
|
|
|
): |
256
|
|
|
raise ValueError(infinite_error_msg.format("max")) |
257
|
|
|
|
258
|
|
|
# Checking for impossible gradient combinations |
259
|
|
|
if self.nonconvex: |
260
|
|
|
if self.nonconvex.positive_gradient_limit[0] is not None and ( |
261
|
|
|
self.positive_gradient_limit[0] is not None |
262
|
|
|
or self.negative_gradient_limit[0] is not None |
263
|
|
|
): |
264
|
|
|
raise ValueError( |
265
|
|
|
"You specified a positive gradient in your nonconvex " |
266
|
|
|
"option. This cannot be combined with a positive or a " |
267
|
|
|
"negative gradient for a standard flow!" |
268
|
|
|
) |
269
|
|
|
|
270
|
|
|
if self.nonconvex.negative_gradient_limit[0] is not None and ( |
271
|
|
|
self.positive_gradient_limit[0] is not None |
272
|
|
|
or self.negative_gradient_limit[0] is not None |
273
|
|
|
): |
274
|
|
|
raise ValueError( |
275
|
|
|
"You specified a negative gradient in your nonconvex " |
276
|
|
|
"option. This cannot be combined with a positive or a " |
277
|
|
|
"negative gradient for a standard flow!" |
278
|
|
|
) |
279
|
|
|
|
280
|
|
|
if ( |
281
|
|
|
self.investment |
282
|
|
|
and self.nonconvex |
283
|
|
|
and not np.isfinite(self.investment.maximum.max()) |
284
|
|
|
): |
285
|
|
|
raise AttributeError( |
286
|
|
|
"Investment into a non-convex flows needs a maximum " |
287
|
|
|
+ "investment to be set." |
288
|
|
|
) |
289
|
|
|
|