Passed
Push — dev ( 749e85...07b7d9 )
by Patrik
01:50 queued 16s
created

solph.components._generic_storage   F

Complexity

Total Complexity 114

Size/Duplication

Total Lines 2004
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 114
eloc 739
dl 0
loc 2004
rs 1.861
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
B GenericInvestmentStorageBlock._add_storage_limit_constraints() 0 71 2
B GenericStorageBlock._objective_expression() 0 42 7
A GenericStorage._check_infeasible_parameter_combinations() 0 13 4
F GenericInvestmentStorageBlock._create() 0 570 41
A GenericStorage._check_number_of_flows() 0 9 3
B GenericStorageBlock._create() 0 137 6
A GenericInvestmentStorageBlock.__init__() 0 2 1
F GenericInvestmentStorageBlock._objective_expression() 0 149 17
A GenericStorage.constraint_group() 0 5 2
B GenericStorage._set_flows() 0 14 7
A GenericStorageBlock.__init__() 0 2 1
B GenericInvestmentStorageBlock._evaluate_remaining_value_difference() 0 76 4
B GenericStorage._check_invest_attributes() 0 25 8
D GenericStorage.__init__() 0 106 11

How to fix   Complexity   

Complexity

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

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

1
# -*- coding: utf-8 -
2
3
"""
4
GenericStorage and associated individual constraints (blocks) and groupings.
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: FranziPl
11
SPDX-FileCopyrightText: jnnr
12
SPDX-FileCopyrightText: Stephan Günther
13
SPDX-FileCopyrightText: FabianTU
14
SPDX-FileCopyrightText: Johannes Röder
15
SPDX-FileCopyrightText: Ekaterina Zolotarevskaia
16
SPDX-FileCopyrightText: Johannes Kochems
17
SPDX-FileCopyrightText: Johannes Giehl
18
SPDX-FileCopyrightText: Raul Ciria Aylagas
19
20
SPDX-License-Identifier: MIT
21
22
"""
23
import numbers
24
from warnings import warn
25
26
import numpy as np
27
from oemof.network import Node
28
from oemof.tools import debugging
29
from oemof.tools import economics
30
from pyomo.core.base.block import ScalarBlock
31
from pyomo.environ import Binary
32
from pyomo.environ import BuildAction
33
from pyomo.environ import Constraint
34
from pyomo.environ import Expression
35
from pyomo.environ import NonNegativeReals
36
from pyomo.environ import Set
37
from pyomo.environ import Var
38
39
from oemof.solph._helpers import check_node_object_for_missing_attribute
40
from oemof.solph._options import Investment
41
from oemof.solph._plumbing import sequence
42
from oemof.solph._plumbing import valid_sequence
43
44
45
class GenericStorage(Node):
46
    r"""
47
    Component `GenericStorage` to model with basic characteristics of storages.
48
49
    The GenericStorage is designed for one input and one output.
50
51
    Parameters
52
    ----------
53
    nominal_capacity : numeric, :math:`E_{nom}` or
54
            :class:`oemof.solph.options.Investment` object
55
        Absolute nominal capacity of the storage, fixed value or
56
        object describing parameter of investment optimisations.
57
    invest_relation_input_capacity : numeric (iterable or scalar) or None, :math:`r_{cap,in}`
58
        Ratio between the investment variable of the input Flow and the
59
        investment variable of the storage:
60
        :math:`\dot{E}_{in,invest} = E_{invest} \cdot r_{cap,in}`
61
    invest_relation_output_capacity : numeric (iterable or scalar) or None, :math:`r_{cap,out}`
62
        Ratio between the investment variable of the output Flow and the
63
        investment variable of the storage:
64
        :math:`\dot{E}_{out,invest} = E_{invest} \cdot r_{cap,out}`
65
    invest_relation_input_output : numeric (iterable or scalar) or None, :math:`r_{in,out}`
66
        Ratio between the investment variable of the output Flow and the
67
        investment variable of the input flow. This ratio used to fix the
68
        flow investments to each other.
69
        Values < 1 set the input flow lower than the output and > 1 will
70
        set the input flow higher than the output flow. If None no relation
71
        will be set:
72
        :math:`\dot{E}_{in,invest} = \dot{E}_{out,invest} \cdot r_{in,out}`
73
    initial_storage_level : numeric, :math:`c(-1)`
74
        The relative storage content in the timestep before the first
75
        time step of optimization (between 0 and 1).
76
77
        Note: When investment mode is used in a multi-period model,
78
        `initial_storage_level` is not supported.
79
        Storage output is forced to zero until the storage unit is invested in.
80
    balanced : boolean
81
        Couple storage level of first and last time step.
82
        (Total inflow and total outflow are balanced.)
83
    loss_rate : numeric (iterable or scalar)
84
        The relative loss of the storage content per hour.
85
    fixed_losses_relative : numeric (iterable or scalar), :math:`\gamma(t)`
86
        Losses per hour that are independent of the storage content but
87
        proportional to nominal storage capacity.
88
89
        Note: Fixed losses are not supported in investment mode.
90
    fixed_losses_absolute : numeric (iterable or scalar), :math:`\delta(t)`
91
        Losses per hour that are independent of storage content and independent
92
        of nominal storage capacity.
93
94
        Note: Fixed losses are not supported in investment mode.
95
    inflow_conversion_factor : numeric (iterable or scalar), :math:`\eta_i(t)`
96
        The relative conversion factor, i.e. efficiency associated with the
97
        inflow of the storage.
98
    outflow_conversion_factor : numeric (iterable or scalar), :math:`\eta_o(t)`
99
        see: inflow_conversion_factor
100
    min_storage_level : numeric (iterable or scalar), :math:`c_{min}(t)`
101
        The normed minimum storage content as fraction of the
102
        nominal storage capacity or the capacity that has been invested into
103
        (between 0 and 1).
104
        To set different values in every time step use a sequence.
105
    max_storage_level : numeric (iterable or scalar), :math:`c_{max}(t)`
106
        see: min_storage_level
107
    investment : :class:`oemof.solph.options.Investment` object
108
        Object indicating if a nominal_capacity of the flow is determined by
109
        the optimization problem. Note: This will refer all attributes to an
110
        investment variable instead of to the nominal_storage_capacity. The
111
        nominal_storage_capacity should not be set (or set to None) if an
112
        investment object is used.
113
    storage_costs : numeric (iterable or scalar), :math:`c_{storage}(t)`
114
        Cost (per energy) for having energy in the storage, starting from
115
        time point :math:`t_{1}`.
116
    lifetime_inflow : int, :math:`n_{in}`
117
        Determine the lifetime of an inflow; only applicable for multi-period
118
        models which can invest in storage capacity and have an
119
        invest_relation_input_capacity defined
120
    lifetime_outflow : int, :math:`n_{in}`
121
        Determine the lifetime of an outflow; only applicable for multi-period
122
        models which can invest in storage capacity and have an
123
        invest_relation_output_capacity defined
124
125
    Notes
126
    -----
127
    The following sets, variables, constraints and objective parts are created
128
     * :py:class:`~oemof.solph.components._generic_storage.GenericStorageBlock`
129
       (if no Investment object present)
130
     * :py:class:`~oemof.solph.components._generic_storage.GenericInvestmentStorageBlock`
131
       (if Investment object present)
132
133
    Examples
134
    --------
135
    Basic usage examples of the GenericStorage with a random selection of
136
    attributes. See the Flow class for all Flow attributes.
137
138
    >>> from oemof import solph
139
140
    >>> my_bus = solph.buses.Bus('my_bus')
141
142
    >>> my_storage = solph.components.GenericStorage(
143
    ...     label='storage',
144
    ...     nominal_capacity=1000,
145
    ...     inputs={my_bus: solph.flows.Flow(nominal_capacity=200, variable_costs=10)},
146
    ...     outputs={my_bus: solph.flows.Flow(nominal_capacity=200)},
147
    ...     loss_rate=0.01,
148
    ...     initial_storage_level=0,
149
    ...     max_storage_level = 0.9,
150
    ...     inflow_conversion_factor=0.9,
151
    ...     outflow_conversion_factor=0.93)
152
153
    >>> my_investment_storage = solph.components.GenericStorage(
154
    ...     label='storage',
155
    ...     nominal_capacity=solph.Investment(ep_costs=50),
156
    ...     inputs={my_bus: solph.flows.Flow()},
157
    ...     outputs={my_bus: solph.flows.Flow()},
158
    ...     loss_rate=0.02,
159
    ...     initial_storage_level=None,
160
    ...     invest_relation_input_capacity=1/6,
161
    ...     invest_relation_output_capacity=1/6,
162
    ...     inflow_conversion_factor=1,
163
    ...     outflow_conversion_factor=0.8)
164
    """  # noqa: E501
165
166
    def __init__(
167
        self,
168
        label=None,
169
        inputs=None,
170
        outputs=None,
171
        nominal_capacity=None,
172
        nominal_storage_capacity=None,  # Can be removed for versions >= v0.7
173
        initial_storage_level=None,
174
        investment=None,
175
        invest_relation_input_output=None,
176
        invest_relation_input_capacity=None,
177
        invest_relation_output_capacity=None,
178
        min_storage_level=0,
179
        max_storage_level=1,
180
        balanced=True,
181
        loss_rate=0,
182
        fixed_losses_relative=0,
183
        fixed_losses_absolute=0,
184
        inflow_conversion_factor=1,
185
        outflow_conversion_factor=1,
186
        fixed_costs=0,
187
        storage_costs=None,
188
        lifetime_inflow=None,
189
        lifetime_outflow=None,
190
        custom_attributes=None,
191
    ):
192
        if inputs is None:
193
            inputs = {}
194
        if outputs is None:
195
            outputs = {}
196
        if custom_attributes is None:
197
            custom_attributes = {}
198
        super().__init__(
199
            label,
200
            inputs=inputs,
201
            outputs=outputs,
202
            custom_properties=custom_attributes,
203
        )
204
        # --- BEGIN: The following code can be removed for versions >= v0.6 ---
205
        if investment is not None:
