Completed
Push — dev ( 077813...629ebf )
by Patrik
02:13 queued 02:06
created

GenericStorageBlock._create()   F

Complexity

Conditions 23

Size

Total Lines 313
Code Lines 189

Duplication

Lines 70
Ratio 22.36 %

Importance

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