Fluid properties¶
The default fluid property engine CoolProp. All
available fluids can be found on their homepage. Also see [11].
Since version 0.7 of TESPy it is possible to use other engines. TESPy supports
one additional predefined engine, which can be used to fit functions to simple
custom fluid property data, the IncompressibleFluidWrapper. See
this section for more information.
On top, there are two additional predefined engines, which are untested but may serve as an inspiration for you to create your own one, i.e.
For each fluid you can specify, which library should be used, and you can easily implement your own engine, for example, if your fluid is not available through the predefined engines, or you need a different implementation of the fluid properties of a specific fluid. Further below on this page we have added an example implementation of the KKH polynomial formulation to illustrate, how you can integrate your own engine into TESPy.
CoolProp¶
CoolProp is the default fluid property back end of TESPy. It provides different back ends for different applications and allows calling mixture functions.
Available back ends¶
CoolProp provides multiple back ends for fluid property calculation. The back ends vary in calculation speed and calculation accuracy. It is possible to choose from the following back ends:
HEOS: Helmhotz Equation Of State with highest accuracy and lowest calculation speed. This is the default back end!REFPROP: Highest accuracy and highest convergence stability. This back end is not free, a separate REFPROP license is required.BICUBIC: Tabular back end with high accuracy and very high calculation speed.TTSE: Tabular back end with lowest accuracy and very high calculation speed.INCOMP: Back end for incompressible fluids.IF97: Back end for the IAPWS-IF97 of water, very accurate and much higher calculation speed thanHEOS. Due to a bug in the CoolProp back end this option is instable, for more information see the CoolProp issue #1918.
For more information on the back ends please visit the CoolProp online documentation.
Pure and pseudo-pure fluids¶
CoolProp covers the most important fluids such as water, air as a pseudo-pure fluid as well as its components, several fuels and refrigerants etc.. Look for the aliases in the list of fluids. All fluids provided in this list cover liquid and gaseous state and the two-phase region.
Incompressible fluids¶
If you are looking for heat transfer fluids, the list of incompressible fluids might be interesting for you. In contrast to the pure fluids, the properties cover liquid state only.
If you have measurement data or data from manufacturer data sheets and want to
use these in your TESPy model, TESPy can create fitting functions for these
with the IncompressibleFluidWrapper. See
this section for more information.
Fluid mixtures¶
TESPy provides support for the following built-in mixture rules:
ideal-cond: gaseous mixtures with flash calculations for water (default).ideal: gaseous mixtures without flash calculations.incompressible: mass-weighted mixtures of CoolProp incompressible fluids.humidair: humid-air mixtures via CoolProp’sHAPropsSI.forced-gas: likeideal, but forces the gas phase for water at saturation.
These mixtures are handled externally by TESPy by using the pure fluid properties of CoolProp and then applying the respective mixing rules, read more about it here. Custom mixing rules can also be registered at runtime - see the same section for details.
More accurate formulations are available directly through CoolProp, which provides a back end for predefined mixtures. This back end is rather instable when using HEOS. In general, to use the mixture feature of CoolProp we recommend using the REFPROP back end instead of HEOS. Also note, the CoolProp mixture back end is not tested thoroughly. Please reach out if you would like to support us in adopting the TESPy implementation.
IncompressibleFluidWrapper¶
You can use the IncompressibleFluidWrapper engine of TESPy to model a
fluid based on your own data. To do this, you need tabular data as function of
temperature:
Mass density
Mass specific heat capacity
Dynamic viscosity
The IncompressibleFluidWrapper will automatically fit functions to your
data:
density and heat capacity through linear interpolation: \(f\left(T\right) = A + B \cdot T\)
viscosity through an exponential polynomial equation \(\eta\left(T\right) = e ^ {\frac{A}{T ^ 3} + \frac{B}{T ^ 2} + \frac{C}{T} + D}\)
We can make use of the engine as shown in the example below. First, we set up a very simple system, just a flow of fluid through a heat exchanger.
>>> from tespy.components import Sink
>>> from tespy.components import Source
>>> from tespy.components import SimpleHeatExchanger
>>> from tespy.connections import Connection
>>> from tespy.networks import Network
>>> from tespy.tools.fluid_properties import IncompressibleFluidWrapper
>>> import numpy as np
>>> nw = Network(iterinfo=False)
>>> nw.units.set_defaults(
... temperature="°C",
... pressure="bar",
... pressure_difference="bar",
... heat="kW"
... )
>>> heatexchanger = SimpleHeatExchanger("heat exchanger")
>>> so = Source("source")
>>> si = Sink("sink")
>>> c1 = Connection(so, "out1", heatexchanger, "in1", label="c1")
>>> c2 = Connection(heatexchanger, "out1", si, "in1", label="c2")
>>> nw.add_conns(c1, c2)
Next, we have to prepare our data to be utilized by TESPy. The information required needs to be passed in SI units and must be gridded with the same spacing of temperature for all measurements.
>>> fluid_kwargs = {
... "temperature_data": np.array([273.15, 373.15]), # K
... "density_data": np.array([1000, 1100]), # kg/m3
... "heat_capacity_data": np.array([4000, 4100]) * 1e3, # J/kg
... "viscosity_data": np.array([0.05, 0.00025]), # Pa*s
... "conductivity_data": np.array([0.15, 0.13]) # W/m/K
... }
Attention
The keys of the dictionary must be named temperature_data,
density_data, heat_capacity_data and
viscosity_data!
Then, we can specify the fluid on one of our connections by providing two additional keywords:
fluid_enginesindicating which fluid uses which wrapper class.fluid_wrapper_kwargsproviding for each fluid (if applicable) the required data.
>>> c1.set_attr(
... fluid={"f": 1},
... fluid_engines={"f": IncompressibleFluidWrapper},
... fluid_wrapper_kwargs={"f": fluid_kwargs},
... )
We can specify missing boundary conditions and solve the problem.
>>> c1.set_attr(v=2, p=1, T=30)
>>> c2.set_attr(p=0.9)
>>> heatexchanger.set_attr(Q=10)
>>> nw.solve("design")
>>> nw.assert_convergence()
Using other engines¶
To use any of the other fluid property engines, you can do the following, e.g. to use the iapws back end:
>>> from tespy.components import Sink
>>> from tespy.components import Source
>>> from tespy.components import Turbine
>>> from tespy.connections import Connection
>>> from tespy.networks import Network
>>> from tespy.tools.fluid_properties.wrappers import IAPWSWrapper
>>> nwk = Network(iterinfo=False)
>>> so = Source("Source")
>>> tu = Turbine("Turbine")
>>> si = Sink("Sink")
>>> c1 = Connection(so, "out1", tu, "in1", label="1")
>>> c2 = Connection(tu, "out1", si, "in1", label="2")
>>> nwk.add_conns(c1, c2)
>>> tu.set_attr(eta_s=0.9)
>>> c1.set_attr(
... v=1, p=1e5, T=500,
... fluid={"IF97::H2O": 1}, fluid_engines={"H2O": IAPWSWrapper}
... )
>>> c2.set_attr(p=1e4)
>>> nwk.solve("design")
>>> float(round(c2.x.val, 3))
0.99
>>> tu.set_attr(eta_s=None)
>>> c2.set_attr(x=1)
>>> nwk.solve("design")
>>> float(round(tu.eta_s.val, 3))
0.841
Implementing a custom engine¶
The fluid property calls to different engines have to be masqueraded with
respective wrappers. The implementation of the wrappers for CoolProp,
iapws and pyromat can be found in the
fluid_properties.wrappers
module, and serve as example implementations for your own wrappers:
The wrapper for your own engine (or an engine from a different library) has to
inherit from the
FluidPropertyWrapper
class. Below we will use the polynomial formulation for gaseous water from
[12] as an example. First we import the necessary dependencies.
>>> import numpy as np
>>> from tespy.tools.fluid_properties.wrappers import FluidPropertyWrapper
>>> from tespy.tools.global_vars import GAS_CONSTANT_UNI
Then we set up a new class and implement the methods to calculate enthalpy and entropy from (pressure and) temperature. The structure and names of the functions have to match the pattern from the FluidPropertyWrapper, in this case h_pT. On top of that, we add a backwards function T_ph and a function to analytically calculate the heat capacity _cp_pT, the derivative of the enthalpy to the temperature. Lastly, to make the calculation of isentropic efficiencies possible, we can add the equation for change in enthalpy on isentropic change of pressure for an ideal gas.
Tip
You can also inject kwargs from the connection specification to
your concrete wrapper instance, see the section on
incompressible fluids.
# coefficients H+ S+ a b c d M
>>> COEF = {
... "H2O": [-253.871, -11.750, 34.376, 7.841, -0.423, 0.0, 18.0152],
... }
>>> class KKHWrapper(FluidPropertyWrapper):
...
... def __init__(self, fluid, back_end=None, reference_temperature=298.15, **kwargs) -> None:
... super().__init__(fluid, back_end)
...
... if self.fluid not in COEF:
... msg = "Fluid not available in KKH database"
... raise KeyError(msg)
...
... self.coefficients = COEF[fluid]
... self.h_ref = self._h_pT(None, reference_temperature)
... self._molar_mass = self.coefficients[-1] * 1e-3
... self._T_min = 100
... self._T_max = 2000
... self._p_min = 1000
... self._p_max = 10000000
... self._T_crit = 647.096
... self._p_crit = 2206400
...
... def cp_pT(self, p, T):
... y = T * 1e-3
... return 1e3 * (
... self.coefficients[2]
... + self.coefficients[3] * y
... + self.coefficients[4] / (y ** 2)
... + self.coefficients[5] * y ** 2
... ) / self.coefficients[6]
...
... def h_pT(self, p, T):
... return self._h_pT(p, T) - self.h_ref
...
... def _h_pT(self, p, T):
... y = T * 1e-3
... return 1e6 * (
... self.coefficients[0]
... + self.coefficients[2] * y
... + self.coefficients[3] / 2 * y ** 2
... - self.coefficients[4] / y
... + self.coefficients[5] / 3 * y ** 3
... ) / self.coefficients[6]
...
... def T_ph(self, p, h):
... return newton(self.h_pT, self.cp_pT, h, p)
...
... def isentropic(self, p_1, h_1, p_2):
... T_1 = self.T_ph(p_1, h_1)
... cp = self.cp_pT(p_1, T_1)
... kappa = cp / (cp - GAS_CONSTANT_UNI / self._molar_mass)
... T_2 = T_1 * (p_2 / p_1) ** ((kappa - 1) / kappa)
... return self.h_pT(p_2, T_2)
We can add a newton for the backwards function:
>>> def newton(func, deriv, h, p):
... # default valaues
... T = 300
... valmin = 70
... valmax = 3000
... max_iter = 10
... tol_rel = 1e-6
...
... # start newton loop
... expr = True
... i = 0
... while expr:
... # calculate function residual and new value
... res = h - func(p, T)
... T += res / deriv(p, T)
...
... # check for value ranges
... if T < valmin:
... T = valmin
... if T > valmax:
... T = valmax
... i += 1
...
... if i > max_iter:
... break
...
... expr = abs(res / h) >= tol_rel
...
... return T
And then we can test a call to the interface and check our results, also compare them to CoolProp.
>>> kkh_water = KKHWrapper("H2O", reference_temperature=298.15) # same as in CoolProp
>>> h = kkh_water.h_pT(1e5, 400)
>>> T = kkh_water.T_ph(1e5, h)
>>> round(T, 1)
400.0
>>> round(h)
189769
>>> from tespy.tools.fluid_properties import CoolPropWrapper
>>> coolprop_water = CoolPropWrapper("H2O")
>>> h_cp = coolprop_water.h_pT(1e5, 400)
>>> T_cp = coolprop_water.T_ph(1e5, h_cp)
>>> round(T_cp, 1)
400.0
>>> round(h)
189769
To use this wrapper in a simple TESPy model, we can then proceed as we have in the previous section:
>>> from tespy.components import Sink
>>> from tespy.components import Source
>>> from tespy.components import Turbine
>>> from tespy.connections import Connection
>>> from tespy.networks import Network
>>> nwk = Network(iterinfo=False)
>>> nwk.units.set_defaults(
... temperature="degC", pressure="MPa", pressure_difference="MPa"
... )
>>> so = Source("Source")
>>> tu = Turbine("Turbine")
>>> si = Sink("Sink")
>>> c1 = Connection(so, "out1", tu, "in1", label="1")
>>> c2 = Connection(tu, "out1", si, "in1", label="2")
>>> nwk.add_conns(c1, c2)
>>> c1.set_attr(
... m=1, p=10, T=600,
... fluid={"H2O": 1}, fluid_engines={"H2O": KKHWrapper}
... )
>>> c2.set_attr(p=1, T=400)
>>> nwk.solve("design")
>>> tu.set_attr(eta_s=0.9)
>>> c2.set_attr(T=None)
>>> nwk.solve("design")
>>> round(c2.T.val, 1)
306.3
Mixture routines in TESPy¶
Mixing rules define how pure-fluid properties are combined into mixture
properties. You select one per subnetwork via the mixing_rule argument.
ideal-cond is the default. The built-in rules are listed in the
Fluid Mixtures section above; the underlying
equations are documented in the
fluid_properties.mixtures
module.
Custom mixing rules¶
New mixing rules can be registered at runtime through the
MIXING_RULES registry.
Each property function must accept (p, T, fluid_data, **kwargs) and
return a scalar value in SI units.
>>> from tespy.tools.fluid_properties import MIXING_RULES
>>> def my_h_pT(p, T, fluid_data, **kwargs):
... h = 0
... for data in fluid_data.values():
... h += data["wrapper"].h_pT(p, T) * data["mass_fraction"]
... return h
>>> def my_s_pT(p, T, fluid_data, **kwargs):
... s = 0
... for data in fluid_data.values():
... s += data["wrapper"].s_pT(p, T) * data["mass_fraction"]
... return s
>>> MIXING_RULES.register(
... "my-rule",
... h_pT=my_h_pT,
... s_pT=my_s_pT,
... )
The T_ph_inversion and T_ps_inversion keyword arguments
(both default to True) control whether the registered h_pT
and s_pT functions are also used as Newton residuals for the
T(p, h) and T(p, s) inversions.