206
            msg = (
207
                "For backward compatibility,"
208
                " the option investment overwrites the option"
209
                + " nominal_storage_capacity."
210
                + " Both options cannot be set at the same time."
211
            )
212
            if nominal_capacity is not None:
213
                raise AttributeError(msg)
214
            else:
215
                warn(msg, FutureWarning)
216
            nominal_storage_capacity = investment
217
        # --- END ---
218
        # --- BEGIN: The following code can be removed for versions >= v0.6 ---
219
        if nominal_storage_capacity is not None:
220
            msg = (
221
                "For backward compatibility,"
222
                + " the option nominal_storage_capacity overwrites the option"
223
                + " nominal_capacity."
224
                + " Both options cannot be set at the same time."
225
            )
226
            if nominal_capacity is not None:
227
                raise AttributeError(msg)
228
            else:
229
                warn(msg, FutureWarning)
230
            nominal_capacity = nominal_storage_capacity
231
        # --- END ---
232
233
        self.nominal_storage_capacity = None
234
        self.investment = None
235
        self._invest_group = False
236
        if isinstance(nominal_capacity, numbers.Real):
237
            self.nominal_storage_capacity = nominal_capacity
238
        elif isinstance(nominal_capacity, Investment):
239
            self.investment = nominal_capacity
240
            self._invest_group = True
241
242
        self.initial_storage_level = initial_storage_level
243
        self.balanced = balanced
244
        self.loss_rate = sequence(loss_rate)
245
        self.fixed_losses_relative = sequence(fixed_losses_relative)
246
        self.fixed_losses_absolute = sequence(fixed_losses_absolute)
247
        self.inflow_conversion_factor = sequence(inflow_conversion_factor)
248
        self.outflow_conversion_factor = sequence(outflow_conversion_factor)
249
        self.max_storage_level = sequence(max_storage_level)
250
        self.min_storage_level = sequence(min_storage_level)
251
        self.fixed_costs = sequence(fixed_costs)
252
        self.storage_costs = sequence(storage_costs)
253
        self.invest_relation_input_output = sequence(
254
            invest_relation_input_output
255
        )
256
        self.invest_relation_input_capacity = sequence(
257
            invest_relation_input_capacity
258
        )
259
        self.invest_relation_output_capacity = sequence(
260
            invest_relation_output_capacity
261
        )
262
        self.lifetime_inflow = lifetime_inflow
263
        self.lifetime_outflow = lifetime_outflow
264
265
        # Check number of flows.
266
        self._check_number_of_flows()
267
        # Check for infeasible parameter combinations
268
        self._check_infeasible_parameter_combinations()
269
270
        if self._invest_group:
271
            self._check_invest_attributes()
272
273
    def _set_flows(self):
274
        """Define inflow / outflow as investment flows when they are
275
        coupled with storage capacity via invest relations
276
        """
277
        for flow in self.inputs.values():
278
            if self.invest_relation_input_capacity[
279
                0
280
            ] is not None and not isinstance(flow.investment, Investment):
281
                flow.investment = Investment(lifetime=self.lifetime_inflow)
282
        for flow in self.outputs.values():
283
            if self.invest_relation_output_capacity[
284
                0
285
            ] is not None and not isinstance(flow.investment, Investment):
286
                flow.investment = Investment(lifetime=self.lifetime_outflow)
287
288
    def _check_invest_attributes(self):
289
        """Raise errors for infeasible investment attribute combinations"""
290
        if (
291
            self.invest_relation_input_output[0] is not None
292
            and self.invest_relation_output_capacity[0] is not None
293
            and self.invest_relation_input_capacity[0] is not None
294
        ):
295
            e2 = (
296
                "Overdetermined. Three investment object will be coupled"
297
                "with three constraints. Set one invest relation to 'None'."
298
            )
299
            raise AttributeError(e2)
300
        if (
301
            self.investment
302
            and self.fixed_losses_absolute.max() != 0
303
            and self.investment.existing == 0
304
            and self.investment.minimum.min() == 0
305
        ):
306
            e3 = (
307
                "With fixed_losses_absolute > 0, either investment.existing "
308
                "or investment.minimum has to be non-zero."
309
            )
310
            raise AttributeError(e3)
311
312
        self._set_flows()
313
314
    def _check_number_of_flows(self):
315
        """Ensure that there is only one inflow and outflow to the storage"""
316
        msg = "Only one {0} flow allowed in the GenericStorage {1}."
317
        check_node_object_for_missing_attribute(self, "inputs")
318
        check_node_object_for_missing_attribute(self, "outputs")
319
        if len(self.inputs) > 1:
320
            raise AttributeError(msg.format("input", self.label))
321
        if len(self.outputs) > 1:
322
            raise AttributeError(msg.format("output", self.label))
323
324
    def _check_infeasible_parameter_combinations(self):
325
        """Check for infeasible parameter combinations and raise error"""
326
        msg = (
327
            "initial_storage_level must be greater or equal to "
328
            "min_storage_level and smaller or equal to "
329
            "max_storage_level."
330
        )
331
        if self.initial_storage_level is not None:
332
            if (
333
                self.initial_storage_level < self.min_storage_level[0]
334
                or self.initial_storage_level > self.max_storage_level[0]
335
            ):
336
                raise ValueError(msg)
337
338
    def constraint_group(self):
339
        if self._invest_group is True:
340
            return GenericInvestmentStorageBlock
341
        else:
342
            return GenericStorageBlock
343
344
345
class GenericStorageBlock(ScalarBlock):
346
    r"""Storage without an :class:`.Investment` object.
347
348
    **The following sets are created:** (-> see basic sets at
349
    :class:`.Model` )
350
351
    STORAGES
352
        A set with all :py:class:`~.GenericStorage` objects, which do not have an
353
        :attr:`investment` of type :class:`.Investment`.
354
355
    STORAGES_BALANCED
356
        A set of  all :py:class:`~.GenericStorage` objects, with 'balanced' attribute set
357
        to True.
358
359
    STORAGES_WITH_INVEST_FLOW_REL
360
        A set with all :py:class:`~.GenericStorage` objects with two investment
361
        flows coupled with the 'invest_relation_input_output' attribute.
362
363
    **The following variables are created:**
364
365
    storage_content
366
        Storage content for every storage and timestep. The value for the
367
        storage content at the beginning is set by the parameter
368
        `initial_storage_level` or not set if `initial_storage_level` is None.
369
        The variable of storage s and timestep t can be accessed by:
370
        `om.GenericStorageBlock.storage_content[s, t]`
371
372
    **The following constraints are created:**
373
374
    Set storage_content of last time step to one at t=0 if balanced == True
375
        .. math::
376
            E(t_{last}) = E(-1)
377
378
    Storage losses :attr:`om.Storage.losses[n, t]`
379
        .. math:: E_{loss}(t) = &E(t-1) \cdot
380
            1 - (1 - \beta(t))^{\tau(t)/(t_u)} \\
381
            &- \gamma(t)\cdot E_{nom} \cdot {\tau(t)/(t_u)}\\
382
            &- \delta(t) \cdot {\tau(t)/(t_u)}
383
384
    Storage balance :attr:`om.Storage.balance[n, t]`
385
        .. math:: E(t) = &E(t-1) - E_{loss}(t)
386
            &- \frac{\dot{E}_o(p, t)}{\eta_o(t)} \cdot \tau(t)
387
            + \dot{E}_i(p, t) \cdot \eta_i(t) \cdot \tau(t)
388
389
    Connect the invest variables of the input and the output flow.
390
        .. math::
391
          InvestmentFlowBlock.invest(source(n), n, p) + existing = \\
392
          (InvestmentFlowBlock.invest(n, target(n), p) + existing) \\
393
          * invest\_relation\_input\_output(n) \\
394
          \forall n \in \textrm{INVEST\_REL\_IN\_OUT} \\
395
          \forall p \in \textrm{PERIODS}
396
397
398
399
    =========================== ======================= =========
400
    symbol                      explanation             attribute
401
    =========================== ======================= =========
402
    :math:`E(t)`                energy currently stored `storage_content`
403
    :math:`E_{nom}`             nominal capacity of     `nominal_storage_capacity`
404
                                the energy storage
405
    :math:`c(-1)`               state before            `initial_storage_level`
406
                                initial time step
407
    :math:`c_{min}(t)`          minimum allowed storage `min_storage_level[t]`
408
    :math:`c_{max}(t)`          maximum allowed storage `max_storage_level[t]`
409
    :math:`\beta(t)`            fraction of lost energy `loss_rate[t]`
410
                                as share of
411
                                :math:`E(t)` per hour
412
    :math:`\gamma(t)`           fixed loss of energy    `fixed_losses_relative[t]`
413
                                per hour relative to
414
                                :math:`E_{nom}`
415
    :math:`\delta(t)`           absolute fixed loss     `fixed_losses_absolute[t]`
416
                                of energy per hour
417
    :math:`\dot{E}_i(t)`        energy flowing in       `inputs`
418
    :math:`\dot{E}_o(t)`        energy flowing out      `outputs`
419
    :math:`\eta_i(t)`           conversion factor       `inflow_conversion_factor[t]`
420
                                (i.e. efficiency)
421
                                when storing energy
422
    :math:`\eta_o(t)`           conversion factor when  `outflow_conversion_factor[t]`
423
                                (i.e. efficiency)
424
                                taking stored energy
425
    :math:`\tau(t)`             duration of time step
426
    :math:`t_u`                 time unit of losses
427
                                :math:`\beta(t)`,
428
                                :math:`\gamma(t)`
429
                                :math:`\delta(t)` and
430
                                timeincrement
431
                                :math:`\tau(t)`
432
    :math:`c_{storage}(t)`      costs of having         `storage_costs`
433
                                energy stored
434
    =========================== ======================= =========
435
436
    **The following parts of the objective function are created:**
437
438
    *Standard model*
439
440
    * :attr: `storage_costs` not 0
441
442
        .. math::
443
            \sum_{t \in \textrm{TIMEPOINTS} > 0} c_{storage}(t) \cdot E(t)
444
445
446
    *Multi-period model*
447
448
    * :attr:`fixed_costs` not None
449
450
        .. math::
451
            \displaystyle \sum_{pp=0}^{year_{max}} E_{nom}
452
            \cdot c_{fixed}(pp) \cdot DF^{-pp}
453
454
    where:
455
456
    * :math:`DF=(1+dr)` is the discount factor with discount rate :math:`dr`.
457
    * :math:`year_{max}` denotes the last year of the optimization
458
      horizon, i.e. at the end of the last period.
459
460
    """  # noqa: E501
