Reference fluids & steam tables¶
Cubic equations of state are fast and general, but when a design lives or dies
on water, CO₂, ammonia, or a refrigerant, engineers reach for reference
equations of state, the multiparameter Helmholtz formulations behind NIST
REFPROP and CoolProp. fugacio.thermo.helmholtz implements that model class
natively in JAX: IAPWS-95 for water/steam, Span–Wagner for CO₂,
Setzmann–Wagner for methane, and the recommended formulations for 23 more
process fluids (reference_fluid_names() lists them: light hydrocarbons
through n-octane, H₂/N₂/O₂/Ar/CO, H₂S, SO₂, ammonia, ethanol, benzene, toluene,
R134a, R32, R1234yf).
One scalar function, every property, by autodiff¶
A multiparameter EOS is a single scalar field: the reduced Helmholtz energy
α(δ, τ) = α⁰(δ, τ) + αʳ(δ, τ) in reduced density and inverse reduced
temperature. Every measurable property is an algebraic combination of its
partial derivatives. Reference implementations hand-derive those derivatives
term family by term family, hundreds of lines of error-prone calculus per
fluid. Fugacio stores only the published coefficient tables and evaluates the
scalar α; every derivative is jax.grad, including the third-order ones
inside solver Jacobians:
from fugacio.thermo import reference_fluid
from fugacio.thermo.helmholtz import pressure, isobaric_heat_capacity, speed_of_sound
water = reference_fluid("water") # frozen dataclass, a JAX pytree
rho, t = 838.025 / water.molar_mass, 500.0 # mol/m^3, K
pressure(water, rho, t) # 10.0003858 MPa (IAPWS-95 check value)
isobaric_heat_capacity(water, rho, t) # J/mol/K
speed_of_sound(water, rho, t) # 1271.28 m/s
The hermetic test suite pins the IAPWS-95 release's printed α derivatives and
single-phase pressures, the IAPWS viscosity/conductivity check tables, and
textbook steam-table anchors; the opt-in oracle suite grades dense grids for
all 26 fluids against CoolProp at ~1e-9 relative, two independent
implementations of the same published equations agreeing to solver precision.
Differentiable saturation by Maxwell construction¶
Coexistence is solved as equal pressure and equal Gibbs energy in
(ln δ', ln δ'') with a damped Newton, seeded by the published ancillary
equations and wrapped in an implicit-function-theorem custom_vjp. The solved
saturation line is therefore exactly differentiable:
import jax
from fugacio.thermo import reference_fluid, saturation_state
from fugacio.thermo.helmholtz import saturation_pressure
water = reference_fluid("water")
sat = saturation_state(water, t=450.0) # p, rho', rho'', h', h'', s', s'', Δh_vap
# Clausius-Clapeyron, both sides computed independently:
dp_dt = jax.grad(lambda t: saturation_pressure(water, t))(450.0)
dv = 1.0 / sat.rho_vapor - 1.0 / sat.rho_liquid
dp_dt, sat.h_vaporization / (450.0 * dv) # agree to ~1e-12 relative
That gradient flows through the EOS coefficients too: d(psat)/d(n_k) for a
published correlation coefficient is one jax.grad away, which is what
sensitivity analysis and EOS refitting need.
Steam-table state functions¶
Process specifications arrive as (T, P), (P, h), (P, s), or a quality,
not as the (ρ, T) a Helmholtz EOS natively speaks. The state_* family
resolves them with two-phase dome handling (q, mixture properties, nan
heat capacities where they're undefined), and stays differentiable through
every embedded solve:
from fugacio.thermo import reference_fluid, state_tp, state_ph, state_ps, state_pq
water = reference_fluid("water")
inlet = state_tp(water, 723.15, 40e5) # superheated steam, auto phase pick
outlet = state_ps(water, 1e5, inlet.s) # isentropic expansion -> wet steam
outlet.two_phase, outlet.q # True, 0.9306 (turbine exhaust wetness)
The solver-backed functions are jit-compiled with the fluid as a pytree
argument: the first call per fluid pays a one-time compilation, after which
calls cost microseconds and compose with jit, vmap, and grad.
IAPWS transport with autodiff critical enhancements¶
Water carries the full IAPWS formulations for viscosity (R12-08) and thermal
conductivity (R15-11), including the critical enhancement terms that other
open transcriptions make the caller parameterize or approximate, because they
need (∂ρ/∂P)_T at the state and at 1.5 T_c. Here those are exact autodiff
compressibilities of IAPWS-95, so the scientific formulation evaluates
everywhere, on one differentiable graph:
from fugacio.thermo import water_viscosity, water_thermal_conductivity
rho = 322.0 / 0.018015268 # critical density, mol/m^3
water_viscosity(647.35, rho) # 42.96 µPa·s (IAPWS check value)
water_thermal_conductivity(647.35, rho) # 1.4438 W/m/K (enhancement peak)
Surface tension uses each fluid's recommended σ(T) correlation
(reference_surface_tension, IAPWS/Mulero family).
Steam & cooling-water utilities (fugacio.sim)¶
The simulation layer turns duties into utility balances on real IAPWS-95 water (real latent heat at the header pressure, real liquid enthalpies, real isentropic enthalpy drops), all differentiable for utility-system optimization:
from fugacio.sim import (
STEAM_LEVELS, steam_heating, cooling_water, steam_turbine,
steam_quality_after_letdown, condensate_flash_fraction,
)
steam_heating(2.5e6, pressure=STEAM_LEVELS["mp"]).mass_flow # reboiler steam, kg/s
cooling_water(3.2e6).mass_flow # condenser CW, kg/s
steam_turbine(10.0, p_in=40e5, t_in=723.15, p_out=1e5).power # W of shaft work
condensate_flash_fraction(42e5, 5e5) # 21.9 % flash steam
The copilot exposes the same capabilities as JSON tools: steam_state
(steam-table lookups by P + one of T/q/h/s), reference_fluid_state,
reference_saturation, steam_utility_requirements (physical sizing + priced
annual cost), and steam_turbine.
Provenance & regeneration¶
Coefficient tables are vendored into
fugacio/thermo/helmholtz/_data.py by scripts/gen_helmholtz.py, which
extracts and normalizes them from CoolProp's JSON fluid library (BibTeX keys of
the original publications are kept per fluid). The generator is run by hand;
the vendored data is deterministic and ships with the package, so runtime needs
no CoolProp.