tespy 0.3.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3291 @@
1
+ # -*- coding: utf-8
2
+
3
+ """Module for components using combustion.
4
+
5
+ Components in this module:
6
+
7
+ - :func:`tespy.components.combustion.combustion_chamber`
8
+ - :func:`tespy.components.combustion.combustion_chamber_stoich`
9
+ - :func:`tespy.components.combustion.combustion_engine`
10
+
11
+
12
+ This file is part of project TESPy (github.com/oemof/tespy). It's copyrighted
13
+ by the contributors recorded in the version control history of the file,
14
+ available from its original location tespy/components/combustion.py
15
+
16
+ SPDX-License-Identifier: MIT
17
+ """
18
+
19
+ import logging
20
+
21
+ import CoolProp.CoolProp as CP
22
+ import numpy as np
23
+
24
+ from tespy.components.components import component
25
+ from tespy.tools.data_containers import dc_cc
26
+ from tespy.tools.data_containers import dc_cp
27
+ from tespy.tools.data_containers import dc_simple
28
+ from tespy.tools.fluid_properties import h_mix_pQ
29
+ from tespy.tools.fluid_properties import h_mix_pT
30
+ from tespy.tools.fluid_properties import h_pT
31
+ from tespy.tools.fluid_properties import s_mix_ph
32
+ from tespy.tools.fluid_properties import s_mix_pT
33
+ from tespy.tools.fluid_properties import tespy_fluid
34
+ from tespy.tools.fluid_properties import v_mix_ph
35
+ from tespy.tools.global_vars import err
36
+ from tespy.tools.global_vars import molar_masses
37
+ from tespy.tools.helpers import TESPyComponentError
38
+ from tespy.tools.helpers import fluid_structure
39
+ from tespy.tools.helpers import molar_mass_flow
40
+
41
+ # %%
42
+
43
+
44
+ class combustion_chamber(component):
45
+ r"""
46
+ The class combustion_chamber is parent class of all combustion components.
47
+
48
+ Equations
49
+
50
+ **mandatory equations**
51
+
52
+ - :func:`tespy.components.combustion.combustion_chamber.reaction_balance`
53
+ - :func:`tespy.components.components.component.mass_flow_func`
54
+
55
+ .. math::
56
+
57
+ 0 = p_{in,i} - p_{out} \;
58
+ \forall i \in \mathrm{inlets}
59
+
60
+ - :func:`tespy.components.combustion.combustion_chamber.energy_balance`
61
+
62
+ **optional equations**
63
+
64
+ - :func:`tespy.components.combustion.combustion_chamber.lambda_func`
65
+ - :func:`tespy.components.combustion.combustion_chamber.ti_func`
66
+
67
+ Available fuels
68
+
69
+ - methane, ethane, propane, butane, hydrogen
70
+
71
+ Inlets/Outlets
72
+
73
+ - in1, in2
74
+ - out1
75
+
76
+ Image
77
+
78
+ .. image:: _images/combustion_chamber.svg
79
+ :scale: 100 %
80
+ :alt: alternative text
81
+ :align: center
82
+
83
+ .. note::
84
+
85
+ The fuel and the air components can be connected to either of the
86
+ inlets.
87
+
88
+ Parameters
89
+ ----------
90
+ label : str
91
+ The label of the component.
92
+
93
+ design : list
94
+ List containing design parameters (stated as String).
95
+
96
+ offdesign : list
97
+ List containing offdesign parameters (stated as String).
98
+
99
+ design_path: str
100
+ Path to the components design case.
101
+
102
+ local_offdesign : boolean
103
+ Treat this component in offdesign mode in a design calculation.
104
+
105
+ local_design : boolean
106
+ Treat this component in design mode in an offdesign calculation.
107
+
108
+ char_warnings: boolean
109
+ Ignore warnings on default characteristics usage for this component.
110
+
111
+ printout: boolean
112
+ Include this component in the network's results printout.
113
+
114
+ lamb : float/tespy.tools.data_containers.dc_cp
115
+ Actual oxygen to stoichiometric oxygen ratio, :math:`\lambda/1`.
116
+
117
+ ti : float/tespy.tools.data_containers.dc_cp
118
+ Thermal input, (:math:`{LHV \cdot \dot{m}_f}`),
119
+ :math:`ti/\text{W}`.
120
+
121
+ Note
122
+ ----
123
+ For more information on the usage of the combustion chamber see the
124
+ examples section on github or look for the combustion chamber tutorials
125
+ at tespy.readthedocs.io.
126
+
127
+ Example
128
+ -------
129
+ The combustion chamber calculates energy input due to combustion as well as
130
+ the flue gas composition based on the type of fuel and the amount of
131
+ oxygen supplied. In this example a mixture of methane, hydrogen and
132
+ carbondioxide is used as fuel.
133
+
134
+ >>> from tespy.components import sink, source, combustion_chamber
135
+ >>> from tespy.connections import connection
136
+ >>> from tespy.networks import network
137
+ >>> from tespy.tools.fluid_properties import T_bp_p
138
+ >>> import shutil
139
+ >>> fluid_list = ['Ar', 'N2', 'H2', 'O2', 'CO2', 'CH4', 'H2O']
140
+ >>> nw = network(fluids=fluid_list, p_unit='bar', T_unit='C',
141
+ ... iterinfo=False)
142
+ >>> amb = source('ambient air')
143
+ >>> sf = source('fuel')
144
+ >>> fg = sink('flue gas outlet')
145
+ >>> comb = combustion_chamber('combustion chamber')
146
+ >>> comb.component()
147
+ 'combustion chamber'
148
+ >>> amb_comb = connection(amb, 'out1', comb, 'in1')
149
+ >>> sf_comb = connection(sf, 'out1', comb, 'in2')
150
+ >>> comb_fg = connection(comb, 'out1', fg, 'in1')
151
+ >>> nw.add_conns(sf_comb, amb_comb, comb_fg)
152
+
153
+ Specify the thermal input of the combustion chamber. At the given fluid
154
+ compositions this determines the mass flow of the fuel. The outlet
155
+ temperature of the flue gas determines the ratio of oxygen to fuel mass
156
+ flow.
157
+
158
+ >>> comb.set_attr(ti=500000)
159
+ >>> amb_comb.set_attr(p=1, T=20, fluid={'Ar': 0.0129, 'N2': 0.7553,
160
+ ... 'H2O': 0, 'CH4': 0, 'CO2': 0.0004, 'O2': 0.2314, 'H2': 0})
161
+ >>> sf_comb.set_attr(T=25, fluid={'CO2': 0.03, 'H2': 0.01, 'Ar': 0,
162
+ ... 'N2': 0, 'O2': 0, 'H2O': 0, 'CH4': 0.96})
163
+ >>> comb_fg.set_attr(T=1200)
164
+ >>> nw.solve('design')
165
+ >>> round(comb.lamb.val, 3)
166
+ 2.017
167
+ >>> comb.set_attr(lamb=2)
168
+ >>> comb_fg.set_attr(T=np.nan)
169
+ >>> nw.solve('design')
170
+ >>> round(comb_fg.T.val, 1)
171
+ 1208.4
172
+ """
173
+
174
+ @staticmethod
175
+ def component():
176
+ return 'combustion chamber'
177
+
178
+ @staticmethod
179
+ def attr():
180
+ return {
181
+ 'lamb': dc_cp(min_val=1),
182
+ 'ti': dc_cp(min_val=0),
183
+ 'S': dc_simple()
184
+ }
185
+
186
+ @staticmethod
187
+ def inlets():
188
+ return ['in1', 'in2']
189
+
190
+ @staticmethod
191
+ def outlets():
192
+ return ['out1']
193
+
194
+ def comp_init(self, nw):
195
+
196
+ component.comp_init(self, nw)
197
+
198
+ if not isinstance(self, combustion_engine):
199
+
200
+ # number of mandatroy equations for
201
+ # reaction balance: num_fl
202
+ # mass flow: 1
203
+ # pressure: 2
204
+ # energy balance: 1
205
+ self.num_eq = self.num_nw_fluids + 4
206
+ for var in [self.lamb, self.ti]:
207
+ if var.is_set is True:
208
+ self.num_eq += 1
209
+
210
+ self.jacobian = np.zeros((
211
+ self.num_eq,
212
+ self.num_i + self.num_o + self.num_vars,
213
+ self.num_nw_vars))
214
+
215
+ self.residual = np.zeros(self.num_eq)
216
+ self.jacobian[0:1] = self.mass_flow_deriv()
217
+ self.jacobian[1:3] = self.pressure_deriv()
218
+
219
+ self.fuel_list = []
220
+ fuels = ['methane', 'ethane', 'propane', 'butane', 'hydrogen']
221
+ for f in fuels:
222
+ self.fuel_list += [x for x in nw.fluids if x in [
223
+ a.replace(' ', '') for a in CP.get_aliases(f)]]
224
+
225
+ if len(self.fuel_list) == 0:
226
+ msg = ('Your network\'s fluids do not contain any fuels, that are '
227
+ 'available for the component ' + self.label + ' of type ' +
228
+ self.component() + '. Available fuels are: ' + str(fuels) +
229
+ '.')
230
+ logging.error(msg)
231
+ raise TESPyComponentError(msg)
232
+
233
+ else:
234
+ msg = ('The fuels for component ' + self.label + ' of type ' +
235
+ self.component() + ' are: ' + str(self.fuel_list) + '.')
236
+ logging.debug(msg)
237
+
238
+ self.o2 = [x for x in nw.fluids if x in [
239
+ a.replace(' ', '') for a in CP.get_aliases('O2')]][0]
240
+ self.co2 = [x for x in nw.fluids if x in [
241
+ a.replace(' ', '') for a in CP.get_aliases('CO2')]][0]
242
+ self.h2o = [x for x in nw.fluids if x in [
243
+ a.replace(' ', '') for a in CP.get_aliases('H2O')]][0]
244
+ self.n2 = [x for x in nw.fluids if x in [
245
+ a.replace(' ', '') for a in CP.get_aliases('N2')]][0]
246
+
247
+ self.fuels = {}
248
+ for f in self.fuel_list:
249
+ self.fuels[f] = {}
250
+ structure = fluid_structure(f)
251
+ for el in ['C', 'H', 'O']:
252
+ if el in structure.keys():
253
+ self.fuels[f][el] = structure[el]
254
+ else:
255
+ self.fuels[f][el] = 0
256
+ self.fuels[f]['LHV'] = self.calc_lhv(f)
257
+
258
+ def calc_lhv(self, f):
259
+ r"""
260
+ Calculate the lower heating value of the combustion chamber's fuel.
261
+
262
+ Parameters
263
+ ----------
264
+ f : str
265
+ Alias of the fuel.
266
+
267
+ Returns
268
+ -------
269
+ val : float
270
+ Lower heating value of the combustion chambers fuel.
271
+
272
+ .. math::
273
+
274
+ LHV = -\frac{\sum_i {\Delta H_f^0}_i -
275
+ \sum_j {\Delta H_f^0}_j }
276
+ {M_{fuel}}\\
277
+ \forall i \in \text{reation products},\\
278
+ \forall j \in \text{reation educts},\\
279
+ \Delta H_f^0: \text{molar formation enthalpy}
280
+ """
281
+ hf = {}
282
+ hf['hydrogen'] = 0
283
+ hf['methane'] = -74.85
284
+ hf['ethane'] = -84.68
285
+ hf['propane'] = -103.8
286
+ hf['butane'] = -124.51
287
+ hf[self.o2] = 0
288
+ hf[self.co2] = -393.5
289
+ # water (gaseous)
290
+ hf[self.h2o] = -241.8
291
+
292
+ key = set(list(hf.keys())).intersection(
293
+ set([a.replace(' ', '')
294
+ for a in CP.get_aliases(f)]))
295
+
296
+ val = (-(self.fuels[f]['H'] / 2 * hf[self.h2o] +
297
+ self.fuels[f]['C'] * hf[self.co2] -
298
+ ((self.fuels[f]['C'] + self.fuels[f]['H'] / 4) * hf[self.o2] +
299
+ hf[list(key)[0]])) /
300
+ molar_masses[f] * 1000)
301
+
302
+ return val
303
+
304
+ def equations(self):
305
+ r"""Calculate residual vector with results of equations."""
306
+ k = 0
307
+ ######################################################################
308
+ # eqation for mass flow balance
309
+ self.residual[k] = self.mass_flow_func()
310
+ k += 1
311
+
312
+ ######################################################################
313
+ # equations for pressure
314
+ for i in self.inl:
315
+ self.residual[k] = self.outl[0].p.val_SI - i.p.val_SI
316
+ k += 1
317
+
318
+ ######################################################################
319
+ # equations for fluids in reaction balance
320
+ for fluid in self.inl[0].fluid.val.keys():
321
+ if np.absolute(self.residual[k]) > err ** 2 or self.it % 3 == 0:
322
+ self.residual[k] = self.reaction_balance(fluid)
323
+ k += 1
324
+
325
+ ######################################################################
326
+ # equation for energy balance
327
+ if np.absolute(self.residual[k]) > err ** 2 or self.it % 4 == 0:
328
+ self.residual[k] = self.energy_balance()
329
+ k += 1
330
+
331
+ ######################################################################
332
+ # equation for specified air to stoichiometric air ratio lamb
333
+ if self.lamb.is_set:
334
+ self.residual[k] = self.lambda_func()
335
+ k += 1
336
+
337
+ ######################################################################
338
+ # equation for speciified thermal input
339
+ if self.ti.is_set:
340
+ self.residual[k] = self.ti_func()
341
+ k += 1
342
+
343
+ def derivatives(self, increment_filter):
344
+ r"""Calculate matrix of partial derivatives for given equations."""
345
+ ######################################################################
346
+ # derivatives for mass flow and pressure are static
347
+ k = 3
348
+
349
+ ######################################################################
350
+ # derivatives for reaction balance
351
+ for fluid in self.nw_fluids:
352
+ for i in range(3):
353
+ if not increment_filter[i, 0]:
354
+ self.jacobian[k, i, 0] = self.rb_numeric_deriv(
355
+ 'm', i, fluid)
356
+ if not all(increment_filter[i, 3:]):
357
+ self.jacobian[k, i, 3:] = self.rb_numeric_deriv(
358
+ 'fluid', i, fluid)
359
+ k += 1
360
+
361
+ ######################################################################
362
+ # derivatives for energy balance equations
363
+ f = self.energy_balance
364
+ for i in range(3):
365
+ if not increment_filter[i, 0]:
366
+ self.jacobian[k, i, 0] = self.numeric_deriv(f, 'm', i)
367
+ if not increment_filter[i, 1]:
368
+ self.jacobian[k, i, 1] = self.numeric_deriv(f, 'p', i)
369
+ if i >= self.num_i:
370
+ self.jacobian[k, i, 2] = -(
371
+ self.inl + self.outl)[i].m.val_SI
372
+ else:
373
+ self.jacobian[k, i, 2] = (
374
+ self.inl + self.outl)[i].m.val_SI
375
+ k += 1
376
+
377
+ ######################################################################
378
+ # derivatives for specified lamb
379
+ if self.lamb.is_set:
380
+ f = self.lambda_func
381
+ if not increment_filter[0, 0]:
382
+ self.jacobian[k, 0, 0] = self.numeric_deriv(f, 'm', 0)
383
+ if not all(increment_filter[0, 3:]):
384
+ self.jacobian[k, 0, 3:] = self.numeric_deriv(f, 'fluid', 0)
385
+ if not increment_filter[1, 0]:
386
+ self.jacobian[k, 1, 0] = self.numeric_deriv(f, 'm', 1)
387
+ if not all(increment_filter[1, 3:]):
388
+ self.jacobian[k, 1, 3:] = self.numeric_deriv(f, 'fluid', 1)
389
+ k += 1
390
+
391
+ ######################################################################
392
+ # derivatives for specified thermal input
393
+ if self.ti.is_set:
394
+ # stoichiometric combustion chamber
395
+ if isinstance(self, combustion_chamber_stoich):
396
+ fuel = self.fuel_alias.val
397
+ pos = 3 + self.nw_fluids.index(fuel)
398
+
399
+ for i in range(2):
400
+ self.jacobian[k, i, 0] = -self.inl[i].fluid.val[fuel]
401
+ self.jacobian[k, i, pos] = -self.inl[i].m.val_SI
402
+ self.jacobian[k, 2, 0] = self.outl[0].fluid.val[fuel]
403
+ self.jacobian[k, 2, pos] = self.outl[0].m.val_SI
404
+ self.jacobian[k] *= self.lhv
405
+ # combustion chamber
406
+ else:
407
+
408
+ self.jacobian[k, 0, 0] = 0
409
+ self.jacobian[k, 1, 0] = 0
410
+ self.jacobian[k, 2, 0] = 0
411
+ for f in self.fuel_list:
412
+ pos = 3 + self.nw_fluids.index(f)
413
+ lhv = self.fuels[f]['LHV']
414
+
415
+ for i in range(2):
416
+ self.jacobian[k, i, 0] += -(
417
+ self.inl[i].fluid.val[f] * lhv)
418
+ self.jacobian[k, i, pos] = -(
419
+ self.inl[i].m.val_SI * lhv)
420
+ self.jacobian[k, 2, 0] += (
421
+ self.outl[0].fluid.val[f] * lhv)
422
+ self.jacobian[k, 2, pos] = (
423
+ self.outl[0].m.val_SI * lhv)
424
+ k += 1
425
+
426
+ def pressure_deriv(self):
427
+ r"""
428
+ Calculate the partial derivatives for all pressure equations.
429
+
430
+ Returns
431
+ -------
432
+ deriv : list
433
+ Matrix with partial derivatives for the fluid equations.
434
+ """
435
+ deriv = np.zeros((2, 3, self.num_nw_vars))
436
+ for k in range(2):
437
+ deriv[k][2][1] = 1
438
+ deriv[k][k][1] = -1
439
+ return deriv
440
+
441
+ def reaction_balance(self, fluid):
442
+ r"""
443
+ Calculate the reaction balance for one fluid.
444
+
445
+ - determine molar mass flows of fuel and oxygen
446
+ - calculate mole number of carbon and hydrogen atoms in fuel
447
+ - calculate molar oxygen flow for stoichiometric combustion
448
+ - calculate residual value of the fluids balance
449
+
450
+ for excess fuel
451
+
452
+ - calculate excess carbon and hydrogen in fuels
453
+ - calculate excess fuel shares
454
+
455
+ General equations
456
+
457
+ .. math::
458
+
459
+ \text{combustion chamber: } i \in [1,2], o \in [1]\\
460
+ \text{combustion engine: } i \in [3,4], o \in [3]\\
461
+
462
+ res = \sum_i \left(x_{fluid,i} \cdot \dot{m}_{i}\right) -
463
+ \sum_j \left(x_{fluid,j} \cdot \dot{m}_{j}\right) \;
464
+ \forall i, \; \forall j
465
+
466
+ \dot{m}_{fluid,m} = \sum_i \frac{x_{fluid,i} \cdot \dot{m}_{i}}
467
+ {M_{fluid}} \; \forall i
468
+
469
+ \dot{m}_{O_2,m,stoich}=\frac{\dot{m}_{H_m}}{4} + \dot{m}_{C_m}
470
+
471
+ \lambda = \frac{\dot{m}_{O_2,m}}{\dot{m}_{O_2,m,stoich}}
472
+
473
+ Excess carbon and hydrogen
474
+
475
+ .. math::
476
+
477
+ \dot{m}_{H_{exc,m}} = \begin{cases}
478
+ 0 & \lambda \geq 1\\
479
+ 4 \cdot \left( \dot{m}_{O_2,m,stoich} -
480
+ \dot{m}_{O_2,m}\right) & \lambda < 1
481
+ \end{cases}
482
+
483
+ \dot{m}_{C_{exc,m}} = \begin{cases}
484
+ 0 & \lambda \geq 1\\
485
+ \dot{m}_{O_2,m,stoich} - \dot{m}_{O_2,m} & \lambda < 1
486
+ \end{cases}
487
+
488
+ Equation for fuel
489
+
490
+ .. math::
491
+
492
+ 0 = res - \left(\dot{m}_{f,m} - \dot{m}_{f,exc,m}\right)
493
+ \cdot M_{fuel}\\
494
+
495
+ \dot{m}_{f,exc,m} = \begin{cases}
496
+ 0 & \lambda \geq 1\\
497
+ \dot{m}_{f,m} - \frac{\dot{m}_{O_2,m}}
498
+ {n_{C,fuel} + 0.25 \cdot n_{H,fuel}}
499
+ \end{cases}
500
+
501
+ Equation for oxygen
502
+
503
+ .. math::
504
+
505
+ 0 = res - \begin{cases}
506
+ -\frac{\dot{m}_{O_2,m} \cdot M_{O_2}}{\lambda} &
507
+ \lambda \geq 1\\
508
+ - \dot{m}_{O_2,m} \cdot M_{O_2} & \lambda < 1
509
+ \end{cases}
510
+
511
+ Equation for water
512
+
513
+ .. math::
514
+
515
+ 0 = res + \left( \dot{m}_{H_m} - \dot{m}_{H_{exc,m}} \right)
516
+ \cdot 0.5 \cdot M_{H_2O}
517
+
518
+ Equation for carbondioxide
519
+
520
+ .. math::
521
+
522
+ 0 = res + \left( \dot{m}_{C_m} - \dot{m}_{C_{exc,m}} \right)
523
+ \cdot M_{CO_2}
524
+
525
+ Equation for all other fluids
526
+
527
+ .. math::
528
+
529
+ 0 = res
530
+
531
+ Parameters
532
+ ----------
533
+ fluid : str
534
+ The fluid to calculate the reation balance for.
535
+
536
+ Returns
537
+ -------
538
+ res : float
539
+ Residual value of equation.
540
+ """
541
+ if isinstance(self, combustion_engine):
542
+ inl = self.inl[2:]
543
+ outl = self.outl[2:]
544
+ else:
545
+ inl = self.inl
546
+ outl = self.outl
547
+
548
+ ######################################################################
549
+ # molar mass flow for fuel and oxygen
550
+ n_fuel = {}
551
+ n_oxy_stoich = {}
552
+ n_h = 0
553
+ n_c = 0
554
+ for f in self.fuel_list:
555
+ n_fuel[f] = 0
556
+ for i in inl:
557
+ n = i.m.val_SI * i.fluid.val[f] / molar_masses[f]
558
+ n_fuel[f] += n
559
+ n_h += n * self.fuels[f]['H']
560
+ n_c += n * self.fuels[f]['C']
561
+
562
+ # stoichiometric oxygen requirement for each fuel
563
+ n_oxy_stoich[f] = n_fuel[f] * (self.fuels[f]['H'] / 4 +
564
+ self.fuels[f]['C'])
565
+
566
+ n_oxygen = 0
567
+ for i in inl:
568
+ n_oxygen += (i.m.val_SI * i.fluid.val[self.o2] /
569
+ molar_masses[self.o2])
570
+
571
+ ######################################################################
572
+ # calculate stoichiometric oxygen
573
+ n_oxygen_stoich = n_h / 4 + n_c
574
+
575
+ ######################################################################
576
+ # calculate lambda if not set
577
+ if not self.lamb.is_set:
578
+ self.lamb.val = n_oxygen / n_oxygen_stoich
579
+
580
+ ######################################################################
581
+ # calculate excess fuel if lambda is lower than 1
582
+ if self.lamb.val < 1:
583
+ n_h_exc = (n_oxygen_stoich - n_oxygen) * 4
584
+ n_c_exc = (n_oxygen_stoich - n_oxygen)
585
+ else:
586
+ n_h_exc = 0
587
+ n_c_exc = 0
588
+
589
+ ######################################################################
590
+ # equation for carbondioxide
591
+ if fluid == self.co2:
592
+ dm = (n_c - n_c_exc) * molar_masses[self.co2]
593
+
594
+ ######################################################################
595
+ # equation for water
596
+ elif fluid == self.h2o:
597
+ dm = (n_h - n_h_exc) / 2 * molar_masses[self.h2o]
598
+
599
+ ######################################################################
600
+ # equation for oxygen
601
+ elif fluid == self.o2:
602
+ if self.lamb.val < 1:
603
+ dm = -n_oxygen * molar_masses[self.o2]
604
+ else:
605
+ dm = -n_oxygen / self.lamb.val * molar_masses[self.o2]
606
+
607
+ ######################################################################
608
+ # equation for fuel
609
+ elif fluid in self.fuel_list:
610
+ if self.lamb.val < 1:
611
+ n_fuel_exc = (-(n_oxygen / n_oxygen_stoich - 1) *
612
+ n_oxy_stoich[fluid] *
613
+ (self.fuels[f]['H'] / 4 + self.fuels[f]['C']))
614
+ else:
615
+ n_fuel_exc = 0
616
+ dm = -(n_fuel[fluid] - n_fuel_exc) * molar_masses[fluid]
617
+
618
+ ######################################################################
619
+ # equation for other fluids
620
+ else:
621
+ dm = 0
622
+
623
+ res = dm
624
+ for i in inl:
625
+ res += i.fluid.val[fluid] * i.m.val_SI
626
+ for o in outl:
627
+ res -= o.fluid.val[fluid] * o.m.val_SI
628
+ return res
629
+
630
+ def rb_numeric_deriv(self, dx, pos, fluid):
631
+ r"""
632
+ Calculate derivative of the reaction balance to dx.
633
+
634
+ Parameters
635
+ ----------
636
+ dx : str
637
+ Partial derivative.
638
+
639
+ pos : int
640
+ Position of connection regarding to inlets and outlet of the
641
+ component, logic: ['in1', 'in2', ..., 'out1', ...] ->
642
+ 0, 1, ..., n, n + 1, ..., n + m
643
+
644
+ fluid : str
645
+ Fluid to calculate partial derivative of reaction balance for.
646
+
647
+ Returns
648
+ -------
649
+ deriv : float/list
650
+ Partial derivative(s) of the function :math:`f` to variable(s)
651
+ :math:`x`.
652
+
653
+ .. math::
654
+
655
+ \frac{\partial f}{\partial x} = \frac{f(x + d) + f(x - d)}{2 d}
656
+ """
657
+ dm, dp, dh, df = 0, 0, 0, 0
658
+ if dx == 'm':
659
+ dm = 1e-4
660
+ else:
661
+ df = 1e-5
662
+
663
+ if dx == 'fluid':
664
+ deriv = []
665
+ for f in self.inl[0].fluid.val.keys():
666
+ val = (self.inl + self.outl)[pos].fluid.val[f]
667
+ exp = 0
668
+ if (self.inl + self.outl)[pos].fluid.val[f] + df <= 1:
669
+ (self.inl + self.outl)[pos].fluid.val[f] += df
670
+ else:
671
+ (self.inl + self.outl)[pos].fluid.val[f] = 1
672
+ exp += self.reaction_balance(fluid)
673
+ if (self.inl + self.outl)[pos].fluid.val[f] - 2 * df >= 0:
674
+ (self.inl + self.outl)[pos].fluid.val[f] -= 2 * df
675
+ else:
676
+ (self.inl + self.outl)[pos].fluid.val[f] = 0
677
+ exp -= self.reaction_balance(fluid)
678
+ (self.inl + self.outl)[pos].fluid.val[f] = val
679
+
680
+ deriv += [exp / (2 * (dm + dp + dh + df))]
681
+
682
+ else:
683
+ exp = 0
684
+ (self.inl + self.outl)[pos].m.val_SI += dm
685
+ (self.inl + self.outl)[pos].p.val_SI += dp
686
+ (self.inl + self.outl)[pos].h.val_SI += dh
687
+ exp += self.reaction_balance(fluid)
688
+
689
+ (self.inl + self.outl)[pos].m.val_SI -= 2 * dm
690
+ (self.inl + self.outl)[pos].p.val_SI -= 2 * dp
691
+ (self.inl + self.outl)[pos].h.val_SI -= 2 * dh
692
+ exp -= self.reaction_balance(fluid)
693
+ deriv = exp / (2 * (dm + dp + dh + df))
694
+
695
+ (self.inl + self.outl)[pos].m.val_SI += dm
696
+ (self.inl + self.outl)[pos].p.val_SI += dp
697
+ (self.inl + self.outl)[pos].h.val_SI += dh
698
+
699
+ return deriv
700
+
701
+ def energy_balance(self):
702
+ r"""
703
+ Calculate the energy balance of the adiabatic combustion chamber.
704
+
705
+ Returns
706
+ -------
707
+ res : float
708
+ Residual value of equation.
709
+
710
+ .. math::
711
+
712
+ \begin{split}
713
+ res = & \sum_i \dot{m}_{in,i} \cdot
714
+ \left( h_{in,i} - h_{in,i,ref} \right)\\
715
+ & - \sum_j \dot{m}_{out,j} \cdot
716
+ \left( h_{out,j} - h_{out,j,ref} \right)\\
717
+ & + H_{I,f} \cdot \left(\sum_i \dot{m}_{in,i} \cdot x_{f,i} -
718
+ \sum_j \dot{m}_{out,j} \cdot x_{f,j} \right)
719
+ \end{split}\\
720
+
721
+ \forall i \in \text{inlets}\; \forall j \in \text{outlets}\\
722
+ x_{f}\text{: mass fraction of fuel}
723
+
724
+ Note
725
+ ----
726
+ The temperature for the reference state is set to 20 °C, thus
727
+ the water may be liquid. In order to make sure, the state is
728
+ referring to the lower heating value, the necessary enthalpy
729
+ difference for evaporation is added. The stoichiometric combustion
730
+ chamber uses a different reference, you will find it in the
731
+ :func:`tespy.components.combustion.combustion_chamber_stoich.energy_balance`
732
+ documentation.
733
+
734
+ - Reference temperature: 293.15 K.
735
+ - Reference pressure: 1 bar.
736
+ """
737
+ T_ref = 293.15
738
+ p_ref = 1e5
739
+
740
+ res = 0
741
+ for i in self.inl:
742
+ res += i.m.val_SI * (
743
+ i.h.val_SI - h_mix_pT([0, p_ref, 0, i.fluid.val], T_ref))
744
+
745
+ for o in self.outl:
746
+ dh = 0
747
+ n_h2o = o.fluid.val[self.h2o] / molar_masses[self.h2o]
748
+ if n_h2o > 0:
749
+ p = p_ref * n_h2o / molar_mass_flow(o.fluid.val)
750
+ h = h_pT(p, T_ref, self.h2o)
751
+ try:
752
+ flow = [0, p, 0, {self.h2o: 1}]
753
+ h_steam = h_mix_pQ(flow, 1)
754
+ except ValueError:
755
+ flow = [0, 615, 0, {self.h2o: 1}]
756
+ h_steam = h_mix_pQ(flow, 1)
757
+ if h < h_steam:
758
+ dh = (h_steam - h) * o.fluid.val[self.h2o]
759
+
760
+ res -= o.m.val_SI * (
761
+ o.h.val_SI - h_mix_pT([0, p_ref, 0, o.fluid.val], T_ref) - dh)
762
+
763
+ res += self.calc_ti()
764
+
765
+ return res
766
+
767
+ def lambda_func(self):
768
+ r"""
769
+ Calculate the residual for specified lambda.
770
+
771
+ Returns
772
+ -------
773
+ val : float
774
+ Residual value of function.
775
+
776
+ .. math::
777
+
778
+ \dot{m}_{fluid,m} = \sum_i \frac{x_{fluid,i} \cdot \dot{m}_{i}}
779
+ {M_{fluid}}\\ \forall i \in inlets
780
+
781
+ val = \frac{\dot{m}_{f,m}}{\dot{m}_{O_2,m} \cdot
782
+ \left(n_{C,fuel} + 0.25 \cdot n_{H,fuel}\right)} - \lambda
783
+ """
784
+ if isinstance(self, combustion_engine):
785
+ inl = self.inl[2:]
786
+ else:
787
+ inl = self.inl
788
+
789
+ n_h = 0
790
+ n_c = 0
791
+ for f in self.fuel_list:
792
+ n_fuel = 0
793
+ for i in inl:
794
+ n_fuel += i.m.val_SI * i.fluid.val[f] / molar_masses[f]
795
+ n_h += n_fuel * self.fuels[f]['H']
796
+ n_c += n_fuel * self.fuels[f]['C']
797
+
798
+ n_oxygen = 0
799
+ for i in inl:
800
+ n_oxygen += (i.m.val_SI * i.fluid.val[self.o2] /
801
+ molar_masses[self.o2])
802
+
803
+ n_oxygen_stoich = n_h / 4 + n_c
804
+
805
+ return n_oxygen / n_oxygen_stoich - self.lamb.val
806
+
807
+ def ti_func(self):
808
+ r"""
809
+ Calculate the residual for specified thermal input.
810
+
811
+ Returns
812
+ -------
813
+ val : float
814
+ Residual value of function.
815
+
816
+ .. math::
817
+
818
+ val = ti - \dot{m}_f \cdot LHV
819
+ """
820
+ return self.ti.val - self.calc_ti()
821
+
822
+ def calc_ti(self):
823
+ r"""
824
+ Calculate the thermal input of the combustion chamber.
825
+
826
+ Returns
827
+ -------
828
+ ti : float
829
+ Thermal input.
830
+
831
+ .. math::
832
+
833
+ ti = LHV \cdot \left[\sum_i \left(\dot{m}_{in,i} \cdot x_{f,i}
834
+ \right) - \dot{m}_{out,1} \cdot x_{f,1} \right]
835
+ \; \forall i \in [1,2]
836
+ """
837
+ ti = 0
838
+ for f in self.fuel_list:
839
+ m = 0
840
+ for i in self.inl:
841
+ m += i.m.val_SI * i.fluid.val[f]
842
+
843
+ for o in self.outl:
844
+ m -= o.m.val_SI * o.fluid.val[f]
845
+
846
+ ti += m * self.fuels[f]['LHV']
847
+
848
+ return ti
849
+
850
+ def bus_func(self, bus):
851
+ r"""
852
+ Calculate the value of the bus function.
853
+
854
+ Parameters
855
+ ----------
856
+ bus : tespy.connections.bus
857
+ TESPy bus object.
858
+
859
+ Returns
860
+ -------
861
+ val : float
862
+ Value of energy transfer :math:`\dot{E}`. This value is passed to
863
+ :py:meth:`tespy.components.components.component.calc_bus_value`
864
+ for value manipulation according to the specified characteristic
865
+ line of the bus.
866
+
867
+ .. math::
868
+
869
+ \dot{E} = LHV \cdot \dot{m}_{f}
870
+ """
871
+ return self.calc_ti()
872
+
873
+ def bus_deriv(self, bus):
874
+ r"""
875
+ Calculate the matrix of partial derivatives of the bus function.
876
+
877
+ Parameters
878
+ ----------
879
+ bus : tespy.connections.bus
880
+ TESPy bus object.
881
+
882
+ Returns
883
+ -------
884
+ deriv : ndarray
885
+ Matrix of partial derivatives.
886
+ """
887
+ deriv = np.zeros((1, 3, self.num_nw_vars))
888
+ f = self.calc_bus_value
889
+ for i in range(3):
890
+ deriv[0, i, 0] = self.numeric_deriv(f, 'm', i, bus=bus)
891
+ deriv[0, i, 3:] = self.numeric_deriv(f, 'fluid', i, bus=bus)
892
+
893
+ return deriv
894
+
895
+ def initialise_fluids(self, nw):
896
+ r"""
897
+ Calculate reaction balance for good generic flue gas starting values.
898
+
899
+ Parameters
900
+ ----------
901
+ nw : tespy.networks.network
902
+ Network using this component object.
903
+ """
904
+ N_2 = 0.7655
905
+ O_2 = 0.2345
906
+
907
+ n_fuel = 1
908
+ lamb = 3
909
+
910
+ fact_fuel = {}
911
+ sum_fuel = 0
912
+ for f in self.fuel_list:
913
+ fact_fuel[f] = 0
914
+ for i in self.inl:
915
+ fact_fuel[f] += i.fluid.val[f] / 2
916
+ sum_fuel += fact_fuel[f]
917
+
918
+ for f in self.fuel_list:
919
+ fact_fuel[f] /= sum_fuel
920
+
921
+ m_co2 = 0
922
+ m_h2o = 0
923
+ m_fuel = 0
924
+ for f in self.fuel_list:
925
+ m_co2 += (n_fuel * self.fuels[f]['C'] * molar_masses[self.co2] *
926
+ fact_fuel[f])
927
+ m_h2o += (n_fuel * self.fuels[f]['H'] / 2 *
928
+ molar_masses[self.h2o] * fact_fuel[f])
929
+ m_fuel += n_fuel * molar_masses[f] * fact_fuel[f]
930
+
931
+ n_o2 = (m_co2 / molar_masses[self.co2] +
932
+ 0.5 * m_h2o / molar_masses[self.h2o]) * lamb
933
+
934
+ m_air = n_o2 * molar_masses[self.o2] / O_2
935
+ m_fg = m_air + m_fuel
936
+
937
+ m_o2 = n_o2 * molar_masses[self.o2] * (1 - 1 / lamb)
938
+ m_n2 = N_2 * m_air
939
+
940
+ fg = {
941
+ self.n2: m_n2 / m_fg,
942
+ self.co2: m_co2 / m_fg,
943
+ self.o2: m_o2 / m_fg,
944
+ self.h2o: m_h2o / m_fg
945
+ }
946
+
947
+ for o in self.outl:
948
+ for fluid, x in o.fluid.val.items():
949
+ if not o.fluid.val_set[fluid] and fluid in fg.keys():
950
+ o.fluid.val[fluid] = fg[fluid]
951
+
952
+ def convergence_check(self, nw):
953
+ r"""
954
+ Perform a convergence check.
955
+
956
+ Parameters
957
+ ----------
958
+ nw : tespy.networks.network
959
+ The network object using this component.
960
+
961
+ Note
962
+ ----
963
+ Manipulate enthalpies/pressure at inlet and outlet if not specified
964
+ by user to match physically feasible constraints, keep fluid
965
+ composition within feasible range and then propagates it towards the
966
+ outlet.
967
+ """
968
+ if isinstance(self, combustion_engine):
969
+ inl = self.inl[2:]
970
+ outl = self.outl[2:]
971
+ else:
972
+ inl = self.inl
973
+ outl = self.outl
974
+
975
+ m = 0
976
+ for i in inl:
977
+ if i.good_starting_values is False:
978
+ if i.m.val_SI < 0 and not i.m.val_set:
979
+ i.m.val_SI = 0.01
980
+ m += i.m.val_SI
981
+
982
+ ######################################################################
983
+ # check fluid composition
984
+ for o in outl:
985
+ if o.good_starting_values is False:
986
+ fluids = [f for f in o.fluid.val.keys()
987
+ if not o.fluid.val_set[f]]
988
+ for f in fluids:
989
+ if f not in [self.o2, self.co2, self.h2o] + self.fuel_list:
990
+ m_f = 0
991
+ for i in inl:
992
+ m_f += i.fluid.val[f] * i.m.val_SI
993
+
994
+ if abs(o.fluid.val[f] - m_f / m) > 0.03:
995
+ o.fluid.val[f] = m_f / m
996
+
997
+ elif f == self.o2:
998
+ if o.fluid.val[f] > 0.25:
999
+ o.fluid.val[f] = 0.2
1000
+ if o.fluid.val[f] < 0.001:
1001
+ o.fluid.val[f] = 0.05
1002
+
1003
+ elif f == self.co2:
1004
+ if o.fluid.val[f] > 0.1:
1005
+ o.fluid.val[f] = 0.075
1006
+ if o.fluid.val[f] < 0.001:
1007
+ o.fluid.val[f] = 0.02
1008
+
1009
+ elif f == self.h2o:
1010
+ if o.fluid.val[f] > 0.1:
1011
+ o.fluid.val[f] = 0.075
1012
+ if o.fluid.val[f] < 0.001:
1013
+ o.fluid.val[f] = 0.02
1014
+
1015
+ elif f in self.fuel_list:
1016
+ if o.fluid.val[f] > 0:
1017
+ o.fluid.val[f] = 0
1018
+
1019
+ ######################################################################
1020
+ # flue gas propagation
1021
+ for o in outl:
1022
+ if o.good_starting_values is False:
1023
+ if o.m.val_SI < 0 and not o.m.val_set:
1024
+ o.m.val_SI = 10
1025
+ nw.init_target(o, o.target)
1026
+
1027
+ if o.h.val_SI < 7.5e5 and not o.h.val_set:
1028
+ o.h.val_SI = 1e6
1029
+
1030
+ ######################################################################
1031
+ # additional checks for performance improvement
1032
+ if self.lamb.val < 2 and not self.lamb.is_set:
1033
+ # search fuel and air inlet
1034
+ for i in inl:
1035
+ fuel_found = False
1036
+ if i.good_starting_values is False:
1037
+ fuel = 0
1038
+ for f in self.fuel_list:
1039
+ fuel += i.fluid.val[f]
1040
+ # found the fuel inlet
1041
+ if fuel > 0.75 and not i.m.val_set:
1042
+ fuel_found = True
1043
+ fuel_inlet = i
1044
+
1045
+ # found the air inlet
1046
+ if fuel < 0.75:
1047
+ air_tmp = i.m.val_SI
1048
+
1049
+ if fuel_found is True:
1050
+ fuel_inlet.m.val_SI = air_tmp / 25
1051
+
1052
+ @staticmethod
1053
+ def initialise_source(c, key):
1054
+ r"""
1055
+ Return a starting value for pressure and enthalpy at outlet.
1056
+
1057
+ Parameters
1058
+ ----------
1059
+ c : tespy.connections.connection
1060
+ Connection to perform initialisation on.
1061
+
1062
+ key : str
1063
+ Fluid property to retrieve.
1064
+
1065
+ Returns
1066
+ -------
1067
+ val : float
1068
+ Starting value for pressure/enthalpy in SI units.
1069
+
1070
+ .. math::
1071
+
1072
+ val = \begin{cases}
1073
+ 5 \cdot 10^5 & \text{key = 'p'}\\
1074
+ 10^6 & \text{key = 'h'}
1075
+ \end{cases}
1076
+ """
1077
+ if key == 'p':
1078
+ return 5e5
1079
+ elif key == 'h':
1080
+ return 10e5
1081
+
1082
+ @staticmethod
1083
+ def initialise_target(c, key):
1084
+ r"""
1085
+ Return a starting value for pressure and enthalpy at inlet.
1086
+
1087
+ Parameters
1088
+ ----------
1089
+ c : tespy.connections.connection
1090
+ Connection to perform initialisation on.
1091
+
1092
+ key : str
1093
+ Fluid property to retrieve.
1094
+
1095
+ Returns
1096
+ -------
1097
+ val : float
1098
+ Starting value for pressure/enthalpy in SI units.
1099
+
1100
+ .. math::
1101
+
1102
+ val = \begin{cases}
1103
+ 5 \cdot 10^5 & \text{key = 'p'}\\
1104
+ 5 \cdot 10^5 & \text{key = 'h'}
1105
+ \end{cases}
1106
+ """
1107
+ if key == 'p':
1108
+ return 5e5
1109
+ elif key == 'h':
1110
+ return 5e5
1111
+
1112
+ def calc_parameters(self):
1113
+ r"""Postprocessing parameter calculation."""
1114
+ self.ti.val = self.calc_ti()
1115
+
1116
+ n_h = 0
1117
+ n_c = 0
1118
+ for f in self.fuel_list:
1119
+ n_fuel = 0
1120
+ for i in self.inl:
1121
+ n_fuel += i.m.val_SI * i.fluid.val[f] / molar_masses[f]
1122
+ n_h += n_fuel * self.fuels[f]['H']
1123
+ n_c += n_fuel * self.fuels[f]['C']
1124
+
1125
+ n_oxygen = 0
1126
+ for i in self.inl:
1127
+ n_oxygen += (i.m.val_SI * i.fluid.val[self.o2] /
1128
+ molar_masses[self.o2])
1129
+
1130
+ n_oxygen_stoich = n_h / 4 + n_c
1131
+
1132
+ self.lamb.val = n_oxygen / n_oxygen_stoich
1133
+
1134
+ self.check_parameter_bounds()
1135
+
1136
+ # %%
1137
+
1138
+
1139
+ class combustion_chamber_stoich(combustion_chamber):
1140
+ r"""
1141
+ The class combustion_chamber_stoich is a simplified combustion chamber.
1142
+
1143
+ Equations
1144
+
1145
+ **mandatory equations**
1146
+
1147
+ - :func:`tespy.components.combustion.combustion_chamber_stoich.reaction_balance`
1148
+ - :func:`tespy.components.components.component.mass_flow_func`
1149
+
1150
+ .. math::
1151
+
1152
+ 0 = p_{in,i} - p_{out} \;
1153
+ \forall i \in \mathrm{inlets}
1154
+
1155
+ - :func:`tespy.components.combustion.combustion_chamber_stoich.energy_balance`
1156
+
1157
+ **optional equations**
1158
+
1159
+ - :func:`tespy.components.combustion.combustion_chamber_stoich.lambda_func`
1160
+ - :func:`tespy.components.combustion.combustion_chamber_stoich.ti_func`
1161
+
1162
+ Available fuels
1163
+
1164
+ - methane, ethane, propane, butane, hydrogen
1165
+
1166
+ Inlets/Outlets
1167
+
1168
+ - in1, in2
1169
+ - out1
1170
+
1171
+ Image
1172
+
1173
+ .. image:: _images/combustion_chamber.svg
1174
+ :scale: 100 %
1175
+ :alt: alternative text
1176
+ :align: center
1177
+
1178
+ .. note::
1179
+
1180
+ The fuel and the air components can be connected to either of the
1181
+ inlets.
1182
+
1183
+ Parameters
1184
+ ----------
1185
+ label : str
1186
+ The label of the component.
1187
+
1188
+ design : list
1189
+ List containing design parameters (stated as String).
1190
+
1191
+ offdesign : list
1192
+ List containing offdesign parameters (stated as String).
1193
+
1194
+ design_path: str
1195
+ Path to the components design case.
1196
+
1197
+ local_offdesign : boolean
1198
+ Treat this component in offdesign mode in a design calculation.
1199
+
1200
+ local_design : boolean
1201
+ Treat this component in design mode in an offdesign calculation.
1202
+
1203
+ char_warnings: boolean
1204
+ Ignore warnings on default characteristics usage for this component.
1205
+
1206
+ printout: boolean
1207
+ Include this component in the network's results printout.
1208
+
1209
+ fuel : dict
1210
+ Fuel composition, e.g. :code:`{'CH4': 0.96, 'CO2': 0.04}`.
1211
+
1212
+ fuel_alias : str
1213
+ Alias for the fuel.
1214
+
1215
+ air : dict
1216
+ Fresh air composition,
1217
+ e.g. :code:`{'N2': 0.76, 'O2': 0.23, 'Ar': 0.01}`.
1218
+
1219
+ air_alias : str
1220
+ Alias for the fresh air.
1221
+
1222
+ path : str
1223
+ Path to existing fluid property table.
1224
+
1225
+ lamb : float/tespy.tools.data_containers.dc_cp
1226
+ Air to stoichiometric air ratio, :math:`\lambda/1`.
1227
+
1228
+ ti : float/tespy.tools.data_containers.dc_cp
1229
+ Thermal input, (:math:`{LHV \cdot \dot{m}_f}`),
1230
+ :math:`ti/\text{W}`.
1231
+
1232
+ Note
1233
+ ----
1234
+ This combustion chamber uses fresh air and its fuel as the only
1235
+ reactive gas components. Therefore note the following restrictions. You
1236
+ are to
1237
+
1238
+ - specify the fluid composition of the fresh air,
1239
+ - fully define the fuel's fluid components,
1240
+ - provide the aliases of the fresh air and the fuel and
1241
+ - make sure, both of the aliases are part of the network fluid vector.
1242
+
1243
+ If you choose 'Air' or 'air' as alias for the fresh air, TESPy will use
1244
+ the fluid properties from CoolProp's air. Else, a custom fluid
1245
+ 'yourairalias' will be created.
1246
+
1247
+ The name of the flue gas will be: 'yourfuelalias_fg'. It is also
1248
+ possible to use fluid mixtures for the fuel, e.g.
1249
+ :code:`fuel={CH4: 0.9, 'CO2': 0.1}`. If you specify a fluid mixture for
1250
+ the fuel, TESPy will automatically create a custom fluid called. For more
1251
+ information see the examples section or look for the combustion chamber
1252
+ tutorials at tespy.readthedocs.io.
1253
+
1254
+ Example
1255
+ -------
1256
+ The stoichiometric combustion chamber follows identical physical properties
1257
+ as the combustion chamber. The main difference is, that the fuel and the
1258
+ air are not stated component wise but are fixed mixtures.
1259
+ The main advantage of using the stoichimetric combustion chamber
1260
+ comes from a strong improvement in terms of calculation speed.
1261
+ This example will show the same calculation as presented in the combustion
1262
+ chamber example
1263
+ (see :func:`tespy.components.combustion.combustion_chamber`).
1264
+
1265
+ >>> from tespy.components import sink, source, combustion_chamber_stoich
1266
+ >>> from tespy.connections import connection
1267
+ >>> from tespy.networks import network
1268
+ >>> from tespy.tools.fluid_properties import T_bp_p
1269
+ >>> import numpy as np
1270
+ >>> import shutil
1271
+ >>> fluid_list = ['myAir', 'myFuel', 'myFuel_fg']
1272
+ >>> nw = network(fluids=fluid_list, p_unit='bar', T_unit='C',
1273
+ ... p_range=[1, 10], iterinfo=False)
1274
+ >>> amb = source('ambient air')
1275
+ >>> sf = source('fuel')
1276
+ >>> fg = sink('flue gas outlet')
1277
+ >>> comb = combustion_chamber_stoich('stoichiometric combustion chamber')
1278
+ >>> comb.component()
1279
+ 'combustion chamber stoichiometric flue gas'
1280
+ >>> amb_comb = connection(amb, 'out1', comb, 'in1')
1281
+ >>> sf_comb = connection(sf, 'out1', comb, 'in2')
1282
+ >>> comb_fg = connection(comb, 'out1', fg, 'in1')
1283
+ >>> nw.add_conns(sf_comb, amb_comb, comb_fg)
1284
+
1285
+ Specify the thermal input of the combustion chamber. At the given fluid
1286
+ compositions this determines the mass flow of the fuel. The outlet
1287
+ temperature of the flue gas determines the ratio of oxygen to fuel mass
1288
+ flow. The fluid composition of the fuel and the air are defined, too. The
1289
+ results show very small deviation from the actual combustion chamber.
1290
+
1291
+ >>> comb.set_attr(fuel={'CH4': 0.96, 'CO2': 0.03, 'H2': 0.01},
1292
+ ... air={'Ar': 0.0129, 'N2': 0.7553, 'H2O': 0, 'CH4': 0, 'CO2': 0.0004,
1293
+ ... 'O2': 0.2314}, fuel_alias='myFuel', air_alias='myAir', ti=500000)
1294
+ >>> amb_comb.set_attr(T=20, p=1, fluid={'myAir': 1,
1295
+ ... 'myFuel': 0,'myFuel_fg': 0})
1296
+ >>> sf_comb.set_attr(T=25, fluid={'myAir': 0, 'myFuel': 1,
1297
+ ... 'myFuel_fg': 0})
1298
+ >>> comb_fg.set_attr(T=1200)
1299
+ >>> nw.solve('design')
1300
+ >>> round(comb.lamb.val, 3)
1301
+ 2.015
1302
+ >>> comb.set_attr(lamb=2)
1303
+ >>> comb_fg.set_attr(T=np.nan)
1304
+ >>> nw.solve('design')
1305
+ >>> round(comb_fg.T.val, 1)
1306
+ 1207.1
1307
+ >>> shutil.rmtree('./LUT', ignore_errors=True)
1308
+ """
1309
+
1310
+ @staticmethod
1311
+ def component():
1312
+ return 'combustion chamber stoichiometric flue gas'
1313
+
1314
+ @staticmethod
1315
+ def attr():
1316
+ return {'fuel': dc_simple(), 'fuel_alias': dc_simple(),
1317
+ 'air': dc_simple(), 'air_alias': dc_simple(),
1318
+ 'path': dc_simple(),
1319
+ 'lamb': dc_cp(min_val=1),
1320
+ 'ti': dc_cp(min_val=0),
1321
+ 'S': dc_simple()}
1322
+
1323
+ @staticmethod
1324
+ def inlets():
1325
+ return ['in1', 'in2']
1326
+
1327
+ @staticmethod
1328
+ def outlets():
1329
+ return ['out1']
1330
+
1331
+ @staticmethod
1332
+ def fuels():
1333
+ return ['methane', 'ethane', 'propane', 'butane',
1334
+ 'hydrogen']
1335
+
1336
+ def comp_init(self, nw):
1337
+
1338
+ component.comp_init(self, nw)
1339
+
1340
+ # number of mandatroy equations for
1341
+ # reaction balance: num_fl
1342
+ # mass flow: 1
1343
+ # pressure: 2
1344
+ # energy balance: 1
1345
+ self.num_eq = self.num_nw_fluids + 4
1346
+ for var in [self.lamb, self.ti]:
1347
+ if var.is_set is True:
1348
+ self.num_eq += 1
1349
+
1350
+ self.jacobian = np.zeros((
1351
+ self.num_eq,
1352
+ self.num_i + self.num_o + self.num_vars,
1353
+ self.num_nw_vars))
1354
+
1355
+ self.residual = np.zeros(self.num_eq)
1356
+ self.jacobian[0:1] = self.mass_flow_deriv()
1357
+ self.jacobian[1:3] = self.pressure_deriv()
1358
+
1359
+ if not self.fuel.is_set or not isinstance(self.fuel.val, dict):
1360
+ msg = ('You must specify the fuel composition for stoichimetric '
1361
+ 'combustion chamber ' + self.label + '.')
1362
+ logging.error(msg)
1363
+ raise TESPyComponentError(msg)
1364
+
1365
+ if not self.fuel_alias.is_set:
1366
+ msg = ('You must specify a fuel alias for stoichimetric '
1367
+ 'combustion chamber ' + self.label + '.')
1368
+ logging.error(msg)
1369
+ raise TESPyComponentError(msg)
1370
+
1371
+ if not self.air.is_set or not isinstance(self.air.val, dict):
1372
+ msg = ('You must specify the air composition for stoichimetric '
1373
+ 'combustion chamber ' + self.label + '.')
1374
+ logging.error(msg)
1375
+ raise TESPyComponentError(msg)
1376
+
1377
+ if not self.air_alias.is_set:
1378
+ msg = ('You must specify an air alias for stoichimetric '
1379
+ 'combustion chamber ' + self.label + '.')
1380
+ logging.error(msg)
1381
+ raise TESPyComponentError(msg)
1382
+
1383
+ # adjust the names for required fluids according to naming in the
1384
+ # network air
1385
+ for f in self.air.val.keys():
1386
+ alias = [x for x in nw.fluids if x in [
1387
+ a.replace(' ', '') for a in CP.get_aliases(f)]]
1388
+ if len(alias) > 0:
1389
+ self.air.val[alias[0]] = self.air.val.pop(f)
1390
+
1391
+ # fuel
1392
+ for f in list(self.fuel.val.keys()):
1393
+ alias = [x for x in self.air.val.keys() if x in [
1394
+ a.replace(' ', '') for a in CP.get_aliases(f)]]
1395
+ if len(alias) > 0:
1396
+ self.fuel.val[alias[0]] = self.fuel.val.pop(f)
1397
+
1398
+ # list of all fluids of air and fuel
1399
+ fluids = list(self.air.val.keys()) + list(self.fuel.val.keys())
1400
+
1401
+ # oxygen
1402
+ alias = [x for x in fluids if x in [
1403
+ a.replace(' ', '') for a in CP.get_aliases('O2')]]
1404
+ if len(alias) == 0:
1405
+ msg = 'Oxygen missing in input fluids.'
1406
+ logging.error(msg)
1407
+ raise TESPyComponentError(msg)
1408
+ else:
1409
+ self.o2 = alias[0]
1410
+
1411
+ # carbondioxide
1412
+ self.co2 = [x for x in nw.fluids if x in [
1413
+ a.replace(' ', '') for a in CP.get_aliases('CO2')]]
1414
+ if len(self.co2) == 0:
1415
+ self.co2 = 'CO2'
1416
+ else:
1417
+ self.co2 = self.co2[0]
1418
+
1419
+ # water
1420
+ self.h2o = [x for x in nw.fluids if x in [
1421
+ a.replace(' ', '') for a in CP.get_aliases('H2O')]]
1422
+ if len(self.h2o) == 0:
1423
+ self.h2o = 'H2O'
1424
+ else:
1425
+ self.h2o = self.h2o[0]
1426
+
1427
+ # calculate lower heating value of specified fuel
1428
+ self.lhv = self.calc_lhv()
1429
+ msg = ('Combustion chamber fuel (' + self.fuel_alias.val +
1430
+ ') LHV is ' + str(self.lhv) + ' for component ' +
1431
+ self.label + '.')
1432
+ logging.debug(msg)
1433
+ # generate fluid properties for stoichiometric flue gas
1434
+ self.stoich_flue_gas(nw)
1435
+
1436
+ def calc_lhv(self):
1437
+ r"""
1438
+ Calculate the lower heating value of the combustion chambers fuel.
1439
+
1440
+ Returns
1441
+ -------
1442
+ val : float
1443
+ Lower heating value of the combustion chambers fuel.
1444
+
1445
+ .. math::
1446
+
1447
+ LHV = \sum_{fuels} \left(-\frac{\sum_i {\Delta H_f^0}_i -
1448
+ \sum_j {\Delta H_f^0}_j }
1449
+ {M_{fuel}} \cdot x_{fuel} \right)\\
1450
+ \forall i \in \text{reation products},\\
1451
+ \forall j \in \text{reation educts},\\
1452
+ \forall fuel \in \text{fuels},\\
1453
+ \Delta H_f^0: \text{molar formation enthalpy},\\
1454
+ x_{fuel}: \text{mass fraction of fuel in fuel mixture}
1455
+ """
1456
+ hf = {}
1457
+ hf['hydrogen'] = 0
1458
+ hf['methane'] = -74.85
1459
+ hf['ethane'] = -84.68
1460
+ hf['propane'] = -103.8
1461
+ hf['butane'] = -124.51
1462
+ hf['O2'] = 0
1463
+ hf['CO2'] = -393.5
1464
+ # water (gaseous)
1465
+ hf['H2O'] = -241.8
1466
+
1467
+ lhv = 0
1468
+
1469
+ for f, x in self.fuel.val.items():
1470
+ molar_masses[f] = CP.PropsSI('M', f)
1471
+ fl = set(list(hf.keys())).intersection(
1472
+ set([a.replace(' ', '') for a in CP.get_aliases(f)]))
1473
+ if len(fl) == 0:
1474
+ continue
1475
+
1476
+ if list(fl)[0] in self.fuels():
1477
+ structure = fluid_structure(f)
1478
+
1479
+ n = {}
1480
+ for el in ['C', 'H', 'O']:
1481
+ if el in structure.keys():
1482
+ n[el] = structure[el]
1483
+ else:
1484
+ n[el] = 0
1485
+
1486
+ lhv += (-(n['H'] / 2 * hf['H2O'] + n['C'] * hf['CO2'] -
1487
+ ((n['C'] + n['H'] / 4) * hf['O2'] +
1488
+ hf[list(fl)[0]])) / molar_masses[f] * 1000) * x
1489
+
1490
+ return lhv
1491
+
1492
+ def stoich_flue_gas(self, nw):
1493
+ r"""
1494
+ Calculate the fluid composition of the stoichiometric flue gas.
1495
+
1496
+ - uses one mole of fuel as reference quantity and :math:`\lambda=1`
1497
+ for stoichiometric flue gas calculation (no oxygen in flue gas)
1498
+ - calculate molar quantities of (reactive) fuel components to determine
1499
+ water and carbondioxide mass fraction in flue gas
1500
+ - calculate required molar quantity for oxygen and required fresh
1501
+ air mass
1502
+ - calculate residual mass fractions for non reactive components of
1503
+ fresh air in the flue gas
1504
+ - calculate flue gas fluid composition
1505
+ - generate custom fluid porperties
1506
+
1507
+
1508
+
1509
+ Reactive components in fuel
1510
+
1511
+ .. math::
1512
+
1513
+ m_{fuel} = \frac{1}{M_{fuel}}\\
1514
+ m_{CO_2} = \sum_{i} \frac{x_{i} \cdot m_{fuel} \cdot num_{C,i}
1515
+ \cdot M_{CO_{2}}}{M_{i}}\\
1516
+ m_{H_{2}O} = \sum_{i} \frac{x_{i} \cdot m_{fuel} \cdot
1517
+ num_{H,i} \cdot M_{H_{2}O}}{2 \cdot M_{i}}\\
1518
+ \forall i \in \text{fuels in fuel vector},\\
1519
+ num = \text{number of atoms in molecule}
1520
+
1521
+ Other components of fuel vector
1522
+
1523
+ .. math::
1524
+
1525
+ m_{fg,j} = x_{j} \cdot m_{fuel}\\
1526
+ \forall j \in \text{non fuels in fuel vecotr, e.g. } CO_2,\\
1527
+ m_{fg,j} = \text{mass of fluid component j in flue gas}
1528
+
1529
+ Non-reactive components in air
1530
+
1531
+ .. math::
1532
+
1533
+ n_{O_2} = \left( \frac{m_{CO_2}}{M_{CO_2}} +
1534
+ \frac{m_{H_{2}O}}
1535
+ {0,5 \cdot M_{H_{2}O}} \right) \cdot \lambda,\\
1536
+ n_{O_2} = \text{mol of oxygen required}\\
1537
+ m_{air} = \frac{n_{O_2} \cdot M_{O_2}}{x_{O_{2}, air}},\\
1538
+ m_{air} = \text{required total air mass}\\
1539
+ m_{fg,j} = x_{j, air} \cdot m_{air}\\
1540
+ m_{fg, O_2} = 0,\\
1541
+ m_{fg,j} = \text{mass of fluid component j in flue gas}
1542
+
1543
+ Flue gas composition
1544
+
1545
+ .. math::
1546
+
1547
+ x_{fg,j} = \frac{m_{fg, j}}{m_{air} + m_{fuel}}
1548
+
1549
+ Parameters
1550
+ ----------
1551
+ nw : tespy.networks.network
1552
+ TESPy network to generate stoichiometric flue gas for.
1553
+ """
1554
+ lamb = 1
1555
+ n_fuel = 1
1556
+ m_fuel = 1 / molar_mass_flow(self.fuel.val) * n_fuel
1557
+ m_fuel_fg = m_fuel
1558
+ m_co2 = 0
1559
+ m_h2o = 0
1560
+ molar_masses[self.h2o] = CP.PropsSI('M', self.h2o)
1561
+ molar_masses[self.co2] = CP.PropsSI('M', self.co2)
1562
+ molar_masses[self.o2] = CP.PropsSI('M', self.o2)
1563
+
1564
+ self.fg = {}
1565
+ self.fg[self.co2] = 0
1566
+ self.fg[self.h2o] = 0
1567
+
1568
+ for f, x in self.fuel.val.items():
1569
+ fl = set(list(self.fuels())).intersection(
1570
+ set([a.replace(' ', '') for a in CP.get_aliases(f)]))
1571
+
1572
+ if len(fl) == 0:
1573
+ if f in self.fg.keys():
1574
+ self.fg[f] += x * m_fuel
1575
+ else:
1576
+ self.fg[f] = x * m_fuel
1577
+ else:
1578
+ n_fluid = x * m_fuel / molar_masses[f]
1579
+ m_fuel_fg -= n_fluid * molar_masses[f]
1580
+ structure = fluid_structure(f)
1581
+ n = {}
1582
+ for el in ['C', 'H', 'O']:
1583
+ if el in structure.keys():
1584
+ n[el] = structure[el]
1585
+ else:
1586
+ n[el] = 0
1587
+
1588
+ m_co2 += n_fluid * n['C'] * molar_masses[self.co2]
1589
+ m_h2o += n_fluid * n['H'] / 2 * molar_masses[self.h2o]
1590
+
1591
+ self.fg[self.co2] += m_co2
1592
+ self.fg[self.h2o] += m_h2o
1593
+
1594
+ n_o2 = (m_co2 / molar_masses[self.co2] +
1595
+ 0.5 * m_h2o / molar_masses[self.h2o]) * lamb
1596
+ m_air = n_o2 * molar_masses[self.o2] / self.air.val[self.o2]
1597
+
1598
+ self.air_min = m_air / m_fuel
1599
+
1600
+ for f, x in self.air.val.items():
1601
+ if f != self.o2:
1602
+ if f in self.fg.keys():
1603
+ self.fg[f] += m_air * x
1604
+ else:
1605
+ self.fg[f] = m_air * x
1606
+
1607
+ m_fg = m_fuel + m_air
1608
+
1609
+ for f in self.fg.keys():
1610
+ self.fg[f] /= m_fg
1611
+
1612
+ if not self.path.is_set:
1613
+ self.path.val = None
1614
+
1615
+ tespy_fluid(
1616
+ self.fuel_alias.val, self.fuel.val, [1000, nw.p_range_SI[1]],
1617
+ path=self.path.val)
1618
+ tespy_fluid(
1619
+ self.fuel_alias.val + '_fg', self.fg, [1000, nw.p_range_SI[1]],
1620
+ path=self.path.val)
1621
+ msg = (
1622
+ 'Generated lookup table for ' + self.fuel_alias.val + ' and for '
1623
+ 'stoichiometric flue gas at component ' + self.label + '.')
1624
+ logging.debug(msg)
1625
+
1626
+ if self.air_alias.val not in ['Air', 'air']:
1627
+ tespy_fluid(
1628
+ self.air_alias.val, self.air.val, [1000, nw.p_range_SI[1]],
1629
+ path=self.path.val)
1630
+ msg = ('Generated lookup table for ' + self.air_alias.val +
1631
+ ' at stoichiometric combustion chamber ' + self.label + '.')
1632
+ else:
1633
+ msg = ('Using CoolProp air at stoichiometric combustion chamber ' +
1634
+ self.label + '.')
1635
+ logging.debug(msg)
1636
+
1637
+ def reaction_balance(self, fluid):
1638
+ r"""
1639
+ Calculate the reaction balance for one fluid.
1640
+
1641
+ - determine molar mass flows of fuel and oxygen
1642
+ - calculate excess fuel
1643
+ - calculate residual value of the fluids balance
1644
+
1645
+ General equations
1646
+
1647
+ .. math::
1648
+
1649
+ res = \sum_i \left(x_{fluid,i} \cdot \dot{m}_{i}\right) -
1650
+ \sum_j \left(x_{fluid,j} \cdot \dot{m}_{j}\right)\\
1651
+ \forall i \in [1,2], \; \forall j \in [1]
1652
+
1653
+ \dot{m}_{air,min} = \dot{m}_{fuel} \cdot air_{min}
1654
+
1655
+ \lambda = \frac{\dot{m}_{air}}{\dot{m}_{air,min}}
1656
+
1657
+ Equation for fuel
1658
+
1659
+ .. math::
1660
+
1661
+ 0 = res - \left(\dot{m}_{f} - \dot{m}_{f,exc}\right)
1662
+
1663
+ \dot{m}_{f,exc} = \begin{cases}
1664
+ 0 & \lambda \geq 1\\
1665
+ \dot{m}_{f} - \frac{\dot{m}_{air}}
1666
+ {\lambda \cdot air_{min}} & \lambda < 1
1667
+ \end{cases}
1668
+
1669
+ Equation for air
1670
+
1671
+ .. math::
1672
+
1673
+ 0 = res - \begin{cases}
1674
+ -\dot{m}_{air,min} & \lambda \geq 1\\
1675
+ -\dot{m}_{air} & \lambda < 1
1676
+ \end{cases}
1677
+
1678
+ Equation for stoichiometric flue gas
1679
+
1680
+ .. math::
1681
+
1682
+ 0 = res + \dot{m}_{air,min} + \dot{m}_{f}
1683
+
1684
+ Equation for all other fluids
1685
+
1686
+ .. math::
1687
+
1688
+ 0 = res
1689
+
1690
+ Parameters
1691
+ ----------
1692
+ fluid : str
1693
+ The fluid to calculate the reation balance for.
1694
+
1695
+ Returns
1696
+ -------
1697
+ res : float
1698
+ Residual value of equation.
1699
+ """
1700
+ air = self.air_alias.val
1701
+ fuel = self.fuel_alias.val
1702
+ flue_gas = self.fuel_alias.val + '_fg'
1703
+
1704
+ ######################################################################
1705
+ # calculate fuel and air mass flow
1706
+ m_fuel = 0
1707
+ for i in self.inl:
1708
+ m_fuel += i.m.val_SI * i.fluid.val[fuel]
1709
+
1710
+ m_air = 0
1711
+ for i in self.inl:
1712
+ m_air += i.m.val_SI * i.fluid.val[air]
1713
+
1714
+ m_air_min = self.air_min * m_fuel
1715
+
1716
+ ######################################################################
1717
+ # calculate lambda if not specified
1718
+ if not self.lamb.is_set:
1719
+ self.lamb.val = m_air / (self.air_min * m_fuel)
1720
+
1721
+ ######################################################################
1722
+ # calculate excess fuel if lambda is smaller than 1
1723
+ m_fuel_exc = 0
1724
+ if self.lamb.val < 1:
1725
+ m_fuel_exc = m_fuel - m_air / (self.lamb.val * self.air_min)
1726
+
1727
+ ######################################################################
1728
+ # equation for air
1729
+ if fluid == air:
1730
+ if self.lamb.val >= 1:
1731
+ dm = -m_air_min
1732
+ else:
1733
+ dm = -m_air
1734
+
1735
+ ######################################################################
1736
+ # equation for fuel
1737
+ elif fluid == fuel:
1738
+ dm = -(m_fuel - m_fuel_exc)
1739
+
1740
+ ######################################################################
1741
+ # equation for flue gas
1742
+ elif fluid == flue_gas:
1743
+ dm = m_air_min + m_fuel
1744
+
1745
+ ######################################################################
1746
+ # equation for other components
1747
+ else:
1748
+ dm = 0
1749
+
1750
+ res = dm
1751
+ for i in self.inl:
1752
+ res += i.fluid.val[fluid] * i.m.val_SI
1753
+ for o in self.outl:
1754
+ res -= o.fluid.val[fluid] * o.m.val_SI
1755
+ return res
1756
+
1757
+ def energy_balance(self):
1758
+ r"""
1759
+ Calculate the energy balance of the adiabatic combustion chamber.
1760
+
1761
+ Returns
1762
+ -------
1763
+ res : float
1764
+ Residual value of equation.
1765
+
1766
+ .. math::
1767
+
1768
+ res = \sum_i \dot{m}_{in,i} \cdot
1769
+ \left( h_{in,i} - h_{in,i,ref} \right) - \sum_j \dot{m}_{out,j}
1770
+ \cdot \left( h_{out,j} - h_{out,j,ref} \right) +
1771
+ H_{I,f} \cdot \left(\sum_i \dot{m}_{in,i} \cdot x_{f,i} -
1772
+ \sum_j \dot{m}_{out,j} \cdot x_{f,j} \right)
1773
+ \; \forall i \in \text{inlets}\; \forall j \in \text{outlets}
1774
+
1775
+ Note
1776
+ ----
1777
+ The temperature for the reference state is set to 100 °C, as the
1778
+ custom fluid properties are inacurate at the dew-point of water in
1779
+ the flue gas!
1780
+
1781
+ - Reference temperature: 373.15 K.
1782
+ - Reference pressure: 1 bar.
1783
+ """
1784
+ T_ref = 373.15
1785
+ p_ref = 1e5
1786
+
1787
+ res = 0
1788
+ for i in self.inl:
1789
+ res += i.m.val_SI * (
1790
+ i.h.val_SI - h_mix_pT([0, p_ref, 0, i.fluid.val], T_ref))
1791
+ for o in self.outl:
1792
+ res -= o.m.val_SI * (
1793
+ o.h.val_SI - h_mix_pT([0, p_ref, 0, o.fluid.val], T_ref))
1794
+
1795
+ return res + self.calc_ti()
1796
+
1797
+ def lambda_func(self):
1798
+ r"""
1799
+ Calculate the residual for specified lambda.
1800
+
1801
+ Returns
1802
+ -------
1803
+ val : float
1804
+ Residual value of function.
1805
+
1806
+ .. math::
1807
+
1808
+ val = \lambda - \frac{\dot{m}_{air}}{\dot{m}_{air,min}}
1809
+ """
1810
+ air = self.air_alias.val
1811
+ fuel = self.fuel_alias.val
1812
+
1813
+ m_air = 0
1814
+ m_fuel = 0
1815
+
1816
+ for i in self.inl:
1817
+ m_air += (i.m.val_SI * i.fluid.val[air])
1818
+ m_fuel += (i.m.val_SI * i.fluid.val[fuel])
1819
+
1820
+ return self.lamb.val - m_air / (m_fuel * self.air_min)
1821
+
1822
+ def ti_func(self):
1823
+ r"""
1824
+ Calculate the residual for specified thermal input.
1825
+
1826
+ Returns
1827
+ -------
1828
+ val : float
1829
+ Residual value of function.
1830
+
1831
+ .. math::
1832
+
1833
+ val = ti - \dot{m}_f \cdot LHV
1834
+ """
1835
+ return self.ti.val - self.calc_ti()
1836
+
1837
+ def calc_ti(self):
1838
+ r"""
1839
+ Calculate the thermal input of the combustion chamber.
1840
+
1841
+ Returns
1842
+ -------
1843
+ ti : float
1844
+ Thermal input.
1845
+
1846
+ .. math::
1847
+
1848
+ ti = LHV \cdot \left[\sum_i \left(\dot{m}_{in,i} \cdot x_{f,i}
1849
+ \right) - \dot{m}_{out,1} \cdot x_{f,1} \right]
1850
+ \; \forall i \in [1,2]
1851
+ """
1852
+ m = 0
1853
+ for i in self.inl:
1854
+ m += i.m.val_SI * i.fluid.val[self.fuel_alias.val]
1855
+
1856
+ for o in self.outl:
1857
+ m -= o.m.val_SI * o.fluid.val[self.fuel_alias.val]
1858
+
1859
+ return m * self.lhv
1860
+
1861
+ def initialise_fluids(self, nw):
1862
+ r"""
1863
+ Calculate reaction balance for good generic flue gas starting values.
1864
+
1865
+ Parameters
1866
+ ----------
1867
+ nw : tespy.networks.network
1868
+ Network using this component object.
1869
+ """
1870
+ air = self.air_alias.val
1871
+ flue_gas = self.fuel_alias.val + '_fg'
1872
+
1873
+ for c in nw.comps.loc[self, 'outlets']:
1874
+ if not c.fluid.val_set[air]:
1875
+ c.fluid.val[air] = 0.8
1876
+ if not c.fluid.val_set[flue_gas]:
1877
+ c.fluid.val[flue_gas] = 0.2
1878
+
1879
+ def convergence_check(self, nw):
1880
+ r"""
1881
+ Perform a convergence check.
1882
+
1883
+ Parameters
1884
+ ----------
1885
+ nw : tespy.networks.network
1886
+ The network object using this component.
1887
+
1888
+ Note
1889
+ ----
1890
+ Manipulate enthalpies/pressure at inlet and outlet if not specified by
1891
+ user to match physically feasible constraints, keep fluid composition
1892
+ within feasible range and then propagates it towards the outlet.
1893
+ """
1894
+ air = self.air_alias.val
1895
+ flue_gas = self.fuel_alias.val + '_fg'
1896
+ fuel = self.fuel_alias.val
1897
+
1898
+ for c in nw.comps.loc[self, 'outlets']:
1899
+ if not c.fluid.val_set[air]:
1900
+ if c.fluid.val[air] > 0.95:
1901
+ c.fluid.val[air] = 0.95
1902
+ if c.fluid.val[air] < 0.5:
1903
+ c.fluid.val[air] = 0.5
1904
+
1905
+ if not c.fluid.val_set[flue_gas]:
1906
+ if c.fluid.val[flue_gas] > 0.5:
1907
+ c.fluid.val[flue_gas] = 0.5
1908
+ if c.fluid.val[flue_gas] < 0.05:
1909
+ c.fluid.val[flue_gas] = 0.05
1910
+
1911
+ if not c.fluid.val_set[fuel]:
1912
+ if c.fluid.val[fuel] > 0:
1913
+ c.fluid.val[fuel] = 0
1914
+
1915
+ nw.init_target(c, c.target)
1916
+
1917
+ for i in nw.comps.loc[self, 'inlets']:
1918
+ if i.m.val_SI < 0 and not i.m.val_set:
1919
+ i.m.val_SI = 0.01
1920
+
1921
+ for c in nw.comps.loc[self, 'outlets']:
1922
+ if c.m.val_SI < 0 and not c.m.val_set:
1923
+ c.m.val_SI = 10
1924
+ nw.init_target(c, c.target)
1925
+
1926
+ if self.lamb.val < 1 and not self.lamb.is_set:
1927
+ self.lamb.val = 2
1928
+
1929
+ def calc_parameters(self):
1930
+ r"""Postprocessing parameter calculation."""
1931
+ air = self.air_alias.val
1932
+ fuel = self.fuel_alias.val
1933
+
1934
+ m_fuel = 0
1935
+ for i in self.inl:
1936
+ m_fuel += i.m.val_SI * i.fluid.val[fuel]
1937
+
1938
+ m_air = 0
1939
+ for i in self.inl:
1940
+ m_air += i.m.val_SI * i.fluid.val[air]
1941
+
1942
+ self.lamb.val = (m_air / m_fuel) / self.air_min
1943
+
1944
+ S = 0
1945
+ T_ref = 373.15
1946
+ p_ref = 1e5
1947
+
1948
+ for i in self.inl:
1949
+ S += i.m.val_SI * (s_mix_ph(i.to_flow()) -
1950
+ s_mix_pT([0, p_ref, 0, i.fluid.val], T_ref))
1951
+
1952
+ for o in self.outl:
1953
+ S -= o.m.val_SI * (s_mix_ph(o.to_flow()) -
1954
+ s_mix_pT([0, p_ref, 0, o.fluid.val], T_ref))
1955
+
1956
+ self.S.val = S
1957
+
1958
+ ti = 0
1959
+ for i in self.inl:
1960
+ ti += i.m.val_SI * i.fluid.val[fuel] * self.lhv
1961
+
1962
+ self.ti.val = ti
1963
+
1964
+ self.check_parameter_bounds()
1965
+
1966
+ # %%
1967
+
1968
+
1969
+ class combustion_engine(combustion_chamber):
1970
+ r"""
1971
+ An internal combustion engine supplies power and heat cogeneration.
1972
+
1973
+ The combustion engine produces power and heat in cogeneration from fuel
1974
+ combustion. The combustion properties are identical to the combustion
1975
+ chamber. Thermal input and power output, heat output and heat losses are
1976
+ linked with an individual characteristic line for each property.
1977
+
1978
+ Equations
1979
+
1980
+ **mandatory equations**
1981
+
1982
+ - :func:`tespy.components.combustion.combustion_engine.reaction_balance`
1983
+ - :func:`tespy.components.combustion.combustion_engine.fluid_func`
1984
+ (for cooling water)
1985
+ - :func:`tespy.components.combustion.combustion_engine.mass_flow_func`
1986
+
1987
+ .. math::
1988
+
1989
+ 0 = p_{3,in} - p_{3,out}\\
1990
+ 0 = p_{4,in} - p_{3,out}
1991
+
1992
+ - :func:`tespy.components.combustion.combustion_engine.energy_balance`
1993
+
1994
+ **optional equations**
1995
+
1996
+ - :func:`tespy.components.combustion.combustion_engine.lambda_func`
1997
+ - :func:`tespy.components.combustion.combustion_engine.ti_func`
1998
+ - :func:`tespy.components.combustion.combustion_engine.Q1_func`
1999
+ - :func:`tespy.components.combustion.combustion_engine.Q2_func`
2000
+
2001
+ .. math::
2002
+
2003
+ 0 = p_{1,in} \cdot pr1 - p_{1,out}\\
2004
+ 0 = p_{2,in} \cdot pr2 - p_{2,out}
2005
+
2006
+ - loop 1 :func:`tespy.components.components.component.zeta_func`
2007
+ - loop 2 :func:`tespy.components.components.component.zeta_func`
2008
+
2009
+ Available fuels
2010
+
2011
+ - methane, ethane, propane, butane, hydrogen
2012
+
2013
+ Inlets/Outlets
2014
+
2015
+ - in1, in2 (cooling water), in3, in4 (air and fuel)
2016
+ - out1, out2 (cooling water), out3 (flue gas)
2017
+
2018
+ Image
2019
+
2020
+ .. image:: _images/combustion_engine.svg
2021
+ :scale: 100 %
2022
+ :alt: alternative text
2023
+ :align: center
2024
+
2025
+ .. note::
2026
+
2027
+ The fuel and the air components can be connected to either of the
2028
+ inlets.
2029
+
2030
+ Parameters
2031
+ ----------
2032
+ label : str
2033
+ The label of the component.
2034
+
2035
+ design : list
2036
+ List containing design parameters (stated as String).
2037
+
2038
+ offdesign : list
2039
+ List containing offdesign parameters (stated as String).
2040
+
2041
+ design_path: str
2042
+ Path to the components design case.
2043
+
2044
+ local_offdesign : boolean
2045
+ Treat this component in offdesign mode in a design calculation.
2046
+
2047
+ local_design : boolean
2048
+ Treat this component in design mode in an offdesign calculation.
2049
+
2050
+ char_warnings: boolean
2051
+ Ignore warnings on default characteristics usage for this component.
2052
+
2053
+ printout: boolean
2054
+ Include this component in the network's results printout.
2055
+
2056
+ lamb : float/tespy.tools.data_containers.dc_cp
2057
+ Air to stoichiometric air ratio, :math:`\lambda/1`.
2058
+
2059
+ ti : float/tespy.tools.data_containers.dc_cp
2060
+ Thermal input, (:math:`{LHV \cdot \dot{m}_f}`),
2061
+ :math:`ti/\text{W}`.
2062
+
2063
+ P : str/float/tespy.tools.data_containers.dc_cp
2064
+ Power output, :math:`P/\text{W}`.
2065
+
2066
+ Q1 : float/tespy.tools.data_containers.dc_cp
2067
+ Heat output 1, :math:`\dot Q/\text{W}`.
2068
+
2069
+ Q2 : float/tespy.tools.data_containers.dc_cp
2070
+ Heat output 2, :math:`\dot Q/\text{W}`.
2071
+
2072
+ Qloss : str/float/tespy.tools.data_containers.dc_cp
2073
+ Heat loss, :math:`\dot Q_{loss}/\text{W}`.
2074
+
2075
+ pr1 : float/tespy.tools.data_containers.dc_cp
2076
+ Pressure ratio heat outlet 1, :math:`pr/1`.
2077
+
2078
+ pr2 : float/tespy.tools.data_containers.dc_cp
2079
+ Pressure ratio heat outlet 2, :math:`pr/1`.
2080
+
2081
+ zeta1 : float/tespy.tools.data_containers.dc_cp
2082
+ Geometry independent friction coefficient heating loop 1,
2083
+ :math:`\zeta/\frac{1}{\text{m}^4}`.
2084
+
2085
+ zeta2 : float/tespy.tools.data_containers.dc_cp
2086
+ Geometry independent friction coefficient heating loop 2,
2087
+ :math:`\zeta/\frac{1}{\text{m}^4}`.
2088
+
2089
+ tiP_char : tespy.tools.charactersitics.char_line/tespy.tools.data_containers.dc_cc
2090
+ Characteristic line linking fuel input to power output.
2091
+
2092
+ Q1_char : tespy.tools.charactersitics.char_line/tespy.tools.data_containers.dc_cc
2093
+ Characteristic line linking heat output 1 to power output.
2094
+
2095
+ Q2_char : tespy.tools.charactersitics.char_line/tespy.tools.data_containers.dc_cc
2096
+ Characteristic line linking heat output 2 to power output.
2097
+
2098
+ Qloss_char : tespy.tools.charactersitics.char_line/tespy.tools.data_containers.dc_cc
2099
+ Characteristic line linking heat loss to power output.
2100
+
2101
+ Note
2102
+ ----
2103
+ For more information on the usage of the combustion engine see the
2104
+ examples in the tespy_examples repository.
2105
+
2106
+ Example
2107
+ -------
2108
+ The combustion chamber calculates energy input due to combustion as well as
2109
+ the flue gas composition based on the type of fuel and the amount of
2110
+ oxygen supplied. In this example a mixture of methane, hydrogen and
2111
+ carbondioxide is used as fuel. There are two cooling ports, the cooling
2112
+ water will flow through them in parallel.
2113
+
2114
+ >>> from tespy.components import (sink, source, combustion_engine, merge,
2115
+ ... splitter)
2116
+ >>> from tespy.connections import connection, ref
2117
+ >>> from tespy.networks import network
2118
+ >>> import numpy as np
2119
+ >>> import shutil
2120
+ >>> fluid_list = ['Ar', 'N2', 'O2', 'CO2', 'CH4', 'H2O']
2121
+ >>> nw = network(fluids=fluid_list, p_unit='bar', T_unit='C',
2122
+ ... iterinfo=False)
2123
+ >>> amb = source('ambient')
2124
+ >>> sf = source('fuel')
2125
+ >>> fg = sink('flue gas outlet')
2126
+ >>> cw_in = source('cooling water inlet')
2127
+ >>> sp = splitter('cooling water splitter', num_out=2)
2128
+ >>> me = merge('cooling water merge', num_in=2)
2129
+ >>> cw_out = sink('cooling water outlet')
2130
+ >>> chp = combustion_engine(label='internal combustion engine')
2131
+ >>> chp.component()
2132
+ 'combustion engine'
2133
+ >>> amb_comb = connection(amb, 'out1', chp, 'in3')
2134
+ >>> sf_comb = connection(sf, 'out1', chp, 'in4')
2135
+ >>> comb_fg = connection(chp, 'out3', fg, 'in1')
2136
+ >>> nw.add_conns(sf_comb, amb_comb, comb_fg)
2137
+ >>> cw_sp = connection(cw_in, 'out1', sp, 'in1')
2138
+ >>> sp_chp1 = connection(sp, 'out1', chp, 'in1')
2139
+ >>> sp_chp2 = connection(sp, 'out2', chp, 'in2')
2140
+ >>> chp1_me = connection(chp, 'out1', me, 'in1')
2141
+ >>> chp2_me = connection(chp, 'out2', me, 'in2')
2142
+ >>> me_cw = connection(me, 'out1', cw_out, 'in1')
2143
+ >>> nw.add_conns(cw_sp, sp_chp1, sp_chp2, chp1_me, chp2_me, me_cw)
2144
+
2145
+ The combustion engine produces a power output of 10 MW the oxygen to
2146
+ stoichiometric oxygen ratio is set to 1. Only pressure ratio 1 is set as
2147
+ we reconnect both cooling water streams. At the merge all pressure values
2148
+ will be identical automatically. Reference the mass flow at the splitter
2149
+ to be split in half.
2150
+
2151
+ >>> chp.set_attr(pr1=0.99, P=10e6, lamb=1.0,
2152
+ ... design=['pr1'], offdesign=['zeta1'])
2153
+ >>> amb_comb.set_attr(p=5, T=30, fluid={'Ar': 0.0129, 'N2': 0.7553,
2154
+ ... 'H2O': 0, 'CH4': 0, 'CO2': 0.0004, 'O2': 0.2314})
2155
+ >>> sf_comb.set_attr(m0=0.1, T=30, fluid={'CO2': 0, 'Ar': 0, 'N2': 0,
2156
+ ... 'O2': 0, 'H2O': 0, 'CH4': 1})
2157
+ >>> cw_sp.set_attr(p=3, T=60, m=50, fluid={'CO2': 0, 'Ar': 0, 'N2': 0,
2158
+ ... 'O2': 0, 'H2O': 1, 'CH4': 0})
2159
+ >>> sp_chp2.set_attr(m=ref(sp_chp1, 1, 0))
2160
+ >>> mode = 'design'
2161
+ >>> nw.solve(mode=mode)
2162
+ >>> nw.save('tmp')
2163
+ >>> round(chp.ti.val)
2164
+ 25300000.0
2165
+ >>> round(chp.Q1.val)
2166
+ 4980000.0
2167
+ >>> chp.set_attr(Q1=4e6, P=np.nan)
2168
+ >>> mode = 'offdesign'
2169
+ >>> nw.solve(mode=mode, init_path='tmp', design_path='tmp')
2170
+ >>> round(chp.ti.val)
2171
+ 17794554.0
2172
+ >>> round(chp.P.val / chp.P.design, 3)
2173
+ 0.617
2174
+ >>> chp.set_attr(P=chp.P.design * 0.75, Q1=np.nan)
2175
+ >>> mode = 'offdesign'
2176
+ >>> nw.solve(mode=mode, init_path='tmp', design_path='tmp')
2177
+ >>> round(chp.ti.val)
2178
+ 20550000.0
2179
+ >>> round(chp.P.val / chp.P.design, 3)
2180
+ 0.75
2181
+ >>> shutil.rmtree('./tmp', ignore_errors=True)
2182
+ """
2183
+
2184
+ @staticmethod
2185
+ def component():
2186
+ return 'combustion engine'
2187
+
2188
+ @staticmethod
2189
+ def attr():
2190
+ return {'lamb': dc_cp(min_val=1),
2191
+ 'ti': dc_cp(min_val=0),
2192
+ 'P': dc_cp(val=1e6, d=1, min_val=1),
2193
+ 'Q1': dc_cp(min_val=1), 'Q2': dc_cp(min_val=1),
2194
+ 'Qloss': dc_cp(val=1e5, d=1, min_val=1),
2195
+ 'pr1': dc_cp(max_val=1),
2196
+ 'pr2': dc_cp(max_val=1),
2197
+ 'zeta1': dc_cp(min_val=0),
2198
+ 'zeta2': dc_cp(min_val=0),
2199
+ 'tiP_char': dc_cc(),
2200
+ 'Q1_char': dc_cc(),
2201
+ 'Q2_char': dc_cc(),
2202
+ 'Qloss_char': dc_cc(),
2203
+ 'S': dc_simple()}
2204
+
2205
+ @staticmethod
2206
+ def inlets():
2207
+ return ['in1', 'in2', 'in3', 'in4']
2208
+
2209
+ @staticmethod
2210
+ def outlets():
2211
+ return ['out1', 'out2', 'out3']
2212
+
2213
+ def comp_init(self, nw):
2214
+
2215
+ if not self.P.is_set:
2216
+ self.set_attr(P='var')
2217
+ msg = ('The power output of combustion engines must be set! '
2218
+ 'We are adding the power output of component ' +
2219
+ self.label + ' as custom variable of the system.')
2220
+ logging.info(msg)
2221
+
2222
+ if not self.Qloss.is_set:
2223
+ self.set_attr(Qloss='var')
2224
+ msg = ('The heat loss of combustion engines must be set! '
2225
+ 'We are adding the heat loss of component ' +
2226
+ self.label + ' as custom variable of the system.')
2227
+ logging.info(msg)
2228
+
2229
+ combustion_chamber.comp_init(self, nw)
2230
+
2231
+ # number of mandatroy equations for
2232
+ # cooling loops fluid balances: 2 * num_fl
2233
+ # mass flow: 3
2234
+ # pressure: 2
2235
+ # reaction balance: num_fl
2236
+ # energy balance, characteristic functions: 5
2237
+ self.num_eq = self.num_nw_fluids * 2 + 3 + 2 + self.num_nw_fluids + 5
2238
+ # P and Qloss are not included, as the equations are mandatory anyway
2239
+ for var in [self.lamb, self.ti, self.Q1, self.Q2,
2240
+ self.pr1, self.pr2, self.zeta1, self.zeta2]:
2241
+ if var.is_set is True:
2242
+ self.num_eq += 1
2243
+
2244
+ self.jacobian = np.zeros((
2245
+ self.num_eq,
2246
+ self.num_i + self.num_o + self.num_vars,
2247
+ self.num_nw_vars))
2248
+
2249
+ pos = self.num_nw_fluids * 2
2250
+
2251
+ self.residual = np.zeros(self.num_eq)
2252
+ self.jacobian[0:pos] = self.fluid_deriv()
2253
+ self.jacobian[pos:pos + 3] = self.mass_flow_deriv()
2254
+ self.jacobian[pos + 3:pos + 5] = self.pressure_deriv()
2255
+
2256
+ def equations(self):
2257
+ r"""Calculate residual vector with results of equations."""
2258
+ k = 0
2259
+ ######################################################################
2260
+ # equations for fluids in cooling loops
2261
+ self.residual[k:self.num_nw_fluids * 2] = self.fluid_func()
2262
+ k += self.num_nw_fluids * 2
2263
+
2264
+ ######################################################################
2265
+ # equations for mass flow
2266
+ self.residual[k:k + 3] = self.mass_flow_func()
2267
+ k += 3
2268
+
2269
+ ######################################################################
2270
+ # equations for pressure balance in combustion
2271
+ self.residual[k] = self.inl[2].p.val_SI - self.outl[2].p.val_SI
2272
+ k += 1
2273
+ self.residual[k] = self.inl[2].p.val_SI - self.inl[3].p.val_SI
2274
+ k += 1
2275
+
2276
+ ######################################################################
2277
+ # equations for fluids in combustion chamber
2278
+ for fluid in self.inl[0].fluid.val.keys():
2279
+ if np.absolute(self.residual[k]) > err ** 2 or self.it % 4 == 0:
2280
+ self.residual[k] = self.reaction_balance(fluid)
2281
+ k += 1
2282
+
2283
+ ######################################################################
2284
+ # equation for combustion engine energy balance
2285
+ if np.absolute(self.residual[k]) > err ** 2 or self.it % 4 == 0:
2286
+ self.residual[k] = self.energy_balance()
2287
+ k += 1
2288
+
2289
+ ######################################################################
2290
+ # equation for power to thermal input ratio from characteristic line
2291
+ if np.absolute(self.residual[k]) > err ** 2 or self.it % 4 == 0:
2292
+ self.residual[k] = self.tiP_char_func()
2293
+ k += 1
2294
+
2295
+ ######################################################################
2296
+ # equations for heat outputs from characteristic line
2297
+ if np.absolute(self.residual[k]) > err ** 2 or self.it % 4 == 0:
2298
+ self.residual[k] = self.Q1_char_func()
2299
+ k += 1
2300
+
2301
+ if np.absolute(self.residual[k]) > err ** 2 or self.it % 4 == 0:
2302
+ self.residual[k] = self.Q2_char_func()
2303
+ k += 1
2304
+
2305
+ ######################################################################
2306
+ # equation for heat loss from characteristic line
2307
+ if np.absolute(self.residual[k]) > err ** 2 or self.it % 4 == 0:
2308
+ self.residual[k] = self.Qloss_char_func()
2309
+ k += 1
2310
+
2311
+ ######################################################################
2312
+ # equation for specified lambda
2313
+ if self.lamb.is_set:
2314
+ self.residual[k] = self.lambda_func()
2315
+ k += 1
2316
+
2317
+ ######################################################################
2318
+ # equation for specified thermal input
2319
+ if self.ti.is_set:
2320
+ self.residual[k] = self.ti_func()
2321
+ k += 1
2322
+
2323
+ ######################################################################
2324
+ # equations for specified heat ouptputs
2325
+ if self.Q1.is_set:
2326
+ self.residual[k] = self.Q1_func()
2327
+ k += 1
2328
+
2329
+ if self.Q2.is_set:
2330
+ self.residual[k] = self.Q2_func()
2331
+ k += 1
2332
+
2333
+ ######################################################################
2334
+ # equations for specified pressure ratios at cooling loops
2335
+ if self.pr1.is_set:
2336
+ self.residual[k] = (
2337
+ self.pr1.val * self.inl[0].p.val_SI - self.outl[0].p.val_SI)
2338
+ k += 1
2339
+
2340
+ if self.pr2.is_set:
2341
+ self.residual[k] = (
2342
+ self.pr2.val * self.inl[1].p.val_SI - self.outl[1].p.val_SI)
2343
+ k += 1
2344
+
2345
+ ######################################################################
2346
+ # equations for specified zeta values at cooling loops
2347
+ if self.zeta1.is_set:
2348
+ if np.absolute(self.residual[k]) > err ** 2 or self.it % 4 == 0:
2349
+ self.residual[k] = self.zeta_func(
2350
+ zeta='zeta1', inconn=0, outconn=0)
2351
+ k += 1
2352
+
2353
+ if self.zeta2.is_set:
2354
+ if np.absolute(self.residual[k]) > err ** 2 or self.it % 4 == 0:
2355
+ self.residual[k] = self.zeta_func(
2356
+ zeta='zeta2', inconn=1, outconn=1)
2357
+ k += 1
2358
+
2359
+ def derivatives(self, increment_filter):
2360
+ r"""Calculate matrix of partial derivatives for given equations."""
2361
+ ######################################################################
2362
+ # derivatives cooling water fluid, mass balance and pressure are static
2363
+ k = self.num_nw_fluids * 2 + 5
2364
+
2365
+ ######################################################################
2366
+ # derivatives for reaction balance
2367
+ for fluid in self.nw_fluids:
2368
+ # fresh air and fuel inlets
2369
+ if not increment_filter[2, 0]:
2370
+ self.jacobian[k, 2, 0] = self.rb_numeric_deriv('m', 2, fluid)
2371
+ if not all(increment_filter[2, 3:]):
2372
+ self.jacobian[k, 2, 3:] = self.rb_numeric_deriv(
2373
+ 'fluid', 2, fluid)
2374
+ if not increment_filter[3, 0]:
2375
+ self.jacobian[k, 3, 0] = self.rb_numeric_deriv('m', 3, fluid)
2376
+ if not all(increment_filter[3, 3:]):
2377
+ self.jacobian[k, 3, 3:] = self.rb_numeric_deriv(
2378
+ 'fluid', 3, fluid)
2379
+
2380
+ # combustion outlet
2381
+ if not increment_filter[6, 0]:
2382
+ self.jacobian[k, 6, 0] = self.rb_numeric_deriv('m', 6, fluid)
2383
+ if not all(increment_filter[6, 3:]):
2384
+ self.jacobian[k, 6, 3:] = self.rb_numeric_deriv(
2385
+ 'fluid', 6, fluid)
2386
+ k += 1
2387
+
2388
+ ######################################################################
2389
+ # derivatives for energy balance
2390
+ f = self.energy_balance
2391
+ # mass flow cooling water
2392
+ for i in [0, 1]:
2393
+ self.jacobian[k, i, 0] = -(
2394
+ self.outl[i].h.val_SI - self.inl[i].h.val_SI)
2395
+
2396
+ # mass flow and pressure for combustion reaction
2397
+ for i in [2, 3, 6]:
2398
+ if not increment_filter[i, 0]:
2399
+ self.jacobian[k, i, 0] = self.numeric_deriv(f, 'm', i)
2400
+ if not increment_filter[i, 1]:
2401
+ self.jacobian[k, i, 1] = self.numeric_deriv(f, 'p', i)
2402
+
2403
+ # enthalpy
2404
+ for i in range(4):
2405
+ self.jacobian[k, i, 2] = self.inl[i].m.val_SI
2406
+ for i in range(3):
2407
+ self.jacobian[k, i + 4, 2] = -self.outl[i].m.val_SI
2408
+
2409
+ # fluid composition
2410
+ for fl in self.fuel_list:
2411
+ pos = 3 + self.nw_fluids.index(fl)
2412
+ lhv = self.fuels[fl]['LHV']
2413
+ self.jacobian[k, 2, pos] = self.inl[2].m.val_SI * lhv
2414
+ self.jacobian[k, 3, pos] = self.inl[3].m.val_SI * lhv
2415
+ self.jacobian[k, 6, pos] = -self.outl[2].m.val_SI * lhv
2416
+
2417
+ # power and heat loss
2418
+ if self.P.is_var:
2419
+ self.jacobian[k, 7 + self.P.var_pos, 0] = (
2420
+ self.numeric_deriv(f, 'P', 7))
2421
+ if self.Qloss.is_var:
2422
+ self.jacobian[k, 7 + self.Qloss.var_pos, 0] = (
2423
+ self.numeric_deriv(f, 'Qloss', 7))
2424
+ k += 1
2425
+
2426
+ ######################################################################
2427
+ # derivatives for thermal input to power charactersitics
2428
+ f = self.tiP_char_func
2429
+ for i in [2, 3, 6]:
2430
+ if not increment_filter[i, 0]:
2431
+ self.jacobian[k, i, 0] = self.numeric_deriv(f, 'm', i)
2432
+ if not all(increment_filter[i, 3:]):
2433
+ self.jacobian[k, i, 3:] = self.numeric_deriv(f, 'fluid', i)
2434
+
2435
+ if self.P.is_var:
2436
+ self.jacobian[k, 7 + self.P.var_pos, 0] = (
2437
+ self.numeric_deriv(f, 'P', 7))
2438
+ k += 1
2439
+
2440
+ ######################################################################
2441
+ # derivatives for heat output 1 to power charactersitics
2442
+ f = self.Q1_char_func
2443
+ if not increment_filter[0, 0]:
2444
+ self.jacobian[k, 0, 0] = self.numeric_deriv(f, 'm', 0)
2445
+ if not increment_filter[0, 2]:
2446
+ self.jacobian[k, 0, 2] = self.numeric_deriv(f, 'h', 0)
2447
+ if not increment_filter[4, 2]:
2448
+ self.jacobian[k, 4, 2] = self.numeric_deriv(f, 'h', 4)
2449
+ for i in [2, 3, 6]:
2450
+ if not increment_filter[i, 0]:
2451
+ self.jacobian[k, i, 0] = self.numeric_deriv(f, 'm', i)
2452
+ if not all(increment_filter[i, 3:]):
2453
+ self.jacobian[k, i, 3:] = self.numeric_deriv(f, 'fluid', i)
2454
+
2455
+ if self.P.is_var:
2456
+ self.jacobian[k, 7 + self.P.var_pos, 0] = (
2457
+ self.numeric_deriv(f, 'P', 7))
2458
+ k += 1
2459
+
2460
+ ######################################################################
2461
+ # derivatives for heat output 2 to power charactersitics
2462
+ f = self.Q2_char_func
2463
+ if not increment_filter[1, 0]:
2464
+ self.jacobian[k, 1, 0] = self.numeric_deriv(f, 'm', 1)
2465
+ if not increment_filter[1, 2]:
2466
+ self.jacobian[k, 1, 2] = self.numeric_deriv(f, 'h', 1)
2467
+ if not increment_filter[5, 2]:
2468
+ self.jacobian[k, 5, 2] = self.numeric_deriv(f, 'h', 5)
2469
+ for i in [2, 3, 6]:
2470
+ if not increment_filter[i, 0]:
2471
+ self.jacobian[k, i, 0] = self.numeric_deriv(f, 'm', i)
2472
+ if not all(increment_filter[i, 3:]):
2473
+ self.jacobian[k, i, 3:] = self.numeric_deriv(f, 'fluid', i)
2474
+
2475
+ if self.P.is_var:
2476
+ self.jacobian[k, 7 + self.P.var_pos, 0] = (
2477
+ self.numeric_deriv(f, 'P', 7))
2478
+ k += 1
2479
+
2480
+ ######################################################################
2481
+ # derivatives for heat loss to power charactersitics
2482
+ f = self.Qloss_char_func
2483
+ for i in [2, 3, 6]:
2484
+ if not increment_filter[i, 0]:
2485
+ self.jacobian[k, i, 0] = self.numeric_deriv(f, 'm', i)
2486
+ if not all(increment_filter[i, 3:]):
2487
+ self.jacobian[k, i, 3:] = self.numeric_deriv(f, 'fluid', i)
2488
+
2489
+ if self.P.is_var:
2490
+ self.jacobian[k, 7 + self.P.var_pos, 0] = (
2491
+ self.numeric_deriv(f, 'P', 7))
2492
+ if self.Qloss.is_var:
2493
+ self.jacobian[k, 7 + self.Qloss.var_pos, 0] = (
2494
+ self.numeric_deriv(f, 'Qloss', 7))
2495
+ k += 1
2496
+
2497
+ ######################################################################
2498
+ # derivatives for specified lambda
2499
+ if self.lamb.is_set:
2500
+ f = self.lambda_func
2501
+ if not increment_filter[2, 0]:
2502
+ self.jacobian[k, 2, 0] = self.numeric_deriv(f, 'm', 2)
2503
+ if not all(increment_filter[2, 3:]):
2504
+ self.jacobian[k, 2, 3:] = self.numeric_deriv(f, 'fluid', 2)
2505
+ if not increment_filter[3, 0]:
2506
+ self.jacobian[k, 3, 0] = self.numeric_deriv(f, 'm', 3)
2507
+ if not all(increment_filter[3, 3:]):
2508
+ self.jacobian[k, 3, 3:] = self.numeric_deriv(f, 'fluid', 3)
2509
+ k += 1
2510
+
2511
+ ######################################################################
2512
+ # derivatives for specified thermal input
2513
+ if self.ti.is_set:
2514
+ f = self.ti_func
2515
+ for i in [2, 3, 6]:
2516
+ self.jacobian[k, i, 0] = self.numeric_deriv(f, 'm', i)
2517
+ self.jacobian[k, i, 3:] = (
2518
+ self.numeric_deriv(self.ti_func, 'fluid', i))
2519
+ k += 1
2520
+
2521
+ ######################################################################
2522
+ # derivatives for specified heat outputs
2523
+ if self.Q1.is_set:
2524
+ self.jacobian[k, 0, 0] = -(
2525
+ self.outl[0].h.val_SI - self.inl[0].h.val_SI)
2526
+ self.jacobian[k, 0, 2] = self.inl[0].m.val_SI
2527
+ self.jacobian[k, 4, 2] = -self.inl[0].m.val_SI
2528
+ k += 1
2529
+
2530
+ if self.Q2.is_set:
2531
+ self.jacobian[k, 1, 0] = -(
2532
+ self.outl[1].h.val_SI - self.inl[1].h.val_SI)
2533
+ self.jacobian[k, 1, 2] = self.inl[1].m.val_SI
2534
+ self.jacobian[k, 5, 2] = -self.inl[1].m.val_SI
2535
+ k += 1
2536
+
2537
+ ######################################################################
2538
+ # derivatives for specified pressure ratio at cooling loops
2539
+ if self.pr1.is_set:
2540
+ self.jacobian[k, 0, 1] = self.pr1.val
2541
+ self.jacobian[k, 4, 1] = -1
2542
+ k += 1
2543
+
2544
+ if self.pr2.is_set:
2545
+ self.jacobian[k, 1, 1] = self.pr2.val
2546
+ self.jacobian[k, 5, 1] = -1
2547
+ k += 1
2548
+
2549
+ ######################################################################
2550
+ # derivatives for specified zeta values at cooling loops
2551
+ if self.zeta1.is_set:
2552
+ f = self.zeta_func
2553
+ if not increment_filter[0, 0]:
2554
+ self.jacobian[k, 0, 0] = self.numeric_deriv(
2555
+ f, 'm', 0, zeta='zeta1', inconn=0, outconn=0)
2556
+ if not increment_filter[0, 1]:
2557
+ self.jacobian[k, 0, 1] = self.numeric_deriv(
2558
+ f, 'p', 0, zeta='zeta1', inconn=0, outconn=0)
2559
+ if not increment_filter[0, 2]:
2560
+ self.jacobian[k, 0, 2] = self.numeric_deriv(
2561
+ f, 'h', 0, zeta='zeta1', inconn=0, outconn=0)
2562
+ if not increment_filter[4, 1]:
2563
+ self.jacobian[k, 4, 1] = self.numeric_deriv(
2564
+ f, 'p', 4, zeta='zeta1', inconn=0, outconn=0)
2565
+ if not increment_filter[4, 2]:
2566
+ self.jacobian[k, 4, 2] = self.numeric_deriv(
2567
+ f, 'h', 4, zeta='zeta1', inconn=0, outconn=0)
2568
+ k += 1
2569
+
2570
+ if self.zeta2.is_set:
2571
+ f = self.zeta_func
2572
+ if not increment_filter[1, 0]:
2573
+ self.jacobian[k, 1, 0] = self.numeric_deriv(
2574
+ f, 'm', 1, zeta='zeta2', inconn=1, outconn=1)
2575
+ if not increment_filter[1, 1]:
2576
+ self.jacobian[k, 1, 1] = self.numeric_deriv(
2577
+ f, 'p', 1, zeta='zeta2', inconn=1, outconn=1)
2578
+ if not increment_filter[1, 2]:
2579
+ self.jacobian[k, 1, 2] = self.numeric_deriv(
2580
+ f, 'h', 1, zeta='zeta2', inconn=1, outconn=1)
2581
+ if not increment_filter[5, 1]:
2582
+ self.jacobian[k, 5, 1] = self.numeric_deriv(
2583
+ f, 'p', 5, zeta='zeta2', inconn=1, outconn=1)
2584
+ if not increment_filter[5, 2]:
2585
+ self.jacobian[k, 5, 2] = self.numeric_deriv(
2586
+ f, 'h', 5, zeta='zeta2', inconn=1, outconn=1)
2587
+ k += 1
2588
+
2589
+ def fluid_func(self):
2590
+ r"""
2591
+ Calculate the vector of residual values for cooling loop fluid balance.
2592
+
2593
+ Returns
2594
+ -------
2595
+ residual : list
2596
+ Vector of residual values for component's fluid balance.
2597
+
2598
+ .. math::
2599
+
2600
+ 0 = fluid_{i,in_{j}} - fluid_{i,out_{j}}\\
2601
+ \forall i \in \mathrm{fluid}, \; \forall j \in [1, 2]
2602
+ """
2603
+ residual = []
2604
+ for i in range(2):
2605
+ for fluid, x in self.inl[i].fluid.val.items():
2606
+ residual += [x - self.outl[i].fluid.val[fluid]]
2607
+ return residual
2608
+
2609
+ def mass_flow_func(self):
2610
+ r"""
2611
+ Calculate the residual value for component's mass flow balance.
2612
+
2613
+ Returns
2614
+ -------
2615
+ residual : list
2616
+ Vector with residual value for component's mass flow balance.
2617
+
2618
+ .. math::
2619
+
2620
+ 0 = \dot{m}_{in,i} - \dot{m}_{out,i}\\
2621
+ \forall i \in [1, 2]\\
2622
+ 0 = \dot{m}_{in,3} + \dot{m}_{in,4} - \dot{m}_{out,3}
2623
+ """
2624
+ residual = []
2625
+ for i in range(2):
2626
+ residual += [self.inl[i].m.val_SI - self.outl[i].m.val_SI]
2627
+ residual += [self.inl[2].m.val_SI + self.inl[3].m.val_SI -
2628
+ self.outl[2].m.val_SI]
2629
+ return residual
2630
+
2631
+ def fluid_deriv(self):
2632
+ r"""
2633
+ Calculate the partial derivatives for cooling loop fluid balance.
2634
+
2635
+ Returns
2636
+ -------
2637
+ deriv : ndarray
2638
+ Matrix with partial derivatives for the fluid equations.
2639
+ """
2640
+ deriv = np.zeros((self.num_nw_fluids * 2,
2641
+ 7 + self.num_vars,
2642
+ self.num_nw_vars))
2643
+ for i in range(self.num_nw_fluids):
2644
+ deriv[i, 0, i + 3] = 1
2645
+ deriv[i, 4, i + 3] = -1
2646
+ for j in range(self.num_nw_fluids):
2647
+ deriv[i + 1 + j, 1, j + 3] = 1
2648
+ deriv[i + 1 + j, 5, j + 3] = -1
2649
+ return deriv
2650
+
2651
+ def mass_flow_deriv(self):
2652
+ r"""
2653
+ Calculate the partial derivatives for all mass flow balance equations.
2654
+
2655
+ Returns
2656
+ -------
2657
+ deriv : list
2658
+ Matrix with partial derivatives for the fluid equations.
2659
+ """
2660
+ deriv = np.zeros((3, 7 + self.num_vars, self.num_nw_vars))
2661
+ for i in range(2):
2662
+ deriv[i, i, 0] = 1
2663
+ for j in range(2):
2664
+ deriv[j, self.num_i + j, 0] = -1
2665
+ deriv[2, 2, 0] = 1
2666
+ deriv[2, 3, 0] = 1
2667
+ deriv[2, 6, 0] = -1
2668
+ return deriv
2669
+
2670
+ def pressure_deriv(self):
2671
+ r"""
2672
+ Calculate the partial derivatives for combustion pressure equations.
2673
+
2674
+ Returns
2675
+ -------
2676
+ deriv : list
2677
+ Matrix with partial derivatives for the fluid equations.
2678
+ """
2679
+ deriv = np.zeros((2, 7 + self.num_vars, self.num_nw_vars))
2680
+ for k in range(2):
2681
+ deriv[k, 2, 1] = 1
2682
+ deriv[0, 6, 1] = -1
2683
+ deriv[1, 3, 1] = -1
2684
+ return deriv
2685
+
2686
+ def energy_balance(self):
2687
+ r"""
2688
+ Calculate the energy balance of the combustion engine.
2689
+
2690
+ Returns
2691
+ -------
2692
+ res : float
2693
+ Residual value of equation.
2694
+
2695
+ .. math::
2696
+
2697
+ \begin{split}
2698
+ res = & \sum_i \dot{m}_{in,i} \cdot
2699
+ \left( h_{in,i} - h_{in,i,ref} \right)\\
2700
+ & - \sum_j \dot{m}_{out,3} \cdot
2701
+ \left( h_{out,3} - h_{out,3,ref} \right)\\
2702
+ & + H_{I,f} \cdot
2703
+ \left(\sum_i \left(\dot{m}_{in,i} \cdot x_{f,i} \right)-
2704
+ \dot{m}_{out,3} \cdot x_{f,3} \right)\\
2705
+ & - \dot{Q}_1 - \dot{Q}_2 - P - \dot{Q}_{loss}\\
2706
+ \end{split}\\
2707
+ \forall i \in [3,4]
2708
+
2709
+ Note
2710
+ ----
2711
+ The temperature for the reference state is set to 20 °C, thus
2712
+ the water may be liquid. In order to make sure, the state is
2713
+ referring to the lower heating value, the necessary enthalpy
2714
+ difference for evaporation is added.
2715
+
2716
+ - Reference temperature: 293.15 K.
2717
+ - Reference pressure: 1 bar.
2718
+ """
2719
+ T_ref = 293.15
2720
+ p_ref = 1e5
2721
+
2722
+ res = 0
2723
+ for i in self.inl[2:]:
2724
+ res += i.m.val_SI * (
2725
+ i.h.val_SI - h_mix_pT([0, p_ref, 0, i.fluid.val], T_ref))
2726
+
2727
+ for o in self.outl[2:]:
2728
+ dh = 0
2729
+ n_h2o = o.fluid.val[self.h2o] / molar_masses[self.h2o]
2730
+ if n_h2o > 0:
2731
+ p = p_ref * n_h2o / molar_mass_flow(o.fluid.val)
2732
+ h = h_pT(p, T_ref, self.h2o)
2733
+ try:
2734
+ flow = [0, p, 0, {self.h2o: 1}]
2735
+ h_steam = h_mix_pQ(flow, 1)
2736
+ # CP.PropsSI('H', 'P', p, 'Q', 1, self.h2o)
2737
+ except ValueError:
2738
+ flow = [0, 615, 0, {self.h2o: 1}]
2739
+ h_steam = h_mix_pQ(flow, 1)
2740
+ if h < h_steam:
2741
+ dh = (h_steam - h) * o.fluid.val[self.h2o]
2742
+
2743
+ res -= o.m.val_SI * (
2744
+ o.h.val_SI - h_mix_pT([0, p_ref, 0, o.fluid.val], T_ref) - dh)
2745
+
2746
+ res += self.calc_ti()
2747
+
2748
+ # cooling water
2749
+ for i in range(2):
2750
+ res -= self.inl[i].m.val_SI * (
2751
+ self.outl[i].h.val_SI - self.inl[i].h.val_SI)
2752
+
2753
+ # power output and heat loss
2754
+ res -= self.P.val + self.Qloss.val
2755
+
2756
+ return res
2757
+
2758
+ def bus_func(self, bus):
2759
+ r"""
2760
+ Calculate the value of the bus function.
2761
+
2762
+ Parameters
2763
+ ----------
2764
+ bus : tespy.connections.bus
2765
+ TESPy bus object.
2766
+
2767
+ Returns
2768
+ -------
2769
+ val : float
2770
+ Value of energy transfer :math:`\dot{E}`. This value is passed to
2771
+ :py:meth:`tespy.components.components.component.calc_bus_value`
2772
+ for value manipulation according to the specified characteristic
2773
+ line of the bus.
2774
+
2775
+ .. math::
2776
+
2777
+ \dot{E} = \begin{cases}
2778
+ LHV \cdot \dot{m}_{f} & \text{key = 'TI'}\\
2779
+ P & \text{key = 'P'}\\
2780
+ \dot{m}_1 \cdot \left( h_{1,out} - h_{1,in} \right) +
2781
+ \dot{m}_2 \cdot \left( h_{2,out} - h_{2,in} \right) &
2782
+ \text{key = 'Q'}\\
2783
+ \dot{m}_1 \cdot \left( h_{1,out} - h_{1,in} \right) &
2784
+ \text{key = 'Q1'}\\
2785
+ \dot{m}_2 \cdot \left( h_{2,out} - h_{2,in} \right) &
2786
+ \text{key = 'Q2'}\\
2787
+ \dot{Q}_{loss} & \text{key = 'Qloss'}
2788
+ \end{cases}
2789
+
2790
+ \dot{Q}_1=\dot{m}_1 \cdot \left( h_{1,out} - h_{1,in} \right)\\
2791
+ \dot{Q}_2=\dot{m}_2 \cdot \left( h_{2,out} - h_{2,in} \right)
2792
+ """
2793
+ ######################################################################
2794
+ # value for bus parameter of thermal input (TI)
2795
+ if bus['param'] == 'TI':
2796
+ val = self.calc_ti()
2797
+
2798
+ ######################################################################
2799
+ # value for bus parameter of power output (P)
2800
+ elif bus['param'] == 'P':
2801
+ val = self.calc_P()
2802
+
2803
+ ######################################################################
2804
+ # value for bus parameter of total heat production (Q)
2805
+ elif bus['param'] == 'Q':
2806
+ val = 0
2807
+ for j in range(2):
2808
+ i = self.inl[j]
2809
+ o = self.outl[j]
2810
+ val += i.m.val_SI * (o.h.val_SI - i.h.val_SI)
2811
+
2812
+ ######################################################################
2813
+ # value for bus parameter of heat production 1 (Q1)
2814
+ elif bus['param'] == 'Q1':
2815
+ i = self.inl[0]
2816
+ o = self.outl[0]
2817
+ val = i.m.val_SI * (o.h.val_SI - i.h.val_SI)
2818
+
2819
+ ######################################################################
2820
+ # value for bus parameter of heat production 2 (Q2)
2821
+ elif bus['param'] == 'Q2':
2822
+ i = self.inl[1]
2823
+ o = self.outl[1]
2824
+ val = i.m.val_SI * (o.h.val_SI - i.h.val_SI)
2825
+
2826
+ ######################################################################
2827
+ # value for bus parameter of heat loss (Qloss)
2828
+ elif bus['param'] == 'Qloss':
2829
+ val = self.calc_Qloss()
2830
+
2831
+ ######################################################################
2832
+ # missing/invalid bus parameter
2833
+ else:
2834
+ msg = ('The parameter ' + str(bus['param']) +
2835
+ ' is not a valid parameter for a ' + self.component() + '.')
2836
+ logging.error(msg)
2837
+ raise ValueError(msg)
2838
+
2839
+ return val
2840
+
2841
+ def bus_deriv(self, bus):
2842
+ r"""
2843
+ Calculate the matrix of partial derivatives of the bus function.
2844
+
2845
+ Parameters
2846
+ ----------
2847
+ bus : tespy.connections.bus
2848
+ TESPy bus object.
2849
+
2850
+ Returns
2851
+ -------
2852
+ deriv : ndarray
2853
+ Matrix of partial derivatives.
2854
+ """
2855
+ deriv = np.zeros((1, 7 + self.num_vars, self.num_nw_vars))
2856
+ f = self.calc_bus_value
2857
+ b = bus.comps.loc[self]
2858
+
2859
+ ######################################################################
2860
+ # derivatives for bus parameter of thermal input (TI)
2861
+ if b['param'] == 'TI':
2862
+ for i in [2, 3, 6]:
2863
+ deriv[0, i, 0] = self.numeric_deriv(f, 'm', i, bus=bus)
2864
+ deriv[0, i, 3:] = self.numeric_deriv(f, 'fluid', i, bus=bus)
2865
+
2866
+ ######################################################################
2867
+ # derivatives for bus parameter of power production (P) or
2868
+ # heat loss (Qloss)
2869
+ elif b['param'] == 'P' or b['param'] == 'Qloss':
2870
+ for i in [2, 3, 6]:
2871
+ deriv[0, i, 0] = self.numeric_deriv(f, 'm', i, bus=bus)
2872
+ deriv[0, i, 3:] = self.numeric_deriv(f, 'fluid', i, bus=bus)
2873
+
2874
+ # variable power
2875
+ if self.P.is_var:
2876
+ deriv[0, 7 + self.P.var_pos, 0] = (
2877
+ self.numeric_deriv(f, 'P', 7, bus=bus))
2878
+
2879
+ ######################################################################
2880
+ # derivatives for bus parameter of total heat production (Q)
2881
+ elif b['param'] == 'Q':
2882
+ for i in range(2):
2883
+ deriv[0, i, 0] = self.numeric_deriv(f, 'm', i, bus=bus)
2884
+ deriv[0, i, 2] = self.numeric_deriv(f, 'h', i, bus=bus)
2885
+ deriv[0, i + 4, 2] = self.numeric_deriv(f, 'h', i + 4, bus=bus)
2886
+
2887
+ ######################################################################
2888
+ # derivatives for bus parameter of heat production 1 (Q1)
2889
+ elif b['param'] == 'Q1':
2890
+ deriv[0, 0, 0] = self.numeric_deriv(f, 'm', 0, bus=bus)
2891
+ deriv[0, 0, 2] = self.numeric_deriv(f, 'h', 0, bus=bus)
2892
+ deriv[0, 4, 2] = self.numeric_deriv(f, 'h', 4, bus=bus)
2893
+
2894
+ ######################################################################
2895
+ # derivatives for bus parameter of heat production 2 (Q2)
2896
+ elif b['param'] == 'Q2':
2897
+ deriv[0, 1, 0] = self.numeric_deriv(f, 'm', 1, bus=bus)
2898
+ deriv[0, 1, 2] = self.numeric_deriv(f, 'h', 1, bus=bus)
2899
+ deriv[0, 5, 2] = self.numeric_deriv(f, 'h', 5, bus=bus)
2900
+
2901
+ ######################################################################
2902
+ # missing/invalid bus parameter
2903
+ else:
2904
+ msg = ('The parameter ' + str(b['param']) +
2905
+ ' is not a valid parameter for a ' + self.component() + '.')
2906
+ logging.error(msg)
2907
+ raise ValueError(msg)
2908
+
2909
+ return deriv
2910
+
2911
+ def Q1_func(self):
2912
+ r"""
2913
+ Calculate residual value with specified Q1.
2914
+
2915
+ Returns
2916
+ -------
2917
+ val : float
2918
+ Residual value of equation.
2919
+
2920
+ .. math::
2921
+
2922
+ val = \dot{m}_1 \cdot \left(h_{out,1} -
2923
+ h_{in,1} \right) - \dot{Q}_1
2924
+ """
2925
+ i = self.inl[0]
2926
+ o = self.outl[0]
2927
+
2928
+ return self.Q1.val - i.m.val_SI * (o.h.val_SI - i.h.val_SI)
2929
+
2930
+ def Q2_func(self):
2931
+ r"""
2932
+ Calculate residual value with specified Q2.
2933
+
2934
+ Returns
2935
+ -------
2936
+ val : float
2937
+ Residual value of equation.
2938
+
2939
+ .. math::
2940
+
2941
+ 0 = \dot{m}_2 \cdot \left(h_{out,2} - h_{in,2} \right) -
2942
+ \dot{Q}_2
2943
+ """
2944
+ i = self.inl[1]
2945
+ o = self.outl[1]
2946
+
2947
+ return self.Q2.val - i.m.val_SI * (o.h.val_SI - i.h.val_SI)
2948
+
2949
+ def tiP_char_func(self):
2950
+ r"""
2951
+ Calculate the relation of output power and thermal input.
2952
+
2953
+ Returns
2954
+ -------
2955
+ val : float
2956
+ Residual value of equation.
2957
+
2958
+ .. math::
2959
+
2960
+ 0 = P \cdot f_{TI}\left(\frac{P}{P_{ref}}\right)- LHV \cdot
2961
+ \left[\sum_i \left(\dot{m}_{in,i} \cdot
2962
+ x_{f,i}\right) - \dot{m}_{out,3} \cdot x_{f,3} \right]
2963
+ \; \forall i \in [1,2]
2964
+ """
2965
+ if np.isnan(self.P.design):
2966
+ expr = 1
2967
+ else:
2968
+ expr = self.P.val / self.P.design
2969
+
2970
+ return self.calc_ti() - self.tiP_char.func.evaluate(expr) * self.P.val
2971
+
2972
+ def Q1_char_func(self):
2973
+ r"""
2974
+ Calculate the relation of heat output 1 and thermal input.
2975
+
2976
+ Returns
2977
+ -------
2978
+ val : float
2979
+ Residual value of equation.
2980
+
2981
+ .. math::
2982
+
2983
+ \begin{split}
2984
+ 0 = & \dot{m}_1 \cdot \left(h_{out,1} - h_{in,1} \right) \cdot
2985
+ f_{TI}\left(\frac{P}{P_{ref}}\right) \\
2986
+ & - LHV \cdot \left[\sum_i
2987
+ \left(\dot{m}_{in,i} \cdot x_{f,i}\right) -
2988
+ \dot{m}_{out,3} \cdot x_{f,3} \right] \cdot
2989
+ f_{Q1}\left(\frac{P}{P_{ref}}\right)\\
2990
+ \end{split}\\
2991
+ \forall i \in [3,4]
2992
+ """
2993
+ i = self.inl[0]
2994
+ o = self.outl[0]
2995
+
2996
+ if np.isnan(self.P.design):
2997
+ expr = 1
2998
+ else:
2999
+ expr = self.P.val / self.P.design
3000
+
3001
+ return (self.calc_ti() * self.Q1_char.func.evaluate(expr) -
3002
+ self.tiP_char.func.evaluate(expr) * i.m.val_SI *
3003
+ (o.h.val_SI - i.h.val_SI))
3004
+
3005
+ def Q2_char_func(self):
3006
+ r"""
3007
+ Calculate the relation of heat output 2 and thermal input.
3008
+
3009
+ Returns
3010
+ -------
3011
+ val : float
3012
+ Residual value of equation.
3013
+
3014
+ .. math::
3015
+
3016
+ \begin{split}
3017
+ 0 = & \dot{m}_2 \cdot \left(h_{out,2} - h_{in,2} \right) \cdot
3018
+ f_{TI}\left(\frac{P}{P_{ref}}\right) \\
3019
+ & - LHV \cdot \left[\sum_i
3020
+ \left(\dot{m}_{in,i} \cdot x_{f,i}\right) -
3021
+ \dot{m}_{out,3} \cdot x_{f,3} \right] \cdot
3022
+ f_{Q2}\left(\frac{P}{P_{ref}}\right)\\
3023
+ \end{split}\\
3024
+ \forall i \in [3,4]
3025
+ """
3026
+ i = self.inl[1]
3027
+ o = self.outl[1]
3028
+
3029
+ if np.isnan(self.P.design):
3030
+ expr = 1
3031
+ else:
3032
+ expr = self.P.val / self.P.design
3033
+
3034
+ return (self.calc_ti() * self.Q2_char.func.evaluate(expr) -
3035
+ self.tiP_char.func.evaluate(expr) * i.m.val_SI *
3036
+ (o.h.val_SI - i.h.val_SI))
3037
+
3038
+ def Qloss_char_func(self):
3039
+ r"""
3040
+ Calculate the relation of heat loss and thermal input.
3041
+
3042
+ Returns
3043
+ -------
3044
+ val : float
3045
+ Residual value of equation.
3046
+
3047
+ .. math::
3048
+
3049
+ \begin{split}
3050
+ 0 = & \dot{Q}_{loss} \cdot
3051
+ f_{TI}\left(\frac{P}{P_{ref}}\right) \\
3052
+ & - LHV \cdot \left[\sum_i
3053
+ \left(\dot{m}_{in,i} \cdot x_{f,i}\right) -
3054
+ \dot{m}_{out,3} \cdot x_{f,3} \right] \cdot
3055
+ f_{QLOSS}\left(\frac{P}{P_{ref}}\right)\\
3056
+ \end{split}\\
3057
+ \forall i \in [3,4]
3058
+ """
3059
+ if np.isnan(self.P.design):
3060
+ expr = 1
3061
+ else:
3062
+ expr = self.P.val / self.P.design
3063
+
3064
+ return (self.calc_ti() * self.Qloss_char.func.evaluate(expr) -
3065
+ self.tiP_char.func.evaluate(expr) * self.Qloss.val)
3066
+
3067
+ def calc_ti(self):
3068
+ r"""
3069
+ Calculate the thermal input of the combustion engine.
3070
+
3071
+ Returns
3072
+ -------
3073
+ ti : float
3074
+ Thermal input.
3075
+
3076
+ .. math::
3077
+
3078
+ ti = LHV \cdot \left[\sum_i \left(\dot{m}_{in,i} \cdot x_{f,i}
3079
+ \right) - \dot{m}_{out,3} \cdot x_{f,3} \right]
3080
+
3081
+ \forall i \in [3,4]
3082
+ """
3083
+ ti = 0
3084
+ for f in self.fuel_list:
3085
+ m = 0
3086
+ for i in self.inl[2:]:
3087
+ m += i.m.val_SI * i.fluid.val[f]
3088
+
3089
+ for o in self.outl[2:]:
3090
+ m -= o.m.val_SI * o.fluid.val[f]
3091
+
3092
+ ti += m * self.fuels[f]['LHV']
3093
+
3094
+ return ti
3095
+
3096
+ def calc_P(self):
3097
+ r"""
3098
+ Calculate the power output of the combustion engine.
3099
+
3100
+ Returns
3101
+ -------
3102
+ P : float
3103
+ Power output.
3104
+
3105
+ .. math::
3106
+
3107
+ P = \frac{LHV \cdot \dot{m}_{f}}
3108
+ {f_{TI}\left(\frac{P}{P_{ref}}\right)}
3109
+
3110
+ """
3111
+ if np.isnan(self.P.design):
3112
+ expr = 1
3113
+ else:
3114
+ expr = self.P.val / self.P.design
3115
+
3116
+ return self.calc_ti() / self.tiP_char.func.evaluate(expr)
3117
+
3118
+ def calc_Qloss(self):
3119
+ r"""
3120
+ Calculate the heat loss of the combustion engine.
3121
+
3122
+ Returns
3123
+ -------
3124
+ Qloss : float
3125
+ Heat loss.
3126
+
3127
+ .. math::
3128
+
3129
+ \dot{Q}_{loss} = \frac{LHV \cdot \dot{m}_{f} \cdot
3130
+ f_{QLOSS}\left(\frac{P}{P_{ref}}\right)}
3131
+ {f_{TI}\left(\frac{P}{P_{ref}}\right)}
3132
+ """
3133
+ if np.isnan(self.P.design):
3134
+ expr = 1
3135
+ else:
3136
+ expr = self.P.val / self.P.design
3137
+
3138
+ return (self.calc_ti() * self.Qloss_char.func.evaluate(expr) /
3139
+ self.tiP_char.func.evaluate(expr))
3140
+
3141
+ def initialise_fluids(self, nw):
3142
+ r"""
3143
+ Calculate reaction balance for generic starting values at outlet.
3144
+
3145
+ Parameters
3146
+ ----------
3147
+ nw : tespy.networks.network
3148
+ Network using this component object.
3149
+ """
3150
+ N_2 = 0.7655
3151
+ O_2 = 0.2345
3152
+
3153
+ n_fuel = 1
3154
+ lamb = 3
3155
+
3156
+ fact_fuel = {}
3157
+ sum_fuel = 0
3158
+ for f in self.fuel_list:
3159
+ fact_fuel[f] = 0
3160
+ for i in self.inl:
3161
+ fact_fuel[f] += i.fluid.val[f] / 2
3162
+ sum_fuel += fact_fuel[f]
3163
+
3164
+ for f in self.fuel_list:
3165
+ fact_fuel[f] /= sum_fuel
3166
+
3167
+ m_co2 = 0
3168
+ m_h2o = 0
3169
+ m_fuel = 0
3170
+ for f in self.fuel_list:
3171
+ m_co2 += (n_fuel * self.fuels[f]['C'] * molar_masses[self.co2] *
3172
+ fact_fuel[f])
3173
+ m_h2o += (n_fuel * self.fuels[f]['H'] /
3174
+ 2 * molar_masses[self.h2o] * fact_fuel[f])
3175
+ m_fuel += n_fuel * molar_masses[f] * fact_fuel[f]
3176
+
3177
+ n_o2 = (m_co2 / molar_masses[self.co2] +
3178
+ 0.5 * m_h2o / molar_masses[self.h2o]) * lamb
3179
+
3180
+ m_air = n_o2 * molar_masses[self.o2] / O_2
3181
+ m_fg = m_air + m_fuel
3182
+
3183
+ m_o2 = n_o2 * molar_masses[self.o2] * (1 - 1 / lamb)
3184
+ m_n2 = N_2 * m_air
3185
+
3186
+ fg = {
3187
+ self.n2: m_n2 / m_fg,
3188
+ self.co2: m_co2 / m_fg,
3189
+ self.o2: m_o2 / m_fg,
3190
+ self.h2o: m_h2o / m_fg
3191
+ }
3192
+
3193
+ o = self.outl[2]
3194
+ for fluid, x in o.fluid.val.items():
3195
+ if not o.fluid.val_set[fluid] and fluid in fg.keys():
3196
+ o.fluid.val[fluid] = fg[fluid]
3197
+
3198
+ @staticmethod
3199
+ def initialise_source(c, key):
3200
+ r"""
3201
+ Return a starting value for pressure and enthalpy at outlet.
3202
+
3203
+ Parameters
3204
+ ----------
3205
+ c : tespy.connections.connection
3206
+ Connection to perform initialisation on.
3207
+
3208
+ key : str
3209
+ Fluid property to retrieve.
3210
+
3211
+ Returns
3212
+ -------
3213
+ val : float
3214
+ Starting value for pressure/enthalpy in SI units.
3215
+
3216
+ .. math::
3217
+
3218
+ val = \begin{cases}
3219
+ 5 \cdot 10^5 & \text{key = 'p'}\\
3220
+ 10^6 & \text{key = 'h'}
3221
+ \end{cases}
3222
+ """
3223
+ if key == 'p':
3224
+ return 5e5
3225
+ elif key == 'h':
3226
+ return 10e5
3227
+
3228
+ @staticmethod
3229
+ def initialise_target(c, key):
3230
+ r"""
3231
+ Return a starting value for pressure and enthalpy at inlet.
3232
+
3233
+ Parameters
3234
+ ----------
3235
+ c : tespy.connections.connection
3236
+ Connection to perform initialisation on.
3237
+
3238
+ key : str
3239
+ Fluid property to retrieve.
3240
+
3241
+ Returns
3242
+ -------
3243
+ val : float
3244
+ Starting value for pressure/enthalpy in SI units.
3245
+
3246
+ .. math::
3247
+
3248
+ val = \begin{cases}
3249
+ 5 \cdot 10^5 & \text{key = 'p'}\\
3250
+ 5 \cdot 10^5 & \text{key = 'h'}
3251
+ \end{cases}
3252
+ """
3253
+ if key == 'p':
3254
+ return 5e5
3255
+ elif key == 'h':
3256
+ return 5e5
3257
+
3258
+ def calc_parameters(self):
3259
+ r"""Postprocessing parameter calculation."""
3260
+ i1 = self.inl[0].to_flow()
3261
+ i2 = self.inl[1].to_flow()
3262
+ o1 = self.outl[0].to_flow()
3263
+ o2 = self.outl[1].to_flow()
3264
+
3265
+ v_i1 = v_mix_ph(i1, T0=self.inl[0].T.val_SI)
3266
+ v_o1 = v_mix_ph(o1, T0=self.outl[0].T.val_SI)
3267
+ v_i2 = v_mix_ph(i2, T0=self.inl[1].T.val_SI)
3268
+ v_o2 = v_mix_ph(o1, T0=self.outl[1].T.val_SI)
3269
+
3270
+ self.pr1.val = o1[1] / i1[1]
3271
+ self.pr2.val = o2[1] / i2[1]
3272
+ self.zeta1.val = ((i1[1] - o1[1]) * np.pi ** 2 /
3273
+ (8 * i1[0] ** 2 * (v_i1 + v_o1) / 2))
3274
+ self.zeta2.val = ((i2[1] - o2[1]) * np.pi ** 2 /
3275
+ (8 * i2[0] ** 2 * (v_i2 + v_o2) / 2))
3276
+ self.Q1.val = i1[0] * (o1[2] - i1[2])
3277
+ self.Q2.val = i2[0] * (o2[2] - i2[2])
3278
+ self.P.val = self.calc_P()
3279
+ self.Qloss.val = self.calc_Qloss()
3280
+
3281
+ # get bound errors for characteristic lines
3282
+ if np.isnan(self.P.design):
3283
+ expr = 1
3284
+ else:
3285
+ expr = self.P.val / self.P.design
3286
+ self.tiP_char.func.get_bound_errors(expr, self.label)
3287
+ self.Qloss_char.func.get_bound_errors(expr, self.label)
3288
+ self.Q1_char.func.get_bound_errors(expr, self.label)
3289
+ self.Q2_char.func.get_bound_errors(expr, self.label)
3290
+
3291
+ combustion_chamber.calc_parameters(self)