Ion exchange competition among NaX, KX, and CaX2

Ion exchange competition among NaX, KX, and CaX2

Written by Svetlana Kyas (ETH Zurich) on Mar 10th, 2022

In this tutorial, we study the dependence of ion exchange species distribution on the initial amounts of K+ and Ca+2.

First, we set up the chemical system, chemical state, and other means of modeling ion exchange using Reaktoro.

import numpy as np
from reaktoro import *
import pandas as pd

# Initialize the PHREEQC database
db = PhreeqcDatabase("phreeqc.dat")

# Define an aqueous phase
solution = AqueousPhase("H2O Na+ Cl- H+ OH- K+ Ca+2 Mg+2")
solution.setActivityModel(ActivityModelHKF())

# Define an ion exchange phase
exchange = IonExchangePhase("NaX KX CaX2")
exchange.setActivityModel(ActivityModelIonExchangeGainesThomas())

# Create the chemical system
system = ChemicalSystem(db, solution, exchange)

As we are going to perform a sequence of chemical equilibrium calculations, we define an object of EquilibriumSolver as well as IonExchangeProps and AqueousProps using the chemical system. These objects will be updated after every equilibrium calculation.

# Define the equilibrium solver
solver = EquilibriumSolver(system)

# Define exchange, aqueous, and chemical properties
exprops = IonExchangeProps(system)
aqprops = AqueousProps(system)

Note

Note that at the end of each equilibrium calculation, state.props() can be used to access a ChemicalProps object with the chemical properties of the system evaluated in the last iteration of the algorithm.

Next, we define the range of Ca+2 and K+ initial amounts as well as auxiliary pandas.DataFrame to collect the results of the ion exchange simulations.

# Output dataframe
df = pd.DataFrame(columns=["amount_K", "amount_Ca",
                           "amount_K_ion", "amount_Na_ion", "amount_Ca_ion",
                           "amount_KX", "amount_NaX", "amount_CaX2",
                           "pH"])

# Sampling arrays storing the amounts of ions
steps = 21
mols_K  = np.flip(np.linspace(0, 0.1, num=steps))
mols_Ca = np.linspace(0, 0.5, num=steps)

We’ll now perform a sequence of equilibrium calculations by varying the initial amounts of ions Ca+2 and K+ ions (taken from the arrays mols_Ca and mols_K, respectively). At the end of each calculation, we’ll extract the following properties from the computed chemical state:

  • amount of K,

  • amount of Ca,

  • amount of K+,

  • amount of Na+,

  • amount of Ca+2,

  • amount of KX,

  • amount of NaX,

  • amount of CaX2,

  • pH.

for mol_K, mol_Ca in zip(mols_K, mols_Ca):

    # Define initial chemical state
    state = ChemicalState(system)
    state.setTemperature(25, "celsius")
    state.setPressure(1, "atm")
    state.set("H2O" , 1.0   , "kg")
    # Exchanger site
    state.set("NaX" , 0.4   , "mol")
    # Changing Ca+2 and K+
    state.set("K+"  , mol_K , "mol")
    state.set("Ca+2", mol_Ca, "mol")

    # Equilibrate the chemical state
    res = solver.solve(state)
    # Stop if the equilibration did not converge or failed
    assert res.optima.succeeded

    # Update exchange and aqueous properties
    exprops.update(state)
    aqprops.update(state)
    chemprops = state.props()

    # Update output arrays:
    # "amount_K", "amount_Ca", "amount_K+", "amount_Na+", "amount_Ca+2",
    # "amount_KX", "amount_NaX", "amount_CaX2", "pH"
    df.loc[len(df)] = [float(chemprops.elementAmount('K')),
                       float(chemprops.elementAmount('Ca')),
                       float(state.speciesAmount('K+')),
                       float(state.speciesAmount('Na+')),
                       float(state.speciesAmount('Ca+2')),
                       float(state.speciesAmount('KX')),
                       float(state.speciesAmount('NaX')),
                       float(state.speciesAmount('CaX2')),
                       float(aqprops.pH())]

We’ll use the bokeh plotting library next. First, we need to import it and initialize it to work with Jupyter Notebooks:

from bokeh.plotting import figure, show
from bokeh.models import HoverTool, Legend
from bokeh.io import output_notebook
output_notebook()
Loading BokehJS ...

We can now construct an interactive plot of ion exchange species dependence on the growing initial amount of Ca+2. We see below that, as expected, the amount of KX is decreasing and the amount of CaX2 increasing as the amount of Ca+2 grows.

