Source code for exerpy.components.heat_exchanger.condenser

import logging

import numpy as np

from exerpy.components.component import Component, component_registry


[docs] @component_registry class Condenser(Component): r""" Class for exergy and exergoeconomic analysis of condensers (only dissipative). This class performs exergy and exergoeconomic analysis calculations for condenser components, accounting for two inlet and two outlet streams. This class should be used only for dissipative condensers. For non-dissipative condensers, use components that are modeled in ExerPy using the `HeatExchanger` class. Attributes ---------- E_F : float Exergy fuel of the component :math:`\dot{E}_\mathrm{F}` in :math:`\mathrm{W}`. E_D : float Exergy destruction of the component :math:`\dot{E}_\mathrm{D}` in :math:`\mathrm{W}`. E_L : float Exergy loss of the component :math:`\dot{E}_\mathrm{L}` in :math:`\mathrm{W}`. 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_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_F : float Specific cost of fuel stream (currency per unit exergy). 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 condenser component. Parameters ---------- **kwargs : dict Arbitrary keyword arguments. Recognized keys: - Ex_C_col (dict): custom cost coefficients, default {} - Z_costs (float): investment cost rate in currency/h, default 0.0 """ super().__init__(**kwargs) def _temperature_case(self, T0): """Classify the condenser temperature configuration. Uses a tolerance of 0.01 K to handle floating-point edge cases where stream temperatures are numerically very close to T0. Parameters ---------- T0 : float Ambient temperature (K). Returns ------- int Case number (1-7). """ tol = 0.01 # K T0_in = self.inl[0]["T"] T1_in = self.inl[1]["T"] T0_out = self.outl[0]["T"] T1_out = self.outl[1]["T"] all_T = [T0_in, T1_in, T0_out, T1_out] def above(T): return T0 + tol < T def at_or_below(T): return T0 + tol >= T # Case 1: All streams at or above T0 if all(T0 - tol <= T for T in all_T): return 1 # Case 2: All streams at or below T0 if all(at_or_below(T) for T in all_T): return 2 # Case 3: Both streams crossing T0 if above(T0_in) and above(T1_out) and at_or_below(T0_out) and at_or_below(T1_in): return 3 # Case 4: Only hot inlet above T0 if above(T0_in) and at_or_below(T1_in) and at_or_below(T0_out) and at_or_below(T1_out): return 4 # Case 5: Only cold inlet below T0 if above(T0_in) and at_or_below(T1_in) and above(T0_out) and above(T1_out): return 5 # Case 6: Hot stream always above, cold stream always below (dissipative) if above(T0_in) and at_or_below(T1_in) and above(T0_out) and at_or_below(T1_out): return 6 # Case 7: Unrecognized return 7
[docs] def calc_exergy_balance(self, T0: float, p0: float, split_physical_exergy) -> None: r""" Compute the exergy balance of the condenser. In order to distinguish between the exergetic destruction because of heat transfer and the exergetic loss (coldf stream leaving the system) the exergetic losses and destruction are calculated as follows: .. math:: \dot{E}_{\mathrm{L}} = \dot{E}^{\mathrm{PH}}_{\mathrm{out},2} - \dot{E}^{\mathrm{PH}}_{\mathrm{in},2} .. math:: \dot{E}_{\mathrm{D}} = \dot{E}^{\mathrm{PH}}_{\mathrm{in},1} - \dot{E}^{\mathrm{PH}}_{\mathrm{out},1} - \dot{E}_{\mathrm{L}} However, these value can only be accessed via the attributes `E_L` and `E_D` of the component. In the table of final results of the exergy analysis of the system, the exergy destruction of the condenser is counted as the exergy loss and the exergetic destruction due to heat transfer. 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 inlets or outlets are missing. """ # Ensure that the component has both inlet and outlet streams if len(self.inl) < 2 or len(self.outl) < 2: raise ValueError("Condenser requires two inlets and two outlets.") # Calculate exergy loss (E_L) for the heat transfer process self.E_L = self.outl[1]["m"] * (self.outl[1]["e_PH"] - self.inl[1]["e_PH"]) # Calculate exergy destruction (E_D) self.E_D = self.outl[0]["m"] * (self.inl[0]["e_PH"] - self.outl[0]["e_PH"]) - self.E_L # Exergy fuel and product are not typically defined for a condenser self.E_F = np.nan self.E_P = np.nan self.epsilon = np.nan # Log the results logging.info( f"Exergy balance of Condenser {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""" Add auxiliary cost equations for the condenser. This method appends rows to the cost matrix to enforce: Case 1: All streams above ambient temperature F rule for thermal exergy of the hot stream: .. math:: -\frac{1}{\dot{E}^{\mathrm{T}}_{\mathrm{out},1}}\,\dot{C}^{\mathrm{T}}_{\mathrm{out},1} + \frac{1}{\dot{E}^{\mathrm{T}}_{\mathrm{in},1}}\,\dot{C}^{\mathrm{T}}_{\mathrm{in},1} = 0 Case 2: All streams below or equal to ambient temperature F rule for thermal exergy of the cold stream: .. math:: -\frac{1}{\dot{E}^{\mathrm{T}}_{\mathrm{out},2}}\,\dot{C}^{\mathrm{T}}_{\mathrm{out},2} + \frac{1}{\dot{E}^{\mathrm{T}}_{\mathrm{in},2}}\,\dot{C}^{\mathrm{T}}_{\mathrm{in},2} = 0 Case 3: Both stream crossing ambient temperature P rule for thermal exergy of both outlets: .. math:: -\frac{1}{\dot{E}^{\mathrm{T}}_{\mathrm{out},1}}\,\dot{C}^{\mathrm{T}}_{\mathrm{out},1} + \frac{1}{\dot{E}^{\mathrm{T}}_{\mathrm{out},2}}\,\dot{C}^{\mathrm{T}}_{\mathrm{out},2} = 0 Case 4: Only the hot inlet above ambient temperature F rule for thermal exergy of the cold stream: .. math:: -\frac{1}{\dot{E}^{\mathrm{T}}_{\mathrm{out},2}}\,\dot{C}^{\mathrm{T}}_{\mathrm{out},2} + \frac{1}{\dot{E}^{\mathrm{T}}_{\mathrm{in},2}}\,\dot{C}^{\mathrm{T}}_{\mathrm{in},2} = 0 Case 5: Only the cold inlet below ambient temperature F rule for thermal exergy of the hot stream: .. math:: -\frac{1}{\dot{E}^{\mathrm{T}}_{\mathrm{out},1}}\,\dot{C}^{\mathrm{T}}_{\mathrm{out},1} + \frac{1}{\dot{E}^{\mathrm{T}}_{\mathrm{in},1}}\,\dot{C}^{\mathrm{T}}_{\mathrm{in},1} = 0 Case 6: Hot stream always above and cold stream always below ambiente temperature (dissipative case): The dissipative is not handeld here! For all cases, the mechanical and chemical exergy costs are handled as follows: F rule for mechanical exergy of the hot stream: .. math:: -\frac{1}{\dot{E}^{\mathrm{M}}_{\mathrm{out},i}}\,\dot{C}^{\mathrm{M}}_{\mathrm{out},i} + \frac{1}{\dot{E}^{\mathrm{M}}_{\mathrm{in},i}}\,\dot{C}^{\mathrm{M}}_{\mathrm{in},i} = 0 F rule for chemical exergy on hot branch: .. math:: -\frac{1}{\dot{E}^{\mathrm{CH}}_{\mathrm{out},i}}\,\dot{C}^{\mathrm{CH}}_{\mathrm{out},i} + \frac{1}{\dot{E}^{\mathrm{CH}}_{\mathrm{in},i}}\,\dot{C}^{\mathrm{CH}}_{\mathrm{in},i} = 0 Parameters ---------- A : numpy.ndarray Current cost matrix. b : numpy.ndarray Current RHS vector. counter : int Starting row index for auxiliary equations. T0 : float Ambient temperature (K). equations : dict or list Structure for equation labels. chemical_exergy_enabled : bool Must be True to include chemical exergy mixing. Returns ------- A : numpy.ndarray Updated cost matrix. b : numpy.ndarray Updated RHS vector. counter : int Updated row index after adding equations. equations : dict or list Updated labels. Raises ------ ValueError If required cost variable indices are missing. """ # Equality equation for mechanical and chemical exergy costs. def set_equal(A, row, in_item, out_item, var): if in_item["e_" + var] != 0 and out_item["e_" + var] != 0: A[row, in_item["CostVar_index"][var]] = 1 / in_item["E_" + var] A[row, out_item["CostVar_index"][var]] = -1 / out_item["E_" + var] elif in_item["e_" + var] == 0 and out_item["e_" + var] != 0: A[row, in_item["CostVar_index"][var]] = 1 elif in_item["e_" + var] != 0 and out_item["e_" + var] == 0: A[row, out_item["CostVar_index"][var]] = 1 else: A[row, in_item["CostVar_index"][var]] = 1 A[row, out_item["CostVar_index"][var]] = -1 # Thermal fuel rule on hot stream: c_T_in0 = c_T_out0. def set_thermal_f_hot(A, row): if self.inl[0]["e_T"] != 0 and self.outl[0]["e_T"] != 0: A[row, self.inl[0]["CostVar_index"]["T"]] = 1 / self.inl[0]["E_T"] A[row, self.outl[0]["CostVar_index"]["T"]] = -1 / self.outl[0]["E_T"] elif self.inl[0]["e_T"] == 0 and self.outl[0]["e_T"] != 0: A[row, self.inl[0]["CostVar_index"]["T"]] = 1 elif self.inl[0]["e_T"] != 0 and self.outl[0]["e_T"] == 0: A[row, self.outl[0]["CostVar_index"]["T"]] = 1 else: A[row, self.inl[0]["CostVar_index"]["T"]] = 1 A[row, self.outl[0]["CostVar_index"]["T"]] = -1 # Thermal fuel rule on cold stream: c_T_in1 = c_T_out1. def set_thermal_f_cold(A, row): if self.inl[1]["e_T"] != 0 and self.outl[1]["e_T"] != 0: A[row, self.inl[1]["CostVar_index"]["T"]] = 1 / self.inl[1]["E_T"] A[row, self.outl[1]["CostVar_index"]["T"]] = -1 / self.outl[1]["E_T"] elif self.inl[1]["e_T"] == 0 and self.outl[1]["e_T"] != 0: A[row, self.inl[1]["CostVar_index"]["T"]] = 1 elif self.inl[1]["e_T"] != 0 and self.outl[1]["e_T"] == 0: A[row, self.outl[1]["CostVar_index"]["T"]] = 1 else: A[row, self.inl[1]["CostVar_index"]["T"]] = 1 A[row, self.outl[1]["CostVar_index"]["T"]] = -1 # Thermal product rule: Equate the two outlet thermal costs (c_T_out0 = c_T_out1). def set_thermal_p_rule(A, row): if self.outl[0]["e_T"] != 0 and self.outl[1]["e_T"] != 0: A[row, self.outl[0]["CostVar_index"]["T"]] = 1 / self.outl[0]["E_T"] A[row, self.outl[1]["CostVar_index"]["T"]] = -1 / self.outl[1]["E_T"] elif self.outl[0]["e_T"] == 0 and self.outl[1]["e_T"] != 0: A[row, self.outl[0]["CostVar_index"]["T"]] = 1 elif self.outl[0]["e_T"] != 0 and self.outl[1]["e_T"] == 0: A[row, self.outl[1]["CostVar_index"]["T"]] = 1 else: A[row, self.outl[0]["CostVar_index"]["T"]] = 1 A[row, self.outl[1]["CostVar_index"]["T"]] = -1 # Determine the thermal case based on temperatures. case = self._temperature_case(T0) # Case 1: All temperatures > T0. if case == 1: set_thermal_f_hot(A, counter + 0) equations[counter] = { "kind": "aux_f_rule_hot", "objects": [self.name, self.inl[0]["name"], self.outl[0]["name"]], "property": "c_T", } # Case 2: All temperatures <= T0. elif case == 2: set_thermal_f_cold(A, counter + 0) equations[counter] = { "kind": "aux_f_rule_cold", "objects": [self.name, self.inl[1]["name"], self.outl[1]["name"]], "property": "c_T", } logging.warning( f"All temperatures in {self.name} are below ambient temperature. " "This is not a typical case for a dissipative condenser." ) # Case 3: Both stream crossing T0 (hot inlet and cold outlet > T0, hot outlet and cold inlet <= T0) elif case == 3: set_thermal_p_rule(A, counter + 0) equations[counter] = { "kind": "aux_p_rule", "objects": [self.name, self.outl[0]["name"], self.outl[1]["name"]], "property": "c_T", } logging.warning( f"Hot inlet and cold outlet in {self.name} are above ambient temperature, " "while hot outlet and cold inlet are below. This is not a typical case for a dissipative condenser. " "The exergoeconomic analysis is counting the outlets as products." ) # Case 4: Only hot inlet > T0 elif case == 4: set_thermal_f_cold(A, counter + 0) equations[counter] = { "kind": "aux_f_rule_cold", "objects": [self.name, self.inl[1]["name"], self.outl[1]["name"]], "property": "c_T", } logging.warning( f"Cold inlet in {self.name} is below ambient temperature. " "This is not a typical case for a dissipative condenser." ) # Case 5: Only cold inlet <= T0 elif case == 5: set_thermal_f_hot(A, counter + 0) equations[counter] = { "kind": "aux_f_rule_hot", "objects": [self.name, self.inl[0]["name"], self.outl[0]["name"]], "property": "c_T", } logging.warning( f"Cold inlet in {self.name} is below ambient temperature. " "This is not a typical case for a dissipative condenser." ) # Case 6: hot stream always above T0, cold stream always below T0 elif case == 6: logging.info( f"Condenser {self.name}: dissipative temperature configuration detected in aux_eqs. " "Skipping auxiliary equations (handled by dis_eqs)." ) return A, b, counter, equations # Case 7: Default case. else: set_thermal_f_hot(A, counter + 0) equations[counter] = { "kind": "aux_f_rule_hot", "objects": [self.name, self.inl[0]["name"], self.outl[0]["name"]], "property": "c_T", } # Mechanical equations (always added) set_equal(A, counter + 1, self.inl[0], self.outl[0], "M") set_equal(A, counter + 2, self.inl[1], self.outl[1], "M") equations[counter + 1] = { "kind": "aux_equality", "objects": [self.name, self.inl[0]["name"], self.outl[0]["name"]], "property": "c_M", } equations[counter + 2] = { "kind": "aux_equality", "objects": [self.name, self.inl[1]["name"], self.outl[1]["name"]], "property": "c_M", } # Only add chemical auxiliary equations if chemical exergy is enabled. if chemical_exergy_enabled: set_equal(A, counter + 3, self.inl[0], self.outl[0], "CH") set_equal(A, counter + 4, self.inl[1], self.outl[1], "CH") equations[counter + 3] = { "kind": "aux_equality", "objects": [self.name, self.inl[0]["name"], self.outl[0]["name"]], "property": "c_CH", } equations[counter + 4] = { "kind": "aux_equality", "objects": [self.name, self.inl[1]["name"], self.outl[1]["name"]], "property": "c_CH", } num_aux_eqs = 5 else: # Skip chemical auxiliary equations. num_aux_eqs = 3 for i in range(num_aux_eqs): b[counter + i] = 0 return A, b, counter + num_aux_eqs, equations
[docs] def dis_eqs(self, A, b, counter, T0, equations, chemical_exergy_enabled=False, all_components=None): r""" Construct cost equations for a dissipative Condenser. Distributes the condenser'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 adds an overall cost balance row that enforces: .. math:: (\dot C_{\mathrm{in},1} - \dot C_{\mathrm{out},1}) + (\dot C_{\mathrm{in},2} - \dot C_{\mathrm{out},2}) - \dot C_{\mathrm{diff}} = -\,\dot Z_{\mathrm{costs}} The equality equations enforce that specific costs are equal between inlet and outlet for each exergy component (thermal, mechanical, chemical) on both the hot and cold streams. 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 (K). equations : dict Dictionary mapping row indices to equation labels. chemical_exergy_enabled : bool, optional Flag indicating whether chemical exergy is considered. all_components : list, optional Global list of all component objects; if not provided, defaults to []. Returns ------- tuple Updated (A, b, counter, equations). """ def set_equal_dis(A, row, in_item, out_item, var): if in_item.get("E_" + var, 0) and out_item.get("E_" + var, 0): A[row, in_item["CostVar_index"][var]] = 1 / in_item["E_" + var] A[row, out_item["CostVar_index"][var]] = -1 / out_item["E_" + var] else: A[row, in_item["CostVar_index"][var]] = 1 A[row, out_item["CostVar_index"][var]] = -1 # --- Thermal equality for hot stream --- set_equal_dis(A, counter, self.inl[0], self.outl[0], "T") b[counter] = 0 equations[counter] = { "kind": "dis_equality", "objects": [self.name, self.inl[0]["name"], self.outl[0]["name"]], "property": "c_T", } counter += 1 # --- Thermal equality for cold stream --- set_equal_dis(A, counter, self.inl[1], self.outl[1], "T") b[counter] = 0 equations[counter] = { "kind": "dis_equality", "objects": [self.name, self.inl[1]["name"], self.outl[1]["name"]], "property": "c_T", } counter += 1 # --- Mechanical equality for hot stream --- set_equal_dis(A, counter, self.inl[0], self.outl[0], "M") b[counter] = 0 equations[counter] = { "kind": "dis_equality", "objects": [self.name, self.inl[0]["name"], self.outl[0]["name"]], "property": "c_M", } counter += 1 # --- Mechanical equality for cold stream --- set_equal_dis(A, counter, self.inl[1], self.outl[1], "M") b[counter] = 0 equations[counter] = { "kind": "dis_equality", "objects": [self.name, self.inl[1]["name"], self.outl[1]["name"]], "property": "c_M", } counter += 1 # --- Chemical equality (if enabled) --- if chemical_exergy_enabled: set_equal_dis(A, counter, self.inl[0], self.outl[0], "CH") b[counter] = 0 equations[counter] = { "kind": "dis_equality", "objects": [self.name, self.inl[0]["name"], self.outl[0]["name"]], "property": "c_CH", } counter += 1 set_equal_dis(A, counter, self.inl[1], self.outl[1], "CH") b[counter] = 0 equations[counter] = { "kind": "dis_equality", "objects": [self.name, self.inl[1]["name"], self.outl[1]["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 = [ 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 # --- Overall cost balance row --- # (C_in_hot - C_out_hot) + (C_in_cold - C_out_cold) - 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 A[counter, self.inl[1]["CostVar_index"]["T"]] = 1 A[counter, self.outl[1]["CostVar_index"]["T"]] = -1 A[counter, self.inl[1]["CostVar_index"]["M"]] = 1 A[counter, self.outl[1]["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 A[counter, self.inl[1]["CostVar_index"]["CH"]] = 1 A[counter, self.outl[1]["CostVar_index"]["CH"]] = -1 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 condenser. The condenser is always dissipative (no identifiable product), so: .. math:: \dot{C}_P = \mathrm{NaN} .. math:: \dot{C}_F = \bigl(\dot{C}^{\mathrm{PH}}_{\mathrm{in},1} - \dot{C}^{\mathrm{PH}}_{\mathrm{out},1}\bigr) + \bigl(\dot{C}^{\mathrm{PH}}_{\mathrm{in},2} - \dot{C}^{\mathrm{PH}}_{\mathrm{out},2}\bigr) Since :math:`\dot{E}_F` and :math:`\dot{E}_P` are not defined for a dissipative condenser, the specific costs :math:`c_F`, :math:`c_P`, the cost of exergy destruction :math:`\dot{C}_D`, the relative cost difference :math:`r`, and the exergoeconomic factor :math:`f` are all NaN. Parameters ---------- T0 : float Ambient temperature (K). chemical_exergy_enabled : bool, optional If True, chemical exergy is considered in the calculations. """ # Condenser is always dissipative: no identifiable product self.C_P = np.nan self.C_F = self.inl[0]["C_PH"] - self.outl[0]["C_PH"] + (self.inl[1]["C_PH"] - self.outl[1]["C_PH"]) self.c_F = np.nan self.c_P = np.nan self.C_D = np.nan self.r = np.nan self.f = np.nan