$37
Computational skill for solving material balance, energy balance, and reaction stoichiometry problems in chemical and petroleum engineering. Covers ChBE 211 (Material and Energy Balances), ChBE 321 (Reaction Engineering), and petroleum processing applications.
No external API required. This skill provides equations, worked examples, and Python (stdlib-only) calculation code.
See references/equations.md for thermodynamic data tables, full equation
derivations, and degree of freedom analysis guide.
Input + Generation = Output + Accumulation
At steady state, Accumulation = 0:
Input + Generation = Output + Consumption
For a non-reactive system (no generation/consumption):
SUM(F_in) = SUM(F_out)
Where F is molar or mass flow rate (mol/s or kg/s).
For component i in a non-reactive system:
F_in * x_i,in = F_out * x_i,out
For N components, write N independent component balances. For a system with N components and S streams, the total number of independent equations is N.
DOF = (number of unknowns) - (number of independent equations)
The system is exactly specified when DOF = 0:
Count unknowns: Stream compositions, flow rates, and process variables (T, P, split fractions) that are not given.
Count equations:
A wellhead separator splits a wellstream (F_feed) into gas (F_gas) and liquid (F_liq) streams. Given feed composition and separator split:
F_feed = F_gas + F_liq [total balance]
F_feed * z_i = F_gas * y_i + F_liq * x_i [component balance, i = 1..N]
Where z_i = feed mole fraction, y_i = gas composition, x_i = liquid composition.
For a system with recycle where one component passes through unreacted:
Example: Natural gas plant with N2 tie component
# Non-reactive steady-state 3-component separator
# Feed splits to overhead and bottoms; given feed composition and split ratio
# Components: methane (1), ethane (2), propane (3)
# Given
F_feed = 100.0 # mol/s
z = [0.70, 0.20, 0.10] # feed mole fractions [CH4, C2H6, C3H8]
split = [0.95, 0.40, 0.05] # fraction of each component going to overhead
# Material balance
F_overhead = sum(F_feed * z[i] * split[i] for i in range(3))
F_bottoms = F_feed - F_overhead
comp_overhead = [(F_feed * z[i] * split[i]) / F_overhead for i in range(3)]
comp_bottoms = [(F_feed * z[i] * (1 - split[i])) / F_bottoms for i in range(3)]
print(f"Overhead: {F_overhead:.2f} mol/s")
print(f" Compositions: {[f'{x:.3f}' for x in comp_overhead]}")
print(f"Bottoms: {F_bottoms:.2f} mol/s")
print(f" Compositions: {[f'{x:.3f}' for x in comp_bottoms]}")
CnH(2n+2) + [(3n+1)/2] O2 -> n CO2 + (n+1) H2O
Key reactions:
| Fuel | Reaction | Stoichiometric O2 (mol/mol fuel) |
|---|---|---|
| Methane (CH4) | CH4 + 2 O2 -> CO2 + 2 H2O | 2.0 |
| Ethane (C2H6) | C2H6 + 3.5 O2 -> 2 CO2 + 3 H2O | 3.5 |
| Propane (C3H8) | C3H8 + 5 O2 -> 3 CO2 + 4 H2O | 5.0 |
| n-Butane (C4H10) | C4H10 + 6.5 O2 -> 4 CO2 + 5 H2O | 6.5 |
| Hydrogen (H2) | H2 + 0.5 O2 -> H2O | 0.5 |
| Carbon (C) | C + O2 -> CO2 | 1.0 |
Air is 21 mol% O2 and 79 mol% N2. Molar ratio N2/O2 in air = 79/21 = 3.76.
Theoretical air for complete combustion of methane:
CH4 + 2 O2 -> CO2 + 2 H2O
Stoichiometric O2: 2 mol per mol CH4
Stoichiometric air: 2 / 0.21 = 9.524 mol air per mol CH4
= 9.524 * 28.97 g/mol / 16.04 g/mol
= 17.2 kg air / kg CH4
% Excess Air = (actual air - theoretical air) / theoretical air x 100%
Equivalence ratio (phi):
phi = (actual fuel/air) / (stoichiometric fuel/air)
phi < 1: fuel-lean (excess air)
phi > 1: fuel-rich (deficient air, incomplete combustion possible)
phi = 1: stoichiometric
For methane with A% excess air (A as decimal fraction, e.g., A=0.10 for 10%):
Basis: 1 mol CH4 fed
Dry flue gas mole fractions (no H2O):
total_dry = 1 + 2*A + 2*(1+A)*3.76
CO2 fraction = 1 / total_dry
O2 fraction = 2*A / total_dry
N2 fraction = 2*(1+A)*3.76 / total_dry
| Fuel | HHV (kJ/kg) | LHV (kJ/kg) | Difference |
|---|---|---|---|
| Methane | 55,530 | 50,050 | 5,480 (water condensed in HHV) |
| Ethane | 51,900 | 47,620 | 4,280 |
| Propane | 50,350 | 46,360 | 3,990 |
| n-Butane | 49,500 | 45,720 | 3,780 |
| Natural gas (avg) | ~53,000 | ~48,000 | Varies with composition |
| Hydrogen | 141,800 | 119,900 | 21,900 |
| Diesel | ~44,800 | ~42,500 | ~2,300 |
HHV vs LHV: Higher Heating Value (HHV) includes the enthalpy of condensation of water in combustion products. Lower Heating Value (LHV) assumes water leaves as vapor. Boiler efficiencies typically reference HHV; flare designs typically reference LHV.
# Combustion air calculation for natural gas
# Basis: 1 mol natural gas with known composition
# Natural gas composition (mole fractions)
gas_comp = {
'CH4': 0.900,
'C2H6': 0.060,
'C3H8': 0.025,
'C4H10': 0.010,
'N2': 0.005,
}
# Stoichiometric O2 required per mol of each component
stoich_o2 = {'CH4': 2.0, 'C2H6': 3.5, 'C3H8': 5.0,
'C4H10': 6.5, 'N2': 0.0}
excess_air_pct = 20.0 # 20% excess air
# Stoichiometric O2 for 1 mol gas
o2_stoich = sum(gas_comp[k] * stoich_o2.get(k, 0) for k in gas_comp)
o2_actual = o2_stoich * (1 + excess_air_pct / 100)
air_actual = o2_actual / 0.21 # mol air per mol gas
print(f"Stoichiometric O2: {o2_stoich:.4f} mol O2/mol gas")
print(f"Actual O2 ({excess_air_pct:.0f}% xs): {o2_actual:.4f} mol O2/mol gas")
print(f"Actual air: {air_actual:.4f} mol air/mol gas")
# Flue gas composition (1 mol gas basis)
n2_from_air = air_actual * 0.79
co2_out = sum(gas_comp.get(k,0)*n for k,n in
[('CH4',1),('C2H6',2),('C3H8',3),('C4H10',4)])
h2o_out = sum(gas_comp.get(k,0)*n for k,n in
[('CH4',2),('C2H6',3),('C3H8',4),('C4H10',5)])
o2_out = o2_actual - o2_stoich
n2_out = n2_from_air + gas_comp.get('N2', 0)
total_flue = co2_out + h2o_out + o2_out + n2_out
print(f"\nFlue gas (wet):")
print(f" CO2: {co2_out/total_flue*100:.2f} mol%")
print(f" H2O: {h2o_out/total_flue*100:.2f} mol%")
print(f" O2: {o2_out/total_flue*100:.2f} mol%")
print(f" N2: {n2_out/total_flue*100:.2f} mol%")
Q_dot - W_dot_shaft = SUM(m_dot_out * h_out) - SUM(m_dot_in * h_in) + dKE + dPE
For process equipment (no shaft work, negligible KE/PE changes):
Q_dot = SUM(m_dot_out * h_out) - SUM(m_dot_in * h_in)
Where h = specific enthalpy (kJ/kg) at stream conditions.
For a stream with constant Cp (approximation for small T range):
Q = m_dot * Cp * (T_out - T_in) [kW if m_dot in kg/s, Cp in kJ/(kg-K)]
Q = n_dot * Cp_molar * (T_out - T_in) [kW if using molar flow, Cp in kJ/(mol-K)]
For variable Cp (polynomial Shomate form):
Cp(T) = A + B*t + C*t^2 + D*t^3 + E/t^2 [kJ/(mol-K), t = T/1000, T in K]
H(T) - H(298) = A*t + B*t^2/2 + C*t^3/3 + D*t^4/4 - E/t + F - H_ref
See references/equations.md for Shomate coefficients for key gases.
Q = m_dot * lambda [lambda = latent heat of vaporization, kJ/kg]
Water latent heats (for process design):
Use pnge:nist-webbook for exact values at any T and P.
Overall energy balance (no shaft work, adiabatic exchanger):
Q = m_hot * Cp_hot * (T_hot,in - T_hot,out) = m_cold * Cp_cold * (T_cold,out - T_cold,in)
Design equation:
Q = U * A * LMTD
Where U = overall heat transfer coefficient [W/(m2-K) or BTU/(hr-ft2-F)], A = heat transfer area.
Counter-current flow (preferred — higher driving force):
dT1 = T_hot,in - T_cold,out
dT2 = T_hot,out - T_cold,in
LMTD = (dT1 - dT2) / ln(dT1/dT2)
Co-current (parallel) flow:
dT1 = T_hot,in - T_cold,in
dT2 = T_hot,out - T_cold,out
LMTD = (dT1 - dT2) / ln(dT1/dT2)
Special case: if dT1 = dT2, then LMTD = dT1 (L'Hopital's rule applies).
A heater-treater heats oil-water emulsion to improve separation. Typical conditions:
Typical Cp values for petroleum engineering:
Typical U values for heat exchanger selection:
| Service | U [BTU/(hr-ft2-F)] | U [W/(m2-K)] |
|---|---|---|
| Oil-oil | 30-60 | 170-340 |
| Oil-water | 50-80 | 285-455 |
| Gas-gas (high P) | 10-25 | 57-142 |
| Steam condenser | 200-400 | 1135-2270 |
import math
# Shell-and-tube heat exchanger sizing
# Hot side: crude oil cooling from 250 F to 120 F
# Cold side: produced water heating from 60 F to 180 F
T_hot_in = 250.0 # F
T_hot_out = 120.0 # F
T_cold_in = 60.0 # F
T_cold_out = 180.0 # F
m_hot = 1000.0 # lb/hr crude oil
Cp_hot = 0.47 # BTU/(lb-F) crude oil
Q = m_hot * Cp_hot * (T_hot_in - T_hot_out) # BTU/hr
Cp_cold = 0.98 # BTU/(lb-F) produced water
m_cold = Q / (Cp_cold * (T_cold_out - T_cold_in))
# LMTD counter-current
dT1 = T_hot_in - T_cold_out
dT2 = T_hot_out - T_cold_in
LMTD = (dT1 - dT2) / math.log(dT1 / dT2)
U = 50.0 # BTU/(hr-ft2-F) typical oil-water
A = Q / (U * LMTD)
print(f"Heat duty: {Q:,.0f} BTU/hr ({Q*0.000293:.1f} kW)")
print(f"Cold side flow: {m_cold:,.0f} lb/hr")
print(f"LMTD: {LMTD:.1f} F")
print(f"Area required: {A:.1f} ft2 ({A*0.0929:.1f} m2)")
Q = -DeltaH_rxn * xi [positive Q = heat removed for exothermic reaction]
For a non-isothermal reactor:
Q = DeltaH_rxn * xi + SUM(n_out_i * Cp_i * T_out) - SUM(n_in_i * Cp_i * T_in)
xi = (N_i - N_i0) / nu_i
Where:
All species give the same xi — use this to check stoichiometric consistency.
DeltaH_rxn = SUM(nu_i * DeltaH_f,i)
Sign convention: positive nu_i for products, negative for reactants. DeltaH_rxn < 0: exothermic; DeltaH_rxn > 0: endothermic.
Common standard heats of formation at 298 K (kJ/mol):
| Species | DeltaH_f (kJ/mol) |
|---|---|
| CH4(g) | -74.87 |
| C2H6(g) | -84.68 |
| C3H8(g) | -103.8 |
| CO2(g) | -393.5 |
| H2O(g) | -241.8 |
| H2O(l) | -285.8 |
| CO(g) | -110.5 |
| H2(g) | 0.0 |
| C(graphite) | 0.0 |
| O2(g) | 0.0 |
| N2(g) | 0.0 |
Combustion of methane example:
CH4 + 2 O2 -> CO2 + 2 H2O(g)
DeltaH_rxn = [1*(-393.5) + 2*(-241.8)] - [1*(-74.87) + 2*(0)]
= [-877.1] - [-74.87]
= -802.2 kJ/mol CH4 (LHV basis, water as vapor)
For HHV basis with liquid water: DeltaH_rxn = -890.3 kJ/mol CH4.
DeltaH_rxn(T) = DeltaH_rxn(298 K) + INTEGRAL[DeltaCp dT from 298 to T]
DeltaCp = SUM(nu_i * Cp_i(T))
For small temperature ranges or approximate calculations with constant Cp:
DeltaH_rxn(T) approx DeltaH_rxn(298) + DeltaCp_avg * (T - 298)
Flare combustion of methane at 600 F inlet (fuel + air mix):
For an ideal liquid-vapor system:
y_i * P = x_i * P_i_sat(T)
Where y_i = vapor mole fraction, x_i = liquid mole fraction, P = total pressure, P_i_sat = pure-component vapor pressure at T.
K-value (volatility ratio):
K_i = y_i / x_i = P_i_sat(T) / P
Components with K_i >> 1 prefer vapor; K_i << 1 prefer liquid.
For a flash with feed z_i and vapor fraction V/F:
SUM_i [ z_i * (K_i - 1) / (1 + (V/F) * (K_i - 1)) ] = 0
Solve iteratively for V/F. Once V/F is found:
x_i = z_i / (1 + (V/F) * (K_i - 1))
y_i = K_i * x_i
Valid range: -1/(K_max - 1) < V/F < 1/(1 - K_min)
Bubble point (first bubble forms from liquid feed):
SUM_i (K_i * z_i) = 1.0
Dew point (first drop forms from vapor feed):
SUM_i (z_i / K_i) = 1.0
log10(P_sat) = A - B / (T + C)
Common constants (P in mmHg, T in C) — see references/equations.md for
full table. Use pnge:nist-webbook for precise P_sat at any temperature.
import math
def rachford_rice(V_F, z, K):
"""Rachford-Rice residual."""
return sum(z[i] * (K[i] - 1.0) / (1.0 + V_F * (K[i] - 1.0))
for i in range(len(z)))
def flash_calc(z, K, tol=1e-8, max_iter=100):
"""Solve Rachford-Rice for vapor fraction V/F by bisection.
Returns: V_F, x (liquid mole fractions), y (vapor mole fractions)
"""
# Bounds
lo = -1.0 / (max(K) - 1.0) + 1e-10
hi = 1.0 / (1.0 - min(K)) - 1e-10
# Single-phase checks
if rachford_rice(0.0, z, K) <= 0:
return 0.0, list(z), [K[i]*z[i] for i in range(len(z))]
if rachford_rice(1.0, z, K) >= 0:
return 1.0, [z[i]/K[i] for i in range(len(z))], list(z)
# Bisection
for _ in range(max_iter):
mid = (lo + hi) / 2.0
f = rachford_rice(mid, z, K)
if abs(f) < tol:
break
if f > 0:
lo = mid
else:
hi = mid
V_F = (lo + hi) / 2.0
x = [z[i] / (1.0 + V_F * (K[i] - 1.0)) for i in range(len(z))]
y = [K[i] * x[i] for i in range(len(z))]
return V_F, x, y
# Example: C1/C3 separator flash at 50 psia, 100 F
# K-values approximated from DePriester chart (use NIST WebBook for precision)
K = [8.5, 0.25] # K_methane, K_propane at these conditions
z = [0.40, 0.60] # feed: 40 mol% methane, 60 mol% propane
V_F, x, y = flash_calc(z, K)
print(f"Vapor fraction V/F: {V_F:.4f}")
print(f"Liquid: x_CH4={x[0]:.4f}, x_C3H8={x[1]:.4f} (sum={sum(x):.6f})")
print(f"Vapor: y_CH4={y[0]:.4f}, y_C3H8={y[1]:.4f} (sum={sum(y):.6f})")
| Problem Type | Module |
|---|---|
| Component flow rates, separator balance, recycle | Module 1 |
| Combustion air, flue gas, HHV/LHV, flare | Module 2 |
| Heat duty, heat exchanger sizing, LMTD, Cp | Module 3 |
| Reaction enthalpy, extent, adiabatic temperature | Module 4 |
| Flash calculation, VLE, K-values, phase split | Module 5 |
Collect required parameters. Confirm:
State all assumptions explicitly before calculating.
Before solving, verify DOF = 0. List unknowns and equations. If DOF > 0, identify what specifications are missing and ask the user.
## [Problem Description]
### Given (Basis: [state basis])
| Variable | Value | Unit | Source |
|----------|-------|------|--------|
### DOF Check
Unknowns: N Equations: N DOF: 0 (exactly specified)
### Equations Applied
1. [equation name and form]
2. [equation name and form]
### Solution
[step-by-step with units]
### Results
| Variable | Value | Unit |
|----------|-------|------|
**Check:** [balances close to ±0.1%, answer is physically reasonable]
**Assumptions:**
- [list all assumptions made]
**Caveats:**
- [validity ranges, when to use more rigorous methods]
**Check your understanding:** [one question to test conceptual grasp]
| Condition | Action |
|---|---|
| Compositions do not sum to 1.0 | Flag error; ask user to verify or normalize |
| DOF != 0 | Report DOF, list which variables are over- or under-specified |
| Negative flow rate or composition | Indicates over-specification or inconsistency; recheck stream specifications |
| Flash V/F outside [0, 1] | System is all vapor (V/F=1) or all liquid (V/F=0); state which and verify K-values |
| Very large K-value range (K>>10 or K<<0.01) | Simple Raoult's law may be unreliable; recommend EOS (PR or SRK) via process simulator |
| Adiabatic flame temperature iteration not converging | Check DeltaH_rxn sign; verify Cp data source for products |
| Units inconsistent in energy balance | Identify the unit mismatch and provide conversion factor |
references/equations.md