GERG-2004 and GERG-2008 Equations of State#

Introduction#

CoolProp provides two strict implementations of the GERG wide-range equations of state for natural gases and other mixtures: GERG2004 [181] and GERG2008 [182]. They are separate backend families, not options on the default multi-fluid backend.

“Strict” means exactly what it says:

  • only the components each model publishes are admitted,

  • only the parameters published with each model are carried — the pure-fluid equations of state, the ideal-gas coefficients, the binary reducing parameters, and the departure functions all come from the model’s own tables rather than from CoolProp’s fluid and mixture libraries,

  • the gas constant is GERG’s \(R = 8.314472\ \mathrm{J\,mol^{-1}\,K^{-1}}\), not the CODATA value CoolProp normally uses, and

  • anything the model does not cover raises an exception instead of quietly answering from somewhere else.

A number obtained from the GERG2008 backend is a GERG-2008 number: no parameter in it is borrowed from another correlation. The refusals are model-level rules enforced through the documented API, not a sandbox — see The strictness rules are model-level, not C++-level below.

Usage#

Change the backend name, exactly as for the other backends. In the high-level interface:

PropsSI("Dmolar", "T", 300, "P", 1e6, "GERG2008::METHANE[0.9]&NITROGEN[0.1]")
# -> 406.9418737 mol/m^3

and in the low-level interface:

AS = CP.AbstractState("GERG2008", "Methane&Nitrogen")
AS.set_mole_fractions([0.9, 0.1])
AS.update(CP.PT_INPUTS, 1e6, 300)
AS.rhomolar()   # -> 406.9418737 mol/m^3

Pure fluids work the same way — GERG2008::Methane is the GERG-2008 pure-methane equation of state, which is not the Setzmann-Wagner reference equation CoolProp’s HEOS::Methane uses.

GERG2004 behaves identically apart from its smaller component set and the handful of parameters GERG-2008 revised.

Components#

GERG-2004 defines 18 components; GERG-2008 keeps all 18 and adds three more, for 21. GERG-2008 also revises the pure-fluid equation of state for carbon monoxide and isopentane, so those two give different numbers under the two models.

Component names resolve through CoolProp’s normal alias and CAS machinery, so CO2, R744, CarbonDioxide and 124-38-9 all reach the same GERG component. The canonical CoolProp names are listed below.

CoolProp name

GERG-2004

GERG-2008

CAS

Methane

yes

yes

74-82-8

Nitrogen

yes

yes

7727-37-9

CarbonDioxide

yes

yes

124-38-9

Ethane

yes

yes

74-84-0

n-Propane

yes

yes

74-98-6

n-Butane

yes

yes

106-97-8

IsoButane

yes

yes

75-28-5

n-Pentane

yes

yes

109-66-0

Isopentane

yes

yes [1]

78-78-4

n-Hexane

yes

yes

110-54-3

n-Heptane

yes

yes

142-82-5

n-Octane

yes

yes

111-65-9

Hydrogen

yes

yes

1333-74-0

Oxygen

yes

yes

7782-44-7

CarbonMonoxide

yes

yes [1]

630-08-0

Water

yes

yes

7732-18-5

Helium

yes

yes

7440-59-7

Argon

yes

yes

7440-37-1

HydrogenSulfide

no

yes

7783-06-4

n-Nonane

no

yes

111-84-2

n-Decane

no

yes

124-18-5

Reducing parameters (\(\beta\), \(\gamma\)) exist for every binary pair — 153 for GERG-2004 and 210 for GERG-2008 — so no pair of components is unsupported. Fifteen of the GERG-2008 pairs additionally carry a departure function; the rest take \(F_{ij} = 0\) and contribute no departure term.

Requesting a fluid outside the model’s set raises ValueError:

AbstractState("GERG2008", "R134a")
# ValueError: [R134a] (CAS 811-97-2) is not a component of this GERG model

AbstractState("GERG2004", "n-Decane")
# ValueError: [n-Decane] is a GERG-2008 component but not a GERG-2004 component

Range of validity#

Kunz & Wagner give two ranges for the mixture model:

Range

Temperature

