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-checkableProtocoldescribing 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 ofGCMinstances, 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
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 |
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]:
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:
Build the fuel’s group decomposition matrix, reindexed to match the table’s canonical column (group) order, via the internal
_get_decomphelper. 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).Look up the property’s group-contribution row from the table via the internal
_get_rowhelper (raisesKeyErrorif the property has no row in the table).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:
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 (fromrefGani.csv) of each compound in the mixture, in the same order asfuel.compounds. Each compound in the GCxGC data must be present in the reference tables, otherwise aValueErroris raised when theFuelis created.nannoolalDecomp: the Nannoolal group decomposition (fromrefNannoolal.csv) of each compound, used by thenannoolalmethod. Each compound must also be present in this table.compoundsData: the rows ofrefCompounds.csvmatched to the GCxGC data, including the hydrocarbonFamilyused by theboehmmethod.Cached properties such as
Tc,Pc,Tb,Cp_stp,dS_fus,RD_coeffs, andphithat evaluate the corresponding registered property withgani_gcm.predict(...)orboehm_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 fromrefCompounds.csvwhere available unlessuseRefProperties=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
Quantity1Dand the unit-aware value types returned by GCM property functions.tests/test_gcm.pyfor further usage examples, including registration, error handling, and decomposition-matrix behavior.tests/test_gcm_gani.pyandtests/test_gcm_nannoolal.pyfor comparisons of automated SMILES decompositions with the reference tables.