Skip to content

Molecular SAFT (PC-SAFT)

Cubic equations of state and the gamma-phi property system both struggle with the same physics: strong, specific association (hydrogen bonding in water, alcohols, and amines), chain-length effects in long or polymeric molecules, and the wide density range between a light gas and a dense liquid. Fugacio's third class of thermodynamic method, PC-SAFT (perturbed-chain statistical associating fluid theory, Gross and Sadowski 2001), is built for exactly that regime. It models a fluid as chains of spherical segments with dispersion attraction and optional short-range association, and it plugs into the rest of the stack as a property package, SAFTPackage, like every other method.

Like the reference Helmholtz fluids, PC-SAFT is one scalar reduced residual Helmholtz energy and every property is an autodiff derivative of it, so the whole model, including the Wertheim association site-fraction solve, is differentiable in both the thermodynamic state and the molecular parameters.

Parameters

A non-associating species needs three parameters: the segment number m, the segment diameter sigma, and the dispersion energy epsilon/k. Associating species carry two more, the association energy epsilon_AB/k and volume kappa_AB, plus a Huang-Radosz site scheme: 2B, 3B, or 4C. The Wertheim term bonds only acceptor-donor site pairs, so it can't represent a self-bonding 1A scheme, and a component with an unsupported scheme raises ValueError. saft_parameters_for assembles a differentiable SaftParameters pytree straight from component names, drawing on the curated Gross and Sadowski parameter bank and filling binary corrections k_ij from the database where available.

from fugacio.thermo import saft_parameters_for

params = saft_parameters_for(["ethanol", "water"])  # both associating (2B scheme)
params.m            # segment numbers, shape (2,)
params.associating  # True: a Wertheim association term is active

You can also build a set from raw arrays with saft_parameters (pass sigma_in_angstrom=True to use the literature unit), which is what the regression layer perturbs when it fits parameters to data.

Properties

Every property is a derivative of alpha_residual(params, rho, T, x). The (T, P, x) entry points first solve P(rho, T, x) = P for the molar density on the requested phase branch, then differentiate the energy:

import jax.numpy as jnp
from fugacio.thermo import saft_parameters_for
from fugacio.thermo.saft import (
    compressibility_factor,
    ln_fugacity_coefficients,
    molar_density,
    residual_properties,
)

params = saft_parameters_for(["water"])
x = jnp.ones(1)
rho = molar_density(params, 298.15, 1e5, x, phase="liquid")   # mol/m^3
z = compressibility_factor(params, rho, 298.15, x)
ln_phi = ln_fugacity_coefficients(params, 298.15, 1e5, x, phase="liquid")
res = residual_properties(params, 298.15, 1e5, x, phase="liquid")
res.enthalpy, res.entropy, res.cp   # departure functions (J/mol, J/mol/K)

The phase argument selects the density branch: "liquid" and "vapor" seed a Newton solve from a dense packing or the ideal gas, and fall back to the other branch's root where the requested one doesn't exist (a compressed liquid has no vapor root, for example). "stable" returns the root with the lower molar Gibbs energy when more than one branch exists.

Phase equilibrium

SAFTPackage wraps a parameter set, the critical constants (used only to seed Wilson K-values), and the ideal-gas heat capacities behind the property-package interface: flash_pt, bubble_pressure / bubble_temperature, dew_pressure / dew_temperature, a tangent-plane stability test, and the energy methods. Each solve also has a checked *_with_info form. fugacio.sim.package_for(components, "pcsaft") builds one from component names; fugacio.thermo.saft_package builds one from a parameter set.

import jax.numpy as jnp
from fugacio.sim import package_for

model = package_for(["propane", "n-butane"], "pcsaft")
res = model.flash_pt(320.0, 8e5, jnp.array([0.5, 0.5]))
res.beta, res.x, res.y   # vapor fraction and phase compositions

p_bub, y = model.bubble_pressure(320.0, jnp.array([0.4, 0.6]))
# beta, p_bub, y are differentiable w.r.t. T, P, z, *and* the PC-SAFT parameters.

The value-only calls return NaN when their solve fails. A bubble or dew iteration that collapses onto one density root with identical phases is reported as TRIVIAL by the *_with_info form rather than returned as a saturation point.

For a pure component, psat_saft solves the saturation pressure by equifugacity from an initial guess (a Wilson or Antoine value is fine). It returns NaN when the solve fails, and psat_saft_with_info returns the value with its report:

from fugacio.thermo import saft_parameters_for
from fugacio.thermo.saft import psat_saft

water = saft_parameters_for(["water"])
psat = psat_saft(water, 373.15, 1.0e5)   # ~ 1 atm

Differentiable parameter regression

Because every PC-SAFT property is differentiable with respect to the molecular parameters, fitting them to data is plain gradient-based least squares straight through the saturation and bubble-point solvers, with no finite-difference parameter sweeps. fit_saft_pure regresses the pure (m, sigma, epsilon) to saturation-pressure and liquid-density data, and fit_saft_kij regresses a binary correction k_ij to isothermal bubble pressures.

import jax.numpy as jnp
from fugacio.thermo import saft_parameters_for
from fugacio.thermo.saft import fit_saft_pure

base = saft_parameters_for(["n-pentane"])
temperatures = jnp.array([280.0, 300.0, 320.0, 340.0])
psat_exp = jnp.array([36_000.0, 73_000.0, 137_000.0, 235_000.0])       # Pa
rho_liquid_exp = jnp.array([8_800.0, 8_550.0, 8_280.0, 7_980.0])       # mol/m^3
fitted, cost = fit_saft_pure(base, temperatures, psat_exp, rho_liquid_exp)

The same differentiability is what lets a SAFTPackage sit inside a flowsheet and still expose gradients of a downstream objective with respect to the thermodynamic model's own parameters.

Copilot tools

The design copilot exposes PC-SAFT through deterministic, JSON-in/JSON-out tools: saft_flash, saft_density, saft_saturation_pressure, saft_bubble_pressure, and saft_residual_enthalpy. They accept the same component names as the rest of the registry but route the calculation through the molecular EOS, the method of choice when the agent reasons about associating fluids. A failed or out-of-domain solve returns a structured error to the agent rather than an unconverged number.

Validation

PC-SAFT is checked the same way as the rest of Fugacio. A first-principles consistency suite verifies exact thermodynamic identities through autodiff: the pressure as the density derivative of the residual energy, the mole-fraction weighted log-fugacity equal to the residual Gibbs energy, the Gibbs-Helmholtz relation for the residual enthalpy, and AD-versus-finite-difference agreement for the density, saturation, flash, and Wertheim site-fraction solvers. The opt-in oracle suite (just oracles) additionally grades pressures and saturation pressures against Clapeyron.jl, an independent PC-SAFT implementation, when a Julia install is present.