solph.components._generic_chp   A
last analyzed

Complexity

Total Complexity 17

Size/Duplication

Total Lines 506
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 17
eloc 194
dl 0
loc 506
rs 10
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A GenericCHPBlock.__init__() 0 2 1
A GenericCHPBlock._objective_expression() 0 10 2
C GenericCHPBlock._create() 0 186 5
A GenericCHP._calculate_alphas() 0 50 4
A GenericCHP.alphas() 0 6 2
A GenericCHP.constraint_group() 0 2 1
A GenericCHP.__init__() 0 28 2
1
# -*- coding: utf-8 -
2
3
"""
4
GenericCHP and associated individual constraints (blocks) and groupings.
5
6
SPDX-FileCopyrightText: Uwe Krien <[email protected]>
7
SPDX-FileCopyrightText: Simon Hilpert
8
SPDX-FileCopyrightText: Cord Kaldemeyer
9
SPDX-FileCopyrightText: Patrik Schönfeldt
10
SPDX-FileCopyrightText: FranziPl
11
SPDX-FileCopyrightText: jnnr
12
SPDX-FileCopyrightText: Stephan Günther
13
SPDX-FileCopyrightText: FabianTU
14
SPDX-FileCopyrightText: Johannes Röder
15
SPDX-FileCopyrightText: Johannes Kochems
16
17
SPDX-License-Identifier: MIT
18
19
"""
20
21
import numpy as np
22
from oemof.network import Node
23
from pyomo.core.base.block import ScalarBlock
24
from pyomo.environ import Binary
25
from pyomo.environ import Constraint
26
from pyomo.environ import NonNegativeReals
27
from pyomo.environ import Set
28
from pyomo.environ import Var
29
30
from oemof.solph._plumbing import sequence
31
32
33
class GenericCHP(Node):
34
    r"""
35
    Component `GenericCHP` to model combined heat and power plants.
36
37
    Can be used to model (combined cycle) extraction or back-pressure turbines
38
    and used a mixed-integer linear formulation. Thus, it induces more
39
    computational effort than the `ExtractionTurbineCHP` for the
40
    benefit of higher accuracy.
41
42
    The full set of equations is described in:
43
    Mollenhauer, E., Christidis, A. & Tsatsaronis, G.
44
    Evaluation of an energy- and exergy-based generic modeling
45
    approach of combined heat and power plants
46
    Int J Energy Environ Eng (2016) 7: 167.
47
    https://doi.org/10.1007/s40095-016-0204-6
48
49
    For a general understanding of (MI)LP CHP representation, see:
50
    Fabricio I. Salgado, P.
51
    Short - Term Operation Planning on Cogeneration Systems: A Survey
52
    Electric Power Systems Research (2007)
53
    Electric Power Systems Research
54
    Volume 78, Issue 5, May 2008, Pages 835-848
55
    https://doi.org/10.1016/j.epsr.2007.06.001
56
57
    Note
58
    ----
59
    * An adaption for the flow parameter `H_L_FG_share_max` has been made to
60
      set the flue gas losses at maximum heat extraction `H_L_FG_max` as share
61
      of the fuel flow `H_F` e.g. for combined cycle extraction turbines.
62
    * The flow parameter `H_L_FG_share_min` can be used to set the flue gas
63
      losses at minimum heat extraction `H_L_FG_min` as share of
64
      the fuel flow `H_F` e.g. for motoric CHPs.
65
    * The boolean component parameter `back_pressure` can be set to model
66
      back-pressure characteristics.
67
68
    Also have a look at the examples on how to use it.
69
70
    Parameters
71
    ----------
72
    fuel_input : dict
73
        Dictionary with key-value-pair of `oemof.solph.Bus` and
74
        `oemof.solph.Flow` objects for the fuel input.
75
    electrical_output : dict
76
        Dictionary with key-value-pair of `oemof.solph.Bus` and
77
        `oemof.solph.Flow` objects for the electrical output.
78
        Related parameters like `P_max_woDH` are passed as
79
        attributes of the `oemof.Flow` object.
80
    heat_output : dict
81
        Dictionary with key-value-pair of `oemof.solph.Bus`
82
        and `oemof.solph.Flow` objects for the heat output.
83
        Related parameters like `Q_CW_min` are passed as
84
        attributes of the `oemof.Flow` object.
85
    beta : list of numerical values
86
        beta values in same dimension as all other parameters (length of
87
        optimization period).
88
    back_pressure : boolean
89
        Flag to use back-pressure characteristics. Set to `True` and
90
        `Q_CW_min` to zero for back-pressure turbines. See paper above for more
91
        information.
92
93
    Note
94
    ----
95
    The following sets, variables, constraints and objective parts are created
96
     * :py:class:`~oemof.solph.components._generic_chp.GenericCHPBlock`
97
98
    Examples
99
    --------
100
    >>> from oemof import solph
101
    >>> bel = solph.buses.Bus(label='electricityBus')
102
    >>> bth = solph.buses.Bus(label='heatBus')
103
    >>> bgas = solph.buses.Bus(label='commodityBus')
104
    >>> ccet = solph.components.GenericCHP(
105
    ...    label='combined_cycle_extraction_turbine',
106
    ...    fuel_input={bgas: solph.flows.Flow(
107
    ...        custom_attributes={"H_L_FG_share_max": [0.183]})},
108
    ...    electrical_output={bel: solph.flows.Flow(
109
    ...        custom_attributes={
110
    ...            "P_max_woDH": [155.946],
111
    ...            "P_min_woDH": [68.787],
112
    ...            "Eta_el_max_woDH": [0.525],
113
    ...            "Eta_el_min_woDH": [0.444],
114
    ...        })},
115
    ...    heat_output={bth: solph.flows.Flow(
116
    ...        custom_attributes={"Q_CW_min": [10.552]})},
117
    ...    beta=[0.122], back_pressure=False)
118
    >>> type(ccet)
119
    <class 'oemof.solph.components._generic_chp.GenericCHP'>
120
    """