Pressure

Normal

90 K to 450 K

\(p \le\) 35 MPa

Extended

60 K to 700 K

\(p \le\) 70 MPa

The backend enforces the extended range, not the normal one. Inside the normal range the model’s uncertainties are the ones quoted in the publications; between the normal and the extended limits the model still evaluates but with larger (and less well characterised) uncertainty. Choosing the extended range as the enforced one means the backend does not refuse to answer questions the authors consider answerable; it does not mean every answer inside it carries the headline accuracy.

A temperature outside the enforced range raises OutOfRangeError:

AS.update(CP.PT_INPUTS, 1e6, 800)
# OutOfRangeError: Temperature [800 K] is outside the GERG range of validity [60, 700] K

Two details of the enforcement are worth knowing.

The check is on temperature only. Pressure is not checked, deliberately. The 70 MPa ceiling is an operating-envelope statement about the mixture model, whereas the underlying single-phase equation of state is legitimately evaluated at \((T, \rho)\) points whose pressure is far outside it — including inside the two-phase dome, where the single-phase equation returns a very large or negative pressure by construction. A pressure check cannot distinguish those two cases, so there is none. Like every other property-limit guard in CoolProp, the temperature check is skipped when the DONT_CHECK_PROPERTY_LIMITS configuration flag is set.

For a mixture, the enforced limits are the intersection of the per-component limits — the largest of the components’ Tmin values and the smallest of their Tmax values. Every GERG component carries the published range unchanged — Tmin = 60 K and Tmax = 700 K — so the intersection is 60–700 K for every composition. The intersection is still what the guard computes rather than a weighted average, because that is what makes the bound independent of \(\sum_i x_i\) (see the next paragraph).

Note this is not the same as what Tmin() and Tmax() report. Those are CoolProp’s generic mixture accessors, which return a mole-fraction-weighted sum of the per-component limits; the GERG range check deliberately does not use them, because that sum is not normalised by \(\sum_i x_i\) and would therefore scale the enforced bounds with the magnitude of the composition vector — a composition summing to 2 would have accepted 900 K. The enforced bounds are composition-independent; Tmin()/Tmax() are unchanged and still report the weighted values.

Density is not enforced either. Each GERG fluid carries EOS.limits.rhomax = 1e6 mol/mÂł, because EquationOfState::limits has the field and leaving it at its default would be worse; nothing in this backend checks it. Treat it as metadata, not as a guard.

Because the 60 K floor applies to every component, GERG2008::Helium at 6 K and GERG2008::Hydrogen at 34 K now raise OutOfRangeError rather than returning an unflagged extrapolation. Both are supercritical throughout 60–700 K, which has a further consequence for saturation — see Helium and hydrogen below.

What these backends deliberately do not provide#

Each of the following is a deliberate refusal, not a gap waiting to be filled in. In every case the alternative would be to return a plausible number computed from something that is not GERG, labelled as GERG.

Transport properties#

Viscosity, thermal conductivity and surface tension are not part of either GERG model. CoolProp has correlations for all three, and they are perfectly good numbers — from a different model. Returning them through a backend named GERG2008 invites misattribution, so they throw:

AS.viscosity()
# NotImplementedError: Transport properties are not part of the
# GERG-2004/GERG-2008 models. Use the HEOS backend if you want
# CoolProp's transport correlations.

AS.surface_tension()
# NotImplementedError: Surface tension is not part of the
# GERG-2004/GERG-2008 models.

If you need transport properties for a natural gas, use the HEOS backend (or REFPROP) and be explicit in your reporting that the transport numbers come from a different source than the thermodynamic ones.

Superancillaries#

GERG fluids carry no superancillary Chebyshev expansions. This one is load-bearing, because the obvious shortcut is a silent correctness bug: CoolProp’s saturation path returns a superancillary value as the answer, not as an iteration guess. Attaching CoolProp’s existing methane superancillary to a GERG methane fluid would therefore return Setzmann-Wagner saturation densities labelled GERG-2008, with no warning and no iteration to correct them.

