{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "73040203-bdb6-4e2e-b631-64da6d08a77d",
   "metadata": {},
   "source": [
    "## Problem 4"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "13c903f6-7b59-413c-88b3-7fa98f2c29ea",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Computing PR saturation curve for CO2...\n",
      "T= 220.000 K  Psat=  5.93635 bar  ZL= 0.0117  ZV= 0.9175\n",
      "T= 222.120 K  Psat=  6.47773 bar  ZL= 0.0128  ZV= 0.9119\n",
      "T= 224.240 K  Psat=  7.05598 bar  ZL= 0.0139  ZV= 0.9061\n",
      "T= 226.360 K  Psat=  7.67271 bar  ZL= 0.0151  ZV= 0.9001\n",
      "T= 228.480 K  Psat=  8.32954 bar  ZL= 0.0164  ZV= 0.8938\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 62228 characters]",
      "text/plain": [
       "<Figure size 1100x420 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "#!/usr/bin/env python3\n",
    "# -*- coding: utf-8 -*-\n",
    "\"\"\"\n",
    "Pure-component saturation with Peng–Robinson EOS (phi^L = phi^V)\n",
    "Outputs Psat(T) *and* saturated liquid/vapor Z and molar volumes.\n",
    "Units: bar, L/mol, K\n",
    "\"\"\"\n",
    "import math\n",
    "from typing import List, Optional, Tuple\n",
    "\n",
    "try:\n",
    "    import numpy as np\n",
    "except Exception:\n",
    "    class _NP:\n",
    "        @staticmethod\n",
    "        def linspace(a, b, n):\n",
    "            step = (b - a) / (n - 1)\n",
    "            return [a + i * step for i in range(n)]\n",
    "    np = _NP()\n",
    "\n",
    "R = 0.08314472  # bar·L/(mol·K)\n",
    "\n",
    "def pr_params(Tc: float, Pc: float, omega: float) -> Tuple[float, float, float]:\n",
    "    a = 0.45724 * R**2 * Tc**2 / Pc\n",
    "    b = 0.07780 * R * Tc / Pc\n",
    "    kappa = 0.37464 + 1.54226 * omega - 0.26992 * omega**2\n",
    "    return a, b, kappa\n",
    "\n",
    "def alpha_pr(T: float, Tc: float, kappa: float) -> float:\n",
    "    Tr = T / Tc\n",
    "    return (1 + kappa * (1 - math.sqrt(Tr))) ** 2\n",
    "\n",
    "def A_B(a: float, b: float, alpha: float, T: float, P: float) -> Tuple[float, float]:\n",
    "    A = a * alpha * P / (R**2 * T**2)\n",
    "    B = b * P / (R * T)\n",
    "    return A, B\n",
    "\n",
    "def _real_cubic_roots(a3: float, a2: float, a1: float, a0: float) -> List[float]:\n",
    "    if abs(a3) < 1e-18:\n",
    "        if abs(a2) < 1e-18:\n",
    "            if abs(a1) < 1e-18:\n",
    "                return []\n",
    "            return [-a0 / a1]\n",
    "        disc = a1 * a1 - 4 * a2 * a0\n",
    "        if disc < 0:\n",
    "            return []\n",
    "        if abs(disc) < 1e-18:\n",
    "            return [-a1 / (2 * a2)]\n",
    "        rt = math.sqrt(disc)\n",
    "        return [(-a1 - rt) / (2 * a2), (-a1 + rt) / (2 * a2)]\n",
    "    p = a2 / a3\n",
    "    q = a1 / a3\n",
    "    r = a0 / a3\n",
    "    shift = p / 3.0\n",
    "    Acoef = q - p * p / 3.0\n",
    "    Bcoef = (2 * p**3) / 27.0 - p * q / 3.0 + r\n",
    "    disc = (Bcoef / 2.0) ** 2 + (Acoef / 3.0) ** 3\n",
    "    roots = []\n",
    "    if disc > 0:\n",
    "        sqrt_disc = math.sqrt(disc)\n",
    "        u = -Bcoef / 2.0 + sqrt_disc\n",
    "        v = -Bcoef / 2.0 - sqrt_disc\n",
    "        u_c = math.copysign(abs(u) ** (1 / 3.0), u) if u != 0 else 0.0\n",
    "        v_c = math.copysign(abs(v) ** (1 / 3.0), v) if v != 0 else 0.0\n",
    "        x = u_c + v_c\n",
    "        roots = [x - shift]\n",
    "    elif abs(disc) < 1e-18:\n",
    "        if abs(Bcoef) < 1e-18:\n",
    "            x = 0.0\n",
    "            roots = [x - shift]\n",
    "        else:\n",
    "            u = math.copysign(abs(-Bcoef / 2.0) ** (1 / 3.0), -Bcoef / 2.0)\n",
    "            roots = [2 * u - shift, -u - shift]\n",
    "    else:\n",
    "        rho = math.sqrt(-Acoef**3 / 27.0)\n",
    "        theta = math.acos(-Bcoef / (2.0 * rho))\n",
    "        m = 2 * math.sqrt(-Acoef / 3.0)\n",
    "        x1 = m * math.cos(theta / 3.0) - shift\n",
    "        x2 = m * math.cos((theta + 2 * math.pi) / 3.0) - shift\n",
    "        x3 = m * math.cos((theta + 4 * math.pi) / 3.0) - shift\n",
    "        roots = [x1, x2, x3]\n",
    "    roots = [float(r) for r in roots]\n",
    "    roots.sort()\n",
    "    return roots\n",
    "\n",
    "def pr_Z_roots(A: float, B: float) -> List[float]:\n",
    "    a3 = 1.0\n",
    "    a2 = -(1.0 - B)\n",
    "    a1 = A - 3 * B * B - 2 * B\n",
    "    a0 = -(A * B - B * B - B**3)\n",
    "    roots = _real_cubic_roots(a3, a2, a1, a0)\n",
    "    roots = [z for z in roots if z > -1e-10]\n",
    "    return roots\n",
    "\n",
    "def lnphi_pure(Z: float, A: float, B: float) -> float:\n",
    "    sqrt2 = math.sqrt(2.0)\n",
    "    term1 = Z - 1.0 - math.log(max(Z - B, 1e-300))\n",
    "    term2 = (A / (2.0 * sqrt2 * B)) * math.log(\n",
    "        max((Z + (1.0 + sqrt2) * B) / (Z + (1.0 - sqrt2) * B), 1e-300)\n",
    "    )\n",
    "    return term1 - term2\n",
    "\n",
    "def lnphi_L_minus_V_and_roots(T: float, P: float, Tc: float, Pc: float, omega: float):\n",
    "    a, b, kappa = pr_params(Tc, Pc, omega)\n",
    "    alpha = alpha_pr(T, Tc, kappa)\n",
    "    A, B = A_B(a, b, alpha, T, P)\n",
    "    roots = pr_Z_roots(A, B)\n",
    "    if len(roots) < 2:\n",
    "        return None, None, None, None, None\n",
    "    Z_L = min(roots)\n",
    "    Z_V = max(roots)\n",
    "    lnphi_L = lnphi_pure(Z_L, A, B)\n",
    "    lnphi_V = lnphi_pure(Z_V, A, B)\n",
    "    return lnphi_L - lnphi_V, Z_L, Z_V, A, B\n",
    "\n",
    "def find_psat_at_T(T: float, Tc: float, Pc: float, omega: float,\n",
    "                   Pmin: float = 1e-6, Pmax: Optional[float] = None, nscan: int = 200):\n",
    "    if Pmax is None:\n",
    "        Pmax = 0.999 * Pc\n",
    "    Ps = [Pmin * (Pmax / Pmin) ** (i / (nscan - 1)) for i in range(nscan)]\n",
    "    vals = []\n",
    "    for P in Ps:\n",
    "        F, ZL, ZV, A, B = lnphi_L_minus_V_and_roots(T, P, Tc, Pc, omega)\n",
    "        if F is not None and math.isfinite(F):\n",
    "            vals.append((P, F))\n",
    "    if len(vals) < 2:\n",
    "        return None\n",
    "    bracket = None\n",
    "    for (P1, F1), (P2, F2) in zip(vals[:-1], vals[1:]):\n",
    "        if F1 == 0.0:\n",
    "            P = P1\n",
    "            _, ZL, ZV, _, _ = lnphi_L_minus_V_and_roots(T, P, Tc, Pc, omega)\n",
    "            VL = ZL * R * T / P\n",
    "            VV = ZV * R * T / P\n",
    "            return P, ZL, ZV, VL, VV\n",
    "        if F1 * F2 < 0.0:\n",
    "            bracket = (P1, F1, P2, F2)\n",
    "            break\n",
    "    if bracket is None:\n",
    "        vals_sorted = sorted(vals, key=lambda t: abs(t[1]))\n",
    "        if len(vals_sorted) >= 2:\n",
    "            P1, F1 = vals_sorted[0]\n",
    "            P2, F2 = vals_sorted[1]\n",
    "            for _ in range(50):\n",
    "                if abs(F2 - F1) < 1e-14:\n",
    "                    break\n",
    "                P_new = P2 - F2 * (P2 - P1) / (F2 - F1)\n",
    "                P_new = max(Pmin, min(P_new, Pmax))\n",
    "                F_new, ZL, ZV, _, _ = lnphi_L_minus_V_and_roots(T, P_new, Tc, Pc, omega)\n",
    "                if F_new is None:\n",
    "                    P_new = 0.5 * (P2 + P1)\n",
    "                    F_new, ZL, ZV, _, _ = lnphi_L_minus_V_and_roots(T, P_new, Tc, Pc, omega)\n",
    "                    if F_new is None:\n",
    "                        break\n",
    "                if abs(F_new) < 1e-10:\n",
    "                    VL = ZL * R * T / P_new\n",
    "                    VV = ZV * R * T / P_new\n",
    "                    return P_new, ZL, ZV, VL, VV\n",
    "                P1, F1, P2, F2 = P2, F2, P_new, F_new\n",
    "        return None\n",
    "    P1, F1, P2, F2 = bracket\n",
    "    ZL=ZV=VL=VV=None\n",
    "    for _ in range(80):\n",
    "        Pm = 0.5 * (P1 + P2)\n",
    "        Fm, ZL, ZV, _, _ = lnphi_L_minus_V_and_roots(T, Pm, Tc, Pc, omega)\n",
    "        if Fm is None:\n",
    "            Pm = math.sqrt(P1 * P2)\n",
    "            Fm, ZL, ZV, _, _ = lnphi_L_minus_V_and_roots(T, Pm, Tc, Pc, omega)\n",
    "            if Fm is None:\n",
    "                break\n",
    "        if abs(Fm) < 1e-12:\n",
    "            VL = ZL * R * T / Pm\n",
    "            VV = ZV * R * T / Pm\n",
    "            return Pm, ZL, ZV, VL, VV\n",
    "        if F1 * Fm < 0:\n",
    "            P2, F2 = Pm, Fm\n",
    "        else:\n",
    "            P1, F1 = Pm, Fm\n",
    "    if ZL is None or ZV is None:\n",
    "        return None\n",
    "    Pfin = 0.5 * (P1 + P2)\n",
    "    VL = ZL * R * T / Pfin\n",
    "    VV = ZV * R * T / Pfin\n",
    "    return Pfin, ZL, ZV, VL, VV\n",
    "\n",
    "def compute_psat_curve_pr(Tc: float, Pc: float, omega: float,\n",
    "                          T_min: Optional[float] = None,\n",
    "                          T_max: Optional[float] = None,\n",
    "                          nT: int = 60):\n",
    "    if T_max is None:\n",
    "        T_max = 0.999 * Tc\n",
    "    if T_min is None:\n",
    "        T_min = 0.55 * Tc\n",
    "    Ts = np.linspace(T_min, T_max, nT)\n",
    "    rows = []\n",
    "    for T in Ts:\n",
    "        out = find_psat_at_T(T, Tc, Pc, omega)\n",
    "        if out is None:\n",
    "            rows.append((float(T), None, None, None, None, None))\n",
    "        else:\n",
    "            P, ZL, ZV, VL, VV = out\n",
    "            rows.append((float(T), float(P), float(ZL), float(ZV), float(VL), float(VV)))\n",
    "    return rows\n",
    "\n",
    "def _maybe_plot(results):\n",
    "    try:\n",
    "        import matplotlib.pyplot as plt\n",
    "    except Exception:\n",
    "        return\n",
    "    # T-P plot (single line)\n",
    "    T = [r[0] for r in results if r[1] is not None]\n",
    "    P = [r[1] for r in results if r[1] is not None]\n",
    "    if len(T) == 0:\n",
    "        return\n",
    "    fig, ax = plt.subplots(1, 2, figsize=(11,4.2))\n",
    "    ax[0].plot(T, P, marker=\"o\", lw=1.5)\n",
    "    ax[0].set_xlabel(\"Temperature, K\")\n",
    "    ax[0].set_ylabel(\"Saturation Pressure, bar\")\n",
    "    ax[0].set_title(\"PR Saturation Curve: T–P\")\n",
    "\n",
    "    # T–V plot (two branches)\n",
    "    VL = [r[4] for r in results if r[1] is not None]  # L molar volume\n",
    "    VV = [r[5] for r in results if r[1] is not None]  # L molar volume\n",
    "    ax[1].plot(T, VL, label=\"V_L (sat. liquid)\", lw=2)\n",
    "    ax[1].plot(T, VV, label=\"V_V (sat. vapor)\", lw=2)\n",
    "    ax[1].set_xlabel(\"Temperature, K\")\n",
    "    ax[1].set_ylabel(\"Molar volume, L/mol\")\n",
    "    ax[1].set_yscale(\"log\")\n",
    "    ax[1].set_title(\"Saturated Volumes\")\n",
    "    ax[1].legend()\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    # Example: CO2\n",
    "    Tc = 304.2   # K\n",
    "    Pc = 73.8    # bar\n",
    "    omega = 0.225\n",
    "    print(\"Computing PR saturation curve for CO2...\")\n",
    "    table = compute_psat_curve_pr(Tc, Pc, omega, T_min=220.0, T_max=0.995*Tc, nT=40)\n",
    "    for row in table[:5]:\n",
    "        T, P, ZL, ZV, VL, VV = row\n",
    "        print(f\"T={T:8.3f} K  Psat={P if P is not None else float('nan'):9.5f} bar  \"\n",
    "              f\"ZL={ZL if ZL is not None else float('nan'):7.4f}  \"\n",
    "              f\"ZV={ZV if ZV is not None else float('nan'):7.4f}\")\n",
    "    _maybe_plot(table)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d9f12983-dcfa-4795-91c7-11eb45aa104c",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python [conda env:base] *",
   "language": "python",
   "name": "conda-base-py"
  },
  "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.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
