Passed
Pull Request — dev (#1232)
by Patrik
01:49
created

GenericCHP.__init__()   A

Complexity

Conditions 2

Size

Total Lines 31
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 25
dl 0
loc 31
rs 9.28
c 0
b 0
f 0
cc 2
nop 9

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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
        parent_node=None,
130
        label=None,
131
        custom_properties=None,
132
    ):
133
        if custom_properties is None:
134
            custom_properties = {}
135
        super().__init__(
136
            label, parent_node=parent_node, custom_properties=custom_properties
137
        )
138
139
        self.fuel_input = fuel_input
140
        self.electrical_output = electrical_output
141
        self.heat_output = heat_output
142
        self.beta = sequence(beta)
143
        self.back_pressure = back_pressure
144
        self._alphas = None
145
146
        # map specific flows to standard API
147
        fuel_bus = list(self.fuel_input.keys())[0]
148
        fuel_flow = list(self.fuel_input.values())[0]
149
        fuel_bus.outputs.update({self: fuel_flow})
150
151
        self.outputs.update(electrical_output)
152
        self.outputs.update(heat_output)
153
154
    def _calculate_alphas(self):
155
        """
156
        Calculate alpha coefficients.
157
158
        A system of linear equations is created from passed capacities and
159
        efficiencies and solved to calculate both coefficients.
160
        """
161
        alphas = [[], []]
162
163
        eb = list(self.electrical_output.keys())[0]
164
165
        attrs = [
166
            self.electrical_output[eb].P_min_woDH,
167
            self.electrical_output[eb].Eta_el_min_woDH,
168
            self.electrical_output[eb].P_max_woDH,
169
            self.electrical_output[eb].Eta_el_max_woDH,
170
        ]
171
172
        length = [len(a) for a in attrs if not isinstance(a, (int, float))]
173
        max_length = max(length)
174
175
        if all(len(a) == max_length for a in attrs):
176
            if max_length == 0:
177
                max_length += 1  # increment dimension for scalars from 0 to 1
178
            for i in range(0, max_length):
179
                A = np.array(
180
                    [
181
                        [1, self.electrical_output[eb].P_min_woDH[i]],
182
                        [1, self.electrical_output[eb].P_max_woDH[i]],
183
                    ]
184
                )
185
                b = np.array(
186
                    [
187
                        self.electrical_output[eb].P_min_woDH[i]
188
                        / self.electrical_output[eb].Eta_el_min_woDH[i],
189
                        self.electrical_output[eb].P_max_woDH[i]
190
                        / self.electrical_output[eb].Eta_el_max_woDH[i],
191
                    ]
192
                )
193
                x = np.linalg.solve(A, b)
194
                alphas[0].append(x[0])
195
                alphas[1].append(x[1])
196
        else:
197
            error_message = (
198
                "Attributes to calculate alphas "
199
                + "must be of same dimension."
200
            )
201
            raise ValueError(error_message)
202
203
        self._alphas = alphas
204
205
    @property
206
    def alphas(self):
207
        """Compute or return the _alphas attribute."""
208
        if self._alphas is None:
209
            self._calculate_alphas()
210
        return self._alphas
211
212
    def constraint_group(self):
213
        return GenericCHPBlock
