Works out the outlet of conversion, equilibrium, plug-flow and stirred-tank reactors, with their heat duty.
How the precision is kept
Conversion is direct arithmetic on molar flows. The equilibrium constant is a closed form from the formation data and heat-capacity polynomials. The equilibrium composition, the adiabatic temperature, the plug-flow profile and the stirred-tank balance are found by the shared root finder, Newton iteration and Runge–Kutta integrator at the tolerances listed here, on component data of three to six digits.
Calculations under this policy
10 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.
The enthalpy change per mole of extent at a temperature, on the ideal-gas formation basis.
ΔH = Σ νᵢ H_ig,ᵢ(T)
A signed sum of formation-based enthalpies; products and reactants are large numbers of opposite sign, so the result has fewer relative digits than its terms.
process/reactor.js
lines 17 to 18
CadCraft 0.8.2
/** Reaction enthalpy J per mol of extent at T (ideal-gas formation basis). */
export function reactionDH(th, rxn, T) { const nu = nuVector(th, rxn); let h = 0; for (let i = 0; i < th.n; i++) if (nu[i]) h += nu[i] * th.hIg(i, T); return h; }
Equilibrium constant
64-bit
ln K of a reaction at a temperature from formation data and heat capacities.
ΔG⁰(T) ÷ RT from ΔH⁰₀, ΔG⁰₀ and the integrated ΔC_p polynomial; ln K = −ΔG⁰ ÷ RT
The heat-capacity polynomial is integrated analytically, so there is no numerical integration error. The integration constants are differences of large terms at 298.15 K. A component without a Gibbs energy of formation raises an error.
process/reactor.js
lines 21 to 32
CadCraft 0.8.2
/** ln K(T) of the reaction (ideal-gas standard state, 1 bar): SVN7 eq. 13.18 with the Cp polynomial integrated analytically. */
export function lnKeq(th, rxn, T) {
const nu = nuVector(th, rxn); let dA = 0, dB = 0, dC = 0, dD = 0, dH0 = 0, dG0 = 0;
for (let i = 0; i < th.n; i++) {
if (!nu[i]) continue; const c = th.c[i]; if (c.gf == null) throw new Error('no Gibbs energy of formation for ' + th.ids[i]);
dA += nu[i] * c.cp.A; dB += nu[i] * c.cp.B; dC += nu[i] * c.cp.C; dD += nu[i] * c.cp.D; dH0 += nu[i] * c.hf; dG0 += nu[i] * c.gf;
}
const J = dH0 / R - dA * T0 - dB / 2 * T0 * T0 - dC / 3 * T0 ** 3 + dD / T0;
const gT = (t) => J - dA * t * Math.log(t) - dB / 2 * t * t - dC / 6 * t ** 3 - dD / (2 * t); // (dG/R) without the integration-constant term
const Ic = (dG0 / R - gT(T0)) / T0;
return -(gT(T) + Ic * T) / T;
}
Conversion reactor
64-bit
The outlet molar flows for reactions with a given conversion of a key component.
ξ = X × n_key ÷ |ν_key|; nᵢ ← nᵢ + νᵢ ξ, one reaction after another
Direct arithmetic. Reactions are applied in list order, each on what the one before left. A flow below −1e-9 of the feed adds a note, and negative flows are then set to zero.
process/reactor.js
lines 41 to 44
CadCraft 0.8.2
const nu = nuVector(th, r), k = th.index(r.key), xi = r.X * n[k] / Math.abs(nu[k]); extents.push(xi); n = applyExtents(n, [nu], [xi]);
}
if (n.some((v) => v < -1e-9 * s.F)) notes.push('a conversion asked for more of a reactant than there is');
n = n.map((v) => Math.max(v, 0)); const F = sum(n), z = n.map((v) => v / F), P = s.P - (p.dP || 0);
Equilibrium condition
64-bit
How far a set of extents is from chemical equilibrium.
Logarithmic form, so species that almost vanish stay resolvable. Mole numbers are floored at 1e-14 of the feed before the logarithm.
process/reactor.js
lines 57 to 61
CadCraft 0.8.2
const res = (xi) => {
const n = applyExtents(n0, nus, xi), F = sum(n), lp = lnphi(n.map((v) => Math.max(v, floor))), out = [];
for (let j = 0; j < nus.length; j++) { let s2 = 0; for (let i = 0; i < n.length; i++) if (nus[j][i]) s2 += nus[j][i] * (Math.log(Math.max(n[i], floor) / F) + lp[i] + Math.log(P / P0)); out.push(s2 - lnK[j]); }
return out;
};
Extent of a single equilibrium reaction
64-bit
The extent of reaction between the limits where a species runs out.
lo = max over products of −n₀ᵢ ÷ νᵢ; hi = min over reactants of −n₀ᵢ ÷ νᵢ; root of the equilibrium condition on (lo + ε, hi − ε), ε = 1e-12 max(1, F)
A bracketed root to 1e-13. The bracket ends are moved in by an absolute 1e-12 × max(1, feed), which is not scaled to a small feed. If the function has no sign change, the end with the matching sign is taken.
process/reactor.js
lines 64 to 67
CadCraft 0.8.2
const nu = nus[0]; let lo = -Infinity, hi = Infinity;
for (let i = 0; i < nu.length; i++) { if (nu[i] > 0) lo = Math.max(lo, -n0[i] / nu[i]); else if (nu[i] < 0) hi = Math.min(hi, -n0[i] / nu[i]); }
const eps = 1e-12 * Math.max(1, Ftot0); lo = lo === -Infinity ? -1e6 * Ftot0 : lo + eps * Math.abs(lo > -Infinity ? 1 : 1); hi = hi === Infinity ? 1e6 * Ftot0 : hi - eps;
const f = (x) => res([x])[0]; const r0 = illinois(f, lo, hi, { tol: 1e-13 }); xi = [r0 == null ? (f(lo) > 0 ? lo : hi) : r0];
Adiabatic equilibrium temperature
64-bit
The outlet temperature at which the equilibrium mixture has the feed's enthalpy.
F_out H(T) − F_in H_in = 0 on [250 K, 3500 K]
A bracketed root with a tolerance of 1e-9 on the enthalpy flow in watts (absolute) or on the relative bracket. Each trial solves the equilibrium and a PT flash.
process/reactor.js
lines 83 to 84
CadCraft 0.8.2
const target = s.F * s.H, hOut = (t) => { const x = solveAt(t), nn = applyExtents(n0, nus, x); const F = sum(nn); return F * th.flashPT(t, P, nn.map((v) => v / F)).H - target; };
T = illinois(hOut, 250, 3500, { tol: 1e-9 }); if (T == null) throw new Error('equilibrium reactor: adiabatic temperature not bracketed');
Reaction rates
64-bit
The rate of each reaction from Arrhenius constants and concentrations.
Integrated by the adaptive Runge–Kutta solver with rtol 1e-10 and atol 1e-14 × feed. The local error per step is controlled; the error at the outlet is not estimated.
process/reactor.js
lines 121 to 126
CadCraft 0.8.2
const f = (v, y) => {
const n = y.slice(0, th.n), T = adiabatic ? y[th.n] : isoT, rt = rates(th, p, n, T, P, vol0), dn = new Array(th.n).fill(0); let q = 0;
for (let j = 0; j < nus.length; j++) { for (let i = 0; i < th.n; i++) dn[i] += nus[j][i] * rt[j]; q += -reactionDHPhase(th, p.reactions[j], T, P, phs) * rt[j]; }
return adiabatic ? [...dn, q / cpMix(th, n, T, P, phase)] : dn;
};
const y0 = adiabatic ? [...n0, s.T] : n0, sol = rk45(f, y0, 0, p.V, { rtol: 1e-10, atol: 1e-14 * s.F });
Stirred-tank reactor
64-bit
The outlet flows and temperature of a continuous stirred tank.
Newton with a finite-difference Jacobian to a residual of 1e-12, reached by growing the volume in 24 quadratic steps. The energy residual is divided by 1e5 × feed, so 1e-12 there allows a larger enthalpy error than on the material balances.
process/reactor.js
lines 129 to 139
CadCraft 0.8.2
const Fin = H(n0, s.T), solveV = (V, guess) => {
const res = (x) => {
const n = x.slice(0, th.n), T = adiabatic ? x[th.n] : isoT, rt = rates(th, p, n, T, P, vol0), r = [];
for (let i = 0; i < th.n; i++) { let g = 0; for (let j = 0; j < nus.length; j++) g += nus[j][i] * rt[j]; r.push((n[i] - n0[i] - V * g) / (s.F)); }
if (adiabatic) r.push((H(n, T) - Fin) / (s.F * 1e5));
return r;
};
return newtonFD(res, guess, { feasible: (x) => x.slice(0, th.n).every((v) => v >= -1e-12 * s.F) && (!adiabatic || (x[th.n] > 100 && x[th.n] < 5000)), tol: 1e-12, maxIter: 80 });
};
let g = adiabatic ? [...n0, s.T] : n0.slice(), ok = true;
const steps = 24; for (let k = 1; k <= steps; k++) { const sol = solveV(p.V * (k / steps) ** 2, g); g = sol.x; ok = sol.ok; }
In detail
Method
Extent-of-reaction formulation of chemical equilibrium (Smith, Van Ness and Abbott ch. 13, eq. 13.18, cited in the file); power-law Arrhenius kinetics; plug flow by Dormand–Prince integration over volume; stirred tank by Newton with continuation in volume.
Number types
64-bit numbers in plain arrays.
Tolerances
Single-reaction equilibrium: root to 1e-13. Several reactions: Newton residual 1e-10, 200 passes. Adiabatic or duty temperature: 1e-9. Plug flow: rtol 1e-10, atol 1e-14 × feed. Stirred tank: residual 1e-12 on balances scaled by the feed, 24 volume steps. Mole numbers are floored at 1e-14 of the feed inside logarithms.
Checked by
tools/process-test/test.mjs compares first- and second-order plug-flow and stirred-tank conversions and a water-gas-shift equilibrium with their closed forms, on the library copy of this file, which is byte-identical.
Limits
Equilibrium is gas phase with the listed independent reactions only. A conversion that asks for more reactant than there is has its negative flow set to zero and adds a note.
Approach: 64-bit, stated limit
The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.