Genuine superancillaries would have to be fitted against the 23 distinct GERG pure equations of state. That is a reasonable follow-on and purely additive, but it has not been done. Until it is, pure-fluid saturation goes through the classical ancillary-seeded VLE solver, which converges to GERG-consistent values — correct, just slower than the superancillary path.

Mutable binary interaction parameters#

set_binary_interaction_double and set_binary_interaction_string throw:

AS.set_binary_interaction_double(0, 1, "betaT", 1.0)
# ValueError: GERG binary interaction parameters are fixed by the published
# model and cannot be modified. A mixture with altered beta/gamma is not GERG.

AbstractState declares four public mutator routes to the same data — an index-keyed and a CAS-keyed overload of each of those two functions. Only the index-keyed pair is overridden here, with the ValueError above; the two CAS-keyed overloads are inherited from AbstractState, which already throws NotImplementedError for them unconditionally. So the exception type you see depends on which overload you called — both refuse, and a test pins that the inherited pair keeps refusing.

A mixture whose \(\beta\) or \(\gamma\) has been altered is not GERG, so the setters refuse rather than producing a mutant model that still answers to the name. If you want to adjust interaction parameters, that is what the HEOS backend is for.

Data GERG does not publish, derived here#

Two quantities CoolProp needs are not part of the published GERG models. Borrowing them from a different equation of state is what the rest of this page refuses to do, so where one can be derived from the GERG equation itself it is derived instead.

Acentric factor#

GERG publishes no acentric factor, but CoolProp needs one. Three separate pieces of machinery read \(\omega\) directly off the equation-of-state object: the Wilson K-factor seed used by every mixture VLE flash and phase envelope, T_DP_PengRobinson, and the SRK density estimate solver_rho_Tp starts from. None of them checks the value first, which is why leaving it at an inf sentinel used to break mixture saturation.

Borrowing CoolProp’s tabulated value for the same fluid would work numerically and would break the property this backend exists to have — that nothing in it traces back to a different equation of state. So \(\omega\) is derived from the GERG equation itself, by Pitzer’s defining relation

\[\omega = -1 - \log_{10}\!\left( \frac{p_{\mathrm{sat}}(0.7\,T_c)}{p_c} \right)\]

where \(p_{\mathrm{sat}}(0.7\,T_c)\) comes from a converged saturation solve on the GERG pure equation (not from the fitted pV ancillary, which is only ever a seed), and \(p_c\) is the pressure the equation produces at GERG’s tabulated reducing state — the same number p_critical() reports, so that the Wilson correlation reads a consistent \(p_c\), \(T_c\) and \(\omega\) off one fluid. There are 23 values for 21 components, because GERG-2008 moves the reducing parameters of carbon monoxide and isopentane.

acentric_factor() therefore returns a number for a pure GERG fluid (it used to throw NotImplementedError), and PropsSI("acentric", ...) returns it too. A mixture still throws ValueError, but that is CoolProp’s rule for every backend, not a GERG restriction.

The values agree with published acentric factors to better than 0.004 across all 23 — reassuring, but not a validation target: GERG’s reducing point is a fitted quantity rather than the true critical point, and these come from the shortened technical form rather than each fluid’s reference equation.

Helium and hydrogen need one deliberate step outside the rules. Their \(0.7\,T_c\) — 3.6367 K and 23.2330 K — is far below the enforced 60 K lower limit, so it is not a state update() lets a caller reach at all. That range is the published operating envelope of the mixture model; the pure equations themselves are perfectly well behaved there (the same VLE solver traces both down to \(0.30\,T_c\) when fitting their ancillaries). Because \(\omega\) is a definitional constant of the equation rather than a state anyone queries, it is evaluated there once, offline, and only the resulting scalar ships. No run-time guard was weakened.

The table is generated by dev/gerg/compute_acentric.py into src/Backends/GERG/GERGAcentric.h and is re-derived and diffed by dev/gerg/verify_transcription.py; the test suite additionally re-derives every row from CoolProp’s own saturation solver and checks it reproduces the committed value.

Known limitations#

These are real and current. They are stated here rather than discovered later.

Which input pairs actually work#

Warning

