Dissolution of carbonate minerals in a CO2-saturated brine¶
In this tutorial, we demonstrate how Reaktoro can be used to kinetically model the dissolution of carbonate minerals (calcite, magnesite and dolomite) in a CO2-saturated brine.
Below is the entire Python script showing the calculation, followed by a step-by-step explanation. You may want to just have a quick look at this relatively long script, and then proceed to the next sections for a more detailed discussion!
# Step 1: Import the reaktoro Python package
from reaktoro import *
# Step 2: Specify the phases in the chemical system and their species
editor = ChemicalEditor()
editor.addAqueousPhaseWithElementsOf("H2O NaCl CaCO3 MgCO3")
editor.addGaseousPhase(["H2O(g)", "CO2(g)"])
editor.addMineralPhase("Calcite")
editor.addMineralPhase("Magnesite")
editor.addMineralPhase("Dolomite")
editor.addMineralPhase("Halite")
# Step 3: Define the kinetically-controlled reactions
editor.addMineralReaction("Calcite") \
.setEquation("Calcite = Ca++ + CO3--") \
.addMechanism("logk = -5.81 mol/(m2*s); Ea = 23.5 kJ/mol") \
.addMechanism("logk = -0.30 mol/(m2*s); Ea = 14.4 kJ/mol; a[H+] = 1.0") \
.setSpecificSurfaceArea(10, "cm2/g")
editor.addMineralReaction("Magnesite") \
.setEquation("Magnesite = Mg++ + CO3--") \
.addMechanism("logk = -9.34 mol/(m2*s); Ea = 23.5 kJ/mol") \
.addMechanism("logk = -6.38 mol/(m2*s); Ea = 14.4 kJ/mol; a[H+] = 1.0") \
.setSpecificSurfaceArea(10, "cm2/g")
editor.addMineralReaction("Dolomite") \
.setEquation("Dolomite = Ca++ + Mg++ + 2*CO3--") \
.addMechanism("logk = -7.53 mol/(m2*s); Ea = 52.2 kJ/mol") \
.addMechanism("logk = -3.19 mol/(m2*s); Ea = 36.1 kJ/mol; a[H+] = 0.5") \
.setSpecificSurfaceArea(10, "cm2/g")
# Step 4: Construct the chemical system
system = ChemicalSystem(editor)
reactions = ReactionSystem(editor)
# Step 5: Specify the equilibrium and kinetic species
partition = Partition(system)
partition.setKineticSpecies(["Calcite", "Magnesite", "Dolomite"])
# Step 6: Define the initial chemical equilibrium state
problem = EquilibriumProblem(system)
problem.setPartition(partition)
problem.setTemperature(60, "celsius")
problem.setPressure(100, "bar")
problem.add("H2O", 1, "kg")
problem.add("NaCl", 1, "mol")
problem.add("CO2", 1, "mol")
# Step 7: Calculate the initial chemical equilibrium state
state0 = equilibrate(problem)
state0.output('demo-kineticpath-carbonates-co2-before-kinetics')
# Step 8: Set the initial mass of the kinetic species
state0.setSpeciesMass("Calcite", 100, "g")
state0.setSpeciesMass("Dolomite", 50, "g")
# Step 9: Create a kinetic path solver
path = KineticPath(reactions)
path.setPartition(partition)
# Step 10: Create plots for the kinetic path calculation
plot0 = path.plot()
plot0.x("time(units=hour)")
plot0.y("pH")
plot0.xlabel("Time [hour]")
plot0.ylabel("pH")
plot0.showlegend(False)
plot1 = path.plot()
plot1.x("time(units=hour)")
plot1.y("elementMolality(Ca)", "Ca")
plot1.y("elementMolality(Mg)", "Mg")
plot1.xlabel("Time [hour]")
plot1.ylabel("Concentration [molal]")
plot1.legend("right center")
plot2 = path.plot()
plot2.x("time(units=hour)")
plot2.y("phaseMass(Calcite units=grams)", "Calcite")
plot2.xlabel("Time [hour]")
plot2.ylabel("Mass [g]")
plot3 = path.plot()
plot3.x("time(units=hour)")
plot3.y("phaseMass(Dolomite units=grams)", "Dolomite")
plot3.xlabel("Time [hour]")
plot3.ylabel("Mass [g]")
# Step 11: Perform the kinetic path calculation
t0, t1 = 0.0, 25.0
path.solve(state0, t0, t1, "hours")
# Step 12: Output the final chemical state of the system
state0.output('demo-kineticpath-carbonates-co2-after-kinetics')
Importing the reaktoro Python package¶
# Step 1: Import the reaktoro Python package
from reaktoro import *
Here we import the reaktoro Python package to enable us to use all its library components (classes, methods, constants).
Defining the chemical system¶
In this simulation, we consider an aqueous phase (to model the brine solution), a gaseous phase (to model the CO2-rich phase with water vapour), and four pure mineral phases: halite, calcite, magnesite, and dolomite. These are the phases that will either exist initially during the simulation, or that could potentially appear later as the calculations proceed.
Attention
All potential phases that could appear in a reactive process should ideally be considered when defining the chemical system. If one or more of these phases are ignored, then the equilibrium and kinetics calculations cannot identify if they should actually exist or not with positive amounts. Unrealistic results may occur, such as, for example, an aqueous phase containing more CO2 dissolved than it could, because a gaseous phase, which should contain the excess of CO2, was not considered in the chemical system.
The code below uses class ChemicalEditor to define our chemical system with the phases of interest and their species:
# Step 2: Specify the phases in the chemical system and their species
editor = ChemicalEditor()
editor.addAqueousPhaseWithElementsOf("H2O NaCl CaCO3 MgCO3")
editor.addGaseousPhase(["H2O(g)", "CO2(g)"])
editor.addMineralPhase("Calcite")
editor.addMineralPhase("Magnesite")
editor.addMineralPhase("Dolomite")
editor.addMineralPhase("Halite")
The aqueous phase is defined by considering all aqueous species in the database that could form once the substances H2O, NaCl, CaCO3, and MgCO3 are mixed. The gaseous phase is defined so that only the gaseous species H2O(g) and CO2(g) are considered. There are four pure mineral phases: calcite (CaCO3), magnesite (MgCO3), dolomite (CaMg(CO3)2), and halite (NaCl).
Defining the kinetically-controlled reactions¶
A partial equilibrium assumption is considered in this simulation. This simplifies the problem by assuming that those species that react at much faster rates are continually in chemical equilibrium. They are referred to as equilibrium species. The remaining species (those reacting at relatively slower rates) are referred to as kinetic species.
Because aqueous and gaseous species, as well as halite, react at relatively fast rates, they are reasonable candidates in this problem for being equilibrium species. The kinetic species are thus the carbonate minerals: calcite, magnesite, and dolomite.
With this partial equilibrium assumption, there is no need to specify kinetic rate laws for the fast reacting equilibrium species. For these species, chemical equilibrium calculations are performed to update their amounts as the amounts of each kinetic species change over time (i.e., the equilibrium species react instantaneously to a new state of equilibrium as the kinetic species react and perturb the current partial equilibrium state).
Thus, we only need to define kinetic rates for the relatively slow reacting carbonate minerals (our kinetic species in this simulation), which is shown below:
# Step 3: Define the kinetically-controlled reactions
editor.addMineralReaction("Calcite") \
.setEquation("Calcite = Ca++ + CO3--") \
.addMechanism("logk = -5.81 mol/(m2*s); Ea = 23.5 kJ/mol") \
.addMechanism("logk = -0.30 mol/(m2*s); Ea = 14.4 kJ/mol; a[H+] = 1.0") \
.setSpecificSurfaceArea(10, "cm2/g")
editor.addMineralReaction("Magnesite") \
.setEquation("Magnesite = Mg++ + CO3--") \
.addMechanism("logk = -9.34 mol/(m2*s); Ea = 23.5 kJ/mol") \
.addMechanism("logk = -6.38 mol/(m2*s); Ea = 14.4 kJ/mol; a[H+] = 1.0") \
.setSpecificSurfaceArea(10, "cm2/g")
editor.addMineralReaction("Dolomite") \
.setEquation("Dolomite = Ca++ + Mg++ + 2*CO3--") \
.addMechanism("logk = -7.53 mol/(m2*s); Ea = 52.2 kJ/mol") \
.addMechanism("logk = -3.19 mol/(m2*s); Ea = 36.1 kJ/mol; a[H+] = 0.5") \
.setSpecificSurfaceArea(10, "cm2/g")
We set the equation of each mineral reaction using method setEquation
of
class MineralReaction.
Todo
The current need for setting a reaction equation for mineral reactions will be removed in the future, which will simplify the setup of the problem.
We then prescribe neutral and acidic mechanisms for each mineral reaction using
the method addMechanism
of class MineralMechanism, using values for
logk
, the kinetic rate constant of the reaction in log scale, and Ea
,
the Arrhenius activation energy. The units of both parameters must be provided
as shown in the example, and other compatible units are allowed.
Finally, we define the specific surface area of the mineral using method
setSpecificSurfaceArea
of class MineralReaction. Any units compatible to
m2/kg
or m2/m3
are allowed (e.g., cm2/g
, mm2/mm3
).
Note
The values shown for logk
and Ea
were collected from
Palandri, J.L., Kharaka, Y.K. (2004). A compilation of rate parameters of water-mineral interaction kinetics for application to geochemical modeling. U.S. Geological Survey Open File Report (Vol. 2004–1068). Menlo Park, California.
Creating the chemical and reaction systems¶
Create instances of ChemicalSystem and ReactionSystem.
# Step 4: Construct the chemical system
system = ChemicalSystem(editor)
reactions = ReactionSystem(editor)
Class ChemicalSystem is used to represent a chemical system, containing one or more phases (in this problem, aqueous, gaseous, and mineral phases), each phase containing one or more species (the aqueous phase containing many aqueous species, the gaseous phase containing two, and each mineral phase containing a single mineral species with the same name as the name of the phase). This class is also used to calculate thermodynamic properties of the phases and species (standard thermodynamic properties, activities, chemical potentials, phase molar volume, etc.).
Class ReactionSystem is a collection of Reaction objects used to represent a system of chemical reactions that are controlled by chemical kinetics. These classes provide convenient methods for the calculation of equilibrium constants, reaction quotients, and rates of the reactions.
Specifying equilibrium and kinetic species¶
In Reaktoro, the species in a chemical system can be partitioned into groups of:
equilibrium species,
kinetic species, and
inert species.
For the equilibrium species, their amounts are governed by chemical equilibrium (calculated via a Gibbs energy minimization). The amount of kinetic species are controlled by chemical kinetics (by solving a system of ordinary differential equations that models the kinetics of a system of reactions). The inert species maintain their amounts constant in all chemical equilibrium or kinetics calculations.
This classification of species can be done using class Partition. By default, all species are considered to be equilibrium species in this class. Thus, we only need to specify which ones are kinetic species:
# Step 5: Specify the equilibrium and kinetic species
partition = Partition(system)
partition.setKineticSpecies(["Calcite", "Magnesite", "Dolomite"])
In this case, the mineral species calcite, magnesite and dolomite are specified
to be kinetic species using method setKineticSpecies
of class Partition.
Note
Method setKineticPhases
of class Partition could also be used here.
This method sets all species in the given phases to be kinetic species, and
it is more convenient if a phase has many species. However, since each of
the mineral phases considered here only contains a single mineral species,
method setKineticSpecies
is a convenient alternative.
Defining the initial state of the equilibrium species¶
In a chemical kinetics calculation, an initial condition is needed for the amounts of both equilibrium and kinetic species.
The equilibrium problem formulated below, using class EquilibriumProblem, is done so that the initial condition for the amounts of each equilibrium species result from the solution of a chemical equilibrium problem in which 1 kg of water is mixed with 1 mol of NaCl and 1 mol of CO2 at 60 °C and 100 bar. This amount of CO2 is sufficient to saturate the brine solution. The excess will exist in the CO2-rich gaseous phase.
# Step 6: Define the initial chemical equilibrium state
problem = EquilibriumProblem(system)
problem.setPartition(partition)
problem.setTemperature(60, "celsius")
problem.setPressure(100, "bar")
problem.add("H2O", 1, "kg")
problem.add("NaCl", 1, "mol")
problem.add("CO2", 1, "mol")
Attention
To ensure that that the equilibrium calculation performed in the next step ignores the kinetic species in the system so that we maintain a disequilibrium state between equilibrium and kinetic species, it is important not to forget the following command:
problem.setPartition(partition)
Ignoring this step will produce an initial condition for the amounts of equilibrium and kinetic species that correspond to a complete equilibrium state in the system so that no kinetics calculation makes sense afterwards!
Calculating the initial amounts of the equilibrium species¶
We now use the convenient equilibrate
function to calculate the amounts of
the equilibrium species by minimizing the Gibbs energy of the equilibrium
partition only, and not of the entire system. The result is stored in the
object state0
of class ChemicalState, a computational representation of
the state of a multiphase chemical system defined by its temperature (T),
pressure (P), and vector of species amounts (n). We then output this
chemical state to a file.
# Step 7: Calculate the initial chemical equilibrium state
state0 = equilibrate(problem)
state0.output('demo-kineticpath-carbonates-co2-before-kinetics')
Setting the initial mass of minerals¶
We have now to prescribe the initial amounts of the kinetic species (i.e., the carbonate minerals). This is done below by setting the initial mass of calcite to 100 g and of dolomite to 50 g. The initial amount of magnesite is zero.
# Step 8: Set the initial mass of the kinetic species
state0.setSpeciesMass("Calcite", 100, "g")
state0.setSpeciesMass("Dolomite", 50, "g")
Setting the kinetic path calculations¶
To be able to run the simulation of the kinetic process of mineral dissolution/precipitation, we introduce the instance of the class KineticPath that enables this functionality.
# Step 9: Create a kinetic path solver
path = KineticPath(reactions)
path.setPartition(partition)
The instance of kinetic path solver is provided with the partition to the equilibrium, kinetic, and inert species defined above.
Attention
For repeated chemical kinetics calculations (e.g., in a reactive transport
simulation, where kinetics is performed for each mesh cell/node), consider
using the class KineticSolver instead for avoiding some overhead of
class KineticPath. Consider also the class EquilibriumSolver,
instead of method equilibrate
, for similar reasons.
Plotting the evolution of thermochemical properties¶
Usage of the ChemicalPlot allows us to create plots illustrating the evolution of different properties of the chemical system over the time interval in which kinetics is calculated.
# Step 10: Create plots for the kinetic path calculation
plot0 = path.plot()
plot0.x("time(units=hour)")
plot0.y("pH")
plot0.xlabel("Time [hour]")
plot0.ylabel("pH")
plot0.showlegend(False)
plot1 = path.plot()
plot1.x("time(units=hour)")
plot1.y("elementMolality(Ca)", "Ca")
plot1.y("elementMolality(Mg)", "Mg")
plot1.xlabel("Time [hour]")
plot1.ylabel("Concentration [molal]")
plot1.legend("right center")
plot2 = path.plot()
plot2.x("time(units=hour)")
plot2.y("phaseMass(Calcite units=grams)", "Calcite")
plot2.xlabel("Time [hour]")
plot2.ylabel("Mass [g]")
plot3 = path.plot()
plot3.x("time(units=hour)")
plot3.y("phaseMass(Dolomite units=grams)", "Dolomite")
plot3.xlabel("Time [hour]")
plot3.ylabel("Mass [g]")
The code above plots four different figures with the evolution of pH, the
concentration of calcium and magnesium, and the mass of calcite dolomite. In
particular, plot1 shows how two properties, i.e., calcium and magnesium
concentrations, with respect to the time (in hours). Methods plot1.x()
and
plot1.y()
fetch properties that are meant to be plotted on x and y axises,
respectively. The arguments of the function plot1.y("elementMolality(Ca)",
"Ca")
stand for the name of the property we want to fetch from the chemical
state and the tag, we want to illustrate it with.
Note
A list of all possible quantities that can be plotted is shown in the class ChemicalQuantity.
Solving the chemical kinetics problem¶
Finally, we calculate the transient state of the entire system comprised of the
carbonate minerals, the CO2-saturated brine fluid, and the CO2-rich gaseous
phase. This is performed by using the method solve
of the KineticPath
class, which expects the initial state (state0
), the initial and final
times (t0
and t1
respectively) of the kinetic path, and time units of
the specified time (e.g., s, minute, hour, day, year, etc.).
# Step 11: Perform the kinetic path calculation
t0, t1 = 0.0, 25.0
path.solve(state0, t0, t1, "hours")
Have you got an issue?¶
Have you found any issue or error in this tutorial? Do you have any recommendations or you think something is not clear enough? Please, let us know by filling a new issue here:
You’ll need a GitHub account - but this is easy to sort out if you don’t have one yet!