Transport-Property Expression Language#

A viscosity or thermal-conductivity correlation can be written directly in a fluid’s JSON file as a formula, instead of being implemented in C++. A block with "type": "expression" is compiled once when the fluid loads and evaluated at every call. Many of the reference correlations shipped with CoolProp, among them xenon, krypton, nitrogen, ammonia and the refrigerants R-32 and R-245fa, are written this way, and their fluid files in dev/fluids/ are working examples.

Where a block can go#

Expression blocks are accepted in five places in a pure or pseudo-pure fluid’s TRANSPORT section:

JSON location

Contribution

viscosity.dilute

dilute-gas viscosity, Pa s

viscosity.initial_density

initial-density term, Pa s

viscosity.higher_order

residual viscosity, Pa s

conductivity.dilute

dilute-gas conductivity, W/(m K)

conductivity.residual

residual conductivity, W/(m K)

Each block returns its own contribution in base SI units, and CoolProp adds the contributions together (for conductivity, with the critical enhancement, which is not available as an expression). In particular the initial_density block returns a viscosity, not the Rainwater-Friend coefficient \(B_\eta\); if a correlation is written in terms of \(B_\eta\), the block has to multiply by the dilute viscosity and the density itself. Expression blocks can sit alongside the built-in correlation types; the xenon conductivity, for example, uses expressions for the dilute and residual terms and the built-in simplified_Olchowy_Sengers type for the critical enhancement.

When viscosity or conductivity is a list of models, only the first is loaded, so an expression block in a later entry is never compiled and its errors are never reported. Try such a block from Python (see Trying a block from Python) before relying on it.

A block also runs when the fluid is a mixture component, evaluated for that component at the mixture’s temperature and density, and when the fluid is the reference fluid of another fluid’s extended-corresponding-states model.

Block structure#

"residual": {
    "type": "expression",
    "note": "Eq. (3) and Table 3 of Velliadou et al. (2021) ...",
    "formula": "let Tr = T/Tc\nlet rhor = Dmolar/rhoc\nsum(i: (B1[i] + B2[i]*Tr)*rhor^d[i])",
    "state_variables": ["T", "Dmolar"],
    "constants": {"Tc": 289.733, "rhoc": 8400.0},
    "arrays": {
        "B1": [0.00694552, 0.00876111, -0.01199, 0.00684476, -0.00102229],
        "B2": [-7.32747e-05, -0.00268366, 0.00563598, -0.00314076, 0.000605394],
        "d": [1, 2, 3, 4, 5]
    }
}

This is the residual thermal conductivity of xenon, as shipped.

formula

The expression. See Formula syntax.

state_variables

The thermodynamic quantities the formula reads from the current state. See State variables.

constants

Named scalars, in SI units.

arrays

Named coefficient vectors, which can only be read inside a sum.

note, BibTeX

Ignored, like any other key apart from type (and hardcoded, which the loader checks before type). The formula has no comment syntax, so note is where to record the source, unit conversions, and any departure from the equation as printed.

Formula syntax#

A formula is zero or more let statements followed by one final expression, whose value is the block’s result. Statements are separated by newlines (\n inside the JSON string) or by semicolons. A newline always ends a statement, even inside parentheses, so one statement cannot be wrapped over several lines; use let to break a long formula up instead. Column numbers in error messages count from the start of the whole formula, not from the start of the line.

let L = ln(T/T_ref)
1e-3*lambda_ref*exp(sum(i: b[i]*L^p[i]))

Arithmetic. + - * / ^ and parentheses. ^ binds tightest and is right-associative, so 2^3^2 is 512, and unary minus binds looser than ^, so -2^2 is -4. Numbers are decimal literals with an optional exponent (8.4e3, .5). The decimal separator is always ., whatever the host program’s locale; hexadecimal literals and literals outside the range of a double are errors.

Functions. exp, ln, log10, sqrt, abs, sinh, cosh, tanh, sin, cos, atan, each of one argument, and pow(x, y). They follow the C++ standard library at the edges of their domains, so ln(0) gives -inf and sqrt(-1) gives nan. Nothing is guarded, so that a formula reproduces a hard-coded C++ correlation bit for bit.

Sums. sum(i: body) sums body over the coefficient arrays. Inside the body, a[i] is the i-th element of array a, and bare i is the index itself, counted from 0. The number of terms is the length of the arrays subscripted in the body, which must all be the same length. Arrays can be subscripted only by the index of the enclosing sum, and sums cannot be nested. The body has to subscript at least one non-empty array. The index can have any name, but inside the body it hides any let, state variable or constant of the same name, so use one the formula does not otherwise need.

