exerpy 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. exerpy/__init__.py +12 -0
  2. exerpy/analyses.py +1711 -0
  3. exerpy/components/__init__.py +16 -0
  4. exerpy/components/combustion/__init__.py +0 -0
  5. exerpy/components/combustion/base.py +248 -0
  6. exerpy/components/component.py +126 -0
  7. exerpy/components/heat_exchanger/__init__.py +0 -0
  8. exerpy/components/heat_exchanger/base.py +449 -0
  9. exerpy/components/heat_exchanger/condenser.py +323 -0
  10. exerpy/components/heat_exchanger/simple.py +358 -0
  11. exerpy/components/heat_exchanger/steam_generator.py +264 -0
  12. exerpy/components/helpers/__init__.py +0 -0
  13. exerpy/components/helpers/cycle_closer.py +104 -0
  14. exerpy/components/nodes/__init__.py +0 -0
  15. exerpy/components/nodes/deaerator.py +318 -0
  16. exerpy/components/nodes/drum.py +164 -0
  17. exerpy/components/nodes/flash_tank.py +89 -0
  18. exerpy/components/nodes/mixer.py +332 -0
  19. exerpy/components/piping/__init__.py +0 -0
  20. exerpy/components/piping/valve.py +394 -0
  21. exerpy/components/power_machines/__init__.py +0 -0
  22. exerpy/components/power_machines/generator.py +168 -0
  23. exerpy/components/power_machines/motor.py +173 -0
  24. exerpy/components/turbomachinery/__init__.py +0 -0
  25. exerpy/components/turbomachinery/compressor.py +318 -0
  26. exerpy/components/turbomachinery/pump.py +310 -0
  27. exerpy/components/turbomachinery/turbine.py +351 -0
  28. exerpy/data/Ahrendts.json +90 -0
  29. exerpy/functions.py +637 -0
  30. exerpy/parser/__init__.py +0 -0
  31. exerpy/parser/from_aspen/__init__.py +0 -0
  32. exerpy/parser/from_aspen/aspen_config.py +61 -0
  33. exerpy/parser/from_aspen/aspen_parser.py +721 -0
  34. exerpy/parser/from_ebsilon/__init__.py +38 -0
  35. exerpy/parser/from_ebsilon/check_ebs_path.py +74 -0
  36. exerpy/parser/from_ebsilon/ebsilon_config.py +1055 -0
  37. exerpy/parser/from_ebsilon/ebsilon_functions.py +181 -0
  38. exerpy/parser/from_ebsilon/ebsilon_parser.py +660 -0
  39. exerpy/parser/from_ebsilon/utils.py +79 -0
  40. exerpy/parser/from_tespy/tespy_config.py +23 -0
  41. exerpy-0.0.1.dist-info/METADATA +158 -0
  42. exerpy-0.0.1.dist-info/RECORD +44 -0
  43. exerpy-0.0.1.dist-info/WHEEL +4 -0
  44. exerpy-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,310 @@
1
+ import logging
2
+
3
+ import numpy as np
4
+
5
+ from exerpy.components.component import Component
6
+ from exerpy.components.component import component_registry
7
+
8
+
9
+ @component_registry
10
+ class Pump(Component):
11
+ r"""
12
+ Class for exergy analysis of pumps.
13
+
14
+ This class performs exergy analysis calculations for pumps, with definitions
15
+ of exergy product and fuel varying based on the temperature relationships between
16
+ inlet stream, outlet stream, and ambient conditions.
17
+
18
+ Parameters
19
+ ----------
20
+ **kwargs : dict
21
+ Arbitrary keyword arguments passed to parent class.
22
+
23
+ Attributes
24
+ ----------
25
+ E_F : float
26
+ Exergy fuel of the component :math:`\dot{E}_\mathrm{F}` in :math:`\mathrm{W}`.
27
+ E_P : float
28
+ Exergy product of the component :math:`\dot{E}_\mathrm{P}` in :math:`\mathrm{W}`.
29
+ E_D : float
30
+ Exergy destruction of the component :math:`\dot{E}_\mathrm{D}` in :math:`\mathrm{W}`.
31
+ epsilon : float
32
+ Exergetic efficiency of the component :math:`\varepsilon` in :math:`-`.
33
+ P : float
34
+ Power input to the pump in :math:`\mathrm{W}`.
35
+ inl : dict
36
+ Dictionary containing inlet stream data with temperature, mass flows,
37
+ enthalpies, and specific exergies.
38
+ outl : dict
39
+ Dictionary containing outlet stream data with temperature, mass flows,
40
+ enthalpies, and specific exergies.
41
+
42
+ Notes
43
+ -----
44
+ The exergy analysis considers three cases based on temperature relationships:
45
+
46
+ Case 1 - **Both temperatures above ambient** (:math:`T_\mathrm{in}, T_\mathrm{out} > T_0`):
47
+
48
+ .. math::
49
+
50
+ \dot{E}_\mathrm{P} &= \dot{m} \cdot (e_\mathrm{out}^\mathrm{PH} -
51
+ e_\mathrm{in}^\mathrm{PH})\\
52
+ \dot{E}_\mathrm{F} &= |\dot{W}|
53
+
54
+ Case 2 - **Inlet below, outlet above ambient** (:math:`T_\mathrm{in} < T_0 < T_\mathrm{out}`):
55
+
56
+ .. math::
57
+
58
+ \dot{E}_\mathrm{P} &= |\dot{W}| + (e_\mathrm{out}^\mathrm{PH} -
59
+ e_\mathrm{in}^\mathrm{M})\\
60
+ \dot{E}_\mathrm{F} &= e_\mathrm{in}^\mathrm{M} + e_\mathrm{in}^\mathrm{PH}
61
+
62
+ Case 3 - **Both temperatures below ambient** (:math:`T_\mathrm{in}, T_\mathrm{out} \leq T_0`):
63
+
64
+ .. math::
65
+
66
+ \dot{E}_\mathrm{P} &= e_\mathrm{out}^\mathrm{M} - e_\mathrm{in}^\mathrm{M}\\
67
+ \dot{E}_\mathrm{F} &= e_\mathrm{in}^\mathrm{PH} - e_\mathrm{out}^\mathrm{PH}
68
+
69
+ For all valid cases, the exergy destruction is:
70
+
71
+ .. math::
72
+
73
+ \dot{E}_\mathrm{D} = \dot{E}_\mathrm{F} - \dot{E}_\mathrm{P}
74
+
75
+ where:
76
+ - :math:`\dot{W}`: Power input
77
+ - :math:`e^\mathrm{PH}`: Physical exergy
78
+ - :math:`e^\mathrm{M}`: Mechanical exergy
79
+ """
80
+
81
+ def __init__(self, **kwargs):
82
+ r"""Initialize pump component with given parameters."""
83
+ super().__init__(**kwargs)
84
+ self.P = None
85
+
86
+ def calc_exergy_balance(self, T0: float, p0: float, split_physical_exergy) -> None:
87
+ r"""
88
+ Calculate the exergy balance of the pump.
89
+
90
+ Performs exergy balance calculations considering the temperature relationships
91
+ between inlet stream, outlet stream, and ambient conditions.
92
+
93
+ Parameters
94
+ ----------
95
+ T0 : float
96
+ Ambient temperature in :math:`\mathrm{K}`.
97
+ p0 : float
98
+ Ambient pressure in :math:`\mathrm{Pa}`.
99
+ split_physical_exergy : bool
100
+ Flag indicating whether physical exergy is split into thermal and mechanical components.
101
+
102
+ """
103
+ # Get power flow if not already available
104
+ if self.P is None:
105
+ self.P = self.outl[0]['m'] * (self.outl[0]['h'] - self.inl[0]['h'])
106
+
107
+ # First, check for the invalid case: outlet temperature smaller than inlet temperature.
108
+ if self.inl[0]['T'] > self.outl[0]['T']:
109
+ logging.warning(
110
+ f"Exergy balance of pump '{self.name}' where outlet temperature ({self.outl[0]['T']}) "
111
+ f"is smaller than inlet temperature ({self.inl[0]['T']}) is not implemented."
112
+ )
113
+ self.E_P = np.nan
114
+ self.E_F = np.nan
115
+
116
+ # Case 1: Both temperatures above ambient
117
+ elif round(self.inl[0]['T'], 5) >= T0 and round(self.outl[0]['T'], 5) > T0:
118
+ self.E_P = self.outl[0]['m'] * (self.outl[0]['e_PH'] - self.inl[0]['e_PH'])
119
+ self.E_F = abs(self.P)
120
+
121
+ # Case 2: Inlet below, outlet above ambient
122
+ elif round(self.inl[0]['T'], 5) < T0 and round(self.outl[0]['T'], 5) > T0:
123
+ if split_physical_exergy:
124
+ self.E_P = (self.outl[0]['m'] * self.outl[0]['e_T'] +
125
+ self.outl[0]['m'] * (self.outl[0]['e_M'] - self.inl[0]['e_M']))
126
+ self.E_F = abs(self.P) + self.inl[0]['m'] * self.inl[0]['e_T']
127
+ else:
128
+ logging.warning("While dealing with pump below ambient, "
129
+ "physical exergy should be split into thermal and mechanical components!")
130
+ self.E_P = self.outl[0]['m'] * (self.outl[0]['e_PH'] - self.inl[0]['e_PH'])
131
+ self.E_F = abs(self.P)
132
+
133
+ # Case 3: Both temperatures below ambient
134
+ elif round(self.inl[0]['T'], 5) < T0 and round(self.outl[0]['T'], 5) <= T0:
135
+ if split_physical_exergy:
136
+ self.E_P = self.outl[0]['m'] * (self.outl[0]['e_M'] - self.inl[0]['e_M'])
137
+ self.E_F = abs(self.P) + self.inl[0]['m'] * (self.inl[0]['e_T'] -
138
+ self.outl[0]['e_T'])
139
+ else:
140
+ logging.warning("While dealing with pump below ambient, "
141
+ "physical exergy should be split into thermal and mechanical components!")
142
+ self.E_P = self.outl[0]['m'] * (self.outl[0]['e_PH'] - self.inl[0]['e_PH'])
143
+ self.E_F = abs(self.P)
144
+
145
+ # Invalid case: outlet temperature smaller than inlet
146
+ else:
147
+ logging.warning(
148
+ 'Exergy balance of a pump where outlet temperature is smaller '
149
+ 'than inlet temperature is not implemented.'
150
+ )
151
+ self.E_P = np.nan
152
+ self.E_F = np.nan
153
+
154
+ # Calculate exergy destruction and efficiency
155
+ self.E_D = self.E_F - self.E_P
156
+ self.epsilon = self.calc_epsilon()
157
+
158
+ # Log the results
159
+ logging.info(
160
+ f"Pump exergy balance calculated: "
161
+ f"E_P={self.E_P:.2f}, E_F={self.E_F:.2f}, E_D={self.E_D:.2f}, "
162
+ f"Efficiency={self.epsilon:.2%}"
163
+ )
164
+
165
+
166
+ def aux_eqs(self, A, b, counter, T0, equations, chemical_exergy_enabled):
167
+ """
168
+ Auxiliary equations for the pump.
169
+
170
+ This function adds rows to the cost matrix A and the right-hand-side vector b to enforce
171
+ the following auxiliary cost relations:
172
+
173
+ (1) Chemical exergy cost equation (if enabled):
174
+ 1/E_CH_in * C_CH_in - 1/E_CH_out * C_CH_out = 0
175
+ - F-principle: specific chemical exergy costs equalized between inlet/outlet
176
+
177
+ (2) Thermal/Mechanical exergy cost equations (based on temperature conditions):
178
+
179
+ Case 1 (T_in > T0, T_out > T0):
180
+ 1/dET * C_T_out - 1/dET * C_T_in - 1/dEM * C_M_out + 1/dEM * C_M_in = 0
181
+ - P-principle: relates inlet/outlet thermal and mechanical exergy costs
182
+
183
+ Case 2 (T_in ≤ T0, T_out > T0):
184
+ 1/E_T_out * C_T_out - 1/dEM * C_M_out + 1/dEM * C_M_in = 0
185
+ - P-principle: relates outlet thermal and inlet/outlet mechanical exergy costs
186
+
187
+ Case 3 (T_in ≤ T0, T_out ≤ T0):
188
+ 1/E_T_out * C_T_out - 1/E_T_in * C_T_in = 0
189
+ - F-principle: specific thermal exergy costs equalized between inlet/outlet
190
+
191
+ Parameters
192
+ ----------
193
+ A : numpy.ndarray
194
+ The current cost matrix.
195
+ b : numpy.ndarray
196
+ The current right-hand-side vector.
197
+ counter : int
198
+ The current row index in the matrix.
199
+ T0 : float
200
+ Ambient temperature.
201
+ equations : list or dict
202
+ Data structure for storing equation labels.
203
+ chemical_exergy_enabled : bool
204
+ Flag indicating whether chemical exergy auxiliary equations should be added.
205
+
206
+ Returns
207
+ -------
208
+ A : numpy.ndarray
209
+ The updated cost matrix.
210
+ b : numpy.ndarray
211
+ The updated right-hand-side vector.
212
+ counter : int
213
+ The updated row index (increased by 2 if chemical exergy is enabled, or by 1 otherwise).
214
+ equations : list or dict
215
+ Updated structure with equation labels.
216
+ """
217
+ # --- Chemical equality equation (row added only if enabled) ---
218
+ if chemical_exergy_enabled:
219
+ # Set the chemical cost equality:
220
+ A[counter, self.inl[0]["CostVar_index"]["CH"]] = (1 / self.inl[0]["E_CH"]) if self.inl[0]["e_CH"] != 0 else 1
221
+ A[counter, self.outl[0]["CostVar_index"]["CH"]] = (-1 / self.outl[0]["E_CH"]) if self.outl[0]["e_CH"] != 0 else 1
222
+ equations[counter] = f"aux_equality_chem_{self.outl[0]['name']}"
223
+ chem_row = 1
224
+ else:
225
+ chem_row = 0
226
+
227
+ # --- Thermal/Mechanical cost equation ---
228
+ # Compute differences in thermal and mechanical exergy:
229
+ dET = self.outl[0]["E_T"] - self.inl[0]["E_T"]
230
+ dEM = self.outl[0]["E_M"] - self.inl[0]["E_M"]
231
+
232
+ # The row for the thermal/mechanical equation:
233
+ row_index = counter + chem_row
234
+ if self.inl[0]["T"] > T0 and self.outl[0]["T"] > T0:
235
+ if dET != 0 and dEM != 0:
236
+ A[row_index, self.inl[0]["CostVar_index"]["T"]] = -1 / dET
237
+ A[row_index, self.outl[0]["CostVar_index"]["T"]] = 1 / dET
238
+ A[row_index, self.inl[0]["CostVar_index"]["M"]] = 1 / dEM
239
+ A[row_index, self.outl[0]["CostVar_index"]["M"]] = -1 / dEM
240
+ equations[row_index] = f"aux_p_rule_{self.name}"
241
+ else:
242
+ logging.warning("Case where thermal or mechanical exergy difference is zero is not implemented.")
243
+ elif self.inl[0]["T"] <= T0 and self.outl[0]["T"] > T0:
244
+ A[row_index, self.outl[0]["CostVar_index"]["T"]] = 1 / self.outl[0]["E_T"]
245
+ A[row_index, self.inl[0]["CostVar_index"]["M"]] = 1 / dEM
246
+ A[row_index, self.outl[0]["CostVar_index"]["M"]] = -1 / dEM
247
+ equations[row_index] = f"aux_p_rule_{self.name}"
248
+ else:
249
+ A[row_index, self.inl[0]["CostVar_index"]["T"]] = -1 / self.inl[0]["E_T"]
250
+ A[row_index, self.outl[0]["CostVar_index"]["T"]] = 1 / self.outl[0]["E_T"]
251
+ equations[row_index] = f"aux_f_rule_{self.name}"
252
+
253
+ # Set the right-hand side entry for the thermal/mechanical row to zero.
254
+ b[row_index] = 0
255
+
256
+ # Update the counter accordingly.
257
+ if chemical_exergy_enabled:
258
+ new_counter = counter + 2
259
+ else:
260
+ new_counter = counter + 1
261
+
262
+ return A, b, new_counter, equations
263
+
264
+ def exergoeconomic_balance(self, T0):
265
+ """
266
+ Perform exergoeconomic balance calculations for the pump.
267
+
268
+ This method calculates various exergoeconomic parameters including:
269
+ - Cost rates of product (C_P) and fuel (C_F)
270
+ - Specific cost of product (c_P) and fuel (c_F)
271
+ - Cost rate of exergy destruction (C_D)
272
+ - Relative cost difference (r)
273
+ - Exergoeconomic factor (f)
274
+
275
+ Parameters
276
+ ----------
277
+ T0 : float
278
+ Ambient temperature
279
+
280
+ Notes
281
+ -----
282
+ The exergoeconomic balance considers thermal (T), chemical (CH),
283
+ and mechanical (M) exergy components for the inlet and outlet streams.
284
+ """
285
+ # Retrieve the cost of power from the inlet stream of kind "power"
286
+ power_cost = None
287
+ for stream in self.inl.values():
288
+ if stream.get("kind") == "power":
289
+ power_cost = stream.get("C_TOT")
290
+ break
291
+ if power_cost is None:
292
+ logging.error("No inlet power stream found to determine power cost (C_TOT).")
293
+ raise ValueError("No inlet power stream found for exergoeconomic_balance.")
294
+
295
+ # Compute product and fuel costs depending on inlet/outlet temperatures relative to T0.
296
+ if self.inl[0]["T"] >= T0 and self.outl[0]["T"] >= T0:
297
+ self.C_P = self.outl[0]["C_PH"] - self.inl[0]["C_PH"]
298
+ self.C_F = power_cost
299
+ elif self.inl[0]["T"] <= T0 and self.outl[0]["T"] > T0:
300
+ self.C_P = self.outl[0]["C_T"] + (self.outl[0]["C_M"] - self.inl[0]["C_M"])
301
+ self.C_F = power_cost + self.inl[0]["C_T"]
302
+ elif self.inl[0]["T"] <= T0 and self.outl[0]["T"] <= T0:
303
+ self.C_P = self.outl[0]["C_M"] - self.inl[0]["C_M"]
304
+ self.C_F = power_cost + (self.inl[0]["C_T"] - self.outl[0]["C_T"])
305
+
306
+ self.c_F = self.C_F / self.E_F
307
+ self.c_P = self.C_P / self.E_P
308
+ self.C_D = self.c_F * self.E_D
309
+ self.r = (self.C_P - self.C_F) / self.C_F
310
+ self.f = self.Z_costs / (self.Z_costs + self.C_D)
@@ -0,0 +1,351 @@
1
+ import logging
2
+
3
+ import numpy as np
4
+
5
+ from exerpy.components.component import Component
6
+ from exerpy.components.component import component_registry
7
+
8
+
9
+ @component_registry
10
+ class Turbine(Component):
11
+ r"""
12
+ Class for exergy analysis of turbines.
13
+
14
+ This class performs exergy analysis calculations for turbines, with definitions
15
+ of exergy product and fuel varying based on the temperature relationships between
16
+ inlet stream, outlet stream, and ambient conditions.
17
+
18
+ Parameters
19
+ ----------
20
+ **kwargs : dict
21
+ Arbitrary keyword arguments passed to parent class.
22
+
23
+ Attributes
24
+ ----------
25
+ E_F : float
26
+ Exergy fuel of the component :math:`\dot{E}_\mathrm{F}` in :math:`\mathrm{W}`.
27
+ E_P : float
28
+ Exergy product of the component :math:`\dot{E}_\mathrm{P}` in :math:`\mathrm{W}`.
29
+ E_D : float
30
+ Exergy destruction of the component :math:`\dot{E}_\mathrm{D}` in :math:`\mathrm{W}`.
31
+ epsilon : float
32
+ Exergetic efficiency of the component :math:`\varepsilon` in :math:`-`.
33
+ P : float
34
+ Power output of the turbine in :math:`\mathrm{W}`.
35
+ inl : dict
36
+ Dictionary containing inlet stream data with temperature, mass flows,
37
+ enthalpies, and specific exergies. Must have at least one inlet.
38
+ outl : dict
39
+ Dictionary containing outlet streams data with temperature, mass flows,
40
+ enthalpies, and specific exergies. Can have multiple outlets, their
41
+ properties will be summed up in the calculations.
42
+
43
+ Notes
44
+ -----
45
+ The exergy analysis considers three cases based on temperature relationships:
46
+
47
+ .. math::
48
+
49
+ \dot{E}_\mathrm{P} =
50
+ \begin{cases}
51
+ -P & T_\mathrm{in}, T_\mathrm{out} \geq T_0\\
52
+ -P + \dot{E}_\mathrm{out}^\mathrm{T}
53
+ & T_\mathrm{in} > T_0 \geq T_\mathrm{out}\\
54
+ -P + \dot{E}_\mathrm{out}^\mathrm{T} - \dot{E}_\mathrm{in}^\mathrm{T}
55
+ & T_0 \geq T_\mathrm{in}, T_\mathrm{out}
56
+ \end{cases}
57
+
58
+ \dot{E}_\mathrm{F} =
59
+ \begin{cases}
60
+ \dot{E}_\mathrm{in}^\mathrm{PH} - \dot{E}_\mathrm{out}^\mathrm{PH}
61
+ & T_\mathrm{in}, T_\mathrm{out} \geq T_0\\
62
+ \dot{E}_\mathrm{in}^\mathrm{T} + \dot{E}_\mathrm{in}^\mathrm{M} -
63
+ \dot{E}_\mathrm{out}^\mathrm{M}
64
+ & T_\mathrm{in} > T_0 \geq T_\mathrm{out}\\
65
+ \dot{E}_\mathrm{in}^\mathrm{M} - \dot{E}_\mathrm{out}^\mathrm{M}
66
+ & T_0 \geq T_\mathrm{in}, T_\mathrm{out}
67
+ \end{cases}
68
+ """
69
+
70
+ def __init__(self, **kwargs):
71
+ r"""Initialize turbine component with given parameters."""
72
+ super().__init__(**kwargs)
73
+ self.P = None
74
+
75
+ def calc_exergy_balance(self, T0: float, p0: float, split_physical_exergy) -> None:
76
+ r"""
77
+ Calculate the exergy balance of the turbine.
78
+
79
+ Performs exergy balance calculations considering the temperature relationships
80
+ between inlet stream, outlet stream, and ambient conditions.
81
+
82
+ Parameters
83
+ ----------
84
+ T0 : float
85
+ Ambient temperature in :math:`\mathrm{K}`.
86
+ p0 : float
87
+ Ambient pressure in :math:`\mathrm{Pa}`.
88
+ split_physical_exergy : bool
89
+ Flag indicating whether physical exergy is split into thermal and mechanical components.
90
+ """
91
+ # Get power flow if not already available
92
+ if self.P is None:
93
+ self.P = self._total_outlet('m', 'h') - self.inl[0]['m'] * self.inl[0]['h']
94
+
95
+ # Case 1: Both temperatures above ambient
96
+ if self.inl[0]['T'] >= T0 and self.outl[0]['T'] >= T0 and self.inl[0]['T'] >= self.outl[0]['T']:
97
+ self.E_P = abs(self.P)
98
+ self.E_F = (self.inl[0]['m'] * self.inl[0]['e_PH'] -
99
+ self._total_outlet('m', 'e_PH'))
100
+
101
+ # Case 2: Inlet above, outlet at/below ambient
102
+ elif self.inl[0]['T'] > T0 and self.outl[0]['T'] <= T0:
103
+ if split_physical_exergy:
104
+ self.E_P = abs(self.P) + self._total_outlet('m', 'e_T')
105
+ self.E_F = (self.inl[0]['m'] * self.inl[0]['e_T'] +
106
+ self.inl[0]['m'] * self.inl[0]['e_M'] -
107
+ self._total_outlet('m', 'e_M'))
108
+ else:
109
+ logging.warning("While dealing with expander below ambient, "
110
+ "physical exergy should be split into thermal and mechanical components!")
111
+ self.E_P = np.nan
112
+ self.E_F = np.nan
113
+
114
+ # Case 3: Both temperatures at/below ambient
115
+ elif self.inl[0]['T'] <= T0 and self.outl[0]['T'] <= T0:
116
+ if split_physical_exergy:
117
+ self.E_P = abs(self.P) + (
118
+ self._total_outlet('m', 'e_T') - self.inl[0]['m'] * self.inl[0]['e_T'])
119
+ self.E_F = (self.inl[0]['m'] * self.inl[0]['e_M'] -
120
+ self._total_outlet('m', 'e_M'))
121
+ else:
122
+ logging.warning("While dealing with expander below ambient, "
123
+ "physical exergy should be split into thermal and mechanical components!")
124
+ self.E_P = np.nan
125
+ self.E_F = np.nan
126
+ # Invalid case: outlet temperature larger than inlet
127
+ else:
128
+ logging.warning(
129
+ 'Exergy balance of a turbine where outlet temperature is larger '
130
+ 'than inlet temperature is not implemented.'
131
+ )
132
+ self.E_P = np.nan
133
+ self.E_F = np.nan
134
+
135
+ # Calculate exergy destruction and efficiency
136
+ self.E_D = self.E_F - self.E_P
137
+ if self.E_F == np.nan:
138
+ self.E_D = self.inl[0]['m'] * self.inl[0]['e_PH'] - self._total_outlet('m', 'e_PH') - abs(self.P)
139
+ self.epsilon = self.calc_epsilon()
140
+
141
+ # Log the results
142
+ logging.info(
143
+ f"Turbine exergy balance calculated: "
144
+ f"E_P={self.E_P:.2f}, E_F={self.E_F:.2f}, E_D={self.E_D:.2f}, "
145
+ f"Efficiency={self.epsilon:.2%}"
146
+ )
147
+
148
+ def _total_outlet(self, mass_flow: str, property_name: str) -> float:
149
+ r"""
150
+ Calculate the sum of mass flow times property across all outlets.
151
+
152
+ Parameters
153
+ ----------
154
+ mass_flow : str
155
+ Key for the mass flow value.
156
+ property_name : str
157
+ Key for the property to be summed.
158
+
159
+ Returns
160
+ -------
161
+ float
162
+ Sum of mass flow times property across all outlets.
163
+ """
164
+ total = 0.0
165
+ for outlet in self.outl.values():
166
+ if outlet and mass_flow in outlet and property_name in outlet:
167
+ total += outlet[mass_flow] * outlet[property_name]
168
+ return total
169
+
170
+
171
+ def aux_eqs(self, A, b, counter, T0, equations, chemical_exergy_enabled):
172
+ """
173
+ Auxiliary equations for the turbine.
174
+
175
+ This function adds rows to the cost matrix A and the right-hand-side vector b to enforce
176
+ the following auxiliary cost relations:
177
+
178
+ For each material outlet (when inlet and first outlet are above ambient temperature T0):
179
+
180
+ (1) 1/E_T_in * C_T_in - 1/E_T_out * C_T_out = 0
181
+ - F-principle: specific thermal exergy costs equalized between inlet and each outlet
182
+
183
+ (2) 1/E_M_in * C_M_in - 1/E_M_out * C_M_out = 0
184
+ - F-principle: specific mechanical exergy costs equalized between inlet and each outlet
185
+
186
+ (3) 1/E_CH_in * C_CH_in - 1/E_CH_out * C_CH_out = 0 (if chemical_exergy_enabled)
187
+ - F-principle: specific chemical exergy costs equalized between inlet and each outlet
188
+
189
+ For power outlets (with both source and target components):
190
+
191
+ (4) 1/E_ref * C_ref - 1/E_out * C_out = 0
192
+ - P-principle: specific power exergy costs equalized across all power outlets
193
+
194
+ Parameters
195
+ ----------
196
+ A : numpy.ndarray
197
+ The current cost matrix.
198
+ b : numpy.ndarray
199
+ The current right-hand-side vector.
200
+ counter : int
201
+ The current row index in the matrix.
202
+ T0 : float
203
+ Ambient temperature.
204
+ equations : list or dict
205
+ Data structure for storing equation labels.
206
+ chemical_exergy_enabled : bool
207
+ Flag indicating whether chemical exergy auxiliary equations should be added.
208
+
209
+ Returns
210
+ -------
211
+ A : numpy.ndarray
212
+ The updated cost matrix.
213
+ b : numpy.ndarray
214
+ The updated right-hand-side vector.
215
+ counter : int
216
+ The updated row index after adding all auxiliary equations.
217
+ equations : list or dict
218
+ Updated structure with equation labels.
219
+ """
220
+ # Process only if the inlet and the first outlet are above T0.
221
+ if self.inl[0]["T"] > T0 and self.outl[0]["T"] > T0:
222
+ # Filter material outlets
223
+ material_outlets = [outlet for outlet in self.outl.values() if outlet.get("kind") == "material"]
224
+ # Determine number of rows per outlet.
225
+ num_rows_per_outlet = 3 if chemical_exergy_enabled else 2
226
+
227
+ for i, outlet in enumerate(material_outlets):
228
+ row_offset = num_rows_per_outlet * i
229
+
230
+ # --- Thermal exergy equation ---
231
+ A[counter + row_offset, self.inl[0]["CostVar_index"]["T"]] = (
232
+ 1 / self.inl[0]["E_T"] if self.inl[0]["e_T"] != 0 else 1
233
+ )
234
+ A[counter + row_offset, outlet["CostVar_index"]["T"]] = (
235
+ -1 / outlet["E_T"] if outlet["e_T"] != 0 else -1
236
+ )
237
+ equations[counter + row_offset] = f"aux_f_rule_{outlet['name']}"
238
+
239
+ # --- Mechanical exergy equation ---
240
+ A[counter + row_offset + 1, self.inl[0]["CostVar_index"]["M"]] = (
241
+ 1 / self.inl[0]["E_M"] if self.inl[0]["e_M"] != 0 else 1
242
+ )
243
+ A[counter + row_offset + 1, outlet["CostVar_index"]["M"]] = (
244
+ -1 / outlet["E_M"] if outlet["e_M"] != 0 else -1
245
+ )
246
+ equations[counter + row_offset + 1] = f"aux_f_rule_{outlet['name']}"
247
+
248
+ # --- Chemical exergy equation (conditionally added) ---
249
+ if chemical_exergy_enabled:
250
+ A[counter + row_offset + 2, self.inl[0]["CostVar_index"]["CH"]] = (
251
+ 1 / self.inl[0]["E_CH"] if self.inl[0]["e_CH"] != 0 else 1
252
+ )
253
+ A[counter + row_offset + 2, outlet["CostVar_index"]["CH"]] = (
254
+ -1 / outlet["E_CH"] if outlet["e_CH"] != 0 else -1
255
+ )
256
+ equations[counter + row_offset + 2] = f"aux_f_rule_{outlet['name']}"
257
+
258
+ # Update counter based on number of rows added for all material outlets.
259
+ num_material_rows = num_rows_per_outlet * len(material_outlets)
260
+ for j in range(num_material_rows):
261
+ b[counter + j] = 0
262
+ counter += num_material_rows
263
+ else:
264
+ logging.warning("Turbine with outlet below T0 not implemented in exergoeconomics yet!")
265
+
266
+ # --- Auxiliary equation for shaft power equality ---
267
+ power_outlets = [outlet for outlet in self.outl.values()
268
+ if outlet.get("kind") == "power" and outlet.get("source_component") and outlet.get("target_component")]
269
+ if len(power_outlets) > 1:
270
+ ref = power_outlets[0]
271
+ ref_idx = ref["CostVar_index"]["exergy"]
272
+ for outlet in power_outlets[1:]:
273
+ cur_idx = outlet["CostVar_index"]["exergy"]
274
+ A[counter, ref_idx] = 1 / ref["E"] if ref["E"] != 0 else 1
275
+ A[counter, cur_idx] = -1 / outlet["E"] if outlet["E"] != 0 else -1
276
+ b[counter] = 0
277
+ equations[counter] = f"aux_p_rule_power_{self.name}_{outlet['name']}"
278
+ counter += 1
279
+
280
+ return A, b, counter, equations
281
+
282
+ def exergoeconomic_balance(self, T0):
283
+ """
284
+ Perform exergoeconomic balance calculations for the turbine.
285
+
286
+ The turbine may have multiple power outputs and multiple material outputs. In this
287
+ function the cost of power is computed as the sum of C_TOT from all inlet streams of kind "power".
288
+ Material outlet costs are summed over all outlets of kind "material". The cost balance is then
289
+ computed according to the following cases:
290
+
291
+ Case 1 (both inlet and first outlet above T0):
292
+ C_P = (total power cost)
293
+ C_F = C_PH_inlet - (sum of C_PH from material outlets)
294
+
295
+ Case 2 (inlet above T0, outlet at or below T0):
296
+ C_P = (total power cost) + (sum of C_T from material outlets)
297
+ C_F = C_T_inlet + (C_M_inlet - (sum of C_M from material outlets))
298
+
299
+ Case 3 (both inlet and outlet at or below T0):
300
+ C_P = (total power cost) + ((sum of C_T from material outlets) - C_T_inlet)
301
+ C_F = C_M_inlet - (sum of C_M from material outlets)
302
+
303
+ Finally, the specific fuel cost (c_F), specific product cost (c_P), total cost destruction (C_D),
304
+ relative difference (r), and exergoeconomic factor (f) are calculated.
305
+
306
+ Parameters
307
+ ----------
308
+ T0 : float
309
+ Ambient temperature.
310
+
311
+ Raises
312
+ ------
313
+ ValueError
314
+ If required cost values are missing.
315
+ """
316
+ # Sum the cost of all outlet power streams.
317
+ C_power_out = sum(stream.get("C_TOT", 0) for stream in self.outl.values() if stream.get("kind") == "power")
318
+ # Assume a single primary inlet for material cost properties.
319
+ inlet = self.inl[0]
320
+ # Filter material outlets and sum their cost components.
321
+ material_outlets = [out for out in self.outl.values() if out.get("kind") == "material"]
322
+ sum_C_PH_out = sum(out.get("C_PH", 0) for out in material_outlets)
323
+ sum_C_T_out = sum(out.get("C_T", 0) for out in material_outlets)
324
+ sum_C_M_out = sum(out.get("C_M", 0) for out in material_outlets)
325
+
326
+ # Case 1: Both inlet and first outlet above ambient.
327
+ if inlet["T"] >= T0 and self.outl[0]["T"] >= T0:
328
+ self.C_P = C_power_out
329
+ self.C_F = inlet.get("C_PH", 0) - sum_C_PH_out
330
+
331
+ # Case 2: Inlet above ambient and outlet at or below ambient.
332
+ elif inlet["T"] > T0 and self.outl[0]["T"] <= T0:
333
+ self.C_P = C_power_out + sum_C_T_out
334
+ self.C_F = inlet.get("C_T", 0) + (inlet.get("C_M", 0) - sum_C_M_out)
335
+
336
+ # Case 3: Both inlet and outlet at or below ambient.
337
+ elif inlet["T"] <= T0 and self.outl[0]["T"] <= T0:
338
+ self.C_P = C_power_out + (sum_C_T_out - inlet.get("C_T", 0))
339
+ self.C_F = inlet.get("C_M", 0) - sum_C_M_out
340
+
341
+ else:
342
+ logging.warning("Exergoeconomic balance of a turbine with outlet temperature larger than inlet is not implemented.")
343
+ self.C_P = np.nan
344
+ self.C_F = np.nan
345
+
346
+ # Calculate the specific cost terms and exergoeconomic parameters.
347
+ self.c_F = self.C_F / self.E_F
348
+ self.c_P = self.C_P / self.E_P
349
+ self.C_D = self.c_F * self.E_D
350
+ self.r = (self.C_P - self.C_F) / self.C_F
351
+ self.f = self.Z_costs / (self.Z_costs + self.C_D)