# -*- 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
if "[" in self.fluid:
self.fluid, self._fractions = self.fluid.split("[")
self._fractions = self._fractions.replace("]", "")
else:
self._fractions = 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()
def _make_p_subcritical(self, p):
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_QT(self, Q, T):
self._not_implemented()
[docs]
def s_QT(self, Q, T):
self._not_implemented()
[docs]
def T_sat(self, p):
self._not_implemented()
[docs]
def p_sat(self, T):
self._not_implemented()
[docs]
def Q_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"
"""
if back_end is None:
back_end = "HEOS"
super().__init__(fluid, back_end)
self.AS = SerializableAbstractState(self.back_end, self.fluid)
self._set_constants()
def _set_constants(self):
self._T_min = self.AS.trivial_keyed_output(CP.iT_min)
self._T_max = self.AS.trivial_keyed_output(CP.iT_max)
try:
self._aliases = CP.CoolProp.get_aliases(self.fluid)
except RuntimeError:
self._aliases = [self.fluid]
if self.back_end == "INCOMP":
if self._fractions is not None:
# how to find if a mixture is volumetric of mass based?
try:
self.AS.set_volu_fractions([float(self._fractions)])
except ValueError:
self.AS.set_mass_fractions([float(self._fractions)])
self._p_min = 1e2
self._p_max = 1e8
self._p_crit = 1e8
self._T_crit = None
self._molar_mass = 1
try:
# how to know that we have a binary mixture?
self._T_min = self.AS.trivial_keyed_output(CP.iT_freeze)
except ValueError:
pass
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
def _make_p_subcritical(self, p):
if p > self._p_crit:
p = self._p_crit * 0.99
return p
[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):
p = self._make_p_subcritical(p)
self.AS.update(CP.PQ_INPUTS, p, 0)
return self.AS.T()
[docs]
def p_sat(self, T):
if T > self._T_crit:
T = self._T_crit * 0.99
self.AS.update(CP.QT_INPUTS, 0, T)
return self.AS.p()
[docs]
def Q_ph(self, p, h):
p = self._make_p_subcritical(p)
self.AS.update(CP.HmassP_INPUTS, h, p)
return self.AS.Q()
[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)
self._aliases = CP.CoolProp.get_aliases("H2O")
if self.fluid not in self._aliases:
msg = "The iapws wrapper only supports water as fluid."
raise ValueError(msg)
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
def _make_p_subcritical(self, p):
if p > self._p_crit:
p = self._p_crit * 0.99
return p
[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):
p = self._make_p_subcritical(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):
p = self._make_p_subcritical(p)
return self.AS(h=h / 1e3, P=p / 1e6).x
[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:
"""_summary_
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_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 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]