461
462
    CONSTRAINT_GROUP = True
463
464
    def __init__(self, *args, **kwargs):
465
        super().__init__(*args, **kwargs)
466
467
    def _create(self, group=None):
468
        """
469
        Parameters
470
        ----------
471
        group : list
472
            List containing storage objects.
473
            e.g. groups=[storage1, storage2,..]
474
        """
475
        m = self.parent_block()
476
477
        if group is None:
478
            return None
479
480
        i = {n: [i for i in n.inputs][0] for n in group}
481
        o = {n: [o for o in n.outputs][0] for n in group}
482
483
        #  ************* SETS *********************************
484
485
        self.STORAGES = Set(initialize=[n for n in group])
486
487
        self.STORAGES_BALANCED = Set(
488
            initialize=[n for n in group if n.balanced is True]
489
        )
490
491
        self.STORAGES_INITITAL_LEVEL = Set(
492
            initialize=[
493
                n for n in group if n.initial_storage_level is not None
494
            ]
495
        )
496
497
        self.STORAGES_WITH_INVEST_FLOW_REL = Set(
498
            initialize=[
499
                n
500
                for n in group
501
                if n.invest_relation_input_output[0] is not None
502
            ]
503
        )
504
505
        #  ************* VARIABLES *****************************
506
507
        def _storage_content_bound_rule(block, n, t):
508
            """
509
            Rule definition for bounds of storage_content variable of
510
            storage n in timestep t.
511
            """
512
            bounds = (
513
                n.nominal_storage_capacity * n.min_storage_level[t],
514
                n.nominal_storage_capacity * n.max_storage_level[t],
515
            )
516
            return bounds
517
518
        self.storage_content = Var(
519
            self.STORAGES, m.TIMEPOINTS, bounds=_storage_content_bound_rule
520
        )
521
522
        self.storage_losses = Var(self.STORAGES, m.TIMESTEPS)
523
524
        # set the initial storage content
525
        # ToDo: More elegant code possible?
526
        for n in group:
527
            if n.initial_storage_level is not None:
528
                self.storage_content[n, 0] = (
529
                    n.initial_storage_level * n.nominal_storage_capacity
530
                )
531
                self.storage_content[n, 0].fix()
532
533
        #  ************* Constraints ***************************
534
535
        def _storage_losses_rule(block, n, t):
536
            expr = block.storage_content[n, t] * (
537
                1 - (1 - n.loss_rate[t]) ** m.timeincrement[t]
538
            )
539
            expr += (
540
                n.fixed_losses_relative[t]
541
                * n.nominal_storage_capacity
542
                * m.timeincrement[t]
543
            )
544
            expr += n.fixed_losses_absolute[t] * m.timeincrement[t]
545
546
            return expr == block.storage_losses[n, t]
547
548
        self.losses = Constraint(
549
            self.STORAGES, m.TIMESTEPS, rule=_storage_losses_rule
550
        )
551
552
        def _storage_balance_rule(block, n, t):
553
            """
554
            Rule definition for the storage balance of every storage n and
555
            every timestep.
556
            """
557
            expr = block.storage_content[n, t]
558
            expr -= block.storage_losses[n, t]
559
            expr += (
560
                m.flow[i[n], n, t] * n.inflow_conversion_factor[t]
0 ignored issues
show
introduced by
The variable i does not seem to be defined for all execution paths.
Loading history...
561
            ) * m.timeincrement[t]
562
            expr -= (
563
                m.flow[n, o[n], t] / n.outflow_conversion_factor[t]
0 ignored issues
show
introduced by
The variable o does not seem to be defined for all execution paths.
Loading history...
564
            ) * m.timeincrement[t]
565
            return expr == block.storage_content[n, t + 1]
566
567
        self.balance = Constraint(
568
            self.STORAGES, m.TIMESTEPS, rule=_storage_balance_rule
569
        )
570
571
        def _balanced_storage_rule(block, n):
572
            """
573
            Storage content of last time step == initial storage content
574
            if balanced.
575
            """
576
            return (
577
                block.storage_content[n, m.TIMEPOINTS.at(-1)]
578
                == block.storage_content[n, m.TIMEPOINTS.at(1)]
579
            )
580
581
        self.balanced_cstr = Constraint(
582
            self.STORAGES_BALANCED, rule=_balanced_storage_rule
583
        )
584
585
        def _power_coupled(_):
586
            """
587
            Rule definition for constraint to connect the input power
588
            and output power
589
            """
590
            for n in self.STORAGES_WITH_INVEST_FLOW_REL:
591
                for p in m.PERIODS:
592
                    expr = (
593
                        m.InvestmentFlowBlock.total[n, o[n], p]
0 ignored issues
show
introduced by
The variable o does not seem to be defined for all execution paths.
Loading history...
594
                    ) * n.invest_relation_input_output[p] == (
595
                        m.InvestmentFlowBlock.total[i[n], n, p]
0 ignored issues
show
introduced by
The variable i does not seem to be defined for all execution paths.
Loading history...
596
                    )
597
                    self.power_coupled.add((n, p), expr)
598
599
        self.power_coupled = Constraint(
600
            self.STORAGES_WITH_INVEST_FLOW_REL, m.PERIODS, noruleinit=True
601
        )
602
603
        self.power_coupled_build = BuildAction(rule=_power_coupled)
604
605
    def _objective_expression(self):
606
        r"""
607
        Objective expression for storages with no investment.
608
609
        Note
610
        ----
611
        * For standard models, this adds nothing as variable costs are
612
          already added in the Block :py:class:`~.SimpleFlowBlock`.
613
        * For multi-period models, fixed costs may be introduced
614
          and added here.
615
        """
616
        m = self.parent_block()
617
618
        fixed_costs = 0
619
620
        if m.es.periods is not None:
621
            for n in self.STORAGES:
622
                if valid_sequence(n.fixed_costs, len(m.PERIODS)):
623
                    fixed_costs += sum(
624
                        n.nominal_storage_capacity
625
                        * n.fixed_costs[pp]
626
                        * (1 + m.discount_rate) ** (-pp)
627
                        for pp in range(m.es.end_year_of_optimization)
628
                    )
629
        self.fixed_costs = Expression(expr=fixed_costs)
630
631
        storage_costs = 0
632
633
        for n in self.STORAGES:
634
            if valid_sequence(n.storage_costs, len(m.TIMESTEPS)):
635
                # We actually want to iterate over all TIMEPOINTS except the
636
                # 0th. As integers are used for the index, this is equicalent
637
                # to iterating over the TIMESTEPS with one offset.
638
                for t in m.TIMESTEPS:
639
                    storage_costs += (
640
                        self.storage_content[n, t + 1] * n.storage_costs[t]
641
                    )
642
643
        self.storage_costs = Expression(expr=storage_costs)
644
        self.costs = Expression(expr=storage_costs + fixed_costs)
645
646
        return self.costs
