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