Low Level Interface

For more advanced use, it can be useful to have access to lower-level internals of the CoolProp code. For simpler use, you can use the high-level interface. The primary reason why this low-level interface is useful is because it is much faster, and actually the high-level interface internally calls the low-level interface. Furthermore, the low-level-interface exclusively operates using enumerated values (integers) and floating point numbers, and uses no strings. String comparison and parsing is computationally expensive and the low-level interface allows for a very efficient execution.

At the C++ level, the code is based on the use of an AbstractState abstract base class which defines a protocol that the property backends must implement. In this way, it is very easy to extend CoolProp to connect with another completely unrelated property library, as was done for REFPROP. As long as the interface to the library can be coerced to work within the AbstractState structure, CoolProp can interface seamlessly with the library.

In order to make most effective use of the low-level interface, you should instantiate one instance of the backend for each fluid (or mixture), and then call methods within the instance. There is a certain amount of computational overhead in calling the constructor for the backend instance, so in order to minimize it, only call the constructor once, and pass around your class instance.

Warning

While the warning about the computational overhead when generating AbstractState instances is more a recommendation, it is required that you allocate as few AbstractState instances as possible when using the tabular backends (TTSE & Bicubic).

Warning

In C++, the AbstractState::factory() function returns a bare pointer to an AbstractState instance, you must be very careful with this instance to make sure it is appropriately destroyed. It is HIGHLY recommended to wrap the generated instance in a shared pointer as shown in the example. The shared pointer will take care of automatically calling the destructor of the AbstractState when needed.

Similar methodology is used in the other wrappers of the low-level interface to (mostly) generate 1-to-1 wrappers of the low-level functions to the target language. Refer to the examples for each language to see how to call the low-level interface, generate an AbstractState instance, etc.

Introduction

To begin with, an illustrative example of using the low-level interface in C++ is shown here:

#include "CoolProp.h"
#include "AbstractState.h"
#include <iostream>
#include "crossplatform_shared_ptr.h"
using namespace CoolProp;
int main()
{
    shared_ptr<AbstractState> Water(AbstractState::factory("HEOS","Water"));
    Water->update(PQ_INPUTS, 101325, 0); // SI units
    std::cout << "T: " << Water->T() << " K" << std::endl;
    std::cout << "rho': " << Water->rhomass() << " kg/m^3" << std::endl;
    std::cout << "rho': " << Water->rhomolar() << " mol/m^3" << std::endl;
    std::cout << "h': " << Water->hmass() << " J/kg" << std::endl;
    std::cout << "h': " << Water->hmolar() << " J/mol" << std::endl;
    std::cout << "s': " << Water->smass() << " J/kg/K" << std::endl;
    std::cout << "s': " << Water->smolar() << " J/mol/K" << std::endl;
    return EXIT_SUCCESS;
}

which yields the output:

T: 373.124 K
rho': 958.367 kg/m^3
rho': 53197.5 mol/m^3
h': 419058 J/kg
h': 7549.44 J/mol
s': 1306.92 J/kg/K
s': 23.5445 J/mol/K

This example demonstrates the most common application of the low-level interface. This example could also be carried out using the high-level interface with a call like:

PropsSI("T", "P", 101325, "Q", 1, "Water")

Generating Input Pairs

A listing of the input pairs for the AbstractState::update() function can be found in the source documentation at CoolProp::input_pairs. If you know the two input variables and their values, but not their order, you can use the function CoolProp::generate_update_pair() to generate the input pair.

Warning

The syntax for this function is slightly different in python since python can do multiple return arguments and C++ cannot.

A simple example of this would be

In [1]: import CoolProp

In [2]: CoolProp.CoolProp.generate_update_pair(CoolProp.iT, 300, CoolProp.iDmolar, 1e-6)
Out[2]: (11, 1e-06, 300.0)

In [3]: CoolProp.DmolarT_INPUTS
Out[3]: 11

Keyed output versus acccessor functions

The simple output functions like AbstractState::rhomolar() that are mapped to keys in CoolProp::parameters can be either obtained using the accessor function or by calling AbstractState::keyed_output(). The advantage of the keyed_output function is that you could in principle iterate over several keys, rather than having to hard-code calls to several accessor functions. For instance:

In [4]: import CoolProp

In [5]: HEOS = CoolProp.AbstractState("HEOS", "Water")

In [6]: HEOS.update(CoolProp.DmolarT_INPUTS, 1e-6, 300)

In [7]: HEOS.p()
Out[7]: 0.002494311404279685

