1 Advanced Cubic Equations of State and Residual Properties

Author: Edward Maginn, CBE 20260

In this notebook, we extend the residual property framework from the van der Waals equation to modern cubic equations of state: the Soave-Redlich-Kwong (SRK) and Peng-Robinson (PR) models. These models incorporate the acentric factor (\(\omega\)) and a temperature-dependent attractive parameter \(a(T)\), vastly improving their accuracy for real fluids, particularly near the vapor-liquid equilibrium region.

1.1 1. The Soave-Redlich-Kwong (SRK) Equation

The SRK equation improves upon the van der Waals model by modifying the attractive term in the denominator and making the parameter \(a\) dependent on temperature and the acentric factor: \[P = \frac{RT}{V - b} - \frac{a(T)}{V(V + b)}\]

The parameters are defined based on the critical properties: \[a_c = 0.42748 \frac{R^2 T_c^2}{P_c} \quad \text{and} \quad b = 0.08664 \frac{R T_c}{P_c}\] \[a(T) = a_c \alpha(T) \quad \text{where} \quad \alpha(T) = \left[1 + m\left(1 - \sqrt{\frac{T}{T_c}}\right)\right]^2\] \[m = 0.480 + 1.574 \omega - 0.176 \omega^2\]

To solve for the molar volume, the SRK equation is rearranged into a standard cubic polynomial in \(V\): \[V^3 - \left(\frac{RT}{P}\right)V^2 + \left(\frac{a}{P} - b^2 - \frac{RTb}{P}\right)V - \frac{ab}{P} = 0\]

1.1.1 SRK Residual Properties

By applying the fundamental thermodynamic property relations to the SRK equation, we can derive analytical expressions for the residual internal energy (\(U^R\)), residual enthalpy (\(H^R\)), and residual entropy (\(S^R\)): \[U^R = \frac{T \frac{da}{dT} - a}{b} \ln\left(\frac{V+b}{V}\right)\] \[H^R = U^R + P V^R\] \[S^R = R \ln\left(\frac{P(V-b)}{RT}\right) + \frac{1}{b} \frac{da}{dT} \ln\left(\frac{V+b}{V}\right)\]

1.2 2. The Peng-Robinson (PR) Equation

The Peng-Robinson equation further refines the volume dependence of the attractive term, which notably improves the prediction of liquid densities compared to SRK: \[P = \frac{RT}{V - b} - \frac{a(T)}{V(V + b) + b(V - b)}\]

The parameter definitions follow a similar structure but use different constants: \[a_c = 0.45724 \frac{R^2 T_c^2}{P_c} \quad \text{and} \quad b = 0.07780 \frac{R T_c}{P_c}\] \[m = 0.37464 + 1.54226 \omega - 0.26992 \omega^2\]

The standard cubic polynomial form for Peng-Robinson is: \[V^3 + \left(b - \frac{RT}{P}\right)V^2 + \left(\frac{a}{P} - \frac{2bRT}{P} - 3b^2\right)V + \left(b^3 + \frac{RTb^2}{P} - \frac{ab}{P}\right) = 0\]

1.2.1 Peng-Robinson Residual Properties

Because the denominator of the PR equation is more complex, the integral for the residual properties yields a slightly different logarithmic term: \[U^R = \frac{T \frac{da}{dT} - a}{2\sqrt{2}b} \ln\left(\frac{V+(1+\sqrt{2})b}{V+(1-\sqrt{2})b}\right)\] \[H^R = U^R + P V^R\] \[S^R = R \ln\left(\frac{P(V-b)}{RT}\right) + \frac{1}{2\sqrt{2}b} \frac{da}{dT} \ln\left(\frac{V+(1+\sqrt{2})b}{V+(1-\sqrt{2})b}\right)\]


1.3 Python Solver & Plotting Code

You can run this Python block to generate the interactive widgets, calculate the roots/residuals, and plot the isotherms.

# 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

# Dictionary mapping friendly names to CoolProp string identifiers
fluid_dict = {
    'Methane': 'Methane',
    'Nitrogen': 'Nitrogen',
    'Oxygen': 'Oxygen',
    'Ethane': 'Ethane',
    'Ammonia': 'Ammonia',
    'Carbon Dioxide': 'CarbonDioxide',
    'Carbon Monoxide': 'CarbonMonoxide',
    'Argon': 'Argon',
    'Water': 'Water',
    'R-32': 'R32'
}

