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.
- TESPy-0.3.0.data/data/src/tespy/data/char_lines.json +213 -0
- TESPy-0.3.0.data/data/src/tespy/data/char_maps.json +52 -0
- TESPy-0.3.0.dist-info/DESCRIPTION.rst +117 -0
- TESPy-0.3.0.dist-info/METADATA +156 -0
- TESPy-0.3.0.dist-info/RECORD +34 -0
- TESPy-0.3.0.dist-info/WHEEL +6 -0
- TESPy-0.3.0.dist-info/metadata.json +1 -0
- TESPy-0.3.0.dist-info/top_level.txt +1 -0
- tespy/__init__.py +27 -0
- tespy/components/__init__.py +28 -0
- tespy/components/basics.py +507 -0
- tespy/components/combustion.py +3291 -0
- tespy/components/components.py +813 -0
- tespy/components/customs.py +729 -0
- tespy/components/heat_exchangers.py +3116 -0
- tespy/components/nodes.py +1890 -0
- tespy/components/piping.py +536 -0
- tespy/components/reactors.py +965 -0
- tespy/components/subsystems.py +89 -0
- tespy/components/turbomachinery.py +1726 -0
- tespy/connections.py +1062 -0
- tespy/data/__init__.py +1 -0
- tespy/data/char_lines.json +213 -0
- tespy/data/char_maps.json +52 -0
- tespy/networks/__init__.py +3 -0
- tespy/networks/network_reader.py +660 -0
- tespy/networks/networks.py +3001 -0
- tespy/tools/__init__.py +13 -0
- tespy/tools/characteristics.py +663 -0
- tespy/tools/data_containers.py +429 -0
- tespy/tools/fluid_properties.py +1763 -0
- tespy/tools/global_vars.py +15 -0
- tespy/tools/helpers.py +592 -0
- tespy/tools/logger.py +216 -0
|
@@ -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)
|