exerpy 0.0.2__py3-none-any.whl → 0.0.4__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 +2 -4
- exerpy/analyses.py +849 -304
- exerpy/components/__init__.py +3 -0
- exerpy/components/combustion/base.py +53 -35
- exerpy/components/component.py +8 -8
- exerpy/components/heat_exchanger/base.py +188 -121
- exerpy/components/heat_exchanger/condenser.py +98 -62
- exerpy/components/heat_exchanger/simple.py +237 -137
- exerpy/components/heat_exchanger/steam_generator.py +46 -41
- exerpy/components/helpers/cycle_closer.py +61 -34
- exerpy/components/helpers/power_bus.py +117 -0
- exerpy/components/nodes/deaerator.py +176 -58
- exerpy/components/nodes/drum.py +50 -39
- exerpy/components/nodes/flash_tank.py +218 -43
- exerpy/components/nodes/mixer.py +249 -69
- exerpy/components/nodes/splitter.py +173 -0
- exerpy/components/nodes/storage.py +130 -0
- exerpy/components/piping/valve.py +311 -115
- exerpy/components/power_machines/generator.py +105 -38
- exerpy/components/power_machines/motor.py +111 -39
- exerpy/components/turbomachinery/compressor.py +214 -68
- exerpy/components/turbomachinery/pump.py +215 -68
- exerpy/components/turbomachinery/turbine.py +182 -74
- exerpy/cost_estimation/__init__.py +65 -0
- exerpy/cost_estimation/turton.py +1260 -0
- exerpy/data/cost_correlations/cepci_index.json +135 -0
- exerpy/data/cost_correlations/component_mapping.json +450 -0
- exerpy/data/cost_correlations/material_factors.json +428 -0
- exerpy/data/cost_correlations/pressure_factors.json +206 -0
- exerpy/data/cost_correlations/turton2008.json +726 -0
- exerpy/data/cost_correlations/turton2008_design_analysis_synthesis_components_tables.pdf +0 -0
- exerpy/data/cost_correlations/turton2008_design_analysis_synthesis_components_theory.pdf +0 -0
- exerpy/functions.py +389 -264
- exerpy/parser/from_aspen/aspen_config.py +57 -48
- exerpy/parser/from_aspen/aspen_parser.py +373 -280
- exerpy/parser/from_ebsilon/__init__.py +2 -2
- exerpy/parser/from_ebsilon/check_ebs_path.py +15 -19
- exerpy/parser/from_ebsilon/ebsilon_config.py +328 -226
- exerpy/parser/from_ebsilon/ebsilon_functions.py +205 -38
- exerpy/parser/from_ebsilon/ebsilon_parser.py +392 -255
- exerpy/parser/from_ebsilon/utils.py +16 -11
- exerpy/parser/from_tespy/tespy_config.py +33 -1
- exerpy/parser/from_tespy/tespy_parser.py +151 -0
- {exerpy-0.0.2.dist-info → exerpy-0.0.4.dist-info}/METADATA +43 -2
- exerpy-0.0.4.dist-info/RECORD +57 -0
- exerpy-0.0.2.dist-info/RECORD +0 -44
- {exerpy-0.0.2.dist-info → exerpy-0.0.4.dist-info}/WHEEL +0 -0
- {exerpy-0.0.2.dist-info → exerpy-0.0.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,31 +4,21 @@ Ebsilon Model Parser
|
|
|
4
4
|
This module defines the EbsilonModelParser class, which is used to parse Ebsilon models,
|
|
5
5
|
simulate them, extract data about components and connections, and write the data to a JSON file.
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
import json
|
|
8
9
|
import logging
|
|
9
10
|
import os
|
|
10
11
|
from typing import Any
|
|
11
|
-
from typing import Dict
|
|
12
|
-
from typing import Optional
|
|
13
12
|
|
|
14
|
-
from exerpy.functions import convert_to_SI
|
|
15
|
-
from exerpy.functions import fluid_property_data
|
|
13
|
+
from exerpy.functions import convert_to_SI, fluid_property_data
|
|
16
14
|
|
|
17
|
-
from . import __ebsilon_available__
|
|
18
|
-
from . import
|
|
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
|
|
15
|
+
from . import __ebsilon_available__, is_ebsilon_available
|
|
16
|
+
from .ebsilon_functions import calc_eph_from_min
|
|
17
|
+
from .utils import EpCalculationResultStatus2Stub, EpFluidTypeStub, EpGasTableStub, EpSteamTableStub, require_ebsilon
|
|
24
18
|
|
|
25
19
|
# Import Ebsilon classes if available
|
|
26
20
|
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
|
|
21
|
+
from EbsOpen import EpCalculationResultStatus2, EpFluidType, EpGasTable, EpSteamTable
|
|
32
22
|
from win32com.client import Dispatch
|
|
33
23
|
else:
|
|
34
24
|
EpFluidType = EpFluidTypeStub
|
|
@@ -36,15 +26,16 @@ else:
|
|
|
36
26
|
EpGasTable = EpGasTableStub
|
|
37
27
|
EpCalculationResultStatus2 = EpCalculationResultStatus2Stub
|
|
38
28
|
|
|
39
|
-
from .ebsilon_config import
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
29
|
+
from .ebsilon_config import (
|
|
30
|
+
composition_params,
|
|
31
|
+
connector_mapping,
|
|
32
|
+
ebs_objects,
|
|
33
|
+
fluid_type_index,
|
|
34
|
+
grouped_components,
|
|
35
|
+
non_thermodynamic_unit_operators,
|
|
36
|
+
two_phase_fluids_mapping,
|
|
37
|
+
unit_id_to_string,
|
|
38
|
+
)
|
|
48
39
|
|
|
49
40
|
# Configure logging to display info-level messages
|
|
50
41
|
logging.basicConfig(level=logging.ERROR)
|
|
@@ -54,6 +45,7 @@ class EbsilonModelParser:
|
|
|
54
45
|
"""
|
|
55
46
|
A class to parse Ebsilon models, simulate them, extract data, and write to JSON.
|
|
56
47
|
"""
|
|
48
|
+
|
|
57
49
|
def __init__(self, model_path: str, split_physical_exergy: bool = True):
|
|
58
50
|
"""
|
|
59
51
|
Initializes the parser with the given model path.
|
|
@@ -83,10 +75,12 @@ class EbsilonModelParser:
|
|
|
83
75
|
self.app = None # Ebsilon application instance
|
|
84
76
|
self.model = None # Opened Ebsilon model
|
|
85
77
|
self.oc = None # ObjectCaster for type casting
|
|
86
|
-
self.components_data:
|
|
87
|
-
self.connections_data:
|
|
88
|
-
self.Tamb:
|
|
89
|
-
self.pamb:
|
|
78
|
+
self.components_data: dict[str, dict[str, dict[str, Any]]] = {} # Dictionary to store component data
|
|
79
|
+
self.connections_data: dict[str, dict[str, Any]] = {} # Dictionary to store connection data
|
|
80
|
+
self.Tamb: float | None = None # Ambient temperature
|
|
81
|
+
self.pamb: float | None = None # Ambient pressure
|
|
82
|
+
|
|
83
|
+
self._storages_to_postprocess: list[dict[str, Any]] = []
|
|
90
84
|
|
|
91
85
|
@require_ebsilon
|
|
92
86
|
def initialize_model(self):
|
|
@@ -103,21 +97,21 @@ class EbsilonModelParser:
|
|
|
103
97
|
except Exception as e:
|
|
104
98
|
logging.error(f"Failed to start Ebsilon COM server: {e}")
|
|
105
99
|
raise RuntimeError(f"Could not start Ebsilon COM server: {e}")
|
|
106
|
-
|
|
100
|
+
|
|
107
101
|
# 2) try to open the .ebs model
|
|
108
102
|
try:
|
|
109
103
|
self.model = self.app.Open(self.model_path)
|
|
110
104
|
except Exception as e:
|
|
111
105
|
logging.error(f"Failed to open model file: {e}")
|
|
112
106
|
raise FileNotFoundError(f"File not found at: {self.model_path}") from e
|
|
113
|
-
|
|
107
|
+
|
|
114
108
|
# 3) grab the ObjectCaster
|
|
115
109
|
try:
|
|
116
110
|
self.oc = self.app.ObjectCaster
|
|
117
111
|
except Exception as e:
|
|
118
112
|
logging.error(f"Failed to obtain ObjectCaster: {e}")
|
|
119
113
|
raise RuntimeError(f"Could not get ObjectCaster: {e}")
|
|
120
|
-
|
|
114
|
+
|
|
121
115
|
logging.info(f"Model opened successfully: {self.model_path}")
|
|
122
116
|
|
|
123
117
|
@require_ebsilon
|
|
@@ -180,11 +174,13 @@ class EbsilonModelParser:
|
|
|
180
174
|
if obj.IsKindOf(16):
|
|
181
175
|
self.parse_connection(obj)
|
|
182
176
|
|
|
177
|
+
# After parsing all components and connections, create storage connections
|
|
178
|
+
self._create_storage_connections()
|
|
179
|
+
|
|
183
180
|
except Exception as e:
|
|
184
181
|
logging.error(f"Error while parsing the model: {e}")
|
|
185
182
|
raise
|
|
186
183
|
|
|
187
|
-
|
|
188
184
|
@require_ebsilon
|
|
189
185
|
def parse_connection(self, obj: Any):
|
|
190
186
|
"""
|
|
@@ -193,8 +189,7 @@ class EbsilonModelParser:
|
|
|
193
189
|
Parameters:
|
|
194
190
|
obj: The Ebsilon component object whose connections are to be parsed.
|
|
195
191
|
"""
|
|
196
|
-
from .ebsilon_functions import calc_eM
|
|
197
|
-
from .ebsilon_functions import calc_eT
|
|
192
|
+
from .ebsilon_functions import calc_eM, calc_eT
|
|
198
193
|
|
|
199
194
|
# Cast the pipe to the correct type
|
|
200
195
|
pipe_cast = self.oc.CastToPipe(obj)
|
|
@@ -210,16 +205,16 @@ class EbsilonModelParser:
|
|
|
210
205
|
# ALL EBSILON CONNECTIONS
|
|
211
206
|
# Initialize connection data with the common fields
|
|
212
207
|
connection_data = {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
208
|
+
"name": pipe_cast.Name,
|
|
209
|
+
"kind": "other", # it will be changed later ("material", "heat", "power") according to the fluid type
|
|
210
|
+
"source_component": None,
|
|
211
|
+
"source_component_type": None,
|
|
212
|
+
"source_connector": None,
|
|
213
|
+
"target_component": None,
|
|
214
|
+
"target_component_type": None,
|
|
215
|
+
"target_connector": None,
|
|
216
|
+
"fluid_type": fluid_type_index.get(pipe_cast.FluidType, "Unknown"),
|
|
217
|
+
"fluid_type_id": pipe_cast.FluidType,
|
|
223
218
|
}
|
|
224
219
|
|
|
225
220
|
# Check if the connection is is not in non-energetic fluids
|
|
@@ -232,158 +227,233 @@ class EbsilonModelParser:
|
|
|
232
227
|
link1 = pipe_cast.Link(1) if pipe_cast.HasComp(1) else None
|
|
233
228
|
|
|
234
229
|
# GENERAL INFORMATION
|
|
235
|
-
connection_data.update(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
230
|
+
connection_data.update(
|
|
231
|
+
{
|
|
232
|
+
"source_component": comp0.Name if comp0 else None,
|
|
233
|
+
"source_component_type": (comp0.Kind - 10000) if comp0 else None,
|
|
234
|
+
"source_connector": link0.Index if link0 else None,
|
|
235
|
+
"target_component": comp1.Name if comp1 else None,
|
|
236
|
+
"target_component_type": (comp1.Kind - 10000) if comp1 else None,
|
|
237
|
+
"target_connector": link1.Index if link1 else None,
|
|
238
|
+
}
|
|
239
|
+
)
|
|
243
240
|
|
|
244
241
|
# MATERIAL CONNECTIONS
|
|
245
242
|
if (pipe_cast.Kind - 1000) not in non_material_fluids:
|
|
246
|
-
#
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
pipe_cast.
|
|
316
|
-
|
|
317
|
-
)
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
243
|
+
# Extract basic thermodynamic properties
|
|
244
|
+
T_value = (
|
|
245
|
+
convert_to_SI("T", pipe_cast.T.Value, unit_id_to_string.get(pipe_cast.T.Dimension, "Unknown"))
|
|
246
|
+
if hasattr(pipe_cast, "T") and pipe_cast.T.Value is not None
|
|
247
|
+
else None
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
p_value = (
|
|
251
|
+
convert_to_SI("p", pipe_cast.P.Value, unit_id_to_string.get(pipe_cast.P.Dimension, "Unknown"))
|
|
252
|
+
if hasattr(pipe_cast, "P") and pipe_cast.P.Value is not None
|
|
253
|
+
else None
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
e_PH_value = (
|
|
257
|
+
convert_to_SI("e", pipe_cast.E.Value, unit_id_to_string.get(pipe_cast.E.Dimension, "Unknown"))
|
|
258
|
+
if hasattr(pipe_cast, "E") and pipe_cast.E.Value is not None
|
|
259
|
+
else None
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# If e_PH is not available from Ebsilon, calculate using min-based formula
|
|
263
|
+
if (
|
|
264
|
+
e_PH_value is None
|
|
265
|
+
and T_value is not None
|
|
266
|
+
and p_value is not None
|
|
267
|
+
and (
|
|
268
|
+
hasattr(pipe_cast, "H")
|
|
269
|
+
and pipe_cast.H.Value is not None
|
|
270
|
+
and hasattr(pipe_cast, "S")
|
|
271
|
+
and pipe_cast.S.Value is not None
|
|
272
|
+
)
|
|
273
|
+
):
|
|
274
|
+
try:
|
|
275
|
+
e_PH_value = calc_eph_from_min(pipe_cast, self.Tamb)
|
|
276
|
+
if e_PH_value is not None:
|
|
277
|
+
logging.info(
|
|
278
|
+
f"Physical exergy calculated using min-based formula for {pipe_cast.Name}: "
|
|
279
|
+
f"{e_PH_value:.2f} J/kg"
|
|
280
|
+
)
|
|
281
|
+
except ValueError as ve:
|
|
282
|
+
logging.error(f"Failed to calculate e_PH from min for {pipe_cast.Name}: {ve}")
|
|
283
|
+
e_PH_value = None
|
|
284
|
+
|
|
285
|
+
connection_data.update(
|
|
286
|
+
{
|
|
287
|
+
"kind": "material",
|
|
288
|
+
"m": (
|
|
289
|
+
convert_to_SI(
|
|
290
|
+
"m", pipe_cast.M.Value, unit_id_to_string.get(pipe_cast.M.Dimension, "Unknown")
|
|
291
|
+
)
|
|
292
|
+
if hasattr(pipe_cast, "M") and pipe_cast.M.Value is not None
|
|
293
|
+
else None
|
|
294
|
+
),
|
|
295
|
+
"m_unit": fluid_property_data["m"]["SI_unit"],
|
|
296
|
+
"T": T_value,
|
|
297
|
+
"T_unit": fluid_property_data["T"]["SI_unit"],
|
|
298
|
+
"p": p_value,
|
|
299
|
+
"p_unit": fluid_property_data["p"]["SI_unit"],
|
|
300
|
+
"h": (
|
|
301
|
+
convert_to_SI(
|
|
302
|
+
"h", pipe_cast.H.Value, unit_id_to_string.get(pipe_cast.H.Dimension, "Unknown")
|
|
303
|
+
)
|
|
304
|
+
if hasattr(pipe_cast, "H") and pipe_cast.H.Value is not None
|
|
305
|
+
else None
|
|
306
|
+
),
|
|
307
|
+
"h_unit": fluid_property_data["h"]["SI_unit"],
|
|
308
|
+
"s": (
|
|
309
|
+
convert_to_SI(
|
|
310
|
+
"s", pipe_cast.S.Value, unit_id_to_string.get(pipe_cast.S.Dimension, "Unknown")
|
|
311
|
+
)
|
|
312
|
+
if hasattr(pipe_cast, "S") and pipe_cast.S.Value is not None
|
|
313
|
+
else None
|
|
314
|
+
),
|
|
315
|
+
"s_unit": fluid_property_data["s"]["SI_unit"],
|
|
316
|
+
"e_PH": e_PH_value,
|
|
317
|
+
"e_PH_unit": fluid_property_data["e"]["SI_unit"],
|
|
318
|
+
"x": (
|
|
319
|
+
convert_to_SI(
|
|
320
|
+
"x", pipe_cast.X.Value, unit_id_to_string.get(pipe_cast.X.Dimension, "Unknown")
|
|
321
|
+
)
|
|
322
|
+
if hasattr(pipe_cast, "X") and pipe_cast.X.Value is not None
|
|
323
|
+
else None
|
|
324
|
+
),
|
|
325
|
+
"x_unit": fluid_property_data["x"]["SI_unit"],
|
|
326
|
+
"VM": (
|
|
327
|
+
convert_to_SI(
|
|
328
|
+
"VM", pipe_cast.VM.Value, unit_id_to_string.get(pipe_cast.VM.Dimension, "Unknown")
|
|
329
|
+
)
|
|
330
|
+
if hasattr(pipe_cast, "VM") and pipe_cast.VM.Value is not None
|
|
331
|
+
else None
|
|
332
|
+
),
|
|
333
|
+
"VM_unit": fluid_property_data["VM"]["SI_unit"],
|
|
334
|
+
}
|
|
335
|
+
)
|
|
321
336
|
|
|
322
337
|
# Add the mechanical and thermal specific exergies unless the flag is set to False
|
|
323
338
|
if self.split_physical_exergy:
|
|
324
|
-
e_T_value = calc_eT(self.app, pipe_cast, connection_data[
|
|
325
|
-
e_M_value = calc_eM(self.app, pipe_cast, connection_data[
|
|
326
|
-
|
|
327
|
-
connection_data.update(
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
339
|
+
e_T_value = calc_eT(self.app, pipe_cast, connection_data["p"], self.Tamb, self.pamb)
|
|
340
|
+
e_M_value = calc_eM(self.app, pipe_cast, connection_data["p"], self.Tamb, self.pamb)
|
|
341
|
+
|
|
342
|
+
connection_data.update(
|
|
343
|
+
{
|
|
344
|
+
"e_T": e_T_value,
|
|
345
|
+
"e_T_unit": fluid_property_data["e"]["SI_unit"],
|
|
346
|
+
"e_M": e_M_value,
|
|
347
|
+
"e_M_unit": fluid_property_data["e"]["SI_unit"],
|
|
348
|
+
}
|
|
349
|
+
)
|
|
333
350
|
|
|
334
351
|
# Handle mass composition logic for fluids
|
|
335
|
-
if fluid_type_index.get(pipe_cast.FluidType, "Unknown") in [
|
|
336
|
-
connection_data[
|
|
337
|
-
elif fluid_type_index.get(pipe_cast.FluidType, "Unknown") in [
|
|
352
|
+
if fluid_type_index.get(pipe_cast.FluidType, "Unknown") in ["Steam", "Water"]:
|
|
353
|
+
connection_data["mass_composition"] = {"H2O": 1}
|
|
354
|
+
elif fluid_type_index.get(pipe_cast.FluidType, "Unknown") in ["2PhaseLiquid", "2PhaseGaseous"]:
|
|
338
355
|
# Get the FMED value to determine the substance
|
|
339
|
-
fmed_value = pipe_cast.FMED.Value if hasattr(pipe_cast,
|
|
340
|
-
if fmed_value in two_phase_fluids_mapping
|
|
341
|
-
connection_data[
|
|
356
|
+
fmed_value = pipe_cast.FMED.Value if hasattr(pipe_cast, "FMED") else None
|
|
357
|
+
if fmed_value in two_phase_fluids_mapping:
|
|
358
|
+
connection_data["mass_composition"] = two_phase_fluids_mapping[fmed_value]
|
|
342
359
|
else:
|
|
343
|
-
connection_data[
|
|
344
|
-
logging.warning(
|
|
360
|
+
connection_data["mass_composition"] = {} # Default if no mapping found
|
|
361
|
+
logging.warning(
|
|
362
|
+
f"FMED value {fmed_value} not found in fluid_composition_mapping. Please add it."
|
|
363
|
+
)
|
|
364
|
+
elif fluid_type_index.get(pipe_cast.FluidType, "Unknown") in ["ThermoLiquid"]:
|
|
365
|
+
# For oil, we assume a default composition
|
|
366
|
+
connection_data["mass_composition"] = {"ThermoLiquid": 1}
|
|
345
367
|
else:
|
|
346
|
-
connection_data[
|
|
347
|
-
param.lstrip(
|
|
368
|
+
connection_data["mass_composition"] = {
|
|
369
|
+
param.lstrip("X"): getattr(pipe_cast, param).Value
|
|
348
370
|
for param in composition_params
|
|
349
371
|
if hasattr(pipe_cast, param) and getattr(pipe_cast, param).Value not in [0, None]
|
|
350
372
|
}
|
|
351
373
|
|
|
352
374
|
# HEAT AND POWER CONNECTIONS from Logic "fluids"
|
|
353
375
|
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 (
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
376
|
+
if (comp0 is not None and comp0.Kind is not None and comp0.Kind - 10000 in heat_components) or (
|
|
377
|
+
comp1 is not None and comp1.Kind is not None and comp1.Kind - 10000 in heat_components
|
|
378
|
+
):
|
|
379
|
+
connection_data.update(
|
|
380
|
+
{
|
|
381
|
+
"kind": "heat",
|
|
382
|
+
"energy_flow": (
|
|
383
|
+
convert_to_SI(
|
|
384
|
+
"heat", pipe_cast.Q.Value, unit_id_to_string.get(pipe_cast.Q.Dimension, "Unknown")
|
|
385
|
+
)
|
|
386
|
+
if hasattr(pipe_cast, "Q") and pipe_cast.Q.Value is not None
|
|
387
|
+
else None
|
|
388
|
+
),
|
|
389
|
+
"energy_flow_unit": fluid_property_data["heat"]["SI_unit"],
|
|
390
|
+
"E": None,
|
|
391
|
+
"E_unit": fluid_property_data["power"]["SI_unit"],
|
|
392
|
+
}
|
|
393
|
+
)
|
|
394
|
+
if comp0 is not None and comp0.Kind is not None and comp0.Kind - 10000 in power_components:
|
|
395
|
+
connection_data.update(
|
|
396
|
+
{
|
|
397
|
+
"kind": "power",
|
|
398
|
+
"energy_flow": (
|
|
399
|
+
convert_to_SI(
|
|
400
|
+
"power", pipe_cast.Q.Value, unit_id_to_string.get(pipe_cast.Q.Dimension, "Unknown")
|
|
401
|
+
)
|
|
402
|
+
if hasattr(pipe_cast, "Q") and pipe_cast.Q.Value is not None
|
|
403
|
+
else None
|
|
404
|
+
),
|
|
405
|
+
"energy_flow_unit": fluid_property_data["power"]["SI_unit"],
|
|
406
|
+
"E": (
|
|
407
|
+
convert_to_SI(
|
|
408
|
+
"power", pipe_cast.Q.Value, unit_id_to_string.get(pipe_cast.Q.Dimension, "Unknown")
|
|
409
|
+
)
|
|
410
|
+
if hasattr(pipe_cast, "Q") and pipe_cast.Q.Value is not None
|
|
411
|
+
else None
|
|
412
|
+
),
|
|
413
|
+
"E_unit": fluid_property_data["power"]["SI_unit"],
|
|
414
|
+
}
|
|
415
|
+
)
|
|
370
416
|
|
|
371
417
|
# POWER CONNECTIONS from power "fluids"
|
|
372
418
|
if (pipe_cast.Kind - 1000) in power_fluids:
|
|
373
|
-
connection_data.update(
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
419
|
+
connection_data.update(
|
|
420
|
+
{
|
|
421
|
+
"kind": "power",
|
|
422
|
+
"energy_flow": (
|
|
423
|
+
convert_to_SI(
|
|
424
|
+
"power", pipe_cast.Q.Value, unit_id_to_string.get(pipe_cast.Q.Dimension, "Unknown")
|
|
425
|
+
)
|
|
426
|
+
if hasattr(pipe_cast, "Q") and pipe_cast.Q.Value is not None
|
|
427
|
+
else None
|
|
428
|
+
),
|
|
429
|
+
"energy_flow_unit": fluid_property_data["power"]["SI_unit"],
|
|
430
|
+
"E": (
|
|
431
|
+
convert_to_SI(
|
|
432
|
+
"power", pipe_cast.Q.Value, unit_id_to_string.get(pipe_cast.Q.Dimension, "Unknown")
|
|
433
|
+
)
|
|
434
|
+
if hasattr(pipe_cast, "Q") and pipe_cast.Q.Value is not None
|
|
435
|
+
else None
|
|
436
|
+
),
|
|
437
|
+
"E_unit": fluid_property_data["power"]["SI_unit"],
|
|
438
|
+
}
|
|
439
|
+
)
|
|
380
440
|
|
|
381
441
|
# Convert the connector numbers to selected standard values for each component
|
|
382
|
-
if
|
|
383
|
-
connection_data[
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
connection_data[
|
|
442
|
+
if (
|
|
443
|
+
connection_data["source_component_type"] in connector_mapping
|
|
444
|
+
and connection_data["source_connector"] in connector_mapping[connection_data["source_component_type"]]
|
|
445
|
+
):
|
|
446
|
+
connection_data["source_connector"] = connector_mapping[connection_data["source_component_type"]][
|
|
447
|
+
connection_data["source_connector"]
|
|
448
|
+
]
|
|
449
|
+
|
|
450
|
+
if (
|
|
451
|
+
connection_data["target_component_type"] in connector_mapping
|
|
452
|
+
and connection_data["target_connector"] in connector_mapping[connection_data["target_component_type"]]
|
|
453
|
+
):
|
|
454
|
+
connection_data["target_connector"] = connector_mapping[connection_data["target_component_type"]][
|
|
455
|
+
connection_data["target_connector"]
|
|
456
|
+
]
|
|
387
457
|
|
|
388
458
|
# Store the connection data
|
|
389
459
|
self.connections_data[obj.Name] = connection_data
|
|
@@ -391,7 +461,6 @@ class EbsilonModelParser:
|
|
|
391
461
|
else:
|
|
392
462
|
logging.info(f"Skipping non-energetic connection: {pipe_cast.Name}")
|
|
393
463
|
|
|
394
|
-
|
|
395
464
|
@require_ebsilon
|
|
396
465
|
def parse_component(self, obj: Any):
|
|
397
466
|
"""
|
|
@@ -402,7 +471,7 @@ class EbsilonModelParser:
|
|
|
402
471
|
"""
|
|
403
472
|
# Cast the component to get its type index
|
|
404
473
|
comp_cast = self.oc.CastToComp(obj)
|
|
405
|
-
type_index =
|
|
474
|
+
type_index = comp_cast.Kind - 10000
|
|
406
475
|
|
|
407
476
|
# Dynamically call the specific CastToCompX method based on type_index
|
|
408
477
|
cast_method_name = f"CastToComp{type_index}"
|
|
@@ -422,79 +491,62 @@ class EbsilonModelParser:
|
|
|
422
491
|
if type_index not in non_thermodynamic_unit_operators:
|
|
423
492
|
# Collect component data
|
|
424
493
|
component_data = {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
comp_cast.ETAIN.Value
|
|
430
|
-
if hasattr(comp_cast, 'ETAIN') and comp_cast.ETAIN.Value is not None else None
|
|
494
|
+
"name": comp_cast.Name,
|
|
495
|
+
"type": type_name,
|
|
496
|
+
"type_index": type_index,
|
|
497
|
+
"eta_s": (
|
|
498
|
+
comp_cast.ETAIN.Value if hasattr(comp_cast, "ETAIN") and comp_cast.ETAIN.Value is not None else None
|
|
431
499
|
),
|
|
432
|
-
|
|
433
|
-
comp_cast.ETAMN.Value
|
|
434
|
-
if hasattr(comp_cast, 'ETAMN') and comp_cast.ETAMN.Value is not None else None
|
|
500
|
+
"eta_mech": (
|
|
501
|
+
comp_cast.ETAMN.Value if hasattr(comp_cast, "ETAMN") and comp_cast.ETAMN.Value is not None else None
|
|
435
502
|
),
|
|
436
|
-
|
|
437
|
-
comp_cast.ETAEN.Value
|
|
438
|
-
if hasattr(comp_cast, 'ETAEN') and comp_cast.ETAEN.Value is not None else None
|
|
503
|
+
"eta_el": (
|
|
504
|
+
comp_cast.ETAEN.Value if hasattr(comp_cast, "ETAEN") and comp_cast.ETAEN.Value is not None else None
|
|
439
505
|
),
|
|
440
|
-
|
|
441
|
-
comp_cast.ETAB.Value
|
|
442
|
-
if hasattr(comp_cast, 'ETAB') and comp_cast.ETAB.Value is not None else None
|
|
506
|
+
"eta_cc": (
|
|
507
|
+
comp_cast.ETAB.Value if hasattr(comp_cast, "ETAB") and comp_cast.ETAB.Value is not None else None
|
|
443
508
|
),
|
|
444
|
-
|
|
445
|
-
comp_cast.ALAMN.Value
|
|
446
|
-
if hasattr(comp_cast, 'ALAMN') and comp_cast.ALAMN.Value is not None else None
|
|
509
|
+
"lamb": (
|
|
510
|
+
comp_cast.ALAMN.Value if hasattr(comp_cast, "ALAMN") and comp_cast.ALAMN.Value is not None else None
|
|
447
511
|
),
|
|
448
|
-
|
|
449
|
-
convert_to_SI(
|
|
450
|
-
|
|
451
|
-
|
|
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
|
|
512
|
+
"Q": (
|
|
513
|
+
convert_to_SI("heat", comp_cast.QT.Value, unit_id_to_string.get(comp_cast.QT.Dimension, "Unknown"))
|
|
514
|
+
if hasattr(comp_cast, "QT") and comp_cast.QT.Value is not None
|
|
515
|
+
else None
|
|
454
516
|
),
|
|
455
|
-
|
|
456
|
-
|
|
517
|
+
"Q_unit": fluid_property_data["heat"]["SI_unit"],
|
|
518
|
+
"P": (
|
|
457
519
|
convert_to_SI(
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
520
|
+
"power", comp_cast.QSHAFT.Value, unit_id_to_string.get(comp_cast.QSHAFT.Dimension, "Unknown")
|
|
521
|
+
)
|
|
522
|
+
if hasattr(comp_cast, "QSHAFT") and comp_cast.QSHAFT.Value is not None
|
|
523
|
+
else None
|
|
462
524
|
),
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
525
|
+
"P_unit": fluid_property_data["power"]["SI_unit"],
|
|
526
|
+
"kA": (comp_cast.KA.Value if hasattr(comp_cast, "KA") and comp_cast.KA.Value is not None else None),
|
|
527
|
+
"kA_unit": fluid_property_data["kA"]["SI_unit"],
|
|
528
|
+
"A": (comp_cast.A.Value if hasattr(comp_cast, "A") and comp_cast.A.Value is not None else None),
|
|
529
|
+
"A_unit": fluid_property_data["A"]["SI_unit"],
|
|
530
|
+
"mass_flow_1": (
|
|
531
|
+
convert_to_SI("m", comp_cast.M1N.Value, unit_id_to_string.get(comp_cast.M1N.Dimension, "Unknown"))
|
|
532
|
+
if hasattr(comp_cast, "M1N") and comp_cast.M1N.Value is not None
|
|
533
|
+
else None
|
|
467
534
|
),
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
comp_cast.
|
|
471
|
-
if hasattr(comp_cast,
|
|
535
|
+
"mass_flow_1_unit": fluid_property_data["m"]["SI_unit"],
|
|
536
|
+
"mass_flow_3": (
|
|
537
|
+
convert_to_SI("m", comp_cast.M3N.Value, unit_id_to_string.get(comp_cast.M3N.Dimension, "Unknown"))
|
|
538
|
+
if hasattr(comp_cast, "M3N") and comp_cast.M3N.Value is not None
|
|
539
|
+
else None
|
|
472
540
|
),
|
|
473
|
-
|
|
474
|
-
|
|
541
|
+
"mass_flow_3_unit": fluid_property_data["m"]["SI_unit"],
|
|
542
|
+
"energy_flow_1": (
|
|
475
543
|
convert_to_SI(
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
544
|
+
"heat", comp_cast.Q1N.Value, unit_id_to_string.get(comp_cast.Q1N.Dimension, "Unknown")
|
|
545
|
+
)
|
|
546
|
+
if hasattr(comp_cast, "Q1N") and comp_cast.Q1N.Value is not None
|
|
547
|
+
else None
|
|
480
548
|
),
|
|
481
|
-
|
|
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'],
|
|
549
|
+
"energy_flow_1_unit": fluid_property_data["heat"]["SI_unit"],
|
|
498
550
|
}
|
|
499
551
|
|
|
500
552
|
# Determine the group for the component based on its type
|
|
@@ -519,14 +571,100 @@ class EbsilonModelParser:
|
|
|
519
571
|
elif type_index == 46:
|
|
520
572
|
comp46 = self.oc.CastToComp46(obj)
|
|
521
573
|
if comp46.FTYP.Value == 26:
|
|
522
|
-
self.Tamb = convert_to_SI(
|
|
574
|
+
self.Tamb = convert_to_SI(
|
|
575
|
+
"T", comp46.MEASM.Value, unit_id_to_string.get(comp46.MEASM.Dimension, "Unknown")
|
|
576
|
+
)
|
|
523
577
|
logging.info(f"Set ambient temperature (Tamb) to {self.Tamb} K from component {comp_cast.Name}")
|
|
524
578
|
elif comp46.FTYP.Value == 13:
|
|
525
|
-
self.pamb = convert_to_SI(
|
|
579
|
+
self.pamb = convert_to_SI(
|
|
580
|
+
"p", comp46.MEASM.Value, unit_id_to_string.get(comp46.MEASM.Dimension, "Unknown")
|
|
581
|
+
)
|
|
526
582
|
logging.info(f"Set ambient pressure (pamb) to {self.pamb} Pa from component {comp_cast.Name}")
|
|
527
583
|
|
|
584
|
+
if type_index == 118:
|
|
585
|
+
storage = self.oc.CastToComp118(obj)
|
|
586
|
+
self._storages_to_postprocess.append(
|
|
587
|
+
{
|
|
588
|
+
"name": storage.Name,
|
|
589
|
+
"kind": storage.Kind,
|
|
590
|
+
"m_flow_load": storage.MLD.Value,
|
|
591
|
+
"m_flow_load_unit": unit_id_to_string.get(storage.MLD.Dimension, "Unknown"),
|
|
592
|
+
"m_flow_unload": storage.MUNLD.Value,
|
|
593
|
+
"m_flow_unload_unit": unit_id_to_string.get(storage.MUNLD.Dimension, "Unknown"),
|
|
594
|
+
"T_storage": storage.TNEW.Value,
|
|
595
|
+
"T_storage_unit": unit_id_to_string.get(storage.TNEW.Dimension, "Unknown"),
|
|
596
|
+
"p_storage": storage.PNEW.Value,
|
|
597
|
+
"p_storage_unit": unit_id_to_string.get(storage.PNEW.Dimension, "Unknown"),
|
|
598
|
+
"h_storage": storage.HNEW.Value,
|
|
599
|
+
"h_storage_unit": unit_id_to_string.get(storage.HNEW.Dimension, "Unknown"),
|
|
600
|
+
}
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
def _create_storage_connections(self):
|
|
604
|
+
"""
|
|
605
|
+
Create fictive charging/discharging connections for storage components.
|
|
606
|
+
|
|
607
|
+
After all real connections are parsed, uses stored storage parameters
|
|
608
|
+
and existing connections_data to generate and insert material connections.
|
|
528
609
|
|
|
529
|
-
|
|
610
|
+
Returns
|
|
611
|
+
-------
|
|
612
|
+
None
|
|
613
|
+
"""
|
|
614
|
+
for raw in self._storages_to_postprocess:
|
|
615
|
+
name = raw["name"]
|
|
616
|
+
m_load = raw["m_flow_load"]
|
|
617
|
+
m_unload = raw["m_flow_unload"]
|
|
618
|
+
sign = "charging" if m_load >= m_unload else "discharging"
|
|
619
|
+
delta_m = abs(m_load - m_unload)
|
|
620
|
+
prefix = f"{name}_{sign}"
|
|
621
|
+
new_conn = {
|
|
622
|
+
"name": prefix,
|
|
623
|
+
"kind": "material",
|
|
624
|
+
"source_component": name if sign == "charging" else None,
|
|
625
|
+
"target_component": name if sign == "discharging" else None,
|
|
626
|
+
"source_component_type": (raw["kind"] - 10000) if sign == "charging" else None,
|
|
627
|
+
"target_component_type": (raw["kind"] - 10000) if sign == "discharging" else None,
|
|
628
|
+
"source_connector": None,
|
|
629
|
+
"target_connector": None,
|
|
630
|
+
"m": convert_to_SI("m", delta_m, raw["m_flow_load_unit"]),
|
|
631
|
+
"m_unit": fluid_property_data["m"]["SI_unit"],
|
|
632
|
+
"T": convert_to_SI("T", raw["T_storage"], raw["T_storage_unit"]),
|
|
633
|
+
"T_unit": fluid_property_data["T"]["SI_unit"],
|
|
634
|
+
"p": convert_to_SI("p", raw["p_storage"], raw["p_storage_unit"]),
|
|
635
|
+
"p_unit": fluid_property_data["p"]["SI_unit"],
|
|
636
|
+
"h": convert_to_SI("h", raw["h_storage"], raw["h_storage_unit"]),
|
|
637
|
+
"h_unit": fluid_property_data["h"]["SI_unit"],
|
|
638
|
+
"s": next(
|
|
639
|
+
(
|
|
640
|
+
c["s"]
|
|
641
|
+
for c in self.connections_data.values()
|
|
642
|
+
if c.get("source_component") == name and c.get("source_connector") == connector_mapping[118][2]
|
|
643
|
+
),
|
|
644
|
+
None,
|
|
645
|
+
),
|
|
646
|
+
"s_unit": fluid_property_data["s"]["SI_unit"],
|
|
647
|
+
"e_PH": next(
|
|
648
|
+
(
|
|
649
|
+
c["e_PH"]
|
|
650
|
+
for c in self.connections_data.values()
|
|
651
|
+
if c.get("source_component") == name and c.get("source_connector") == connector_mapping[118][2]
|
|
652
|
+
),
|
|
653
|
+
None,
|
|
654
|
+
),
|
|
655
|
+
"e_PH_unit": fluid_property_data["e"]["SI_unit"],
|
|
656
|
+
"mass_composition": next(
|
|
657
|
+
(
|
|
658
|
+
c["mass_composition"]
|
|
659
|
+
for c in self.connections_data.values()
|
|
660
|
+
if c.get("source_component") == name and c.get("source_connector") == connector_mapping[118][2]
|
|
661
|
+
),
|
|
662
|
+
None,
|
|
663
|
+
),
|
|
664
|
+
}
|
|
665
|
+
self.connections_data[prefix] = new_conn
|
|
666
|
+
|
|
667
|
+
def get_sorted_data(self) -> dict[str, Any]:
|
|
530
668
|
"""
|
|
531
669
|
Sorts the component and connection data alphabetically by name.
|
|
532
670
|
|
|
@@ -542,17 +680,16 @@ class EbsilonModelParser:
|
|
|
542
680
|
sorted_connections = dict(sorted(self.connections_data.items()))
|
|
543
681
|
# Return data including ambient conditions
|
|
544
682
|
return {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
}
|
|
683
|
+
"components": sorted_components,
|
|
684
|
+
"connections": sorted_connections,
|
|
685
|
+
"ambient_conditions": {
|
|
686
|
+
"Tamb": self.Tamb,
|
|
687
|
+
"Tamb_unit": fluid_property_data["T"]["SI_unit"],
|
|
688
|
+
"pamb": self.pamb,
|
|
689
|
+
"pamb_unit": fluid_property_data["p"]["SI_unit"],
|
|
690
|
+
},
|
|
553
691
|
}
|
|
554
692
|
|
|
555
|
-
|
|
556
693
|
def write_to_json(self, output_path: str):
|
|
557
694
|
"""
|
|
558
695
|
Writes the parsed and sorted data to a JSON file.
|
|
@@ -566,7 +703,7 @@ class EbsilonModelParser:
|
|
|
566
703
|
data = self.get_sorted_data()
|
|
567
704
|
try:
|
|
568
705
|
# Write the data to a JSON file with indentation for readability
|
|
569
|
-
with open(output_path,
|
|
706
|
+
with open(output_path, "w") as json_file:
|
|
570
707
|
json.dump(data, json_file, indent=4)
|
|
571
708
|
logging.info(f"Data successfully written to {output_path}")
|
|
572
709
|
except Exception as e:
|
|
@@ -574,7 +711,7 @@ class EbsilonModelParser:
|
|
|
574
711
|
raise
|
|
575
712
|
|
|
576
713
|
|
|
577
|
-
def run_ebsilon(model_path: str, output_dir:
|
|
714
|
+
def run_ebsilon(model_path: str, output_dir: str | None = None, split_physical_exergy: bool = True) -> dict[str, Any]:
|
|
578
715
|
"""
|
|
579
716
|
Main function to process the Ebsilon model and return parsed data.
|
|
580
717
|
Optionally writes the parsed data to a JSON file.
|
|
@@ -616,9 +753,9 @@ def run_ebsilon(model_path: str, output_dir: Optional[str] = None, split_physica
|
|
|
616
753
|
# Initialize the Ebsilon model within the parser
|
|
617
754
|
parser.initialize_model()
|
|
618
755
|
except FileNotFoundError:
|
|
619
|
-
|
|
756
|
+
# allow an invalid/corrupt‐model file to bubble up as FileNotFoundError
|
|
620
757
|
raise
|
|
621
|
-
except Exception
|
|
758
|
+
except Exception:
|
|
622
759
|
# other COM/server errors should still be RuntimeErrors
|
|
623
760
|
error_msg = f"File not found: {model_path}"
|
|
624
761
|
logging.error(error_msg)
|
|
@@ -657,4 +794,4 @@ def run_ebsilon(model_path: str, output_dir: Optional[str] = None, split_physica
|
|
|
657
794
|
raise RuntimeError(error_msg)
|
|
658
795
|
|
|
659
796
|
# Return the parsed data as a dictionary (not as a JSON string)
|
|
660
|
-
return parsed_data
|
|
797
|
+
return parsed_data
|