InvestmentFlowBlock._create_constraints()   F
last analyzed

Complexity

Conditions 32

Size

Total Lines 478
Code Lines 164

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 164
dl 0
loc 478
rs 0
c 0
b 0
f 0
cc 32
nop 1

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._investment_flow_block.InvestmentFlowBlock._create_constraints() 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
"""Creating sets, variables, constraints and parts of the objective function
4
for Flow objects with investment but without nonconvex option.
5
6
SPDX-FileCopyrightText: Uwe Krien <[email protected]>
7
SPDX-FileCopyrightText: Simon Hilpert
8
SPDX-FileCopyrightText: Cord Kaldemeyer
9
SPDX-FileCopyrightText: Patrik Schönfeldt
10
SPDX-FileCopyrightText: Birgit Schachler
11
SPDX-FileCopyrightText: jnnr
12
SPDX-FileCopyrightText: jmloenneberga
13
SPDX-FileCopyrightText: Johannes Kochems
14
15
SPDX-License-Identifier: MIT
16
17
"""
18
from warnings import warn
19
20
import numpy as np
21
from oemof.tools import debugging
22
from oemof.tools import economics
23
from pyomo.core import Binary
24
from pyomo.core import BuildAction
25
from pyomo.core import Constraint
26
from pyomo.core import Expression
27
from pyomo.core import NonNegativeReals
28
from pyomo.core import Set
29
from pyomo.core import Var
30
from pyomo.core.base.block import ScalarBlock
31
32
from oemof.solph._plumbing import valid_sequence
33
34
35
class InvestmentFlowBlock(ScalarBlock):
36
    r"""Block for all flows with :attr:`Investment` being not None.
37
38
    .. automethod:: _create_constraints
39
    .. automethod:: _create_variables
40
    .. automethod:: _create_sets
41
42
    .. automethod:: _objective_expression
43
44
    See :class:`oemof.solph.options.Investment` for all parameters of the
45
    *Investment* class.
46
47
    See :class:`oemof.solph.flows._simple_flow_block.SimpleFlowBlock`
48
    for all parameters of the *SimpleFlowBlock* class.
49
50
    The overall summed cost expressions for all *InvestmentFlowBlock* objects
51
    can be accessed by
52
53
    * :attr:`om.InvestmentFlowBlock.investment_costs`,
54
    * :attr:`om.InvestmentFlowBlock.fixed_costs` and
55
    * :attr:`om.InvestmentFlowBlock.costs`.
56
57
    Their values  after optimization can be retrieved by
58
59
    * :meth:`om.InvestmentFlowBlock.investment_costs`,
60
    * :attr:`om.InvestmentFlowBlock.period_investment_costs` (yielding a dict
61
      keyed by periods); note: this is not a Pyomo expression, but calculated,
62
    * :meth:`om.InvestmentFlowBlock.fixed_costs` and
63
    * :meth:`om.InvestmentFlowBlock.costs`.
64
65
    Note
66
    ----
67
    In case of a nonconvex investment flow (:attr:`nonconvex=True`),
68
    the existing flow capacity :math:`P_{exist}` needs to be zero.
69
70
    Note
71
    ----
72
    See also :class:`~oemof.solph.flows._flow.Flow`,
73
    :class:`~oemof.solph.flows._simple_flow_block.SimpleFlowBlock` and
74
    :class:`~oemof.solph._options.Investment`
75
76
    """  # noqa: E501
77
78
    def __init__(self, *args, **kwargs):
79
        super().__init__(*args, **kwargs)
80
81
    def _create(self, group=None):
82
        r"""Creates sets, variables and constraints for SimpleFlowBlock
83
        with investment attribute of type class:`.Investment`.
84
85
        Parameters
86
        ----------
87
        group : list
88
            List containing tuples containing flow (f) objects that have an
89
            attribute investment and the associated source (s) and target (t)
90
            of flow e.g. groups=[(s1, t1, f1), (s2, t2, f2),..]
91
        """
92
        if group is None:
93
            return None
94
95
        self._create_sets(group)
96
        self._create_variables(group)
97
        self._create_constraints()
98
99
    def _create_sets(self, group):
100
        """
101
        Creates all sets for investment flows.
102
        """
103
        self.INVESTFLOWS = Set(initialize=[(g[0], g[1]) for g in group])
104
105
        self.CONVEX_INVESTFLOWS = Set(
106
            initialize=[
107
                (g[0], g[1])
108
                for g in group
109
                if g[2].investment.nonconvex is False
110
            ]
111
        )
112
113
        self.NON_CONVEX_INVESTFLOWS = Set(
114
            initialize=[
115
                (g[0], g[1])
116
                for g in group
117
                if g[2].investment.nonconvex is True
118
            ]
119
        )
120
121
        self.FIXED_INVESTFLOWS = Set(
122
            initialize=[(g[0], g[1]) for g in group if g[2].fix[0] is not None]
123
        )
124
125
        self.NON_FIXED_INVESTFLOWS = Set(
126
            initialize=[(g[0], g[1]) for g in group if g[2].fix[0] is None]
127
        )
128
129
        self.FULL_LOAD_TIME_MAX_INVESTFLOWS = Set(
130
            initialize=[
131
                (g[0], g[1])
132
                for g in group
133
                if g[2].full_load_time_max is not None
134
            ]
135
        )
136
137
        self.FULL_LOAD_TIME_MIN_INVESTFLOWS = Set(
138
            initialize=[
139
                (g[0], g[1])
140
                for g in group
141
                if g[2].full_load_time_min is not None
142
            ]
143
        )
