• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

oemof / oemof-solph / 17950514894

23 Sep 2025 03:06PM UTC coverage: 81.149% (+0.2%) from 80.998%
17950514894

Pull #1217

github

web-flow
Merge 557e28f34 into 2a134ee32
Pull Request #1217: Bugfix/genericstorage relations

938 of 1262 branches covered (74.33%)

Branch coverage included in aggregate %.

507 of 597 new or added lines in 1 file covered. (84.92%)

63 existing lines in 1 file now uncovered.

2734 of 3263 relevant lines covered (83.79%)

1.68 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.54
/src/oemof/solph/components/_generic_storage.py
1
# -*- coding: utf-8 -
2

3
"""
4
GenericStorage and associated individual constraints (blocks) and groupings.
5

6
SPDX-FileCopyrightText: Uwe Krien <krien@uni-bremen.de>
7
SPDX-FileCopyrightText: Simon Hilpert
8
SPDX-FileCopyrightText: Cord Kaldemeyer
9
SPDX-FileCopyrightText: Patrik Schönfeldt
10
SPDX-FileCopyrightText: FranziPl
11
SPDX-FileCopyrightText: jnnr
12
SPDX-FileCopyrightText: Stephan Günther
13
SPDX-FileCopyrightText: FabianTU
14
SPDX-FileCopyrightText: Johannes Röder
15
SPDX-FileCopyrightText: Ekaterina Zolotarevskaia
16
SPDX-FileCopyrightText: Johannes Kochems
17
SPDX-FileCopyrightText: Johannes Giehl
18
SPDX-FileCopyrightText: Raul Ciria Aylagas
19
SPDX-FileCopyrightText: Lennart Schürmann (Fraunhofer UMSICHT)
20

21
SPDX-License-Identifier: MIT
22

23
"""
24

25
import math
2✔
26
import numbers
2✔
27
from warnings import warn
2✔
28

29
import numpy as np
2✔
30
from oemof.network import Node
2✔
31
from oemof.tools import debugging
2✔
32
from oemof.tools import economics
2✔
33
from pyomo.core.base.block import ScalarBlock
2✔
34
from pyomo.environ import Binary
2✔
35
from pyomo.environ import BuildAction
2✔
36
from pyomo.environ import Constraint
2✔
37
from pyomo.environ import Expression
2✔
38
from pyomo.environ import NonNegativeReals
2✔
39
from pyomo.environ import Set
2✔
40
from pyomo.environ import Var
2✔
41

42
from oemof.solph._helpers import check_node_object_for_missing_attribute
2✔
43
from oemof.solph._options import Investment
2✔
44
from oemof.solph._plumbing import sequence
2✔
45
from oemof.solph._plumbing import valid_sequence
2✔
46

47

48
class GenericStorage(Node):
2✔
49
    r"""
50
    Component `GenericStorage` to model with basic characteristics of storages.
51

52
    The GenericStorage is designed for one input and one output.
53

54
    Parameters
55
    ----------
56
    nominal_capacity : numeric, :math:`E_{nom}` or
57
            :class:`oemof.solph.options.Investment` object
58
        Absolute nominal capacity of the storage, fixed value or
59
        object describing parameter of investment optimisations.
60
    invest_relation_input_capacity : numeric (iterable or scalar) or None, :math:`r_{cap,in}`
61
        Ratio between the investment variable of the input flow and the
62
        investment variable of the storage:
63
        :math:`\dot{E}_{in,invest} = E_{invest} \cdot r_{cap,in}`
64
    invest_relation_output_capacity : numeric (iterable or scalar) or None, :math:`r_{cap,out}`
65
        Ratio between the investment variable of the output flow and the
66
        investment variable of the storage:
67
        :math:`\dot{E}_{out,invest} = E_{invest} \cdot r_{cap,out}`
68
    invest_relation_input_output : numeric (iterable or scalar) or None, :math:`r_{in,out}`
69
        Ratio between the investment variable of the input flow and the
70
        investment variable of the output flow. This ratio used to fix the
71
        flow investments to each other.
72
        Values < 1 set the input flow lower than the output and > 1 will
73
        set the input flow higher than the output flow. If set to None no relation
74
        will be set:
75
        :math:`\dot{E}_{in,invest} = \dot{E}_{out,invest} \cdot r_{in,out}`
76
    initial_storage_level : numeric, :math:`c(-1)`
77
        The relative storage content in the timestep before the first
78
        time step of optimization (between 0 and 1).
79

80
        Note: When investment mode is used in a multi-period model,
81
        `initial_storage_level` is not supported.
82
        Storage output is forced to zero until the storage unit is invested in.
83
    balanced : boolean
84
        Couple storage level of first and last time step.
85
        (Total inflow and total outflow are balanced.)
86
    loss_rate : numeric (iterable or scalar)
87
        The relative loss of the storage content per hour.
88
    fixed_losses_relative : numeric (iterable or scalar), :math:`\gamma(t)`
89
        Losses per hour that are independent of the storage content but
90
        proportional to nominal storage capacity.
91

92
        Note: Fixed losses are not supported in investment mode.
93
    fixed_losses_absolute : numeric (iterable or scalar), :math:`\delta(t)`
94
        Losses per hour that are independent of storage content and independent
95
        of nominal storage capacity.
96

97
        Note: Fixed losses are not supported in investment mode.
98
    inflow_conversion_factor : numeric (iterable or scalar), :math:`\eta_i(t)`
99
        The relative conversion factor, i.e. efficiency associated with the
100
        inflow of the storage.
101
    outflow_conversion_factor : numeric (iterable or scalar), :math:`\eta_o(t)`
102
        see: inflow_conversion_factor
103
    min_storage_level : numeric (iterable or scalar), :math:`c_{min}(t)`
104
        The normed minimum storage content as fraction of the
105
        nominal storage capacity or the capacity that has been invested into
106
        (between 0 and 1).
107
        To set different values in every time step use a sequence.
108
    max_storage_level : numeric (iterable or scalar), :math:`c_{max}(t)`
109
        see: min_storage_level
110
    storage_costs : numeric (iterable or scalar), :math:`c_{storage}(t)`
111
        Cost (per energy) for having energy in the storage, starting from
112
        time point :math:`t_{1}`. (:math:`t_{0}` is left out to avoid counting
113
        it twice if balanced=True.)
114
    lifetime_inflow : int, :math:`n_{in}`
115
        Determine the lifetime of an inflow; only applicable for multi-period
116
        models which can invest in storage capacity and have an
117
        invest_relation_input_capacity defined
118
    lifetime_outflow : int, :math:`n_{in}`
119
        Determine the lifetime of an outflow; only applicable for multi-period
120
        models which can invest in storage capacity and have an
121
        invest_relation_output_capacity defined
122

123
    Notes
124
    -----
125
    The following sets, variables, constraints and objective parts are created
126
     * :py:class:`~oemof.solph.components._generic_storage.GenericStorageBlock`
127
       (if no Investment object present)
128
     * :py:class:`~oemof.solph.components._generic_storage.GenericInvestmentStorageBlock`
129
       (if Investment object present)
130

131
    Examples
132
    --------
133
    Basic usage examples of the GenericStorage with a random selection of
134
    attributes. See the Flow class for all Flow attributes.
135

136
    >>> from oemof import solph
137

138
    >>> my_bus = solph.buses.Bus('my_bus')
139

140
    >>> my_storage = solph.components.GenericStorage(
141
    ...     label='storage',
142
    ...     nominal_capacity=1000,
143
    ...     inputs={my_bus: solph.flows.Flow(nominal_capacity=200, variable_costs=10)},
144
    ...     outputs={my_bus: solph.flows.Flow(nominal_capacity=200)},
145
    ...     loss_rate=0.01,
146
    ...     initial_storage_level=0,
147
    ...     max_storage_level = 0.9,
148
    ...     inflow_conversion_factor=0.9,
149
    ...     outflow_conversion_factor=0.93)
150

151
    >>> my_investment_storage = solph.components.GenericStorage(
152
    ...     label='storage',
153
    ...     nominal_capacity=solph.Investment(ep_costs=50),
154
    ...     inputs={my_bus: solph.flows.Flow(nominal_capacity=solph.Investment())},
155
    ...     outputs={my_bus: solph.flows.Flow(nominal_capacity=solph.Investment())},
156
    ...     loss_rate=0.02,
157
    ...     initial_storage_level=None,
158
    ...     invest_relation_input_capacity=1/6,
159
    ...     invest_relation_output_capacity=1/6,
160
    ...     inflow_conversion_factor=1,
161
    ...     outflow_conversion_factor=0.8)
162
    """  # noqa: E501
163

164
    def __init__(
2✔
165
        self,
166
        label=None,
167
        inputs=None,
168
        outputs=None,
169
        parent_node=None,
170
        nominal_capacity=None,
171
        nominal_storage_capacity=None,  # Can be removed for versions >= v0.7
172
        initial_storage_level=None,
173
        invest_relation_input_output=None,
174
        invest_relation_input_capacity=None,
175
        invest_relation_output_capacity=None,
176
        min_storage_level=0,
177
        max_storage_level=1,
178
        balanced=True,
179
        loss_rate=0,
180
        fixed_losses_relative=0,
181
        fixed_losses_absolute=0,
182
        inflow_conversion_factor=1,
183
        outflow_conversion_factor=1,
184
        fixed_costs=0,
185
        storage_costs=None,
186
        lifetime_inflow=None,
187
        lifetime_outflow=None,
188
        custom_attributes=None,
189
    ):
190
        if inputs is None:
2✔
191
            inputs = {}
2✔
192
        if outputs is None:
2✔
193
            outputs = {}
2✔
194
        if custom_attributes is None:
2!
195
            custom_attributes = {}
2✔
196
        super().__init__(
2✔
197
            label,
198
            inputs=inputs,
199
            outputs=outputs,
200
            parent_node=parent_node,
201
            custom_properties=custom_attributes,
202
        )
203
        # --- BEGIN: The following code can be removed for versions >= v0.7 ---
204
        if nominal_storage_capacity is not None:
2✔
205
            msg = (
2✔
206
                "For backward compatibility,"
207
                + " the option nominal_storage_capacity overwrites the option"
208
                + " nominal_capacity."
209
                + " Both options cannot be set at the same time."
210
            )
211
            if nominal_capacity is not None:
2✔
212
                raise AttributeError(msg)
2✔
213
            else:
214
                warn(msg, FutureWarning)
2✔
215
            nominal_capacity = nominal_storage_capacity
2✔
216
        # --- END ---
217

218
        self.nominal_storage_capacity = None
2✔
219
        self._invest_group = False
2✔
220
        self.invest_relation_input_output = sequence(
2✔
221
            invest_relation_input_output
222
        )
223
        self.invest_relation_input_capacity = sequence(
2✔
224
            invest_relation_input_capacity
225
        )
226
        self.invest_relation_output_capacity = sequence(
2✔
227
            invest_relation_output_capacity
228
        )
229
        if isinstance(nominal_capacity, numbers.Real):
2✔
230
            self.nominal_storage_capacity = nominal_capacity
2✔
231
        elif isinstance(nominal_capacity, Investment):
2✔
232
            self.investment = nominal_capacity
2✔
233
            self._invest_group = True
2✔
234

235
        self.initial_storage_level = initial_storage_level
2✔
236
        self.balanced = balanced
2✔
237
        self.loss_rate = sequence(loss_rate)
2✔
238
        self.fixed_losses_relative = sequence(fixed_losses_relative)
2✔
239
        self.fixed_losses_absolute = sequence(fixed_losses_absolute)
2✔
240
        self.inflow_conversion_factor = sequence(inflow_conversion_factor)
2✔
241
        self.outflow_conversion_factor = sequence(outflow_conversion_factor)
2✔
242
        self.max_storage_level = sequence(max_storage_level)
2✔
243
        self.min_storage_level = sequence(min_storage_level)
2✔
244
        self.fixed_costs = sequence(fixed_costs)
2✔
245
        self.storage_costs = sequence(storage_costs)
2✔
246
        self.lifetime_inflow = lifetime_inflow
2✔
247
        self.lifetime_outflow = lifetime_outflow
2✔
248

249
        # Check number of flows.
250
        self._check_number_of_flows()
2✔
251
        # Check for infeasible invest_relations
252
        self._check_invest_relations()
2✔
253
        # Check for infeasible parameter combinations
254
        self._check_infeasible_parameter_combinations()
2✔
255

256
    def _check_number_of_flows(self):
2✔
257
        """Ensure that there is only one inflow and outflow to the storage"""
258
        msg = "Only one {0} flow allowed in the GenericStorage {1}."
2✔
259
        check_node_object_for_missing_attribute(self, "inputs")
2✔
260
        check_node_object_for_missing_attribute(self, "outputs")
2✔
261
        if len(self.inputs) > 1:
2✔
262
            raise AttributeError(msg.format("input", self.label))
2✔
263
        if len(self.outputs) > 1:
2✔
264
            raise AttributeError(msg.format("output", self.label))
2✔
265

266
    def _check_input_for_investment(self):
2✔
267
        """Checks the input flow for an investment object. For sanity,
268
        this should be executed after _check_number_of_flows()"""
269
        for flow in self.inputs.values():
2✔
270
            is_investment = isinstance(flow.investment, Investment)
2✔
271
        return is_investment
2✔
272

273
    def _check_output_for_investment(self):
2✔
274
        """Checks the output flow for an investment object. For sanity,
275
        this should be executed after _check_number_of_flows()"""
276
        for flow in self.outputs.values():
2✔
277
            is_investment = isinstance(flow.investment, Investment)
2✔
278
        return is_investment
2✔
279

280
    def _check_storage_for_investment(self):
2✔
281
        """Checks the storage for an investment object (i.e. if investment
282
        into the capacity is possible)"""
283
        return hasattr(self, "investment")
2✔
284

285
    def _check_invest_relations(self):
2✔
286
        """Checks if the passed invest_relation keywords fit the
287
        passed Investment objects"""
288
        if self.invest_relation_input_capacity[0] is not None:
2✔
289
            if not self._check_input_for_investment():
2✔
290
                msg = (
2✔
291
                    "The input flow needs to have an Investment object "
292
                    "if `invest_relation_input_capacity` is set."
293
                )
294
                raise AttributeError(msg)
2✔
295
            if not self._check_storage_for_investment():
2✔
296
                msg = (
2✔
297
                    "If `invest_relation_input_capacity` is set, "
298
                    "`nominal_capacity` needs to be an Investment "
299
                    "object as well."
300
                )
301
                raise AttributeError(msg)
2✔
302
            self._invest_group = True
2✔
303
        if self.invest_relation_output_capacity[0] is not None:
2✔
304
            if not self._check_output_for_investment():
2✔
305
                msg = (
2✔
306
                    "The output flow needs to have an Investment object "
307
                    "if `invest_relation_output_capacity` is set."
308
                )
309
                raise AttributeError(msg)
2✔
310
            if not self._check_storage_for_investment():
2✔
311
                msg = (
2✔
312
                    "If `invest_relation_output_capacity` is set, "
313
                    "`nominal_capacity` needs to be an Investment "
314
                    "object as well."
315
                )
316
                raise AttributeError(msg)
2✔
317
            self._invest_group = True
2✔
318
        if self.invest_relation_input_output[0] is not None:
2✔
319
            if not self._check_input_for_investment():
2✔
320
                msg = (
2✔
321
                    "The input flow needs to have an Investment object "
322
                    "if `invest_relation_input_output` is set."
323
                )
324
                raise AttributeError(msg)
2✔
325
            if not self._check_output_for_investment():
2✔
326
                msg = (
2✔
327
                    "The output flow needs to have an Investment object "
328
                    "if `invest_relation_input_output` is set."
329
                )
330
                raise AttributeError(msg)
2✔
331

332
    def _check_infeasible_parameter_combinations(self):
2✔
333
        """Check for infeasible parameter combinations and raise error"""
334
        if self.initial_storage_level is not None:
2✔
335
            if (
2✔
336
                self.initial_storage_level < self.min_storage_level[0]
337
                or self.initial_storage_level > self.max_storage_level[0]
338
            ):
339
                e1 = (
2✔
340
                    "initial_storage_level must be greater or equal to "
341
                    "min_storage_level and smaller or equal to "
342
                    "max_storage_level."
343
                )
344
                raise ValueError(e1)
2✔
345
        """Raise errors for infeasible investment attribute combinations"""
2✔
346
        if (
2✔
347
            self.invest_relation_input_output[0] is not None
348
            and self.invest_relation_output_capacity[0] is not None
349
            and self.invest_relation_input_capacity[0] is not None
350
        ):
351
            e2 = (
2✔
352
                "Overdetermined. Three investment object will be coupled"
353
                "with three constraints. Set one invest relation to 'None'."
354
            )
355
            raise AttributeError(e2)
2✔
356
        if (
2✔
357
            hasattr(self, "investment")
358
            and self.fixed_losses_absolute.max() != 0
359
            and self.investment.existing == 0
360
            and self.investment.minimum.min() == 0
361
        ):
362
            e3 = (
2✔
363
                "With fixed_losses_absolute > 0, either investment.existing "
364
                "or investment.minimum has to be non-zero."
365
            )
366
            raise AttributeError(e3)
2✔
367

368
    def constraint_group(self):
2✔
369
        if self._invest_group is True:
2✔
370
            return GenericInvestmentStorageBlock
2✔
371
        else:
372
            return GenericStorageBlock
2✔
373

374

375
class GenericStorageBlock(ScalarBlock):
2✔
376
    r"""Storage without an :class:`.Investment` object.
377

378
    **The following sets are created:** (-> see basic sets at
379
    :class:`.Model` )
380

381
    STORAGES
382
        A set with all :py:class:`~.GenericStorage` objects, which do not have an
383
        :attr:`investment` of type :class:`.Investment`.
384

385
    STORAGES_BALANCED
386
        A set of  all :py:class:`~.GenericStorage` objects, with 'balanced' attribute set
387
        to True.
388

389
    STORAGES_WITH_INVEST_FLOW_REL
390
        A set with all :py:class:`~.GenericStorage` objects with two investment
391
        flows coupled with the 'invest_relation_input_output' attribute.
392

393
    **The following variables are created:**
394

395
    storage_content
396
        Storage content for every storage and timestep. The value for the
397
        storage content at the beginning is set by the parameter
398
        `initial_storage_level` or not set if `initial_storage_level` is None.
399
        The variable of storage s and timestep t can be accessed by:
400
        `om.GenericStorageBlock.storage_content[s, t]`
401

402
    intra_storage_delta
403
        Storage content for every storage and timestep of typical periods
404
        (only used in TSAM-mode). The variable of storage s and timestep t can
405
        be accessed by: `om.GenericStorageBlock.intra_storage_delta[s, k, t]`
406

407
    **The following constraints are created:**
408

409
    Set storage_content of last time step to one at t=0 if balanced == True
410
        .. math::
411
            E(t_{last}) = E(-1)
412

413
    Storage losses :attr:`om.Storage.losses[n, t]`
414
        .. math:: E_{loss}(t) = &E(t-1) \cdot
415
            1 - (1 - \beta(t))^{\tau(t)/(t_u)} \\
416
            &- \gamma(t)\cdot E_{nom} \cdot {\tau(t)/(t_u)}\\
417
            &- \delta(t) \cdot {\tau(t)/(t_u)}
418

419
    Storage balance :attr:`om.Storage.balance[n, t]`
420
        .. math:: E(t) = &E(t-1) - E_{loss}(t)\\
421
            &- \frac{\dot{E}_o(p, t)}{\eta_o(t)} \cdot \tau(t)\\
422
            &+ \dot{E}_i(p, t) \cdot \eta_i(t) \cdot \tau(t)
423

424
    Connect the invest variables of the input and the output flow.
425
        .. math::
426
          InvestmentFlowBlock.invest(source(n), n, p) + existing = \\
427
          (InvestmentFlowBlock.invest(n, target(n), p) + existing) \\
428
          * invest\_relation\_input\_output(n) \\
429
          \forall n \in \textrm{INVEST\_REL\_IN\_OUT} \\
430
          \forall p \in \textrm{PERIODS}
431

432

433

434
    =========================== ======================= =========
435
    symbol                      explanation             attribute
436
    =========================== ======================= =========
437
    :math:`E(t)`                energy currently stored `storage_content`
438
    :math:`E_{nom}`             nominal capacity of     `nominal_storage_capacity`
439
                                the energy storage
440
    :math:`c(-1)`               state before            `initial_storage_level`
441
                                initial time step
442
    :math:`c_{min}(t)`          minimum allowed storage `min_storage_level[t]`
443
    :math:`c_{max}(t)`          maximum allowed storage `max_storage_level[t]`
444
    :math:`\beta(t)`            fraction of lost energy `loss_rate[t]`
445
                                as share of
446
                                :math:`E(t)` per hour
447
    :math:`\gamma(t)`           fixed loss of energy    `fixed_losses_relative[t]`
448
                                per hour relative to
449
                                :math:`E_{nom}`
450
    :math:`\delta(t)`           absolute fixed loss     `fixed_losses_absolute[t]`
451
                                of energy per hour
452
    :math:`\dot{E}_i(t)`        energy flowing in       `inputs`
453
    :math:`\dot{E}_o(t)`        energy flowing out      `outputs`
454
    :math:`\eta_i(t)`           conversion factor       `inflow_conversion_factor[t]`
455
                                (i.e. efficiency)
456
                                when storing energy
457
    :math:`\eta_o(t)`           conversion factor when  `outflow_conversion_factor[t]`
458
                                (i.e. efficiency)
459
                                taking stored energy
460
    :math:`\tau(t)`             duration of time step
461
    :math:`t_u`                 time unit of losses
462
                                :math:`\beta(t)`,
463
                                :math:`\gamma(t)`
464
                                :math:`\delta(t)` and
465
                                timeincrement
466
                                :math:`\tau(t)`
467
    :math:`c_{storage}(t)`      costs of having         `storage_costs`
468
                                energy stored
469
    =========================== ======================= =========
470

471
    **The following parts of the objective function are created:**
472

473
    * :attr: `storage_costs` not 0
474

475
        .. math::
476
            \sum_{t \in \textrm{TIMEPOINTS} > 0} c_{storage}(t) \cdot E(t)
477

478
    * :attr:`fixed_costs` not 0
479

480
        .. math::
481
            \displaystyle \sum_{pp=0}^{year_{max}} E_{nom}
482
            \cdot c_{fixed}(pp)
483

484
    where :math:`year_{max}` denotes the last year of the optimization
485
      horizon, i.e. at the end of the last period.
486

487
    """  # noqa: E501
488

489
    CONSTRAINT_GROUP = True
2✔
490

491
    def __init__(self, *args, **kwargs):
2✔
492
        super().__init__(*args, **kwargs)
2✔
493

494
    def _create(self, group=None):
2✔
495
        """
496
        Parameters
497
        ----------
498
        group : list
499
            List containing storage objects.
500
            e.g. groups=[storage1, storage2,..]
501
        """
502
        m = self.parent_block()
2✔
503

504
        if group is None:
2!
NEW
UNCOV
505
            return None
×
506

507
        i = {n: [i for i in n.inputs][0] for n in group}
2✔
508
        o = {n: [o for o in n.outputs][0] for n in group}
2✔
509

510
        #  ************* SETS *********************************
511

512
        self.STORAGES = Set(initialize=[n for n in group])
2✔
513

514
        self.STORAGES_BALANCED = Set(
2✔
515
            initialize=[n for n in group if n.balanced is True]
516
        )
517

518
        self.STORAGES_INITITAL_LEVEL = Set(
2✔
519
            initialize=[
520
                n for n in group if n.initial_storage_level is not None
521
            ]
522
        )
523

524
        self.STORAGES_WITH_INVEST_FLOW_REL = Set(
2✔
525
            initialize=[
526
                n
527
                for n in group
528
                if n.invest_relation_input_output[0] is not None
529
            ]
530
        )
531

532
        #  ************* VARIABLES *****************************
533

534
        def _storage_content_bound_rule(block, n, t):
2✔
535
            """
536
            Rule definition for bounds of storage_content variable of
537
            storage n in timestep t.
538
            """
539
            bounds = (
2✔
540
                n.nominal_storage_capacity * n.min_storage_level[t],
541
                n.nominal_storage_capacity * n.max_storage_level[t],
542
            )
543
            return bounds
2✔
544

545
        if not m.TSAM_MODE:
2✔
546
            self.storage_content = Var(
2✔
547
                self.STORAGES, m.TIMEPOINTS, bounds=_storage_content_bound_rule
548
            )
549

550
            self.storage_losses = Var(self.STORAGES, m.TIMESTEPS)
2✔
551

552
            # set the initial storage content
553
            # ToDo: More elegant code possible?
554
            for n in group:
2✔
555
                if n.initial_storage_level is not None:
2✔
556
                    self.storage_content[n, 0] = (
2✔
557
                        n.initial_storage_level * n.nominal_storage_capacity
558
                    )
559
                    self.storage_content[n, 0].fix()
2✔
560
        else:
561
            # called "inter" in https://doi.org/10.1016/j.apenergy.2018.01.023
562
            self.inter_storage_content = Var(
2✔
563
                self.STORAGES, m.CLUSTERS_OFFSET, within=NonNegativeReals
564
            )
565
            # called "intra" in https://doi.org/10.1016/j.apenergy.2018.01.023
566
            self.intra_storage_delta = Var(
2✔
567
                self.STORAGES, m.TIMEINDEX_TYPICAL_CLUSTER_OFFSET
568
            )
569
            # set the initial intra storage content
570
            # first timestep in intra storage is always zero
571
            for n in group:
2✔
572
                for p, k in m.TYPICAL_CLUSTERS:
2✔
573
                    self.intra_storage_delta[n, p, k, 0] = 0
2✔
574
                    self.intra_storage_delta[n, p, k, 0].fix()
2✔
575
                if n.initial_storage_level is not None:
2!
NEW
UNCOV
576
                    self.inter_storage_content[n, 0] = (
×
577
                        n.initial_storage_level * n.nominal_storage_capacity
578
                    )
NEW
UNCOV
579
                    self.inter_storage_content[n, 0].fix()
×
580
        #  ************* Constraints ***************************
581

582
        def _storage_inter_minimum_level_rule(block):
2✔
583
            # See FINE implementation at
584
            # https://github.com/FZJ-IEK3-VSA/FINE/blob/
585
            # 57ec32561fb95e746c505760bd0d61c97d2fd2fb/FINE/storage.py#L1329
586
            for n in self.STORAGES:
2✔
587
                for p, i, g in m.TIMEINDEX_CLUSTER:
2✔
588
                    t = m.get_timestep_from_tsam_timestep(p, i, g)
2✔
589
                    lhs = n.nominal_storage_capacity * n.min_storage_level[t]
2✔
590
                    k = m.es.tsa_parameters[p]["order"][i]
2✔
591
                    tk = m.get_timestep_from_tsam_timestep(p, k, g)
2✔
592
                    inter_i = (
2✔
593
                        sum(
594
                            len(m.es.tsa_parameters[ip]["order"])
595
                            for ip in range(p)
596
                        )
597
                        + i
598
                    )
599
                    rhs = (
2✔
600
                        self.inter_storage_content[n, inter_i]
601
                        * (1 - n.loss_rate[t]) ** (g * m.timeincrement[tk])
602
                        + self.intra_storage_delta[n, p, k, g]
603
                    )
604
                    self.storage_inter_minimum_level.add(
2✔
605
                        (n, p, i, g), lhs <= rhs
606
                    )
607

608
        if m.TSAM_MODE:
2✔
609
            self.storage_inter_minimum_level = Constraint(
2✔
610
                self.STORAGES, m.TIMEINDEX_CLUSTER, noruleinit=True
611
            )
612

613
            self.storage_inter_minimum_level_build = BuildAction(
2✔
614
                rule=_storage_inter_minimum_level_rule
615
            )
616

617
        def _storage_inter_maximum_level_rule(block):
2✔
618
            for n in self.STORAGES:
2✔
619
                for p, i, g in m.TIMEINDEX_CLUSTER:
2✔
620
                    t = m.get_timestep_from_tsam_timestep(p, i, g)
2✔
621
                    k = m.es.tsa_parameters[p]["order"][i]
2✔
622
                    tk = m.get_timestep_from_tsam_timestep(p, k, g)
2✔
623
                    inter_i = (
2✔
624
                        sum(
625
                            len(m.es.tsa_parameters[ip]["order"])
626
                            for ip in range(p)
627
                        )
628
                        + i
629
                    )
630
                    lhs = (
2✔
631
                        self.inter_storage_content[n, inter_i]
632
                        * (1 - n.loss_rate[t]) ** (g * m.timeincrement[tk])
633
                        + self.intra_storage_delta[n, p, k, g]
634
                    )
635
                    rhs = n.nominal_storage_capacity * n.max_storage_level[t]
2✔
636
                    self.storage_inter_maximum_level.add(
2✔
637
                        (n, p, i, g), lhs <= rhs
638
                    )
639

640
        if m.TSAM_MODE:
2✔
641
            self.storage_inter_maximum_level = Constraint(
2✔
642
                self.STORAGES, m.TIMEINDEX_CLUSTER, noruleinit=True
643
            )
644

645
            self.storage_inter_maximum_level_build = BuildAction(
2✔
646
                rule=_storage_inter_maximum_level_rule
647
            )
648

649
        def _storage_losses_rule(block, n, t):
2✔
650
            expr = block.storage_content[n, t] * (
2✔
651
                1 - (1 - n.loss_rate[t]) ** m.timeincrement[t]
652
            )
653
            expr += (
2✔
654
                n.fixed_losses_relative[t]
655
                * n.nominal_storage_capacity
656
                * m.timeincrement[t]
657
            )
658
            expr += n.fixed_losses_absolute[t] * m.timeincrement[t]
2✔
659

660
            return expr == block.storage_losses[n, t]
2✔
661

662
        if not m.TSAM_MODE:
2✔
663
            self.losses = Constraint(
2✔
664
                self.STORAGES, m.TIMESTEPS, rule=_storage_losses_rule
665
            )
666

667
        def _storage_balance_rule(block, n, t):
2✔
668
            """
669
            Rule definition for the storage balance of every storage n and
670
            every timestep.
671
            """
672
            expr = block.storage_content[n, t]
2✔
673
            expr -= block.storage_losses[n, t]
2✔
674
            expr += (
2✔
675
                m.flow[i[n], n, t] * n.inflow_conversion_factor[t]
676
            ) * m.timeincrement[t]
677
            expr -= (
2✔
678
                m.flow[n, o[n], t] / n.outflow_conversion_factor[t]
679
            ) * m.timeincrement[t]
680
            return expr == block.storage_content[n, t + 1]
2✔
681

682
        def _intra_storage_balance_rule(block, n, p, k, g):
2✔
683
            """
684
            Rule definition for the storage balance of every storage n and
685
            every timestep.
686
            """
687
            t = m.get_timestep_from_tsam_timestep(p, k, g)
2✔
688
            expr = 0
2✔
689
            expr += block.intra_storage_delta[n, p, k, g + 1]
2✔
690
            expr += (
2✔
691
                -block.intra_storage_delta[n, p, k, g]
692
                * (1 - n.loss_rate[t]) ** m.timeincrement[t]
693
            )
694
            expr += (
2✔
695
                n.fixed_losses_relative[t]
696
                * n.nominal_storage_capacity
697
                * m.timeincrement[t]
698
            )
699
            expr += n.fixed_losses_absolute[t] * m.timeincrement[t]
2✔
700
            expr += (
2✔
701
                -m.flow[i[n], n, t] * n.inflow_conversion_factor[t]
702
            ) * m.timeincrement[t]
703
            expr += (
2✔
704
                m.flow[n, o[n], t] / n.outflow_conversion_factor[t]
705
            ) * m.timeincrement[t]
706
            return expr == 0
2✔
707

708
        if not m.TSAM_MODE:
2✔
709
            self.balance = Constraint(
2✔
710
                self.STORAGES, m.TIMESTEPS, rule=_storage_balance_rule
711
            )
712
        else:
713
            self.intra_balance = Constraint(
2✔
714
                self.STORAGES,
715
                m.TIMEINDEX_TYPICAL_CLUSTER,
716
                rule=_intra_storage_balance_rule,
717
            )
718

719
        def _inter_storage_balance_rule(block, n, i):
2✔
720
            """
721
            Rule definition for the storage balance of every storage n and
722
            every timestep.
723
            """
724
            ii = 0
2✔
725
            for p in m.PERIODS:
2!
726
                ii += len(m.es.tsa_parameters[p]["order"])
2✔
727
                if ii > i:
2!
728
                    ii -= len(m.es.tsa_parameters[p]["order"])
2✔
729
                    ii = i - ii
2✔
730
                    break
2✔
731

732
            k = m.es.tsa_parameters[p]["order"][ii]
2✔
733

734
            # Calculate inter losses over whole typical period
735
            t0 = m.get_timestep_from_tsam_timestep(p, k, 0)
2✔
736
            losses = math.prod(
2✔
737
                (
738
                    (1 - n.loss_rate[t0 + s])
739
                    ** m.es.tsa_parameters[p]["segments"][(k, s)]
740
                    if "segments" in m.es.tsa_parameters[p]
741
                    else 1 - n.loss_rate[t0 + s]
742
                )
743
                for s in range(m.es.tsa_parameters[p]["timesteps"])
744
            )
745

746
            expr = 0
2✔
747
            expr += block.inter_storage_content[n, i + 1]
2✔
748
            expr += -block.inter_storage_content[n, i] * losses
2✔
749
            expr += -self.intra_storage_delta[
2✔
750
                n, p, k, m.es.tsa_parameters[p]["timesteps"]
751
            ]
752
            return expr == 0
2✔
753

754
        if m.TSAM_MODE:
2✔
755
            self.inter_balance = Constraint(
2✔
756
                self.STORAGES,
757
                m.CLUSTERS,
758
                rule=_inter_storage_balance_rule,
759
            )
760

761
        def _balanced_storage_rule(block, n):
2✔
762
            """
763
            Storage content of last time step == initial storage content
764
            if balanced.
765
            """
766
            return (
2✔
767
                block.storage_content[n, m.TIMEPOINTS.at(-1)]
768
                == block.storage_content[n, m.TIMEPOINTS.at(1)]
769
            )
770

771
        def _balanced_inter_storage_rule(block, n):
2✔
772
            """
773
            Storage content of last time step == initial storage content
774
            if balanced.
775
            """
776
            return (
2✔
777
                block.inter_storage_content[n, m.CLUSTERS_OFFSET.at(-1)]
778
                == block.inter_storage_content[n, m.CLUSTERS_OFFSET.at(1)]
779
            )
780

781
        if not m.TSAM_MODE:
2✔
782
            self.balanced_cstr = Constraint(
2✔
783
                self.STORAGES_BALANCED, rule=_balanced_storage_rule
784
            )
785
        else:
786
            self.balanced_cstr = Constraint(
2✔
787
                self.STORAGES_BALANCED, rule=_balanced_inter_storage_rule
788
            )
789

790
        def _power_coupled(_):
2✔
791
            """
792
            Rule definition for constraint to connect the input power
793
            and output power
794
            """
795
            for n in self.STORAGES_WITH_INVEST_FLOW_REL:
2✔
796
                for p in m.PERIODS:
2✔
797
                    expr = (
2✔
798
                        m.InvestmentFlowBlock.total[n, o[n], p]
799
                    ) * n.invest_relation_input_output[p] == (
800
                        m.InvestmentFlowBlock.total[i[n], n, p]
801
                    )
802
                    self.power_coupled.add((n, p), expr)
2✔
803

804
        self.power_coupled = Constraint(
2✔
805
            self.STORAGES_WITH_INVEST_FLOW_REL, m.PERIODS, noruleinit=True
806
        )
807

808
        self.power_coupled_build = BuildAction(rule=_power_coupled)
2✔
809

810
    def _objective_expression(self):
2✔
811
        r"""
812
        Objective expression for storages with no investment.
813

814
        * Fixed costs (will not have an impact on the actual optimisation).
815
        * Variable costs for storage content.
816
        """
817
        m = self.parent_block()
2✔
818

819
        fixed_costs = 0
2✔
820

821
        for n in self.STORAGES:
2✔
822
            if valid_sequence(n.fixed_costs, len(m.PERIODS)):
2!
823
                fixed_costs += sum(
2✔
824
                    n.nominal_storage_capacity * n.fixed_costs[pp]
825
                    for pp in range(m.es.end_year_of_optimization)
826
                )
827
        self.fixed_costs = Expression(expr=fixed_costs)
2✔
828

829
        storage_costs = 0
2✔
830

831
        for n in self.STORAGES:
2✔
832
            if valid_sequence(n.storage_costs, len(m.TIMESTEPS)):
2✔
833
                # We actually want to iterate over all TIMEPOINTS except the
834
                # 0th. As integers are used for the index, this is equicalent
835
                # to iterating over the TIMESTEPS with one offset.
836
                if not m.TSAM_MODE:
2!
837
                    for t in m.TIMESTEPS:
2✔
838
                        storage_costs += (
2✔
839
                            self.storage_content[n, t + 1] * n.storage_costs[t]
840
                        )
841
                else:
NEW
UNCOV
842
                    for t in m.TIMESTEPS_ORIGINAL:
×
NEW
843
                        storage_costs += (
×
844
                            self.storage_content[n, t + 1]
845
                            * n.storage_costs[t + 1]
846
                        )
847

848
        self.storage_costs = Expression(expr=storage_costs)
2✔
849
        self.costs = Expression(expr=storage_costs + fixed_costs)
2✔
850

851
        return self.costs
2✔
852

853

