Passed
Pull Request — dev (#821)
by Uwe
02:17
created

GenericInvestmentStorageBlock.__init__()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 2
rs 10
c 0
b 0
f 0
cc 1
nop 3
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
16
SPDX-License-Identifier: MIT
17
18
"""
19
20
from oemof.network import network
21
from pyomo.core.base.block import ScalarBlock
22
from pyomo.environ import Binary
23
from pyomo.environ import Constraint
24
from pyomo.environ import Expression
25
from pyomo.environ import NonNegativeReals
26
from pyomo.environ import Set
27
from pyomo.environ import Var
28
29
from oemof.solph._helpers import check_node_object_for_missing_attribute
30
from oemof.solph._options import Investment
31
from oemof.solph._plumbing import sequence as solph_sequence
32
33
34
class GenericStorage(network.Node):
35
    r"""
36
    Component `GenericStorage` to model with basic characteristics of storages.
37
38
    The GenericStorage is designed for one input and one output.
39
40
    Parameters
41
    ----------
42
    nominal_storage_capacity : numeric, :math:`E_{nom}`
43
        Absolute nominal capacity of the storage
44
    invest_relation_input_capacity : numeric or None, :math:`r_{cap,in}`
45
        Ratio between the investment variable of the input Flow and the
46
        investment variable of the storage:
47
        :math:`\dot{E}_{in,invest} = E_{invest} \cdot r_{cap,in}`
48
    invest_relation_output_capacity : numeric or None, :math:`r_{cap,out}`
49
        Ratio between the investment variable of the output Flow and the
50
        investment variable of the storage:
51
        :math:`\dot{E}_{out,invest} = E_{invest} \cdot r_{cap,out}`
52
    invest_relation_input_output : numeric or None, :math:`r_{in,out}`
53
        Ratio between the investment variable of the output Flow and the
54
        investment variable of the input flow. This ratio used to fix the
55
        flow investments to each other.
56
        Values < 1 set the input flow lower than the output and > 1 will
57
        set the input flow higher than the output flow. If None no relation
58
        will be set:
59
        :math:`\dot{E}_{in,invest} = \dot{E}_{out,invest} \cdot r_{in,out}`
60
    initial_storage_level : numeric, :math:`c(-1)`
61
        The relative storage content in the timestep before the first
62
        time step of optimization (between 0 and 1).
63
    balanced : boolean
64
        Couple storage level of first and last time step.
65
        (Total inflow and total outflow are balanced.)
66
    loss_rate : numeric (iterable or scalar)
67
        The relative loss of the storage content per time unit.
68
    fixed_losses_relative : numeric (iterable or scalar), :math:`\gamma(t)`
69
        Losses independent of state of charge between two consecutive
70
        timesteps relative to nominal storage capacity.
71
    fixed_losses_absolute : numeric (iterable or scalar), :math:`\delta(t)`
72
        Losses independent of state of charge and independent of
73
        nominal storage capacity between two consecutive timesteps.
74
    inflow_conversion_factor : numeric (iterable or scalar), :math:`\eta_i(t)`
75
        The relative conversion factor, i.e. efficiency associated with the
76
        inflow of the storage.
77
    outflow_conversion_factor : numeric (iterable or scalar), :math:`\eta_o(t)`
78
        see: inflow_conversion_factor
79
    min_storage_level : numeric (iterable or scalar), :math:`c_{min}(t)`
80
        The normed minimum storage content as fraction of the
81
        nominal storage capacity (between 0 and 1).
82
        To set different values in every time step use a sequence.
83
    max_storage_level : numeric (iterable or scalar), :math:`c_{max}(t)`
84
        see: min_storage_level
85
    investment : :class:`oemof.solph.options.Investment` object
86
        Object indicating if a nominal_value of the flow is determined by
87
        the optimization problem. Note: This will refer all attributes to an
88
        investment variable instead of to the nominal_storage_capacity. The
89
        nominal_storage_capacity should not be set (or set to None) if an
90
        investment object is used.
91
92
    Notes
93
    -----
94
    The following sets, variables, constraints and objective parts are created
95
     * :py:class:`~oemof.solph.components._generic_storage.GenericStorageBlock`
96
       (if no Investment object present)
97
     * :py:class:`~oemof.solph.components._generic_storage.GenericInvestmentStorageBlock`
98
       (if Investment object present)
99
100
    Examples
101
    --------
102
    Basic usage examples of the GenericStorage with a random selection of
103
    attributes. See the Flow class for all Flow attributes.
104
105
    >>> from oemof import solph
106
107
    >>> my_bus = solph.buses.Bus('my_bus')
108
109
    >>> my_storage = solph.components.GenericStorage(
110
    ...     label='storage',
111
    ...     nominal_storage_capacity=1000,
112
    ...     inputs={my_bus: solph.flows.Flow(nominal_value=200, variable_costs=10)},
113
    ...     outputs={my_bus: solph.flows.Flow(nominal_value=200)},
114
    ...     loss_rate=0.01,
115
    ...     initial_storage_level=0,
116
    ...     max_storage_level = 0.9,
117
    ...     inflow_conversion_factor=0.9,
118
    ...     outflow_conversion_factor=0.93)
119
120
    >>> my_investment_storage = solph.components.GenericStorage(
121
    ...     label='storage',
122
    ...     investment=solph.Investment(ep_costs=50),
123
    ...     inputs={my_bus: solph.flows.Flow()},
124
    ...     outputs={my_bus: solph.flows.Flow()},
125
    ...     loss_rate=0.02,
126
    ...     initial_storage_level=None,
127
    ...     invest_relation_input_capacity=1/6,
128
    ...     invest_relation_output_capacity=1/6,
129
    ...     inflow_conversion_factor=1,
130
    ...     outflow_conversion_factor=0.8)
131
    """  # noqa: E501
132
133
    def __init__(
134
        self, *args, max_storage_level=1, min_storage_level=0, **kwargs
135
    ):
136
        super().__init__(*args, **kwargs)
137
        self.nominal_storage_capacity = kwargs.get("nominal_storage_capacity")
138
        self.initial_storage_level = kwargs.get("initial_storage_level")
139
        self.balanced = kwargs.get("balanced", True)
140
        self.loss_rate = solph_sequence(kwargs.get("loss_rate", 0))
141
        self.fixed_losses_relative = solph_sequence(
142
            kwargs.get("fixed_losses_relative", 0)
143
        )
144
        self.fixed_losses_absolute = solph_sequence(
145
            kwargs.get("fixed_losses_absolute", 0)
146
        )
147
        self.inflow_conversion_factor = solph_sequence(
148
            kwargs.get("inflow_conversion_factor", 1)
149
        )
150
        self.outflow_conversion_factor = solph_sequence(
151
            kwargs.get("outflow_conversion_factor", 1)
152
        )
153
        self.max_storage_level = solph_sequence(max_storage_level)
154
        self.min_storage_level = solph_sequence(min_storage_level)
155
        self.investment = kwargs.get("investment")
156
        self.invest_relation_input_output = kwargs.get(
157
            "invest_relation_input_output"
158
        )
159
        self.invest_relation_input_capacity = kwargs.get(
160
            "invest_relation_input_capacity"
161
        )
162
        self.invest_relation_output_capacity = kwargs.get(
163
            "invest_relation_output_capacity"
164
        )
165
        self._invest_group = isinstance(self.investment, Investment)
166
167
        # Check number of flows.
168
        self._check_number_of_flows()
169
        # Check for infeasible parameter combinations
170
        self._check_infeasible_parameter_combinations()
171
172
        # Check attributes for the investment mode.
173
        if self._invest_group is True:
174
            self._check_invest_attributes()
175
176
        # Check for old parameter names. This is a temporary fix and should
177
        # be removed once a general solution is found.
178
        # TODO: https://github.com/oemof/oemof-solph/issues/560
179
        renamed_parameters = [
180
            ("nominal_capacity", "nominal_storage_capacity"),
181
            ("initial_capacity", "initial_storage_level"),
182
            ("capacity_loss", "loss_rate"),
183
            ("capacity_min", "min_storage_level"),
184
            ("capacity_max", "max_storage_level"),
185
        ]
186
        messages = [
187
            "`{0}` to `{1}`".format(old_name, new_name)
188
            for old_name, new_name in renamed_parameters
189
            if old_name in kwargs
190
        ]
191
        if messages:
192
            message = (
193
                "The following attributes have been renamed from v0.2 to v0.3:"
194
                "\n\n  {}\n\n"
195
                "You are using the old names as parameters, thus setting "
196
                "deprecated\n"
197
                "attributes, which is not what you might have intended.\n"
198
                "Use the new names, or, if you know what you're doing, set "
199
                "these\n"
200
                "attributes explicitly after construction instead."
201
            )
202
            raise AttributeError(message.format("\n  ".join(messages)))
203
204
    def _set_flows(self):
205
        for flow in self.inputs.values():
206
            if (
207
                self.invest_relation_input_capacity is not None
208
                and not isinstance(flow.investment, Investment)
209
            ):
210
                flow.investment = Investment()
211
        for flow in self.outputs.values():
212
            if (
213
                self.invest_relation_output_capacity is not None
214
                and not isinstance(flow.investment, Investment)
215
            ):
216
                flow.investment = Investment()
217
218
    def _check_invest_attributes(self):
219
        if self.investment and self.nominal_storage_capacity is not None:
220
            e1 = (
221
                "If an investment object is defined the invest variable "
222
                "replaces the nominal_storage_capacity.\n Therefore the "
223
                "nominal_storage_capacity should be 'None'.\n"
224
            )
225
            raise AttributeError(e1)
226
        if (
227
            self.invest_relation_input_output is not None
228
            and self.invest_relation_output_capacity is not None
229
            and self.invest_relation_input_capacity is not None
230
        ):
231
            e2 = (
232
                "Overdetermined. Three investment object will be coupled"
233
                "with three constraints. Set one invest relation to 'None'."
234
            )
235
            raise AttributeError(e2)
236
        if (
237
            self.investment
238
            and sum(solph_sequence(self.fixed_losses_absolute)) != 0
239
            and self.investment.existing == 0
240
            and self.investment.minimum == 0
241
        ):
242
            e3 = (
243
                "With fixed_losses_absolute > 0, either investment.existing "
244
                "or investment.minimum has to be non-zero."
245
            )
246
            raise AttributeError(e3)
247
248
        self._set_flows()
249
250
    def _check_number_of_flows(self):
251
        msg = "Only one {0} flow allowed in the GenericStorage {1}."
252
        check_node_object_for_missing_attribute(self, "inputs")
253
        check_node_object_for_missing_attribute(self, "outputs")
254
        if len(self.inputs) > 1:
255
            raise AttributeError(msg.format("input", self.label))
256
        if len(self.outputs) > 1:
257
            raise AttributeError(msg.format("output", self.label))
258
259
    def _check_infeasible_parameter_combinations(self):
260
        """Checks for infeasible parameter combinations and raises error"""
261
        msg = (
262
            "initial_storage_level must be greater or equal to "
263
            "min_storage_level and smaller or equal to "
264
            "max_storage_level."
265
        )
266
        if self.initial_storage_level is not None:
267
            if (
268
                self.initial_storage_level < self.min_storage_level[0]
269
                or self.initial_storage_level > self.max_storage_level[0]
270
            ):
271
                raise ValueError(msg)
272
273
    def constraint_group(self):
274
        if self._invest_group is True:
275
            return GenericInvestmentStorageBlock
276
        else:
277
            return GenericStorageBlock
278
279
280
class GenericStorageBlock(ScalarBlock):
281
    r"""Storage without an :class:`.Investment` object.
282
283
    **The following sets are created:** (-> see basic sets at
284
    :class:`.Model` )
285
286
    STORAGES
287
        A set with all :class:`.Storage` objects, which do not have an
288
         attr:`investment` of type :class:`.Investment`.
289
290
    STORAGES_BALANCED
291
        A set of  all :py:class:`~.GenericStorage` objects, with 'balanced' attribute set
292
        to True.
293
294
    STORAGES_WITH_INVEST_FLOW_REL
295
        A set with all :class:`.Storage` objects with two investment flows
296
        coupled with the 'invest_relation_input_output' attribute.
297
298
    **The following variables are created:**
299
300
    storage_content
301
        Storage content for every storage and timestep. The value for the
302
        storage content at the beginning is set by the parameter
303
        `initial_storage_level` or not set if `initial_storage_level` is None.
304
        The variable of storage s and timestep t can be accessed by:
305
        `om.Storage.storage_content[s, t]`
306
307
    **The following constraints are created:**
308
309
    Set storage_content of last time step to one at t=0 if balanced == True
310
        .. math::
311
            E(t_{last}) = &E(-1)
312
313
    Storage balance :attr:`om.Storage.balance[n, t]`
314
        .. math:: E(t) = &E(t-1) \cdot
315
            (1 - \beta(t)) ^{\tau(t)/(t_u)} \\
316
            &- \gamma(t)\cdot E_{nom} \cdot {\tau(t)/(t_u)}\\
317
            &- \delta(t) \cdot {\tau(t)/(t_u)}\\
318
            &- \frac{\dot{E}_o(t)}{\eta_o(t)} \cdot \tau(t)
319
            + \dot{E}_i(t) \cdot \eta_i(t) \cdot \tau(t)
320
321
    Connect the invest variables of the input and the output flow.
322
        .. math::
323
          InvestmentFlowBlock.invest(source(n), n) + existing = \\
324
          (InvestmentFlowBlock.invest(n, target(n)) + existing) * \\
325
          invest\_relation\_input\_output(n) \\
326
          \forall n \in \textrm{INVEST\_REL\_IN\_OUT}
327
328
329
330
    =========================== ======================= =========
331
    symbol                      explanation             attribute
332
    =========================== ======================= =========
333
    :math:`E(t)`                energy currently stored `storage_content`
334
    :math:`E_{nom}`             nominal capacity of     `nominal_storage_capacity`
335
                                the energy storage
336
    :math:`c(-1)`               state before            `initial_storage_level`
337
                                initial time step
338
    :math:`c_{min}(t)`          minimum allowed storage `min_storage_level[t]`
339
    :math:`c_{max}(t)`          maximum allowed storage `max_storage_level[t]`
340
    :math:`\beta(t)`            fraction of lost energy `loss_rate[t]`
341
                                as share of
342
                                :math:`E(t)`
343
                                per time unit
344
    :math:`\gamma(t)`           fixed loss of energy    `fixed_losses_relative[t]`
345
                                relative to
346
                                :math:`E_{nom}` per
347
                                time unit
348
    :math:`\delta(t)`           absolute fixed loss     `fixed_losses_absolute[t]`
349
                                of energy per
350
                                time unit
351
    :math:`\dot{E}_i(t)`        energy flowing in       `inputs`
352
    :math:`\dot{E}_o(t)`        energy flowing out      `outputs`
353
    :math:`\eta_i(t)`           conversion factor       `inflow_conversion_factor[t]`
354
                                (i.e. efficiency)
355
                                when storing energy
356
    :math:`\eta_o(t)`           conversion factor when  `outflow_conversion_factor[t]`
357
                                (i.e. efficiency)
358
                                taking stored energy
359
    :math:`\tau(t)`             duration of time step
360
    :math:`t_u`                 time unit of losses
361
                                :math:`\beta(t)`,
362
                                :math:`\gamma(t)`
363
                                :math:`\delta(t)` and
364
                                timeincrement
365
                                :math:`\tau(t)`
366
    =========================== ======================= =========
367
368
    **The following parts of the objective function are created:**
369
370
    Nothing added to the objective function.
371
372
373
    """  # noqa: E501
374
375
    CONSTRAINT_GROUP = True
376
377
    def __init__(self, *args, **kwargs):
378
        super().__init__(*args, **kwargs)
379
380
    def _create(self, group=None):
381
        """
382
        Parameters
383
        ----------
384
        group : list
385
            List containing storage objects.
386
            e.g. groups=[storage1, storage2,..]
387
        """
388
        m = self.parent_block()
389
390
        if group is None:
391
            return None
392
393
        i = {n: [i for i in n.inputs][0] for n in group}
394
        o = {n: [o for o in n.outputs][0] for n in group}
395
396
        #  ************* SETS *********************************
397
398
        self.STORAGES = Set(initialize=[n for n in group])
399
400
        self.STORAGES_BALANCED = Set(
401
            initialize=[n for n in group if n.balanced is True]
402
        )
403
404
        self.STORAGES_WITH_INVEST_FLOW_REL = Set(
405
            initialize=[
406
                n for n in group if n.invest_relation_input_output is not None
407
            ]
408
        )
409
410
        #  ************* VARIABLES *****************************
411
412
        def _storage_content_bound_rule(block, n, t):
413
            """
414
            Rule definition for bounds of storage_content variable of
415
            storage n in timestep t.
416
            """
417
            bounds = (
418
                n.nominal_storage_capacity * n.min_storage_level[t],
419
                n.nominal_storage_capacity * n.max_storage_level[t],
420
            )
421
            return bounds
422
423
        self.storage_content = Var(
424
            self.STORAGES, m.TIMESTEPS, bounds=_storage_content_bound_rule
425
        )
426
427
        def _storage_init_content_bound_rule(block, n):
428
            return 0, n.nominal_storage_capacity
429
430
        self.init_content = Var(
431
            self.STORAGES,
432
            within=NonNegativeReals,
433
            bounds=_storage_init_content_bound_rule,
434
        )
435
436
        # set the initial storage content
437
        for n in group:
438
            if n.initial_storage_level is not None:
439
                self.init_content[n] = (
440
                    n.initial_storage_level * n.nominal_storage_capacity
441
                )
442
                self.init_content[n].fix()
443
444
        #  ************* Constraints ***************************
445
446
        reduced_timesteps = [x for x in m.TIMESTEPS if x > 0]
447
448
        # storage balance constraint (first time step)
449
        def _storage_balance_first_rule(block, n):
450
            """
451
            Rule definition for the storage balance of every storage n for
452
            the first timestep.
453
            """
454
            expr = 0
455
            expr += block.storage_content[n, 0]
456
            expr += (
457
                -block.init_content[n]
458
                * (1 - n.loss_rate[0]) ** m.timeincrement[0]
459
            )
460
            expr += (
461
                n.fixed_losses_relative[0]
462
                * n.nominal_storage_capacity
463
                * m.timeincrement[0]
464
            )
465
            expr += n.fixed_losses_absolute[0] * m.timeincrement[0]
466
            expr += (
467
                -m.flow[i[n], n, 0] * n.inflow_conversion_factor[0]
0 ignored issues
show
introduced by
The variable i does not seem to be defined for all execution paths.
Loading history...
468
            ) * m.timeincrement[0]
469
            expr += (
470
                m.flow[n, o[n], 0] / n.outflow_conversion_factor[0]
0 ignored issues
show
introduced by
The variable o does not seem to be defined for all execution paths.
Loading history...
471
            ) * m.timeincrement[0]
472
            return expr == 0
473
474
        self.balance_first = Constraint(
475
            self.STORAGES, rule=_storage_balance_first_rule
476
        )
477
478
        # storage balance constraint (every time step but the first)
479 View Code Duplication
        def _storage_balance_rule(block, n, t):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
480
            """
481
            Rule definition for the storage balance of every storage n and
482
            every timestep but the first (t > 0).
483
            """
484
            expr = 0
485
            expr += block.storage_content[n, t]
486
            expr += (
487
                -block.storage_content[n, t - 1]
488
                * (1 - n.loss_rate[t]) ** m.timeincrement[t]
489
            )
490
            expr += (
491
                n.fixed_losses_relative[t]
492
                * n.nominal_storage_capacity
493
                * m.timeincrement[t]
494
            )
495
            expr += n.fixed_losses_absolute[t] * m.timeincrement[t]
496
            expr += (
497
                -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...
498
            ) * m.timeincrement[t]
499
            expr += (
500
                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...
501
            ) * m.timeincrement[t]
502
            return expr == 0
503
504
        self.balance = Constraint(
505
            self.STORAGES, reduced_timesteps, rule=_storage_balance_rule
506
        )
507
508
        def _balanced_storage_rule(block, n):
509
            """
510
            Storage content of last time step == initial storage content
511
            if balanced.
512
            """
513
            return (
514
                block.storage_content[n, m.TIMESTEPS[-1]]
515
                == block.init_content[n]
516
            )
517
518
        self.balanced_cstr = Constraint(
519
            self.STORAGES_BALANCED, rule=_balanced_storage_rule
520
        )
521
522 View Code Duplication
        def _power_coupled(block, n):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
523
            """
524
            Rule definition for constraint to connect the input power
525
            and output power
526
            """
527
            expr = (
528
                m.InvestmentFlowBlock.invest[n, o[n]]
0 ignored issues
show
introduced by
The variable o does not seem to be defined for all execution paths.
Loading history...
529
                + m.flows[n, o[n]].investment.existing
530
            ) * n.invest_relation_input_output == (
531
                m.InvestmentFlowBlock.invest[i[n], n]
0 ignored issues
show
introduced by
The variable i does not seem to be defined for all execution paths.
Loading history...
532
                + m.flows[i[n], n].investment.existing
533
            )
534
            return expr
535
536
        self.power_coupled = Constraint(
537
            self.STORAGES_WITH_INVEST_FLOW_REL, rule=_power_coupled
538
        )
539
540
    def _objective_expression(self):
541
        r"""
542
        Objective expression for storages with no investment.
543
        Note: This adds nothing as variable costs are already
544
        added in the Block :class:`FlowBlock`.
545
        """
546
        if not hasattr(self, "STORAGES"):
547
            return 0
548
549
        return 0
550
551
552
class GenericInvestmentStorageBlock(ScalarBlock):
553
    r"""
554
    Block for all storages with :attr:`Investment` being not None.
555
    See :class:`oemof.solph.options.Investment` for all parameters of the
556
    Investment class.
557
558
    **Variables**
559
560
    All Storages are indexed by :math:`n`, which is omitted in the following
561
    for the sake of convenience.
562
    The following variables are created as attributes of
563
    :attr:`om.InvestmentStorage`:
564
565
    * :math:`P_i(t)`
566
567
        Inflow of the storage
568
        (created in :class:`oemof.solph.models.BaseModel`).
569
570
    * :math:`P_o(t)`
571
572
        Outflow of the storage
573
        (created in :class:`oemof.solph.models.BaseModel`).
574
575
    * :math:`E(t)`
576
577
        Current storage content (Absolute level of stored energy).
578
579
    * :math:`E_{invest}`
580
581
        Invested (nominal) capacity of the storage.
582
583
    * :math:`E(-1)`
584
585
        Initial storage content (before timestep 0).
586
587
    * :math:`b_{invest}`
588
589
        Binary variable for the status of the investment, if
590
        :attr:`nonconvex` is `True`.
591
592
    **Constraints**
593
594
    The following constraints are created for all investment storages:
595
596
            Storage balance (Same as for :class:`.GenericStorageBlock`)
597
598
        .. math:: E(t) = &E(t-1) \cdot
599
            (1 - \beta(t)) ^{\tau(t)/(t_u)} \\
600
            &- \gamma(t)\cdot (E_{exist} + E_{invest}) \cdot {\tau(t)/(t_u)}\\
601
            &- \delta(t) \cdot {\tau(t)/(t_u)}\\
602
            &- \frac{P_o(t)}{\eta_o(t)} \cdot \tau(t)
603
            + P_i(t) \cdot \eta_i(t) \cdot \tau(t)
604
605
    Depending on the attribute :attr:`nonconvex`, the constraints for the
606
    bounds of the decision variable :math:`E_{invest}` are different:\
607
608
        * :attr:`nonconvex = False`
609
610
        .. math::
611
            E_{invest, min} \le E_{invest} \le E_{invest, max}
612
613
        * :attr:`nonconvex = True`
614
615
        .. math::
616
            &
617
            E_{invest, min} \cdot b_{invest} \le E_{invest}\\
618
            &
619
            E_{invest} \le E_{invest, max} \cdot b_{invest}\\
620
621
    The following constraints are created depending on the attributes of
622
    the :class:`.components.GenericStorage`:
623
624
        * :attr:`initial_storage_level is None`
625
626
            Constraint for a variable initial storage content:
627
628
        .. math::
629
               E(-1) \le E_{invest} + E_{exist}
630
631
        * :attr:`initial_storage_level is not None`
632
633
            An initial value for the storage content is given:
634
635
        .. math::
636
               E(-1) = (E_{invest} + E_{exist}) \cdot c(-1)
637
638
        * :attr:`balanced=True`
639
640
            The energy content of storage of the first and the last timestep
641
            are set equal:
642
643
        .. math::
644
            E(-1) = E(t_{last})
645
646
        * :attr:`invest_relation_input_capacity is not None`
647
648
            Connect the invest variables of the storage and the input flow:
649
650
        .. math::
651
            P_{i,invest} + P_{i,exist} =
652
            (E_{invest} + E_{exist}) \cdot r_{cap,in}
653
654
        * :attr:`invest_relation_output_capacity is not None`
655
656
            Connect the invest variables of the storage and the output flow:
657
658
        .. math::
659
            P_{o,invest} + P_{o,exist} =
660
            (E_{invest} + E_{exist}) \cdot r_{cap,out}
661
662
        * :attr:`invest_relation_input_output is not None`
663
664
            Connect the invest variables of the input and the output flow:
665
666
        .. math::
667
            P_{i,invest} + P_{i,exist} =
668
            (P_{o,invest} + P_{o,exist}) \cdot r_{in,out}
669
670
        * :attr:`max_storage_level`
671
672
            Rule for upper bound constraint for the storage content:
673
674
        .. math::
675
            E(t) \leq E_{invest} \cdot c_{max}(t)
676
677
        * :attr:`min_storage_level`
678
679
            Rule for lower bound constraint for the storage content:
680
681
        .. math:: E(t) \geq E_{invest} \cdot c_{min}(t)
682
683
684
    **Objective function**
685
686
    The part of the objective function added by the investment storages
687
    also depends on whether a convex or nonconvex
688
    investment option is selected. The following parts of the objective
689
    function are created:
690
691
        * :attr:`nonconvex = False`
692
693
            .. math::
694
                E_{invest} \cdot c_{invest,var}
695
696
        * :attr:`nonconvex = True`
697
698
            .. math::
699
                E_{invest} \cdot c_{invest,var}
700
                + c_{invest,fix} \cdot b_{invest}\\
701
702
    The total value of all investment costs of all *InvestmentStorages*
703
    can be retrieved calling
704
    :meth:`om.GenericInvestmentStorageBlock.investment_costs.expr()`.
705
706
    .. csv-table:: List of Variables
707
        :header: "symbol", "attribute", "explanation"
708
        :widths: 1, 1, 1
709
710
        ":math:`P_i(t)`", ":attr:`flow[i[n], n, t]`", "Inflow of the storage"
711
        ":math:`P_o(t)`", ":attr:`flow[n, o[n], t]`", "Outlfow of the storage"
712
        ":math:`E(t)`", ":attr:`storage_content[n, t]`", "Current storage
713
        content (current absolute stored energy)"
714
        ":math:`E_{invest}`", ":attr:`invest[n, t]`", "Invested (nominal)
715
        capacity of the storage"
716
        ":math:`E(-1)`", ":attr:`init_cap[n]`", "Initial storage capacity
717
        (before timestep 0)"
718
        ":math:`b_{invest}`", ":attr:`invest_status[i, o]`", "Binary variable
719
        for the status of investment"
720
        ":math:`P_{i,invest}`", ":attr:`InvestmentFlowBlock.invest[i[n], n]`",
721
            "Invested (nominal) inflow (Investmentflow)"
722
        ":math:`P_{o,invest}`", ":attr:`InvestmentFlowBlock.invest[n, o[n]]`",
723
            "Invested (nominal) outflow (Investmentflow)"
724
725
    .. csv-table:: List of Parameters
726
        :header: "symbol", "attribute", "explanation"
727
        :widths: 1, 1, 1
728
729
        ":math:`E_{exist}`", "`flows[i, o].investment.existing`", "
730
        Existing storage capacity"
731
        ":math:`E_{invest,min}`", "`flows[i, o].investment.minimum`", "
732
        Minimum investment value"
733
        ":math:`E_{invest,max}`", "`flows[i, o].investment.maximum`", "
734
        Maximum investment value"
735
        ":math:`P_{i,exist}`", "`flows[i[n], n].investment.existing`
736
        ", "Existing inflow capacity"
737
        ":math:`P_{o,exist}`", "`flows[n, o[n]].investment.existing`
738
        ", "Existing outlfow capacity"
739
        ":math:`c_{invest,var}`", "`flows[i, o].investment.ep_costs`
740
        ", "Variable investment costs"
741
        ":math:`c_{invest,fix}`", "`flows[i, o].investment.offset`", "
742
        Fix investment costs"
743
        ":math:`r_{cap,in}`", ":attr:`invest_relation_input_capacity`", "
744
        Relation of storage capacity and nominal inflow"
745
        ":math:`r_{cap,out}`", ":attr:`invest_relation_output_capacity`", "
746
        Relation of storage capacity and nominal outflow"
747
        ":math:`r_{in,out}`", ":attr:`invest_relation_input_output`", "
748
        Relation of nominal in- and outflow"
749
        ":math:`\beta(t)`", "`loss_rate[t]`", "Fraction of lost energy
750
        as share of :math:`E(t)` per time unit"
751
        ":math:`\gamma(t)`", "`fixed_losses_relative[t]`", "Fixed loss
752
        of energy relative to :math:`E_{invest} + E_{exist}` per time unit"
753
        ":math:`\delta(t)`", "`fixed_losses_absolute[t]`", "Absolute
754
        fixed loss of energy per time unit"
755
        ":math:`\eta_i(t)`", "`inflow_conversion_factor[t]`", "
756
        Conversion factor (i.e. efficiency) when storing energy"
757
        ":math:`\eta_o(t)`", "`outflow_conversion_factor[t]`", "
758
        Conversion factor when (i.e. efficiency) taking stored energy"
759
        ":math:`c(-1)`", "`initial_storage_level`", "Initial relativ
760
        storage content (before timestep 0)"
761
        ":math:`c_{max}`", "`flows[i, o].max[t]`", "Normed maximum
762
        value of storage content"
763
        ":math:`c_{min}`", "`flows[i, o].min[t]`", "Normed minimum
764
        value of storage content"
765
        ":math:`\tau(t)`", "", "Duration of time step"
766
        ":math:`t_u`", "", "Time unit of losses :math:`\beta(t)`,
767
        :math:`\gamma(t)`, :math:`\delta(t)` and timeincrement :math:`\tau(t)`"
768
769
    """
770
771
    CONSTRAINT_GROUP = True
772
773
    def __init__(self, *args, **kwargs):
774
        super().__init__(*args, **kwargs)
775
776
    def _create(self, group=None):
777
        """ """
778
        m = self.parent_block()
779
        if group is None:
780
            return None
781
782
        # ########################## SETS #####################################
783
784
        self.INVESTSTORAGES = Set(initialize=[n for n in group])
785
786
        self.CONVEX_INVESTSTORAGES = Set(
787
            initialize=[n for n in group if n.investment.nonconvex is False]
788
        )
789
790
        self.NON_CONVEX_INVESTSTORAGES = Set(
791
            initialize=[n for n in group if n.investment.nonconvex is True]
792
        )
793
794
        self.INVESTSTORAGES_BALANCED = Set(
795
            initialize=[n for n in group if n.balanced is True]
796
        )
797
798
        self.INVESTSTORAGES_NO_INIT_CONTENT = Set(
799
            initialize=[n for n in group if n.initial_storage_level is None]
800
        )
801
802
        self.INVESTSTORAGES_INIT_CONTENT = Set(
803
            initialize=[
804
                n for n in group if n.initial_storage_level is not None
805
            ]
806
        )
807
808
        self.INVEST_REL_CAP_IN = Set(
809
            initialize=[
810
                n
811
                for n in group
812
                if n.invest_relation_input_capacity is not None
813
            ]
814
        )
815
816
        self.INVEST_REL_CAP_OUT = Set(
817
            initialize=[
818
                n
819
                for n in group
820
                if n.invest_relation_output_capacity is not None
821
            ]
822
        )
823
824
        self.INVEST_REL_IN_OUT = Set(
825
            initialize=[
826
                n for n in group if n.invest_relation_input_output is not None
827
            ]
828
        )
829
830
        # The storage content is a non-negative variable, therefore it makes no
831
        # sense to create an additional constraint if the lower bound is zero
832
        # for all time steps.
833
        self.MIN_INVESTSTORAGES = Set(
834
            initialize=[
835
                n
836
                for n in group
837
                if sum([n.min_storage_level[t] for t in m.TIMESTEPS]) > 0
838
            ]
839
        )
840
841
        # ######################### Variables  ################################
842
        self.storage_content = Var(
843
            self.INVESTSTORAGES, m.TIMESTEPS, within=NonNegativeReals
844
        )
845
846
        def _storage_investvar_bound_rule(block, n):
847
            """
848
            Rule definition to bound the invested storage capacity `invest`.
849
            """
850
            if n in self.CONVEX_INVESTSTORAGES:
851
                return n.investment.minimum, n.investment.maximum
852
            elif n in self.NON_CONVEX_INVESTSTORAGES:
853
                return 0, n.investment.maximum
854
855
        self.invest = Var(
856
            self.INVESTSTORAGES,
857
            within=NonNegativeReals,
858
            bounds=_storage_investvar_bound_rule,
859
        )
860
861
        self.init_content = Var(self.INVESTSTORAGES, within=NonNegativeReals)
862
863
        # create status variable for a non-convex investment storage
864
        self.invest_status = Var(self.NON_CONVEX_INVESTSTORAGES, within=Binary)
865
866
        # ######################### CONSTRAINTS ###############################
867
        i = {n: [i for i in n.inputs][0] for n in group}
868
        o = {n: [o for o in n.outputs][0] for n in group}
869
870
        reduced_timesteps = [x for x in m.TIMESTEPS if x > 0]
871
872
        def _inv_storage_init_content_max_rule(block, n):
873
            """Constraint for a variable initial storage capacity."""
874
            return (
875
                block.init_content[n]
876
                <= n.investment.existing + block.invest[n]
877
            )
878
879
        self.init_content_limit = Constraint(
880
            self.INVESTSTORAGES_NO_INIT_CONTENT,
881
            rule=_inv_storage_init_content_max_rule,
882
        )
883
884
        def _inv_storage_init_content_fix_rule(block, n):
885
            """Constraint for a fixed initial storage capacity."""
886
            return block.init_content[n] == n.initial_storage_level * (
887
                n.investment.existing + block.invest[n]
888
            )
889
890
        self.init_content_fix = Constraint(
891
            self.INVESTSTORAGES_INIT_CONTENT,
892
            rule=_inv_storage_init_content_fix_rule,
893
        )
894
895 View Code Duplication
        def _storage_balance_first_rule(block, n):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
896
            """
897
            Rule definition for the storage balance of every storage n for the
898
            first time step.
899
            """
900
            expr = 0
901
            expr += block.storage_content[n, 0]
902
            expr += (
903
                -block.init_content[n]
904
                * (1 - n.loss_rate[0]) ** m.timeincrement[0]
905
            )
906
            expr += (
907
                n.fixed_losses_relative[0]
908
                * (n.investment.existing + self.invest[n])
909
                * m.timeincrement[0]
910
            )
911
            expr += n.fixed_losses_absolute[0] * m.timeincrement[0]
912
            expr += (
913
                -m.flow[i[n], n, 0] * n.inflow_conversion_factor[0]
0 ignored issues
show
introduced by
The variable i does not seem to be defined for all execution paths.
Loading history...
914
            ) * m.timeincrement[0]
915
            expr += (
916
                m.flow[n, o[n], 0] / n.outflow_conversion_factor[0]
0 ignored issues
show
introduced by
The variable o does not seem to be defined for all execution paths.
Loading history...
917
            ) * m.timeincrement[0]
918
            return expr == 0
919
920
        self.balance_first = Constraint(
921
            self.INVESTSTORAGES, rule=_storage_balance_first_rule
922
        )
923
924 View Code Duplication
        def _storage_balance_rule(block, n, t):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
925
            """
926
            Rule definition for the storage balance of every storage n for the
927
            every time step but the first.
928
            """
929
            expr = 0
930
            expr += block.storage_content[n, t]
931
            expr += (
932
                -block.storage_content[n, t - 1]
933
                * (1 - n.loss_rate[t]) ** m.timeincrement[t]
934
            )
935
            expr += (
936
                n.fixed_losses_relative[t]
937
                * (n.investment.existing + self.invest[n])
938
                * m.timeincrement[t]
939
            )
940
            expr += n.fixed_losses_absolute[t] * m.timeincrement[t]
941
            expr += (
942
                -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...
943
            ) * m.timeincrement[t]
944
            expr += (
945
                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...
946
            ) * m.timeincrement[t]
947
            return expr == 0
948
949
        self.balance = Constraint(
950
            self.INVESTSTORAGES, reduced_timesteps, rule=_storage_balance_rule
951
        )
952
953
        def _balanced_storage_rule(block, n):
954
            return (
955
                block.storage_content[n, m.TIMESTEPS[-1]]
956
                == block.init_content[n]
957
            )
958
959
        self.balanced_cstr = Constraint(
960
            self.INVESTSTORAGES_BALANCED, rule=_balanced_storage_rule
961
        )
962
963 View Code Duplication
        def _power_coupled(block, n):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
964
            """
965
            Rule definition for constraint to connect the input power
966
            and output power
967
            """
968
            expr = (
969
                m.InvestmentFlowBlock.invest[n, o[n]]
0 ignored issues
show
introduced by
The variable o does not seem to be defined for all execution paths.
Loading history...
970
                + m.flows[n, o[n]].investment.existing
971
            ) * n.invest_relation_input_output == (
972
                m.InvestmentFlowBlock.invest[i[n], n]
0 ignored issues
show
introduced by
The variable i does not seem to be defined for all execution paths.
Loading history...
973
                + m.flows[i[n], n].investment.existing
974
            )
975
            return expr
976
977
        self.power_coupled = Constraint(
978
            self.INVEST_REL_IN_OUT, rule=_power_coupled
979
        )
980
981
        def _storage_capacity_inflow_invest_rule(block, n):
982
            """
983
            Rule definition of constraint connecting the inflow
984
            `InvestmentFlowBlock.invest of storage with invested capacity
985
            `invest` by nominal_storage_capacity__inflow_ratio
986
            """
987
            expr = (
988
                m.InvestmentFlowBlock.invest[i[n], n]
0 ignored issues
show
introduced by
The variable i does not seem to be defined for all execution paths.
Loading history...
989
                + m.flows[i[n], n].investment.existing
990
            ) == (
991
                n.investment.existing + self.invest[n]
992
            ) * n.invest_relation_input_capacity
993
            return expr
994
995
        self.storage_capacity_inflow = Constraint(
996
            self.INVEST_REL_CAP_IN, rule=_storage_capacity_inflow_invest_rule
997
        )
998
999
        def _storage_capacity_outflow_invest_rule(block, n):
1000
            """
1001
            Rule definition of constraint connecting outflow
1002
            `InvestmentFlowBlock.invest` of storage and invested capacity
1003
            `invest` by nominal_storage_capacity__outflow_ratio
1004
            """
1005
            expr = (
1006
                m.InvestmentFlowBlock.invest[n, o[n]]
0 ignored issues
show
introduced by
The variable o does not seem to be defined for all execution paths.
Loading history...
1007
                + m.flows[n, o[n]].investment.existing
1008
            ) == (
1009
                n.investment.existing + self.invest[n]
1010
            ) * n.invest_relation_output_capacity
1011
            return expr
1012
1013
        self.storage_capacity_outflow = Constraint(
1014
            self.INVEST_REL_CAP_OUT, rule=_storage_capacity_outflow_invest_rule
1015
        )
1016
1017
        def _max_storage_content_invest_rule(block, n, t):
1018
            """
1019
            Rule definition for upper bound constraint for the
1020
            storage content.
1021
            """
1022
            expr = (
1023
                self.storage_content[n, t]
1024
                <= (n.investment.existing + self.invest[n])
1025
                * n.max_storage_level[t]
1026
            )
1027
            return expr
1028
1029
        self.max_storage_content = Constraint(
1030
            self.INVESTSTORAGES,
1031
            m.TIMESTEPS,
1032
            rule=_max_storage_content_invest_rule,
1033
        )
1034
1035
        def _min_storage_content_invest_rule(block, n, t):
1036
            """
1037
            Rule definition of lower bound constraint for the
1038
            storage content.
1039
            """
1040
            expr = (
1041
                self.storage_content[n, t]
1042
                >= (n.investment.existing + self.invest[n])
1043
                * n.min_storage_level[t]
1044
            )
1045
            return expr
1046
1047
        # Set the lower bound of the storage content if the attribute exists
1048
        self.min_storage_content = Constraint(
1049
            self.MIN_INVESTSTORAGES,
1050
            m.TIMESTEPS,
1051
            rule=_min_storage_content_invest_rule,
1052
        )
1053
1054
        def maximum_invest_limit(block, n):
1055
            """
1056
            Constraint for the maximal investment in non convex investment
1057
            storage.
1058
            """
1059
            return (
1060
                n.investment.maximum * self.invest_status[n] - self.invest[n]
1061
            ) >= 0
1062
1063
        self.limit_max = Constraint(
1064
            self.NON_CONVEX_INVESTSTORAGES, rule=maximum_invest_limit
1065
        )
1066
1067
        def smallest_invest(block, n):
1068
            """
1069
            Constraint for the minimal investment in non convex investment
1070
            storage if the invest is greater than 0. So the invest variable
1071
            can be either 0 or greater than the minimum.
1072
            """
1073
            return (
1074
                self.invest[n] - (n.investment.minimum * self.invest_status[n])
1075
                >= 0
1076
            )
1077
1078
        self.limit_min = Constraint(
1079
            self.NON_CONVEX_INVESTSTORAGES, rule=smallest_invest
1080
        )
1081
1082
    def _objective_expression(self):
1083
        """Objective expression with fixed and investement costs."""
1084
        if not hasattr(self, "INVESTSTORAGES"):
1085
            return 0
1086
1087
        investment_costs = 0
1088
1089
        for n in self.CONVEX_INVESTSTORAGES:
1090
            investment_costs += self.invest[n] * n.investment.ep_costs
1091
        for n in self.NON_CONVEX_INVESTSTORAGES:
1092
            investment_costs += (
1093
                self.invest[n] * n.investment.ep_costs
1094
                + self.invest_status[n] * n.investment.offset
1095
            )
1096
        self.investment_costs = Expression(expr=investment_costs)
1097
1098
        return investment_costs
1099