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

oemof / oemof-solph / 17377487432

01 Sep 2025 12:22PM UTC coverage: 81.765% (-5.8%) from 87.55%
17377487432

Pull #1174

github

web-flow
Merge 553ade8ee into 49e9272c9
Pull Request #1174: Feature/custom attributes for investments

925 of 1228 branches covered (75.33%)

Branch coverage included in aggregate %.

2 of 17 new or added lines in 2 files covered. (11.76%)

179 existing lines in 7 files now uncovered.

2707 of 3214 relevant lines covered (84.23%)

0.84 hits per line

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

81.77
/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

20
SPDX-License-Identifier: MIT
21

22
"""
23
import math
1✔
24
import numbers
1✔
25
from warnings import warn
1✔
26

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

40
from oemof.solph._helpers import check_node_object_for_missing_attribute
1✔
41
from oemof.solph._options import Investment
1✔
42
from oemof.solph._plumbing import sequence
1✔
43
from oemof.solph._plumbing import valid_sequence
1✔
44

45

46
class GenericStorage(Node):
1✔
47
    r"""
48
    Component `GenericStorage` to model with basic characteristics of storages.
49

50
    The GenericStorage is designed for one input and one output.
51

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

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

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

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

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

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

134
    >>> from oemof import solph
135

136
    >>> my_bus = solph.buses.Bus('my_bus')
137

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

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

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

214
        self.nominal_storage_capacity = None
1✔
215
        self.investment = None
1✔
216
        self._invest_group = False
1✔
217
        if isinstance(nominal_capacity, numbers.Real):
1✔
218
            self.nominal_storage_capacity = nominal_capacity
1✔
219
        elif isinstance(nominal_capacity, Investment):
1✔
220
            self.investment = nominal_capacity
1✔
221
            self._invest_group = True
1✔
222

223
        self.initial_storage_level = initial_storage_level
1✔
224
        self.balanced = balanced
1✔
225
        self.loss_rate = sequence(loss_rate)
1✔
226
        self.fixed_losses_relative = sequence(fixed_losses_relative)
1✔
227
        self.fixed_losses_absolute = sequence(fixed_losses_absolute)
1✔
228
        self.inflow_conversion_factor = sequence(inflow_conversion_factor)
1✔
229
        self.outflow_conversion_factor = sequence(outflow_conversion_factor)
1✔
230
        self.max_storage_level = sequence(max_storage_level)
1✔
231
        self.min_storage_level = sequence(min_storage_level)
1✔
232
        self.fixed_costs = sequence(fixed_costs)
1✔
233
        self.storage_costs = sequence(storage_costs)
1✔
234
        self.invest_relation_input_output = sequence(
1✔
235
            invest_relation_input_output
236
        )
237
        self.invest_relation_input_capacity = sequence(
1✔
238
            invest_relation_input_capacity
239
        )
240
        self.invest_relation_output_capacity = sequence(
1✔
241
            invest_relation_output_capacity
242
        )
243
        self.lifetime_inflow = lifetime_inflow
1✔
244
        self.lifetime_outflow = lifetime_outflow
1✔
245

246
        # Check number of flows.
247
        self._check_number_of_flows()
1✔
248
        # Check for infeasible parameter combinations
249
        self._check_infeasible_parameter_combinations()
1✔
250

251
        if self._invest_group:
1✔
252
            self._check_invest_attributes()
1✔
253

254
    def _set_flows(self):
1✔
255
        """Define inflow / outflow as investment flows when they are
256
        coupled with storage capacity via invest relations
257
        """
258
        for flow in self.inputs.values():
1✔
259
            if self.invest_relation_input_capacity[
1✔
260
                0
261
            ] is not None and not isinstance(flow.investment, Investment):
262
                flow.investment = Investment(lifetime=self.lifetime_inflow)
1✔
263
        for flow in self.outputs.values():
1✔
264
            if self.invest_relation_output_capacity[
1✔
265
                0
266
            ] is not None and not isinstance(flow.investment, Investment):
267
                flow.investment = Investment(lifetime=self.lifetime_outflow)
1✔
268

269
    def _check_invest_attributes(self):
1✔
270
        """Raise errors for infeasible investment attribute combinations"""
271
        if (
1✔
272
            self.invest_relation_input_output[0] is not None
273
            and self.invest_relation_output_capacity[0] is not None
274
            and self.invest_relation_input_capacity[0] is not None
275
        ):
276
            e2 = (
1✔
277
                "Overdetermined. Three investment object will be coupled"
278
                "with three constraints. Set one invest relation to 'None'."
279
            )
280
            raise AttributeError(e2)
1✔
281
        if (
1✔
282
            self.investment
283
            and self.fixed_losses_absolute.max() != 0
284
            and self.investment.existing == 0
285
            and self.investment.minimum.min() == 0
286
        ):
287
            e3 = (
1✔
288
                "With fixed_losses_absolute > 0, either investment.existing "
289
                "or investment.minimum has to be non-zero."
290
            )
291
            raise AttributeError(e3)
1✔
292

293
        self._set_flows()
1✔
294

295
    def _check_number_of_flows(self):
1✔
296
        """Ensure that there is only one inflow and outflow to the storage"""
297
        msg = "Only one {0} flow allowed in the GenericStorage {1}."
1✔
298
        check_node_object_for_missing_attribute(self, "inputs")
1✔
299
        check_node_object_for_missing_attribute(self, "outputs")
1✔
300
        if len(self.inputs) > 1:
1✔
301
            raise AttributeError(msg.format("input", self.label))
1✔
302
        if len(self.outputs) > 1:
1✔
303
            raise AttributeError(msg.format("output", self.label))
1✔
304

305
    def _check_infeasible_parameter_combinations(self):
1✔
306
        """Check for infeasible parameter combinations and raise error"""
307
        msg = (
1✔
308
            "initial_storage_level must be greater or equal to "
309
            "min_storage_level and smaller or equal to "
310
            "max_storage_level."
311
        )
312
        if self.initial_storage_level is not None:
1✔
313
            if (
1✔
314
                self.initial_storage_level < self.min_storage_level[0]
315
                or self.initial_storage_level > self.max_storage_level[0]
316
            ):
317
                raise ValueError(msg)
1✔
318

319
    def constraint_group(self):
1✔
320
        if self._invest_group is True:
1✔
321
            return GenericInvestmentStorageBlock
1✔
322
        else:
323
            return GenericStorageBlock
1✔
324

325

326
class GenericStorageBlock(ScalarBlock):
1✔
327
    r"""Storage without an :class:`.Investment` object.
328

329
    **The following sets are created:** (-> see basic sets at
330
    :class:`.Model` )
331

332
    STORAGES
333
        A set with all :py:class:`~.GenericStorage` objects, which do not have an
334
        :attr:`investment` of type :class:`.Investment`.
335

336
    STORAGES_BALANCED
337
        A set of  all :py:class:`~.GenericStorage` objects, with 'balanced' attribute set
338
        to True.
339

340
    STORAGES_WITH_INVEST_FLOW_REL
341
        A set with all :py:class:`~.GenericStorage` objects with two investment
342
        flows coupled with the 'invest_relation_input_output' attribute.
343

344
    **The following variables are created:**
345

346
    storage_content
347
        Storage content for every storage and timestep. The value for the
348
        storage content at the beginning is set by the parameter
349
        `initial_storage_level` or not set if `initial_storage_level` is None.
350
        The variable of storage s and timestep t can be accessed by:
351
        `om.GenericStorageBlock.storage_content[s, t]`
352

353
    intra_storage_delta
354
        Storage content for every storage and timestep of typical periods
355
        (only used in TSAM-mode). The variable of storage s and timestep t can
356
        be accessed by: `om.GenericStorageBlock.intra_storage_delta[s, k, t]`
357

358
    **The following constraints are created:**
359

360
    Set storage_content of last time step to one at t=0 if balanced == True
361
        .. math::
362
            E(t_{last}) = E(-1)
363

364
    Storage losses :attr:`om.Storage.losses[n, t]`
365
        .. math:: E_{loss}(t) = &E(t-1) \cdot
366
            1 - (1 - \beta(t))^{\tau(t)/(t_u)} \\
367
            &- \gamma(t)\cdot E_{nom} \cdot {\tau(t)/(t_u)}\\
368
            &- \delta(t) \cdot {\tau(t)/(t_u)}
369

370
    Storage balance :attr:`om.Storage.balance[n, t]`
371
        .. math:: E(t) = &E(t-1) - E_{loss}(t)\\
372
            &- \frac{\dot{E}_o(p, t)}{\eta_o(t)} \cdot \tau(t)\\
373
            &+ \dot{E}_i(p, t) \cdot \eta_i(t) \cdot \tau(t)
374

375
    Connect the invest variables of the input and the output flow.
376
        .. math::
377
          InvestmentFlowBlock.invest(source(n), n, p) + existing = \\
378
          (InvestmentFlowBlock.invest(n, target(n), p) + existing) \\
379
          * invest\_relation\_input\_output(n) \\
380
          \forall n \in \textrm{INVEST\_REL\_IN\_OUT} \\
381
          \forall p \in \textrm{PERIODS}
382

383

384

385
    =========================== ======================= =========
386
    symbol                      explanation             attribute
387
    =========================== ======================= =========
388
    :math:`E(t)`                energy currently stored `storage_content`
389
    :math:`E_{nom}`             nominal capacity of     `nominal_storage_capacity`
390
                                the energy storage
391
    :math:`c(-1)`               state before            `initial_storage_level`
392
                                initial time step
393
    :math:`c_{min}(t)`          minimum allowed storage `min_storage_level[t]`
394
    :math:`c_{max}(t)`          maximum allowed storage `max_storage_level[t]`
395
    :math:`\beta(t)`            fraction of lost energy `loss_rate[t]`
396
                                as share of
397
                                :math:`E(t)` per hour
398
    :math:`\gamma(t)`           fixed loss of energy    `fixed_losses_relative[t]`
399
                                per hour relative to
400
                                :math:`E_{nom}`
401
    :math:`\delta(t)`           absolute fixed loss     `fixed_losses_absolute[t]`
402
                                of energy per hour
403
    :math:`\dot{E}_i(t)`        energy flowing in       `inputs`
404
    :math:`\dot{E}_o(t)`        energy flowing out      `outputs`
405
    :math:`\eta_i(t)`           conversion factor       `inflow_conversion_factor[t]`
406
                                (i.e. efficiency)
407
                                when storing energy
408
    :math:`\eta_o(t)`           conversion factor when  `outflow_conversion_factor[t]`
409
                                (i.e. efficiency)
410
                                taking stored energy
411
    :math:`\tau(t)`             duration of time step
412
    :math:`t_u`                 time unit of losses
413
                                :math:`\beta(t)`,
414
                                :math:`\gamma(t)`
415
                                :math:`\delta(t)` and
416
                                timeincrement
417
                                :math:`\tau(t)`
418
    :math:`c_{storage}(t)`      costs of having         `storage_costs`
419
                                energy stored
420
    =========================== ======================= =========
421

422
    **The following parts of the objective function are created:**
423

424
    * :attr: `storage_costs` not 0
425

426
        .. math::
427
            \sum_{t \in \textrm{TIMEPOINTS} > 0} c_{storage}(t) \cdot E(t)
428

429
    * :attr:`fixed_costs` not 0
430

431
        .. math::
432
            \displaystyle \sum_{pp=0}^{year_{max}} E_{nom}
433
            \cdot c_{fixed}(pp)
434

435
    where :math:`year_{max}` denotes the last year of the optimization
436
      horizon, i.e. at the end of the last period.
437

438
    """  # noqa: E501
