exerpy 0.0.2__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 +53 -35
  5. exerpy/components/component.py +8 -8
  6. exerpy/components/heat_exchanger/base.py +186 -119
  7. exerpy/components/heat_exchanger/condenser.py +96 -60
  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 +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 +328 -226
  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.2.dist-info → exerpy-0.0.3.dist-info}/METADATA +43 -2
  36. exerpy-0.0.3.dist-info/RECORD +48 -0
  37. exerpy-0.0.2.dist-info/RECORD +0 -44
  38. {exerpy-0.0.2.dist-info → exerpy-0.0.3.dist-info}/WHEEL +0 -0
  39. {exerpy-0.0.2.dist-info → exerpy-0.0.3.dist-info}/licenses/LICENSE +0 -0
@@ -2,8 +2,7 @@ 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
@@ -57,7 +56,7 @@ class Valve(Component):
57
56
 
58
57
  \dot{E}_\mathrm{P} =
59
58
  \begin{cases}
60
- \mathrm{not defined (nan)}
59
+ \text{not defined (nan)}
61
60
  & T_\mathrm{in}, T_\mathrm{out} > T_0\\
62
61
  \dot{m} \cdot e_\mathrm{out}^\mathrm{T}
63
62
  & T_\mathrm{in} > T_0 \geq T_\mathrm{out}\\
@@ -69,19 +68,19 @@ class Valve(Component):
69
68
  \begin{cases}
70
69
  \dot{m} \cdot (e_\mathrm{in}^\mathrm{PH} - e_\mathrm{out}^\mathrm{PH})
71
70
  & T_\mathrm{in}, T_\mathrm{out} > T_0\\
72
- \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}
73
72
  - e_\mathrm{out}^\mathrm{M})
74
73
  & T_\mathrm{in} > T_0 \geq T_\mathrm{out}\\
75
74
  \dot{m} \cdot (e_\mathrm{in}^\mathrm{M} - e_\mathrm{out}^\mathrm{M})
76
75
  & T_0 \geq T_\mathrm{in}, T_\mathrm{out}
77
76
  \end{cases}
78
77
 
79
- 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
80
79
  destruction is calculated as:
81
80
 
82
81
  .. math::
83
82
  \dot{E}_\mathrm{D} = \begin{cases}
84
- \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}\\
85
84
  \dot{E}_\mathrm{F} - \dot{E}_\mathrm{P} & \mathrm{otherwise}
86
85
  \end{cases}
87
86
 
@@ -115,36 +114,64 @@ class Valve(Component):
115
114
  ------
116
115
  ValueError
117
116
  If the required inlet and outlet streams are not properly defined.
118
- """
117
+ """
119
118
  # Ensure that the component has both inlet and outlet streams
120
119
  if len(self.inl) < 1 or len(self.outl) < 1:
121
120
  raise ValueError("Valve requires at least one inlet and one outlet.")
122
121
 
123
- T_in = self.inl[0]['T']
124
- 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"]
125
127
 
126
- # Case-specific exergy calculations
127
- 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.")
128
131
  self.E_P = np.nan
129
- self.E_F = self.inl[0]['m'] * (self.inl[0]['e_PH'] - self.outl[0]['e_PH'])
130
- 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:
131
158
  if split_physical_exergy:
132
- self.E_P = self.inl[0]['m'] * self.outl[0]['e_T']
133
- self.E_F = self.inl[0]['m'] * (self.inl[0]['e_T'] + self.inl[0]['e_M'] -
134
- 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"])
135
161
  else:
136
162
  logging.warning(
137
163
  "Exergy balance of a valve, where outlet temperature is smaller than "
138
- "ambient temperature, is not implemented for non-split physical exergy."
164
+ "ambient temperature, is not implemented for non-split physical exergy. "
139
165
  "Valve is treated as dissipative."
140
166
  )
141
167
  self.E_P = np.nan
142
- 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"])
143
169
 
170
+ # Case 3: Both temperatures below ambient
144
171
  elif T_in <= T0 and T_out <= T0:
145
172
  if split_physical_exergy:
146
- self.E_P = self.inl[0]['m'] * (self.outl[0]['e_T'] - self.inl[0]['e_T'])
147
- 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"])
148
175
  else:
149
176
  logging.warning(
150
177
  "Exergy balance of a valve, where both temperatures are smaller than "
@@ -152,14 +179,28 @@ class Valve(Component):
152
179
  "Valve is treated as dissipative."
153
180
  )
154
181
  self.E_P = np.nan
155
- self.E_F = self.inl[0]['m'] * (self.inl[0]['e_PH'] - self.outl[0]['e_PH'])
156
- 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:
157
186
  logging.warning(
158
- "Exergy balance of a valve, where outlet temperature is larger than "
159
- "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."
160
198
  )
161
199
  self.E_P = np.nan
162
200
  self.E_F = np.nan
201
+ self.E_D = np.nan
202
+ self.epsilon = np.nan
203
+ return
163
204
 
164
205
  # Calculate exergy destruction
165
206
  if np.isnan(self.E_P):
@@ -172,35 +213,34 @@ class Valve(Component):
172
213
 
173
214
  # Log the results
174
215
  logging.info(
175
- f"Valve exergy balance calculated: "
216
+ f"Exergy balance of Valve {self.name} calculated: "
176
217
  f"E_P={self.E_P:.2f}, E_F={self.E_F:.2f}, E_D={self.E_D:.2f}, "
177
218
  f"Efficiency={self.epsilon:.2%}"
178
219
  )
179
220
 
180
-
181
221
  def aux_eqs(self, A, b, counter, T0, equations, chemical_exergy_enabled):
182
222
  """
183
223
  Auxiliary equations for the valve.
184
-
224
+
185
225
  This function adds rows to the cost matrix A and the right-hand-side vector b to enforce
186
226
  the following auxiliary cost relations:
187
-
188
- 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):
189
229
  - Valve is treated as dissipative (warning issued)
190
-
230
+
191
231
  For T_out <= T0:
192
232
  (1) 1/E_M_in * C_M_in - 1/E_M_out * C_M_out = 0
193
233
 
194
234
  - F-principle: specific mechanical exergy costs equalized between inlet/outlet
195
235
 
196
236
  - If E_M is zero for either stream, appropriate fallback coefficients are used
197
-
237
+
198
238
  When chemical_exergy_enabled is True:
199
239
  (2) 1/E_CH_in * C_CH_in - 1/E_CH_out * C_CH_out = 0
200
240
 
201
241
  - F-principle: specific chemical exergy costs equalized between inlet/outlet
202
242
  - If E_CH is zero for either stream, appropriate fallback coefficients are used
203
-
243
+
204
244
  Parameters
205
245
  ----------
206
246
  A : numpy.ndarray
@@ -215,7 +255,7 @@ class Valve(Component):
215
255
  Dictionary for storing equation labels.
216
256
  chemical_exergy_enabled : bool
217
257
  Flag indicating whether chemical exergy auxiliary equations should be added.
218
-
258
+
219
259
  Returns
220
260
  -------
221
261
  A : numpy.ndarray
@@ -227,46 +267,56 @@ class Valve(Component):
227
267
  equations : dict
228
268
  Updated dictionary with equation labels.
229
269
  """
230
- if self.inl[0]["T"] > T0 and self.outl[0]["T"] > T0:
231
- logging.warning("This case is not implemented. The Valve should be treated as dissipative!")
232
-
233
- elif self.outl[0]["T"] <= T0:
234
- # --- Mechanical cost equation (always added) ---
235
- if self.inl[0]["e_M"] != 0 and self.outl[0]["e_M"] != 0:
236
- A[counter, self.inl[0]["CostVar_index"]["M"]] = 1 / self.inl[0]["E_M"]
237
- A[counter, self.outl[0]["CostVar_index"]["M"]] = -1 / self.outl[0]["E_M"]
238
- elif self.inl[0]["e_M"] == 0 and self.outl[0]["e_M"] != 0:
239
- A[counter, self.inl[0]["CostVar_index"]["M"]] = 1
240
- elif self.inl[0]["e_M"] != 0 and self.outl[0]["e_M"] == 0:
241
- A[counter, self.outl[0]["CostVar_index"]["M"]] = 1
242
- else:
243
- A[counter, self.inl[0]["CostVar_index"]["M"]] = 1
244
- A[counter, self.outl[0]["CostVar_index"]["M"]] = -1
245
- equations[counter] = f"aux_{self.name}_mech_{self.outl[0]['name']}"
246
- b[counter] = 0
247
- 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
248
285
  else:
249
- msg = ('Exergy balance of a valve, where outlet temperature is larger than inlet temperature is not implemented.')
250
- logging.warning(msg)
251
-
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
+
252
296
  if chemical_exergy_enabled:
253
297
  # --- Chemical cost equation (conditionally added) ---
254
- A[counter, self.inl[0]["CostVar_index"]["CH"]] = (1 / self.inl[0]["E_CH"] if self.inl[0]["e_CH"] != 0 else 1)
255
- A[counter, self.outl[0]["CostVar_index"]["CH"]] = (-1 / self.outl[0]["E_CH"] if self.outl[0]["e_CH"] != 0 else -1)
256
- 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
+ }
257
307
  # Set right-hand side for both rows.