647
648
649
class GenericInvestmentStorageBlock(ScalarBlock):
650
    r"""
651
    Block for all storages with :attr:`Investment` being not None.
652
    See :class:`.Investment` for all parameters of the
653
    Investment class.
654
655
    **Variables**
656
657
    All Storages are indexed by :math:`n` (denoting the respective storage
658
    unit), which is omitted in the following for the sake of convenience.
659
    The following variables are created as attributes of
660
    :attr:`om.GenericInvestmentStorageBlock`:
661
662
    * :math:`P_i(p, t)`
663
664
        Inflow of the storage
665
        (created in :class:`oemof.solph.models.Model`).
666
667
    * :math:`P_o(p, t)`
668
669
        Outflow of the storage
670
        (created in :class:`oemof.solph.models.Model`).
671
672
    * :math:`E(t)`
673
674
        Current storage content (Absolute level of stored energy).
675
676
    * :math:`E_{invest}(p)`
677
678
        Invested (nominal) capacity of the storage in period p.
679
680
    * :math:`E_{total}(p)`
681
682
        Total installed (nominal) capacity of the storage in period p.
683
684
    * :math:`E_{old}(p)`
685
686
        Old (nominal) capacity of the storage to be decommissioned in period p.
687
688
    * :math:`E_{old,exo}(p)`
689
690
        Exogenous old (nominal) capacity of the storage to be decommissioned
691
        in period p; existing capacity reaching its lifetime.
692
693
    * :math:`E_{old,endo}(p)`
694
695
        Endogenous old (nominal) capacity of the storage to be decommissioned
696
        in period p; endgenous investments reaching their lifetime.
697
698
    * :math:`E(-1)`
699
700
        Initial storage content (before timestep 0).
701
        Not applicable for a multi-period model.
702
703
    * :math:`b_{invest}(p)`
704
705
        Binary variable for the status of the investment, if
706
        :attr:`nonconvex` is `True`.
707
708
    **Constraints**
709
710
    The following constraints are created for all investment storages:
711
712
        Storage balance (Same as for :class:`.GenericStorageBlock`)
713
714
        .. math:: E(t) = &E(t-1) \cdot
715
            (1 - \beta(t)) ^{\tau(t)/(t_u)} \\
716
            &- \gamma(t)\cdot (E_{total}(p)) \cdot {\tau(t)/(t_u)}\\
717
            &- \delta(t) \cdot {\tau(t)/(t_u)}\\
718
            &- \frac{\dot{E}_o(p, t))}{\eta_o(t)} \cdot \tau(t)
719
            + \dot{E}_i(p, t) \cdot \eta_i(t) \cdot \tau(t)
720
721
        Total storage capacity (p > 0 for multi-period model only)
722
723
        .. math::
724
            &
725
            if \quad p=0:\\
726
            &
727
            E_{total}(p) = E_{exist} + E_{invest}(p)\\
728
            &\\
729
            &
730
            else:\\
731
            &
732
            E_{total}(p) = E_{total}(p-1) + E_{invest}(p) - E_{old}(p)\\
733
            &\\
734
            &
735
            \forall p \in \textrm{PERIODS}
736
737
        Old storage capacity (p > 0 for multi-period model only)
738
739
        .. math::
740
            &
741
            E_{old}(p) = E_{old,exo}(p) + E_{old,end}(p)\\
742
            &\\
743
            &
744
            if \quad p=0:\\
745
            &
746
            E_{old,end}(p) = 0\\
747
            &\\
748
            &
749
            else \quad if \quad l \leq year(p):\\
750
            &
751
            E_{old,end}(p) = E_{invest}(p_{comm})\\
752
            &\\
753
            &
754
            else:\\
755
            &
756
            E_{old,end}(p)\\
757
            &\\
758
            &
759
            if \quad p=0:\\
760
            &
761
            E_{old,exo}(p) = 0\\
762
            &\\
763
            &
764
            else \quad if \quad l - a \leq year(p):\\
765
            &
766
            E_{old,exo}(p) = E_{exist} (*)\\
767
            &\\
768
            &
769
            else:\\
770
            &
771
            E_{old,exo}(p) = 0\\
772
            &\\
773
            &
774
            \forall p \in \textrm{PERIODS}
775
776
        where:
777
778
        * (*) is only performed for the first period the condition is True.
779
          A decommissioning flag is then set to True to prevent having falsely
780
          added old capacity in future periods.
781
        * :math:`year(p)` is the year corresponding to period p
782
        * :math:`p_{comm}` is the commissioning period of the storage
783
784
    Depending on the attribute :attr:`nonconvex`, the constraints for the
785
    bounds of the decision variable :math:`E_{invest}(p)` are different:\
786
787
        * :attr:`nonconvex = False`
788
789
        .. math::
790
            &
791
            E_{invest, min}(p) \le E_{invest}(p) \le E_{invest, max}(p) \\
792
            &
793
            \forall p \in \textrm{PERIODS}
794
795
        * :attr:`nonconvex = True`
796
797
        .. math::
798
            &
799
            E_{invest, min}(p) \cdot b_{invest}(p) \le E_{invest}(p)\\
800
            &
801
            E_{invest}(p) \le E_{invest, max}(p) \cdot b_{invest}(p)\\
802
            &
803
            \forall p \in \textrm{PERIODS}
804
805
    The following constraints are created depending on the attributes of
806
    the :class:`.GenericStorage`:
807
808
        * :attr:`initial_storage_level is None`;
809
          not applicable for multi-period model
810
811
            Constraint for a variable initial storage content:
812
813
        .. math::
814
               E(-1) \le E_{exist} + E_{invest}(0)
815
816
        * :attr:`initial_storage_level is not None`;
817
          not applicable for multi-period model
818
819
            An initial value for the storage content is given:
820
821
        .. math::
822
               E(-1) = (E_{invest}(0) + E_{exist}) \cdot c(-1)
823
824
        * :attr:`balanced=True`;
825
          not applicable for multi-period model
826
827
            The energy content of storage of the first and the last timestep
828
            are set equal:
829
830
        .. math::
831
            E(-1) = E(t_{last})
832
833
        * :attr:`invest_relation_input_capacity is not None`
834
835
            Connect the invest variables of the storage and the input flow:
836
837
        .. math::
838
            &
839
            P_{i,total}(p) =
840
            E_{total}(p) \cdot r_{cap,in} \\
841
            &
842
            \forall p \in \textrm{PERIODS}
843
844
        * :attr:`invest_relation_output_capacity is not None`
845
846
            Connect the invest variables of the storage and the output flow:
847
848
        .. math::
849
            &
850
            P_{o,total}(p) =
851
            E_{total}(p) \cdot r_{cap,out}\\
852
            &
853
            \forall p \in \textrm{PERIODS}
854
855
        * :attr:`invest_relation_input_output is not None`
856
857
            Connect the invest variables of the input and the output flow:
858
859
        .. math::
860
            &
861
            P_{i,total}(p) =
862
            P_{o,total}(p) \cdot r_{in,out}\\
863
            &
864
            \forall p \in \textrm{PERIODS}
865
866
        * :attr:`max_storage_level`
867
868
            Rule for upper bound constraint for the storage content:
869
870
        .. math::
871
            &
872
            E(t) \leq E_{total}(p) \cdot c_{max}(t)\\
873
            &
874
            \forall p, t \in \textrm{TIMEINDEX}
875
876
        * :attr:`min_storage_level`
877
878
            Rule for lower bound constraint for the storage content:
879
880
        .. math::
881
            &
882
            E(t) \geq E_{total}(p) \cdot c_{min}(t)\\
883
            &
884
            \forall p, t \in \textrm{TIMEINDEX}
885
886
887
    **Objective function**
888
889
    Objective terms for a standard model and a multi-period model differ
890
    quite strongly. Besides, the part of the objective function added by the
891
    investment storages also depends on whether a convex or nonconvex
892
    investment option is selected. The following parts of the objective
893
    function are created:
894
895
    *Standard model*
896
897
        * :attr:`nonconvex = False`
898
899
            .. math::
900
                E_{invest}(0) \cdot c_{invest,var}(0)
901
902
        * :attr:`nonconvex = True`
903
904
            .. math::
905
                E_{invest}(0) \cdot c_{invest,var}(0)
906
                + c_{invest,fix}(0) \cdot b_{invest}(0)\\
907
908
    Where 0 denotes the 0th (investment) period since
909
    in a standard model, there is only this one period.
910
911
    *Multi-period model*
912
913
        * :attr:`nonconvex = False`
914
915
            .. math::
916
                &
917
                E_{invest}(p) \cdot A(c_{invest,var}(p), l, ir)
918
                \cdot \frac {1}{ANF(d, ir)} \cdot DF^{-p}\\
919
                &
920
                \forall p \in \textrm{PERIODS}
921
922
        In case, the remaining lifetime of a storage is greater than 0 and
923
        attribute `use_remaining_value` of the energy system is True,
924
        the difference in value for the investment period compared to the
925
        last period of the optimization horizon is accounted for
926
        as an adder to the investment costs:
927
928
            .. math::
929
                &
930
                E_{invest}(p) \cdot (A(c_{invest,var}(p), l_{r}, ir) -
931
                A(c_{invest,var}(|P|), l_{r}, ir)\\
932
                & \cdot \frac {1}{ANF(l_{r}, ir)} \cdot DF^{-|P|}\\
933
                &\\
934
                &
935
                \forall p \in \textrm{PERIODS}
936
937
        * :attr:`nonconvex = True`
938
939
            .. math::
940
                &
941
                (E_{invest}(p) \cdot A(c_{invest,var}(p), l, ir)
942
                \cdot \frac {1}{ANF(d, ir)}\\
943
                &
944
                +  c_{invest,fix}(p) \cdot b_{invest}(p)) \cdot DF^{-p} \\
945
                &
946
                \forall p \in \textrm{PERIODS}
947
948
        In case, the remaining lifetime of a storage is greater than 0 and
949
        attribute `use_remaining_value` of the energy system is True,
950
        the difference in value for the investment period compared to the
951
        last period of the optimization horizon is accounted for
952
        as an adder to the investment costs:
953
954
            .. math::
955
                &
956
                (E_{invest}(p) \cdot (A(c_{invest,var}(p), l_{r}, ir) -
957
                A(c_{invest,var}(|P|), l_{r}, ir)\\
958
                & \cdot \frac {1}{ANF(l_{r}, ir)} \cdot DF^{-|P|}\\
959
                &
960
                +  (c_{invest,fix}(p) - c_{invest,fix}(|P|))
961
                \cdot b_{invest}(p)) \cdot DF^{-p}\\
962
                &\\
963
                &
964
                \forall p \in \textrm{PERIODS}
965
966
        * :attr:`fixed_costs` not None for investments
967
968
            .. math::
969
                &
970
                \sum_{pp=year(p)}^{limit_{end}}
971
                E_{invest}(p) \cdot c_{fixed}(pp) \cdot DF^{-pp})
972
                \cdot DF^{-p}\\
973
                &
974
                \forall p \in \textrm{PERIODS}
975
976
        * :attr:`fixed_costs` not None for existing capacity
977
978
            .. math::
979
                \sum_{pp=0}^{limit_{exo}} E_{exist} \cdot c_{fixed}(pp)
980
                \cdot DF^{-pp}
981
982
    where:
983
984
    * :math:`A(c_{invest,var}(p), l, ir)` A is the annuity for
985
      investment expenses :math:`c_{invest,var}(p)`, lifetime :math:`l`
986
      and interest rate :math:`ir`.
987
    * :math:`l_{r}` is the remaining lifetime at the end of the
988
      optimization horizon (in case it is greater than 0 and
989
      smaller than the actual lifetime).
990
    * :math:`ANF(d, ir)` is the annuity factor for duration :math:`d`
991
      and interest rate :math:`ir`.
992
    * :math:`d=min\{year_{max} - year(p), l\}` defines the
993
      number of years within the optimization horizon that investment
994
      annuities are accounted for.
995
    * :math:`year(p)` denotes the start year of period :math:`p`.
996
    * :math:`year_{max}` denotes the last year of the optimization
997
      horizon, i.e. at the end of the last period.
998
    * :math:`limit_{end}=min\{year_{max}, year(p) + l\}` is used as an
999
      upper bound to ensure fixed costs for endogenous investments
1000
      to occur within the optimization horizon.
1001
    * :math:`limit_{exo}=min\{year_{max}, l - a\}` is used as an
1002
      upper bound to ensure fixed costs for existing capacities to occur
1003
      within the optimization horizon. :math:`a` is the initial age
1004
      of an asset.
1005
    * :math:`DF=(1+dr)` is the discount factor.
1006
1007
    The annuity / annuity factor hereby is:
1008
1009
        .. math::
1010
            &
1011
            A(c_{invest,var}(p), l, ir) = c_{invest,var}(p) \cdot
1012
                \frac {(1+ir)^l \cdot ir} {(1+ir)^l - 1}\\
1013
            &\\
1014
            &
1015
            ANF(d, ir)=\frac {(1+ir)^d \cdot ir} {(1+ir)^d - 1}
1016
1017
    They are retrieved, using oemof.tools.economics annuity function. The
1018
    interest rate :math:`ir` for the annuity is defined as weighted
1019
    average costs of capital (wacc) and assumed constant over time.
1020
1021
    The overall summed cost expressions for all *InvestmentFlowBlock* objects
1022
    can be accessed by
1023
1024
    * :attr:`om.GenericInvestmentStorageBlock.investment_costs`,
1025
    * :attr:`om.GenericInvestmentStorageBlock.fixed_costs` and
1026
    * :attr:`om.GenericInvestmentStorageBlock.costs`.
1027
1028
    Their values  after optimization can be retrieved by
1029
1030
    * :meth:`om.GenericInvestmentStorageBlock.investment_costs`,
1031
    * :attr:`om.GenericInvestmentStorageBlock.period_investment_costs`
1032
      (yielding a dict keyed by periods); note: this is not a Pyomo expression,
1033
      but calculated,
1034
    * :meth:`om.GenericInvestmentStorageBlock.fixed_costs` and
1035
    * :meth:`om.GenericInvestmentStorageBlock.costs`.
1036
1037
    .. csv-table:: List of Variables
1038
        :header: "symbol", "attribute", "explanation"
1039
        :widths: 1, 1, 1
1040
1041
        ":math:`P_i(p, t)`", ":attr:`flow[i[n], n, p, t]`", "Inflow
1042
        of the storage"
1043
        ":math:`P_o(p, t)`", ":attr:`flow[n, o[n], p, t]`", "Outflow
1044
        of the storage"
1045
        ":math:`E(t)`", ":attr:`storage_content[n, t]`", "Current storage
1046
        content (current absolute stored energy)"
1047
        ":math:`E_{loss}(t)`", ":attr:`storage_losses[n, t]`", "Current storage
1048
        losses (absolute losses per time step)"
1049
        ":math:`E_{invest}(p)`", ":attr:`invest[n, p]`", "Invested (nominal)
1050
        capacity of the storage"
1051
        ":math:`E_{old}(p)`", ":attr:`old[n, p]`", "
1052
        | Old (nominal) capacity of the storage
1053
        | to be decommissioned in period p"
1054
        ":math:`E_{old,exo}(p)`", ":attr:`old_exo[n, p]`", "
1055
        | Old (nominal) capacity of the storage
1056
        | to be decommissioned in period p
1057
        | which was exogenously given by :math:`E_{exist}`"
1058
        ":math:`E_{old,end}(p)`", ":attr:`old_end[n, p]`", "
1059
        | Old (nominal) capacity of the storage
1060
        | to be decommissioned in period p
1061
        | which was endogenously determined by :math:`E_{invest}(p_{comm})`
1062
        | where :math:`p_{comm}` is the commissioning period"
1063
        ":math:`E(-1)`", ":attr:`init_cap[n]`", "Initial storage capacity
1064
        (before timestep 0)"
1065
        ":math:`b_{invest}(p)`", ":attr:`invest_status[i, o, p]`", "Binary
1066
        variable for the status of investment"
1067
        ":math:`P_{i,invest}(p)`", "
1068
        :attr:`InvestmentFlowBlock.invest[i[n], n, p]`", "
1069
        Invested (nominal) inflow (InvestmentFlowBlock)"
1070
        ":math:`P_{o,invest}`", "
1071
        :attr:`InvestmentFlowBlock.invest[n, o[n]]`", "
1072
        Invested (nominal) outflow (InvestmentFlowBlock)"
1073
1074
    .. csv-table:: List of Parameters
1075
        :header: "symbol", "attribute", "explanation"
1076
        :widths: 1, 1, 1
1077
1078
        ":math:`E_{exist}`", "`flows[i, o].investment.existing`", "
1079
        Existing storage capacity"
1080
        ":math:`E_{invest,min}`", "`flows[i, o].investment.minimum`", "
1081
        Minimum investment value"
1082
        ":math:`E_{invest,max}`", "`flows[i, o].investment.maximum`", "
1083
        Maximum investment value"
1084
        ":math:`P_{i,exist}`", "`flows[i[n], n].investment.existing`
1085
        ", "Existing inflow capacity"
1086
        ":math:`P_{o,exist}`", "`flows[n, o[n]].investment.existing`
1087
        ", "Existing outflow capacity"
1088
        ":math:`c_{invest,var}`", "`flows[i, o].investment.ep_costs`
1089
        ", "Variable investment costs"
1090
        ":math:`c_{invest,fix}`", "`flows[i, o].investment.offset`", "
1091
        Fix investment costs"
1092
        ":math:`c_{fixed}`", "`flows[i, o].investment.fixed_costs`", "
1093
        Fixed costs; only allowed in multi-period model"
1094
        ":math:`r_{cap,in}`", ":attr:`invest_relation_input_capacity`", "
1095
        Relation of storage capacity and nominal inflow"
1096
        ":math:`r_{cap,out}`", ":attr:`invest_relation_output_capacity`", "
1097
        Relation of storage capacity and nominal outflow"
1098
        ":math:`r_{in,out}`", ":attr:`invest_relation_input_output`", "
1099
        Relation of nominal in- and outflow"
1100
        ":math:`\beta(t)`", "`loss_rate[t]`", "Fraction of lost energy
1101
        as share of :math:`E(t)` per hour"
1102
        ":math:`\gamma(t)`", "`fixed_losses_relative[t]`", "Fixed loss
1103
        of energy relative to :math:`E_{invest} + E_{exist}` per hour"
1104
        ":math:`\delta(t)`", "`fixed_losses_absolute[t]`", "Absolute
1105
        fixed loss of energy per hour"
1106
        ":math:`\eta_i(t)`", "`inflow_conversion_factor[t]`", "
1107
        Conversion factor (i.e. efficiency) when storing energy"
1108
        ":math:`\eta_o(t)`", "`outflow_conversion_factor[t]`", "
1109
        Conversion factor when (i.e. efficiency) taking stored energy"
1110
        ":math:`c(-1)`", "`initial_storage_level`", "Initial relative
1111
        storage content (before timestep 0)"
1112
        ":math:`c_{max}`", "`flows[i, o].max[t]`", "Normed maximum
1113
        value of storage content"
1114
        ":math:`c_{min}`", "`flows[i, o].min[t]`", "Normed minimum
1115
        value of storage content"
1116
        ":math:`l`", "`flows[i, o].investment.lifetime`", "
1117
        Lifetime for investments in storage capacity"
1118
        ":math:`a`", "`flows[i, o].investment.age`", "
1119
        Initial age of existing capacity / energy"
1120
        ":math:`ir`", "`flows[i, o].investment.interest_rate`", "
1121
        interest rate for investment"
1122
        ":math:`\tau(t)`", "", "Duration of time step"
1123
        ":math:`t_u`", "", "Time unit of losses :math:`\beta(t)`,
1124
        :math:`\gamma(t)`, :math:`\delta(t)` and timeincrement :math:`\tau(t)`"
1125
1126
    """
