Source code for connector.mass_connector

# -*- coding: utf-8 -*-
"""
Created on Wed Jan 10 14:09:18 2024

@author: Elise Neven
@email: elise.neven@uliege.be

"""

from CoolProp.CoolProp import PropsSI
import CoolProp.CoolProp as CP
import warnings

[docs] class MassConnector: """ MassConnector is a class that handle and calculate fluid properties based on given state variables. **Attributes**: fluid : str, optional The name of the fluid. m_dot : float, optional Mass flow rate in kg/s. V_dot : float, optional Volume flow rate in m^3/s. T : float, optional Temperature in Kelvin. p : float, optional Pressure in Pascal. h : float, optional Specific enthalpy in J/kg. s : float, optional Specific entropy in J/kg/K. D : float, optional Mass density in kg/m^3. x : float, optional Quality in kg/kg. cp : float, optional Specific heat capacity in J/kg/K. SC : float, optional Subcooling in K. SH : float, optional Superheating in K. completely_known : bool True if all properties and mass flow rate are known, otherwise False. state_known : bool True if all state properties are known, otherwise False. variables_input : list of lists A list of the state variables used to define the fluid state. Each entry is a list of [variable_name, value]. **Methods**: __init__(self, fluid=None): Initializes the MassConnector object with optional fluid. reset(self): Resets all properties to None and flags to False. check_completely_known(self): Checks if all necessary properties and mass flow rate are known to determine if the state is completely known. calculate_properties(self): Calculates fluid properties using CoolProp based on known variables. set_properties(self, kwargs): Sets various properties of the fluid. Accepts named arguments for 'fluid', 'm_dot', 'V_dot', 'T', 'P', 'H', 'S', 'D', 'x', and 'cp'. set_fluid(self, value): Sets the fluid type and checks if it's valid. set_m_dot(self, value): Sets the mass flow rate and ensures volume flow rate is not set simultaneously. set_V_dot(self, value): Sets the volume flow rate and ensures mass flow rate is not set simultaneously. set_T(self, value): Sets the temperature and updates the list of known variables. set_p(self, value): Sets the pressure and updates the list of known variables. set_h(self, value): Sets the specific enthalpy and updates the list of known variables. set_s(self, value): Sets the specific entropy and updates the list of known variables. set_D(self, value): Sets the mass density and updates the list of known variables. set_x(self, value): Sets the quality and updates the list of known variables. set_cp(self, value): Sets the specific heat capacity without calculating other properties. set_SC(self, value): Sets the subcooling and updates the list of known variables. set_SH(self, value): Sets the superheating and updates the list of known variables. print_resume(self, unit_T='K', unit_p='Pa'): Prints a summary of the current properties in specified units. **Exceptions**: ValueError Raised if an unsupported fluid name is used or if required variables for property calculation are missing. """ def __init__(self, fluid=None): self.completely_known = False # True if all the properties and the mass flow rate are known self.state_known = False # True if all the properties are known self.variables_input = [] # List of the variables used to define the state of the fluid if fluid != None: try: try: self.AS = CP.AbstractState("BICUBIC&HEOS", fluid) self.fluid = fluid except: if "INCOMP" in fluid: fluid = fluid.split(":")[-1] self.AS = CP.AbstractState("INCOMP", fluid) self.fluid = fluid except: warnings.warn("Error: Incorrect fluid name:", fluid) else: self.fluid = None # Fluid name self.m_dot = None # Mass flow rate [kg/s] self.V_dot = None # Volume flow rate [m^3/s] self.T = None # Temperature [K] self.p = None # Pressure [Pa] self.h = None # Specific enthalpy [J/kg] self.s = None # Specific entropy [J/kg/K] self.D = None # Mass density [kg/m^3] self.x = None # Quality [kg/kg] self.cp = None # Specific heat capacity [J/kg/K] self.SC = None # Subcooling [K] self.SH = None # Superheating [K] self.CP_map = { # --- Mass-based inputs --- ('D', 'H'): CP.DmassHmass_INPUTS, ('D', 'P'): CP.DmassP_INPUTS, ('D', 'Q'): CP.DmassQ_INPUTS, ('D', 'S'): CP.DmassSmass_INPUTS, ('D', 'T'): CP.DmassT_INPUTS, ('D', 'U'): CP.DmassUmass_INPUTS, ('H', 'P'): CP.HmassP_INPUTS, ('H', 'Q'): CP.HmassQ_INPUTS, ('H', 'S'): CP.HmassSmass_INPUTS, ('H', 'T'): CP.HmassT_INPUTS, ('P', 'Q'): CP.PQ_INPUTS, ('P', 'S'): CP.PSmass_INPUTS, ('P', 'T'): CP.PT_INPUTS, ('P', 'U'): CP.PUmass_INPUTS, ('Q', 'S'): CP.QSmass_INPUTS, ('Q', 'T'): CP.QT_INPUTS, ('S', 'T'): CP.SmassT_INPUTS, ('S', 'U'): CP.SmassUmass_INPUTS, ('T', 'U'): CP.TUmass_INPUTS, # --- Molar-based inputs --- ('Dmolar', 'Hmolar'): CP.DmolarHmolar_INPUTS, ('Dmolar', 'P'): CP.DmolarP_INPUTS, ('Dmolar', 'Q'): CP.DmolarQ_INPUTS, ('Dmolar', 'Smolar'): CP.DmolarSmolar_INPUTS, ('Dmolar', 'T'): CP.DmolarT_INPUTS, ('Dmolar', 'Umolar'): CP.DmolarUmolar_INPUTS, ('Hmolar', 'P'): CP.HmolarP_INPUTS, ('Hmolar', 'Q'): CP.HmolarQ_INPUTS, ('Hmolar', 'Smolar'): CP.HmolarSmolar_INPUTS, ('Hmolar', 'T'): CP.HmolarT_INPUTS, ('P', 'Smolar'): CP.PSmolar_INPUTS, ('P', 'Umolar'): CP.PUmolar_INPUTS, ('Q', 'Smolar'): CP.QSmolar_INPUTS, ('Smolar', 'T'): CP.SmolarT_INPUTS, ('Smolar', 'Umolar'): CP.SmolarUmolar_INPUTS, ('T', 'Umolar'): CP.TUmolar_INPUTS, } def reset(self): self.completely_known = False self.state_known = False self.variables_input = [] self.m_dot = None self.V_dot = None self.T = None self.p = None self.h = None self.s = None self.D = None self.x = None self.cp = None def check_completely_known(self): if self.fluid != None: if len(self.variables_input)>2: warnings.warn("Error: Too many state variables") elif (len(self.variables_input) == 2 or (len(self.variables_input) == 1 and (self.SC is not None or self.SH is not None))): self.calculate_properties() elif len(self.variables_input)<2: pass if (self.m_dot != None or self.V_dot !=None) and self.state_known: if self.m_dot != None: self.V_dot = self.m_dot / self.D * 3600 elif self.V_dot != None: self.m_dot = self.V_dot * self.D / 3600 self.completely_known = True else: pass def get_AS_inputs(self, key1, key2): """ Return: - The appropriate CoolProp AbstractState INPUT constant. - A flag (0 if original order kept, 1 if reversed). Example: self.get_AS_inputs('P', 'T') -> (CP.PT_INPUTS, 0) self.get_AS_inputs('T', 'P') -> (CP.PT_INPUTS, 1) """ # Determine if we reversed key order during sorting pair_sorted = tuple(sorted([key1, key2])) reversed_flag = int(pair_sorted != (key1, key2)) # 1 if reversed if pair_sorted not in self.CP_map: raise ValueError(f"Unsupported input pair: {pair_sorted}. " f"Valid keys: {list(self.CP_map.keys())}") return self.CP_map[pair_sorted], reversed_flag def _replace_state_variable(self, key, value): """ Replace or insert a state variable in variables_input. Ensures only valid CoolProp inputs are used. """ for i, var in enumerate(self.variables_input): if var[0] == key: self.variables_input[i][1] = value return self.variables_input.append([key, value]) def calculate_properties(self): # 1) Resolve superheating/subcooling into T or P try: if self.SH is not None and self.SC is not None: raise ValueError("Cannot specify both SH and SC simultaneously.") # ---- SUPERHEATING ---- if self.SH is not None: if self.p is not None: # p known -> compute T self.AS.update(CP.PQ_INPUTS, self.p, 1) # Saturation temp at given p T_sat = self.AS.T() self.T = T_sat + self.SH elif self.T is not None: # T known -> compute p self.AS.update(CP.QT_INPUTS, 1, self.T - self.SH) # Saturation p at given T self.p = self.AS.p() else: pass # Cannot resolve SH without T or p # ---- SUBCOOLING ---- elif self.SC is not None: if self.p is not None: # p known -> compute T self.AS.update(CP.PQ_INPUTS, self.p, 0) # Saturation temp at given p T_sat = self.AS.T() self.T = T_sat - self.SC elif self.T is not None: # T known -> compute p self.AS.update(CP.QT_INPUTS, 0, self.T + self.SC) # Saturation p at given T self.p = self.AS.p() else: pass # Cannot resolve SC without T or p except Exception as e: warnings.warn(f"SH/SC resolution failed: {e}") return # --- After resolving SH / SC --- if self.SH is not None or self.SC is not None: # Ensure variables_input contains T or P, not SC/SH if self.T is not None: self._replace_state_variable('T', self.T) if self.p is not None: self._replace_state_variable('P', self.p) # Remove any illegal keys just in case self.variables_input = [ v for v in self.variables_input if v[0] in ['T', 'P', 'H', 'S', 'D', 'Q'] ] # 2) Calculate properties based on two known variables AS_inputs, reversed_flag = self.get_AS_inputs(self.variables_input[0][0], self.variables_input[1][0]) try: if not reversed_flag: self.AS.update(AS_inputs, self.variables_input[0][1], self.variables_input[1][1]) else: self.AS.update(AS_inputs, self.variables_input[1][1], self.variables_input[0][1]) self.T = self.AS.T() self.p = self.AS.p() self.h = self.AS.hmass() self.s = self.AS.smass() self.D = self.AS.rhomass() self.cp = self.AS.cpmass() self.x = self.AS.Q() self.state_known = True except: warnings.warn("Error: This pair of inputs is not yet supported.") def set_properties(self, **kwargs): if 'fluid' in kwargs: self.set_fluid(kwargs['fluid']) for key, value in kwargs.items(): if key.lower() == 'fluid': self.set_fluid(value) elif key == 'm_dot': self.set_m_dot(value) elif key == 'V_dot': self.set_V_dot(value) elif key.upper() == 'T': self.set_T(value) elif key.upper() == 'P': self.set_p(value) elif key.upper() == 'H': self.set_h(value) elif key.upper() == 'S': self.set_s(value) elif key.upper() == 'D': self.set_D(value) elif key.lower() == 'x': self.set_x(value) elif key.lower() == 'cp': self.set_cp(value) elif key.upper() == 'SC': self.set_SC(value) elif key.upper() == 'SH': self.set_SH(value) else: warnings.warn(f"Error: Invalid property '{key}'") def set_fluid(self, value): # print('set_fluid', value) if self.fluid != None: pass elif self.fluid == None: try: try: self.AS = CP.AbstractState("BICUBIC&HEOS", value) self.fluid = value except: if "INCOMP" in value: value = value.split(":")[-1] self.AS = CP.AbstractState("INCOMP", value) self.fluid = value except: warnings.warn("Error: Incorrect fluid name:", value) self.check_completely_known() def set_m_dot(self, value): self.m_dot = value self.V_dot = None #makes sure that the volume flow rate and the mass flow rate are not both known self.check_completely_known() def set_V_dot(self, value): self.V_dot = value self.m_dot = None #makes sure that the volume flow rate and the mass flow rate are not both known self.check_completely_known() def set_T(self, value): # print('set_T', value) if self.T != None: # If the temperature is already known, update the value and the corresponding variable in the list self.T = value for i, var in enumerate(self.variables_input): if var[0] == 'T': self.variables_input[i][1] = value if i == 1: self.variables_input[0], self.variables_input[1] = self.variables_input[1], self.variables_input[0] self.check_completely_known() return self.variables_input.pop() self.variables_input.insert(0, ['T', value]) self.check_completely_known() return else: # If the temperature is not known, set the value and add the variable to the list self.T = value self.variables_input = self.variables_input+[['T',value]] self.check_completely_known() def set_p(self, value): # print('set_p', value) if self.p != None: # If the pressure is already known, update the value and the corresponding variable in the list self.p = value for i, var in enumerate(self.variables_input): if var[0] == 'P': self.variables_input[i][1] = value if i == 1: self.variables_input[0], self.variables_input[1] = self.variables_input[1], self.variables_input[0] self.check_completely_known() return self.variables_input.pop() self.variables_input.insert(0, ['P', value]) self.check_completely_known() return else: # If the pressure is not known, set the value and add the variable to the list self.p = value self.variables_input = self.variables_input+[['P',value]] self.check_completely_known() def set_h(self, value): # print('set_h', value) if self.h != None: # If the specific enthalpy is already known, update the value and the corresponding variable in the list self.h = value for i, var in enumerate(self.variables_input): if var[0] == 'H': self.variables_input[i][1] = value if i == 1: self.variables_input[0], self.variables_input[1] = self.variables_input[1], self.variables_input[0] self.check_completely_known() return self.variables_input.pop() self.variables_input.insert(0, ['H', value]) self.check_completely_known() return else: # If the specific enthalpy is not known, set the value and add the variable to the list self.h = value self.variables_input = self.variables_input+[['H',value]] self.check_completely_known() def set_s(self, value): if self.s != None: # If the specific entropy is already known, update the value and the corresponding variable in the list self.s = value for i, var in enumerate(self.variables_input): if var[0] == 'S': self.variables_input[i][1] = value if i == 1: self.variables_input[0], self.variables_input[1] = self.variables_input[1], self.variables_input[0] self.check_completely_known() return self.variables_input.pop() self.variables_input.insert(0, ['S', value]) self.check_completely_known() return else: # If the specific entropy is not known, set the value and add the variable to the list self.s = value self.variables_input = self.variables_input+[['S',value]] self.check_completely_known() def set_D(self, value): if self.D != None: # If the mass density is already known, update the value and the corresponding variable in the list self.D = value for i, var in enumerate(self.variables_input): if var[0] == 'D': self.variables_input[i][1] = value if i == 1: self.variables_input[0], self.variables_input[1] = self.variables_input[1], self.variables_input[0] self.check_completely_known() return self.variables_input.pop() self.variables_input.insert(0, ['D', value]) self.check_completely_known() return else: # If the mass density is not known, set the value and add the variable to the list self.D = value self.variables_input = self.variables_input+[['D',value]] self.check_completely_known() def set_x(self, value): if self.x != None: # If the quality is already known, update the value and the corresponding variable in the list self.x = value for i, var in enumerate(self.variables_input): if var[0] == 'Q': self.variables_input[i][1] = value if i == 1: self.variables_input[0], self.variables_input[1] = self.variables_input[1], self.variables_input[0] self.check_completely_known() return self.variables_input.pop() self.variables_input.insert(0, ['Q', value]) self.check_completely_known() return else: # If the quality is not known, set the value and add the variable to the list self.x = value self.variables_input = self.variables_input+[['Q',value]] self.check_completely_known() def set_cp(self, value): if self.cp != None: # If the cp is already known, update the value and the corresponding variable in the list self.cp = value else: # If the quality is not known, set the value and add the variable to the list self.cp = value # We don't want to calculate in this case def set_SC(self, value): self.SC = value self.SH = None # enforce exclusivity self.check_completely_known() def set_SH(self, value): self.SH = value self.SC = None # enforce exclusivity self.check_completely_known() def print_resume(self, unit_T='K', unit_p='Pa'): """ Parameters ---------- unit_T = Temperature unit: 'K' or 'C' unit_p = Temperature unit: 'Pa' or 'bar' """ if self.fluid is not None: print("Fluid: " + self.fluid + "") else: print("Fluid: None") print("Mass flow rate: " + str(self.m_dot) + "[kg/s]") print("Volume flow rate: " + str(self.V_dot) + "[m^3/h]") if unit_T == 'K': print("Temperature: " + str(self.T) + "[K]") elif unit_T == 'C': print("Temperature: " + str(self.T-273.15) + "[°C]") else: print("Error: Wrong argument unit_T in the method print_resume") if unit_p == 'Pa': print("Pressure: " + str(self.p) + "[Pa]") elif unit_p == 'bar': print("Pressure: " + str(self.p/1e5) + "[bar]") else: print("Error: Wrong argument unit_p in the method print_resume") print("Spec. enthalpy: " + str(self.h) + "[J/kg]") print("Spec. entropy: " + str(self.s) + "[J/kg/K]") print("Mass density: " + str(self.D) + "[kg/m^3]") print("Quality: " + str(self.x) + "[-]") def reset(self): self.T = None self.h = None self.m_dot = None