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 |
|---|---|
|
dilute-gas viscosity, Pa s |
|
initial-density term, Pa s |
|
residual viscosity, Pa s |
|
dilute-gas conductivity, W/(m K) |
|
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.
formulaThe expression. See Formula syntax.
state_variablesThe thermodynamic quantities the formula reads from the current state. See State variables.
constantsNamed scalars, in SI units.
arraysNamed coefficient vectors, which can only be read inside a
sum.note,BibTeXIgnored, like any other key apart from
type(andhardcoded, which the loader checks beforetype). The formula has no comment syntax, sonoteis 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 |
|---|---|
|
temperature, K |
|
pressure, Pa |
|
molar density, mol/m3 |
|
mass density, kg/m3 |
|
molar mass, kg/mol |
|
residual molar entropy, J/(mol K) |
|
second virial coefficient, m3/mol |
|
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.