exerpy 0.0.1__py3-none-any.whl → 0.0.3__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 (39) hide show
  1. exerpy/__init__.py +2 -4
  2. exerpy/analyses.py +597 -297
  3. exerpy/components/__init__.py +3 -0
  4. exerpy/components/combustion/base.py +157 -114
  5. exerpy/components/component.py +8 -8
  6. exerpy/components/heat_exchanger/base.py +593 -256
  7. exerpy/components/heat_exchanger/condenser.py +353 -166
  8. exerpy/components/heat_exchanger/simple.py +575 -225
  9. exerpy/components/heat_exchanger/steam_generator.py +153 -123
  10. exerpy/components/helpers/cycle_closer.py +61 -34
  11. exerpy/components/helpers/power_bus.py +117 -0
  12. exerpy/components/nodes/deaerator.py +221 -102
  13. exerpy/components/nodes/drum.py +50 -39
  14. exerpy/components/nodes/flash_tank.py +218 -43
  15. exerpy/components/nodes/mixer.py +296 -115
  16. exerpy/components/nodes/splitter.py +173 -0
  17. exerpy/components/nodes/storage.py +130 -0
  18. exerpy/components/piping/valve.py +351 -139
  19. exerpy/components/power_machines/generator.py +105 -38
  20. exerpy/components/power_machines/motor.py +111 -39
  21. exerpy/components/turbomachinery/compressor.py +181 -63
  22. exerpy/components/turbomachinery/pump.py +182 -63
  23. exerpy/components/turbomachinery/turbine.py +182 -74
  24. exerpy/functions.py +388 -263
  25. exerpy/parser/from_aspen/aspen_config.py +57 -48
  26. exerpy/parser/from_aspen/aspen_parser.py +373 -280
  27. exerpy/parser/from_ebsilon/__init__.py +2 -2
  28. exerpy/parser/from_ebsilon/check_ebs_path.py +15 -19
  29. exerpy/parser/from_ebsilon/ebsilon_config.py +329 -227
  30. exerpy/parser/from_ebsilon/ebsilon_functions.py +205 -38
  31. exerpy/parser/from_ebsilon/ebsilon_parser.py +392 -255
  32. exerpy/parser/from_ebsilon/utils.py +16 -11
  33. exerpy/parser/from_tespy/tespy_config.py +32 -1
  34. exerpy/parser/from_tespy/tespy_parser.py +151 -0
  35. {exerpy-0.0.1.dist-info → exerpy-0.0.3.dist-info}/METADATA +45 -4
  36. exerpy-0.0.3.dist-info/RECORD +48 -0
  37. exerpy-0.0.1.dist-info/RECORD +0 -44
  38. {exerpy-0.0.1.dist-info → exerpy-0.0.3.dist-info}/WHEEL +0 -0
  39. {exerpy-0.0.1.dist-info → exerpy-0.0.3.dist-info}/licenses/LICENSE +0 -0
@@ -2,24 +2,17 @@ import logging
2
2
 
3
3
  import numpy as np
4
4
 
5
- from exerpy.components.component import Component
6
- from exerpy.components.component import component_registry
5
+ from exerpy.components.component import Component, component_registry
7
6
 
8
7
 
9
8
  @component_registry
10
9
  class Valve(Component):