1127
1128
    CONSTRAINT_GROUP = True
1129
1130
    def __init__(self, *args, **kwargs):
1131
        super().__init__(*args, **kwargs)
1132
1133
    def _create(self, group):
1134
        """Create a storage block for investment modeling"""
1135
        m = self.parent_block()
1136
1137
        # ########################## CHECKS ###################################
1138
        if m.es.periods is not None:
1139
            for n in group:
1140
                error_fixed_absolute_losses = (
1141
                    "For a multi-period investment model, fixed absolute"
1142
                    " losses are not supported. Please remove parameter."
1143
                )
1144
                if n.fixed_losses_absolute[0] != 0:
1145
                    raise ValueError(error_fixed_absolute_losses)
1146
                error_initial_storage_level = (
1147
                    "For a multi-period model, initial_storage_level is"
1148
                    " not supported.\nIt needs to be removed since it"
1149
                    " has no effect.\nstorage_content will be zero,"
1150
                    " until there is some usable storage capacity installed."
1151
                )
1152
                if n.initial_storage_level is not None:
1153
                    raise ValueError(error_initial_storage_level)
1154
1155
        # ########################## SETS #####################################
1156
1157
        self.INVESTSTORAGES = Set(initialize=[n for n in group])
1158
1159
        self.CONVEX_INVESTSTORAGES = Set(
1160
            initialize=[n for n in group if n.investment.nonconvex is False]
1161
        )
1162
1163
        self.NON_CONVEX_INVESTSTORAGES = Set(
1164
            initialize=[n for n in group if n.investment.nonconvex is True]
1165
        )
1166
1167
        self.INVESTSTORAGES_BALANCED = Set(
1168
            initialize=[n for n in group if n.balanced is True]
1169
        )
1170
1171
        self.INVESTSTORAGES_NO_INIT_CONTENT = Set(
1172
            initialize=[n for n in group if n.initial_storage_level is None]
1173
        )
1174
1175
        self.INVESTSTORAGES_INIT_CONTENT = Set(
1176
            initialize=[
1177
                n for n in group if n.initial_storage_level is not None
1178
            ]
1179
        )
1180
1181
        self.INVEST_REL_CAP_IN = Set(
1182
            initialize=[
1183
                n
1184
                for n in group
1185
                if n.invest_relation_input_capacity[0] is not None
1186
            ]
1187
        )
1188
1189
        self.INVEST_REL_CAP_OUT = Set(
1190
            initialize=[
1191
                n
1192
                for n in group
1193
                if n.invest_relation_output_capacity[0] is not None
1194
            ]
1195
        )
1196
1197
        self.INVEST_REL_IN_OUT = Set(
1198
            initialize=[
1199
                n
1200
                for n in group
1201
                if n.invest_relation_input_output[0] is not None
1202
            ]
1203
        )
1204
1205
        # The storage content is a non-negative variable, therefore it makes no
1206
        # sense to create an additional constraint if the lower bound is zero
1207
        # for all time steps.