121
122
    def __init__(
123
        self,
124
        fuel_input,
125
        electrical_output,
126
        heat_output,
127
        beta,
128
        back_pressure,
129
        label=None,
130
        custom_properties=None,
131
    ):
132
        if custom_properties is None:
133
            custom_properties = {}
134
        super().__init__(label, custom_properties=custom_properties)
135
136
        self.fuel_input = fuel_input
137
        self.electrical_output = electrical_output
138
        self.heat_output = heat_output
139
        self.beta = sequence(beta)
140
        self.back_pressure = back_pressure
141
        self._alphas = None
142
143
        # map specific flows to standard API
144
        fuel_bus = list(self.fuel_input.keys())[0]
145
        fuel_flow = list(self.fuel_input.values())[0]
146
        fuel_bus.outputs.update({self: fuel_flow})
147
148
        self.outputs.update(electrical_output)
149
        self.outputs.update(heat_output)
150
151
    def _calculate_alphas(self):
152
        """
153
        Calculate alpha coefficients.
154
155
        A system of linear equations is created from passed capacities and
156
        efficiencies and solved to calculate both coefficients.
157
        """
158
        alphas = [[], []]
159
160
        eb = list(self.electrical_output.keys())[0]
161
162
        attrs = [
163
            self.electrical_output[eb].P_min_woDH,
164
            self.electrical_output[eb].Eta_el_min_woDH,
165
            self.electrical_output[eb].P_max_woDH,
166
            self.electrical_output[eb].Eta_el_max_woDH,
167
        ]
168
169
        length = [len(a) for a in attrs if not isinstance(a, (int, float))]
170
        max_length = max(length)
171
172
        if all(len(a) == max_length for a in attrs):
173
            if max_length == 0:
174
                max_length += 1  # increment dimension for scalars from 0 to 1
175
            for i in range(0, max_length):
176
                A = np.array(
177
                    [
178
                        [1, self.electrical_output[eb].P_min_woDH[i]],
179
                        [1, self.electrical_output[eb].P_max_woDH[i]],
180
                    ]
181
                )
182
                b = np.array(
183
                    [
184
                        self.electrical_output[eb].P_min_woDH[i]
185
                        / self.electrical_output[eb].Eta_el_min_woDH[i],
186
                        self.electrical_output[eb].P_max_woDH[i]
187
                        / self.electrical_output[eb].Eta_el_max_woDH[i],
188
                    ]
189
                )
190
                x = np.linalg.solve(A, b)
191
                alphas[0].append(x[0])
192
                alphas[1].append(x[1])
193
        else:
194
            error_message = (
195
                "Attributes to calculate alphas "
196
                + "must be of same dimension."
197
            )
198
            raise ValueError(error_message)
199
200
        self._alphas = alphas
201
202
    @property
203
    def alphas(self):
204
        """Compute or return the _alphas attribute."""
205
        if self._alphas is None:
206
            self._calculate_alphas()
207
        return self._alphas
208
209
    def constraint_group(self):
210
        return GenericCHPBlock
