Property packages¶
A process simulator needs two things from its thermodynamics: what splits
(fugacities, K-values, flashes) and how much energy (enthalpy, entropy,
volume). A property package answers both. fugacio.thermo.PropertyPackage
is one object that owns phase equilibrium and energy for a set of components,
and it's the single model type that every unit operation, the rigorous column,
the two-sided heat exchanger, the reactors, and the equation-oriented engine
consume through their model argument. It's implemented for all four method
classes Fugacio carries:
| Package | Route | Energy side |
|---|---|---|
CubicPackage |
phi-phi on PR, SRK, RK, or vdW | residual functions of the cubic (Peng-Robinson is the default package) |
GammaPhiPackage |
activity-coefficient liquid (NRTL, UNIQUAC, Wilson, UNIFAC, Dortmund) with an ideal or EOS vapor | pure-liquid enthalpies plus the excess enthalpy from autodiff of (g^E) |
SAFTPackage |
PC-SAFT with Wertheim association | temperature derivatives of the residual Helmholtz energy |
HelmholtzPackage |
one pure reference fluid (IAPWS-95 water, Span-Wagner CO2, and the other vendored formulations) | the published multiparameter equation itself |
Build one¶
fugacio.sim.package_for is the constructor a flowsheet author reaches for. It
takes component names and a method keyword from fugacio.sim.METHODS:
from fugacio.sim import package_for
pr = package_for(("propane", "n-butane", "n-pentane")) # Peng-Robinson, the default
srk = package_for(("methane", "ethane"), "srk", use_database_kij=True)
nrtl = package_for(("ethanol", "water"), "nrtl") # curated binaries
unifac = package_for(("acetone", "chloroform"), "unifac") # predictive
saft = package_for(("methanol", "n-hexane"), "pcsaft")
steam = package_for(("water",), "iapws") # reference fluid
Method-specific options (kij, use_database_kij, alpha_default,
vapor="eos", poynting, phi_saturation, and so on) are forwarded to the
underlying factory; see package_for in the API reference.
An option the method doesn't accept raises TypeError. NRTL and UNIQUAC
packages raise KeyError for a pair without curated parameters unless you pass
parameter_policy="allow_ideal", which accepts zero interactions and records
that assumption in the package's evidence. The lower-level factories in
fugacio.thermo build a package without the name lookup: cubic_package,
gamma_phi_package, and saft_package from component constants and model
parameters, and helmholtz_package from a reference fluid.
Every model= argument in fugacio.sim takes a package, or None for
Peng-Robinson over the stream's components; anything else raises TypeError. A
package built by name also rejects a stream whose components differ from its
own or come in another order.
One interface¶
Every package exposes the same calls:
- Single-phase properties at ((T, P, x)) with an explicit
phase:ln_phi,enthalpy,entropy,volume,gibbs, andheat_capacity. - Phase equilibrium:
flash_pt,bubble_pressure,dew_pressure,bubble_temperature,dew_temperature,k_values, andk_seed(the method's natural initial K-value estimate, which the column solver uses). The saturation calls returnSaturationResult(value, composition), a named tuple, sop, y = pkg.bubble_pressure(t, x)unpacks. - Stability:
stability(t, p, z), the shared tangent-plane search on the package's liquid and vapor branches, returning aStabilityResult. - Two-phase-aware bulk properties, implemented once on top of the flash:
mixture_enthalpy,mixture_entropy,mixture_volume. - Energy-specified flashes, also generic:
flash_ph,flash_ps,flash_tv.
Each iterative calculation also has a checked *_with_info form
(flash_pt_with_info, bubble_pressure_with_info, dew_temperature_with_info,
flash_ph_with_info, and so on) that returns the best state and a
SolveReport. The value-only form returns NaN whenever that report fails,
including for a state outside the package's domain (in_domain), such as a
gamma-phi state in which a present component is supercritical. See the
reliability guide for the full contract.
import jax.numpy as jnp
pkg = package_for(("ethanol", "water"), "nrtl")
x = jnp.array([0.4, 0.6])
h_liq = pkg.enthalpy(350.0, 1.013e5, x, phase="liquid") # J/mol, ideal-gas reference
res = pkg.flash_pt(360.0, 1.013e5, x) # beta, x, y, K
# T and phase split at fixed P and H: about 355.9 K with 48% vapor.
solved = pkg.flash_ph_with_info(1.013e5, h_liq + 20_000.0, x, t_min=300.0, t_max=450.0)
solved.value.t, solved.value.beta, solved.report.converged
flash_ph and flash_ps search [t_min, t_max], 50 K to 1500 K by default. A
gamma-phi package is defined only below its components' critical temperatures,
so give it a bracket inside that range; a search that ends outside the domain
reports OUT_OF_DOMAIN.
The solve methods run through compiled kernels cached per method and static options, with the package itself as a dynamic argument, so a new state or new parameter values reuse the compiled program. A package whose pytree holds a non-array leaf, such as an unregistered activity-model object, runs eagerly instead.
Enthalpy and entropy are relative to the ideal-gas reference at T_REF and
P_REF from fugacio.thermo.constants, except for HelmholtzPackage, which
keeps the reference of the published formulation. Only differences are physical
and any consistent reference cancels in a balance, so don't mix packages with
different references across one energy balance (a HelmholtzPackage steam side
and a cubic process side inside one exchanger is fine, because each side is
balanced separately).
Associating mixtures on PC-SAFT¶
A SAFTPackage gives the same interface to PC-SAFT, whose
Wertheim association term handles hydrogen-bonding components such as methanol:
import jax.numpy as jnp
from fugacio.sim import package_for
saft = package_for(("methanol", "n-hexane"), "pcsaft") # methanol uses the 2B scheme
x = jnp.array([0.4, 0.6])
bubble = saft.bubble_pressure_with_info(320.0, x)
bubble.report.converged # True
p, y = bubble.value # about 77.7 kPa, incipient vapor
h = saft.enthalpy(320.0, p, x, phase="liquid") # J/mol, association included
The package carries no curated k_ij for this pair, so it's a prediction from
the pure-component parameters.
Heat of mixing for free¶
The gamma-phi package doesn't need a separate heat-of-mixing correlation. The excess enthalpy follows from the Gibbs-Helmholtz relation,
[ h^E = -R T^2 \,\frac{\partial}{\partial T}!\left(\frac{g^E}{RT}\right) = -R T^2 \sum_i x_i \,\frac{\partial \ln\gamma_i}{\partial T}, ]
and the temperature derivative is taken by automatic differentiation of the very
ln_gamma the activity model already provides. The heat of mixing is therefore
exactly consistent with the activity coefficients that decide the VLE, and it
inherits any temperature dependence the parameters carry. excess_enthalpy and
excess_entropy are exported from fugacio.thermo for direct use:
from fugacio.thermo import excess_enthalpy
h_e = excess_enthalpy(nrtl.activity, jnp.array([0.3, 0.7]), 320.0) # J/mol
Consistency checks¶
fugacio.thermo.consistency has two data-free checks that tie a package's
energy side to its equilibrium side. Each returns a relative residual that
should be zero; neither raises.
gibbs_helmholtz_residual(pkg, t, p, x, *, phase)checks (H = -T^2 \left(\partial (G/T) / \partial T\right)_P) on one branch, from the package's own enthalpy and entropy.fugacity_enthalpy_residual(pkg, t, p, x)checks (H^V - H^L = -R T^2 \, \partial / \partial T \sum_i x_i (\ln\phi_i^V - \ln\phi_i^L)), which ties the fugacity coefficients that decide the phase split to the enthalpies that close energy balances. Evaluate it where both branches exist.
Cubic, PC-SAFT, and reference-fluid packages satisfy both to about (10^{-10})
or better. A gamma-phi package built with poynting=True and
phi_saturation=True satisfies them to a few parts per million. The default
gamma-phi package, with neither correction, deviates by about 1% in
fugacity_enthalpy_residual, because its liquid fugacity and liquid enthalpy
use different reference states. When energy balances matter, build the package
with both corrections:
from fugacio.thermo.consistency import fugacity_enthalpy_residual
nrtl_hx = package_for(("ethanol", "water"), "nrtl", poynting=True, phi_saturation=True)
fugacity_enthalpy_residual(nrtl_hx, 350.0, 1.013e5, jnp.array([0.4, 0.6]))
Everything takes model=¶
The unit operations in fugacio.sim.units (flash_drum, adiabatic_flash,
heater, valve, pump, compressor, turbine, and mix), the stream
property helpers (molar_enthalpy, enthalpy_flow, vapor_density,
column_diameter_for, and the rest), the reactors, the rigorous column, the
two-sided heat_exchanger, and EOFlowsheet all accept model=. Omitting it
selects Peng-Robinson over the stream's components. Flowsheet(model=...)
audits every solved stream on that package. The liquid-liquid separators,
decanter and three_phase_flash, take a gamma-phi package as a positional
argument.
from fugacio.sim import Stream, heater
feed = Stream.from_fractions(("ethanol", "water"), jnp.array([0.1, 0.9]), 100.0, 300.0, 1.5e5)
warm = heater(feed, t_out=350.0, model=nrtl) # NRTL liquid enthalpy, heat of mixing included
warm.outlet, warm.duty
A package is a registered JAX pytree whose model parameters are leaves, so a
flowsheet built on any of them is differentiable with respect to the
thermodynamic parameters as well as the operating conditions: jax.grad of a
column duty with respect to a kij or an NRTL (\tau_{ij}) works the same way
as a gradient with respect to a reflux ratio.
64-bit arithmetic is on by default¶
Cubic-root selection, Newton iterations to (10^{-10}) residuals, and the
implicit-function-theorem gradients throughout Fugacio assume double precision.
Importing fugacio.thermo enables jax_enable_x64 automatically. Set the
environment variable FUGACIO_X64=0 before importing if you deliberately want
single precision and accept the loss of digits.