11
10
  r"""
12
- Class for exergy analysis of valves.
11
+ Class for exergy and exergoeconomic analysis of valves.
13
12
 
14
- This class performs exergy analysis calculations for isenthalpic valves with
15
- one inlet and one outlet stream. The exergy product and fuel definitions
16
- vary based on the temperature relationships between inlet stream, outlet stream,
17
- and ambient conditions.
18
-
19
- Parameters
20
- ----------
21
- **kwargs : dict
22
- Arbitrary keyword arguments passed to parent class.
13
+ This class performs exergy and exergoeconomic analysis calculations for valve components,
14
+ accounting for one inlet and one outlet streams across various temperature regimes, including
15
+ above and below ambient temperature.
23
16
 
24
17
  Attributes
25
18
  ----------
@@ -32,11 +25,27 @@ class Valve(Component):
32
25
  epsilon : float
33
26
  Exergetic efficiency of the component :math:`\varepsilon` in :math:`-`.
34
27
  inl : dict
35
- Dictionary containing inlet stream data with temperature, mass flows,
36
- and specific exergies.
28
+ Dictionary containing inlet stream data with mass flows and specific exergies.
37
29
  outl : dict
38
- Dictionary containing outlet stream data with temperature, mass flows,
39
- and specific exergies.
30
+ Dictionary containing outlet stream data with mass flows and specific exergies.
31
+ Z_costs : float
32
+ Investment cost rate of the component in currency/h.
33
+ C_P : float
34
+ Cost of product stream :math:`\dot{C}_P` in currency/h.
35
+ C_F : float
36
+ Cost of fuel stream :math:`\dot{C}_F` in currency/h.
37
+ C_D : float
38
+ Cost of exergy destruction :math:`\dot{C}_D` in currency/h.
39
+ c_P : float
40
+ Specific cost of product stream (currency per unit exergy).
41
+ c_F : float
42
+ Specific cost of fuel stream (currency per unit exergy).
43
+ r : float
44
+ Relative cost difference, :math:`(c_P - c_F)/c_F`.
45
+ f : float
46
+ Exergoeconomic factor, :math:`\dot{Z}/(\dot{Z} + \dot{C}_D)`.
47
+ Ex_C_col : dict
48
+ Custom cost coefficients collection passed via `kwargs`.
40
49
 
41
50
  Notes
42
51
  -----
@@ -47,7 +56,7 @@ class Valve(Component):
47
56
 
48
57
  \dot{E}_\mathrm{P} =
49
58
  \begin{cases}
50
- \mathrm{not defined (nan)}
59
+ \text{not defined (nan)}
51
60
  & T_\mathrm{in}, T_\mathrm{out} > T_0\\
52
61
  \dot{m} \cdot e_\mathrm{out}^\mathrm{T}
53
62
  & T_\mathrm{in} > T_0 \geq T_\mathrm{out}\\
@@ -59,19 +68,19 @@ class Valve(Component):
59
68
  \begin{cases}
60
69
  \dot{m} \cdot (e_\mathrm{in}^\mathrm{PH} - e_\mathrm{out}^\mathrm{PH})
61
70
  & T_\mathrm{in}, T_\mathrm{out} > T_0\\
62
- \dot{m} \cdot (e_\mathrm{in}^\mathrm{T} + e_\mathrm{in}^\mathrm{M}
71
+ \dot{m} \cdot (e_\mathrm{in}^\mathrm{T} + e_\mathrm{in}^\mathrm{M}
63
72
  - e_\mathrm{out}^\mathrm{M})
64
73
  & T_\mathrm{in} > T_0 \geq T_\mathrm{out}\\
65
74
  \dot{m} \cdot (e_\mathrm{in}^\mathrm{M} - e_\mathrm{out}^\mathrm{M})
66
75
  & T_0 \geq T_\mathrm{in}, T_\mathrm{out}
67
76
  \end{cases}
68
77
 
69
- For all cases, except when :math:`T_\mathrm{out} > T_\mathrm{in}`, the exergy
78
+ For all cases, except when :math:`T_\mathrm{out} > T_\mathrm{in}`, the exergy
70
79
  destruction is calculated as:
71
80
 
72
81
  .. math::
73
82
  \dot{E}_\mathrm{D} = \begin{cases}
74
- \dot{E}_\mathrm{F} & \mathrm{if } \dot{E}_\mathrm{P} = \mathrm{nan}\\
83
+ \dot{E}_\mathrm{F} & \text{if } \dot{E}_\mathrm{P} = \text{nan}\\
75
84
  \dot{E}_\mathrm{F} - \dot{E}_\mathrm{P} & \mathrm{otherwise}
76
85
  \end{cases}
77
86
 
@@ -105,36 +114,64 @@ class Valve(Component):
105
114
  ------
106
115
  ValueError
107
116
  If the required inlet and outlet streams are not properly defined.
108
- """
117
+ """
109
118
  # Ensure that the component has both inlet and outlet streams
110
119
  if len(self.inl) < 1 or len(self.outl) < 1:
111
120
  raise ValueError("Valve requires at least one inlet and one outlet.")
112
121
 
113
- T_in = self.inl[0]['T']
114
- T_out = self.outl[0]['T']
122
+ T_in = self.inl[0]["T"]
123
+ T_out = self.outl[0]["T"]
124
+
125
+ p_in = self.inl[0]["p"]
126
+ p_out = self.outl[0]["p"]
115
127
 
116
- # Case-specific exergy calculations
117
- if T_in > T0 and T_out > T0 and T_in > T_out:
128
+ # Check for zero mass flow
129
+ if abs(self.inl[0]["m"]) < 1e-10:
130
+ logging.info(f"Valve {self.name} has zero mass flow: exergy balance not considered.")
118
131
  self.E_P = np.nan
119
- self.E_F = self.inl[0]['m'] * (self.inl[0]['e_PH'] - self.outl[0]['e_PH'])
120
- elif T_out <= T0 and T_in > T0:
132
+ self.E_F = np.nan
133
+ self.E_D = np.nan
134
+ self.epsilon = np.nan
135
+ return
136
+
137
+ # Check if inlet and outlet are physically identical
138
+ if abs(T_in - T_out) < 1e-2 and abs(p_in - p_out) <= 1e-4 * max(p_in, 1e-9):
139
+ logging.info(f"Valve {self.name} inlet and outlet are physically identical.")
140
+ self.E_P = 0.0
141
+ self.E_F = 0.0
142
+ self.E_D = 0.0
143
+ self.epsilon = 1.0
144
+ logging.info(
145
+ f"Valve exergy balance calculated: "
146
+ f"E_P={self.E_P:.2f}, E_F={self.E_F:.2f}, E_D={self.E_D:.2f}, "
147
+ f"Efficiency={self.epsilon:.2%}"
148
+ )
149
+ return
150
+
151
+ # Case 1: Both temperatures above ambient
152
+ if T_in > T0 and T_out > T0:
153
+ self.E_P = np.nan
154
+ self.E_F = self.inl[0]["m"] * (self.inl[0]["e_PH"] - self.outl[0]["e_PH"])
155
+
156
+ # Case 2: Inlet above ambient, outlet below or equal to ambient
157
+ elif T_in > T0 and T_out <= T0:
121
158
  if split_physical_exergy:
122
- self.E_P = self.inl[0]['m'] * self.outl[0]['e_T']
123
- self.E_F = self.inl[0]['m'] * (self.inl[0]['e_T'] + self.inl[0]['e_M'] -
124
- self.outl[0]['e_M'])
159
+ self.E_P = self.inl[0]["m"] * self.outl[0]["e_T"]
160
+ self.E_F = self.inl[0]["m"] * (self.inl[0]["e_T"] + self.inl[0]["e_M"] - self.outl[0]["e_M"])
125
161
  else:
126
162
  logging.warning(
127
163
  "Exergy balance of a valve, where outlet temperature is smaller than "
128
- "ambient temperature, is not implemented for non-split physical exergy."
164
+ "ambient temperature, is not implemented for non-split physical exergy. "
129
165
  "Valve is treated as dissipative."
130
166
  )
131
167
  self.E_P = np.nan
132
- self.E_F = self.inl[0]['m'] * (self.inl[0]['e_PH'] - self.outl[0]['e_PH'])
168
+ self.E_F = self.inl[0]["m"] * (self.inl[0]["e_PH"] - self.outl[0]["e_PH"])
133
169
 
170
+ # Case 3: Both temperatures below ambient
134
171
  elif T_in <= T0 and T_out <= T0:
135
172
  if split_physical_exergy:
136
- self.E_P = self.inl[0]['m'] * (self.outl[0]['e_T'] - self.inl[0]['e_T'])
137
- self.E_F = self.inl[0]['m'] * (self.inl[0]['e_M'] - self.outl[0]['e_M'])
173
+ self.E_P = self.inl[0]["m"] * (self.outl[0]["e_T"] - self.inl[0]["e_T"])
174
+ self.E_F = self.inl[0]["m"] * (self.inl[0]["e_M"] - self.outl[0]["e_M"])
138
175
  else:
139
176
  logging.warning(
140
177
  "Exergy balance of a valve, where both temperatures are smaller than "
@@ -142,14 +179,28 @@ class Valve(Component):
142
179
  "Valve is treated as dissipative."
143
180
  )
144
181
  self.E_P = np.nan
145
- self.E_F = self.inl[0]['m'] * (self.inl[0]['e_PH'] - self.outl[0]['e_PH'])
146
- else:
182
+ self.E_F = self.inl[0]["m"] * (self.inl[0]["e_PH"] - self.outl[0]["e_PH"])
183
+
184
+ # Case 4: Inlet below or at ambient, outlet above ambient
185
+ elif T_in <= T0 and T_out > T0:
147
186
  logging.warning(
148
- "Exergy balance of a valve, where outlet temperature is larger than "
149
- "inlet temperature, is not implemented."
187
+ f"Valve {self.name} with temperature increase from below ambient to above ambient - "
188
+ "non-physical behavior. Treated as dissipative."
189
+ )
190
+ self.E_P = np.nan
191
+ self.E_F = self.inl[0]["m"] * (self.inl[0]["e_PH"] - self.outl[0]["e_PH"])
192
+
193
+ else:
194
+ logging.error(
195
+ f"Valve {self.name} encountered an unexpected condition: "
196
+ f"T_in={T_in:.2f} K, T_out={T_out:.2f} K, T0={T0:.2f} K. "
197
+ "This should not occur - exergy balance cannot be calculated."
150
198
  )
151
199
  self.E_P = np.nan
152
200
  self.E_F = np.nan
201
+ self.E_D = np.nan
202
+ self.epsilon = np.nan
203
+ return
153
204
 
154
205
  # Calculate exergy destruction
155
206
  if np.isnan(self.E_P):
@@ -162,32 +213,34 @@ class Valve(Component):
162
213
 
163
214
  # Log the results
164
215
  logging.info(
165
- f"Valve exergy balance calculated: "
216
+ f"Exergy balance of Valve {self.name} calculated: "
166
217
  f"E_P={self.E_P:.2f}, E_F={self.E_F:.2f}, E_D={self.E_D:.2f}, "
167
218
  f"Efficiency={self.epsilon:.2%}"
168
219
  )
169
220
 
170
-
171
221
  def aux_eqs(self, A, b, counter, T0, equations, chemical_exergy_enabled):
172
222
  """
