import logging
import numpy as np
from exerpy.components.component import Component, component_registry
[docs]
@component_registry
class Valve(Component):
r"""
Class for exergy and exergoeconomic analysis of valves.
This class performs exergy and exergoeconomic analysis calculations for valve components,
accounting for one inlet and one outlet streams across various temperature regimes, including
above and below ambient temperature.
Attributes
----------
E_F : float
Exergy fuel of the component :math:`\dot{E}_\mathrm{F}` in :math:`\mathrm{W}`.
E_P : float
Exergy product of the component :math:`\dot{E}_\mathrm{P}` in :math:`\mathrm{W}`.
E_D : float
Exergy destruction of the component :math:`\dot{E}_\mathrm{D}` in :math:`\mathrm{W}`.
epsilon : float
Exergetic efficiency of the component :math:`\varepsilon` in :math:`-`.
inl : dict
Dictionary containing inlet stream data with mass flows and specific exergies.
outl : dict
Dictionary containing outlet stream data with mass flows and specific exergies.
Z_costs : float
Investment cost rate of the component in currency/h.
C_P : float
Cost of product stream :math:`\dot{C}_P` in currency/h.
C_F : float
Cost of fuel stream :math:`\dot{C}_F` in currency/h.
C_D : float
Cost of exergy destruction :math:`\dot{C}_D` in currency/h.
c_P : float
Specific cost of product stream (currency per unit exergy).
c_F : float
Specific cost of fuel stream (currency per unit exergy).
r : float
Relative cost difference, :math:`(c_P - c_F)/c_F`.
f : float
Exergoeconomic factor, :math:`\dot{Z}/(\dot{Z} + \dot{C}_D)`.
Ex_C_col : dict
Custom cost coefficients collection passed via `kwargs`.
Notes
-----
The exergy analysis accounts for physical, thermal, and mechanical exergy
based on temperature relationships:
.. math::
\dot{E}_\mathrm{P} =
\begin{cases}
\text{not defined (nan)}
& T_\mathrm{in}, T_\mathrm{out} > T_0\\
\dot{m} \cdot e_\mathrm{out}^\mathrm{T}
& T_\mathrm{in} > T_0 \geq T_\mathrm{out}\\
\dot{m} \cdot (e_\mathrm{out}^\mathrm{T} - e_\mathrm{in}^\mathrm{T})
& T_0 \geq T_\mathrm{in}, T_\mathrm{out}
\end{cases}
\dot{E}_\mathrm{F} =
\begin{cases}
\dot{m} \cdot (e_\mathrm{in}^\mathrm{PH} - e_\mathrm{out}^\mathrm{PH})
& T_\mathrm{in}, T_\mathrm{out} > T_0\\
\dot{m} \cdot (e_\mathrm{in}^\mathrm{T} + e_\mathrm{in}^\mathrm{M}
- e_\mathrm{out}^\mathrm{M})
& T_\mathrm{in} > T_0 \geq T_\mathrm{out}\\
\dot{m} \cdot (e_\mathrm{in}^\mathrm{M} - e_\mathrm{out}^\mathrm{M})
& T_0 \geq T_\mathrm{in}, T_\mathrm{out}
\end{cases}
For all cases, except when :math:`T_\mathrm{out} > T_\mathrm{in}`, the exergy
destruction is calculated as:
.. math::
\dot{E}_\mathrm{D} = \begin{cases}
\dot{E}_\mathrm{F} & \text{if } \dot{E}_\mathrm{P} = \text{nan}\\
\dot{E}_\mathrm{F} - \dot{E}_\mathrm{P} & \mathrm{otherwise}
\end{cases}
Where:
- :math:`e^\mathrm{T}`: Thermal exergy
- :math:`e^\mathrm{PH}`: Physical exergy
- :math:`e^\mathrm{M}`: Mechanical exergy
"""
def __init__(self, **kwargs):
r"""Initialize valve component with given parameters."""
super().__init__(**kwargs)
[docs]
def calc_exergy_balance(self, T0: float, p0: float, split_physical_exergy) -> None:
r"""
Calculate the exergy balance of the valve.
Performs exergy balance calculations considering the temperature relationships
between inlet stream, outlet stream, and ambient conditions.
Parameters
----------
T0 : float
Ambient temperature in :math:`\mathrm{K}`.
p0 : float
Ambient pressure in :math:`\mathrm{Pa}`.
split_physical_exergy : bool
Flag indicating whether physical exergy is split into thermal and mechanical components.
Raises
------
ValueError
If the required inlet and outlet streams are not properly defined.
"""
# Ensure that the component has both inlet and outlet streams
if len(self.inl) < 1 or len(self.outl) < 1:
raise ValueError("Valve requires at least one inlet and one outlet.")
T_in = self.inl[0]["T"]
T_out = self.outl[0]["T"]
p_in = self.inl[0]["p"]
p_out = self.outl[0]["p"]
# Check for zero mass flow
if abs(self.inl[0]["m"]) < 1e-10:
logging.info(f"Valve {self.name} has zero mass flow: exergy balance not considered.")
self.E_P = np.nan
self.E_F = np.nan
self.E_D = np.nan
self.epsilon = np.nan
return
# Check if inlet and outlet are physically identical
if abs(T_in - T_out) < 1e-2 and abs(p_in - p_out) <= 1e-4 * max(p_in, 1e-9):
logging.info(f"Valve {self.name} inlet and outlet are physically identical.")
self.E_P = 0.0
self.E_F = 0.0
self.E_D = 0.0
self.epsilon = 1.0
logging.info(
f"Valve exergy balance calculated: "
f"E_P={self.E_P:.2f}, E_F={self.E_F:.2f}, E_D={self.E_D:.2f}, "
f"Efficiency={self.epsilon:.2%}"
)
return
# Case 1: Both temperatures above ambient
if T_in > T0 and T_out > T0:
self.E_P = np.nan
self.E_F = self.inl[0]["m"] * (self.inl[0]["e_PH"] - self.outl[0]["e_PH"])
# Case 2: Inlet above ambient, outlet below or equal to ambient
elif T_in > T0 and T_out <= T0:
if split_physical_exergy:
self.E_P = self.inl[0]["m"] * self.outl[0]["e_T"]
self.E_F = self.inl[0]["m"] * (self.inl[0]["e_T"] + self.inl[0]["e_M"] - self.outl[0]["e_M"])
else:
logging.warning(
"Exergy balance of a valve, where outlet temperature is smaller than "
"ambient temperature, is not implemented for non-split physical exergy. "
"Valve is treated as dissipative."
)
self.E_P = np.nan
self.E_F = self.inl[0]["m"] * (self.inl[0]["e_PH"] - self.outl[0]["e_PH"])
# Case 3: Both temperatures below ambient
elif T_in <= T0 and T_out <= T0:
if split_physical_exergy:
self.E_P = self.inl[0]["m"] * (self.outl[0]["e_T"] - self.inl[0]["e_T"])
self.E_F = self.inl[0]["m"] * (self.inl[0]["e_M"] - self.outl[0]["e_M"])
else:
logging.warning(
"Exergy balance of a valve, where both temperatures are smaller than "
"ambient temperature, is not implemented for non-split physical exergy."
"Valve is treated as dissipative."
)
self.E_P = np.nan
self.E_F = self.inl[0]["m"] * (self.inl[0]["e_PH"] - self.outl[0]["e_PH"])
# Case 4: Inlet below or at ambient, outlet above ambient
elif T_in <= T0 and T_out > T0:
logging.warning(
f"Valve {self.name} with temperature increase from below ambient to above ambient - "
"non-physical behavior. Treated as dissipative."
)
self.E_P = np.nan
self.E_F = self.inl[0]["m"] * (self.inl[0]["e_PH"] - self.outl[0]["e_PH"])
else:
logging.error(
f"Valve {self.name} encountered an unexpected condition: "
f"T_in={T_in:.2f} K, T_out={T_out:.2f} K, T0={T0:.2f} K. "
"This should not occur - exergy balance cannot be calculated."
)
self.E_P = np.nan
self.E_F = np.nan
self.E_D = np.nan
self.epsilon = np.nan
return
# Calculate exergy destruction
if np.isnan(self.E_P):
self.E_D = self.E_F
else:
self.E_D = self.E_F - self.E_P
# Calculate exergy efficiency
self.epsilon = self.calc_epsilon()
# Log the results
logging.info(
f"Exergy balance of Valve {self.name} calculated: "
f"E_P={self.E_P:.2f}, E_F={self.E_F:.2f}, E_D={self.E_D:.2f}, "
f"Efficiency={self.epsilon:.2%}"
)
[docs]
def aux_eqs(self, A, b, counter, T0, equations, chemical_exergy_enabled):
"""
Auxiliary equations for the valve.
This function adds rows to the cost matrix A and the right-hand-side vector b to enforce
the following auxiliary cost relations:
For (T_in > T0 and T_out > T0) or (T_in <= T0 and T_out > T0):
- Valve is treated as dissipative (warning issued)
For T_out <= T0:
(1) 1/E_M_in * C_M_in - 1/E_M_out * C_M_out = 0
- F-principle: specific mechanical exergy costs equalized between inlet/outlet
- If E_M is zero for either stream, appropriate fallback coefficients are used
When chemical_exergy_enabled is True:
(2) 1/E_CH_in * C_CH_in - 1/E_CH_out * C_CH_out = 0
- F-principle: specific chemical exergy costs equalized between inlet/outlet
- If E_CH is zero for either stream, appropriate fallback coefficients are used
Parameters
----------
A : numpy.ndarray
The current cost matrix.
b : numpy.ndarray
The current right-hand-side vector.
counter : int
The current row index in the matrix.
T0 : float
Ambient temperature.
equations : dict
Dictionary for storing equation labels.
chemical_exergy_enabled : bool
Flag indicating whether chemical exergy auxiliary equations should be added.
Returns
-------
A : numpy.ndarray
The updated cost matrix.
b : numpy.ndarray
The updated right-hand-side vector.
counter : int
The updated row index.
equations : dict
Updated dictionary with equation labels.
"""
# Check if valve is dissipative
if np.isnan(self.E_P):
logging.warning(f"Valve {self.name} is dissipative - no auxiliary equations added.")
return A, b, counter, equations
# Productive valve - T_out must be ≤ T0 (Cases 2 or 3)
# Mechanical cost equation (always added for productive valves)
if self.inl[0]["e_M"] != 0 and self.outl[0]["e_M"] != 0:
A[counter, self.inl[0]["CostVar_index"]["M"]] = 1 / self.inl[0]["E_M"]
A[counter, self.outl[0]["CostVar_index"]["M"]] = -1 / self.outl[0]["E_M"]
elif self.inl[0]["e_M"] == 0 and self.outl[0]["e_M"] != 0:
A[counter, self.inl[0]["CostVar_index"]["M"]] = 1
elif self.inl[0]["e_M"] != 0 and self.outl[0]["e_M"] == 0:
A[counter, self.outl[0]["CostVar_index"]["M"]] = 1
else:
A[counter, self.inl[0]["CostVar_index"]["M"]] = 1
A[counter, self.outl[0]["CostVar_index"]["M"]] = -1
equations[counter] = {
"kind": "aux_equality",
"objects": [self.name, self.inl[0]["name"], self.outl[0]["name"]],
"property": "c_M",
}
b[counter] = 0
counter += 1
if chemical_exergy_enabled:
# --- Chemical cost equation (conditionally added) ---
A[counter, self.inl[0]["CostVar_index"]["CH"]] = 1 / self.inl[0]["E_CH"] if self.inl[0]["e_CH"] != 0 else 1
A[counter, self.outl[0]["CostVar_index"]["CH"]] = (
-1 / self.outl[0]["E_CH"] if self.outl[0]["e_CH"] != 0 else -1
)
equations[counter] = {
"kind": "aux_equality",
"objects": [self.name, self.inl[0]["name"], self.outl[0]["name"]],
"property": "c_CH",
}
# Set right-hand side for both rows.
b[counter] = 0
counter += 1
return A, b, counter, equations
[docs]
def dis_eqs(self, A, b, counter, T0, equations, chemical_exergy_enabled=False, all_components=None):
r"""
Constructs the cost equations for a dissipative Valve in ExerPy,
distributing the valve's extra cost difference (C_diff) to all other productive
components (non-dissipative and non-CycleCloser) in proportion to their exergy destruction (E_D)
and adding an extra overall cost balance row that enforces:
.. math::
(\dot C_{\mathrm{in},T} - \dot C_{\mathrm{out},T})
+ (\dot C_{\mathrm{in},M} - \dot C_{\mathrm{out},M})
- \dot C_{\mathrm{diff}}
= -\,\dot Z_{\mathrm{costs}}
In this formulation, the unknown cost variable in the "dissipative" column (i.e. C_diff)
is solved for, ensuring the valve's cost balance.
Parameters
----------
A : numpy.ndarray
The current cost matrix.
b : numpy.ndarray
The current right-hand-side vector.
counter : int
The current row index in the cost matrix.
T0 : float
Ambient temperature (not explicitly used here).
equations : dict
Dictionary mapping row indices to equation labels.
chemical_exergy_enabled : bool, optional
Flag indicating whether chemical exergy is considered. (Ignored here.)
all_components : list, optional
Global list of all component objects; if not provided, defaults to [].
Returns
-------
tuple
Updated (A, b, counter, equations).
Notes
-----
- It is assumed that each inlet/outlet stream's CostVar_index dictionary has keys: "T" (thermal), "M" (mechanical), and "dissipative" (the extra unknown).
- self.Z_costs is the known cost rate (in currency/s) for the valve.
"""
# --- Thermal difference row ---
if self.inl[0].get("E_T", 0) and self.outl[0].get("E_T", 0):
A[counter, self.inl[0]["CostVar_index"]["T"]] = 1 / self.inl[0]["E_T"]
A[counter, self.outl[0]["CostVar_index"]["T"]] = -1 / self.outl[0]["E_T"]
else:
A[counter, self.inl[0]["CostVar_index"]["T"]] = 1
A[counter, self.outl[0]["CostVar_index"]["T"]] = -1
b[counter] = 0
equations[counter] = {
"kind": "dis_equality",
"objects": [self.name, self.inl[0]["name"], self.outl[0]["name"]],
"property": "c_T",
}
counter += 1
# --- Mechanical difference row ---
if self.inl[0].get("E_M", 0) and self.outl[0].get("E_M", 0):
A[counter, self.inl[0]["CostVar_index"]["M"]] = 1 / self.inl[0]["E_M"]
A[counter, self.outl[0]["CostVar_index"]["M"]] = -1 / self.outl[0]["E_M"]
else:
A[counter, self.inl[0]["CostVar_index"]["M"]] = 1
A[counter, self.outl[0]["CostVar_index"]["M"]] = -1
b[counter] = 0
equations[counter] = {
"kind": "dis_equality",
"objects": [self.name, self.inl[0]["name"], self.outl[0]["name"]],
"property": "c_M",
}
counter += 1
# --- Chemical difference row (if chemical exergy is enabled) ---
if chemical_exergy_enabled:
A[counter, self.inl[0]["CostVar_index"]["CH"]] = 1 / self.inl[0]["E_CH"] if self.inl[0]["E_CH"] != 0 else 1
A[counter, self.outl[0]["CostVar_index"]["CH"]] = (
-1 / self.outl[0]["E_CH"] if self.outl[0]["E_CH"] != 0 else -1
)
b[counter] = 0
equations[counter] = {
"kind": "dis_equality",
"objects": [self.name, self.inl[0]["name"], self.outl[0]["name"]],
"property": "c_CH",
}
counter += 1
# --- Distribution of dissipative cost difference to other components based on E_D ---
if all_components is None:
all_components = []
# Serving components: all productive components (excluding self, any dissipative, and CycleCloser)
serving = [
comp
for comp in all_components
if comp is not self
and hasattr(comp, "exergy_cost_line")
and not comp.__class__.__name__.endswith("PowerBus")
and hasattr(comp, "E_D")
and not np.isnan(comp.E_D)
]
total_E_D = sum(comp.E_D for comp in serving)
diss_col = self.inl[0]["CostVar_index"].get("dissipative")
if diss_col is None:
logging.error(f"No 'dissipative' column allocated for {self.name}.")
else:
if total_E_D == 0:
if len(serving) > 0:
for comp in serving:
A[comp.exergy_cost_line, diss_col] = 1 / len(serving)
else:
logging.warning(f"No serving components found for dissipative component {self.name}")
else:
for comp in serving:
weight = getattr(comp, "E_D", 0) / total_E_D
comp.serving_weight = weight
A[comp.exergy_cost_line, diss_col] = weight
# --- Extra overall cost balance row ---
# This row enforces:
# (C_in,T - C_out,T) + (C_in,M - C_out,M) - C_diff = - Z_costs
A[counter, self.inl[0]["CostVar_index"]["T"]] = 1
A[counter, self.outl[0]["CostVar_index"]["T"]] = -1
A[counter, self.inl[0]["CostVar_index"]["M"]] = 1
A[counter, self.outl[0]["CostVar_index"]["M"]] = -1
if chemical_exergy_enabled:
A[counter, self.inl[0]["CostVar_index"]["CH"]] = 1
A[counter, self.outl[0]["CostVar_index"]["CH"]] = -1
# Subtract the unknown dissipative cost difference:
A[counter, self.inl[0]["CostVar_index"]["dissipative"]] = -1
b[counter] = -self.Z_costs
equations[counter] = {"kind": "dis_balance", "objects": [self.name], "property": "dissipative_cost_balance"}
counter += 1
return A, b, counter, equations
[docs]
def exergoeconomic_balance(self, T0, chemical_exergy_enabled=False):
r"""
Perform exergoeconomic cost balance for the valve (throttling component).
The valve is a throttling device that reduces pressure without doing work.
Unlike other components, a valve can be either **dissipative** (purely destructive)
or **productive** depending on temperature conditions and whether physical
exergy is split into thermal and mechanical components.
**Dissipative Valve (E_P is NaN):**
When the valve has no identifiable product (Cases 1, 4, or Cases 2-3 without split),
the entire exergy loss is considered destruction. The cost of fuel is the decrease
in physical exergy cost:
.. math::
\dot{C}_{\mathrm{F}} = \dot{C}_{\mathrm{in}}^{\mathrm{PH}} - \dot{C}_{\mathrm{out}}^{\mathrm{PH}}
.. math::
\dot{C}_{\mathrm{P}} = \text{NaN (no product)}
**Productive Valve (E_P has a value):**
When physical exergy is split (split_physical_exergy=True) and specific temperature
conditions are met, the valve can have a product:
*Case 2 (Inlet above T0, outlet below T0):*
The product is the thermal exergy at the outlet (cooling capacity), and the fuel
is the thermal exergy decrease plus the mechanical exergy loss:
.. math::
\dot{C}_{\mathrm{P}} = \dot{C}_{\mathrm{out}}^{\mathrm{T}}
.. math::
\dot{C}_{\mathrm{F}} = \dot{C}_{\mathrm{in}}^{\mathrm{T}} + (\dot{C}_{\mathrm{in}}^{\mathrm{M}} - \dot{C}_{\mathrm{out}}^{\mathrm{M}})
*Case 3 (Both inlet and outlet below T0):*
The product is the increase in thermal exergy (cooling capacity increase), and
the fuel is the mechanical exergy loss:
.. math::
\dot{C}_{\mathrm{P}} = \dot{C}_{\mathrm{out}}^{\mathrm{T}} - \dot{C}_{\mathrm{in}}^{\mathrm{T}}
.. math::
\dot{C}_{\mathrm{F}} = \dot{C}_{\mathrm{in}}^{\mathrm{M}} - \dot{C}_{\mathrm{out}}^{\mathrm{M}}
**Calculated exergoeconomic indicators:**
Specific cost of fuel:
.. math::
c_{\mathrm{F}} = \frac{\dot{C}_{\mathrm{F}}}{\dot{E}_{\mathrm{F}}}
Specific cost of product:
.. math::
c_{\mathrm{P}} = \frac{\dot{C}_{\mathrm{P}}}{\dot{E}_{\mathrm{P}}}
Cost rate of exergy destruction:
.. math::
\dot{C}_{\mathrm{D}} = c_{\mathrm{F}} \cdot \dot{E}_{\mathrm{D}}
Relative cost difference:
.. math::
r = \frac{c_{\mathrm{P}} - c_{\mathrm{F}}}{c_{\mathrm{F}}}
Exergoeconomic factor:
.. math::
f = \frac{\dot{Z}}{\dot{Z} + \dot{C}_{\mathrm{D}}}
Parameters
----------
T0 : float
Ambient temperature (K).
chemical_exergy_enabled : bool, optional
If True, chemical exergy is considered in the calculations.
Default is False.
Attributes Set
--------------
C_P : float or NaN
Cost rate of product (currency/time). NaN for dissipative valves.
C_F : float or NaN
Cost rate of fuel (currency/time).
c_P : float or NaN
Specific cost of product (currency/energy). NaN for dissipative valves.
c_F : float or NaN
Specific cost of fuel (currency/energy).
C_D : float or NaN
Cost rate of exergy destruction (currency/time).
r : float or NaN
Relative cost difference (dimensionless). NaN for dissipative valves.
f : float or NaN
Exergoeconomic factor (dimensionless).
Notes
-----
The valve is unique among components because:
1. It typically has no capital cost (Z_costs ≈ 0), making the exergoeconomic
factor f close to zero.
2. It can be dissipative (no product) or productive (identifiable product)
depending on the analysis approach and temperature conditions.
3. For dissipative valves, many exergoeconomic parameters are NaN since there
is no identifiable product.
4. The relative cost difference r is calculated using specific costs (c_P and c_F)
rather than total cost rates, unlike generator and motor components.
5. The method handles NaN values throughout to accommodate both dissipative
and productive valve configurations.
The exergy destruction E_D and product/fuel definitions E_P and E_F must be
computed prior to calling this method (via exergy_balance).
For refrigeration cycles (Case 2) or cryogenic applications (Case 3), the
productive valve approach may be more appropriate. For typical throttling
applications, the dissipative approach is standard.
"""
# Check if valve is dissipative
if np.isnan(self.E_P):
# Dissipative valve (Cases 1, 4, or Cases 2-3 without split)
self.C_F = self.inl[0]["C_PH"] - self.outl[0]["C_PH"]
self.C_P = np.nan
else:
# Productive valve (Cases 2 or 3 with split_physical_exergy=True)
if self.outl[0]["T"] <= T0 and self.inl[0]["T"] > T0:
# Case 2 with split
self.C_P = self.outl[0]["C_T"]
self.C_F = self.inl[0]["C_T"] + (self.inl[0]["C_M"] - self.outl[0]["C_M"])
elif self.inl[0]["T"] <= T0 and self.outl[0]["T"] <= T0:
# Case 3 with split
self.C_P = self.outl[0]["C_T"] - self.inl[0]["C_T"]
self.C_F = self.inl[0]["C_M"] - self.outl[0]["C_M"]
else:
logging.error(f"Productive valve {self.name} in unexpected temperature regime.")
self.C_P = np.nan
self.C_F = np.nan
self.c_F = self.C_F / self.E_F if self.E_F != 0 else np.nan
self.c_P = self.C_P / self.E_P if not np.isnan(self.E_P) and self.E_P != 0 else np.nan
self.C_D = self.c_F * self.E_D if not np.isnan(self.c_F) else np.nan
self.r = (
(self.c_P - self.c_F) / self.c_F
if not np.isnan(self.c_P) and not np.isnan(self.c_F) and self.c_F != 0
else np.nan
)
self.f = (
self.Z_costs / (self.Z_costs + self.C_D)
if not np.isnan(self.C_D) and (self.Z_costs + self.C_D) != 0
else np.nan
)