Source code for tespy.tools.fluid_properties.wrappers

# -*- 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]