{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "7af16f2a",
   "metadata": {},
   "source": [
    "# Two examples using the Peng-Robinson EOS to plot several isotherms for carbon dioxide"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3c29e0e8",
   "metadata": {},
   "source": [
    "## Generalized Peng-Robinson Equation of State\n",
    "\n",
    "Routines to calcualte the Generalized Peng-Robinson Equation of State\n",
    "\n",
    "SIS is Stanley I. Sandler, *Chemical, Biochemcial and Engineering Thermodynamics*, 5th ed.\n",
    "\n",
    "Eric Furst  \n",
    "November 2025\n",
    "\n",
    "The Generalized Peng-Robinson equation of state is\n",
    "\n",
    "$$ P = \\frac{RT}{\\underline{V}-b} - \\frac{a(T)}{\\underline{V}(\\underline{V}+b) + b(\\underline{V}-b)} \\tag{Eq. 6.4-2}$$\n",
    "\n",
    "with\n",
    "\n",
    "$$ b = 0.07780 \\frac{RT_c}{P_c} \\tag{Eq. 6.7-2}$$\n",
    "$$ a(T) = a(T_c)\\alpha(T) = 0.45724 \\frac{R^2T_c^2}{P_c}\\alpha(T) \\tag{Eq. 6.7-1}$$\n",
    "$$ \\sqrt{\\alpha} = 1 + \\kappa \\left ( 1- \\sqrt{\\frac{T}{T_c}} \\right ) \\tag{Eq. 6.7-3}$$\n",
    "$$ \\kappa = 0.37464 + 1.54226\\omega − 0.26992\\omega^2 \\tag{Eq. 6.7-4}$$\n",
    "\n",
    "The acentric factor $\\omega$ and the crticial temperatures and pressures are given in SIS table 6.6-1.\n",
    "\n",
    "Calculating the pressure $P$ given $\\underline{V}$ and $T$ is straightforward, but to calculate the molar volume given $P$ and $T$, we need to solve the cubic equation of state of the form\n",
    "\n",
    "$$ Z^3 + \\alpha Z^2 + \\beta Z + \\gamma = 0 \\tag{Eq. 6.4-4}$$\n",
    "\n",
    "where $Z$ is the compressibility factor\n",
    "\n",
    "$$ Z = \\frac{P \\underbar{V{}}}{RT} $$\n",
    "\n",
    "For the Peng-Robinson EOS (see SIS Table 6.4-3),\n",
    "\n",
    "$$ \\alpha = -1 + B $$\n",
    "$$ \\beta = A - 3B^2 -2B $$\n",
    "$$ \\gamma = -AB + B^2 + B^3 $$\n",
    "\n",
    "and \n",
    "\n",
    "$$ A = \\frac{aP}{(RT)^2} $$\n",
    "$$ B = \\frac{bP}{RT} $$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "47da809f",
   "metadata": {},
   "outputs": [],
   "source": [
    "\"\"\"\n",
    "Generalized Peng-Robinson EOS \n",
    "PR_pressure returns the pressure given V, T, Pc, Tc, omega\n",
    "PR_volume returns all molar volumes (real roots of EOS) given P, T, Pc, Tc, omega\n",
    "\"\"\"\n",
    "import numpy as np\n",
    "from scipy import constants\n",
    "from numpy.polynomial import Polynomial\n",
    "\n",
    "R = constants.R # Set the gas constant to R\n",
    "\n",
    "def calc_b(Pc,Tc):\n",
    "    return 0.07780*R*Tc/Pc\n",
    "\n",
    "def calc_a(T,Pc,Tc,omega):\n",
    "    kappa = 0.37464 + 1.54226*omega - 0.26992*omega**2\n",
    "    sqrtalpha = 1 + kappa*(1-np.sqrt(T/Tc))\n",
    "    return 0.45724*R**2*Tc**2/Pc*sqrtalpha**2\n",
    "\n",
    "# Calculate the presure given V, T for PR EOS\n",
    "def PR_pressure(V,T,Pc,Tc,omega):\n",
    "    a = calc_a(T,Pc,Tc,omega)\n",
    "    b = calc_b(Pc,Tc)\n",
    "\n",
    "    P = R*T/(V-b) - a/(V*(V+b)+b*(V-b))\n",
    "    return P\n",
    "\n",
    "# Calculate the molar volume given P, T for PR EOS in m^3/mol\n",
    "# Note that we can return multiple real roots (up to three)\n",
    "# The largest and smallest will be the vapor and liquid, respectively\n",
    "def PR_volume(P, T, Pc, Tc, omega):\n",
    "    # Calculate a, b, A, and B\n",
    "    a = calc_a(T, Pc, Tc, omega)\n",
    "    b = calc_b(Pc, Tc)\n",
    "    A = a*P/R**2/T**2\n",
    "    B = b*P/R/T\n",
    "\n",
    "    # Definitions of alpha, beta, gamma in SIS Table 6.4-3 for PR EOS\n",
    "    alpha = -1 + B\n",
    "    beta = A - 3*B**2 -2*B\n",
    "    gamma = -A*B + B**2 + B**3\n",
    "\n",
    "    # polynomial with coefficients in increasing order: c0 + c1 x + c2 x**2 + ...\n",
    "    p = Polynomial([ gamma, beta, alpha, 1 ])  \n",
    "\n",
    "    roots = p.roots()        # returns all (possibly complex) roots of Z\n",
    "    real_roots = roots.real[abs(roots.imag) < 1e-12]  # filter real ones\n",
    "\n",
    "    # Convert real values of Z to molar volume\n",
    "    V = real_roots*R*T/P\n",
    "\n",
    "    return V\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2f55b187",
   "metadata": {},
   "source": [
    "## Example calculation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "fd028ebd",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Text(0.5, 1.0, 'PV diagram for carbon dioxide')"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 102592 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "\n",
    "# Critical values for carbon dioxide and the acentric factor\n",
    "Pc = 7.376*10**6 # critical pressure in Pa\n",
    "Tc = 304.2 # critical temperature in K\n",
    "omega = 0.225 # acentric factor\n",
    "\n",
    "fig, ax = plt.subplots()\n",
    "\n",
    "# plot several isotherms\n",
    "# use the same color for IG and vdW EOS\n",
    "for T in range(250, 350, 10) : \n",
    "    V_max = constants.R*T/(100*1000)\n",
    "    V_min = constants.R*T/(1000*1000)*0.015\n",
    "    V = np.linspace(V_min,V_max,10000)\n",
    "\n",
    "    ax.plot(V, PR_pressure(V,T,Pc,Tc,omega)/1e6, '-',label=T)\n",
    "\n",
    "ax.plot(V, PR_pressure(V,Tc,Pc,Tc,omega)/1e6, '-',label=Tc)\n",
    "\n",
    "ax.legend(frameon=True, fancybox=True, edgecolor='none')\n",
    "ax.set_xscale('log')\n",
    "plt.ylim([-1,20])\n",
    "plt.xlim([1*10**-5,0.003])\n",
    "plt.xlabel('V (m$^3$/mol)')\n",
    "plt.ylabel('P (MPa)')\n",
    "plt.title('PV diagram for carbon dioxide')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "faf1a487",
   "metadata": {},
   "source": [
    "Our second example makes a prettier plot using Seaborn..."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f387f98f",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 94812 characters]",
      "text/plain": [
       "<Figure size 800x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "import seaborn as sns\n",
    "import matplotlib.pyplot as plt\n",
    "from scipy import constants\n",
    "\n",
    "# Critical values for carbon dioxide and the acentric factor\n",
    "Pc = 7.376*10**6\n",
    "Tc = 304.2\n",
    "omega = 0.225\n",
    "\n",
    "rows = []\n",
    "\n",
    "# Make a list of temperatures for the isotherms and add Tc\n",
    "temps = list(range(250, 350, 10))\n",
    "temps.append(Tc)\n",
    "temps = sorted(temps)\n",
    "\n",
    "# Generate all isotherms including Tc\n",
    "for T in temps:\n",
    "    V_max = constants.R*T/(100*1000)\n",
    "    V_min = constants.R*T/(1000*1000)*0.015\n",
    "    V = np.linspace(V_min, V_max, 5000)\n",
    "    P = PR_pressure(V, T, Pc, Tc, omega)/1e6\n",
    "    \n",
    "    rows.append(pd.DataFrame({\n",
    "        \"V\": V,\n",
    "        \"P\": P,\n",
    "        \"T\": T\n",
    "    }))\n",
    "\n",
    "df = pd.concat(rows, ignore_index=True)\n",
    "\n",
    "# Convert T to string for categorical hue (fixes legend)\n",
    "df[\"T\"] = df[\"T\"].astype(str)\n",
    "\n",
    "sns.set_theme(style=\"ticks\")\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(8, 6))\n",
    "\n",
    "sns.lineplot(\n",
    "    data=df,\n",
    "    x=\"V\", y=\"P\",\n",
    "    hue=\"T\",\n",
    "    palette=\"viridis\",\n",
    "    linewidth=1.2,\n",
    "    ax=ax\n",
    ")\n",
    "\n",
    "ax.set_xscale(\"log\")\n",
    "ax.set_ylim([0, 10])\n",
    "ax.set_xlim([1e-5, 1e-2])\n",
    "\n",
    "ax.set_xlabel(\"V (m$^3$/mol)\")\n",
    "ax.set_ylabel(\"P (MPa)\")\n",
    "ax.set_title(\"PV diagram for carbon dioxide\")\n",
    "\n",
    "# Remove legend box\n",
    "leg = ax.legend(title=\"Temperature (K)\", frameon=False)\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "83c296bf",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