211
212
213
class GenericCHPBlock(ScalarBlock):
214
    r"""
215
    Block for the relation of the :math:`n` nodes with
216
    type class:`.GenericCHP`.
217
218
    **The following constraints are created:**
219
220
    .. _GenericCHP-equations1-10:
221
222
    .. math::
223
        &
224
        (1)\qquad \dot{H}_F(t) = fuel\ input \\
225
        &
226
        (2)\qquad \dot{Q}(t) = heat\ output \\
227
        &
228
        (3)\qquad P_{el}(t) = power\ output\\
229
        &
230
        (4)\qquad \dot{H}_F(t) = \alpha_0(t) \cdot Y(t) + \alpha_1(t) \cdot
231
        P_{el,woDH}(t)\\
232
        &
233
        (5)\qquad \dot{H}_F(t) = \alpha_0(t) \cdot Y(t) + \alpha_1(t) \cdot
234
        ( P_{el}(t) + \beta \cdot \dot{Q}(t) )\\
235
        &
236
        (6)\qquad \dot{H}_F(t) \leq Y(t) \cdot
237
        \frac{P_{el, max, woDH}(t)}{\eta_{el,max,woDH}(t)}\\
238
        &
239
        (7)\qquad \dot{H}_F(t) \geq Y(t) \cdot
240
        \frac{P_{el, min, woDH}(t)}{\eta_{el,min,woDH}(t)}\\
241
        &
242
        (8)\qquad \dot{H}_{L,FG,max}(t) = \dot{H}_F(t) \cdot
243
        \dot{H}_{L,FG,sharemax}(t)\\
244
        &
245
        (9)\qquad \dot{H}_{L,FG,min}(t) = \dot{H}_F(t) \cdot
246
        \dot{H}_{L,FG,sharemin}(t)\\
247
        &
248
        (10)\qquad P_{el}(t) + \dot{Q}(t) + \dot{H}_{L,FG,max}(t) +
249
        \dot{Q}_{CW, min}(t) \cdot Y(t) = / \leq \dot{H}_F(t)\\
250
251
    where :math:`= / \leq` depends on the CHP being back pressure or not.
252
253
    The coefficients :math:`\alpha_0` and :math:`\alpha_1`
254
    can be determined given the efficiencies maximal/minimal load:
255
256
    .. math::
257
        &
258
        \eta_{el,max,woDH}(t) = \frac{P_{el,max,woDH}(t)}{\alpha_0(t)
259
        \cdot Y(t) + \alpha_1(t) \cdot P_{el,max,woDH}(t)}\\
260
        &
261
        \eta_{el,min,woDH}(t) = \frac{P_{el,min,woDH}(t)}{\alpha_0(t)
262
        \cdot Y(t) + \alpha_1(t) \cdot P_{el,min,woDH}(t)}\\
263
264
265
    **For the attribute** :math:`\dot{H}_{L,FG,min}` **being not None**,
266
    e.g. for a motoric CHP, **the following is created:**
267
268
        **Constraint:**
269
270
    .. _GenericCHP-equations11:
271
272
    .. math::
273
        &
274
        (11)\qquad P_{el}(t) + \dot{Q}(t) + \dot{H}_{L,FG,min}(t) +
275
        \dot{Q}_{CW, min}(t) \cdot Y(t) \geq \dot{H}_F(t)\\[10pt]
276
277
    The symbols used are defined as follows (with Variables (V) and Parameters (P)):
278
279
    =============================== ======================= ==== =============================================
280
    math. symbol                    attribute               type explanation
281
    =============================== ======================= ==== =============================================
282
    :math:`\dot{H}_{F}`             `H_F[n,t]`              V    input of enthalpy through fuel input
283
    :math:`P_{el}`                  `P[n,t]`                V    provided electric power
284
    :math:`P_{el,woDH}`             `P_woDH[n,t]`           V    electric power without district heating
285
    :math:`P_{el,min,woDH}`         `P_min_woDH[n,t]`       P    min. electric power without district heating
286
    :math:`P_{el,max,woDH}`         `P_max_woDH[n,t]`       P    max. electric power without district heating
287
    :math:`\dot{Q}`                 `Q[n,t]`                V    provided heat
288
    :math:`\dot{Q}_{CW, min}`       `Q_CW_min[n,t]`         P    minimal therm. condenser load to cooling water
289
    :math:`\dot{H}_{L,FG,min}`      `H_L_FG_min[n,t]`       V    flue gas enthalpy loss at min heat extraction
290
    :math:`\dot{H}_{L,FG,max}`      `H_L_FG_max[n,t]`       V    flue gas enthalpy loss at max heat extraction
291
    :math:`\dot{H}_{L,FG,sharemin}` `H_L_FG_share_min[n,t]` P    share of flue gas loss at min heat extraction
292
    :math:`\dot{H}_{L,FG,sharemax}` `H_L_FG_share_max[n,t]` P    share of flue gas loss at max heat extraction
293
    :math:`Y`                       `Y[n,t]`                V    status variable on/off
294
    :math:`\alpha_0`                `n.alphas[0][n,t]`      P    coefficient describing efficiency
295
    :math:`\alpha_1`                `n.alphas[1][n,t]`      P    coefficient describing efficiency
296
    :math:`\beta`                   `beta[n,t]`             P    power loss index
297
    :math:`\eta_{el,min,woDH}`      `Eta_el_min_woDH[n,t]`  P    el. eff. at min. fuel flow w/o distr. heating
298
    :math:`\eta_{el,max,woDH}`      `Eta_el_max_woDH[n,t]`  P    el. eff. at max. fuel flow w/o distr. heating
299
    =============================== ======================= ==== =============================================
300
301
    """  # noqa: E501