1208
        self.MIN_INVESTSTORAGES = Set(
1209
            initialize=[
1210
                n
1211
                for n in group
1212
                if sum([n.min_storage_level[t] for t in m.TIMESTEPS]) > 0
1213
            ]
1214
        )
1215
1216
        self.OVERALL_MAXIMUM_INVESTSTORAGES = Set(
1217
            initialize=[
1218
                n for n in group if n.investment.overall_maximum is not None
1219
            ]
1220
        )
1221
1222
        self.OVERALL_MINIMUM_INVESTSTORAGES = Set(
1223
            initialize=[
1224
                n for n in group if n.investment.overall_minimum is not None
1225
            ]
1226
        )
1227
1228
        self.EXISTING_INVESTSTORAGES = Set(
1229
            initialize=[n for n in group if n.investment.existing is not None]
1230
        )
1231
1232
        # ######################### Variables  ################################
1233
        self.storage_content = Var(
1234
            self.INVESTSTORAGES, m.TIMEPOINTS, within=NonNegativeReals
1235
        )
1236
1237
        def _storage_investvar_bound_rule(_, n, p):
1238
            """
1239
            Rule definition to bound the invested storage capacity `invest`.
1240
            """
1241
            if n in self.CONVEX_INVESTSTORAGES:
1242
                return n.investment.minimum[p], n.investment.maximum[p]
1243
            else:  # n in self.NON_CONVEX_INVESTSTORAGES
1244
                return 0, n.investment.maximum[p]
1245
1246
        self.invest = Var(
1247
            self.INVESTSTORAGES,
1248
            m.PERIODS,
1249
            within=NonNegativeReals,
1250
            bounds=_storage_investvar_bound_rule,
1251
        )
1252
1253
        # Total capacity
1254
        self.total = Var(
1255
            self.INVESTSTORAGES,
1256
            m.PERIODS,
1257
            within=NonNegativeReals,
1258
            initialize=0,
1259
        )
1260
1261
        if m.es.periods is not None:
1262
            # Old capacity to be decommissioned (due to lifetime)
1263
            self.old = Var(
1264
                self.INVESTSTORAGES, m.PERIODS, within=NonNegativeReals
1265
            )
1266
1267
            # Old endogenous capacity to be decommissioned (due to lifetime)
1268
            self.old_end = Var(
1269
                self.INVESTSTORAGES, m.PERIODS, within=NonNegativeReals
1270
            )
1271
1272
            # Old exogenous capacity to be decommissioned (due to lifetime)
1273
            self.old_exo = Var(
1274
                self.INVESTSTORAGES, m.PERIODS, within=NonNegativeReals
1275
            )
1276
1277
        else:
1278
            self.init_content = Var(
1279
                self.INVESTSTORAGES, within=NonNegativeReals
1280
            )
1281
1282
        # create status variable for a non-convex investment storage
1283
        self.invest_status = Var(
1284
            self.NON_CONVEX_INVESTSTORAGES, m.PERIODS, within=Binary
1285
        )
1286
1287
        # ######################### CONSTRAINTS ###############################
1288
        i = {n: [i for i in n.inputs][0] for n in group}
1289
        o = {n: [o for o in n.outputs][0] for n in group}
1290
1291
        # Handle unit lifetimes
1292
        def _total_storage_capacity_rule(block):
1293
            """Rule definition for determining total installed
1294
            capacity (taking decommissioning into account)
1295
            """
1296
            for n in self.INVESTSTORAGES:
1297
                for p in m.PERIODS:
1298
                    if p == 0:
1299
                        expr = (
1300
                            self.total[n, p]
1301
                            == self.invest[n, p] + n.investment.existing
1302
                        )
1303
                        self.total_storage_rule.add((n, p), expr)
1304
                    else:
1305
                        expr = (
1306
                            self.total[n, p]
1307
                            == self.invest[n, p]
1308
                            + self.total[n, p - 1]
1309
                            - self.old[n, p]
1310
                        )
1311
                        self.total_storage_rule.add((n, p), expr)
1312
1313
        self.total_storage_rule = Constraint(
1314
            self.INVESTSTORAGES, m.PERIODS, noruleinit=True
1315
        )
1316
1317
        self.total_storage_rule_build = BuildAction(
1318
            rule=_total_storage_capacity_rule
1319
        )
1320
1321
        # multi-period storage implementation for time intervals
1322
        if m.es.periods is not None:
1323
1324
            def _old_storage_capacity_rule_end(block):
1325
                """Rule definition for determining old endogenously installed
1326
                capacity to be decommissioned due to reaching its lifetime.
1327
                Investment and decommissioning periods are linked within
1328
                the constraint. The respective decommissioning period is
1329
                determined for every investment period based on the components
1330
                lifetime and a matrix describing its age of each endogenous
1331
                investment. Decommissioning can only occur at the beginning of
1332
                each period.
1333
1334
                Note
1335
                ----
1336
                For further information on the implementation check
1337
                PR#957 https://github.com/oemof/oemof-solph/pull/957
1338
                """
1339
                for n in self.INVESTSTORAGES:
1340
                    lifetime = n.investment.lifetime
1341
                    if lifetime is None:
1342
                        msg = (
1343
                            "You have to specify a lifetime "
1344
                            "for a Flow going into or out of "
1345
                            "a GenericStorage unit "
1346
                            "in a multi-period model!"
1347
                            f" Value for {n} is missing."
1348
                        )
1349
                        raise ValueError(msg)
1350
                    # get the period matrix describing the temporal distance
1351
                    # between all period combinations.
1352
                    periods_matrix = m.es.periods_matrix
1353
1354
                    # get the index of the minimum value in each row greater
1355
                    # equal than the lifetime. This value equals the
1356
                    # decommissioning period if not zero. The index of this
1357
                    # value represents the investment period. If np.where
1358
                    # condition is not met in any row, min value will be zero
1359
                    decomm_periods = np.argmin(
1360
                        np.where(
1361
                            (periods_matrix >= lifetime),
1362
                            periods_matrix,
1363
                            np.inf,
1364
                        ),
1365
                        axis=1,
1366
                    )
1367
1368
                    # no decommissioning in first period
1369
                    expr = self.old_end[n, 0] == 0
1370
                    self.old_rule_end.add((n, 0), expr)
1371
1372
                    # all periods not in decomm_periods have no decommissioning
1373
                    # zero is excluded
1374
                    for p in m.PERIODS:
1375
                        if p not in decomm_periods and p != 0:
1376
                            expr = self.old_end[n, p] == 0
1377
                            self.old_rule_end.add((n, p), expr)
1378
1379
                    # multiple invests can be decommissioned in the same period
1380
                    # but only sequential ones, thus a bookkeeping is
1381
                    # introduced andconstraints are added to equation one
1382
                    # iteration later.
1383
                    last_decomm_p = np.nan
1384
                    # loop over invest periods (values are decomm_periods)
1385
                    for invest_p, decomm_p in enumerate(decomm_periods):
1386
                        # Add constraint of iteration before
1387
                        # (skipped in first iteration by last_decomm_p = nan)
1388
                        if (decomm_p != last_decomm_p) and (
1389
                            last_decomm_p is not np.nan
1390
                        ):
1391
                            expr = self.old_end[n, last_decomm_p] == expr
1392
                            self.old_rule_end.add((n, last_decomm_p), expr)
1393
1394
                        # no decommissioning if decomm_p is zero
1395
                        if decomm_p == 0:
1396
                            # overwrite decomm_p with zero to avoid
1397
                            # chaining invest periods in next iteration
1398
                            last_decomm_p = 0
1399
1400
                        # if decomm_p is the same as the last one chain invest
1401
                        # period
1402
                        elif decomm_p == last_decomm_p:
1403
                            expr += self.invest[n, invest_p]
1404
                            # overwrite decomm_p
1405
                            last_decomm_p = decomm_p
1406
1407
                        # if decomm_p is not zero, not the same as the last one
1408
                        # and it's not the first period
1409
                        else:
1410
                            expr = self.invest[n, invest_p]
1411
                            # overwrite decomm_p
1412
                            last_decomm_p = decomm_p
1413
1414
                    # Add constraint of very last iteration
1415
                    if last_decomm_p != 0:
1416
                        expr = self.old_end[n, last_decomm_p] == expr
1417
                        self.old_rule_end.add((n, last_decomm_p), expr)
1418
1419
            self.old_rule_end = Constraint(
1420
                self.INVESTSTORAGES, m.PERIODS, noruleinit=True
1421
            )
1422
1423
            self.old_rule_end_build = BuildAction(
1424
                rule=_old_storage_capacity_rule_end
1425
            )
1426
1427
            def _old_storage_capacity_rule_exo(block):
1428
                """Rule definition for determining old exogenously given
1429
                capacity to be decommissioned due to reaching its lifetime
1430
                """
1431
                for n in self.INVESTSTORAGES:
1432
                    age = n.investment.age
1433
                    lifetime = n.investment.lifetime
1434
                    is_decommissioned = False
1435
                    for p in m.PERIODS:
1436
                        # No shutdown in first period
1437
                        if p == 0:
1438
                            expr = self.old_exo[n, p] == 0
1439
                            self.old_rule_exo.add((n, p), expr)
1440
                        elif lifetime - age <= m.es.periods_years[p]:
1441
                            # Track decommissioning status
1442
                            if not is_decommissioned:
1443
                                expr = (
1444
                                    self.old_exo[n, p] == n.investment.existing
1445
                                )
1446
                                is_decommissioned = True
1447
                            else:
1448
                                expr = self.old_exo[n, p] == 0
1449
                            self.old_rule_exo.add((n, p), expr)
1450
                        else:
1451
                            expr = self.old_exo[n, p] == 0
1452
                            self.old_rule_exo.add((n, p), expr)
1453
1454
            self.old_rule_exo = Constraint(
1455
                self.INVESTSTORAGES, m.PERIODS, noruleinit=True
1456
            )
1457
1458
            self.old_rule_exo_build = BuildAction(
1459
                rule=_old_storage_capacity_rule_exo
1460
            )
1461
1462
            def _old_storage_capacity_rule(block):
1463
                """Rule definition for determining (overall) old capacity
1464
                to be decommissioned due to reaching its lifetime
1465
                """
