In the case that the pure-component isotherms $N_{i,pure}(P)$ follow the Langmuir model with the same saturation loading $M$:
$N_{i,pure} = M \frac{K_iP}{1+K_iP},$
The mixed gas adsorption isotherm follows the competitive Langmuir isotherm:
$N_i = M \frac{K_i p_i}{1 + \sum_j K_jp_j},$
where $p_i$ is the partial pressure of component $i$. Here, we generate synthetic pure-component adsorption isotherm data and confirm that pyIAST agrees with the competitive Langmuir isotherm for 3 components.
In [1]:
    
import numpy as np
import pyiast
import pandas as pd
import matplotlib.pyplot as plt
plt.style.use('fivethirtyeight')
%config InlineBackend.rc = {'font.size': 13, 'lines.linewidth':3,\
                            'axes.facecolor':'w', 'legend.numpoints':1,\
                           'figure.figsize': (6.0, 4.0)}
%matplotlib inline
colors = ['b', 'g', 'r']  # for representing each component
component_names = {0: 'A', 1: 'B', 2:'C'}
    
Model parameters ($M$, $\{K_i\}$)
In [2]:
    
M = 1.0
langmuirKs = [2.0, 10.0, 20.0]  # K_i
    
Generate data according to Langmuir model, store in list of Pandas DataFrames
In [3]:
    
pressure = np.logspace(-3, np.log10(10), 20)
dfs = [pd.DataFrame({'P': pressure, 
                     'L': M * langmuirKs[i] * pressure / (
                1.0 + langmuirKs[i] * pressure)})
      for i in range(3)]
    
Use pyIAST to fit Lanmguir models to the data, then plot fits
In [4]:
    
isotherms = [pyiast.ModelIsotherm(dfs[i], pressure_key='P', 
                                  loading_key='L', model='Langmuir') 
             for i in range(3)]
for i in range(len(isotherms)):
    isotherms[i].print_params()
    pyiast.plot_isotherm(isotherms[i])
    
    
    
    
    
    
    
Plot synthetic data all in one plot for paper
In [5]:
    
p_plot = np.logspace(-3, np.log10(11))  # for plotting
fig = plt.figure(facecolor='w')
for i in range(len(isotherms)):
    plt.scatter(dfs[i]['P'], dfs[i]['L'], color=colors[i],
               s=50, label=None)
    plt.plot(p_plot, M * langmuirKs[i] * p_plot / (1.0 + langmuirKs[i] * p_plot),
            color=colors[i], linewidth=2, label=r'$N_%s(P) = \frac{%d P}{1+%dP}$' % (
                component_names[i], langmuirKs[i], langmuirKs[i]))
plt.xlim([-.05 * 10, 1.05 * 10])
plt.ylim([-.05 * M, M * 1.05])
plt.xlabel('Pressure (bar)')
plt.ylabel('Gas uptake (mmol/g)')
plt.legend(loc='lower right')
plt.tight_layout()
plt.savefig('pure_component_Langmuir.pdf', format='pdf',
            facecolor=fig.get_facecolor())
plt.show()
    
    
Let us consider a tertiary mixture of components 0, 1, and 2 above at a total pressure of total_pressure bar.
In [6]:
    
total_pressure = 1.0
    
We will explore gas phase composition space ($\{y_i\}$) by generating random compositions and checking that they are within the triangle. We do not want to get too close to a pure phase boundary becuase of numerical instability, so we keep a distance dx away from pure phases. We will perform num_tests tests.
In [7]:
    
dx = 0.0001
num_tests = 100
    
Generate the compositions and store in list compositions
In [8]:
    
compositions = []
test_no = 0
while test_no < num_tests:
    # generate random compoisitions
    y1 = np.random.uniform(dx, 1.0 - dx)
    y2 = np.random.uniform(dx, 1.0 - dx)
    y3 = 1.0 - y2 - y1
    # check that composition is within the triangle
    if y3 < dx:
        continue
    # viable composition
    compositions.append([y1, y2, y3])
    # keep generating until we have num_tests
    test_no += 1
    