173
223
  Auxiliary equations for the valve.
174
-
224
+
175
225
  This function adds rows to the cost matrix A and the right-hand-side vector b to enforce
176
226
  the following auxiliary cost relations:
177
-
178
- For T_in > T0 and T_out > T0:
227
+
228
+ For (T_in > T0 and T_out > T0) or (T_in <= T0 and T_out > T0):
179
229
  - Valve is treated as dissipative (warning issued)
180
-
230
+
181
231
  For T_out <= T0:
182
- (1) 1/E_M_in * C_M_in - 1/E_M_out * C_M_out = 0
183
- - F-principle: specific mechanical exergy costs equalized between inlet/outlet
184
- - If E_M is zero for either stream, appropriate fallback coefficients are used
185
-
232
+ (1) 1/E_M_in * C_M_in - 1/E_M_out * C_M_out = 0
233
+
234
+ - F-principle: specific mechanical exergy costs equalized between inlet/outlet
235
+
236
+ - If E_M is zero for either stream, appropriate fallback coefficients are used
237
+
186
238
  When chemical_exergy_enabled is True:
187
- (2) 1/E_CH_in * C_CH_in - 1/E_CH_out * C_CH_out = 0
188
- - F-principle: specific chemical exergy costs equalized between inlet/outlet
189
- - If E_CH is zero for either stream, appropriate fallback coefficients are used
190
-
239
+ (2) 1/E_CH_in * C_CH_in - 1/E_CH_out * C_CH_out = 0
240
+
241
+ - F-principle: specific chemical exergy costs equalized between inlet/outlet
242
+ - If E_CH is zero for either stream, appropriate fallback coefficients are used
243
+
191
244
  Parameters
