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.
Files changed (48) hide show
  1. exerpy/__init__.py +2 -4
  2. exerpy/analyses.py +849 -304
  3. exerpy/components/__init__.py +3 -0
  4. exerpy/components/combustion/base.py +53 -35
  5. exerpy/components/component.py +8 -8
  6. exerpy/components/heat_exchanger/base.py +188 -121
  7. exerpy/components/heat_exchanger/condenser.py +98 -62
  8. exerpy/components/heat_exchanger/simple.py +237 -137
  9. exerpy/components/heat_exchanger/steam_generator.py +46 -41
  10. exerpy/components/helpers/cycle_closer.py +61 -34
  11. exerpy/components/helpers/power_bus.py +117 -0
  12. exerpy/components/nodes/deaerator.py +176 -58
  13. exerpy/components/nodes/drum.py +50 -39
  14. exerpy/components/nodes/flash_tank.py +218 -43
  15. exerpy/components/nodes/mixer.py +249 -69
  16. exerpy/components/nodes/splitter.py +173 -0
  17. exerpy/components/nodes/storage.py +130 -0
  18. exerpy/components/piping/valve.py +311 -115
  19. exerpy/components/power_machines/generator.py +105 -38
  20. exerpy/components/power_machines/motor.py +111 -39
  21. exerpy/components/turbomachinery/compressor.py +214 -68
  22. exerpy/components/turbomachinery/pump.py +215 -68
  23. exerpy/components/turbomachinery/turbine.py +182 -74
  24. exerpy/cost_estimation/__init__.py +65 -0
  25. exerpy/cost_estimation/turton.py +1260 -0
  26. exerpy/data/cost_correlations/cepci_index.json +135 -0
  27. exerpy/data/cost_correlations/component_mapping.json +450 -0
  28. exerpy/data/cost_correlations/material_factors.json +428 -0
  29. exerpy/data/cost_correlations/pressure_factors.json +206 -0
  30. exerpy/data/cost_correlations/turton2008.json +726 -0
  31. exerpy/data/cost_correlations/turton2008_design_analysis_synthesis_components_tables.pdf +0 -0
  32. exerpy/data/cost_correlations/turton2008_design_analysis_synthesis_components_theory.pdf +0 -0
  33. exerpy/functions.py +389 -264
  34. exerpy/parser/from_aspen/aspen_config.py +57 -48
  35. exerpy/parser/from_aspen/aspen_parser.py +373 -280
  36. exerpy/parser/from_ebsilon/__init__.py +2 -2
  37. exerpy/parser/from_ebsilon/check_ebs_path.py +15 -19
  38. exerpy/parser/from_ebsilon/ebsilon_config.py +328 -226
  39. exerpy/parser/from_ebsilon/ebsilon_functions.py +205 -38
  40. exerpy/parser/from_ebsilon/ebsilon_parser.py +392 -255
  41. exerpy/parser/from_ebsilon/utils.py +16 -11
  42. exerpy/parser/from_tespy/tespy_config.py +33 -1
  43. exerpy/parser/from_tespy/tespy_parser.py +151 -0
  44. {exerpy-0.0.2.dist-info → exerpy-0.0.4.dist-info}/METADATA +43 -2
  45. exerpy-0.0.4.dist-info/RECORD +57 -0
  46. exerpy-0.0.2.dist-info/RECORD +0 -44
  47. {exerpy-0.0.2.dist-info → exerpy-0.0.4.dist-info}/WHEEL +0 -0
  48. {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 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
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 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
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: 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
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
- '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,
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
- '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
- })
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
- # 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
- })
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['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
- })
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 ['Steam', 'Water']:
336
- connection_data['mass_composition'] = {'H2O': 1}
337
- elif fluid_type_index.get(pipe_cast.FluidType, "Unknown") in ['2PhaseLiquid', '2PhaseGaseous']:
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, 'FMED') else None
340
- if fmed_value in two_phase_fluids_mapping.keys():
341
- connection_data['mass_composition'] = two_phase_fluids_mapping[fmed_value]
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['mass_composition'] = {} # Default if no mapping found
344
- logging.warning(f"FMED value {fmed_value} not found in fluid_composition_mapping. Please add it.")
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['mass_composition'] = {
347
- param.lstrip('X'): getattr(pipe_cast, param).Value
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 (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
- })
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
- '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
- })
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 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']]
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 = (comp_cast.Kind - 10000)
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
- '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
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
- 'eta_mech': (
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
- 'eta_el': (
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
- 'eta_cc': (
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
- 'lamb': (
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
- '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
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
- 'Q_unit': fluid_property_data['heat']['SI_unit'],
456
- 'P': (
517
+ "Q_unit": fluid_property_data["heat"]["SI_unit"],
518
+ "P": (
457
519
  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
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
- '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
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
- '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
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
- 'A_unit': fluid_property_data['A']['SI_unit'],
474
- 'mass_flow_1': (
541
+ "mass_flow_3_unit": fluid_property_data["m"]["SI_unit"],
542
+ "energy_flow_1": (
475
543
  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
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
- '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'],
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('T', comp46.MEASM.Value, unit_id_to_string.get(comp46.MEASM.Dimension, "Unknown"))
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('p', comp46.MEASM.Value, unit_id_to_string.get(comp46.MEASM.Dimension, "Unknown"))
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
- def get_sorted_data(self) -> Dict[str, Any]:
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
- '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
- }
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, 'w') as json_file:
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: Optional[str] = None, split_physical_exergy: bool = True) -> Dict[str, Any]:
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
- # allow an invalid/corrupt‐model file to bubble up as FileNotFoundError
756
+ # allow an invalid/corrupt‐model file to bubble up as FileNotFoundError
620
757
  raise
621
- except Exception as e:
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