Next, we assert that pyIAST gives the same result as the competitive Langmuir isotherm for each of these compositions.
Function to compute loading according to competitive Langmuir
In [9]:
    
def competitive_langmuir_loading(partial_pressures, i):
    """
    Calculate loading of component i according to competitive Langmuir
    """
    return M * langmuirKs[i] * partial_pressures[i] / (
        1.0 + np.dot(langmuirKs, partial_pressures))
    
Function to compute loading according to pyIAST
In [10]:
    
def iast_loading(partial_pressures, i):
    """
    Calculate loading of component i according to IAST
    
    partial_pressures: Array, partial pressures of each component
    i: component in the mixture
    """
    component_loadings = pyiast.iast(partial_pressures, isotherms)
    return component_loadings[i]
    
Loop over compositions, assert pyIAST agrees with competitive Langmuir for each component. If this runs, then there is agreement!
In [11]:
    
for i in range(num_tests):
    partial_pressure = np.array(compositions[i]) * total_pressure
    # for each component...
    for c in range(len(langmuirKs)):
        np.testing.assert_almost_equal(
                competitive_langmuir_loading(partial_pressure, c),
                iast_loading(partial_pressure, c), decimal=4)
    
Use ternary to plot phase diagram https://github.com/marcharper/python-ternary
In [12]:
    
import ternary
scale = 10  # resolution in triangle
axis_colors = {'l':colors[1], 'r':colors[0], 'b':colors[2]}
cmaps = ["Blues", "Greens", "Reds"]
iast_or_lang = 'iast'  # plot results for IAST or for Langmuir isotherm?
for c in range(3):
    if iast_or_lang == 'lang':
        f = lambda p: competitive_langmuir_loading(p, c)
    else:
        f = lambda p: iast_loading(p, c)
    
    # loop over component
    fig, ax = plt.subplots(facecolor='w')
    ax.axis("off")
    figure, tax = ternary.figure(ax=ax, scale=scale)
    tax.heatmapf(f, boundary=False, 
                 style="hexagonal", cmap=plt.cm.get_cmap(cmaps[c]), 
                 vmax=M, vmin=0.0, 
                 cbarlabel="%s uptake (mmol/g)" % component_names[c])
    tax.boundary(linewidth=2.0, axes_colors=axis_colors)
    tax.left_axis_label("$p_B$ (bar)", color=axis_colors['l'], offset=0.16)
    tax.right_axis_label("$p_A$ (bar)", color=axis_colors['r'], offset=0.16)
    tax.bottom_axis_label("$p_C$ (bar)", color=axis_colors['b'], offset=-0.06)
    tax.gridlines(color="blue", multiple=1, linewidth=2,
                 horizontal_kwargs={'color':axis_colors['b']},
                 left_kwargs={'color':axis_colors['l']},
                 right_kwargs={'color':axis_colors['r']},
                 alpha=0.7) # Every 5th gridline, can be a float
    tax.ticks(axis='rlb', linewidth=1, locations=np.arange(scale+1), clockwise=True,
              axes_colors=axis_colors,
              ticks=["%.1f" % (1.0 * i / scale) for i in range(scale+1)], offset=0.03)
    tax.clear_matplotlib_ticks()
    tax._redraw_labels()
   # if iast_or_lang == 'iast':
   #     tax.set_title("IAST uptake, component %d" % c, y=1.08, fontsize=14)
   # if iast_or_lang == 'lang':
   #     tax.set_title("Competitive Langmuir uptake, component %d" % c, y=1.08, fontsize=14)
    plt.tight_layout()
    if iast_or_lang == 'iast':
        plt.savefig("Tertiary_diagram_IAST_component_%d.pdf" % c, format='pdf',
                facecolor=fig.get_facecolor())
    if iast_or_lang == 'lang':
        plt.savefig("Tertiary_diagram_Langmuir_component_%d.pdf" % c, format='pdf',
                facecolor=fig.get_facecolor())
    tax.show()