Determining Equilibrium Composition using Equation of State Method#

Teng-Jui Lin

Content adapted from UW CHEME 326, Chemical Engineering Thermodynamics, in Winter 2022.

Problems reprinted from Koretsky, M. D. Engineering and Chemical Thermodynamics. (Wiley, 2012).

Using van der Waals EOS to predict VLE behavior#

Koretsky Example 8.15. Using the equation of state method, determine the equilibrium composition in the vapor phase and the system pressure of a mixture of methane (1) and n-pentane (2) with a liquid mole fraction of x1=0.3 at 37.78°C. Use the van der Waals equation of state to determine fugacity for both vapor and liquid phases. Iterate until |Σyi1|<0.001.

Property calculations#

All properties are converted to SI units to avoid confusions.

Given temperature

  • T=37.78C=310.93 K

and look up Antoine coefficients (Appendix A1)

  • Methane (1)

    • A1=8.6041

    • B1=897.84

    • C1=7.16

  • n-pentane (2)

    • A2=9.2131

    • B2=2477.07

    • C2=39.94

we can calculate vapor pressure of methane and n-pentane using Antoine’s equation:

  • Psat1=exp(A1B1T+C1)=283.85 bar=283.85×105 Pa

  • Psat2=exp(A2B2T+C2)=1.07 bar=1.07×105 Pa

Given Liquid compositions

  • x1=0.3

  • x2=1x1=0.7

we can calculate the pressure as the weighted sum of the saturation pressure:

  • P=xiPsat,i=x1Psat1+x2Psat2=85.9×105 Pa

Using Raoult’s law, we can estimate the vapor compositions:

  • y1=x1Psat1P=0.991

  • y2=1y1=0.009

vdW EOS property calculations#

The results from Raoult’s law is a rough estimate, and we want to get more accurate results using the vdW EOS. Recall that the vdW EOS requires two vdW parameters a and b.

Look up critical temperatures and pressures of methane and n-pentane

  • Tc1=190.6×105 K

  • Tc2=469.6×105 K

  • Pc1=46×105 Pa

  • Pc2=33.74×105 Pa

we can calculate the vdW parameters of pure species

  • a1=2764R2Tc12Pc1=0.2303 J m3/mol3

  • a2=2764R2Tc22Pc2=1.906 J m3/mol3

  • b1=RTc18Pc1=4.31×105 m3/mol

  • b2=RTc28Pc2=1.45×104 m3/mol

and interaction term

  • a12=a1a2=1.462 J m3/mol3

where the ideal gas constant is R=8.314 J/mol K. We can then calculate vdW parameters of the mixed system liquid and vapor phase using mixing rules (note that we assumed the vdW EOS works well in liquid phase)

  • Liquid phase (we know for sure, same for every iteration)

    • al=x12a1+x22a2+2x1x2a12=1.23 Jm3/mol3

    • bl=x1b1+x2b2=1.14×105 Jm3/mol3

  • Vapor phase (an estimate for first iteration, to be recalculated for each iteration)

    • av=y12a1+y22a2+2y1y2a12=0.238 Jm3/mol3

    • bv=y1b1+y2b2=4.40×105 m3/mol

vdW EOS calculations#

The vdW EOS

P=RTvbav2

now has all the parameters. Solve for vl using R,P,T,al,bl and take the smallest root. Solve for vv using R,P,T,av,bv and take the largest root:

  • vl=1.59×104 m3/mol

  • vv=2.55×104 m3/mol

vdW fugacity coefficient calculations#

Knowing the molar volume of liquid and vapor phases, we can calculate the fugacity coefficient with equation from Koretsky Table 7.1:

lnφ^i=bivbln((vb)PRT)2n=1mykaikRTv

with appropriate properties in each phase (vapor and liquid) for each species (methane and n-pentane).

  • φ^1v=exp[b1vvbvln((vvbv)PRT)2(y1a1+y2a12)RTvv]=0.86

  • φ^2v=exp[b2vvbvln((vvbv)PRT)2(y2a2+y1a12)RTvv]=0.37

  • φ^1l=exp[b1vlblln((vlbl)PRT)2(x1a1+x2a12)RTvl]=1.30

  • φ^2l=exp[b2vlblln((vlbl)PRT)2(x2a2+x1a12)RTvl]=0.10

