Source code for carbatpy.models.fluids.fluid_props

# -*- coding: utf-8 -*-
"""
Created on Mon May 22 21:09:12 2023

@author: atakan
"""

import os
import copy
# from time import time
from ctREFPROP.ctREFPROP import REFPROPFunctionLibrary
import numpy as np
# import CoolProp.CoolProp as CP
import carbatpy

[docs]VERBOSE = False
if carbatpy.TREND["TREND_INSTALLED"]:
[docs] trend_dll = carbatpy.TREND["TREND_DLL"]
trend_path = carbatpy.TREND["TREND_PATH"] try: import fluid as tr_fl # TREND fluids except ImportError as e: print(f"Import error for 'fluid': {e}") # os.environ['RPPREFIX'] = r'C:/Program Files (x86)/REFPROP' # os.environ['RPPREFIXs'] = r'C:/Program Files (x86)/REFPROP/secondCopyREFPROP'
[docs]_PROPS = "REFPROP" # or "CoolProp"
[docs]_fl_properties_names = ("Temperature", "Pressure", "spec_Enthalpy", "spec_Volume", "spec_Entropy", "quality", "spec_internal_Energy", "viscosity", "thermal_conductivity", "Prandtl_number", "specific isobaric heat capacity", "molecular_mass", "speed_of_sound", )
[docs]_THERMO_STRING = "T;P;H;V;S;QMASS;E"
_THERMO_TREND = request = ["T", "P", "H", "D", "S", "QEOS", "U"] # careful density not volume
[docs]_TRANS_STRING = _THERMO_STRING + ";VIS;TCX;PRANDTL;CP;M;W"
[docs]_TRANS_TREND = copy.copy(_THERMO_TREND)
_TRANS_TREND.extend(["ETA", "TCX", "CP", "WS"])
[docs]_TV_STRING = "T;V"
[docs]_T_SURROUNDING = 288.15 # K
[docs]_MODEL_ARGS = {}
# order for coolprop,alle_0:[_temp, p, h, 1/ rho, s,x,cp, mu, lambda_s, # prandtl, phase]"
[docs]_UNITS = 21
rp_instance = "" if _PROPS == "REFPROP": try:
[docs] rp_instance = REFPROPFunctionLibrary(os.environ['RPPREFIX'])
# be careful pressure is in Pa! _UNITS = rp_instance.GETENUMdll(0, "MASS BASE SI").iEnum except: # ModuleNotFoundError as errefprop: print(f"Refprop is not installed!") # " {errefprop}") elif _PROPS == "TREND": pass else: print(f"_PROPS value, not installed {_PROPS}")
[docs]class FluidModel: """ Fluid model to be used Refprop or TREND Only model, chemical compunds units and for TREND some more values are set here. The mixture composition, state etc. is set in Fluid. """
[docs] def __init__(self, fluid, units=_UNITS, props=_PROPS, rp_inst=rp_instance, args=_MODEL_ARGS): """ For Class Fluidmodell a fluid (mixture) must be defined, the evaluation takes place with props, units can be set and an instance can be set, the latter is important, if more than one fluid is used. Parameters ---------- fluid : String as defined in the props (Model), for RefProp it is "fluid1 * fluid2 * fluid3". units : integer, optional units, normally SI wuth MASS base, see props(RefProp). The default is _UNITS. props : string, optional select the property model. The default is _PROPS. rp_inst : RefProp-Instance, optional where Refprop is installed, for two fluids, as in heat exchangers, two installations/instances are needed. The default is rp_instance. args : Dictionary, optional Further arguments passed to the module used for fluid property evaluation. for props =="REFPROP" nothing is needed. The default is the empty dictionary _MODEL_ARGS. Returns ------- None. """ self.fluid = fluid self.props = props self.units = units self.args = args if props == "REFPROP": self.rp_instance = rp_inst self.set_rp_fluid() elif props == "TREND": self.set_tr_fluid()
[docs] def set_rp_fluid(self, modwf=REFPROPFunctionLibrary, name='RPPREFIX'): """ A new instance of Refpropdll for the given fluid. It can then be called using fluid ="" Parameters ---------- fluid : string fluid (mixture) name, as described in REFPROP. Returns ------- self.rp_instance : REFPROP Instance for further usage. """ self.rp_instance = modwf(os.environ[name]) self.rp_instance.SETPATHdll(os.environ[name]) ierr = self.rp_instance.SETFLUIDSdll(self.fluid) if ierr != 0: print(f"Fehler in setfluid {ierr}") print(self.rp_instance.ERRMSGdll(ierr)) return self.rp_instance
[docs] def set_tr_fluid(self): self.fluid = self.fluid_to_list() if self.units == 21: self.units = "specific" n_compounds = len(self.fluid) if len(self.args["moles"]) < n_compounds: self.args["moles"] = list(np.zeros(n_compounds)) self.args["moles"][0] = 1. if len(self.args["eos_ind"]) < n_compounds: self.args["eos_ind"] = list(np.ones(n_compounds)) if self.args["mix_ind"] < 1: self.args["mix_ind"] = 1 _trend_dict = {"Input": "TP", 'calctype': "H", 'fluids': self.fluid, "moles": self.args["moles"], "eos_ind": self.args["eos_ind"], 'mix_ind': self.args["mix_ind"], 'path': trend_path, 'unit': self.units, 'dll_path': trend_dll} self.rp_instance = tr_fl.Fluid(*_trend_dict.values()) err_flag = self.rp_instance.errorflag.value if err_flag > 0: print(f"fluid-Trend problem, errorflag:{err_flag}") return self.rp_instance
[docs] def fluid_to_list(self): """ conversts the fluid names string from Refprop to list for TREND Returns ------- List of strings the fluid names in a list. """ no_blank = self.fluid.replace(" ", "") return no_blank.split("*")
[docs]class Fluid: """ The Fluid class is used to set, get, and print states of a fluid with a given model (e.g. RefProp). The compounds are set in the fluidmodel, while the composition is also set here. """
[docs] def __init__(self, fluidmodel, composition=None, option=1): """ Generate a Fluid instance a FluidModel instance, the composition and an unused option has to be passed. Parameters ---------- fluidmodel : FluidModel the instance of the property model to be used. composition : list, optional mole fractions of all compounds. The default is [1.0]. option : TYPE, optional unused. The default is 1. Returns ------- None. """ self.fluidmodel = fluidmodel if composition is None: composition = [1.0] else: self.composition = composition self.comp_mole = copy.copy(composition) self.properties = None self.state_v = None self.option = option self.no_compounds = len(composition) self.comp_mass = np.zeros(self.no_compounds) self.herr = 0 self.props = fluidmodel.props # BA something is wrong here (probably 2024-07-10 if self.props == "TREND": # important for trend mass->mole" self.set_composition(composition) self.fluidmodel.rp_instance.set_moles(self.composition) if VERBOSE: print("Konversion", self.composition, self.comp_mass, self.comp_mole, self.fluidmodel.rp_instance.get_moles())
[docs] def set_composition(self, composition): """ Mainly for TREND mole fractions have to converted to mass fractions Sets the self.composition used in the Fluid settings and the self.comp_mass and self.comp_mole by calling calc_mass_fraction() Parameters ---------- composition : list/numpy.array mole fractions must sum up to 1. Returns ------- None. """ self.composition = composition if self.fluidmodel.props == "TREND": self.comp_mass = composition self.calc_mass_fraction() self.fluidmodel.rp_instance.set_moles(self.composition)
# here is the mass fraction to internal mol fraction conversion
[docs] def calc_mass_fraction(self): """ mole to mass fractions for TREND""" mass_frac = np.zeros(self.no_compounds) for n_c in range(self.no_compounds): mass_frac[n_c] = self.fluidmodel.rp_instance.get_mw(n_c) * \ self.comp_mole[n_c] self.comp_mass = mass_frac/mass_frac.sum() self.composition = copy.copy(self.comp_mass) return self.composition
[docs] def set_state(self, values, given="TP", wanted=_THERMO_STRING, composition=None, **kwargs): """ Sets the state of a fluid and calculates the wanted Parameters ---------- values : list[2] the two state parameter values. given : String, optional What are the two values (T,P,H,S,Q,V). The default is "TP". wanted : String, optional Which parameters shall be calculated? The default is _THERMO_STRING. composition : list, optional mole fraction of each compound, must sum up to 1. When empty, the last/actual value will be used. The default is []. kwargs : dictionary, optional Flags like iMass or iFlag can be passed to Refprop. with iMass=1 the input composition is taken as mass fraction, otherwise they are mole fractions. iMass is set to 1 if 'Q' is in wanted. Raises ------ Exception DESCRIPTION. Returns ------- TYPE DESCRIPTION. """ composition = self._update_composition(composition) if self.fluidmodel.props == "REFPROP": self._set_state_refprop(values, given, wanted, composition, **kwargs) elif self.fluidmodel.props == "TREND": self._set_state_trend(values, given, wanted) else: raise NotImplementedError( f"Property model {self.fluidmodel.props} not implemented yet!") return np.array([*self.state])
[docs] def _update_composition(self, composition): if composition is None: # mole fractions only! return self.composition self.composition = composition if self.props == "TREND": self.set_composition(composition) # important for trend mass->mole if VERBOSE: print("comp-set", composition, self.composition, self.comp_mass, self.comp_mole) return composition
[docs] def _set_state_refprop(self, values, given, wanted, composition, **kwargs): i_mass = kwargs.get("iMass", 0) i_flag = kwargs.get("iFlag", 0) full_dll = False if i_mass == 0 and i_flag == 0: state = self.fluidmodel.rp_instance.REFPROP2dll( self.fluidmodel.fluid, given, wanted, self.fluidmodel.units, i_flag, values[0], values[1], composition ) else: full_dll = True state = self.fluidmodel.rp_instance.REFPROPdll( self.fluidmodel.fluid, given, wanted, self.fluidmodel.units, i_mass, i_flag, values[0], values[1], composition ) if state.ierr == 0: state_val = state.Output z_act = state.z x_act = y_act =None if state.q > 0 and state.q < 1: # mole -> mass quality problem of Refprop state_val[5] = state.q if full_dll: x_act = state.x y_act = state.y #self.properties self._set_fluid_properties(state_val, wanted, x=x_act, y=y_act, z=z_act) # if wanted == _THERMO_STRING: # self.properties = FluidState(state_val) # elif wanted == _TRANS_STRING: # self.properties = FluidStateTransport(state_val) # elif wanted == _TV_STRING: # self.properties = FluidStateTV(state_val) # else: # raise NotImplementedError(f"properties {wanted} not implemented yet!") else: self.herr = state.herr raise RuntimeError(f"Property-Refprop problem: {state.herr}!")
[docs] def _set_state_trend(self, values, given, wanted): if VERBOSE: print("in TREND set state") values, given = self._adjust_trend_values(values, given) all_values = self.fluidmodel.rp_instance.ALLPROP(given, values[0], values[1]) all_val_initial = copy.copy(all_values) if all_values["T"] > 0: # on error it is -8888 all_values["P"] = all_values["P"] * 1e6 # MPa all_values["D"] = 1 / all_values["D"] # V (spec. volume) all_values = [all_values[key] for key in _THERMO_TREND] if wanted == _TRANS_STRING: trans_dict = {} speed_sound = all_val_initial["WS"] for tr_want in _TRANS_TREND[-3:]: self.fluidmodel.rp_instance.set_calctype(tr_want) self.fluidmodel.rp_instance.set_input(given) trans_dict[tr_want], err = self.fluidmodel.rp_instance.TREND_EOS( *values) print(tr_want, ":", trans_dict[tr_want], err) if trans_dict[tr_want] < 0: print("WARNING: Problem / NOT IMPLEMENTED IN TREND!") # self.properties = FluidState(all_values) self._set_fluid_properties(all_values, wanted) else: raise RuntimeError( f"property calculation in TREND failed, error code TREND: {all_values['T']}")
[docs] def _adjust_trend_values(self, values, given): pressure_in = given[:2].find("P") quality_in = given[:2].find("Q") temp_in = given[:2].find("T") if pressure_in >= 0: values[pressure_in] = values[pressure_in] / 1e6 # MPa in Trend if quality_in > 0: if pressure_in >= 0: values, given = self._adjust_values_for_quality_pressure( values, quality_in, pressure_in) elif temp_in >= 0: values, given = self._adjust_values_for_quality_temperature( values, quality_in, temp_in) else: raise NotImplementedError( f'Error: Qualities cannot be combined with {given}!') return values, given
[docs] def _adjust_values_for_quality_pressure(self, values, quality_in, pressure_in): match values[quality_in]: case 0.: given = "PLIQ" case 1.: given = "PVAP" case _: raise ValueError('Error: only qualities 0 or 1 for mixtures!') values[0] = values[pressure_in] values[1] = 10 return values, given
[docs] def _adjust_values_for_quality_temperature(self, values, quality_in, temp_in): match values[quality_in]: case 0.: given = "TLIQ" case 1.: given = "TVAP" case _: raise ValueError('Error: only qualities 0 or 1 for mixtures!') values[0] = values[temp_in] values[1] = 10 return values, given
[docs] def set_state_v(self, values, given="TP", wanted=_THERMO_STRING, **kwargs): """ set many Fluid states with the values given as array(vector) Parameters ---------- values : numpy.array((N,2)) the N value pairs to fix the states. given : String, optional what are the input values?(T,P, Q, S,...). The default is "TP". wanted : String, optional with all the outputs wanted. The default is _THERMO_STRING. Returns ------- output : numpy.array(N, n_wanted) all the wanted properties for the N inputs. """ dimension = np.shape(values) number_wanted = wanted.count(";")+1 output = np.zeros((dimension[0], number_wanted)) for count, value in enumerate(values): output[count, :] = self.set_state(value, given, wanted, **kwargs) self.state_v = output return output
[docs] def _set_fluid_properties(self, state, wanted, **kwargs): """ The thermodynamic state of a fluid is stored here """ self.liq_x = kwargs.get("x",None) self.vap_y = kwargs.get("y",None) self.total_z = kwargs.get("y",None) verbose = kwargs.get("verbose",False) n_prop =wanted.count(";")+1 n_states = len(state) if verbose: print(n_prop, state) # "T;P;H;V;S;QMASS;E" self.temperature = state[0] self.pressure = state[1] self.enthalpy = state[2] self.sp_volume = state[3] self.entropy = state[4] self.quality = state[5] self.int_energy = state[6] self.state = state[:7] self.prop_names = _fl_properties_names if n_prop == 7 or n_states ==7: return else: if state[5] > 0 and state[5] < 1: two_phase = True # ";VIS;TCX;PRANDTL;KV;M" self.viscosity = state[7] self.thermal_conductivity = state[8] self.prandtl = state[9] self.cp = state[10] self.molecular_mass = state[11] self.speed_of_sound = state[12] self.transport = state[7:13] self.state = state[:13] return
[docs] def print_state(self): """ Print the actual state of the Fluid""" pr = self flmod = self.fluidmodel print(f"\n{flmod.fluid}, composition: {self.composition}") print(f"T:{pr.temperature:.2f} K, p: {pr.pressure/1e5 :.2f} bar, h: {pr.enthalpy/1000: .2f} kJ/kg, s: {pr.entropy/1000:.3f} kJ/kg K\n") if pr.quality >= 0: print(f"Quality: {pr.quality :.3f}")
[docs] def calc_temp_mean(self, h_final): """ Calculate the thermodynamic mean temperature between the actual state and the final enthalpy along an isobaric (Delta h /Delta s) Parameters ---------- h_final : float enthalpy of he final stat. Returns ------- temp_mean : float the thermodynamic mean temperature. """ actual_props = self.state final_props = self.set_state([h_final, actual_props[1]], "HP") temp_mean = (final_props[2] - actual_props[2]) \ / (final_props[4] - actual_props[4]) return temp_mean
[docs]def init_fluid(fluid, composition, **keywords): """ short way to define a Fluid and a FluidModel Parameters ---------- fluid : string The species within the fluid. composition : List mole fraction for each fluid. **keywords : TYPE all keywords needed for the FluidModel, if non-defults shall be set. Returns ------- actual_fluid : Fluid Instance of the actually set Fluid. """ flmod = FluidModel(fluid, **keywords) actual_fluid = Fluid(flmod, composition) return actual_fluid
if __name__ == "__main__":
[docs] FLUID = "Propane * Pentane"
comp = [.50, 0.5] P_ACT = 1e5 T_ACT = 370 flm = FluidModel(FLUID) myFluid = Fluid(flm, comp) st00 = myFluid.set_state([T_ACT, P_ACT], "TP") st1 = myFluid.set_state([T_ACT, P_ACT], "TP", _TRANS_STRING) print(st00, st1) myFluid.print_state() myFluid.set_composition([.2, .8]) st0 = myFluid.set_state([T_ACT, P_ACT], "TP", composition=[.35, .65]) myFluid.print_state() mean_temp_act = myFluid.calc_temp_mean(st0[2]+1e5) print(f"Mean Temperature {mean_temp_act} K") # value_vec = np.array([[300, 1e5], [400, 1e5], [500, 1e5]]) # stv = myFluid.set_state_v(value_vec, "TP") # print(myFluid.set_state_v(value_vec, "TP")) # print(myFluid.set_state([300., 1.], "TQ")) # New simple way to get an instance of Fluid myFluid2 = init_fluid(FLUID, comp) # Refprop with mass fractions as input print('\n------------------------------ Refprop Mass fraction Input') FLUID = "Propane * Pentane" comp = [.5, .5] myFluid = init_fluid(FLUID, comp) quality = 0.5 # is interpreted by carbatpy as mol/mol to calculate state # but given back in string as kg/kg st0 = myFluid.set_state([300., quality], "TQ", iMass=0) print(f"quality prescribed: {quality}") print(f"resulting quality from carbatpy in st0: {st0[5]}") print(st0) stb = myFluid.set_state([st0[1], st0[2]], "PH", iMass=0) print(f"quality prescribed: {quality}") print(f"resulting quality from carbatpy in st0: {stb[5]}") print(stb) print("\n------------\n") # Trend: my_dict = {"Input": "TP", 'calctype': "H", 'fluids': FLUID, "moles": comp, "eos_ind": [1, 1], 'mix_ind': 1, 'path': trend_path, 'unit': 'specific', 'dll_path': trend_dll} # comp =[.5,0.5] flm_trend = FluidModel(FLUID, props="TREND", args=my_dict) # [0.6206645653468038, 0.3793354346531962]) my_tr_fl = Fluid(flm_trend, comp) state_trend = my_tr_fl.set_state([T_ACT, P_ACT], "TP") print("\nComparison Refprop vs. Trend\n", st00, "\n", state_trend, my_tr_fl.enthalpy) state_tr_trans = my_tr_fl.set_state([T_ACT, P_ACT], "TP", wanted=_TRANS_STRING) print(f"Trans-String: {state_tr_trans}") # shortcut to generate a trend fluid flt0 = carbatpy.init_fluid(FLUID, comp, props="TREND", args=my_dict) # saturated mixture: state_trend_satp = my_tr_fl.set_state([P_ACT, 0], "PQ") print("\nSaturated mixture, liq:\n", state_trend_satp, my_tr_fl.enthalpy) state_trend_satp = my_tr_fl.set_state([P_ACT, 1], "PQ") print("Saturated mixture, vap:\n", state_trend_satp, my_tr_fl.enthalpy) # some temperature dependence state_trend_c = my_tr_fl.set_state([230., P_ACT], "TP") enth = np.linspace(state_trend_c[2], state_trend[2], 29) all_props_act = [] for h_act in enth: all_props_act.append(my_tr_fl.set_state([P_ACT, h_act], "PH")) all_props_act = np.array(all_props_act) import matplotlib.pyplot as plt fi, ax = plt.subplots(1) ax.plot(all_props_act[:, 2], all_props_act[:, 0])