439

440
    CONSTRAINT_GROUP = True
1✔
441

442
    def __init__(self, *args, **kwargs):
1✔
443
        super().__init__(*args, **kwargs)
1✔
444

445
    def _create(self, group=None):
1✔
446
        """
447
        Parameters
448
        ----------
449
        group : list
450
            List containing storage objects.
451
            e.g. groups=[storage1, storage2,..]
452
        """
453
        m = self.parent_block()
1✔
454

455
        if group is None:
1!
UNCOV
456
            return None
×
457

458
        i = {n: [i for i in n.inputs][0] for n in group}
1✔
459
        o = {n: [o for o in n.outputs][0] for n in group}
1✔
460

461
        #  ************* SETS *********************************
462

463
        self.STORAGES = Set(initialize=[n for n in group])
1✔
464

465
        self.STORAGES_BALANCED = Set(
1✔
466
            initialize=[n for n in group if n.balanced is True]
467
        )
468

469
        self.STORAGES_INITITAL_LEVEL = Set(
1✔
470
            initialize=[
471
                n for n in group if n.initial_storage_level is not None
472
            ]
473
        )
474

475
        self.STORAGES_WITH_INVEST_FLOW_REL = Set(
1✔
476
            initialize=[
477
                n
478
                for n in group
479
                if n.invest_relation_input_output[0] is not None
480
            ]
481
        )
482

483
        #  ************* VARIABLES *****************************
484

485
        def _storage_content_bound_rule(block, n, t):
1✔
486
            """
487
            Rule definition for bounds of storage_content variable of
488
            storage n in timestep t.
489
            """
490
            bounds = (
1✔
491
                n.nominal_storage_capacity * n.min_storage_level[t],
492
                n.nominal_storage_capacity * n.max_storage_level[t],
493
            )
494
            return bounds
1✔
495

496
        if not m.TSAM_MODE:
1✔
497
            self.storage_content = Var(
1✔
498
                self.STORAGES, m.TIMEPOINTS, bounds=_storage_content_bound_rule
499
            )
500

501
            self.storage_losses = Var(self.STORAGES, m.TIMESTEPS)
1✔
502

503
            # set the initial storage content
504
            # ToDo: More elegant code possible?
505
            for n in group:
1✔
506
                if n.initial_storage_level is not None:
1✔
507
                    self.storage_content[n, 0] = (
1✔
508
                        n.initial_storage_level * n.nominal_storage_capacity
509
                    )
510
                    self.storage_content[n, 0].fix()
1✔
511
        else:
512
            # called "inter" in https://doi.org/10.1016/j.apenergy.2018.01.023
513
            self.inter_storage_content = Var(
1✔
514
                self.STORAGES, m.CLUSTERS_OFFSET, within=NonNegativeReals
515
            )
516
            # called "intra" in https://doi.org/10.1016/j.apenergy.2018.01.023
517
            self.intra_storage_delta = Var(
1✔
518
                self.STORAGES, m.TIMEINDEX_TYPICAL_CLUSTER_OFFSET
519
            )
520
            # set the initial intra storage content
521
            # first timestep in intra storage is always zero
522
            for n in group:
1✔
523
                for p, k in m.TYPICAL_CLUSTERS:
1✔
524
                    self.intra_storage_delta[n, p, k, 0] = 0
1✔
525
                    self.intra_storage_delta[n, p, k, 0].fix()
1✔
526
                if n.initial_storage_level is not None:
1!
527
                    self.inter_storage_content[n, 0] = (
×
528
                        n.initial_storage_level * n.nominal_storage_capacity
529
                    )
UNCOV
530
                    self.inter_storage_content[n, 0].fix()
×
531
        #  ************* Constraints ***************************
532

533
        def _storage_inter_minimum_level_rule(block):
1✔
534
            # See FINE implementation at
535
            # https://github.com/FZJ-IEK3-VSA/FINE/blob/
536
            # 57ec32561fb95e746c505760bd0d61c97d2fd2fb/FINE/storage.py#L1329
537
            for n in self.STORAGES:
1✔
538
                for p, i, g in m.TIMEINDEX_CLUSTER:
1✔
539
                    t = m.get_timestep_from_tsam_timestep(p, i, g)
1✔
540
                    lhs = n.nominal_storage_capacity * n.min_storage_level[t]
1✔
541
                    k = m.es.tsa_parameters[p]["order"][i]
1✔
542
                    tk = m.get_timestep_from_tsam_timestep(p, k, g)
1✔
543
                    inter_i = (
1✔
544
                        sum(
545
                            len(m.es.tsa_parameters[ip]["order"])
546
                            for ip in range(p)
547
                        )
548
                        + i
549
                    )
550
                    rhs = (
1✔
551
                        self.inter_storage_content[n, inter_i]
552
                        * (1 - n.loss_rate[t]) ** (g * m.timeincrement[tk])
553
                        + self.intra_storage_delta[n, p, k, g]
554
                    )
555
                    self.storage_inter_minimum_level.add(
1✔
556
                        (n, p, i, g), lhs <= rhs
557
                    )
558

559
        if m.TSAM_MODE:
1✔
560
            self.storage_inter_minimum_level = Constraint(
1✔
561
                self.STORAGES, m.TIMEINDEX_CLUSTER, noruleinit=True
562
            )
563

564
            self.storage_inter_minimum_level_build = BuildAction(
1✔
565
                rule=_storage_inter_minimum_level_rule
566
            )
567

568
        def _storage_inter_maximum_level_rule(block):
1✔
569
            for n in self.STORAGES:
1✔
570
                for p, i, g in m.TIMEINDEX_CLUSTER:
1✔
571
                    t = m.get_timestep_from_tsam_timestep(p, i, g)
1✔
572
                    k = m.es.tsa_parameters[p]["order"][i]
1✔
573
                    tk = m.get_timestep_from_tsam_timestep(p, k, g)
1✔
574
                    inter_i = (
1✔
575
                        sum(
576
                            len(m.es.tsa_parameters[ip]["order"])
577
                            for ip in range(p)
578
                        )
579
                        + i
580
                    )
581
                    lhs = (
1✔
582
                        self.inter_storage_content[n, inter_i]
583
                        * (1 - n.loss_rate[t]) ** (g * m.timeincrement[tk])
584
                        + self.intra_storage_delta[n, p, k, g]
585
                    )
586
                    rhs = n.nominal_storage_capacity * n.max_storage_level[t]
1✔
587
                    self.storage_inter_maximum_level.add(
1✔
588
                        (n, p, i, g), lhs <= rhs
589
                    )
590

591
        if m.TSAM_MODE:
1✔
592
            self.storage_inter_maximum_level = Constraint(
1✔
593
                self.STORAGES, m.TIMEINDEX_CLUSTER, noruleinit=True
594
            )
595

596
            self.storage_inter_maximum_level_build = BuildAction(
1✔
597
                rule=_storage_inter_maximum_level_rule
598
            )
599

600
        def _storage_losses_rule(block, n, t):
1✔
601
            expr = block.storage_content[n, t] * (
1✔
602
                1 - (1 - n.loss_rate[t]) ** m.timeincrement[t]
603
            )
604
            expr += (
1✔
605
                n.fixed_losses_relative[t]
606
                * n.nominal_storage_capacity
607
                * m.timeincrement[t]
608
            )
609
            expr += n.fixed_losses_absolute[t] * m.timeincrement[t]
1✔
610

611
            return expr == block.storage_losses[n, t]
1✔
612

613
        if not m.TSAM_MODE:
1✔
614
            self.losses = Constraint(
1✔
615
                self.STORAGES, m.TIMESTEPS, rule=_storage_losses_rule
616
            )
617

618
        def _storage_balance_rule(block, n, t):
1✔
619
            """
620
            Rule definition for the storage balance of every storage n and
621
            every timestep.
622
            """
623
            expr = block.storage_content[n, t]
1✔
624
            expr -= block.storage_losses[n, t]
1✔
625
            expr += (
1✔
626
                m.flow[i[n], n, t] * n.inflow_conversion_factor[t]
627
            ) * m.timeincrement[t]
628
            expr -= (
1✔
629
                m.flow[n, o[n], t] / n.outflow_conversion_factor[t]
630
            ) * m.timeincrement[t]
631
            return expr == block.storage_content[n, t + 1]
1✔
632

633
        def _intra_storage_balance_rule(block, n, p, k, g):
1✔
634
            """
635
            Rule definition for the storage balance of every storage n and
636
            every timestep.
637
            """
638
            t = m.get_timestep_from_tsam_timestep(p, k, g)
1✔
639
            expr = 0
1✔
640
            expr += block.intra_storage_delta[n, p, k, g + 1]
1✔
641
            expr += (
1✔
642
                -block.intra_storage_delta[n, p, k, g]
643
                * (1 - n.loss_rate[t]) ** m.timeincrement[t]
644
            )
645
            expr += (
1✔
646
                n.fixed_losses_relative[t]
647
                * n.nominal_storage_capacity
648
                * m.timeincrement[t]
649
            )
650
            expr += n.fixed_losses_absolute[t] * m.timeincrement[t]
1✔
651
            expr += (
1✔
652
                -m.flow[i[n], n, t] * n.inflow_conversion_factor[t]
653
            ) * m.timeincrement[t]
654
            expr += (
1✔
655
                m.flow[n, o[n], t] / n.outflow_conversion_factor[t]
656
            ) * m.timeincrement[t]
657
            return expr == 0
1✔
658

659
        if not m.TSAM_MODE:
1✔
660
            self.balance = Constraint(
1✔
661
                self.STORAGES, m.TIMESTEPS, rule=_storage_balance_rule
662
            )
663
        else:
664
            self.intra_balance = Constraint(
1✔
665
                self.STORAGES,
666
                m.TIMEINDEX_TYPICAL_CLUSTER,
667
                rule=_intra_storage_balance_rule,
668
            )
669

670
        def _inter_storage_balance_rule(block, n, i):
1✔
671
            """
672
            Rule definition for the storage balance of every storage n and
673
            every timestep.
674
            """
675
            ii = 0
1✔
676
            for p in m.PERIODS:
1!
677
                ii += len(m.es.tsa_parameters[p]["order"])
1✔
678
                if ii > i:
1!
679
                    ii -= len(m.es.tsa_parameters[p]["order"])
1✔
680
                    ii = i - ii
1✔
681
                    break
1✔
682

683
            k = m.es.tsa_parameters[p]["order"][ii]
1✔
684

685
            # Calculate inter losses over whole typical period
686
            t0 = m.get_timestep_from_tsam_timestep(p, k, 0)
1✔
687
            losses = math.prod(
1✔
688
                (
689
                    (1 - n.loss_rate[t0 + s])
690
                    ** m.es.tsa_parameters[p]["segments"][(k, s)]
691
                    if "segments" in m.es.tsa_parameters[p]
692
                    else 1 - n.loss_rate[t0 + s]
693
                )
694
                for s in range(m.es.tsa_parameters[p]["timesteps"])
695
            )
696

697
            expr = 0
1✔
698
            expr += block.inter_storage_content[n, i + 1]
1✔
699
            expr += -block.inter_storage_content[n, i] * losses
1✔
700
            expr += -self.intra_storage_delta[
1✔
701
                n, p, k, m.es.tsa_parameters[p]["timesteps"]
702
            ]
