• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

oemof / oemof-solph / 17377487432

01 Sep 2025 12:22PM UTC coverage: 81.765% (-5.8%) from 87.55%
17377487432

Pull #1174

github

web-flow
Merge 553ade8ee into 49e9272c9
Pull Request #1174: Feature/custom attributes for investments

925 of 1228 branches covered (75.33%)

Branch coverage included in aggregate %.

2 of 17 new or added lines in 2 files covered. (11.76%)

179 existing lines in 7 files now uncovered.

2707 of 3214 relevant lines covered (84.23%)

0.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

38.32
/src/oemof/solph/flows/_investment_flow_block.py
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 <krien@uni-bremen.de>
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
1✔
19

20
import numpy as np
1✔
21
from oemof.tools import debugging
1✔
22
from oemof.tools import economics
1✔
23
from pyomo.core import Binary
1✔
24
from pyomo.core import BuildAction
1✔
25
from pyomo.core import Constraint
1✔
26
from pyomo.core import Expression
1✔
27
from pyomo.core import NonNegativeReals
1✔
28
from pyomo.core import Set
1✔
29
from pyomo.core import Var
1✔
30
from pyomo.core.base.block import ScalarBlock
1✔
31

32
from oemof.solph._plumbing import valid_sequence
1✔
33

34

35
class InvestmentFlowBlock(ScalarBlock):
1✔
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):
1✔
79
        super().__init__(*args, **kwargs)
1✔
80

81
    def _create(self, group=None):
1✔
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:
1✔
93
            return None
1✔
94

95
        self._create_sets(group)
1✔
96
        self._create_variables(group)
1✔
97
        self._create_constraints()
1✔
98

99
    def _create_sets(self, group):
1✔
100
        """
101
        Creates all sets for investment flows.
102
        """
103
        self.INVESTFLOWS = Set(initialize=[(g[0], g[1]) for g in group])
1✔
104