854
class GenericInvestmentStorageBlock(ScalarBlock):
2✔
855
    r"""
856
    Block for all storages with :attr:`Investment` being not None.
857
    See :class:`.Investment` for all parameters of the
858
    Investment class.
859

860
    **Variables**
861

862
    All Storages are indexed by :math:`n` (denoting the respective storage
863
    unit), which is omitted in the following for the sake of convenience.
864
    The following variables are created as attributes of
865
    :attr:`om.GenericInvestmentStorageBlock`:
866

867
    * :math:`P_i(p, t)`
868

869
        Inflow of the storage
870
        (created in :class:`oemof.solph.models.Model`).
871

872
    * :math:`P_o(p, t)`
873

874
        Outflow of the storage
875
        (created in :class:`oemof.solph.models.Model`).
876

877
    * :math:`E(t)`
878

879
        Current storage content (Absolute level of stored energy).
880

881
    * :math:`E_{invest}(p)`
882

883
        Invested (nominal) capacity of the storage in period p.
884

885
    * :math:`E_{total}(p)`
886

887
        Total installed (nominal) capacity of the storage in period p.
888

889
    * :math:`E_{old}(p)`
890

891
        Old (nominal) capacity of the storage to be decommissioned in period p.
892

893
    * :math:`E_{old,exo}(p)`
894

895
        Exogenous old (nominal) capacity of the storage to be decommissioned
896
        in period p; existing capacity reaching its lifetime.
897

898
    * :math:`E_{old,endo}(p)`
899

900
        Endogenous old (nominal) capacity of the storage to be decommissioned
901
        in period p; endgenous investments reaching their lifetime.
902

903
    * :math:`E(-1)`
904

905
        Initial storage content (before timestep 0).
906
        Not applicable for a multi-period model.
907

908
    * :math:`b_{invest}(p)`
909

910
        Binary variable for the status of the investment, if
911
        :attr:`nonconvex` is `True`.
912

913
    **Constraints**
914

915
    The following constraints are created for all investment storages:
916

917
        Storage balance (Same as for :class:`.GenericStorageBlock`)
918

919
        .. math:: E(t) = &E(t-1) \cdot
920
            (1 - \beta(t)) ^{\tau(t)/(t_u)} \\
921
            &- \gamma(t)\cdot (E_{total}(p)) \cdot {\tau(t)/(t_u)}\\
922
            &- \delta(t) \cdot {\tau(t)/(t_u)}\\
923
            &- \frac{\dot{E}_o(p, t))}{\eta_o(t)} \cdot \tau(t)
924
            + \dot{E}_i(p, t) \cdot \eta_i(t) \cdot \tau(t)
925

926
        Total storage capacity (p > 0 for multi-period model only)
927

928
        .. math::
929
            &
930
            if \quad p=0:\\
931
            &
932
            E_{total}(p) = E_{exist} + E_{invest}(p)\\
933
            &\\
934
            &
935
            else:\\
936
            &
937
            E_{total}(p) = E_{total}(p-1) + E_{invest}(p) - E_{old}(p)\\
938
            &\\
939
            &
940
            \forall p \in \textrm{PERIODS}
941

942
        Old storage capacity (p > 0 for multi-period model only)
943

944
        .. math::
945
            &
946
            E_{old}(p) = E_{old,exo}(p) + E_{old,end}(p)\\
947
            &\\
948
            &
949
            if \quad p=0:\\
950
            &
951
            E_{old,end}(p) = 0\\
952
            &\\
953
            &
954
            else \quad if \quad l \leq year(p):\\
955
            &
956
            E_{old,end}(p) = E_{invest}(p_{comm})\\
957
            &\\
958
            &
959
            else:\\
960
            &
961
            E_{old,end}(p)\\
962
            &\\
963
            &
964
            if \quad p=0:\\
965
            &
966
            E_{old,exo}(p) = 0\\
967
            &\\
968
            &
969
            else \quad if \quad l - a \leq year(p):\\
970
            &
971
            E_{old,exo}(p) = E_{exist} (*)\\
972
            &\\
973
            &
974
            else:\\
975
            &
976
            E_{old,exo}(p) = 0\\
977
            &\\
978
            &
979
            \forall p \in \textrm{PERIODS}
980

981
        where:
982

983
        * (*) is only performed for the first period the condition is True.
984
          A decommissioning flag is then set to True to prevent having falsely
985
          added old capacity in future periods.
986
        * :math:`year(p)` is the year corresponding to period p
987
        * :math:`p_{comm}` is the commissioning period of the storage
988

989
    Depending on the attribute :attr:`nonconvex`, the constraints for the
990
    bounds of the decision variable :math:`E_{invest}(p)` are different:\
991

992
        * :attr:`nonconvex = False`
993

994
        .. math::
995
            &
996
            E_{invest, min}(p) \le E_{invest}(p) \le E_{invest, max}(p) \\
997
            &
998
            \forall p \in \textrm{PERIODS}
999

1000
        * :attr:`nonconvex = True`
1001

1002
        .. math::
1003
            &
1004
            E_{invest, min}(p) \cdot b_{invest}(p) \le E_{invest}(p)\\
1005
            &
1006
            E_{invest}(p) \le E_{invest, max}(p) \cdot b_{invest}(p)\\
1007
            &
1008
            \forall p \in \textrm{PERIODS}
1009

1010
    The following constraints are created depending on the attributes of
1011
    the :class:`.GenericStorage`:
1012

1013
        * :attr:`initial_storage_level is None`;
1014
          not applicable for multi-period model
1015

1016
            Constraint for a variable initial storage content:
1017

1018
        .. math::
1019
               E(-1) \le E_{exist} + E_{invest}(0)
1020

1021
        * :attr:`initial_storage_level is not None`;
1022
          not applicable for multi-period model
1023

1024
            An initial value for the storage content is given:
1025

1026
        .. math::
1027
               E(-1) = (E_{invest}(0) + E_{exist}) \cdot c(-1)
1028

1029
        * :attr:`balanced=True`;
1030
          not applicable for multi-period model
1031

1032
            The energy content of storage of the first and the last timestep
1033
            are set equal:
1034

1035
        .. math::
1036
            E(-1) = E(t_{last})
1037

1038
        * :attr:`invest_relation_input_capacity is not None`
1039

1040
            Connect the invest variables of the storage and the input flow:
1041

1042
        .. math::
1043
            &
1044
            P_{i,total}(p) =
1045
            E_{total}(p) \cdot r_{cap,in} \\
1046
            &
1047
            \forall p \in \textrm{PERIODS}
1048

1049
        * :attr:`invest_relation_output_capacity is not None`
1050

1051
            Connect the invest variables of the storage and the output flow:
1052

1053
        .. math::
1054
            &
1055
            P_{o,total}(p) =
1056
            E_{total}(p) \cdot r_{cap,out}\\
1057
            &
1058
            \forall p \in \textrm{PERIODS}
1059

1060
        * :attr:`invest_relation_input_output is not None`
1061

1062
            Connect the invest variables of the input and the output flow:
1063

1064
        .. math::
1065
            &
1066
            P_{i,total}(p) =
1067
            P_{o,total}(p) \cdot r_{in,out}\\
1068
            &
1069
            \forall p \in \textrm{PERIODS}
1070

1071
        * :attr:`max_storage_level`
1072

1073
            Rule for upper bound constraint for the storage content:
1074

1075
        .. math::
1076
            &
1077
            E(t) \leq E_{total}(p) \cdot c_{max}(t)\\
1078
            &
1079
            \forall p, t \in \textrm{TIMEINDEX}
1080

1081
        * :attr:`min_storage_level`
1082

1083
            Rule for lower bound constraint for the storage content:
1084

1085
        .. math::
1086
            &
1087
            E(t) \geq E_{total}(p) \cdot c_{min}(t)\\
1088
            &
1089
            \forall p, t \in \textrm{TIMEINDEX}
1090

1091

1092
    **Objective function**
1093

1094
    Objective terms for a standard model and a multi-period model differ
1095
    quite strongly. Besides, the part of the objective function added by the
1096
    investment storages also depends on whether a convex or nonconvex
1097
    investment option is selected. The following parts of the objective
1098
    function are created:
1099

1100
    *Standard model*
1101

1102
        * :attr:`nonconvex = False`
1103

1104
            .. math::
1105
                E_{invest}(0) \cdot c_{invest,var}(0)
1106

1107
        * :attr:`nonconvex = True`
1108

1109
            .. math::
1110
                E_{invest}(0) \cdot c_{invest,var}(0)
1111
                + c_{invest,fix}(0) \cdot b_{invest}(0)\\
1112

1113
    Where 0 denotes the 0th (investment) period since
1114
    in a standard model, there is only this one period.
1115

1116
    *Multi-period model*
1117

1118
        * :attr:`nonconvex = False`
1119

1120
            .. math::
1121
                &
1122
                E_{invest}(p) \cdot A(c_{invest,var}(p), l, ir)
1123
                \cdot \frac {1}{ANF(d, ir)} \cdot DF^{-p}\\
1124
                &
1125
                \forall p \in \textrm{PERIODS}
1126

1127
        In case, the remaining lifetime of a storage is greater than 0 and
1128
        attribute `use_remaining_value` of the energy system is True,
1129
        the difference in value for the investment period compared to the
1130
        last period of the optimization horizon is accounted for
1131
        as an adder to the investment costs:
1132

1133
            .. math::
1134
                &
1135
                E_{invest}(p) \cdot (A(c_{invest,var}(p), l_{r}, ir) -
1136
                A(c_{invest,var}(|P|), l_{r}, ir)\\
1137
                & \cdot \frac {1}{ANF(l_{r}, ir)} \cdot DF^{-|P|}\\
1138
                &\\
1139
                &
1140
                \forall p \in \textrm{PERIODS}
1141

1142
        * :attr:`nonconvex = True`
1143

1144
            .. math::
1145
                &
1146
                (E_{invest}(p) \cdot A(c_{invest,var}(p), l, ir)
1147
                \cdot \frac {1}{ANF(d, ir)}\\
1148
                &
1149
                +  c_{invest,fix}(p) \cdot b_{invest}(p)) \cdot DF^{-p} \\
1150
                &
1151
                \forall p \in \textrm{PERIODS}
1152

1153
        In case, the remaining lifetime of a storage is greater than 0 and
1154
        attribute `use_remaining_value` of the energy system is True,
1155
        the difference in value for the investment period compared to the
1156
        last period of the optimization horizon is accounted for
1157
        as an adder to the investment costs:
1158

1159
            .. math::
1160
                &
1161
                (E_{invest}(p) \cdot (A(c_{invest,var}(p), l_{r}, ir) -
1162
                A(c_{invest,var}(|P|), l_{r}, ir)\\
1163
                & \cdot \frac {1}{ANF(l_{r}, ir)} \cdot DF^{-|P|}\\
1164
                &
1165
                +  (c_{invest,fix}(p) - c_{invest,fix}(|P|))
1166
                \cdot b_{invest}(p)) \cdot DF^{-p}\\
1167
                &\\
1168
                &
1169
                \forall p \in \textrm{PERIODS}
1170

1171
        * :attr:`fixed_costs` not None for investments
1172

1173
            .. math::
1174
                &
1175
                \sum_{pp=year(p)}^{limit_{end}}
1176
                E_{invest}(p) \cdot c_{fixed}(pp) \cdot DF^{-pp})
1177
                \cdot DF^{-p}\\
1178
                &
1179
                \forall p \in \textrm{PERIODS}
1180

1181
        * :attr:`fixed_costs` not None for existing capacity
1182

1183
            .. math::
1184
                \sum_{pp=0}^{limit_{exo}} E_{exist} \cdot c_{fixed}(pp)
1185
                \cdot DF^{-pp}
1186

1187
    where:
1188

1189
    * :math:`A(c_{invest,var}(p), l, ir)` A is the annuity for
1190
      investment expenses :math:`c_{invest,var}(p)`, lifetime :math:`l`
1191
      and interest rate :math:`ir`.
1192
    * :math:`l_{r}` is the remaining lifetime at the end of the
1193
      optimization horizon (in case it is greater than 0 and
1194
      smaller than the actual lifetime).
1195
    * :math:`ANF(d, ir)` is the annuity factor for duration :math:`d`
1196
      and interest rate :math:`ir`.
1197
    * :math:`d=min\{year_{max} - year(p), l\}` defines the
1198
      number of years within the optimization horizon that investment
1199
      annuities are accounted for.
1200
    * :math:`year(p)` denotes the start year of period :math:`p`.
1201
    * :math:`year_{max}` denotes the last year of the optimization
1202
      horizon, i.e. at the end of the last period.
1203
    * :math:`limit_{end}=min\{year_{max}, year(p) + l\}` is used as an
1204
      upper bound to ensure fixed costs for endogenous investments
1205
      to occur within the optimization horizon.
1206
    * :math:`limit_{exo}=min\{year_{max}, l - a\}` is used as an
1207
      upper bound to ensure fixed costs for existing capacities to occur
1208
      within the optimization horizon. :math:`a` is the initial age
1209
      of an asset.
1210
    * :math:`DF=(1+dr)` is the discount factor.
1211

1212
    The annuity / annuity factor hereby is:
1213

1214
        .. math::
1215
            &
1216
            A(c_{invest,var}(p), l, ir) = c_{invest,var}(p) \cdot
1217
                \frac {(1+ir)^l \cdot ir} {(1+ir)^l - 1}\\
1218
            &\\
1219
            &
1220
            ANF(d, ir)=\frac {(1+ir)^d \cdot ir} {(1+ir)^d - 1}
1221

1222
    They are retrieved, using oemof.tools.economics annuity function. The
1223
    interest rate :math:`ir` for the annuity is defined as weighted
1224
    average costs of capital (wacc) and assumed constant over time.
1225

1226
    The overall summed cost expressions for all *InvestmentFlowBlock* objects
1227
    can be accessed by
1228

1229
    * :attr:`om.GenericInvestmentStorageBlock.investment_costs`,
1230
    * :attr:`om.GenericInvestmentStorageBlock.fixed_costs` and
1231
    * :attr:`om.GenericInvestmentStorageBlock.costs`.
1232

1233
    Their values  after optimization can be retrieved by
1234

1235
    * :meth:`om.GenericInvestmentStorageBlock.investment_costs`,
1236
    * :attr:`om.GenericInvestmentStorageBlock.period_investment_costs`
1237
      (yielding a dict keyed by periods); note: this is not a Pyomo expression,
1238
      but calculated,
1239
    * :meth:`om.GenericInvestmentStorageBlock.fixed_costs` and
1240
    * :meth:`om.GenericInvestmentStorageBlock.costs`.
1241

1242
    .. csv-table:: List of Variables
1243
        :header: "symbol", "attribute", "explanation"
1244
        :widths: 1, 1, 1
1245

1246
        ":math:`P_i(p, t)`", ":attr:`flow[i[n], n, p, t]`", "Inflow
1247
        of the storage"
1248
        ":math:`P_o(p, t)`", ":attr:`flow[n, o[n], p, t]`", "Outflow
1249
        of the storage"
1250
        ":math:`E(t)`", ":attr:`storage_content[n, t]`", "Current storage
1251
        content (current absolute stored energy)"
1252
        ":math:`E_{loss}(t)`", ":attr:`storage_losses[n, t]`", "Current storage
1253
        losses (absolute losses per time step)"
1254
        ":math:`E_{invest}(p)`", ":attr:`invest[n, p]`", "Invested (nominal)
1255
        capacity of the storage"
1256
        ":math:`E_{old}(p)`", ":attr:`old[n, p]`", "
1257
        | Old (nominal) capacity of the storage
1258
        | to be decommissioned in period p"
1259
        ":math:`E_{old,exo}(p)`", ":attr:`old_exo[n, p]`", "
1260
        | Old (nominal) capacity of the storage
1261
        | to be decommissioned in period p
1262
        | which was exogenously given by :math:`E_{exist}`"
1263
        ":math:`E_{old,end}(p)`", ":attr:`old_end[n, p]`", "
1264
        | Old (nominal) capacity of the storage
1265
        | to be decommissioned in period p
1266
        | which was endogenously determined by :math:`E_{invest}(p_{comm})`
1267
        | where :math:`p_{comm}` is the commissioning period"
1268
        ":math:`E(-1)`", ":attr:`init_cap[n]`", "Initial storage capacity
1269
        (before timestep 0)"
1270
        ":math:`b_{invest}(p)`", ":attr:`invest_status[i, o, p]`", "Binary
1271
        variable for the status of investment"
1272
        ":math:`P_{i,invest}(p)`", "
1273
        :attr:`InvestmentFlowBlock.invest[i[n], n, p]`", "
1274
        Invested (nominal) inflow (InvestmentFlowBlock)"
1275
        ":math:`P_{o,invest}`", "
1276
        :attr:`InvestmentFlowBlock.invest[n, o[n]]`", "
1277
        Invested (nominal) outflow (InvestmentFlowBlock)"
1278

1279
    .. csv-table:: List of Parameters
1280
        :header: "symbol", "attribute", "explanation"
1281
        :widths: 1, 1, 1
1282

1283
        ":math:`E_{exist}`", "`flows[i, o].investment.existing`", "
1284
        Existing storage capacity"
1285
        ":math:`E_{invest,min}`", "`flows[i, o].investment.minimum`", "
1286
        Minimum investment value"
1287
        ":math:`E_{invest,max}`", "`flows[i, o].investment.maximum`", "
1288
        Maximum investment value"
1289
        ":math:`P_{i,exist}`", "`flows[i[n], n].investment.existing`
1290
        ", "Existing inflow capacity"
1291
        ":math:`P_{o,exist}`", "`flows[n, o[n]].investment.existing`
1292
        ", "Existing outflow capacity"
1293
        ":math:`c_{invest,var}`", "`flows[i, o].investment.ep_costs`
1294
        ", "Variable investment costs"
1295
        ":math:`c_{invest,fix}`", "`flows[i, o].investment.offset`", "
1296
        Fix investment costs"
1297
        ":math:`c_{fixed}`", "`flows[i, o].investment.fixed_costs`", "
1298
        Fixed costs; only allowed in multi-period model"
1299
        ":math:`r_{cap,in}`", ":attr:`invest_relation_input_capacity`", "
1300
        Relation of storage capacity and nominal inflow"
1301
        ":math:`r_{cap,out}`", ":attr:`invest_relation_output_capacity`", "
1302
        Relation of storage capacity and nominal outflow"
1303
        ":math:`r_{in,out}`", ":attr:`invest_relation_input_output`", "
1304
        Relation of nominal in- and outflow"
1305
        ":math:`\beta(t)`", "`loss_rate[t]`", "Fraction of lost energy
1306
        as share of :math:`E(t)` per hour"
1307
        ":math:`\gamma(t)`", "`fixed_losses_relative[t]`", "Fixed loss
1308
        of energy relative to :math:`E_{invest} + E_{exist}` per hour"
1309
        ":math:`\delta(t)`", "`fixed_losses_absolute[t]`", "Absolute
1310
        fixed loss of energy per hour"
1311
        ":math:`\eta_i(t)`", "`inflow_conversion_factor[t]`", "
1312
        Conversion factor (i.e. efficiency) when storing energy"
1313
        ":math:`\eta_o(t)`", "`outflow_conversion_factor[t]`", "
1314
        Conversion factor when (i.e. efficiency) taking stored energy"
1315
        ":math:`c(-1)`", "`initial_storage_level`", "Initial relative
1316
        storage content (before timestep 0)"
1317
        ":math:`c_{max}`", "`flows[i, o].max[t]`", "Normed maximum
1318
        value of storage content"
1319
        ":math:`c_{min}`", "`flows[i, o].min[t]`", "Normed minimum
1320
        value of storage content"
1321
        ":math:`l`", "`flows[i, o].investment.lifetime`", "
1322
        Lifetime for investments in storage capacity"
1323
        ":math:`a`", "`flows[i, o].investment.age`", "
1324
        Initial age of existing capacity / energy"
1325
        ":math:`\tau(t)`", "", "Duration of time step"
1326
        ":math:`t_u`", "", "Time unit of losses :math:`\beta(t)`,
1327
        :math:`\gamma(t)`, :math:`\delta(t)` and timeincrement :math:`\tau(t)`"
1328

1329
    """