In [8]: [HEOS.keyed_output(k) for k in [CoolProp.iP, CoolProp.iHmass, CoolProp.iHmolar]]
Out[8]: [0.002494311404279685, 2551431.0685640117, 45964.71448370705]

Things only in the low-level interface

You might reasonably ask at this point why we would want to use the low-level interface as opposed to the “simple” high-level interface. In the first example, if you wanted to calculate all these output parameters using the high-level interface, it would require several calls to the pressure-quality flash routine, which is extremely slow as it requires a complex iteration to find the phases that are in equilibrium. Furthermore, there is a lot of functionality that is only accessible through the low-level interface. Here are a few examples of things that can be done in the low-level interface that cannot be done in the high-level interface:

In [9]: import CoolProp

In [10]: HEOS = CoolProp.AbstractState("HEOS", "Water")

# Do a flash call that is a very low density state point, definitely vapor
In [11]: %timeit HEOS.update(CoolProp.DmolarT_INPUTS, 1e-6, 300)
100000 loops, best of 3: 7.92 us per loop

# Specify the phase - for some inputs (especially density-temperature), this will result in a
# more direct evaluation of the equation of state without checking the saturation boundary
In [12]: HEOS.specify_phase(CoolProp.iphase_gas)

# We try it again - a bit faster
In [13]: %timeit HEOS.update(CoolProp.DmolarT_INPUTS, 1e-6, 300)
100000 loops, best of 3: 7.31 us per loop

# Reset the specification of phase
In [14]: HEOS.specify_phase(CoolProp.iphase_unknown)

# A mixture of methane and ethane
In [15]: HEOS = CoolProp.AbstractState("HEOS", "Methane&Ethane")

# Set the mole fractions of the mixture
In [16]: HEOS.set_mole_fractions([0.2,0.8])

# Do the dewpoint calculation
In [17]: HEOS.update(CoolProp.PQ_INPUTS, 101325, 1)

# Liquid phase molar density
In [18]: HEOS.saturated_liquid_keyed_output(CoolProp.iDmolar)
Out[18]: 18274.948846938456

# Vapor phase molar density
In [19]: HEOS.saturated_vapor_keyed_output(CoolProp.iDmolar)
Out[19]: 69.503104630333

# Liquid phase mole fractions
In [20]: HEOS.mole_fractions_liquid()
Out[20]: [0.006106791215290547, 0.9938932087847094]

# Vapor phase mole fractions -
# Should be the bulk composition back since we are doing a dewpoint calculation
In [21]: HEOS.mole_fractions_vapor()
Out[21]: [0.19999999999999996, 0.7999999999999999]

Partial Derivatives

It is possible to get the partial derivatives in a very computationally efficient manner using the low-level interface, using something like (python here):

For more information, see the docs: CoolProp::AbstractState::first_partial_deriv() and CoolProp::AbstractState::second_partial_deriv()

In [22]: import CoolProp

In [23]: HEOS = CoolProp.AbstractState("HEOS", "Water")

In [24]: HEOS.update(CoolProp.PT_INPUTS, 101325, 300)

In [25]: HEOS.cpmass()
Out[25]: 4180.6357765560715

In [26]: HEOS.first_partial_deriv(CoolProp.iHmass, CoolProp.iT, CoolProp.iP)
Out[26]: 4180.6357765560715

In [27]: %timeit HEOS.first_partial_deriv(CoolProp.iHmass, CoolProp.iT, CoolProp.iP)
The slowest run took 4.85 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 787 ns per loop

# See how much faster this is?
In [28]: %timeit CoolProp.CoolProp.PropsSI('d(Hmass)/d(T)|P', 'P', 101325, 'T', 300, 'Water')
10000 loops, best of 3: 181 us per loop

In [29]: HEOS.first_partial_deriv(CoolProp.iSmass, CoolProp.iT, CoolProp.iDmass)
Out[29]: 13.767247481715131

# In the same way you can do second partial derivatives
# This is the second mixed partial of entropy with respect to density and temperature
In [30]: HEOS.second_partial_deriv(CoolProp.iSmass, CoolProp.iT, CoolProp.iDmass, CoolProp.iT, CoolProp.iDmass)
Out[30]: -0.0660604153649426

Two-Phase and Saturation Derivatives

The two-phase derivatives of Thorade [1] are implemented in the CoolProp::AbstractState::first_two_phase_deriv() function, and derivatives along the saturation curve in the functions CoolProp::AbstractState::first_saturation_deriv() and CoolProp::AbstractState::second_saturation_deriv(). Here are some examples of using these functions:

In [31]: import CoolProp

In [32]: HEOS = CoolProp.AbstractState("HEOS", "Water")

In [33]: HEOS.update(CoolProp.QT_INPUTS, 0, 300)

# First saturation derivative calculated analytically
In [34]: HEOS.first_saturation_deriv(CoolProp.iP, CoolProp.iT)
Out[34]: 207.90345997878606

In [35]: HEOS.update(CoolProp.QT_INPUTS, 0, 300 + 0.001); p2 = HEOS.p()

In [36]: HEOS.update(CoolProp.QT_INPUTS, 0, 300 - 0.001); p1 = HEOS.p()

# First saturation derivative calculated numerically
In [37]: (p2-p1)/(2*0.001)
Out[37]: 207.92542935896563

In [38]: HEOS.update(CoolProp.QT_INPUTS, 0.1, 300)

# The d(Dmass)/d(Hmass)|P two-phase derivative
In [39]: HEOS.first_two_phase_deriv(CoolProp.iDmass, CoolProp.iHmass, CoolProp.iP)
Out[39]: -1.0494114658359546e-06

# The d(Dmass)/d(Hmass)|P two-phase derivative using splines
In [40]: HEOS.first_two_phase_deriv_splined(CoolProp.iDmass, CoolProp.iHmass, CoolProp.iP, 0.3)
Out[40]: -0.0018169665165181468

An example of plotting these derivatives is here:

(Source code, png, .pdf)

../_images/LowLevelAPI-1.png

Reference States

To begin with, you should read the high-level docs about the reference state. Those docs are also applicable to the low-level interface.

Warning

As with the high-level interface, calling set_reference_stateS (or set_reference_state in python) should be called right at the beginning of your code, and not changed again later on.

Importantly, once an AbstractState-derived instance has been generated from the factory function, it DOES NOT pick up the change in the reference state. This is intentional, but you should watch out for this behavior.

Here is an example showing how to change the reference state and demonstrating the potential issues

In [41]: import CoolProp as CP

# This one doesn't see the change in reference state
In [42]: AS1 = CoolProp.AbstractState('HEOS','n-Propane');

In [43]: AS1.update(CoolProp.QT_INPUTS, 0, 233.15);

In [44]: CoolProp.CoolProp.set_reference_state('n-Propane','ASHRAE')

# This one gets the update in the reference state
In [45]: AS2 = CoolProp.AbstractState('HEOS','n-Propane');

In [46]: AS2.update(CoolProp.QT_INPUTS, 0, 233.15);

# Note how the AS1 has its default value (change in reference state is not seen)
# and AS2 does see the new reference state
In [47]: print AS1.hmass(), AS2.hmass()
105123.272138 2.92843859384e-11

# Back to the original value
In [48]: CoolProp.CoolProp.set_reference_state('n-Propane','DEF')

Low-level interface using REFPROP

If you have the REFPROP library installed, you can call REFPROP in the same way that you call CoolProp, but with REFPROP as the backend instead of HEOS. For instance, as in python:

In [49]: import CoolProp

In [50]: REFPROP = CoolProp.AbstractState("REFPROP", "Water")

In [51]: REFPROP.update(CoolProp.DmolarT_INPUTS, 1e-6, 300)

In [52]: REFPROP.p(), REFPROP.hmass(), REFPROP.hmolar()
Out[52]: (0.0024943114072761, 2551431.0695983223, 45964.71450234043)

In [53]: [REFPROP.keyed_output(k) for k in [CoolProp.iP, CoolProp.iHmass, CoolProp.iHmolar]]
Out[53]: [0.0024943114072761, 2551431.0695983223, 45964.71450234043]

Access from High-Level Interface

For languages that cannot directly instantiate C++ classes or their wrappers but can call a DLL (Excel, FORTRAN, Julia, etc. ) an interface layer has been developed. These functions can be found in CoolPropLib.h, and all these function names start with AbstractState. A somewhat limited subset of the functionality has been implemented, if more functionality is desired, please open an issue at https://github.com/CoolProp/CoolProp/issues. Essentially, this interface generates AbstractState pointers that are managed internally, and the interface allows you to call the methods of the low-level instances.

Here is an example of the shared library usage with Julia wrapper:

julia> import CoolProp

julia> PT_INPUTS = CoolProp.get_input_pair_index("PT_INPUTS")
7

julia> cpmass = CoolProp.get_param_index("C")
34