For a pure GERG fluid, the pressure-plus-caloric input pairs (HmolarP / HmassP, PSmolar / PSmass, PUmolar / PUmass) do not work at all. Through PropsSI this is not an exception you can catch — it is inf plus an error string.

Measured on a 4x4 grid, \(p \in \{10^5, 10^6, 10^7, 5 \times 10^7\}\) Pa and \(T \in \{200, 300, 500, 650\}\) K, round-tripping a state first obtained from PT_INPUTS on the same backend:

Backend / fluid

HmolarP

PSmolar

PUmolar

DmolarP

HEOS::Methane

16/16

16/16

16/16

16/16

GERG2008::Methane

0/16

0/16

0/16

16/16

GERG2008::Nitrogen

0/16

0/16

0/16

16/16

GERG2008::CO2

0/16

0/16

0/16

16/16

So:

h = PropsSI("Hmolar", "P", 1e6, "T", 300, "GERG2008::Methane")
PropsSI("T", "P", 1e6, "Hmolar", h, "GERG2008::Methane")
# -> inf   (and an errstring; NOT an exception)

DmolarP used to be in the same state (3/16, 2/16, 5/16 above). It failed through FlashRoutines::T_DP_PengRobinson, which reads the acentric factor; now that the backend derives one from its own equation that pair round-trips everywhere on this grid.

The three caloric pairs are blocked by two different causes, neither of which is the acentric factor:

  1. No triple-point pressure. GERG publishes no triple point, so p_triple() is the internal inf sentinel. The single-phase pressure-plus-caloric flash brackets temperature, and its gas-phase branch tests p < p_triple() to decide whether a saturation temperature exists. With p_triple() infinite that test is always true, so the bracket’s lower end drops to Tmin() = 60 K instead of the saturation temperature (149.1 K for methane at 1 MPa). At 60 K and 1 MPa methane is a compressed liquid while the solver has been told the phase is gas, and the density iteration diverges to a negative root.

  2. The bracket’s upper end is outside the model’s range. The same solver takes 1.5 * Tmax() = 1050 K as its upper bound, and this backend’s own range guard rejects any temperature above 700 K — including the internal update() calls the bracketing solver makes. Clearing cause 1 in a local experiment moves the failure straight to this one.

What does work, and is covered by the test suite: PT, DmolarT / DmassT, DmolarP / DmassP, and — for pure fluids — QT, PQ and DmolarQ.

The caloric-pair restriction affects pure fluids only. The same HmolarP flash on a GERG mixture succeeds, because the mixture code path takes a different density-guess route. Until this is fixed, obtain pure-fluid states from PT or DmolarT inputs, or use HEOS when you need a caloric input pair.

Mixture DQ and DmolarP flashes#

Mixture saturation, phase envelopes and VLE flashes work — this section used to say they did not. A QT or PQ flash on a GERG mixture and build_phase_envelope() on a GERG mixture all converge:

AS = CP.AbstractState("GERG2008", "Methane&Ethane")
AS.set_mole_fractions([0.9, 0.1])
AS.update(CP.QT_INPUTS, 0.0, 150)   # bubble point: p = 9.2408e5 Pa
AS.update(CP.QT_INPUTS, 1.0, 150)   # dew point:    p = 9.3875e4 Pa
AS.build_phase_envelope("")         # 222 points

They did not before. CoolProp’s mixture VLE machinery seeds itself with Wilson K-factors and an SRK density estimate, both of which read the acentric factor, and GERG publishes none — so the fluids carried the internal \(\omega = +\infty\) sentinel, the Wilson pressure estimate collapsed to exactly zero, and the flash failed several solver frames later with solver_rho_Tp was unable to find a solution for T=150, p=0, with guess value nan. The fix was to derive an acentric factor from GERG’s own equation rather than borrow one; see Acentric factor.

Pure-fluid saturation was never affected and is unchanged: QT, PQ and DQ inputs on GERG2008::Methane converge normally. All 23 GERG pure equations of state carry a fitted ancillary; the ones reachable through the public API are those whose saturation curve lies inside the enforced temperature range, which is 19 of the 21 GERG-2008 components — helium and hydrogen are the exceptions and throw OutOfRangeError (see below).