258
308
  b[counter] = 0
259
309
  counter += 1
260
-
310
+
261
311
  return A, b, counter, equations
262
-
312
+
263
313
  def dis_eqs(self, A, b, counter, T0, equations, chemical_exergy_enabled=False, all_components=None):
264
- """
314
+ r"""
265
315
  Constructs the cost equations for a dissipative Valve in ExerPy,
266
- 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
267
317
  components (non-dissipative and non-CycleCloser) in proportion to their exergy destruction (E_D)
268
318
  and adding an extra overall cost balance row that enforces:
269
-
319
+
270
320
  .. math::
271
321
  (\dot C_{\mathrm{in},T} - \dot C_{\mathrm{out},T})
272
322
  + (\dot C_{\mathrm{in},M} - \dot C_{\mathrm{out},M})
@@ -274,8 +324,8 @@ class Valve(Component):
274
324
  = -\,\dot Z_{\mathrm{costs}}
275
325
 
276
326
  In this formulation, the unknown cost variable in the "dissipative" column (i.e. C_diff)
277
- is solved for, ensuring the valves cost balance.
278
-
327
+ is solved for, ensuring the valve's cost balance.
328
+
279
329
  Parameters
280
330
  ----------
281
331
  A : numpy.ndarray
@@ -292,12 +342,12 @@ class Valve(Component):
292
342
  Flag indicating whether chemical exergy is considered. (Ignored here.)
293
343
  all_components : list, optional
294
344
  Global list of all component objects; if not provided, defaults to [].
295
-
345
+
296
346
  Returns
297
347
  -------
298
348
  tuple
299
349
  Updated (A, b, counter, equations).
300
-
350
+
301
351
  Notes
302
352
  -----
303
353
  - It is assumed that each inlet/outlet stream's CostVar_index dictionary has keys: "T" (thermal), "M" (mechanical), and "dissipative" (the extra unknown).
@@ -311,7 +361,11 @@ class Valve(Component):
311
361
  A[counter, self.inl[0]["CostVar_index"]["T"]] = 1
312
362
  A[counter, self.outl[0]["CostVar_index"]["T"]] = -1
313
363
  b[counter] = 0
314
- 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
+ }
315
369
  counter += 1
316
370
 
317
371
  # --- Mechanical difference row ---
@@ -322,30 +376,56 @@ class Valve(Component):
322
376
  A[counter, self.inl[0]["CostVar_index"]["M"]] = 1
323
377
  A[counter, self.outl[0]["CostVar_index"]["M"]] = -1
324
378
  b[counter] = 0
325
- 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
+ }
326
384
  counter += 1
327
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
+
328
400
  # --- Distribution of dissipative cost difference to other components based on E_D ---
329
401
  if all_components is None:
330
402
  all_components = []
331
403
  # Serving components: all productive components (excluding self, any dissipative, and CycleCloser)