703
            return expr == 0
1✔
704

705
        if m.TSAM_MODE:
1✔
706
            self.inter_balance = Constraint(
1✔
707
                self.STORAGES,
708
                m.CLUSTERS,
709
                rule=_inter_storage_balance_rule,
710
            )
711

712
        def _balanced_storage_rule(block, n):
1✔
713
            """
714
            Storage content of last time step == initial storage content
715
            if balanced.
716
            """
717
            return (
1✔
718
                block.storage_content[n, m.TIMEPOINTS.at(-1)]
719
                == block.storage_content[n, m.TIMEPOINTS.at(1)]
720
            )
721

722
        def _balanced_inter_storage_rule(block, n):
1✔
723
            """
724
            Storage content of last time step == initial storage content
725
            if balanced.
726
            """
727
            return (
1✔
728
                block.inter_storage_content[n, m.CLUSTERS_OFFSET.at(-1)]
729
                == block.inter_storage_content[n, m.CLUSTERS_OFFSET.at(1)]
730
            )
731

732
        if not m.TSAM_MODE:
1✔
733
            self.balanced_cstr = Constraint(
1✔
734
                self.STORAGES_BALANCED, rule=_balanced_storage_rule
735
            )
736
        else:
737
            self.balanced_cstr = Constraint(
1✔
738
                self.STORAGES_BALANCED, rule=_balanced_inter_storage_rule
739
            )
740

741
        def _power_coupled(_):
1✔
742
            """
743
            Rule definition for constraint to connect the input power
744
            and output power
745
            """
746
            for n in self.STORAGES_WITH_INVEST_FLOW_REL:
1✔
747
                for p in m.PERIODS:
1✔
748
                    expr = (
1✔
749
                        m.InvestmentFlowBlock.total[n, o[n], p]
750
                    ) * n.invest_relation_input_output[p] == (
751
                        m.InvestmentFlowBlock.total[i[n], n, p]
752
                    )
753
                    self.power_coupled.add((n, p), expr)
1✔
754

755
        self.power_coupled = Constraint(
1✔
756
            self.STORAGES_WITH_INVEST_FLOW_REL, m.PERIODS, noruleinit=True
757
        )
758

759
        self.power_coupled_build = BuildAction(rule=_power_coupled)
1✔
760

761
    def _objective_expression(self):
1✔
762
        r"""
763
        Objective expression for storages with no investment.
764

765
        * Fixed costs (will not have an impact on the actual optimisation).
766
        * Variable costs for storage content.
767
        """
768
        m = self.parent_block()
1✔
769

770
        fixed_costs = 0
1✔
771

772
        for n in self.STORAGES:
1✔
773
            if valid_sequence(n.fixed_costs, len(m.PERIODS)):
1!
774
                fixed_costs += sum(
1✔
775
                    n.nominal_storage_capacity * n.fixed_costs[pp]
776
                    for pp in range(m.es.end_year_of_optimization)
777
                )
778
        self.fixed_costs = Expression(expr=fixed_costs)
1✔
779

780
        storage_costs = 0
1✔
781

782
        for n in self.STORAGES:
1✔
783
            if valid_sequence(n.storage_costs, len(m.TIMESTEPS)):
1✔
784
                # We actually want to iterate over all TIMEPOINTS except the
785
                # 0th. As integers are used for the index, this is equicalent
786
                # to iterating over the TIMESTEPS with one offset.
787
                if not m.TSAM_MODE:
1!
788
                    for t in m.TIMESTEPS:
1✔
789
                        storage_costs += (
1✔
790
                            self.storage_content[n, t + 1] * n.storage_costs[t]
791
                        )
792
                else:
UNCOV
793
                    for t in m.TIMESTEPS_ORIGINAL:
×
UNCOV
794
                        storage_costs += (
×
795
                            self.storage_content[n, t + 1]
796
                            * n.storage_costs[t + 1]
797
                        )
798

799
        self.storage_costs = Expression(expr=storage_costs)
1✔
800
        self.costs = Expression(expr=storage_costs + fixed_costs)
1✔
801

802
        return self.costs
1✔
803

804

