Source code for exerpy.components.heat_exchanger.simple

import logging

import numpy as np

from exerpy.components.component import Component, component_registry


[docs] @component_registry class SimpleHeatExchanger(Component): r""" Class for exergy and exergoeconomic analysis of simple heat exchangers. This class performs exergy and exergoeconomic analysis calculations for heat exchanger components, accounting for one inlet and one outlet stream across various temperature regimes, including above and below ambient temperature, and optional dissipative behavior. 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`. """ def __init__(self, **kwargs): r""" Initialize the heat exchanger component. Parameters ---------- **kwargs : dict Arbitrary keyword arguments. Recognized keys: - dissipative (bool): whether component has dissipative behavior, default False - Ex_C_col (dict): custom cost coefficients, default {} - Z_costs (float): investment cost rate in currency/h, default 0.0 """ super().__init__(**kwargs)
[docs] def calc_exergy_balance(self, T0: float, p0: float, split_physical_exergy) -> None: r""" Compute the exergy balance of the simple heat exchanger. **Heat release** :math:\dot{Q}<0 Case 1: Both streams above ambient temperature If split_physical_exergy=True: .. math:: \dot{E}_{\mathrm{P}} = \dot{E}^{\mathrm{T}}_{\mathrm{out}} - \dot{E}^{\mathrm{T}}_{\mathrm{in}} .. math:: \dot{E}_{\mathrm{F}} = \dot{E}^{\mathrm{PH}}_{\mathrm{in}} - \dot{E}^{\mathrm{PH}}_{\mathrm{out}} Else: .. math:: \dot{E}_{\mathrm{P}} = \dot{E}^{\mathrm{PH}}_{\mathrm{in}} - \dot{E}^{\mathrm{PH}}_{\mathrm{out}} .. math:: \dot{E}_{\mathrm{F}} = \dot{E}^{\mathrm{PH}}_{\mathrm{in}} - \dot{E}^{\mathrm{PH}}_{\mathrm{out}} Case 2: Inlet above and outlet below ambient temperature If split_physical_exergy=True: .. math:: \dot{E}_{\mathrm{P}} = \dot{E}^{\mathrm{T}}_{\mathrm{out}} .. math:: \dot{E}_{\mathrm{F}} = \dot{E}^{\mathrm{T}}_{\mathrm{in}} + \dot{E}^{\mathrm{T}}_{\mathrm{out}} + \bigl(\dot{E}^{\mathrm{M}}_{\mathrm{in}} - \dot{E}^{\mathrm{M}}_{\mathrm{out}}\bigr) Else: .. math:: \dot{E}_{\mathrm{P}} = \dot{E}^{\mathrm{PH}}_{\mathrm{out}} .. math:: \dot{E}_{\mathrm{F}} = \dot{E}^{\mathrm{PH}}_{\mathrm{in}} Case 3: Both streams below ambient temperature If split_physical_exergy=True: .. math:: \dot{E}_{\mathrm{P}} = \dot{E}^{\mathrm{T}}_{\mathrm{out}} - \dot{E}^{\mathrm{T}}_{\mathrm{in}} .. math:: \dot{E}_{\mathrm{F}} = \bigl(\dot{E}^{\mathrm{T}}_{\mathrm{out}} - \dot{E}^{\mathrm{T}}_{\mathrm{in}}\bigr) + \bigl(\dot{E}^{\mathrm{M}}_{\mathrm{in}} - \dot{E}^{\mathrm{M}}_{\mathrm{out}}\bigr) Else: .. math:: \dot{E}_{\mathrm{P}} = \dot{E}^{\mathrm{PH}}_{\mathrm{out}} - \dot{E}^{\mathrm{PH}}_{\mathrm{in}} .. math:: \dot{E}_{\mathrm{F}} = \dot{E}^{\mathrm{PH}}_{\mathrm{out}} - \dot{E}^{\mathrm{PH}}_{\mathrm{in}} **Heat injection** :math:\dot{Q}>0 Case 1: Both streams above ambient temperature If split_physical_exergy=True: .. math:: \dot{E}_{\mathrm{P}} = \dot{E}^{\mathrm{PH}}_{\mathrm{out}} - \dot{E}^{\mathrm{PH}}_{\mathrm{in}} .. math:: \dot{E}_{\mathrm{F}} = \dot{E}^{\mathrm{T}}_{\mathrm{out}} - \dot{E}^{\mathrm{T}}_{\mathrm{in}} Else: .. math:: \dot{E}_{\mathrm{P}} = \dot{E}^{\mathrm{PH}}_{\mathrm{out}} - \dot{E}^{\mathrm{PH}}_{\mathrm{in}} .. math:: \dot{E}_{\mathrm{F}} = \dot{E}^{\mathrm{PH}}_{\mathrm{out}} - \dot{E}^{\mathrm{PH}}_{\mathrm{in}} Case 2: Inlet below and outlet above ambient temperature If split_physical_exergy=True: .. math:: \dot{E}_{\mathrm{P}} = \dot{E}^{\mathrm{T}}_{\mathrm{out}} + \dot{E}^{\mathrm{T}}_{\mathrm{in}} .. math:: \dot{E}_{\mathrm{F}} = \dot{E}^{\mathrm{T}}_{\mathrm{in}} + \bigl(\dot{E}^{\mathrm{M}}_{\mathrm{in}} - \dot{E}^{\mathrm{M}}_{\mathrm{out}}\bigr) Else: .. math:: \dot{E}_{\mathrm{P}} = \dot{E}^{\mathrm{PH}}_{\mathrm{out}} - \dot{E}^{\mathrm{PH}}_{\mathrm{in}} .. math:: \dot{E}_{\mathrm{F}} = \dot{E}^{\mathrm{PH}}_{\mathrm{out}} - \dot{E}^{\mathrm{PH}}_{\mathrm{in}} Case 3: Both streams below ambient temperature If split_physical_exergy=True: .. math:: \dot{E}_{\mathrm{P}} = \dot{E}^{\mathrm{T}}_{\mathrm{in}} - \dot{E}^{\mathrm{T}}_{\mathrm{out}} + \bigl(\dot{E}^{\mathrm{M}}_{\mathrm{out}} - \dot{E}^{\mathrm{M}}_{\mathrm{in}}\bigr) .. math:: \dot{E}_{\mathrm{F}} = \dot{E}^{\mathrm{T}}_{\mathrm{in}} - \dot{E}^{\mathrm{T}}_{\mathrm{out}} Else: .. math:: \dot{E}_{\mathrm{P}} = \dot{E}^{\mathrm{PH}}_{\mathrm{in}} - \dot{E}^{\mathrm{PH}}_{\mathrm{out}} .. math:: \dot{E}_{\mathrm{F}} = \dot{E}^{\mathrm{PH}}_{\mathrm{in}} - \dot{E}^{\mathrm{PH}}_{\mathrm{out}} Fully dissipative or :math:\dot{Q}=0 .. math:: \dot{E}_{\mathrm{P}} = \mathrm{NaN} .. math:: \dot{E}_{\mathrm{F}} = \dot{E}^{\mathrm{PH}}_{\mathrm{in}} - \dot{E}^{\mathrm{PH}}_{\mathrm{out}} Parameters ---------- T0 : float Ambient temperature (K). p0 : float Ambient pressure (Pa). split_physical_exergy : bool Whether to split thermal and mechanical exergy. Raises ------ ValueError If required inlet or outlet are missing. """ # Validate the number of inlets and outlets if not hasattr(self, "inl") or not hasattr(self, "outl") or len(self.inl) < 1 or len(self.outl) < 1: msg = "SimpleHeatExchanger requires at least one inlet and one outlet as well as one heat flow." logging.error(msg) raise ValueError(msg) if len(self.inl) > 2 or len(self.outl) > 2: msg = "SimpleHeatExchanger requires a maximum of two inlets and two outlets." logging.error(msg) raise ValueError(msg) # Extract inlet and outlet streams inlet = self.inl[0] outlet = self.outl[0] # Calculate heat transfer Q Q = outlet["m"] * outlet["h"] - inlet["m"] * inlet["h"] # Initialize E_P and E_F self.E_P = 0.0 self.E_F = 0.0 # Case 1: Heat is released (Q < 0) if Q < 0: if inlet["T"] >= T0 and outlet["T"] >= T0: if split_physical_exergy: self.E_P = ( np.nan if getattr(self, "dissipative", False) else inlet["m"] * (inlet["e_T"] - outlet["e_T"]) ) else: self.E_P = ( np.nan if getattr(self, "dissipative", False) else inlet["m"] * (inlet["e_PH"] - outlet["e_PH"]) ) self.E_F = inlet["m"] * (inlet["e_PH"] - outlet["e_PH"]) elif inlet["T"] >= T0 and outlet["T"] < T0: if split_physical_exergy: self.E_P = outlet["m"] * outlet["e_T"] self.E_F = ( inlet["m"] * inlet["e_T"] + outlet["m"] * outlet["e_T"] + (inlet["m"] * inlet["e_M"] - outlet["m"] * outlet["e_M"]) ) else: self.E_P = outlet["m"] * outlet["e_PH"] self.E_F = inlet["m"] * inlet["e_PH"] elif inlet["T"] <= T0 and outlet["T"] < T0: if split_physical_exergy: self.E_P = outlet["m"] * (outlet["e_T"] - inlet["e_T"]) self.E_F = self.E_P + inlet["m"] * (inlet["e_M"] - outlet["m"] * outlet["e_M"]) else: self.E_P = ( np.nan if getattr(self, "dissipative", False) else outlet["m"] * (outlet["e_PH"] - inlet["e_PH"]) ) self.E_F = outlet["m"] * (outlet["e_PH"] - inlet["e_PH"]) else: # Unimplemented corner case logging.warning("SimpleHeatExchanger: unimplemented case (Q < 0, T_in < T0 < T_out?).") self.E_P = np.nan self.E_F = np.nan # Case 2: Heat is added (Q > 0) elif Q > 0: if inlet["T"] >= T0 and outlet["T"] >= T0: if split_physical_exergy: self.E_P = outlet["m"] * (outlet["e_PH"] - inlet["e_PH"]) self.E_F = outlet["m"] * (outlet["e_T"] - inlet["e_T"]) else: self.E_P = outlet["m"] * (outlet["e_PH"] - inlet["e_PH"]) self.E_F = outlet["m"] * (outlet["e_PH"] - inlet["e_PH"]) elif inlet["T"] < T0 and outlet["T"] >= T0: if split_physical_exergy: self.E_P = outlet["m"] * (outlet["e_T"] + inlet["e_T"]) self.E_F = inlet["m"] * inlet["e_T"] + (inlet["m"] * inlet["e_M"] - outlet["m"] * outlet["e_M"]) else: self.E_P = outlet["m"] * (outlet["e_PH"] - inlet["e_PH"]) self.E_F = outlet["m"] * (outlet["e_PH"] - inlet["e_PH"]) elif inlet["T"] < T0 and outlet["T"] <= T0: if split_physical_exergy: self.E_P = ( np.nan if getattr(self, "dissipative", False) else inlet["m"] * (inlet["e_T"] - outlet["e_T"]) + (outlet["m"] * outlet["e_M"] - inlet["m"] * inlet["e_M"]) ) self.E_F = inlet["m"] * (inlet["e_T"] - outlet["e_T"]) else: self.E_P = ( np.nan if getattr(self, "dissipative", False) else inlet["m"] * (inlet["e_PH"] - outlet["e_PH"]) ) self.E_F = inlet["m"] * (inlet["e_PH"] - outlet["e_PH"]) else: logging.warning("SimpleHeatExchanger: unimplemented case (Q > 0, T_in > T0 > T_out?).") self.E_P = np.nan self.E_F = np.nan # Case 3: Fully dissipative or Q == 0 else: self.E_P = np.nan self.E_F = inlet["m"] * (inlet["e_PH"] - outlet["e_PH"]) # 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 SimpleHeatExchanger {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): r""" This function must be implemented in the future. The exergoeconomic analysis of SimpleHeatExchanger is not implemented yet. """ # Extract inlet and outlet inlet = self.inl[0] outlet = self.outl[0] # Calculate heat transfer Q Q = outlet["m"] * outlet["h"] - inlet["m"] * inlet["h"] # Extract temperatures T_in = inlet["T"] T_out = outlet["T"] # Equality equation for mechanical exergy costs (c_M,in = c_M,out) A[counter, inlet["CostVar_index"]["M"]] = 1 / inlet["E_M"] if inlet["e_M"] != 0 else 1 A[counter, outlet["CostVar_index"]["M"]] = -1 / outlet["E_M"] if outlet["e_M"] != 0 else -1 equations[counter] = { "kind": "aux_equality", "objects": [self.name, inlet["name"], outlet["name"]], "property": "c_M", } b[counter] = 0 counter += 1 # Equality equation for chemical exergy costs (c_CH,in = c_CH,out) if chemical_exergy_enabled: A[counter, inlet["CostVar_index"]["CH"]] = 1 / inlet["E_CH"] if inlet["e_CH"] != 0 else 1 A[counter, outlet["CostVar_index"]["CH"]] = -1 / outlet["E_CH"] if outlet["e_CH"] != 0 else -1 equations[counter] = { "kind": "aux_equality", "objects": [self.name, inlet["name"], outlet["name"]], "property": "c_CH", } b[counter] = 0 counter += 1 # Thermal exergy cost equations # Case 1: Heat is released (Q < 0) if Q < 0: # Case 1.1: Both streams above ambient temperature if T_in >= T0 and T_out >= T0: # Apply F-rule to thermal exergy (c_T,in = c_T,out) A[counter, inlet["CostVar_index"]["T"]] = 1 / inlet["E_T"] if inlet["e_T"] != 0 else 1 A[counter, outlet["CostVar_index"]["T"]] = -1 / outlet["E_T"] if outlet["e_T"] != 0 else -1 equations[counter] = { "kind": "aux_f_rule", "objects": [self.name, inlet["name"], outlet["name"]], "property": "c_T", } b[counter] = 0 counter += 1 elif T_in >= T0 and T_out < T0: # Tricky case: inlet above T0, outlet below T0 logging.warning( f"SimpleHeatExchanger '{self.name}': Stream crossing ambient temperature " f"during heat release not implemented in exergoeconomics yet!" ) else: # Tricky case: both streams below T0 while heat is released logging.warning( f"SimpleHeatExchanger '{self.name}': Both streams below T0 during heat release " f"not implemented in exergoeconomics yet!" ) # Case 2: Heat is added (Q > 0) elif Q > 0: # Case 2.1: Both streams below ambient temperature if T_in < T0 and T_out < T0: # No auxiliary equation needed for thermal exergy # The cost balance will determine c_T,out based on c_T,in and c_heat pass elif T_in < T0 and T_out >= T0: # Tricky case: inlet below T0, outlet above T0 logging.warning( f"SimpleHeatExchanger '{self.name}': Stream crossing ambient temperature " f"during heat absorption not implemented in exergoeconomics yet!" ) # Case 2.2: Both streams above ambient temperature elif T_in >= T0 and T_out >= T0: # No auxiliary equation needed for thermal exergy # The cost balance will determine c_T,out based on c_T,in and c_heat pass return A, b, counter, equations
[docs] def exergoeconomic_balance(self, T0, chemical_exergy_enabled=False): r""" Perform exergoeconomic cost balance for the simple heat exchanger. The general exergoeconomic balance equation is: .. math:: \dot{C}^{\mathrm{T}}_{\mathrm{in}} + \dot{C}^{\mathrm{M}}_{\mathrm{in}} - \dot{C}^{\mathrm{T}}_{\mathrm{out}} - \dot{C}^{\mathrm{M}}_{\mathrm{out}} + \dot{Z} = 0 In case the chemical exergy of the streams is known: .. math:: \dot{C}^{\mathrm{CH}}_{\mathrm{in}} = \dot{C}^{\mathrm{CH}}_{\mathrm{out}} This method computes cost rates for product and fuel, and derives exergoeconomic indicators based on the operating conditions. **Heat release** (:math:`\dot{Q} < 0`) Case 1: Both streams above ambient temperature .. math:: \dot{E}_{\mathrm{P}} = \dot{E}^{\mathrm{T}}_{\mathrm{out}} - \dot{E}^{\mathrm{T}}_{\mathrm{in}} .. math:: \dot{E}_{\mathrm{F}} = \dot{E}^{\mathrm{PH}}_{\mathrm{in}} - \dot{E}^{\mathrm{PH}}_{\mathrm{out}} Case 2: Inlet above and outlet below ambient temperature .. math:: \dot{E}_{\mathrm{P}} = \dot{E}^{\mathrm{T}}_{\mathrm{out}} .. math:: \dot{E}_{\mathrm{F}} = \dot{E}^{\mathrm{T}}_{\mathrm{in}} + \dot{E}^{\mathrm{T}}_{\mathrm{out}} + \bigl(\dot{E}^{\mathrm{M}}_{\mathrm{in}} - \dot{E}^{\mathrm{M}}_{\mathrm{out}}\bigr) Case 3: Both streams below ambient temperature .. math:: \dot{E}_{\mathrm{P}} = \dot{E}^{\mathrm{T}}_{\mathrm{out}} - \dot{E}^{\mathrm{T}}_{\mathrm{in}} .. math:: \dot{E}_{\mathrm{F}} = \bigl(\dot{E}^{\mathrm{T}}_{\mathrm{out}} - \dot{E}^{\mathrm{T}}_{\mathrm{in}}\bigr) + \bigl(\dot{E}^{\mathrm{M}}_{\mathrm{in}} - \dot{E}^{\mathrm{M}}_{\mathrm{out}}\bigr) **Heat injection** (:math:`\dot{Q} > 0`) Case 1: Both streams above ambient temperature .. math:: \dot{E}_{\mathrm{P}} = \dot{E}^{\mathrm{PH}}_{\mathrm{out}} - \dot{E}^{\mathrm{PH}}_{\mathrm{in}} .. math:: \dot{E}_{\mathrm{F}} = \dot{E}^{\mathrm{T}}_{\mathrm{out}} - \dot{E}^{\mathrm{T}}_{\mathrm{in}} Case 2: Inlet below and outlet above ambient temperature .. math:: \dot{E}_{\mathrm{P}} = \dot{E}^{\mathrm{T}}_{\mathrm{out}} + \dot{E}^{\mathrm{T}}_{\mathrm{in}} .. math:: \dot{E}_{\mathrm{F}} = \dot{E}^{\mathrm{T}}_{\mathrm{in}} + \bigl(\dot{E}^{\mathrm{M}}_{\mathrm{in}} - \dot{E}^{\mathrm{M}}_{\mathrm{out}}\bigr) Case 3: Both streams below ambient temperature .. math:: \dot{E}_{\mathrm{P}} = \dot{E}^{\mathrm{T}}_{\mathrm{in}} - \dot{E}^{\mathrm{T}}_{\mathrm{out}} + \bigl(\dot{E}^{\mathrm{M}}_{\mathrm{out}} - \dot{E}^{\mathrm{M}}_{\mathrm{in}}\bigr) .. math:: \dot{E}_{\mathrm{F}} = \dot{E}^{\mathrm{T}}_{\mathrm{in}} - \dot{E}^{\mathrm{T}}_{\mathrm{out}} **Fully dissipative or** :math:`\dot{Q} = 0` .. math:: \dot{E}_{\mathrm{P}} = \mathrm{NaN} .. math:: \dot{E}_{\mathrm{F}} = \dot{E}^{\mathrm{PH}}_{\mathrm{in}} - \dot{E}^{\mathrm{PH}}_{\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). """ inlet = self.inl[0] outlet = self.outl[0] # Determine heat transfer direction Q = outlet["m"] * outlet["h"] - inlet["m"] * inlet["h"] # Case 1: Heat is released (Q < 0) if Q < 0: if inlet["T"] >= T0 and outlet["T"] >= T0: # Both streams above ambient self.C_P = outlet["C_T"] - inlet["C_T"] self.C_F = inlet["C_PH"] - outlet["C_PH"] elif inlet["T"] >= T0 and outlet["T"] < T0: # Inlet above, outlet below ambient self.C_P = outlet["C_T"] self.C_F = inlet["C_T"] + outlet["C_T"] + (inlet["C_M"] - outlet["C_M"]) elif inlet["T"] <= T0 and outlet["T"] < T0: # Both streams below ambient self.C_P = outlet["C_T"] - inlet["C_T"] self.C_F = self.C_P + (inlet["C_M"] - outlet["C_M"]) else: self.C_P = np.nan self.C_F = np.nan # Case 2: Heat is added (Q > 0) elif Q > 0: if inlet["T"] >= T0 and outlet["T"] >= T0: # Both streams above ambient self.C_P = outlet["C_PH"] - inlet["C_PH"] self.C_F = outlet["C_T"] - inlet["C_T"] elif inlet["T"] < T0 and outlet["T"] >= T0: # Inlet below, outlet above ambient self.C_P = outlet["C_T"] + inlet["C_T"] self.C_F = inlet["C_T"] + (inlet["C_M"] - outlet["C_M"]) elif inlet["T"] < T0 and outlet["T"] <= T0: # Both streams below ambient self.C_P = inlet["C_T"] - outlet["C_T"] + (outlet["C_M"] - inlet["C_M"]) self.C_F = inlet["C_T"] - outlet["C_T"] else: self.C_P = np.nan self.C_F = np.nan # Case 3: Fully dissipative or Q == 0 else: self.C_P = np.nan self.C_F = inlet["C_PH"] - outlet["C_PH"] # Calculate specific costs and exergoeconomic indicators self.c_F = self.C_F / self.E_F if self.E_F else np.nan self.c_P = self.C_P / self.E_P if self.E_P else np.nan self.C_D = self.c_F * self.E_D if self.E_D else np.nan self.r = (self.c_P - self.c_F) / self.c_F if self.c_F else np.nan self.f = self.Z_costs / (self.Z_costs + self.C_D) if self.C_D else np.nan