144
145
        self.MIN_INVESTFLOWS = Set(
146
            initialize=[(g[0], g[1]) for g in group if g[2].min.min() != 0]
147
        )
148
149
        self.EXISTING_INVESTFLOWS = Set(
150
            initialize=[
151
                (g[0], g[1])
152
                for g in group
153
                if g[2].investment.existing is not None
154
            ]
155
        )
156
157
        self.OVERALL_MAXIMUM_INVESTFLOWS = Set(
158
            initialize=[
159
                (g[0], g[1])
160
                for g in group
161
                if g[2].investment.overall_maximum is not None
162
            ]
163
        )
164
165
        self.OVERALL_MINIMUM_INVESTFLOWS = Set(
166
            initialize=[
167
                (g[0], g[1])
168
                for g in group
169
                if g[2].investment.overall_minimum is not None
170
            ]
171
        )
172
173
    def _create_variables(self, _):
174
        r"""Creates all variables for investment flows.
175
176
        All *InvestmentFlowBlock* objects are indexed by a starting and
177
        ending node :math:`(i, o)`, which is omitted in the following
178
        for the sake of convenience. The following variables are created:
179
180
        * :math:`P(p, t)`
181
182
            Actual flow value
183
            (created in :class:`oemof.solph.models.Model`),
184
            indexed by tuple of periods p and timestep t
185
186
        * :math:`P_{invest}(p)`
187
188
            Value of the investment variable in period p,
189
            equal to what is being invested and equivalent resp. similar to
190
            the nominal capacity of the flows after optimization.
191
192
        * :math:`P_{total}(p)`
193
194
            Total installed capacity / energy in period p,
195
            equivalent to the nominal capacity of the flows after optimization.
196
197
        * :math:`P_{old}(p)`
198
199
            Old capacity / energy to be decommissioned in period p
200
            due to reaching its lifetime; applicable only
201
            for multi-period models.
202
203
        * :math:`P_{old,exo}(p)`
204
205
            Old exogenous capacity / energy to be decommissioned in period p
206
            due to reaching its lifetime, i.e. the amount that has
207
            been specified by :attr:`existing` when it is decommisioned;
208
            applicable only for multi-period models.
209
210
        * :math:`P_{old,end}(p)`
211
212
            Old endogenous capacity / energy to be decommissioned in period p
213
            due to reaching its lifetime, i.e. the amount that has been
214
            invested in by the model itself that is decommissioned in
215
            a later period because of reaching its lifetime;
216
            applicable only for multi-period models.
217
218
        * :math:`Y_{invest}(p)`
219
220
            Binary variable for the status of the investment, if
221
            :attr:`nonconvex` is `True`.
222
        """
223
        m = self.parent_block()
224
225
        def _investvar_bound_rule(block, i, o, p):
226
            """Rule definition for bounds of invest variable."""
227
            if (i, o) in self.CONVEX_INVESTFLOWS:
228
                return (
229
                    m.flows[i, o].investment.minimum[p],
230
                    m.flows[i, o].investment.maximum[p],
231
                )
232
            elif (i, o) in self.NON_CONVEX_INVESTFLOWS:
233
                return 0, m.flows[i, o].investment.maximum[p]
234
235
        # create invest variable for an investment flow
236
        self.invest = Var(
237
            self.INVESTFLOWS,
238
            m.PERIODS,
239
            within=NonNegativeReals,
240
            bounds=_investvar_bound_rule,
241
        )
242
243
        # Total capacity
244
        self.total = Var(self.INVESTFLOWS, m.PERIODS, within=NonNegativeReals)
245
246
        if m.es.periods is not None:
247
            self.old = Var(
248
                self.INVESTFLOWS, m.PERIODS, within=NonNegativeReals
249
            )
250
251
            # Old endogenous capacity to be decommissioned (due to lifetime)
252
            self.old_end = Var(
253
                self.INVESTFLOWS, m.PERIODS, within=NonNegativeReals
254
            )
255
256
            # Old exogenous capacity to be decommissioned (due to lifetime)
257
            self.old_exo = Var(
258
                self.INVESTFLOWS, m.PERIODS, within=NonNegativeReals
259
            )
260
261
        # create status variable for a non-convex investment flow
262
        self.invest_status = Var(
263
            self.NON_CONVEX_INVESTFLOWS, m.PERIODS, within=Binary
264
        )
265
266
    def _create_constraints(self):