805
class GenericInvestmentStorageBlock(ScalarBlock):
1✔
806
    r"""
807
    Block for all storages with :attr:`Investment` being not None.
808
    See :class:`.Investment` for all parameters of the
809
    Investment class.
810

811
    **Variables**
812

813
    All Storages are indexed by :math:`n` (denoting the respective storage
814
    unit), which is omitted in the following for the sake of convenience.
815
    The following variables are created as attributes of
816
    :attr:`om.GenericInvestmentStorageBlock`:
817

818
    * :math:`P_i(p, t)`
819

820
        Inflow of the storage
821
        (created in :class:`oemof.solph.models.Model`).
822

823
    * :math:`P_o(p, t)`
824

825
        Outflow of the storage
826
        (created in :class:`oemof.solph.models.Model`).
827

828
    * :math:`E(t)`
829

830
        Current storage content (Absolute level of stored energy).
831

832
    * :math:`E_{invest}(p)`
833

834
        Invested (nominal) capacity of the storage in period p.
835

836
    * :math:`E_{total}(p)`
837

838
        Total installed (nominal) capacity of the storage in period p.
839

840
    * :math:`E_{old}(p)`
841

842
        Old (nominal) capacity of the storage to be decommissioned in period p.
843

844
    * :math:`E_{old,exo}(p)`
845

846
        Exogenous old (nominal) capacity of the storage to be decommissioned
847
        in period p; existing capacity reaching its lifetime.
848

849
    * :math:`E_{old,endo}(p)`
850

851
        Endogenous old (nominal) capacity of the storage to be decommissioned
852
        in period p; endgenous investments reaching their lifetime.
853

854
    * :math:`E(-1)`
855

856
        Initial storage content (before timestep 0).
857
        Not applicable for a multi-period model.
858

859
    * :math:`b_{invest}(p)`
860

861
        Binary variable for the status of the investment, if
862
        :attr:`nonconvex` is `True`.
863

864
    **Constraints**
865

866
    The following constraints are created for all investment storages:
867

868
        Storage balance (Same as for :class:`.GenericStorageBlock`)
869

870
        .. math:: E(t) = &E(t-1) \cdot
871
            (1 - \beta(t)) ^{\tau(t)/(t_u)} \\
872
            &- \gamma(t)\cdot (E_{total}(p)) \cdot {\tau(t)/(t_u)}\\
873
            &- \delta(t) \cdot {\tau(t)/(t_u)}\\
874
            &- \frac{\dot{E}_o(p, t))}{\eta_o(t)} \cdot \tau(t)
875
            + \dot{E}_i(p, t) \cdot \eta_i(t) \cdot \tau(t)
876

877
        Total storage capacity (p > 0 for multi-period model only)
878

879
        .. math::
880
            &
881
            if \quad p=0:\\
882
            &
883
            E_{total}(p) = E_{exist} + E_{invest}(p)\\
884
            &\\
885
            &
886
            else:\\
887
            &
888
            E_{total}(p) = E_{total}(p-1) + E_{invest}(p) - E_{old}(p)\\
889
            &\\
890
            &
891
            \forall p \in \textrm{PERIODS}
892

893
        Old storage capacity (p > 0 for multi-period model only)
894

895
        .. math::
896
            &
897
            E_{old}(p) = E_{old,exo}(p) + E_{old,end}(p)\\
898
            &\\
899
            &
900
            if \quad p=0:\\
901
            &
902
            E_{old,end}(p) = 0\\
903
            &\\
904
            &
905
            else \quad if \quad l \leq year(p):\\
906
            &
907
            E_{old,end}(p) = E_{invest}(p_{comm})\\
908
            &\\
909
            &
910
            else:\\
911
            &
912
            E_{old,end}(p)\\
913
            &\\
914
            &
915
            if \quad p=0:\\
916
            &
917
            E_{old,exo}(p) = 0\\
918
            &\\
919
            &
920
            else \quad if \quad l - a \leq year(p):\\
921
            &
922
            E_{old,exo}(p) = E_{exist} (*)\\
923
            &\\
924
            &
925
            else:\\
926
            &
927
            E_{old,exo}(p) = 0\\
928
            &\\
929
            &
930
            \forall p \in \textrm{PERIODS}
931

932
        where:
933

934
        * (*) is only performed for the first period the condition is True.
935
          A decommissioning flag is then set to True to prevent having falsely
936
          added old capacity in future periods.
937
        * :math:`year(p)` is the year corresponding to period p
938
        * :math:`p_{comm}` is the commissioning period of the storage
939

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

943
        * :attr:`nonconvex = False`
944

945
        .. math::
946
            &
947
            E_{invest, min}(p) \le E_{invest}(p) \le E_{invest, max}(p) \\
948
            &
949
            \forall p \in \textrm{PERIODS}
950

951
        * :attr:`nonconvex = True`
952

953
        .. math::
954
            &
955
            E_{invest, min}(p) \cdot b_{invest}(p) \le E_{invest}(p)\\
956
            &
957
            E_{invest}(p) \le E_{invest, max}(p) \cdot b_{invest}(p)\\
958
            &
959
            \forall p \in \textrm{PERIODS}
960

961
    The following constraints are created depending on the attributes of
962
    the :class:`.GenericStorage`:
963

964
        * :attr:`initial_storage_level is None`;
965
          not applicable for multi-period model
966

967
            Constraint for a variable initial storage content:
968

969
        .. math::
970
               E(-1) \le E_{exist} + E_{invest}(0)
971

972
        * :attr:`initial_storage_level is not None`;
973
          not applicable for multi-period model
974

975
            An initial value for the storage content is given:
976

977
        .. math::
978
               E(-1) = (E_{invest}(0) + E_{exist}) \cdot c(-1)
979

980
        * :attr:`balanced=True`;
981
          not applicable for multi-period model
982

983
            The energy content of storage of the first and the last timestep
984
            are set equal:
985

986
        .. math::
987
            E(-1) = E(t_{last})
988

989
        * :attr:`invest_relation_input_capacity is not None`
990

991
            Connect the invest variables of the storage and the input flow:
992

993
        .. math::
994
            &
995
            P_{i,total}(p) =
996
            E_{total}(p) \cdot r_{cap,in} \\
997
            &
998
            \forall p \in \textrm{PERIODS}
999

1000
        * :attr:`invest_relation_output_capacity is not None`
1001

1002
            Connect the invest variables of the storage and the output flow:
1003

1004
        .. math::
1005
            &
1006
            P_{o,total}(p) =
1007
            E_{total}(p) \cdot r_{cap,out}\\
1008
            &
1009
            \forall p \in \textrm{PERIODS}
1010

1011
        * :attr:`invest_relation_input_output is not None`
1012

1013
            Connect the invest variables of the input and the output flow:
1014

1015
        .. math::
1016
            &
1017
            P_{i,total}(p) =
1018
            P_{o,total}(p) \cdot r_{in,out}\\
1019
            &
1020
            \forall p \in \textrm{PERIODS}
1021

1022
        * :attr:`max_storage_level`
1023

1024
            Rule for upper bound constraint for the storage content:
1025

1026
        .. math::
1027
            &
1028
            E(t) \leq E_{total}(p) \cdot c_{max}(t)\\
1029
            &
1030
            \forall p, t \in \textrm{TIMEINDEX}
1031

1032
        * :attr:`min_storage_level`
1033

1034
            Rule for lower bound constraint for the storage content:
1035

1036
        .. math::
1037
            &
1038
            E(t) \geq E_{total}(p) \cdot c_{min}(t)\\
1039
            &
1040
            \forall p, t \in \textrm{TIMEINDEX}
1041

1042

1043
    **Objective function**
1044

1045
    Objective terms for a standard model and a multi-period model differ
1046
    quite strongly. Besides, the part of the objective function added by the
1047
    investment storages also depends on whether a convex or nonconvex
1048
    investment option is selected. The following parts of the objective
1049
    function are created:
1050

1051
    *Standard model*
1052

1053
        * :attr:`nonconvex = False`
1054

1055
            .. math::
1056
                E_{invest}(0) \cdot c_{invest,var}(0)
1057

1058
        * :attr:`nonconvex = True`
1059

1060
            .. math::
1061
                E_{invest}(0) \cdot c_{invest,var}(0)
1062
                + c_{invest,fix}(0) \cdot b_{invest}(0)\\
1063

1064
    Where 0 denotes the 0th (investment) period since
1065
    in a standard model, there is only this one period.
1066

1067
    *Multi-period model*
1068

1069
        * :attr:`nonconvex = False`
1070

1071
            .. math::
1072
                &
1073
                E_{invest}(p) \cdot A(c_{invest,var}(p), l, ir)
1074
                \cdot \frac {1}{ANF(d, ir)} \cdot DF^{-p}\\
1075
                &
1076
                \forall p \in \textrm{PERIODS}
1077

1078
        In case, the remaining lifetime of a storage is greater than 0 and
1079
        attribute `use_remaining_value` of the energy system is True,
1080
        the difference in value for the investment period compared to the
1081
        last period of the optimization horizon is accounted for
1082
        as an adder to the investment costs:
1083

1084
            .. math::
1085
                &
1086
                E_{invest}(p) \cdot (A(c_{invest,var}(p), l_{r}, ir) -
1087
                A(c_{invest,var}(|P|), l_{r}, ir)\\
1088
                & \cdot \frac {1}{ANF(l_{r}, ir)} \cdot DF^{-|P|}\\
1089
                &\\
1090
                &
1091
                \forall p \in \textrm{PERIODS}
1092

1093
        * :attr:`nonconvex = True`
1094

1095
            .. math::
1096
                &
1097
                (E_{invest}(p) \cdot A(c_{invest,var}(p), l, ir)
1098
                \cdot \frac {1}{ANF(d, ir)}\\
1099
                &
1100
                +  c_{invest,fix}(p) \cdot b_{invest}(p)) \cdot DF^{-p} \\
1101
                &
1102
                \forall p \in \textrm{PERIODS}
1103

1104
        In case, the remaining lifetime of a storage is greater than 0 and
1105
        attribute `use_remaining_value` of the energy system is True,
1106
        the difference in value for the investment period compared to the
1107
        last period of the optimization horizon is accounted for
1108
        as an adder to the investment costs:
1109

1110
            .. math::
1111
                &
1112
                (E_{invest}(p) \cdot (A(c_{invest,var}(p), l_{r}, ir) -
1113
                A(c_{invest,var}(|P|), l_{r}, ir)\\
1114
                & \cdot \frac {1}{ANF(l_{r}, ir)} \cdot DF^{-|P|}\\
1115
                &
1116
                +  (c_{invest,fix}(p) - c_{invest,fix}(|P|))
1117
                \cdot b_{invest}(p)) \cdot DF^{-p}\\
1118
                &\\
1119
                &
1120
                \forall p \in \textrm{PERIODS}
1121

1122
        * :attr:`fixed_costs` not None for investments
1123

1124
            .. math::
1125
                &
1126
                \sum_{pp=year(p)}^{limit_{end}}
1127
                E_{invest}(p) \cdot c_{fixed}(pp) \cdot DF^{-pp})
1128
                \cdot DF^{-p}\\
1129
                &
1130
                \forall p \in \textrm{PERIODS}
1131

1132
        * :attr:`fixed_costs` not None for existing capacity
1133

1134
            .. math::
1135
                \sum_{pp=0}^{limit_{exo}} E_{exist} \cdot c_{fixed}(pp)
1136
                \cdot DF^{-pp}
1137

1138
    where:
1139

1140
    * :math:`A(c_{invest,var}(p), l, ir)` A is the annuity for
1141
      investment expenses :math:`c_{invest,var}(p)`, lifetime :math:`l`
1142
      and interest rate :math:`ir`.
1143
    * :math:`l_{r}` is the remaining lifetime at the end of the
1144
      optimization horizon (in case it is greater than 0 and
1145
      smaller than the actual lifetime).
1146
    * :math:`ANF(d, ir)` is the annuity factor for duration :math:`d`
1147
      and interest rate :math:`ir`.
1148
    * :math:`d=min\{year_{max} - year(p), l\}` defines the
1149
      number of years within the optimization horizon that investment
1150
      annuities are accounted for.
1151
    * :math:`year(p)` denotes the start year of period :math:`p`.
1152
    * :math:`year_{max}` denotes the last year of the optimization
1153
      horizon, i.e. at the end of the last period.
1154
    * :math:`limit_{end}=min\{year_{max}, year(p) + l\}` is used as an
1155
      upper bound to ensure fixed costs for endogenous investments
1156
      to occur within the optimization horizon.
1157
    * :math:`limit_{exo}=min\{year_{max}, l - a\}` is used as an
1158
      upper bound to ensure fixed costs for existing capacities to occur
1159
      within the optimization horizon. :math:`a` is the initial age
1160
      of an asset.
1161
    * :math:`DF=(1+dr)` is the discount factor.
1162

1163
    The annuity / annuity factor hereby is:
1164

1165
        .. math::
1166
            &
1167
            A(c_{invest,var}(p), l, ir) = c_{invest,var}(p) \cdot
1168
                \frac {(1+ir)^l \cdot ir} {(1+ir)^l - 1}\\
1169
            &\\
1170
            &
1171
            ANF(d, ir)=\frac {(1+ir)^d \cdot ir} {(1+ir)^d - 1}
1172

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

1177
    The overall summed cost expressions for all *InvestmentFlowBlock* objects
1178
    can be accessed by
1179

1180
    * :attr:`om.GenericInvestmentStorageBlock.investment_costs`,
1181
    * :attr:`om.GenericInvestmentStorageBlock.fixed_costs` and
1182
    * :attr:`om.GenericInvestmentStorageBlock.costs`.
1183

1184
    Their values  after optimization can be retrieved by
1185

1186
    * :meth:`om.GenericInvestmentStorageBlock.investment_costs`,
1187
    * :attr:`om.GenericInvestmentStorageBlock.period_investment_costs`
1188
      (yielding a dict keyed by periods); note: this is not a Pyomo expression,
1189
      but calculated,
1190
    * :meth:`om.GenericInvestmentStorageBlock.fixed_costs` and
1191
    * :meth:`om.GenericInvestmentStorageBlock.costs`.
1192

1193
    .. csv-table:: List of Variables
1194
        :header: "symbol", "attribute", "explanation"
1195
        :widths: 1, 1, 1
1196

1197
        ":math:`P_i(p, t)`", ":attr:`flow[i[n], n, p, t]`", "Inflow
1198
        of the storage"
1199
        ":math:`P_o(p, t)`", ":attr:`flow[n, o[n], p, t]`", "Outflow
1200
        of the storage"
1201
        ":math:`E(t)`", ":attr:`storage_content[n, t]`", "Current storage
1202
        content (current absolute stored energy)"
1203
        ":math:`E_{loss}(t)`", ":attr:`storage_losses[n, t]`", "Current storage
1204
        losses (absolute losses per time step)"
1205
        ":math:`E_{invest}(p)`", ":attr:`invest[n, p]`", "Invested (nominal)
1206
        capacity of the storage"
1207
        ":math:`E_{old}(p)`", ":attr:`old[n, p]`", "
1208
        | Old (nominal) capacity of the storage
1209
        | to be decommissioned in period p"
1210
        ":math:`E_{old,exo}(p)`", ":attr:`old_exo[n, p]`", "
1211
        | Old (nominal) capacity of the storage
1212
        | to be decommissioned in period p
1213
        | which was exogenously given by :math:`E_{exist}`"
1214
        ":math:`E_{old,end}(p)`", ":attr:`old_end[n, p]`", "
1215
        | Old (nominal) capacity of the storage
1216
        | to be decommissioned in period p
1217
        | which was endogenously determined by :math:`E_{invest}(p_{comm})`
1218
        | where :math:`p_{comm}` is the commissioning period"
1219
        ":math:`E(-1)`", ":attr:`init_cap[n]`", "Initial storage capacity
1220
        (before timestep 0)"
1221
        ":math:`b_{invest}(p)`", ":attr:`invest_status[i, o, p]`", "Binary
1222
        variable for the status of investment"
1223
        ":math:`P_{i,invest}(p)`", "
1224
        :attr:`InvestmentFlowBlock.invest[i[n], n, p]`", "
1225
        Invested (nominal) inflow (InvestmentFlowBlock)"
1226
        ":math:`P_{o,invest}`", "
1227
        :attr:`InvestmentFlowBlock.invest[n, o[n]]`", "
1228
        Invested (nominal) outflow (InvestmentFlowBlock)"
1229

1230
    .. csv-table:: List of Parameters
1231
        :header: "symbol", "attribute", "explanation"
1232
        :widths: 1, 1, 1
1233

1234
        ":math:`E_{exist}`", "`flows[i, o].investment.existing`", "
1235
        Existing storage capacity"
1236
        ":math:`E_{invest,min}`", "`flows[i, o].investment.minimum`", "
1237
        Minimum investment value"
1238
        ":math:`E_{invest,max}`", "`flows[i, o].investment.maximum`", "
1239
        Maximum investment value"
1240
        ":math:`P_{i,exist}`", "`flows[i[n], n].investment.existing`
1241
        ", "Existing inflow capacity"
1242
        ":math:`P_{o,exist}`", "`flows[n, o[n]].investment.existing`
1243
        ", "Existing outflow capacity"
1244
        ":math:`c_{invest,var}`", "`flows[i, o].investment.ep_costs`
1245
        ", "Variable investment costs"
1246
        ":math:`c_{invest,fix}`", "`flows[i, o].investment.offset`", "
1247
        Fix investment costs"
1248
        ":math:`c_{fixed}`", "`flows[i, o].investment.fixed_costs`", "
1249
        Fixed costs; only allowed in multi-period model"
1250
        ":math:`r_{cap,in}`", ":attr:`invest_relation_input_capacity`", "
1251
        Relation of storage capacity and nominal inflow"
1252
        ":math:`r_{cap,out}`", ":attr:`invest_relation_output_capacity`", "
1253
        Relation of storage capacity and nominal outflow"
1254
        ":math:`r_{in,out}`", ":attr:`invest_relation_input_output`", "
1255
        Relation of nominal in- and outflow"
1256
        ":math:`\beta(t)`", "`loss_rate[t]`", "Fraction of lost energy
1257
        as share of :math:`E(t)` per hour"
1258
        ":math:`\gamma(t)`", "`fixed_losses_relative[t]`", "Fixed loss
1259
        of energy relative to :math:`E_{invest} + E_{exist}` per hour"
1260
        ":math:`\delta(t)`", "`fixed_losses_absolute[t]`", "Absolute
1261
        fixed loss of energy per hour"
1262
        ":math:`\eta_i(t)`", "`inflow_conversion_factor[t]`", "
1263
        Conversion factor (i.e. efficiency) when storing energy"
1264
        ":math:`\eta_o(t)`", "`outflow_conversion_factor[t]`", "
1265
        Conversion factor when (i.e. efficiency) taking stored energy"
1266
        ":math:`c(-1)`", "`initial_storage_level`", "Initial relative
1267
        storage content (before timestep 0)"
1268
        ":math:`c_{max}`", "`flows[i, o].max[t]`", "Normed maximum
1269
        value of storage content"
1270
        ":math:`c_{min}`", "`flows[i, o].min[t]`", "Normed minimum
1271
        value of storage content"
1272
        ":math:`l`", "`flows[i, o].investment.lifetime`", "
1273
        Lifetime for investments in storage capacity"
1274
        ":math:`a`", "`flows[i, o].investment.age`", "
1275
        Initial age of existing capacity / energy"
1276
        ":math:`\tau(t)`", "", "Duration of time step"
1277
        ":math:`t_u`", "", "Time unit of losses :math:`\beta(t)`,
1278
        :math:`\gamma(t)`, :math:`\delta(t)` and timeincrement :math:`\tau(t)`"
1279

1280
    """
