Fitting interaction parameters of binary systems

Sometimes Equations of State needs an extra help to better predict behaviours.

Let’s see how we can optimize binary interaction parameters of a Cubic Equation, using equilibria data.

Data structure

For all the default approachs of parameter optimization the datapoints must be in the format of a pandas DataFrame, with the following columns:

  • kind: Which kind of point is defined ["bubble", "dew", "liquid-liquid", "PT"].

  • T: Temperature (in Kelvin).

  • P: Pressure (in bar).

  • x1: Mole fraction of component 1 (lightest component) in heavy phase.

  • y1: Mole fraction of component 1 (lightest component) in light phase.

In the following cell we import the datapoints from a .csv file

[1]:
import pandas as pd
df = pd.read_csv('./data/CO2_C6.csv')
df
[1]:
kind T P x1 y1
0 bubble 303.15 20.24 0.2385 0.9721
1 bubble 303.15 30.36 0.3698 0.9768
2 bubble 303.15 39.55 0.5063 0.9815
3 bubble 303.15 50.95 0.7078 0.9842
4 bubble 303.15 57.83 0.8430 0.9855
5 bubble 303.15 64.68 0.9410 0.9884
6 bubble 303.15 67.46 0.9656 0.9909
7 bubble 315.15 20.84 0.2168 0.9560
8 bubble 315.15 30.40 0.3322 0.9660
9 bubble 315.15 40.52 0.4446 0.9748
10 bubble 315.15 50.66 0.5809 0.9753
11 bubble 315.15 60.29 0.7180 0.9753
12 bubble 315.15 69.11 0.8457 0.9764
13 bubble 315.15 72.89 0.8928 0.9773
14 bubble 315.15 75.94 0.9252 0.9777
15 bubble 315.15 77.50 0.9424 0.9768
16 bubble 315.15 78.66 0.9520 0.9756

Default model prediction

Before starting to fit BIPs, let’s see how the model predicts without BIPs.

First, let’s define the model

[2]:
import yaeos

Tc = [304.1, 504.0]
Pc = [73.75, 30.12]
w = [0.4, 0.299]

model = yaeos.PengRobinson76(Tc, Pc, w)

Claculation of Pxy diagrams

Now that the model is defined, lets calculate Pxy diagrams for each temperature.

First, we find the unique values of temperature to iterate over them later.

[3]:
Ts = df["T"].unique()
Ts
[3]:
array([303.15, 315.15])
[4]:
import matplotlib.pyplot as plt

z0 = [0, 1]
zi = [1, 0]

for T in Ts:

    # Let's use a mask to only get the data for a specific temperature
    mask = df["T"] == T
    px = model.phase_envelope_px(z0=z0, zi=zi, temperature=T, a0=0.001, ns0=len(z0)+3)

    plt.plot(px.main_phases_compositions[:, 0, 0], px["P"], label="x")
    plt.plot(px.reference_phase_compositions[:, 0], px["P"], label="y")

    plt.scatter(df[mask]["x1"], df[mask]["P"], label="data Bubble")
    plt.scatter(df[mask]["y1"], df[mask]["P"], label="data Dew")

    plt.title(f"Pxy diagram at T = {T} K")
    plt.ylabel("P [bar]")
    plt.xlabel(r"$x_1, y_1$")
    plt.xlim(0, 1)
    plt.legend()
    plt.show()
../_images/tutorial_fitting_8_0.png
../_images/tutorial_fitting_8_1.png

Here we can see that the PengRobinson EoS present a negative deviation with respect to the experimental points. Let’s se how we can improve these predictions.

Definition of the optimization problem

Now that we already have our data points defined. We need to define the optimization problem.

The yaeos.fitting package includes the BinaryFitter object, this object encapsulates all the logic that is needed to fit binary systems.

[5]:
import yaeos
from yaeos.fitting import BinaryFitter

BinaryFitter?

Model setter function

Looking at the object’s documentation we can see that we need to define a function model_setter that takes the vector of fitting parameters and extra required arguments.