267
        r"""Creates all constraints for standard flows.
268
269
        Depending on the attributes of the *InvestmentFlowBlock*
270
        and *SimpleFlowBlock*, different constraints are created.
271
        The following constraints are created
272
        for all *InvestmentFlowBlock* objects:\
273
274
            Total capacity / energy
275
276
            .. math::
277
                &
278
                if \quad p=0:\\
279
                &
280
                P_{total}(p) = P_{invest}(p) + P_{exist}(p) \\
281
                &\\
282
                &
283
                else:\\
284
                &
285
                P_{total}(p) = P_{total}(p-1) + P_{invest}(p) - P_{old}(p) \\
286
                &\\
287
                &
288
                \forall p \in \textrm{PERIODS}
289
290
            Upper bound for the flow value
291
292
            .. math::
293
                &
294
                P(p, t) \le ( P_{total}(p) ) \cdot f_{max}(t) \\
295
                &
296
                \forall p, t \in \textrm{TIMEINDEX}
297
298
        For a multi-period model, the old capacity is defined as follows:
299
300
            .. math::
301
                &
302
                P_{old}(p) = P_{old,exo}(p) + P_{old,end}(p)\\
303
                &\\
304
                &
305
                if \quad p=0:\\
306
                &
307
                P_{old,end}(p) = 0\\
308
                &\\
309
                &
310
                else \quad if \quad l \leq year(p):\\
311
                &
312
                P_{old,end}(p) = P_{invest}(p_{comm})\\
313
                &\\
314
                &
315
                else:\\
316
                &
317
                P_{old,end}(p) = 0\\
318
                &\\
319
                &
320
                if \quad p=0:\\
321
                &
322
                P_{old,exo}(p) = 0\\
323
                &\\
324
                &
325
                else \quad if \quad l - a \leq year(p):\\
326
                &
327
                P_{old,exo}(p) = P_{exist} (*)\\
328
                &\\
329
                &
330
                else:\\
331
                &
332
                P_{old,exo}(p) = 0\\
333
                &\\
334
                &
335
                \forall p \in \textrm{PERIODS}
336
337
            where:
338
339
            * (*) is only performed for the first period the condition
340
              is True. A decommissioning flag is then set to True
341
              to prevent having falsely added old capacity in future periods.
342
            * :math:`year(p)` is the year corresponding to period p
343
            * :math:`p_{comm}` is the commissioning period of the flow
344
              (which is determined by the model itself)
345
346
        Depending on the attribute :attr:`nonconvex`, the constraints for the
347
        bounds of the decision variable :math:`P_{invest}(p)` are different:\
348
349
            * :attr:`nonconvex = False`
350
351
            .. math::
352
                &
353
                P_{invest, min}(p) \le P_{invest}(p) \le P_{invest, max}(p) \\
354
                &
355
                \forall p \in \textrm{PERIODS}
356
357
            * :attr:`nonconvex = True`
358
359
            .. math::
360
                &
361
                P_{invest, min}(p) \cdot Y_{invest}(p) \le P_{invest}(p)\\
362
                &
363
                P_{invest}(p) \le P_{invest, max}(p) \cdot Y_{invest}(p)\\
364
                &\\
365
                &
366
                \forall p \in \textrm{PERIODS}
367
368
        For all *InvestmentFlowBlock* objects
369
        (independent of the attribute :attr:`nonconvex`),
370
        the following additional constraints are created, if the appropriate
371
        attribute of the *SimpleFlowBlock*
372
        (see :class:`oemof.solph.flows._simple_flow_block.SimpleFlowBlock`)
373
        is set:
374
375
            * :attr:`fix` is not None
376
377
                Actual value constraint for investments with fixed flow values
378
379
            .. math::
380
                &
381
                P(p, t) = P_{total}(p) \cdot f_{fix}(t) \\
382
                &\\
383
                &
384
                \forall p, t \in \textrm{TIMEINDEX}
385
386
            * :attr:`min != 0`
387
388
                Lower bound for the flow values
389
390
            .. math::
391
                &
392
                P(p, t) \geq P_{total}(p) \cdot f_{min}(t) \\
393
                &\\
394
                &
395
                \forall p, t \in \textrm{TIMEINDEX}
396
397
            * :attr:`full_load_time_max is not None`
398
399
                Upper bound for the sum of all flow values
400
                (e.g. maximum full load hours)
401
402
            .. math::
403
                \sum_{p, t} P(p, t) \cdot \tau(t) \leq P_{total}(p)
404
                \cdot t_{full\_load, min}
405
406
            * :attr:`full_load_time_min is not None`
407
408
                Lower bound for the sum of all flow values
409
                (e.g. minimum full load hours)
410
411
            .. math::
412
                \sum_{p, t} P(t) \cdot \tau(t) \geq P_{total}
413
                \cdot t_{full\_load, min}
414
415
            * :attr:`overall_maximum` is not None
416
              (for multi-period model only)
417
418
                Overall maximum of total installed capacity / energy for flow
419
420
            .. math::
421
                &
422
                P_{total}(p) \leq P_{overall,max} \\
423
                &\\
424
                &
425
                \forall p \in \textrm{PERIODS}
426
427
            * :attr:`overall_minimum` is not None
428
              (for multi-period model only)
429
430
                Overall minimum of total installed capacity / energy for flow;
431
                applicable only in last period
432
433
            .. math::
434
                P_{total}(p_{last}) \geq P_{overall,min}
435
        """
436
        m = self.parent_block()
437
438
        self.minimum_rule = self._minimum_investment_constraint()
439
        self.maximum_rule = self._maximum_investment_constraint()
440
441
        # Handle unit lifetimes
442
        def _total_capacity_rule(block):
443
            """Rule definition for determining total installed
444
            capacity (taking decommissioning into account)
445
            """
446
            for i, o in self.INVESTFLOWS:
447
                for p in m.PERIODS:
448
                    if p == 0:
449
                        expr = (
450
                            self.total[i, o, p]
451
                            == self.invest[i, o, p]
452
                            + m.flows[i, o].investment.existing
453
                        )
454
                        self.total_rule.add((i, o, p), expr)
455
                    # applicable for multi-period model only
456
                    else:
457
                        expr = (
458
                            self.total[i, o, p]
459
                            == self.invest[i, o, p]
460
                            + self.total[i, o, p - 1]
461
                            - self.old[i, o, p]
462
                        )
463
                        self.total_rule.add((i, o, p), expr)
464
465
        self.total_rule = Constraint(
466
            self.INVESTFLOWS, m.PERIODS, noruleinit=True
467
        )
468
        self.total_rule_build = BuildAction(rule=_total_capacity_rule)
469
470
        if m.es.periods is not None:
471
472
            def _old_capacity_rule_end(block):
473
                """Rule definition for determining old endogenously installed
474
                capacity to be decommissioned due to reaching its lifetime.
475
                Investment and decommissioning periods are linked within
476
                the constraint. The respective decommissioning period is
477
                determined for every investment period based on the components
478
                lifetime and a matrix describing its age of each endogenous
479
                investment. Decommissioning can only occur at the beginning of
480
                each period.
481
482
                Note
483
                ----
484
                For further information on the implementation check
485
                PR#957 https://github.com/oemof/oemof-solph/pull/957
486
                """
487
                for i, o in self.INVESTFLOWS:
488
                    lifetime = m.flows[i, o].investment.lifetime
489
                    if lifetime is None:
490
                        msg = (
491
                            "You have to specify a lifetime "
492
                            "for a Flow with an associated "
493
                            "investment object in "
494
                            f"a multi-period model! Value for {(i, o)} "
495
                            "is missing."
496
                        )
497
                        raise ValueError(msg)
498
499
                    # get the period matrix describing the temporal distance
500
                    # between all period combinations.
501
                    periods_matrix = m.es.periods_matrix
502
503
                    # get the index of the minimum value in each row greater
504
                    # equal than the lifetime. This value equals the
505
                    # decommissioning period if not zero. The index of this
506
                    # value represents the investment period. If np.where
507
                    # condition is not met in any row, min value will be zero
508
                    decomm_periods = np.argmin(
509
                        np.where(
510
                            (periods_matrix >= lifetime),
511
                            periods_matrix,
512
                            np.inf,
513
                        ),
514
                        axis=1,
515
                    )
516
517
                    # no decommissioning in first period
518
                    expr = self.old_end[i, o, 0] == 0
519
                    self.old_rule_end.add((i, o, 0), expr)
520
521
                    # all periods not in decomm_periods have no decommissioning
522
                    # zero is excluded
523
                    for p in m.PERIODS:
524
                        if p not in decomm_periods and p != 0:
525
                            expr = self.old_end[i, o, p] == 0
526
                            self.old_rule_end.add((i, o, p), expr)
527
528
                    # multiple invests can be decommissioned in the same period
529
                    # but only sequential ones, thus a bookkeeping is
530
                    # introduced and constraints are added to equation one
531
                    # iteration later.
532
                    last_decomm_p = np.nan
533
                    # loop over invest periods (values are decomm_periods)
534
                    for invest_p, decomm_p in enumerate(decomm_periods):
535
                        # Add constraint of iteration before
536
                        # (skipped in first iteration by last_decomm_p = nan)
537
                        if (decomm_p != last_decomm_p) and (
538
                            last_decomm_p is not np.nan
539
                        ):
540
                            expr = self.old_end[i, o, last_decomm_p] == expr
541
                            self.old_rule_end.add((i, o, last_decomm_p), expr)
542
543
                        # no decommissioning if decomm_p is zero
544
                        if decomm_p == 0:
545
                            # overwrite decomm_p with zero to avoid
546
                            # chaining invest periods in next iteration
547
                            last_decomm_p = 0
548
549
                        # if decomm_p is the same as the last one chain invest
550
                        # period
551
                        elif decomm_p == last_decomm_p:
552
                            expr += self.invest[i, o, invest_p]
553
                            # overwrite decomm_p
554
                            last_decomm_p = decomm_p
555
556
                        # if decomm_p is not zero, not the same as the last one
557
                        # and it's not the first period
558
                        else:
559
                            expr = self.invest[i, o, invest_p]
560
                            # overwrite decomm_p
561
                            last_decomm_p = decomm_p
562
563
                    # Add constraint of very last iteration
564
                    if last_decomm_p != 0:
565
                        expr = self.old_end[i, o, last_decomm_p] == expr
566
                        self.old_rule_end.add((i, o, last_decomm_p), expr)
567
568
            self.old_rule_end = Constraint(
569
                self.INVESTFLOWS, m.PERIODS, noruleinit=True
570
            )
571
            self.old_rule_end_build = BuildAction(rule=_old_capacity_rule_end)
572
573
            def _old_capacity_rule_exo(block):
574
                """Rule definition for determining old exogenously given
575
                capacity to be decommissioned due to reaching its lifetime
576
                """
577
                for i, o in self.INVESTFLOWS:
578
                    age = m.flows[i, o].investment.age
579
                    lifetime = m.flows[i, o].investment.lifetime
580
                    is_decommissioned = False
581
                    for p in m.PERIODS:
582
                        # No shutdown in first period
583
                        if p == 0:
584
                            expr = self.old_exo[i, o, p] == 0
585
                            self.old_rule_exo.add((i, o, p), expr)
586
                        elif lifetime - age <= m.es.periods_years[p]:
587
                            # Track decommissioning status
588
                            if not is_decommissioned:
589
                                expr = (
590
                                    self.old_exo[i, o, p]
591
                                    == m.flows[i, o].investment.existing
592
                                )
593
                                is_decommissioned = True
594
                            else:
595
                                expr = self.old_exo[i, o, p] == 0
596
                            self.old_rule_exo.add((i, o, p), expr)
597
                        else:
598
                            expr = self.old_exo[i, o, p] == 0
599
                            self.old_rule_exo.add((i, o, p), expr)
600
601
            self.old_rule_exo = Constraint(
602
                self.INVESTFLOWS, m.PERIODS, noruleinit=True
603
            )
604
            self.old_rule_exo_build = BuildAction(rule=_old_capacity_rule_exo)
605
606
            def _old_capacity_rule(block):
