# Install CoolProp if it is missing (e.g., on Google Colab)
try:
import CoolProp
except ImportError:
%pip install -q CoolProp
import numpy as np
import matplotlib.pyplot as plt
import ipywidgets as widgets
from IPython.display import display, clear_output
import CoolProp.CoolProp as CP
fluid_dict = {
'Methane': 'Methane',
'Ethane': 'Ethane',
'Propane': 'Propane',
'n-Hexane': 'n-Hexane',
'Nitrogen': 'Nitrogen',
'Oxygen': 'Oxygen',
'Ammonia': 'Ammonia',
'Carbon Dioxide': 'CarbonDioxide',
'Water': 'Water',
'Argon': 'Argon',
}
R_Lbar = 0.0831446 # L·bar/(mol·K)
R_J = 8.31446 # J/(mol·K)
# ── EOS parameter builders ─────────────────────────────────────────────────────
def get_eos_params(eos, fluid, T, P, Tc, Pc, omega):
"""Return (a, b, da_dT, roots_V) for the chosen EOS."""
Tr = T / Tc
if eos == 'vdW':
a = 27 * (R_Lbar * Tc)**2 / (64 * Pc)
b = R_Lbar * Tc / (8 * Pc)
da_dT = 0.0
C2 = -(b + R_Lbar * T / P)
C1 = a / P
C0 = -a * b / P
elif eos == 'SRK':
ac = 0.42748 * R_Lbar**2 * Tc**2 / Pc
b = 0.08664 * R_Lbar * Tc / Pc
m = 0.480 + 1.574 * omega - 0.176 * omega**2
alpha = (1 + m * (1 - Tr**0.5))**2
a = ac * alpha
da_dT = -ac * m * (alpha / (T * Tc))**0.5
C2 = -(R_Lbar * T / P)
C1 = a / P - b**2 - R_Lbar * T * b / P
C0 = -(a * b / P)
else: # PR
ac = 0.45724 * R_Lbar**2 * Tc**2 / Pc
b = 0.07780 * R_Lbar * Tc / Pc
m = 0.37464 + 1.54226 * omega - 0.26992 * omega**2
alpha = (1 + m * (1 - Tr**0.5))**2
a = ac * alpha
da_dT = -ac * m * (alpha / (T * Tc))**0.5
C2 = b - R_Lbar * T / P
C1 = a / P - 2 * b * R_Lbar * T / P - 3 * b**2
C0 = b**3 + R_Lbar * T / P * b**2 - a * b / P
r = np.roots([1.0, C2, C1, C0])
roots_V = np.sort(r[np.abs(r.imag) < 1e-9].real)
roots_V = roots_V[roots_V > b]
return a, b, da_dT, roots_V
# ── Fugacity coefficient formulas ──────────────────────────────────────────────
def ln_phi(eos, Z, A, B):
"""Evaluate ln φ for the given EOS and compressibility factor Z."""
if eos == 'vdW':
return (Z - 1) - np.log(Z - B) - A / Z
elif eos == 'SRK':
return (Z - 1) - np.log(Z - B) - (A / B) * np.log(1 + B / Z)
else: # PR
sq2 = np.sqrt(2)
return (Z - 1) - np.log(Z - B) - A / (2 * sq2 * B) * np.log(
(Z + (1 + sq2) * B) / (Z + (1 - sq2) * B)
)
# ── Main solver/plotter ────────────────────────────────────────────────────────
def run(eos_choice, fluid_name, T_val, T_unit, P_val, P_unit):
# Unit conversions
T = T_val + 273.15 if T_unit == '°C' else (T_val - 32) * 5/9 + 273.15 if T_unit == '°F' else T_val
conv = {'bar': 1, 'atm': 1.01325, 'Pa': 1e-5, 'MPa': 10, 'psi': 1/14.5038}
P = P_val * conv[P_unit]
if T <= 0 or P <= 0:
print("Temperature and pressure must be positive."); return
fluid = fluid_dict[fluid_name]
Tc = CP.PropsSI('TCRIT', fluid)
Pc = CP.PropsSI('PCRIT', fluid) / 1e5 # bar
omega = CP.PropsSI('ACENTRIC', fluid)
a, b, da_dT, roots_V = get_eos_params(eos_choice, fluid, T, P, Tc, Pc, omega)
A = a * P / (R_Lbar * T)**2
B = b * P / (R_Lbar * T)
V_ig = R_Lbar * T / P
# ── Print header ──────────────────────────────────────────────────────────
print("=" * 72)
print(f" EOS: {eos_choice} | Fluid: {fluid_name}")
print(f" Tc = {Tc:.2f} K Pc = {Pc:.2f} bar ω = {omega:.4f}")
print(f" T = {T:.2f} K P = {P:.3f} bar")
print(f" A = {A:.5f} B = {B:.5f}")
print(f" V_ideal = {V_ig:.4f} L/mol")
print("=" * 72)
fugacities = [] # (label, V, Z, lnphi, f)
for i, V in enumerate(roots_V):
Z = P * V / (R_Lbar * T)
lnp = ln_phi(eos_choice, Z, A, B)
phi = np.exp(lnp)
f = phi * P
n = len(roots_V)
if n == 1:
label = "Vapor" if T >= Tc or V > 5 * b else "Liquid"
else:
if i == 0: label = "Liquid root"
elif i == 1: label = "Unstable root (discard)"
else: label = "Vapor root"
fugacities.append((label, V, Z, lnp, phi, f))
print(f"\n [{label}]")
print(f" V = {V:.5f} L/mol")
print(f" Z = {Z:.5f}")
if "Unstable" in label:
print(" (fugacity not meaningful for unstable root — skip)")
continue
print(f" ln φ = {lnp:.5f}")
print(f" φ = {phi:.5f}")
print(f" f = {f:.4f} bar")
# ── Phase stability verdict ────────────────────────────────────────────────
physical = [(lbl, V, Z, lnp, phi, f)
for (lbl, V, Z, lnp, phi, f) in fugacities
if "Unstable" not in lbl]
print("\n" + "-" * 72)
if len(physical) == 2:
lbl_L, _, _, _, _, f_L = physical[0]
lbl_V, _, _, _, _, f_V = physical[1]
print(f" PHASE STABILITY ANALYSIS:")
print(f" f^L = {f_L:.4f} bar (liquid root)")
print(f" f^V = {f_V:.4f} bar (vapor root)")
if abs(f_L - f_V) / max(f_L, f_V) < 1e-4:
print(" → f^L ≈ f^V : VLE condition satisfied — system is on the saturation curve.")
elif f_L < f_V:
print(" → f^L < f^V : LIQUID is the stable phase at this T and P.")
else:
print(" → f^V < f^L : VAPOR is the stable phase at this T and P.")
elif len(physical) == 1:
print(f" Single physical root — {physical[0][0]} phase.")
print(f" f = {physical[0][5]:.4f} bar, φ = {physical[0][4]:.5f}")
print("=" * 72)
# ── Plot ──────────────────────────────────────────────────────────────────
V_max = max(V_ig * 1.5, 10 * b)
if len(roots_V) == 3:
V_max = max(V_max, roots_V[2] * 1.3)
V_arr = np.linspace(b * 1.005, V_max, 3000)
if eos_choice == 'vdW':
P_iso = R_Lbar * T / (V_arr - b) - a / V_arr**2
elif eos_choice == 'SRK':
P_iso = R_Lbar * T / (V_arr - b) - a / (V_arr * (V_arr + b))
else:
P_iso = R_Lbar * T / (V_arr - b) - a / (V_arr**2 + 2*b*V_arr - b**2)
fig, axes = plt.subplots(1, 2, figsize=(13, 5.5))
# Left panel: P-V diagram
ax = axes[0]
ax.plot(V_arr, P_iso, 'b-', lw=2, label=f'{eos_choice} isotherm (T = {T:.1f} K)')
ax.axhline(P, color='k', ls=':', lw=1.5, label=f'P = {P:.2f} bar')
colors_root = {'Liquid root': '#d62728', 'Vapor root': '#1f77b4',
'Unstable root (discard)': 'lightgray'}
for (lbl, V, Z, lnp, phi, f) in fugacities:
color = colors_root.get(lbl, 'green')
marker = 'x' if 'Unstable' in lbl else 'o'
tag = '' if 'Unstable' in lbl else f' f={f:.3f} bar'
ax.plot(V, P, marker=marker, ms=9, color=color, zorder=5, label=lbl + tag)
ax.set_xlim(0, V_max)
ax.set_ylim(bottom=0, top=max(P * 2.2, Pc * 1.6))
ax.set_xlabel('Molar volume, $V$ (L/mol)', fontsize=12)
ax.set_ylabel('Pressure, $P$ (bar)', fontsize=12)
ax.set_title(f'{eos_choice} isotherm — {fluid_name}', fontsize=12, fontweight='bold')
ax.legend(fontsize=8.5, loc='upper right')
ax.grid(True, ls='--', alpha=0.5)
# Right panel: fugacity bar chart
ax2 = axes[1]
bar_labels = []
bar_vals = []
bar_colors = []
for (lbl, V, Z, lnp, phi, f) in fugacities:
if 'Unstable' in lbl:
continue
bar_labels.append(lbl.replace(' root', ''))
bar_vals.append(f)
bar_colors.append('#d62728' if 'Liquid' in lbl else '#1f77b4' if 'Vapor' in lbl else '#2ca02c')
# Also add f = P reference
bar_labels.append('Ideal (f = P)')
bar_vals.append(P)
bar_colors.append('lightgray')
bars = ax2.bar(bar_labels, bar_vals, color=bar_colors, edgecolor='k', width=0.4)
for bar, val in zip(bars, bar_vals):
ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.01 * max(bar_vals),
f'{val:.3f}', ha='center', va='bottom', fontsize=10, fontweight='bold')
if len(physical) == 2:
stable_f = min(physical[0][5], physical[1][5])
ax2.axhline(stable_f, color='green', ls='--', lw=1.5, label='Stable phase (lower f)')
ax2.legend(fontsize=9)
ax2.set_ylabel('Fugacity, $f$ (bar)', fontsize=12)
ax2.set_title('Fugacity comparison', fontsize=12, fontweight='bold')
ax2.grid(True, axis='y', ls='--', alpha=0.5)
ax2.set_ylim(bottom=0)
plt.suptitle(f'{fluid_name} at T = {T:.1f} K, P = {P:.2f} bar [{eos_choice}]',
fontsize=13, y=1.01)
plt.tight_layout()
plt.show()
# ── Widgets ───────────────────────────────────────────────────────────────────
style = {'description_width': 'initial'}
lay = widgets.Layout
eos_dd = widgets.Dropdown(options=['Peng-Robinson', 'SRK', 'vdW'], value='Peng-Robinson',
description='EOS:', style=style)
fluid_dd = widgets.Dropdown(options=list(fluid_dict.keys()), value='Propane',
description='Fluid:', style=style)
T_in = widgets.FloatText(value=300, description='Temperature:', style=style, layout=lay(width='200px'))
T_unit = widgets.Dropdown(options=['K', '°C', '°F'], value='K', layout=lay(width='80px'))
P_in = widgets.FloatText(value=10, description='Pressure:', style=style, layout=lay(width='200px'))
P_unit = widgets.Dropdown(options=['bar', 'atm', 'Pa', 'MPa', 'psi'], value='bar', layout=lay(width='80px'))
ui = widgets.VBox([eos_dd, fluid_dd,
widgets.HBox([T_in, T_unit]),
widgets.HBox([P_in, P_unit])])
out = widgets.interactive_output(run, {
'eos_choice': eos_dd, 'fluid_name': fluid_dd,
'T_val': T_in, 'T_unit': T_unit,
'P_val': P_in, 'P_unit': P_unit,
})
display(ui, out)Fugacity from Cubic Equations of State
Author: Edward Maginn, CBE 20260
This notebook computes the fugacity coefficient \(\phi\) and fugacity \(f = \phi P\) for a real fluid using the van der Waals (vdW), Soave-Redlich-Kwong (SRK), and Peng-Robinson (PR) equations of state. When the cubic EOS yields two physical roots (liquid and vapor), the notebook computes both fugacities and identifies the thermodynamically stable phase as the one with the lower fugacity.
1 Background: \(\ln \phi\) Formulas
Starting from the general formula \[\ln\phi = \int_0^P \frac{Z-1}{P}\,dP\] and evaluating analytically for each EOS, we obtain closed-form expressions. Define the dimensionless parameters \[A \equiv \frac{aP}{(RT)^2}, \qquad B \equiv \frac{bP}{RT}\] where \(a\) includes the \(\alpha(T)\) correction for SRK and PR.
van der Waals: \[\ln\phi = (Z-1) - \ln(Z-B) - \frac{A}{Z}\]
Soave-Redlich-Kwong (SRK): \[\ln\phi = (Z-1) - \ln(Z-B) - \frac{A}{B}\ln\!\left(1 + \frac{B}{Z}\right)\]
Peng-Robinson (PR): \[\ln\phi = (Z-1) - \ln(Z-B) - \frac{A}{2\sqrt{2}\,B}\ln\!\left(\frac{Z+(1+\sqrt{2})B}{Z+(1-\sqrt{2})B}\right)\]
The phase stability rule: when both vapor (\(Z^V\)) and liquid (\(Z^L\)) roots exist, nature selects the phase with the lower fugacity \(f = \phi P\).
2 Python Solver
Select the EOS, fluid, temperature, and pressure using the widgets below, then read off the fugacity of each root and the predicted stable phase.
# Static preview: results for the default inputs above.
# The output is saved so the course website can show it.
# To try other inputs, use the controls above, or open
# this notebook in Colab (badge on the course website).
run(eos_choice=eos_dd.value, fluid_name=fluid_dd.value, T_val=T_in.value, T_unit=T_unit.value,
P_val=P_in.value, P_unit=P_unit.value)========================================================================
EOS: Peng-Robinson | Fluid: Propane
Tc = 369.89 K Pc = 42.51 bar ω = 0.1521
T = 300.00 K P = 10.000 bar
A = 0.18370 B = 0.02256
V_ideal = 2.4943 L/mol
========================================================================
[Liquid root]
V = 0.08670 L/mol
Z = 0.03476
ln φ = -0.17370
φ = 0.84055
f = 8.4055 bar
[Unstable root (discard)]
V = 0.31927 L/mol
Z = 0.12800
(fugacity not meaningful for unstable root — skip)
[Vapor root]
V = 2.03209 L/mol
Z = 0.81468
ln φ = -0.17179
φ = 0.84216
f = 8.4216 bar
------------------------------------------------------------------------
PHASE STABILITY ANALYSIS:
f^L = 8.4055 bar (liquid root)
f^V = 8.4216 bar (vapor root)
→ f^L < f^V : LIQUID is the stable phase at this T and P.
========================================================================
