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