Passed
Pull Request — dev (#836)
by Uwe
03:17 queued 01:53
created

solph.components.experimental._generic_caes   A

Complexity

Total Complexity 9

Size/Duplication

Total Lines 702
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 9
eloc 224
dl 0
loc 702
rs 10
c 0
b 0
f 0

4 Methods

Rating   Name   Duplication   Size   Complexity  
A GenericCAES.constraint_group() 0 2 1
C GenericCAESBlock._create() 0 377 6
A GenericCAESBlock.__init__() 0 2 1
A GenericCAES.__init__() 0 13 1
1
# -*- coding: utf-8 -*-
2
3
"""
4
In-development generic compressed air energy storage.
5
6
SPDX-FileCopyrightText: Uwe Krien <[email protected]>
7
SPDX-FileCopyrightText: Simon Hilpert
8
SPDX-FileCopyrightText: Cord Kaldemeyer
9
SPDX-FileCopyrightText: Patrik Schönfeldt
10
SPDX-FileCopyrightText: Johannes Röder
11
SPDX-FileCopyrightText: jakob-wo
12
SPDX-FileCopyrightText: gplssm
13
SPDX-FileCopyrightText: jnnr
14
15
SPDX-License-Identifier: MIT
16
17
"""
18
19
from oemof.network import network as on
20
from pyomo.core.base.block import ScalarBlock
21
from pyomo.environ import Binary
22
from pyomo.environ import Constraint
23
from pyomo.environ import NonNegativeReals
24
from pyomo.environ import Set
25
from pyomo.environ import Var
26
27
28
class GenericCAES(on.Transformer):
29
    """
30
    Component `GenericCAES` to model arbitrary compressed air energy storages.
31
32
    The full set of equations is described in:
33
    Kaldemeyer, C.; Boysen, C.; Tuschy, I.
34
    A Generic Formulation of Compressed Air Energy Storage as
35
    Mixed Integer Linear Program – Unit Commitment of Specific
36
    Technical Concepts in Arbitrary Market Environments
37
    Materials Today: Proceedings 00 (2018) 0000–0000
38
    [currently in review]
39
40
    Parameters
41
    ----------
42
    electrical_input : dict
43
        Dictionary with key-value-pair of `oemof.Bus` and `oemof.Flow` object
44
        for the electrical input.
45
    fuel_input : dict
46
        Dictionary with key-value-pair of `oemof.Bus` and `oemof.Flow` object
47
        for the fuel input.
48
    electrical_output : dict
49
        Dictionary with key-value-pair of `oemof.Bus` and `oemof.Flow` object
50
        for the electrical output.
51
52
    Note: This component is experimental. Use it with care.
53
54
    Notes
55
    -----
56
    The following sets, variables, constraints and objective parts are created
57
     * :py:class:`~oemof.solph.blocks.generic_caes.GenericCAES`
58
59
    TODO: Add description for constraints. See referenced paper until then!
60
61
    Examples
62
    --------
63
64
    >>> from oemof import solph
65
    >>> bel = solph.buses.Bus(label='bel')
66
    >>> bth = solph.buses.Bus(label='bth')
67
    >>> bgas = solph.buses.Bus(label='bgas')
68
    >>> # dictionary with parameters for a specific CAES plant
69
    >>> concept = {
70
    ...    'cav_e_in_b': 0,
71
    ...    'cav_e_in_m': 0.6457267578,
72
    ...    'cav_e_out_b': 0,
73
    ...    'cav_e_out_m': 0.3739636077,
74
    ...    'cav_eta_temp': 1.0,
75
    ...    'cav_level_max': 211.11,
76
    ...    'cmp_p_max_b': 86.0918959849,
77
    ...    'cmp_p_max_m': 0.0679999932,
78
    ...    'cmp_p_min': 1,
79
    ...    'cmp_q_out_b': -19.3996965679,
80
    ...    'cmp_q_out_m': 1.1066036114,
81
    ...    'cmp_q_tes_share': 0,
82
    ...    'exp_p_max_b': 46.1294016678,
83
    ...    'exp_p_max_m': 0.2528340303,
84
    ...    'exp_p_min': 1,
85
    ...    'exp_q_in_b': -2.2073411014,
86
    ...    'exp_q_in_m': 1.129249765,
87
    ...    'exp_q_tes_share': 0,
88
    ...    'tes_eta_temp': 1.0,
89
    ...    'tes_level_max': 0.0}
90
    >>> # generic compressed air energy storage (caes) plant
91
    >>> caes = solph.components.experimental.GenericCAES(
92
    ...    label='caes',
93
    ...    electrical_input={bel: solph.flows.Flow()},
94
    ...    fuel_input={bgas: solph.flows.Flow()},
95
    ...    electrical_output={bel: solph.flows.Flow()},
96
    ...    params=concept, fixed_costs=0)
97
    >>> type(caes)
98
    <class 'oemof.solph.components.experimental._generic_caes.GenericCAES'>
99
    """
100
101
    def __init__(self, *args, **kwargs):
102
103
        super().__init__(*args, **kwargs)
104
105
        self.electrical_input = kwargs.get("electrical_input")
106
        self.fuel_input = kwargs.get("fuel_input")
107
        self.electrical_output = kwargs.get("electrical_output")
108
        self.params = kwargs.get("params")
109
110
        # map specific flows to standard API
111
        self.inputs.update(kwargs.get("electrical_input"))
112
        self.inputs.update(kwargs.get("fuel_input"))
113
        self.outputs.update(kwargs.get("electrical_output"))
114
115
    def constraint_group(self):
116
        return GenericCAESBlock
117
118
119
class GenericCAESBlock(ScalarBlock):
120
    r"""Block for nodes of class:`.GenericCAES`.
121
122
    Note: This component is experimental. Use it with care.
123
124
    **The following constraints are created:**
125
126
    .. _GenericCAES-equations:
127
128
    .. math::
129
        &
130
        (1) \qquad P_{cmp}(t) = electrical\_input (t)
131
            \quad \forall t \in T \\
132
        &
133
        (2) \qquad P_{cmp\_max}(t) = m_{cmp\_max} \cdot CAS_{fil}(t-1)
134
            + b_{cmp\_max}
135
            \quad \forall t \in\left[1, t_{max}\right] \\
136
        &
137
        (3) \qquad P_{cmp\_max}(t) = b_{cmp\_max}
138
            \quad \forall t \notin\left[1, t_{max}\right] \\
139
        &
140
        (4) \qquad P_{cmp}(t) \leq P_{cmp\_max}(t)
141
            \quad \forall t \in T  \\
142
        &
143
        (5) \qquad P_{cmp}(t) \geq P_{cmp\_min} \cdot ST_{cmp}(t)
144
            \quad \forall t \in T  \\
145
        &
146
        (6) \qquad P_{cmp}(t) = m_{cmp\_max} \cdot CAS_{fil\_max}
147
            + b_{cmp\_max} \cdot ST_{cmp}(t)
148
            \quad \forall t \in T \\
149
        &
150
        (7) \qquad \dot{Q}_{cmp}(t) =
151
            m_{cmp\_q} \cdot P_{cmp}(t) + b_{cmp\_q} \cdot ST_{cmp}(t)
152
            \quad \forall t \in T  \\
153
        &
154
        (8) \qquad \dot{Q}_{cmp}(t) = \dot{Q}_{cmp_out}(t)
155
            + \dot{Q}_{tes\_in}(t)
156
            \quad \forall t \in T \\
157
        &
158
        (9) \qquad r_{cmp\_tes} \cdot\dot{Q}_{cmp\_out}(t) =
159
            \left(1-r_{cmp\_tes}\right) \dot{Q}_{tes\_in}(t)
160
            \quad \forall t \in T \\
161
        &
162
        (10) \quad\; P_{exp}(t) = electrical\_output (t)
163
             \quad \forall t \in T \\
164
        &
165
        (11) \quad\; P_{exp\_max}(t) = m_{exp\_max} CAS_{fil}(t-1)
166
             + b_{exp\_max}
167
             \quad \forall t \in\left[1, t_{\max }\right] \\
168
        &
169
        (12) \quad\; P_{exp\_max}(t) = b_{exp\_max}
170
             \quad \forall t \notin\left[1, t_{\max }\right] \\
171
        &
172
        (13) \quad\; P_{exp}(t) \leq P_{exp\_max}(t)
173
             \quad \forall t \in T \\
174
        &
175
        (14) \quad\; P_{exp}(t) \geq P_{exp\_min}(t) \cdot ST_{exp}(t)
176
             \quad \forall t \in T \\
177
        &
178
        (15) \quad\; P_{exp}(t) \leq m_{exp\_max} \cdot CAS_{fil\_max}
179
             + b_{exp\_max} \cdot ST_{exp}(t)
180
             \quad \forall t \in T \\
181
        &
182
        (16) \quad\; \dot{Q}_{exp}(t) = m_{exp\_q} \cdot P_{exp}(t)
183
             + b_{cxp\_q} \cdot ST_{cxp}(t)
184
             \quad \forall t \in T \\
185
        &
186
        (17) \quad\; \dot{Q}_{exp\_in}(t) = fuel\_input(t)
187
             \quad \forall t \in T \\
188
        &
189
        (18) \quad\; \dot{Q}_{exp}(t) = \dot{Q}_{exp\_in}(t)
190
             + \dot{Q}_{tes\_out}(t)+\dot{Q}_{cxp\_add}(t)
191
             \quad \forall t \in T \\
192
        &
193
        (19) \quad\; r_{exp\_tes} \cdot \dot{Q}_{exp\_in}(t) =
194
             (1 - r_{exp\_tes})(\dot{Q}_{tes\_out}(t) + \dot{Q}_{exp\_add}(t))
195
             \quad \forall t \in T \\
196
        &
197
        (20) \quad\; \dot{E}_{cas\_in}(t) = m_{cas\_in}\cdot P_{cmp}(t)
198
             + b_{cas\_in}\cdot ST_{cmp}(t)
199
             \quad \forall t \in T \\
200
        &
201
        (21) \quad\; \dot{E}_{cas\_out}(t) = m_{cas\_out}\cdot P_{cmp}(t)
202
             + b_{cas\_out}\cdot ST_{cmp}(t)
203
             \quad \forall t \in T \\
204
        &
205
        (22) \quad\; \eta_{cas\_tmp} \cdot CAS_{fil}(t) = CAS_{fil}(t-1)
206
             + \tau\left(\dot{E}_{cas\_in}(t) - \dot{E}_{cas\_out}(t)\right)
207
             \quad \forall t \in\left[1, t_{max}\right] \\
208
         &
209
        (23) \quad\; \eta_{cas\_tmp} \cdot CAS_{fil}(t) =
210
             \tau\left(\dot{E}_{cas\_in}(t) - \dot{E}_{cas\_out}(t)\right)
211
             \quad \forall t \notin\left[1, t_{max}\right] \\
212
        &
213
        (24) \quad\; CAS_{fil}(t) \leq CAS_{fil\_max}
214
             \quad \forall t \in T \\
215
        &
216
        (25) \quad\; TES_{fil}(t) = TES_{fil}(t-1)
217
             + \tau\left(\dot{Q}_{tes\_in}(t)
218
             - \dot{Q}_{tes\_out}(t)\right)
219
             \quad \forall t \in\left[1, t_{max}\right] \\
220
         &
221
        (26) \quad\; TES_{fil}(t) =
222
             \tau\left(\dot{Q}_{tes\_in}(t)
223
             - \dot{Q}_{tes\_out}(t)\right)
224
             \quad \forall t \notin\left[1, t_{max}\right] \\
225
        &
226
        (27) \quad\; TES_{fil}(t) \leq TES_{fil\_max}
227
             \quad \forall t \in T \\
228
        &
229
230
231
    **Table: Symbols and attribute names of variables and parameters**
232
233
    .. csv-table:: Variables (V) and Parameters (P)
234
        :header: "symbol", "attribute", "type", "explanation"
235
        :widths: 1, 1, 1, 1
236
237
        ":math:`ST_{cmp}` ", "`cmp_st[n,t]` ", "V", "Status of
238
        compression"
239
        ":math:`{P}_{cmp}` ", "`cmp_p[n,t]`", "V", "Compression power"
240
        ":math:`{P}_{cmp\_max}`", "`cmp_p_max[n,t]`", "V", "Max.
241
        compression power"
242
        ":math:`\dot{Q}_{cmp}` ", "`cmp_q_out_sum[n,t]`", "V", "Summed
243
         heat flow in compression"
244
        ":math:`\dot{Q}_{cmp\_out}` ", "`cmp_q_waste[n,t]`", "V", "
245
        Waste heat flow from compression"
246
        ":math:`ST_{exp}(t)`", "`exp_st[n,t]`", "V", "Status of
247
        expansion (binary)"
248
        ":math:`P_{exp}(t)`", "`exp_p[n,t]`", "V", "Expansion power"
249
        ":math:`P_{exp\_max}(t)`", "`exp_p_max[n,t]`", "V", "Max.
250
        expansion power"
251
        ":math:`\dot{Q}_{exp}(t)`", "`exp_q_in_sum[n,t]`", "V", "
252
        Summed heat flow in expansion"
253
        ":math:`\dot{Q}_{exp\_in}(t)`", "`exp_q_fuel_in[n,t]`", "V", "
254
        Heat (external) flow into expansion"
255
        ":math:`\dot{Q}_{exp\_add}(t)`", "`exp_q_add_in[n,t]`", "V", "
256
        Additional heat flow into expansion"
257
        ":math:`CAV_{fil}(t)`", "`cav_level[n,t]`", "V", "Filling level
258
         if CAE"
259
        ":math:`\dot{E}_{cas\_in}(t)`", "`cav_e_in[n,t]`", "V", "
260
        Exergy flow into CAS"
261
        ":math:`\dot{E}_{cas\_out}(t)`", "`cav_e_out[n,t]`", "V", "
262
        Exergy flow from CAS"
263
        ":math:`TES_{fil}(t)`", "`tes_level[n,t]`", "V", "Filling
264
        level of Thermal Energy Storage (TES)"
265
        ":math:`\dot{Q}_{tes\_in}(t)`", "`tes_e_in[n,t]`", "V", "Heat
266
         flow into TES"
267
        ":math:`\dot{Q}_{tes\_out}(t)`", "`tes_e_out[n,t]`", "V", "Heat
268
         flow from TES"
269
        ":math:`b_{cmp\_max}`", "`cmp_p_max_b[n,t]`", "P", "Specific
270
         y-intersection"
271
        ":math:`b_{cmp\_q}`", "`cmp_q_out_b[n,t]`", "P", "Specific
272
        y-intersection"
273
        ":math:`b_{exp\_max}`", "`exp_p_max_b[n,t]`", "P", "Specific
274
        y-intersection"
275
        ":math:`b_{exp\_q}`", "`exp_q_in_b[n,t]`", "P", "Specific
276
        y-intersection"
277
        ":math:`b_{cas\_in}`", "`cav_e_in_b[n,t]`", "P", "Specific
278
        y-intersection"
279
        ":math:`b_{cas\_out}`", "`cav_e_out_b[n,t]`", "P", "Specific
280
        y-intersection"
281
        ":math:`m_{cmp\_max}`", "`cmp_p_max_m[n,t]`", "P", "Specific
282
         slope"
283
        ":math:`m_{cmp\_q}`", "`cmp_q_out_m[n,t]`", "P", "Specific
284
         slope"
285
        ":math:`m_{exp\_max}`", "`exp_p_max_m[n,t]`", "P", "Specific
286
         slope"
287
        ":math:`m_{exp\_q}`", "`exp_q_in_m[n,t]`", "P", "Specific
288
         slope"
289
        ":math:`m_{cas\_in}`", "`cav_e_in_m[n,t]`", "P", "Specific
290
         slope"
291
        ":math:`m_{cas\_out}`", "`cav_e_out_m[n,t]`", "P", "Specific
292
         slope"
293
        ":math:`P_{cmp\_min}`", "`cmp_p_min[n,t]`", "P", "Min.
294
        compression power"
295
        ":math:`r_{cmp\_tes}`", "`cmp_q_tes_share[n,t]`", "P", "Ratio
296
         between waste heat flow and heat flow into TES"
297
        ":math:`r_{exp\_tes}`", "`exp_q_tes_share[n,t]`", "P", "Ratio
298
         between external heat flow into expansion and heat flows from TES and
299
          additional source"
300
        ":math:`\tau`", "`m.timeincrement[n,t]`", "P", "Time interval
301
         length"
302
        ":math:`TES_{fil\_max}`", "`tes_level_max[n,t]`", "P", "Max.
303
        filling level of TES"
304
        ":math:`CAS_{fil\_max}`", "`cav_level_max[n,t]`", "P", "Max.
305
         filling level of TES"
306
        ":math:`\tau`", "`cav_eta_tmp[n,t]`", "P", "Temporal efficiency
307
         (loss factor to take intertemporal losses into account)"
308
        ":math:`electrical\_input`", "
309
        `flow[list(n.electrical_input.keys())[0], n, t]`", "P", "
310
        Electr. power input into compression"
311
        ":math:`electrical\_output`", "
312
        `flow[n, list(n.electrical_output.keys())[0], t]`", "P", "
313
        Electr. power output of expansion"
314
        ":math:`fuel\_input`", "
315
        `flow[list(n.fuel_input.keys())[0], n, t]`", "P", "Heat input
316
         (external) into Expansion"
317
318
    """
319
320
    CONSTRAINT_GROUP = True
321
322
    def __init__(self, *args, **kwargs):
323
        super().__init__(*args, **kwargs)
324
325
    def _create(self, group=None):
326
        """
327
        Create constraints for GenericCAESBlock.
328
329
        Parameters
330
        ----------
331
        group : list
332
            List containing `.GenericCAES` objects.
333
            e.g. groups=[gcaes1, gcaes2,..]
334
        """
335
        m = self.parent_block()
336
337
        if group is None:
338
            return None
339
340
        self.GENERICCAES = Set(initialize=[n for n in group])
341
342
        # Compression: Binary variable for operation status
343
        self.cmp_st = Var(self.GENERICCAES, m.TIMESTEPS, within=Binary)
344
345
        # Compression: Realized capacity
346
        self.cmp_p = Var(
347
            self.GENERICCAES, m.TIMESTEPS, within=NonNegativeReals
348
        )
349
350
        # Compression: Max. Capacity
351
        self.cmp_p_max = Var(
352
            self.GENERICCAES, m.TIMESTEPS, within=NonNegativeReals
353
        )
354
355
        # Compression: Heat flow
356
        self.cmp_q_out_sum = Var(
357
            self.GENERICCAES, m.TIMESTEPS, within=NonNegativeReals
358
        )
359
360
        # Compression: Waste heat
361
        self.cmp_q_waste = Var(
362
            self.GENERICCAES, m.TIMESTEPS, within=NonNegativeReals
363
        )
364
365
        # Expansion: Binary variable for operation status
366
        self.exp_st = Var(self.GENERICCAES, m.TIMESTEPS, within=Binary)
367
368
        # Expansion: Realized capacity
369
        self.exp_p = Var(
370
            self.GENERICCAES, m.TIMESTEPS, within=NonNegativeReals
371
        )
372
373
        # Expansion: Max. Capacity
374
        self.exp_p_max = Var(
375
            self.GENERICCAES, m.TIMESTEPS, within=NonNegativeReals
376
        )
377
378
        # Expansion: Heat flow of natural gas co-firing
379
        self.exp_q_in_sum = Var(
380
            self.GENERICCAES, m.TIMESTEPS, within=NonNegativeReals
381
        )
382
383
        # Expansion: Heat flow of natural gas co-firing
384
        self.exp_q_fuel_in = Var(
385
            self.GENERICCAES, m.TIMESTEPS, within=NonNegativeReals
386
        )
387
388
        # Expansion: Heat flow of additional firing
389
        self.exp_q_add_in = Var(
390
            self.GENERICCAES, m.TIMESTEPS, within=NonNegativeReals
391
        )
392
393
        # Cavern: Filling levelh
394
        self.cav_level = Var(
395
            self.GENERICCAES, m.TIMESTEPS, within=NonNegativeReals
396
        )
397
398
        # Cavern: Energy inflow
399
        self.cav_e_in = Var(
400
            self.GENERICCAES, m.TIMESTEPS, within=NonNegativeReals
401
        )
402
403
        # Cavern: Energy outflow
404
        self.cav_e_out = Var(
405
            self.GENERICCAES, m.TIMESTEPS, within=NonNegativeReals
406
        )
407
408
        # TES: Filling levelh
409
        self.tes_level = Var(
410
            self.GENERICCAES, m.TIMESTEPS, within=NonNegativeReals
411
        )
412
413
        # TES: Energy inflow
414
        self.tes_e_in = Var(
415
            self.GENERICCAES, m.TIMESTEPS, within=NonNegativeReals
416
        )
417
418
        # TES: Energy outflow
419
        self.tes_e_out = Var(
420
            self.GENERICCAES, m.TIMESTEPS, within=NonNegativeReals
421
        )
422
423
        # Spot market: Positive capacity
424
        self.exp_p_spot = Var(
425
            self.GENERICCAES, m.TIMESTEPS, within=NonNegativeReals
426
        )
427
428
        # Spot market: Negative capacity
429
        self.cmp_p_spot = Var(
430
            self.GENERICCAES, m.TIMESTEPS, within=NonNegativeReals
431
        )
432
433
        # Compression: Capacity on markets
434
        def cmp_p_constr_rule(block, n, t):
435
            expr = 0
436
            expr += -self.cmp_p[n, t]
437
            expr += m.flow[list(n.electrical_input.keys())[0], n, t]
438
            return expr == 0
439
440
        self.cmp_p_constr = Constraint(
441
            self.GENERICCAES, m.TIMESTEPS, rule=cmp_p_constr_rule
442
        )
443
444
        # Compression: Max. capacity depending on cavern filling level
445
        def cmp_p_max_constr_rule(block, n, t):
446
            if t != 0:
447
                return (
448
                    self.cmp_p_max[n, t]
449
                    == n.params["cmp_p_max_m"] * self.cav_level[n, t - 1]
450
                    + n.params["cmp_p_max_b"]
451
                )
452
            else:
453
                return self.cmp_p_max[n, t] == n.params["cmp_p_max_b"]
454
455
        self.cmp_p_max_constr = Constraint(
456
            self.GENERICCAES, m.TIMESTEPS, rule=cmp_p_max_constr_rule
457
        )
458
459
        def cmp_p_max_area_constr_rule(block, n, t):
460
            return self.cmp_p[n, t] <= self.cmp_p_max[n, t]
461
462
        self.cmp_p_max_area_constr = Constraint(
463
            self.GENERICCAES, m.TIMESTEPS, rule=cmp_p_max_area_constr_rule
464
        )
465
466
        # Compression: Status of operation (on/off)
467
        def cmp_st_p_min_constr_rule(block, n, t):
468
            return (
469
                self.cmp_p[n, t] >= n.params["cmp_p_min"] * self.cmp_st[n, t]
470
            )
471
472
        self.cmp_st_p_min_constr = Constraint(
473
            self.GENERICCAES, m.TIMESTEPS, rule=cmp_st_p_min_constr_rule
474
        )
475
476
        def cmp_st_p_max_constr_rule(block, n, t):
477
            return (
478
                self.cmp_p[n, t]
479
                <= (
480
                    n.params["cmp_p_max_m"] * n.params["cav_level_max"]
481
                    + n.params["cmp_p_max_b"]
482
                )
483
                * self.cmp_st[n, t]
484
            )
485
486
        self.cmp_st_p_max_constr = Constraint(
487
            self.GENERICCAES, m.TIMESTEPS, rule=cmp_st_p_max_constr_rule
488
        )
489
490
        # (7) Compression: Heat flow out
491
        def cmp_q_out_constr_rule(block, n, t):
492
            return (
493
                self.cmp_q_out_sum[n, t]
494
                == n.params["cmp_q_out_m"] * self.cmp_p[n, t]
495
                + n.params["cmp_q_out_b"] * self.cmp_st[n, t]
496
            )
497
498
        self.cmp_q_out_constr = Constraint(
499
            self.GENERICCAES, m.TIMESTEPS, rule=cmp_q_out_constr_rule
500
        )
501
502
        #  (8) Compression: Definition of single heat flows
503
        def cmp_q_out_sum_constr_rule(block, n, t):
504
            return (
505
                self.cmp_q_out_sum[n, t]
506
                == self.cmp_q_waste[n, t] + self.tes_e_in[n, t]
507
            )
508
509
        self.cmp_q_out_sum_constr = Constraint(
510
            self.GENERICCAES, m.TIMESTEPS, rule=cmp_q_out_sum_constr_rule
511
        )
512
513
        # (9) Compression: Heat flow out ratio
514
        def cmp_q_out_shr_constr_rule(block, n, t):
515
            return self.cmp_q_waste[n, t] * n.params[
516
                "cmp_q_tes_share"
517
            ] == self.tes_e_in[n, t] * (1 - n.params["cmp_q_tes_share"])
518
519
        self.cmp_q_out_shr_constr = Constraint(
520
            self.GENERICCAES, m.TIMESTEPS, rule=cmp_q_out_shr_constr_rule
521
        )
522
523
        # (10) Expansion: Capacity on markets
524
        def exp_p_constr_rule(block, n, t):
525
            expr = 0
526
            expr += -self.exp_p[n, t]
527
            expr += m.flow[n, list(n.electrical_output.keys())[0], t]
528
            return expr == 0
529
530
        self.exp_p_constr = Constraint(
531
            self.GENERICCAES, m.TIMESTEPS, rule=exp_p_constr_rule
532
        )
533
534
        # (11-12) Expansion: Max. capacity depending on cavern filling level
535
        def exp_p_max_constr_rule(block, n, t):
536
            if t != 0:
537
                return (
538
                    self.exp_p_max[n, t]
539
                    == n.params["exp_p_max_m"] * self.cav_level[n, t - 1]
540
                    + n.params["exp_p_max_b"]
541
                )
542
            else:
543
                return self.exp_p_max[n, t] == n.params["exp_p_max_b"]
544
545
        self.exp_p_max_constr = Constraint(
546
            self.GENERICCAES, m.TIMESTEPS, rule=exp_p_max_constr_rule
547
        )
548
549
        # (13)
550
        def exp_p_max_area_constr_rule(block, n, t):
551
            return self.exp_p[n, t] <= self.exp_p_max[n, t]
552
553
        self.exp_p_max_area_constr = Constraint(
554
            self.GENERICCAES, m.TIMESTEPS, rule=exp_p_max_area_constr_rule
555
        )
556
557
        # (14) Expansion: Status of operation (on/off)
558
        def exp_st_p_min_constr_rule(block, n, t):
559
            return (
560
                self.exp_p[n, t] >= n.params["exp_p_min"] * self.exp_st[n, t]
561
            )
562
563
        self.exp_st_p_min_constr = Constraint(
564
            self.GENERICCAES, m.TIMESTEPS, rule=exp_st_p_min_constr_rule
565
        )
566
567
        # (15)
568
        def exp_st_p_max_constr_rule(block, n, t):
569
            return (
570
                self.exp_p[n, t]
571
                <= (
572
                    n.params["exp_p_max_m"] * n.params["cav_level_max"]
573
                    + n.params["exp_p_max_b"]
574
                )
575
                * self.exp_st[n, t]
576
            )
577
578
        self.exp_st_p_max_constr = Constraint(
579
            self.GENERICCAES, m.TIMESTEPS, rule=exp_st_p_max_constr_rule
580
        )
581
582
        # (16) Expansion: Heat flow in
583
        def exp_q_in_constr_rule(block, n, t):
584
            return (
585
                self.exp_q_in_sum[n, t]
586
                == n.params["exp_q_in_m"] * self.exp_p[n, t]
587
                + n.params["exp_q_in_b"] * self.exp_st[n, t]
588
            )
589
590
        self.exp_q_in_constr = Constraint(
591
            self.GENERICCAES, m.TIMESTEPS, rule=exp_q_in_constr_rule
592
        )
593
594
        # (17) Expansion: Fuel allocation
595
        def exp_q_fuel_constr_rule(block, n, t):
596
            expr = 0
597
            expr += -self.exp_q_fuel_in[n, t]
598
            expr += m.flow[list(n.fuel_input.keys())[0], n, t]
599
            return expr == 0
600
601
        self.exp_q_fuel_constr = Constraint(
602
            self.GENERICCAES, m.TIMESTEPS, rule=exp_q_fuel_constr_rule
603
        )
604
605
        # (18) Expansion: Definition of single heat flows
606
        def exp_q_in_sum_constr_rule(block, n, t):
607
            return (
608
                self.exp_q_in_sum[n, t]
609
                == self.exp_q_fuel_in[n, t]
610
                + self.tes_e_out[n, t]
611
                + self.exp_q_add_in[n, t]
612
            )
613
614
        self.exp_q_in_sum_constr = Constraint(
615
            self.GENERICCAES, m.TIMESTEPS, rule=exp_q_in_sum_constr_rule
616
        )
617
618
        # (19) Expansion: Heat flow in ratio
619
        def exp_q_in_shr_constr_rule(block, n, t):
620
            return n.params["exp_q_tes_share"] * self.exp_q_fuel_in[n, t] == (
621
                1 - n.params["exp_q_tes_share"]
622
            ) * (self.exp_q_add_in[n, t] + self.tes_e_out[n, t])
623
624
        self.exp_q_in_shr_constr = Constraint(
625
            self.GENERICCAES, m.TIMESTEPS, rule=exp_q_in_shr_constr_rule
626
        )
627
628
        # (20) Cavern: Energy inflow
629
        def cav_e_in_constr_rule(block, n, t):
630
            return (
631
                self.cav_e_in[n, t]
632
                == n.params["cav_e_in_m"] * self.cmp_p[n, t]
633
                + n.params["cav_e_in_b"]
634
            )
635
636
        self.cav_e_in_constr = Constraint(
637
            self.GENERICCAES, m.TIMESTEPS, rule=cav_e_in_constr_rule
638
        )
639
640
        # (21) Cavern: Energy outflow
641
        def cav_e_out_constr_rule(block, n, t):
642
            return (
643
                self.cav_e_out[n, t]
644
                == n.params["cav_e_out_m"] * self.exp_p[n, t]
645
                + n.params["cav_e_out_b"]
646
            )
647
648
        self.cav_e_out_constr = Constraint(
649
            self.GENERICCAES, m.TIMESTEPS, rule=cav_e_out_constr_rule
650
        )
651
652
        # (22-23) Cavern: Storage balance
653
        def cav_eta_constr_rule(block, n, t):
654
            if t != 0:
655
                return n.params["cav_eta_temp"] * self.cav_level[
656
                    n, t
657
                ] == self.cav_level[n, t - 1] + m.timeincrement[t] * (
658
                    self.cav_e_in[n, t] - self.cav_e_out[n, t]
659
                )
660
            else:
661
                return n.params["cav_eta_temp"] * self.cav_level[
662
                    n, t
663
                ] == m.timeincrement[t] * (
664
                    self.cav_e_in[n, t] - self.cav_e_out[n, t]
665
                )
666
667
        self.cav_eta_constr = Constraint(
668
            self.GENERICCAES, m.TIMESTEPS, rule=cav_eta_constr_rule
669
        )
670
671
        # (24) Cavern: Upper bound
672
        def cav_ub_constr_rule(block, n, t):
673
            return self.cav_level[n, t] <= n.params["cav_level_max"]
674
675
        self.cav_ub_constr = Constraint(
676
            self.GENERICCAES, m.TIMESTEPS, rule=cav_ub_constr_rule
677
        )
678
679
        # (25-26) TES: Storage balance
680
        def tes_eta_constr_rule(block, n, t):
681
            if t != 0:
682
                return self.tes_level[n, t] == self.tes_level[
683
                    n, t - 1
684
                ] + m.timeincrement[t] * (
685
                    self.tes_e_in[n, t] - self.tes_e_out[n, t]
686
                )
687
            else:
688
                return self.tes_level[n, t] == m.timeincrement[t] * (
689
                    self.tes_e_in[n, t] - self.tes_e_out[n, t]
690
                )
691
692
        self.tes_eta_constr = Constraint(
693
            self.GENERICCAES, m.TIMESTEPS, rule=tes_eta_constr_rule
694
        )
695
696
        # (27) TES: Upper bound
697
        def tes_ub_constr_rule(block, n, t):
698
            return self.tes_level[n, t] <= n.params["tes_level_max"]
699
700
        self.tes_ub_constr = Constraint(
701
            self.GENERICCAES, m.TIMESTEPS, rule=tes_ub_constr_rule
702
        )
703