fairyfly-therm 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,925 @@
1
+ # coding=utf-8
2
+ """Gas materials."""
3
+ from __future__ import division
4
+ import xml.etree.ElementTree as ET
5
+
6
+ from fairyfly._lockable import lockable
7
+ from fairyfly.typing import float_positive, float_in_range, tuple_with_length, \
8
+ uuid_from_therm_id
9
+
10
+ from ._base import _ResourceObjectBase
11
+
12
+
13
+ @lockable
14
+ class PureGas(_ResourceObjectBase):
15
+ """Custom gas gap layer.
16
+
17
+ This object allows you to specify specific values for conductivity,
18
+ viscosity and specific heat through the following formula:
19
+
20
+ property = A + (B * T) + (C * T ** 2)
21
+
22
+ where:
23
+
24
+ * A, B, and C = regression coefficients for the gas
25
+ * T = temperature [K]
26
+
27
+ Note that setting properties B and C to 0 will mean the property will be
28
+ equal to the A coefficient.
29
+
30
+ Args:
31
+ conductivity_coeff_a: First conductivity coefficient.
32
+ Or conductivity in [W/m-K] if b and c coefficients are 0.
33
+ viscosity_coeff_a: First viscosity coefficient.
34
+ Or viscosity in [kg/m-s] if b and c coefficients are 0.
35
+ specific_heat_coeff_a: First specific heat coefficient.
36
+ Or specific heat in [J/kg-K] if b and c coefficients are 0.
37
+ conductivity_coeff_b: Second conductivity coefficient. Default = 0.
38
+ viscosity_coeff_b: Second viscosity coefficient. Default = 0.
39
+ specific_heat_coeff_b: Second specific heat coefficient. Default = 0.
40
+ conductivity_coeff_c: Third conductivity coefficient. Default = 0.
41
+ viscosity_coeff_c: Third viscosity coefficient. Default = 0.
42
+ specific_heat_coeff_c: Third specific heat coefficient. Default = 0.
43
+ specific_heat_ratio: A number for the the ratio of the specific heat at
44
+ constant pressure, to the specific heat at constant volume.
45
+ Default is 1.0 for Air.
46
+ molecular_weight: Number between 20 and 200 for the mass of 1 mol of
47
+ the substance in grams. Default is 20.0.
48
+ identifier: Text string for a unique object ID. Must be a UUID in the
49
+ format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. If None, a UUID will
50
+ automatically be generated. (Default: None).
51
+
52
+ Properties:
53
+ * identifier
54
+ * display_name
55
+ * conductivity_coeff_a
56
+ * viscosity_coeff_a
57
+ * specific_heat_coeff_a
58
+ * conductivity_coeff_b
59
+ * viscosity_coeff_b
60
+ * specific_heat_coeff_b
61
+ * conductivity_coeff_c
62
+ * viscosity_coeff_c
63
+ * specific_heat_coeff_c
64
+ * specific_heat_ratio
65
+ * molecular_weight
66
+ * protected
67
+ * user_data
68
+
69
+ Usage:
70
+
71
+ .. code-block:: python
72
+
73
+ co2_mat = PureGas(0.0146, 0.000014, 827.73)
74
+ co2_mat.display_name = 'CO2'
75
+ co2_gap.specific_heat_ratio = 1.4
76
+ co2_gap.molecular_weight = 44
77
+ print(co2_gap)
78
+ """
79
+ __slots__ = ('_conductivity_coeff_a', '_viscosity_coeff_a', '_specific_heat_coeff_a',
80
+ '_conductivity_coeff_b', '_viscosity_coeff_b', '_specific_heat_coeff_b',
81
+ '_conductivity_coeff_c', '_viscosity_coeff_c', '_specific_heat_coeff_c',
82
+ '_specific_heat_ratio', '_molecular_weight')
83
+
84
+ def __init__(
85
+ self, conductivity_coeff_a, viscosity_coeff_a, specific_heat_coeff_a,
86
+ conductivity_coeff_b=0, viscosity_coeff_b=0, specific_heat_coeff_b=0,
87
+ conductivity_coeff_c=0, viscosity_coeff_c=0, specific_heat_coeff_c=0,
88
+ specific_heat_ratio=1.0, molecular_weight=20.0, identifier=None):
89
+ """Initialize custom gas energy material."""
90
+ _ResourceObjectBase.__init__(self, identifier)
91
+ self.conductivity_coeff_a = conductivity_coeff_a
92
+ self.viscosity_coeff_a = viscosity_coeff_a
93
+ self.specific_heat_coeff_a = specific_heat_coeff_a
94
+ self.conductivity_coeff_b = conductivity_coeff_b
95
+ self.viscosity_coeff_b = viscosity_coeff_b
96
+ self.specific_heat_coeff_b = specific_heat_coeff_b
97
+ self.conductivity_coeff_c = conductivity_coeff_c
98
+ self.viscosity_coeff_c = viscosity_coeff_c
99
+ self.specific_heat_coeff_c = specific_heat_coeff_c
100
+ self.specific_heat_ratio = specific_heat_ratio
101
+ self.molecular_weight = molecular_weight
102
+
103
+ @property
104
+ def conductivity_coeff_a(self):
105
+ """Get or set the first conductivity coefficient."""
106
+ return self._conductivity_coeff_a
107
+
108
+ @conductivity_coeff_a.setter
109
+ def conductivity_coeff_a(self, coeff):
110
+ self._conductivity_coeff_a = float(coeff)
111
+
112
+ @property
113
+ def viscosity_coeff_a(self):
114
+ """Get or set the first viscosity coefficient."""
115
+ return self._viscosity_coeff_a
116
+
117
+ @viscosity_coeff_a.setter
118
+ def viscosity_coeff_a(self, coeff):
119
+ self._viscosity_coeff_a = float_positive(coeff)
120
+
121
+ @property
122
+ def specific_heat_coeff_a(self):
123
+ """Get or set the first specific heat coefficient."""
124
+ return self._specific_heat_coeff_a
125
+
126
+ @specific_heat_coeff_a.setter
127
+ def specific_heat_coeff_a(self, coeff):
128
+ self._specific_heat_coeff_a = float_positive(coeff)
129
+
130
+ @property
131
+ def conductivity_coeff_b(self):
132
+ """Get or set the second conductivity coefficient."""
133
+ return self._conductivity_coeff_b
134
+
135
+ @conductivity_coeff_b.setter
136
+ def conductivity_coeff_b(self, coeff):
137
+ self._conductivity_coeff_b = float(coeff)
138
+
139
+ @property
140
+ def viscosity_coeff_b(self):
141
+ """Get or set the second viscosity coefficient."""
142
+ return self._viscosity_coeff_b
143
+
144
+ @viscosity_coeff_b.setter
145
+ def viscosity_coeff_b(self, coeff):
146
+ self._viscosity_coeff_b = float(coeff)
147
+
148
+ @property
149
+ def specific_heat_coeff_b(self):
150
+ """Get or set the second specific heat coefficient."""
151
+ return self._specific_heat_coeff_b
152
+
153
+ @specific_heat_coeff_b.setter
154
+ def specific_heat_coeff_b(self, coeff):
155
+ self._specific_heat_coeff_b = float(coeff)
156
+
157
+ @property
158
+ def conductivity_coeff_c(self):
159
+ """Get or set the third conductivity coefficient."""
160
+ return self._conductivity_coeff_c
161
+
162
+ @conductivity_coeff_c.setter
163
+ def conductivity_coeff_c(self, coeff):
164
+ self._conductivity_coeff_c = float(coeff)
165
+
166
+ @property
167
+ def viscosity_coeff_c(self):
168
+ """Get or set the third viscosity coefficient."""
169
+ return self._viscosity_coeff_c
170
+
171
+ @viscosity_coeff_c.setter
172
+ def viscosity_coeff_c(self, coeff):
173
+ self._viscosity_coeff_c = float(coeff)
174
+
175
+ @property
176
+ def specific_heat_coeff_c(self):
177
+ """Get or set the third specific heat coefficient."""
178
+ return self._specific_heat_coeff_c
179
+
180
+ @specific_heat_coeff_c.setter
181
+ def specific_heat_coeff_c(self, coeff):
182
+ self._specific_heat_coeff_c = float(coeff)
183
+
184
+ @property
185
+ def specific_heat_ratio(self):
186
+ """Get or set the specific heat ratio."""
187
+ return self._specific_heat_ratio
188
+
189
+ @specific_heat_ratio.setter
190
+ def specific_heat_ratio(self, number):
191
+ number = float(number)
192
+ assert 1 <= number, 'Input specific_heat_ratio ({}) must be > 1.'.format(number)
193
+ self._specific_heat_ratio = number
194
+
195
+ @property
196
+ def molecular_weight(self):
197
+ """Get or set the molecular weight."""
198
+ return self._molecular_weight
199
+
200
+ @molecular_weight.setter
201
+ def molecular_weight(self, number):
202
+ self._molecular_weight = float_in_range(
203
+ number, 2.0, 300.0, 'gas material molecular weight')
204
+
205
+ @property
206
+ def conductivity(self):
207
+ """Conductivity of the gas in the absence of convection at 0C [W/m-K]."""
208
+ return self.conductivity_at_temperature(273.15)
209
+
210
+ @property
211
+ def viscosity(self):
212
+ """Viscosity of the gas at 0C [kg/m-s]."""
213
+ return self.viscosity_at_temperature(273.15)
214
+
215
+ @property
216
+ def specific_heat(self):
217
+ """Specific heat of the gas at 0C [J/kg-K]."""
218
+ return self.specific_heat_at_temperature(273.15)
219
+
220
+ @property
221
+ def density(self):
222
+ """Density of the gas at 0C and sea-level pressure [J/kg-K]."""
223
+ return self.density_at_temperature(273.15)
224
+
225
+ @property
226
+ def prandtl(self):
227
+ """Prandtl number of the gas at 0C."""
228
+ return self.prandtl_at_temperature(273.15)
229
+
230
+ def conductivity_at_temperature(self, t_kelvin):
231
+ """Get the conductivity of the gas [W/m-K] at a given Kelvin temperature."""
232
+ return self.conductivity_coeff_a + self.conductivity_coeff_b * t_kelvin + \
233
+ self.conductivity_coeff_c * t_kelvin ** 2
234
+
235
+ def viscosity_at_temperature(self, t_kelvin):
236
+ """Get the viscosity of the gas [kg/m-s] at a given Kelvin temperature."""
237
+ return self.viscosity_coeff_a + self.viscosity_coeff_b * t_kelvin + \
238
+ self.viscosity_coeff_c * t_kelvin ** 2
239
+
240
+ def specific_heat_at_temperature(self, t_kelvin):
241
+ """Get the specific heat of the gas [J/kg-K] at a given Kelvin temperature."""
242
+ return self.specific_heat_coeff_a + self.specific_heat_coeff_b * t_kelvin + \
243
+ self.specific_heat_coeff_c * t_kelvin ** 2
244
+
245
+ def density_at_temperature(self, t_kelvin, pressure=101325):
246
+ """Get the density of the gas [kg/m3] at a given temperature and pressure.
247
+
248
+ This method uses the ideal gas law to estimate the density.
249
+
250
+ Args:
251
+ t_kelvin: The average temperature of the gas cavity in Kelvin.
252
+ pressure: The average pressure of the gas cavity in Pa.
253
+ Default is 101325 Pa for standard pressure at sea level.
254
+ """
255
+ return (pressure * self.molecular_weight * 0.001) / (8.314 * t_kelvin)
256
+
257
+ def prandtl_at_temperature(self, t_kelvin):
258
+ """Get the Prandtl number of the gas at a given Kelvin temperature."""
259
+ return self.viscosity_at_temperature(t_kelvin) * \
260
+ self.specific_heat_at_temperature(t_kelvin) / \
261
+ self.conductivity_at_temperature(t_kelvin)
262
+
263
+ @classmethod
264
+ def from_therm_xml(cls, xml_element):
265
+ """Create PureGas from an XML element of a THERM PureGas material.
266
+
267
+ Args:
268
+ xml_element: An XML element of a THERM PureGas material.
269
+ """
270
+ # get the identifier, molecular weight and specific heat ratio
271
+ xml_uuid = xml_element.find('UUID')
272
+ identifier = xml_uuid.text
273
+ if len(identifier) == 31:
274
+ identifier = uuid_from_therm_id(identifier)
275
+ xml_prop = xml_element.find('Properties')
276
+ xml_mw = xml_prop.find('MolecularWeight')
277
+ molecular_weight = xml_mw.text
278
+ xml_shr = xml_prop.find('SpecificHeatRatio')
279
+ specific_heat_ratio = xml_shr.text
280
+ # extract the conductivity curve
281
+ xml_cond = xml_prop.find('Conductivity')
282
+ xml_c_a = xml_cond.find('A')
283
+ conductivity_coeff_a = xml_c_a.text
284
+ xml_c_b = xml_cond.find('B')
285
+ conductivity_coeff_b = xml_c_b.text
286
+ xml_c_c = xml_cond.find('C')
287
+ conductivity_coeff_c = xml_c_c.text
288
+ # extract the viscosity curve
289
+ xml_vis = xml_prop.find('Viscosity')
290
+ xml_v_a = xml_vis.find('A')
291
+ viscosity_coeff_a = xml_v_a.text
292
+ xml_v_b = xml_vis.find('B')
293
+ viscosity_coeff_b = xml_v_b.text
294
+ xml_v_c = xml_vis.find('C')
295
+ viscosity_coeff_c = xml_v_c.text
296
+ # extract the specific heat curve
297
+ xml_sh = xml_prop.find('SpecificHeat')
298
+ xml_sh_a = xml_sh.find('A')
299
+ specific_heat_coeff_a = xml_sh_a.text
300
+ xml_sh_b = xml_sh.find('B')
301
+ specific_heat_coeff_b = xml_sh_b.text
302
+ xml_sh_c = xml_sh.find('C')
303
+ specific_heat_coeff_c = xml_sh_c.text
304
+ # create the PureGas material
305
+ mat = PureGas(
306
+ conductivity_coeff_a, viscosity_coeff_a, specific_heat_coeff_a,
307
+ conductivity_coeff_b, viscosity_coeff_b, specific_heat_coeff_b,
308
+ conductivity_coeff_c, viscosity_coeff_c, specific_heat_coeff_c,
309
+ specific_heat_ratio, molecular_weight, identifier=identifier)
310
+ # assign the name if it is specified
311
+ xml_name = xml_element.find('Name')
312
+ if xml_name is not None:
313
+ mat.display_name = xml_name.text
314
+ xml_protect = xml_element.find('Protected')
315
+ if xml_protect is not None:
316
+ mat.protected = True if xml_protect.text == 'true' else False
317
+ return mat
318
+
319
+ @classmethod
320
+ def from_therm_xml_str(cls, xml_str):
321
+ """Create a PureGas from an XML text string of a THERM PureGas.
322
+
323
+ Args:
324
+ xml_str: An XML text string of a THERM PureGas.
325
+ """
326
+ root = ET.fromstring(xml_str)
327
+ return cls.from_therm_xml(root)
328
+
329
+ @classmethod
330
+ def from_dict(cls, data):
331
+ """Create a PureGas from a dictionary.
332
+
333
+ Args:
334
+ data: A python dictionary in the following format
335
+
336
+ .. code-block:: python
337
+
338
+ {
339
+ "type": 'PureGas',
340
+ "identifier": '7b4a5a47-ebec-4d95-b028-a78485130c34',
341
+ "display_name": 'CO2'
342
+ "conductivity_coeff_a": 0.0146,
343
+ "viscosity_coeff_a": 0.000014,
344
+ "specific_heat_coeff_a": 827.73,
345
+ "specific_heat_ratio": 1.4
346
+ "molecular_weight": 44
347
+ }
348
+ """
349
+ assert data['type'] == 'PureGas', \
350
+ 'Expected PureGas. Got {}.'.format(data['type'])
351
+ con_b = 0 if 'conductivity_coeff_b' not in data else data['conductivity_coeff_b']
352
+ vis_b = 0 if 'viscosity_coeff_b' not in data else data['viscosity_coeff_b']
353
+ sph_b = 0 if 'specific_heat_coeff_b' not in data \
354
+ else data['specific_heat_coeff_b']
355
+ con_c = 0 if 'conductivity_coeff_c' not in data else data['conductivity_coeff_c']
356
+ vis_c = 0 if 'viscosity_coeff_c' not in data else data['viscosity_coeff_c']
357
+ sph_c = 0 if 'specific_heat_coeff_c' not in data \
358
+ else data['specific_heat_coeff_c']
359
+ sphr = 1.0 if 'specific_heat_ratio' not in data else data['specific_heat_ratio']
360
+ mw = 20.0 if 'molecular_weight' not in data else data['molecular_weight']
361
+ new_obj = cls(
362
+ data['conductivity_coeff_a'], data['viscosity_coeff_a'],
363
+ data['specific_heat_coeff_a'],
364
+ con_b, vis_b, sph_b, con_c, vis_c, sph_c, sphr, mw,
365
+ identifier=data['identifier'])
366
+ if 'display_name' in data and data['display_name'] is not None:
367
+ new_obj.display_name = data['display_name']
368
+ if 'protected' in data and data['protected'] is not None:
369
+ new_obj.protected = data['protected']
370
+ if 'user_data' in data and data['user_data'] is not None:
371
+ new_obj.user_data = data['user_data']
372
+ return new_obj
373
+
374
+ def to_therm_xml(self, gases_element=None):
375
+ """Get an THERM XML element of the gas.
376
+
377
+ Args:
378
+ gases_element: An optional XML Element for the Gases to which the
379
+ generated objects will be added. If None, a new XML Element
380
+ will be generated.
381
+
382
+ .. code-block:: xml
383
+
384
+ <PureGas>
385
+ <UUID>8d33196f-f052-46e6-8353-bccb9a779f9c</UUID>
386
+ <Name>Air</Name>
387
+ <Protected>true</Protected>
388
+ <Properties>
389
+ <MolecularWeight>28.97</MolecularWeight>
390
+ <SpecificHeatRatio>1.4</SpecificHeatRatio>
391
+ <Conductivity>
392
+ <A>0.002873</A>
393
+ <B>7.76e-05</B>
394
+ <C>0</C>
395
+ </Conductivity>
396
+ <Viscosity>
397
+ <A>3.723e-06</A>
398
+ <B>4.94e-08</B>
399
+ <C>0</C>
400
+ </Viscosity>
401
+ <SpecificHeat>
402
+ <A>1002.737</A>
403
+ <B>0.012324</B>
404
+ <C>0</C>
405
+ </SpecificHeat>
406
+ </Properties>
407
+ </PureGas>
408
+ """
409
+ # create a new Materials element if one is not specified
410
+ if gases_element is not None:
411
+ xml_mat = ET.SubElement(gases_element, 'PureGas')
412
+ else:
413
+ xml_mat = ET.Element('PureGas')
414
+ # add all of the required basic attributes
415
+ xml_id = ET.SubElement(xml_mat, 'UUID')
416
+ xml_id.text = self.identifier
417
+ xml_name = ET.SubElement(xml_mat, 'Name')
418
+ xml_name.text = self.display_name
419
+ xml_protect = ET.SubElement(xml_mat, 'Protected')
420
+ xml_protect.text = 'true' if self.protected else 'false'
421
+ xml_prop = ET.SubElement(xml_mat, 'Properties')
422
+ # molecular weight and specific heat ratio
423
+ xml_mw = ET.SubElement(xml_prop, 'MolecularWeight')
424
+ xml_mw.text = str(self.molecular_weight)
425
+ xml_shr = ET.SubElement(xml_prop, 'SpecificHeatRatio')
426
+ xml_shr.text = str(self.specific_heat_ratio)
427
+ # add the conductivity curve
428
+ xml_cond = ET.SubElement(xml_prop, 'Conductivity')
429
+ xml_cond_a = ET.SubElement(xml_cond, 'A')
430
+ xml_cond_a.text = str(self.conductivity_coeff_a)
431
+ xml_cond_b = ET.SubElement(xml_cond, 'B')
432
+ xml_cond_b.text = str(self.conductivity_coeff_b)
433
+ xml_cond_c = ET.SubElement(xml_cond, 'C')
434
+ xml_cond_c.text = str(self.conductivity_coeff_c)
435
+ # add the viscosity curve
436
+ xml_vis = ET.SubElement(xml_prop, 'Viscosity')
437
+ xml_vis_a = ET.SubElement(xml_vis, 'A')
438
+ xml_vis_a.text = str(self.viscosity_coeff_a)
439
+ xml_vis_b = ET.SubElement(xml_vis, 'B')
440
+ xml_vis_b.text = str(self.viscosity_coeff_b)
441
+ xml_vis_c = ET.SubElement(xml_vis, 'C')
442
+ xml_vis_c.text = str(self.viscosity_coeff_c)
443
+ # add the specific heat curve
444
+ xml_sh = ET.SubElement(xml_prop, 'SpecificHeat')
445
+ xml_sh_a = ET.SubElement(xml_sh, 'A')
446
+ xml_sh_a.text = str(self.specific_heat_coeff_a)
447
+ xml_sh_b = ET.SubElement(xml_sh, 'B')
448
+ xml_sh_b.text = str(self.specific_heat_coeff_b)
449
+ xml_sh_c = ET.SubElement(xml_sh, 'C')
450
+ xml_sh_c.text = str(self.specific_heat_coeff_c)
451
+ return xml_mat
452
+
453
+ def to_therm_xml_str(self):
454
+ """Get an THERM XML string of the gas."""
455
+ xml_root = self.to_therm_xml()
456
+ try: # try to indent the XML to make it read-able
457
+ ET.indent(xml_root)
458
+ return ET.tostring(xml_root, encoding='unicode')
459
+ except AttributeError: # we are in Python 2 and no indent is available
460
+ return ET.tostring(xml_root)
461
+
462
+ def to_dict(self):
463
+ """PureGas dictionary representation."""
464
+ base = {
465
+ 'type': 'PureGas',
466
+ 'identifier': self.identifier,
467
+ 'conductivity_coeff_a': self.conductivity_coeff_a,
468
+ 'viscosity_coeff_a': self.viscosity_coeff_a,
469
+ 'specific_heat_coeff_a': self.specific_heat_coeff_a,
470
+ 'conductivity_coeff_b': self.conductivity_coeff_b,
471
+ 'viscosity_coeff_b': self.viscosity_coeff_b,
472
+ 'specific_heat_coeff_b': self.specific_heat_coeff_b,
473
+ 'conductivity_coeff_c': self.conductivity_coeff_c,
474
+ 'viscosity_coeff_c': self.viscosity_coeff_c,
475
+ 'specific_heat_coeff_c': self.specific_heat_coeff_c,
476
+ 'specific_heat_ratio': self.specific_heat_ratio,
477
+ 'molecular_weight': self.molecular_weight
478
+ }
479
+ if self._display_name is not None:
480
+ base['display_name'] = self.display_name
481
+ base['protected'] = self._protected
482
+ if self._user_data is not None:
483
+ base['user_data'] = self.user_data
484
+ return base
485
+
486
+ def __key(self):
487
+ """A tuple based on the object properties, useful for hashing."""
488
+ return (self.identifier, self.conductivity_coeff_a,
489
+ self.viscosity_coeff_a, self.specific_heat_coeff_a,
490
+ self.conductivity_coeff_b, self.viscosity_coeff_b,
491
+ self.specific_heat_coeff_b, self.conductivity_coeff_c,
492
+ self.viscosity_coeff_c, self.specific_heat_coeff_c,
493
+ self.specific_heat_ratio, self.molecular_weight)
494
+
495
+ def __hash__(self):
496
+ return hash(self.__key())
497
+
498
+ def __eq__(self, other):
499
+ return isinstance(other, PureGas) and \
500
+ self.__key() == other.__key()
501
+
502
+ def __ne__(self, other):
503
+ return not self.__eq__(other)
504
+
505
+ def __repr__(self):
506
+ return 'THERM Pure Gas: {}'.format(self.display_name)
507
+
508
+ def __copy__(self):
509
+ new_obj = PureGas(
510
+ self.conductivity_coeff_a, self.viscosity_coeff_a, self.specific_heat_coeff_a,
511
+ self.conductivity_coeff_b, self.viscosity_coeff_b, self.specific_heat_coeff_b,
512
+ self.conductivity_coeff_c, self.viscosity_coeff_c, self.specific_heat_coeff_c,
513
+ self.specific_heat_ratio, self.molecular_weight, identifier=self.identifier)
514
+ new_obj._display_name = self._display_name
515
+ new_obj._protected = self._protected
516
+ new_obj._user_data = None if self._user_data is None else self._user_data.copy()
517
+ return new_obj
518
+
519
+
520
+ @lockable
521
+ class Gas(_ResourceObjectBase):
522
+ """Gas gap material defined by a mixture of gases.
523
+
524
+ Args:
525
+ pure_gases: A list of PureGas objects describing the types of gas in the gap.
526
+ gas_fractions: A list of fractional numbers describing the volumetric
527
+ fractions of gas types in the mixture. This list must align with
528
+ the pure_gases input list and must sum to 1.
529
+ identifier: Text string for a unique object ID. Must be a UUID in the
530
+ format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. If None, a UUID will
531
+ automatically be generated. (Default: None).
532
+
533
+ Properties:
534
+ * identifier
535
+ * display_name
536
+ * pure_gases
537
+ * gas_fractions
538
+ * gas_count
539
+ * protected
540
+ * user_data
541
+ """
542
+ __slots__ = ('_gas_count', '_pure_gases', '_gas_fractions')
543
+
544
+ def __init__(self, pure_gases, gas_fractions, identifier=None):
545
+ """Initialize gas mixture material."""
546
+ _ResourceObjectBase.__init__(self, identifier)
547
+ try: # check the number of gases
548
+ self._gas_count = len(pure_gases)
549
+ except (TypeError, ValueError):
550
+ raise TypeError(
551
+ 'Expected list for pure_gases. Got {}.'.format(type(pure_gases)))
552
+ assert 1 <= self._gas_count, 'Number of gases in gas mixture must be ' \
553
+ 'greater than 1. Got {}.'.format(self._gas_count)
554
+ self.pure_gases = pure_gases
555
+ self.gas_fractions = gas_fractions
556
+
557
+ @property
558
+ def pure_gases(self):
559
+ """Get or set a tuple of text describing the gases in the gas gap layer."""
560
+ return self._pure_gases
561
+
562
+ @pure_gases.setter
563
+ def pure_gases(self, value):
564
+ try:
565
+ if not isinstance(value, tuple):
566
+ value = tuple(value)
567
+ except TypeError:
568
+ raise TypeError('Expected list or tuple for pure_gases. '
569
+ 'Got {}'.format(type(value)))
570
+ for g in value:
571
+ assert isinstance(g, PureGas), 'Expected PureGas' \
572
+ ' material for Gas. Got {}.'.format(type(g))
573
+ assert len(value) > 0, 'Gas must possess at least one pure gas.'
574
+ self._pure_gases = value
575
+
576
+ @property
577
+ def gas_fractions(self):
578
+ """Get or set a tuple of numbers the fractions of gases in the gas gap layer."""
579
+ return self._gas_fractions
580
+
581
+ @gas_fractions.setter
582
+ def gas_fractions(self, g_fracs):
583
+ self._gas_fractions = tuple_with_length(
584
+ g_fracs, self._gas_count, float, 'gas mixture gas_fractions')
585
+ assert sum(self._gas_fractions) == 1, 'Gas fractions must sum to 1. ' \
586
+ 'Got {}.'.format(sum(self._gas_fractions))
587
+
588
+ @property
589
+ def molecular_weight(self):
590
+ """Get the gas molecular weight."""
591
+ return sum(tuple(gas.molecular_weight * frac for gas, frac
592
+ in zip(self._pure_gases, self._gas_fractions)))
593
+
594
+ @property
595
+ def gas_count(self):
596
+ """An integer indicating the number of gases in the mixture."""
597
+ return self._gas_count
598
+
599
+ @property
600
+ def conductivity(self):
601
+ """Conductivity of the gas in the absence of convection at 0C [W/m-K]."""
602
+ return self.conductivity_at_temperature(273.15)
603
+
604
+ @property
605
+ def viscosity(self):
606
+ """Viscosity of the gas at 0C [kg/m-s]."""
607
+ return self.viscosity_at_temperature(273.15)
608
+
609
+ @property
610
+ def specific_heat(self):
611
+ """Specific heat of the gas at 0C [J/kg-K]."""
612
+ return self.specific_heat_at_temperature(273.15)
613
+
614
+ @property
615
+ def density(self):
616
+ """Density of the gas at 0C and sea-level pressure [J/kg-K]."""
617
+ return self.density_at_temperature(273.15)
618
+
619
+ @property
620
+ def prandtl(self):
621
+ """Prandtl number of the gas at 0C."""
622
+ return self.prandtl_at_temperature(273.15)
623
+
624
+ def conductivity_at_temperature(self, t_kelvin):
625
+ """Get the conductivity of the gas [W/m-K] at a given Kelvin temperature."""
626
+ return self._weighted_avg_coeff_property('conductivity_coeff', t_kelvin)
627
+
628
+ def viscosity_at_temperature(self, t_kelvin):
629
+ """Get the viscosity of the gas [kg/m-s] at a given Kelvin temperature."""
630
+ return self._weighted_avg_coeff_property('viscosity_coeff', t_kelvin)
631
+
632
+ def specific_heat_at_temperature(self, t_kelvin):
633
+ """Get the specific heat of the gas [J/kg-K] at a given Kelvin temperature."""
634
+ return self._weighted_avg_coeff_property('specific_heat_coeff', t_kelvin)
635
+
636
+ def density_at_temperature(self, t_kelvin, pressure=101325):
637
+ """Get the density of the gas [kg/m3] at a given temperature and pressure.
638
+
639
+ This method uses the ideal gas law to estimate the density.
640
+
641
+ Args:
642
+ t_kelvin: The average temperature of the gas cavity in Kelvin.
643
+ pressure: The average pressure of the gas cavity in Pa.
644
+ Default is 101325 Pa for standard pressure at sea level.
645
+ """
646
+ return (pressure * self.molecular_weight * 0.001) / (8.314 * t_kelvin)
647
+
648
+ def prandtl_at_temperature(self, t_kelvin):
649
+ """Get the Prandtl number of the gas at a given Kelvin temperature."""
650
+ return self.viscosity_at_temperature(t_kelvin) * \
651
+ self.specific_heat_at_temperature(t_kelvin) / \
652
+ self.conductivity_at_temperature(t_kelvin)
653
+
654
+ @classmethod
655
+ def from_therm_xml(cls, xml_element, pure_gases):
656
+ """Create Gas from an XML element of a THERM Gas material.
657
+
658
+ Args:
659
+ xml_element: An XML element of a THERM Gas material.
660
+ pure_gases: A dictionary with pure gas names as keys and PureGas
661
+ object instances as values. These will be used to reassign
662
+ the pure gases that make up this gas.
663
+ """
664
+ # get the identifier, gases and initialize the object
665
+ xml_uuid = xml_element.find('UUID')
666
+ identifier = xml_uuid.text
667
+ if len(identifier) == 31:
668
+ identifier = uuid_from_therm_id(identifier)
669
+ xml_comps = xml_element.find('Components')
670
+ pure_gas_objs, gas_fractions = [], []
671
+ for xml_comp in xml_comps:
672
+ xml_gas_name = xml_comp.find('PureGas')
673
+ try:
674
+ pure_gas_objs.append(pure_gases[xml_gas_name.text])
675
+ except KeyError as e:
676
+ raise ValueError('Failed to find {} in pure gases.'.format(e))
677
+ xml_gas_fract = xml_comp.find('Fraction')
678
+ gas_fractions.append(xml_gas_fract.text)
679
+ mat = Gas(pure_gas_objs, gas_fractions, identifier=identifier)
680
+ # assign the name if it is specified
681
+ xml_name = xml_element.find('Name')
682
+ if xml_name is not None:
683
+ mat.display_name = xml_name.text
684
+ xml_protect = xml_element.find('Protected')
685
+ if xml_protect is not None:
686
+ mat.protected = True if xml_protect.text == 'true' else False
687
+ return mat
688
+
689
+ @classmethod
690
+ def from_therm_xml_str(cls, xml_str, pure_gases):
691
+ """Create a Gas from an XML text string of a THERM Gas.
692
+
693
+ Args:
694
+ xml_str: An XML text string of a THERM Gas.
695
+ pure_gases: A dictionary with pure gas names as keys and PureGas
696
+ object instances as values. These will be used to reassign
697
+ the pure gases that make up this gas gap.
698
+ """
699
+ root = ET.fromstring(xml_str)
700
+ return cls.from_therm_xml(root, pure_gases)
701
+
702
+ @classmethod
703
+ def from_dict(cls, data):
704
+ """Create a Gas from a dictionary.
705
+
706
+ Args:
707
+ data: A python dictionary in the following format
708
+
709
+ .. code-block:: python
710
+
711
+ {
712
+ 'type': 'Gas',
713
+ 'identifier': 'e34f4b07-a012-4142-ae29-d7967c921c71',
714
+ 'display_name': 'Argon Mixture',
715
+ 'pure_gases': [{}, {}], # list of PureGas objects
716
+ 'gas_fractions': [0.95, 0.05]
717
+ }
718
+ """
719
+ assert data['type'] == 'Gas', 'Expected Gas. Got {}.'.format(data['type'])
720
+ pure_gases = [PureGas.from_dict(pg) for pg in data['pure_gases']]
721
+ new_obj = cls(pure_gases, data['gas_fractions'], data['identifier'])
722
+ cls._assign_optional_from_dict(new_obj, data)
723
+ return new_obj
724
+
725
+ @classmethod
726
+ def from_dict_abridged(cls, data, pure_gases):
727
+ """Create a Gas from an abridged dictionary.
728
+
729
+ Args:
730
+ data: An GasAbridged dictionary.
731
+ pure_gases: A dictionary with pure gas identifiers as keys and PureGas
732
+ object instances as values. These will be used to reassign
733
+ the pure gases that make up this gas gap.
734
+
735
+ .. code-block:: python
736
+
737
+ {
738
+ 'type': 'GasAbridged',
739
+ 'identifier': 'e34f4b07-a012-4142-ae29-d7967c921c71',
740
+ 'display_name': 'Argon Mixture',
741
+ 'pure_gases': [
742
+ 'ca280a4b-aba9-416f-9443-484285d52227',
743
+ 'ba65b928-f766-4044-bc17-e53c42040bde'
744
+ ],
745
+ 'gas_fractions': [0.95, 0.05]
746
+ }
747
+ """
748
+ assert data['type'] == 'GasAbridged', \
749
+ 'Expected GasAbridged. Got {}.'.format(data['type'])
750
+ try:
751
+ pure_gas_objs = [pure_gases[mat_id] for mat_id in data['pure_gases']]
752
+ except KeyError as e:
753
+ raise ValueError('Failed to find {} in pure gases.'.format(e))
754
+ new_obj = cls(pure_gas_objs, data['gas_fractions'], data['identifier'])
755
+ cls._assign_optional_from_dict(new_obj, data)
756
+ return new_obj
757
+
758
+ @staticmethod
759
+ def _assign_optional_from_dict(new_obj, data):
760
+ """Assign optional attributes when serializing from dict."""
761
+ if 'display_name' in data and data['display_name'] is not None:
762
+ new_obj.display_name = data['display_name']
763
+ if 'protected' in data and data['protected'] is not None:
764
+ new_obj.protected = data['protected']
765
+ if 'user_data' in data and data['user_data'] is not None:
766
+ new_obj.user_data = data['user_data']
767
+
768
+ def to_therm_xml(self, gases_element=None):
769
+ """Get an THERM XML element of the gas.
770
+
771
+ Note that this method only outputs a single element for the gas and,
772
+ to write the full gas into an XML, the gas's pure gases
773
+ must also be written.
774
+
775
+ Args:
776
+ gases_element: An optional XML Element for the Gases to which the
777
+ generated objects will be added. If None, a new XML Element
778
+ will be generated.
779
+
780
+ .. code-block:: xml
781
+
782
+ <Gas>
783
+ <UUID>6c2409e9-5296-46c1-be11-9029b59a549b</UUID>
784
+ <Name>Air</Name>
785
+ <Protected>true</Protected>
786
+ <Components>
787
+ <Component>
788
+ <Fraction>1</Fraction>
789
+ <PureGas>Air</PureGas>
790
+ </Component>
791
+ </Components>
792
+ </Gas>
793
+ """
794
+ # create a new Materials element if one is not specified
795
+ if gases_element is not None:
796
+ xml_mat = ET.SubElement(gases_element, 'Gas')
797
+ else:
798
+ xml_mat = ET.Element('Gas')
799
+ # add all of the required basic attributes
800
+ xml_id = ET.SubElement(xml_mat, 'UUID')
801
+ xml_id.text = self.identifier
802
+ xml_name = ET.SubElement(xml_mat, 'Name')
803
+ xml_name.text = self.display_name
804
+ xml_protect = ET.SubElement(xml_mat, 'Protected')
805
+ xml_protect.text = 'true' if self.protected else 'false'
806
+ # add the gas components
807
+ xml_comps = ET.SubElement(xml_mat, 'Components')
808
+ for pure_gas, gas_fract in zip(self.pure_gases, self.gas_fractions):
809
+ xml_comp = ET.SubElement(xml_comps, 'Component')
810
+ xml_fact = ET.SubElement(xml_comp, 'Fraction')
811
+ xml_fact.text = str(gas_fract)
812
+ xml_gas = ET.SubElement(xml_comp, 'PureGas')
813
+ xml_gas.text = pure_gas.display_name
814
+ return xml_mat
815
+
816
+ def to_therm_xml_str(self):
817
+ """Get an THERM XML string of the gas."""
818
+ xml_root = self.to_therm_xml()
819
+ try: # try to indent the XML to make it read-able
820
+ ET.indent(xml_root)
821
+ return ET.tostring(xml_root, encoding='unicode')
822
+ except AttributeError: # we are in Python 2 and no indent is available
823
+ return ET.tostring(xml_root)
824
+
825
+ def to_dict(self, abridged=False):
826
+ """Gas dictionary representation."""
827
+ base = {'type': 'Gas'} if not abridged else {'type': 'GasAbridged'}
828
+ base['identifier'] = self.identifier
829
+ base['pure_gases'] = \
830
+ [m.identifier for m in self.pure_gases] if abridged else \
831
+ [m.to_dict() for m in self.pure_gases]
832
+ base['gas_fractions'] = self.gas_fractions
833
+ if self._display_name is not None:
834
+ base['display_name'] = self.display_name
835
+ base['protected'] = self._protected
836
+ if self._user_data is not None:
837
+ base['user_data'] = self.user_data
838
+ return base
839
+
840
+ def lock(self):
841
+ """The lock() method will also lock the pure gases."""
842
+ self._locked = True
843
+ for gas in self.pure_gases:
844
+ gas.lock()
845
+
846
+ def unlock(self):
847
+ """The unlock() method will also unlock the pure gases."""
848
+ self._locked = False
849
+ for gas in self.pure_gases:
850
+ gas.unlock()
851
+
852
+ @staticmethod
853
+ def extract_all_from_xml_file(xml_file):
854
+ """Extract all Gas objects from a THERM XML file.
855
+
856
+ Args:
857
+ xml_file: A path to an XML file containing objects Gas objects and
858
+ corresponding PureGas components.
859
+
860
+ Returns:
861
+ A tuple with two elements
862
+
863
+ - gases: A list of all Gas objects in the XML file as fairyfly_therm
864
+ Gas objects.
865
+
866
+ - pure_gases: A list of all PureGas objects in the XML file as
867
+ fairyfly_therm PureGas objects.
868
+ """
869
+ # read the file and get the root
870
+ tree = ET.parse(xml_file)
871
+ root = tree.getroot()
872
+ # extract all of the PureGas objects
873
+ pure_dict = {}
874
+ for gas_obj in root:
875
+ if gas_obj.tag == 'PureGas':
876
+ try:
877
+ xml_name = gas_obj.find('Name')
878
+ pure_dict[xml_name.text] = PureGas.from_therm_xml(gas_obj)
879
+ except Exception: # not a valid pure gas material
880
+ pass
881
+ # extract all of the gas objects
882
+ gases = []
883
+ for gas_obj in root:
884
+ if gas_obj.tag == 'Gas':
885
+ try:
886
+ gases.append(Gas.from_therm_xml(gas_obj, pure_dict))
887
+ except Exception: # not a valid gas material
888
+ pass
889
+ return gases, list(pure_dict.values())
890
+
891
+ def _weighted_avg_coeff_property(self, attr, t_kelvin):
892
+ """Get a weighted average property given a dictionary of coefficients."""
893
+ property = []
894
+ for gas in self._pure_gases:
895
+ property.append(
896
+ getattr(gas, attr + '_a') +
897
+ getattr(gas, attr + '_b') * t_kelvin +
898
+ getattr(gas, attr + '_c') * t_kelvin ** 2)
899
+ return sum(tuple(pr * frac for pr, frac in zip(property, self._gas_fractions)))
900
+
901
+ def __key(self):
902
+ """A tuple based on the object properties, useful for hashing."""
903
+ return (self.identifier, self.gas_fractions) + \
904
+ tuple(hash(g) for g in self.pure_gases)
905
+
906
+ def __hash__(self):
907
+ return hash(self.__key())
908
+
909
+ def __eq__(self, other):
910
+ return isinstance(other, Gas) and self.__key() == other.__key()
911
+
912
+ def __ne__(self, other):
913
+ return not self.__eq__(other)
914
+
915
+ def __repr__(self):
916
+ return 'THERM Gas: {}'.format(self.display_name)
917
+
918
+ def __copy__(self):
919
+ new_obj = Gas(
920
+ [g.duplicate() for g in self.pure_gases],
921
+ self.gas_fractions, self.identifier)
922
+ new_obj._display_name = self._display_name
923
+ new_obj._protected = self._protected
924
+ new_obj._user_data = None if self._user_data is None else self._user_data.copy()
925
+ return new_obj