Warning

The Koretsky Book has typos in the latter two equations. The third term should use x1 and x2 because they’re in liquid phase, but NOT y1 and y2.

Vapor composition calculation#

We can calculate the vapor composition using fugacity coefficients:

  • y1=x1φ^1lφ^1v=0.45

  • y2=x2φ^2lφ^2v=0.18

Warning

The Koretsky Book has a typo in the y2 equation: use x2 instead of x1.

Note that the compositions does not sum to 1, indicating that further iteration is needed for the result to converge. We iterate by updating the pressure

  • PP(y1+y2)

Iteration#

Repeat calculations including and following vdW EOS property calculation until the pressure and vapor compositions converge (no longer changes a lot from last iteration).

Iteration result#

After iteration, we get P=34.5×105 Pa and y1=0.79.

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sympy import Symbol
from sympy.solvers import solve
# universal constant
R = 8.314  # J/mol K
# given properties
T = 37.78 + 273.15  # K
x1 = 0.3
x2 = 1 - x1
# formula for vdW params and saturation pressure
pure_a = lambda Tc, Pc, R: 27 / 64 * (R * Tc) ** 2 / Pc
pure_b = lambda Tc, Pc, R: 1 / 8 * (R * Tc) / Pc
antoine = lambda A, B, C, T: np.exp(A - B / (T + C)) * 1e5  # Pa
# physical properties - appendix a1
# methane (1)
A1 = 8.6041
B1 = 897.84
C1 = -7.16
Tc1 = 190.6  # K
Pc1 = 46.00e5  # Pa
a1 = pure_a(Tc1, Pc1, R)
b1 = pure_b(Tc1, Pc1, R)
# n-pentane (2)
A2 = 9.2131
B2 = 2477.07
C2 = -39.94
Tc2 = 469.6  # K
Pc2 = 33.74e5  # Pa
a2 = pure_a(Tc2, Pc2, R)
b2 = pure_b(Tc2, Pc2, R)
# mixture
a12 = np.sqrt(a1 * a2)
# saturation pressures
Psat1 = antoine(A1, B1, C1, T)
Psat2 = antoine(A2, B2, C2, T)
Psat1, Psat2
(28384910.01139723, 107495.95099231375)
# estimate total pressure
P = x1 * Psat1 + x2 * Psat2
P
8590720.169113789
# estimate vapor compositions
y1 = x1 * Psat1 / P
y2 = x2 * Psat2 / P
y1, y2
(0.9912408780389382, 0.008759121961061624)
def solve_v(T, P, a, b, R, v_before, threshold=1e-10):
    """Solve vdW EOS for v."""
    coeff = [-a * b, a, -(R * T + P * b), P]
    # solve for compressibility factor
    v = np.polynomial.polynomial.polyroots(coeff)
    # z is real and positive
    v = np.real(v)
    v = v[v > 0]
    # double check if solution is good in case of algorithmic instability
    error = np.abs(np.polynomial.polynomial.polyval(v, coeff))
    v = v[error < threshold]
    # use the root closest to the last v
    closest_idx = np.argmin(np.abs(v - v_before))
    v = v[closest_idx]
    return v
# formula for mixed vdW params
mixed_a = (
    lambda comp1, comp2, a1, a2: comp1**2 * a1
    + 2 * comp1 * comp2 * np.sqrt(a1 * a2)
    + comp2**2 * a2
)
mixed_b = lambda comp1, comp2, b1, b2: comp1 * b1 + comp2 * b2
# formula for fugacity coefficients (phi_1, for phi_2, switch arguments of 1 and 2 properties)
phi = lambda comp1, comp2, P, v, T, R, a1, a12, b, b1: np.exp(
    b1 / (v - b)
    - np.log((v - b) * P / (R * T))
    - 2 * (comp1 * a1 + comp2 * a12) / (R * T * v)
)
# initialize guess
v_v = 0
v_l = 0
# generate lists to store iteration results
vdw_a_v = []
vdw_b_v = []
vdw_P = []
vdw_y1 = []
vdw_y2 = []
vdw_v_v = []
vdw_v_l = []
vdw_phi_v1 = []
vdw_phi_v2 = []
vdw_phi_l1 = []
vdw_phi_l2 = []