105
        self.CONVEX_INVESTFLOWS = Set(
1✔
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(
1✔
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(
1✔
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(
1✔
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(
1✔
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(
1✔
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(
1✔
146
            initialize=[(g[0], g[1]) for g in group if g[2].min.min() != 0]
147
        )
148

149
        self.EXISTING_INVESTFLOWS = Set(
1✔
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(
1✔
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(
1✔
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, _):
1✔
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()
1✔
224

225
        def _investvar_bound_rule(block, i, o, p):
1✔
226
            """Rule definition for bounds of invest variable."""
227
            if (i, o) in self.CONVEX_INVESTFLOWS:
1!
228
                return (
1✔
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(
1✔
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)
1✔
245

246
        if m.es.periods is not None:
1!
UNCOV
247
            self.old = Var(
×
248
                self.INVESTFLOWS, m.PERIODS, within=NonNegativeReals
249
            )
250

251
            # Old endogenous capacity to be decommissioned (due to lifetime)
UNCOV
252
            self.old_end = Var(
×
253
                self.INVESTFLOWS, m.PERIODS, within=NonNegativeReals
254
            )
255

256
            # Old exogenous capacity to be decommissioned (due to lifetime)
UNCOV
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(
1✔
263
            self.NON_CONVEX_INVESTFLOWS, m.PERIODS, within=Binary
264
        )
265

266
    def _create_constraints(self):
1✔
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()
1✔
437

438
        self.minimum_rule = self._minimum_investment_constraint()
1✔
439
        self.maximum_rule = self._maximum_investment_constraint()
1✔
440

441
        # Handle unit lifetimes
442
        def _total_capacity_rule(block):
1✔
443
            """Rule definition for determining total installed
444
            capacity (taking decommissioning into account)
445
            """
446
            for i, o in self.INVESTFLOWS:
1✔
447
                for p in m.PERIODS:
1✔
448
                    if p == 0:
1!
449
                        expr = (
1✔
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)
1✔
455
                    # applicable for multi-period model only
456
                    else:
UNCOV
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
                        )
UNCOV
463
                        self.total_rule.add((i, o, p), expr)
×
464

465
        self.total_rule = Constraint(
1✔
466
            self.INVESTFLOWS, m.PERIODS, noruleinit=True
467
        )
468
        self.total_rule_build = BuildAction(rule=_total_capacity_rule)
1✔
469

470
        if m.es.periods is not None:
1!
471

UNCOV
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
                """
UNCOV
487
                for i, o in self.INVESTFLOWS:
×
UNCOV
488
                    lifetime = m.flows[i, o].investment.lifetime
×
UNCOV
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.
UNCOV
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
UNCOV
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
UNCOV
518
                    expr = self.old_end[i, o, 0] == 0
×
UNCOV
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
UNCOV
523
                    for p in m.PERIODS:
×
UNCOV
524
                        if p not in decomm_periods and p != 0:
×
UNCOV
525
                            expr = self.old_end[i, o, p] == 0
×
UNCOV
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.
UNCOV
532
                    last_decomm_p = np.nan
×
533
                    # loop over invest periods (values are decomm_periods)
UNCOV
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)
UNCOV
537
                        if (decomm_p != last_decomm_p) and (
×
538
                            last_decomm_p is not np.nan
539
                        ):
UNCOV
540
                            expr = self.old_end[i, o, last_decomm_p] == expr
×
UNCOV
541
                            self.old_rule_end.add((i, o, last_decomm_p), expr)
×
542

543
                        # no decommissioning if decomm_p is zero
UNCOV
544
                        if decomm_p == 0:
×
545
                            # overwrite decomm_p with zero to avoid
546
                            # chaining invest periods in next iteration
UNCOV
547
                            last_decomm_p = 0
×
548

549
                        # if decomm_p is the same as the last one chain invest
550
                        # period
UNCOV
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:
UNCOV
559
                            expr = self.invest[i, o, invest_p]
×
560
                            # overwrite decomm_p
UNCOV
561
                            last_decomm_p = decomm_p
×
562

563
                    # Add constraint of very last iteration
UNCOV
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

UNCOV
568
            self.old_rule_end = Constraint(
×
569
                self.INVESTFLOWS, m.PERIODS, noruleinit=True
570
            )
UNCOV
571
            self.old_rule_end_build = BuildAction(rule=_old_capacity_rule_end)
×
572

UNCOV
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
                """
UNCOV
577
                for i, o in self.INVESTFLOWS:
×
UNCOV
578
                    age = m.flows[i, o].investment.age
×
UNCOV
579
                    lifetime = m.flows[i, o].investment.lifetime
×
UNCOV
580
                    is_decommissioned = False
×
UNCOV
581
                    for p in m.PERIODS:
×
582
                        # No shutdown in first period
UNCOV
583
                        if p == 0:
×
UNCOV
584
                            expr = self.old_exo[i, o, p] == 0
×
UNCOV
585
                            self.old_rule_exo.add((i, o, p), expr)
×
UNCOV
586
                        elif lifetime - age <= m.es.periods_years[p]:
×
587
                            # Track decommissioning status
UNCOV
588
                            if not is_decommissioned:
×
UNCOV
589
                                expr = (
×
590
                                    self.old_exo[i, o, p]
591
                                    == m.flows[i, o].investment.existing
592
                                )
UNCOV
593
                                is_decommissioned = True
×
594
                            else:
UNCOV
595
                                expr = self.old_exo[i, o, p] == 0
×
UNCOV
596
                            self.old_rule_exo.add((i, o, p), expr)
×
597
                        else:
UNCOV
598
                            expr = self.old_exo[i, o, p] == 0
×
UNCOV
599
                            self.old_rule_exo.add((i, o, p), expr)
×
600

UNCOV
601
            self.old_rule_exo = Constraint(
×
602
                self.INVESTFLOWS, m.PERIODS, noruleinit=True
603
            )
UNCOV
604
            self.old_rule_exo_build = BuildAction(rule=_old_capacity_rule_exo)
×
605

UNCOV
606
            def _old_capacity_rule(block):
×
607
                """Rule definition for determining (overall) old capacity
608
                to be decommissioned due to reaching its lifetime
609
                """
UNCOV
610
                for i, o in self.INVESTFLOWS:
×
UNCOV
611
                    for p in m.PERIODS:
×
UNCOV
612
                        expr = (
×
613
                            self.old[i, o, p]
614
                            == self.old_end[i, o, p] + self.old_exo[i, o, p]
615
                        )
UNCOV
616
                        self.old_rule.add((i, o, p), expr)
×
617

UNCOV
618
            self.old_rule = Constraint(
×
619
                self.INVESTFLOWS, m.PERIODS, noruleinit=True
620
            )
UNCOV
621
            self.old_rule_build = BuildAction(rule=_old_capacity_rule)
×
622

623
        def _investflow_fixed_rule(block):
1✔
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:
1✔
628
                for p, t in m.TIMEINDEX:
1✔
629
                    expr = (
1✔
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)
1✔
634

635
        self.fixed = Constraint(
1✔
636
            self.FIXED_INVESTFLOWS, m.TIMEINDEX, noruleinit=True
637
        )
638
        self.fixed_build = BuildAction(rule=_investflow_fixed_rule)
1✔
639

640
        def _max_investflow_rule(block):
1✔
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:
1✔
645
                for p, t in m.TIMEINDEX:
1✔
646
                    expr = (
1✔
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)
1✔
651

652
        self.max = Constraint(
1✔
653
            self.NON_FIXED_INVESTFLOWS, m.TIMEINDEX, noruleinit=True
654
        )
655
        self.max_build = BuildAction(rule=_max_investflow_rule)
1✔
656

657
        def _min_investflow_rule(block):
1✔
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:
1!
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(
1✔
670
            self.MIN_INVESTFLOWS, m.TIMEINDEX, noruleinit=True
671
        )
672
        self.min_build = BuildAction(rule=_min_investflow_rule)
1✔
673

674
        def _full_load_time_max_investflow_rule(_, i, o):
1✔
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(
1✔
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):
1✔
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(
1✔
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:
1!
709

UNCOV
710
            def _overall_maximum_investflow_rule(block):
×
711
                """Rule definition for maximum overall investment
712
                in investment case.
713
                """
UNCOV
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

UNCOV
722
            self.overall_maximum = Constraint(
×
723
                self.OVERALL_MAXIMUM_INVESTFLOWS, m.PERIODS, noruleinit=True
724
            )
UNCOV
725
            self.overall_maximum_build = BuildAction(
×
726
                rule=_overall_maximum_investflow_rule
727
            )
728

UNCOV
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

UNCOV
741
            self.overall_minimum = Constraint(
×
742
                self.OVERALL_MINIMUM_INVESTFLOWS,
743
                rule=_overall_minimum_investflow_rule,
744
            )
745

746
    def _objective_expression(self):
1✔
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"):
1✔
890
            return 0
1✔
891

892
        m = self.parent_block()
1✔
893
        investment_costs = 0
1✔
894
        period_investment_costs = {p: 0 for p in m.PERIODS}
1✔
895
        fixed_costs = 0
1✔
896

897
        if m.es.periods is None:
1!
898
            for i, o in self.CONVEX_INVESTFLOWS:
1✔
899
                for p in m.PERIODS:
1✔
900
                    investment_costs += (
1✔
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:
1!
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:
UNCOV
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
            )
UNCOV
922
            for i, o in self.CONVEX_INVESTFLOWS:
×
UNCOV
923
                lifetime = m.flows[i, o].investment.lifetime
×
UNCOV
924
                interest = 0
×
UNCOV
925
                if interest == 0:
×
UNCOV
926
                    warn(
×
927
                        msg.format(m.discount_rate),
928
                        debugging.SuspiciousUsageWarning,
929
                    )
UNCOV
930
                    interest = m.discount_rate
×
UNCOV
931
                for p in m.PERIODS:
×
UNCOV
932
                    annuity = economics.annuity(
×
933
                        capex=m.flows[i, o].investment.ep_costs[p],
934
                        n=lifetime,
935
                        wacc=interest,
936
                    )
UNCOV
937
                    duration = min(
×
938
                        m.es.end_year_of_optimization - m.es.periods_years[p],
939
                        lifetime,
940
                    )
UNCOV
941
                    present_value_factor_remaining = 1 / economics.annuity(
×
942
                        capex=1, n=duration, wacc=interest
943
                    )
UNCOV
944
                    investment_costs_increment = (
×
945
                        self.invest[i, o, p]
946
                        * annuity
947
                        * present_value_factor_remaining
948
                    )
UNCOV
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
                    )
UNCOV
960
                    investment_costs += (
×
961
                        investment_costs_increment + remaining_value_difference
962
                    )
UNCOV
963
                    period_investment_costs[p] += investment_costs_increment
×
964

UNCOV
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

UNCOV
1011
            for i, o in self.INVESTFLOWS:
×
UNCOV
1012
                if valid_sequence(
×
1013
                    m.flows[i, o].investment.fixed_costs, len(m.PERIODS)
1014
                ):
UNCOV
1015
                    lifetime = m.flows[i, o].investment.lifetime
×
UNCOV
1016
                    for p in m.PERIODS:
×
UNCOV
1017
                        range_limit = min(
×
1018
                            m.es.end_year_of_optimization,
1019
                            m.es.periods_years[p] + lifetime,
1020
                        )
UNCOV
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

UNCOV
1027
            for i, o in self.EXISTING_INVESTFLOWS:
×
UNCOV
1028
                if valid_sequence(
×
1029
                    m.flows[i, o].investment.fixed_costs, len(m.PERIODS)
1030
                ):
UNCOV
1031
                    lifetime = m.flows[i, o].investment.lifetime
×
UNCOV
1032
                    age = m.flows[i, o].investment.age
×
UNCOV
1033
                    range_limit = min(
×
1034
                        m.es.end_year_of_optimization, lifetime - age
1035
                    )
UNCOV
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)
1✔
1043
        self.period_investment_costs = period_investment_costs
1✔
1044
        self.fixed_costs = Expression(expr=fixed_costs)
1✔
1045
        self.costs = Expression(expr=investment_costs + fixed_costs)
1✔
1046

1047
        return self.costs
1✔
1048

1049
    def _evaluate_remaining_value_difference(
1✔
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
        """
UNCOV
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:
UNCOV
1127
            return 0
×
1128

1129
    def _minimum_investment_constraint(self):
1✔
1130
        """Constraint factory for a minimum investment"""
1131
        m = self.parent_block()
1✔
1132

1133
        def _min_invest_rule(_):
1✔
1134
            """Rule definition for applying a minimum investment"""
1135
            for i, o in self.NON_CONVEX_INVESTFLOWS:
1!
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(
1✔
1145
            self.NON_CONVEX_INVESTFLOWS, m.PERIODS, noruleinit=True
1146
        )
1147
        self.minimum_rule_build = BuildAction(rule=_min_invest_rule)
1✔
1148

1149
        return self.minimum_rule
1✔
1150

1151
    def _maximum_investment_constraint(self):
1✔
1152
        """Constraint factory for a maximum investment"""
1153
        m = self.parent_block()
1✔
1154

1155
        def _max_invest_rule(_):
1✔
1156
            """Rule definition for applying a minimum investment"""
1157
            for i, o in self.NON_CONVEX_INVESTFLOWS:
1!
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(
1✔
1166
            self.NON_CONVEX_INVESTFLOWS, m.PERIODS, noruleinit=True
1167
        )
1168
        self.maximum_rule_build = BuildAction(rule=_max_invest_rule)
1✔
1169

1170
        return self.maximum_rule
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc