Resources Numerical accuracy Calculation policy

Reactors

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.

process/reactor.js CadCraft 0.8.2 64-bit 10 calculations Reviewed

Tests tools/process-test/test.mjs

Heat of reaction

64-bit

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.

Σᵢ νᵢⱼ (ln(nᵢ ÷ F) + ln φᵢ + ln(P ÷ P₀)) − ln Kⱼ = 0

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.

r = k₀ exp(−E_a ÷ RT) Π Cᵢ^order; gas: Cᵢ = (nᵢ ÷ F) P ÷ RT; liquid: Cᵢ = nᵢ ÷ volumetric flow

One exponential and one power per species. Negative concentrations are taken as zero. The liquid volume flow is the inlet's and is held constant.

  • process/reactor.js
  • lines 94 to 106
  • CadCraft 0.8.2
function rates(th, p, n, T, P, vol) {
  // mol/(m3 s) of each reaction; concentrations from the ideal gas (phase 'gas') or the inlet volumetric flow (phase 'liquid', constant density)
  const F = sum(n), out = [];
  for (const r of p.reactions) {
    let rate = r.k0 * Math.exp(-(r.Ea || 0) / (R * T));
    for (const [id, a] of Object.entries(r.orders || Object.fromEntries(Object.entries(r.stoich).filter(([, v]) => v < 0).map(([k, v]) => [k, -v])))) {
      const i = th.index(id), C = p.phase === 'gas' ? (n[i] / F) * P / (R * T) : n[i] / vol;
      rate *= Math.pow(Math.max(C, 0), a);
    }
    out.push(rate);
  }
  return out;
}

Heat capacity of the reacting mixture

64-bit

The heat capacity flow used in the adiabatic plug-flow balance.

gas: F Σ xᵢ C_p,ig,ᵢ; liquid: F (H(T + d) − H(T − d)) ÷ 2d, d = 0.001 T

For a liquid the heat capacity is a central difference of the phase enthalpy, with the truncation and rounding error that carries.

  • process/reactor.js
  • lines 107 to 111
  • CadCraft 0.8.2
function cpMix(th, n, T, P, phase) {
  const F = sum(n), x = n.map((v) => v / F);
  if (phase === 'gas') return F * th.cpIgMix(x, T);
  const d = 1e-3 * T; return F * (th.phase(T + d, P, x, 'L').H - th.phase(T - d, P, x, 'L').H) / (2 * d);
}

Plug-flow reactor

64-bit

The molar flows and temperature along the reactor volume.

dnᵢ/dV = Σⱼ νᵢⱼ rⱼ; adiabatic: dT/dV = Σⱼ (−ΔH_j) rⱼ ÷ (F C_p)

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.

(nᵢ − n₀ᵢ − V Σⱼ νᵢⱼ rⱼ) ÷ F = 0; adiabatic: (H_out − H_in) ÷ (F × 1e5) = 0

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.

Every policy that takes this approach