1281

1282
    CONSTRAINT_GROUP = True
1✔
1283

1284
    def __init__(self, *args, **kwargs):
1✔
1285
        super().__init__(*args, **kwargs)
1✔
1286

1287
    def _create(self, group):
1✔
1288
        """Create a storage block for investment modeling"""
1289
        m = self.parent_block()
1✔
1290

1291
        # ########################## CHECKS ###################################
1292
        if m.es.periods is not None:
1✔
1293
            for n in group:
1✔
1294
                error_fixed_absolute_losses = (
1✔
1295
                    "For a multi-period investment model, fixed absolute"
1296
                    " losses are not supported. Please remove parameter."
1297
                )
1298
                if n.fixed_losses_absolute[0] != 0:
1!
UNCOV
1299
                    raise ValueError(error_fixed_absolute_losses)
×
1300
                error_initial_storage_level = (
1✔
1301
                    "For a multi-period model, initial_storage_level is"
1302
                    " not supported.\nIt needs to be removed since it"
1303
                    " has no effect.\nstorage_content will be zero,"
1304
                    " until there is some usable storage capacity installed."
1305
                )
1306
                if n.initial_storage_level is not None:
1!
UNCOV
1307
                    raise ValueError(error_initial_storage_level)
×
1308

1309
        # ########################## SETS #####################################
1310

1311
        self.INVESTSTORAGES = Set(initialize=[n for n in group])
1✔
1312

1313
        self.CONVEX_INVESTSTORAGES = Set(
1✔
1314
            initialize=[n for n in group if n.investment.nonconvex is False]
1315
        )
1316

1317
        self.NON_CONVEX_INVESTSTORAGES = Set(
1✔
1318
            initialize=[n for n in group if n.investment.nonconvex is True]
1319
        )
1320

1321
        self.INVESTSTORAGES_BALANCED = Set(
1✔
1322
            initialize=[n for n in group if n.balanced is True]
1323
        )
1324

1325
        self.INVESTSTORAGES_NO_INIT_CONTENT = Set(
1✔
1326
            initialize=[n for n in group if n.initial_storage_level is None]
1327
        )
1328

1329
        self.INVESTSTORAGES_INIT_CONTENT = Set(
1✔
1330
            initialize=[
1331
                n for n in group if n.initial_storage_level is not None
1332
            ]
1333
        )
1334

1335
        self.INVEST_REL_CAP_IN = Set(
1✔
1336
            initialize=[
1337
                n
1338
                for n in group
1339
                if n.invest_relation_input_capacity[0] is not None
1340
            ]
1341
        )
1342

1343
        self.INVEST_REL_CAP_OUT = Set(
1✔
1344
            initialize=[
1345
                n
1346
                for n in group
1347
                if n.invest_relation_output_capacity[0] is not None
1348
            ]
1349
        )
1350

1351
        self.INVEST_REL_IN_OUT = Set(
1✔
1352
            initialize=[
1353
                n
1354
                for n in group
1355
                if n.invest_relation_input_output[0] is not None
1356
            ]
1357
        )
1358

1359
        # The storage content is a non-negative variable, therefore it makes no
1360
        # sense to create an additional constraint if the lower bound is zero
1361
        # for all time steps.
1362
        self.MIN_INVESTSTORAGES = Set(
1✔
1363
            initialize=[
1364
                n
1365
                for n in group
1366
                if sum([n.min_storage_level[t] for t in m.TIMESTEPS]) > 0
1367
            ]
1368
        )
1369

1370
        self.OVERALL_MAXIMUM_INVESTSTORAGES = Set(
1✔
1371
            initialize=[
1372
                n for n in group if n.investment.overall_maximum is not None
1373
            ]
1374
        )
1375

1376
        self.OVERALL_MINIMUM_INVESTSTORAGES = Set(
1✔
1377
            initialize=[
1378
                n for n in group if n.investment.overall_minimum is not None
1379
            ]
1380
        )
1381

1382
        self.EXISTING_INVESTSTORAGES = Set(
1✔
1383
            initialize=[n for n in group if n.investment.existing is not None]
1384
        )
1385

1386
        # ######################### Variables  ################################
1387
        if not m.TSAM_MODE:
1✔
1388
            self.storage_content = Var(
1✔
1389
                self.INVESTSTORAGES, m.TIMEPOINTS, within=NonNegativeReals
1390
            )
1391
        else:
1392
            self.inter_storage_content = Var(
1✔
1393
                self.INVESTSTORAGES, m.CLUSTERS_OFFSET, within=NonNegativeReals
1394
            )
1395
            self.intra_storage_delta = Var(
1✔
1396
                self.INVESTSTORAGES, m.TIMEINDEX_TYPICAL_CLUSTER_OFFSET
1397
            )
1398
            # set the initial intra storage content
1399
            # first timestep in intra storage is always zero
1400
            for n in group:
1✔
1401
                for p, k in m.TYPICAL_CLUSTERS:
1✔
1402
                    self.intra_storage_delta[n, p, k, 0] = 0
1✔
1403
                    self.intra_storage_delta[n, p, k, 0].fix()
1✔
1404

1405
        def _storage_investvar_bound_rule(_, n, p):
1✔
1406
            """
1407
            Rule definition to bound the invested storage capacity `invest`.
1408
            """
1409
            if n in self.CONVEX_INVESTSTORAGES:
1✔
1410
                return n.investment.minimum[p], n.investment.maximum[p]
1✔
1411
            else:  # n in self.NON_CONVEX_INVESTSTORAGES
1412
                return 0, n.investment.maximum[p]
1✔
1413

1414
        self.invest = Var(
1✔
1415
            self.INVESTSTORAGES,
1416
            m.PERIODS,
1417
            within=NonNegativeReals,
1418
            bounds=_storage_investvar_bound_rule,
1419
        )
1420

1421
        # Total capacity
1422
        self.total = Var(
1✔
1423
            self.INVESTSTORAGES,
1424
            m.PERIODS,
1425
            within=NonNegativeReals,
1426
            initialize=0,
1427
        )
1428

1429
        if m.es.periods is not None:
1✔
1430
            # Old capacity to be decommissioned (due to lifetime)
1431
            self.old = Var(
1✔
1432
                self.INVESTSTORAGES, m.PERIODS, within=NonNegativeReals
1433
            )
1434

1435
            # Old endogenous capacity to be decommissioned (due to lifetime)
1436
            self.old_end = Var(
1✔
1437
                self.INVESTSTORAGES, m.PERIODS, within=NonNegativeReals
1438
            )
1439

1440
            # Old exogenous capacity to be decommissioned (due to lifetime)
1441
            self.old_exo = Var(
1✔
1442
                self.INVESTSTORAGES, m.PERIODS, within=NonNegativeReals
1443
            )
1444

1445
        # create status variable for a non-convex investment storage
1446
        self.invest_status = Var(
1✔
1447
            self.NON_CONVEX_INVESTSTORAGES, m.PERIODS, within=Binary
1448
        )
1449

1450
        # ######################### CONSTRAINTS ###############################
1451
        i = {n: [i for i in n.inputs][0] for n in group}
1✔
1452
        o = {n: [o for o in n.outputs][0] for n in group}
1✔
1453

1454
        # Handle unit lifetimes
1455
        def _total_storage_capacity_rule(block):
1✔
1456
            """Rule definition for determining total installed
1457
            capacity (taking decommissioning into account)
1458
            """
1459
            for n in self.INVESTSTORAGES:
1✔
1460
                for p in m.PERIODS:
1✔
1461
                    if p == 0:
1!
1462
                        expr = (
1✔
1463
                            self.total[n, p]
1464
                            == self.invest[n, p] + n.investment.existing
1465
                        )
1466
                        self.total_storage_rule.add((n, p), expr)
1✔
1467
                    else:
UNCOV
1468
                        expr = (
×
1469
                            self.total[n, p]
1470
                            == self.invest[n, p]
1471
                            + self.total[n, p - 1]
1472
                            - self.old[n, p]
1473
                        )
UNCOV
1474
                        self.total_storage_rule.add((n, p), expr)
×
1475

1476
        self.total_storage_rule = Constraint(
1✔
1477
            self.INVESTSTORAGES, m.PERIODS, noruleinit=True
1478
        )
1479

1480
        self.total_storage_rule_build = BuildAction(
1✔
1481
            rule=_total_storage_capacity_rule
1482
        )
1483

1484
        # multi-period storage implementation for time intervals
1485
        if m.es.periods is not None:
1✔
1486

1487
            def _old_storage_capacity_rule_end(block):
1✔
1488
                """Rule definition for determining old endogenously installed
1489
                capacity to be decommissioned due to reaching its lifetime.
1490
                Investment and decommissioning periods are linked within
1491
                the constraint. The respective decommissioning period is
1492
                determined for every investment period based on the components
1493
                lifetime and a matrix describing its age of each endogenous
1494
                investment. Decommissioning can only occur at the beginning of
1495
                each period.
1496

1497
                Note
1498
                ----
1499
                For further information on the implementation check
1500
                PR#957 https://github.com/oemof/oemof-solph/pull/957
1501
                """
1502
                for n in self.INVESTSTORAGES:
1✔
1503
                    lifetime = n.investment.lifetime
1✔
1504
                    if lifetime is None:
1!
UNCOV
1505
                        msg = (
×
1506
                            "You have to specify a lifetime "
1507
                            "for a Flow going into or out of "
1508
                            "a GenericStorage unit "
1509
                            "in a multi-period model!"
1510
                            f" Value for {n} is missing."
1511
                        )
UNCOV
1512
                        raise ValueError(msg)
×
1513
                    # get the period matrix describing the temporal distance
1514
                    # between all period combinations.
1515
                    periods_matrix = m.es.periods_matrix
1✔
1516

1517
                    # get the index of the minimum value in each row greater
1518
                    # equal than the lifetime. This value equals the
1519
                    # decommissioning period if not zero. The index of this
1520
                    # value represents the investment period. If np.where
1521
                    # condition is not met in any row, min value will be zero
1522
                    decomm_periods = np.argmin(
1✔
1523
                        np.where(
1524
                            (periods_matrix >= lifetime),
1525
                            periods_matrix,
1526
                            np.inf,
1527
                        ),
1528
                        axis=1,
1529
                    )
1530

1531
                    # no decommissioning in first period
1532
                    expr = self.old_end[n, 0] == 0
