thermo-eos
SKILL.md
Equations of State and Phase Equilibrium
Computational skill for cubic equations of state (vdW, PR, SRK), fugacity calculations, departure functions, and vapor-liquid equilibrium. Calibrated for oil and gas reservoir fluid thermodynamics.
Units: SI unless noted. T in Kelvin, P in bar (1 bar = 14.504 psi = 100 kPa), V in L/mol. R = 0.08314 L·bar/mol·K.
Module 1 — Cubic Equations of State
import math
R = 0.08314 # L*bar / (mol*K)
def van_der_waals_pressure(T_K, V_m_L_mol, a_L2_bar_mol2, b_L_mol):
"""
van der Waals EOS: P = R*T/(V-b) - a/V^2
a: attraction parameter (L^2*bar/mol^2)
b: excluded volume parameter (L/mol)
Returns: pressure (bar)
Common vdW parameters:
CO2: a=3.658, b=0.04286
H2O: a=5.537, b=0.03049
CH4: a=2.300, b=0.04301
N2: a=1.370, b=0.03870
"""
if V_m_L_mol <= b_L_mol:
raise ValueError("V must be greater than b (excluded volume)")
return R * T_K / (V_m_L_mol - b_L_mol) - a_L2_bar_mol2 / V_m_L_mol**2
def peng_robinson_parameters(Tc_K, Pc_bar, omega):
"""
Peng-Robinson EOS parameters at a given temperature.
Returns: a, b, alpha, A, B coefficients for PR EOS.
PR: P = R*T/(V-b) - a*alpha/(V*(V+b) + b*(V-b))
kappa = 0.37464 + 1.54226*omega - 0.26992*omega^2
alpha(T) = (1 + kappa*(1 - sqrt(T/Tc)))^2
a_c = 0.45724 * R^2 * Tc^2 / Pc
b = 0.07780 * R * Tc / Pc
"""
kappa = 0.37464 + 1.54226 * omega - 0.26992 * omega**2
a_c = 0.45724 * R**2 * Tc_K**2 / Pc_bar
b = 0.07780 * R * Tc_K / Pc_bar
return {"kappa": kappa, "a_c": a_c, "b": b}
def peng_robinson_Z(T_K, P_bar, Tc_K, Pc_bar, omega):
"""
Solve PR EOS for compressibility factor Z.
Z^3 + (B-1)*Z^2 + (A-3B^2-2B)*Z + (B^3+B^2-AB) = 0
A = a*alpha*P/(R*T)^2, B = b*P/(R*T)
Returns: list of real positive Z roots (largest = vapor, smallest = liquid)
"""
params = peng_robinson_parameters(Tc_K, Pc_bar, omega)
kappa = params["kappa"]
a_c = params["a_c"]
b = params["b"]
alpha = (1.0 + kappa * (1.0 - math.sqrt(T_K / Tc_K)))**2
a = a_c * alpha
A = a * P_bar / (R * T_K)**2
B = b * P_bar / (R * T_K)
# Cubic: Z^3 + p*Z^2 + q*Z + r = 0
p_coef = B - 1.0
q_coef = A - 3.0*B**2 - 2.0*B
r_coef = B**3 + B**2 - A*B
# Solve cubic using analytical method (Cardano/depressed cubic)
# Substitution Z = t - p/3
a2 = p_coef
a1 = q_coef
a0 = r_coef
f = a1 - a2**2 / 3.0
g = 2.0*a2**3/27.0 - a2*a1/3.0 + a0
h = g**2/4.0 + f**3/27.0
roots = []
if h > 1e-10: # one real root
sqrt_h = math.sqrt(h)
S = (-g/2.0 + sqrt_h)
U = (-g/2.0 - sqrt_h)
S3 = math.copysign(abs(S)**(1.0/3.0), S)
U3 = math.copysign(abs(U)**(1.0/3.0), U)
z1 = S3 + U3 - a2/3.0
roots = [z1]
else: # three real roots
i2 = math.sqrt(g**2/4.0 - h)
j = math.copysign(abs(i2)**(1.0/3.0), i2)
k2 = math.acos(-g / (2.0*j)) if abs(j) > 1e-12 else 0.0
m2 = math.cos(k2/3.0)
n2 = math.sqrt(3.0) * math.sin(k2/3.0)
z1 = 2.0*j*math.cos(k2/3.0) - a2/3.0
z2 = -j*(m2 + n2) - a2/3.0
z3 = -j*(m2 - n2) - a2/3.0
roots = sorted([z1, z2, z3])
# Return only roots > B (physically meaningful)
valid = [z for z in roots if z > B]
return {"Z_roots": [round(z, 6) for z in valid],
"Z_vapor": round(max(valid), 6) if valid else None,
"Z_liquid": round(min(valid), 6) if valid else None,
"A": round(A, 6), "B": round(B, 6)}
def srk_parameters(Tc_K, Pc_bar, omega):
"""
Soave-Redlich-Kwong EOS parameters.
SRK: P = R*T/(V-b) - a*alpha / (V*(V+b))
m = 0.480 + 1.574*omega - 0.176*omega^2
a_c = 0.42748 * R^2 * Tc^2 / Pc
b = 0.08664 * R * Tc / Pc
"""
m = 0.480 + 1.574*omega - 0.176*omega**2
a_c = 0.42748 * R**2 * Tc_K**2 / Pc_bar
b = 0.08664 * R * Tc_K / Pc_bar
return {"m": m, "a_c": a_c, "b": b}
Critical property reference (common reservoir fluids):
| Component | Tc (K) | Pc (bar) | omega | Notes |
|---|---|---|---|---|
| CH4 (methane) | 190.6 | 46.0 | 0.011 | Main gas component |
| C2H6 (ethane) | 305.3 | 48.7 | 0.099 | |
| C3H8 (propane) | 369.8 | 42.5 | 0.153 | |
| n-C4 (butane) | 425.1 | 37.96 | 0.200 | |
| n-C7 (heptane) | 540.2 | 27.4 | 0.351 | |
| CO2 | 304.1 | 73.8 | 0.239 | EOR, CCS |
| H2O | 647.1 | 220.6 | 0.345 | |
| N2 | 126.2 | 33.9 | 0.039 |
Module 2 — Fugacity from PR EOS
def pr_fugacity_coefficient(Z, A, B):
"""
Fugacity coefficient from PR EOS:
ln(phi) = (Z-1) - ln(Z-B) - A/(2*sqrt(2)*B) * ln((Z+(1+sqrt(2))*B)/(Z+(1-sqrt(2))*B))
Z: compressibility factor (vapor or liquid root)
A, B: PR parameters from peng_robinson_Z output
Returns: fugacity coefficient phi (dimensionless)
"""
sqrt2 = math.sqrt(2.0)
if Z <= B or Z <= 0:
return None
arg_ln1 = Z - B
if arg_ln1 <= 0:
return None
arg_ln2_num = Z + (1.0 + sqrt2) * B
arg_ln2_den = Z + (1.0 - sqrt2) * B
if arg_ln2_den <= 0 or arg_ln2_num <= 0:
return None
ln_phi = (Z - 1.0) - math.log(arg_ln1) - A / (2.0 * sqrt2 * B) * math.log(arg_ln2_num / arg_ln2_den)
return {"ln_phi": round(ln_phi, 6), "phi": round(math.exp(ln_phi), 6)}
def fugacity(phi, y_mole_frac, P_bar):
"""
Fugacity of component i: f_i = phi_i * y_i * P
phi: fugacity coefficient
y_mole_frac: mole fraction
P_bar: total pressure (bar)
Returns: fugacity (bar)
"""
return phi * y_mole_frac * P_bar
def vle_equilibrium_condition(phi_L, phi_V, x_i, y_i, P_bar):
"""
VLE equilibrium: f_i^L = f_i^V => phi_i^L * x_i * P = phi_i^V * y_i * P
K-value: K_i = y_i / x_i = phi_i^L / phi_i^V
Returns: K_i and deviation from equilibrium (should be 1.0 at equilibrium)
"""
K_i = phi_L / phi_V
ratio = (phi_L * x_i) / (phi_V * y_i) if phi_V * y_i > 0 else None
return {"K_i": round(K_i, 5), "equilibrium_check": round(ratio, 5) if ratio else None}
Module 3 — Departure Functions (Residual Properties)
def pr_enthalpy_departure(T_K, Tc_K, Pc_bar, omega, Z, A, B):
"""
Enthalpy departure from PR EOS:
(H - H_ig) / (R*T) = Z - 1 - A/(2*sqrt(2)*B) * (1 + kappa*sqrt(T/Tc)/sqrt(alpha)) * ln(...)
Returns: (H - H^ig) in J/mol (residual enthalpy)
"""
sqrt2 = math.sqrt(2.0)
kappa = 0.37464 + 1.54226 * omega - 0.26992 * omega**2
Tr = T_K / Tc_K
alpha = (1.0 + kappa * (1.0 - math.sqrt(Tr)))**2
d_alpha_dT = -kappa / math.sqrt(T_K * Tc_K) * (1.0 + kappa * (1.0 - math.sqrt(Tr)))
a_c = 0.45724 * R**2 * Tc_K**2 / Pc_bar
b = 0.07780 * R * Tc_K / Pc_bar
T_da_dT = T_K * a_c * d_alpha_dT # T * da/dT
arg_num = Z + (1.0 + sqrt2) * B
arg_den = Z + (1.0 - sqrt2) * B
if arg_den <= 0 or arg_num <= 0 or Z <= 0:
return None
H_dep = R * T_K * (Z - 1.0) + (T_da_dT * alpha / (a_c) - a_c * alpha) / (2.0 * sqrt2 * b) * math.log(arg_num / arg_den)
# Simplified version: use A*R*T factor
H_dep_RT = Z - 1.0 - A / (2.0*sqrt2*B) * (1.0 + kappa*math.sqrt(Tr)/math.sqrt(alpha)) * math.log(arg_num/arg_den)
return {"H_dep_J_mol": round(H_dep_RT * R * T_K * 1000.0, 2),
"H_dep_kJ_mol": round(H_dep_RT * R * T_K, 4)}
def pr_entropy_departure(T_K, Tc_K, Pc_bar, omega, Z, A, B, P_bar, P_ref_bar=1.0):
"""
Entropy departure from PR EOS (simplified):
(S - S_ig) / R = ln(Z - B) - A/(2*sqrt(2)*B) * (kappa*sqrt(Tr/alpha)) * ln(...) - ln(P/P_ref)
Returns: (S - S^ig) in J/mol/K
"""
sqrt2 = math.sqrt(2.0)
kappa = 0.37464 + 1.54226 * omega - 0.26992 * omega**2
Tr = T_K / Tc_K
alpha = (1.0 + kappa * (1.0 - math.sqrt(Tr)))**2
arg_num = Z + (1.0 + sqrt2) * B
arg_den = Z + (1.0 - sqrt2) * B
if arg_den <= 0 or Z <= B or arg_num <= 0:
return None
S_dep_R = math.log(Z - B) - A / (2.0*sqrt2*B) * kappa * math.sqrt(Tr/alpha) * math.log(arg_num/arg_den) - math.log(P_bar/P_ref_bar)
return {"S_dep_J_mol_K": round(S_dep_R * R * 1000.0, 3)}
Module 4 — Phase Equilibrium (VLE)
def raoult_law_bubble_pressure(x_list, P_sat_list):
"""
Bubble point pressure (Raoult's law for ideal mixtures):
P_bubble = sum(x_i * P_sat_i)
x_list: liquid mole fractions [x1, x2, ...]
P_sat_list: pure component saturation pressures [P_sat1, P_sat2, ...]
Returns: bubble point pressure and vapor compositions y_i = x_i * P_sat_i / P
"""
P_bubble = sum(x * Ps for x, Ps in zip(x_list, P_sat_list))
y_list = [x * Ps / P_bubble for x, Ps in zip(x_list, P_sat_list)]
return {"P_bubble": round(P_bubble, 4),
"y_vapor": [round(y, 5) for y in y_list],
"sum_y": round(sum(y_list), 6)}
def rachford_rice_flash(z_list, K_list, tol=1e-8, max_iter=100):
"""
Rachford-Rice equation for isothermal flash:
g(psi) = sum(z_i*(K_i - 1) / (1 + psi*(K_i - 1))) = 0
psi: vapor mole fraction (V/F)
z_list: overall mole fractions
K_list: K-values (K_i = y_i/x_i = phi_i_L/phi_i_V)
Returns: psi (vapor fraction), x (liquid), y (vapor) compositions
"""
# Bounds for psi
K_min = min(K_list)
K_max = max(K_list)
psi_min = 1.0 / (1.0 - K_max) + 1e-6
psi_max = 1.0 / (1.0 - K_min) - 1e-6
psi_min = max(0.0, psi_min)
psi_max = min(1.0, psi_max)
if psi_max <= psi_min:
# All vapor or all liquid
sum_zK = sum(z * K for z, K in zip(z_list, K_list))
if sum_zK <= 1.0:
return {"psi": 0.0, "phase": "all liquid", "x": z_list, "y": K_list}
sum_z_over_K = sum(z / K for z, K in zip(z_list, K_list))
if sum_z_over_K <= 1.0:
return {"psi": 1.0, "phase": "all vapor", "x": z_list, "y": z_list}
# Bisection method
psi = 0.5 * (psi_min + psi_max)
for _ in range(max_iter):
g = sum(z * (K - 1.0) / (1.0 + psi * (K - 1.0))
for z, K in zip(z_list, K_list))
if abs(g) < tol:
break
if g > 0:
psi_min = psi
else:
psi_max = psi
psi = 0.5 * (psi_min + psi_max)
# Compositions
x_list = [z / (1.0 + psi * (K - 1.0)) for z, K in zip(z_list, K_list)]
y_list = [K * x for K, x in zip(K_list, x_list)]
return {"psi_vapor_frac": round(psi, 6),
"x_liquid": [round(x, 6) for x in x_list],
"y_vapor": [round(y, 6) for y in y_list],
"sum_x": round(sum(x_list), 6),
"sum_y": round(sum(y_list), 6)}
def wilson_K_value(Tc_K, Pc_bar, omega, T_K, P_bar):
"""
Wilson correlation for initial K-value estimate:
K_i = (Pc_i/P) * exp(5.373*(1+omega_i)*(1 - Tc_i/T))
Good starting point for iterative VLE calculations.
"""
return (Pc_bar / P_bar) * math.exp(5.373 * (1.0 + omega) * (1.0 - Tc_K / T_K))
Learning Resources
Textbooks
| Author(s) | Title | Edition | ISBN |
|---|---|---|---|
| Smith, Van Ness & Abbott | Introduction to ChE Thermodynamics | 9th ed. (2022) | 978-1-260-57775-6 |
| Matsoukas, T. | Fundamentals of ChE Thermodynamics | (2013) | 978-0-13-285388-1 |
| Sandler, S.I. | Chemical Engineering Thermodynamics | 5th ed. (2017) | 978-1-118-76594-0 |
| Prausnitz, Lichtenthaler & Azevedo | Molecular Thermodynamics of Fluid-Phase Equilibria | 3rd ed. | 978-0-13-977745-5 |
Free Online Resources
| Resource | URL | Notes |
|---|---|---|
| LearnChemE Thermo YouTube | youtube.com/playlist?list=PL4xAk5aclnUjG24pHjFBRb1l2jY1v2r_c | Prof. Liberatore, CSM; covers PR EOS, VLE |
| MIT OCW 10.40 | ocw.mit.edu/courses/10-40-chemical-engineering-thermodynamics-fall-2003/ | Full course, lecture notes |
| NIST WebBook | webbook.nist.gov/chemistry/fluid/ | Reference EOS data for validation |
| Knovel (via WVU Library) | knovel.com | Perry's Chemical Engineers' Handbook |
WVU Courses
- ChBE 231 Chemical Engineering Thermodynamics I — EOS, departure functions, VLE
- ChBE 311 Fluid Mechanics and Thermodynamics II — applied phase equilibrium
- PNGE (mass-energy-balance skill) — reservoir fluid flash calculations
Output Format
## EOS Calculation — [Fluid / Mixture]
### EOS Parameters
| Component | Tc (K) | Pc (bar) | omega | a | b |
|-----------|--------|----------|-------|---|---|
| | | | | | |
### Compressibility Factor (PR EOS)
| Phase | Z | Molar Volume (L/mol) | Density (kg/m^3) |
|-------|---|---------------------|----------------|
| Vapor | | | |
| Liquid | | | |
### Fugacity / VLE
| Component | phi_L | phi_V | K_i | x_i | y_i |
|-----------|-------|-------|-----|-----|-----|
| | | | | | |
**Flash result:** psi = [vapor fraction] at T = [K], P = [bar]
**Departure Functions:**
| Property | Value | Units |
|----------|-------|-------|
| H_dep | | kJ/mol |
| S_dep | | J/mol/K |
**Certainty:** HIGH for light hydrocarbons | MEDIUM for polar/associating fluids (use CPA/SAFT)
Error Handling
| Condition | Cause | Action |
|---|---|---|
| No real Z roots > B | Inputs outside EOS validity | Check T, P, Tc, Pc; verify units (bar not psi) |
| Rachford-Rice no convergence | K-values inconsistent with stability | Run Michelsen stability test; consider two-phase check |
| Z negative | Numerical error in cubic solver | Check A and B values; increase precision |
| phi > 100 | Conditions near spinodal | Near phase boundary; use smaller pressure step |
Caveats
- PR EOS is not suitable for highly polar fluids (water, alcohols, acids). Use CPA (Cubic Plus Association) or SAFT for polar systems.
- Mixing rules: This skill uses van der Waals one-fluid mixing rules (no binary interaction parameters k_ij). For accurate mixture calculations, k_ij values from literature or regression are needed.
- Three roots of the cubic: The middle root is always unphysical (spinodal region). Select the largest Z for vapor phase, smallest for liquid.
- Wilson K-values are a good starting estimate but can diverge for near-critical mixtures. Always verify convergence of VLE iterations.
- For reservoir gas calculations, the
pnge:mass-energy-balanceskill andpnge:nist-webbookskill provide validated property lookup alongside EOS.
Weekly Installs
1
Repository
jpfielding/claude.pngeFirst Seen
4 days ago
Security Audits
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
codex1