Chemical equilibrium with fixed pH

Chemical equilibrium with fixed pH

Written by Allan Leal (ETH Zurich) on Jan 7th, 2022

Fixing the pH of an aqueous solution is a common procedure in biogeochemical modeling. This requires, however, the introduction of a new degree of freedom in the problem. This is because we cannot, in general, obtain a desired pH without titrating a substance in the solution. Thus, in the example given below, the system will be open to H+. The unknown amount of H+ necessary to enter or leave the system to attain the requested pH is the new degree of freedom, and is calculated along with the amounts of species in equilibrium.

Note

The choice of H+ as the titrant when fixing the pH of an aqueous solution may seem arbitrary and unintuitive to some coming from a different background or experienced with another convention used by other chemical modeling codes. For example, it can be questioned that a charged species not found isolated in nature (compared to HCl, for example) is chosen as the titrant. However, there are mathematical reasons behind this choice that make the problem to be solved more simply and efficiently. We will give examples later on how to combine this pH constraint with other conditions to demonstrate the full potential of this convention.

For our example, let’s create a ChemicalSystem object containing a simple aqueous phase (with thermodynamic data for the species fetched from a PhreeqcDatabase object):

from reaktoro import *

db = PhreeqcDatabase("pitzer.dat")

solution = AqueousPhase("H2O H+ OH- Na+ Cl- HCO3- CO2 CO3-2")
solution.setActivityModel(ActivityModelPitzerHMW())

system = ChemicalSystem(db, solution)

Let’s create a chemical equilibrium solver that expects the following properties as input conditions:

  • temperature,

  • pressure, and

  • pH.

The code below will first create an EquilibriumSpecs object in which the above properties are marked as inputs and then use that to create our EquilibriumSolver object:

specs = EquilibriumSpecs(system)
specs.temperature()
specs.pressure()
specs.pH()

solver = EquilibriumSolver(specs)

We create now an initial chemical state for our system (using ChemicalState), which will represent a 1 molal NaCl solution with 0.4 molal dissolved CO2:

state = ChemicalState(system)
state.temperature(30.0, "celsius")
state.pressure(1.0, "atm")
state.set("H2O", 1.0, "kg")
state.set("Na+", 1.0, "mol")
state.set("Cl-", 1.0, "mol")
state.set("CO2", 0.4, "mol")

print("INITIAL STATE")
print(state)
INITIAL STATE
+-----------------+-------------+------+
| Property        |       Value | Unit |
+-----------------+-------------+------+
| Temperature     |    303.1500 |    K |
| Pressure        |      1.0132 |  bar |
| Charge:         | -3.0000e-16 |  mol |
| Element Amount: |             |      |
| :: H            |  1.1101e+02 |  mol |
| :: C            |  4.0000e-01 |  mol |
| :: O            |  5.6306e+01 |  mol |
| :: Na           |  1.0000e+00 |  mol |
| :: Cl           |  1.0000e+00 |  mol |
| Species Amount: |             |      |
| :: H2O          |  5.5506e+01 |  mol |
| :: H+           |  1.0000e-16 |  mol |
| :: OH-          |  1.0000e-16 |  mol |
| :: Na+          |  1.0000e+00 |  mol |
| :: Cl-          |  1.0000e+00 |  mol |
| :: HCO3-        |  1.0000e-16 |  mol |
| :: CO2          |  4.0000e-01 |  mol |
| :: CO3-2        |  1.0000e-16 |  mol |
+-----------------+-------------+------+

We need now to specify the desired values for temperature, pressure, and pH in the equilibrium state we want to compute. This is done next using EquilibriumConditions:

conditions = EquilibriumConditions(specs)
conditions.temperature(50.0, "celsius")
conditions.pressure(10.0, "atm")
conditions.pH(1.0)

We are now ready for our chemical equilibrium calculation:

solver.solve(state, conditions)

