GenericStorageBlock._objective_expression()   B
last analyzed

Complexity

Conditions 8

Size

Total Lines 42
Code Lines 23

Duplication

Lines 14
Ratio 33.33 %

Importance

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