607
                """Rule definition for determining (overall) old capacity
608
                to be decommissioned due to reaching its lifetime
609
                """
610
                for i, o in self.INVESTFLOWS:
611
                    for p in m.PERIODS:
612
                        expr = (
613
                            self.old[i, o, p]
614
                            == self.old_end[i, o, p] + self.old_exo[i, o, p]
615
                        )
616
                        self.old_rule.add((i, o, p), expr)
617
618
            self.old_rule = Constraint(
619
                self.INVESTFLOWS, m.PERIODS, noruleinit=True
620
            )
621
            self.old_rule_build = BuildAction(rule=_old_capacity_rule)
622
623
        def _investflow_fixed_rule(block):
624
            """Rule definition of constraint to fix flow variable
625
            of investment flow to (normed) actual value
626
            """
627
            for i, o in self.FIXED_INVESTFLOWS:
628
                for p, t in m.TIMEINDEX:
629
                    expr = (
630
                        m.flow[i, o, t]
631
                        == self.total[i, o, p] * m.flows[i, o].fix[t]
632
                    )
633
                    self.fixed.add((i, o, p, t), expr)
634
635
        self.fixed = Constraint(
636
            self.FIXED_INVESTFLOWS, m.TIMEINDEX, noruleinit=True
637
        )
638
        self.fixed_build = BuildAction(rule=_investflow_fixed_rule)
639
640
        def _max_investflow_rule(block):
641
            """Rule definition of constraint setting an upper bound of flow
642
            variable in investment case.
643
            """
644
            for i, o in self.NON_FIXED_INVESTFLOWS:
645
                for p, t in m.TIMEINDEX:
646
                    expr = (
647
                        m.flow[i, o, t]
648
                        <= self.total[i, o, p] * m.flows[i, o].max[t]
649
                    )
650
                    self.max.add((i, o, p, t), expr)
651
652
        self.max = Constraint(
653
            self.NON_FIXED_INVESTFLOWS, m.TIMEINDEX, noruleinit=True
654
        )
655
        self.max_build = BuildAction(rule=_max_investflow_rule)
656
657
        def _min_investflow_rule(block):
658
            """Rule definition of constraint setting a lower bound on flow
659
            variable in investment case.
660
            """
661
            for i, o in self.MIN_INVESTFLOWS:
662
                for p, t in m.TIMEINDEX:
663
                    expr = (
664
                        m.flow[i, o, t]
665
                        >= self.total[i, o, p] * m.flows[i, o].min[t]
666
                    )
667
                    self.min.add((i, o, p, t), expr)
668
669
        self.min = Constraint(
670
            self.MIN_INVESTFLOWS, m.TIMEINDEX, noruleinit=True
671
        )
672
        self.min_build = BuildAction(rule=_min_investflow_rule)
673
674
        def _full_load_time_max_investflow_rule(_, i, o):
675
            """Rule definition for build action of max. sum flow constraint
676
            in investment case.
677
            """
678
            expr = sum(
679
                m.flow[i, o, t] * m.timeincrement[t] for t in m.TIMESTEPS
680
            ) <= (
681
                m.flows[i, o].full_load_time_max
682
                * sum(self.total[i, o, p] for p in m.PERIODS)
683
            )
684
            return expr
685
686
        self.full_load_time_max = Constraint(
687
            self.FULL_LOAD_TIME_MAX_INVESTFLOWS,
688
            rule=_full_load_time_max_investflow_rule,
689
        )
690
691
        def _full_load_time_min_investflow_rule(_, i, o):
692
            """Rule definition for build action of min. sum flow constraint
693
            in investment case.
694
            """
695
            expr = sum(
696
                m.flow[i, o, t] * m.timeincrement[t] for t in m.TIMESTEPS
697
            ) >= (
698
                sum(self.total[i, o, p] for p in m.PERIODS)
699
                * m.flows[i, o].full_load_time_min
700
            )
701
            return expr
702
703
        self.full_load_time_min = Constraint(
704
            self.FULL_LOAD_TIME_MIN_INVESTFLOWS,
705
            rule=_full_load_time_min_investflow_rule,
706
        )
707
708
        if m.es.periods is not None:
709
710
            def _overall_maximum_investflow_rule(block):
711
                """Rule definition for maximum overall investment
712
                in investment case.
713
                """
714
                for i, o in self.OVERALL_MAXIMUM_INVESTFLOWS:
715
                    for p in m.PERIODS:
716
                        expr = (
717
                            self.total[i, o, p]
718
                            <= m.flows[i, o].investment.overall_maximum
719
                        )
720
                        self.overall_maximum.add((i, o, p), expr)
721
722
            self.overall_maximum = Constraint(
723
                self.OVERALL_MAXIMUM_INVESTFLOWS, m.PERIODS, noruleinit=True
724
            )
725
            self.overall_maximum_build = BuildAction(
726
                rule=_overall_maximum_investflow_rule
727
            )
728
729
            def _overall_minimum_investflow_rule(block, i, o):
730
                """Rule definition for minimum overall investment
731
                in investment case.
732
733
                Note: This is only applicable for the last period
734
                """
735
                expr = (
736
                    m.flows[i, o].investment.overall_minimum
737
                    <= self.total[i, o, m.PERIODS[-1]]
738
                )
739
                return expr
740
741
            self.overall_minimum = Constraint(
742
                self.OVERALL_MINIMUM_INVESTFLOWS,
743
                rule=_overall_minimum_investflow_rule,
744
            )
745
746
    def _objective_expression(self):