214
215
216
class GenericCHPBlock(ScalarBlock):
217
    r"""
218
    Block for the relation of the :math:`n` nodes with
219
    type class:`.GenericCHP`.
220
221
    **The following constraints are created:**
222
223
    .. _GenericCHP-equations1-10:
224
225
    .. math::
226
        &
227
        (1)\qquad \dot{H}_F(t) = fuel\ input \\
228
        &
229
        (2)\qquad \dot{Q}(t) = heat\ output \\
230
        &
231
        (3)\qquad P_{el}(t) = power\ output\\
232
        &
233
        (4)\qquad \dot{H}_F(t) = \alpha_0(t) \cdot Y(t) + \alpha_1(t) \cdot
234
        P_{el,woDH}(t)\\
235
        &
236
        (5)\qquad \dot{H}_F(t) = \alpha_0(t) \cdot Y(t) + \alpha_1(t) \cdot
237
        ( P_{el}(t) + \beta \cdot \dot{Q}(t) )\\
238
        &
239
        (6)\qquad \dot{H}_F(t) \leq Y(t) \cdot
240
        \frac{P_{el, max, woDH}(t)}{\eta_{el,max,woDH}(t)}\\
241
        &
242
        (7)\qquad \dot{H}_F(t) \geq Y(t) \cdot
243
        \frac{P_{el, min, woDH}(t)}{\eta_{el,min,woDH}(t)}\\
244
        &
245
        (8)\qquad \dot{H}_{L,FG,max}(t) = \dot{H}_F(t) \cdot
246
        \dot{H}_{L,FG,sharemax}(t)\\
247
        &
248
        (9)\qquad \dot{H}_{L,FG,min}(t) = \dot{H}_F(t) \cdot
249
        \dot{H}_{L,FG,sharemin}(t)\\
250
        &
251
        (10)\qquad P_{el}(t) + \dot{Q}(t) + \dot{H}_{L,FG,max}(t) +
252
        \dot{Q}_{CW, min}(t) \cdot Y(t) = / \leq \dot{H}_F(t)\\
253
254
    where :math:`= / \leq` depends on the CHP being back pressure or not.
255
256
    The coefficients :math:`\alpha_0` and :math:`\alpha_1`
257
    can be determined given the efficiencies maximal/minimal load:
258
259
    .. math::
260
        &
261
        \eta_{el,max,woDH}(t) = \frac{P_{el,max,woDH}(t)}{\alpha_0(t)
262
        \cdot Y(t) + \alpha_1(t) \cdot P_{el,max,woDH}(t)}\\
263
        &
264
        \eta_{el,min,woDH}(t) = \frac{P_{el,min,woDH}(t)}{\alpha_0(t)
265
        \cdot Y(t) + \alpha_1(t) \cdot P_{el,min,woDH}(t)}\\
266
267
268
    **For the attribute** :math:`\dot{H}_{L,FG,min}` **being not None**,
269
    e.g. for a motoric CHP, **the following is created:**
270
271
        **Constraint:**
272
273
    .. _GenericCHP-equations11:
274
275
    .. math::
276
        &
277
        (11)\qquad P_{el}(t) + \dot{Q}(t) + \dot{H}_{L,FG,min}(t) +
278
        \dot{Q}_{CW, min}(t) \cdot Y(t) \geq \dot{H}_F(t)\\[10pt]
279
280
    The symbols used are defined as follows (with Variables (V) and Parameters (P)):
281
282
    =============================== ======================= ==== =============================================
283
    math. symbol                    attribute               type explanation
284
    =============================== ======================= ==== =============================================
285
    :math:`\dot{H}_{F}`             `H_F[n,t]`              V    input of enthalpy through fuel input
286
    :math:`P_{el}`                  `P[n,t]`                V    provided electric power
287
    :math:`P_{el,woDH}`             `P_woDH[n,t]`           V    electric power without district heating
288
    :math:`P_{el,min,woDH}`         `P_min_woDH[n,t]`       P    min. electric power without district heating
289
    :math:`P_{el,max,woDH}`         `P_max_woDH[n,t]`       P    max. electric power without district heating
290
    :math:`\dot{Q}`                 `Q[n,t]`                V    provided heat
291
    :math:`\dot{Q}_{CW, min}`       `Q_CW_min[n,t]`         P    minimal therm. condenser load to cooling water
292
    :math:`\dot{H}_{L,FG,min}`      `H_L_FG_min[n,t]`       V    flue gas enthalpy loss at min heat extraction
293
    :math:`\dot{H}_{L,FG,max}`      `H_L_FG_max[n,t]`       V    flue gas enthalpy loss at max heat extraction
294
    :math:`\dot{H}_{L,FG,sharemin}` `H_L_FG_share_min[n,t]` P    share of flue gas loss at min heat extraction
295
    :math:`\dot{H}_{L,FG,sharemax}` `H_L_FG_share_max[n,t]` P    share of flue gas loss at max heat extraction
296
    :math:`Y`                       `Y[n,t]`                V    status variable on/off
297
    :math:`\alpha_0`                `n.alphas[0][n,t]`      P    coefficient describing efficiency
298
    :math:`\alpha_1`                `n.alphas[1][n,t]`      P    coefficient describing efficiency
299
    :math:`\beta`                   `beta[n,t]`             P    power loss index
300
    :math:`\eta_{el,min,woDH}`      `Eta_el_min_woDH[n,t]`  P    el. eff. at min. fuel flow w/o distr. heating
301
    :math:`\eta_{el,max,woDH}`      `Eta_el_max_woDH[n,t]`  P    el. eff. at max. fuel flow w/o distr. heating
302
    =============================== ======================= ==== =============================================
303
304
    """  # noqa: E501
