# -*- coding: utf-8
"""Module for fluid property wrappers.
This file is part of project TESPy (github.com/oemof/tespy). It's copyrighted
by the contributors recorded in the version control history of the file,
available from its original location
tespy/tools/fluid_properties/wrappers.py
SPDX-License-Identifier: MIT
"""
import CoolProp as CP
from tespy.tools.global_vars import ERR
[docs]
def wrapper_registry(type):
wrapper_registry.items[type.__name__] = type
return type
wrapper_registry.items = {}
[docs]
class SerializableAbstractState(CP.AbstractState):
def __init__(self, back_end, fluid_name):
self.back_end = back_end
self.fluid_name = fluid_name
def __reduce__(self):
return (self.__class__, (self.back_end, self.fluid_name))
[docs]
@wrapper_registry
class FluidPropertyWrapper:
def __init__(self, fluid, back_end=None) -> None:
"""Base class for fluid property wrappers
Parameters
----------
fluid : str
Name of the fluid.
back_end : str, optional
Name of the back end, by default None
"""
self.back_end = back_end
self.fluid = fluid
self.mixture_type = None
def _not_implemented(self) -> None:
raise NotImplementedError(
f"Method is not implemented for {self.__class__.__name__}."
)
[docs]
def isentropic(self, p_1, h_1, p_2):
self._not_implemented()
def _is_below_T_critical(self, T):
self._not_implemented()
[docs]
def T_ph(self, p, h):
self._not_implemented()
[docs]
def T_ps(self, p, s):
self._not_implemented()
[docs]
def h_pT(self, p, T):
self._not_implemented()
[docs]
def h_ps(self, p, T):
self._not_implemented()
[docs]
def h_QT(self, Q, T):
self._not_implemented()
[docs]
def h_pQ(self, p, Q):
self._not_implemented()
[docs]
def s_QT(self, Q, T):
self._not_implemented()
[docs]
def T_sat(self, p):
self._not_implemented()
[docs]
def T_dew(self, p):
return self.T_sat(p)
[docs]
def T_bubble(self, p):
return self.T_sat(p)
[docs]
def p_sat(self, T):
self._not_implemented()
[docs]
def p_dew(self, T):
return self.p_sat(T)
[docs]
def p_bubble(self, T):
return self.p_sat(T)
[docs]
def Q_ph(self, p, h):
self._not_implemented()
[docs]
def phase_ph(self, p, h):
self._not_implemented()
[docs]
def d_ph(self, p, h):
self._not_implemented()
[docs]
def d_pT(self, p, T):
self._not_implemented()
[docs]
def d_QT(self, Q, T):
self._not_implemented()
[docs]
def viscosity_ph(self, p, h):
self._not_implemented()
[docs]
def viscosity_pT(self, p, T):
self._not_implemented()
[docs]
def s_ph(self, p, h):
self._not_implemented()
[docs]
def s_pT(self, p, T):
self._not_implemented()
[docs]
@wrapper_registry
class CoolPropWrapper(FluidPropertyWrapper):
def __init__(self, fluid, back_end=None) -> None:
"""Wrapper for CoolProp.CoolProp.AbstractState instance calls
Parameters
----------
fluid : str
Name of the fluid
back_end : str, optional
CoolProp back end for the AbstractState object, by default "HEOS"
"""
super().__init__(fluid, back_end)
if self.back_end is None:
self.back_end = "HEOS"
self._identify_mixture()
self.AS = SerializableAbstractState(self.back_end, self.fluid)
self._set_mixture_fractions()
self._set_constants()
def _identify_mixture(self):
"""Parse the fluid name to identify, if and what kind of mixture we are
working with
"""
if "[" in self.fluid:
if "|" not in self.fluid:
msg = (
f"The fluid {self.fluid} requires the specification of "
"mass, volume or molar based composition information."
"You can do this by appending '|' and 'mass' at the end "
"of the fluid string. For example, "
"'NAMEOFFLUID[0.5]|mass' to indicate a mass based mixture."
)
raise ValueError(msg)
self.fluid, self.mixture_type = self.fluid.split("|")
allowed = ["mass", "molar", "volume"]
if self.mixture_type not in allowed:
msg = (
"For the specification of the composition type you have "
f"to select from {', '.join(allowed)}."
)
if "&" in self.fluid:
_fluids_with_fractions = self.fluid.split("&")
else:
_fluids_with_fractions = [self.fluid]
fluid_names = []
fractions = []
for fluid in _fluids_with_fractions:
if "[" in fluid:
_fluid_name, _fraction = fluid.split("[")
_fraction = float(_fraction.replace("]", ""))
fractions += [_fraction]
else:
_fluid_name = fluid
fluid_names += [_fluid_name]
self.fractions = fractions
self.fluid = "&".join(fluid_names)
def _set_mixture_fractions(self):
"""Set the fractions for provided mixture"""
if self.mixture_type == "mass":
self.AS.set_mass_fractions(self.fractions)
elif self.mixture_type == "molar":
self.AS.set_mole_fractions(self.fractions)
elif self.mixture_type == "volume":
self.AS.set_volu_fractions(self.fractions)
def _set_constants(self):
"""Setup constants for later quick access, e.g. mixture fractions
minimum/maximum pressure/temperature and critical point properties
"""
self._T_min = self.AS.trivial_keyed_output(CP.iT_min)
self._T_max = self.AS.trivial_keyed_output(CP.iT_max)
if self.back_end == "INCOMP":
self._p_min = 1e2
self._p_max = 1e8
self._p_crit = 1e8
self._T_crit = None
self._molar_mass = 1
if self.mixture_type is not None:
try:
self._T_min = max(
self.AS.trivial_keyed_output(CP.iT_freeze),
self._T_min
)
except ValueError:
pass
else:
if self.back_end == "HEOS":
# see https://github.com/CoolProp/CoolProp/discussions/2443
self._T_max *= 1.45
if self.back_end == "REFPROP":
if self.mixture_type is not None:
self._T_min += 5
self._p_min = 1e1
else:
self._p_min = self.AS.trivial_keyed_output(CP.iP_min)
self._p_max = self.AS.trivial_keyed_output(CP.iP_max)
self._p_crit = self.AS.trivial_keyed_output(CP.iP_critical)
self._T_crit = self.AS.trivial_keyed_output(CP.iT_critical)
self._molar_mass = self.AS.trivial_keyed_output(CP.imolar_mass)
def _is_below_T_critical(self, T):
return T < self._T_crit
[docs]
def get_T_max(self, p):
if self.back_end == "INCOMP":
return self.T_sat(p)
else:
return self._T_max
[docs]
def isentropic(self, p_1, h_1, p_2):
return self.h_ps(p_2, self.s_ph(p_1, h_1))
[docs]
def T_ph(self, p, h):
self.AS.update(CP.HmassP_INPUTS, h, p)
return self.AS.T()
[docs]
def T_ps(self, p, s):
self.AS.update(CP.PSmass_INPUTS, p, s)
return self.AS.T()
[docs]
def h_pQ(self, p, Q):
self.AS.update(CP.PQ_INPUTS, p, Q)
return self.AS.hmass()
[docs]
def h_ps(self, p, s):
self.AS.update(CP.PSmass_INPUTS, p, s)
return self.AS.hmass()
[docs]
def h_pT(self, p, T):
self.AS.update(CP.PT_INPUTS, p, T)
return self.AS.hmass()
[docs]
def h_QT(self, Q, T):
self.AS.update(CP.QT_INPUTS, Q, T)
return self.AS.hmass()
[docs]
def s_QT(self, Q, T):
self.AS.update(CP.QT_INPUTS, Q, T)
return self.AS.smass()
[docs]
def T_sat(self, p):
self.AS.update(CP.PQ_INPUTS, p, 0)
return self.AS.T()
[docs]
def T_dew(self, p):
self.AS.update(CP.PQ_INPUTS, p, 1)
return self.AS.T()
[docs]
def T_bubble(self, p):
self.AS.update(CP.PQ_INPUTS, p, 0)
return self.AS.T()
[docs]
def p_sat(self, T):
self.AS.update(CP.QT_INPUTS, 0.5, T)
return self.AS.p()
[docs]
def p_dew(self, T):
self.AS.update(CP.QT_INPUTS, 1, T)
return self.AS.p()
[docs]
def p_bubble(self, T):
self.AS.update(CP.QT_INPUTS, 0, T)
return self.AS.p()
[docs]
def Q_ph(self, p, h):
self.AS.update(CP.HmassP_INPUTS, h, p)
if len(self.fractions) > 1:
return self.AS.Q()
phase = self.AS.phase()
if phase == CP.iphase_twophase:
return self.AS.Q()
elif phase == CP.iphase_liquid:
return 0
elif phase == CP.iphase_gas:
return 1
else: # all other phases - though this should be unreachable as p is sub-critical
return -1
[docs]
def phase_ph(self, p, h):
if self.back_end == "INCOMP":
return "state not recognized"
self.AS.update(CP.HmassP_INPUTS, h, p)
phase = self.AS.phase()
if phase == CP.iphase_twophase:
return "tp"
elif phase == CP.iphase_liquid:
return "l"
elif phase == CP.iphase_gas:
return "g"
elif phase == CP.iphase_supercritical_gas:
return "g"
else:
return "state not recognised"
[docs]
def d_ph(self, p, h):
self.AS.update(CP.HmassP_INPUTS, h, p)
return self.AS.rhomass()
[docs]
def d_pT(self, p, T):
self.AS.update(CP.PT_INPUTS, p, T)
return self.AS.rhomass()
[docs]
def d_QT(self, Q, T):
self.AS.update(CP.QT_INPUTS, Q, T)
return self.AS.rhomass()
[docs]
def viscosity_ph(self, p, h):
self.AS.update(CP.HmassP_INPUTS, h, p)
return self.AS.viscosity()
[docs]
def viscosity_pT(self, p, T):
self.AS.update(CP.PT_INPUTS, p, T)
return self.AS.viscosity()
[docs]
def s_ph(self, p, h):
self.AS.update(CP.HmassP_INPUTS, h, p)
return self.AS.smass()
[docs]
def s_pT(self, p, T):
self.AS.update(CP.PT_INPUTS, p, T)
return self.AS.smass()
[docs]
@wrapper_registry
class IAPWSWrapper(FluidPropertyWrapper):
def __init__(self, fluid, back_end=None) -> None:
"""Wrapper for iapws library calls
Parameters
----------
fluid : str
Name of the fluid
back_end : str, optional
CoolProp back end for the AbstractState object, by default "IF97"
"""
# avoid unncessary loading time if not used
try:
import iapws
except ModuleNotFoundError:
msg = (
"To use the iapws fluid properties you need to install "
"iapws."
)
raise ModuleNotFoundError(msg)
if back_end is None:
back_end = "IF97"
super().__init__(fluid, back_end)
if self.back_end == "IF97":
self.AS = iapws.IAPWS97
elif self.back_end == "IF95":
self.AS = iapws.IAPWS95
else:
msg = f"The specified back_end {self.back_end} is not available."
raise NotImplementedError(msg)
self._set_constants(iapws)
def _set_constants(self, iapws):
self._T_min = iapws._iapws.Tt
self._T_max = 2000
self._p_min = iapws._iapws.Pt * 1e6
self._p_max = 100e6
self._p_crit = iapws._iapws.Pc * 1e6
self._T_crit = iapws._iapws.Tc
self._molar_mass = iapws._iapws.M
def _is_below_T_critical(self, T):
return T < self._T_crit
[docs]
def isentropic(self, p_1, h_1, p_2):
return self.h_ps(p_2, self.s_ph(p_1, h_1))
[docs]
def T_ph(self, p, h):
return self.AS(h=h / 1e3, P=p / 1e6).T
[docs]
def T_ps(self, p, s):
return self.AS(s=s / 1e3, P=p / 1e6).T
[docs]
def h_pQ(self, p, Q):
return self.AS(P=p / 1e6, x=Q).h * 1e3
[docs]
def h_ps(self, p, s):
return self.AS(P=p / 1e6, s=s / 1e3).h * 1e3
[docs]
def h_pT(self, p, T):
return self.AS(P=p / 1e6, T=T).h * 1e3
[docs]
def h_QT(self, Q, T):
return self.AS(T=T, x=Q).h * 1e3
[docs]
def s_QT(self, Q, T):
return self.AS(T=T, x=Q).s * 1e3
[docs]
def T_sat(self, p):
return self.AS(P=p / 1e6, x=0).T
[docs]
def p_sat(self, T):
if T > self._T_crit:
T = self._T_crit * 0.99
return self.AS(T=T / 1e6, x=0).P * 1e6
[docs]
def Q_ph(self, p, h):
return self.AS(h=h / 1e3, P=p / 1e6).x
[docs]
def phase_ph(self, p, h):
phase = self.AS(h=h / 1e3, P=p / 1e6).phase
if phase in ["Liquid"]:
return "l"
elif phase in ["Vapour"]:
return "g"
elif phase in ["Two phases", "Saturated vapor", "Saturated liquid"]:
return "tp"
else: # to ensure consistent behaviour to CoolPropWrapper
return "phase not recognised"
[docs]
def d_ph(self, p, h):
return self.AS(h=h / 1e3, P=p / 1e6).rho
[docs]
def d_pT(self, p, T):
return self.AS(T=T, P=p / 1e6).rho
[docs]
def d_QT(self, Q, T):
return self.AS(T=T, x=Q).rho
[docs]
def viscosity_ph(self, p, h):
return self.AS(P=p / 1e6, h=h / 1e3).mu
[docs]
def viscosity_pT(self, p, T):
return self.AS(T=T, P=p / 1e6).mu
[docs]
def s_ph(self, p, h):
return self.AS(P=p / 1e6, h=h / 1e3).s * 1e3
[docs]
def s_pT(self, p, T):
return self.AS(P=p / 1e6, T=T).s * 1e3
[docs]
@wrapper_registry
class PyromatWrapper(FluidPropertyWrapper):
def __init__(self, fluid, back_end=None) -> None:
"""Wrapper for the Pyromat fluid property library
Parameters
----------
fluid : str
Name of the fluid
back_end : str, optional
CoolProp back end for the AbstractState object, by default None
"""
# avoid unnecessary loading time if not used
try:
import pyromat as pm
pm.config['unit_energy'] = "J"
pm.config['unit_pressure'] = "Pa"
pm.config['unit_molar'] = "mol"
except ModuleNotFoundError:
msg = (
"To use the pyromat fluid properties you need to install "
"pyromat."
)
raise ModuleNotFoundError(msg)
super().__init__(fluid, back_end)
self._create_AS(pm)
self._set_constants()
def _create_AS(self, pm):
self.AS = pm.get(f"{self.back_end}.{self.fluid}")
def _set_constants(self):
self._p_min, self._p_max = 100, 1000e5
self._T_crit, self._p_crit = self.AS.critical()
self._T_min, self._T_max = self.AS.Tlim()
self._molar_mass = self.AS.mw()
[docs]
def isentropic(self, p_1, h_1, p_2):
return self.h_ps(p_2, self.s_ph(p_1, h_1))
def T_ph(self, p, h):
return self.AS.T(p=p, h=h)[0]
def T_ps(self, p, s):
return self.AS.T(p=p, s=s)[0]
def h_pT(self, p, T):
return self.AS.h(p=p, T=T)[0]
[docs]
def T_ph(self, p, h):
return self.AS.T(p=p, h=h)[0]
[docs]
def T_ps(self, p, s):
return self.AS.T(p=p, s=s)[0]
[docs]
def h_pT(self, p, T):
return self.AS.h(p=p, T=T)[0]
[docs]
def h_ps(self, p, s):
return self.AS.h(p=p, s=s)[0]
[docs]
def d_ph(self, p, h):
return self.AS.d(p=p, h=h)[0]
[docs]
def d_pT(self, p, T):
return self.AS.d(p=p, T=T)[0]
[docs]
def s_ph(self, p, h):
return self.AS.s(p=p, h=h)[0]
[docs]
def s_pT(self, p, T):
if self.back_end == "ig":
self._not_implemented()
return self.AS.s(p=p, T=T)[0]
[docs]
def h_QT(self, Q, T):
if self.back_end == "ig":
self._not_implemented()
return self.AS.h(x=Q, T=T)[0]
[docs]
def h_pQ(self, p, Q):
if self.back_end == "ig":
self._not_implemented()
return self.AS.h(p=p, x=Q)[0]
[docs]
def s_QT(self, Q, T):
if self.back_end == "ig":
self._not_implemented()
return self.AS.s(x=Q, T=T)[0]
[docs]
def T_boiling(self, p):
if self.back_end == "ig":
self._not_implemented()
return self.AS.T(x=1, p=p)[0]
[docs]
def p_boiling(self, T):
if self.back_end == "ig":
self._not_implemented()
return self.AS.p(x=1, T=T)[0]
[docs]
def Q_ph(self, p, h):
if self.back_end == "ig":
self._not_implemented()
return self.AS.x(p=p, h=h)[0]
[docs]
def d_QT(self, Q, T):
if self.back_end == "ig":
self._not_implemented()
return self.AS.d(x=Q, T=T)[0]