[6]:
def fit_kij(x, model):
    """Fit kij function.

    Args:
        x (list): kij value.
        model (yaeos.CubicEoS): CEOS model that will be fitted
    """
    kij = x[0]

    kij_matrix = [
        [0, kij],
        [kij, 0]
    ]

    lij = [
        [0, 0],
        [0, 0]
    ]

    mixrule = yaeos.QMR(kij=kij_matrix, lij=lij)

    model.set_mixrule(mixrule)

    return model
[7]:
problem = BinaryFitter(
    model_setter=fit_kij,
    model_setter_args=(model,),
    data=df,
    verbose=True
)

x0 = [0.0]

problem.fit(x0, bounds=None)
1.7427277515958346 [0.]
1.7367297572626244 [0.00025]
1.7307370080649485 [0.0005]
1.72474952538773 [0.00075]
1.712790445469462 [0.00125]
1.700852689778661 [0.00175]
1.6770418462495278 [0.00275]
1.6533183947480148 [0.00375]
1.6061393533646415 [0.00575]
1.559327111499111 [0.00775]
1.4668506368944916 [0.01175]
1.3759871697248645 [0.01575]
1.199517124958797 [0.02375]
1.030807090584202 [0.03175]
0.7207125515768745 [0.04775]
0.45494890252076453 [0.06375]
0.1633296153190301 [0.09575]
0.22052696455206036 [0.12775]
0.22052696455206036 [0.12775]
0.09792206009347366 [0.11175]
0.22052696455206036 [0.12775]
0.11825669217983152 [0.10375]
0.10598288555962829 [0.11975]
0.09823429765712599 [0.11575]
0.10459879763851507 [0.10775]
0.09712360613884066 [0.11375]
0.09823429765712599 [0.11575]
0.09730049165208345 [0.11275]
0.05599850855641941 [0.11475]
0.09823429765712599 [0.11575]
0.09823429765712599 [0.11575]
0.05537507866049399 [0.11425]
0.09712360613884066 [0.11375]
0.05567258588798029 [0.1145]
0.05510588012730981 [0.114]
0.09712360613884066 [0.11375]
0.09712360613884066 [0.11375]
0.05523694758807889 [0.114125]
0.054981862883314914 [0.113875]
0.09712360613884066 [0.11375]
0.09712360613884066 [0.11375]
0.05504299106424188 [0.1139375]

Optimization result

Now that we have fitted the parameter, let’s see the solution. For this, we can see the solution property of the problem. Where we can see that the optimization terminated succesfully with a kij value of around 0.113

[8]:
problem.solution
[8]:
       message: Optimization terminated successfully.
       success: True
        status: 0
           fun: 0.054981862883314914
             x: [ 1.139e-01]
           nit: 21
          nfev: 42
 final_simplex: (array([[ 1.139e-01],
                       [ 1.139e-01]]), array([ 5.498e-02,  5.504e-02]))

Obtain the fitted model

Now that the problem has been optimized, let’s redefine our model with the solution. For this, we use the _get_model method inside of the BinaryFitter object. This just uses the function that we have provided originally (fit_kij)

[9]:
model = problem._get_model(problem.solution.x, model)

Make predictions

Let’s repeat the calculation of Pxy diagrams that we have done earlier

[10]:
for T in Ts:

    # Let's use a mask to only get the data for a specific temperature
    mask = df["T"] == T
    px = model.phase_envelope_px(z0=z0, zi=zi, temperature=T, ds0=1e-5, max_points=500, a0=0.01)

    plt.plot(px.main_phases_compositions[:, 0, 0], px["P"], label="x")
    plt.plot(px.reference_phase_compositions[:, 0], px["P"], label="y")

    plt.scatter(df[mask]["x1"], df[mask]["P"], label="data Bubble")
    plt.scatter(df[mask]["y1"], df[mask]["P"], label="data Dew")

    plt.title(f"Pxy diagram at T = {T} K")
    plt.ylabel("P [bar]")
    plt.xlabel(r"$x_1, y_1$")
    plt.xlim(0, 1)
    plt.legend()
    plt.show()
../_images/tutorial_fitting_20_0.png
../_images/tutorial_fitting_20_1.png