def compute_residuals_and_plot(eos_choice, fluid_name, T_val, T_unit, P_val, P_unit):
    fluid = fluid_dict[fluid_name]
    
    # --- 1. Unit Conversions ---
    if T_unit == '°C': T = T_val + 273.15
    elif T_unit == '°F': T = (T_val - 32) * 5/9 + 273.15
    else: T = T_val
        
    if P_unit == 'atm': P = P_val * 1.01325
    elif P_unit == 'Pa': P = P_val / 1e5
    elif P_unit == 'MPa': P = P_val * 10
    elif P_unit == 'psi': P = P_val / 14.5038
    else: P = P_val

    if T <= 0 or P <= 0:
        print("Temperature and Pressure must be greater than zero.")
        return

    # --- 2. Fetch Critical Properties ---
    Tc = CP.PropsSI('TCRIT', fluid)
    Pc_Pa = CP.PropsSI('PCRIT', fluid)
    Pc = Pc_Pa / 1e5  # Convert Pa to bar
    omega = CP.PropsSI('ACENTRIC', fluid)
    
    # --- 3. Calculate EOS Parameters ---
    R_Lbar = 0.0831446 # L*bar/(mol*K)
    R_J = 8.31446      # J/(mol*K)
    Tr = T / Tc
    
    if eos_choice == '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
    else: # Peng-Robinson
        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 - np.sqrt(Tr)))**2
    a = ac * alpha
    da_dT = -ac * m * np.sqrt(alpha / (T * Tc))
    
    # --- 4. Solve the Cubic Equation ---
    C3 = 1.0
    if eos_choice == 'SRK':
        C2 = -(R_Lbar * T / P)
        C1 = (a / P) - b**2 - (R_Lbar * T * b / P)
        C0 = -(a * b / P)
    else: # Peng-Robinson
        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)
    
    roots = np.roots([C3, C2, C1, C0])
    real_roots = np.sort(roots[np.abs(roots.imag) < 1e-9].real)
    real_roots = real_roots[real_roots > b] # Must be physically greater than b

    # --- 5. Output Summary & Residuals ---
    print("="*70)
    print(f"SYSTEM: {fluid_name} modeled with {eos_choice} EOS")
    print(f"Critical State: Tc = {Tc:.2f} K  |  Pc = {Pc:.2f} bar  |  ω = {omega:.4f}")
    print(f"Parameters: a(T) = {a:.4f} L^2·bar/mol^2  |  b = {b:.5f} L/mol")
    print("-" * 70)
    print(f"TARGET STATE:   T = {T:.2f} K  |  P = {P:.2f} bar")
    
    V_ideal = R_Lbar * T / P
    print(f"Ideal Gas Vol:  V_ig = {V_ideal:.4f} L/mol")
    print("="*70)
    
    for i, root in enumerate(real_roots):
        if len(real_roots) == 1:
            phase = "Supercritical Gas" if T > Tc else ("Liquid" if root < 5*b else "Vapor")
        elif len(real_roots) == 3:
            if i == 0: phase = "Liquid Root"
            elif i == 1: phase = "Unstable Root (Non-Physical)"
            else: phase = "Vapor Root"
            
        print(f"\n[{phase}]")
        print(f"  Molar Volume (V) = {root:.4f} L/mol")
        
        if phase != "Unstable Root (Non-Physical)":
            V_R = root - V_ideal
            
            if eos_choice == 'SRK':
                log_term = np.log((root + b) / root)
                U_R = 100 * (T * da_dT - a) / b * log_term
                S_R = R_J * np.log(P * (root - b) / (R_Lbar * T)) + 100 * (da_dT / b) * log_term
            else: # Peng-Robinson
                log_term = np.log((root + (1 + np.sqrt(2))*b) / (root + (1 - np.sqrt(2))*b))
                U_R = 100 * (T * da_dT - a) / (2 * np.sqrt(2) * b) * log_term
                S_R = R_J * np.log(P * (root - b) / (R_Lbar * T)) + 100 * (da_dT / (2 * np.sqrt(2) * b)) * log_term
                
            H_R = U_R + 100 * P * V_R
            
            print(f"  Residual Volume   (V^R) = {V_R:.4f} L/mol")
            print(f"  Residual Internal (U^R) = {U_R:.1f} J/mol")
            print(f"  Residual Enthalpy (H^R) = {H_R:.1f} J/mol")
            print(f"  Residual Entropy  (S^R) = {S_R:.3f} J/(mol·K)")
            
    print("\n" + "="*70)
    
    # --- 6. Plotting the P-V Diagram ---
    V_max = max(V_ideal * 1.5, 10 * b)
    if len(real_roots) == 3:
        V_max = max(V_max, real_roots[2] * 1.3)
        
    V_arr = np.linspace(b * 1.05, V_max, 2000)
    
    if eos_choice == 'SRK':
        P_iso = (R_Lbar * T) / (V_arr - b) - a / (V_arr * (V_arr + b))
        alpha_c = (1 + m * (1 - 1))**2
        a_crit = ac * alpha_c
        P_crit = (R_Lbar * Tc) / (V_arr - b) - a_crit / (V_arr * (V_arr + b))
    else: # Peng-Robinson
        P_iso = (R_Lbar * T) / (V_arr - b) - a / (V_arr**2 + 2*b*V_arr - b**2)
        alpha_c = (1 + m * (1 - 1))**2
        a_crit = ac * alpha_c
        P_crit = (R_Lbar * Tc) / (V_arr - b) - a_crit / (V_arr**2 + 2*b*V_arr - b**2)
    
    plt.figure(figsize=(9, 6))
    plt.plot(V_arr, P_crit, 'r--', lw=1.5, label=f'Critical Isotherm (Tc = {Tc:.1f} K)')
    plt.plot(V_arr, P_iso, 'b-', lw=2, label=f'Target Isotherm (T = {T:.1f} K)')
    plt.axhline(P, color='k', linestyle=':', lw=1.5, label=f'Target Pressure (P = {P:.1f} bar)')
    
    for i, root in enumerate(real_roots):
        if len(real_roots) == 3 and i == 1:
            plt.plot(root, P, 'wo', markeredgecolor='k', markersize=8, label='Unstable Root' if i==1 else "")
        else:
            plt.plot(root, P, 'go', markersize=8, label='Physical Root(s)' if i==0 else "")
            
    plt.ylim(0, max(P * 1.5, Pc * 1.5))
    plt.xlim(0, V_max)
    plt.xlabel('Molar Volume, V (L/mol)', fontsize=12)
    plt.ylabel('Pressure, P (bar)', fontsize=12)
    plt.title(f'{eos_choice} Properties for {fluid_name}', fontsize=14, fontweight='bold')
    plt.legend(loc='upper right')
    plt.grid(True, linestyle='--', alpha=0.6)
    plt.show()

