import logging
import numpy as np
from exerpy.components.component import Component, component_registry
[docs]
@component_registry
class Mixer(Component):
r"""
Class for exergy analysis of mixers.
This class performs exergy analysis calculations for mixers with multiple
inlet streams and generally one outlet stream (multiple outlets are possible).
The exergy product and fuel definitions vary based on the temperature
relationships between inlet streams, outlet streams, and ambient conditions.
Parameters
----------
**kwargs : dict
Arbitrary keyword arguments passed to parent class.
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 streams data with temperature, mass flows,
and specific exergies.
outl : dict
Dictionary containing outlet stream data with temperature, mass flows,
and specific exergies.
Notes
-----
The exergy analysis accounts for physical exergy only. The equations for exergy
product and fuel are defined based on temperature relationships:
.. math::
\displaystyle
\dot E_{P} =
\begin{cases}
\displaystyle
\sum_{i}\dot m_{i}\,\bigl(e_{\mathrm{out}}^{\mathrm{PH}}
-e_{\mathrm{in},i}^{\mathrm{PH}}\bigr),
\quad \text{if }T_{\mathrm{in},i}<T_{\mathrm{out}}\text{ and }T_{\mathrm{in},i}\ge T_{0},\\[8pt]
\displaystyle
\sum_{i}\dot m_{i}\,e_{\mathrm{out}}^{\mathrm{PH}},
\quad \text{if }T_{\mathrm{in},i}<T_{\mathrm{out}}\text{ and }T_{\mathrm{in},i}<T_{0},\\[8pt]
\displaystyle
\text{not defined (nan)},
\quad \text{if }T_{\mathrm{out}}=T_{0},\\[8pt]
\displaystyle
\sum_{i}\dot m_{i}\,e_{\mathrm{out}}^{\mathrm{PH}},
\quad \text{if }T_{\mathrm{in},i}>T_{\mathrm{out}}\text{ and }T_{\mathrm{in},i}\ge T_{0},\\[8pt]
\displaystyle
\sum_{i}\dot m_{i}\,\bigl(e_{\mathrm{out}}^{\mathrm{PH}}
-e_{\mathrm{in},i}^{\mathrm{PH}}\bigr),
\quad \text{if }T_{\mathrm{in},i}>T_{\mathrm{out}}\text{ and }T_{\mathrm{in},i}<T_{0}.
\end{cases}
.. math::
\displaystyle
\dot E_{F} =
\begin{cases}
\displaystyle
\sum_{i}\dot m_{i}\,\bigl(e_{\mathrm{in},i}^{\mathrm{PH}}
-e_{\mathrm{out}}^{\mathrm{PH}}\bigr),
\quad \text{if }T_{\mathrm{out}}>T_{0}\text{ and }T_{\mathrm{in},i}>T_{\mathrm{out}},\\[8pt]
\displaystyle
\sum_{i}\dot m_{i}\,e_{\mathrm{in},i}^{\mathrm{PH}},
\quad \text{if }T_{\mathrm{out}}>T_{0}\text{ and }T_{\mathrm{in},i}<T_{\mathrm{out}}
\text{ and }T_{\mathrm{in},i}<T_{0},\\[8pt]
\displaystyle
\sum_{i}\dot m_{i}\,e_{\mathrm{in},i}^{\mathrm{PH}},
\quad \text{if }T_{\mathrm{out}}=T_{0},\\[8pt]
\displaystyle
\sum_{i}\dot m_{i}\,e_{\mathrm{in},i}^{\mathrm{PH}},
\quad \text{if }T_{\mathrm{out}}<T_{0}\text{ and }T_{\mathrm{in},i}>T_{\mathrm{out}},\\[8pt]
\displaystyle
\sum_{i}\dot m_{i}\,\bigl(e_{\mathrm{in},i}^{\mathrm{PH}}
-e_{\mathrm{out}}^{\mathrm{PH}}\bigr),
\quad \text{if }T_{\mathrm{out}}<T_{0}\text{ and }T_{\mathrm{in},i}<T_{\mathrm{out}}.
\end{cases}
"""
def __init__(self, **kwargs):
r"""Initialize mixer 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 mixer.
Performs exergy balance calculations considering the temperature relationships
between inlet streams, outlet stream(s), 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 at least two inlets and one outlet.
if len(self.inl) < 2 or len(self.outl) < 1:
raise ValueError("Mixer requires at least two inlets and one outlet.")
# Compute effective outlet state by aggregating all outlet streams.
# Assume that all outlets share the same thermodynamic state.
outlet_list = list(self.outl.values())
first_outlet = outlet_list[0]
T_out = first_outlet["T"]
e_out_PH = first_outlet["e_PH"]
# Verify that all outlets have the same thermodynamic state.
for outlet in outlet_list:
if outlet["T"] != T_out or outlet["e_PH"] != e_out_PH:
msg = "All outlets in Mixer must have the same thermodynamic state."
logging.error(msg)
raise ValueError(msg)
# Sum the mass of all outlet streams (if needed for further analysis)
sum(outlet.get("m", 0) for outlet in outlet_list)
# Initialize exergy product and fuel.
self.E_P = 0
self.E_F = 0
# Case 1: Outlet temperature is greater than ambient.
if T_out > T0:
for _, inlet in self.inl.items():
# Case when inlet temperature is lower than outlet temperature.
if inlet["T"] < T_out:
if inlet["T"] >= T0:
# Contribution to exergy product from inlets above ambient.
self.E_P += inlet["m"] * (e_out_PH - inlet["e_PH"])
else: # inlet['T'] < T0
self.E_P += inlet["m"] * e_out_PH
self.E_F += inlet["m"] * inlet["e_PH"]
else: # inlet['T'] > T_out
self.E_F += inlet["m"] * (inlet["e_PH"] - e_out_PH)
# Case 2: Outlet temperature equals ambient.
elif T_out == T0:
self.E_P = np.nan
for _, inlet in self.inl.items():
self.E_F += inlet["m"] * inlet["e_PH"]
# Case 3: Outlet temperature is less than ambient.
else: # T_out < T0
for _, inlet in self.inl.items():
if inlet["T"] > T_out:
if inlet["T"] >= T0:
self.E_P += inlet["m"] * e_out_PH
self.E_F += inlet["m"] * inlet["e_PH"]
else: # inlet['T'] < T0
self.E_P += inlet["m"] * (e_out_PH - inlet["e_PH"])
else: # inlet['T'] <= T_out
self.E_F += inlet["m"] * (inlet["e_PH"] - e_out_PH)
# Calculate exergy destruction and efficiency.
self.E_D = self.E_F - self.E_P
self.epsilon = self.calc_epsilon()
# Log the results.
logging.info(
f"Exergy balance of Mixer {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 mixer.
This function adds rows to the cost matrix A and the right-hand-side vector b to enforce
the following auxiliary cost relations:
(1) Mixing equation for chemical exergy costs (if enabled):
- The outlet's specific chemical exergy cost is calculated as a mass-weighted average of the inlet streams' specific chemical exergy costs
- This enforces proper chemical exergy cost distribution through the deaerator
(2) Mixing equation for mechanical exergy costs:
- The outlet's specific mechanical exergy cost is calculated as a mass-weighted average of the inlet streams' specific mechanical exergy costs
- This ensures mechanical exergy costs are properly conserved in the mixing process
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 (provided for consistency; not used in this function).
equations : list or dict
Data structure 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 (increased by 2 if chemical exergy is enabled, or by 1 otherwise).
equations : list or dict
Updated structure with equation labels.
"""
# --- Chemical cost auxiliary equation (conditionally added) ---
if chemical_exergy_enabled:
if self.outl[0]["e_CH"] != 0:
A[counter, self.outl[0]["CostVar_index"]["CH"]] = -1 / self.outl[0]["E_CH"]
# Iterate over inlet streams for chemical mixing.
for inlet in self.inl.values():
if inlet["e_CH"] != 0:
A[counter, inlet["CostVar_index"]["CH"]] = inlet["m"] / (self.outl[0]["m"] * inlet["E_CH"])
else:
A[counter, inlet["CostVar_index"]["CH"]] = 1
else:
# Outlet chemical exergy is zero: enforce cost conservation sum(C_CH_inlets) - C_CH_outlet = 0
for inlet in self.inl.values():
A[counter, inlet["CostVar_index"]["CH"]] = 1
A[counter, self.outl[0]["CostVar_index"]["CH"]] = -1
equations[counter] = {
"kind": "aux_mixing",
"objects": [self.name, self.inl[0]["name"], self.inl[1]["name"], self.outl[0]["name"]],
"property": "c_CH",
}
chem_row = 1 # One row added for chemical equation.
else:
chem_row = 0 # No row added.
# --- Mechanical cost auxiliary equation ---
if self.outl[0]["e_M"] != 0:
A[counter + chem_row, self.outl[0]["CostVar_index"]["M"]] = -1 / self.outl[0]["E_M"]
# Iterate over inlet streams for mechanical mixing.
for inlet in self.inl.values():
if inlet["e_M"] != 0:
A[counter + chem_row, inlet["CostVar_index"]["M"]] = inlet["m"] / (self.outl[0]["m"] * inlet["E_M"])
else:
A[counter + chem_row, inlet["CostVar_index"]["M"]] = 1
else:
# When outlet e_M = 0, enforce cost conservation: sum(C_M_inlets) - C_M_outlet = 0
for inlet in self.inl.values():
A[counter + chem_row, inlet["CostVar_index"]["M"]] = 1
A[counter + chem_row, self.outl[0]["CostVar_index"]["M"]] = -1
# Dynamically build the list of inlet names
inlet_names = [inlet["name"] for inlet in self.inl.values()]
equations[counter + chem_row] = {
"kind": "aux_mixing",
"objects": [self.name] + inlet_names + [self.outl[0]["name"]],
"property": "c_M",
}
# Set the right-hand side entries to zero for the added rows.
if chemical_exergy_enabled:
b[counter] = 0
b[counter + 1] = 0
counter += 2 # Two rows were added.
else:
b[counter] = 0
counter += 1 # Only one row was added.
return A, b, counter, equations
[docs]
def exergoeconomic_balance(self, T0, chemical_exergy_enabled=False):
r"""
Perform exergoeconomic cost balance for the mixer.
The mixer is a component where multiple streams combine. The general
exergoeconomic balance equation is:
.. math::
\sum_{\mathrm{in}} \left(\dot{C}^{\mathrm{T}}_{\mathrm{in}}
+ \dot{C}^{\mathrm{M}}_{\mathrm{in}}
+ \dot{C}^{\mathrm{CH}}_{\mathrm{in}}\right)
- \sum_{\mathrm{out}} \left(\dot{C}^{\mathrm{T}}_{\mathrm{out}}
+ \dot{C}^{\mathrm{M}}_{\mathrm{out}}
+ \dot{C}^{\mathrm{CH}}_{\mathrm{out}}\right)
+ \dot{Z}
= 0
The product is defined as the outlet stream. The fuel consists of all inlet
streams, with treatment depending on temperature levels and whether chemical
exergy is enabled. The cost balance is closed using:
.. math::
\dot{C}_{\mathrm{P}} = \dot{C}_{\mathrm{F}} + \dot{Z}
**Case 1: Outlet above ambient temperature**
When :math:`T_{\mathrm{out}} > T_0`:
For cold inlets (:math:`T_{\mathrm{in}} < T_{\mathrm{out}}`):
Without chemical exergy:
.. math::
\dot{C}_{\mathrm{F,cold}} = \dot{C}^{\mathrm{M}}_{\mathrm{in}}
With chemical exergy enabled:
.. math::
\dot{C}_{\mathrm{F,cold}} = \dot{C}^{\mathrm{M}}_{\mathrm{in}}
+ \dot{C}^{\mathrm{CH}}_{\mathrm{in}}
For hot inlets (:math:`T_{\mathrm{in}} \geq T_{\mathrm{out}}`):
Without chemical exergy:
.. math::
\dot{C}_{\mathrm{F,hot}} = -\dot{m}_{\mathrm{in}} \cdot c^{\mathrm{T}}_{\mathrm{in}}
\cdot e^{\mathrm{T}}_{\mathrm{in}}
+ \left(\dot{C}^{\mathrm{T}}_{\mathrm{in}}
+ \dot{C}^{\mathrm{M}}_{\mathrm{in}}\right)
With chemical exergy enabled:
.. math::
\dot{C}_{\mathrm{F,hot}} = -\dot{m}_{\mathrm{in}} \cdot c^{\mathrm{T}}_{\mathrm{in}}
\cdot e^{\mathrm{T}}_{\mathrm{in}}
+ \left(\dot{C}^{\mathrm{T}}_{\mathrm{in}}
+ \dot{C}^{\mathrm{M}}_{\mathrm{in}}
+ \dot{C}^{\mathrm{CH}}_{\mathrm{in}}\right)
Total fuel cost (without chemical exergy):
.. math::
\dot{C}_{\mathrm{F}} = \sum \dot{C}_{\mathrm{F,cold}}
+ \sum \dot{C}_{\mathrm{F,hot}}
- \dot{C}^{\mathrm{M}}_{\mathrm{out}}
Total fuel cost (with chemical exergy):
.. math::
\dot{C}_{\mathrm{F}} = \sum \dot{C}_{\mathrm{F,cold}}
+ \sum \dot{C}_{\mathrm{F,hot}}
- \dot{C}^{\mathrm{M}}_{\mathrm{out}}
- \dot{C}^{\mathrm{CH}}_{\mathrm{out}}
**Case 2: Outlet at ambient temperature (dissipative)**
When :math:`|T_{\mathrm{out}} - T_0| < 10^{-6}`:
.. math::
\dot{C}_{\mathrm{F}} = \sum_{\mathrm{in}} \dot{C}^{\mathrm{TOT}}_{\mathrm{in}}
**Case 3: Outlet below ambient temperature**
When :math:`T_{\mathrm{out}} < T_0`:
For hot inlets (:math:`T_{\mathrm{in}} > T_{\mathrm{out}}`):
Without chemical exergy:
.. math::
\dot{C}_{\mathrm{F,hot}} = \dot{C}^{\mathrm{M}}_{\mathrm{in}}
With chemical exergy enabled:
.. math::
\dot{C}_{\mathrm{F,hot}} = \dot{C}^{\mathrm{M}}_{\mathrm{in}}
+ \dot{C}^{\mathrm{CH}}_{\mathrm{in}}
For cold inlets (:math:`T_{\mathrm{in}} \leq T_{\mathrm{out}}`):
Without chemical exergy:
.. math::
\dot{C}_{\mathrm{F,cold}} = -\dot{m}_{\mathrm{in}} \cdot c^{\mathrm{T}}_{\mathrm{in}}
\cdot e^{\mathrm{T}}_{\mathrm{in}}
+ \left(\dot{C}^{\mathrm{T}}_{\mathrm{in}}
+ \dot{C}^{\mathrm{M}}_{\mathrm{in}}\right)
With chemical exergy enabled:
.. math::
\dot{C}_{\mathrm{F,cold}} = -\dot{m}_{\mathrm{in}} \cdot c^{\mathrm{T}}_{\mathrm{in}}
\cdot e^{\mathrm{T}}_{\mathrm{in}}
+ \left(\dot{C}^{\mathrm{T}}_{\mathrm{in}}
+ \dot{C}^{\mathrm{M}}_{\mathrm{in}}
+ \dot{C}^{\mathrm{CH}}_{\mathrm{in}}\right)
Total fuel cost (without chemical exergy):
.. math::
\dot{C}_{\mathrm{F}} = \sum \dot{C}_{\mathrm{F,hot}}
+ \sum \dot{C}_{\mathrm{F,cold}}
- \dot{C}^{\mathrm{M}}_{\mathrm{out}}
Total fuel cost (with chemical exergy):
.. math::
\dot{C}_{\mathrm{F}} = \sum \dot{C}_{\mathrm{F,hot}}
+ \sum \dot{C}_{\mathrm{F,cold}}
- \dot{C}^{\mathrm{M}}_{\mathrm{out}}
- \dot{C}^{\mathrm{CH}}_{\mathrm{out}}
**Calculated exergoeconomic indicators:**
.. math::
c_{\mathrm{F}} = \frac{\dot{C}_{\mathrm{F}}}{\dot{E}_{\mathrm{F}}}
.. math::
c_{\mathrm{P}} = \frac{\dot{C}_{\mathrm{P}}}{\dot{E}_{\mathrm{P}}}
.. math::
\dot{C}_{\mathrm{D}} = c_{\mathrm{F}} \cdot \dot{E}_{\mathrm{D}}
.. math::
r = \frac{c_{\mathrm{P}} - c_{\mathrm{F}}}{c_{\mathrm{F}}}
.. 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
Cost rate of product (currency/time).
C_F : float
Cost rate of fuel (currency/time).
c_P : float
Specific cost of product (currency/energy).
c_F : float
Specific cost of fuel (currency/energy).
C_D : float
Cost rate of exergy destruction (currency/time).
r : float
Relative cost difference (dimensionless).
f : float
Exergoeconomic factor (dimensionless).
Notes
-----
The mixer treats thermal, mechanical, and chemical exergy components
differently depending on whether inlets are "hot" or "cold" relative to
the outlet temperature. The distinction ensures proper cost allocation
for streams that provide heating versus those being heated.
Future development may include merging profits from dissipative components.
"""
self.C_P = 0
self.C_F = 0
if self.outl[0]["T"] > T0:
for i in self.inl.values():
if i["T"] < self.outl[0]["T"]:
# cold inlets
self.C_F += i["C_M"]
if chemical_exergy_enabled:
self.C_F += i["C_CH"]
else:
# hot inlets
self.C_F += -i["m"] * i["c_T"] * i["e_T"] + (i["C_T"] + i["C_M"])
if chemical_exergy_enabled:
self.C_F += i["C_CH"]
self.C_F += -self.outl[0]["C_M"]
if chemical_exergy_enabled:
self.C_F += -self.outl[0]["C_CH"]
elif self.outl[0]["T"] - 1e-6 < T0 and self.outl[0]["T"] + 1e-6 > T0:
# dissipative
for i in self.inl.values():
self.C_F += i["C_TOT"]
else:
for i in self.inl.values():
if i["T"] > self.outl[0]["T"]:
# hot inlets
self.C_F += i["C_M"]
if chemical_exergy_enabled:
self.C_F += i["C_CH"]
else:
# cold inlets
self.C_F += -i["m"] * i["c_T"] * i["e_T"] + (i["C_T"] + i["C_M"])
if chemical_exergy_enabled:
self.C_F += i["C_CH"]
self.C_F += -self.outl[0]["C_M"]
if chemical_exergy_enabled:
self.C_F += -self.outl[0]["C_CH"]
self.C_P = self.C_F + self.Z_costs + getattr(self, "Z_diss", 0)
self.c_F = self.C_F / self.E_F
self.c_P = self.C_P / self.E_P
self.C_D = self.c_F * self.E_D
self.r = (self.c_P - self.c_F) / self.c_F
self.f = self.Z_costs / (self.Z_costs + self.C_D)