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
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ebsilon Model Parser
|
|
3
|
+
|
|
4
|
+
This module defines the EbsilonModelParser class, which is used to parse Ebsilon models,
|
|
5
|
+
simulate them, extract data about components and connections, and write the data to a JSON file.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any
|
|
11
|
+
from typing import Dict
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from exerpy.functions import convert_to_SI
|
|
15
|
+
from exerpy.functions import fluid_property_data
|
|
16
|
+
|
|
17
|
+
from . import __ebsilon_available__
|
|
18
|
+
from . import is_ebsilon_available
|
|
19
|
+
from .utils import EpCalculationResultStatus2Stub
|
|
20
|
+
from .utils import EpFluidTypeStub
|
|
21
|
+
from .utils import EpGasTableStub
|
|
22
|
+
from .utils import EpSteamTableStub
|
|
23
|
+
from .utils import require_ebsilon
|
|
24
|
+
|
|
25
|
+
# Import Ebsilon classes if available
|
|
26
|
+
if __ebsilon_available__:
|
|
27
|
+
from EbsOpen import EpCalculationResultStatus2
|
|
28
|
+
from EbsOpen import EpFluidType
|
|
29
|
+
from EbsOpen import EpGasTable
|
|
30
|
+
from EbsOpen import EpSteamTable
|
|
31
|
+
from EbsOpen import EpSubstance
|
|
32
|
+
from win32com.client import Dispatch
|
|
33
|
+
else:
|
|
34
|
+
EpFluidType = EpFluidTypeStub
|
|
35
|
+
EpSteamTable = EpSteamTableStub
|
|
36
|
+
EpGasTable = EpGasTableStub
|
|
37
|
+
EpCalculationResultStatus2 = EpCalculationResultStatus2Stub
|
|
38
|
+
|
|
39
|
+
from .ebsilon_config import composition_params
|
|
40
|
+
from .ebsilon_config import connection_kinds
|
|
41
|
+
from .ebsilon_config import connector_mapping
|
|
42
|
+
from .ebsilon_config import ebs_objects
|
|
43
|
+
from .ebsilon_config import fluid_type_index
|
|
44
|
+
from .ebsilon_config import grouped_components
|
|
45
|
+
from .ebsilon_config import non_thermodynamic_unit_operators
|
|
46
|
+
from .ebsilon_config import two_phase_fluids_mapping
|
|
47
|
+
from .ebsilon_config import unit_id_to_string
|
|
48
|
+
|
|
49
|
+
# Configure logging to display info-level messages
|
|
50
|
+
logging.basicConfig(level=logging.ERROR)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class EbsilonModelParser:
|
|
54
|
+
"""
|
|
55
|
+
A class to parse Ebsilon models, simulate them, extract data, and write to JSON.
|
|
56
|
+
"""
|
|
57
|
+
def __init__(self, model_path: str, split_physical_exergy: bool = True):
|
|
58
|
+
"""
|
|
59
|
+
Initializes the parser with the given model path.
|
|
60
|
+
|
|
61
|
+
Parameters:
|
|
62
|
+
model_path (str): Path to the Ebsilon model file.
|
|
63
|
+
split_physical_exergy (bool): Flag to split physical exergy into thermal and mechanical components.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
RuntimeError: If Ebsilon is not available but is required for parsing.
|
|
67
|
+
"""
|
|
68
|
+
# Check if Ebsilon is available
|
|
69
|
+
if not is_ebsilon_available():
|
|
70
|
+
logging.warning(
|
|
71
|
+
"EbsilonModelParser initialized without Ebsilon support. "
|
|
72
|
+
"EBS environment variable is not set or EbsOpen could not be imported; "
|
|
73
|
+
"Ebsilon functionality will not be available."
|
|
74
|
+
)
|
|
75
|
+
# Raise an error since this parser specifically requires Ebsilon
|
|
76
|
+
raise RuntimeError(
|
|
77
|
+
"EbsilonModelParser requires Ebsilon to be available. "
|
|
78
|
+
"Please set the EBS environment variable to your Ebsilon installation path."
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
self.model_path = model_path
|
|
82
|
+
self.split_physical_exergy = split_physical_exergy
|
|
83
|
+
self.app = None # Ebsilon application instance
|
|
84
|
+
self.model = None # Opened Ebsilon model
|
|
85
|
+
self.oc = None # ObjectCaster for type casting
|
|
86
|
+
self.components_data: Dict[str, Dict[str, Dict[str, Any]]] = {} # Dictionary to store component data
|
|
87
|
+
self.connections_data: Dict[str, Dict[str, Any]] = {} # Dictionary to store connection data
|
|
88
|
+
self.Tamb: Optional[float] = None # Ambient temperature
|
|
89
|
+
self.pamb: Optional[float] = None # Ambient pressure
|
|
90
|
+
|
|
91
|
+
@require_ebsilon
|
|
92
|
+
def initialize_model(self):
|
|
93
|
+
"""
|
|
94
|
+
Initializes the Ebsilon application and opens the specified model.
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
FileNotFoundError: If the model file cannot be opened.
|
|
98
|
+
RuntimeError: If the COM server cannot be started or ObjectCaster cannot be obtained.
|
|
99
|
+
"""
|
|
100
|
+
# 1) start the COM server
|
|
101
|
+
try:
|
|
102
|
+
self.app = Dispatch("EbsOpen.Application")
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logging.error(f"Failed to start Ebsilon COM server: {e}")
|
|
105
|
+
raise RuntimeError(f"Could not start Ebsilon COM server: {e}")
|
|
106
|
+
|
|
107
|
+
# 2) try to open the .ebs model
|
|
108
|
+
try:
|
|
109
|
+
self.model = self.app.Open(self.model_path)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logging.error(f"Failed to open model file: {e}")
|
|
112
|
+
raise FileNotFoundError(f"File not found at: {self.model_path}") from e
|
|
113
|
+
|
|
114
|
+
# 3) grab the ObjectCaster
|
|
115
|
+
try:
|
|
116
|
+
self.oc = self.app.ObjectCaster
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logging.error(f"Failed to obtain ObjectCaster: {e}")
|
|
119
|
+
raise RuntimeError(f"Could not get ObjectCaster: {e}")
|
|
120
|
+
|
|
121
|
+
logging.info(f"Model opened successfully: {self.model_path}")
|
|
122
|
+
|
|
123
|
+
@require_ebsilon
|
|
124
|
+
def simulate_model(self):
|
|
125
|
+
"""
|
|
126
|
+
Simulates the Ebsilon model and logs any calculation errors.
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
Exception: If model simulation fails.
|
|
130
|
+
"""
|
|
131
|
+
try:
|
|
132
|
+
# Prepare to collect calculation errors
|
|
133
|
+
calc_errors = self.model.CalculationErrors
|
|
134
|
+
# Run the simulation
|
|
135
|
+
self.model.SimulateNew()
|
|
136
|
+
error_count = calc_errors.Count
|
|
137
|
+
logging.warning(f"Simulation has {error_count} warning(s).")
|
|
138
|
+
# Log each error if any exist
|
|
139
|
+
if error_count > 0:
|
|
140
|
+
for i in range(1, error_count + 1):
|
|
141
|
+
error = calc_errors.Item(i)
|
|
142
|
+
logging.warning(f"Warning {i}: {error.Description}")
|
|
143
|
+
except Exception as e:
|
|
144
|
+
logging.error(f"Failed during simulation: {e}")
|
|
145
|
+
raise
|
|
146
|
+
|
|
147
|
+
@require_ebsilon
|
|
148
|
+
def parse_model(self):
|
|
149
|
+
"""
|
|
150
|
+
Parses all objects in the Ebsilon model to extract component and connection data.
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
ValueError: If ambient conditions are not set.
|
|
154
|
+
Exception: If model parsing fails.
|
|
155
|
+
"""
|
|
156
|
+
try:
|
|
157
|
+
total_objects = self.model.Objects.Count
|
|
158
|
+
logging.info(f"Parsing {total_objects} objects from the model")
|
|
159
|
+
# Iterate over all objects in the model and select the components
|
|
160
|
+
for j in range(1, total_objects + 1):
|
|
161
|
+
obj = self.model.Objects.Item(j)
|
|
162
|
+
# Check if the object is a component (epObjectKindComp = 10)
|
|
163
|
+
if obj.IsKindOf(10):
|
|
164
|
+
self.parse_component(obj)
|
|
165
|
+
|
|
166
|
+
# After parsing all components, check if Tamb and pamb have been set
|
|
167
|
+
if self.Tamb is None or self.pamb is None:
|
|
168
|
+
error_msg = (
|
|
169
|
+
"Ambient temperature (Tamb) and/or ambient pressure (pamb) have not been set.\n"
|
|
170
|
+
"Please ensure that your Ebsilon model includes component(s) of type 46 (Measuring Point) "
|
|
171
|
+
"with a setting for the Ambient Temperature and the Ambient Pressure in MEASM."
|
|
172
|
+
)
|
|
173
|
+
logging.error(error_msg)
|
|
174
|
+
raise ValueError(error_msg)
|
|
175
|
+
|
|
176
|
+
# Iterate over all objects in the model and select the connections
|
|
177
|
+
for j in range(1, total_objects + 1):
|
|
178
|
+
obj = self.model.Objects.Item(j)
|
|
179
|
+
# Check if the object is a pipe (epObjectKindPipe = 16)
|
|
180
|
+
if obj.IsKindOf(16):
|
|
181
|
+
self.parse_connection(obj)
|
|
182
|
+
|
|
183
|
+
except Exception as e:
|
|
184
|
+
logging.error(f"Error while parsing the model: {e}")
|
|
185
|
+
raise
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@require_ebsilon
|
|
189
|
+
def parse_connection(self, obj: Any):
|
|
190
|
+
"""
|
|
191
|
+
Parses the connections (pipes) associated with a component.
|
|
192
|
+
|
|
193
|
+
Parameters:
|
|
194
|
+
obj: The Ebsilon component object whose connections are to be parsed.
|
|
195
|
+
"""
|
|
196
|
+
from .ebsilon_functions import calc_eM
|
|
197
|
+
from .ebsilon_functions import calc_eT
|
|
198
|
+
|
|
199
|
+
# Cast the pipe to the correct type
|
|
200
|
+
pipe_cast = self.oc.CastToPipe(obj)
|
|
201
|
+
|
|
202
|
+
# Define fluid types that are considered non-material or non-energetic
|
|
203
|
+
non_material_fluids = {5, 6, 9, 10, 13} # Scheduled, Actual, Electric, Shaft, Logic
|
|
204
|
+
non_energetic_fluids = {5, 6} # Scheduled, Actual
|
|
205
|
+
power_fluids = {9, 10} # Electric, Shaft
|
|
206
|
+
logic_fluids = 13 # Logic "fluids" for heat and power flows
|
|
207
|
+
heat_components = {5, 15, 16, 35} # Components that handle with heat flows as input or output
|
|
208
|
+
power_components = {31} # Power-summerized with power flows ONLY as output
|
|
209
|
+
|
|
210
|
+
# ALL EBSILON CONNECTIONS
|
|
211
|
+
# Initialize connection data with the common fields
|
|
212
|
+
connection_data = {
|
|
213
|
+
'name': pipe_cast.Name,
|
|
214
|
+
'kind': "other", # it will be changed later ("material", "heat", "power") according to the fluid type
|
|
215
|
+
'source_component': None,
|
|
216
|
+
'source_component_type': None,
|
|
217
|
+
'source_connector': None,
|
|
218
|
+
'target_component': None,
|
|
219
|
+
'target_component_type': None,
|
|
220
|
+
'target_connector': None,
|
|
221
|
+
'fluid_type': fluid_type_index.get(pipe_cast.FluidType, "Unknown"),
|
|
222
|
+
'fluid_type_id': pipe_cast.FluidType,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# Check if the connection is is not in non-energetic fluids
|
|
226
|
+
if (pipe_cast.Kind - 1000) not in non_energetic_fluids:
|
|
227
|
+
# Get the components at both ends of the pipe
|
|
228
|
+
comp0 = pipe_cast.Comp(0) if pipe_cast.HasComp(0) else None
|
|
229
|
+
comp1 = pipe_cast.Comp(1) if pipe_cast.HasComp(1) else None
|
|
230
|
+
# Get the connectors (links) at both ends of the pipe
|
|
231
|
+
link0 = pipe_cast.Link(0) if pipe_cast.HasComp(0) else None
|
|
232
|
+
link1 = pipe_cast.Link(1) if pipe_cast.HasComp(1) else None
|
|
233
|
+
|
|
234
|
+
# GENERAL INFORMATION
|
|
235
|
+
connection_data.update({
|
|
236
|
+
'source_component': comp0.Name if comp0 else None,
|
|
237
|
+
'source_component_type': (comp0.Kind - 10000) if comp0 else None,
|
|
238
|
+
'source_connector': link0.Index if link0 else None,
|
|
239
|
+
'target_component': comp1.Name if comp1 else None,
|
|
240
|
+
'target_component_type': (comp1.Kind - 10000) if comp1 else None,
|
|
241
|
+
'target_connector': link1.Index if link1 else None,
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
# MATERIAL CONNECTIONS
|
|
245
|
+
if (pipe_cast.Kind - 1000) not in non_material_fluids:
|
|
246
|
+
# Retrieve all data and convert them in SI units
|
|
247
|
+
connection_data.update({
|
|
248
|
+
'kind': 'material',
|
|
249
|
+
'm': (
|
|
250
|
+
convert_to_SI(
|
|
251
|
+
'm',
|
|
252
|
+
pipe_cast.M.Value,
|
|
253
|
+
unit_id_to_string.get(pipe_cast.M.Dimension, "Unknown")
|
|
254
|
+
) if hasattr(pipe_cast, 'M') and pipe_cast.M.Value is not None else None
|
|
255
|
+
),
|
|
256
|
+
'm_unit': fluid_property_data['m']['SI_unit'],
|
|
257
|
+
|
|
258
|
+
'T': (
|
|
259
|
+
convert_to_SI(
|
|
260
|
+
'T',
|
|
261
|
+
pipe_cast.T.Value,
|
|
262
|
+
unit_id_to_string.get(pipe_cast.T.Dimension, "Unknown")
|
|
263
|
+
) if hasattr(pipe_cast, 'T') and pipe_cast.T.Value is not None else None
|
|
264
|
+
),
|
|
265
|
+
'T_unit': fluid_property_data['T']['SI_unit'],
|
|
266
|
+
|
|
267
|
+
'p': (
|
|
268
|
+
convert_to_SI(
|
|
269
|
+
'p',
|
|
270
|
+
pipe_cast.P.Value,
|
|
271
|
+
unit_id_to_string.get(pipe_cast.P.Dimension, "Unknown")
|
|
272
|
+
) if hasattr(pipe_cast, 'P') and pipe_cast.P.Value is not None else None
|
|
273
|
+
),
|
|
274
|
+
'p_unit': fluid_property_data['p']['SI_unit'],
|
|
275
|
+
|
|
276
|
+
'h': (
|
|
277
|
+
convert_to_SI(
|
|
278
|
+
'h',
|
|
279
|
+
pipe_cast.H.Value,
|
|
280
|
+
unit_id_to_string.get(pipe_cast.H.Dimension, "Unknown")
|
|
281
|
+
) if hasattr(pipe_cast, 'H') and pipe_cast.H.Value is not None else None
|
|
282
|
+
),
|
|
283
|
+
'h_unit': fluid_property_data['h']['SI_unit'],
|
|
284
|
+
|
|
285
|
+
's': (
|
|
286
|
+
convert_to_SI(
|
|
287
|
+
's',
|
|
288
|
+
pipe_cast.S.Value,
|
|
289
|
+
unit_id_to_string.get(pipe_cast.S.Dimension, "Unknown")
|
|
290
|
+
) if hasattr(pipe_cast, 'S') and pipe_cast.S.Value is not None else None
|
|
291
|
+
),
|
|
292
|
+
's_unit': fluid_property_data['s']['SI_unit'],
|
|
293
|
+
|
|
294
|
+
'e_PH': (
|
|
295
|
+
convert_to_SI(
|
|
296
|
+
'e',
|
|
297
|
+
pipe_cast.E.Value,
|
|
298
|
+
unit_id_to_string.get(pipe_cast.E.Dimension, "Unknown")
|
|
299
|
+
) if hasattr(pipe_cast, 'E') and pipe_cast.E.Value is not None else None
|
|
300
|
+
),
|
|
301
|
+
'e_PH_unit': fluid_property_data['e']['SI_unit'],
|
|
302
|
+
|
|
303
|
+
'x': (
|
|
304
|
+
convert_to_SI(
|
|
305
|
+
'x',
|
|
306
|
+
pipe_cast.X.Value,
|
|
307
|
+
unit_id_to_string.get(pipe_cast.X.Dimension, "Unknown")
|
|
308
|
+
) if hasattr(pipe_cast, 'X') and pipe_cast.X.Value is not None else None
|
|
309
|
+
),
|
|
310
|
+
'x_unit': fluid_property_data['x']['SI_unit'],
|
|
311
|
+
|
|
312
|
+
'VM': (
|
|
313
|
+
convert_to_SI(
|
|
314
|
+
'VM',
|
|
315
|
+
pipe_cast.VM.Value,
|
|
316
|
+
unit_id_to_string.get(pipe_cast.VM.Dimension, "Unknown")
|
|
317
|
+
) if hasattr(pipe_cast, 'VM') and pipe_cast.VM.Value is not None else None
|
|
318
|
+
),
|
|
319
|
+
'VM_unit': fluid_property_data['VM']['SI_unit'],
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
# Add the mechanical and thermal specific exergies unless the flag is set to False
|
|
323
|
+
if self.split_physical_exergy:
|
|
324
|
+
e_T_value = calc_eT(self.app, pipe_cast, connection_data['p'], self.Tamb, self.pamb)
|
|
325
|
+
e_M_value = calc_eM(self.app, pipe_cast, connection_data['p'], self.Tamb, self.pamb)
|
|
326
|
+
|
|
327
|
+
connection_data.update({
|
|
328
|
+
'e_T': e_T_value,
|
|
329
|
+
'e_T_unit': fluid_property_data['e']['SI_unit'],
|
|
330
|
+
'e_M': e_M_value,
|
|
331
|
+
'e_M_unit': fluid_property_data['e']['SI_unit']
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
# Handle mass composition logic for fluids
|
|
335
|
+
if fluid_type_index.get(pipe_cast.FluidType, "Unknown") in ['Steam', 'Water']:
|
|
336
|
+
connection_data['mass_composition'] = {'H2O': 1}
|
|
337
|
+
elif fluid_type_index.get(pipe_cast.FluidType, "Unknown") in ['2PhaseLiquid', '2PhaseGaseous']:
|
|
338
|
+
# Get the FMED value to determine the substance
|
|
339
|
+
fmed_value = pipe_cast.FMED.Value if hasattr(pipe_cast, 'FMED') else None
|
|
340
|
+
if fmed_value in two_phase_fluids_mapping.keys():
|
|
341
|
+
connection_data['mass_composition'] = two_phase_fluids_mapping[fmed_value]
|
|
342
|
+
else:
|
|
343
|
+
connection_data['mass_composition'] = {} # Default if no mapping found
|
|
344
|
+
logging.warning(f"FMED value {fmed_value} not found in fluid_composition_mapping. Please add it.")
|
|
345
|
+
else:
|
|
346
|
+
connection_data['mass_composition'] = {
|
|
347
|
+
param.lstrip('X'): getattr(pipe_cast, param).Value
|
|
348
|
+
for param in composition_params
|
|
349
|
+
if hasattr(pipe_cast, param) and getattr(pipe_cast, param).Value not in [0, None]
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
# HEAT AND POWER CONNECTIONS from Logic "fluids"
|
|
353
|
+
if (pipe_cast.Kind - 1000) == logic_fluids:
|
|
354
|
+
if (comp0 is not None and comp0.Kind is not None and comp0.Kind - 10000 in heat_components) or (comp1 is not None and comp1.Kind is not None and comp1.Kind - 10000 in heat_components):
|
|
355
|
+
connection_data.update({
|
|
356
|
+
'kind': "heat",
|
|
357
|
+
'energy_flow': convert_to_SI('heat', pipe_cast.Q.Value, unit_id_to_string.get(pipe_cast.Q.Dimension, "Unknown")) if hasattr(pipe_cast, 'Q') and pipe_cast.Q.Value is not None else None,
|
|
358
|
+
'energy_flow_unit': fluid_property_data['heat']['SI_unit'],
|
|
359
|
+
'E': None,
|
|
360
|
+
'E_unit': fluid_property_data['power']['SI_unit'],
|
|
361
|
+
})
|
|
362
|
+
if (comp0 is not None and comp0.Kind is not None and comp0.Kind - 10000 in power_components):
|
|
363
|
+
connection_data.update({
|
|
364
|
+
'kind': "power",
|
|
365
|
+
'energy_flow': convert_to_SI('power', pipe_cast.Q.Value, unit_id_to_string.get(pipe_cast.Q.Dimension, "Unknown")) if hasattr(pipe_cast, 'Q') and pipe_cast.Q.Value is not None else None,
|
|
366
|
+
'energy_flow_unit': fluid_property_data['power']['SI_unit'],
|
|
367
|
+
'E': convert_to_SI('power', pipe_cast.Q.Value, unit_id_to_string.get(pipe_cast.Q.Dimension, "Unknown")) if hasattr(pipe_cast, 'Q') and pipe_cast.Q.Value is not None else None,
|
|
368
|
+
'E_unit': fluid_property_data['power']['SI_unit'],
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
# POWER CONNECTIONS from power "fluids"
|
|
372
|
+
if (pipe_cast.Kind - 1000) in power_fluids:
|
|
373
|
+
connection_data.update({
|
|
374
|
+
'kind': "power",
|
|
375
|
+
'energy_flow': convert_to_SI('power', pipe_cast.Q.Value, unit_id_to_string.get(pipe_cast.Q.Dimension, "Unknown")) if hasattr(pipe_cast, 'Q') and pipe_cast.Q.Value is not None else None,
|
|
376
|
+
'energy_flow_unit': fluid_property_data['power']['SI_unit'],
|
|
377
|
+
'E': convert_to_SI('power', pipe_cast.Q.Value, unit_id_to_string.get(pipe_cast.Q.Dimension, "Unknown")) if hasattr(pipe_cast, 'Q') and pipe_cast.Q.Value is not None else None,
|
|
378
|
+
'E_unit': fluid_property_data['power']['SI_unit'],
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
# Convert the connector numbers to selected standard values for each component
|
|
382
|
+
if connection_data['source_component_type'] in connector_mapping and connection_data['source_connector'] in connector_mapping[connection_data['source_component_type']]:
|
|
383
|
+
connection_data['source_connector'] = connector_mapping[connection_data['source_component_type']][connection_data['source_connector']]
|
|
384
|
+
|
|
385
|
+
if connection_data['target_component_type'] in connector_mapping and connection_data['target_connector'] in connector_mapping[connection_data['target_component_type']]:
|
|
386
|
+
connection_data['target_connector'] = connector_mapping[connection_data['target_component_type']][connection_data['target_connector']]
|
|
387
|
+
|
|
388
|
+
# Store the connection data
|
|
389
|
+
self.connections_data[obj.Name] = connection_data
|
|
390
|
+
|
|
391
|
+
else:
|
|
392
|
+
logging.info(f"Skipping non-energetic connection: {pipe_cast.Name}")
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
@require_ebsilon
|
|
396
|
+
def parse_component(self, obj: Any):
|
|
397
|
+
"""
|
|
398
|
+
Parses data from a component, including its type and various properties.
|
|
399
|
+
|
|
400
|
+
Parameters:
|
|
401
|
+
obj: The Ebsilon component object to parse.
|
|
402
|
+
"""
|
|
403
|
+
# Cast the component to get its type index
|
|
404
|
+
comp_cast = self.oc.CastToComp(obj)
|
|
405
|
+
type_index = (comp_cast.Kind - 10000)
|
|
406
|
+
|
|
407
|
+
# Dynamically call the specific CastToCompX method based on type_index
|
|
408
|
+
cast_method_name = f"CastToComp{type_index}"
|
|
409
|
+
|
|
410
|
+
# Check if the method exists and call it, otherwise fallback to general casting
|
|
411
|
+
if hasattr(self.oc, cast_method_name):
|
|
412
|
+
comp_cast = getattr(self.oc, cast_method_name)(obj)
|
|
413
|
+
logging.info(f"Using method {cast_method_name} to cast the component.")
|
|
414
|
+
else:
|
|
415
|
+
logging.warning(f"No specific cast method for type_index {type_index}, using generic CastToComp.")
|
|
416
|
+
comp_cast = self.oc.CastToComp(obj)
|
|
417
|
+
|
|
418
|
+
# Get the human-readable type name of the component
|
|
419
|
+
type_name = ebs_objects.get(type_index, f"Unknown Type {type_index}")
|
|
420
|
+
|
|
421
|
+
# Exclude non-thermodynamic unit operators
|
|
422
|
+
if type_index not in non_thermodynamic_unit_operators:
|
|
423
|
+
# Collect component data
|
|
424
|
+
component_data = {
|
|
425
|
+
'name': comp_cast.Name,
|
|
426
|
+
'type': type_name,
|
|
427
|
+
'type_index': type_index,
|
|
428
|
+
'eta_s': (
|
|
429
|
+
comp_cast.ETAIN.Value
|
|
430
|
+
if hasattr(comp_cast, 'ETAIN') and comp_cast.ETAIN.Value is not None else None
|
|
431
|
+
),
|
|
432
|
+
'eta_mech': (
|
|
433
|
+
comp_cast.ETAMN.Value
|
|
434
|
+
if hasattr(comp_cast, 'ETAMN') and comp_cast.ETAMN.Value is not None else None
|
|
435
|
+
),
|
|
436
|
+
'eta_el': (
|
|
437
|
+
comp_cast.ETAEN.Value
|
|
438
|
+
if hasattr(comp_cast, 'ETAEN') and comp_cast.ETAEN.Value is not None else None
|
|
439
|
+
),
|
|
440
|
+
'eta_cc': (
|
|
441
|
+
comp_cast.ETAB.Value
|
|
442
|
+
if hasattr(comp_cast, 'ETAB') and comp_cast.ETAB.Value is not None else None
|
|
443
|
+
),
|
|
444
|
+
'lamb': (
|
|
445
|
+
comp_cast.ALAMN.Value
|
|
446
|
+
if hasattr(comp_cast, 'ALAMN') and comp_cast.ALAMN.Value is not None else None
|
|
447
|
+
),
|
|
448
|
+
'Q': (
|
|
449
|
+
convert_to_SI(
|
|
450
|
+
'heat',
|
|
451
|
+
comp_cast.QT.Value,
|
|
452
|
+
unit_id_to_string.get(comp_cast.QT.Dimension, "Unknown")
|
|
453
|
+
) if hasattr(comp_cast, 'QT') and comp_cast.QT.Value is not None else None
|
|
454
|
+
),
|
|
455
|
+
'Q_unit': fluid_property_data['heat']['SI_unit'],
|
|
456
|
+
'P': (
|
|
457
|
+
convert_to_SI(
|
|
458
|
+
'power',
|
|
459
|
+
comp_cast.QSHAFT.Value,
|
|
460
|
+
unit_id_to_string.get(comp_cast.QSHAFT.Dimension, "Unknown")
|
|
461
|
+
) if hasattr(comp_cast, 'QSHAFT') and comp_cast.QSHAFT.Value is not None else None
|
|
462
|
+
),
|
|
463
|
+
'P_unit': fluid_property_data['power']['SI_unit'],
|
|
464
|
+
'kA': (
|
|
465
|
+
comp_cast.KA.Value
|
|
466
|
+
if hasattr(comp_cast, 'KA') and comp_cast.KA.Value is not None else None
|
|
467
|
+
),
|
|
468
|
+
'kA_unit': fluid_property_data['kA']['SI_unit'],
|
|
469
|
+
'A': (
|
|
470
|
+
comp_cast.A.Value
|
|
471
|
+
if hasattr(comp_cast, 'A') and comp_cast.A.Value is not None else None
|
|
472
|
+
),
|
|
473
|
+
'A_unit': fluid_property_data['A']['SI_unit'],
|
|
474
|
+
'mass_flow_1': (
|
|
475
|
+
convert_to_SI(
|
|
476
|
+
'm',
|
|
477
|
+
comp_cast.M1N.Value,
|
|
478
|
+
unit_id_to_string.get(comp_cast.M1N.Dimension, "Unknown")
|
|
479
|
+
) if hasattr(comp_cast, 'M1N') and comp_cast.M1N.Value is not None else None
|
|
480
|
+
),
|
|
481
|
+
'mass_flow_1_unit': fluid_property_data['m']['SI_unit'],
|
|
482
|
+
'mass_flow_3': (
|
|
483
|
+
convert_to_SI(
|
|
484
|
+
'm',
|
|
485
|
+
comp_cast.M3N.Value,
|
|
486
|
+
unit_id_to_string.get(comp_cast.M3N.Dimension, "Unknown")
|
|
487
|
+
) if hasattr(comp_cast, 'M3N') and comp_cast.M3N.Value is not None else None
|
|
488
|
+
),
|
|
489
|
+
'mass_flow_3_unit': fluid_property_data['m']['SI_unit'],
|
|
490
|
+
'energy_flow_1': (
|
|
491
|
+
convert_to_SI(
|
|
492
|
+
'heat',
|
|
493
|
+
comp_cast.Q1N.Value,
|
|
494
|
+
unit_id_to_string.get(comp_cast.Q1N.Dimension, "Unknown")
|
|
495
|
+
) if hasattr(comp_cast, 'Q1N') and comp_cast.Q1N.Value is not None else None
|
|
496
|
+
),
|
|
497
|
+
'mass_flow_1_unit': fluid_property_data['heat']['SI_unit'],
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
# Determine the group for the component based on its type
|
|
501
|
+
group = None
|
|
502
|
+
for group_name, type_list in grouped_components.items():
|
|
503
|
+
if type_index in type_list:
|
|
504
|
+
group = group_name
|
|
505
|
+
break
|
|
506
|
+
|
|
507
|
+
# If the component type doesn't belong to any predefined group, use its type name
|
|
508
|
+
if not group:
|
|
509
|
+
group = type_name
|
|
510
|
+
|
|
511
|
+
# Initialize the group in the components_data dictionary if not already present
|
|
512
|
+
if group not in self.components_data:
|
|
513
|
+
self.components_data[group] = {}
|
|
514
|
+
|
|
515
|
+
# Store the component data using the component's name as the key
|
|
516
|
+
self.components_data[group][comp_cast.Name] = component_data
|
|
517
|
+
|
|
518
|
+
# For components of type 46, set ambient temperature and pressure
|
|
519
|
+
elif type_index == 46:
|
|
520
|
+
comp46 = self.oc.CastToComp46(obj)
|
|
521
|
+
if comp46.FTYP.Value == 26:
|
|
522
|
+
self.Tamb = convert_to_SI('T', comp46.MEASM.Value, unit_id_to_string.get(comp46.MEASM.Dimension, "Unknown"))
|
|
523
|
+
logging.info(f"Set ambient temperature (Tamb) to {self.Tamb} K from component {comp_cast.Name}")
|
|
524
|
+
elif comp46.FTYP.Value == 13:
|
|
525
|
+
self.pamb = convert_to_SI('p', comp46.MEASM.Value, unit_id_to_string.get(comp46.MEASM.Dimension, "Unknown"))
|
|
526
|
+
logging.info(f"Set ambient pressure (pamb) to {self.pamb} Pa from component {comp_cast.Name}")
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def get_sorted_data(self) -> Dict[str, Any]:
|
|
530
|
+
"""
|
|
531
|
+
Sorts the component and connection data alphabetically by name.
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
dict: A dictionary containing sorted 'components', 'connections', and ambient conditions data.
|
|
535
|
+
"""
|
|
536
|
+
# Sort components within each group by component name
|
|
537
|
+
sorted_components = {
|
|
538
|
+
comp_type: dict(sorted(self.components_data[comp_type].items()))
|
|
539
|
+
for comp_type in sorted(self.components_data)
|
|
540
|
+
}
|
|
541
|
+
# Sort connections by their names
|
|
542
|
+
sorted_connections = dict(sorted(self.connections_data.items()))
|
|
543
|
+
# Return data including ambient conditions
|
|
544
|
+
return {
|
|
545
|
+
'components': sorted_components,
|
|
546
|
+
'connections': sorted_connections,
|
|
547
|
+
'ambient_conditions': {
|
|
548
|
+
'Tamb': self.Tamb,
|
|
549
|
+
'Tamb_unit': fluid_property_data['T']['SI_unit'],
|
|
550
|
+
'pamb': self.pamb,
|
|
551
|
+
'pamb_unit': fluid_property_data['p']['SI_unit']
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def write_to_json(self, output_path: str):
|
|
557
|
+
"""
|
|
558
|
+
Writes the parsed and sorted data to a JSON file.
|
|
559
|
+
|
|
560
|
+
Parameters:
|
|
561
|
+
output_path (str): Path where the JSON file will be saved.
|
|
562
|
+
|
|
563
|
+
Raises:
|
|
564
|
+
Exception: If writing to JSON fails.
|
|
565
|
+
"""
|
|
566
|
+
data = self.get_sorted_data()
|
|
567
|
+
try:
|
|
568
|
+
# Write the data to a JSON file with indentation for readability
|
|
569
|
+
with open(output_path, 'w') as json_file:
|
|
570
|
+
json.dump(data, json_file, indent=4)
|
|
571
|
+
logging.info(f"Data successfully written to {output_path}")
|
|
572
|
+
except Exception as e:
|
|
573
|
+
logging.error(f"Failed to write data to JSON: {e}")
|
|
574
|
+
raise
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def run_ebsilon(model_path: str, output_dir: Optional[str] = None, split_physical_exergy: bool = True) -> Dict[str, Any]:
|
|
578
|
+
"""
|
|
579
|
+
Main function to process the Ebsilon model and return parsed data.
|
|
580
|
+
Optionally writes the parsed data to a JSON file.
|
|
581
|
+
|
|
582
|
+
Parameters:
|
|
583
|
+
model_path (str): Path to the Ebsilon model file.
|
|
584
|
+
output_dir (str): Optional path where the parsed data should be saved as a JSON file.
|
|
585
|
+
split_physical_exergy (bool): Flag to split physical exergy into thermal and mechanical components.
|
|
586
|
+
|
|
587
|
+
Returns:
|
|
588
|
+
dict: Parsed data in dictionary format.
|
|
589
|
+
|
|
590
|
+
Raises:
|
|
591
|
+
FileNotFoundError: If the model file is not found at the specified path.
|
|
592
|
+
RuntimeError: For any error during model initialization, simulation, parsing, or writing.
|
|
593
|
+
"""
|
|
594
|
+
# Check if Ebsilon is available
|
|
595
|
+
if not is_ebsilon_available():
|
|
596
|
+
raise RuntimeError(
|
|
597
|
+
"Ebsilon functionality is required for running this function. "
|
|
598
|
+
"Please set the EBS environment variable to your Ebsilon installation path."
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
# Check if the model file exists at the specified path
|
|
602
|
+
if not os.path.exists(model_path):
|
|
603
|
+
error_msg = f"Model file not found at: {model_path}"
|
|
604
|
+
logging.error(error_msg)
|
|
605
|
+
raise FileNotFoundError(error_msg)
|
|
606
|
+
|
|
607
|
+
# Initialize the Ebsilon model parser with the model file path
|
|
608
|
+
try:
|
|
609
|
+
parser = EbsilonModelParser(model_path, split_physical_exergy=split_physical_exergy)
|
|
610
|
+
except RuntimeError as e:
|
|
611
|
+
# This will catch the RuntimeError raised in __init__ if Ebsilon is not available
|
|
612
|
+
logging.error(f"Failed to initialize EbsilonModelParser: {e}")
|
|
613
|
+
raise
|
|
614
|
+
|
|
615
|
+
try:
|
|
616
|
+
# Initialize the Ebsilon model within the parser
|
|
617
|
+
parser.initialize_model()
|
|
618
|
+
except FileNotFoundError:
|
|
619
|
+
# allow an invalid/corrupt‐model file to bubble up as FileNotFoundError
|
|
620
|
+
raise
|
|
621
|
+
except Exception as e:
|
|
622
|
+
# other COM/server errors should still be RuntimeErrors
|
|
623
|
+
error_msg = f"File not found: {model_path}"
|
|
624
|
+
logging.error(error_msg)
|
|
625
|
+
raise RuntimeError(error_msg)
|
|
626
|
+
|
|
627
|
+
try:
|
|
628
|
+
# Simulate the Ebsilon model
|
|
629
|
+
parser.simulate_model()
|
|
630
|
+
except Exception as e:
|
|
631
|
+
# Log and raise an error if something goes wrong during simulation
|
|
632
|
+
error_msg = f"An error occurred during model simulation: {e}"
|
|
633
|
+
logging.error(error_msg)
|
|
634
|
+
raise RuntimeError(error_msg)
|
|
635
|
+
|
|
636
|
+
try:
|
|
637
|
+
# Parse data from the simulated model
|
|
638
|
+
parser.parse_model()
|
|
639
|
+
except Exception as e:
|
|
640
|
+
# Log and raise an error if something goes wrong during parsing
|
|
641
|
+
error_msg = f"An error occurred during model parsing: {e}"
|
|
642
|
+
logging.error(error_msg)
|
|
643
|
+
raise RuntimeError(error_msg)
|
|
644
|
+
|
|
645
|
+
# Get the parsed and sorted data
|
|
646
|
+
parsed_data = parser.get_sorted_data()
|
|
647
|
+
|
|
648
|
+
if output_dir is not None:
|
|
649
|
+
try:
|
|
650
|
+
# Write the parsed data to the JSON file
|
|
651
|
+
parser.write_to_json(output_dir)
|
|
652
|
+
logging.info(f"Data successfully written to {output_dir}")
|
|
653
|
+
except Exception as e:
|
|
654
|
+
# Log and raise an error if something goes wrong while writing the output file
|
|
655
|
+
error_msg = f"An error occurred while writing the output file: {e}"
|
|
656
|
+
logging.error(error_msg)
|
|
657
|
+
raise RuntimeError(error_msg)
|
|
658
|
+
|
|
659
|
+
# Return the parsed data as a dictionary (not as a JSON string)
|
|
660
|
+
return parsed_data
|