{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "7af16f2a",
   "metadata": {},
   "source": [
    "# Goal of this notebook\n",
    "\n",
    "The goal of this notebook is to reproduce the following figures in chapters 6 and 7 of Stanley I. Sandler, *Chemical, Biochemcial and Engineering Thermodynamics*, 5th ed. These illustrations are based on the van der Waals equation of state.\n",
    "\n",
    "- Fig 6.6-1 -- five isotherms illustrated around the critical isotherm\n",
    "- Fig 7.3-1 -- five isotherms labeled with local maxima and minima\n",
    "- Fig 7.3-2 -- a diagram showing the isotherm construction for a sub-critical temperature\n",
    "- Fig 7.3-3 -- the resulting isotherm for a sub-critical temperature\n",
    "- Fig 7.3-4 -- the van der Waals EOS PV diagram with multiple isotherms and two phase region\n",
    "\n",
    "<small>\n",
    "Copyright (c) 2025 Eric M. Furst\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3c29e0e8",
   "metadata": {},
   "source": [
    "## van der Waals equation of state\n",
    "\n",
    "Below are routines to calculate the van der Waals equation of state.\n",
    "\n",
    "The van der Waals equation of state (vdW EOS) is\n",
    "\n",
    "$$ P = \\frac{RT}{\\underline{V}-b} - \\frac{a}{\\underline{V}^2} \\tag{Eq. 6.2-38b}$$\n",
    "\n",
    "Like other cubic equations of state, 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 a cubic equation 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 van der Waals EOS (see SIS **Table 6.4-3**),\n",
    "\n",
    "$$ \\alpha = -1 - B $$\n",
    "$$ \\beta = A $$\n",
    "$$ \\gamma = -AB $$\n",
    "\n",
    "and \n",
    "\n",
    "$$ A = \\frac{aP}{(RT)^2} $$\n",
    "$$ B = \\frac{bP}{RT} $$\n",
    "\n",
    "Here, we will use the reduced van der Waals equation of state\n",
    "\n",
    "$$ \\left ( P_r + \\frac{3}{\\underline{V}_r} \\right )(3\\underline{V}_r - 1) = 8T_r \\tag{Eq. 6.6-6}$$\n",
    "\n",
    "This can be written in the form \n",
    "\n",
    "$$ \\underline{V}_r^3 + \\alpha \\underline{V}_r^2 + \\beta \\underline{V}_r + \\gamma = 0 $$\n",
    "\n",
    "with\n",
    "\n",
    "$$\\alpha = -\\frac{P_r+8T_r}{3P_r}$$\n",
    "$$\\beta = \\frac{3}{P_r}$$\n",
    "$$\\gamma = -\\frac{1}{P_r}$$\n",
    "\n",
    "which is left as an exercise for the reader."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "47da809f",
   "metadata": {},
   "outputs": [],
   "source": [
    "\"\"\"\n",
    "Reduced van der Waals equation of state \n",
    "rvdw_P returns the pressure given Vr, Tr\n",
    "rvdw_V returns all reduced molar volumes (real roots of EOS) given Pr and Tr\n",
    "\"\"\"\n",
    "import numpy as np\n",
    "from numpy.polynomial import Polynomial\n",
    "\n",
    "\n",
    "# Calculate the reduced presure given Vr, Tr for reduced vdw EOS\n",
    "def rvdw_P(Vr,Tr):\n",
    "\n",
    "    return 8*Tr/(3*Vr-1) - 3/Vr**2\n",
    "\n",
    "# Calculate the reduced molar volume given Pr, Tr\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 rvdw_V(Pr, Tr):\n",
    "\n",
    "    # Definitions of alpha, beta, gamma in SIS Table 6.4-3 for PR EOS\n",
    "    alpha = -(Pr+8*Tr)/3/Pr\n",
    "    beta = 3/Pr\n",
    "    gamma = -1/Pr\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",
    "    return real_roots\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2f55b187",
   "metadata": {},
   "source": [
    "# Figure 6.6-1 -- The pressure-volume behavior of the van der Waals equation of state\n",
    "\n",
    "We begin by plotting isotherms on the reduced scale. We'll plot a few above and below the critical isotherm."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "fd028ebd",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 34840 characters]",
      "text/plain": [
       "<Figure size 500x450 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import matplotlib as mpl\n",
    "from matplotlib.transforms import offset_copy\n",
    "\n",
    "# May cause problems in Colab or installations without LaTeX\n",
    "mpl.rcParams[\"text.usetex\"] = True\n",
    "\n",
    "# Good PDF defaults\n",
    "mpl.rcParams.update({\n",
    "    \"pdf.fonttype\": 42,   # keep text as text in PDF\n",
    "    \"ps.fonttype\": 42,\n",
    "})\n",
    "\n",
    "# Function to make plot labels\n",
    "def label_curve(\n",
    "    ax,\n",
    "    x, y, text,\n",
    "    *,\n",
    "    dx=0, dy=3,          # offset in points\n",
    "    ha=\"left\", va=\"bottom\",\n",
    "    fontsize=9,\n",
    "    bbox=True\n",
    "):\n",
    "    \"\"\"\n",
    "    Place a text label in data coordinates with a point offset.\n",
    "    \"\"\"\n",
    "    transform = offset_copy(ax.transData, fig=ax.figure, x=dx, y=dy, units=\"points\")\n",
    "\n",
    "    bbox_kw = None\n",
    "    if bbox:\n",
    "        bbox_kw = dict(facecolor=\"white\", edgecolor=\"none\", boxstyle=\"round,pad=0.2\")\n",
    "\n",
    "    ax.text(float(x), float(y), text, ha=ha, va=va, fontsize=fontsize, transform=transform,\n",
    "        bbox=bbox_kw)\n",
    "\n",
    "# Make the plot\n",
    "fig, ax = plt.subplots(figsize=(5.0, 4.5))  # choose size you like\n",
    "\n",
    "# Range of reduced molar volumes\n",
    "Vr_max = 3\n",
    "Vr_min = 0.5\n",
    "Vr = np.linspace(Vr_min,Vr_max,1000)\n",
    "\n",
    "# plot the labeled lines, 0.95, 1.0, 1.05\n",
    "Tr = 0.95\n",
    "ax.plot(Vr, rvdw_P(Vr,Tr), '-',label=Tr, color=\"black\", linewidth=1)\n",
    "label_curve(ax, 1, rvdw_P(1,Tr), rf\"$T < T_c$\", dy=15)\n",
    "\n",
    "Tr = 1.0\n",
    "ax.plot(Vr, rvdw_P(Vr,Tr), '-',label=Tr, color=\"black\", linewidth=1)\n",
    "label_curve(ax, 1, rvdw_P(1,Tr), rf\"$T = T_c$\")\n",
    "\n",
    "Tr = 1.05\n",
    "ax.plot(Vr, rvdw_P(Vr,Tr), '-',label=Tr, color=\"black\", linewidth=1)\n",
    "label_curve(ax, 1, rvdw_P(1,Tr), rf\"$T > T_c$\")\n",
    "\n",
    "# plot several isotherms above the critical temperature\n",
    "for Tr in np.array([0.9, 1.1]): \n",
    "    ax.plot(Vr, rvdw_P(Vr,Tr), '-',label=Tr, color=\"black\", linewidth=1)\n",
    "\n",
    "plt.ylim([0.4,1.4])\n",
    "plt.xlabel(r\"$\\underline{V}$\", labelpad=15)\n",
    "plt.ylabel(r'$P$', rotation=0, labelpad=20)\n",
    "\n",
    "fig.tight_layout()\n",
    "\n",
    "ax.tick_params(\n",
    "    labelbottom=False,\n",
    "    labelleft=False\n",
    ")\n",
    "\n",
    "# ---- SAVE HIGH-QUALITY PDF ----\n",
    "fig.savefig(\"pdf/Fig_6.6-1.pdf\", bbox_inches=\"tight\", pad_inches=0.02)\n",
    "\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ce9572ad",
   "metadata": {},
   "source": [
    "# Figure 7.3-1 Isotherms of the van der Waals equation in the pressure-volume plane\n",
    "\n",
    "This figure is similar to figure 6.6-1, but it labels the curves differently."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "b3546598",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 34864 characters]",
      "text/plain": [
       "<Figure size 500x450 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import matplotlib as mpl\n",
    "from matplotlib.transforms import offset_copy\n",
    "\n",
    "# May cause problems in Colab or installations without LaTeX\n",
    "mpl.rcParams[\"text.usetex\"] = True\n",
    "\n",
    "# Good PDF defaults\n",
    "mpl.rcParams.update({\n",
    "    \"pdf.fonttype\": 42,   # keep text as text in PDF\n",
    "    \"ps.fonttype\": 42,\n",
    "})\n",
    "\n",
    "# Function to make plot labels\n",
    "def label_curve(ax, x, y, text, *, dx=2, dy=3, ha=\"left\", va=\"bottom\", fontsize=9, bbox=False):\n",
    "    # Place a text label in data coordinates with a point offset.\n",
    "    transform = offset_copy(ax.transData, fig=ax.figure, x=dx, y=dy, units=\"points\")\n",
    "\n",
    "    bbox_kw = None\n",
    "    if bbox:\n",
    "        bbox_kw = dict(facecolor=\"white\", edgecolor=\"none\", boxstyle=\"round,pad=0.2\")\n",
    "\n",
    "    ax.text(float(x), float(y), text, ha=ha, va=va, fontsize=fontsize, transform=transform,\n",
    "        bbox=bbox_kw)\n",
    "\n",
    "# Function to label local minimum and maximum for isotherms below Tc\n",
    "def label_loop_extrema(ax, Vr, P, *, label_min=\"A\", label_max=\"B\",\n",
    "                       dy_min=-8, dy_max=6, marker=True):\n",
    "    # numerical derivative\n",
    "    dP = np.gradient(P, Vr)\n",
    "    s = np.sign(dP)\n",
    "\n",
    "    # sign flips\n",
    "    imax = np.where((s[:-1] > 0) & (s[1:] < 0))[0] + 1   # + to - : local max\n",
    "    imin = np.where((s[:-1] < 0) & (s[1:] > 0))[0] + 1   # - to + : local min\n",
    "\n",
    "    if len(imax) == 0 or len(imin) == 0:\n",
    "        return None  # no loop/extrema found (near/above critical)\n",
    "\n",
    "    iB = imax[0]   # local maximum\n",
    "    iA = imin[0]   # local minimum\n",
    "\n",
    "    if marker:\n",
    "        ax.plot(Vr[iA], P[iA], \"o\", ms=4, color=\"black\")\n",
    "        ax.plot(Vr[iB], P[iB], \"o\", ms=4, color=\"black\")\n",
    "\n",
    "    label_curve(ax, Vr[iA], float(P[iA]), label_min, dx=-2, dy=dy_min, \n",
    "                ha=\"left\", va=\"top\", bbox=False)\n",
    "    label_curve(ax, Vr[iB], float(P[iB]), label_max, dx =-2, dy=dy_max, \n",
    "                ha=\"left\", va=\"bottom\", bbox=False)\n",
    "\n",
    "    return (Vr[iA], P[iA], Vr[iB], P[iB])\n",
    "\n",
    "# Make the plot\n",
    "fig, ax = plt.subplots(figsize=(5.0, 4.5))  # choose size you like\n",
    "\n",
    "# Range of reduced molar volumes\n",
    "Vr_max = 3\n",
    "Vr_min = 0.5\n",
    "Vr = np.linspace(Vr_min,Vr_max,1000)\n",
    "\n",
    "\"\"\"\n",
    "Plot five isotherms\n",
    "\"\"\"\n",
    "label_horiz_Vr = 2\n",
    "\n",
    "# These are the labels for the sub-critical isotherm minima and maxima\n",
    "extrema_labels = {\n",
    "    0.9:  (r\"$A$\",  r\"$B$\"),\n",
    "    0.95: (r\"$A'$\", r\"$B'$\"),\n",
    "}\n",
    "\n",
    "for k, Tr in enumerate([0.9, 0.95, 1.0, 1.1, 1.2], start=1):\n",
    "    P = rvdw_P(Vr, Tr)\n",
    "    ax.plot(Vr, P, '-', color=\"black\", linewidth=1)\n",
    "\n",
    "    # label the isotherm itself\n",
    "    i = np.argmin(np.abs(Vr - label_horiz_Vr))\n",
    "    label_curve(ax, Vr[i], float(P[i]), rf\"$T_{k}$\", dy=2)\n",
    "\n",
    "    # label loop extrema for the subcritical ones\n",
    "    labels = extrema_labels.get(Tr)\n",
    "    if labels is not None:\n",
    "        label_loop_extrema(\n",
    "            ax, Vr, P,\n",
    "            label_min=labels[0],\n",
    "            label_max=labels[1],\n",
    "            dy_min=15,\n",
    "            dy_max=5\n",
    "        )\n",
    "\n",
    "# Mark and label the critical point\n",
    "plt.plot(1, 1, \"o\", ms=4, color=\"black\")\n",
    "label_curve(ax, 1.0, float(np.asarray(rvdw_P(1.0, 1.0)).squeeze()), \n",
    "            \"C\", dx = -2, dy=6, bbox=True)\n",
    "\n",
    "# Plot scaling and axes labels\n",
    "plt.ylim([0.4,1.4])\n",
    "plt.xlabel(r\"$\\underline{V}$\", labelpad=15)\n",
    "plt.ylabel(r'$P$', rotation=0, labelpad=20)\n",
    "\n",
    "fig.tight_layout()\n",
    "\n",
    "# Hide the tick labels\n",
    "ax.tick_params(\n",
    "    labelbottom=False,\n",
    "    labelleft=False\n",
    ")\n",
    "\n",
    "# ---- SAVE HIGH-QUALITY PDF ----\n",
    "fig.savefig(\"pdf/Fig_7.3-1.pdf\", bbox_inches=\"tight\", pad_inches=0.02)\n",
    "\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0acfc455",
   "metadata": {},
   "source": [
    "# Fugacity calculation for VLE\n",
    "\n",
    "Next, we will build the vapor-liquid equilibrium line using a fugacity calculation. This will give us the vapor and liquid molar volumes at the vapor pressure. We use this to draw tie lines through the vapor-liquid equlibrium."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cbffc9d7",
   "metadata": {},
   "source": [
    "## Fugacity calculation for the van der Waals equation of state\n",
    "\n",
    "The fugacity of the van der Waals equation of state is\n",
    "\n",
    "$$ \\ln \\frac{f}{P} = (Z-1) -\\ln (Z-B) - \\frac{A}{Z} $$\n",
    "\n",
    "First we define a function that evaluates the equation. Then we define a function to calculate the residual of the vapor and liquid fugacities."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "38cd8c8e",
   "metadata": {},
   "outputs": [],
   "source": [
    "\"\"\"Fugacity coefficient of the van der Waals EOS\n",
    "Returns the natural log of the fugacity coefficient\n",
    "\"\"\"\n",
    "import numpy as np\n",
    "\n",
    "def fugacity_vdw(Z,A,B):\n",
    "    return (Z-1) - np.log(Z-B)-A/Z"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fa8878ef",
   "metadata": {},
   "source": [
    "### Residual calculation\n",
    "\n",
    "The following function calculates the residual between the liquid and vapor fugacities from the EOS. We will use this and Newton's method to solve\n",
    "\n",
    "$$ \\ln \\frac{f^V}{P} - \\ln \\frac{f^L}{P}  = 0$$\n",
    "\n",
    "After defining the function, we test it with a calculation.\n",
    "\n",
    "Newton's method will fail if the the reduced pressure guess is too far off. In this case, the pressure will be outside of the van der Waals loops of the isotherm. We can see this using the calculation above and noting the local maximum and minimum (reduced) pressures from the isotherms we plotted earlier.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "a8782881",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Function that returns the residual of the fugacity calculation\n",
    "def residual_lnphi(Pr):\n",
    "    \"\"\"\n",
    "    Returns ln(phi_L) - ln(phi_V) at given the reduced pressure Pr.\n",
    "    At P_r^sat, this should be zero.\n",
    "    As written, requires globals Tr\n",
    "    \"\"\"\n",
    "    \n",
    "    A = 27*Pr/64/Tr**2\n",
    "    B = Pr/8/Tr\n",
    "\n",
    "    # All compressibility roots\n",
    "    V_real = rvdw_V(Pr,Tr)\n",
    "    Z_real = V_real*Pr/Tr*3/8\n",
    "\n",
    "    ZL = np.min(Z_real)  # liquid root (smallest Z)\n",
    "    ZV = np.max(Z_real)  # vapor root (largest Z)\n",
    "\n",
    "    ln_f_L = fugacity_vdw(ZL, A, B)  # ln(phi_L)\n",
    "    ln_f_V = fugacity_vdw(ZV, A, B)  # ln(phi_V)\n",
    "\n",
    "    return ln_f_L - ln_f_V"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b02b7331",
   "metadata": {},
   "source": [
    "## Create a dataframe of the resulting VLE\n",
    "\n",
    "With a method for calculating the vapor-liquid equilibrium line using fugacity, we construct a dataframe of the values. The dataframe will be a table that gives the equilibrium temperature, pressure, and molar volumes of the liquid and vapor in reduced values. We also include the vapor and liquid compressibility factors.\n",
    "\n",
    "We use a large number of points to get close to the critical point. Each guess of the vapor pressure for a higher isotherm is the previous vapor pressure. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "3f46e5d9",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>T</th>\n",
       "      <th>P</th>\n",
       "      <th>vL</th>\n",
       "      <th>vV</th>\n",
       "      <th>zL</th>\n",
       "      <th>zV</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0.620000</td>\n",
       "      <td>0.104701</td>\n",
       "      <td>0.438683</td>\n",
       "      <td>14.146629</td>\n",
       "      <td>0.027781</td>\n",
       "      <td>0.895866</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0.620038</td>\n",
       "      <td>0.104737</td>\n",
       "      <td>0.438695</td>\n",
       "      <td>14.142255</td>\n",
       "      <td>0.027789</td>\n",
       "      <td>0.895842</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0.620076</td>\n",
       "      <td>0.104773</td>\n",
       "      <td>0.438707</td>\n",
       "      <td>14.137883</td>\n",
       "      <td>0.027798</td>\n",
       "      <td>0.895818</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>0.620114</td>\n",
       "      <td>0.104809</td>\n",
       "      <td>0.438719</td>\n",
       "      <td>14.133512</td>\n",
       "      <td>0.027806</td>\n",
       "      <td>0.895794</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>0.620152</td>\n",
       "      <td>0.104845</td>\n",
       "      <td>0.438731</td>\n",
       "      <td>14.129143</td>\n",
       "      <td>0.027815</td>\n",
       "      <td>0.895770</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9995</th>\n",
       "      <td>0.998848</td>\n",
       "      <td>0.995400</td>\n",
       "      <td>0.936057</td>\n",
       "      <td>1.072259</td>\n",
       "      <td>0.349809</td>\n",
       "      <td>0.400709</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9996</th>\n",
       "      <td>0.998886</td>\n",
       "      <td>0.995551</td>\n",
       "      <td>0.937057</td>\n",
       "      <td>1.070984</td>\n",
       "      <td>0.350223</td>\n",
       "      <td>0.400278</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9997</th>\n",
       "      <td>0.998924</td>\n",
       "      <td>0.995702</td>\n",
       "      <td>0.938076</td>\n",
       "      <td>1.069691</td>\n",
       "      <td>0.350644</td>\n",
       "      <td>0.399840</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9998</th>\n",
       "      <td>0.998962</td>\n",
       "      <td>0.995854</td>\n",
       "      <td>0.939116</td>\n",
       "      <td>1.068376</td>\n",
       "      <td>0.351073</td>\n",
       "      <td>0.399394</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9999</th>\n",
       "      <td>0.999000</td>\n",
       "      <td>0.996005</td>\n",
       "      <td>0.940177</td>\n",
       "      <td>1.067041</td>\n",
       "      <td>0.351509</td>\n",
       "      <td>0.398941</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>10000 rows × 6 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "             T         P        vL         vV        zL        zV\n",
       "0     0.620000  0.104701  0.438683  14.146629  0.027781  0.895866\n",
       "1     0.620038  0.104737  0.438695  14.142255  0.027789  0.895842\n",
       "2     0.620076  0.104773  0.438707  14.137883  0.027798  0.895818\n",
       "3     0.620114  0.104809  0.438719  14.133512  0.027806  0.895794\n",
       "4     0.620152  0.104845  0.438731  14.129143  0.027815  0.895770\n",
       "...        ...       ...       ...        ...       ...       ...\n",
       "9995  0.998848  0.995400  0.936057   1.072259  0.349809  0.400709\n",
       "9996  0.998886  0.995551  0.937057   1.070984  0.350223  0.400278\n",
       "9997  0.998924  0.995702  0.938076   1.069691  0.350644  0.399840\n",
       "9998  0.998962  0.995854  0.939116   1.068376  0.351073  0.399394\n",
       "9999  0.999000  0.996005  0.940177   1.067041  0.351509  0.398941\n",
       "\n",
       "[10000 rows x 6 columns]"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import pandas as pd\n",
    "from scipy.optimize import newton\n",
    "\n",
    "# Temperature, pressure, and volume are in reduced units\n",
    "\n",
    "# Example temperature grid\n",
    "# Many points are used to keep the solver working near the c.p.\n",
    "T_min = 0.62\n",
    "T_max = 0.999\n",
    "n_T   = 10000\n",
    "\n",
    "T_list = np.linspace(T_min, T_max, n_T)\n",
    "\n",
    "rows = []\n",
    "P_guess = 0.2  # A good guess for Tr close to 0.7\n",
    "\n",
    "for Tr in T_list:\n",
    "    Psat = newton(residual_lnphi, P_guess)\n",
    "\n",
    "    V_roots = np.array(rvdw_V(Psat, Tr), dtype=complex)\n",
    "    V_real = V_roots[np.isreal(V_roots)].real\n",
    "    V_real = V_real[np.isfinite(V_real)]\n",
    "\n",
    "    if V_real.size == 0:\n",
    "        rows.append({\"T\": Tr, \"P\": Psat, \"vL\": np.nan, \"vV\": np.nan})\n",
    "        continue\n",
    "\n",
    "    vL = V_real.min()\n",
    "    vV = V_real.max()\n",
    "\n",
    "    zL = vL*Psat/Tr*3/8\n",
    "    zV = vV*Psat/Tr*3/8\n",
    "\n",
    "    rows.append({\"T\": Tr, \"P\": Psat, \"vL\": vL, \"vV\": vV, \"zL\": zL, \"zV\": zV})\n",
    "    P_guess = Psat\n",
    "\n",
    "df_eq = pd.DataFrame(rows)\n",
    "df_eq"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a7184f6c",
   "metadata": {},
   "source": [
    "# Figure 7.3-2 A low-temperature isotherm of the van der Waals equation\n",
    "\n",
    "Here we reconstruct Figure 7.3-2, which shows an isotherm below $T_c$.\n",
    "\n",
    "This is a busy plot. We have to identify and label points on the curve. Several helper functions are defined in order to create the labels."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "1fa98126",
   "metadata": {},
   "outputs": [],
   "source": [
    "\"\"\" \n",
    "These are helper functions for generating plot 7.3-2\n",
    "\n",
    "find_loop_extrema -- finds the local min and max of the isotherm\n",
    "draw_loop_extrema -- labels the local min and max\n",
    "root_between_extrema_bisect -- finds the unstable solution molar volume\n",
    "label_bottom_axis -- routine for adding bottom axis labels\n",
    "label_left_axis -- routine for adding left axis labels\n",
    "\"\"\"\n",
    "\n",
    "\n",
    "# --- Find the local maximum and minimum along a van der Waals loop\n",
    "def find_loop_extrema(Vr, P):\n",
    "    \"\"\"\n",
    "    Find local minimum (A) and local maximum (B) on a vdW loop.\n",
    "\n",
    "    Returns a dict with:\n",
    "      - iA, VA, PA : index/coords of local minimum\n",
    "      - iB, VB, PB : index/coords of local maximum\n",
    "    or None if no extrema are found.\n",
    "    \"\"\"\n",
    "    Vr = np.asarray(Vr)\n",
    "    P  = np.asarray(P)\n",
    "\n",
    "    dP = np.gradient(P, Vr)\n",
    "    s  = np.sign(dP)\n",
    "\n",
    "    imax = np.where((s[:-1] > 0) & (s[1:] < 0))[0] + 1  # + -> - : max\n",
    "    imin = np.where((s[:-1] < 0) & (s[1:] > 0))[0] + 1  # - -> + : min\n",
    "\n",
    "    if len(imax) == 0 or len(imin) == 0:\n",
    "        return None\n",
    "\n",
    "    iB = int(imax[0])\n",
    "    iA = int(imin[0])\n",
    "\n",
    "    return {\n",
    "        \"iA\": iA, \"VA\": float(Vr[iA]), \"PA\": float(P[iA]),\n",
    "        \"iB\": iB, \"VB\": float(Vr[iB]), \"PB\": float(P[iB]),\n",
    "    }\n",
    "\n",
    "# --- Given a minimum or maximum, label the point on the curve\n",
    "def draw_loop_extrema(ax, extrema, *, label_min=\"A\", label_max=\"B\",\n",
    "                      dx=-2, dy_min=-8, dy_max=6, marker=True, bbox=False):\n",
    "    \"\"\"\n",
    "    Given the output of find_loop_extrema, draw markers + labels on ax.\n",
    "    \"\"\"\n",
    "    if extrema is None:\n",
    "        return None\n",
    "\n",
    "    VA, PA = extrema[\"VA\"], extrema[\"PA\"]\n",
    "    VB, PB = extrema[\"VB\"], extrema[\"PB\"]\n",
    "\n",
    "    if marker:\n",
    "        ax.plot(VA, PA, \"o\", ms=4, color=\"black\")\n",
    "        ax.plot(VB, PB, \"o\", ms=4, color=\"black\")\n",
    "\n",
    "    label_curve(ax, VA, PA, label_min, dx=dx, dy=dy_min,\n",
    "                ha=\"left\", va=\"top\", bbox=bbox)\n",
    "    label_curve(ax, VB, PB, label_max, dx=dx, dy=dy_max,\n",
    "                ha=\"left\", va=\"bottom\", bbox=bbox)\n",
    "\n",
    "    return extrema\n",
    "\n",
    "# --- Routine to find the unstable molar volume where P = Pvap\n",
    "def root_between_extrema_bisect(Tr, Pvap, VA, VB, *, tol=1e-10, max_iter=200):\n",
    "    lo, hi = (VB, VA) if VB < VA else (VA, VB)\n",
    "\n",
    "    def f(V):\n",
    "        return float(np.asarray(rvdw_P(V, Tr)).squeeze()) - Pvap\n",
    "\n",
    "    flo, fhi = f(lo), f(hi)\n",
    "    if flo == 0.0: return lo\n",
    "    if fhi == 0.0: return hi\n",
    "    if flo * fhi > 0:\n",
    "        return None  # not bracketed\n",
    "\n",
    "    for _ in range(max_iter):\n",
    "        mid = 0.5 * (lo + hi)\n",
    "        fmid = f(mid)\n",
    "\n",
    "        if abs(fmid) < tol or (hi - lo) < tol:\n",
    "            return mid\n",
    "\n",
    "        if flo * fmid < 0:\n",
    "            hi, fhi = mid, fmid\n",
    "        else:\n",
    "            lo, flo = mid, fmid\n",
    "\n",
    "    return 0.5 * (lo + hi)\n",
    "\n",
    "from matplotlib.transforms import blended_transform_factory\n",
    "\n",
    "# --- Routine to label x-axis\n",
    "def label_bottom_axis(ax, x, text, *, dy=-10, fontsize=9):\n",
    "    trans = blended_transform_factory(ax.transData, ax.transAxes)\n",
    "    text_trans = offset_copy(trans, fig=ax.figure, y=dy, units=\"points\")\n",
    "\n",
    "    ax.text(\n",
    "        x, 0.0, text,\n",
    "        transform=text_trans,\n",
    "        ha=\"center\",\n",
    "        va=\"top\",\n",
    "        fontsize=fontsize\n",
    "    )\n",
    "\n",
    "# -- Routine to label y-axis\n",
    "def label_left_axis(ax, y, text, *, dx=-5, fontsize=9):\n",
    "    trans = blended_transform_factory(ax.transAxes, ax.transData)\n",
    "    text_trans = offset_copy(trans, fig=ax.figure, x=dx, units=\"points\")\n",
    "\n",
    "    ax.text(\n",
    "        0.0, y, text,\n",
    "        transform=text_trans,\n",
    "        ha=\"right\",\n",
    "        va=\"center\",\n",
    "        fontsize=fontsize\n",
    "    )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "ce25198f",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 14656 characters]",
      "text/plain": [
       "<Figure size 500x250 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import matplotlib as mpl\n",
    "\n",
    "# May cause problems in Colab or installations without LaTeX\n",
    "mpl.rcParams[\"text.usetex\"] = True\n",
    "\n",
    "# Good PDF defaults\n",
    "mpl.rcParams.update({\n",
    "    \"pdf.fonttype\": 42,   # keep text as text in PDF\n",
    "    \"ps.fonttype\": 42,\n",
    "})\n",
    "\n",
    "# Make the plot\n",
    "fig, ax = plt.subplots(figsize=(5.0, 2.5))  # choose size you like\n",
    "\n",
    "\"\"\"\n",
    "Plot one isotherm and add plot annotations\n",
    "\"\"\"\n",
    "# Range of reduced molar volumes\n",
    "Vr_min = 0.5\n",
    "Vr_max = 2.1\n",
    "Vr = np.linspace(Vr_min,Vr_max,1000)\n",
    "\n",
    "# Plot the isotherm and label it\n",
    "Tr = 0.95\n",
    "P = rvdw_P(Vr, Tr)\n",
    "ax.plot(Vr, P, '-', color=\"black\", linewidth=1)\n",
    "\n",
    "i = np.argmin(np.abs(Vr - label_horiz_Vr))\n",
    "label_curve(ax, 0.8*Vr_max, rvdw_P(0.8*Vr_max, Tr), rf\"$T<T_c$\", dy=15)\n",
    "\n",
    "# label loop extrema for the isotherm\n",
    "ext = find_loop_extrema(Vr, P)  # get VA,PA,VB,PB for calculations\n",
    "                                # ext[\"VA\"], ext[\"PA\"], ext[\"VB\"], ext[\"PB\"]\n",
    "\n",
    "draw_loop_extrema(ax, ext, label_min=r\"$a$\", label_max=r\"$b$\",\n",
    "                  dy_min=-3, dy_max=5, marker=False, bbox=False)\n",
    "\n",
    "# Add a line at the vapor pressure of for this isotherm\n",
    "# This finds the closest tie line in the dataframe\n",
    "# Include markers at the vapor and liquid molar volumes\n",
    "row = df_eq.loc[(df_eq[\"T\"] - Tr).abs().idxmin()]\n",
    "Pvap  = row[\"P\"]\n",
    "vL = row[\"vL\"]\n",
    "vV = row[\"vV\"]\n",
    "ax.plot([Vr_min, Vr_max],[Pvap, Pvap], color=\"gray\", linewidth=1)\n",
    "ax.plot(vL,Pvap,\"o\",ms=4,color=\"black\")\n",
    "ax.plot(vV,Pvap,\"o\",ms=4,color=\"black\")\n",
    "\n",
    "# Add P_a and P_b lines and labels\n",
    "VA, PA = ext[\"VA\"], ext[\"PA\"]\n",
    "VB, PB = ext[\"VB\"], ext[\"PB\"]\n",
    "ax.plot([Vr_min, VA],[PA, PA], \"--\", color=\"black\", linewidth=0.5)\n",
    "ax.plot([Vr_min, VB],[PB, PB], \"--\", color=\"black\", linewidth=0.5)\n",
    "label_left_axis(ax, PA, r\"$P_a$\")\n",
    "label_left_axis(ax, PB, r\"$P_b$\")\n",
    "label_left_axis(ax, Pvap, r\"$P_\\alpha$\")\n",
    "\n",
    "# Add V-alpha and V-alpha-prime lines and labels\n",
    "ax.plot([vL, vL],[0.6, Pvap], \":\", color=\"black\", linewidth=1)\n",
    "label_bottom_axis(ax, vL, r\"$\\underline{V}_\\alpha$\")\n",
    "ax.plot([vV, vV],[0.6, Pvap], \":\", color=\"black\", linewidth=1)\n",
    "label_bottom_axis(ax, vV, r\"$\\underline{V}_\\alpha'$\")\n",
    "\n",
    "# Label the unstable molar volume of the isotherm, V-alpha-doubleprime\n",
    "VU = root_between_extrema_bisect(Tr, Pvap, ext[\"VA\"], ext[\"VB\"])\n",
    "PU = Pvap\n",
    "ax.plot(VU, PU, \"o\", ms=4, color=\"black\")\n",
    "ax.plot([VU, VU],[0.6, Pvap], \":\", color=\"black\", linewidth=1)\n",
    "label_bottom_axis(ax, VU, r\"$\\underline{V}_\\alpha''$\")\n",
    "\n",
    "# Label area I and area II\n",
    "ax.text((((VU-vL)/2+vL)-VA)/2+VA, (Pvap-PA)/2+PA, \n",
    "        r\"$I$\", ha=\"center\", va=\"center\", fontsize=10)\n",
    "ax.text(VB, (PB-Pvap)/2+Pvap, r\"$II$\", ha=\"center\", va=\"center\", fontsize=10)\n",
    "\n",
    "\"\"\"\n",
    "Adjust plot scaling, axis labels, and figure properties\n",
    "\"\"\" \n",
    "\n",
    "# Plot scaling\n",
    "plt.ylim([0.72,0.9])\n",
    "plt.xlim([0.6,0.9*Vr_max])\n",
    "\n",
    "# Axes labels\n",
    "plt.xlabel(r\"$\\underline{V}$\", labelpad=16)\n",
    "plt.ylabel(r'$P$', rotation=0, labelpad=20)\n",
    "\n",
    "# Hide the tick labels\n",
    "ax.tick_params(\n",
    "    labelbottom=False,\n",
    "    labelleft=False\n",
    ")\n",
    "\n",
    "fig.tight_layout()\n",
    "\n",
    "# ---- SAVE HIGH-QUALITY PDF ----\n",
    "fig.savefig(\"pdf/Fig_7.3-2.pdf\", bbox_inches=\"tight\", pad_inches=0.02)\n",
    "\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e4165ccf",
   "metadata": {},
   "source": [
    "# Figure 7.3-3 A low-temperature isotherm of a real fluid\n",
    "\n",
    "This is a simpler plot of the resulting isotherm through the two-phase region."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "116ce547",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 6804 characters]",
      "text/plain": [
       "<Figure size 500x250 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import matplotlib as mpl\n",
    "\n",
    "# May cause problems in Colab or installations without LaTeX\n",
    "mpl.rcParams[\"text.usetex\"] = True\n",
    "\n",
    "# Good PDF defaults\n",
    "mpl.rcParams.update({\n",
    "    \"pdf.fonttype\": 42,   # keep text as text in PDF\n",
    "    \"ps.fonttype\": 42,\n",
    "})\n",
    "\n",
    "# Make the plot\n",
    "fig, ax = plt.subplots(figsize=(5.0, 2.5))  # choose size you like\n",
    "\n",
    "\"\"\"\n",
    "Plot one isotherm and add plot annotations\n",
    "\"\"\"\n",
    "# Range of reduced molar volumes\n",
    "Vr_min = 0.5\n",
    "Vr_max = 2.1\n",
    "Vr = np.linspace(Vr_min,Vr_max,1000)\n",
    "\n",
    "# Plot the isotherm and label it\n",
    "# Plot from Vr_min to vL\n",
    "# at Pvap from vL to vV\n",
    "# from vV to Vr_max\n",
    "\n",
    "Tr = 0.95\n",
    "P = rvdw_P(Vr, Tr)\n",
    "\n",
    "i = np.argmin(np.abs(Vr - label_horiz_Vr))\n",
    "label_curve(ax, 0.8*Vr_max, rvdw_P(0.8*Vr_max, Tr), rf\"$T<T_c$\", dy=15)\n",
    "\n",
    "# Add a line at the vapor pressure of for this isotherm\n",
    "# This finds the closest tie line in the dataframe\n",
    "# Include markers at the vapor and liquid molar volumes\n",
    "row = df_eq.loc[(df_eq[\"T\"] - Tr).abs().idxmin()]\n",
    "Pvap  = row[\"P\"]\n",
    "vL = row[\"vL\"]\n",
    "vV = row[\"vV\"]\n",
    "\n",
    "# Isotherm above vapor line\n",
    "V = np.linspace(row[\"vV\"], Vr_max, 1000)\n",
    "ax.plot(V, rvdw_P(V, row[\"T\"]), color='black', linestyle='-', linewidth=1)\n",
    "\n",
    "# Isotherm below liquid line\n",
    "V = np.linspace(row[\"vL\"], Vr_min, 1000)\n",
    "ax.plot(V, rvdw_P(V, row[\"T\"]), color='black', linestyle='-', linewidth=1)\n",
    "\n",
    "# Isotherm through the VLE\n",
    "ax.plot([vL, vV],[Pvap,Pvap],color='black', linestyle='-', linewidth=1)\n",
    "\n",
    "# Add V_L and V_V lines and labels\n",
    "ax.plot([vL, vL],[0.6, Pvap], \":\", color=\"black\", linewidth=1)\n",
    "label_bottom_axis(ax, vL, r\"$\\underline{V}_L$\")\n",
    "ax.plot([vV, vV],[0.6, Pvap], \":\", color=\"black\", linewidth=1)\n",
    "label_bottom_axis(ax, vV, r\"$\\underline{V}_V$\")\n",
    "\n",
    "\"\"\"\n",
    "Adjust plot scaling, axis labels, and figure properties\n",
    "\"\"\" \n",
    "\n",
    "# Plot scaling\n",
    "plt.ylim([0.72,0.9])\n",
    "plt.xlim([0.6,0.9*Vr_max])\n",
    "\n",
    "# Axes labels\n",
    "plt.xlabel(r\"$\\underline{V}$\", labelpad=16)\n",
    "plt.ylabel(r'$P$', rotation=0, labelpad=20)\n",
    "\n",
    "# Hide the tick labels\n",
    "ax.tick_params(\n",
    "    labelbottom=False,\n",
    "    labelleft=False\n",
    ")\n",
    "\n",
    "fig.tight_layout()\n",
    "\n",
    "# ---- SAVE HIGH-QUALITY PDF ----\n",
    "fig.savefig(\"pdf/Fig_7.3-3.pdf\", bbox_inches=\"tight\", pad_inches=0.02)\n",
    "\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "964803b5",
   "metadata": {},
   "source": [
    "# Figure 7.3-4 The van der Waals fluid with the vapor-liquid coexistence region identified.\n",
    "\n",
    "Generate a high-resolution PDF version of the van der Waals PV plot."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "3d18811d",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 55852 characters]",
      "text/plain": [
       "<Figure size 600x450 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import seaborn as sns\n",
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "from matplotlib.transforms import offset_copy\n",
    "import numpy as np\n",
    "\n",
    "mpl.rcParams[\"text.usetex\"] = True\n",
    "\n",
    "# Good PDF defaults\n",
    "mpl.rcParams.update({\n",
    "    \"pdf.fonttype\": 42,   # keep text as text in PDF\n",
    "    \"ps.fonttype\": 42,\n",
    "})\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(6.0, 4.5))  # choose size you like\n",
    "\n",
    "# plot several isotherms above the critical temperature\n",
    "for Tr in np.arange(1, 3.5, 0.5):\n",
    "    Vr_max = 14\n",
    "    Vr_min = 0.4\n",
    "    Vr = np.linspace(Vr_min, Vr_max, 1000)\n",
    "\n",
    "    P = rvdw_P(Vr, Tr)\n",
    "    ax.plot(Vr, P, '-', color=\"black\", linewidth=1)\n",
    "\n",
    "    # Label at Vr=8.5 using the already-computed curve (scalar-safe)\n",
    "    Vlab = 8.5\n",
    "    i = np.argmin(np.abs(Vr - Vlab))\n",
    "    Plab = float(np.asarray(P[i]).squeeze())\n",
    "\n",
    "    text_transform = offset_copy(ax.transData, fig=ax.figure, y=3, units='points')\n",
    "    ax.text(\n",
    "        float(Vr[i]),\n",
    "        Plab,\n",
    "        rf\"$T/T_c = {Tr:.1f}$\",\n",
    "        ha=\"left\",\n",
    "        va=\"bottom\",\n",
    "        fontsize=9,\n",
    "        transform=text_transform,\n",
    "        bbox=dict(facecolor=\"white\", edgecolor=\"none\", boxstyle=\"round,pad=0.2\"),\n",
    "    )\n",
    "\n",
    "# plot several isotherms below the critical temperature\n",
    "for Tr in np.array([0.7, 0.8, 0.9]):\n",
    "    Vr_max = 14\n",
    "    Vr_min = 0.4\n",
    "    Vr = np.linspace(Vr_min, Vr_max, 1000)\n",
    "\n",
    "    row = df_eq.loc[(df_eq[\"T\"] - Tr).abs().idxmin()]\n",
    "    P  = row[\"P\"]\n",
    "    vL = row[\"vL\"]\n",
    "    vV = row[\"vV\"]\n",
    "\n",
    "    ax.plot([vL, vV], [P, P], color=\"black\", linewidth=1)\n",
    "\n",
    "    text_transform = offset_copy(ax.transData, fig=ax.figure, y=-5, units='points')\n",
    "    ax.text(\n",
    "        0.5 * (vL + vV),\n",
    "        P,\n",
    "        rf\"$T/T_c = {Tr:.1f}$\",\n",
    "        color=\"black\",\n",
    "        ha=\"center\",\n",
    "        va=\"top\",\n",
    "        transform=text_transform,\n",
    "    )\n",
    "\n",
    "    # Above vapor line\n",
    "    V = np.linspace(row[\"vV\"], Vr_max, 1000)\n",
    "    ax.plot(V, rvdw_P(V, row[\"T\"]), color='black', linestyle='-', linewidth=1)\n",
    "\n",
    "    # Below liquid line\n",
    "    V = np.linspace(row[\"vL\"], Vr_min, 1000)\n",
    "    ax.plot(V, rvdw_P(V, row[\"T\"]), color='black', linestyle='-', linewidth=1)\n",
    "\n",
    "# coexistence lines behind\n",
    "sns.lineplot(data=df_eq, x=\"vV\", y=\"P\", color=\"gray\", linewidth=1,\n",
    "             ax=ax, linestyle=\"--\", zorder=0, legend=False)\n",
    "sns.lineplot(data=df_eq, x=\"vL\", y=\"P\", color=\"gray\", linewidth=1,\n",
    "             ax=ax, linestyle=\"--\", zorder=0, legend=False)\n",
    "\n",
    "ax.set_xlim(.01, 10)\n",
    "ax.set_ylim(0, 1.4)\n",
    "ax.set_xlabel(r\"$\\underline{V}/ \\underline{V}_c$\")\n",
    "ax.set_ylabel(r\"$P/P_c$\")\n",
    "\n",
    "fig.tight_layout()\n",
    "\n",
    "# ---- SAVE HIGH-QUALITY PDF ----\n",
    "fig.savefig(\"pdf/Fig_7.3-4.pdf\", bbox_inches=\"tight\", pad_inches=0.02)\n",
    "\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2b4181bf",
   "metadata": {},
   "source": [
    "# Exercises\n",
    "\n",
    "1. Plot the van der Waals PV diagram on a log-log scale.\n",
    "2. Locate and plot the spinodal in addition to the binodal."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ec213591",
   "metadata": {},
   "source": [
    "# Extra figures\n",
    "\n",
    "Here we build plots of the van der Waals EOS vapor-liquid equilibrium using different scales and incorporating the isotherms with the van der Waals loops. We draw tie lines for several reduced temperatures below the critical temperature in color and grayscale."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "8a37ffca",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 53256 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import seaborn as sns\n",
    "import matplotlib as mpl\n",
    "\n",
    "# This line can cause problems in Google Colab\n",
    "mpl.rcParams[\"text.usetex\"] = True\n",
    "\n",
    "fig, ax = plt.subplots()\n",
    "\n",
    "\n",
    "# plot several isotherms above the critical temperature\n",
    "for Tr in np.arange(1, 3.5, 0.5) : \n",
    "    Vr_max = 10\n",
    "    Vr_min = 0.4\n",
    "    Vr = np.linspace(Vr_min,Vr_max,1000)\n",
    "\n",
    "    ax.plot(Vr, rvdw_P(Vr,Tr), '-',label=Tr, color=\"gray\", linewidth=0.5)\n",
    "\n",
    "# plot several isotherms below the critical temperature\n",
    "for Tr in np.array([0.7, 0.8, 0.9]) :  \n",
    "    Vr_max = 10\n",
    "    Vr_min = 0.4\n",
    "    Vr = np.linspace(Vr_min,Vr_max,1000)\n",
    "\n",
    "    ax.plot(Vr, rvdw_P(Vr,Tr), '-',label=Tr, color=\"gray\", linewidth=0.5)\n",
    "\n",
    "    # tie lines given Tr\n",
    "    # This finds the closest tie line in the dataframe\n",
    "    row = df_eq.loc[(df_eq[\"T\"] - Tr).abs().idxmin()]\n",
    "    P  = row[\"P\"]\n",
    "    vL = row[\"vL\"]\n",
    "    vV = row[\"vV\"]\n",
    "    ax.plot([vL, vV], [P, P], color=\"magenta\", linewidth=2) \n",
    "\n",
    "# Plot the vapor coexistence line\n",
    "sns.lineplot(data=df_eq, x=\"vV\", y=\"P\", color=\"blue\", linewidth=2, ax=ax, legend=False)\n",
    "\n",
    "# Plot the liquid coexistence line\n",
    "sns.lineplot(data=df_eq, x=\"vL\", y=\"P\", color=\"red\", linewidth=2, ax=ax, legend=False)\n",
    "\n",
    "#ax.grid(color='gray', linestyle='--', linewidth=0.5, alpha=0.7)\n",
    "ax.set_xlim(.01, 10)\n",
    "ax.set_ylim(0, 1.4)\n",
    "ax.set_xlabel(r\"$\\underline{V}/ \\underline{V}_c$\")\n",
    "ax.set_ylabel(r\"$P/P_c$\")\n",
    "ax.set_title(r\"$P-V$ diagram for van der Waals EOS\")\n",
    "\n",
    "#ax.set_xscale(\"log\")\n",
    "\n",
    "#plt.legend(frameon=True, fancybox=True, edgecolor='none')\n",
    "#plt.tight_layout()\n",
    "\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "18b7a6d0",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 54340 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import seaborn as sns\n",
    "import matplotlib as mpl\n",
    "\n",
    "# For label spacing on tie lines\n",
    "from matplotlib.transforms import offset_copy\n",
    "\n",
    "# This line can cause problems in Google Colab\n",
    "mpl.rcParams[\"text.usetex\"] = True\n",
    "\n",
    "fig, ax = plt.subplots()\n",
    "\n",
    "# plot several isotherms above the critical temperature\n",
    "for Tr in np.arange(1, 3.5, 0.5) : \n",
    "    Vr_max = 14\n",
    "    Vr_min = 0.4\n",
    "    Vr = np.linspace(Vr_min,Vr_max,1000)\n",
    "\n",
    "    ax.plot(Vr, rvdw_P(Vr,Tr), '-',label=Tr, color=\"gray\", linewidth=0.5)\n",
    "\n",
    "# plot several isotherms below the critical temperature\n",
    "for Tr in np.array([0.65, 0.8, 0.9]) : \n",
    "    Vr_max = 14\n",
    "    Vr_min = 0.4\n",
    "    Vr = np.linspace(Vr_min,Vr_max,1000)\n",
    "\n",
    "    ax.plot(Vr, rvdw_P(Vr,Tr), '-',label=Tr, color=\"gray\", linewidth=0.5)\n",
    "\n",
    "    # tie lines given Tr\n",
    "    # This finds the closest tie line in the dataframe\n",
    "    row = df_eq.loc[(df_eq[\"T\"] - Tr).abs().idxmin()]\n",
    "    P  = row[\"P\"]\n",
    "    vL = row[\"vL\"]\n",
    "    vV = row[\"vV\"]\n",
    "    ax.plot([vL, vV], [P, P], color=\"magenta\", linewidth=2) \n",
    "\n",
    "    text_transform = offset_copy(ax.transData, fig=ax.figure, y=-5, units='points')\n",
    "\n",
    "    ax.text(\n",
    "        0.5 * (vL + vV),\n",
    "        P,\n",
    "        rf\"$T_r = {Tr:.2f}$\",\n",
    "        color=\"black\",\n",
    "        ha=\"center\",\n",
    "        va=\"top\",\n",
    "        transform=text_transform\n",
    "    )\n",
    "\n",
    "# Plot the vapor coexistence line\n",
    "sns.lineplot(data=df_eq, x=\"vV\", y=\"P\", color=\"blue\", linewidth=2, ax=ax, legend=False)\n",
    "\n",
    "# Plot the liquid coexistence line\n",
    "sns.lineplot(data=df_eq, x=\"vL\", y=\"P\", color=\"red\", linewidth=2, ax=ax, legend=False)\n",
    "\n",
    "ax.set_xlim(.4, 14)\n",
    "ax.set_ylim(0.1, 2)\n",
    "ax.set_xlabel(r\"$\\underline{V}/ \\underline{V}_c$\")\n",
    "ax.set_ylabel(r\"$P/P_c$\")\n",
    "#ax.set_title(r\"$P-V$ diagram for van der Waals EOS\")\n",
    "ax.plot(1,1,\"mo\")\n",
    "\n",
    "\n",
    "#ax.set_xscale(\"log\")\n",
    "\n",
    "#plt.legend(frameon=True, fancybox=True, edgecolor='none')\n",
    "#plt.tight_layout()\n",
    "\n",
    "ax.set_xscale(\"log\")\n",
    "ax.set_yscale(\"log\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b1cb7c4f",
   "metadata": {},
   "source": [
    "## Grayscale, log-log scale\n",
    "\n",
    "Notice that the \"equal areas\" of the van der Waals loop is more difficult to see on this scale. One of the interesting things about this plot is the shape of the binodal along the vapor curve and the fact that, far from the critical point, the vapor isotherms as they approach the binodal behave nearly ideally. That is an assumption made deriving the Clausius-Clapeyron equation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "c50cde2b",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 60028 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import seaborn as sns\n",
    "import matplotlib as mpl\n",
    "\n",
    "# For label spacing on tie lines\n",
    "from matplotlib.transforms import offset_copy\n",
    "\n",
    "# This line can cause problems in Google Colab\n",
    "mpl.rcParams[\"text.usetex\"] = True\n",
    "\n",
    "fig, ax = plt.subplots()\n",
    "\n",
    "# plot several isotherms above the critical temperature\n",
    "for Tr in np.arange(1, 3.5, 0.5) : \n",
    "    Vr_max = 14\n",
    "    Vr_min = 0.4\n",
    "    Vr = np.linspace(Vr_min,Vr_max,1000)\n",
    "\n",
    "    ax.plot(Vr, rvdw_P(Vr,Tr), '-',label=Tr, color=\"black\", linewidth=1)\n",
    "\n",
    "# plot several isotherms below the critical temperature\n",
    "for Tr in np.array([0.65, 0.8, 0.9]) : \n",
    "    Vr_max = 14\n",
    "    Vr_min = 0.4\n",
    "    Vr = np.linspace(Vr_min,Vr_max,1000)\n",
    "\n",
    "    ax.plot(Vr, rvdw_P(Vr,Tr), '',label=Tr, color=\"gray\", linewidth=0.5)\n",
    "\n",
    "    # tie lines given Tr\n",
    "    # This finds the closest tie line in the dataframe\n",
    "    row = df_eq.loc[(df_eq[\"T\"] - Tr).abs().idxmin()]\n",
    "    P  = row[\"P\"]\n",
    "    vL = row[\"vL\"]\n",
    "    vV = row[\"vV\"]\n",
    "    ax.plot([vL, vV], [P, P], color=\"black\", linewidth=1) \n",
    "\n",
    "    text_transform = offset_copy(ax.transData, fig=ax.figure, y=-5, units='points')\n",
    "\n",
    "    ax.text(\n",
    "        0.5 * (vL + vV),\n",
    "        P,\n",
    "        rf\"$T_r = {Tr:.2f}$\",\n",
    "        color=\"black\",\n",
    "        ha=\"center\",\n",
    "        va=\"top\",\n",
    "        transform=text_transform\n",
    "    )\n",
    "\n",
    "    # Plot the isotherm above the vapor line\n",
    "    V = np.linspace(row[\"vV\"],Vr_max,1000)\n",
    "    plt.plot(V, rvdw_P(V,row[\"T\"]), color='black', linestyle='-', linewidth=1)\n",
    "\n",
    "    # Plot the isotherm below the liquid line\n",
    "    V = np.linspace(row[\"vL\"],Vr_min,1000)\n",
    "    plt.plot(V, rvdw_P(V,row[\"T\"]), color='black', linestyle='-', linewidth=1)\n",
    "\n",
    "# Plot the vapor coexistence line\n",
    "sns.lineplot(data=df_eq, \n",
    "             x=\"vV\", y=\"P\", \n",
    "             color=\"gray\", \n",
    "             linewidth=1, \n",
    "             ax=ax, \n",
    "             linestyle=\"--\",\n",
    "             zorder=0,\n",
    "             legend=False)\n",
    "\n",
    "# Plot the liquid coexistence line\n",
    "sns.lineplot(data=df_eq, \n",
    "             x=\"vL\", y=\"P\", \n",
    "             color=\"gray\", \n",
    "             linewidth=1, \n",
    "             ax=ax, \n",
    "             linestyle=\"--\",\n",
    "             zorder=0,\n",
    "             legend=False)\n",
    "\n",
    "ax.set_xlim(.4, 14)\n",
    "ax.set_ylim(0.1, 2)\n",
    "ax.set_xlabel(r\"$\\underline{V}/ \\underline{V}_c$\")\n",
    "ax.set_ylabel(r\"$P/P_c$\")\n",
    "\n",
    "ax.set_xscale(\"log\")\n",
    "ax.set_yscale(\"log\")\n",
    "plt.show()"
   ]
  }
 ],
 "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
}
