Completed
Push — dev ( 0e48bc...f6f9f6 )
by Patrik
27s queued 17s
created

solph.flows._flow   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 366
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 43
eloc 165
dl 0
loc 366
rs 8.96
c 0
b 0
f 0

1 Method

Rating   Name   Duplication   Size   Complexity  
F Flow.__init__() 0 240 43

How to fix   Complexity   

Complexity

Complex classes like solph.flows._flow often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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