Passed
Pull Request — dev (#854)
by Uwe
03:24 queued 21s
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_INITITAL_LEVEL = Set(
405
            initialize=[
406
                n for n in group if n.initial_storage_level is not None
407
            ]
408
        )
409
410
        self.STORAGES_WITH_INVEST_FLOW_REL = Set(
411
            initialize=[
412
                n for n in group if n.invest_relation_input_output is not None
413
            ]
414
        )
415
416
        #  ************* VARIABLES *****************************
417
418
        def _storage_content_bound_rule(block, n, t):
419
            """
420
            Rule definition for bounds of storage_content variable of
421
            storage n in timestep t.
422
            """
423
            bounds = (
424
                n.nominal_storage_capacity * n.min_storage_level[t],
425
                n.nominal_storage_capacity * n.max_storage_level[t],
426
            )
427
            return bounds
428
429
        self.storage_content = Var(
430
            self.STORAGES, m.TIMEPOINTS, bounds=_storage_content_bound_rule
431
        )
432
433
        # set the initial storage content
434
        # ToDo: More elegant code possible?
435
        for n in group:
436
            if n.initial_storage_level is not None:
437
                self.storage_content[n, 0] = (
438
                    n.initial_storage_level * n.nominal_storage_capacity
439
                )
440
                self.storage_content[n, 0].fix()
441
442
        #  ************* Constraints ***************************
443
444 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...
445
            """
446
            Rule definition for the storage balance of every storage n and
447
            every timestep.
448
            """
449
            expr = 0
450
            expr += block.storage_content[n, t + 1]
451
            expr += (
452
                -block.storage_content[n, t]
453
                * (1 - n.loss_rate[t]) ** m.timeincrement[t]
454
            )
455
            expr += (
456
                n.fixed_losses_relative[t]
457
                * n.nominal_storage_capacity
458
                * m.timeincrement[t]
459
            )
460
            expr += n.fixed_losses_absolute[t] * m.timeincrement[t]
461
            expr += (
462
                -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...
463
            ) * m.timeincrement[t]
464
            expr += (
465
                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...
466
            ) * m.timeincrement[t]
467
            return expr == 0
468
469
        self.balance = Constraint(
470
            self.STORAGES, m.TIMESTEPS, rule=_storage_balance_rule
471
        )
472
473
        def _balanced_storage_rule(block, n):
474
            """
475
            Storage content of last time step == initial storage content
476
            if balanced.
477
            """
478
            return (
479
                block.storage_content[n, m.TIMEPOINTS.at(-1)]
480
                == block.storage_content[n, m.TIMEPOINTS.at(1)]
481
            )
482
483
        self.balanced_cstr = Constraint(
484
            self.STORAGES_BALANCED, rule=_balanced_storage_rule
485
        )
486
487 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...
488
            """
489
            Rule definition for constraint to connect the input power
490
            and output power
491
            """
492
            expr = (
493
                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...
494
                + m.flows[n, o[n]].investment.existing
495
            ) * n.invest_relation_input_output == (
496
                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...
497
                + m.flows[i[n], n].investment.existing
498
            )
499
            return expr
500
501
        self.power_coupled = Constraint(
502
            self.STORAGES_WITH_INVEST_FLOW_REL, rule=_power_coupled
503
        )
504
505
    def _objective_expression(self):
506
        r"""
507
        Objective expression for storages with no investment.
508
        Note: This adds nothing as variable costs are already
509
        added in the Block :class:`FlowBlock`.
510
        """
511
        if not hasattr(self, "STORAGES"):
512
            return 0
513
514
        return 0
515
516
517
class GenericInvestmentStorageBlock(ScalarBlock):
518
    r"""
519
    Block for all storages with :attr:`Investment` being not None.
520
    See :class:`oemof.solph.options.Investment` for all parameters of the
521
    Investment class.
522
523
    **Variables**
524
525
    All Storages are indexed by :math:`n`, which is omitted in the following
526
    for the sake of convenience.
527
    The following variables are created as attributes of
528
    :attr:`om.InvestmentStorage`:
529
530
    * :math:`P_i(t)`
531
532
        Inflow of the storage
533
        (created in :class:`oemof.solph.models.BaseModel`).
534
535
    * :math:`P_o(t)`
536
537
        Outflow of the storage
538
        (created in :class:`oemof.solph.models.BaseModel`).
539
540
    * :math:`E(t)`
541
542
        Current storage content (Absolute level of stored energy).
543
544
    * :math:`E_{invest}`
545
546
        Invested (nominal) capacity of the storage.
547
548
    * :math:`E(-1)`
549
550
        Initial storage content (before timestep 0).
551
552
    * :math:`b_{invest}`
553
554
        Binary variable for the status of the investment, if
555
        :attr:`nonconvex` is `True`.
556
557
    **Constraints**
558
559
    The following constraints are created for all investment storages:
560
561
            Storage balance (Same as for :class:`.GenericStorageBlock`)
562
563
        .. math:: E(t) = &E(t-1) \cdot
564
            (1 - \beta(t)) ^{\tau(t)/(t_u)} \\
565
            &- \gamma(t)\cdot (E_{exist} + E_{invest}) \cdot {\tau(t)/(t_u)}\\
566
            &- \delta(t) \cdot {\tau(t)/(t_u)}\\
567
            &- \frac{P_o(t)}{\eta_o(t)} \cdot \tau(t)
568
            + P_i(t) \cdot \eta_i(t) \cdot \tau(t)
569
570
    Depending on the attribute :attr:`nonconvex`, the constraints for the
571
    bounds of the decision variable :math:`E_{invest}` are different:\
572
573
        * :attr:`nonconvex = False`
574
575
        .. math::
576
            E_{invest, min} \le E_{invest} \le E_{invest, max}
577
578
        * :attr:`nonconvex = True`
579
580
        .. math::
581
            &
582
            E_{invest, min} \cdot b_{invest} \le E_{invest}\\
583
            &
584
            E_{invest} \le E_{invest, max} \cdot b_{invest}\\
585
586
    The following constraints are created depending on the attributes of
587
    the :class:`.components.GenericStorage`:
588
589
        * :attr:`initial_storage_level is None`
590
591
            Constraint for a variable initial storage content:
592
593
        .. math::
594
               E(-1) \le E_{invest} + E_{exist}
595
596
        * :attr:`initial_storage_level is not None`
597
598
            An initial value for the storage content is given:
599
600
        .. math::
601
               E(-1) = (E_{invest} + E_{exist}) \cdot c(-1)
602
603
        * :attr:`balanced=True`
604
605
            The energy content of storage of the first and the last timestep
606
            are set equal:
607
608
        .. math::
609
            E(-1) = E(t_{last})
610
611
        * :attr:`invest_relation_input_capacity is not None`
612
613
            Connect the invest variables of the storage and the input flow:
614
615
        .. math::
616
            P_{i,invest} + P_{i,exist} =
617
            (E_{invest} + E_{exist}) \cdot r_{cap,in}
618
619
        * :attr:`invest_relation_output_capacity is not None`
620
621
            Connect the invest variables of the storage and the output flow:
622
623
        .. math::
624
            P_{o,invest} + P_{o,exist} =
625
            (E_{invest} + E_{exist}) \cdot r_{cap,out}
626
627
        * :attr:`invest_relation_input_output is not None`
628
629
            Connect the invest variables of the input and the output flow:
630
631
        .. math::
632
            P_{i,invest} + P_{i,exist} =
633
            (P_{o,invest} + P_{o,exist}) \cdot r_{in,out}
634
635
        * :attr:`max_storage_level`
636
637
            Rule for upper bound constraint for the storage content:
638
639
        .. math::
640
            E(t) \leq E_{invest} \cdot c_{max}(t)
641
642
        * :attr:`min_storage_level`
643
644
            Rule for lower bound constraint for the storage content:
645
646
        .. math:: E(t) \geq E_{invest} \cdot c_{min}(t)
647
648
649
    **Objective function**
650
651
    The part of the objective function added by the investment storages
652
    also depends on whether a convex or nonconvex
653
    investment option is selected. The following parts of the objective
654
    function are created:
655
656
        * :attr:`nonconvex = False`
657
658
            .. math::
659
                E_{invest} \cdot c_{invest,var}
660
661
        * :attr:`nonconvex = True`
662
663
            .. math::
664
                E_{invest} \cdot c_{invest,var}
665
                + c_{invest,fix} \cdot b_{invest}\\
666
667
    The total value of all investment costs of all *InvestmentStorages*
668
    can be retrieved calling
669
    :meth:`om.GenericInvestmentStorageBlock.investment_costs.expr()`.
670
671
    .. csv-table:: List of Variables
672
        :header: "symbol", "attribute", "explanation"
673
        :widths: 1, 1, 1
674
675
        ":math:`P_i(t)`", ":attr:`flow[i[n], n, t]`", "Inflow of the storage"
676
        ":math:`P_o(t)`", ":attr:`flow[n, o[n], t]`", "Outlfow of the storage"
677
        ":math:`E(t)`", ":attr:`storage_content[n, t]`", "Current storage
678
        content (current absolute stored energy)"
679
        ":math:`E_{invest}`", ":attr:`invest[n, t]`", "Invested (nominal)
680
        capacity of the storage"
681
        ":math:`E(-1)`", ":attr:`init_cap[n]`", "Initial storage capacity
682
        (before timestep 0)"
683
        ":math:`b_{invest}`", ":attr:`invest_status[i, o]`", "Binary variable
684
        for the status of investment"
685
        ":math:`P_{i,invest}`", ":attr:`InvestmentFlowBlock.invest[i[n], n]`",
686
            "Invested (nominal) inflow (Investmentflow)"
687
        ":math:`P_{o,invest}`", ":attr:`InvestmentFlowBlock.invest[n, o[n]]`",
688
            "Invested (nominal) outflow (Investmentflow)"
689
690
    .. csv-table:: List of Parameters
691
        :header: "symbol", "attribute", "explanation"
692
        :widths: 1, 1, 1
693
694
        ":math:`E_{exist}`", "`flows[i, o].investment.existing`", "
695
        Existing storage capacity"
696
        ":math:`E_{invest,min}`", "`flows[i, o].investment.minimum`", "
697
        Minimum investment value"
698
        ":math:`E_{invest,max}`", "`flows[i, o].investment.maximum`", "
699
        Maximum investment value"
700
        ":math:`P_{i,exist}`", "`flows[i[n], n].investment.existing`
701
        ", "Existing inflow capacity"
702
        ":math:`P_{o,exist}`", "`flows[n, o[n]].investment.existing`
703
        ", "Existing outlfow capacity"
704
        ":math:`c_{invest,var}`", "`flows[i, o].investment.ep_costs`
705
        ", "Variable investment costs"
706
        ":math:`c_{invest,fix}`", "`flows[i, o].investment.offset`", "
707
        Fix investment costs"
708
        ":math:`r_{cap,in}`", ":attr:`invest_relation_input_capacity`", "
709
        Relation of storage capacity and nominal inflow"
710
        ":math:`r_{cap,out}`", ":attr:`invest_relation_output_capacity`", "
711
        Relation of storage capacity and nominal outflow"
712
        ":math:`r_{in,out}`", ":attr:`invest_relation_input_output`", "
713
        Relation of nominal in- and outflow"
714
        ":math:`\beta(t)`", "`loss_rate[t]`", "Fraction of lost energy
715
        as share of :math:`E(t)` per time unit"
716
        ":math:`\gamma(t)`", "`fixed_losses_relative[t]`", "Fixed loss
717
        of energy relative to :math:`E_{invest} + E_{exist}` per time unit"
718
        ":math:`\delta(t)`", "`fixed_losses_absolute[t]`", "Absolute
719
        fixed loss of energy per time unit"
720
        ":math:`\eta_i(t)`", "`inflow_conversion_factor[t]`", "
721
        Conversion factor (i.e. efficiency) when storing energy"
722
        ":math:`\eta_o(t)`", "`outflow_conversion_factor[t]`", "
723
        Conversion factor when (i.e. efficiency) taking stored energy"
724
        ":math:`c(-1)`", "`initial_storage_level`", "Initial relativ
725
        storage content (before timestep 0)"
726
        ":math:`c_{max}`", "`flows[i, o].max[t]`", "Normed maximum
727
        value of storage content"
728
        ":math:`c_{min}`", "`flows[i, o].min[t]`", "Normed minimum
729
        value of storage content"
730
        ":math:`\tau(t)`", "", "Duration of time step"
731
        ":math:`t_u`", "", "Time unit of losses :math:`\beta(t)`,
732
        :math:`\gamma(t)`, :math:`\delta(t)` and timeincrement :math:`\tau(t)`"
733
734
    """
735
736
    CONSTRAINT_GROUP = True
737
738
    def __init__(self, *args, **kwargs):
739
        super().__init__(*args, **kwargs)
740
741
    def _create(self, group=None):
742
        """ """
743
        m = self.parent_block()
744
        if group is None:
745
            return None
746
747
        # ########################## SETS #####################################
748
749
        self.INVESTSTORAGES = Set(initialize=[n for n in group])
750
751
        self.CONVEX_INVESTSTORAGES = Set(
752
            initialize=[n for n in group if n.investment.nonconvex is False]
753
        )
754
755
        self.NON_CONVEX_INVESTSTORAGES = Set(
756
            initialize=[n for n in group if n.investment.nonconvex is True]
757
        )
758
759
        self.INVESTSTORAGES_BALANCED = Set(
760
            initialize=[n for n in group if n.balanced is True]
761
        )
762
763
        self.INVESTSTORAGES_NO_INIT_CONTENT = Set(
764
            initialize=[n for n in group if n.initial_storage_level is None]
765
        )
766
767
        self.INVESTSTORAGES_INIT_CONTENT = Set(
768
            initialize=[
769
                n for n in group if n.initial_storage_level is not None
770
            ]
771
        )
772
773
        self.INVEST_REL_CAP_IN = Set(
774
            initialize=[
775
                n
776
                for n in group
777
                if n.invest_relation_input_capacity is not None
778
            ]
779
        )
780
781
        self.INVEST_REL_CAP_OUT = Set(
782
            initialize=[
783
                n
784
                for n in group
785
                if n.invest_relation_output_capacity is not None
786
            ]
787
        )
788
789
        self.INVEST_REL_IN_OUT = Set(
790
            initialize=[
791
                n for n in group if n.invest_relation_input_output is not None
792
            ]
793
        )
794
795
        # The storage content is a non-negative variable, therefore it makes no
796
        # sense to create an additional constraint if the lower bound is zero
797
        # for all time steps.
798
        self.MIN_INVESTSTORAGES = Set(
799
            initialize=[
800
                n
801
                for n in group
802
                if sum([n.min_storage_level[t] for t in m.TIMESTEPS]) > 0
803
            ]
804
        )
805
806
        # ######################### Variables  ################################
807
        self.storage_content = Var(
808
            self.INVESTSTORAGES, m.TIMESTEPS, within=NonNegativeReals
809
        )
810
811
        def _storage_investvar_bound_rule(block, n):
812
            """
813
            Rule definition to bound the invested storage capacity `invest`.
814
            """
815
            if n in self.CONVEX_INVESTSTORAGES:
816
                return n.investment.minimum, n.investment.maximum
817
            elif n in self.NON_CONVEX_INVESTSTORAGES:
818
                return 0, n.investment.maximum
819
820
        self.invest = Var(
821
            self.INVESTSTORAGES,
822
            within=NonNegativeReals,
823
            bounds=_storage_investvar_bound_rule,
824
        )
825
826
        self.init_content = Var(self.INVESTSTORAGES, within=NonNegativeReals)
827
828
        # create status variable for a non-convex investment storage
829
        self.invest_status = Var(self.NON_CONVEX_INVESTSTORAGES, within=Binary)
830
831
        # ######################### CONSTRAINTS ###############################
832
        i = {n: [i for i in n.inputs][0] for n in group}
833
        o = {n: [o for o in n.outputs][0] for n in group}
834
835
        reduced_timesteps = [x for x in m.TIMESTEPS if x > 0]
836
837
        def _inv_storage_init_content_max_rule(block, n):
838
            """Constraint for a variable initial storage capacity."""
839
            return (
840
                block.init_content[n]
841
                <= n.investment.existing + block.invest[n]
842
            )
843
844
        self.init_content_limit = Constraint(
845
            self.INVESTSTORAGES_NO_INIT_CONTENT,
846
            rule=_inv_storage_init_content_max_rule,
847
        )
848
849
        def _inv_storage_init_content_fix_rule(block, n):
850
            """Constraint for a fixed initial storage capacity."""
851
            return block.init_content[n] == n.initial_storage_level * (
852
                n.investment.existing + block.invest[n]
853
            )
854
855
        self.init_content_fix = Constraint(
856
            self.INVESTSTORAGES_INIT_CONTENT,
857
            rule=_inv_storage_init_content_fix_rule,
858
        )
859
860
        def _storage_balance_first_rule(block, n):
861
            """
862
            Rule definition for the storage balance of every storage n for the
863
            first time step.
864
            """
865
            expr = 0
866
            expr += block.storage_content[n, 0]
867
            expr += (
868
                -block.init_content[n]
869
                * (1 - n.loss_rate[0]) ** m.timeincrement[0]
870
            )
871
            expr += (
872
                n.fixed_losses_relative[0]
873
                * (n.investment.existing + self.invest[n])
874
                * m.timeincrement[0]
875
            )
876
            expr += n.fixed_losses_absolute[0] * m.timeincrement[0]
877
            expr += (
878
                -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...
879
            ) * m.timeincrement[0]
880
            expr += (
881
                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...
882
            ) * m.timeincrement[0]
883
            return expr == 0
884
885
        self.balance_first = Constraint(
886
            self.INVESTSTORAGES, rule=_storage_balance_first_rule
887
        )
888
889 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...
890
            """
891
            Rule definition for the storage balance of every storage n for the
892
            every time step but the first.
893
            """
894
            expr = 0
895
            expr += block.storage_content[n, t]
896
            expr += (
897
                -block.storage_content[n, t - 1]
898
                * (1 - n.loss_rate[t]) ** m.timeincrement[t]
899
            )
900
            expr += (
901
                n.fixed_losses_relative[t]
902
                * (n.investment.existing + self.invest[n])
903
                * m.timeincrement[t]
904
            )
905
            expr += n.fixed_losses_absolute[t] * m.timeincrement[t]
906
            expr += (
907
                -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...
908
            ) * m.timeincrement[t]
909
            expr += (
910
                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...
911
            ) * m.timeincrement[t]
912
            return expr == 0
913
914
        self.balance = Constraint(
915
            self.INVESTSTORAGES, reduced_timesteps, rule=_storage_balance_rule
916
        )
917
918
        def _balanced_storage_rule(block, n):
919
            return (
920
                block.storage_content[n, m.TIMESTEPS[-1]]
921
                == block.init_content[n]
922
            )
923
924
        self.balanced_cstr = Constraint(
925
            self.INVESTSTORAGES_BALANCED, rule=_balanced_storage_rule
926
        )
927
928 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...
929
            """
930
            Rule definition for constraint to connect the input power
931
            and output power
932
            """
933
            expr = (
934
                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...
935
                + m.flows[n, o[n]].investment.existing
936
            ) * n.invest_relation_input_output == (
937
                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...
938
                + m.flows[i[n], n].investment.existing
939
            )
940
            return expr
941
942
        self.power_coupled = Constraint(
943
            self.INVEST_REL_IN_OUT, rule=_power_coupled
944
        )
945
946
        def _storage_capacity_inflow_invest_rule(block, n):
947
            """
948
            Rule definition of constraint connecting the inflow
949
            `InvestmentFlowBlock.invest of storage with invested capacity
950
            `invest` by nominal_storage_capacity__inflow_ratio
951
            """
952
            expr = (
953
                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...
954
                + m.flows[i[n], n].investment.existing
955
            ) == (
956
                n.investment.existing + self.invest[n]
957
            ) * n.invest_relation_input_capacity
958
            return expr
959
960
        self.storage_capacity_inflow = Constraint(
961
            self.INVEST_REL_CAP_IN, rule=_storage_capacity_inflow_invest_rule
962
        )
963
964
        def _storage_capacity_outflow_invest_rule(block, n):
965
            """
966
            Rule definition of constraint connecting outflow
967
            `InvestmentFlowBlock.invest` of storage and invested capacity
968
            `invest` by nominal_storage_capacity__outflow_ratio
969
            """
970
            expr = (
971
                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...
972
                + m.flows[n, o[n]].investment.existing
973
            ) == (
974
                n.investment.existing + self.invest[n]
975
            ) * n.invest_relation_output_capacity
976
            return expr
977
978
        self.storage_capacity_outflow = Constraint(
979
            self.INVEST_REL_CAP_OUT, rule=_storage_capacity_outflow_invest_rule
980
        )
981
982
        def _max_storage_content_invest_rule(block, n, t):
983
            """
984
            Rule definition for upper bound constraint for the
985
            storage content.
986
            """
987
            expr = (
988
                self.storage_content[n, t]
989
                <= (n.investment.existing + self.invest[n])
990
                * n.max_storage_level[t]
991
            )
992
            return expr
993
994
        self.max_storage_content = Constraint(
995
            self.INVESTSTORAGES,
996
            m.TIMESTEPS,
997
            rule=_max_storage_content_invest_rule,
998
        )
999
1000
        def _min_storage_content_invest_rule(block, n, t):
1001
            """
1002
            Rule definition of lower bound constraint for the
1003
            storage content.
1004
            """
1005
            expr = (
1006
                self.storage_content[n, t]
1007
                >= (n.investment.existing + self.invest[n])
1008
                * n.min_storage_level[t]
1009
            )
1010
            return expr
1011
1012
        # Set the lower bound of the storage content if the attribute exists
1013
        self.min_storage_content = Constraint(
1014
            self.MIN_INVESTSTORAGES,
1015
            m.TIMESTEPS,
1016
            rule=_min_storage_content_invest_rule,
1017
        )
1018
1019
        def maximum_invest_limit(block, n):
1020
            """
1021
            Constraint for the maximal investment in non convex investment
1022
            storage.
1023
            """
1024
            return (
1025
                n.investment.maximum * self.invest_status[n] - self.invest[n]
1026
            ) >= 0
1027
1028
        self.limit_max = Constraint(
1029
            self.NON_CONVEX_INVESTSTORAGES, rule=maximum_invest_limit
1030
        )
1031
1032
        def smallest_invest(block, n):
1033
            """
1034
            Constraint for the minimal investment in non convex investment
1035
            storage if the invest is greater than 0. So the invest variable
1036
            can be either 0 or greater than the minimum.
1037
            """
1038
            return (
1039
                self.invest[n] - (n.investment.minimum * self.invest_status[n])
1040
                >= 0
1041
            )
1042
1043
        self.limit_min = Constraint(
1044
            self.NON_CONVEX_INVESTSTORAGES, rule=smallest_invest
1045
        )
1046
1047
    def _objective_expression(self):
1048
        """Objective expression with fixed and investement costs."""
1049
        if not hasattr(self, "INVESTSTORAGES"):
1050
            return 0
1051
1052
        investment_costs = 0
1053
1054
        for n in self.CONVEX_INVESTSTORAGES:
1055
            investment_costs += self.invest[n] * n.investment.ep_costs
1056
        for n in self.NON_CONVEX_INVESTSTORAGES:
1057
            investment_costs += (
1058
                self.invest[n] * n.investment.ep_costs
1059
                + self.invest_status[n] * n.investment.offset
1060
            )
1061
        self.investment_costs = Expression(expr=investment_costs)
1062
1063
        return investment_costs
1064