# iteratre through vdW EOS
num_iter = 0
max_iter = 10000
tol = 0.001
tol_criteria = tol * 2
while tol_criteria > tol and num_iter < max_iter:
    # calculate vdW parameters for mixtures in each phase
    a_v = mixed_a(y1, y2, a1, a2)
    b_v = mixed_b(y1, y2, b1, b2)
    a_l = mixed_a(x1, x2, a1, a2)
    b_l = mixed_b(x1, x2, b1, b2)
    # solve vdW EOS for molar volume in each phase
    v_v = solve_v(T, P, a_v, b_v, R, v_before=v_v)
    v_l = solve_v(T, P, a_l, b_l, R, v_before=v_l)
    # calculate fugacity coefficient of each species in each phase
    phi_v1 = phi(y1, y2, P, v_v, T, R, a1, a12, b_v, b1)
    phi_v2 = phi(y2, y1, P, v_v, T, R, a2, a12, b_v, b2)
    phi_l1 = phi(x1, x2, P, v_l, T, R, a1, a12, b_l, b1)
    phi_l2 = phi(x2, x1, P, v_l, T, R, a2, a12, b_l, b2)
    # update vapor composition
    y1 = x1 * phi_l1 / phi_v1
    y2 = x2 * phi_l2 / phi_v2
    # update total pressure
    P = P * (y1 + y2)
    # calculate if calculation converges
    tol_criteria = abs(y1 + y2 - 1)
    num_iter += 1
    # store iteration info for debugging
    vdw_a_v.append(a_v)
    vdw_b_v.append(b_v)
    vdw_v_v.append(v_v)
    vdw_v_l.append(v_l)
    vdw_phi_v1.append(phi_v1)
    vdw_phi_v2.append(phi_v2)
    vdw_phi_l1.append(phi_l1)
    vdw_phi_l2.append(phi_l2)
    vdw_y1.append(y1)
    vdw_y2.append(y2)
    vdw_P.append(P)
pd.set_option("display.precision", 3)
pd.DataFrame({
    'vdw_a_v': vdw_a_v,
    'vdw_b_v': vdw_b_v,
    'vdw_v_v': vdw_v_v,
    'vdw_v_l': vdw_v_l,
    'vdw_phi_v1': vdw_phi_v1,
    'vdw_phi_v2': vdw_phi_v2,
    'vdw_phi_l1': vdw_phi_l1,
    'vdw_phi_l2': vdw_phi_l2,
    'vdw_y1': vdw_y1,
    'vdw_y2': vdw_y2,
    'vdw_P': vdw_P,
})
vdw_a_v vdw_b_v vdw_v_v vdw_v_l vdw_phi_v1 vdw_phi_v2 vdw_phi_l1 vdw_phi_l2 vdw_y1 vdw_y2 vdw_P
0 0.238 4.395e-05 2.550e-04 1.595e-04 0.860 0.367 1.296 0.095 0.452 0.182 5.449e+06
1 0.219 4.580e-05 4.379e-04 1.653e-04 0.908 0.558 1.780 0.120 0.588 0.151 4.027e+06
2 0.241 4.715e-05 5.969e-04 1.689e-04 0.931 0.631 2.245 0.147 0.724 0.163 3.571e+06
3 0.328 5.476e-05 6.492e-04 1.702e-04 0.944 0.606 2.472 0.161 0.786 0.186 3.469e+06
4 0.401 6.070e-05 6.429e-04 1.705e-04 0.956 0.573 2.530 0.164 0.794 0.201 3.451e+06
5 0.433 6.323e-05 6.341e-04 1.706e-04 0.962 0.558 2.541 0.165 0.792 0.207 3.447e+06
6 0.443 6.403e-05 6.309e-04 1.706e-04 0.965 0.553 2.543 0.165 0.791 0.209 3.446e+06
print(
    f"Converged in {num_iter} iterations, with convergence {round(tol_criteria, 4)} < {tol}"
)
print(f"phi_v1 = {round(phi_v1, 2)}")
print(f"phi_v2 = {round(phi_v2, 2)}")
print(f"phi_l1 = {round(phi_l1, 2)}")
print(f"phi_l2 = {round(phi_l2, 2)}")
print(f"y1 = {round(y1, 2)}")
print(f"y2 = {round(y2, 2)}")
print(f"P = {round(P)} Pa")
Converged in 7 iterations, with convergence 0.0003 < 0.001
phi_v1 = 0.96
phi_v2 = 0.55
phi_l1 = 2.54
phi_l2 = 0.17
y1 = 0.79
y2 = 0.21
P = 3445993 Pa

