Passed
Pull Request — dev (#815)
by Uwe
01:21
created

solph.flows._flow.Flow.__init__()   F

Complexity

Conditions 25

Size

Total Lines 110
Code Lines 66

Duplication

Lines 26
Ratio 23.64 %

Importance

Changes 0
Metric Value
eloc 66
dl 26
loc 110
rs 0
c 0
b 0
f 0
cc 25
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.Flow.__init__() 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 SimpleBlock
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
    max_capacity_factor : 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
    min_capacity_factor : 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
            "max_capacity_factor",
144
            "min_capacity_factor",
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, "costs": 0},
154
            "negative_gradient": {"ub": None, "costs": 0},
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
        for attribute in set(scalars + sequences + dictionaries + keys):
196
            value = kwargs.get(attribute, defaults.get(attribute))
197 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...
198
                setattr(
199
                    self,
200
                    attribute,
201
                    {"ub": sequence(value["ub"]), "costs": value["costs"]},
202
                )
203
204
            else:
205
                setattr(
206
                    self,
207
                    attribute,
208
                    sequence(value) if attribute in sequences else value,
209
                )
210
211
        # Checking for impossible attribute combinations
212
        if self.investment and self.nominal_value is not None:
213
            raise ValueError(
214
                "Using the investment object the nominal_value"
215
                " has to be set to None."
216
            )
217
        if self.investment and self.nonconvex:
218
            raise ValueError(
219
                "Investment flows cannot be combined with "
220
                + "nonconvex flows!"
221
            )
222
223
        # Checking for impossible gradient combinations
224 View Code Duplication
        if self.nonconvex:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
225
            if self.nonconvex.positive_gradient["ub"][0] is not None and (
226
                self.positive_gradient["ub"][0] is not None
227
                or self.negative_gradient["ub"][0] is not None
228
            ):
229
                raise ValueError(
230
                    "You specified a positive gradient in your nonconvex "
231
                    "option. This cannot be combined with a positive or a "
232
                    "negative gradient for a standard flow!"
233
                )
234
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.negative_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 negative 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
247
class FlowBlock(SimpleBlock):
248
    r""" FlowBlock block with definitions for standard flows.
249
250
    **The following variables are created**:
251
252
    negative_gradient :
253
        Difference of a flow in consecutive timesteps if flow is reduced
254
        indexed by NEGATIVE_GRADIENT_FLOWS, TIMESTEPS.
255
256
    positive_gradient :
257
        Difference of a flow in consecutive timesteps if flow is increased
258
        indexed by NEGATIVE_GRADIENT_FLOWS, TIMESTEPS.
259
260
    **The following sets are created:** (-> see basic sets at :class:`.Model` )
261
262
    MAX_CAPACITY_FACTOR_FLOWS
263
        A set of flows with the attribute :attr:`max_capacity_factor` being not
264
        None.
265
    MIN_CAPACITY_FACTOR_FLOWS
266
        A set of flows with the attribute :attr:`min_capacity_factor` being not
267
        None.
268
    NEGATIVE_GRADIENT_FLOWS
269
        A set of flows with the attribute :attr:`negative_gradient` being not
270
        None.
271
    POSITIVE_GRADIENT_FLOWS
272
        A set of flows with the attribute :attr:`positive_gradient` being not
273
        None
274
    INTEGER_FLOWS
275
        A set of flows where the attribute :attr:`integer` is True (forces flow
276
        to only take integer values)
277
278
    **The following constraints are build:**
279
280
    FlowBlock max sum :attr:`om.FlowBlock.max_capacity_factor[i, o]`
281
      .. math::
282
        \sum_t flow(i, o, t) \cdot \tau
283
            \leq summed\_max(i, o) \cdot nominal\_value(i, o), \\
284
        \forall (i, o) \in \textrm{SUMMED\_MAX\_FLOWS}.
285
286
    FlowBlock min sum :attr:`om.FlowBlock.min_capacity_factor[i, o]`
287
      .. math::
288
        \sum_t flow(i, o, t) \cdot \tau
289
            \geq summed\_min(i, o) \cdot nominal\_value(i, o), \\
290
        \forall (i, o) \in \textrm{SUMMED\_MIN\_FLOWS}.
291
292
    Negative gradient constraint
293
      :attr:`om.FlowBlock.negative_gradient_constr[i, o]`:
294
        .. math::
295
          flow(i, o, t-1) - flow(i, o, t) \geq \
296
          negative\_gradient(i, o, t), \\
297
          \forall (i, o) \in \textrm{NEGATIVE\_GRADIENT\_FLOWS}, \\
298
          \forall t \in \textrm{TIMESTEPS}.
299
300
    Positive gradient constraint
301
      :attr:`om.FlowBlock.positive_gradient_constr[i, o]`:
302
        .. math:: flow(i, o, t) - flow(i, o, t-1) \geq \
303
          positive\__gradient(i, o, t), \\
304
          \forall (i, o) \in \textrm{POSITIVE\_GRADIENT\_FLOWS}, \\
305
          \forall t \in \textrm{TIMESTEPS}.
306
307
    **The following parts of the objective function are created:**
308
309
    If :attr:`variable_costs` are set by the user:
310
      .. math::
311
          \sum_{(i,o)} \sum_t flow(i, o, t) \cdot variable\_costs(i, o, t)
312
313
    The expression can be accessed by :attr:`om.FlowBlock.variable_costs` and
314
    their value after optimization by :meth:`om.FlowBlock.variable_costs()` .
315
316
    """
317
318
    def __init__(self, *args, **kwargs):
319
        super().__init__(*args, **kwargs)
320
321
    def _create(self, group=None):
322
        r"""Creates sets, variables and constraints for all standard flows.
323
324
        Parameters
325
        ----------
326
        group : list
327
            List containing tuples containing flow (f) objects and the
328
            associated source (s) and target (t)
329
            of flow e.g. groups=[(s1, t1, f1), (s2, t2, f2),..]
330
        """
331
        if group is None:
332
            return None
333
334
        m = self.parent_block()
335
336
        # ########################## SETS #################################
337
        # set for all flows with an global limit on the flow over time
338
        self.MAX_CAPACITY_FACTOR_FLOWS = Set(
339
            initialize=[
340
                (g[0], g[1])
341
                for g in group
342
                if g[2].max_capacity_factor is not None
343
                and g[2].nominal_value is not None
344
            ]
345
        )
346
347
        self.MIN_CAPACITY_FACTOR_FLOWS = Set(
348
            initialize=[
349
                (g[0], g[1])
350
                for g in group
351
                if g[2].min_capacity_factor is not None
352
                and g[2].nominal_value is not None
353
            ]
354
        )
355
356
        self.NEGATIVE_GRADIENT_FLOWS = Set(
357
            initialize=[
358
                (g[0], g[1])
359
                for g in group
360
                if g[2].negative_gradient["ub"][0] is not None
361
            ]
362
        )
363
364
        self.POSITIVE_GRADIENT_FLOWS = Set(
365
            initialize=[
366
                (g[0], g[1])
367
                for g in group
368
                if g[2].positive_gradient["ub"][0] is not None
369
            ]
370
        )
371
372
        self.INTEGER_FLOWS = Set(
373
            initialize=[(g[0], g[1]) for g in group if g[2].integer]
374
        )
375
        # ######################### Variables  ################################
376
377
        self.positive_gradient = Var(self.POSITIVE_GRADIENT_FLOWS, m.TIMESTEPS)
378
379
        self.negative_gradient = Var(self.NEGATIVE_GRADIENT_FLOWS, m.TIMESTEPS)
380
381
        self.integer_flow = Var(
382
            self.INTEGER_FLOWS, m.TIMESTEPS, within=NonNegativeIntegers
383
        )
384
        # set upper bound of gradient variable
385
        for i, o, f in group:
386
            if m.flows[i, o].positive_gradient["ub"][0] is not None:
387
                for t in m.TIMESTEPS:
388
                    self.positive_gradient[i, o, t].setub(
389
                        f.positive_gradient["ub"][t] * f.nominal_value
390
                    )
391
            if m.flows[i, o].negative_gradient["ub"][0] is not None:
392
                for t in m.TIMESTEPS:
393
                    self.negative_gradient[i, o, t].setub(
394
                        f.negative_gradient["ub"][t] * f.nominal_value
395
                    )
396
397
        # ######################### CONSTRAINTS ###############################
398
399
        def _flow_max_capacity_factor_rule(model):
400
            """Rule definition for build action of max. sum flow constraint."""
401
            for inp, out in self.MAX_CAPACITY_FACTOR_FLOWS:
402
                lhs = sum(
403
                    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...
404
                    for ts in m.TIMESTEPS
405
                )
406
                rhs = (
407
                    m.flows[inp, out].max_capacity_factor
408
                    * m.flows[inp, out].nominal_value
409
                )
410
                self.max_capacity_factor.add((inp, out), lhs <= rhs)
411
412
        self.max_capacity_factor = Constraint(
413
            self.MAX_CAPACITY_FACTOR_FLOWS, noruleinit=True
414
        )
415
        self.max_capacity_factor_build = BuildAction(
416
            rule=_flow_max_capacity_factor_rule
417
        )
418
419
        def _flow_min_capacity_factor_rule(model):
420
            """Rule definition for build action of min. sum flow constraint."""
421
            for inp, out in self.MIN_CAPACITY_FACTOR_FLOWS:
422
                lhs = sum(
423
                    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...
424
                    for ts in m.TIMESTEPS
425
                )
426
                rhs = (
427
                    m.flows[inp, out].min_capacity_factor
428
                    * m.flows[inp, out].nominal_value
429
                )
430
                self.min_capacity_factor.add((inp, out), lhs >= rhs)
431
432
        self.min_capacity_factor = Constraint(
433
            self.MIN_CAPACITY_FACTOR_FLOWS, noruleinit=True
434
        )
435
        self.min_capacity_factor_build = BuildAction(
436
            rule=_flow_min_capacity_factor_rule
437
        )
438
439
        def _positive_gradient_flow_rule(model):
440
            """Rule definition for positive gradient constraint."""
441
            for inp, out in self.POSITIVE_GRADIENT_FLOWS:
442
                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...
443
                    if ts > 0:
444
                        lhs = m.flow[inp, out, ts] - m.flow[inp, out, ts - 1]
445
                        rhs = self.positive_gradient[inp, out, ts]
446
                        self.positive_gradient_constr.add(
447
                            (inp, out, ts), lhs <= rhs
448
                        )
449
                    else:
450
                        pass  # return(Constraint.Skip)
451
452
        self.positive_gradient_constr = Constraint(
453
            self.POSITIVE_GRADIENT_FLOWS, m.TIMESTEPS, noruleinit=True
454
        )
455
        self.positive_gradient_build = BuildAction(
456
            rule=_positive_gradient_flow_rule
457
        )
458
459
        def _negative_gradient_flow_rule(model):
460
            """Rule definition for negative gradient constraint."""
461
            for inp, out in self.NEGATIVE_GRADIENT_FLOWS:
462
                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...
463
                    if ts > 0:
464
                        lhs = m.flow[inp, out, ts - 1] - m.flow[inp, out, ts]
465
                        rhs = self.negative_gradient[inp, out, ts]
466
                        self.negative_gradient_constr.add(
467
                            (inp, out, ts), lhs <= rhs
468
                        )
469
                    else:
470
                        pass  # return(Constraint.Skip)
471
472
        self.negative_gradient_constr = Constraint(
473
            self.NEGATIVE_GRADIENT_FLOWS, m.TIMESTEPS, noruleinit=True
474
        )
475
        self.negative_gradient_build = BuildAction(
476
            rule=_negative_gradient_flow_rule
477
        )
478
479
        def _integer_flow_rule(block, ii, oi, ti):
480
            """Force flow variable to NonNegativeInteger values."""
481
            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...
482
483
        self.integer_flow_constr = Constraint(
484
            self.INTEGER_FLOWS, m.TIMESTEPS, rule=_integer_flow_rule
485
        )
486
487
    def _objective_expression(self):
488
        r"""Objective expression for all standard flows with fixed costs
489
        and variable costs.
490
        """
491
        m = self.parent_block()
492
493
        variable_costs = 0
494
        gradient_costs = 0
495
496
        for i, o in m.FLOWS:
497
            if m.flows[i, o].variable_costs[0] is not None:
498
                for t in m.TIMESTEPS:
499
                    variable_costs += (
500
                        m.flow[i, o, t]
501
                        * m.objective_weighting[t]
502
                        * m.flows[i, o].variable_costs[t]
503
                    )
504
505
            if m.flows[i, o].positive_gradient["ub"][0] is not None:
506
                for t in m.TIMESTEPS:
507
                    gradient_costs += (
508
                        self.positive_gradient[i, o, t]
509
                        * m.flows[i, o].positive_gradient["costs"]
510
                    )
511
512
            if m.flows[i, o].negative_gradient["ub"][0] is not None:
513
                for t in m.TIMESTEPS:
514
                    gradient_costs += (
515
                        self.negative_gradient[i, o, t]
516
                        * m.flows[i, o].negative_gradient["costs"]
517
                    )
518
519
        return variable_costs + gradient_costs
520