747
        r"""Objective expression for flows with investment attribute of type
748
        class:`.Investment`. The returned costs are fixed and
749
        investment costs. Variable costs are added from the standard flow
750
        objective expression.
751
752
        Objective terms for a standard model and a multi-period model differ
753
        quite strongly. Besides, the part of the objective function added by
754
        the *InvestmentFlowBlock* also depends on whether a convex
755
        or nonconvex *InvestmentFlowBlock* is selected.
756
        The following parts of the objective function are created:
757
758
        *Standard model*
759
760
            * :attr:`nonconvex = False`
761
762
                .. math::
763
                    P_{invest}(0) \cdot c_{invest,var}(0)
764
765
            * :attr:`nonconvex = True`
766
767
                .. math::
768
                    P_{invest}(0) \cdot c_{invest,var}(0)
769
                    + c_{invest,fix}(0) \cdot Y_{invest}(0) \\
770
771
        Where 0 denotes the 0th (investment) period since
772
        in a standard model, there is only this one period.
773
774
        *Multi-period model*
775
776
            * :attr:`nonconvex = False`
777
778
                .. math::
779
                    &
780
                    P_{invest}(p) \cdot A(c_{invest,var}(p), l, ir)
781
                    \cdot \frac {1}{ANF(d, ir)} \cdot DF^{-p}\\
782
                    &\\
783
                    &
784
                    \forall p \in \textrm{PERIODS}
785
786
            In case, the remaining lifetime of an asset is greater than 0 and
787
            attribute `use_remaining_value` of the energy system is True,
788
            the difference in value for the investment period compared to the
789
            last period of the optimization horizon is accounted for
790
            as an adder to the investment costs:
791
792
                .. math::
793
                    &
794
                    P_{invest}(p) \cdot (A(c_{invest,var}(p), l_{r}, ir) -
795
                    A(c_{invest,var}(|P|), l_{r}, ir)\\
796
                    & \cdot \frac {1}{ANF(l_{r}, ir)} \cdot DF^{-|P|}\\
797
                    &\\
798
                    &
799
                    \forall p \in \textrm{PERIODS}
800
801
            * :attr:`nonconvex = True`
802
803
                .. math::
804
                    &
805
                    (P_{invest}(p) \cdot A(c_{invest,var}(p), l, ir)
806
                    \cdot \frac {1}{ANF(d, ir)}\\
807
                    &
808
                    +  c_{invest,fix}(p) \cdot b_{invest}(p)) \cdot DF^{-p}\\
809
                    &\\
810
                    &
811
                    \forall p \in \textrm{PERIODS}
812
813
            In case, the remaining lifetime of an asset is greater than 0 and
814
            attribute `use_remaining_value` of the energy system is True,
815
            the difference in value for the investment period compared to the
816
            last period of the optimization horizon is accounted for
817
            as an adder to the investment costs:
818
819
                .. math::
820
                    &
821
                    (P_{invest}(p) \cdot (A(c_{invest,var}(p), l_{r}, ir) -
822
                    A(c_{invest,var}(|P|), l_{r}, ir)\\
823
                    & \cdot \frac {1}{ANF(l_{r}, ir)} \cdot DF^{-|P|}\\
824
                    &
825
                    +  (c_{invest,fix}(p) - c_{invest,fix}(|P|))
826
                    \cdot b_{invest}(p)) \cdot DF^{-p}\\
827
                    &\\
828
                    &
829
                    \forall p \in \textrm{PERIODS}
830
831
            * :attr:`fixed_costs` not None for investments
832
833
                .. math::
834
                    &
835
                    (\sum_{pp=year(p)}^{limit_{end}}
836
                    P_{invest}(p) \cdot c_{fixed}(pp) \cdot DF^{-pp})
837
                    \cdot DF^{-p}\\
838
                    &\\
839
                    &
840
                    \forall p \in \textrm{PERIODS}
841
842
            * :attr:`fixed_costs` not None for existing capacity
843
844
                .. math::
845
                    \sum_{pp=0}^{limit_{exo}} P_{exist} \cdot c_{fixed}(pp)
846
                    \cdot DF^{-pp}
847
848
849
        where:
850
851
        * :math:`A(c_{invest,var}(p), l, ir)` A is the annuity for
852
          investment expenses :math:`c_{invest,var}(p)`, lifetime :math:`l`
853
          and interest rate :math:`ir`.
854
        * :math:`l_{r}` is the remaining lifetime at the end of the
855
          optimization horizon (in case it is greater than 0 and
856
          smaller than the actual lifetime).
857
        * :math:`ANF(d, ir)` is the annuity factor for duration :math:`d`
858
          and interest rate :math:`ir`.
859
        * :math:`d=min\{year_{max} - year(p), l\}` defines the
860
          number of years within the optimization horizon that investment
861
          annuities are accounted for.
862
        * :math:`year(p)` denotes the start year of period :math:`p`.
863
        * :math:`year_{max}` denotes the last year of the optimization
864
          horizon, i.e. at the end of the last period.
865
        * :math:`limit_{end}=min\{year_{max}, year(p) + l\}` is used as an
866
          upper bound to ensure fixed costs for endogenous investments
867
          to occur within the optimization horizon.
868
        * :math:`limit_{exo}=min\{year_{max}, l - a\}` is used as an
869
          upper bound to ensure fixed costs for existing capacities to occur
870
          within the optimization horizon. :math:`a` is the initial age
871
          of an asset.
872
        * :math:`DF=(1+dr)` is the discount factor.
873
874
        The annuity / annuity factor hereby is:
875
876
            .. math::
877
                &
878
                A(c_{invest,var}(p), l, ir) = c_{invest,var}(p) \cdot
879
                    \frac {(1+ir)^l \cdot ir} {(1+ir)^l - 1}\\
880
                &\\
881
                &
882
                ANF(d, ir)=\frac {(1+ir)^d \cdot ir} {(1+ir)^d - 1}
883
884
        They are derived using the reciprocal of the oemof.tools.economics
885
        annuity function with a capex of 1.
886
        The interest rate :math:`ir` for the annuity is defined as weighted
887
        average costs of capital (wacc) and assumed constant over time.
888
        """
