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 through the same
EquilibriumModel interface as everything else.
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. 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, while "stable" returns the
root with the lower molar Gibbs energy when more than one branch exists.
Phase equilibrium¶
SAFTModel wraps a parameter set and the critical constants (used only to seed
Wilson K-values) behind the unified model interface: flash_pt,
bubble_pressure / bubble_temperature, dew_pressure / dew_temperature, and a
tangent-plane stability test. The fugacio.sim helper saft_model_for builds
one from component names.
import jax.numpy as jnp
from fugacio.sim import saft_model_for
model = saft_model_for(["propane", "n-butane"])
res = model.flash_pt(320.0, 8e5, jnp.array([0.5, 0.5]))
res.beta, res.x, res.y # vapour 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.
For a pure component, psat_saft solves the saturation pressure by equifugacity
from an initial guess (a Wilson or Antoine value is fine):
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 SAFTModel 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.
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.