A mixture DQ or DmolarP flash still fails, but for an unrelated and pre-existing reason — DQ_flash not ready for mixtures / DP_flash not ready for mixtures — which fails identically on HEOS and is not a GERG limitation. See Which input pairs actually work for the pure-fluid caloric-input restriction, which has a different root cause.

Compositions with two or more exactly-zero mole fractions#

The natural way to hand over a natural-gas analysis is the full 21-name GERG-2008 component list with a mole fraction for each, most of them exactly zero. Until this release every such composition returned NaN for every property, silently. It is now fixed as far as it mathematically can be, and the boundary is worth understanding, because it is a property of the function rather than a choice about how much work to do.

Why there is a boundary at all#

The reducing function sums pairwise terms

\[f_{Y,ij}(x_i, x_j) = \frac{x_i x_j (x_i + x_j)}{\beta_{Y,ij}^2 x_i + x_j}\]

which is \(0/0\) as soon as two mole fractions are exactly zero. The numerator is cubic and the denominator linear, so \(f_{Y,ij}\) is homogeneous of degree 2:

\[f_{Y,ij}(t x_i, t x_j) = t^2 f_{Y,ij}(x_i, x_j), \qquad t > 0.\]

Differentiating that identity \(k\) times in the compositions lowers the degree by \(k\), so the \(k\)-th composition derivative is homogeneous of degree \(2 - k\). Restricted to non-negative mole fractions the denominator is bounded away from zero on the unit directions — \(\beta^2 a + b \ge \min(\beta^2, 1)\) for \(a, b \ge 0\) with \(a^2 + b^2 = 1\) — so that direction set is compact and each derivative is bounded on it by some \(M\). Writing an approach to the corner as \(t \mathbf{u}\) with \(\mathbf{u}\) a unit direction,

\[\left| \partial^k f_{Y,ij}(t \mathbf{u}) \right| \le M \, t^{\,2-k}.\]

That single inequality decides everything:

  • \(k = 0\) and \(k = 1\) — the bound vanishes as \(t \to 0\), and it does so uniformly in the direction \(\mathbf{u}\). The limit is therefore \(0\) no matter how the corner is approached, and defining the value to be \(0\) there is the unique continuous extension. It is not a convention or a fudge: the extended function is genuinely \(C^1\). Physically this is just the statement that a pair of components which are both absent contributes nothing, and that adding a little of both changes the reducing state only at second order in that amount.

  • \(k \ge 2\) — the exponent is zero or negative. A degree-zero function is constant along each ray but generally different between rays, so it has no limit at the corner and no value is correct. Measured at \(\beta = 1.3\), \(\partial^2 f / \partial x_i^2\) is \(-7.09 \times 10^{-2}\) approaching along \((1,1)\), \(-8.24 \times 10^{-1}\) along \((1,9)\) and \(-3.24 \times 10^{-4}\) along \((9,1)\) — identical at \(t = 10^{-3}\), \(10^{-5}\) and \(10^{-7}\), so this is genuine direction dependence rather than slow convergence.

So the value and the first derivatives are repairable and are repaired; the second and higher derivatives are not, and are deliberately left as NaN. Substituting any number for them would replace an honest NaN with a silently direction-dependent one.

Note

The uniform bound is what fails outside the physical domain. With \(\beta = 1\) and \(x_i = -x_j\) the denominator cancels while both mole fractions are non-zero, and the direction set is no longer bounded away from the singularity. That corner is unreachable from a valid composition and is a separate pre-existing defect, not something these guards address.

What works#

The reducing state and everything that flows from it — \(T_r\), \(\rho_r\), \(\alpha^0\), \(\alpha^r\), \(p\), \(\rho\), \(c_v\), \(c_p\), speed of sound, \(h\), \(s\), molar mass — and, since this release, the first composition derivatives and therefore fugacity() and fugacity_coefficient(). All of these are identical, to the last digit, to the same composition with the zero components trimmed away:

AS = CP.AbstractState("GERG2008", "Methane&Nitrogen&Ethane&Propane")
AS.set_mole_fractions([0.9, 0.1, 0.0, 0.0])
AS.update(CP.PT_INPUTS, 1e6, 300)
AS.rhomolar()                 # 406.942     correct
AS.fugacity_coefficient(0)    # 0.983354    correct

Five call sites contain the \(0/0\), and all five are now guarded: three helper functions — f_Y_ij itself and its two first-derivative helpers dfYkidxi__constxk / dfYikdxi__constxk — plus, added in this release, the two inlined expressions in the XN_DEPENDENT branch of dYrdxi__constxj, which duplicate those helpers rather than calling them. That last one was the reason a single trailing zero was so destructive: the inlined loop runs over every component, so with x[N-1] == 0 one further zero anywhere in the composition made the whole first derivative NaN.

What still does not work#

The second and higher composition derivatives, per the argument above. These are not reachable from the Python API; at the C++ level, with z the mole fraction vector used above,

heos->Reducing->d2Trdxidxj(z, 0, 1, XN_DEPENDENT);   // nan, and correctly so

In practice this means phase envelopes, which need \(\partial \ln \varphi_i / \partial x_j\). Both backends throw Unable to calculate at least 4 points in phase envelope; quitting on a composition with two exact zeros, and both build the envelope normally once the zero-mole-fraction components are trimmed out. PQ and QT flashes succeed with the zeros present.

If you need phase envelopes, trim the zero-mole-fraction components out of the composition rather than passing the full component list. The trimmed result is exact.

This is not GERG-specific. The reducing function is shared, so the same behaviour — before and after — applies to the default HEOS backend, and it predates these backends entirely. It is tracked as GitHub #1677.

Saturation states below the enforced Tmin#

The published range of validity, 60–700 K, is applied verbatim to every component. GERG states it for the mixture model as a whole and publishes no per-component lower limit, so the backend does not invent one.

Seven components have a fitted saturation ancillary whose low-temperature end lies below 60 K: methane (57.17 K), nitrogen (37.86 K), oxygen (46.41 K), carbon monoxide (39.86 K), argon (45.24 K), hydrogen (9.96 K) and helium (1.56 K). get_state("triple_liquid") — and, from C++, calc_Tmin_sat() / calc_pmin_sat() on the Helmholtz backend — therefore report a state that update() will refuse to evaluate:

AS = CP.AbstractState("GERG2008", "Methane")
AS.update(CP.QT_INPUTS, 0.0, 58.0)
# OutOfRangeError: Temperature [58 K] is outside the GERG range of validity [60, 700] K

The ancillary data below 60 K is real and was traced with teqp; it is simply not reachable through the public API, because the model’s own range of validity stops first.

Properties GERG does not define at all#

GERG publishes no triple point. Ttriple() returns 0, p_triple() returns inf, and get_state("triple_liquid") returns the low-temperature end of the fitted saturation curve under a name CoolProp inherited — none of them is a GERG triple point, because there is no such thing in these models. The p_triple() sentinel is not inert: it is one of the two reasons the pure-fluid caloric input pairs still fail (see Which input pairs actually work).

set_reference_stateS and set_reference_stateD are not available#

Both throw NotImplementedError on these backends:

CP.set_reference_state("GERG2008::Methane", "NBP")
# NotImplementedError: set_reference_stateS is not implemented for the
# GERG2008 backend. ...

CP.set_reference_state("GERG2008::Methane", 300.0, 1000.0, 0.0, 0.0)
# NotImplementedError: set_reference_stateD is not implemented for the
# GERG2008 backend. ...

CoolProp applies a reference-state change by writing an offset into the global fluid-library entry for the fluid, and the GERG backends do not read that library. Before this was made explicit the S call was a silent no-op — it returned without error and without effect, and did not even validate the reference-state string. The D variant, which ends in the same library write, failed in two different unhelpful ways instead: with a backend prefix it raised an internal key [GERG2008::Methane] was not found in string_to_index_map in JSONFluidLibrary, and without one it silently adjusted HEOS. See Reference state below for what to do instead.