302
303
    CONSTRAINT_GROUP = True
304
305
    def __init__(self, *args, **kwargs):
306
        super().__init__(*args, **kwargs)
307
308
    def _create(self, group=None):
309
        """
310
        Create constraints for GenericCHPBlock.
311
312
        Parameters
313
        ----------
314
        group : list
315
            List containing `GenericCHP` objects.
316
            e.g. groups=[ghcp1, gchp2,..]
317
        """
318
        m = self.parent_block()
319
320
        if group is None:
321
            return None
322
323
        self.GENERICCHPS = Set(initialize=[n for n in group])
324
325
        # variables
326
        self.H_F = Var(self.GENERICCHPS, m.TIMESTEPS, within=NonNegativeReals)
327
        self.H_L_FG_max = Var(
328
            self.GENERICCHPS, m.TIMESTEPS, within=NonNegativeReals
329
        )
330
        self.H_L_FG_min = Var(
331
            self.GENERICCHPS, m.TIMESTEPS, within=NonNegativeReals
332
        )
333
        self.P_woDH = Var(
334
            self.GENERICCHPS, m.TIMESTEPS, within=NonNegativeReals
335
        )
336
        self.P = Var(self.GENERICCHPS, m.TIMESTEPS, within=NonNegativeReals)
337
        self.Q = Var(self.GENERICCHPS, m.TIMESTEPS, within=NonNegativeReals)
338
        self.Y = Var(self.GENERICCHPS, m.TIMESTEPS, within=Binary)
339
340
        # constraint rules
341
        def _H_flow_rule(block, n, t):
342
            """Link fuel consumption to component inflow."""
343
            expr = 0
344
            expr += self.H_F[n, t]
345
            expr += -m.flow[list(n.fuel_input.keys())[0], n, t]
346
            return expr == 0
347
348
        self.H_flow = Constraint(
349
            self.GENERICCHPS, m.TIMESTEPS, rule=_H_flow_rule
350
        )
351
352
        def _Q_flow_rule(block, n, t):
353
            """Link heat flow to component outflow."""
354
            expr = 0
355
            expr += self.Q[n, t]
356
            expr += -m.flow[n, list(n.heat_output.keys())[0], t]
357
            return expr == 0
358
359
        self.Q_flow = Constraint(
360
            self.GENERICCHPS, m.TIMESTEPS, rule=_Q_flow_rule
361
        )
362
363
        def _P_flow_rule(block, n, t):
364
            """Link power flow to component outflow."""
365
            expr = 0
366
            expr += self.P[n, t]
367
            expr += -m.flow[n, list(n.electrical_output.keys())[0], t]
368
            return expr == 0
369
370
        self.P_flow = Constraint(
371
            self.GENERICCHPS, m.TIMESTEPS, rule=_P_flow_rule
372
        )
373
374
        def _H_F_1_rule(block, n, t):
375
            """Set P_woDH depending on H_F."""
376
            expr = 0
377
            expr += -self.H_F[n, t]
378
            expr += n.alphas[0][t] * self.Y[n, t]
379
            expr += n.alphas[1][t] * self.P_woDH[n, t]
380
            return expr == 0
381
382
        self.H_F_1 = Constraint(
383
            self.GENERICCHPS, m.TIMESTEPS, rule=_H_F_1_rule
384
        )
385
386
        def _H_F_2_rule(block, n, t):
387
            """Determine relation between H_F, P and Q."""
388
            expr = 0
389
            expr += -self.H_F[n, t]
390
            expr += n.alphas[0][t] * self.Y[n, t]
391
            expr += n.alphas[1][t] * (self.P[n, t] + n.beta[t] * self.Q[n, t])
