{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Phase equilibrium" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "It is not necessary to use a Stream object to use phase equilibrium methods. In fact, thermosteam makes it just as easy to compute vapor-liquid equilibrium, bubble and dew points, and fugacities. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Fugacities" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The easiest way to calculate fugacities is through LiquidFugacities and GasFugacities objects:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "tags": [ "nbval-ignore-output" ] }, "outputs": [ { "data": { "text/plain": [ "array([43338.226, 57731.001])" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import thermosteam as tmo\n", "import numpy as np\n", "chemicals = tmo.Chemicals(['Water', 'Ethanol'])\n", "tmo.settings.set_thermo(chemicals)\n", "\n", "# Create a LiquidFugacities object\n", "F_l = tmo.equilibrium.LiquidFugacities(chemicals)\n", "\n", "# Compute liquid fugacities\n", "liquid_molar_composition = np.array([0.72, 0.28])\n", "f_l = F_l(x=liquid_molar_composition, T=355)\n", "f_l " ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "tags": [ "nbval-ignore-output" ] }, "outputs": [ { "data": { "text/plain": [ "array([43569.75, 57755.25])" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Create a GasFugacities object\n", "F_g = tmo.equilibrium.GasFugacities(chemicals)\n", "\n", "# Compute gas fugacities\n", "gas_molar_composition = np.array([0.43, 0.57])\n", "f_g = F_g(y=gas_molar_composition, T=355, P=101325)\n", "f_g" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Bubble and dew points" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Similarly bubble and dew point can be calculated through BubblePoint and DewPoint objects:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "BubblePointValues(T=355.00, P=109407, IDs=('Water', 'Ethanol'), z=[0.5 0.5], y=[0.344 0.656])" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Create a BubblePoint object\n", "BP = tmo.equilibrium.BubblePoint(chemicals)\n", "molar_composition = np.array([0.5, 0.5])\n", "\n", "# Solve bubble point at constant temperature\n", "bp = BP(z=molar_composition, T=355)\n", "bp" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "tags": [ "nbval-skip" ] }, "outputs": [ { "data": { "text/plain": [ "(355,\n", " 109406.50434728098,\n", " ('Water', 'Ethanol'),\n", " array([0.5, 0.5]),\n", " array([0.344, 0.656]))" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Note that the result is a BubblePointValues object which contain all results as attibutes\n", "(bp.T, bp.P, bp.IDs, bp.z, bp.y)" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "BubblePointValues(T=371.86, P=202650, IDs=('Water', 'Ethanol'), z=[0.5 0.5], y=[0.351 0.649])" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Solve bubble point at constant pressure\n", "BP(z=molar_composition, P=2*101325)" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "DewPointValues(T=355.00, P=92008, IDs=('Water', 'Ethanol'), z=[0.5 0.5], x=[0.849 0.151])" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Create a DewPoint object\n", "DP = tmo.equilibrium.DewPoint(chemicals)\n", "\n", "# Solve for dew point at constant temperautre\n", "dp = DP(z=molar_composition, T=355)\n", "dp" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "tags": [ "nbval-skip" ] }, "outputs": [ { "data": { "text/plain": [ "(355,\n", " 92008.17334352719,\n", " ('Water', 'Ethanol'),\n", " array([0.5, 0.5]),\n", " array([0.849, 0.151]))" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Note that the result is a DewPointValues object which contain all results as attibutes\n", "(dp.T, dp.P, dp.IDs, dp.z, dp.x)" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "DewPointValues(T=376.25, P=202648, IDs=('Water', 'Ethanol'), z=[0.5 0.5], x=[0.83 0.17])" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Solve for dew point at constant pressure\n", "DP(z=molar_composition, P=2*101324)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Vapor liquid equilibrium" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Vapor-liquid equilibrium can be calculated through a VLE object:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "VLE(imol=MaterialIndexer(\n", " g=[('Water', 0.5), ('Ethanol', 0.5)],\n", " l=[('Water', 0.5), ('Ethanol', 0.5)]),\n", " thermal_condition=ThermalCondition(T=298.15, P=101325))" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# First create a material indexer for the VLE object to manage material data\n", "imol = tmo.indexer.MaterialIndexer(l=[('Water', 0.5), ('Ethanol', 0.5)],\n", " g=[('Water', 0.5), ('Ethanol', 0.5)])\n", "\n", "# Create a VLE object\n", "vle = tmo.equilibrium.VLE(imol)\n", "vle" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can call the VLE object by setting 2 degrees of freedom from the following list: \n", "\n", "* `T` - Temperature [K]\n", "* `P` - Pressure [Pa]\n", "* `V` - Molar vapor fraction\n", "* `H` - Enthalpy [kJ/hr]\n", "* `S` - Entropy [kJ/K/hr]\n", "* `y` - Binary molar vapor composition\n", "* `x` - Binary molar liquid composition\n", "\n", "Here are some examples:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "VLE(imol=MaterialIndexer(\n", " g=[('Water', 0.6361), ('Ethanol', 0.8548)],\n", " l=[('Water', 0.3639), ('Ethanol', 0.1452)]),\n", " thermal_condition=ThermalCondition(T=355.00, P=101325))" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vle(T=355, P=101325)\n", "vle" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "VLE(imol=MaterialIndexer(\n", " g=[('Water', 0.6081), ('Ethanol', 0.8183)],\n", " l=[('Water', 0.3919), ('Ethanol', 0.1817)]),\n", " thermal_condition=ThermalCondition(T=373.69, P=202650))" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "mixture_enthalpy = vle.mixture.xH(imol, *vle.thermal_condition)\n", "vle(H=mixture_enthalpy, P=202650)\n", "vle" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "VLE(imol=MaterialIndexer(\n", " g=[('Water', 0.3874), ('Ethanol', 0.6126)],\n", " l=[('Water', 0.6126), ('Ethanol', 0.3874)]),\n", " thermal_condition=ThermalCondition(T=353.94, P=101325))" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vle(V=0.5, P=101325)\n", "vle" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "VLE(imol=MaterialIndexer(\n", " g=[('Water', 0.3899), ('Ethanol', 0.6101)],\n", " l=[('Water', 0.6101), ('Ethanol', 0.3899)]),\n", " thermal_condition=ThermalCondition(T=360.00, P=127822))" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vle(V=0.5, T=360)\n", "vle" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "VLE(imol=MaterialIndexer(\n", " g=[('Water', 0.8434), ('Ethanol', 0.9609)],\n", " l=[('Water', 0.1566), ('Ethanol', 0.03914)]),\n", " thermal_condition=ThermalCondition(T=356.31, P=127822))" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vle(x=np.array([0.8, 0.2]), P=101325)\n", "vle" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "VLE(imol=MaterialIndexer(\n", " g=[('Water', 0.4627), ('Ethanol', 0.6941)],\n", " l=[('Water', 0.5373), ('Ethanol', 0.3059)]),\n", " thermal_condition=ThermalCondition(T=356.31, P=126587))" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vle(y=np.array([0.4, 0.6]), T=360)\n", "vle" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that some compositions are infeasible; so it is not advised to pass x or y unless you know what you're doing:" ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "tags": [ "nbval-raises-exception" ] }, "outputs": [ { "ename": "InfeasibleRegion", "evalue": "phase composition is infeasible", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mInfeasibleRegion\u001b[0m Traceback (most recent call last)", "\u001b[1;32m~\\AppData\\Local\\Temp\\ipykernel_14104\\2096573133.py\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mvle\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0marray\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0.2\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;36m0.8\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mP\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;36m101325\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 2\u001b[0m \u001b[0mvle\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;32m~\\Code\\biosteam\\thermosteam\\thermosteam\\equilibrium\\vle.py\u001b[0m in \u001b[0;36m__call__\u001b[1;34m(self, T, P, V, H, S, x, y)\u001b[0m\n\u001b[0;32m 389\u001b[0m \u001b[0mthermal_condition\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mP\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mP\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 390\u001b[0m \u001b[1;32melif\u001b[0m \u001b[0mx_spec\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 391\u001b[1;33m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mset_Px\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mP\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0masarray\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 392\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m \u001b[1;31m# y_spec\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 393\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mset_Py\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mP\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0masarray\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0my\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;32m~\\Code\\biosteam\\thermosteam\\thermosteam\\equilibrium\\vle.py\u001b[0m in \u001b[0;36mset_Px\u001b[1;34m(self, P, x)\u001b[0m\n\u001b[0;32m 671\u001b[0m \u001b[1;32massert\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_N\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;36m2\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'number of species in equilibrium must be 2 to specify x'\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 672\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_thermal_condition\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mT\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0my\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_bubble_point\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msolve_Ty\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mP\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 673\u001b[1;33m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_lever_rule\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0my\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 674\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 675\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0mset_Ty\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mT\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0my\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;32m~\\Code\\biosteam\\thermosteam\\thermosteam\\equilibrium\\vle.py\u001b[0m in \u001b[0;36m_lever_rule\u001b[1;34m(self, x, y)\u001b[0m\n\u001b[0;32m 653\u001b[0m \u001b[0msplit_frac\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_z\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m-\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m/\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0my\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m-\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 654\u001b[0m \u001b[1;32mif\u001b[0m \u001b[1;32mnot\u001b[0m \u001b[1;33m-\u001b[0m\u001b[1;36m0.00001\u001b[0m \u001b[1;33m<\u001b[0m \u001b[0msplit_frac\u001b[0m \u001b[1;33m<\u001b[0m \u001b[1;36m1.00001\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 655\u001b[1;33m \u001b[1;32mraise\u001b[0m \u001b[0mInfeasibleRegion\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'phase composition'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 656\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0msplit_frac\u001b[0m \u001b[1;33m>\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 657\u001b[0m \u001b[0msplit_frac\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;31mInfeasibleRegion\u001b[0m: phase composition is infeasible" ] } ], "source": [ "vle(x=np.array([0.2, 0.8]), P=101325)\n", "vle" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Liquid-liquid equilibrium" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Liquid-liquid equilibrium can be calculated through a LLE object:" ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "tags": [ "nbval-skip" ] }, "outputs": [ { "data": { "text/plain": [ "LLE(imol=MolarFlowIndexer(\n", " L=[('Water', 13.35), ('Octane', 99.98), ('Butanol', 25.7)],\n", " l=[('Water', 290.6), ('Octane', 0.02062), ('Butanol', 4.3)]),\n", " thermal_condition=ThermalCondition(T=360.00, P=101325))" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "tmo.settings.set_thermo(['Water', 'Octane', 'Butanol'])\n", "imol = tmo.indexer.MolarFlowIndexer(\n", " l=[('Water', 304), ('Butanol', 30)],\n", " L=[('Octane', 100)])\n", "lle = tmo.equilibrium.LLE(imol)\n", "lle(T=360)\n", "lle" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Pressure is not a significant factor in liquid-liquid equilibrium, so only temperature is needed." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Vapor-liquid-liquid equilibrium" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For now, the only way to perform vapor-liquid-liquid equilibrium calculations is through Stream objects:" ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "tags": [ "nbval-skip" ] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "MultiStream: s\n", "phases: ('L', 'g', 'l'), T: 362 K, P: 101325 Pa\n", "flow (kmol/hr): (g) Water 97\n", " Ethanol 9.69\n", " Octane 45.8\n", " (l) Water 2.98\n", " Ethanol 0.311\n", " Octane 54.2\n" ] } ], "source": [ "tmo.settings.set_thermo(['Water', 'Ethanol', 'Octane'])\n", "s = tmo.Stream('s', Water=100, Ethanol=10, Octane=100)\n", "s.vlle(T=362, P=101325)\n", "s.show()" ] } ], "metadata": { "celltoolbar": "Tags", "kernelspec": { "display_name": "Python 3 (ipykernel)", "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.11.4" } }, "nbformat": 4, "nbformat_minor": 2 }