The throw covers every spelling that resolves to a GERG family, including GERG2008Backend::, GERG2008?<options>:: and composed strings such as BICUBIC&GERG2008::. It does not cover a bare fluid name with no :: prefix: set_reference_stateS("Methane", "NBP") resolves to the default backend and adjusts the HEOS fluid library, which has no effect on a GERG state and raises no error. That is inherent to the string API — the name carries no backend information — so always pass the prefix.

The strictness rules are model-level, not C++-level#

The throws listed under What these backends deliberately do not provide cover the documented API. They are not a sandbox.

Two further routes that are reachable from a plain AbstractState — with no downcast — are also closed, and are not listed above only because they are not “properties GERG does not publish”:

  • change_EOS(i, "SRK") throws ValueError. Swapping the equation of state out from under a GERG backend leaves an object that still answers to the name GERG2008 while computing from a cubic; it was confirmed to install an SRK cubic silently before this refusal existed.

  • update_with_guesses(...) applies the same range check as update(). It is a separate virtual entry point and previously accepted \(T = 900\) K.

What remains open needs C++ that already holds a concrete backend pointer or reaches through a public data member — for example Reducing->set_binary_interaction_double(...) on the reducing-function object, direct assignment into residual_helmholtz->Excess.F[i][j], or update_DmolarT_direct(), which bypasses the range check by design because it is what the backend uses to build its own fluids. These are enumerated in GERGBackend.h and pinned by tests, so closing or reopening one shows up as a test change. The strictness rules exist to stop a plausible mistake, not a determined one.

Tabular backends wrapping GERG#

BICUBIC&GERG2008 and TTSE&GERG2008 are not supported. They do not fail loudly, which is the problem: the table build completes without error and persists a cache under ~/.CoolProp/Tables/GERG2008Backend(...), and then every lookup inside the model’s own range is rejected:

AS = CP.AbstractState("BICUBIC&GERG2008", "Methane")
AS.update(CP.PT_INPUTS, 1e6, 300)
# ValueError: inputs are not in range, p=1e+06 Pa, T=300 K

Delete the cache directory if you have created one. Use the GERG2008 backend directly.

Both halves of that — the factory succeeding, and every subsequent lookup being rejected — are pinned by a test, so if a future change makes these wrappers work, this section starts failing rather than quietly going stale.

Helium and hydrogen are supercritical-only as pure fluids#

Helium is critical at 5.1953 K and hydrogen at 33.19 K, both far below the published 60 K lower limit that every component carries. As pure fluids they are therefore supercritical across the entire valid range, their whole subcritical saturation curve lies outside it, and any saturation call raises OutOfRangeError:

AS = CP.AbstractState("GERG2008", "Helium")
AS.update(CP.QT_INPUTS, 0.0, 4.67577)
# OutOfRangeError: Temperature [4.67577 K] is outside the GERG range of
# validity [60, 700] K

That is the physics rather than a defect: at 60 K helium is supercritical. Single-phase properties work normally, and both are ordinary components in a mixture, where the mixture reducing temperature governs rather than the pure-component critical temperature. Both exist in GERG because they appear as dilute constituents of natural gas, not because GERG is a helium or hydrogen saturation model. If you need subcritical helium or hydrogen, use HEOS.

The one place this limit is deliberately stepped past is the acentric factor, which is defined at \(0.7\,T_c\) and is a constant of the equation rather than a state a caller queries. It is evaluated offline, once, and only the resulting scalar is compiled in; the run-time guard above is unchanged.

Reference state#

Warning

These backends report \(h = 0\) and \(s = 0\) for the ideal gas at 298.15 K and 101325 Pa, per component.

That is not any of CoolProp’s usual reference states (IIR, ASHRAE, NBP), and it is the ideal-gas value at that state, not the real-fluid value. The published integration constants are deliberately discarded and recomputed to satisfy it, exactly as teqp does; without that recomputation \(h\) and \(s\) would not match teqp even though \(p\), \(c_v\) and the speed of sound did.

The practical consequence: enthalpy and entropy from a GERG backend differ from the same property from ``HEOS`` by a large offset. For methane at 101325 Pa,

\(T\)