1✔
1533
                    self.old_rule_end.add((n, 0), expr)
1✔
1534

1535
                    # all periods not in decomm_periods have no decommissioning
1536
                    # zero is excluded
1537
                    for p in m.PERIODS:
1✔
1538
                        if p not in decomm_periods and p != 0:
1!
UNCOV
1539
                            expr = self.old_end[n, p] == 0
×
UNCOV
1540
                            self.old_rule_end.add((n, p), expr)
×
1541

1542
                    # multiple invests can be decommissioned in the same period
1543
                    # but only sequential ones, thus a bookkeeping is
1544
                    # introduced andconstraints are added to equation one
1545
                    # iteration later.
1546
                    last_decomm_p = np.nan
1✔
1547
                    # loop over invest periods (values are decomm_periods)
1548
                    for invest_p, decomm_p in enumerate(decomm_periods):
1✔
1549
                        # Add constraint of iteration before
1550
                        # (skipped in first iteration by last_decomm_p = nan)
1551
                        if (decomm_p != last_decomm_p) and (
1!
1552
                            last_decomm_p is not np.nan
1553
                        ):
UNCOV
1554
                            expr = self.old_end[n, last_decomm_p] == expr
×
UNCOV
1555
                            self.old_rule_end.add((n, last_decomm_p), expr)
×
1556

1557
                        # no decommissioning if decomm_p is zero
1558
                        if decomm_p == 0:
1!
1559
                            # overwrite decomm_p with zero to avoid
1560
                            # chaining invest periods in next iteration
1561
                            last_decomm_p = 0
1✔
1562

1563
                        # if decomm_p is the same as the last one chain invest
1564
                        # period
UNCOV
1565
                        elif decomm_p == last_decomm_p:
×
UNCOV
1566
                            expr += self.invest[n, invest_p]
×
1567
                            # overwrite decomm_p
UNCOV
1568
                            last_decomm_p = decomm_p
×
1569

1570
                        # if decomm_p is not zero, not the same as the last one
1571
                        # and it's not the first period
1572
                        else:
UNCOV
1573
                            expr = self.invest[n, invest_p]
×
1574
                            # overwrite decomm_p
UNCOV
1575
                            last_decomm_p = decomm_p
×
1576

1577
                    # Add constraint of very last iteration
1578
                    if last_decomm_p != 0:
1!
UNCOV
1579
                        expr = self.old_end[n, last_decomm_p] == expr
×
UNCOV
1580
                        self.old_rule_end.add((n, last_decomm_p), expr)
×
1581

1582
            self.old_rule_end = Constraint(
1✔
1583
                self.INVESTSTORAGES, m.PERIODS, noruleinit=True
1584
            )
1585

1586
            self.old_rule_end_build = BuildAction(
1✔
1587
                rule=_old_storage_capacity_rule_end
1588
            )
1589

1590
            def _old_storage_capacity_rule_exo(block):
1✔
1591
                """Rule definition for determining old exogenously given
1592
                capacity to be decommissioned due to reaching its lifetime
1593
                """
1594
                for n in self.INVESTSTORAGES:
1✔
1595
                    age = n.investment.age
1✔
1596
                    lifetime = n.investment.lifetime
1✔
1597
                    is_decommissioned = False
1✔
1598
                    for p in m.PERIODS:
1✔
1599
                        # No shutdown in first period
1600
                        if p == 0:
1!
1601
                            expr = self.old_exo[n, p] == 0
1✔
1602
                            self.old_rule_exo.add((n, p), expr)
1✔
UNCOV
1603
                        elif lifetime - age <= m.es.periods_years[p]:
×
1604
                            # Track decommissioning status
UNCOV
1605
                            if not is_decommissioned:
×
UNCOV
1606
                                expr = (
×
1607
                                    self.old_exo[n, p] == n.investment.existing
1608
                                )
UNCOV
1609
                                is_decommissioned = True
×
1610
                            else:
UNCOV
1611
                                expr = self.old_exo[n, p] == 0
×
UNCOV
1612
                            self.old_rule_exo.add((n, p), expr)
×
1613
                        else:
UNCOV
1614
                            expr = self.old_exo[n, p] == 0
×
UNCOV
1615
                            self.old_rule_exo.add((n, p), expr)
×
1616

1617
            self.old_rule_exo = Constraint(
1✔
1618
                self.INVESTSTORAGES, m.PERIODS, noruleinit=True
1619
            )
1620

1621
            self.old_rule_exo_build = BuildAction(
1✔
1622
                rule=_old_storage_capacity_rule_exo
1623
            )
1624

1625
            def _old_storage_capacity_rule(block):
1✔
1626
                """Rule definition for determining (overall) old capacity
1627
                to be decommissioned due to reaching its lifetime
1628
                """
1629
                for n in self.INVESTSTORAGES:
1✔
1630
                    for p in m.PERIODS:
1✔
1631
                        expr = (
1✔
1632
                            self.old[n, p]
1633
                            == self.old_end[n, p] + self.old_exo[n, p]
1634
                        )
1635
                        self.old_rule.add((n, p), expr)
1✔
1636

1637
            self.old_rule = Constraint(
1✔
1638
                self.INVESTSTORAGES, m.PERIODS, noruleinit=True
1639
            )
1640

1641
            self.old_rule_build = BuildAction(rule=_old_storage_capacity_rule)
1✔
1642

1643
            def _initially_empty_rule(_):
1✔
1644
                """Ensure storage to be empty initially"""
UNCOV
1645
                for n in self.INVESTSTORAGES:
×
UNCOV
1646
                    expr = self.storage_content[n, 0] == 0
×
UNCOV
1647
                    self.initially_empty.add((n, 0), expr)
×
1648

1649
            if not m.TSAM_MODE:
1!
1650
                # inter and intra initial storage contents are handled above
UNCOV
1651
                self.initially_empty = Constraint(
×
1652
                    self.INVESTSTORAGES, m.TIMESTEPS, noruleinit=True
1653
                )
1654

UNCOV
1655
                self.initially_empty_build = BuildAction(
×
1656
                    rule=_initially_empty_rule
1657
                )
1658

1659
        # Standard storage implementation for discrete time points
1660
        else:
1661

1662
            def _inv_storage_init_content_max_rule(block, n):
1✔
1663
                """Constraint for a variable initial storage capacity."""
1664
                if not m.TSAM_MODE:
1✔
1665
                    lhs = block.storage_content[n, 0]
1✔
1666
                else:
1667
                    lhs = block.intra_storage_delta[n, 0, 0, 0]
1✔
1668
                return lhs <= n.investment.existing + block.invest[n, 0]
1✔
1669

1670
            self.init_content_limit = Constraint(
1✔
1671
                self.INVESTSTORAGES_NO_INIT_CONTENT,
1672
                rule=_inv_storage_init_content_max_rule,
1673
            )
1674

1675
            def _inv_storage_init_content_fix_rule(block, n):
1✔
1676
                """Constraint for a fixed initial storage capacity."""
1677
                if not m.TSAM_MODE:
1!
1678
                    lhs = block.storage_content[n, 0]
1✔
1679
                else:
UNCOV
1680
                    lhs = block.intra_storage_delta[n, 0, 0, 0]
×
1681
                return lhs == n.initial_storage_level * (
1✔
1682
                    n.investment.existing + block.invest[n, 0]
1683
                )
1684

1685
            self.init_content_fix = Constraint(
1✔
1686
                self.INVESTSTORAGES_INIT_CONTENT,
1687
                rule=_inv_storage_init_content_fix_rule,
1688
            )
1689

1690
        def _storage_balance_rule(block, n, p, t):
1✔
1691
            """
1692
            Rule definition for the storage balance of every storage n and
1693
            every timestep.
1694
            """
1695
            expr = 0
1✔
1696
            expr += block.storage_content[n, t + 1]
1✔
1697
            expr += (
1✔
1698
                -block.storage_content[n, t]
1699
                * (1 - n.loss_rate[t]) ** m.timeincrement[t]
1700
            )
1701
            expr += (
1✔
1702
                n.fixed_losses_relative[t]
1703
                * self.total[n, p]
1704
                * m.timeincrement[t]
1705
            )
1706
            expr += n.fixed_losses_absolute[t] * m.timeincrement[t]
1✔
1707
            expr += (
1✔
1708
                -m.flow[i[n], n, t] * n.inflow_conversion_factor[t]
1709
            ) * m.timeincrement[t]
1710
            expr += (
1✔
1711
                m.flow[n, o[n], t] / n.outflow_conversion_factor[t]
1712
            ) * m.timeincrement[t]
1713
            return expr == 0
1✔
1714

1715
        def _intra_storage_balance_rule(block, n, p, k, g):
1✔
1716
            """
1717
            Rule definition for the storage balance of every storage n and
1718
            every timestep.
1719
            """
1720
            t = m.get_timestep_from_tsam_timestep(p, k, g)
1✔
1721
            expr = 0
1✔
1722
            expr += block.intra_storage_delta[n, p, k, g + 1]
1✔
1723
            expr += (
1✔
1724
                -block.intra_storage_delta[n, p, k, g]
1725
                * (1 - n.loss_rate[t]) ** m.timeincrement[t]
1726
            )
1727
            expr += (
1✔
1728
                n.fixed_losses_relative[t]
1729
                * self.total[n, p]
1730
                * m.timeincrement[t]
1731
            )
1732
            expr += n.fixed_losses_absolute[t] * m.timeincrement[t]
1✔
1733
            expr += (
1✔
1734
                -m.flow[i[n], n, t] * n.inflow_conversion_factor[t]
1735
            ) * m.timeincrement[t]
1736
            expr += (
1✔
1737
                m.flow[n, o[n], t] / n.outflow_conversion_factor[t]
1738
            ) * m.timeincrement[t]
1739
            return expr == 0
1✔
1740

1741
        if not m.TSAM_MODE:
1✔
1742
            self.balance = Constraint(
1✔
1743
                self.INVESTSTORAGES,
1744
                m.TIMEINDEX,
1745
                rule=_storage_balance_rule,
1746
            )
1747
        else:
1748
            self.intra_balance = Constraint(
1✔
1749
                self.INVESTSTORAGES,
1750
                m.TIMEINDEX_TYPICAL_CLUSTER,
1751
                rule=_intra_storage_balance_rule,
1752
            )
1753

1754
        def _inter_storage_balance_rule(block, n, i):
1✔
1755
            """
1756
            Rule definition for the storage balance of every storage n and
1757
            every timestep.
1758
            """
1759
            ii = 0
1✔
1760
            for p in m.PERIODS:
1!
1761
                ii += len(m.es.tsa_parameters[p]["order"])
1✔
1762
                if ii > i:
1!
1763
                    ii -= len(m.es.tsa_parameters[p]["order"])
1✔
1764
                    ii = i - ii
1✔
1765
                    break
1✔
1766

1767
            k = m.es.tsa_parameters[p]["order"][ii]
1✔
1768
            t = m.get_timestep_from_tsam_timestep(
1✔
1769
                p, k, m.es.tsa_parameters[p]["timesteps"] - 1
1770
            )
1771
            expr = 0
1✔
1772
            expr += block.inter_storage_content[n, i + 1]
1✔
1773
            expr += -block.inter_storage_content[n, i] * (
1✔
1774
                1 - n.loss_rate[t]
1775
            ) ** (m.timeincrement[t] * m.es.tsa_parameters[p]["timesteps"])
