exerpy 0.0.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.
Files changed (44) hide show
  1. exerpy/__init__.py +12 -0
  2. exerpy/analyses.py +1711 -0
  3. exerpy/components/__init__.py +16 -0
  4. exerpy/components/combustion/__init__.py +0 -0
  5. exerpy/components/combustion/base.py +248 -0
  6. exerpy/components/component.py +126 -0
  7. exerpy/components/heat_exchanger/__init__.py +0 -0
  8. exerpy/components/heat_exchanger/base.py +449 -0
  9. exerpy/components/heat_exchanger/condenser.py +323 -0
  10. exerpy/components/heat_exchanger/simple.py +358 -0
  11. exerpy/components/heat_exchanger/steam_generator.py +264 -0
  12. exerpy/components/helpers/__init__.py +0 -0
  13. exerpy/components/helpers/cycle_closer.py +104 -0
  14. exerpy/components/nodes/__init__.py +0 -0
  15. exerpy/components/nodes/deaerator.py +318 -0
  16. exerpy/components/nodes/drum.py +164 -0
  17. exerpy/components/nodes/flash_tank.py +89 -0
  18. exerpy/components/nodes/mixer.py +332 -0
  19. exerpy/components/piping/__init__.py +0 -0
  20. exerpy/components/piping/valve.py +394 -0
  21. exerpy/components/power_machines/__init__.py +0 -0
  22. exerpy/components/power_machines/generator.py +168 -0
  23. exerpy/components/power_machines/motor.py +173 -0
  24. exerpy/components/turbomachinery/__init__.py +0 -0
  25. exerpy/components/turbomachinery/compressor.py +318 -0
  26. exerpy/components/turbomachinery/pump.py +310 -0
  27. exerpy/components/turbomachinery/turbine.py +351 -0
  28. exerpy/data/Ahrendts.json +90 -0
  29. exerpy/functions.py +637 -0
  30. exerpy/parser/__init__.py +0 -0
  31. exerpy/parser/from_aspen/__init__.py +0 -0
  32. exerpy/parser/from_aspen/aspen_config.py +61 -0
  33. exerpy/parser/from_aspen/aspen_parser.py +721 -0
  34. exerpy/parser/from_ebsilon/__init__.py +38 -0
  35. exerpy/parser/from_ebsilon/check_ebs_path.py +74 -0
  36. exerpy/parser/from_ebsilon/ebsilon_config.py +1055 -0
  37. exerpy/parser/from_ebsilon/ebsilon_functions.py +181 -0
  38. exerpy/parser/from_ebsilon/ebsilon_parser.py +660 -0
  39. exerpy/parser/from_ebsilon/utils.py +79 -0
  40. exerpy/parser/from_tespy/tespy_config.py +23 -0
  41. exerpy-0.0.1.dist-info/METADATA +158 -0
  42. exerpy-0.0.1.dist-info/RECORD +44 -0
  43. exerpy-0.0.1.dist-info/WHEEL +4 -0
  44. exerpy-0.0.1.dist-info/licenses/LICENSE +21 -0
