Reactions & reactors¶
Fugacio models chemical reactions end to end: standard-state thermochemistry and
the equilibrium constant K(T), chemical-equilibrium composition, reaction
kinetics, ideal reactor unit operations, and reactive separations. Like the rest
of the stack everything is written in JAX, so conversions, yields, and duties are
differentiable with respect to temperature, pressure, feed, and the underlying
thermochemical / kinetic parameters.
Stoichiometry & thermochemistry¶
A Reaction is a stoichiometric vector over an ordered component list. Build one
from an equation string or from reactant/product maps:
from fugacio.thermo import Reaction, reaction_properties
components = ("nitrogen", "hydrogen", "ammonia")
rxn = Reaction.parse("nitrogen + 3 hydrogen = 2 ammonia", components)
props = reaction_properties(rxn, 298.15)
props.delta_h # standard enthalpy of reaction DH_rxn(T) (J/mol)
props.delta_g # standard Gibbs energy of reaction DG_rxn(T) (J/mol)
props.k # equilibrium constant K(T) = exp(-DG_rxn / R T)
DH_rxn, DS_rxn, DG_rxn, and K(T) follow from each component's ideal-gas
standard formation properties (hform_ig, gform_ig) corrected to temperature
with Kirchhoff's law (integrating the ideal-gas Cp correlations). The standard
state is the ideal gas at P_REF (1 bar), matching the tabulated formation data.
The component-level entry points are delta_h_rxn, delta_s_rxn, delta_g_rxn,
and equilibrium_constant.
Chemical-reaction equilibrium¶
equilibrium solves for the extents of reaction that make every reaction's
activity quotient equal to its K(T). A single reaction is solved by a robust
bracketed root; several simultaneous reactions by a damped Newton system. Both
differentiate the converged composition with respect to T, P, and the feed by
the implicit function theorem.
import jax
import jax.numpy as jnp
from fugacio.thermo import Reaction
from fugacio.thermo.reaction_equilibrium import equilibrium
components = ("nitrogen", "hydrogen", "ammonia")
rxn = Reaction.parse("nitrogen + 3 hydrogen = 2 ammonia", components)
feed = jnp.array([1.0, 3.0, 0.0])
res = equilibrium(rxn, feed, 700.0, 100e5)
res.y # equilibrium mole fractions
res.extent # extent of each reaction
# Le Chatelier, exactly: ammonia yield rises with pressure (Delta_n = -2).
jax.grad(lambda p: equilibrium(rxn, feed, 700.0, p).y[2])(100e5) # > 0
For a real gas pass basis="phi" with tc, pc, omega (and optional kij) to
use cubic-EOS fugacity coefficients in the activities instead of the ideal-gas
a_i = y_i P / P_ref.
Kinetics¶
Rate laws are differentiable pytrees, so their parameters are gradient leaves
(handy for fitting): PowerLaw (Arrhenius pre-exponential, activation energy, and
per-component orders), MassActionReversible, and LHHW (Langmuir-Hinshelwood).
The temperature dependence is the Arrhenius form (arrhenius, arrhenius_ref).
import jax.numpy as jnp
from fugacio.thermo import PowerLaw
# First-order in A: rate = A exp(-Ea/RT) * c_A
law = PowerLaw(a=jnp.asarray(1.0e7), ea=jnp.asarray(75_000.0), orders=jnp.array([1.0, 0.0]))
Reactors¶
The fugacio.sim layer turns reactions into energy-balanced unit operations on a
differentiable Stream. Every reactor accepts one or more reactions and runs
either isothermal (reporting the heat duty to hold t_out) or adiabatic
(adiabatic=True, solving for the outlet temperature). All return a
ReactorResult with outlet, duty, and extent.
| Unit | Model |
|---|---|
equilibrium_reactor |
Outlet at chemical equilibrium (Gibbs / K(T)). |
stoichiometric_reactor |
Specified extent or fractional conversion. |
cstr |
Continuous stirred tank, kinetics balanced over a volume. |
pfr |
Plug-flow tubular reactor (RK4 integration along the volume). |
batch_reactor |
Constant-volume batch over a reaction time. |
import jax.numpy as jnp
from fugacio.sim import Stream, equilibrium_reactor, cstr, conversion
from fugacio.thermo import PowerLaw, Reaction
feed = Stream.from_fractions(
("nitrogen", "hydrogen", "ammonia"),
jnp.array([0.25, 0.75, 0.0]),
flow=100.0, t=700.0, p=100e5,
)
rxn = Reaction.parse("nitrogen + 3 hydrogen = 2 ammonia", feed.components)
# Isothermal equilibrium reactor: outlet composition + the cooling duty.
eq = equilibrium_reactor(feed, rxn, t_out=700.0)
eq.outlet.z, eq.duty
# Adiabatic CSTR sized by volume, with a power-law rate:
law = PowerLaw(a=jnp.asarray(5.0e3), ea=jnp.asarray(40_000.0), orders=jnp.array([1.0, 1.0, 0.0]))
out = cstr(feed, rxn, law, volume=10.0, adiabatic=True)
conversion(feed, out.outlet, 0) # fractional N2 conversion
Reactive separations¶
When reaction and phase separation happen together, use the fugacio.sim
reactive units, which couple kinetics / chemical equilibrium to a GammaPhiModel:
reactive_flash: simultaneous chemical and vapour-liquid equilibrium in a single drum (liquid-activity reaction quotient), returning vapour/liquid products, the vapour fractionbeta, and the extents.reactive_distillation: a rate-based column that adds per-stage reaction source terms (kinetics × molar holdup) to the Wang-Henke mass balances, returning the stage profiles, products, and the netgenerationon every stage.
These make classic reaction-separation processes (e.g. esterification with in-situ water removal) tractable while staying differentiable through the coupled solve.
Validation¶
Reaction thermochemistry and equilibrium are cross-checked against
Cantera in the opt-in oracle suite
(just oracles). The oracle builds a Cantera ideal-gas phase from Fugacio's own
formation and Cp data, so DG_rxn, K(T), and the equilibrium composition agree
to (near) machine precision and any discrepancy isolates the temperature
integration or the equilibrium solver rather than a difference in reference data.