\(h_{GERG} - h_{HEOS}\)

\(s_{GERG} - s_{HEOS}\)

300 K

-14614.05 J/mol

-107.1158 J/mol/K

400 K

-14614.26 J/mol

-107.1164 J/mol/K

500 K

-14613.81 J/mol

-107.1154 J/mol/K

This is correct and it matches teqp. The offset is essentially constant — it varies by about 3 parts in \(10^5\) across the table above, which is the genuine difference between the two models’ ideal-gas and residual parts, not a reference-state effect.

Any cross-check of a GERG backend against ``HEOS`` must compare *differences* in \(h\) and \(s\) , never absolute values. Comparing absolute enthalpies will make a correct implementation look catastrophically wrong. Neither set_reference_stateS nor set_reference_stateD is available as an escape hatch here (see above) — subtract your own offset, or take a single reference point from each backend and compare everything relative to it.

Relationship to CoolProp’s default HEOS mixture model#

CoolProp’s default multi-fluid (HEOS) backend descends from the same Kunz & Wagner formulation, so it is structurally GERG-shaped. It is nevertheless a different model and gives different numbers. Two independent reasons:

Pure-fluid equations of state. This is the load-bearing one. HEOS ships the reference equation of state for each fluid — Setzmann-Wagner for methane, Span-Wagner for carbon dioxide, and so on. GERG uses its own shortened technical form, 12 to 24 terms with a largely shared exponent set. These are different equations producing different numbers.

Binary interaction parameters. All 210 GERG-2008 binary pairs are present in CoolProp’s binary-pair library, and 194 of them are the GERG values. The other 16 are later refits — 15 from Gernert’s thesis and 1 from Tkaczuk et al. (2020) — which is exactly what a general-purpose library should prefer, and exactly what a backend named GERG2008 must not use. Twelve of those 16 shift the mixture reducing temperature by more than 0.03 K at \(z = (0.35, 0.65)\); four leave \(\beta\)/\(\gamma\) unchanged but attach a different departure function.

The gas constant. GERG specifies \(R = 8.314472\ \mathrm{J\,mol^{-1}\,K^{-1}}\). Under CoolProp’s default NORMALIZE_GAS_CONSTANTS configuration the HEOS backend uses the CODATA value for mixtures, which rescales \(p\), \(\alpha^{ig}\), \(c_v\) and \(w\) by about \(1.1 \times 10^{-6}\). The GERG backends override this.

The scale of the difference is small but systematic. For 90 % methane / 10 % nitrogen at 300 K and 1 MPa:

Backend

\(\rho\) [mol/mÂł]

GERG2008

406.9418737

HEOS

406.9525552

— a relative difference of \(2.6 \times 10^{-5}\), which is small compared to a typical custody-transfer tolerance but is not numerical noise. If you need GERG numbers, ask for GERG.

Note also that this is not the same thing as REFPROP’s REFPROP_USE_GERG flag, which substitutes GERG pure-fluid equations of state inside REFPROP’s own mixture model. These backends implement GERG whole.

Implementation notes and references#

The reference implementation is teqp, specifically include/teqp/models/GERG/GERG.hpp. The coefficient tables in CoolProp’s GERG backend are transcribed from it, with a durable verification script (dev/gerg/verify_transcription.py) that checks every table family against teqp, and the test suite compares against teqp-generated reference values at relative tolerances of 1e-12 for \(\alpha^r\) and \(\alpha^{ig}\) and 1e-10 for \(p\), \(c_v\) and the speed of sound. teqp’s own values are in turn checked against the AGA8 reference implementation.

The design of record for these backends — including the rationale for every strictness rule above — is docs/superpowers/specs/2026-07-25-gerg-strict-backend-design.md in the CoolProp repository.

The models themselves are published in:

  • GERG-2004: Kunz, Klimeck, Wagner & Jaeschke, The GERG-2004 Wide-Range Equation of State for Natural Gases and Other Mixtures, GERG Technical Monograph 15, VDI Verlag, 2007 [181].

  • GERG-2008: Kunz & Wagner, J. Chem. Eng. Data 57, 3032-3091, 2012 [182].