889
        if not hasattr(self, "INVESTFLOWS"):
890
            return 0
891
892
        m = self.parent_block()
893
        investment_costs = 0
894
        period_investment_costs = {p: 0 for p in m.PERIODS}
895
        fixed_costs = 0
896
897
        if m.es.periods is None:
898
            for i, o in self.CONVEX_INVESTFLOWS:
899
                for p in m.PERIODS:
900
                    investment_costs += (
901
                        self.invest[i, o, p]
902
                        * m.flows[i, o].investment.ep_costs[p]
903
                    )
904
905
            for i, o in self.NON_CONVEX_INVESTFLOWS:
906
                for p in m.PERIODS:
907
                    investment_costs += (
908
                        self.invest[i, o, p]
909
                        * m.flows[i, o].investment.ep_costs[p]
910
                        + self.invest_status[i, o, p]
911
                        * m.flows[i, o].investment.offset[p]
912
                    )
913
914
        else:
915
            msg = (
916
                "You did not specify an interest rate.\n"
917
                "It will be set equal to the discount_rate of {} "
918
                "of the model as a default.\nThis corresponds to a "
919
                "social planner point of view and does not reflect "
920
                "microeconomic interest requirements."
921
            )
922
            for i, o in self.CONVEX_INVESTFLOWS:
923
                lifetime = m.flows[i, o].investment.lifetime
924
                interest = 0
925
                if interest == 0:
926
                    warn(
927
                        msg.format(m.discount_rate),
928
                        debugging.SuspiciousUsageWarning,
929
                    )
930
                    interest = m.discount_rate
931
                for p in m.PERIODS:
932
                    annuity = economics.annuity(
933
                        capex=m.flows[i, o].investment.ep_costs[p],
934
                        n=lifetime,
935
                        wacc=interest,
936
                    )
937
                    duration = min(
938
                        m.es.end_year_of_optimization - m.es.periods_years[p],
939
                        lifetime,
940
                    )
941
                    present_value_factor_remaining = 1 / economics.annuity(
942
                        capex=1, n=duration, wacc=interest
943
                    )
944
                    investment_costs_increment = (
945
                        self.invest[i, o, p]
946
                        * annuity
947
                        * present_value_factor_remaining
948
                    )
949
                    remaining_value_difference = (
950
                        self._evaluate_remaining_value_difference(
951
                            m,
952
                            p,
953
                            i,
954
                            o,
955
                            m.es.end_year_of_optimization,
956
                            lifetime,
957
                            interest,
958
                        )
959
                    )
960
                    investment_costs += (
961
                        investment_costs_increment + remaining_value_difference
962
                    )
963
                    period_investment_costs[p] += investment_costs_increment
964
965
            for i, o in self.NON_CONVEX_INVESTFLOWS:
966
                lifetime = m.flows[i, o].investment.lifetime
967
                interest = 0
968
                if interest == 0:
969
                    warn(
970
                        msg.format(m.discount_rate),
971
                        debugging.SuspiciousUsageWarning,
972
                    )
973
                    interest = m.discount_rate
974
                for p in m.PERIODS:
975
                    annuity = economics.annuity(
976
                        capex=m.flows[i, o].investment.ep_costs[p],
977
                        n=lifetime,
978
                        wacc=interest,
979
                    )
980
                    duration = min(
981
                        m.es.end_year_of_optimization - m.es.periods_years[p],
982
                        lifetime,
983
                    )
984
                    present_value_factor_remaining = 1 / economics.annuity(
985
                        capex=1, n=duration, wacc=interest
986
                    )
987
                    investment_costs_increment = (
988
                        self.invest[i, o, p]
989
                        * annuity
990
                        * present_value_factor_remaining
991
                        + self.invest_status[i, o, p]
992
                        * m.flows[i, o].investment.offset[p]
993
                    )
994
                    remaining_value_difference = (
995
                        self._evaluate_remaining_value_difference(
996
                            m,
997
                            p,
998
                            i,
999
                            o,
1000
                            m.es.end_year_of_optimization,
1001
                            lifetime,
1002
                            interest,
1003
                            nonconvex=True,
1004
                        )
1005
                    )
1006
                    investment_costs += (
1007
                        investment_costs_increment + remaining_value_difference
1008
                    )
1009
                    period_investment_costs[p] += investment_costs_increment
1010
1011
            for i, o in self.INVESTFLOWS:
1012 View Code Duplication
                if valid_sequence(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1013
                    m.flows[i, o].investment.fixed_costs, len(m.PERIODS)
1014
                ):
1015
                    lifetime = m.flows[i, o].investment.lifetime
1016
                    for p in m.PERIODS:
1017
                        range_limit = min(
1018
                            m.es.end_year_of_optimization,
1019
                            m.es.periods_years[p] + lifetime,
1020
                        )
1021
                        fixed_costs += sum(
1022
                            self.invest[i, o, p]
1023
                            * m.flows[i, o].investment.fixed_costs[pp]
1024
                            for pp in range(m.es.periods_years[p], range_limit)
1025
                        )
