Passed
Pull Request — dev (#821)
by Uwe
02:17
created

solph.flows._flow.FlowBlock._create()   F

Complexity

Conditions 15

Size

Total Lines 152
Code Lines 91

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 91
dl 0
loc 152
rs 2.3945
c 0
b 0
f 0
cc 15
nop 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like solph.flows._flow.FlowBlock._create() 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 including base constraints
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
14
SPDX-License-Identifier: MIT
15
16
"""
17
18
from warnings import warn
19
20
from oemof.network import network as on
21
from oemof.tools import debugging
22
from pyomo.core import BuildAction
23
from pyomo.core import Constraint
24
from pyomo.core import NonNegativeIntegers
25
from pyomo.core import Set
26
from pyomo.core import Var
27
from pyomo.core.base.block import ScalarBlock
28
29
from oemof.solph._plumbing import sequence
30
31
32
class Flow(on.Edge):
33
    r"""Defines a flow between two nodes.
34
35
    Keyword arguments are used to set the attributes of this flow. Parameters
36
    which are handled specially are noted below.
37
    For the case where a parameter can be either a scalar or an iterable, a
38
    scalar value will be converted to a sequence containing the scalar value at
39
    every index. This sequence is then stored under the paramter's key.
40
41
    Parameters
42
    ----------
43
    nominal_value : numeric, :math:`P_{nom}`
44
        The nominal value of the flow. If this value is set the corresponding
45
        optimization variable of the flow object will be bounded by this value
46
        multiplied with min(lower bound)/max(upper bound).
47
    max : numeric (iterable or scalar), :math:`f_{max}`
48
        Normed maximum value of the flow. The flow absolute maximum will be
49
        calculated by multiplying :attr:`nominal_value` with :attr:`max`
50
    min : numeric (iterable or scalar), :math:`f_{min}`
51
        Normed minimum value of the flow (see :attr:`max`).
52
    fix : numeric (iterable or scalar), :math:`f_{actual}`
53
        Normed fixed value for the flow variable. Will be multiplied with the
54
        :attr:`nominal_value` to get the absolute value. If :attr:`fixed` is
55
        set to :obj:`True` the flow variable will be fixed to `fix
56
        * nominal_value`, i.e. this value is set exogenous.
57
    positive_gradient : :obj:`dict`, default: `{'ub': None, 'costs': 0}`
58
        A dictionary containing the following two keys:
59
60
         * `'ub'`: numeric (iterable, scalar or None), the normed *upper
61
           bound* on the positive difference (`flow[t-1] < flow[t]`) of
62
           two consecutive flow values.
63
         * `'costs``: numeric (scalar or None), the gradient cost per
64
           unit.
65
66
    negative_gradient : :obj:`dict`, default: `{'ub': None, 'costs': 0}`
67
68
        A dictionary containing the following two keys:
69
70
          * `'ub'`: numeric (iterable, scalar or None), the normed *upper
71
            bound* on the negative difference (`flow[t-1] > flow[t]`) of
72
            two consecutive flow values.
73
          * `'costs``: numeric (scalar or None), the gradient cost per
74
            unit.
75
76
    summed_max : numeric, :math:`f_{sum,max}`
77
        Specific maximum value summed over all timesteps. Will be multiplied
78
        with the nominal_value to get the absolute limit.
79
    summed_min : numeric, :math:`f_{sum,min}`
80
        see above
81
    variable_costs : numeric (iterable or scalar)
82
        The costs associated with one unit of the flow. If this is set the
83
        costs will be added to the objective expression of the optimization
84
        problem.
85
    fixed : boolean
86
        Boolean value indicating if a flow is fixed during the optimization
87
        problem to its ex-ante set value. Used in combination with the
88
        :attr:`fix`.
89
    investment : :class:`Investment <oemof.solph.options.Investment>`
90
        Object indicating if a nominal_value of the flow is determined by
91
        the optimization problem. Note: This will refer all attributes to an
92
        investment variable instead of to the nominal_value. The nominal_value
93
        should not be set (or set to None) if an investment object is used.
94
    nonconvex : :class:`NonConvex <oemof.solph.options.NonConvex>`
95
        If a nonconvex flow object is added here, the flow constraints will
96
        be altered significantly as the mathematical model for the flow
97
        will be different, i.e. constraint etc. from
98
        :class:`NonConvexFlowBlock <oemof.solph.blocks.NonConvexFlowBlock>`
99
        will be used instead of
100
        :class:`FlowBlock <oemof.solph.blocks.FlowBlock>`.
101
        Note: at the moment this does not work if the investment attribute is
102
        set .
103
104
    Notes
105
    -----
106
    The following sets, variables, constraints and objective parts are created
107
     * :py:class:`~oemof.solph..flows.flow.FlowBlock`
108
     * :py:class:`~oemof.solph..flows.investment_flow.InvestmentFlowBlock`
109
        (additionally if Investment object is present)
110
     * :py:class:`~oemof.solph..flows.non_convex_flow.NonConvexFlowBlock`
111
        (If nonconvex  object is present, CAUTION: replaces
112
        :py:class:`~oemof.solph.flows.flow.FlowBlock`
113
        class and a MILP will be build)
114
115
    Examples
116
    --------
117
    Creating a fixed flow object:
118
119
    >>> f = Flow(fix=[10, 4, 4], variable_costs=5)
120
    >>> f.variable_costs[2]
121
    5
122
    >>> f.fix[2]
123
    4
124
125
    Creating a flow object with time-depended lower and upper bounds:
126
127
    >>> f1 = Flow(min=[0.2, 0.3], max=0.99, nominal_value=100)
128
    >>> f1.max[1]
129
    0.99
130
    """
131
132
    def __init__(self, **kwargs):
133
        # TODO: Check if we can inherit from pyomo.core.base.var _VarData
134
        # then we need to create the var object with
135
        # pyomo.core.base.IndexedVarWithDomain before any FlowBlock is created.
136
        # E.g. create the variable in the energy system and populate with
137
        # information afterwards when creating objects.
138
139
        super().__init__()
140
141
        scalars = [
142
            "nominal_value",
143
            "summed_max",
144
            "summed_min",
145
            "investment",
146
            "nonconvex",
147
            "integer",
148
        ]
149
        sequences = ["fix", "variable_costs", "min", "max"]
150
        dictionaries = ["positive_gradient", "negative_gradient"]
151
        defaults = {
152
            "variable_costs": 0,
153
            "positive_gradient": {"ub": None},
154
            "negative_gradient": {"ub": None},
155
        }
156
        keys = [k for k in kwargs if k != "label"]
157
158
        if "fixed_costs" in keys:
159
            raise AttributeError(
160
                "The `fixed_costs` attribute has been removed" " with v0.2!"
161
            )
162
163
        if "actual_value" in keys:
164
            raise AttributeError(
165
                "The `actual_value` attribute has been renamed"
166
                " to `fix` with v0.4. The attribute `fixed` is"
167
                " set to True automatically when passing `fix`."
168
            )
169
170
        if "fixed" in keys:
171
            msg = (
172
                "The `fixed` attribute is deprecated.\nIf you have defined "
173
                "the `fix` attribute the flow variable will be fixed.\n"
174
                "The `fixed` attribute does not change anything."
175
            )
176
            warn(msg, debugging.SuspiciousUsageWarning)
177
178
        # It is not allowed to define min or max if fix is defined.
179
        if kwargs.get("fix") is not None and (
180
            kwargs.get("min") is not None or kwargs.get("max") is not None
181
        ):
182
            raise AttributeError(
183
                "It is not allowed to define min/max if fix is defined."
184
            )
185
186
        # Set default value for min and max
187
        if kwargs.get("min") is None:
188
            if "bidirectional" in keys:
189
                defaults["min"] = -1
190
            else:
191
                defaults["min"] = 0
192
        if kwargs.get("max") is None:
193
            defaults["max"] = 1
194
195
        # Check gradient dictionaries for non-valid keys
196
        for gradient_dict in ["negative_gradient", "positive_gradient"]:
197
            if gradient_dict in kwargs:
198
                if list(kwargs[gradient_dict].keys()) != list(
199
                    defaults[gradient_dict].keys()
200
                ):
201
                    msg = (
202
                        "Only the key 'ub' is allowed for the '{0}' attribute"
203
                    )
204
                    raise AttributeError(msg.format(gradient_dict))
205
206
        for attribute in set(scalars + sequences + dictionaries + keys):
207
            value = kwargs.get(attribute, defaults.get(attribute))
208 View Code Duplication
            if attribute in dictionaries:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
209
                setattr(
210
                    self,
211
                    attribute,
212
                    {"ub": sequence(value["ub"])},
213
                )
214
215
            else:
216
                setattr(
217
                    self,
218
                    attribute,
219
                    sequence(value) if attribute in sequences else value,
220
                )
221
222
        # Checking for impossible attribute combinations
223
        if self.investment and self.nominal_value is not None:
224
            raise ValueError(
225
                "Using the investment object the nominal_value"
226
                " has to be set to None."
227
            )
228
        if self.investment and self.nonconvex:
229
            raise ValueError(
230
                "Investment flows cannot be combined with "
231
                + "nonconvex flows!"
232
            )
233
234
        # Checking for impossible gradient combinations
235 View Code Duplication
        if self.nonconvex:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
236
            if self.nonconvex.positive_gradient["ub"][0] is not None and (
237
                self.positive_gradient["ub"][0] is not None
238
                or self.negative_gradient["ub"][0] is not None
239
            ):
240
                raise ValueError(
241
                    "You specified a positive gradient in your nonconvex "
242
                    "option. This cannot be combined with a positive or a "
243
                    "negative gradient for a standard flow!"
244
                )
245
246 View Code Duplication
        if self.nonconvex:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
247
            if self.nonconvex.negative_gradient["ub"][0] is not None and (
248
                self.positive_gradient["ub"][0] is not None
249
                or self.negative_gradient["ub"][0] is not None
250
            ):
251
                raise ValueError(
252
                    "You specified a negative gradient in your nonconvex "
253
                    "option. This cannot be combined with a positive or a "
254
                    "negative gradient for a standard flow!"
255
                )
256
257
258
class FlowBlock(ScalarBlock):
259
    r""" FlowBlock block with definitions for standard flows.
260
261
    **The following variables are created**:
262
263
    negative_gradient :
264
        Difference of a flow in consecutive timesteps if flow is reduced
265
        indexed by NEGATIVE_GRADIENT_FLOWS, TIMESTEPS.
266
267
    positive_gradient :
268
        Difference of a flow in consecutive timesteps if flow is increased
269
        indexed by NEGATIVE_GRADIENT_FLOWS, TIMESTEPS.
270
271
    **The following sets are created:** (-> see basic sets at :class:`.Model` )
272
273
    SUMMED_MAX_FLOWS
274
        A set of flows with the attribute :attr:`summed_max` being not None.
275
    SUMMED_MIN_FLOWS
276
        A set of flows with the attribute :attr:`summed_min` being not None.
277
    NEGATIVE_GRADIENT_FLOWS
278
        A set of flows with the attribute :attr:`negative_gradient` being not
279
        None.
280
    POSITIVE_GRADIENT_FLOWS
281
        A set of flows with the attribute :attr:`positive_gradient` being not
282
        None
283
    INTEGER_FLOWS
284
        A set of flows where the attribute :attr:`integer` is True (forces flow
285
        to only take integer values)
286
287
    **The following constraints are build:**
288
289
    FlowBlock max sum :attr:`om.FlowBlock.summed_max[i, o]`
290
      .. math::
291
        \sum_t flow(i, o, t) \cdot \tau
292
            \leq summed\_max(i, o) \cdot nominal\_value(i, o), \\
293
        \forall (i, o) \in \textrm{SUMMED\_MAX\_FLOWS}.
294
295
    FlowBlock min sum :attr:`om.FlowBlock.summed_min[i, o]`
296
      .. math::
297
        \sum_t flow(i, o, t) \cdot \tau
298
            \geq summed\_min(i, o) \cdot nominal\_value(i, o), \\
299
        \forall (i, o) \in \textrm{SUMMED\_MIN\_FLOWS}.
300
301
    Negative gradient constraint
302
      :attr:`om.FlowBlock.negative_gradient_constr[i, o]`:
303
        .. math::
304
          flow(i, o, t-1) - flow(i, o, t) \geq \
305
          negative\_gradient(i, o, t), \\
306
          \forall (i, o) \in \textrm{NEGATIVE\_GRADIENT\_FLOWS}, \\
307
          \forall t \in \textrm{TIMESTEPS}.
308
309
    Positive gradient constraint
310
      :attr:`om.FlowBlock.positive_gradient_constr[i, o]`:
311
        .. math:: flow(i, o, t) - flow(i, o, t-1) \geq \
312
          positive\__gradient(i, o, t), \\
313
          \forall (i, o) \in \textrm{POSITIVE\_GRADIENT\_FLOWS}, \\
314
          \forall t \in \textrm{TIMESTEPS}.
315
316
    **The following parts of the objective function are created:**
317
318
    If :attr:`variable_costs` are set by the user:
319
      .. math::
320
          \sum_{(i,o)} \sum_t flow(i, o, t) \cdot variable\_costs(i, o, t)
321
322
    The expression can be accessed by :attr:`om.FlowBlock.variable_costs` and
323
    their value after optimization by :meth:`om.FlowBlock.variable_costs()` .
324
325
    """
326
327
    def __init__(self, *args, **kwargs):
328
        super().__init__(*args, **kwargs)
329
330
    def _create(self, group=None):
331
        r"""Creates sets, variables and constraints for all standard flows.
332
333
        Parameters
334
        ----------
335
        group : list
336
            List containing tuples containing flow (f) objects and the
337
            associated source (s) and target (t)
338
            of flow e.g. groups=[(s1, t1, f1), (s2, t2, f2),..]
339
        """
340
        if group is None:
341
            return None
342
343
        m = self.parent_block()
344
345
        # ########################## SETS #################################
346
        # set for all flows with an global limit on the flow over time
347
        self.SUMMED_MAX_FLOWS = Set(
348
            initialize=[
349
                (g[0], g[1])
350
                for g in group
351
                if g[2].summed_max is not None
352
                and g[2].nominal_value is not None
353
            ]
354
        )
355
356
        self.SUMMED_MIN_FLOWS = Set(
357
            initialize=[
358
                (g[0], g[1])
359
                for g in group
360
                if g[2].summed_min is not None
361
                and g[2].nominal_value is not None
362
            ]
363
        )
364
365
        self.NEGATIVE_GRADIENT_FLOWS = Set(
366
            initialize=[
367
                (g[0], g[1])
368
                for g in group
369
                if g[2].negative_gradient["ub"][0] is not None
370
            ]
371
        )
372
373
        self.POSITIVE_GRADIENT_FLOWS = Set(
374
            initialize=[
375
                (g[0], g[1])
376
                for g in group
377
                if g[2].positive_gradient["ub"][0] is not None
378
            ]
379
        )
380
381
        self.INTEGER_FLOWS = Set(
382
            initialize=[(g[0], g[1]) for g in group if g[2].integer]
383
        )
384
        # ######################### Variables  ################################
385
386
        self.positive_gradient = Var(self.POSITIVE_GRADIENT_FLOWS, m.TIMESTEPS)
387
388
        self.negative_gradient = Var(self.NEGATIVE_GRADIENT_FLOWS, m.TIMESTEPS)
389
390
        self.integer_flow = Var(
391
            self.INTEGER_FLOWS, m.TIMESTEPS, within=NonNegativeIntegers
392
        )
393
        # set upper bound of gradient variable
394
        for i, o, f in group:
395
            if m.flows[i, o].positive_gradient["ub"][0] is not None:
396
                for t in m.TIMESTEPS:
397
                    self.positive_gradient[i, o, t].setub(
398
                        f.positive_gradient["ub"][t] * f.nominal_value
399
                    )
400
            if m.flows[i, o].negative_gradient["ub"][0] is not None:
401
                for t in m.TIMESTEPS:
402
                    self.negative_gradient[i, o, t].setub(
403
                        f.negative_gradient["ub"][t] * f.nominal_value
404
                    )
405
406
        # ######################### CONSTRAINTS ###############################
407
408
        def _flow_summed_max_rule(model):
409
            """Rule definition for build action of max. sum flow constraint."""
410
            for inp, out in self.SUMMED_MAX_FLOWS:
411
                lhs = sum(
412
                    m.flow[inp, out, ts] * m.timeincrement[ts]
0 ignored issues
show
introduced by
The variable m does not seem to be defined for all execution paths.
Loading history...
413
                    for ts in m.TIMESTEPS
414
                )
415
                rhs = (
416
                    m.flows[inp, out].summed_max
417
                    * m.flows[inp, out].nominal_value
418
                )
419
                self.summed_max.add((inp, out), lhs <= rhs)
420
421
        self.summed_max = Constraint(self.SUMMED_MAX_FLOWS, noruleinit=True)
422
        self.summed_max_build = BuildAction(rule=_flow_summed_max_rule)
423
424
        def _flow_summed_min_rule(model):
425
            """Rule definition for build action of min. sum flow constraint."""
426
            for inp, out in self.SUMMED_MIN_FLOWS:
427
                lhs = sum(
428
                    m.flow[inp, out, ts] * m.timeincrement[ts]
0 ignored issues
show
introduced by
The variable m does not seem to be defined for all execution paths.
Loading history...
429
                    for ts in m.TIMESTEPS
430
                )
431
                rhs = (
432
                    m.flows[inp, out].summed_min
433
                    * m.flows[inp, out].nominal_value
434
                )
435
                self.summed_min.add((inp, out), lhs >= rhs)
436
437
        self.summed_min = Constraint(self.SUMMED_MIN_FLOWS, noruleinit=True)
438
        self.summed_min_build = BuildAction(rule=_flow_summed_min_rule)
439
440
        def _positive_gradient_flow_rule(model):
441
            """Rule definition for positive gradient constraint."""
442
            for inp, out in self.POSITIVE_GRADIENT_FLOWS:
443
                for ts in m.TIMESTEPS:
0 ignored issues
show
introduced by
The variable m does not seem to be defined for all execution paths.
Loading history...
444
                    if ts > 0:
445
                        lhs = m.flow[inp, out, ts] - m.flow[inp, out, ts - 1]
446
                        rhs = self.positive_gradient[inp, out, ts]
447
                        self.positive_gradient_constr.add(
448
                            (inp, out, ts), lhs <= rhs
449
                        )
450
451
        self.positive_gradient_constr = Constraint(
452
            self.POSITIVE_GRADIENT_FLOWS, m.TIMESTEPS, noruleinit=True
453
        )
454
        self.positive_gradient_build = BuildAction(
455
            rule=_positive_gradient_flow_rule
456
        )
457
458
        def _negative_gradient_flow_rule(model):
459
            """Rule definition for negative gradient constraint."""
460
            for inp, out in self.NEGATIVE_GRADIENT_FLOWS:
461
                for ts in m.TIMESTEPS:
0 ignored issues
show
introduced by
The variable m does not seem to be defined for all execution paths.
Loading history...
462
                    if ts > 0:
463
                        lhs = m.flow[inp, out, ts - 1] - m.flow[inp, out, ts]
464
                        rhs = self.negative_gradient[inp, out, ts]
465
                        self.negative_gradient_constr.add(
466
                            (inp, out, ts), lhs <= rhs
467
                        )
468
469
        self.negative_gradient_constr = Constraint(
470
            self.NEGATIVE_GRADIENT_FLOWS, m.TIMESTEPS, noruleinit=True
471
        )
472
        self.negative_gradient_build = BuildAction(
473
            rule=_negative_gradient_flow_rule
474
        )
475
476
        def _integer_flow_rule(block, ii, oi, ti):
477
            """Force flow variable to NonNegativeInteger values."""
478
            return self.integer_flow[ii, oi, ti] == m.flow[ii, oi, ti]
0 ignored issues
show
introduced by
The variable m does not seem to be defined for all execution paths.
Loading history...
479
480
        self.integer_flow_constr = Constraint(
481
            self.INTEGER_FLOWS, m.TIMESTEPS, rule=_integer_flow_rule
482
        )
483
484
    def _objective_expression(self):
485
        r"""Objective expression for all standard flows with fixed costs
486
        and variable costs.
487
        """
488
        m = self.parent_block()
489
490
        variable_costs = 0
491
492
        for i, o in m.FLOWS:
493
            if m.flows[i, o].variable_costs[0] is not None:
494
                for t in m.TIMESTEPS:
495
                    variable_costs += (
496
                        m.flow[i, o, t]
497
                        * m.objective_weighting[t]
498
                        * m.flows[i, o].variable_costs[t]
499
                    )
500
501
        return variable_costs
502