print("FINAL STATE")
print(state)
FINAL STATE
+-----------------+------------+------+
| Property        |      Value | Unit |
+-----------------+------------+------+
| Temperature     |   323.1500 |    K |
| Pressure        |    10.1325 |  bar |
| Charge:         | 1.2292e-01 |  mol |
| Element Amount: |            |      |
| :: H            | 1.1114e+02 |  mol |
| :: C            | 4.0000e-01 |  mol |
| :: O            | 5.6306e+01 |  mol |
| :: Na           | 1.0000e+00 |  mol |
| :: Cl           | 1.0000e+00 |  mol |
| Species Amount: |            |      |
| :: H2O          | 5.5506e+01 |  mol |
| :: H+           | 1.2292e-01 |  mol |
| :: OH-          | 9.0381e-13 |  mol |
| :: Na+          | 1.0000e+00 |  mol |
| :: Cl-          | 1.0000e+00 |  mol |
| :: HCO3-        | 4.3699e-06 |  mol |
| :: CO2          | 4.0000e-01 |  mol |
| :: CO3-2        | 1.8183e-14 |  mol |
+-----------------+------------+------+

The printed state above does not show pH. We can obtain aqueous properties from a chemical state using AqueousProps:

aprops = AqueousProps(state)

print("AQUEOUS PROPERTIES AT EQUILIBRIUM")
print(aprops)
AQUEOUS PROPERTIES AT EQUILIBRIUM
+-----------------------------------+------------+-------+
| Property                          |      Value |  Unit |
+-----------------------------------+------------+-------+
| Temperature                       |   323.1500 |     K |
| Pressure                          |    10.1325 |   bar |
| Ionic Strength (Effective)        |     1.0615 | molal |
| Ionic Strength (Stoichiometric)   |     1.0615 | molal |
| pH                                |     1.0000 |       |
| pE                                |    -5.1853 |       |
| Eh                                |    -0.3325 |     V |
| Element Molality:                 |            |       |
| :: C                              | 4.0002e-01 | molal |
| :: Na                             | 1.0000e+00 | molal |
| :: Cl                             | 1.0000e+00 | molal |
| Species Molality:                 |            |       |
| :: H+                             | 1.2293e-01 | molal |
| :: OH-                            | 9.0385e-13 | molal |
| :: Na+                            | 1.0000e+00 | molal |
| :: Cl-                            | 1.0000e+00 | molal |
| :: HCO3-                          | 4.3700e-06 | molal |
| :: CO2                            | 4.0001e-01 | molal |
| :: CO3-2                          | 1.8184e-14 | molal |
| Saturation Indices (log base 10): |            |       |
| :: CO2(g)                         |     0.3819 |     - |
| :: H2O(g)                         |    -1.9384 |     - |
| :: Halite :: NaCl                 |    -1.9448 |     - |
| :: Nahcolite :: NaHCO3            |    -5.2238 |     - |
| :: Natron :: Na2CO3:10H2O         |   -14.4637 |     - |
| :: Trona :: Na3H(CO3)2:2H2O       |   -19.7458 |     - |
+-----------------------------------+------------+-------+

In the table above, we see that temperature, pressure, and pH have values exactly like those given earlier in the EquilibriumConditions object (although with different temperature and pressure units).

As a result of enforcing pH via specs.pH(), this executed equilibrium calculation considered the system to be open to H+. Let’s find out below how much H+ entered/leaved the system so that the desired pH could be reached:

print("Amount of H+ that entered the system:", float(state.equilibrium().implicitTitrantAmounts()), "mol")
Amount of H+ that entered the system: 0.12291636480068265 mol

Since H+ is a charged species, our final chemical state does not have zero charge as the initial state:

print("Charge at equilibrium state:", state.charge(), "mol")
Charge at equilibrium state: 0.122916 mol

We’ll learn in the next tutorial how a pH constraint can be combined with a charge constraint so that the system has zero charge at equilibrium (or whatever other value you may want to impose).