305
306
    CONSTRAINT_GROUP = True
307
308
    def __init__(self, *args, **kwargs):
309
        super().__init__(*args, **kwargs)
310
311
    def _create(self, group=None):
312
        """
313
        Create constraints for GenericCHPBlock.
314
315
        Parameters
316
        ----------
317
        group : list
318
            List containing `GenericCHP` objects.
319
            e.g. groups=[ghcp1, gchp2,..]
320
        """
321
        m = self.parent_block()
322
323
        if group is None:
324
            return None
325
326
        self.GENERICCHPS = Set(initialize=[n for n in group])
327
328
        # variables
329
        self.H_F = Var(self.GENERICCHPS, m.TIMESTEPS, within=NonNegativeReals)
330
        self.H_L_FG_max = Var(
331
            self.GENERICCHPS, m.TIMESTEPS, within=NonNegativeReals
332
        )
333
        self.H_L_FG_min = Var(
334
            self.GENERICCHPS, m.TIMESTEPS, within=NonNegativeReals
335
        )
336
        self.P_woDH = Var(
337
            self.GENERICCHPS, m.TIMESTEPS, within=NonNegativeReals
338
        )
339
        self.P = Var(self.GENERICCHPS, m.TIMESTEPS, within=NonNegativeReals)
340
        self.Q = Var(self.GENERICCHPS, m.TIMESTEPS, within=NonNegativeReals)
341
        self.Y = Var(self.GENERICCHPS, m.TIMESTEPS, within=Binary)
342
343
        # constraint rules
344
        def _H_flow_rule(block, n, t):
345
            """Link fuel consumption to component inflow."""
346
            expr = 0
347
            expr += self.H_F[n, t]
348
            expr += -m.flow[list(n.fuel_input.keys())[0], n, t]
349
            return expr == 0
350
351
        self.H_flow = Constraint(
352
            self.GENERICCHPS, m.TIMESTEPS, rule=_H_flow_rule
353
        )
354
355
        def _Q_flow_rule(block, n, t):
356
            """Link heat flow to component outflow."""
357
            expr = 0
358
            expr += self.Q[n, t]
359
            expr += -m.flow[n, list(n.heat_output.keys())[0], t]
360
            return expr == 0
361
362
        self.Q_flow = Constraint(
363
            self.GENERICCHPS, m.TIMESTEPS, rule=_Q_flow_rule
364
        )
365
366
        def _P_flow_rule(block, n, t):
367
            """Link power flow to component outflow."""
368
            expr = 0
369
            expr += self.P[n, t]
370
            expr += -m.flow[n, list(n.electrical_output.keys())[0], t]
371
            return expr == 0
372
373
        self.P_flow = Constraint(
374
            self.GENERICCHPS, m.TIMESTEPS, rule=_P_flow_rule
375
        )
376
377
        def _H_F_1_rule(block, n, t):
378
            """Set P_woDH depending on H_F."""
379
            expr = 0
380
            expr += -self.H_F[n, t]
381
            expr += n.alphas[0][t] * self.Y[n, t]
382
            expr += n.alphas[1][t] * self.P_woDH[n, t]
383
            return expr == 0
384
385
        self.H_F_1 = Constraint(
386
            self.GENERICCHPS, m.TIMESTEPS, rule=_H_F_1_rule
387
        )
388
389
        def _H_F_2_rule(block, n, t):
390
            """Determine relation between H_F, P and Q."""
391
            expr = 0
392
            expr += -self.H_F[n, t]
393
            expr += n.alphas[0][t] * self.Y[n, t]
394
            expr += n.alphas[1][t] * (self.P[n, t] + n.beta[t] * self.Q[n, t])
395
            return expr == 0
396
397
        self.H_F_2 = Constraint(
398
            self.GENERICCHPS, m.TIMESTEPS, rule=_H_F_2_rule
399
        )
400
401
        def _H_F_3_rule(block, n, t):
402
            """Set upper value of operating range via H_F."""
