Group Contribution Method Abstraction

The fuellib.gcm module provides a generic, extensible framework for registering and evaluating Group Contribution Method (GCM) property predictions. It decouples the definition of individual property formulas (such as the Constantinou-Gani correlations described in Fuel Property Prediction Model) from the Fuel class, so new methods or properties can be added without modifying fuel.py.

Core concepts

Three pieces make up the abstraction, all defined in fuellib.gcm.core:

  • PropertyProtocol: a runtime-checkable Protocol describing the required signature of a property function, (fuel: Fuel) -> Quantity1D. Any callable matching this signature can be registered as a property predictor.

  • GCM: a named collection of property functions (a single group-contribution method, e.g. "gani"). It supports registering, listing, retrieving, and evaluating property functions.

  • GCMRegistry: a process-wide registry of GCM instances, keyed by method name.

from fuellib.gcm import GCMRegistry

GCMRegistry.list_methods()
# ['boehm', 'gani', 'nannoolal']

gani = GCMRegistry.get_gcm("gani")
gani.list_property_fns()
# ['alibakhshi_phi', 'cp_b', 'cp_c', 'cp_stp', 'gf', 'hf', 'hv_stp', 'mw', ...]

gani.predict("Tc", fuel)       # Quantity1D of critical temperatures
gani.predict_all(fuel)         # {'gani': {'tc': ..., 'pc': ..., ...}}

Method and property name lookups (GCMRegistry.get_gcm, GCM.get_property, GCM.predict) are case-insensitive. Looking up an unregistered method or property raises a ValueError listing the available options.

The gani method

fuellib.gcm.gani implements the Constantinou-Gani (and extended) correlations from Fuel Property Prediction Model on top of this abstraction. On import, it registers a "gani" GCM with GCMRegistry and populates it with property functions for MW, Tc, Pc, Vc, Tb, Tm, Hf, Gf, Hv_stp, omega, Vm_stp, Cp_stp, Cp_B, Cp_C, rd_A, rd_B, rd_D, and alibakhshi_phi. Note that properties such as Tm are not the raw gani predictions: they use reference values from refCompounds.csv when available (see Reference Property Data) and fall back to gani otherwise. Group-contribution coefficients are read from fuellib/gcm/gani.csv into a module-level table indexed by property name, with one column per first- or second-order group.

fuellib.gcm.gani.decompose() builds a group decomposition from a SMILES string using RDKit SMARTS patterns. It returns a single-row pandas.DataFrame, indexed by the SMILES string, with a column for every group in the table:

from fuellib.gcm.gani import decompose

counts = decompose("CCc1ccccc1")
counts.loc[:, (counts != 0).any()]

A ValueError is raised if the SMILES cannot be parsed or an atom is not matched to a first-order group. The same functionality is available from the command line as fl-decomp-smiles (see Command-Line Interface (CLI) Tools).

Warning

Automated decompositions must be verified. A known shortcoming is indene, where the non-aromatic C=C bond is categorized as a CH group.

Table of GCM properties

Table 8 GCM properties of the i-th component in a mixture. The subscript stp denotes a standard pressure assumption.

Property

Units

Group Contributions

Units

Description

\(M_{w,i}\)

kg/mol

\(m_{w1k}\)

g/mol

Molecular weight.

\(T_{c,i}\)

K

\(t_{c1k}\), \(t_{c2j}\)

1

Critical temperature[1].

\(p_{c,i}\)

Pa

\(p_{c1k}\), \(p_{c2j}\)

bar-0.5

Critical pressure[1].

\(V_{c,i}\)

m3/mol

\(v_{c1k}\), \(v_{c2j}\)

m3/kmol

Critical volume[1].

\(T_{b,i}\)

K

\(t_{b1k}\), \(t_{b2j}\)

1

Normal boiling point[1].

\(T_{m,i}\)

K

\(t_{m1k}\), \(t_{m2j}\)

1

Normal melting point[1].

\(\Delta H_{f,i}\)

J/mol

\(h_{f1k}\), \(h_{f2j}\)

kJ/mol

Enthalpy of formation at 298 K[1].

\(\Delta G_{f,i}\)

J/mol

\(g_{f1k}\), \(g_{f2j}\)

kJ/mol

Standard Gibbs free energy at 298 K[1].

\(\Delta H_{v,\textit{stp},i}\)

J/mol

\(h_{v1k}\), \(h_{v2j}\)

kJ/mol

Enthalpy of vaporization at 298 K[1].

\(\omega_i\)

1

\(\omega_{1k}\), \(\omega_{2j}\)

1

Acentric factor[2].

\(V_{m,\textit{stp},i}\)

m3/mol

\(v_{m1k}\), \(v_{m2j}\)

m3/kmol

Liquid molar volume at 298 K[2].

\(C_{p,i}\)

J/mol/K

\(C_{pA1_k}\), \(C_{pA2_k}\),…

J/mol/K

Molar specific heat capacity[3] [4].

Each of these properties maps directly onto a registered gani property function of the same (lowercased) name (e.g. \(T_{c,i}\) -> Tc, \(\Delta H_{f,i}\) -> Hf). See Fuel Property Prediction Model for the full description of the underlying physics and additional derived correlations (density, viscosity, vapor pressure, surface tension, thermal conductivity) built on top of these GCM properties.

Equations for GCM properties

The properties of each compound in a mixture can be calculated as the sum of contributions from the first- and second-order groups that make up the compound. For a given mixture, let \(\mathbf{N}\) be an \(N_c \times N_{g_1}\) matrix that represents the number of first-order groups in each compound, where \(N_c\) is the number of compounds in the mixture and \(N_{g_1}\) is the total number of first-order groups as defined by Constantinou and Gani[1][2]. Similarly, let \(\mathbf{M}\) be an \(N_c \times N_{g_2}\) matrix that specifies the number of second-order groups in each compound, where \(N_{g_2}\) is the total number of second-order groups. The total number of groups \(N_g = N_{g_1} + N_{g_2} = 121\). The GCM properties for the i-th compound in the mixture are calculated as follows[1] [2] [4]:

\[\begin{split}M_{w,i} &= \bigg[\sum_{k = 1}^{N_{g_1}}\mathbf{N}_{ik}m_{w1k} \bigg] \times 10^{-3}, \\ T_{c,i} &= 181.28 \ln \bigg[ \sum_{k=1}^{N_{g_1}} \mathbf{N}_{ik} t_{c1k} + \sum_{j=1}^{N_{g_2}} \mathbf{M}_{ij} t_{c2j} \bigg],\\ p_{c,i} &= \Bigg( \bigg[ \sum_{k=1}^{N_{g_1}} \mathbf{N}_{ik} p_{c1k} + \sum_{j=1}^{N_{g_2}} \mathbf{M}_{ij} p_{c2j} + 0.10022\bigg]^{-2} + 1.3705\Bigg)\times 10^{5}, \\ V_{c,i} &= \Bigg( \bigg[ \sum_{k=1}^{N_{g_1}} \mathbf{N}_{ik} v_{c1k} + \sum_{j=1}^{N_{g_2}} \mathbf{M}_{ij} v_{c2j} \bigg] -0.00435 \Bigg)\times 10^{-3}, \\ T_{b,i} &= 204.359 \ln \bigg[ \sum_{k = 1}^{N_{g_1}} \mathbf{N}_{ik} t_{b1k} + \sum_{j=1}^{N_{g_2}} \mathbf{M}_{ij} t_{b2j}\bigg],\\ T_{m,i} &= 102.425 \ln \bigg[ \sum_{k = 1}^{N_{g_1}} \mathbf{N}_{ik} t_{m1k} + \sum_{j=1}^{N_{g_2}} \mathbf{M}_{ij} t_{m2j}\bigg],\\ \Delta H_{f,i} &= \Bigg( \bigg[ \sum_{k = 1}^{N_{g_1}} \mathbf{N}_{ik} h_{f1k} + \sum_{j=1}^{N_{g_2}} \mathbf{M}_{ij} h_{f2j} \bigg] + 10.835\Bigg) \times 10^3,\\ \Delta G_{f,i} &= \Bigg( \bigg[ \sum_{k = 1}^{N_{g_1}} \mathbf{N}_{ik} g_{f1k} + \sum_{j=1}^{N_{g_2}} \mathbf{M}_{ij} g_{f2j} \bigg] -14.828 \Bigg) \times 10^3,\\ \Delta H_{v,\textit{stp},i} &= \Bigg( \bigg[ \sum_{k = 1}^{N_{g_1}} \mathbf{N}_{ik} h_{v1k} + \sum_{j=1}^{N_{g_2}} \mathbf{M}_{ij} h_{v2j} \bigg] + 6.829\Bigg) \times 10^3, \\ \omega_i &= 0.4085 \bigg[ \ln \Big( \sum_{k=1}^{N_{g_1}} \mathbf{N}_{ik} \omega_{1k} + \sum_{j=1}^{N_{g_2}} \mathbf{M}_{ij} \omega_{2j} + 1.1507\Big) \bigg]^{1/0.5050}, \\ V_{m,\textit{stp},i} &= \Bigg( \bigg[ \sum_{k=1}^{N_{g_1}} \mathbf{N}_{ik} v_{m1k} + \sum_{j=1}^{N_{g_2}} \mathbf{M}_{ij} v_{m2j} \bigg] + 0.01211 \Bigg)\times 10^{-3}, \\ C_{p,i} & =\bigg[\sum_{k=1}^{N_{g_1}} \mathbf{N}_{ik} C_{pA1_k} + \sum_{j=1}^{N_{g_2}} \mathbf{M}_{ij} C_{pA2_j} -19.7779\bigg] \\ & +\bigg[\sum_{k=1}^{N_{g_1}} \mathbf{N}_{ik} C_{pB1_k} + \sum_{j=1}^{N_{g_2}} \mathbf{M}_{ij} C_{pB2_j} + 22.5981\bigg] \theta \\ & +\bigg[\sum_{k=1}^{N_{g_1}} \mathbf{N}_{ik} C_{pC1_k} + \sum_{j=1}^{N_{g_2}} \mathbf{M}_{ij} C_{pC2_j} - 10.7983\bigg] \theta^2 \\ \theta &= \frac{T - 298.15}{700}\end{split}\]