1330

1331
    CONSTRAINT_GROUP = True
2✔
1332

1333
    def __init__(self, *args, **kwargs):
2✔
1334
        super().__init__(*args, **kwargs)
2✔
1335

1336
    def _create(self, group):
2✔
1337
        """Create a storage block for investment modeling"""
1338
        m = self.parent_block()
2✔
1339

1340
        # ########################## CHECKS ###################################
1341
        if m.es.periods is not None:
2✔
1342
            for n in group:
2✔
1343
                error_fixed_absolute_losses = (
2✔
1344
                    "For a multi-period investment model, fixed absolute"
1345
                    " losses are not supported. Please remove parameter."
1346
                )
1347
                if n.fixed_losses_absolute[0] != 0:
2!
NEW
UNCOV
1348
                    raise ValueError(error_fixed_absolute_losses)
×
1349
                error_initial_storage_level = (
2✔
1350
                    "For a multi-period model, initial_storage_level is"
1351
                    " not supported.\nIt needs to be removed since it"
1352
                    " has no effect.\nstorage_content will be zero,"
1353
                    " until there is some usable storage capacity installed."
1354
                )
1355
                if n.initial_storage_level is not None:
2!
NEW
UNCOV
1356
                    raise ValueError(error_initial_storage_level)
×
1357

1358
        # ########################## SETS #####################################
1359

1360
        self.INVESTSTORAGES = Set(initialize=[n for n in group])
2✔
1361

1362
        self.CONVEX_INVESTSTORAGES = Set(
2✔
1363
            initialize=[n for n in group if n.investment.nonconvex is False]
1364
        )
1365

1366
        self.NON_CONVEX_INVESTSTORAGES = Set(
2✔
1367
            initialize=[n for n in group if n.investment.nonconvex is True]
1368
        )
1369

1370
        self.INVESTSTORAGES_BALANCED = Set(
2✔
1371
            initialize=[n for n in group if n.balanced is True]
1372
        )
1373

1374
        self.INVESTSTORAGES_NO_INIT_CONTENT = Set(
2✔
1375
            initialize=[n for n in group if n.initial_storage_level is None]
1376
        )
1377

1378
        self.INVESTSTORAGES_INIT_CONTENT = Set(
2✔
1379
            initialize=[
1380
                n for n in group if n.initial_storage_level is not None
1381
            ]
1382
        )
1383

1384
        self.INVEST_REL_CAP_IN = Set(
2✔
1385
            initialize=[
1386
                n
1387
                for n in group
1388
                if n.invest_relation_input_capacity[0] is not None
1389
            ]
1390
        )
1391

1392
        self.INVEST_REL_CAP_OUT = Set(
2✔
1393
            initialize=[
1394
                n
1395
                for n in group
1396
                if n.invest_relation_output_capacity[0] is not None
1397
            ]
1398
        )
1399

1400
        self.INVEST_REL_IN_OUT = Set(
2✔
1401
            initialize=[
1402
                n
1403
                for n in group
1404
                if n.invest_relation_input_output[0] is not None
1405
            ]
1406
        )
1407

1408
        # The storage content is a non-negative variable, therefore it makes no
1409
        # sense to create an additional constraint if the lower bound is zero
1410
        # for all time steps.
1411
        self.MIN_INVESTSTORAGES = Set(
2✔
1412
            initialize=[
1413
                n
1414
                for n in group
1415
                if sum([n.min_storage_level[t] for t in m.TIMESTEPS]) > 0
1416
            ]
1417
        )
1418

1419
        self.OVERALL_MAXIMUM_INVESTSTORAGES = Set(
2✔
1420
            initialize=[
1421
                n for n in group if n.investment.overall_maximum is not None
1422
            ]
1423
        )
1424

1425
        self.OVERALL_MINIMUM_INVESTSTORAGES = Set(
2✔
1426
            initialize=[
1427
                n for n in group if n.investment.overall_minimum is not None
1428
            ]
1429
        )
1430

1431
        self.EXISTING_INVESTSTORAGES = Set(
2✔
1432
            initialize=[n for n in group if n.investment.existing is not None]
1433
        )
1434

1435
        # ######################### Variables  ################################
1436
        if not m.TSAM_MODE:
2✔
1437
            self.storage_content = Var(
2✔
1438
                self.INVESTSTORAGES, m.TIMEPOINTS, within=NonNegativeReals
1439
            )
1440
        else:
1441
            self.inter_storage_content = Var(
2✔
1442
                self.INVESTSTORAGES, m.CLUSTERS_OFFSET, within=NonNegativeReals
1443
            )
1444
            self.intra_storage_delta = Var(
2✔
1445
                self.INVESTSTORAGES, m.TIMEINDEX_TYPICAL_CLUSTER_OFFSET
1446
            )
1447
            # set the initial intra storage content
1448
            # first timestep in intra storage is always zero
1449
            for n in group:
2✔
1450
                for p, k in m.TYPICAL_CLUSTERS:
2✔
1451
                    self.intra_storage_delta[n, p, k, 0] = 0
2✔
1452
                    self.intra_storage_delta[n, p, k, 0].fix()
2✔
1453

1454
        def _storage_investvar_bound_rule(_, n, p):
2✔
1455
            """
1456
            Rule definition to bound the invested storage capacity `invest`.
1457
            """
1458
            if n in self.CONVEX_INVESTSTORAGES:
2✔
1459
                return n.investment.minimum[p], n.investment.maximum[p]
2✔
1460
            else:  # n in self.NON_CONVEX_INVESTSTORAGES
1461
                return 0, n.investment.maximum[p]
2✔
1462

1463
        self.invest = Var(
2✔
1464
            self.INVESTSTORAGES,
1465
            m.PERIODS,
1466
            within=NonNegativeReals,
1467
            bounds=_storage_investvar_bound_rule,
1468
        )
1469

1470
        # Total capacity
1471
        self.total = Var(
2✔
1472
            self.INVESTSTORAGES,
1473
            m.PERIODS,
1474
            within=NonNegativeReals,
1475
            initialize=0,
1476
        )
1477

1478
        if m.es.periods is not None:
2✔
1479
            # Old capacity to be decommissioned (due to lifetime)
1480
            self.old = Var(
2✔
1481
                self.INVESTSTORAGES, m.PERIODS, within=NonNegativeReals
1482
            )
1483

1484
            # Old endogenous capacity to be decommissioned (due to lifetime)