Using Peng-Robinson EOS to predict VLE behavior#

Koretsky Example 8.16. Repeat Example 8.15 using the Peng–Robinson equation of state. Compare your answer to the reported measured value of P = 69.1 bar and y1=0.95. (Use k12=0)

The approach is the same with using vdW EOS, but instead we use Peng-Robinson EOS:

P=RTvbaαv2+2bvb2

To solve Peng-Robinson EOS numerically, we use its cubic form:

z3(1B)z2+(A2B3B2)z(ABB2B3)=0

where

  • z=PvRT is the compressibility factor

  • A=aαP(RT)2

  • B=bPRT

We calculate fugacity coefficients using

φ^i=exp[bib(z1)ln((vb)PRT)+aα22bRT[bib2aαk=1myk(aα)ik]ln[v+(1+2)bv+(12)b]]

with careful substitution of aα,b noting the species and v noting the phase. Use xk in the sum if calculating fugacity coefficient in the liquid phase.

We calculate pure species Peng-Robinson parameters with

  • a=0.45724R2Tc2Pc

  • b=0.07780RTcPc

  • α(T)=[1+κ(1Tr)]2

    • κ=0.37464+1.54226ω0.26992ω2

      • ω is the acentric factor of the species found in Appendix A.1

and calculate Peng-Robinson parameters of mixtures using mixing rules of

  • aα=y12(aα)1+2y1y1(aα)1(aα)2(1k12)+y22(aα)2

    • k is binary interaction parameter of the two species found in literature

  • b=y1b1+y1b2

with appropriate use of x with liquid phase and y with vapor phase.