These closed-form expressions are exactly what each corresponding gani property function evaluates, using the decomposition matrices nij (\(\mathbf{N}\)) and, where second-order groups are involved, mij (\(\mathbf{M}\)) returned by _get_decomp and the coefficient rows (\(m_{w1k}\), \(t_{c1k}\), …) returned by _get_row.

Each property function follows the same pattern:

  1. Build the fuel’s group decomposition matrix, reindexed to match the table’s canonical column (group) order, via the internal _get_decomp helper. Any group present in the table but missing from the fuel’s decomposition data is treated as zero, so a fuel need not define every group as long as the ones it does define are covered by the table (partial group coverage).

  2. Look up the property’s group-contribution row from the table via the internal _get_row helper (raises KeyError if the property has no row in the table).

  3. Apply the property’s closed-form correlation and return the result as a unit-aware Quantity1D (see Units).

The boehm method

fuellib.gcm.boehm registers a "boehm" GCM that provides the fusion entropy, \(\Delta S_{\textit{fus},i}\), of each compound following Boehm (2022). It currently registers a single property, dS_fus (J/mol/K), which is used by freeze_point_boehm().

Each compound’s hydrocarbon family is read from the Family column of refCompounds.csv (available as fuel.compoundsData["Family"]). The families are n-alkane, isoalkane, alkene, monocycloalkane, dicycloalkane, tricycloalkane, alkylbenzene, cycloaromatic, and diaromatic. The fusion entropy is then a linear function of the carbon number \(n_{C,i}\), with parameters \(A_f\), \(B_f\), and \(C_{\textit{ref},f}\) for family \(f\) read from fuellib/gcm/boehm.csv:

\[\Delta S_{\textit{fus},i} = \max\big[A_f + B_f (n_{C,i} - C_{\textit{ref},f}),\, 20\big].\]

Compounds whose family is not in the table fall back to the Walden-rule value of 56.5 J/mol/K.

fuel.dS_fus   # Quantity1D in J/(mol*K), evaluated via boehm_gcm.predict("dS_fus", fuel)

The nannoolal method

fuellib.gcm.nannoolal registers a "nannoolal" GCM with the saturated liquid viscosity group contributions of Nannoolal et al.[5]. The coefficients are read from fuellib/gcm/nannoolal.csv, which has one row per first- or second-order group and columns for the group Order, matching Priority, and the dBv and Tv contributions. Two properties are registered:

  • dBv: the group-contribution sum \(\sum_k \mathbf{N}_{ik} \Delta b_k\) for the viscosity curve slope.

  • Tv: the group-contribution sum \(\sum_k \mathbf{N}_{ik} t_{v,k}\) for the viscosity reference temperature.

Both are dimensionless sums. The corrections for the number of heavy atoms and the normal boiling point are applied by dynamic_viscosity_nannoolal() (see Fuel Property Prediction Model). The decomposition matrix \(\mathbf{N}\) contains both first- and second-order groups and is read from refNannoolal.csv as nannoolalDecomp.