1485
            self.old_end = Var(
2✔
1486
                self.INVESTSTORAGES, m.PERIODS, within=NonNegativeReals
1487
            )
1488

1489
            # Old exogenous capacity to be decommissioned (due to lifetime)
1490
            self.old_exo = Var(
2✔
1491
                self.INVESTSTORAGES, m.PERIODS, within=NonNegativeReals
1492
            )
1493

1494
        # create status variable for a non-convex investment storage
1495
        self.invest_status = Var(
2✔
1496
            self.NON_CONVEX_INVESTSTORAGES, m.PERIODS, within=Binary
1497
        )
1498

1499
        # ######################### CONSTRAINTS ###############################
1500
        i = {n: [i for i in n.inputs][0] for n in group}
2✔
1501
        o = {n: [o for o in n.outputs][0] for n in group}
2✔
1502

1503
        # Handle unit lifetimes
1504
        def _total_storage_capacity_rule(block):
2✔
1505
            """Rule definition for determining total installed
1506
            capacity (taking decommissioning into account)
1507
            """
1508
            for n in self.INVESTSTORAGES:
2✔
1509
                for p in m.PERIODS:
2✔
1510
                    if p == 0:
2!
1511
                        expr = (
2✔
1512
                            self.total[n, p]
1513
                            == self.invest[n, p] + n.investment.existing
1514
                        )
1515
                        self.total_storage_rule.add((n, p), expr)
2✔
1516
                    else:
NEW
UNCOV
1517
                        expr = (
×
1518
                            self.total[n, p]
1519
                            == self.invest[n, p]
1520
                            + self.total[n, p - 1]
1521
                            - self.old[n, p]
1522
                        )
NEW
UNCOV
1523
                        self.total_storage_rule.add((n, p), expr)
×
1524

1525
        self.total_storage_rule = Constraint(
2✔
1526
            self.INVESTSTORAGES, m.PERIODS, noruleinit=True
1527
        )
1528

1529
        self.total_storage_rule_build = BuildAction(
2✔
1530
            rule=_total_storage_capacity_rule
1531
        )
1532

1533
        # multi-period storage implementation for time intervals
1534
        if m.es.periods is not None:
2✔
1535

1536
            def _old_storage_capacity_rule_end(block):
2✔
1537
                """Rule definition for determining old endogenously installed
1538
                capacity to be decommissioned due to reaching its lifetime.
1539
                Investment and decommissioning periods are linked within
1540
                the constraint. The respective decommissioning period is
1541
                determined for every investment period based on the components
1542
                lifetime and a matrix describing its age of each endogenous
1543
                investment. Decommissioning can only occur at the beginning of
1544
                each period.
1545

1546
                Note
1547
                ----
1548
                For further information on the implementation check
1549
                PR#957 https://github.com/oemof/oemof-solph/pull/957
1550
                """
1551
                for n in self.INVESTSTORAGES:
2✔
1552
                    lifetime = n.investment.lifetime
2✔
1553
                    if lifetime is None:
2!
NEW
UNCOV
1554
                        msg = (
×
1555
                            "You have to specify a lifetime "
1556
                            "for a Flow going into or out of "
1557
                            "a GenericStorage unit "
1558
                            "in a multi-period model!"
1559
                            f" Value for {n} is missing."
1560
                        )
NEW
UNCOV
1561
                        raise ValueError(msg)
×
1562
                    # get the period matrix describing the temporal distance
1563
                    # between all period combinations.
1564
                    periods_matrix = m.es.periods_matrix
2✔
1565

1566
                    # get the index of the minimum value in each row greater
1567
                    # equal than the lifetime. This value equals the
1568
                    # decommissioning period if not zero. The index of this
1569
                    # value represents the investment period. If np.where
1570
                    # condition is not met in any row, min value will be zero
1571
                    decomm_periods = np.argmin(
2✔
1572
                        np.where(
1573
                            (periods_matrix >= lifetime),
1574
                            periods_matrix,
1575
                            np.inf,
1576
                        ),
1577
                        axis=1,
1578
                    )
1579

1580
                    # no decommissioning in first period
1581
                    expr = self.old_end[n, 0] == 0
2✔
1582
                    self.old_rule_end.add((n, 0), expr)
2✔
1583

1584
                    # all periods not in decomm_periods have no decommissioning
1585
                    # zero is excluded
1586
                    for p in m.PERIODS:
2✔
1587
                        if p not in decomm_periods and p != 0:
2!
NEW
UNCOV
1588
                            expr = self.old_end[n, p] == 0
×
NEW
1589
                            self.old_rule_end.add((n, p), expr)
×
1590

1591
                    # multiple invests can be decommissioned in the same period
1592
                    # but only sequential ones, thus a bookkeeping is
1593
                    # introduced andconstraints are added to equation one
1594
                    # iteration later.
1595
                    last_decomm_p = np.nan
2✔
1596
                    # loop over invest periods (values are decomm_periods)
1597
                    for invest_p, decomm_p in enumerate(decomm_periods):
2✔
1598
                        # Add constraint of iteration before
1599
                        # (skipped in first iteration by last_decomm_p = nan)
1600
                        if (decomm_p != last_decomm_p) and (
2!
1601
                            last_decomm_p is not np.nan
1602
                        ):
NEW
UNCOV
1603
                            expr = self.old_end[n, last_decomm_p] == expr
×
NEW
1604
                            self.old_rule_end.add((n, last_decomm_p), expr)
×
1605

1606
                        # no decommissioning if decomm_p is zero
1607
                        if decomm_p == 0:
2!
1608
                            # overwrite decomm_p with zero to avoid
1609
                            # chaining invest periods in next iteration
1610
                            last_decomm_p = 0
2✔
1611

1612
                        # if decomm_p is the same as the last one chain invest
1613
                        # period
NEW
UNCOV
1614
                        elif decomm_p == last_decomm_p:
×
NEW
1615
                            expr += self.invest[n, invest_p]
×
1616
                            # overwrite decomm_p
NEW
UNCOV
1617
                            last_decomm_p = decomm_p
×
1618

1619
                        # if decomm_p is not zero, not the same as the last one
1620
                        # and it's not the first period
1621
                        else:
NEW
UNCOV
1622
                            expr = self.invest[n, invest_p]
×
1623
                            # overwrite decomm_p
NEW
UNCOV
1624
                            last_decomm_p = decomm_p
×
1625

1626
                    # Add constraint of very last iteration
1627
                    if last_decomm_p != 0:
2!
NEW
UNCOV
1628
                        expr = self.old_end[n, last_decomm_p] == expr
×
NEW
1629
                        self.old_rule_end.add((n, last_decomm_p), expr)
×
1630

1631
            self.old_rule_end = Constraint(
2✔
1632
                self.INVESTSTORAGES, m.PERIODS, noruleinit=True
1633
            )
1634

1635
            self.old_rule_end_build = BuildAction(
2✔
1636
                rule=_old_storage_capacity_rule_end
1637
            )
1638

1639
            def _old_storage_capacity_rule_exo(block):
2✔
1640
                """Rule definition for determining old exogenously given
1641
                capacity to be decommissioned due to reaching its lifetime
1642
                """
1643
                for n in self.INVESTSTORAGES:
2✔
1644
                    age = n.investment.age
2✔
1645
                    lifetime = n.investment.lifetime
2✔
1646
                    is_decommissioned = False
2✔
1647
                    for p in m.PERIODS:
2✔
1648
                        # No shutdown in first period
1649
                        if p == 0:
2!
1650
                            expr = self.old_exo[n, p] == 0
2✔
1651
                            self.old_rule_exo.add((n, p), expr)
2✔
NEW
UNCOV
1652
                        elif lifetime - age <= m.es.periods_years[p]:
×
1653
                            # Track decommissioning status
NEW
UNCOV
1654
                            if not is_decommissioned:
×
NEW
1655
                                expr = (
×
1656
                                    self.old_exo[n, p] == n.investment.existing
1657
                                )
NEW
UNCOV
1658
                                is_decommissioned = True
×
1659
                            else:
NEW
UNCOV
1660
                                expr = self.old_exo[n, p] == 0
×
NEW
1661
                            self.old_rule_exo.add((n, p), expr)
×
1662
                        else:
NEW
UNCOV
1663
                            expr = self.old_exo[n, p] == 0
×
NEW
1664
                            self.old_rule_exo.add((n, p), expr)
×
1665

1666
            self.old_rule_exo = Constraint(
2✔
1667
                self.INVESTSTORAGES, m.PERIODS, noruleinit=True
1668
            )
1669

1670
            self.old_rule_exo_build = BuildAction(
2✔
1671
                rule=_old_storage_capacity_rule_exo
1672
            )
1673

1674
            def _old_storage_capacity_rule(block):
2✔
1675
                """Rule definition for determining (overall) old capacity
1676
                to be decommissioned due to reaching its lifetime
1677
                """
1678
                for n in self.INVESTSTORAGES:
2✔
1679
                    for p in m.PERIODS:
2✔
1680
                        expr = (
2✔
1681
                            self.old[n, p]
1682
                            == self.old_end[n, p] + self.old_exo[n, p]
1683
                        )
1684
                        self.old_rule.add((n, p), expr)
2✔
1685

1686
            self.old_rule = Constraint(
2✔
1687
                self.INVESTSTORAGES, m.PERIODS, noruleinit=True
1688
            )
1689

1690
            self.old_rule_build = BuildAction(rule=_old_storage_capacity_rule)
2✔
1691

1692
            def _initially_empty_rule(_):
2✔
1693
                """Ensure storage to be empty initially"""
NEW
UNCOV
1694
                for n in self.INVESTSTORAGES:
×
NEW
1695
                    expr = self.storage_content[n, 0] == 0
×
NEW
1696
                    self.initially_empty.add((n, 0), expr)
×
1697

1698
            if not m.TSAM_MODE:
2!
1699
                # inter and intra initial storage contents are handled above
NEW
UNCOV
1700
                self.initially_empty = Constraint(
×
1701
                    self.INVESTSTORAGES, m.TIMESTEPS, noruleinit=True
1702
                )
1703

NEW
UNCOV
1704
                self.initially_empty_build = BuildAction(
×
1705
                    rule=_initially_empty_rule
1706
                )
1707

1708
        # Standard storage implementation for discrete time points
1709
        else:
1710

1711
            def _inv_storage_init_content_max_rule(block, n):
2✔
1712
                """Constraint for a variable initial storage capacity."""
1713
                if not m.TSAM_MODE:
2✔
1714
                    lhs = block.storage_content[n, 0]
2✔
1715
                else:
1716
                    lhs = block.intra_storage_delta[n, 0, 0, 0]
2✔
1717
                return lhs <= n.investment.existing + block.invest[n, 0]
2✔
1718

1719
            self.init_content_limit = Constraint(
2✔
1720
                self.INVESTSTORAGES_NO_INIT_CONTENT,
1721
                rule=_inv_storage_init_content_max_rule,
1722
            )
1723

1724
            def _inv_storage_init_content_fix_rule(block, n):
2✔
1725
                """Constraint for a fixed initial storage capacity."""
1726
                if not m.TSAM_MODE:
2!
1727
                    lhs = block.storage_content[n, 0]
2✔
1728
                else:
NEW
UNCOV
1729
                    lhs = block.intra_storage_delta[n, 0, 0, 0]
×
1730
                return lhs == n.initial_storage_level * (
2✔
1731
                    n.investment.existing + block.invest[n, 0]
1732
                )
1733

1734
            self.init_content_fix = Constraint(
2✔
1735
                self.INVESTSTORAGES_INIT_CONTENT,
1736
                rule=_inv_storage_init_content_fix_rule,
1737
            )
1738

1739
        def _storage_balance_rule(block, n, p, t):
2✔
1740
            """
1741
            Rule definition for the storage balance of every storage n and
1742
            every timestep.
1743
            """
1744
            expr = 0
2✔
1745
            expr += block.storage_content[n, t + 1]
2✔
1746
            expr += (
2✔
1747
                -block.storage_content[n, t]
1748
                * (1 - n.loss_rate[t]) ** m.timeincrement[t]
1749
            )
1750
            expr += (
2✔
1751
                n.fixed_losses_relative[t]
1752
                * self.total[n, p]
1753
                * m.timeincrement[t]
1754
            )
1755
            expr += n.fixed_losses_absolute[t] * m.timeincrement[t]
2✔
1756
            expr += (
2✔
1757
                -m.flow[i[n], n, t] * n.inflow_conversion_factor[t]
1758
            ) * m.timeincrement[t]
1759
            expr += (
2✔
1760
                m.flow[n, o[n], t] / n.outflow_conversion_factor[t]
1761
            ) * m.timeincrement[t]
1762
            return expr == 0
2✔
1763

1764
        def _intra_storage_balance_rule(block, n, p, k, g):
2✔
1765
            """
1766
            Rule definition for the storage balance of every storage n and
1767
            every timestep.
1768
            """
1769
            t = m.get_timestep_from_tsam_timestep(p, k, g)
2✔
1770
            expr = 0
