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
@@ -0,0 +1,721 @@
1
+ import json
2
+ import logging
3
+ import os
4
+
5
+ from exerpy.functions import convert_to_SI
6
+ from exerpy.functions import fluid_property_data
7
+
8
+ from .aspen_config import connector_mappings
9
+ from .aspen_config import grouped_components
10
+
11
+
12
+ class AspenModelParser:
13
+ """
14
+ A class to parse Aspen Plus models, simulate them, extract data, and write to JSON.
15
+ """
16
+ def __init__(self, model_path, split_physical_exergy=True):
17
+ """
18
+ Initializes the parser with the given model path.
19
+
20
+ Parameters:
21
+ model_path (str): Path to the Aspen Plus model file.
22
+ split_physical_exergy (bool): Flag to split physical exergy into thermal and mechanical components.
23
+ """
24
+ self.model_path = model_path
25
+ self.split_physical_exergy = split_physical_exergy
26
+ self.aspen = None # Aspen Plus application instance
27
+ self.components_data = {} # Dictionary to store component data
28
+ self.connections_data = {} # Dictionary to store connection data
29
+
30
+ # Dictionary to map component types to specific connector assignment functions
31
+ self.connector_assignment_functions = {
32
+ 'Mixer': self.assign_mixer_connectors,
33
+ 'RStoic': self.assign_combustion_chamber_connectors,
34
+ 'FSplit': self.assign_splitter_connectors,
35
+ # Add other specific component functions here
36
+ }
37
+
38
+
39
+ def initialize_model(self):
40
+ """
41
+ Initializes the Aspen Plus application and opens the specified model.
42
+ """
43
+ from win32com.client import Dispatch
44
+ try:
45
+ # Start Aspen Plus application via COM Dispatch
46
+ self.aspen = Dispatch('Apwn.Document')
47
+ # Load the Aspen model file
48
+ self.aspen.InitFromArchive2(self.model_path)
49
+ logging.info(f"Model opened successfully: {self.model_path}")
50
+ except Exception as e:
51
+ logging.error(f"Failed to initialize the model: {e}")
52
+ raise
53
+
54
+ def parse_model(self):
55
+ """
56
+ Parses the components and connections from the Aspen model.
57
+ """
58
+ try:
59
+ # Parse Tamb and pamb
60
+ self.parse_ambient_conditions()
61
+
62
+ # Parse streams (connections)
63
+ self.parse_streams()
64
+
65
+ # Parse blocks (components)
66
+ self.parse_blocks()
67
+
68
+ except Exception as e:
69
+ logging.error(f"Error while parsing the model: {e}")
70
+ raise
71
+
72
+
73
+ def parse_streams(self):
74
+ """
75
+ Parses the streams (connections) in the Aspen model.
76
+ """
77
+ # Get the stream nodes and their names
78
+ stream_nodes = self.aspen.Tree.FindNode(r'\Data\Streams').Elements
79
+ stream_names = [stream_node.Name for stream_node in stream_nodes]
80
+
81
+ # ALL ASPEN CONNECTIONS
82
+ # Initialize connection data with the common fields
83
+ for stream_name in stream_names:
84
+ stream_node = self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}')
85
+ connection_data = {
86
+ 'name': stream_name,
87
+ 'kind': None,
88
+ 'source_component': None,
89
+ 'source_connector': None,
90
+ 'target_component': None,
91
+ 'target_connector': None,
92
+ }
93
+
94
+ # Find the source and target components
95
+ source_port_node = self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Ports\SOURCE')
96
+ if source_port_node is not None and source_port_node.Elements.Count > 0:
97
+ connection_data["source_component"] = source_port_node.Elements(0).Name
98
+
99
+ destination_port_node = self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Ports\DEST')
100
+ if destination_port_node is not None and destination_port_node.Elements.Count > 0:
101
+ connection_data["target_component"] = destination_port_node.Elements(0).Name
102
+
103
+ # HEAT AND POWER STREAMS
104
+ if self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Input\WORK') is not None:
105
+ connection_data['kind'] = 'power'
106
+ connection_data['energy_flow'] = convert_to_SI(
107
+ 'power',
108
+ abs(self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\POWER_OUT').Value),
109
+ self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\POWER_OUT').UnitString
110
+ ) if self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\POWER_OUT') is not None else None
111
+ elif self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Input\HEAT') is not None:
112
+ connection_data['kind'] = 'heat'
113
+ connection_data['energy_flow'] = convert_to_SI(
114
+ 'power',
115
+ abs(self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\QCALC').Value),
116
+ self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\QCALC').UnitString
117
+ ) if self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\QCALC') is not None else None
118
+
119
+ # MATERIAL STREAMS
120
+ else:
121
+ # Assume it's a material stream and retrieve additional properties
122
+ connection_data.update({
123
+ 'kind': 'material',
124
+ 'T': (
125
+ convert_to_SI(
126
+ 'T',
127
+ self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\TEMP_OUT\MIXED').Value,
128
+ self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\TEMP_OUT\MIXED').UnitString
129
+ ) if self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\TEMP_OUT\MIXED') is not None else None
130
+ ),
131
+ 'T_unit': fluid_property_data['T']['SI_unit'],
132
+ 'p': (
133
+ convert_to_SI(
134
+ 'p',
135
+ self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\PRES_OUT\MIXED').Value,
136
+ self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\PRES_OUT\MIXED').UnitString
137
+ ) if self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\PRES_OUT\MIXED') is not None else None
138
+ ),
139
+ 'p_unit': fluid_property_data['p']['SI_unit'],
140
+ 'h': (
141
+ convert_to_SI(
142
+ 'h',
143
+ self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\HMX_MASS\MIXED').Value,
144
+ self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\HMX_MASS\MIXED').UnitString
145
+ ) if self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\HMX_MASS\MIXED') is not None else None
146
+ ),
147
+ 'h_unit': fluid_property_data['h']['SI_unit'],
148
+ 's': (
149
+ convert_to_SI(
150
+ 's',
151
+ self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\SMX_MASS\MIXED').Value,
152
+ self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\SMX_MASS\MIXED').UnitString
153
+ ) if self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\SMX_MASS\MIXED') is not None else None
154
+ ),
155
+ 's_unit': fluid_property_data['s']['SI_unit'],
156
+ 'm': (
157
+ convert_to_SI(
158
+ 'm',
159
+ self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\MASSFLMX\MIXED').Value,
160
+ self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\MASSFLMX\MIXED').UnitString
161
+ ) if self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\MASSFLMX\MIXED') is not None else None
162
+ ),
163
+ 'm_unit': fluid_property_data['m']['SI_unit'],
164
+ 'energy_flow': (
165
+ convert_to_SI(
166
+ 'power',
167
+ abs(self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\HMX_FLOW\MIXED').Value),
168
+ self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\HMX_FLOW\MIXED').UnitString
169
+ ) if self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\HMX_FLOW\MIXED') is not None else None
170
+ ),
171
+ 'energy_flow_unit': fluid_property_data['power']['SI_unit'],
172
+ 'e_PH': (
173
+ convert_to_SI(
174
+ 'e',
175
+ self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\STRM_UPP\EXERGYMS\MIXED\TOTAL').Value,
176
+ self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\STRM_UPP\EXERGYMS\MIXED\TOTAL').UnitString
177
+ ) if self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\STRM_UPP\EXERGYMS\MIXED\TOTAL') is not None else (
178
+ logging.warning(f"e_PH node not found for stream {stream_name}"),
179
+ None
180
+ )[1]
181
+ ),
182
+ 'e_PH_unit': fluid_property_data['e']['SI_unit'],
183
+ 'n': (
184
+ convert_to_SI(
185
+ 'n',
186
+ self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\TOT_FLOW').Value,
187
+ self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\TOT_FLOW').UnitString
188
+ ) if self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\TOT_FLOW') is not None else None
189
+ ),
190
+ 'n_unit': fluid_property_data['n']['SI_unit'],
191
+ 'mass_composition': {},
192
+ 'molar_composition': {},
193
+ })
194
+ # Retrieve the fluid names for the stream
195
+ mole_frac_node = self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\MOLEFRAC\MIXED')
196
+ if mole_frac_node is not None:
197
+ fluid_names = [fluid.Name for fluid in mole_frac_node.Elements]
198
+
199
+ # Retrieve the molar composition for each fluid
200
+ for fluid_name in fluid_names:
201
+ mole_frac = self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\MOLEFRAC\MIXED\{fluid_name}').Value
202
+ if mole_frac not in [0, None]: # Skip fluids with 0 or None as the fraction
203
+ connection_data["molar_composition"][fluid_name] = mole_frac
204
+
205
+ mass_frac_node = self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\MASSFRAC\MIXED')
206
+ if mass_frac_node is not None:
207
+ # Retrieve the mass composition for each fluid
208
+ for fluid_name in [fluid.Name for fluid in mass_frac_node.Elements]:
209
+ mass_frac = self.aspen.Tree.FindNode(fr'\Data\Streams\{stream_name}\Output\MASSFRAC\MIXED\{fluid_name}').Value
210
+ if mass_frac not in [0, None]: # Skip fluids with 0 or None as the fraction
211
+ connection_data["mass_composition"][fluid_name] = mass_frac
212
+
213
+ # Store connection data
214
+ self.connections_data[stream_name] = connection_data
215
+
216
+
217
+ def parse_blocks(self):
218
+ """
219
+ Parses the blocks (components) in the Aspen model and ensures that all components, including motors created from pumps, are properly grouped.
220
+ """
221
+ block_nodes = self.aspen.Tree.FindNode(r'\Data\Blocks').Elements
222
+ block_names = [block_node.Name for block_node in block_nodes]
223
+
224
+ # Process each block
225
+ for block_name in block_names:
226
+ model_type_node = self.aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Input\MODEL_TYPE')
227
+ model_type = model_type_node.Value if model_type_node is not None else None
228
+
229
+ component_type_node = self.aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}')
230
+ if component_type_node is None:
231
+ continue
232
+ component_type = component_type_node.AttributeValue(6)
233
+ if component_type == "Mixer":
234
+ mixer_value = component_type_node.Value
235
+ if mixer_value in ["TRIANGLE", "HEAT"]:
236
+ logging.info(f"Ignoring Mixer {block_name} with value {mixer_value}.")
237
+ continue
238
+
239
+ component_data = {
240
+ 'name': block_name,
241
+ 'type': component_type,
242
+ 'eta_s': (
243
+ self.aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Output\EFF_ISEN').Value
244
+ if self.aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Output\EFF_ISEN') is not None else None
245
+ ),
246
+ 'eta_mech': (
247
+ self.aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Output\EFF_MECH').Value
248
+ if self.aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Output\EFF_MECH') is not None else None
249
+ ),
250
+ 'Q': (
251
+ convert_to_SI(
252
+ 'heat',
253
+ self.aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Output\QNET').Value,
254
+ self.aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Output\QNET').UnitString,
255
+ ) if self.aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Output\QNET') is not None else None
256
+ ),
257
+ 'Q_unit': fluid_property_data['heat']['SI_unit'],
258
+ 'P': (
259
+ convert_to_SI(
260
+ 'power',
261
+ abs(self.aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Output\BRAKE_POWER').Value),
262
+ self.aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Output\BRAKE_POWER').UnitString,
263
+ ) if self.aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Output\BRAKE_POWER') is not None else None
264
+ ),
265
+ 'P_unit': fluid_property_data['power']['SI_unit'],
266
+ }
267
+
268
+ # Override component type based on model_type
269
+ if model_type is not None:
270
+ if model_type == "COMPRESSOR":
271
+ component_data['type'] = "Compressor"
272
+ elif model_type == "TURBINE":
273
+ component_data['type'] = "Turbine"
274
+
275
+
276
+ # Handle Generators & Motors (if not in a Pump) as multiplier blocks
277
+ if component_type == 'Mult':
278
+ mult_value_node = self.aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}')
279
+ mult_value = mult_value_node.Value if mult_value_node is not None else None
280
+ if mult_value == 'WORK':
281
+ factor_node = self.aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Input\FACTOR')
282
+ factor = factor_node.Value if factor_node is not None else None
283
+ if factor is not None:
284
+ if factor < 1:
285
+ component_data.update({
286
+ 'eta_el': factor,
287
+ 'type': 'Generator'
288
+ })
289
+ elif factor > 1:
290
+ elec_power_node = self.aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Ports\WS(OUT)').Elements(0)
291
+ elec_power_name = elec_power_node.Name
292
+ if elec_power_name in self.connections_data:
293
+ elec_power = abs(self.connections_data[elec_power_name]['energy_flow'])
294
+ else:
295
+ logging.warning(f"No WS(IN) ports found for block {block_name}")
296
+ elec_power = None
297
+ brake_power_node = self.aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Ports\WS(IN)').Elements(0)
298
+ brake_power_name = brake_power_node.Name
299
+ if brake_power_name in self.connections_data:
300
+ brake_power = abs(self.connections_data[brake_power_name]['energy_flow'])
301
+ else:
302
+ logging.warning(f"No WS(IN) ports found for block {block_name}")
303
+ brake_power = None
304
+ component_data.update({
305
+ 'eta_el': 1/factor,
306
+ 'multiplier factor' : factor,
307
+ 'type': 'Motor',
308
+ 'P_el': elec_power,
309
+ 'P_el_unit': fluid_property_data['power']['SI_unit'],
310
+ 'P_mech': brake_power,
311
+ 'P_mech_unit': fluid_property_data['power']['SI_unit'],
312
+ })
313
+ else: # factor == 1
314
+ choice = input(f"Multiplier Block '{block_name}' has factor = 1. Enter 'G' if it is a Generator or 'M' for Motor: ").strip().upper()
315
+ if choice == 'M':
316
+ elec_power_node = self.aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Ports\WS(OUT)').Elements(0)
317
+ elec_power_name = elec_power_node.Name
318
+ if elec_power_name in self.connections_data:
319
+ elec_power = abs(self.connections_data[elec_power_name]['energy_flow'])
320
+ else:
321
+ logging.warning(f"No WS(IN) ports found for block {block_name}")
322
+ elec_power = None
323
+ brake_power_node = self.aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Ports\WS(IN)').Elements(0)
324
+ brake_power_name = brake_power_node.Name
325
+ if brake_power_name in self.connections_data:
326
+ brake_power = abs(self.connections_data[brake_power_name]['energy_flow'])
327
+ else:
328
+ logging.warning(f"No WS(IN) ports found for block {block_name}")
329
+ brake_power = None
330
+ component_data.update({
331
+ 'eta_el': factor,
332
+ 'type': 'Motor',
333
+ 'P_el': elec_power,
334
+ 'P_el_unit': fluid_property_data['power']['SI_unit'],
335
+ 'P_mech': brake_power,
336
+ 'P_mech_unit': fluid_property_data['power']['SI_unit'],
337
+ })
338
+ else:
339
+ component_data.update({
340
+ 'eta_el': factor,
341
+ 'type': 'Generator'
342
+ })
343
+
344
+ # Create a connection for the heat flows of the SimpleHeatExchanger blocks
345
+ if component_type == 'Heater':
346
+ heat_connection_name = f"{block_name}_HEAT"
347
+ heat_connection_data = {
348
+ 'name': heat_connection_name,
349
+ 'kind': 'heat',
350
+ 'source_component': block_name,
351
+ 'source_connector': 1, # 00 is reserved for the fluid streams
352
+ 'target_component': None, # Heat assumed to leave the system (not relevant for exergy analysis)
353
+ 'target_connector': None, # Heat assumed to leave the system (not relevant for exergy analysis)
354
+ 'energy_flow': abs(component_data['Q']), # the user defines in the balances if the heat flow is positive or negative
355
+ 'energy_flow_unit': fluid_property_data['heat']['SI_unit'],
356
+ }
357
+
358
+ # Store the heat connection
359
+ self.connections_data[heat_connection_name] = heat_connection_data
360
+
361
+ # Group the component
362
+ self.group_component(component_data, block_name)
363
+
364
+ # Handle Pumps and their associated Motors
365
+ if component_type == 'Pump':
366
+ motor_name = f"{block_name}-MOTOR"
367
+ elec_power_node = self.aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Output\ELEC_POWER')
368
+ elec_power = abs(convert_to_SI('power', elec_power_node.Value, elec_power_node.UnitString,)) if elec_power_node is not None else None
369
+ brake_power_node = self.aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Output\BRAKE_POWER')
370
+ brake_power = abs(convert_to_SI('power', brake_power_node.Value, brake_power_node.UnitString,)) if brake_power_node is not None else None
371
+ eff_driv_node = self.aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Output\EFF_DRIV')
372
+ eff_driv = eff_driv_node.Value if eff_driv_node is not None else None
373
+
374
+ motor_data = {
375
+ 'name': motor_name,
376
+ 'type': 'Motor',
377
+ 'P_el': elec_power,
378
+ 'P_el_unit': fluid_property_data['power']['SI_unit'],
379
+ 'P_mech': brake_power,
380
+ 'P_mech_unit': fluid_property_data['power']['SI_unit'],
381
+ 'eta_el': (
382
+ eff_driv
383
+ if eff_driv is not None else None
384
+ ),
385
+ }
386
+
387
+ # Group the motor
388
+ self.group_component(motor_data, motor_name)
389
+
390
+ # Create a new connection for the motor
391
+ if elec_power is not None:
392
+ electr_connection_name = f"{block_name}_ELEC"
393
+ electr_connection_data = {
394
+ 'name': electr_connection_name,
395
+ 'kind': 'power',
396
+ 'source_component': None, # Electrical power usually leaves the system
397
+ 'source_connector': None, # Electrical power usually leaves the system
398
+ 'target_component': motor_name,
399
+ 'target_connector': 0,
400
+ 'energy_flow': motor_data['P_el'],
401
+ 'energy_flow_unit': fluid_property_data['power']['SI_unit'],
402
+ }
403
+
404
+ mech_connection_name = f"{block_name}_MECH"
405
+ mech_connection_data = {
406
+ 'name': mech_connection_name,
407
+ 'kind': 'power',
408
+ 'source_component': motor_name,
409
+ 'source_connector': 0,
410
+ 'target_component': block_name,
411
+ 'target_connector': 1,
412
+ 'energy_flow': motor_data['P_mech'],
413
+ 'energy_flow_unit': fluid_property_data['power']['SI_unit'],
414
+ }
415
+
416
+ # Store the motor connection
417
+ self.connections_data[electr_connection_name] = electr_connection_data
418
+ self.connections_data[mech_connection_name] = mech_connection_data
419
+
420
+ # Assign connectors
421
+ self.assign_connectors(component_data, block_name)
422
+
423
+
424
+ def assign_connectors(self, component_data, block_name):
425
+ """
426
+ Assigns connectors to streams for each component based on its type.
427
+ """
428
+ component_type = component_data['type']
429
+
430
+ # Check if there is a specific assignment function for this component type
431
+ if component_type in self.connector_assignment_functions:
432
+ # Call the specific function for the component type
433
+ self.connector_assignment_functions[component_type](block_name, self.aspen, self.connections_data)
434
+ else:
435
+ # Fall back to the generic connector assignment logic
436
+ self.assign_generic_connectors(block_name, component_type, self.aspen, self.connections_data, connector_mappings)
437
+
438
+
439
+ def assign_mixer_connectors(self, block_name, aspen, connections_data):
440
+ """
441
+ Assign connectors for a Mixer by examining connected streams and their source/target components.
442
+ """
443
+ ports_node = aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Ports')
444
+ if ports_node is None:
445
+ logging.warning(f"No Ports node found for Mixer block: {block_name}")
446
+ return
447
+
448
+ inlet_streams = []
449
+ outlet_streams = []
450
+
451
+ for port in ports_node.Elements:
452
+ port_label = port.Name
453
+ port_node = aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Ports\{port_label}')
454
+ if port_node is not None and port_node.Elements.Count > 0:
455
+ for element in port_node.Elements:
456
+ stream_name = element.Name
457
+ if stream_name in connections_data:
458
+ stream_data = connections_data[stream_name]
459
+ if stream_data.get('target_component') == block_name:
460
+ inlet_streams.append((port_label, stream_name))
461
+ elif stream_data.get('source_component') == block_name:
462
+ outlet_streams.append((port_label, stream_name))
463
+ else:
464
+ logging.warning(f"Stream {stream_name} connected to {block_name} but source/target components do not match.")
465
+
466
+ # Assign connectors to inlet streams
467
+ for idx, (port_label, stream_name) in enumerate(inlet_streams):
468
+ connections_data[stream_name]['target_connector'] = idx
469
+ logging.debug(f"Assigned connector {idx} to inlet stream: {stream_name}")
470
+
471
+ # Assign connector to outlet stream
472
+ if outlet_streams:
473
+ for idx, (port_label, stream_name) in enumerate(outlet_streams):
474
+ connections_data[stream_name]['source_connector'] = 0 # Assuming single outlet for mixer
475
+ logging.debug(f"Assigned connector 0 to outlet stream: {stream_name}")
476
+
477
+
478
+ def assign_splitter_connectors(self, block_name, aspen, connections_data):
479
+ """
480
+ Assign connectors for a Splitter (FSplit) by examining connected streams and their source/target components.
481
+ The inlet stream is assigned 'target_connector' = 0.
482
+ The outlet streams are assigned 'source_connector' numbers starting from 0.
483
+ """
484
+ ports_node = aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Ports')
485
+ if ports_node is None:
486
+ logging.warning(f"No Ports node found for Splitter block: {block_name}")
487
+ return
488
+
489
+ inlet_streams = []
490
+ outlet_streams = []
491
+
492
+ # Iterate over all ports connected to the splitter
493
+ for port in ports_node.Elements:
494
+ port_label = port.Name
495
+ port_node = aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Ports\{port_label}')
496
+ if port_node is not None and port_node.Elements.Count > 0:
497
+ for element in port_node.Elements:
498
+ stream_name = element.Name
499
+ if stream_name in connections_data:
500
+ stream_data = connections_data[stream_name]
501
+ # Determine if the stream is an inlet or outlet based on source and target components
502
+ if stream_data.get('target_component') == block_name:
503
+ inlet_streams.append((port_label, stream_name))
504
+ elif stream_data.get('source_component') == block_name:
505
+ outlet_streams.append((port_label, stream_name))
506
+ else:
507
+ logging.warning(f"Stream {stream_name} connected to {block_name} but source/target components do not match.")
508
+
509
+ # Assign connector to inlet stream(s)
510
+ for idx, (port_label, stream_name) in enumerate(inlet_streams):
511
+ connections_data[stream_name]['target_connector'] = 0 # Assuming single inlet for splitter
512
+ logging.debug(f"Assigned connector 0 to inlet stream: {stream_name}")
513
+
514
+ # Assign connectors to outlet streams
515
+ for idx, (port_label, stream_name) in enumerate(outlet_streams):
516
+ connections_data[stream_name]['source_connector'] = idx
517
+ logging.debug(f"Assigned connector {idx} to outlet stream: {stream_name}")
518
+
519
+
520
+ def assign_combustion_chamber_connectors(self, block_name, aspen, connections_data):
521
+ """
522
+ Assign connectors for a combustion chamber (RStoic), based on stream types (air, fuel, etc.).
523
+ """
524
+ ports_node = aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Ports')
525
+ if ports_node is None:
526
+ logging.warning(f"No Ports node found for combustion chamber block: {block_name}")
527
+ return
528
+
529
+ # Iterate over all ports and assign connectors based on port labels
530
+ for port in ports_node.Elements:
531
+ port_label = port.Name
532
+
533
+ # Handle inlet ports
534
+ if '(IN)' in port_label:
535
+ port_node = aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Ports\{port_label}')
536
+ if port_node is not None and port_node.Elements.Count > 0:
537
+ for element in port_node.Elements:
538
+ stream_name = element.Name
539
+ if stream_name in connections_data:
540
+ molar_composition = connections_data[stream_name].get('molar_composition', {})
541
+ if molar_composition.get('O2', 0) > 0.15:
542
+ connections_data[stream_name]['target_connector'] = 0 # Air inlet
543
+ logging.debug(f"Assigned connector 0 to air inlet stream: {stream_name}")
544
+ elif molar_composition.get('CH4', 0) > 0.15:
545
+ connections_data[stream_name]['target_connector'] = 1 # Fuel inlet
546
+ logging.debug(f"Assigned connector 1 to fuel inlet stream: {stream_name}")
547
+ else:
548
+ logging.warning(f"Stream {stream_name} in {block_name} has ambiguous composition.")
549
+
550
+ # Handle outlet ports
551
+ elif '(OUT)' in port_label:
552
+ port_node = aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Ports\{port_label}')
553
+ if port_node is not None and port_node.Elements.Count > 0:
554
+ for element in port_node.Elements:
555
+ stream_name = element.Name
556
+ if stream_name in connections_data:
557
+ connections_data[stream_name]['source_connector'] = 0 # Outlet stream
558
+ logging.info(f"Assigned connector 0 to outlet stream: {stream_name}")
559
+
560
+
561
+ def assign_generic_connectors(self, block_name, component_type, aspen, connections_data, connector_mappings):
562
+ """
563
+ Generic function for components with predefined connector mappings.
564
+ """
565
+ if component_type in connector_mappings:
566
+ mapping = connector_mappings[component_type]
567
+
568
+ # Access the ports of the component to find the connected streams
569
+ for port_label, connector_num in mapping.items():
570
+ port_node = aspen.Tree.FindNode(fr'\Data\Blocks\{block_name}\Ports\{port_label}')
571
+ if port_node is not None and port_node.Elements.Count > 0:
572
+ for element in port_node.Elements:
573
+ stream_name = element.Name
574
+ # Assign the connector number to the appropriate stream in the connection data
575
+ if stream_name in connections_data:
576
+ if 'source_component' in connections_data[stream_name] and \
577
+ connections_data[stream_name]['source_component'] == block_name:
578
+ connections_data[stream_name]['source_connector'] = connector_num
579
+ logging.debug(f"Assigned connector {connector_num} to source stream: {stream_name}")
580
+ elif 'target_component' in connections_data[stream_name] and \
581
+ connections_data[stream_name]['target_component'] == block_name:
582
+ connections_data[stream_name]['target_connector'] = connector_num
583
+ logging.debug(f"Assigned connector {connector_num} to target stream: {stream_name}")
584
+ else:
585
+ logging.warning(f"No connector mapping defined for component type {component_type}.")
586
+
587
+
588
+ def group_component(self, component_data, component_name):
589
+ """
590
+ Group the component based on its type into the correct group within components_data.
591
+
592
+ Parameters:
593
+ - component_data: The dictionary of component attributes.
594
+ - component_name: The name of the component.
595
+ """
596
+ # Determine the group for the component based on its type
597
+ group = None
598
+ for group_name, type_list in grouped_components.items():
599
+ if component_data['type'] in type_list:
600
+ group = group_name
601
+ break
602
+
603
+ # If the component doesn't belong to any predefined group, use its type name
604
+ if not group:
605
+ group = component_data['type']
606
+
607
+ # Initialize the group in the components_data dictionary if not already present
608
+ if group not in self.components_data:
609
+ self.components_data[group] = {}
610
+
611
+ # Store the component data in the appropriate group
612
+ self.components_data[group][component_name] = component_data
613
+
614
+
615
+ def parse_ambient_conditions(self):
616
+ """
617
+ Parses the ambient conditions from the Aspen model and stores them as class attributes.
618
+ Raises an error if Tamb or pamb are not found or are set to None.
619
+ """
620
+ try:
621
+ # Parse ambient temperature (Tamb)
622
+ temp_node = self.aspen.Tree.FindNode(r"\Data\Setup\Sim-Options\Input\REF_TEMP")
623
+ self.Tamb = convert_to_SI(
624
+ 'T',
625
+ temp_node.Value,
626
+ temp_node.UnitString
627
+ ) if temp_node is not None else None
628
+
629
+ if self.Tamb is None:
630
+ raise ValueError("Ambient temperature (Tamb) not found in the Aspen model. Please set it in Setup > Calculation Options.")
631
+
632
+ # Parse ambient pressure (pamb)
633
+ pres_node = self.aspen.Tree.FindNode(r"\Data\Setup\Sim-Options\Input\REF_PRES")
634
+ self.pamb = convert_to_SI(
635
+ 'p',
636
+ pres_node.Value,
637
+ pres_node.UnitString
638
+ ) if pres_node is not None else None
639
+
640
+ if self.pamb is None:
641
+ raise ValueError("Ambient pressure (pamb) not found in the Aspen model. Please set it in Setup > Calculation Options.")
642
+
643
+ logging.info(f"Parsed ambient conditions: Tamb = {self.Tamb} K, pamb = {self.pamb} Pa")
644
+
645
+ except Exception as e:
646
+ logging.error(f"Error parsing ambient conditions: {e}")
647
+ raise
648
+
649
+ def get_sorted_data(self):
650
+ """
651
+ Sorts the component and connection data alphabetically by name.
652
+
653
+ Returns:
654
+ dict: A dictionary containing sorted 'components', 'connections', and ambient conditions data.
655
+ """
656
+ sorted_components = {comp_name: self.components_data[comp_name] for comp_name in sorted(self.components_data)}
657
+ sorted_connections = {conn_name: self.connections_data[conn_name] for conn_name in sorted(self.connections_data)}
658
+ ambient_conditions = {
659
+ 'Tamb': self.Tamb,
660
+ 'Tamb_unit': fluid_property_data['T']['SI_unit'],
661
+ 'pamb': self.pamb,
662
+ 'pamb_unit': fluid_property_data['p']['SI_unit']
663
+ }
664
+ return {'components': sorted_components, 'connections': sorted_connections, 'ambient_conditions': ambient_conditions}
665
+
666
+
667
+ def write_to_json(self, output_path):
668
+ """
669
+ Writes the parsed and sorted data to a JSON file.
670
+
671
+ Parameters:
672
+ output_path (str): Path where the JSON file will be saved.
673
+ """
674
+ data = self.get_sorted_data()
675
+
676
+ try:
677
+ with open(output_path, 'w') as json_file:
678
+ json.dump(data, json_file, indent=4)
679
+ logging.info(f"Data successfully written to {output_path}")
680
+ except Exception as e:
681
+ logging.error(f"Failed to write data to JSON: {e}")
682
+ raise
683
+
684
+
685
+ def run_aspen(model_path, output_dir=None, split_physical_exergy=True):
686
+ """
687
+ Main function to process the Aspen model and return parsed data.
688
+ Optionally writes the parsed data to a JSON file.
689
+
690
+ Parameters:
691
+ model_path (str): Path to the Aspen model file.
692
+ output_dir (str): Optional path where the parsed data should be saved as a JSON file.
693
+ split_physical_exergy (bool): Flag to split physical exergy into thermal and mechanical components.
694
+
695
+ Returns:
696
+ dict: Parsed data in dictionary format.
697
+ """
698
+ if not os.path.exists(model_path):
699
+ error_msg = f"Model file not found at: {model_path}"
700
+ logging.error(error_msg)
701
+ raise FileNotFoundError(error_msg)
702
+
703
+ parser = AspenModelParser(model_path, split_physical_exergy=split_physical_exergy)
704
+
705
+ try:
706
+ parser.initialize_model()
707
+ parser.parse_model()
708
+ except Exception as e:
709
+ logging.error(f"An error occurred: {e}")
710
+ raise RuntimeError(f"An error occurred: {e}")
711
+
712
+ parsed_data = parser.get_sorted_data()
713
+
714
+ if output_dir is not None:
715
+ try:
716
+ parser.write_to_json(output_dir)
717
+ except Exception as e:
718
+ logging.error(f"Failed to write output file: {e}")
719
+ raise RuntimeError(f"Failed to write output file: {e}")
720
+
721
+ return parsed_data