332
- serving = [comp for comp in all_components
333
- if comp is not self
334
- and not getattr(comp, "dissipative", False)
335
- and comp.__class__.__name__ != "CycleCloser"]
336
- 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)
337
414
  diss_col = self.inl[0]["CostVar_index"].get("dissipative")
338
415
  if diss_col is None:
339
- logging.warning(f"No 'dissipative' column allocated for {self.name}.")
416
+ logging.error(f"No 'dissipative' column allocated for {self.name}.")
340
417
  else:
341
418
  if total_E_D == 0:
342
- # Fall back to equal distribution if total exergy destruction is zero.
343
- for comp in serving:
344
- 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}")
345
424
  else:
346
425
  for comp in serving:
347
426
  weight = getattr(comp, "E_D", 0) / total_E_D
348
- A[comp.exergy_cost_line, diss_col] += weight
427
+ comp.serving_weight = weight
428
+ A[comp.exergy_cost_line, diss_col] = weight
349
429
 
350
430
  # --- Extra overall cost balance row ---
351
431
  # This row enforces:
@@ -354,57 +434,173 @@ class Valve(Component):
354
434
  A[counter, self.outl[0]["CostVar_index"]["T"]] = -1
355
435
  A[counter, self.inl[0]["CostVar_index"]["M"]] = 1
356
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
357
440
  # Subtract the unknown dissipative cost difference:
358
441
  A[counter, self.inl[0]["CostVar_index"]["dissipative"]] = -1
359
442
  b[counter] = -self.Z_costs
360
- equations[counter] = f"diss_valve_balance_{self.name}"
443
+ equations[counter] = {"kind": "dis_balance", "objects": [self.name], "property": "dissipative_cost_balance"}
361
444
  counter += 1
362
445
 
363
446
  return A, b, counter, equations
364
447
 
448
+ def exergoeconomic_balance(self, T0, chemical_exergy_enabled=False):
449
+ r"""
450
+ Perform exergoeconomic cost balance for the valve (throttling component).
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.
365
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}}}
366
522
 
367
- def exergoeconomic_balance(self, T0):
368
- """
369
- Perform exergoeconomic balance calculations for the valve.
370
-
371
- This method calculates various exergoeconomic parameters including:
372
- - Cost rates of product (C_P) and fuel (C_F)
373
- - Specific cost of product (c_P) and fuel (c_F)
374
- - Cost rate of exergy destruction (C_D)
375
- - Relative cost difference (r)
376
- - Exergoeconomic factor (f)
377
-
378
523
  Parameters
379
524
  ----------
380
525
  T0 : float
381
- Ambient temperature
382
-
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
+
383
548
  Notes
384
549
  -----
385
- The exergoeconomic balance considers thermal (T), chemical (CH),
386
- 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.
387
573
  """
388
- if self.inl[0]["T"] > T0 and self.outl[0]["T"] > T0:
389
- 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"]
390
578
  self.C_P = np.nan
391
- # dissipative
392
- elif self.outl[0]["T"] <= T0 and self.inl[0]["T"] > T0:
393
- self.C_P = self.outl[0]["C_T"]
394
- self.C_F = self.inl[0]["C_T"] + (
395
- self.inl[0]["C_M"] - self.outl[0]["C_M"])
396
- elif self.inl[0]["T"] <= T0 and self.outl[0]["T"] <= T0:
397
- self.C_P = self.outl[0]["C_T"] - self.inl[0]["C_T"]
398
- self.C_F = self.inl[0]["C_M"] - self.outl[0]["C_M"]
399
579
  else:
400
- msg = ('Exergy balance of a valve, where outlet temperature is '
401
- 'larger than inlet temperature is not implmented.')
402
- logging.warning(msg)
403
- self.C_P = np.nan
404
- self.C_F = np.nan
405
-
406
- self.c_F = self.C_F / self.E_F
407
- self.c_P = self.C_P / self.E_P
408
- self.C_D = self.c_F * self.E_D
409
- self.r = (self.c_P - self.c_F) / self.c_F
410
- 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
+ )