# universal constant
R = 8.314  # J/mol K
# given properties
T = 37.78 + 273.15  # K
x1 = 0.3
x2 = 1 - x1
k12 = 0
# formula for Peng-Robinson params and saturation pressure
pure_a = lambda Tc, Pc, R: 0.45724 * (R * Tc) ** 2 / Pc
pure_b = lambda Tc, Pc, R: 0.07780 * (R * Tc) / Pc
kappa = lambda omega: 0.37464 + 1.54226 * omega - 0.26992 * omega**2
alpha = lambda Tr, kappa: (1 + kappa * (1 - np.sqrt(Tr))) ** 2
antoine = lambda A, B, C, T: np.exp(A - B / (T + C)) * 1e5  # Pa
# physical properties - appendix a1
# methane (1)
A1 = 8.6041
B1 = 897.84
C1 = -7.16
omega1 = 0.008
Tc1 = 190.6  # K
Pc1 = 46.00e5  # Pa
Tr1 = T / Tc1
a1 = pure_a(Tc1, Pc1, R)
b1 = pure_b(Tc1, Pc1, R)
kappa1 = kappa(omega1)
alpha1 = alpha(Tr1, kappa1)
a_alpha_1 = a1 * alpha1
# n-pentane (2)
A2 = 9.2131
B2 = 2477.07
C2 = -39.94
omega2 = 0.251
Tc2 = 469.6  # K
Pc2 = 33.74e5  # Pa
Tr2 = T / Tc2
a2 = pure_a(Tc2, Pc2, R)
b2 = pure_b(Tc2, Pc2, R)
kappa2 = kappa(omega2)
alpha2 = alpha(Tr2, kappa2)
a_alpha_2 = a2 * alpha2
# mixture
a12 = np.sqrt(a1 * a2)
a_alpha_12 = np.sqrt(a_alpha_1 * a_alpha_2)
# saturation pressures
Psat1 = antoine(A1, B1, C1, T)
Psat2 = antoine(A2, B2, C2, T)
Psat1, Psat2
(28384910.01139723, 107495.95099231375)
# estimate total pressure
P = x1 * Psat1 + x2 * Psat2
P
8590720.169113789
# estimate vapor compositions
y1 = x1 * Psat1 / P
y2 = x2 * Psat2 / P
y1, y2
(0.9912408780389382, 0.008759121961061624)
def solve_v(T, P, a_alpha, b, R, v_before, threshold=1e-10):
    """Solve Peng-Robinson EOS for v."""
    A = a_alpha * P / (R * T) ** 2
    B = b * P / (R * T)
    coeff = [-(A * B - B**2 - B**3), A - 2 * B - 3 * B**2, -(1 - B), 1]
    # solve for compressibility factor
    z = np.polynomial.polynomial.polyroots(coeff)
    # z is real and positive
    z = np.real(z)
    z = z[z > 0]
    # double check if solution is good in case of algorithmic instability
    error = np.abs(np.polynomial.polynomial.polyval(z, coeff))
    z = z[error < threshold]
    # convert to v
    v = z * R * T / P
    v = v[v > b]
    # use the root closest to the last v
    closest_idx = np.argmin(np.abs(v - v_before))
    v = v[closest_idx]
    return v
# formula for mixed Peng-Robinson params
mixed_a_alpha = (
    lambda comp1, comp2, a_alpha_1, a_alpha_2, k12: comp1**2 * a_alpha_1
    + 2 * comp1 * comp2 * np.sqrt(a_alpha_1 * a_alpha_2) * (1 - k12)
    + comp2**2 * a_alpha_2
)
mixed_b = lambda comp1, comp2, b1, b2: comp1 * b1 + comp2 * b2
# formula for fugacity coefficients (phi_1, for phi_2, switch arguments of 1 and 2 properties)
phi = lambda comp1, comp2, P, v, T, R, a_alpha, a_alpha_1, a_alpha_12, b, b1: np.exp(
    b1 / b * (P * v / (R * T) - 1)
    - np.log((v - b) * P / (R * T))
    + a_alpha
    / (2 * np.sqrt(2) * b * R * T)
    * (b1 / b - 2 / a_alpha * (comp1 * a_alpha_1 + comp2 * a_alpha_12))
    * np.log((v + (1 + np.sqrt(2)) * b) / (v + (1 - np.sqrt(2)) * b))
)
# initialize guess
v_v = 0
v_l = 0
# generate lists to store iteration results
pr_a_alpha_v = []
pr_b_v = []
pr_P = []
pr_y1 = []
pr_y2 = []
pr_v_v = []
pr_v_l = []
pr_phi_v1 = []
pr_phi_v2 = []
pr_phi_l1 = []
pr_phi_l2 = []
pr_tol_criteria = []