Some groups have no published viscosity contribution. Groups stored as 0.0 (>C=C=C<, -C#C-C#C-, C2C-CC2, and Ortho-pair(s)) trigger a UserWarning listing the affected compounds. Groups with no value (C3C-CC2 and C3C-CC3) propagate NaN to the compounds that contain them.

fuellib.gcm.nannoolal.decompose() builds a Nannoolal decomposition from a SMILES string and returns a single-row pandas.DataFrame with one column per group in nannoolal.csv:

from fuellib.gcm.nannoolal import decompose

counts = decompose("CCc1ccccc1")
counts.loc[:, (counts != 0).any()]

First-order groups are matched with RDKit SMARTS patterns in order of the Nannoolal priority, and each atom is assigned to only one first-order group. Second-order groups are additive and are counted from SMARTS matches and structural checks: the number of hydrogens (No hydrogen, One hydrogen), non-aromatic three-, four-, and five-membered rings (3/4 ring, 5 ring), and ortho, meta, or para substituent pairs on benzene rings (counted only when a single pair type is present). A ValueError is raised if the SMILES cannot be parsed or an atom is not matched to a first-order group. The same functionality is available as fl-decomp-smiles --method nannoolal (see Command-Line Interface (CLI) Tools).

from fuellib.gcm import nannoolal_gcm

nannoolal_gcm.predict("dBv", fuel)   # dimensionless Quantity1D
nannoolal_gcm.predict("Tv", fuel)    # dimensionless Quantity1D

Registering a new property

New properties can be added to an existing GCM (or a new one) with the register_property() decorator. A property function must accept a Fuel and return a Quantity1D with one value per compound in fuel.compounds:

from fuellib.gcm.gani import gani_gcm
from fuellib.utils import Units, types


@gani_gcm.register_property
def my_new_property(fuel: "Fuel") -> types.Quantity1D:
    """Predict some new property from the Gani decomposition."""
    nij = fuel.ganiDecomp.to_numpy()
    coeffs = ...  # derive coefficients, e.g. from a new table row
    value = ...  # apply the correlation
    return Units.Quantity(value, "K")

The function is registered under its (lowercased) __name__, so it immediately becomes available via gani_gcm.predict("my_new_property", fuel) and, once a corresponding cached property is added to Fuel, as an attribute of the fuel object.

A completely new method can likewise be registered with register():

from fuellib.gcm import GCMRegistry

my_gcm = GCMRegistry.register("my_method", property_fns=[])


@my_gcm.register_property
def some_property(fuel: "Fuel") -> types.Quantity1D:
    ...

Fuel integration

Fuel ties into the registry through the following members:

  • ganiDecomp: the group decomposition (from refGani.csv) of each compound in the mixture, in the same order as fuel.compounds. Each compound in the GCxGC data must be present in the reference tables, otherwise a ValueError is raised when the Fuel is created.

  • nannoolalDecomp: the Nannoolal group decomposition (from refNannoolal.csv) of each compound, used by the nannoolal method. Each compound must also be present in this table.

  • compoundsData: the rows of refCompounds.csv matched to the GCxGC data, including the hydrocarbon Family used by the boehm method.

  • Cached properties such as Tc, Pc, Tb, Cp_stp, dS_fus, RD_coeffs, and phi that evaluate the corresponding registered property with gani_gcm.predict(...) or boehm_gcm.predict(...) the first time they are accessed. Properties that can be measured (Tc, Pc, Vc, Tm, Tb, Hf_stp, Hv_stp, Vm_stp, omega) take reference values from refCompounds.csv where available unless useRefProperties=False.

fuel = Fuel("decane")

fuel.ganiDecomp      # DataFrame, one row per compound
fuel.nannoolalDecomp # DataFrame, one row per compound
fuel.compoundsData   # DataFrame of matched reference compounds
fuel.Tc              # Quantity1D of critical temperatures
fuel.dS_fus          # Quantity1D of fusion entropies (boehm)

Previously, Fuel.get_property, Fuel.gcm_properties, and Fuel.gani_decomp were used to access these values. They have been replaced by the attributes above, so the attributes and the registry stay in sync through a single predict call.

See also

  • Fuel Property Prediction Model for the underlying Constantinou-Gani correlations and their physical meaning.

  • Units for details on Quantity1D and the unit-aware value types returned by GCM property functions.

  • tests/test_gcm.py for further usage examples, including registration, error handling, and decomposition-matrix behavior.

  • tests/test_gcm_gani.py and tests/test_gcm_nannoolal.py for comparisons of automated SMILES decompositions with the reference tables.

References