192
245
  ----------
193
246
  A : numpy.ndarray
@@ -202,7 +255,7 @@ class Valve(Component):
202
255
  Dictionary for storing equation labels.
203
256
  chemical_exergy_enabled : bool
204
257
  Flag indicating whether chemical exergy auxiliary equations should be added.
205
-
258
+
206
259
  Returns
207
260
  -------
208
261
  A : numpy.ndarray
@@ -214,51 +267,65 @@ class Valve(Component):
214
267
  equations : dict
215
268
  Updated dictionary with equation labels.
216
269
  """
217
- if self.inl[0]["T"] > T0 and self.outl[0]["T"] > T0:
218
- logging.warning("This case is not implemented. The Valve should be treated as dissipative!")
219
-
220
- elif self.outl[0]["T"] <= T0:
221
- # --- Mechanical cost equation (always added) ---
222
- if self.inl[0]["e_M"] != 0 and self.outl[0]["e_M"] != 0:
223
- A[counter, self.inl[0]["CostVar_index"]["M"]] = 1 / self.inl[0]["E_M"]
224
- A[counter, self.outl[0]["CostVar_index"]["M"]] = -1 / self.outl[0]["E_M"]
225
- elif self.inl[0]["e_M"] == 0 and self.outl[0]["e_M"] != 0:
226
- A[counter, self.inl[0]["CostVar_index"]["M"]] = 1
227
- elif self.inl[0]["e_M"] != 0 and self.outl[0]["e_M"] == 0:
228
- A[counter, self.outl[0]["CostVar_index"]["M"]] = 1
229
- else:
230
- A[counter, self.inl[0]["CostVar_index"]["M"]] = 1
231
- A[counter, self.outl[0]["CostVar_index"]["M"]] = -1
232
- equations[counter] = f"aux_{self.name}_mech_{self.outl[0]['name']}"
233
- b[counter] = 0
234
- counter += 1
270
+
271
+ # Check if valve is dissipative
272
+ if np.isnan(self.E_P):
273
+ logging.warning(f"Valve {self.name} is dissipative - no auxiliary equations added.")
274
+ return A, b, counter, equations
275
+
276
+ # Productive valve - T_out must be ≤ T0 (Cases 2 or 3)
277
+ # Mechanical cost equation (always added for productive valves)
278
+ if self.inl[0]["e_M"] != 0 and self.outl[0]["e_M"] != 0:
279
+ A[counter, self.inl[0]["CostVar_index"]["M"]] = 1 / self.inl[0]["E_M"]
280
+ A[counter, self.outl[0]["CostVar_index"]["M"]] = -1 / self.outl[0]["E_M"]
281
+ elif self.inl[0]["e_M"] == 0 and self.outl[0]["e_M"] != 0:
282
+ A[counter, self.inl[0]["CostVar_index"]["M"]] = 1
283
+ elif self.inl[0]["e_M"] != 0 and self.outl[0]["e_M"] == 0:
284
+ A[counter, self.outl[0]["CostVar_index"]["M"]] = 1
235
285
  else:
236
- msg = ('Exergy balance of a valve, where outlet temperature is larger than inlet temperature is not implemented.')
237
- logging.warning(msg)
238
-
286
+ A[counter, self.inl[0]["CostVar_index"]["M"]] = 1
287
+ A[counter, self.outl[0]["CostVar_index"]["M"]] = -1
288
+ equations[counter] = {
289
+ "kind": "aux_equality",
290
+ "objects": [self.name, self.inl[0]["name"], self.outl[0]["name"]],
291
+ "property": "c_M",
292
+ }
293
+ b[counter] = 0
294
+ counter += 1
295
+
239
296
  if chemical_exergy_enabled:
240
297
  # --- Chemical cost equation (conditionally added) ---
241
- A[counter, self.inl[0]["CostVar_index"]["CH"]] = (1 / self.inl[0]["E_CH"] if self.inl[0]["e_CH"] != 0 else 1)
242
- A[counter, self.outl[0]["CostVar_index"]["CH"]] = (-1 / self.outl[0]["E_CH"] if self.outl[0]["e_CH"] != 0 else -1)
243
- equations[counter+1] = f"aux_{self.name}_chem_{self.outl[0]['name']}"
298
+ A[counter, self.inl[0]["CostVar_index"]["CH"]] = 1 / self.inl[0]["E_CH"] if self.inl[0]["e_CH"] != 0 else 1
299
+ A[counter, self.outl[0]["CostVar_index"]["CH"]] = (
300
+ -1 / self.outl[0]["E_CH"] if self.outl[0]["e_CH"] != 0 else -1
301
+ )
302
+ equations[counter] = {
303
+ "kind": "aux_equality",
304
+ "objects": [self.name, self.inl[0]["name"], self.outl[0]["name"]],
305
+ "property": "c_CH",
306
+ }
244
307
  # Set right-hand side for both rows.
245
308
  b[counter] = 0
246
309
  counter += 1
247
-
310
+
248
311
  return A, b, counter, equations
249
-
312
+
250
313
  def dis_eqs(self, A, b, counter, T0, equations, chemical_exergy_enabled=False, all_components=None):
251
- """
314
+ r"""
252
315
  Constructs the cost equations for a dissipative Valve in ExerPy,
253
- distributing the valves extra cost difference (C_diff) to all other productive
316
+ distributing the valve's extra cost difference (C_diff) to all other productive
254
317
  components (non-dissipative and non-CycleCloser) in proportion to their exergy destruction (E_D)
255
318
  and adding an extra overall cost balance row that enforces:
256
-
257
- (C_in,T - C_out,T) + (C_in,M - C_out,M) - C_diff = - Z_costs
258
-
319
+
320
+ .. math::
321
+ (\dot C_{\mathrm{in},T} - \dot C_{\mathrm{out},T})
322
+ + (\dot C_{\mathrm{in},M} - \dot C_{\mathrm{out},M})
323
+ - \dot C_{\mathrm{diff}}
324
+ = -\,\dot Z_{\mathrm{costs}}
325
+
259
326
  In this formulation, the unknown cost variable in the "dissipative" column (i.e. C_diff)
260
- is solved for, ensuring the valves cost balance.
261
-
327
+ is solved for, ensuring the valve's cost balance.
328
+
262
329
  Parameters
263
330
  ----------
264
331
  A : numpy.ndarray
@@ -275,16 +342,15 @@ class Valve(Component):
275
342
  Flag indicating whether chemical exergy is considered. (Ignored here.)
276
343
  all_components : list, optional
277
344
  Global list of all component objects; if not provided, defaults to [].
278
-
345
+
279
346
  Returns
280
347
  -------
281
348
  tuple
282
349
  Updated (A, b, counter, equations).
283
-
350
+
284
351
  Notes
285
352
  -----
286
- - It is assumed that each inlet/outlet streams CostVar_index dictionary has keys:
287
- "T" (thermal), "M" (mechanical), and "dissipative" (the extra unknown).
353
+ - It is assumed that each inlet/outlet stream's CostVar_index dictionary has keys: "T" (thermal), "M" (mechanical), and "dissipative" (the extra unknown).
288
354
  - self.Z_costs is the known cost rate (in currency/s) for the valve.
289
355
  """