# iteratre through Peng-Robinson EOS
num_iter = 0
max_iter = 10000
tol = 0.001
tol_criteria = tol * 2
while tol_criteria > tol and num_iter < max_iter:
    # calculate Peng-Robinson parameters for mixtures in each phase
    a_alpha_v = mixed_a_alpha(y1, y2, a_alpha_1, a_alpha_2, k12)
    b_v = mixed_b(y1, y2, b1, b2)
    a_alpha_l = mixed_a_alpha(x1, x2, a_alpha_1, a_alpha_2, k12)
    b_l = mixed_b(x1, x2, b1, b2)
    # solve Peng-Robinson EOS for molar volume in each phase
    v_v = solve_v(T, P, a_alpha_v, b_v, R, v_before=v_v)
    v_l = solve_v(T, P, a_alpha_l, b_l, R, v_before=v_l)
    # v_v = solve_v(T, P, a_alpha_v, b_v, R, phase='v')
    # v_l = solve_v(T, P, a_alpha_l, b_l, R, phase='l')
    # calculate fugacity coefficient of each species in each phase
    phi_v1 = phi(y1, y2, P, v_v, T, R, a_alpha_v, a_alpha_1, a_alpha_12, b_v, b1)
    phi_v2 = phi(y2, y1, P, v_v, T, R, a_alpha_v, a_alpha_2, a_alpha_12, b_v, b2)
    phi_l1 = phi(x1, x2, P, v_l, T, R, a_alpha_l, a_alpha_1, a_alpha_12, b_l, b1)
    phi_l2 = phi(x2, x1, P, v_l, T, R, a_alpha_l, a_alpha_2, a_alpha_12, b_l, b2)
    # update vapor composition
    y1 = x1 * phi_l1 / phi_v1
    y2 = x2 * phi_l2 / phi_v2
    # update total pressure
    P = P * (y1 + y2)
    # calculate if calculation converges
    tol_criteria = abs(y1 + y2 - 1)
    num_iter += 1
    # store iteration info for debugging
    pr_a_alpha_v.append(a_alpha_v)
    pr_b_v.append(b_v)
    pr_v_v.append(v_v)
    pr_v_l.append(v_l)
    pr_phi_v1.append(phi_v1)
    pr_phi_v2.append(phi_v2)
    pr_phi_l1.append(phi_l1)
    pr_phi_l2.append(phi_l2)
    pr_y1.append(y1)
    pr_y2.append(y2)
    pr_P.append(P)
    pr_tol_criteria.append(tol_criteria)
pd.set_option("display.precision", 2)
pd.DataFrame({
    'pr_a_alpha_v': pr_a_alpha_v,
    'pr_b_v': pr_b_v,
    'pr_v_v': pr_v_v,
    'pr_v_l': pr_v_l,
    'pr_phi_v1': pr_phi_v1,
    'pr_phi_v2': pr_phi_v2,
    'pr_phi_l1': pr_phi_l1,
    'pr_phi_l2': pr_phi_l2,
    'pr_y1': pr_y1,
    'pr_y2': pr_y2,
    'pr_P': pr_P,
    'pr_tol_criteria': pr_tol_criteria,
})
pr_a_alpha_v pr_b_v pr_v_v pr_v_l pr_phi_v1 pr_phi_v2 pr_phi_l1 pr_phi_l2 pr_y1 pr_y2 pr_P pr_tol_criteria
0 0.21 2.74e-05 2.58e-04 9.74e-05 0.86 0.27 2.21 0.02 0.77 0.05 7.03e+06 1.82e-01
1 0.18 2.48e-05 3.32e-04 9.82e-05 0.88 0.38 2.59 0.02 0.88 0.04 6.46e+06 8.00e-02
2 0.21 2.71e-05 3.54e-04 9.85e-05 0.89 0.37 2.77 0.02 0.94 0.04 6.31e+06 2.30e-02
3 0.24 2.88e-05 3.55e-04 9.86e-05 0.89 0.35 2.83 0.02 0.95 0.04 6.28e+06 5.74e-03
4 0.25 2.95e-05 3.53e-04 9.86e-05 0.90 0.34 2.84 0.02 0.95 0.05 6.27e+06 1.62e-03
5 0.25 2.97e-05 3.53e-04 9.86e-05 0.90 0.34 2.85 0.02 0.95 0.05 6.27e+06 5.04e-04
print(
    f"Converged in {num_iter} iterations, with convergence {round(tol_criteria, 4)} < {tol}"
)
print(f"phi_v1 = {round(phi_v1, 2)}")
print(f"phi_v2 = {round(phi_v2, 2)}")
print(f"phi_l1 = {round(phi_l1, 2)}")
print(f"phi_l2 = {round(phi_l2, 2)}")
print(f"y1 = {round(y1, 2)}")
print(f"y2 = {round(y2, 2)}")
print(f"P = {round(P)} Pa")
Converged in 6 iterations, with convergence 0.0005 < 0.001
phi_v1 = 0.9
phi_v2 = 0.34
phi_l1 = 2.85
phi_l2 = 0.02
y1 = 0.95
y2 = 0.05
P = 6265035 Pa