392
            return expr == 0
393
394
        self.H_F_2 = Constraint(
395
            self.GENERICCHPS, m.TIMESTEPS, rule=_H_F_2_rule
396
        )
397
398
        def _H_F_3_rule(block, n, t):
399
            """Set upper value of operating range via H_F."""
400
            expr = 0
401
            expr += self.H_F[n, t]
402
            expr += -self.Y[n, t] * (
403
                list(n.electrical_output.values())[0].P_max_woDH[t]
404
                / list(n.electrical_output.values())[0].Eta_el_max_woDH[t]
405
            )
406
            return expr <= 0
407
408
        self.H_F_3 = Constraint(
409
            self.GENERICCHPS, m.TIMESTEPS, rule=_H_F_3_rule
410
        )
411
412
        def _H_F_4_rule(block, n, t):
413
            """Set lower value of operating range via H_F."""
414
            expr = 0
415
            expr += self.H_F[n, t]
416
            expr += -self.Y[n, t] * (
417
                list(n.electrical_output.values())[0].P_min_woDH[t]
418
                / list(n.electrical_output.values())[0].Eta_el_min_woDH[t]
419
            )
420
            return expr >= 0
421
422
        self.H_F_4 = Constraint(
423
            self.GENERICCHPS, m.TIMESTEPS, rule=_H_F_4_rule
424
        )
425
426
        def _H_L_FG_max_rule(block, n, t):
427
            """Set max. flue gas loss as share fuel flow share."""
428
            expr = 0
429
            expr += -self.H_L_FG_max[n, t]
430
            expr += (
431
                self.H_F[n, t]
432
                * list(n.fuel_input.values())[0].H_L_FG_share_max[t]
433
            )
434
            return expr == 0
435
436
        self.H_L_FG_max_def = Constraint(
437
            self.GENERICCHPS, m.TIMESTEPS, rule=_H_L_FG_max_rule
438
        )
439
440
        def _Q_max_res_rule(block, n, t):
441
            """Set maximum Q depending on fuel and electrical flow."""
442
            expr = 0
443
            expr += self.P[n, t] + self.Q[n, t] + self.H_L_FG_max[n, t]
444
            expr += list(n.heat_output.values())[0].Q_CW_min[t] * self.Y[n, t]
445
            expr += -self.H_F[n, t]
446
            # back-pressure characteristics or one-segment model
447
            if n.back_pressure is True:
448
                return expr == 0
449
            else:
450
                return expr <= 0
451
452
        self.Q_max_res = Constraint(
453
            self.GENERICCHPS, m.TIMESTEPS, rule=_Q_max_res_rule
454
        )
455
456
        def _H_L_FG_min_rule(block, n, t):
457
            """Set min. flue gas loss as fuel flow share."""
458
            # minimum flue gas losses e.g. for motoric CHPs
459
            if getattr(
460
                list(n.fuel_input.values())[0], "H_L_FG_share_min", None
461
            ):
462
                expr = 0
463
                expr += -self.H_L_FG_min[n, t]
464
                expr += (
465
                    self.H_F[n, t]
466
                    * list(n.fuel_input.values())[0].H_L_FG_share_min[t]
467
                )
468
                return expr == 0
469
            else:
470
                return Constraint.Skip
471
472
        self.H_L_FG_min_def = Constraint(
473
            self.GENERICCHPS, m.TIMESTEPS, rule=_H_L_FG_min_rule
474
        )
475
476
        def _Q_min_res_rule(block, n, t):
477
            """Set minimum Q depending on fuel and eletrical flow."""
478
            # minimum restriction for heat flows e.g. for motoric CHPs
479
            if getattr(
480
                list(n.fuel_input.values())[0], "H_L_FG_share_min", None
481
            ):
482
                expr = 0
483
                expr += self.P[n, t] + self.Q[n, t] + self.H_L_FG_min[n, t]
484
                expr += (
485
                    list(n.heat_output.values())[0].Q_CW_min[t] * self.Y[n, t]
486
                )
487
                expr += -self.H_F[n, t]
488
                return expr >= 0
489
            else:
490
                return Constraint.Skip
491
492
        self.Q_min_res = Constraint(
493
            self.GENERICCHPS, m.TIMESTEPS, rule=_Q_min_res_rule
494
        )
495
496
    def _objective_expression(self):
497
        r"""Objective expression for generic CHPs with no investment.
498
499
        Note: This adds nothing as variable costs are already
500
        added in the Block :class:`SimpleFlowBlock`.
501
        """
502
        if not hasattr(self, "GENERICCHPS"):
503
            return 0
504
505
        return 0
506