1466
                for n in self.INVESTSTORAGES:
1467
                    for p in m.PERIODS:
1468
                        expr = (
1469
                            self.old[n, p]
1470
                            == self.old_end[n, p] + self.old_exo[n, p]
1471
                        )
1472
                        self.old_rule.add((n, p), expr)
1473
1474
            self.old_rule = Constraint(
1475
                self.INVESTSTORAGES, m.PERIODS, noruleinit=True
1476
            )
1477
1478
            self.old_rule_build = BuildAction(rule=_old_storage_capacity_rule)
1479
1480
            def _initially_empty_rule(_):
1481
                """Ensure storage to be empty initially"""
1482
                for n in self.INVESTSTORAGES:
1483
                    expr = self.storage_content[n, 0] == 0
1484
                    self.initially_empty.add((n, 0), expr)
1485
1486
            self.initially_empty = Constraint(
1487
                self.INVESTSTORAGES, m.TIMESTEPS, noruleinit=True
1488
            )
1489
1490
            self.initially_empty_build = BuildAction(
1491
                rule=_initially_empty_rule
1492
            )
1493
1494
        # Standard storage implementation for discrete time points
1495
        else:
1496
1497
            def _inv_storage_init_content_max_rule(block, n):
1498
                """Constraint for a variable initial storage capacity."""
1499
                return (
1500
                    block.init_content[n]
1501
                    <= n.investment.existing + block.invest[n, 0]
1502
                )
1503
1504
            self.init_content_limit = Constraint(
1505
                self.INVESTSTORAGES_NO_INIT_CONTENT,
1506
                rule=_inv_storage_init_content_max_rule,
1507
            )
1508
1509
            def _inv_storage_init_content_fix_rule(block, n):
1510
                """Constraint for a fixed initial storage capacity."""
1511
                return block.storage_content[
1512
                    n, 0
1513
                ] == n.initial_storage_level * (
1514
                    n.investment.existing + block.invest[n, 0]
1515
                )
1516
1517
            self.init_content_fix = Constraint(
1518
                self.INVESTSTORAGES_INIT_CONTENT,
1519
                rule=_inv_storage_init_content_fix_rule,
1520
            )
1521
1522
        def _storage_balance_rule(block, n, p, t):
1523
            """
1524
            Rule definition for the storage balance of every storage n and
1525
            every timestep.
1526
            """
1527
            expr = 0
1528
            expr += block.storage_content[n, t + 1]
1529
            expr += (
1530
                -block.storage_content[n, t]
1531
                * (1 - n.loss_rate[t]) ** m.timeincrement[t]
1532
            )
1533
            expr += (
1534
                n.fixed_losses_relative[t]
1535
                * self.total[n, p]
1536
                * m.timeincrement[t]
1537
            )
1538
            expr += n.fixed_losses_absolute[t] * m.timeincrement[t]
1539
            expr += (
1540
                -m.flow[i[n], n, t] * n.inflow_conversion_factor[t]
1541
            ) * m.timeincrement[t]
1542
            expr += (
1543
                m.flow[n, o[n], t] / n.outflow_conversion_factor[t]
1544
            ) * m.timeincrement[t]
1545
            return expr == 0
1546
1547
        self.balance = Constraint(
1548
            self.INVESTSTORAGES,
1549
            m.TIMEINDEX,
1550
            rule=_storage_balance_rule,
1551
        )
1552
1553
        if m.es.periods is None:
1554
1555
            def _balanced_storage_rule(block, n):
1556
                return (
1557
                    block.storage_content[n, m.TIMESTEPS.at(-1)]
1558
                    == block.init_content[n]
1559
                )
1560
1561
            self.balanced_cstr = Constraint(
1562
                self.INVESTSTORAGES_BALANCED, rule=_balanced_storage_rule
1563
            )
1564
1565
        def _power_coupled(block):
1566
            """
1567
            Rule definition for constraint to connect the input power
1568
            and output power
1569
            """
1570
            for n in self.INVEST_REL_IN_OUT:
1571
                for p in m.PERIODS:
1572
                    expr = (
1573
                        m.InvestmentFlowBlock.total[n, o[n], p]
1574
                    ) * n.invest_relation_input_output[p] == (
1575
                        m.InvestmentFlowBlock.total[i[n], n, p]
1576
                    )
1577
                    self.power_coupled.add((n, p), expr)
1578
1579
        self.power_coupled = Constraint(
1580
            self.INVEST_REL_IN_OUT, m.PERIODS, noruleinit=True
1581
        )
1582
1583
        self.power_coupled_build = BuildAction(rule=_power_coupled)
1584
1585
        def _storage_capacity_inflow_invest_rule(block):
1586
            """
1587
            Rule definition of constraint connecting the inflow
1588
            `InvestmentFlowBlock.invest of storage with invested capacity
1589
            `invest` by nominal_storage_capacity__inflow_ratio
1590
            """
1591
            for n in self.INVEST_REL_CAP_IN:
1592
                for p in m.PERIODS:
1593
                    expr = (
1594
                        m.InvestmentFlowBlock.total[i[n], n, p]
1595
                        == self.total[n, p]
1596
                        * n.invest_relation_input_capacity[p]
1597
                    )
1598
                    self.storage_capacity_inflow.add((n, p), expr)
1599
1600
        self.storage_capacity_inflow = Constraint(
1601
            self.INVEST_REL_CAP_IN, m.PERIODS, noruleinit=True
1602
        )
1603
1604
        self.storage_capacity_inflow_build = BuildAction(
1605
            rule=_storage_capacity_inflow_invest_rule
1606
        )
1607
1608
        def _storage_capacity_outflow_invest_rule(block):
1609
            """
1610
            Rule definition of constraint connecting outflow
1611
            `InvestmentFlowBlock.invest` of storage and invested capacity
1612
            `invest` by nominal_storage_capacity__outflow_ratio
1613
            """
1614
            for n in self.INVEST_REL_CAP_OUT:
1615
                for p in m.PERIODS:
1616
                    expr = (
1617
                        m.InvestmentFlowBlock.total[n, o[n], p]
1618
                        == self.total[n, p]
1619
                        * n.invest_relation_output_capacity[p]
1620
                    )
1621
                    self.storage_capacity_outflow.add((n, p), expr)
1622
1623
        self.storage_capacity_outflow = Constraint(
1624
            self.INVEST_REL_CAP_OUT, m.PERIODS, noruleinit=True
1625
        )
1626
1627
        self.storage_capacity_outflow_build = BuildAction(
1628
            rule=_storage_capacity_outflow_invest_rule
1629
        )
1630
1631
        self._add_storage_limit_constraints()
1632
1633
        def maximum_invest_limit(block, n, p):
1634
            """
1635
            Constraint for the maximal investment in non convex investment
1636
            storage.
1637
            """
1638
            return (
1639
                n.investment.maximum[p] * self.invest_status[n, p]
1640
                - self.invest[n, p]
1641
            ) >= 0
1642
1643
        self.limit_max = Constraint(
1644
            self.NON_CONVEX_INVESTSTORAGES,
1645
            m.PERIODS,
1646
            rule=maximum_invest_limit,
1647
        )
1648
1649
        def smallest_invest(block, n, p):
1650
            """
1651
            Constraint for the minimal investment in non convex investment
1652
            storage if the invest is greater than 0. So the invest variable
1653
            can be either 0 or greater than the minimum.
1654
            """
1655
            return (
1656
                self.invest[n, p]
1657
                - n.investment.minimum[p] * self.invest_status[n, p]
1658
                >= 0
1659
            )
1660
1661
        self.limit_min = Constraint(
1662
            self.NON_CONVEX_INVESTSTORAGES, m.PERIODS, rule=smallest_invest
1663
        )
1664
1665
        if m.es.periods is not None:
1666
1667
            def _overall_storage_maximum_investflow_rule(block):
1668
                """Rule definition for maximum overall investment
1669
                in investment case.
1670
                """
1671
                for n in self.OVERALL_MAXIMUM_INVESTSTORAGES:
1672
                    for p in m.PERIODS:
1673
                        expr = self.total[n, p] <= n.investment.overall_maximum
1674
                        self.overall_storage_maximum.add((n, p), expr)
1675
1676
            self.overall_storage_maximum = Constraint(
1677
                self.OVERALL_MAXIMUM_INVESTSTORAGES, m.PERIODS, noruleinit=True
1678
            )
1679
1680
            self.overall_maximum_build = BuildAction(
1681
                rule=_overall_storage_maximum_investflow_rule
1682
            )
1683
1684
            def _overall_minimum_investflow_rule(block):
1685
                """Rule definition for minimum overall investment
1686
                in investment case.
1687
1688
                Note: This is only applicable for the last period
1689
                """
1690
                for n in self.OVERALL_MINIMUM_INVESTSTORAGES:
1691
                    expr = (
1692
                        n.investment.overall_minimum
1693
                        <= self.total[n, m.PERIODS[-1]]
1694
                    )
1695
                    self.overall_minimum.add(n, expr)
1696
1697
            self.overall_minimum = Constraint(
1698
                self.OVERALL_MINIMUM_INVESTSTORAGES, noruleinit=True
1699
            )
1700
1701
            self.overall_minimum_build = BuildAction(
1702
                rule=_overall_minimum_investflow_rule
1703
            )
1704
1705
    def _add_storage_limit_constraints(self):
1706
        m = self.parent_block()
1707
        if m.es.periods is None:
1708
1709
            def _max_storage_content_invest_rule(_, n, t):
1710
                """
1711
                Rule definition for upper bound constraint for the
1712
                storage content.
1713
                """
1714
                expr = (
1715
                    self.storage_content[n, t]
1716
                    <= self.total[n, 0] * n.max_storage_level[t]
1717
                )
1718
                return expr
1719
1720
            self.max_storage_content = Constraint(
1721
                self.INVESTSTORAGES,
1722
                m.TIMEPOINTS,
1723
                rule=_max_storage_content_invest_rule,
1724
            )
1725
1726
            def _min_storage_content_invest_rule(_, n, t):
1727
                """
1728
                Rule definition of lower bound constraint for the
1729
                storage content.
1730
                """
