#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""Pychemqt, Chemical Engineering Process simulator
Copyright (C) 2009-2025, Juan José Gómez Romera <jjgomera@gmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Library to multiparameter equation of state calculation using coolprop \
http://coolprop.sourceforge.net/index.html
If available, this is a optional speed up method for mEoS internal library
All the functionality is included in the main class:
* :class:`CoolProp`: Stream definition using coolProp external library
API reference
-------------
"""
import os
from tools.qt import translate
try:
import CoolProp as CP
except ImportError as e:
pass
from lib import unidades, mEoS
from lib.compuestos import Componente
from lib.thermo import ThermoAdvanced
__doi__ = {
1:
{"autor": "Bell, I.H., Wronski, J., Quoilin, S., Lemort, V.",
"title": "Pure and Pseudo-pure Fluid Thermophysical Property"
"Evaluation and the Open-Source Thermophysical Property"
"Library CoolProp",
"ref": "Ind. Eng. Chem. Res. 53(6) (2014) 2498-2508",
"doi": "10.1021/ie4033999"}}
# Automatic loading of coolProp name from meos subclass _coolPropName property
all__ = {}
noIds = []
for cmp in mEoS.__all__:
if cmp.id and cmp._coolPropName:
all__[cmp.id] = cmp._coolPropName
elif cmp._coolPropName:
noIds.append(cmp._coolPropName)
[docs]
class CoolProp(ThermoAdvanced):
"""Stream class using coolProp external library
Parameters needed to define it are:
-ids: index of fluid
-fraccionMolar: molar fraction
-T: Temperature, Kelvin
-P: Pressure, Pa
-rho: Density, kg/m3
-h: Enthalpy, J/kg
-s: Entropy, J/kgK
-x: Quality, -
"""
kwargs = {"ids": [],
"fraccionMolar": [],
"T": 0.0,
"P": 0.0,
"x": None,
"Q": None,
"rho": 0.0,
"v": 0.0,
"Dmass": 0.0,
"h": None,
"Hmass": None,
"u": None,
"Umass": None,
"s": None,
"Smass": None}
def __call__(self, **kwargs):
self.kwargs.update(kwargs)
if self.calculable:
try:
self.calculo()
except ValueError as e:
self.msg = e
self.status = 0
else:
self.status = 1
self.msg = "Solved"
elif self._definition and not self._multicomponent and "ids" in kwargs:
if os.environ["CoolProp"] == "True":
fluido = self._name()
estado = CP.AbstractState(b"HEOS", fluido.encode())
self.Tc = unidades.Temperature(estado.T_critical())
self.Pc = unidades.Pressure(estado.p_critical())
self.rhoc = unidades.Density(estado.rhomass_critical())
self.M = unidades.Dimensionless(estado.molar_mass()*1000)
self.R = unidades.SpecificHeat(estado.gas_constant()/self.M)
self.Tt = unidades.Temperature(estado.Ttriple())
self.f_accent = unidades.Dimensionless(
estado.acentric_factor())
self.name = fluido
self.CAS = estado.fluid_param_string(b"CAS")
self.synonim = estado.fluid_param_string(b"aliases")
self.formula = estado.fluid_param_string(b"formula")
self.eq = self._limit(fluido, estado)
[docs]
def _limit(self, name, estado):
eq = {}
eq["Tmin"] = estado.Tmin()
eq["Tmax"] = estado.Tmax()
eq["Pmin"] = CP.CoolProp.PropsSI(b"pmin", name.encode())
eq["Pmax"] = estado.pmax()
return eq
@property
def calculable(self):
"""Check in the class is fully defined"""
# Check mix state
self._multicomponent = False
if len(self.kwargs["ids"]) > 1:
self._multicomponent = True
# Check supported fluid
COOLPROP_available = True
for id in self.kwargs["ids"]:
if id not in all__ and id not in noIds:
COOLPROP_available = False
if not COOLPROP_available:
raise(ValueError)
# Check correct fluid definition
if self._multicomponent:
if self.kwargs["ids"] and len(self.kwargs["fraccionMolar"]) == \
len(self.kwargs["ids"]):
self._definition = True
else:
self._definition = False
elif self.kwargs["ids"]:
self._definition = True
else:
self._definition = False
# Update the kwargs with the special coolprop namespace
if self.kwargs["x"] != CoolProp.kwargs["x"]:
self.kwargs["Q"] = self.kwargs["x"]
if self.kwargs["v"] != CoolProp.kwargs["v"]:
self.kwargs["Dmass"] = 1/self.kwargs["v"]
if self.kwargs["rho"] != CoolProp.kwargs["rho"]:
self.kwargs["Dmass"] = self.kwargs["rho"]
if self.kwargs["s"] != CoolProp.kwargs["s"]:
self.kwargs["Smass"] = self.kwargs["s"]
if self.kwargs["h"] != CoolProp.kwargs["h"]:
self.kwargs["Hmass"] = self.kwargs["h"]
if self.kwargs["u"] != CoolProp.kwargs["u"]:
self.kwargs["Umass"] = self.kwargs["u"]
# Check thermo definition
self._thermo = ""
for def_ in ["P-T", "Q-T", "P-Q", "Dmass-T", "Dmass-P", "Hmass-P",
"P-Smass", "Hmass-Smass", "Hmass-T", "T-Umass", "P-Umass",
"Dmass-Hmass", "Dmass-Smass", "Dmass-Umass",
"Hmass-Umass", "Smass-Umass"]:
inputs = def_.split("-")
if self.kwargs[inputs[0]] != CoolProp.kwargs[inputs[0]] and \
self.kwargs[inputs[1]] != CoolProp.kwargs[inputs[1]]:
self._thermo = def_.replace("-", "")
self._inputs = inputs
self._par = CP.__getattribute__("%s_INPUTS" % self._thermo)
break
return self._definition and self._thermo
[docs]
def args(self):
var1 = self.kwargs[self._inputs[0]]
var2 = self.kwargs[self._inputs[1]]
args = [var1, var2]
return args
[docs]
def _name(self):
lst = []
for fld in self.kwargs["ids"]:
if fld in all__:
lst.append(all__[fld])
elif fld in noIds:
lst.append(fld)
name = "&".join(lst)
return name
[docs]
def calculo(self):
fluido = self._name()
args = self.args()
estado = CP.AbstractState("HEOS", fluido)
self.eq = self._limit(fluido, estado)
if self._multicomponent:
estado.set_mole_fractions(self.kwargs["fraccionMolar"])
estado.update(self._par, *args)
self.M = unidades.Dimensionless(estado.molar_mass()*1000)
if self._multicomponent:
# Disabled CoolProp critical properties for multicomponent,
# see issue #1087
# Calculate critical properties with mezcla method
# Coolprop for mixtures can fail and it's slow
Cmps = [Componente(int(i)) for i in self.kwargs["ids"]]
# Calculate critic temperature, API procedure 4B1.1 pag 304
V = sum([xi*cmp.Vc for xi, cmp in
zip(self.kwargs["fraccionMolar"], Cmps)])
k = [xi*cmp.Vc/V for xi, cmp in
zip(self.kwargs["fraccionMolar"], Cmps)]
Tcm = sum([ki*cmp.Tc for ki, cmp in zip(k, Cmps)])
self.Tc = unidades.Temperature(Tcm)
# Calculate pseudocritic temperature
tpc = sum([x*cmp.Tc for x, cmp in
zip(self.kwargs["fraccionMolar"], Cmps)])
# Calculate pseudocritic pressure
ppc = sum([x*cmp.Pc for x, cmp in
zip(self.kwargs["fraccionMolar"], Cmps)])
# Calculate critic pressure, API procedure 4B2.1 pag 307
sumaw = 0
for xi, cmp in zip(self.kwargs["fraccionMolar"], Cmps):
sumaw += xi*cmp.f_acent
pc = ppc+ppc*(5.808+4.93*sumaw)*(self.Tc-tpc)/tpc
self.Pc = unidades.Pressure(pc)
# Calculate critic volume, API procedure 4B3.1 pag 314
sumaxvc23 = sum([xi*cmp.Vc**(2./3) for xi, cmp in
zip(self.kwargs["fraccionMolar"], Cmps)])
k = [xi*cmp.Vc**(2./3)/sumaxvc23 for xi, cmp in
zip(self.kwargs["fraccionMolar"], Cmps)]
# TODO: Calculate C value from component type.
# For now it suppose all are hidrycarbon (C=0)
C = 0
V = [[-1.4684*abs((cmpi.Vc-cmpj.Vc)/(cmpi.Vc+cmpj.Vc))+C
for cmpj in Cmps] for cmpi in Cmps]
v = [[V[i][j]*(cmpi.Vc+cmpj.Vc)/2. for j, cmpj in enumerate(
Cmps)] for i, cmpi in enumerate(Cmps)]
suma1 = sum([ki*cmp.Vc for ki, cmp in zip(k, Cmps)])
suma2 = sum([ki*kj*v[i][j] for j, kj in enumerate(k)
for i, ki in enumerate(k)])
self.rhoc = unidades.Density((suma1+suma2)*self.M)
else:
self.Tc = unidades.Temperature(estado.T_critical())
self.Pc = unidades.Pressure(estado.p_critical())
self.rhoc = unidades.Density(estado.rhomass_critical())
self.R = unidades.SpecificHeat(estado.gas_constant()/self.M)
self.Tt = unidades.Temperature(estado.Ttriple())
if self._multicomponent:
estado2 = CP.AbstractState("HEOS", fluido)
estado2.set_mole_fractions(self.kwargs["fraccionMolar"])
estado2.update(CP.PQ_INPUTS, 101325, 1)
self.Tb = unidades.Temperature(estado2.T())
else:
Pt = estado.trivial_keyed_output(CP.iP_triple)
if Pt < 101325:
estado2 = CP.AbstractState("HEOS", fluido)
estado2.update(CP.PQ_INPUTS, 101325, 1)
self.Tb = unidades.Temperature(estado2.T())
self.f_accent = unidades.Dimensionless(estado.acentric_factor())
# Dipole moment only available for REFPROP backend
# self.momentoDipolar(estado.keyed_output(CP.idipole_moment))
self.phase, x = self.getphase(estado)
self.x = unidades.Dimensionless(x)
if self._multicomponent:
string = fluido.replace("&", " (%0.2f), ")
string += " (%0.2f)"
self.name = string % tuple(self.kwargs["fraccionMolar"])
self.CAS = ""
self.synonim = ""
self.formula = ""
else:
self.name = fluido
self.CAS = estado.fluid_param_string("CAS")
self.synonim = estado.fluid_param_string("aliases")
self.formula = estado.fluid_param_string("formula")
self.P = unidades.Pressure(estado.p())
self.T = unidades.Temperature(estado.T())
self.Tr = unidades.Dimensionless(self.T/self.Tc)
self.Pr = unidades.Dimensionless(self.P/self.Pc)
self.rho = unidades.Density(estado.rhomass())
self.v = unidades.SpecificVolume(1./self.rho)
cp0 = self._prop0(estado)
self._cp0(cp0)
self.Liquido = ThermoAdvanced()
self.Gas = ThermoAdvanced()
if self.x == 0:
# liquid phase
self.fill(self.Liquido, estado)
self.fill(self, estado)
self.fillNone(self.Gas)
elif self.x == 1:
# vapor phase
self.fill(self.Gas, estado)
self.fill(self, estado)
self.fillNone(self.Liquido)
else:
# Two phase
liquido = CP.AbstractState("HEOS", fluido)
if self._multicomponent:
xi = estado.mole_fractions_liquid()
liquido.set_mole_fractions(xi)
liquido.specify_phase(CP.iphase_liquid)
liquido.update(CP.QT_INPUTS, 0, self.T)
self.fill(self.Liquido, liquido)
vapor = CP.AbstractState("HEOS", fluido)
if self._multicomponent:
yi = estado.mole_fractions_vapor()
vapor.set_mole_fractions(yi)
vapor.specify_phase(CP.iphase_gas)
vapor.update(CP.QT_INPUTS, 1, self.T)
self.fill(self.Gas, vapor)
self.fill(self, estado)
# Calculate special properties useful only for one phase
if self._multicomponent:
self.sigma = unidades.Tension(None)
elif x < 1 and self.Tt <= self.T <= self.Tc:
try:
self.sigma = unidades.Tension(estado.surface_tension())
except ValueError:
self.sigma = unidades.Tension(None)
else:
self.sigma = unidades.Tension(None)
self.virialB = unidades.SpecificVolume(estado.Bvirial())
self.virialC = unidades.SpecificVolume_square(estado.Cvirial())
self.invT = unidades.InvTemperature(-1/self.T)
if 0 < self.x < 1:
self.Hvap = unidades.Enthalpy(self.Gas.h-self.Liquido.h)
self.Svap = unidades.SpecificHeat(self.Gas.s-self.Liquido.s)
else:
self.Hvap = unidades.Enthalpy(None)
self.Svap = unidades.SpecificHeat(None)
[docs]
def _prop0(self, estado):
"""Ideal gas properties"""
tau = self.Tc/self.T
fio = estado.alpha0()
fiot = estado.dalpha0_dTau()
fiott = estado.d2alpha0_dTau2()
propiedades = {}
propiedades["v"] = self.R*self.T/self.P.kPa
propiedades["h"] = self.R*self.T*(1+tau*fiot)*1000
propiedades["s"] = self.R*(tau*fiot-fio)*1000
propiedades["cv"] = -self.R*tau**2*fiott*1000
propiedades["cp"] = self.R*(-tau**2*fiott+1)*1000
propiedades["w"] = (self.R*self.T*1000/(1+tau**2/abs(fiott)))**0.5
return propiedades
[docs]
def fill(self, fase, estado):
fase._bool = True
fase.M = unidades.Dimensionless(estado.molar_mass()*1000)
fase.rho = unidades.Density(estado.rhomass())
fase.v = unidades.SpecificVolume(1./fase.rho)
fase.Z = unidades.Dimensionless(estado.keyed_output(CP.iZ))
fase.h = unidades.Enthalpy(estado.hmass())
fase.s = unidades.SpecificHeat(estado.smass())
fase.u = unidades.Enthalpy(estado.umass())
fase.a = unidades.Enthalpy(fase.u-self.T*fase.s)
fase.g = unidades.Enthalpy(fase.h-self.T*fase.s)
if self._multicomponent:
fase.fi = []
fase.f = []
for i in range(len(self.kwargs["ids"])):
fase.fi.append(unidades.Dimensionless(
estado.fugacity_coefficient(i)))
fase.f.append(unidades.Pressure(estado.fugacity(i)))
else:
fase.fi = [unidades.Dimensionless(1)]
# print(estado.alphar(), estado.delta(), estado.dalphar_dDelta(),
# (1+estado.delta()*estado.dalphar_dDelta()))
# fase.fi = [unidades.Dimensionless(
# exp(estado.alphar()+estado.delta()*estado.dalphar_dDelta() -
# log(1+estado.delta()*estado.dalphar_dDelta())))]
fase.f = [unidades.Pressure(self.P*f) for f in fase.fi]
fase.cv = unidades.SpecificHeat(estado.cvmass())
fase.cp = unidades.SpecificHeat(estado.cpmass())
fase.cp_cv = unidades.Dimensionless(fase.cp/fase.cv)
fase.gamma = fase.cp_cv
fase.w = unidades.Speed(estado.speed_sound())
fase.rhoM = unidades.MolarDensity(estado.rhomolar(), "molm3")
fase.hM = unidades.MolarEnthalpy(estado.hmolar(), "Jmol")
fase.sM = unidades.MolarSpecificHeat(estado.smolar(), "JmolK")
fase.uM = unidades.MolarEnthalpy(estado.umolar(), "Jmol")
fase.aM = unidades.MolarEnthalpy(fase.a*self.M)
fase.gM = unidades.MolarEnthalpy(fase.g*self.M)
fase.cvM = unidades.MolarSpecificHeat(estado.cvmolar(), "JmolK")
fase.cpM = unidades.MolarSpecificHeat(estado.cpmolar(), "JmolK")
fase.joule = unidades.TemperaturePressure(
estado.first_partial_deriv(CP.iT, CP.iP, CP.iHmass))
fase.Gruneisen = unidades.Dimensionless(
fase.v/fase.cv*estado.first_partial_deriv(CP.iP, CP.iT, CP.iDmass))
fase.alfav = unidades.InvTemperature(
estado.isobaric_expansion_coefficient())
fase.kappa = unidades.InvPressure(estado.isothermal_compressibility())
fase.kappas = unidades.InvPressure(
-1/fase.v*self.derivative("v", "P", "s", fase))
fase.alfap = unidades.Density(fase.alfav/self.P/fase.kappa)
fase.betap = unidades.Density(
-1/self.P*self.derivative("P", "v", "T", fase))
fase.betas = unidades.TemperaturePressure(
estado.first_partial_deriv(CP.iT, CP.iP, CP.iSmass))
fase.kt = unidades.Dimensionless(
fase.rho/self.P*estado.first_partial_deriv(
CP.iP, CP.iDmass, CP.iT))
fase.Ks = unidades.Pressure(
fase.rho*estado.first_partial_deriv(CP.iP, CP.iDmass, CP.iSmass))
fase.Kt = unidades.Pressure(
fase.rho*estado.first_partial_deriv(CP.iP, CP.iDmass, CP.iT))
fase.ks = unidades.Dimensionless(
fase.rho/self.P*estado.first_partial_deriv(
CP.iP, CP.iDmass, CP.iSmass))
fase.dhdT_rho = unidades.SpecificHeat(
estado.first_partial_deriv(CP.iHmass, CP.iT, CP.iDmass))
fase.dhdT_P = unidades.SpecificHeat(
estado.first_partial_deriv(CP.iHmass, CP.iT, CP.iP))
fase.dhdP_T = unidades.EnthalpyPressure(
estado.first_partial_deriv(CP.iHmass, CP.iP, CP.iT)) # deltat
fase.deltat = fase.dhdP_T
fase.dhdP_rho = unidades.EnthalpyPressure(
estado.first_partial_deriv(CP.iHmass, CP.iP, CP.iDmass))
fase.dhdrho_T = unidades.EnthalpyDensity(
estado.first_partial_deriv(CP.iHmass, CP.iDmass, CP.iT))
fase.dhdrho_P = unidades.EnthalpyDensity(
estado.first_partial_deriv(CP.iHmass, CP.iDmass, CP.iP))
fase.dpdT_rho = unidades.PressureTemperature(
estado.first_partial_deriv(CP.iP, CP.iT, CP.iDmass))
fase.dpdrho_T = unidades.PressureDensity(
estado.first_partial_deriv(CP.iP, CP.iDmass, CP.iT))
fase.drhodP_T = unidades.DensityPressure(
estado.first_partial_deriv(CP.iDmass, CP.iP, CP.iT))
fase.drhodT_P = unidades.DensityTemperature(
estado.first_partial_deriv(CP.iDmass, CP.iT, CP.iP))
fase.Z_rho = unidades.SpecificVolume((fase.Z-1)/fase.rho)
fase.IntP = unidades.Pressure(
self.T*estado.first_partial_deriv(CP.iP, CP.iT, CP.iDmass)-self.P)
fase.hInput = unidades.Enthalpy(
-fase.rho*estado.first_partial_deriv(CP.iHmass, CP.iDmass, CP.iP))
fase.virialB = unidades.SpecificVolume(estado.Bvirial())
fase.virialC = unidades.SpecificVolume_square(estado.Cvirial())
fase.invT = unidades.InvTemperature(-1/self.T)
try:
fase.mu = unidades.Viscosity(estado.viscosity())
except ValueError:
fase.mu = unidades.Viscosity(None)
fase.Prandt = unidades.Dimensionless(None)
try:
fase.k = unidades.ThermalConductivity(estado.conductivity())
except ValueError:
fase.k = unidades.ThermalConductivity(None)
fase.nu = unidades.Diffusivity(fase.mu/fase.rho)
fase.alfa = unidades.Diffusivity(fase.k/fase.rho/fase.cp)
fase.fraccion = estado.get_mole_fractions()
fase.fraccion_masica = estado.get_mass_fractions()
fase.epsilon = unidades.Dimensionless(None)
[docs]
def getphase(self, estado):
"""Return fluid phase with translation support"""
phase = estado.phase()
if phase == CP.iphase_supercritical:
msg = translate("CoolProp", "Supercritical fluid")
x = 1
elif phase == CP.iphase_supercritical_liquid:
msg = translate("CoolProp", "Supercritical liquid")
x = 1
elif phase == CP.iphase_supercritical_gas:
msg = translate("CoolProp", "Supercritical gas")
x = 1
elif phase == CP.iphase_gas:
msg = translate("CoolProp", "Vapor")
x = 1
elif phase == CP.iphase_liquid:
msg = translate("CoolProp", "Liquid")
x = 0
elif phase == CP.iphase_twophase:
msg = translate("CoolProp", "Two phases")
x = estado.Q()
elif phase == CP.iphase_critical_point:
msg = translate("CoolProp", "Critical point")
x = 1
return msg, x
if __name__ == '__main__':
fluido = CoolProp(ids=[107], T=300, P=1e5)
print(fluido.status, fluido.msg)
print(fluido.rho, fluido.msg)