2✔
1771
            expr += block.intra_storage_delta[n, p, k, g + 1]
2✔
1772
            expr += (
2✔
1773
                -block.intra_storage_delta[n, p, k, g]
1774
                * (1 - n.loss_rate[t]) ** m.timeincrement[t]
1775
            )
1776
            expr += (
2✔
1777
                n.fixed_losses_relative[t]
1778
                * self.total[n, p]
1779
                * m.timeincrement[t]
1780
            )
1781
            expr += n.fixed_losses_absolute[t] * m.timeincrement[t]
2✔
1782
            expr += (
2✔
1783
                -m.flow[i[n], n, t] * n.inflow_conversion_factor[t]
1784
            ) * m.timeincrement[t]
1785
            expr += (
2✔
1786
                m.flow[n, o[n], t] / n.outflow_conversion_factor[t]
1787
            ) * m.timeincrement[t]
1788
            return expr == 0
2✔
1789

1790
        if not m.TSAM_MODE:
2✔
1791
            self.balance = Constraint(
2✔
1792
                self.INVESTSTORAGES,
1793
                m.TIMEINDEX,
1794
                rule=_storage_balance_rule,
1795
            )
1796
        else:
1797
            self.intra_balance = Constraint(
2✔
1798
                self.INVESTSTORAGES,
1799
                m.TIMEINDEX_TYPICAL_CLUSTER,
1800
                rule=_intra_storage_balance_rule,
1801
            )
1802

1803
        def _inter_storage_balance_rule(block, n, i):
2✔
1804
            """
1805
            Rule definition for the storage balance of every storage n and
1806
            every timestep.
1807
            """
1808
            ii = 0
2✔
1809
            for p in m.PERIODS:
2!
1810
                ii += len(m.es.tsa_parameters[p]["order"])
2✔
1811
                if ii > i:
2!
1812
                    ii -= len(m.es.tsa_parameters[p]["order"])
2✔
1813
                    ii = i - ii
2✔
1814
                    break
2✔
1815

1816
            k = m.es.tsa_parameters[p]["order"][ii]
2✔
1817
            t = m.get_timestep_from_tsam_timestep(
2✔
1818
                p, k, m.es.tsa_parameters[p]["timesteps"] - 1
1819
            )
1820
            expr = 0
2✔
1821
            expr += block.inter_storage_content[n, i + 1]
2✔
1822
            expr += -block.inter_storage_content[n, i] * (
2✔
1823
                1 - n.loss_rate[t]
1824
            ) ** (m.timeincrement[t] * m.es.tsa_parameters[p]["timesteps"])
1825
            expr += -self.intra_storage_delta[
2✔
1826
                n, p, k, m.es.tsa_parameters[p]["timesteps"]
1827
            ]
1828
            return expr == 0
2✔
1829

1830
        if m.TSAM_MODE:
2✔
1831
            self.inter_balance = Constraint(
2✔
1832
                self.INVESTSTORAGES,
1833
                m.CLUSTERS,
1834
                rule=_inter_storage_balance_rule,
1835
            )
1836

1837
        if m.es.periods is None and not m.TSAM_MODE:
2✔
1838

1839
            def _balanced_storage_rule(block, n):
2✔
1840
                return (
2✔
1841
                    block.storage_content[n, m.TIMEPOINTS.at(-1)]
1842
                    == block.storage_content[n, m.TIMEPOINTS.at(1)]
1843
                )
1844

1845
            self.balanced_cstr = Constraint(
2✔
1846
                self.INVESTSTORAGES_BALANCED, rule=_balanced_storage_rule
1847
            )
1848

1849
        def _power_coupled(block):
2✔
1850
            """
1851
            Rule definition for constraint to connect the input power
1852
            and output power
1853
            """
1854
            for n in self.INVEST_REL_IN_OUT:
2!
NEW
UNCOV
1855
                for p in m.PERIODS:
×
NEW
1856
                    expr = (
×
1857
                        m.InvestmentFlowBlock.total[n, o[n], p]
1858
                    ) * n.invest_relation_input_output[p] == (
1859
                        m.InvestmentFlowBlock.total[i[n], n, p]
1860
                    )
NEW
UNCOV
1861
                    self.power_coupled.add((n, p), expr)
×
1862

1863
        self.power_coupled = Constraint(
2✔
1864
            self.INVEST_REL_IN_OUT, m.PERIODS, noruleinit=True
1865
        )
1866

1867
        self.power_coupled_build = BuildAction(rule=_power_coupled)
2✔
1868

1869
        def _storage_capacity_inflow_invest_rule(block):
2✔
1870
            """
1871
            Rule definition of constraint connecting the inflow
1872
            `InvestmentFlowBlock.invest of storage with invested capacity
1873
            `invest` by nominal_storage_capacity__inflow_ratio
1874
            """
1875
            for n in self.INVEST_REL_CAP_IN:
2✔
1876
                for p in m.PERIODS:
2✔
1877
                    expr = (
2✔
1878
                        m.InvestmentFlowBlock.total[i[n], n, p]
1879
                        == self.total[n, p]
1880
                        * n.invest_relation_input_capacity[p]
1881
                    )
1882
                    self.storage_capacity_inflow.add((n, p), expr)
2✔
1883

1884
        self.storage_capacity_inflow = Constraint(
2✔
1885
            self.INVEST_REL_CAP_IN, m.PERIODS, noruleinit=True
1886
        )
1887

1888
        self.storage_capacity_inflow_build = BuildAction(
2✔
1889
            rule=_storage_capacity_inflow_invest_rule
1890
        )
1891

1892
        def _storage_capacity_outflow_invest_rule(block):
2✔
1893
            """
1894
            Rule definition of constraint connecting outflow
1895
            `InvestmentFlowBlock.invest` of storage and invested capacity
1896
            `invest` by nominal_storage_capacity__outflow_ratio
1897
            """
1898
            for n in self.INVEST_REL_CAP_OUT:
2✔
1899
                for p in m.PERIODS:
2✔
1900
                    expr = (
2✔
1901
                        m.InvestmentFlowBlock.total[n, o[n], p]
1902
                        == self.total[n, p]
1903
                        * n.invest_relation_output_capacity[p]
1904
                    )
1905
                    self.storage_capacity_outflow.add((n, p), expr)
2✔
1906

1907
        self.storage_capacity_outflow = Constraint(
2✔
1908
            self.INVEST_REL_CAP_OUT, m.PERIODS, noruleinit=True
1909
        )
1910

1911
        self.storage_capacity_outflow_build = BuildAction(
2✔
1912
            rule=_storage_capacity_outflow_invest_rule
1913
        )
1914

1915
        self._add_storage_limit_constraints()
2✔
1916

1917
        def maximum_invest_limit(block, n, p):
2✔
1918
            """
1919
            Constraint for the maximal investment in non convex investment
1920
            storage.
1921
            """
1922
            return (
2✔
1923
                n.investment.maximum[p] * self.invest_status[n, p]
1924
                - self.invest[n, p]
1925
            ) >= 0
1926

1927
        self.limit_max = Constraint(
2✔
1928
            self.NON_CONVEX_INVESTSTORAGES,
1929
            m.PERIODS,
1930
            rule=maximum_invest_limit,
1931
        )
1932

1933
        def smallest_invest(block, n, p):
2✔
1934
            """
1935
            Constraint for the minimal investment in non convex investment
1936
            storage if the invest is greater than 0. So the invest variable
1937
            can be either 0 or greater than the minimum.
1938
            """
1939
            return (
2✔
1940
                self.invest[n, p]
1941
                - n.investment.minimum[p] * self.invest_status[n, p]
1942
                >= 0
1943
            )
1944

1945
        self.limit_min = Constraint(
2✔
1946
            self.NON_CONVEX_INVESTSTORAGES, m.PERIODS, rule=smallest_invest
1947
        )
1948

1949
        if m.es.periods is not None:
2✔
1950

1951
            def _overall_storage_maximum_investflow_rule(block):
2✔
1952
                """Rule definition for maximum overall investment
1953
                in investment case.
1954
                """
1955
                for n in self.OVERALL_MAXIMUM_INVESTSTORAGES:
2!
NEW
UNCOV
1956
                    for p in m.PERIODS:
×
NEW
1957
                        expr = self.total[n, p] <= n.investment.overall_maximum
×
NEW
1958
                        self.overall_storage_maximum.add((n, p), expr)
×
1959

1960
            self.overall_storage_maximum = Constraint(
2✔
1961
                self.OVERALL_MAXIMUM_INVESTSTORAGES, m.PERIODS, noruleinit=True
1962
            )
1963

1964
            self.overall_maximum_build = BuildAction(
2✔
1965
                rule=_overall_storage_maximum_investflow_rule
1966
            )
1967

1968
            def _overall_minimum_investflow_rule(block):
2✔
1969
                """Rule definition for minimum overall investment
1970
                in investment case.
1971

1972
                Note: This is only applicable for the last period
1973
                """
1974
                for n in self.OVERALL_MINIMUM_INVESTSTORAGES:
2!
NEW
UNCOV
1975
                    expr = (
×
1976
                        n.investment.overall_minimum
1977
                        <= self.total[n, m.PERIODS[-1]]
1978
                    )
NEW
UNCOV
1979
                    self.overall_minimum.add(n, expr)
×
1980

1981
            self.overall_minimum = Constraint(
2✔
1982
                self.OVERALL_MINIMUM_INVESTSTORAGES, noruleinit=True
1983
            )
1984

1985
            self.overall_minimum_build = BuildAction(
2✔
1986
                rule=_overall_minimum_investflow_rule
1987
            )
1988

1989
    def _add_storage_limit_constraints(self):
2✔
1990
        m = self.parent_block()
2✔
1991
        if not m.TSAM_MODE:
2✔
1992
            if m.es.periods is None:
2!
1993

1994
                def _max_storage_content_invest_rule(_, n, t):
2✔
1995
                    """
1996
                    Rule definition for upper bound constraint for the
1997
                    storage content.
1998
                    """
1999
                    expr = (
2✔
2000
                        self.storage_content[n, t]
2001
                        <= self.total[n, 0] * n.max_storage_level[t]
2002
                    )
2003
                    return expr
2✔
2004

2005
                self.max_storage_content = Constraint(
2✔
2006
                    self.INVESTSTORAGES,
2007
                    m.TIMEPOINTS,
2008
                    rule=_max_storage_content_invest_rule,
2009
                )
2010

2011
                def _min_storage_content_invest_rule(_, n, t):
2✔
2012
                    """
2013
                    Rule definition of lower bound constraint for the
2014
                    storage content.
2015
                    """
NEW
UNCOV
2016
                    expr = (
×
2017
                        self.storage_content[n, t]
2018
                        >= self.total[n, 0] * n.min_storage_level[t]
2019
                    )
NEW
UNCOV
2020
                    return expr
×
2021

2022
                self.min_storage_content = Constraint(
2✔
2023
                    self.MIN_INVESTSTORAGES,
2024
                    m.TIMEPOINTS,
2025
                    rule=_min_storage_content_invest_rule,
2026
                )
2027
            else:
2028

NEW
UNCOV
2029
                def _max_storage_content_invest_rule(_, n, p, t):
×
2030
                    """
2031
                    Rule definition for upper bound constraint for the
2032
                    storage content.
2033
                    """
NEW
UNCOV
2034
                    expr = (
×
2035
                        self.storage_content[n, t]
2036
                        <= self.total[n, p] * n.max_storage_level[t]
2037
                    )
NEW
UNCOV
2038
                    return expr
×
2039

NEW
UNCOV
2040
                self.max_storage_content = Constraint(
×
2041
                    self.INVESTSTORAGES,
2042
                    m.TIMEINDEX,
2043
                    rule=_max_storage_content_invest_rule,
2044
                )
2045

NEW
UNCOV
2046
                def _min_storage_content_invest_rule(_, n, p, t):
×
2047
                    """
2048
                    Rule definition of lower bound constraint for the
2049
                    storage content.
2050
                    """
NEW
UNCOV
2051
                    expr = (
×
2052
                        self.storage_content[n, t]
2053
                        >= self.total[n, p] * n.min_storage_level[t]
2054
                    )
NEW
UNCOV
2055
                    return expr
×
2056

NEW
UNCOV
2057
                self.min_storage_content = Constraint(
×
2058
                    self.MIN_INVESTSTORAGES,
2059
                    m.TIMEINDEX,
2060
                    rule=_min_storage_content_invest_rule,
2061
                )
2062
        else:
2063

2064
            def _storage_inter_maximum_level_rule(block):
2✔
2065
                for n in self.INVESTSTORAGES:
2✔
2066
                    for p, i, g in m.TIMEINDEX_CLUSTER:
2✔
2067
                        t = m.get_timestep_from_tsam_timestep(p, i, g)
2✔
2068
                        k = m.es.tsa_parameters[p]["order"][i]
2✔
2069
                        tk = m.get_timestep_from_tsam_timestep(p, k, g)