1026
1027
            for i, o in self.EXISTING_INVESTFLOWS:
1028 View Code Duplication
                if valid_sequence(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1029
                    m.flows[i, o].investment.fixed_costs, len(m.PERIODS)
1030
                ):
1031
                    lifetime = m.flows[i, o].investment.lifetime
1032
                    age = m.flows[i, o].investment.age
1033
                    range_limit = min(
1034
                        m.es.end_year_of_optimization, lifetime - age
1035
                    )
1036
                    fixed_costs += sum(
1037
                        m.flows[i, o].investment.existing
1038
                        * m.flows[i, o].investment.fixed_costs[pp]
1039
                        for pp in range(range_limit)
1040
                    )
1041
1042
        self.investment_costs = Expression(expr=investment_costs)
1043
        self.period_investment_costs = period_investment_costs
1044
        self.fixed_costs = Expression(expr=fixed_costs)
1045
        self.costs = Expression(expr=investment_costs + fixed_costs)
1046
1047
        return self.costs
1048
1049
    def _evaluate_remaining_value_difference(
1050
        self,
1051
        m,
1052
        p,
1053
        i,
1054
        o,
1055
        end_year_of_optimization,
1056
        lifetime,
1057
        interest,
1058
        nonconvex=False,
1059
    ):
1060
        """Evaluate and return the remaining value difference of an investment
1061
1062
        The remaining value difference in the net present values if the asset
1063
        was to be liquidated at the end of the optimization horizon and the
1064
        net present value using the original investment expenses.
1065
1066
        Parameters
1067
        ----------
1068
        m : oemof.solph.models.Model
1069
            Optimization model
1070
1071
        p : int
1072
            Period in which investment occurs
1073
1074
        i : any instance of oemof.solph.components
1075
            start node of flow
1076
1077
        o : any instance of oemof.solph.components
1078
            end node of flow
1079
1080
        end_year_of_optimization : int
1081
            Last year of the optimization horizon
1082
1083
        lifetime : int
1084
            lifetime of investment considered
1085
1086
        interest : float
1087
            Demanded interest rate for investment
1088
1089
        nonconvex : bool
1090
            Indicating whether considered flow is nonconvex.
1091
        """
1092
        if m.es.use_remaining_value:
1093
            if end_year_of_optimization - m.es.periods_years[p] < lifetime:
1094
                remaining_lifetime = lifetime - (
1095
                    end_year_of_optimization - m.es.periods_years[p]
1096
                )
1097
                remaining_annuity = economics.annuity(
1098
                    capex=m.flows[i, o].investment.ep_costs[-1],
1099
                    n=remaining_lifetime,
1100
                    wacc=interest,
1101
                )
1102
                original_annuity = economics.annuity(
1103
                    capex=m.flows[i, o].investment.ep_costs[p],
1104
                    n=remaining_lifetime,
1105
                    wacc=interest,
1106
                )
1107
                present_value_factor_remaining = 1 / economics.annuity(
1108
                    capex=1, n=remaining_lifetime, wacc=interest
1109
                )
1110
                convex_investment_costs = (
1111
                    self.invest[i, o, p]
1112
                    * (remaining_annuity - original_annuity)
1113
                    * present_value_factor_remaining
1114
                )
1115
                if nonconvex:
1116
                    return convex_investment_costs + self.invest_status[
1117
                        i, o, p
1118
                    ] * (
1119
                        m.flows[i, o].investment.offset[-1]
1120
                        - m.flows[i, o].investment.offset[p]
1121
                    )
1122
                else:
1123
                    return convex_investment_costs
1124
            else:
1125
                return 0
1126
        else:
1127
            return 0
1128
1129 View Code Duplication
    def _minimum_investment_constraint(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1130
        """Constraint factory for a minimum investment"""
1131
        m = self.parent_block()
1132
1133
        def _min_invest_rule(_):
1134
            """Rule definition for applying a minimum investment"""
1135
            for i, o in self.NON_CONVEX_INVESTFLOWS:
1136
                for p in m.PERIODS:
1137
                    expr = (
1138
                        m.flows[i, o].investment.minimum[p]
1139
                        * self.invest_status[i, o, p]
1140
                        <= self.invest[i, o, p]
1141
                    )
1142
                    self.minimum_rule.add((i, o, p), expr)
1143
1144
        self.minimum_rule = Constraint(
1145
            self.NON_CONVEX_INVESTFLOWS, m.PERIODS, noruleinit=True
1146
        )
1147
        self.minimum_rule_build = BuildAction(rule=_min_invest_rule)
1148
1149
        return self.minimum_rule
1150
1151 View Code Duplication
    def _maximum_investment_constraint(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1152
        """Constraint factory for a maximum investment"""
1153
        m = self.parent_block()
1154
1155
        def _max_invest_rule(_):
1156
            """Rule definition for applying a minimum investment"""
1157
            for i, o in self.NON_CONVEX_INVESTFLOWS:
1158
                for p in m.PERIODS:
1159
                    expr = self.invest[i, o, p] <= (
1160
                        m.flows[i, o].investment.maximum[p]
1161
                        * self.invest_status[i, o, p]
1162
                    )
1163
                    self.maximum_rule.add((i, o, p), expr)
1164
1165
        self.maximum_rule = Constraint(
1166
            self.NON_CONVEX_INVESTFLOWS, m.PERIODS, noruleinit=True
1167
        )
1168
        self.maximum_rule_build = BuildAction(rule=_max_invest_rule)
1169
1170
        return self.maximum_rule
1171