403
            expr = 0
404
            expr += self.H_F[n, t]
405
            expr += -self.Y[n, t] * (
406
                list(n.electrical_output.values())[0].P_max_woDH[t]
407
                / list(n.electrical_output.values())[0].Eta_el_max_woDH[t]
408
            )
409
            return expr <= 0
410
411
        self.H_F_3 = Constraint(
412
            self.GENERICCHPS, m.TIMESTEPS, rule=_H_F_3_rule
413
        )
414
415
        def _H_F_4_rule(block, n, t):
416
            """Set lower value of operating range via H_F."""
417
            expr = 0
418
            expr += self.H_F[n, t]
419
            expr += -self.Y[n, t] * (
420
                list(n.electrical_output.values())[0].P_min_woDH[t]
421
                / list(n.electrical_output.values())[0].Eta_el_min_woDH[t]
422
            )
423
            return expr >= 0
424
425
        self.H_F_4 = Constraint(
426
            self.GENERICCHPS, m.TIMESTEPS, rule=_H_F_4_rule
427
        )
428
429
        def _H_L_FG_max_rule(block, n, t):
430
            """Set max. flue gas loss as share fuel flow share."""
431
            expr = 0
432
            expr += -self.H_L_FG_max[n, t]
433
            expr += (
434
                self.H_F[n, t]
435
                * list(n.fuel_input.values())[0].H_L_FG_share_max[t]
436
            )
437
            return expr == 0
438
439
        self.H_L_FG_max_def = Constraint(
440
            self.GENERICCHPS, m.TIMESTEPS, rule=_H_L_FG_max_rule
441
        )
442
443
        def _Q_max_res_rule(block, n, t):
444
            """Set maximum Q depending on fuel and electrical flow."""
445
            expr = 0
446
            expr += self.P[n, t] + self.Q[n, t] + self.H_L_FG_max[n, t]
447
            expr += list(n.heat_output.values())[0].Q_CW_min[t] * self.Y[n, t]
448
            expr += -self.H_F[n, t]
449
            # back-pressure characteristics or one-segment model
450
            if n.back_pressure is True:
451
                return expr == 0
452
            else:
453
                return expr <= 0
454
455
        self.Q_max_res = Constraint(
456
            self.GENERICCHPS, m.TIMESTEPS, rule=_Q_max_res_rule
457
        )
458
459
        def _H_L_FG_min_rule(block, n, t):
460
            """Set min. flue gas loss as fuel flow share."""
461
            # minimum flue gas losses e.g. for motoric CHPs
462
            if getattr(
463
                list(n.fuel_input.values())[0], "H_L_FG_share_min", None
464
            ):
465
                expr = 0
466
                expr += -self.H_L_FG_min[n, t]
467
                expr += (
468
                    self.H_F[n, t]
469
                    * list(n.fuel_input.values())[0].H_L_FG_share_min[t]
470
                )
471
                return expr == 0
472
            else:
473
                return Constraint.Skip
474
475
        self.H_L_FG_min_def = Constraint(
476
            self.GENERICCHPS, m.TIMESTEPS, rule=_H_L_FG_min_rule
477
        )
478
479
        def _Q_min_res_rule(block, n, t):
480
            """Set minimum Q depending on fuel and eletrical flow."""
481
            # minimum restriction for heat flows e.g. for motoric CHPs
482
            if getattr(
483
                list(n.fuel_input.values())[0], "H_L_FG_share_min", None
484
            ):
485
                expr = 0
486
                expr += self.P[n, t] + self.Q[n, t] + self.H_L_FG_min[n, t]
487
                expr += (
488
                    list(n.heat_output.values())[0].Q_CW_min[t] * self.Y[n, t]
489
                )
490
                expr += -self.H_F[n, t]
491
                return expr >= 0
492
            else:
493
                return Constraint.Skip
494
495
        self.Q_min_res = Constraint(
496
            self.GENERICCHPS, m.TIMESTEPS, rule=_Q_min_res_rule
497
        )
498
499
    def _objective_expression(self):
500
        r"""Objective expression for generic CHPs with no investment.
501
502
        Note: This adds nothing as variable costs are already
503
        added in the Block :class:`SimpleFlowBlock`.
504
        """
505
        if not hasattr(self, "GENERICCHPS"):
506
            return 0
507
508
        return 0
509