Practice Problem#

Koretsky Problem 8.61. Using the equation of state method, determine the equilibrium composition in the vapor phase and the system pressure of a mixture of methane (1) and n-pentane (2) with a liquid mole fraction of x1=0.1 at 60°C. Use the van der Waals equation of state to determine fugacity for both vapor and liquid phases. Iterate until |Σyi1|<0.001.

Note

Code your own answer before checking the answer!

Hide code cell source
# universal constant
R = 8.314  # J/mol K
# given properties
T = 60 + 273.15  # K
x1 = 0.2
x2 = 1 - x1

# formula for vdW params and saturation pressure
pure_a = lambda Tc, Pc, R: 27 / 64 * (R * Tc) ** 2 / Pc
pure_b = lambda Tc, Pc, R: 1 / 8 * (R * Tc) / Pc
antoine = lambda A, B, C, T: np.exp(A - B / (T + C)) * 1e5  # Pa

# physical properties - appendix a1
# methane (1)
A1 = 8.6041
B1 = 897.84
C1 = -7.16
Tc1 = 190.6  # K
Pc1 = 46.00e5  # Pa
a1 = pure_a(Tc1, Pc1, R)
b1 = pure_b(Tc1, Pc1, R)
# n-pentane (2)
A2 = 9.2131
B2 = 2477.07
C2 = -39.94
Tc2 = 469.6  # K
Pc2 = 33.74e5  # Pa
a2 = pure_a(Tc2, Pc2, R)
b2 = pure_b(Tc2, Pc2, R)
# mixture
a12 = np.sqrt(a1 * a2)

# saturation pressures
Psat1 = antoine(A1, B1, C1, T)
Psat2 = antoine(A2, B2, C2, T)
# estimate total pressure
P = x1 * Psat1 + x2 * Psat2
# estimate vapor compositions
y1 = x1 * Psat1 / P
y2 = x2 * Psat2 / P


def solve_v(T, P, a, b, R, v_before, threshold=1e-10):
    """Solve vdW EOS for v."""
    coeff = [-a * b, a, -(R * T + P * b), P]
    # solve for compressibility factor
    v = np.polynomial.polynomial.polyroots(coeff)
    # z is real and positive
    v = np.real(v)
    v = v[v > 0]
    # double check if solution is good in case of algorithmic instability
    error = np.abs(np.polynomial.polynomial.polyval(v, coeff))
    v = v[error < threshold]
    # use the root closest to the last v
    closest_idx = np.argmin(np.abs(v - v_before))
    v = v[closest_idx]
    return v


# formula for mixed vdW params
mixed_a = (
    lambda comp1, comp2, a1, a2: comp1**2 * a1
    + 2 * comp1 * comp2 * np.sqrt(a1 * a2)
    + comp2**2 * a2
)
mixed_b = lambda comp1, comp2, b1, b2: comp1 * b1 + comp2 * b2
# formula for fugacity coefficients (phi_1, for phi_2, switch arguments of 1 and 2 properties)
phi = lambda comp1, comp2, P, v, T, R, a1, a12, b, b1: np.exp(
    b1 / (v - b)
    - np.log((v - b) * P / (R * T))
    - 2 * (comp1 * a1 + comp2 * a12) / (R * T * v)
)

# initialize guess
v_v = 0
v_l = 0

# generate lists to store iteration results
vdw_a_v = []
vdw_b_v = []
vdw_P = []
vdw_y1 = []
vdw_y2 = []
vdw_v_v = []
vdw_v_l = []
vdw_phi_v1 = []
vdw_phi_v2 = []
vdw_phi_l1 = []
vdw_phi_l2 = []