# --- 7. Interactive UI Setup ---
style = {'description_width': 'initial'}

eos_dropdown = widgets.Dropdown(options=['Peng-Robinson', 'SRK'], value='Peng-Robinson', description='EOS:', style=style)
fluid_dropdown = widgets.Dropdown(options=list(fluid_dict.keys()), value='Methane', description='Fluid:', style=style)

T_input = widgets.FloatText(value=300, description='Temperature:', style=style, layout=widgets.Layout(width='200px'))
T_unit = widgets.Dropdown(options=['K', '°C', '°F'], value='K', layout=widgets.Layout(width='80px'))

P_input = widgets.FloatText(value=100, description='Pressure:', style=style, layout=widgets.Layout(width='200px'))
P_unit = widgets.Dropdown(options=['bar', 'atm', 'Pa', 'MPa', 'psi'], value='bar', layout=widgets.Layout(width='80px'))

T_box = widgets.HBox([T_input, T_unit])
P_box = widgets.HBox([P_input, P_unit])

ui = widgets.VBox([eos_dropdown, fluid_dropdown, T_box, P_box])
out = widgets.interactive_output(compute_residuals_and_plot, {
    'eos_choice': eos_dropdown,
    'fluid_name': fluid_dropdown,
    'T_val': T_input,
    'T_unit': T_unit,
    'P_val': P_input,
    'P_unit': P_unit
})

display(ui, out)
# 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).
compute_residuals_and_plot(eos_choice=eos_dropdown.value, fluid_name=fluid_dropdown.value,
                           T_val=T_input.value, T_unit=T_unit.value,
                           P_val=P_input.value, P_unit=P_unit.value)
======================================================================
SYSTEM: Methane modeled with Peng-Robinson EOS
Critical State: Tc = 190.56 K  |  Pc = 45.99 bar  |  ω = 0.0114
Parameters: a(T) = 2.0221 L^2·bar/mol^2  |  b = 0.02680 L/mol
----------------------------------------------------------------------
TARGET STATE:   T = 300.00 K  |  P = 100.00 bar
Ideal Gas Vol:  V_ig = 0.2494 L/mol
======================================================================

[Supercritical Gas]
  Molar Volume (V) = 0.2080 L/mol
  Residual Volume   (V^R) = -0.0414 L/mol
  Residual Internal (U^R) = -1343.8 J/mol
  Residual Enthalpy (H^R) = -1758.1 J/mol
  Residual Entropy  (S^R) = -4.241 J/(mol·K)

======================================================================