2✔
2070
                        inter_i = (
2✔
2071
                            sum(
2072
                                len(m.es.tsa_parameters[ip]["order"])
2073
                                for ip in range(p)
2074
                            )
2075
                            + i
2076
                        )
2077
                        lhs = (
2✔
2078
                            self.inter_storage_content[n, inter_i]
2079
                            * (1 - n.loss_rate[t]) ** (g * m.timeincrement[tk])
2080
                            + self.intra_storage_delta[n, p, k, g]
2081
                        )
2082
                        rhs = self.total[n, p] * n.max_storage_level[t]
2✔
2083
                        self.storage_inter_maximum_level.add(
2✔
2084
                            (n, p, i, g), lhs <= rhs
2085
                        )
2086

2087
            self.storage_inter_maximum_level = Constraint(
2✔
2088
                self.INVESTSTORAGES, m.TIMEINDEX_CLUSTER, noruleinit=True
2089
            )
2090

2091
            self.storage_inter_maximum_level_build = BuildAction(
2✔
2092
                rule=_storage_inter_maximum_level_rule
2093
            )
2094

2095
            def _storage_inter_minimum_level_rule(block):
2✔
2096
                # See FINE implementation at
2097
                # https://github.com/FZJ-IEK3-VSA/FINE/blob/
2098
                # 57ec32561fb95e746c505760bd0d61c97d2fd2fb/FINE/storage.py#L1329
2099
                for n in self.INVESTSTORAGES:
2✔
2100
                    for p, i, g in m.TIMEINDEX_CLUSTER:
2✔
2101
                        t = m.get_timestep_from_tsam_timestep(p, i, g)
2✔
2102
                        lhs = self.total[n, p] * n.min_storage_level[t]
2✔
2103
                        k = m.es.tsa_parameters[p]["order"][i]
2✔
2104
                        tk = m.get_timestep_from_tsam_timestep(p, k, g)
2✔
2105
                        inter_i = (
2✔
2106
                            sum(
2107
                                len(m.es.tsa_parameters[ip]["order"])
2108
                                for ip in range(p)
2109
                            )
2110
                            + i
2111
                        )
2112
                        rhs = (
2✔
2113
                            self.inter_storage_content[n, inter_i]
2114
                            * (1 - n.loss_rate[t]) ** (g * m.timeincrement[tk])
2115
                            + self.intra_storage_delta[n, p, k, g]
2116
                        )
2117
                        self.storage_inter_minimum_level.add(
2✔
2118
                            (n, p, i, g), lhs <= rhs
2119
                        )
2120

2121
            self.storage_inter_minimum_level = Constraint(
2✔
2122
                self.INVESTSTORAGES, m.TIMEINDEX_CLUSTER, noruleinit=True
2123
            )
2124

2125
            self.storage_inter_minimum_level_build = BuildAction(
2✔
2126
                rule=_storage_inter_minimum_level_rule
2127
            )
2128

2129
    def _objective_expression(self):
2✔
2130
        """Objective expression with fixed and investment costs."""
2131
        m = self.parent_block()
2✔
2132

2133
        investment_costs = 0
2✔
2134
        storage_costs = 0
2✔
2135
        period_investment_costs = {p: 0 for p in m.PERIODS}
2✔
2136
        fixed_costs = 0
2✔
2137

2138
        if m.es.periods is None:
2✔
2139
            for n in self.CONVEX_INVESTSTORAGES:
2✔
2140
                for p in m.PERIODS:
2✔
2141
                    investment_costs += (
2✔
2142
                        self.invest[n, p] * n.investment.ep_costs[p]
2143
                    )
2144
            for n in self.NON_CONVEX_INVESTSTORAGES:
2✔
2145
                for p in m.PERIODS:
2✔
2146
                    investment_costs += (
2✔
2147
                        self.invest[n, p] * n.investment.ep_costs[p]
2148
                        + self.invest_status[n, p] * n.investment.offset[p]
2149
                    )
2150

2151
        else:
2152
            msg = (
2✔
2153
                "You did not specify an interest rate.\n"
2154
                "It will be set equal to the discount_rate of {} "
2155
                "of the model as a default.\nThis corresponds to a "
2156
                "social planner point of view and does not reflect "
2157
                "microeconomic interest requirements."
2158
            )
2159
            for n in self.CONVEX_INVESTSTORAGES:
2✔
2160
                lifetime = n.investment.lifetime
2✔
2161
                interest = 0
2✔
2162
                if interest == 0:
2!
2163
                    warn(
2✔
2164
                        msg.format(m.discount_rate),
2165
                        debugging.SuspiciousUsageWarning,
2166
                    )
2167
                    interest = m.discount_rate
2✔
2168
                for p in m.PERIODS:
2✔
2169
                    annuity = economics.annuity(
2✔
2170
                        capex=n.investment.ep_costs[p],
2171
                        n=lifetime,
2172
                        wacc=interest,
2173
                    )
2174
                    duration = min(
2✔
2175
                        m.es.end_year_of_optimization - m.es.periods_years[p],
2176
                        lifetime,
2177
                    )
2178
                    present_value_factor = 1 / economics.annuity(
2✔
2179
                        capex=1, n=duration, wacc=interest
2180
                    )
2181
                    investment_costs_increment = (
2✔
2182
                        self.invest[n, p] * annuity * present_value_factor
2183
                    )
2184
                    remaining_value_difference = (
2✔
2185
                        self._evaluate_remaining_value_difference(
2186
                            m,
2187
                            p,
2188
                            n,
2189
                            m.es.end_year_of_optimization,
2190
                            lifetime,
2191
                            interest,
2192
                        )
2193
                    )
2194
                    investment_costs += (
2✔
2195
                        investment_costs_increment + remaining_value_difference
2196
                    )
2197
                    period_investment_costs[p] += investment_costs_increment
2✔
2198

2199
            for n in self.NON_CONVEX_INVESTSTORAGES:
2!
NEW
UNCOV
2200
                lifetime = n.investment.lifetime
×
NEW
2201
                interest = 0
×
NEW
2202
                if interest == 0:
×
NEW
2203
                    warn(
×
2204
                        msg.format(m.discount_rate),
2205
                        debugging.SuspiciousUsageWarning,
2206
                    )
NEW
UNCOV
2207
                    interest = m.discount_rate
×
NEW
2208
                for p in m.PERIODS:
×
NEW
2209
                    annuity = economics.annuity(
×
2210
                        capex=n.investment.ep_costs[p],
2211
                        n=lifetime,
2212
                        wacc=interest,
2213
                    )
NEW
UNCOV
2214
                    duration = min(
×
2215
                        m.es.end_year_of_optimization - m.es.periods_years[p],
2216
                        lifetime,
2217
                    )
NEW
UNCOV
2218
                    present_value_factor = 1 / economics.annuity(
×
2219
                        capex=1, n=duration, wacc=interest
2220
                    )
NEW
UNCOV
2221
                    investment_costs_increment = (
×
2222
                        self.invest[n, p] * annuity * present_value_factor
2223
                        + self.invest_status[n, p] * n.investment.offset[p]
2224
                    )
NEW
UNCOV
2225
                    remaining_value_difference = (
×
2226
                        self._evaluate_remaining_value_difference(
2227
                            m,
2228
                            p,
2229
                            n,
2230
                            m.es.end_year_of_optimization,
2231
                            lifetime,
2232
                            interest,
2233
                            nonconvex=True,
2234
                        )
2235
                    )
NEW
UNCOV
2236
                    investment_costs += (
×
2237
                        investment_costs_increment + remaining_value_difference
2238
                    )
NEW
UNCOV
2239
                    period_investment_costs[p] += investment_costs_increment
×
2240

2241
            for n in self.INVESTSTORAGES:
2✔
2242
                if valid_sequence(n.investment.fixed_costs, len(m.PERIODS)):
2!
NEW
UNCOV
2243
                    lifetime = n.investment.lifetime
×
NEW
2244
                    for p in m.PERIODS:
×
NEW
2245
                        range_limit = min(
×
2246
                            m.es.end_year_of_optimization,
2247
                            m.es.periods_years[p] + lifetime,
2248
                        )
NEW
UNCOV
2249
                        fixed_costs += sum(
×
2250
                            self.invest[n, p] * n.investment.fixed_costs[pp]
2251
                            for pp in range(
2252
                                m.es.periods_years[p],
2253
                                range_limit,
2254
                            )
2255
                        )
2256

2257
            for n in self.EXISTING_INVESTSTORAGES:
2✔
2258
                if valid_sequence(n.investment.fixed_costs, len(m.PERIODS)):
2!
NEW
UNCOV
2259
                    lifetime = n.investment.lifetime
×
NEW
2260
                    age = n.investment.age
×
NEW
2261
                    range_limit = min(
×
2262
                        m.es.end_year_of_optimization, lifetime - age
2263
                    )
NEW
UNCOV
2264
                    fixed_costs += sum(
×
2265
                        n.investment.existing * n.investment.fixed_costs[pp]
2266
                        for pp in range(range_limit)
2267
                    )
2268

2269
        for n in self.INVESTSTORAGES:
2✔
2270
            if valid_sequence(n.storage_costs, len(m.TIMESTEPS)):
2!
2271
                # We actually want to iterate over all TIMEPOINTS except the
2272
                # 0th. As integers are used for the index, this is equicalent
2273
                # to iterating over the TIMESTEPS with one offset.
NEW
UNCOV
2274
                if not m.TSAM_MODE:
×
NEW
2275
                    for t in m.TIMESTEPS:
×
NEW
2276
                        storage_costs += (
×
2277
                            self.storage_content[n, t + 1] * n.storage_costs[t]
2278
                        )
2279
                else:
NEW
UNCOV
2280
                    for t in m.TIMESTEPS_ORIGINAL:
×
NEW
2281
                        storage_costs += (
×
2282
                            self.storage_content[n, t + 1]
2283
                            * n.storage_costs[t + 1]
2284
                        )
2285

2286
        self.storage_costs = Expression(expr=storage_costs)
2✔
2287

2288
        self.investment_costs = Expression(expr=investment_costs)
2✔
2289
        self.period_investment_costs = period_investment_costs
2✔
2290
        self.fixed_costs = Expression(expr=fixed_costs)
2✔
2291
        self.costs = Expression(
2✔
2292
            expr=investment_costs + fixed_costs + storage_costs
2293
        )
2294

2295
        return self.costs
2✔
2296

2297
    def _evaluate_remaining_value_difference(
2✔
2298
        self,
2299
        m,
2300
        p,
2301
        n,
2302
        end_year_of_optimization,
2303
        lifetime,
2304
        interest,
2305
        nonconvex=False,
2306
    ):
2307
        """Evaluate and return the remaining value difference of an investment
2308

2309
        The remaining value difference in the net present values if the asset
2310
        was to be liquidated at the end of the optimization horizon and the
2311
        net present value using the original investment expenses.
2312

2313
        Parameters
2314
        ----------
2315
        m : oemof.solph.models.Model
2316
            Optimization model
2317

2318
        p : int
2319
            Period in which investment occurs
2320

2321
        n : oemof.solph.components.GenericStorage
2322
            storage unit
2323

2324
        end_year_of_optimization : int
2325
            Last year of the optimization horizon
2326

2327
        lifetime : int
2328
            lifetime of investment considered
2329

2330
        interest : float
2331
            Demanded interest rate for investment
2332

2333
        nonconvex : bool
2334
            Indicating whether considered flow is nonconvex.
2335
        """
2336
        if m.es.use_remaining_value:
2!
NEW
UNCOV
2337
            if end_year_of_optimization - m.es.periods_years[p] < lifetime:
×
NEW
2338
                remaining_lifetime = lifetime - (
×
2339
                    end_year_of_optimization - m.es.periods_years[p]
2340
                )
NEW
UNCOV
2341
                remaining_annuity = economics.annuity(
×
2342
                    capex=n.investment.ep_costs[-1],
2343
                    n=remaining_lifetime,
2344
                    wacc=interest,
2345
                )
NEW
UNCOV
2346
                original_annuity = economics.annuity(
×
2347
                    capex=n.investment.ep_costs[p],
2348
                    n=remaining_lifetime,
2349
                    wacc=interest,
2350
                )
NEW
UNCOV
2351
                present_value_factor_remaining = 1 / economics.annuity(
×
2352
                    capex=1, n=remaining_lifetime, wacc=interest
2353
                )
NEW
UNCOV
2354
                convex_investment_costs = (
×
2355
                    self.invest[n, p]
2356
                    * (remaining_annuity - original_annuity)
2357
                    * present_value_factor_remaining
2358
                )
NEW
UNCOV
2359
                if nonconvex:
×
NEW
2360
                    return convex_investment_costs + self.invest_status[
×
2361
                        n, p
2362
                    ] * (n.investment.offset[-1] - n.investment.offset[p])
2363
                else:
NEW
UNCOV
2364
                    return convex_investment_costs
×
2365
            else:
NEW
UNCOV
2366
                return 0
×
2367
        else:
2368
            return 0
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc