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.
- exerpy/__init__.py +12 -0
- exerpy/analyses.py +1711 -0
- exerpy/components/__init__.py +16 -0
- exerpy/components/combustion/__init__.py +0 -0
- exerpy/components/combustion/base.py +248 -0
- exerpy/components/component.py +126 -0
- exerpy/components/heat_exchanger/__init__.py +0 -0
- exerpy/components/heat_exchanger/base.py +449 -0
- exerpy/components/heat_exchanger/condenser.py +323 -0
- exerpy/components/heat_exchanger/simple.py +358 -0
- exerpy/components/heat_exchanger/steam_generator.py +264 -0
- exerpy/components/helpers/__init__.py +0 -0
- exerpy/components/helpers/cycle_closer.py +104 -0
- exerpy/components/nodes/__init__.py +0 -0
- exerpy/components/nodes/deaerator.py +318 -0
- exerpy/components/nodes/drum.py +164 -0
- exerpy/components/nodes/flash_tank.py +89 -0
- exerpy/components/nodes/mixer.py +332 -0
- exerpy/components/piping/__init__.py +0 -0
- exerpy/components/piping/valve.py +394 -0
- exerpy/components/power_machines/__init__.py +0 -0
- exerpy/components/power_machines/generator.py +168 -0
- exerpy/components/power_machines/motor.py +173 -0
- exerpy/components/turbomachinery/__init__.py +0 -0
- exerpy/components/turbomachinery/compressor.py +318 -0
- exerpy/components/turbomachinery/pump.py +310 -0
- exerpy/components/turbomachinery/turbine.py +351 -0
- exerpy/data/Ahrendts.json +90 -0
- exerpy/functions.py +637 -0
- exerpy/parser/__init__.py +0 -0
- exerpy/parser/from_aspen/__init__.py +0 -0
- exerpy/parser/from_aspen/aspen_config.py +61 -0
- exerpy/parser/from_aspen/aspen_parser.py +721 -0
- exerpy/parser/from_ebsilon/__init__.py +38 -0
- exerpy/parser/from_ebsilon/check_ebs_path.py +74 -0
- exerpy/parser/from_ebsilon/ebsilon_config.py +1055 -0
- exerpy/parser/from_ebsilon/ebsilon_functions.py +181 -0
- exerpy/parser/from_ebsilon/ebsilon_parser.py +660 -0
- exerpy/parser/from_ebsilon/utils.py +79 -0
- exerpy/parser/from_tespy/tespy_config.py +23 -0
- exerpy-0.0.1.dist-info/METADATA +158 -0
- exerpy-0.0.1.dist-info/RECORD +44 -0
- exerpy-0.0.1.dist-info/WHEEL +4 -0
- 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
|