julia> handle = CoolProp.AbstractState_factory("HEOS", "Water")
0

julia> CoolProp.AbstractState_update(handle,PT_INPUTS,101325, 300)

julia> CoolProp.AbstractState_keyed_output(handle,cpmass)
4180.635776569655

julia> CoolProp.AbstractState_free(handle)

julia> handle = CoolProp.AbstractState_factory("HEOS", "Water&Ethanol")
1

julia> PQ_INPUTS = CoolProp.get_input_pair_index("PQ_INPUTS")
2

julia> T = CoolProp.get_param_index("T")
18

julia> CoolProp.AbstractState_set_fractions(handle, [0.4, 0.6])

julia> CoolProp.AbstractState_update(handle,PQ_INPUTS,101325, 0)

julia> CoolProp.AbstractState_keyed_output(handle,T)
352.3522142890429

julia> CoolProp.AbstractState_free(handle)

The call to AbstractState_free is not strictly needed as all managed AbstractState instances will auto-deallocate. But if you are generating thousands or millions of AbstractState instances in this fashion, you might want to tidy up periodically.

Here is a further example in C++:

#include "CoolPropLib.h"
#include "CoolPropTools.h"
#include <vector>
#include <time.h>

int main(){
    double t1, t2;
    const long buffersize = 500;
    long errcode = 0;
    char buffer[buffersize];
    long handle = AbstractState_factory("BICUBIC&HEOS","Water", &errcode, buffer, buffersize);
    long _HmassP = get_input_pair_index("HmassP_INPUTS");
    long _Dmass = get_param_index("Dmass");
    long len = 20000;
    std::vector<double> h = linspace(700000.0, 1500000.0, len);
    std::vector<double> p = linspace(2.8e6, 3.0e6, len);
    double summer = 0;
    t1 = clock();
    for (long i = 0; i < len; ++i){
        AbstractState_update(handle, _HmassP, h[i], p[i], &errcode, buffer, buffersize);
        summer += AbstractState_keyed_output(handle, _Dmass, &errcode, buffer, buffersize);
    }
    t2 = clock();
    std::cout << format("value(all): %0.13g, %g us/call\n", summer, ((double)(t2-t1))/CLOCKS_PER_SEC/double(len)*1e6);
    return EXIT_SUCCESS;
}

which yields the output:

value(all): 8339004.432514, 0.8 us/call

Here is a further example in C++ that shows how to obtain many output variables at the same time, either 5 common outputs, or up to 5 user-selected vectors:

#include "CoolPropLib.h"
#include "CoolPropTools.h"
#include <vector>
#include <time.h>

int main(){
    const long buffer_size = 1000, length = 100000;
    long ierr;
    char herr[buffer_size];
    long handle = AbstractState_factory("BICUBIC&HEOS", "Water", &ierr, herr, buffer_size);
    std::vector<double> T(length), p(length), rhomolar(length), hmolar(length), smolar(length);
    std::vector<double> input1 = linspace(700000.0, 1500000.0, length);
    std::vector<double> input2 = linspace(2.8e6, 3.0e6, length);
    long input_pair = get_input_pair_index("HmassP_INPUTS");
    double t1 = clock();
    AbstractState_update_and_common_out(handle, input_pair, &(input1[0]), &(input2[0]), length, 
                                        &(T[0]), &(p[0]), &(rhomolar[0]), &(hmolar[0]), &(smolar[0]), 
                                        &ierr, herr, buffer_size);
    double t2 = clock();
    std::cout << format("value(commons): %g us/call\n", ((double)(t2-t1))/CLOCKS_PER_SEC/double(length)*1e6);

    std::vector<long> outputs(5);
    outputs[0] = get_param_index("T");
    outputs[1] = get_param_index("P");
    outputs[2] = get_param_index("Dmolar");
    outputs[3] = get_param_index("Hmolar");
    outputs[4] = get_param_index("Smolar");
    std::vector<double> out1(length), out2(length), out3(length), out4(length), out5(length);
    t1 = clock();
    AbstractState_update_and_5_out(handle, input_pair, &(input1[0]), &(input2[0]), length,
        &(outputs[0]), &(out1[0]), &(out2[0]), &(out3[0]), &(out4[0]), &(out5[0]),
        &ierr, herr, buffer_size);
    t2 = clock();
    std::cout << format("value(user-specified): %g us/call\n", ((double)(t2-t1))/CLOCKS_PER_SEC/double(length)*1e6);
}

which yields the output:

value(commons): 0.78 us/call
value(user-specified): 0.78 us/call