Passed
Pull Request — dev (#1183)
by Patrik
04:02
created

GenericInvestmentStorageBlock._objective_expression()   C

Complexity

Conditions 10

Size

Total Lines 42
Code Lines 27

Duplication

Lines 14
Ratio 33.33 %

Importance

Changes 0
Metric Value
eloc 27
dl 14
loc 42
rs 5.9999
c 0
b 0
f 0
cc 10
nop 1

How to fix   Complexity   

Complexity

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

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

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