290
356
  # --- Thermal difference row ---
@@ -295,7 +361,11 @@ class Valve(Component):
295
361
  A[counter, self.inl[0]["CostVar_index"]["T"]] = 1
296
362
  A[counter, self.outl[0]["CostVar_index"]["T"]] = -1
297
363
  b[counter] = 0
298
- equations[counter] = f"diss_valve_thermal_{self.name}"
364
+ equations[counter] = {
365
+ "kind": "dis_equality",
366
+ "objects": [self.name, self.inl[0]["name"], self.outl[0]["name"]],
367
+ "property": "c_T",
368
+ }
299
369
  counter += 1
300
370
 
301
371
  # --- Mechanical difference row ---
@@ -306,30 +376,56 @@ class Valve(Component):
306
376
  A[counter, self.inl[0]["CostVar_index"]["M"]] = 1
307
377
  A[counter, self.outl[0]["CostVar_index"]["M"]] = -1
308
378
  b[counter] = 0
309
- equations[counter] = f"diss_valve_mechanical_{self.name}"
379
+ equations[counter] = {
380
+ "kind": "dis_equality",
381
+ "objects": [self.name, self.inl[0]["name"], self.outl[0]["name"]],
382
+ "property": "c_M",
383
+ }
310
384
  counter += 1
