GenericStorageBlock._create()   F
last analyzed

Complexity

Conditions 23

Size

Total Lines 315
Code Lines 189

Duplication

Lines 70
Ratio 22.22 %

Importance

Changes 0
Metric Value
eloc 189
dl 70
loc 315
rs 0
c 0
b 0
f 0
cc 23
nop 2

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

Complexity

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

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

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