Names. Identifiers start with a letter or underscore and are case-sensitive. A name is resolved first as a let, then as a declared state variable, then as a constant. A let may not reuse the name of a declared state variable. It may reuse the name of a constant, which then hides that constant from the rest of the formula, so it is best avoided. A let can rebind an earlier let (let x = 1; let x = x + 1). let and sum are reserved words; function names are recognized only when called, so a constant may be named exp.

State variables#

A formula can only read the quantities its block lists in state_variables, using CoolProp’s own parameter names:

Name

Quantity

T

temperature, K

P

pressure, Pa

Dmolar

molar density, mol/m3

Dmass

mass density, kg/m3

molar_mass

molar mass, kg/mol

Smolar_residual

residual molar entropy, J/(mol K)

Bvirial

second virial coefficient, m3/mol

dBvirial_dT

its temperature derivative, m3/(mol K)

The declaration exists so that a block claims only the names it uses. A correlation that never needs pressure is free to call its exponent array p, which is what most viscosity papers call it. The list is checked when the fluid loads:

  • a name the formula reads without declaring it is an error, and the message says to add it to state_variables;

  • a declared name the formula never reads is an error, since each one costs a property evaluation at every call;

  • a declared name that is also a constant or an array is an error.

Anything outside the table is refused, including CoolProp’s aliases (D, S and so on) and every other output. Two refusals are deliberate. Transport properties would re-enter the correlation being defined. The critical point and the EOS reducing state depend on configuration (with superancillaries enabled, the critical point is the numerical one), so a correlation’s reducing constants belong in constants, at the values its authors regressed against. The set is extended when a correlation needs a new quantity.

A larger example#

The krypton viscosity of Polychroniadou et al. (2022) is an entropy-scaling correlation, so its residual block reads the residual entropy and the second virial coefficient. Because a block cannot read another block’s result, it recomputes the dilute viscosity eta0 from the same coefficients, computes the total viscosity, and subtracts eta0, so that the dilute and higher_order contributions add up to the paper’s total:

let L = ln(T/T_ref)
let eta0 = eta_ref*exp(sum(i: a[i]*L^np[i]))
let NA = R/k_B
let mass = M/NA
let root_mkT = (mass*k_B*T)^0.5
let Theta2 = (Bvirial + T*dBvirial_dT)/NA
let eplus_dilute = eta0/root_mkT*Theta2^(2/3)
let splus = -Smolar_residual/R
let eplus_res = f*(exp(sum(i: d[i]*splus^dp[i])) - 1)
(Dmolar*NA)^(2/3)*root_mkT*(eplus_res + eplus_dilute)/splus^(2/3) - eta0

with "state_variables": ["T", "Bvirial", "dBvirial_dT", "Smolar_residual", "Dmolar"]. The complete block is in dev/fluids/Krypton.json.

Trying a block from Python#

CoolProp.CoolProp.Expression compiles a block from the same JSON text that goes in the fluid file and evaluates it at the state of any AbstractState, so a correlation can be checked against its paper without rebuilding CoolProp or editing a fluid file:

import json
from CoolProp import CoolProp as CP

block = {
    "formula": "let Tr = T/Tc\nlet rhor = Dmolar/rhoc\nsum(i: (B1[i] + B2[i]*Tr)*rhor^d[i])",
    "state_variables": ["T", "Dmolar"],
    "constants": {"Tc": 289.733, "rhoc": 8400.0},
    "arrays": {
        "B1": [0.00694552, 0.00876111, -0.01199, 0.00684476, -0.00102229],
        "B2": [-7.32747e-05, -0.00268366, 0.00563598, -0.00314076, 0.000605394],
        "d": [1, 2, 3, 4, 5],
    },
}
expr = CP.Expression(json.dumps(block))
print(expr.required_inputs())       # ['T', 'Dmolar']

AS = CP.AbstractState("HEOS", "Xenon")
AS.update(CP.DmassT_INPUTS, 1200.0, 300.0)
print(expr.evaluate(AS) * 1e3)      # 11.0620..., the paper's check value is 11.0621 mW/(m K)

required_inputs() lists the state variables in the order the formula first reads them. Compilation errors raise ValueError with the same diagnostic the fluid loader reports when it loads the fluid. evaluate() raises if a state variable reads back as non-finite, which usually means the AbstractState was never updated. The C++ equivalent is CoolProp::expression::ExpressionBlock in include/CoolProp/expression/ExpressionBlock.h.