311
385
 
386
+ # --- Chemical difference row (if chemical exergy is enabled) ---
387
+ if chemical_exergy_enabled:
388
+ A[counter, self.inl[0]["CostVar_index"]["CH"]] = 1 / self.inl[0]["E_CH"] if self.inl[0]["E_CH"] != 0 else 1
389
+ A[counter, self.outl[0]["CostVar_index"]["CH"]] = (
390
+ -1 / self.outl[0]["E_CH"] if self.outl[0]["E_CH"] != 0 else -1
391
+ )
392
+ b[counter] = 0
393
+ equations[counter] = {
394
+ "kind": "dis_equality",
395
+ "objects": [self.name, self.inl[0]["name"], self.outl[0]["name"]],
396
+ "property": "c_CH",
397
+ }
398
+ counter += 1
399
+
312
400
  # --- Distribution of dissipative cost difference to other components based on E_D ---
313
401
  if all_components is None:
314
402
  all_components = []
315
403
  # Serving components: all productive components (excluding self, any dissipative, and CycleCloser)
316
- serving = [comp for comp in all_components
317
- if comp is not self
318
- and not getattr(comp, "dissipative", False)
319
- and comp.__class__.__name__ != "CycleCloser"]
320
- total_E_D = sum(getattr(comp, "E_D", 0) for comp in serving)
404
+ serving = [
405
+ comp
406
+ for comp in all_components
407
+ if comp is not self
408
+ and hasattr(comp, "exergy_cost_line")
409
+ and not comp.__class__.__name__.endswith("PowerBus")
410
+ and hasattr(comp, "E_D")
411
+ and not np.isnan(comp.E_D)
412
+ ]
413
+ total_E_D = sum(comp.E_D for comp in serving)
321
414
  diss_col = self.inl[0]["CostVar_index"].get("dissipative")
322
415
  if diss_col is None:
323
- logging.warning(f"No 'dissipative' column allocated for {self.name}.")
416
+ logging.error(f"No 'dissipative' column allocated for {self.name}.")
324
417
  else:
325
418
  if total_E_D == 0:
326
- # Fall back to equal distribution if total exergy destruction is zero.
327
- for comp in serving:
328
- A[comp.exergy_cost_line, diss_col] += 1 / len(serving) if len(serving) > 0 else 0
419
+ if len(serving) > 0:
420
+ for comp in serving:
421
+ A[comp.exergy_cost_line, diss_col] = 1 / len(serving)
422
+ else:
423
+ logging.warning(f"No serving components found for dissipative component {self.name}")
329
424
  else:
330
425
  for comp in serving:
331
426
  weight = getattr(comp, "E_D", 0) / total_E_D
332
- A[comp.exergy_cost_line, diss_col] += weight
427
+ comp.serving_weight = weight
428
+ A[comp.exergy_cost_line, diss_col] = weight
333
429
 
334
430
  # --- Extra overall cost balance row ---
335
431
  # This row enforces:
@@ -338,57 +434,173 @@ class Valve(Component):
338
434
  A[counter, self.outl[0]["CostVar_index"]["T"]] = -1
339
435
  A[counter, self.inl[0]["CostVar_index"]["M"]] = 1
340
436
  A[counter, self.outl[0]["CostVar_index"]["M"]] = -1
437
+ if chemical_exergy_enabled:
438
+ A[counter, self.inl[0]["CostVar_index"]["CH"]] = 1
439
+ A[counter, self.outl[0]["CostVar_index"]["CH"]] = -1
341
440
  # Subtract the unknown dissipative cost difference:
342
441
  A[counter, self.inl[0]["CostVar_index"]["dissipative"]] = -1
343
442
  b[counter] = -self.Z_costs
344
- equations[counter] = f"diss_valve_balance_{self.name}"
443
+ equations[counter] = {"kind": "dis_balance", "objects": [self.name], "property": "dissipative_cost_balance"}
345
444
  counter += 1
346
445
 
347
446
  return A, b, counter, equations
348
447
 