1731
                expr = (
1732
                    self.storage_content[n, t]
1733
                    >= self.total[n, 0] * n.min_storage_level[t]
1734
                )
1735
                return expr
1736
1737
            self.min_storage_content = Constraint(
1738
                self.MIN_INVESTSTORAGES,
1739
                m.TIMEPOINTS,
1740
                rule=_min_storage_content_invest_rule,
1741
            )
1742
        else:
1743
1744
            def _max_storage_content_invest_rule(_, n, p, t):
1745
                """
1746
                Rule definition for upper bound constraint for the
1747
                storage content.
1748
                """
1749
                expr = (
1750
                    self.storage_content[n, t]
1751
                    <= self.total[n, p] * n.max_storage_level[t]
1752
                )
1753
                return expr
1754
1755
            self.max_storage_content = Constraint(
1756
                self.INVESTSTORAGES,
1757
                m.TIMEINDEX,
1758
                rule=_max_storage_content_invest_rule,
1759
            )
1760
1761
            def _min_storage_content_invest_rule(_, n, p, t):
1762
                """
1763
                Rule definition of lower bound constraint for the
1764
                storage content.
1765
                """
1766
                expr = (
1767
                    self.storage_content[n, t]
1768
                    >= self.total[n, p] * n.min_storage_level[t]
1769
                )
1770
                return expr
1771
1772
            self.min_storage_content = Constraint(
1773
                self.MIN_INVESTSTORAGES,
1774
                m.TIMEINDEX,
1775
                rule=_min_storage_content_invest_rule,
1776
            )
1777
1778
    def _objective_expression(self):
1779
        """Objective expression with fixed and investment costs."""
1780
        m = self.parent_block()
1781
1782
        investment_costs = 0
1783
        period_investment_costs = {p: 0 for p in m.PERIODS}
1784
        fixed_costs = 0
1785
1786
        if m.es.periods is None:
1787
            for n in self.CONVEX_INVESTSTORAGES:
1788
                for p in m.PERIODS:
1789
                    investment_costs += (
1790
                        self.invest[n, p] * n.investment.ep_costs[p]
1791
                    )
1792
            for n in self.NON_CONVEX_INVESTSTORAGES:
1793
                for p in m.PERIODS:
1794
                    investment_costs += (
1795
                        self.invest[n, p] * n.investment.ep_costs[p]
1796
                        + self.invest_status[n, p] * n.investment.offset[p]
1797
                    )
1798
1799
        else:
1800
            msg = (
1801
                "You did not specify an interest rate.\n"
1802
                "It will be set equal to the discount_rate of {} "
1803
                "of the model as a default.\nThis corresponds to a "
1804
                "social planner point of view and does not reflect "
1805
                "microeconomic interest requirements."
1806
            )
1807
            for n in self.CONVEX_INVESTSTORAGES:
1808
                lifetime = n.investment.lifetime
1809
                interest = n.investment.interest_rate
1810
                if interest == 0:
1811
                    warn(
1812
                        msg.format(m.discount_rate),
1813
                        debugging.SuspiciousUsageWarning,
1814
                    )
1815
                    interest = m.discount_rate
1816
                for p in m.PERIODS:
1817
                    annuity = economics.annuity(
1818
                        capex=n.investment.ep_costs[p],
1819
                        n=lifetime,
1820
                        wacc=interest,
1821
                    )
1822
                    duration = min(
1823
                        m.es.end_year_of_optimization - m.es.periods_years[p],
1824
                        lifetime,
1825
                    )
1826
                    present_value_factor = 1 / economics.annuity(
1827
                        capex=1, n=duration, wacc=interest
1828
                    )
1829
                    investment_costs_increment = (
1830
                        self.invest[n, p] * annuity * present_value_factor
1831
                    ) * (1 + m.discount_rate) ** (-m.es.periods_years[p])
1832
                    remaining_value_difference = (
1833
                        self._evaluate_remaining_value_difference(
1834
                            m,
1835
                            p,
1836
                            n,
1837
                            m.es.end_year_of_optimization,
1838
                            lifetime,
1839
                            interest,
1840
                        )
1841
                    )
1842
                    investment_costs += (
1843
                        investment_costs_increment + remaining_value_difference
1844
                    )
1845
                    period_investment_costs[p] += investment_costs_increment
1846
1847
            for n in self.NON_CONVEX_INVESTSTORAGES:
1848
                lifetime = n.investment.lifetime
1849
                interest = n.investment.interest_rate
1850
                if interest == 0:
1851
                    warn(
1852
                        msg.format(m.discount_rate),
1853
                        debugging.SuspiciousUsageWarning,
1854
                    )
1855
                    interest = m.discount_rate
1856
                for p in m.PERIODS:
1857
                    annuity = economics.annuity(
1858
                        capex=n.investment.ep_costs[p],
1859
                        n=lifetime,
1860
                        wacc=interest,
1861
                    )
1862
                    duration = min(
1863
                        m.es.end_year_of_optimization - m.es.periods_years[p],
1864
                        lifetime,
1865
                    )
1866
                    present_value_factor = 1 / economics.annuity(
1867
                        capex=1, n=duration, wacc=interest
1868
                    )
1869
                    investment_costs_increment = (
1870
                        self.invest[n, p] * annuity * present_value_factor
1871
                        + self.invest_status[n, p] * n.investment.offset[p]
1872
                    ) * (1 + m.discount_rate) ** (-m.es.periods_years[p])
1873
                    remaining_value_difference = (
1874
                        self._evaluate_remaining_value_difference(
1875
                            m,
1876
                            p,
1877
                            n,
1878
                            m.es.end_year_of_optimization,
1879
                            lifetime,
1880
                            interest,
1881
                            nonconvex=True,
1882
                        )
1883
                    )
1884
                    investment_costs += (
1885
                        investment_costs_increment + remaining_value_difference
1886
                    )
1887
                    period_investment_costs[p] += investment_costs_increment
1888
1889
            for n in self.INVESTSTORAGES:
1890
                if valid_sequence(n.investment.fixed_costs, len(m.PERIODS)):
1891
                    lifetime = n.investment.lifetime
1892
                    for p in m.PERIODS:
1893
                        range_limit = min(
1894
                            m.es.end_year_of_optimization,
1895
                            m.es.periods_years[p] + lifetime,
1896
                        )
1897
                        fixed_costs += sum(
1898
                            self.invest[n, p]
1899
                            * n.investment.fixed_costs[pp]
1900
                            * (1 + m.discount_rate) ** (-pp)
1901
                            for pp in range(
1902
                                m.es.periods_years[p],
1903
                                range_limit,
1904
                            )
1905
                        )
1906
1907
            for n in self.EXISTING_INVESTSTORAGES:
1908
                if valid_sequence(n.investment.fixed_costs, len(m.PERIODS)):
1909
                    lifetime = n.investment.lifetime
1910
                    age = n.investment.age
1911
                    range_limit = min(
1912
                        m.es.end_year_of_optimization, lifetime - age
1913
                    )
1914
                    fixed_costs += sum(
1915
                        n.investment.existing
1916
                        * n.investment.fixed_costs[pp]
1917
                        * (1 + m.discount_rate) ** (-pp)
1918
                        for pp in range(range_limit)
1919
                    )
1920
1921
        self.investment_costs = Expression(expr=investment_costs)
1922
        self.period_investment_costs = period_investment_costs
1923
        self.fixed_costs = Expression(expr=fixed_costs)
1924
        self.costs = Expression(expr=investment_costs + fixed_costs)
1925
1926
        return self.costs
1927
1928
    def _evaluate_remaining_value_difference(
1929
        self,
1930
        m,
1931
        p,
1932
        n,
1933
        end_year_of_optimization,
1934
        lifetime,
1935
        interest,
1936
        nonconvex=False,
1937
    ):
1938
        """Evaluate and return the remaining value difference of an investment
1939
1940
        The remaining value difference in the net present values if the asset
1941
        was to be liquidated at the end of the optimization horizon and the
1942
        net present value using the original investment expenses.
1943
1944
        Parameters
1945
        ----------
1946
        m : oemof.solph.models.Model
1947
            Optimization model
1948
1949
        p : int
1950
            Period in which investment occurs
1951
1952
        n : oemof.solph.components.GenericStorage
1953
            storage unit
1954
1955
        end_year_of_optimization : int
1956
            Last year of the optimization horizon
1957
1958
        lifetime : int
1959
            lifetime of investment considered
1960
1961
        interest : float
1962
            Demanded interest rate for investment
1963
1964
        nonconvex : bool
1965
            Indicating whether considered flow is nonconvex.
1966
        """
1967
        if m.es.use_remaining_value:
1968
            if end_year_of_optimization - m.es.periods_years[p] < lifetime:
1969
                remaining_lifetime = lifetime - (
1970
                    end_year_of_optimization - m.es.periods_years[p]
1971
                )
1972
                remaining_annuity = economics.annuity(
1973
                    capex=n.investment.ep_costs[-1],
1974
                    n=remaining_lifetime,
1975
                    wacc=interest,
1976
                )
1977
                original_annuity = economics.annuity(
1978
                    capex=n.investment.ep_costs[p],
1979
                    n=remaining_lifetime,
1980
                    wacc=interest,
1981
                )
1982
                present_value_factor_remaining = 1 / economics.annuity(
1983
                    capex=1, n=remaining_lifetime, wacc=interest
1984
                )
1985
                convex_investment_costs = (
1986
                    self.invest[n, p]
1987
                    * (remaining_annuity - original_annuity)
1988
                    * present_value_factor_remaining
1989
                ) * (1 + m.discount_rate) ** (-end_year_of_optimization)
1990
                if nonconvex:
1991
                    return convex_investment_costs + self.invest_status[
1992
                        n, p
1993
                    ] * (n.investment.offset[-1] - n.investment.offset[p]) * (
1994
                        1 + m.discount_rate
1995
                    ) ** (
1996
                        -end_year_of_optimization
1997
                    )
1998
                else:
1999
                    return convex_investment_costs
2000
            else:
2001
                return 0
2002
        else:
2003
            return 0
2004