exerpy/functions.py ADDED
@@ -0,0 +1,637 @@
1
+ import json
2
+ import logging
3
+ import math
4
+ import os
5
+ import sys
6
+
7
+ import CoolProp.CoolProp as CP
8
+
9
+ from exerpy import __datapath__
10
+
11
+
12
+ def mass_to_molar_fractions(mass_fractions):
13
+ """
14
+ Convert mass fractions to molar fractions.
15
+
16
+ Parameters:
17
+ - mass_fractions: Dictionary with component names as keys and mass fractions as values.
18
+
19
+ Returns:
20
+ - molar_fractions: Dictionary with component names as keys and molar fractions as values.
21
+ """
22
+ molar_masses = {}
23
+ molar_fractions = {}
24
+
25
+ # Step 1: Get the molar masses for each component
26
+ for fraction in mass_fractions.keys():
27
+ try:
28
+ molar_masses[fraction] = CP.PropsSI('M', fraction)
29
+ except Exception as e:
30
+ # print(f"Warning: Could not retrieve molar mass for {fraction} ({fraction}). Error: {e}")
31
+ continue # Skip this fraction if there's an issue
32
+
33
+ # Step 2: Check if we have valid molar masses
34
+ if not molar_masses:
35
+ raise ValueError("No valid molar masses were retrieved. Exiting...")
36
+
37
+ # Step 3: Calculate total moles in the mixture
38
+ total_moles = sum(mass_fractions[comp] / molar_masses[comp] for comp in molar_masses)
39
+
40
+ # Step 4: Calculate molar fractions
41
+ for component in molar_masses.keys():
42
+ molar_fractions[component] = (mass_fractions[component] / molar_masses[component]) / total_moles
43
+
44
+ # Step 5: Check if molar fractions sum to approximately 1
45
+ molar_sum = sum(molar_fractions.values())
46
+ if abs(molar_sum - 1.0) > 1e-6:
47
+ raise ValueError(f"Error: Molar fractions do not sum to 1. Sum is {molar_sum}")
48
+
49
+ return molar_fractions
50
+
51
+
52
+ def molar_to_mass_fractions(molar_fractions):
53
+ """
54
+ Convert molar fractions to mass fractions.
55
+
56
+ Parameters:
57
+ - molar_fractions: Dictionary with component names as keys and molar fractions as values.
58
+
59
+ Returns:
60
+ - mass_fractions: Dictionary with component names as keys and mass fractions as values.
61
+ """
62
+ molar_masses = {}
63
+ mass_fractions = {}
64
+
65
+ # Step 1: Get the molar masses for each component
66
+ for fraction in molar_fractions.keys():
67
+ try:
68
+ molar_masses[fraction] = CP.PropsSI('M', fraction)
69
+ except Exception as e:
70
+ # print(f"Warning: Could not retrieve molar mass for {fraction} ({fraction}). Error: {e}")
71
+ continue # Skip this fraction if there's an issue
72
+
73
+ # Step 2: Check if we have valid molar masses
74
+ if not molar_masses:
75
+ raise ValueError("No valid molar masses were retrieved. Exiting...")
76
+
77
+ # Step 3: Calculate total mass in the mixture
78
+ total_mass = sum(molar_fractions[comp] * molar_masses[comp] for comp in molar_masses)
79
+
80
+ # Step 4: Calculate mass fractions
81
+ for component in molar_masses.keys():
82
+ mass_fractions[component] = (molar_fractions[component] * molar_masses[component]) / total_mass
83
+
84
+ # Step 5: Check if mass fractions sum to approximately 1
85
+ mass_sum = sum(mass_fractions.values())
86
+ if abs(mass_sum - 1.0) > 1e-6:
87
+ raise ValueError(f"Error: Mass fractions do not sum to 1. Sum is {mass_sum}")
88
+
89
+ return mass_fractions
90
+
91
+
92
+ def calc_chemical_exergy(stream_data, Tamb, pamb, chemExLib):
93
+ """
94
+ Calculate the chemical exergy of a stream based on the molar fractions and chemical exergy data. There are three cases:
95
+ - Case A: Handle pure substance.
96
+ - Case B: If water condenses, handle the liquid and gas phases separately.
97
+ - Case C: If water doesn't condense or if water is not present, handle the mixture using the standard approach (ideal mixture).
98
+
99
+ Parameters:
100
+ - stream_data: Dictionary containing 'mass_composition' of the stream.
101
+ - Tamb: Ambient temperature in Celsius.
102
+ - pamb: Ambient pressure in bar.
103
+
104
+ Returns:
105
+ - eCH: Chemical exergy in kJ/kg.
106
+ """
107
+ logging.info(f"Starting chemical exergy calculation with Tamb={Tamb}, pamb={pamb}")
108
+
109
+ try:
110
+ # Check if molar fractions already exist
111
+ if 'molar_composition' in stream_data:
112
+ molar_fractions = stream_data['molar_composition']
113
+ logging.info("Molar fractions found in stream.")
114
+ else:
115
+ # If not, convert mass composition to molar fractions
116
+ molar_fractions = mass_to_molar_fractions(stream_data['mass_composition'])
117
+ logging.info(f"Converted mass composition to molar fractions: {molar_fractions}")
118
+
119
+ try:
120
+ # Load chemical exergy data
121
+ chem_ex_file = os.path.join(__datapath__, f'{chemExLib}.json')
122
+ with open(chem_ex_file, 'r') as file:
123
+ chem_ex_data = json.load(file) # data in J/kmol
124
+ logging.info("Chemical exergy data loaded successfully.")
125
+ except FileNotFoundError:
126
+ error_msg = f"Chemical exergy data file '{chemExLib}.json' not found. Please ensure the file exists or set chemExLib to 'Ahrendts'."
127
+ logging.error(error_msg)
128
+ raise FileNotFoundError(error_msg)
129
+
130
+
131
+ R = 8.314 # Universal gas constant in J/(molK)
132
+ aliases_water = CP.get_aliases('H2O')
133
+
134
+ # Handle pure substance (Case A)
135
+ if len(molar_fractions) == 1:
136
+ logging.info("Handling pure substance case (Case A).")
137
+ substance = next(iter(molar_fractions)) # Get the single key
138
+ aliases = CP.get_aliases(substance)
139
+
140
+ if set(aliases) & set(aliases_water):
141
+ eCH = chem_ex_data['WATER'][2] / CP.PropsSI('M', 'H2O') # liquid water, in J/kg
142
+ logging.info(f"Pure water detected. Chemical exergy: {eCH} J/kg")
143
+ else:
144
+ for alias in aliases:
145
+ if alias.upper() in chem_ex_data:
146
+ eCH = chem_ex_data[alias.upper()][3] / CP.PropsSI('M', substance) # in J/kg
147
+ logging.info(f"Found exergy data for {substance}. Chemical exergy: {eCH} J/kg")
148
+ break
149
+ else:
150
+ logging.error(f"No matching alias found for {substance}")
151
+ raise KeyError(f"No matching alias found for {substance}")
152
+
153
+ # Handle mixtures (Case B or C)
154
+ else:
155
+ logging.info("Handling mixture case (Case B or C).")
156
+ total_molar_mass = 0 # To compute the molar mass of the mixture
157
+ eCH_gas_mol = 0 # Molar chemical exergy of the gas phase if condensation
158
+ eCH_liquid_mol = 0 # Molar chemical exergy of the liquid phase if condensation
159
+ molar_fractions_gas = {} # Molar fractions within the gas phase if condensation
160
+ entropy_mixing = 0 # Entropy of mixing of ideal mixtures
161
+
162
+ # Calculate the total molar mass of the mixture
163
+ for substance, fraction in molar_fractions.items():
164
+ molar_mass = CP.PropsSI('M', substance) # Molar mass in kg/mol
165
+ total_molar_mass += fraction * molar_mass # Weighted sum for molar mass in kg/mol
166
+ logging.info(f"Total molar mass of the mixture: {total_molar_mass} kg/mol")
167
+
168
+ water_present = any(alias in molar_fractions.keys() for alias in aliases_water)
169
+
170
+ if water_present:
171
+ water_alias = next(alias for alias in aliases_water if alias in molar_fractions.keys())
172
+ pH2O_sat = CP.PropsSI('P', 'T', Tamb, 'Q', 1, 'Water') # Saturation pressure of water in bar
173
+ pH2O = molar_fractions[water_alias] * pamb # Partial pressure of water
174
+
175
+ if pH2O > pH2O_sat: # Case B: Water condenses
176
+ logging.info(f"Condensation occurs in the mixture.")
177
+ x_dry = sum(fraction for comp, fraction in molar_fractions.items() if comp != water_alias)
178
+ x_H2O_gas = x_dry / (pamb/pH2O_sat - 1) # Vaporous water fraction in the total mixture
179
+ x_H2O_liquid = molar_fractions[water_alias] - x_H2O_gas # Liquid water fraction
180
+ x_total_gas = 1 - x_H2O_liquid # Total gas phase fraction
181
+
182
+ eCH_liquid_mol = x_H2O_liquid * (chem_ex_data['WATER'][2]) # Liquid phase contribution, in J/mol
183
+
184
+ for substance, fraction in molar_fractions.items():
185
+ if substance == water_alias:
186
+ molar_fractions_gas[substance] = x_H2O_gas / x_total_gas
187
+ else:
188
+ molar_fractions_gas[substance] = molar_fractions[substance] / x_total_gas
189
+
190
+ for substance, fraction in molar_fractions_gas.items():
191
+ aliases = CP.get_aliases(substance)
192
+ for alias in aliases:
193
+ if alias.upper() in chem_ex_data:
194
+ eCH_gas_mol += fraction * (chem_ex_data[alias.upper()][3]) # Exergy is in J/mol
195
+ break
196
+ else:
197
+ logging.error(f"No matching alias found for {substance}")
198
+ raise KeyError(f"No matching alias found for {substance}")
199
+
200
+ if fraction > 0: # Avoid log(0)
201
+ entropy_mixing += fraction * math.log(fraction)
202
+
203
+ eCH_gas_mol += R * Tamb * entropy_mixing
204
+ eCH_mol = eCH_gas_mol + eCH_liquid_mol
205
+ logging.info(f"Condensed phase chemical exergy: {eCH_mol} J/kmol")
206
+
207
+ else: # Case C: Water doesn't condense
208
+ logging.info(f"Water does not condense.")
209
+ eCH_mol = 0
210
+ for substance, fraction in molar_fractions.items():
211
+ aliases = CP.get_aliases(substance)
212
+ for alias in aliases:
213
+ if alias.upper() in chem_ex_data:
214
+ eCH_mol += fraction * (chem_ex_data[alias.upper()][3]) # Exergy in J/kmol
215
+ break
216
+ else:
217
+ logging.error(f"No matching alias found for {substance}")
218
+ raise KeyError(f"No matching alias found for {substance}")
219
+
220
+ if fraction > 0: # Avoid log(0)
221
+ entropy_mixing += fraction * math.log(fraction)
222
+
223
+ eCH_mol += R * Tamb * entropy_mixing
224
+
225
+ else: # Case C: No water present
226
+ logging.info(f"No water present in the mixture.")
227
+ eCH_mol = 0
228
+ for substance, fraction in molar_fractions.items():
229
+ aliases = CP.get_aliases(substance)
230
+ for alias in aliases:
231
+ if alias.upper() in chem_ex_data:
232
+ eCH_mol += fraction * (chem_ex_data[alias.upper()][3]) # Exergy in J/kmol
233
+ break
234
+ else:
235
+ logging.error(f"No matching alias found for {substance}")
236
+ raise KeyError(f"No matching alias found for {substance}")
237
+
238
+ if fraction > 0: # Avoid log(0)
239
+ entropy_mixing += fraction * math.log(fraction)
240
+
241
+ eCH_mol += R * Tamb * entropy_mixing
242
+
243
+ eCH = eCH_mol / total_molar_mass # Divide molar exergy by molar mass of mixture
244
+ logging.info(f"Chemical exergy: {eCH} kJ/kg")
245
+
246
+ return eCH
247
+
248
+ except Exception as e:
249
+ logging.error(f"Error in calc_chemical_exergy: {e}")
250
+ raise
251
+
252
+
253
+ def add_chemical_exergy(my_json, Tamb, pamb, chemExLib):
254
+ """
255
+ Adds the chemical exergy to each connection in the JSON data, prioritizing molar composition if available.
256
+
257
+ Parameters:
258
+ - my_json: The JSON object containing the components and connections.
259
+ - Tamb: Ambient temperature in Celsius.
260
+ - pamb: Ambient pressure in bar.
261
+
262
+ Returns:
263
+ - The modified JSON object with added chemical exergy for each connection.
264
+ """
265
+ # Check if Tamb and pamb are provided and not None
266
+ if Tamb is None or pamb is None:
267
+ raise ValueError("Ambient temperature (Tamb) and pressure (pamb) are required for chemical exergy calculation. "
268
+ "Please ensure they are included in the JSON or passed as arguments.")
269
+
270
+ # Iterate over each material connection with kind == 'material'
271
+ for conn_name, conn_data in my_json['connections'].items():
272
+ if conn_data['kind'] == 'material':
273
+ # Prefer molar composition if available, otherwise use mass composition
274
+ molar_composition = conn_data.get('molar_composition', {})
275
+ mass_composition = conn_data.get('mass_composition', {})
276
+
277
+ # Prepare stream data for exergy calculation, prioritizing molar composition
278
+ if molar_composition:
279
+ stream_data = {'molar_composition': molar_composition}
280
+ logging.info(f"Using molar composition for connection {conn_name}")
281
+ else:
282
+ stream_data = {'mass_composition': mass_composition}
283
+ logging.info(f"Using mass composition for connection {conn_name}")
284
+
285
+ # Add the chemical exergy value
286
+ conn_data['e_CH'] = calc_chemical_exergy(stream_data, Tamb, pamb, chemExLib)
287
+ conn_data['e_CH_unit'] = fluid_property_data['e']['SI_unit']
288
+ logging.info(f"Added chemical exergy to connection {conn_name}: {conn_data['e_CH']} kJ/kg")
289
+ else:
290
+ logging.info(f"Skipped chemical exergy calculation for non-material connection {conn_name} ({conn_data['kind']})")
291
+
292
+ return my_json
293
+
294
+
295
+ def add_total_exergy_flow(my_json, split_physical_exergy):
296
+ """
297
+ Adds the total exergy flow to each connection in the JSON data based on its kind.
298
+
299
+ - For 'material' connections, the exergy is calculated as before.
300
+ - For 'power' connections, the energy flow value is used directly.
301
+ - For 'heat' connections, if the associated component is of class
302
+ SimpleHeatExchanger, the thermal exergy difference is computed as:
303
+ ..math::
304
+ E = (e^\mathrm{T}_\mathrm{in} \cdot \dot m_\mathrm{in})
305
+ - (e^\mathrm{T}_\mathrm{out} \cdot \dot m_\mathrm{out})
306
+
307
+ Otherwise, a warning is logged and E is set to None.
308
+
309
+ Parameters
310
+ ----------
311
+ my_json : dict
312
+ The JSON object containing the components and connections.
313
+ split_physical_exergy : bool
314
+ Split physical exergy in mechanical and thermal shares.
315
+
316
+ Returns
317
+ -------
318
+ dict
319
+ The modified JSON object with added total exergy flow for each
320
+ connection.
321
+ """
322
+ for conn_name, conn_data in my_json['connections'].items():
323
+ try:
324
+ if conn_data['kind'] == 'material':
325
+ # For material connections: E = m * (e^PH + e^CH)
326
+ conn_data['E_PH'] = conn_data['m'] * conn_data['e_PH']
327
+ if conn_data.get('e_CH') is not None:
328
+ conn_data['E_CH'] = conn_data['m'] * conn_data['e_CH']
329
+ conn_data['E'] = conn_data['E_PH'] + conn_data['E_CH']
330
+ else:
331
+ conn_data['E'] = conn_data['E_PH']
332
+ logging.info(f"Missing chemical exergy for connection {conn_name}. Using only physical exergy.")
333
+ if split_physical_exergy:
334
+ if conn_data.get('e_T') is not None:
335
+ conn_data['E_T'] = conn_data['m'] * conn_data['e_T']
336
+ else:
337
+ msg = f"Missing thermal exergy for connection {conn_name}."
338
+ logging.error(msg)
339
+ raise KeyError(msg)
340
+ if conn_data.get('e_M') is not None:
341
+ conn_data['E_M'] = conn_data['m'] * conn_data['e_M']
342
+ else:
343
+ msg = f"Missing mechanical exergy for connection {conn_name}."
344
+ logging.error(msg)
345
+ raise KeyError(msg)
346
+ elif conn_data['kind'] == 'power':
347
+ # For power connections, use the energy flow value directly.
348
+ conn_data['E'] = conn_data['energy_flow']
349
+ elif conn_data['kind'] == 'heat':
350
+ # For heat connections, attempt the new calculation.
351
+ # Identify the associated component (either source or target)
352
+ comp_name = conn_data['source_component'] or conn_data['target_component']
353
+ # Check if the component is either a SimpleHeatExchanger or a SteamGenerator.
354
+ if ("SimpleHeatExchanger" in my_json['components'] and
355
+ comp_name in my_json['components']["SimpleHeatExchanger"]):
356
+ # Retrieve the inlet material streams: those with this component as target.
357
+ inlet_conns = [c for c in my_json['connections'].values()
358
+ if c.get('target_component') == comp_name and c.get('kind') == 'material']
359
+ # Retrieve the outlet material streams: those with this component as source.
360
+ outlet_conns = [c for c in my_json['connections'].values()
361
+ if c.get('source_component') == comp_name and c.get('kind') == 'material']
362
+ # Determine which exergy key to use based on the flag.
363
+ exergy_key = 'e_T' if split_physical_exergy else 'e_PH'
364
+
365
+ if inlet_conns and outlet_conns:
366
+ # For simplicity, take the first inlet and first outlet.
367
+ inlet = inlet_conns[0]
368
+ outlet = outlet_conns[0]
369
+ # Calculate the heat exergy difference using the selected key:
370
+ conn_data['E'] = inlet.get(exergy_key, 0) * inlet.get('m', 0) - outlet.get(exergy_key, 0) * outlet.get('m', 0)
371
+ else:
372
+ conn_data['E'] = None
373
+ logging.warning(f"Not enough material connections for heat exchanger {comp_name} for heat exergy calculation.")
374
+ elif ("SteamGenerator" in my_json['components'] and
375
+ comp_name in my_json['components']["SteamGenerator"]):
376
+ # Retrieve material connections for the steam generator.
377
+ inlet_conns = [c for c in my_json['connections'].values()
378
+ if c.get('target_component') == comp_name and c.get('kind') == 'material']
379
+ outlet_conns = [c for c in my_json['connections'].values()
380
+ if c.get('source_component') == comp_name and c.get('kind') == 'material']
381
+ if inlet_conns and outlet_conns:
382
+ # For the steam generator, group the material connections as follows:
383
+ feed_water = inlet_conns[0] # inl[0]: Feed water inlet (HP)
384
+ steam_inlet = inlet_conns[1] if len(inlet_conns) > 1 else {} # inl[1]: Steam inlet (IP)
385
+ superheated_HP = outlet_conns[0] # outl[0]: Superheated steam outlet (HP)
386
+ superheated_IP = outlet_conns[1] if len(outlet_conns) > 1 else {} # outl[1]: Superheated steam outlet (IP)
387
+ water_inj_HP = inlet_conns[2] if len(inlet_conns) > 2 else {} # inl[2]: Water injection (HP)
388
+ water_inj_IP = inlet_conns[3] if len(inlet_conns) > 3 else {} # inl[3]: Water injection (IP)
389
+
390
+ exergy_type = 'e_T' if split_physical_exergy else 'e_PH'
391
+ # Calculate the contributions based on the new E_F definition:
392
+ E_F_HP = superheated_HP.get('m', 0) * superheated_HP.get(exergy_type, 0) - \
393
+ feed_water.get('m', 0) * feed_water.get(exergy_type, 0)
394
+ E_F_IP = (superheated_IP.get('m', 0) * superheated_IP.get(exergy_type, 0) -
395
+ steam_inlet.get('m', 0) * steam_inlet.get(exergy_type, 0))
396
+ E_F_w_inj = (water_inj_HP.get('m', 0) * water_inj_HP.get(exergy_type, 0) +
397
+ water_inj_IP.get('m', 0) * water_inj_IP.get(exergy_type, 0))
398
+ # Total exergy flow for the heat input (E_TOT) is taken as the exergy fuel E_F:
399
+ E_TOT = E_F_HP + E_F_IP - E_F_w_inj
400
+ conn_data['E'] = E_TOT
401
+ else:
402
+ conn_data['E'] = None
403
+ logging.warning(f"Not enough material connections for steam generator {comp_name} for heat exergy calculation.")
404
+ else:
405
+ conn_data['E'] = None
406
+ logging.warning(f"Heat connection {conn_name} is not associated with a recognized heat exchanger component.")
407
+ elif conn_data['kind'] == 'other':
408
+ # No exergy flow calculation for 'other' kind.
409
+ pass
410
+ else:
411
+ logging.warning(f"Unknown connection kind: {conn_data['kind']} for connection {conn_name}. Skipping exergy flow calculation.")
412
+ conn_data['E'] = None
413
+
414
+ # Assign the exergy unit (assuming fluid_property_data is defined elsewhere)
415
+ conn_data['E_unit'] = fluid_property_data['power']['SI_unit']
416
+
417
+ except Exception as e:
418
+ logging.error(f"Error calculating total exergy flow for connection {conn_name}: {e}")
419
+ conn_data['E'] = None
420
+
421
+ return my_json
422
+
423
+
424
+
425
+ def convert_to_SI(property, value, unit):
426
+ r"""
427
+ Convert a value to its SI value.
428
+
429
+ Parameters
430
+ ----------
431
+ property : str
432
+ Fluid property to convert.
433
+
434
+ value : float
435
+ Value to convert.
436
+
437
+ unit : str
438
+ Unit of the value.
439
+
440
+ Returns
441
+ -------
442
+ SI_value : float
443
+ Specified fluid property in SI value.
444
+
445
+ Raises
446
+ ------
447
+ ValueError: If the property or unit is invalid or conversion is not possible.
448
+ """
449
+ # Check if value is None
450
+ if value is None:
451
+ logging.warning(f"Value is None for property '{property}', cannot convert.")
452
+ return None
453
+
454
+ # Check if the property is valid and exists in fluid_property_data
455
+ if property not in fluid_property_data:
456
+ logging.warning(f"Unrecognized property: '{property}'. Returning original value.")
457
+ return value
458
+
459
+ # Check if the unit is valid
460
+ if unit == 'Unknown':
461
+ logging.warning(f"Unrecognized unit for property '{property}'. Returning original value.")
462
+ return value
463
+
464
+ try:
465
+ # Handle temperature conversions separately
466
+ if property == 'T':
467
+ if unit not in fluid_property_data['T']['units']:
468
+ raise ValueError(f"Invalid unit '{unit}' for temperature. Unit not found.")
469
+ converters = fluid_property_data['T']['units'][unit]
470
+ return (value + converters[0]) * converters[1]
471
+
472
+ # Handle all other property conversions
473
+ else:
474
+ if unit not in fluid_property_data[property]['units']:
475
+ raise ValueError(f"Invalid unit '{unit}' for property '{property}'. Unit not found.")
476
+ conversion_factor = fluid_property_data[property]['units'][unit]
477
+ return value * conversion_factor
478
+
479
+ except KeyError as e:
480
+ raise ValueError(f"Conversion error: {e}")
481
+ except Exception as e:
482
+ raise ValueError(f"An error occurred during the unit conversion: {e}")
483
+
484
+
485
+
486
+ fluid_property_data = {
487
+ 'm': {
488
+ 'text': 'mass flow',
489
+ 'SI_unit': 'kg / s',
490
+ 'units': {
491
+ 'kg / s': 1, 'kg / min': 1 / 60, 'kg / h': 1 / 3.6e3, 'kg/s': 1, 'kg/min': 1 / 60, 'kg/h': 1 / 3.6e3,
492
+ 'kg / sec': 1, 'kg/sec': 1,
493
+ 't / h': 1 / 3.6, 'g / s': 1e-3, 't/h': 1 / 3.6, 'g/s': 1e-3,
494
+ 'g / sec': 1e-3, 'g/sec': 1e-3,
495
+ },
496
+ 'latex_eq': r'0 = \dot{m} - \dot{m}_\mathrm{spec}',
497
+ 'documentation': {'float_fmt': '{:,.3f}'}
498
+ },
499
+ 'n': {
500
+ 'text': 'molar flow',
501
+ 'SI_unit': 'mol / s',
502
+ 'units': {
503
+ 'mol / s': 1, 'mol / min': 1 / 60, 'mol / h': 1 / 3.6e3, 'mol/s': 1, 'mol/min': 1 / 60, 'mol/h': 1 / 3.6e3,
504
+ 'kmol / s': 1e3, 'kmol / min': 1 / 60e3, 'kmol / h': 1 / 3.6e6, 'kmol/s': 1e3, 'kmol/min': 1 / 60e3, 'kmol/h': 1 / 3.6e6,
505
+ 'mol / sec': 1, 'mol/sec': 1, 'kmol / sec': 1e3, 'kmol/sec': 1e3,
506
+ },
507
+ 'latex_eq': r'0 = \dot{n} - \dot{n}_\mathrm{spec}',
508
+ 'documentation': {'float_fmt': '{:,.3f}'}
509
+ },
510
+ 'v': {
511
+ 'text': 'volumetric flow',
512
+ 'SI_unit': 'm3 / s',
513
+ 'units': {
514
+ 'm3 / s': 1, 'm3 / min': 1 / 60, 'm3 / h': 1 / 3.6e3,
515
+ 'l / s': 1 / 1e3, 'l / min': 1 / 60e3, 'l / h': 1 / 3.6e6
516
+ },
517
+ 'latex_eq': (
518
+ r'0 = \dot{m} \cdot v \left(p,h\right)- \dot{V}_\mathrm{spec}'),
519
+ 'documentation': {'float_fmt': '{:,.3f}'}
520
+ },
521
+ 'p': {
522
+ 'text': 'pressure',
523
+ 'SI_unit': 'Pa',
524
+ 'units': {
525
+ 'Pa': 1, 'kPa': 1e3, 'psi': 6.8948e3,
526
+ 'bar': 1e5, 'atm': 1.01325e5, 'MPa': 1e6
527
+ },
528
+ 'latex_eq': r'0 = p - p_\mathrm{spec}',
529
+ 'documentation': {'float_fmt': '{:,.3f}'}
530
+ },
531
+ 'h': {
532
+ 'text': 'enthalpy',
533
+ 'SI_unit': 'J / kg',
534
+ 'SI_unit_molar:': 'J / mol',
535
+ 'units': {
536
+ 'J / kg': 1, 'kJ / kg': 1e3, 'MJ / kg': 1e6, 'J/kg': 1, 'kJ/kg': 1e3, 'MJ/kg': 1e6,
537
+ 'cal / kg': 4.184, 'kcal / kg': 4.184e3, 'cal/kg': 4.184, 'kcal/kg': 4.184e3,
538
+ 'Wh / kg': 3.6e3, 'kWh / kg': 3.6e6, 'Wh/kg': 3.6e3, 'kWh kg': 3.6e6,
539
+ 'J / mol': 1, 'kJ / mol': 1e3, 'MJ / mol': 1e6, 'J/mol': 1, 'kJ/mol': 1e3, 'MJ/mol': 1e6,
540
+ 'J / kmol': 1e-3, 'kJ / kmol': 1, 'MJ / kmol': 1e3, 'J/kmol': 1e-3, 'kJ/kmol': 1, 'MJ/kmol': 1e3
541
+ },
542
+ 'latex_eq': r'0 = h - h_\mathrm{spec}',
543
+ 'documentation': {'float_fmt': '{:,.3f}'}
544
+ },
545
+ 'e': {
546
+ 'text': 'exergy',
547
+ 'SI_unit': 'J / kg',
548
+ 'SI_unit_molar:': 'J / mol',
549
+ 'units': {
550
+ 'J / kg': 1, 'kJ / kg': 1e3, 'MJ / kg': 1e6, 'J/kg': 1, 'kJ/kg': 1e3, 'MJ/kg': 1e6,
551
+ 'cal / kg': 4.184, 'kcal / kg': 4.184e3, 'cal/kg': 4.184, 'kcal/kg': 4.184e3,
552
+ 'Wh / kg': 3.6e3, 'kWh / kg': 3.6e6, 'Wh/kg': 3.6e3, 'kWh kg': 3.6e6,
553
+ 'J / mol': 1, 'kJ / mol': 1e3, 'MJ / mol': 1e6, 'J/mol': 1, 'kJ/mol': 1e3, 'MJ/mol': 1e6,
554
+ 'J / kmol': 1e-3, 'kJ / kmol': 1, 'MJ / kmol': 1e3, 'J/kmol': 1e-3, 'kJ/kmol': 1, 'MJ/kmol': 1e3
555
+ },
556
+ 'latex_eq': r'0 = h - h_\mathrm{spec}',
557
+ 'documentation': {'float_fmt': '{:,.3f}'}
558
+ },
559
+ 'T': {
560
+ 'text': 'temperature',
561
+ 'SI_unit': 'K',
562
+ 'units': {
563
+ 'K': [0, 1], 'R': [0, 5 / 9],
564
+ 'C': [273.15, 1], 'F': [459.67, 5 / 9]
565
+ },
566
+ 'latex_eq': r'0 = T \left(p, h \right) - T_\mathrm{spec}',
567
+ 'documentation': {'float_fmt': '{:,.1f}'}
568
+ },
569
+ 'Td_bp': {
570
+ 'text': 'temperature difference to boiling point',
571
+ 'SI_unit': 'K',
572
+ 'units': {
573
+ 'K': 1, 'R': 5 / 9, 'C': 1, 'F': 5 / 9
574
+ },
575
+ 'latex_eq': r'0 = \Delta T_\mathrm{spec}- T_\mathrm{sat}\left(p\right)',
576
+ 'documentation': {'float_fmt': '{:,.1f}'}
577
+ },
578
+ 'vol': {
579
+ 'text': 'specific volume',
580
+ 'SI_unit': 'm3 / kg',
581
+ 'units': {'m3 / kg': 1, 'l / kg': 1e-3},
582
+ 'latex_eq': (
583
+ r'0 = v\left(p,h\right) \cdot \dot{m} - \dot{V}_\mathrm{spec}'),
584
+ 'documentation': {'float_fmt': '{:,.3f}'}
585
+ },
586
+ 'x': {
587
+ 'text': 'vapor mass fraction',
588
+ 'SI_unit': '-',
589
+ 'units': {'1': 1, '-': 1, '%': 1e-2, 'ppm': 1e-6},
590
+ 'latex_eq': r'0 = h - h\left(p, x_\mathrm{spec}\right)',
591
+ 'documentation': {'float_fmt': '{:,.2f}'}
592
+ },
593
+ 's': {
594
+ 'text': 'entropy',
595
+ 'SI_unit': 'J / kgK',
596
+ 'SI_unit_molar:': 'J / molK',
597
+ 'units': {
598
+ 'J / kgK': 1, 'kJ / kgK': 1e3, 'MJ / kgK': 1e6, 'J/kgK': 1, 'kJ/kgK': 1e3, 'MJ/kgK': 1e6,
599
+ 'J / kg-K': 1, 'kJ / kg-K': 1e3, 'MJ / kg-K': 1e6, 'J/kg-K': 1, 'kJ/kg-K': 1e3, 'MJ/kg-K': 1e6,
600
+ 'J / molK': 1, 'kJ / molK': 1e3, 'MJ / molK': 1e6, 'J/molK': 1, 'kJ/molK': 1e3, 'MJ/molK': 1e6,
601
+ 'J / mol-K': 1, 'kJ / mol-K': 1e3, 'MJ / mol-K': 1e6, 'J/mol-K': 1, 'kJ/mol-K': 1e3, 'MJ/mol-K': 1e6,
602
+ 'J / kmolK': 1e-3, 'kJ / kmolK': 1, 'MJ / kmolK': 1e3, 'J/kmolK': 1e-3, 'kJ/kmolK': 1, 'MJ/kmolK': 1e3,
603
+ 'J / kmol-K': 1e-3, 'kJ / kmol-K': 1, 'MJ / kmol-K': 1e3, 'J/kmol-K': 1e-3, 'kJ/kmol-K': 1, 'MJ/kmol-K': 1e3
604
+ },
605
+ 'latex_eq': r'0 = s_\mathrm{spec} - s\left(p, h \right)',
606
+ 'documentation': {'float_fmt': '{:,.2f}'}
607
+ },
608
+ 'power': {
609
+ 'text': 'power',
610
+ 'SI_unit': 'W',
611
+ 'units': {'W': 1, 'kW': 1e3, 'MW': 1e6},
612
+ },
613
+ 'heat': {
614
+ 'text': 'heat',
615
+ 'SI_unit': 'W',
616
+ 'units': {'W': 1, 'kW': 1e3, 'MW': 1e6},
617
+ },
618
+ 'kA': {
619
+ 'text': 'kA',
620
+ 'SI_unit': 'W / K',
621
+ 'units': {
622
+ 'W / K': 1, 'kW / K': 1e3, 'MW / K': 1e6,
623
+ 'W/K': 1, 'kW/K': 1e3, 'MW/K': 1e6},
624
+ },
625
+ 'A': {
626
+ 'text': 'area',
627
+ 'SI_unit': 'm2',
628
+ 'units': {'m2': 1, 'cm2': 1e-4, 'mm2': 1e-6,
629
+ 'm²': 1, 'cm²': 1e-4, 'mm²': 1e-6},
630
+ },
631
+ 'VM': {
632
+ 'text': 'volume flow',
633
+ 'SI_unit': 'm3 / s',
634
+ 'units': {'m3 / s': 1, 'l / s': 1e-3, 'l/s': 1e-3,
635
+ 'm³/s': 1, 'l/min': 1 / 60e3, 'l/h': 1 / 3.6e6},
636
+ }
637
+ }
File without changes
File without changes