# iteratre through vdW EOS
num_iter = 0
max_iter = 10000
tol = 0.001
tol_criteria = tol * 2
while tol_criteria > tol and num_iter < max_iter:
    # calculate vdW parameters for mixtures in each phase
    a_v = mixed_a(y1, y2, a1, a2)
    b_v = mixed_b(y1, y2, b1, b2)
    a_l = mixed_a(x1, x2, a1, a2)
    b_l = mixed_b(x1, x2, b1, b2)
    # solve vdW EOS for molar volume in each phase
    v_v = solve_v(T, P, a_v, b_v, R, v_before=v_v)
    v_l = solve_v(T, P, a_l, b_l, R, v_before=v_l)
    # calculate fugacity coefficient of each species in each phase
    phi_v1 = phi(y1, y2, P, v_v, T, R, a1, a12, b_v, b1)
    phi_v2 = phi(y2, y1, P, v_v, T, R, a2, a12, b_v, b2)
    phi_l1 = phi(x1, x2, P, v_l, T, R, a1, a12, b_l, b1)
    phi_l2 = phi(x2, x1, P, v_l, T, R, a2, a12, b_l, b2)
    # update vapor composition
    y1 = x1 * phi_l1 / phi_v1
    y2 = x2 * phi_l2 / phi_v2
    # update total pressure
    P = P * (y1 + y2)
    # calculate if calculation converges
    tol_criteria = abs(y1 + y2 - 1)
    num_iter += 1
    # store iteration info for debugging
    vdw_a_v.append(a_v)
    vdw_b_v.append(b_v)
    vdw_v_v.append(v_v)
    vdw_v_l.append(v_l)
    vdw_phi_v1.append(phi_v1)
    vdw_phi_v2.append(phi_v2)
    vdw_phi_l1.append(phi_l1)
    vdw_phi_l2.append(phi_l2)
    vdw_y1.append(y1)
    vdw_y2.append(y2)
    vdw_P.append(P)

# report results
print(
    f"Converged in {num_iter} iterations, with convergence {round(tol_criteria, 4)} < {tol}"
)
print(f"phi_v1 = {round(phi_v1, 2)}")
print(f"phi_v2 = {round(phi_v2, 2)}")
print(f"phi_l1 = {round(phi_l1, 2)}")
print(f"phi_l2 = {round(phi_l2, 2)}")
print(f"y1 = {round(y1, 2)}")
print(f"y2 = {round(y2, 2)}")
print(f"P = {round(P)} Pa")
Converged in 5 iterations, with convergence 0.0 < 0.001
phi_v1 = 1.0
phi_v2 = 0.64
phi_l1 = 3.35
phi_l2 = 0.26
y1 = 0.67
y2 = 0.33
P = 2802522 Pa
Hide code cell source
pd.set_option("display.precision", 2)
pd.DataFrame({
    'vdw_a_v': vdw_a_v,
    'vdw_b_v': vdw_b_v,
    'vdw_v_v': vdw_v_v,
    'vdw_v_l': vdw_v_l,
    'vdw_phi_v1': vdw_phi_v1,
    'vdw_phi_v2': vdw_phi_v2,
    'vdw_phi_l1': vdw_phi_l1,
    'vdw_phi_l2': vdw_phi_l2,
    'vdw_y1': vdw_y1,
    'vdw_y2': vdw_y2,
    'vdw_P': vdw_P,
})
vdw_a_v vdw_b_v vdw_v_v vdw_v_l vdw_phi_v1 vdw_phi_v2 vdw_phi_l1 vdw_phi_l2 vdw_y1 vdw_y2 vdw_P
0 0.25 4.55e-05 3.46e-04 1.76e-04 0.90 0.49 1.63 0.14 0.36 0.22 4.16e+06
1 0.23 4.79e-05 6.31e-04 1.83e-04 0.94 0.68 2.43 0.19 0.52 0.23 3.09e+06
2 0.31 5.49e-05 8.38e-04 1.87e-04 0.96 0.71 3.09 0.24 0.64 0.27 2.83e+06
3 0.47 6.72e-05 8.68e-04 1.88e-04 0.98 0.67 3.32 0.26 0.68 0.31 2.80e+06
4 0.57 7.41e-05 8.43e-04 1.88e-04 1.00 0.64 3.35 0.26 0.67 0.33 2.80e+06