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,323 @@
1
+ import logging
2
+
3
+ from exerpy.components.component import Component
4
+ from exerpy.components.component import component_registry
5
+
6
+
7
+ @component_registry
8
+ class Condenser(Component):
9
+ """
10
+ Condenser component class.
11
+
12
+ This class represents a condenser within the system, responsible for
13
+ calculating the exergy balance specific to condensation processes.
14
+ It evaluates the exergy interactions between multiple inlet and outlet
15
+ streams to determine exergy loss and exergy destruction.
16
+
17
+ Attributes
18
+ ----------
19
+ E_L : float
20
+ Exergy loss associated with heat transfer (difference in physical exergy
21
+ between specific outlet and inlet streams).
22
+ E_D : float
23
+ Exergy destruction, calculated as the difference between the primary
24
+ inlet and outlet streams minus exergy loss (E_L), representing
25
+ irreversibilities in the condensation process.
26
+ E_P : None
27
+ Exergy product, not defined for a condenser as there is no exergy output
28
+ intended for productive use.
29
+ E_F : None
30
+ Exergy fuel, typically undefined for a condenser as it does not involve
31
+ an external exergy input for production purposes.
32
+ epsilon : None
33
+ Exergy efficiency, not applicable to a condenser due to the nature of
34
+ exergy interactions focused on loss and destruction.
35
+
36
+ Methods
37
+ -------
38
+ __init__(**kwargs)
39
+ Initializes the Condenser component with given parameters.
40
+ calc_exergy_balance(T0, p0)
41
+ Calculates the exergy balance of the condenser.
42
+ """
43
+
44
+ def __init__(self, **kwargs):
45
+ """
46
+ Initialize the Condenser component.
47
+
48
+ Parameters
49
+ ----------
50
+ **kwargs : dict
51
+ Arbitrary keyword arguments passed to the base class initializer.
52
+ """
53
+ super().__init__(**kwargs)
54
+
55
+ def calc_exergy_balance(self, T0: float, p0: float, split_physical_exergy) -> None:
56
+ """
57
+ Calculate the exergy balance of the condenser.
58
+
59
+ This method computes exergy loss and exergy destruction based on the inlet
60
+ and outlet streams involved in the condensation process.
61
+
62
+ Parameters
63
+ ----------
64
+ T0 : float
65
+ Reference temperature in Kelvin.
66
+ p0 : float
67
+ Reference pressure in Pascals.
68
+ split_physical_exergy : bool
69
+ Flag indicating whether physical exergy is split into thermal and mechanical components.
70
+
71
+ Raises
72
+ ------
73
+ ValueError
74
+ If the condenser does not have exactly two inlets and two outlets.
75
+
76
+ Calculation Details
77
+ -------------------
78
+ The exergy balance is determined based on exergy transfer due to heat loss (`E_L`)
79
+ and the exergy destruction within the system:
80
+
81
+ - **Exergy Loss (E_L)**:
82
+ \[
83
+ E_L = \dot{m}_{\mathrm{out,1}} \cdot (e_{\mathrm{PH,out,1}} - e_{\mathrm{PH,in,1}})
84
+ \]
85
+ Represents the exergy loss due to heat transfer from the process.
86
+
87
+ - **Exergy Destruction (E_D)**:
88
+ \[
89
+ E_D = \dot{m}_{\mathrm{out,0}} \cdot (e_{\mathrm{PH,in,0}} - e_{\mathrm{PH,out,0}}) - E_L
90
+ \]
91
+ Accounts for the irreversibilities and losses in the condenser.
92
+
93
+ Note
94
+ ----
95
+ Exergy product (E_P) and exergy fuel (E_F) are generally undefined in a
96
+ condenser due to the focus on exergy loss rather than productive exergy usage.
97
+ """
98
+ # Ensure that the component has both inlet and outlet streams
99
+ if len(self.inl) < 2 or len(self.outl) < 2:
100
+ raise ValueError("Condenser requires two inlets and two outlets.")
101
+
102
+ # Calculate exergy loss (E_L) for the heat transfer process
103
+ self.E_L = self.outl[1]['m'] * (self.outl[1]['e_PH'] - self.inl[1]['e_PH'])
104
+
105
+ # Calculate exergy destruction (E_D)
106
+ self.E_D = self.outl[0]['m'] * (self.inl[0]['e_PH'] - self.outl[0]['e_PH']) - self.E_L
107
+
108
+ # Exergy fuel and product are not typically defined for a condenser
109
+ self.E_F = None
110
+ self.E_P = None
111
+ self.epsilon = None
112
+
113
+ # Log the exergy balance results
114
+ logging.info(f"Condenser exergy balance calculated: E_D={self.E_D}, E_L={self.E_L}")
115
+
116
+
117
+ def aux_eqs(self, A, b, counter, T0, equations, chemical_exergy_enabled):
118
+ """
119
+ Auxiliary equations for the condenser.
120
+
121
+ This function adds rows to the cost matrix A and the right-hand-side vector b to enforce
122
+ the following auxiliary cost relations:
123
+
124
+ (1) Thermal auxiliary equation based on temperature cases:
125
+ - Case 1 (T > T0): c_T(hot_inlet)/E_T(hot_in) = c_T(hot_outlet)/E_T(hot_out)
126
+ F-principle: specific thermal exergy costs equalized in hot stream
127
+ - Case 2 (T <= T0): c_T(cold_inlet)/E_T(cold_in) = c_T(cold_outlet)/E_T(cold_out)
128
+ F-principle: specific thermal exergy costs equalized in cold stream
129
+ - Case 3 (mixed temperatures): c_T(hot_outlet)/E_T(hot_out) = c_T(cold_outlet)/E_T(cold_out)
130
+ P-principle: equal specific costs of thermal exergy in outlets
131
+
132
+ (2) c_M(hot_inlet)/E_M(hot_in) = c_M(hot_outlet)/E_M(hot_out)
133
+ - F-principle: specific mechanical exergy costs equalized in hot stream
134
+
135
+ (3) c_M(cold_inlet)/E_M(cold_in) = c_M(cold_outlet)/E_M(cold_out)
136
+ - F-principle: specific mechanical exergy costs equalized in cold stream
137
+
138
+ (4-5) Chemical exergy cost equations (if enabled) for hot and cold streams
139
+ - F-principle: specific chemical exergy costs equalized between inlets/outlets
140
+
141
+ Parameters
142
+ ----------
143
+ A : numpy.ndarray
144
+ The current cost matrix.
145
+ b : numpy.ndarray
146
+ The current right-hand-side vector.
147
+ counter : int
148
+ The current row index in the matrix.
149
+ T0 : float
150
+ Ambient temperature.
151
+ equations : dict
152
+ Dictionary for storing equation labels.
153
+ chemical_exergy_enabled : bool
154
+ Flag indicating whether chemical exergy auxiliary equations should be added.
155
+
156
+ Returns
157
+ -------
158
+ A : numpy.ndarray
159
+ The updated cost matrix.
160
+ b : numpy.ndarray
161
+ The updated right-hand-side vector.
162
+ counter : int
163
+ The updated row index.
164
+ equations : dict
165
+ Updated dictionary with equation labels.
166
+ """
167
+ # Equality equation for mechanical and chemical exergy costs.
168
+ def set_equal(A, row, in_item, out_item, var):
169
+ if in_item["e_" + var] != 0 and out_item["e_" + var] != 0:
170
+ A[row, in_item["CostVar_index"][var]] = 1 / in_item["e_" + var]
171
+ A[row, out_item["CostVar_index"][var]] = -1 / out_item["e_" + var]
172
+ elif in_item["e_" + var] == 0 and out_item["e_" + var] != 0:
173
+ A[row, in_item["CostVar_index"][var]] = 1
174
+ elif in_item["e_" + var] != 0 and out_item["e_" + var] == 0:
175
+ A[row, out_item["CostVar_index"][var]] = 1
176
+ else:
177
+ A[row, in_item["CostVar_index"][var]] = 1
178
+ A[row, out_item["CostVar_index"][var]] = -1
179
+
180
+ # Thermal fuel rule on hot stream: c_T_in0 = c_T_out0.
181
+ def set_thermal_f_hot(A, row):
182
+ if self.inl[0]["e_T"] != 0 and self.outl[0]["e_T"] != 0:
183
+ A[row, self.inl[0]["CostVar_index"]["T"]] = 1 / self.inl[0]["E_T"]
184
+ A[row, self.outl[0]["CostVar_index"]["T"]] = -1 / self.outl[0]["E_T"]
185
+ elif self.inl[0]["e_T"] == 0 and self.outl[0]["e_T"] != 0:
186
+ A[row, self.inl[0]["CostVar_index"]["T"]] = 1
187
+ elif self.inl[0]["e_T"] != 0 and self.outl[0]["e_T"] == 0:
188
+ A[row, self.outl[0]["CostVar_index"]["T"]] = 1
189
+ else:
190
+ A[row, self.inl[0]["CostVar_index"]["T"]] = 1
191
+ A[row, self.outl[0]["CostVar_index"]["T"]] = -1
192
+
193
+ # Thermal fuel rule on cold stream: c_T_in1 = c_T_out1.
194
+ def set_thermal_f_cold(A, row):
195
+ if self.inl[1]["e_T"] != 0 and self.outl[1]["e_T"] != 0:
196
+ A[row, self.inl[1]["CostVar_index"]["T"]] = 1 / self.inl[1]["E_T"]
197
+ A[row, self.outl[1]["CostVar_index"]["T"]] = -1 / self.outl[1]["E_T"]
198
+ elif self.inl[1]["e_T"] == 0 and self.outl[1]["e_T"] != 0:
199
+ A[row, self.inl[1]["CostVar_index"]["T"]] = 1
200
+ elif self.inl[1]["e_T"] != 0 and self.outl[1]["e_T"] == 0:
201
+ A[row, self.outl[1]["CostVar_index"]["T"]] = 1
202
+ else:
203
+ A[row, self.inl[1]["CostVar_index"]["T"]] = 1
204
+ A[row, self.outl[1]["CostVar_index"]["T"]] = -1
205
+
206
+ # Thermal product rule: Equate the two outlet thermal costs (c_T_out0 = c_T_out1).
207
+ def set_thermal_p_rule(A, row):
208
+ if self.outl[0]["e_T"] != 0 and self.outl[1]["e_T"] != 0:
209
+ A[row, self.outl[0]["CostVar_index"]["T"]] = 1 / self.outl[0]["E_T"]
210
+ A[row, self.outl[1]["CostVar_index"]["T"]] = -1 / self.outl[1]["E_T"]
211
+ elif self.outl[0]["e_T"] == 0 and self.outl[1]["e_T"] != 0:
212
+ A[row, self.outl[0]["CostVar_index"]["T"]] = 1
213
+ elif self.outl[0]["e_T"] != 0 and self.outl[1]["e_T"] == 0:
214
+ A[row, self.outl[1]["CostVar_index"]["T"]] = 1
215
+ else:
216
+ A[row, self.outl[0]["CostVar_index"]["T"]] = 1
217
+ A[row, self.outl[1]["CostVar_index"]["T"]] = -1
218
+
219
+ # Determine the thermal case based on temperatures.
220
+ # Case 1: All temperatures > T0.
221
+ if all([c["T"] > T0 for c in list(self.inl.values()) + list(self.outl.values())]):
222
+ set_thermal_f_hot(A, counter + 0)
223
+ equations[counter] = f"aux_f_rule_hot_{self.name}"
224
+ # Case 2: All temperatures <= T0.
225
+ elif all([c["T"] <= T0 for c in self.inl + self.outl]):
226
+ set_thermal_f_cold(A, counter + 0)
227
+ equations[counter] = f"aux_f_rule_cold_{self.name}"
228
+ # Case 3: Mixed temperatures: inl[0]["T"] > T0 and outl[1]["T"] > T0, while outl[0]["T"] <= T0 and inl[1]["T"] <= T0.
229
+ elif (self.inl[0]["T"] > T0 and self.outl[1]["T"] > T0 and
230
+ self.outl[0]["T"] <= T0 and self.inl[1]["T"] <= T0):
231
+ set_thermal_p_rule(A, counter + 0)
232
+ equations[counter] = f"aux_p_rule_{self.name}"
233
+ # Case 4: Mixed temperatures: inl[0]["T"] > T0, inl[1]["T"] <= T0, and both outl[0]["T"] and outl[1]["T"] <= T0.
234
+ elif (self.inl[0]["T"] > T0 and self.inl[1]["T"] <= T0 and
235
+ self.outl[0]["T"] <= T0 and self.outl[1]["T"] <= T0):
236
+ set_thermal_f_cold(A, counter + 0)
237
+ equations[counter] = f"aux_f_rule_cold_{self.name}"
238
+ # Case 5: Mixed temperatures: inl[0]["T"] > T0, inl[1]["T"] <= T0, and both outl[0]["T"] and outl[1]["T"] > T0.
239
+ elif (self.inl[0]["T"] > T0 and self.inl[1]["T"] <= T0 and
240
+ self.outl[0]["T"] > T0 and self.outl[1]["T"] > T0):
241
+ set_thermal_f_hot(A, counter + 0)
242
+ equations[counter] = f"aux_f_rule_hot_{self.name}"
243
+ # Case 6: Mixed temperatures (dissipative case): inl[0]["T"] > T0, inl[1]["T"] <= T0, outl[0]["T"] > T0, and outl[1]["T"] <= T0.
244
+ elif (self.inl[0]["T"] > T0 and self.inl[1]["T"] <= T0 and
245
+ self.outl[0]["T"] > T0 and self.outl[1]["T"] <= T0):
246
+ print("you shouldn't see this")
247
+ return
248
+ # Case 7: Default case.
249
+ else:
250
+ set_thermal_f_hot(A, counter + 0)
251
+ equations[counter] = f"aux_f_rule_hot_{self.name}"
252
+
253
+ # Mechanical equations (always added)
254
+ set_equal(A, counter + 1, self.inl[0], self.outl[0], "M")
255
+ set_equal(A, counter + 2, self.inl[1], self.outl[1], "M")
256
+ equations[counter + 1] = f"aux_equality_mech_{self.outl[0]['name']}"
257
+ equations[counter + 2] = f"aux_equality_mech_{self.outl[1]['name']}"
258
+
259
+ # Only add chemical auxiliary equations if chemical exergy is enabled.
260
+ if chemical_exergy_enabled:
261
+ set_equal(A, counter + 3, self.inl[0], self.outl[0], "CH")
262
+ set_equal(A, counter + 4, self.inl[1], self.outl[1], "CH")
263
+ equations[counter + 3] = f"aux_equality_chem_{self.outl[0]['name']}"
264
+ equations[counter + 4] = f"aux_equality_chem_{self.outl[1]['name']}"
265
+ num_aux_eqs = 5
266
+ else:
267
+ # Skip chemical auxiliary equations.
268
+ num_aux_eqs = 3
269
+
270
+ for i in range(num_aux_eqs):
271
+ b[counter + i] = 0
272
+
273
+ return A, b, counter + num_aux_eqs, equations
274
+
275
+ def exergoeconomic_balance(self, T0):
276
+ """
277
+ Perform exergoeconomic balance calculations for the condenser.
278
+
279
+ This method calculates various exergoeconomic parameters including:
280
+ - Cost rates of product (C_P) and fuel (C_F)
281
+ - Specific cost of product (c_P) and fuel (c_F)
282
+ - Cost rate of exergy destruction (C_D)
283
+ - Relative cost difference (r)
284
+ - Exergoeconomic factor (f)
285
+
286
+ Parameters
287
+ ----------
288
+ T0 : float
289
+ Ambient temperature
290
+
291
+ Notes
292
+ -----
293
+ The exergoeconomic balance considers thermal (T), chemical (CH),
294
+ and mechanical (M) exergy components for the inlet and outlet streams.
295
+ """
296
+ if all([c["T"] > T0 for c in list(self.inl.values()) + list(self.outl.values())]):
297
+ self.C_P = self.outl[1]["C_T"] - self.inl[1]["C_T"]
298
+ self.C_F = self.inl[0]["C_PH"] - self.outl[0]["C_PH"] + (
299
+ self.inl[1]["C_M"] - self.outl[1]["C_M"])
300
+ elif all([c["T"] <= T0 for c in list(self.inl.values()) + list(self.outl.values())]):
301
+ self.C_P = self.outl[0]["C_T"] - self.inl[0]["C_T"]
302
+ self.C_F = self.inl[1]["C_PH"] - self.outl[1]["C_PH"] + (
303
+ self.inl[0]["C_M"] - self.outl[0]["C_M"])
304
+ elif (self.inl[0]["T"] > T0 and self.outl[1]["T"] > T0 and
305
+ self.outl[0]["T"] <= T0 and self.inl[1]["T"] <= T0):
306
+ self.C_P = self.outl[0]["C_T"] + self.outl[1]["C_T"]
307
+ self.C_F = self.inl[0]["C_PH"] + self.inl[1]["C_PH"] - (
308
+ self.outl[0]["C_M"] + self.outl[1]["C_M"])
309
+ elif (self.inl[0]["T"] > T0 and self.inl[1]["T"] <= T0 and
310
+ self.outl[0]["T"] <= T0 and self.outl[1]["T"] <= T0):
311
+ self.C_P = self.outl[0]["C_T"]
312
+ self.C_F = self.inl[0]["C_PH"] + self.inl[1]["C_PH"] - (
313
+ self.outl[1]["C_PH"] + self.outl[0]["C_M"])
314
+ else:
315
+ self.C_P = self.outl[1]["C_T"]
316
+ self.C_F = self.inl[0]["C_PH"] - self.outl[0]["C_PH"] + (
317
+ self.inl[1]["C_PH"] - self.outl[1]["C_M"])
318
+
319
+ self.c_F = self.C_F / self.E_F
320
+ self.c_P = self.C_P / self.E_P
321
+ self.C_D = self.c_F * self.E_D
322
+ self.r = (self.c_P - self.c_F) / self.c_F
323
+ self.f = self.Z_costs / (self.Z_costs + self.C_D)
@@ -0,0 +1,358 @@
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 SimpleHeatExchanger(Component):
11
+ r"""
12
+ Class for exergy analysis of simple heat exchangers.
13
+
14
+ This class performs exergy analysis calculations for simple heat exchangers with
15
+ one primary flow stream and heat transfer. The exergy product and fuel definitions
16
+ vary based on the direction of heat transfer and temperature levels relative to
17
+ ambient temperature.
18
+
19
+ Parameters
20
+ ----------
21
+ **kwargs : dict
22
+ Arbitrary keyword arguments passed to parent class.
23
+ Optional parameter 'dissipative' (bool) to indicate if the component
24
+ is considered fully dissipative.
25
+
26
+ Attributes
27
+ ----------
28
+ E_F : float
29
+ Exergy fuel of the component :math:`\dot{E}_\mathrm{F}` in :math:`\mathrm{W}`.
30
+ E_P : float
31
+ Exergy product of the component :math:`\dot{E}_\mathrm{P}` in :math:`\mathrm{W}`.
32
+ E_D : float
33
+ Exergy destruction of the component :math:`\dot{E}_\mathrm{D}` in :math:`\mathrm{W}`.
34
+ epsilon : float
35
+ Exergetic efficiency of the component :math:`\varepsilon` in :math:`-`.
36
+ inl : dict
37
+ Dictionary containing inlet stream data with temperature, mass flows,
38
+ enthalpies, and specific exergies.
39
+ outl : dict
40
+ Dictionary containing outlet stream data with temperature, mass flows,
41
+ enthalpies, and specific exergies.
42
+
43
+ Notes
44
+ -----
45
+ The exergy analysis considers three main cases based on heat transfer direction
46
+ and temperatures relative to ambient temperature :math:`T_0`:
47
+
48
+ Case 1 - **Heat Release** (:math:`\dot{Q} < 0`):
49
+
50
+ a) Both temperatures above ambient:
51
+
52
+ .. math::
53
+ \dot{E}_\mathrm{P} &= \dot{m} \cdot (e^\mathrm{T}_\mathrm{in} -
54
+ e^\mathrm{T}_\mathrm{out})\\
55
+ \dot{E}_\mathrm{F} &= \dot{m} \cdot (e^\mathrm{PH}_\mathrm{in} -
56
+ e^\mathrm{PH}_\mathrm{out})
57
+
58
+ b) Inlet above, outlet below ambient:
59
+
60
+ .. math::
61
+ \dot{E}_\mathrm{P} &= \dot{m}_\mathrm{out} \cdot e^\mathrm{T}_\mathrm{out}\\
62
+ \dot{E}_\mathrm{F} &= \dot{m}_\mathrm{in} \cdot e^\mathrm{T}_\mathrm{in} +
63
+ \dot{m}_\mathrm{out} \cdot e^\mathrm{T}_\mathrm{out} +
64
+ (\dot{m}_\mathrm{in} \cdot e^\mathrm{M}_\mathrm{in} -
65
+ \dot{m}_\mathrm{out} \cdot e^\mathrm{M}_\mathrm{out})
66
+
67
+ c) Both temperatures below ambient:
68
+
69
+ .. math::
70
+ \dot{E}_\mathrm{P} &= \dot{m}_\mathrm{out} \cdot
71
+ (e^\mathrm{T}_\mathrm{out} - e^\mathrm{T}_\mathrm{in})\\
72
+ \dot{E}_\mathrm{F} &= \dot{E}_\mathrm{P} + \dot{m}_\mathrm{in} \cdot
73
+ (e^\mathrm{M}_\mathrm{in} - e^\mathrm{M}_\mathrm{out})
74
+
75
+ Case 2 - **Heat Addition** (:math:`\dot{Q} > 0`):
76
+
77
+ a) Both temperatures above ambient:
78
+
79
+ .. math::
80
+ \dot{E}_\mathrm{P} &= \dot{m}_\mathrm{out} \cdot
81
+ (e^\mathrm{PH}_\mathrm{out} - e^\mathrm{PH}_\mathrm{in})\\
82
+ \dot{E}_\mathrm{F} &= \dot{m}_\mathrm{out} \cdot
83
+ (e^\mathrm{T}_\mathrm{out} - e^\mathrm{T}_\mathrm{in})
84
+
85
+ b) Inlet below, outlet above ambient:
86
+
87
+ .. math::
88
+ \dot{E}_\mathrm{P} &= \dot{m}_\mathrm{out} \cdot
89
+ (e^\mathrm{T}_\mathrm{out} + e^\mathrm{T}_\mathrm{in})\\
90
+ \dot{E}_\mathrm{F} &= \dot{m}_\mathrm{in} \cdot e^\mathrm{T}_\mathrm{in} +
91
+ (\dot{m}_\mathrm{in} \cdot e^\mathrm{M}_\mathrm{in} -
92
+ \dot{m}_\mathrm{out} \cdot e^\mathrm{M}_\mathrm{out})
93
+
94
+ c) Both temperatures below ambient:
95
+
96
+ .. math::
97
+ \dot{E}_\mathrm{P} &= \dot{m}_\mathrm{in} \cdot
98
+ (e^\mathrm{T}_\mathrm{in} - e^\mathrm{T}_\mathrm{out}) +
99
+ (\dot{m}_\mathrm{out} \cdot e^\mathrm{M}_\mathrm{out} -
100
+ \dot{m}_\mathrm{in} \cdot e^\mathrm{M}_\mathrm{in})\\
101
+ \dot{E}_\mathrm{F} &= \dot{m}_\mathrm{in} \cdot
102
+ (e^\mathrm{T}_\mathrm{in} - e^\mathrm{T}_\mathrm{out})
103
+
104
+ Case 3 - **Dissipative** (it is not possible to specify the exergy product :math:`\dot{E}_\mathrm{P}` for this component):
105
+
106
+ .. math::
107
+ \dot{E}_\mathrm{P} &= \mathrm{NaN}\\
108
+ \dot{E}_\mathrm{F} &= \dot{m}_\mathrm{in} \cdot
109
+ (e^\mathrm{PH}_\mathrm{in} - e^\mathrm{PH}_\mathrm{out})
110
+
111
+ For all cases, the exergy destruction is calculated as:
112
+
113
+ .. math::
114
+ \dot{E}_\mathrm{D} = \dot{E}_\mathrm{F} - \dot{E}_\mathrm{P}
115
+
116
+ Where:
117
+ - :math:`e^\mathrm{T}`: Thermal exergy
118
+ - :math:`e^\mathrm{PH}`: Physical exergy
119
+ - :math:`e^\mathrm{M}`: Mechanical exergy
120
+ """
121
+
122
+ def __init__(self, **kwargs):
123
+ r"""Initialize simple heat exchanger component with given parameters."""
124
+ super().__init__(**kwargs)
125
+
126
+ def calc_exergy_balance(self, T0: float, p0: float, split_physical_exergy) -> None:
127
+ r"""
128
+ Calculate the exergy balance of the simple heat exchanger.
129
+
130
+ Performs exergy balance calculations considering both heat transfer direction
131
+ and temperature levels relative to ambient temperature.
132
+
133
+ Parameters
134
+ ----------
135
+ T0 : float
136
+ Ambient temperature in :math:`\mathrm{K}`.
137
+ p0 : float
138
+ Ambient pressure in :math:`\mathrm{Pa}`.
139
+ split_physical_exergy : bool
140
+ Flag indicating whether physical exergy is split into thermal and mechanical components.
141
+
142
+ Raises
143
+ ------
144
+ ValueError
145
+ If the required inlet and outlet streams are not properly defined or
146
+ exceed the maximum allowed number.
147
+ """
148
+ # Validate the number of inlets and outlets
149
+ if not hasattr(self, 'inl') or not hasattr(self, 'outl') or len(self.inl) < 1 or len(self.outl) < 1:
150
+ msg = "SimpleHeatExchanger requires at least one inlet and one outlet as well as one heat flow."
151
+ logging.error(msg)
152
+ raise ValueError(msg)
153
+ if len(self.inl) > 2 or len(self.outl) > 2:
154
+ msg = "SimpleHeatExchanger requires a maximum of two inlets and two outlets."
155
+ logging.error(msg)
156
+ raise ValueError(msg)
157
+
158
+ # Extract inlet and outlet streams
159
+ inlet = self.inl[0]
160
+ outlet = self.outl[0]
161
+
162
+ # Calculate heat transfer Q
163
+ Q = outlet['m'] * outlet['h'] - inlet['m'] * inlet['h']
164
+
165
+ # Initialize E_P and E_F
166
+ self.E_P = 0.0
167
+ self.E_F = 0.0
168
+
169
+ # Case 1: Heat is released (Q < 0)
170
+ if Q < 0:
171
+ if inlet['T'] >= T0 and outlet['T'] >= T0:
172
+ if split_physical_exergy:
173
+ self.E_P = np.nan if getattr(self, 'dissipative', False) else inlet['m'] * (inlet['e_T'] - outlet['e_T'])
174
+ else:
175
+ self.E_P = np.nan if getattr(self, 'dissipative', False) else inlet['m'] * (inlet['e_PH'] - outlet['e_PH'])
176
+ self.E_F = inlet['m'] * (inlet['e_PH'] - outlet['e_PH'])
177
+
178
+ elif inlet['T'] >= T0 and outlet['T'] < T0:
179
+ if split_physical_exergy:
180
+ self.E_P = outlet['m'] * outlet['e_T']
181
+ self.E_F = (inlet['m'] * inlet['e_T'] + outlet['m'] * outlet['e_T'] +
182
+ (inlet['m'] * inlet['e_M'] - outlet['m'] * outlet['e_M']))
183
+ else:
184
+ self.E_P = outlet['m'] * outlet['e_PH']
185
+ self.E_F = inlet['m'] * inlet['e_PH']
186
+
187
+ elif inlet['T'] <= T0 and outlet['T'] <= T0:
188
+ if split_physical_exergy:
189
+ self.E_P = outlet['m'] * (outlet['e_T'] - inlet['e_T'])
190
+ self.E_F = self.E_P + inlet['m'] * (inlet['e_M'] - outlet['m'] * outlet['e_M'])
191
+ else:
192
+ self.E_P = np.nan if getattr(self, 'dissipative', False) else \
193
+ outlet['m'] * (outlet['e_PH'] - inlet['e_PH'])
194
+ self.E_F = outlet['m'] * (outlet['e_PH'] - inlet['e_PH'])
195
+
196
+ else:
197
+ # Unimplemented corner case
198
+ logging.warning(
199
+ "SimpleHeatExchanger: unimplemented case (Q < 0, T_in < T0 < T_out?)."
200
+ )
201
+ self.E_P = np.nan
202
+ self.E_F = np.nan
203
+
204
+ # Case 2: Heat is added (Q > 0)
205
+ elif Q > 0:
206
+ if inlet['T'] >= T0 and outlet['T'] >= T0:
207
+ if split_physical_exergy:
208
+ self.E_P = outlet['m'] * (outlet['e_PH'] - inlet['e_PH'])
209
+ self.E_F = outlet['m'] * (outlet['e_T'] - inlet['e_T'])
210
+ else:
211
+ self.E_P = outlet['m'] * (outlet['e_PH'] - inlet['e_PH'])
212
+ self.E_F = outlet['m'] * (outlet['e_PH'] - inlet['e_PH'])
213
+ elif inlet['T'] < T0 and outlet['T'] > T0:
214
+ if split_physical_exergy:
215
+ self.E_P = outlet['m'] * (outlet['e_T'] + inlet['e_T'])
216
+ self.E_F = (inlet['m'] * inlet['e_T'] +
217
+ (inlet['m'] * inlet['e_M'] - outlet['m'] * outlet['e_M']))
218
+ else:
219
+ self.E_P = outlet['m'] * (outlet['e_PH'] - inlet['e_PH'])
220
+ self.E_F = outlet['m'] * (outlet['e_PH'] - inlet['e_PH'])
221
+
222
+ elif inlet['T'] < T0 and outlet['T'] < T0:
223
+ if split_physical_exergy:
224
+ self.E_P = np.nan if getattr(self, 'dissipative', False) else \
225
+ inlet['m'] * (inlet['e_T'] - outlet['e_T']) + \
226
+ (outlet['m'] * outlet['e_M'] - inlet['m'] * inlet['e_M'])
227
+ self.E_F = inlet['m'] * (inlet['e_T'] - outlet['e_T'])
228
+ else:
229
+ self.E_P = np.nan if getattr(self, 'dissipative', False) else \
230
+ inlet['m'] * (inlet['e_PH'] - outlet['e_PH'])
231
+ self.E_F = inlet['m'] * (inlet['e_PH'] - outlet['e_PH'])
232
+ else:
233
+ logging.warning(
234
+ "SimpleHeatExchanger: unimplemented case (Q > 0, T_in > T0 > T_out?)."
235
+ )
236
+ self.E_P = np.nan
237
+ self.E_F = np.nan
238
+
239
+ # Case 3: Fully dissipative or Q == 0
240
+ else:
241
+ self.E_P = np.nan
242
+ self.E_F = inlet['m'] * (inlet['e_PH'] - outlet['e_PH'])
243
+
244
+ # Calculate exergy destruction
245
+ if np.isnan(self.E_P):
246
+ self.E_D = self.E_F
247
+ else:
248
+ self.E_D = self.E_F - self.E_P
249
+
250
+ # Calculate exergy efficiency
251
+ self.epsilon = self.calc_epsilon()
252
+
253
+ # Log the results
254
+ logging.info(
255
+ f"SimpleHeatExchanger exergy balance calculated: "
256
+ f"E_P={self.E_P:.2f}, E_F={self.E_F:.2f}, E_D={self.E_D:.2f}, "
257
+ f"Efficiency={self.epsilon:.2%}"
258
+ )
259
+
260
+
261
+ def aux_eqs(self, A, b, counter, T0, equations, chemical_exergy_enabled):
262
+ """
263
+ Auxiliary equations for the simple heat exchanger.
264
+
265
+ This function adds rows to the cost matrix A and the right-hand-side vector b to enforce
266
+ the following auxiliary cost relations:
267
+
268
+ (1) Thermal exergy cost equation:
269
+ - For heat release (T_in > T_out > T0): F-principle is applied
270
+ 1/E_T_in * C_T_in - 1/E_T_out * C_T_out = 0
271
+ - For heat addition (T_in < T_out > T0): P-principle is applied
272
+ 1/ΔE_T * (C_T_out - C_T_in) = 1/ΔE_M * (C_M_out - C_M_in)
273
+
274
+ (2) Mechanical exergy cost equation:
275
+ 1/E_M_in * C_M_in - 1/E_M_out * C_M_out = 0
276
+ - F-principle: specific mechanical exergy costs equalized between inlet/outlet
277
+
278
+ (3) Chemical exergy cost equation (if enabled):
279
+ 1/E_CH_in * C_CH_in - 1/E_CH_out * C_CH_out = 0
280
+ - F-principle: specific chemical exergy costs equalized between inlet/outlet
281
+
282
+ Parameters
283
+ ----------
284
+ A : numpy.ndarray
285
+ The current cost matrix.
286
+ b : numpy.ndarray
287
+ The current right-hand-side vector.
288
+ counter : int
289
+ The current row index in the matrix.
290
+ T0 : float
291
+ Ambient temperature.
292
+ equations : dict
293
+ Dictionary for storing equation labels.
294
+ chemical_exergy_enabled : bool
295
+ Flag indicating whether chemical exergy auxiliary equations should be added.
296
+
297
+ Returns
298
+ -------
299
+ A : numpy.ndarray
300
+ The updated cost matrix.
301
+ b : numpy.ndarray
302
+ The updated right-hand-side vector.
303
+ counter : int
304
+ The updated row index.
305
+ equations : dict
306
+ Updated dictionary with equation labels.
307
+ """
308
+ # --- Thermal cost equation (row counter) ---
309
+ if self.inl[0]["T"] > T0 and self.outl[0]["T"] > T0:
310
+ if self.inl[0]["T"] > self.outl[0]["T"]:
311
+ # Heat is released (turbine-like behavior, f‑rule).
312
+ A[counter, self.inl[0]["CostVar_index"]["T"]] = (1 / self.inl[0]["e_T"]
313
+ if self.inl[0]["e_T"] != 0 else 1)
314
+ A[counter, self.outl[0]["CostVar_index"]["T"]] = (-1 / self.outl[0]["e_T"]
315
+ if self.outl[0]["e_T"] != 0 else -1)
316
+ equations[counter] = f"aux_f_rule_{self.name}"
317
+ elif self.inl[0]["T"] < self.outl[0]["T"]:
318
+ # Heat is injected (compressor-like behavior, p‑rule):
319
+ dET = self.outl[0]["e_T"] - self.inl[0]["e_T"]
320
+ dEM = self.outl[0]["e_M"] - self.inl[0]["e_M"]
321
+ if dET != 0 and dEM != 0:
322
+ A[counter, self.inl[0]["CostVar_index"]["T"]] = -1 / dET
323
+ A[counter, self.outl[0]["CostVar_index"]["T"]] = 1 / dET
324
+ A[counter, self.inl[0]["CostVar_index"]["M"]] = 1 / dEM
325
+ A[counter, self.outl[0]["CostVar_index"]["M"]] = -1 / dEM
326
+ equations[counter] = f"aux_p_rule_{self.name}"
327
+ else:
328
+ logging.warning("SimpleHeatExchanger: dET or dEM is zero; case not implemented.")
329
+ equations[counter] = "aux_unimpl_HEX"
330
+ else:
331
+ logging.warning("SimpleHeatExchanger: Inlet and outlet temperatures are equal; case not implemented.")
332
+ equations[counter] = "aux_unimpl_HEX"
333
+ else:
334
+ logging.warning("SimpleHeatExchanger: Cases with T_in or T_out below T0 are not implemented.")
335
+ equations[counter] = "aux_unimpl_HEX"
336
+ b[counter] = 0
337
+
338
+ # --- Mechanical cost equality (row counter+1) ---
339
+ A[counter+1, self.inl[0]["CostVar_index"]["M"]] = (1 / self.inl[0]["e_M"]
340
+ if self.inl[0]["e_M"] != 0 else 1)
341
+ A[counter+1, self.outl[0]["CostVar_index"]["M"]] = (-1 / self.outl[0]["e_M"]
342
+ if self.outl[0]["e_M"] != 0 else 1)
343
+ equations[counter+1] = f"aux_equality_mech_{self.outl[0]['name']}"
344
+ b[counter+1] = 0
345
+
346
+ # --- Chemical cost equality (conditionally added) ---
347
+ if chemical_exergy_enabled:
348
+ A[counter+2, self.inl[0]["CostVar_index"]["CH"]] = (1 / self.inl[0]["e_CH"]
349
+ if self.inl[0]["e_CH"] != 0 else 1)
350
+ A[counter+2, self.outl[0]["CostVar_index"]["CH"]] = (-1 / self.outl[0]["e_CH"]
351
+ if self.outl[0]["e_CH"] != 0 else 1)
352
+ equations[counter+2] = f"aux_equality_chem_{self.outl[0]['name']}"
353
+ b[counter+2] = 0
354
+ counter += 3
355
+ else:
356
+ counter += 2
357
+
358
+ return A, b, counter, equations