Source code for exerpy.components.turbomachinery.turbine

import logging

import numpy as np

from exerpy.components.component import Component, component_registry


[docs] @component_registry class Turbine(Component): r""" Class for exergy analysis of turbines. This class performs exergy analysis calculations for turbines, with definitions of exergy product and fuel varying based on the temperature relationships between inlet stream, outlet stream, 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:`-`. P : float Power output of the turbine in :math:`\mathrm{W}`. inl : dict Dictionary containing inlet stream data with temperature, mass flows, enthalpies, and specific exergies. Must have at least one inlet. outl : dict Dictionary containing outlet streams data with temperature, mass flows, enthalpies, and specific exergies. Can have multiple outlets, their properties will be summed up in the calculations. Notes ----- The exergy analysis considers three cases based on temperature relationships: .. math:: \dot{E}_\mathrm{P} = \begin{cases} -P & T_\mathrm{in}, T_\mathrm{out} \geq T_0\\ -P + \dot{E}_\mathrm{out}^\mathrm{T} & T_\mathrm{in} > T_0 \geq T_\mathrm{out}\\ -P + \dot{E}_\mathrm{out}^\mathrm{T} - \dot{E}_\mathrm{in}^\mathrm{T} & T_0 \geq T_\mathrm{in}, T_\mathrm{out} \end{cases} \dot{E}_\mathrm{F} = \begin{cases} \dot{E}_\mathrm{in}^\mathrm{PH} - \dot{E}_\mathrm{out}^\mathrm{PH} & T_\mathrm{in}, T_\mathrm{out} \geq T_0\\ \dot{E}_\mathrm{in}^\mathrm{T} + \dot{E}_\mathrm{in}^\mathrm{M} - \dot{E}_\mathrm{out}^\mathrm{M} & T_\mathrm{in} > T_0 \geq T_\mathrm{out}\\ \dot{E}_\mathrm{in}^\mathrm{M} - \dot{E}_\mathrm{out}^\mathrm{M} & T_0 \geq T_\mathrm{in}, T_\mathrm{out} \end{cases} """ def __init__(self, **kwargs): r"""Initialize turbine component with given parameters.""" super().__init__(**kwargs) self.P = None
[docs] def calc_exergy_balance(self, T0: float, p0: float, split_physical_exergy) -> None: r""" Calculate the exergy balance of the turbine. 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. """ # Get net power flow net_power = 0.0 # Initialize to 0.0, not None for idx, conn in self.inl.items(): if conn is not None and conn.get("kind") == "power" and "energy_flow" in conn: net_power -= conn["energy_flow"] # Subtract inlet power for idx, conn in self.outl.items(): if conn is not None and conn.get("kind") == "power" and "energy_flow" in conn: net_power += conn["energy_flow"] # Add outlet power # Use net_power if available, otherwise calculate from enthalpy balance if net_power != 0.0: self.P = abs(net_power) else: self.P = self._total_outlet("m", "h") - self.inl[0]["m"] * self.inl[0]["h"] # Case 1: Both temperatures above ambient if self.inl[0]["T"] >= T0 and self.outl[0]["T"] >= T0 and self.inl[0]["T"] >= self.outl[0]["T"]: self.E_P = abs(self.P) self.E_F = self.inl[0]["m"] * self.inl[0]["e_PH"] - self._total_outlet("m", "e_PH") # Case 2: Inlet above, outlet at/below ambient elif self.inl[0]["T"] > T0 and self.outl[0]["T"] <= T0: if split_physical_exergy: self.E_P = abs(self.P) + self._total_outlet("m", "e_T") self.E_F = ( self.inl[0]["m"] * self.inl[0]["e_T"] + self.inl[0]["m"] * self.inl[0]["e_M"] - self._total_outlet("m", "e_M") ) else: logging.warning( "While dealing with expander below ambient, " "physical exergy should be split into thermal and mechanical components!" ) self.E_P = np.nan self.E_F = np.nan # Case 3: Both temperatures at/below ambient elif self.inl[0]["T"] <= T0 and self.outl[0]["T"] <= T0: if split_physical_exergy: self.E_P = abs(self.P) + (self._total_outlet("m", "e_T") - self.inl[0]["m"] * self.inl[0]["e_T"]) self.E_F = self.inl[0]["m"] * self.inl[0]["e_M"] - self._total_outlet("m", "e_M") else: logging.warning( "While dealing with expander below ambient, " "physical exergy should be split into thermal and mechanical components!" ) self.E_P = np.nan self.E_F = np.nan # Invalid case: outlet temperature larger than inlet else: logging.warning( "Exergy balance of a turbine where outlet temperature is larger " "than inlet temperature is not implemented." ) self.E_P = np.nan self.E_F = np.nan # Calculate exergy destruction and efficiency self.E_D = self.E_F - self.E_P if np.nan == self.E_F: self.E_D = self.inl[0]["m"] * self.inl[0]["e_PH"] - self._total_outlet("m", "e_PH") - abs(self.P) self.epsilon = self.calc_epsilon() # Log the results logging.info( f"Exergy balance of Turbine {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%}" )
def _total_outlet(self, mass_flow: str, property_name: str) -> float: r""" Calculate the sum of mass flow times property across all outlets. Parameters ---------- mass_flow : str Key for the mass flow value. property_name : str Key for the property to be summed. Returns ------- float Sum of mass flow times property across all outlets. """ total = 0.0 for outlet in self.outl.values(): # Skip power connections; treat missing "kind" as material for backward compatibility if outlet and outlet.get("kind", "material") != "power" and mass_flow in outlet and property_name in outlet: total += outlet[mass_flow] * outlet[property_name] return total
[docs] def aux_eqs(self, A, b, counter, T0, equations, chemical_exergy_enabled): """ Auxiliary equations for the turbine. This function adds rows to the cost matrix A and the right-hand-side vector b to enforce the following auxiliary cost relations: For each material outlet (when inlet and first outlet are above ambient temperature T0): (1) 1/E_T_in * C_T_in - 1/E_T_out * C_T_out = 0 - F-principle: specific thermal exergy costs equalized between inlet and each outlet (2) 1/E_M_in * C_M_in - 1/E_M_out * C_M_out = 0 - F-principle: specific mechanical exergy costs equalized between inlet and each outlet (3) 1/E_CH_in * C_CH_in - 1/E_CH_out * C_CH_out = 0 (if chemical_exergy_enabled) - F-principle: specific chemical exergy costs equalized between inlet and each outlet For power outlets (with both source and target components): (4) 1/E_ref * C_ref - 1/E_out * C_out = 0 - P-principle: specific power exergy costs equalized across all power outlets 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 : 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 after adding all auxiliary equations. equations : list or dict Updated structure with equation labels. """ # Process only if the inlet and the first outlet are above T0. if self.inl[0]["T"] > T0 and self.outl[0]["T"] > T0: # Filter material outlets material_outlets = [outlet for outlet in self.outl.values() if outlet.get("kind") == "material"] # Determine number of rows per outlet. num_rows_per_outlet = 3 if chemical_exergy_enabled else 2 for i, outlet in enumerate(material_outlets): row_offset = num_rows_per_outlet * i # --- Thermal exergy equation --- A[counter + row_offset, self.inl[0]["CostVar_index"]["T"]] = ( 1 / self.inl[0]["E_T"] if self.inl[0]["e_T"] != 0 else 1 ) A[counter + row_offset, outlet["CostVar_index"]["T"]] = -1 / outlet["E_T"] if outlet["e_T"] != 0 else -1 equations[counter + row_offset] = { "kind": "aux_f_rule", "objects": [self.name, self.inl[0]["name"], outlet["name"]], "property": "c_T", } # --- Mechanical exergy equation --- A[counter + row_offset + 1, self.inl[0]["CostVar_index"]["M"]] = ( 1 / self.inl[0]["E_M"] if self.inl[0]["e_M"] != 0 else 1 ) A[counter + row_offset + 1, outlet["CostVar_index"]["M"]] = ( -1 / outlet["E_M"] if outlet["e_M"] != 0 else -1 ) equations[counter + row_offset + 1] = { "kind": "aux_f_rule", "objects": [self.name, self.inl[0]["name"], outlet["name"]], "property": "c_M", } # --- Chemical exergy equation (conditionally added) --- if chemical_exergy_enabled: A[counter + row_offset + 2, self.inl[0]["CostVar_index"]["CH"]] = ( 1 / self.inl[0]["E_CH"] if self.inl[0]["e_CH"] != 0 else 1 ) A[counter + row_offset + 2, outlet["CostVar_index"]["CH"]] = ( -1 / outlet["E_CH"] if outlet["e_CH"] != 0 else -1 ) equations[counter + row_offset + 2] = { "kind": "aux_equality", "objects": [self.name, self.inl[0]["name"], outlet["name"]], "property": "c_CH", } # Update counter based on number of rows added for all material outlets. num_material_rows = num_rows_per_outlet * len(material_outlets) for j in range(num_material_rows): b[counter + j] = 0 counter += num_material_rows else: logging.warning("Turbine with outlet below T0 not implemented in exergoeconomics yet!") # --- Auxiliary equation for shaft power equality --- power_outlets = [ outlet for outlet in self.outl.values() if outlet.get("kind") == "power" and outlet.get("source_component") and outlet.get("target_component") ] if len(power_outlets) > 1: ref = power_outlets[0] ref_idx = ref["CostVar_index"]["exergy"] for outlet in power_outlets[1:]: cur_idx = outlet["CostVar_index"]["exergy"] A[counter, ref_idx] = 1 / ref["E"] if ref["E"] != 0 else 1 A[counter, cur_idx] = -1 / outlet["E"] if outlet["E"] != 0 else -1 b[counter] = 0 equations[counter] = { "kind": "aux_p_rule", "objects": [self.name], "property": "c_TOT (more power flows)", } counter += 1 return A, b, counter, equations
[docs] def exergoeconomic_balance(self, T0, chemical_exergy_enabled=False): r""" Perform exergoeconomic cost balance for the turbine. 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. The turbine may have multiple power outputs and multiple material outlets. The product cost includes the total cost of all power streams, while material outlet costs are summed accordingly. **Case 1: Inlet and outlet above ambient temperature** Both inlet and first material outlet satisfy :math:`T \geq T_0`: .. math:: \dot{C}_{\mathrm{P}} = \sum \dot{C}^{\mathrm{TOT}}_{\mathrm{power,out}} .. math:: \dot{C}_{\mathrm{F}} = \dot{C}^{\mathrm{PH}}_{\mathrm{in}} - \sum \dot{C}^{\mathrm{PH}}_{\mathrm{material,out}} **Case 2: Inlet above and outlet at or below ambient temperature** Inlet satisfies :math:`T > T_0` and first material outlet :math:`T \leq T_0`: .. math:: \dot{C}_{\mathrm{P}} = \sum \dot{C}^{\mathrm{TOT}}_{\mathrm{power,out}} + \sum \dot{C}^{\mathrm{T}}_{\mathrm{material,out}} .. math:: \dot{C}_{\mathrm{F}} = \dot{C}^{\mathrm{T}}_{\mathrm{in}} + \bigl(\dot{C}^{\mathrm{M}}_{\mathrm{in}} - \sum \dot{C}^{\mathrm{M}}_{\mathrm{material,out}}\bigr) **Case 3: Both inlet and outlet at or below ambient temperature** Both inlet and first material outlet satisfy :math:`T \leq T_0`: .. math:: \dot{C}_{\mathrm{P}} = \sum \dot{C}^{\mathrm{TOT}}_{\mathrm{power,out}} + \bigl(\sum \dot{C}^{\mathrm{T}}_{\mathrm{material,out}} - \dot{C}^{\mathrm{T}}_{\mathrm{in}}\bigr) .. math:: \dot{C}_{\mathrm{F}} = \dot{C}^{\mathrm{M}}_{\mathrm{in}} - \sum \dot{C}^{\mathrm{M}}_{\mathrm{material,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). """ # Sum the cost of all outlet power streams. C_power_out = sum(stream.get("C_TOT", 0) for stream in self.outl.values() if stream.get("kind") == "power") # Assume a single primary inlet for material cost properties. inlet = self.inl[0] # Filter material outlets and sum their cost components. material_outlets = [out for out in self.outl.values() if out.get("kind") == "material"] sum_C_PH_out = sum(out.get("C_PH", 0) for out in material_outlets) sum_C_T_out = sum(out.get("C_T", 0) for out in material_outlets) sum_C_M_out = sum(out.get("C_M", 0) for out in material_outlets) # Case 1: Both inlet and first outlet above ambient. if inlet["T"] >= T0 and self.outl[0]["T"] >= T0: self.C_P = C_power_out self.C_F = inlet.get("C_PH", 0) - sum_C_PH_out # Case 2: Inlet above ambient and outlet at or below ambient. elif inlet["T"] > T0 and self.outl[0]["T"] <= T0: self.C_P = C_power_out + sum_C_T_out self.C_F = inlet.get("C_T", 0) + (inlet.get("C_M", 0) - sum_C_M_out) # Case 3: Both inlet and outlet at or below ambient. elif inlet["T"] <= T0 and self.outl[0]["T"] <= T0: self.C_P = C_power_out + (sum_C_T_out - inlet.get("C_T", 0)) self.C_F = inlet.get("C_M", 0) - sum_C_M_out else: logging.warning( "Exergoeconomic balance of a turbine with outlet temperature larger than inlet is not implemented." ) self.C_P = np.nan self.C_F = np.nan # Calculate the specific cost terms and exergoeconomic parameters. 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)