448
+ def exergoeconomic_balance(self, T0, chemical_exergy_enabled=False):
449
+ r"""
450
+ Perform exergoeconomic cost balance for the valve (throttling component).
349
451
 
452
+ The valve is a throttling device that reduces pressure without doing work.
453
+ Unlike other components, a valve can be either **dissipative** (purely destructive)
454
+ or **productive** depending on temperature conditions and whether physical
455
+ exergy is split into thermal and mechanical components.
456
+
457
+ **Dissipative Valve (E_P is NaN):**
458
+
459
+ When the valve has no identifiable product (Cases 1, 4, or Cases 2-3 without split),
460
+ the entire exergy loss is considered destruction. The cost of fuel is the decrease
461
+ in physical exergy cost:
462
+
463
+ .. math::
464
+ \dot{C}_{\mathrm{F}} = \dot{C}_{\mathrm{in}}^{\mathrm{PH}} - \dot{C}_{\mathrm{out}}^{\mathrm{PH}}
465
+
466
+ .. math::
467
+ \dot{C}_{\mathrm{P}} = \text{NaN (no product)}
468
+
469
+ **Productive Valve (E_P has a value):**
470
+
471
+ When physical exergy is split (split_physical_exergy=True) and specific temperature
472
+ conditions are met, the valve can have a product:
473
+
474
+ *Case 2 (Inlet above T0, outlet below T0):*
475
+
476
+ The product is the thermal exergy at the outlet (cooling capacity), and the fuel
477
+ is the thermal exergy decrease plus the mechanical exergy loss:
478
+
479
+ .. math::
480
+ \dot{C}_{\mathrm{P}} = \dot{C}_{\mathrm{out}}^{\mathrm{T}}
481
+
482
+ .. math::
483
+ \dot{C}_{\mathrm{F}} = \dot{C}_{\mathrm{in}}^{\mathrm{T}} + (\dot{C}_{\mathrm{in}}^{\mathrm{M}} - \dot{C}_{\mathrm{out}}^{\mathrm{M}})
484
+
485
+ *Case 3 (Both inlet and outlet below T0):*
486
+
487
+ The product is the increase in thermal exergy (cooling capacity increase), and
488
+ the fuel is the mechanical exergy loss:
489
+
490
+ .. math::
491
+ \dot{C}_{\mathrm{P}} = \dot{C}_{\mathrm{out}}^{\mathrm{T}} - \dot{C}_{\mathrm{in}}^{\mathrm{T}}
492
+
493
+ .. math::
494
+ \dot{C}_{\mathrm{F}} = \dot{C}_{\mathrm{in}}^{\mathrm{M}} - \dot{C}_{\mathrm{out}}^{\mathrm{M}}
495
+
496
+ **Calculated exergoeconomic indicators:**
497
+
498
+ Specific cost of fuel:
499
+
500
+ .. math::
501
+ c_{\mathrm{F}} = \frac{\dot{C}_{\mathrm{F}}}{\dot{E}_{\mathrm{F}}}
502
+
503
+ Specific cost of product:
504
+
505
+ .. math::
506
+ c_{\mathrm{P}} = \frac{\dot{C}_{\mathrm{P}}}{\dot{E}_{\mathrm{P}}}
507
+
508
+ Cost rate of exergy destruction:
509
+
510
+ .. math::
511
+ \dot{C}_{\mathrm{D}} = c_{\mathrm{F}} \cdot \dot{E}_{\mathrm{D}}
512
+
513
+ Relative cost difference:
514
+
515
+ .. math::
516
+ r = \frac{c_{\mathrm{P}} - c_{\mathrm{F}}}{c_{\mathrm{F}}}
517
+
518
+ Exergoeconomic factor:
519
+
520
+ .. math::
521
+ f = \frac{\dot{Z}}{\dot{Z} + \dot{C}_{\mathrm{D}}}
350
522
 
351
- def exergoeconomic_balance(self, T0):
352
- """
353
- Perform exergoeconomic balance calculations for the valve.
354
-
355
- This method calculates various exergoeconomic parameters including:
356
- - Cost rates of product (C_P) and fuel (C_F)
357
- - Specific cost of product (c_P) and fuel (c_F)
358
- - Cost rate of exergy destruction (C_D)
359
- - Relative cost difference (r)
360
- - Exergoeconomic factor (f)
361
-
362
523
  Parameters
363
524
  ----------
364
525
  T0 : float
365
- Ambient temperature
366
-
526
+ Ambient temperature (K).
527
+ chemical_exergy_enabled : bool, optional
528
+ If True, chemical exergy is considered in the calculations.
529
+ Default is False.
530
+
531
+ Attributes Set
532
+ --------------
533
+ C_P : float or NaN
534
+ Cost rate of product (currency/time). NaN for dissipative valves.
535
+ C_F : float or NaN
536
+ Cost rate of fuel (currency/time).
537
+ c_P : float or NaN
538
+ Specific cost of product (currency/energy). NaN for dissipative valves.
539
+ c_F : float or NaN
540
+ Specific cost of fuel (currency/energy).
541
+ C_D : float or NaN
542
+ Cost rate of exergy destruction (currency/time).
543
+ r : float or NaN
544
+ Relative cost difference (dimensionless). NaN for dissipative valves.
545
+ f : float or NaN
546
+ Exergoeconomic factor (dimensionless).
547
+
367
548
  Notes