hovertool = HoverTool()
hovertool.tooltips = [("amount(KX)"  , "@amount_KX mol"),
                      ("amount(CaX2)", "@amount_CaX2 mol"),
                      ("amount(NaX)" , "@amount_NaX mol"), 
                      ("added amount(Ca)"  , "@amount_Ca mol"),
                      ]

p = figure(
    title="DEPENDENCE OF ION EXCHANGE SPECIES ON ADDED CA+2",
    y_axis_label='AMOUNT [MOL]',
    x_axis_label='AMOUNT OF CA+2 [MOL]',
    sizing_mode="scale_width",
    plot_height=300)

p.add_tools(hovertool)

r11 = p.line("amount_Ca", "amount_KX", line_width=2, line_cap="round", line_color="midnightblue", source=df)
r12 = p.circle("amount_Ca", "amount_KX", fill_color=None, size=8, line_color="midnightblue", source=df)

r21 = p.line("amount_Ca", "amount_CaX2", line_width=2, line_cap="round", line_color="deeppink", source=df)
r22 = p.square("amount_Ca", "amount_CaX2", fill_color=None, size=8, line_color="deeppink", source=df)

r31 = p.line("amount_Ca", "amount_NaX", line_width=2, line_cap="round", line_color="green", line_dash=[4, 4], source=df)
r32 = p.x("amount_Ca", "amount_NaX", line_color="green", size=8, line_width=2, source=df)

legend = Legend(items=[
    ("KX"  , [r11, r12]),
    ("CaX2", [r21, r22]),
    ("NaX" , [r31, r32]),
], location="center")

p.add_layout(legend, 'right')

show(p)

We also show the dependence of aqueous ions amounts increasing amounts of Ca+2.

hovertool = HoverTool()
hovertool.tooltips = [("amount(K+)"  , "@amount_K_ion mol"),
                      ("amount(Ca+2)", "@amount_Ca_ion mol"),
                      ("amount(Na+)" , "@amount_Na_ion mol"), 
                      ("added amount(Ca)" , "@amount_Ca mol")]

p = figure(
    title="DEPENDENCE OF THE SOLUTE IONS ON ADDED CA+2",
    y_axis_label='AMOUNT [MOL]',
    x_axis_label='AMOUNT OF CA+2 [MOL]',
    sizing_mode="scale_width",
    plot_height=300)

p.add_tools(hovertool)

r11 = p.line("amount_Ca", "amount_K_ion", line_width=2, line_cap="round", line_color="midnightblue", source=df)
r12 = p.circle("amount_Ca", "amount_K_ion", fill_color=None, size=8, line_color="midnightblue", source=df)

r21 = p.line("amount_Ca", "amount_Ca_ion", line_width=2, line_cap="round", line_color="deeppink", source=df)
r22 = p.square("amount_Ca", "amount_Ca_ion", fill_color=None, size=8, line_color="deeppink", source=df)

r31 = p.line("amount_Ca", "amount_Na_ion", line_width=2, line_cap="round", line_color="green", line_dash=[4, 4], source=df)
r32 = p.x("amount_Ca", "amount_Na_ion", line_color="green", fill_color=None, size=8, line_width=2, source=df)

legend = Legend(items=[
    ("K+"  , [r11, r12]),
    ("Ca+2", [r21, r22]),
    ("Na+" , [r31, r32]),
], location="center")

p.add_layout(legend, 'right')

show(p)

Finally, we present the dependence of pH levels on changing values of Ca+2 amount.

hovertool = HoverTool()
hovertool.tooltips = [("pH"              , "@pH "), 
                      ("added amount(Ca)", "@amount_Ca mol")]

p = figure(
    title="DEPENDENCE OF PH ON ADDED CA+2",
    y_axis_label='PH [-]',
    x_axis_label='AMOUNT OF CA+2 [MOL]',
    sizing_mode="scale_width",
    plot_height=300)

p.add_tools(hovertool)

r1 = p.line("amount_Ca", "pH", line_width=2, line_cap="round", line_color="teal", source=df)
r2 = p.circle("amount_Ca", "pH", line_width=2, line_color="teal", source=df)

legend = Legend(items=[
    ("pH"  , [r1, r2]),
], location="center")

p.add_layout(legend, 'right')

show(p)

In this tutorial, we tried to vary the initial composition of the solution in contact with the exchanger to see how it reacts in terms of ion exchange distribution. The obtained numerical results have confirmed that an increase in Ca+2 amounts causes the growth of the ion exchange site CaX2 and the decrease of NaX while sodium ions are replaced by calcium ions.