1776
            expr += -self.intra_storage_delta[
1✔
1777
                n, p, k, m.es.tsa_parameters[p]["timesteps"]
1778
            ]
1779
            return expr == 0
1✔
1780

1781
        if m.TSAM_MODE:
1✔
1782
            self.inter_balance = Constraint(
1✔
1783
                self.INVESTSTORAGES,
1784
                m.CLUSTERS,
1785
                rule=_inter_storage_balance_rule,
1786
            )
1787

1788
        if m.es.periods is None and not m.TSAM_MODE:
1✔
1789

1790
            def _balanced_storage_rule(block, n):
1✔
1791
                return (
1✔
1792
                    block.storage_content[n, m.TIMEPOINTS.at(-1)]
1793
                    == block.storage_content[n, m.TIMEPOINTS.at(1)]
1794
                )
1795

1796
            self.balanced_cstr = Constraint(
1✔
1797
                self.INVESTSTORAGES_BALANCED, rule=_balanced_storage_rule
1798
            )
1799

1800
        def _power_coupled(block):
1✔
1801
            """
1802
            Rule definition for constraint to connect the input power
1803
            and output power
1804
            """
1805
            for n in self.INVEST_REL_IN_OUT:
1!
UNCOV
1806
                for p in m.PERIODS:
×
UNCOV
1807
                    expr = (
×
1808
                        m.InvestmentFlowBlock.total[n, o[n], p]
1809
                    ) * n.invest_relation_input_output[p] == (
1810
                        m.InvestmentFlowBlock.total[i[n], n, p]
1811
                    )
UNCOV
1812
                    self.power_coupled.add((n, p), expr)
×
1813

1814
        self.power_coupled = Constraint(
1✔
1815
            self.INVEST_REL_IN_OUT, m.PERIODS, noruleinit=True
1816
        )
1817

1818
        self.power_coupled_build = BuildAction(rule=_power_coupled)
1✔
1819

1820
        def _storage_capacity_inflow_invest_rule(block):
1✔
1821
            """
1822
            Rule definition of constraint connecting the inflow
1823
            `InvestmentFlowBlock.invest of storage with invested capacity
1824
            `invest` by nominal_storage_capacity__inflow_ratio
1825
            """
1826
            for n in self.INVEST_REL_CAP_IN:
1✔
1827
                for p in m.PERIODS:
1✔
1828
                    expr = (
1✔
1829
                        m.InvestmentFlowBlock.total[i[n], n, p]
1830
                        == self.total[n, p]
1831
                        * n.invest_relation_input_capacity[p]
1832
                    )
1833
                    self.storage_capacity_inflow.add((n, p), expr)
1✔
1834

1835
        self.storage_capacity_inflow = Constraint(
1✔
1836
            self.INVEST_REL_CAP_IN, m.PERIODS, noruleinit=True
1837
        )
1838

1839
        self.storage_capacity_inflow_build = BuildAction(
1✔
1840
            rule=_storage_capacity_inflow_invest_rule
1841
        )
1842

1843
        def _storage_capacity_outflow_invest_rule(block):
1✔
1844
            """
1845
            Rule definition of constraint connecting outflow
1846
            `InvestmentFlowBlock.invest` of storage and invested capacity
1847
            `invest` by nominal_storage_capacity__outflow_ratio
1848
            """
1849
            for n in self.INVEST_REL_CAP_OUT:
1✔
1850
                for p in m.PERIODS:
1✔
1851
                    expr = (
1✔
1852
                        m.InvestmentFlowBlock.total[n, o[n], p]
1853
                        == self.total[n, p]
1854
                        * n.invest_relation_output_capacity[p]
1855
                    )
1856
                    self.storage_capacity_outflow.add((n, p), expr)
1✔
1857

1858
        self.storage_capacity_outflow = Constraint(
1✔
1859
            self.INVEST_REL_CAP_OUT, m.PERIODS, noruleinit=True
1860
        )
1861

1862
        self.storage_capacity_outflow_build = BuildAction(
1✔
1863
            rule=_storage_capacity_outflow_invest_rule
1864
        )
1865

1866
        self._add_storage_limit_constraints()
1✔
1867

1868
        def maximum_invest_limit(block, n, p):
1✔
1869
            """
1870
            Constraint for the maximal investment in non convex investment
1871
            storage.
1872
            """
1873
            return (
1✔
1874
                n.investment.maximum[p] * self.invest_status[n, p]
1875
                - self.invest[n, p]
1876
            ) >= 0
1877

1878
        self.limit_max = Constraint(
1✔
1879
            self.NON_CONVEX_INVESTSTORAGES,
1880
            m.PERIODS,
1881
            rule=maximum_invest_limit,
1882
        )
1883

1884
        def smallest_invest(block, n, p):
1✔
1885
            """
1886
            Constraint for the minimal investment in non convex investment
1887
            storage if the invest is greater than 0. So the invest variable
1888
            can be either 0 or greater than the minimum.
1889
            """
1890
            return (
1✔
1891
                self.invest[n, p]
1892
                - n.investment.minimum[p] * self.invest_status[n, p]
1893
                >= 0
1894
            )
1895

1896
        self.limit_min = Constraint(
1✔
1897
            self.NON_CONVEX_INVESTSTORAGES, m.PERIODS, rule=smallest_invest
1898
        )
1899

1900
        if m.es.periods is not None:
1✔
1901

1902
            def _overall_storage_maximum_investflow_rule(block):
1✔
1903
                """Rule definition for maximum overall investment
1904
                in investment case.
1905
                """
1906
                for n in self.OVERALL_MAXIMUM_INVESTSTORAGES:
1!
1907
                    for p in m.PERIODS:
×
1908
                        expr = self.total[n, p] <= n.investment.overall_maximum
×
1909
                        self.overall_storage_maximum.add((n, p), expr)
×
1910

1911
            self.overall_storage_maximum = Constraint(
1✔
1912
                self.OVERALL_MAXIMUM_INVESTSTORAGES, m.PERIODS, noruleinit=True
1913
            )
1914

1915
            self.overall_maximum_build = BuildAction(
1✔
1916
                rule=_overall_storage_maximum_investflow_rule
1917
            )
1918

1919
            def _overall_minimum_investflow_rule(block):
1✔
1920
                """Rule definition for minimum overall investment
1921
                in investment case.
1922

1923
                Note: This is only applicable for the last period
1924
                """
1925
                for n in self.OVERALL_MINIMUM_INVESTSTORAGES:
1!
1926
                    expr = (
×
1927
                        n.investment.overall_minimum
1928
                        <= self.total[n, m.PERIODS[-1]]
1929
                    )
1930
                    self.overall_minimum.add(n, expr)
×
1931

1932
            self.overall_minimum = Constraint(
1✔
1933
                self.OVERALL_MINIMUM_INVESTSTORAGES, noruleinit=True
1934
            )
1935

1936
            self.overall_minimum_build = BuildAction(
1✔
1937
                rule=_overall_minimum_investflow_rule
1938
            )
1939

1940
    def _add_storage_limit_constraints(self):
1✔
1941
        m = self.parent_block()
1✔
1942
        if not m.TSAM_MODE:
1✔
1943
            if m.es.periods is None:
1!
1944

1945
                def _max_storage_content_invest_rule(_, n, t):
1✔
1946
                    """
1947
                    Rule definition for upper bound constraint for the
1948
                    storage content.
1949
                    """
1950
                    expr = (
1✔
1951
                        self.storage_content[n, t]
1952
                        <= self.total[n, 0] * n.max_storage_level[t]
1953
                    )
1954
                    return expr
1✔
1955

1956
                self.max_storage_content = Constraint(
1✔
1957
                    self.INVESTSTORAGES,
1958
                    m.TIMEPOINTS,
1959
                    rule=_max_storage_content_invest_rule,
1960
                )
1961

1962
                def _min_storage_content_invest_rule(_, n, t):
1✔
1963
                    """
1964
                    Rule definition of lower bound constraint for the
1965
                    storage content.
1966
                    """
1967
                    expr = (
×
1968
                        self.storage_content[n, t]
1969
                        >= self.total[n, 0] * n.min_storage_level[t]
1970
                    )
1971
                    return expr
×
1972

1973
                self.min_storage_content = Constraint(
1✔
1974
                    self.MIN_INVESTSTORAGES,
1975
                    m.TIMEPOINTS,
1976
                    rule=_min_storage_content_invest_rule,
1977
                )
1978
            else:
1979

UNCOV
1980
                def _max_storage_content_invest_rule(_, n, p, t):
×
1981
                    """
1982
                    Rule definition for upper bound constraint for the
1983
                    storage content.
1984
                    """
UNCOV
1985
                    expr = (
×
1986
                        self.storage_content[n, t]
1987
                        <= self.total[n, p] * n.max_storage_level[t]
1988
                    )
UNCOV
1989
                    return expr
×
1990

UNCOV
1991
                self.max_storage_content = Constraint(
×
1992
                    self.INVESTSTORAGES,
1993
                    m.TIMEINDEX,
1994
                    rule=_max_storage_content_invest_rule,
1995
                )
1996

UNCOV
1997
                def _min_storage_content_invest_rule(_, n, p, t):
×
1998
                    """
1999
                    Rule definition of lower bound constraint for the
2000
                    storage content.
2001
                    """
2002
                    expr = (
×
2003
                        self.storage_content[n, t]
2004
                        >= self.total[n, p] * n.min_storage_level[t]
2005
                    )
2006
                    return expr
×
2007

UNCOV
2008
                self.min_storage_content = Constraint(
×
2009
                    self.MIN_INVESTSTORAGES,
2010
                    m.TIMEINDEX,
2011
                    rule=_min_storage_content_invest_rule,
2012
                )
2013
        else:
2014

2015
            def _storage_inter_maximum_level_rule(block):
1✔
2016
                for n in self.INVESTSTORAGES:
1✔
2017
                    for p, i, g in m.TIMEINDEX_CLUSTER:
1✔
2018
                        t = m.get_timestep_from_tsam_timestep(p, i, g)
1✔
2019
                        k = m.es.tsa_parameters[p]["order"][i]
1✔
2020
                        tk = m.get_timestep_from_tsam_timestep(p, k, g)
1✔
2021
                        inter_i = (
1✔
2022
                            sum(
2023
                                len(m.es.tsa_parameters[ip]["order"])
2024
                                for ip in range(p)
2025
                            )
2026
                            + i
2027
                        )
2028
                        lhs = (
1✔
2029
                            self.inter_storage_content[n, inter_i]
2030
                            * (1 - n.loss_rate[t]) ** (g * m.timeincrement[tk])
2031
                            + self.intra_storage_delta[n, p, k, g]
2032
                        )
2033
                        rhs = self.total[n, p] * n.max_storage_level[t]
1✔
2034
                        self.storage_inter_maximum_level.add(
1✔
2035
                            (n, p, i, g), lhs <= rhs
2036
                        )
2037

2038
            self.storage_inter_maximum_level = Constraint(
1✔
2039
                self.INVESTSTORAGES, m.TIMEINDEX_CLUSTER, noruleinit=True
2040
            )
2041

2042
            self.storage_inter_maximum_level_build = BuildAction(
1✔
2043
                rule=_storage_inter_maximum_level_rule
2044
            )
2045

2046
            def _storage_inter_minimum_level_rule(block):
1✔
2047
                # See FINE implementation at
2048
                # https://github.com/FZJ-IEK3-VSA/FINE/blob/
2049
                # 57ec32561fb95e746c505760bd0d61c97d2fd2fb/FINE/storage.py#L1329