368
549
  -----
369
- The exergoeconomic balance considers thermal (T), chemical (CH),
370
- and mechanical (M) exergy components for the inlet and outlet streams.
550
+ The valve is unique among components because:
551
+
552
+ 1. It typically has no capital cost (Z_costs ≈ 0), making the exergoeconomic
553
+ factor f close to zero.
554
+
555
+ 2. It can be dissipative (no product) or productive (identifiable product)
556
+ depending on the analysis approach and temperature conditions.
557
+
558
+ 3. For dissipative valves, many exergoeconomic parameters are NaN since there
559
+ is no identifiable product.
560
+
561
+ 4. The relative cost difference r is calculated using specific costs (c_P and c_F)
562
+ rather than total cost rates, unlike generator and motor components.
563
+
564
+ 5. The method handles NaN values throughout to accommodate both dissipative
565
+ and productive valve configurations.
566
+
567
+ The exergy destruction E_D and product/fuel definitions E_P and E_F must be
568
+ computed prior to calling this method (via exergy_balance).
569
+
570
+ For refrigeration cycles (Case 2) or cryogenic applications (Case 3), the
571
+ productive valve approach may be more appropriate. For typical throttling
572
+ applications, the dissipative approach is standard.
371
573
  """
372
- if self.inl[0]["T"] > T0 and self.outl[0]["T"] > T0:
373
- self.C_F = self.inl[0]['C_PH'] - self.outl[0]['C_PH']
574
+ # Check if valve is dissipative
575
+ if np.isnan(self.E_P):
576
+ # Dissipative valve (Cases 1, 4, or Cases 2-3 without split)
577
+ self.C_F = self.inl[0]["C_PH"] - self.outl[0]["C_PH"]
374
578
  self.C_P = np.nan
375
- # dissipative
376
- elif self.outl[0]["T"] <= T0 and self.inl[0]["T"] > T0:
377
- self.C_P = self.outl[0]["C_T"]
378
- self.C_F = self.inl[0]["C_T"] + (
379
- self.inl[0]["C_M"] - self.outl[0]["C_M"])
380
- elif self.inl[0]["T"] <= T0 and self.outl[0]["T"] <= T0:
381
- self.C_P = self.outl[0]["C_T"] - self.inl[0]["C_T"]
382
- self.C_F = self.inl[0]["C_M"] - self.outl[0]["C_M"]
383
579
  else:
384
- msg = ('Exergy balance of a valve, where outlet temperature is '
385
- 'larger than inlet temperature is not implmented.')
386
- logging.warning(msg)
387
- self.C_P = np.nan
388
- self.C_F = np.nan
389
-
390
- self.c_F = self.C_F / self.E_F
391
- self.c_P = self.C_P / self.E_P
392
- self.C_D = self.c_F * self.E_D
393
- self.r = (self.c_P - self.c_F) / self.c_F
394
- self.f = self.Z_costs / (self.Z_costs + self.C_D)
580
+ # Productive valve (Cases 2 or 3 with split_physical_exergy=True)
581
+ if self.outl[0]["T"] <= T0 and self.inl[0]["T"] > T0:
582
+ # Case 2 with split
583
+ self.C_P = self.outl[0]["C_T"]
584
+ self.C_F = self.inl[0]["C_T"] + (self.inl[0]["C_M"] - self.outl[0]["C_M"])
585
+ elif self.inl[0]["T"] <= T0 and self.outl[0]["T"] <= T0:
586
+ # Case 3 with split
587
+ self.C_P = self.outl[0]["C_T"] - self.inl[0]["C_T"]
588
+ self.C_F = self.inl[0]["C_M"] - self.outl[0]["C_M"]
589
+ else:
590
+ logging.error(f"Productive valve {self.name} in unexpected temperature regime.")
591
+ self.C_P = np.nan
592
+ self.C_F = np.nan
593
+
594
+ self.c_F = self.C_F / self.E_F if self.E_F != 0 else np.nan
595
+ self.c_P = self.C_P / self.E_P if not np.isnan(self.E_P) and self.E_P != 0 else np.nan
596
+ self.C_D = self.c_F * self.E_D if not np.isnan(self.c_F) else np.nan
597
+ self.r = (
598
+ (self.c_P - self.c_F) / self.c_F
599
+ if not np.isnan(self.c_P) and not np.isnan(self.c_F) and self.c_F != 0
600
+ else np.nan
601
+ )
602
+ self.f = (
603
+ self.Z_costs / (self.Z_costs + self.C_D)
604
+ if not np.isnan(self.C_D) and (self.Z_costs + self.C_D) != 0
605
+ else np.nan
606
+ )