2050
                for n in self.INVESTSTORAGES:
1✔
2051
                    for p, i, g in m.TIMEINDEX_CLUSTER:
1✔
2052
                        t = m.get_timestep_from_tsam_timestep(p, i, g)
1✔
2053
                        lhs = self.total[n, p] * n.min_storage_level[t]
1✔
2054
                        k = m.es.tsa_parameters[p]["order"][i]
1✔
2055
                        tk = m.get_timestep_from_tsam_timestep(p, k, g)
1✔
2056
                        inter_i = (
1✔
2057
                            sum(
2058
                                len(m.es.tsa_parameters[ip]["order"])
2059
                                for ip in range(p)
2060
                            )
2061
                            + i
2062
                        )
2063
                        rhs = (
1✔
2064
                            self.inter_storage_content[n, inter_i]
2065
                            * (1 - n.loss_rate[t]) ** (g * m.timeincrement[tk])
2066
                            + self.intra_storage_delta[n, p, k, g]
2067
                        )
2068
                        self.storage_inter_minimum_level.add(
1✔
2069
                            (n, p, i, g), lhs <= rhs
2070
                        )
2071

2072
            self.storage_inter_minimum_level = Constraint(
1✔
2073
                self.INVESTSTORAGES, m.TIMEINDEX_CLUSTER, noruleinit=True
2074
            )
2075

2076
            self.storage_inter_minimum_level_build = BuildAction(
1✔
2077
                rule=_storage_inter_minimum_level_rule
2078
            )
2079

2080
    def _objective_expression(self):
1✔
2081
        """Objective expression with fixed and investment costs."""
2082
        m = self.parent_block()
1✔
2083

2084
        investment_costs = 0
1✔
2085
        storage_costs = 0
1✔
2086
        period_investment_costs = {p: 0 for p in m.PERIODS}
1✔
2087
        fixed_costs = 0
1✔
2088

2089
        if m.es.periods is None:
1✔
2090
            for n in self.CONVEX_INVESTSTORAGES:
1✔
2091
                for p in m.PERIODS:
1✔
2092
                    investment_costs += (
1✔
2093
                        self.invest[n, p] * n.investment.ep_costs[p]
2094
                    )
2095
            for n in self.NON_CONVEX_INVESTSTORAGES:
1✔
2096
                for p in m.PERIODS:
1✔
2097
                    investment_costs += (
1✔
2098
                        self.invest[n, p] * n.investment.ep_costs[p]
2099
                        + self.invest_status[n, p] * n.investment.offset[p]
2100
                    )
2101

2102
        else:
2103
            msg = (
1✔
2104
                "You did not specify an interest rate.\n"
2105
                "It will be set equal to the discount_rate of {} "
2106
                "of the model as a default.\nThis corresponds to a "
2107
                "social planner point of view and does not reflect "
2108
                "microeconomic interest requirements."
2109
            )
2110
            for n in self.CONVEX_INVESTSTORAGES:
1✔
2111
                lifetime = n.investment.lifetime
1✔
2112
                interest = 0
1✔
2113
                if interest == 0:
1!
2114
                    warn(
1✔
2115
                        msg.format(m.discount_rate),
2116
                        debugging.SuspiciousUsageWarning,
2117
                    )
2118
                    interest = m.discount_rate
1✔
2119
                for p in m.PERIODS:
1✔
2120
                    annuity = economics.annuity(
1✔
2121
                        capex=n.investment.ep_costs[p],
2122
                        n=lifetime,
2123
                        wacc=interest,
2124
                    )
2125
                    duration = min(
1✔
2126
                        m.es.end_year_of_optimization - m.es.periods_years[p],
2127
                        lifetime,
2128
                    )
2129
                    present_value_factor = 1 / economics.annuity(
1✔
2130
                        capex=1, n=duration, wacc=interest
2131
                    )
2132
                    investment_costs_increment = (
1✔
2133
                        self.invest[n, p] * annuity * present_value_factor
2134
                    )
2135
                    remaining_value_difference = (
1✔
2136
                        self._evaluate_remaining_value_difference(
2137
                            m,
2138
                            p,
2139
                            n,
2140
                            m.es.end_year_of_optimization,
2141
                            lifetime,
2142
                            interest,
2143
                        )
2144
                    )
2145
                    investment_costs += (
1✔
2146
                        investment_costs_increment + remaining_value_difference
2147
                    )
2148
                    period_investment_costs[p] += investment_costs_increment
1✔
2149

2150
            for n in self.NON_CONVEX_INVESTSTORAGES:
1!
2151
                lifetime = n.investment.lifetime
×
2152
                interest = 0
×
2153
                if interest == 0:
×
UNCOV
2154
                    warn(
×
2155
                        msg.format(m.discount_rate),
2156
                        debugging.SuspiciousUsageWarning,
2157
                    )
2158
                    interest = m.discount_rate
×
2159
                for p in m.PERIODS:
×
UNCOV
2160
                    annuity = economics.annuity(
×
2161
                        capex=n.investment.ep_costs[p],
2162
                        n=lifetime,
2163
                        wacc=interest,
2164
                    )
UNCOV
2165
                    duration = min(
×
2166
                        m.es.end_year_of_optimization - m.es.periods_years[p],
2167
                        lifetime,
2168
                    )
UNCOV
2169
                    present_value_factor = 1 / economics.annuity(
×
2170
                        capex=1, n=duration, wacc=interest
2171
                    )
UNCOV
2172
                    investment_costs_increment = (
×
2173
                        self.invest[n, p] * annuity * present_value_factor
2174
                        + self.invest_status[n, p] * n.investment.offset[p]
2175
                    )
UNCOV
2176
                    remaining_value_difference = (
×
2177
                        self._evaluate_remaining_value_difference(
2178
                            m,
2179
                            p,
2180
                            n,
2181
                            m.es.end_year_of_optimization,
2182
                            lifetime,
2183
                            interest,
2184
                            nonconvex=True,
2185
                        )
2186
                    )
UNCOV
2187
                    investment_costs += (
×
2188
                        investment_costs_increment + remaining_value_difference
2189
                    )
UNCOV
2190
                    period_investment_costs[p] += investment_costs_increment
×
2191

2192
            for n in self.INVESTSTORAGES:
1✔
2193
                if valid_sequence(n.investment.fixed_costs, len(m.PERIODS)):
1!
UNCOV
2194
                    lifetime = n.investment.lifetime
×
UNCOV
2195
                    for p in m.PERIODS:
×
UNCOV
2196
                        range_limit = min(
×
2197
                            m.es.end_year_of_optimization,
2198
                            m.es.periods_years[p] + lifetime,
2199
                        )
UNCOV
2200
                        fixed_costs += sum(
×
2201
                            self.invest[n, p] * n.investment.fixed_costs[pp]
2202
                            for pp in range(
2203
                                m.es.periods_years[p],
2204
                                range_limit,
2205
                            )
2206
                        )
2207

2208
            for n in self.EXISTING_INVESTSTORAGES:
1✔
2209
                if valid_sequence(n.investment.fixed_costs, len(m.PERIODS)):
1!
UNCOV
2210
                    lifetime = n.investment.lifetime
×
UNCOV
2211
                    age = n.investment.age
×
UNCOV
2212
                    range_limit = min(
×
2213
                        m.es.end_year_of_optimization, lifetime - age
2214
                    )
UNCOV
2215
                    fixed_costs += sum(
×
2216
                        n.investment.existing * n.investment.fixed_costs[pp]
2217
                        for pp in range(range_limit)
2218
                    )
2219

2220
        for n in self.INVESTSTORAGES:
1✔
2221
            if valid_sequence(n.storage_costs, len(m.TIMESTEPS)):
1!
2222
                # We actually want to iterate over all TIMEPOINTS except the
2223
                # 0th. As integers are used for the index, this is equicalent
2224
                # to iterating over the TIMESTEPS with one offset.
UNCOV
2225
                if not m.TSAM_MODE:
×
UNCOV
2226
                    for t in m.TIMESTEPS:
×
UNCOV
2227
                        storage_costs += (
×
2228
                            self.storage_content[n, t + 1] * n.storage_costs[t]
2229
                        )
2230
                else:
UNCOV
2231
                    for t in m.TIMESTEPS_ORIGINAL:
×
UNCOV
2232
                        storage_costs += (
×
2233
                            self.storage_content[n, t + 1]
2234
                            * n.storage_costs[t + 1]
2235
                        )
2236

2237
        self.storage_costs = Expression(expr=storage_costs)
1✔
2238

2239
        self.investment_costs = Expression(expr=investment_costs)
1✔
2240
        self.period_investment_costs = period_investment_costs
1✔
2241
        self.fixed_costs = Expression(expr=fixed_costs)
1✔
2242
        self.costs = Expression(
1✔
2243
            expr=investment_costs + fixed_costs + storage_costs
2244
        )
2245

2246
        return self.costs
1✔
2247

2248
    def _evaluate_remaining_value_difference(
1✔
2249
        self,
2250
        m,
2251
        p,
2252
        n,
2253
        end_year_of_optimization,
2254
        lifetime,
2255
        interest,
2256
        nonconvex=False,
2257
    ):
2258
        """Evaluate and return the remaining value difference of an investment
2259

2260
        The remaining value difference in the net present values if the asset
2261
        was to be liquidated at the end of the optimization horizon and the
2262
        net present value using the original investment expenses.
2263

2264
        Parameters
2265
        ----------
2266
        m : oemof.solph.models.Model
2267
            Optimization model
2268

2269
        p : int
2270
            Period in which investment occurs
2271

2272
        n : oemof.solph.components.GenericStorage
2273
            storage unit
2274

2275
        end_year_of_optimization : int
2276
            Last year of the optimization horizon
2277

2278
        lifetime : int
2279
            lifetime of investment considered
2280

2281
        interest : float
2282
            Demanded interest rate for investment
2283

2284
        nonconvex : bool
2285
            Indicating whether considered flow is nonconvex.
2286
        """
2287
        if m.es.use_remaining_value:
1!
2288
            if end_year_of_optimization - m.es.periods_years[p] < lifetime:
×
2289
                remaining_lifetime = lifetime - (
×
2290
                    end_year_of_optimization - m.es.periods_years[p]
2291
                )
UNCOV
2292
                remaining_annuity = economics.annuity(
×
2293
                    capex=n.investment.ep_costs[-1],
2294
                    n=remaining_lifetime,
2295
                    wacc=interest,
2296
                )
UNCOV
2297
                original_annuity = economics.annuity(
×
2298
                    capex=n.investment.ep_costs[p],
2299
                    n=remaining_lifetime,
2300
                    wacc=interest,
2301
                )
UNCOV
2302
                present_value_factor_remaining = 1 / economics.annuity(
×
2303
                    capex=1, n=remaining_lifetime, wacc=interest
2304
                )
UNCOV
2305
                convex_investment_costs = (
×
2306
                    self.invest[n, p]
2307
                    * (remaining_annuity - original_annuity)
2308
                    * present_value_factor_remaining
2309
                )
UNCOV
2310
                if nonconvex:
×
UNCOV
2311
                    return convex_investment_costs + self.invest_status[
×
2312
                        n, p
2313
                    ] * (n.investment.offset[-1] - n.investment.offset[p])
2314
                else:
UNCOV
2315
                    return convex_investment_costs
×
2316
            else:
UNCOV
2317
                return 0
×
2318
        else:
2319
            return 0
1✔
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