# Coupling Reaktoro into other reactive transport codes¶

In this tutorial, we show how Reaktoro can be used in other codes for reactive transport modeling. Here, you will find that we have split the mass transport and chemical reaction calculations so that you can clearly see how a dedicated and advanced transport solver could be combined with Reaktoro’s solvers for chemical reaction calculations. A basic transport solver is used here instead for the sake of software coupling demonstration.

The reactive transport problem we consider here is also relatively simple, similar to the one presented in the tutorial Reactive transport of CO2-saturated brine along a porous rock column. Unlike other tutorials, we proceed here first with a step-by-step explanation of the full script found at the end of this tutorial.

## Importing python packages¶

First, we need to import a few Python packages to enable us to perform the numerical calculations and plotting.

```from reaktoro import *
from numpy import *
import matplotlib.pyplot as plt
from joblib import Parallel, delayed
import os, time
```

We import the reaktoro Python package so that we can use its classes and methods for performing chemical reaction calculations, numpy for working with arrays, matplotlib for plotting capabilities, joblib for simple parallel computing, and, finally, os, to provide a portable way of using operating system dependent functionality.

## Defining parameters for the reactive transport simulation¶

Next, we define reactive transport and numerical discretization parameters.

```xl = 0.0          # the x-coordinate of the left boundary
xr = 1.0          # the x-coordinate of the right boundary
nsteps = 100      # the number of steps in the reactive transport simulation
ncells = 100      # the number of cells in the discretization

D  = 1.0e-9       # the diffusion coefficient (in units of m2/s)
v  = 1.0/day      # the fluid pore velocity (in units of m/s)
dt = 10*minute    # the time step (in units of s)
T = 60.0 + 273.15 # the temperature (in units of K)
P = 100 * 1e5     # the pressure (in units of Pa)

dirichlet = False  # the parameter that determines whether Dirichlet BC must be used
```

We specify the considered rock domain by setting coordinates of its left and right boundaries to 0.0 m and 100.0 m, respectively. The discretization parameters, i.e., the number of cells and steps in time, are both set to 100. The reactive transport modeling procedure assumes a constant fluid velocity of 1 m/day (1.16 · 10-5 m/s) and the same diffusion coefficient of 10-9 m2/s for all fluid species (without dispersivity). The size of the time-step is set to 10 minutes. Temperature and pressure are set to 60 °C and 100 bar, respectively, throughout the whole tutorial. The chemical equilibrium calculations performed in each mesh cell, at every time step, using Gibbs energy minimisation algorithm (provided by the class EquilibriumSolver).

The boolean variable `dirichlet` is set to `True` or `False` depending on which boundary condition is considered in the numerical calculation. Set to `False` for imposing the flux of the injected fluid, otherwise, set to `True` for imposing the composition of the fluid on the left boundary.

## Specifying the quantities and properties to be outputted¶

Before running the reactive transport simulations, we specify the list of parameters we are interested in outputting. In this case, it is pH, molality of H+, Ca2+, Mg2+, HCO3-, CO2 (aq), as well as a phase volume of calcite and dolomite.

```output_quantities = """
pH
speciesMolality(H+)
speciesMolality(Ca++)
speciesMolality(Mg++)
speciesMolality(HCO3-)
speciesMolality(CO2(aq))
phaseVolume(Calcite)
phaseVolume(Dolomite)
""".split()
```

## Organizing the main function in three parts¶

Below is the main function in our script, which shows three parts, each represented by a Python function documented in the next sections:

```if __name__ == '__main__':

# Create folders for the results
make_results_folders()

# Run the reactive transport simulations
simulate()

# Plotting the result
plot()
```

Here, we first create the required folders for the results, we then run the reactive transport simulation, and, finally, plot the outputted results.

## Creating folders for the outputted results¶

```def make_results_folders():
os.system('mkdir -p results')
os.system('mkdir -p figures/ph')
os.system('mkdir -p figures/aqueous-species')
os.system('mkdir -p figures/calcite-dolomite')
os.system('mkdir -p videos')
```

Using os package, we create required folders for outputting the obtained results and for the plot and video files later.

## Perform the reactive transport simulation¶

The reactive transport simulation is performed in the function `simulate` (you may want to have a quick look in the code below and move to a detailed explanation in the subsequent sections):

```def simulate():

# Step 7.1: Construct the chemical system with its phases and species
db = Database('supcrt98.xml')

editor = ChemicalEditor(db)

editor.addAqueousPhase([
'H2O(l)',
'H+',
'OH-',
'Na+',
'Cl-',
'Ca++',
'Mg++',
'HCO3-',
'CO2(aq)',
'CO3--'])  # aqueous species are individually selected for performance reasons
editor.addMineralPhase('Quartz')
editor.addMineralPhase('Calcite')
editor.addMineralPhase('Dolomite')

# Step 7.2: Create the ChemicalSystem object using the configured editor
system = ChemicalSystem(editor)

# Step 7.3: Define the initial condition of the reactive transport modeling problem
problem_ic = EquilibriumProblem(system)
problem_ic.setTemperature(T)
problem_ic.setPressure(P)
problem_ic.add('H2O', 1.0, 'kg')
problem_ic.add('NaCl', 0.7, 'mol')
problem_ic.add('CaCO3', 10, 'mol')
problem_ic.add('SiO2', 10, 'mol')

# Step 7.4: Define the boundary condition of the reactive transport modeling problem
problem_bc = EquilibriumProblem(system)
problem_bc.setTemperature(T)
problem_bc.setPressure(P)
problem_bc.add('H2O', 1.0, 'kg')
problem_bc.add('NaCl', 0.90, 'mol')
problem_bc.add('MgCl2', 0.05, 'mol')
problem_bc.add('CaCl2', 0.01, 'mol')
problem_bc.add('CO2', 0.75, 'mol')

# Step 7.5: Calculate the equilibrium states for the initial and boundary conditions
state_ic = equilibrate(problem_ic)
state_bc = equilibrate(problem_bc)

# Step 7.6: Scale the volumes of the phases in the initial condition
state_ic.scalePhaseVolume('Aqueous', 0.1, 'm3')
state_ic.scalePhaseVolume('Quartz', 0.882, 'm3')
state_ic.scalePhaseVolume('Calcite', 0.018, 'm3')

# Scale the boundary condition state to 1 m3
state_bc.scaleVolume(1.0, 'm3')

# Step 7.7: Partitioning fluid and solid species

# The number of elements in the chemical system
nelems = system.numElements()

# The indices of the fluid and solid species
ifluid_species = system.indicesFluidSpecies()
isolid_species = system.indicesSolidSpecies()

# The concentrations of each element in each mesh cell (in the current time step)
b = zeros((ncells, nelems))

# The concentrations (mol/m3) of each element in the fluid partition, in each mesh cell
bfluid = zeros((ncells, nelems))

# The concentrations (mol/m3) of each element in the solid partition, in each mesh cell
bsolid = zeros((ncells, nelems))

# Initialize the concentrations (mol/m3) of the elements in each mesh cell
b[:] = state_ic.elementAmounts()

# Initialize the concentrations (mol/m3) of each element on the boundary
b_bc = state_bc.elementAmounts()

# Step 7.8: Create a list of chemical states for the mesh cells

# The list of chemical states, one for each cell, initialized to state_ic
states = [state_ic.clone() for _ in range(ncells)]

# Step 7.9: Create the equilibrium solver object for the repeated equilibrium calculation

# The interval [xl, xr] split into ncells
x = linspace(xl, xr, ncells + 1)

# The length of each mesh cell (in units of m)
dx = (xr - xl)/ncells

# Step 7.10: Create the equilibrium solver object for the repeated equilibrium calculation
solver = EquilibriumSolver(system)

# Step 7.11: The auxiliary function to create an output file each time step
def outputstate():
# Create the instance of ChemicalOutput class
output = ChemicalOutput(system)

# The number of digits in number of steps (e.g., 100 has 3 digits)
ndigits = len(str(nsteps))

# Provide the output file name, which will correspond
output.filename('results/{}.txt'.format(str(step).zfill(ndigits)))

# We define the columns' tags filled with the name of the quantities
# The first column has a tag 'x' (which corresponds to the center coordinates of the cells )
output.add('tag', 'x') # The value of the center coordinates of the cells

# The rest of the columns correspond to the requested properties
for quantity in output_quantities:
output.add(quantity)

# We update the file with states that correspond to the cells' coordinates stored in x
output.open()
for state, tag in zip(states, x):
output.update(state, tag)
output.close()

# Step 7.12: Running the reactive transport simulation loop
step = 0  # the current step number
t = 0.0  # the current time (in seconds)

while step <= nsteps:
# Print the progress of the simulation
print("Progress: {}/{} steps, {} min".format(step, nsteps, t/minute))

# Output the current state of the reactive transport calculation
outputstate()

# Collect the amounts of elements from fluid and solid partitions
for icell in range(ncells):
bfluid[icell] = states[icell].elementAmountsInSpecies(ifluid_species)
bsolid[icell] = states[icell].elementAmountsInSpecies(isolid_species)

# Transport each element in the fluid phase
for j in range(nelems):
transport(bfluid[:, j], dt, dx, v, D, b_bc[j])

# Update the amounts of elements in both fluid and solid partitions
b[:] = bsolid + bfluid

# Equilibrating all cells with the updated element amounts
for icell in range(ncells):
solver.solve(states[icell], T, P, b[icell])

# Increment time step and number of time steps
t += dt
step += 1

print("Finished!")
```

We now explain step-by-step the function `simulate`. We begin by defining the chemical system using the ChemicalEditor class:

```    db = Database('supcrt98.xml')

editor = ChemicalEditor(db)

editor.addAqueousPhase([
'H2O(l)',
'H+',
'OH-',
'Na+',
'Cl-',
'Ca++',
'Mg++',
'HCO3-',
'CO2(aq)',
'CO3--'])  # aqueous species are individually selected for performance reasons
editor.addMineralPhase('Quartz')
editor.addMineralPhase('Calcite')
editor.addMineralPhase('Dolomite')
```

We specify aqueous and mineral phases that should be considered in the chemical system. For performance reasons, the aqueous phase is defined by manually specifying the chemical species, instead of automatic collection of species from database. There are three pure mineral phase considered: quartz (SiO2), calcite (CaCO3), and dolomite (CaMg(CO3)2).

### Creating the chemical system¶

This step is where we create an object of class ChemicalSystem using the chemical system definition details stored in the object `editor`.

```    system = ChemicalSystem(editor)
```

### Initial condition (IC) of the reactive transport problem¶

After constructing the chemical system of interest, we can proceed to the definition of a chemical equilibrium problem to set the initial condition for the fluid and rock composition that is consistent with our intention of modeling reactive transport in a porous rock column composed of quartz and calcite, at 60 °C and 100 bar, with a 0.7 NaCl molal brine in equilibrium with the rock minerals. To ensure equilibrium with both quartz and calcite, the equilibrium problem setup below considers a relatively large amount for these minerals (10 moles each).

```    problem_ic = EquilibriumProblem(system)
problem_ic.setTemperature(T)
problem_ic.setPressure(P)
problem_ic.add('H2O', 1.0, 'kg')
problem_ic.add('NaCl', 0.7, 'mol')
problem_ic.add('CaCO3', 10, 'mol')
problem_ic.add('SiO2', 10, 'mol')
```

We will later scale the volumes of the aqueous and mineral phases so that they are consistent with a 10 % porosity and the required volume percentages of the rock minerals (98 %vol of quartz and 2 %vol of calcite).

### Boundary condition (BC) of the reactive transport problem¶

For the boundary condition, we need to specify the composition of the fluid that is injected into the rock. This is done below, by defining an equilibrium problem that will later be solved to produce an aqueous fluid with 0.9 molal NaCl, 0.05 molal MgCl2, 0.01 CaCl2, and 0.75 molal CO2, in a state very close to CO2 saturation.

```    problem_bc = EquilibriumProblem(system)
problem_bc.setTemperature(T)
problem_bc.setPressure(P)
problem_bc.add('H2O', 1.0, 'kg')
problem_bc.add('NaCl', 0.90, 'mol')
problem_bc.add('MgCl2', 0.05, 'mol')
problem_bc.add('CaCl2', 0.01, 'mol')
problem_bc.add('CO2', 0.75, 'mol')
```

The composition of the injected aqueous fluid results from the solution of the above equilibrium problem, in which 1 kg of H2O is mixed with 0.90 moles of NaCl, 0.05 moles of MgCl2, 0.01 moles of CaCl2, and 0.75 moles of CO2.

### Calculate the equilibrium states for the initial and boundary conditions¶

```    state_ic = equilibrate(problem_ic)
state_bc = equilibrate(problem_bc)
```

In this step, we use method `equilibrate` to calculate the chemical equilibrium state of the system with the given initial and boundary equilibrium conditions stored in the objects `problem_ic` and `problem_bc`. The numerical solution of each problem results in the objects `state_ic` and `state_bc`, respectively, of class ChemicalState, which stores the temperature, pressure, and the amounts of every species in the system.

### Scaling the volumes of the phases¶

We scale the volumes of the phases in the initial condition according to the condition that porosity is 10 % (ratio of fluid volume to total volume), and the volume fractions of minerals are 98 %vol of quartz (SiO2) and 2 %vol of calcite (ratio of mineral volume to solid volume).

For the chemical state representing the boundary condition for the injected fluid composition, we scale its volume to 1 m3. This is done so that the amounts of the species in the fluid is consistent with a mol/m3 scale.

```    state_ic.scalePhaseVolume('Aqueous', 0.1, 'm3')
state_ic.scalePhaseVolume('Quartz', 0.882, 'm3')
state_ic.scalePhaseVolume('Calcite', 0.018, 'm3')

# Scale the boundary condition state to 1 m3
state_bc.scaleVolume(1.0, 'm3')
```

### Partitioning fluid and solid species¶

Only species in fluid phases are mobile and transported by advection and diffusion mechanisms. The solid phases are immobile.

The code below identify the indices of the fluid and solid species, and also create arrays to keep track of the amounts of elements in the fluid and solid partition (i.e., the amounts of elements among all fluid phases, here only an aqueous phase, and the amounts of elements among all solid phases, here the mineral phases).

```    # The number of elements in the chemical system
nelems = system.numElements()

# The indices of the fluid and solid species
ifluid_species = system.indicesFluidSpecies()
isolid_species = system.indicesSolidSpecies()

# The concentrations of each element in each mesh cell (in the current time step)
b = zeros((ncells, nelems))

# The concentrations (mol/m3) of each element in the fluid partition, in each mesh cell
bfluid = zeros((ncells, nelems))

# The concentrations (mol/m3) of each element in the solid partition, in each mesh cell
bsolid = zeros((ncells, nelems))

# Initialize the concentrations (mol/m3) of the elements in each mesh cell
b[:] = state_ic.elementAmounts()

# Initialize the concentrations (mol/m3) of each element on the boundary
b_bc = state_bc.elementAmounts()
```

We use methods `indicesFluidSpecies` and `indicesSolidSpecies` of class ChemicalSystem to get the indices of the fluid and solid species, which are then stored in the lists `ifluid_species` and `isolid_species`, respectively. Then, we define the arrays `b`, `bfluid`, `bsolid`, that will store, respectively, the concentrations (mol/m3) of each element in the system, in the fluid partition, and in the solid partition at every time step.

The array `b` is initialized with the concentrations of the elements at the initial chemical state, `state_ic`, using method `elementAmounts` of class ChemicalState. The array `b_bc` stores the concentrations of each element on the boundary.

### Create a list of chemical states for the mesh cells¶

Every mesh cell needs a ChemicalState object, which is done below with the creation of a list `states` of length `ncells`, initialized with `state_ic` for each cell (i.e., all cells have initially the same fluid and rock composition).

```    # The list of chemical states, one for each cell, initialized to state_ic
states = [state_ic.clone() for _ in range(ncells)]
```

### Cell coordinates and lengths¶

Next, we generate the coordinates of the mesh nodes (array `x`) by equally dividing the interval [xr, xl] into `ncells`. The length between each consecutive mesh nodes is computed and stored in `dx` (the length of the mesh cells).

```    # The interval [xl, xr] split into ncells
x = linspace(xl, xr, ncells + 1)

# The length of each mesh cell (in units of m)
dx = (xr - xl)/ncells
```

### Creating the equilibrium solver¶

For the repeated equilibrium calculation, we define an equilibrium solver object using EquilibriumSolver class.

```    solver = EquilibriumSolver(system)
```

### Define an auxiliary function for creating the output file¶

The auxiliary function `outputstate` uses the class ChemicalOutput to output quantities and properties at each mesh cell, at every time step.

```    def outputstate():
# Create the instance of ChemicalOutput class
output = ChemicalOutput(system)

# The number of digits in number of steps (e.g., 100 has 3 digits)
ndigits = len(str(nsteps))

# Provide the output file name, which will correspond
output.filename('results/{}.txt'.format(str(step).zfill(ndigits)))

# We define the columns' tags filled with the name of the quantities
# The first column has a tag 'x' (which corresponds to the center coordinates of the cells )
output.add('tag', 'x') # The value of the center coordinates of the cells

# The rest of the columns correspond to the requested properties
for quantity in output_quantities:
output.add(quantity)

# We update the file with states that correspond to the cells' coordinates stored in x
output.open()
for state, tag in zip(states, x):
output.update(state, tag)
output.close()
```

### Running the reactive transport simulation¶

Before proceeding to the reactive transport calculations, we set the initial time and a counter for the number of time steps.

```    step = 0  # the current step number
t = 0.0  # the current time (in seconds)

while step <= nsteps:
# Print the progress of the simulation
print("Progress: {}/{} steps, {} min".format(step, nsteps, t/minute))

# Output the current state of the reactive transport calculation
outputstate()

# Collect the amounts of elements from fluid and solid partitions
for icell in range(ncells):
bfluid[icell] = states[icell].elementAmountsInSpecies(ifluid_species)
bsolid[icell] = states[icell].elementAmountsInSpecies(isolid_species)

# Transport each element in the fluid phase
for j in range(nelems):
transport(bfluid[:, j], dt, dx, v, D, b_bc[j])

# Update the amounts of elements in both fluid and solid partitions
b[:] = bsolid + bfluid

# Equilibrating all cells with the updated element amounts
for icell in range(ncells):
solver.solve(states[icell], T, P, b[icell])

# Increment time step and number of time steps
t += dt
step += 1

print("Finished!")
```

The reactive transport simulation continues until the maximum number of time steps is achieved. At each time step, we output the progress of the simulation using function `outputstate`. Then, we collect the amounts of elements from fluid and solid partition. We now apply an operator splitting procedure, in which we first update the amounts of elements in the fluid partition (`bfluid`) using the transport equations (without reactions). These updated amounts of elements in the fluid partition are now summed with the amounts of elements in the solid partition (`bsolid`, which remained constant during the transport step), and thus updating the amounts of elements in the chemical system (`b`). We now use these updated amounts of elements in the cell (`b`) to evaluate its new chemical equilibrium state, thus producing new amounts of the species in both the fluid and solid phases (available in the list `states` of ChemicalState objects). This chemical reaction equilibrium calculation step, at each mesh cell, permits, for example, aqueous species and minerals to react, and thus causes mineral dissolution or precipitation, depending on how much the amount of mineral species changes. This can then be used, for example, to compute new porosity value for the cell.

### Solving the transport equations¶

Reactive transport calculations involve the solution of a system of advection-diffusion-reaction equations. Due to the use of operator splitting scheme in this tutorial, we solve the transport equations and then perform the chemical reaction calculations for each cell before proceeding to the next time step.

The `transport` function below is responsible for solving an advection-diffusion equation, that is later applied to transport the concentrations (mol/m3) of elements in the fluid partition (a simplification that is possible because of common diffusion coefficients and velocities of the fluid species, otherwise the transport of individual fluid species would be needed).

```def transport(u, dt, dx, v, D, g):
# Number of DOFs
n = len(u)
alpha = D*dt/dx**2
beta = v*dt/dx
a = full(n, -beta - alpha)
b = full(n, 1 + beta + 2*alpha)
c = full(n, -alpha)

# Set the boundary condition
if dirichlet:
# Use Dirichlet BC boundary conditions
b = 1.0
c = 0.0
u = g
else:
# Use flux boundary conditions
u += beta*g
b = 1 + beta + alpha
b[-1] = 1 + beta + alpha

# Solve a tridiagonal matrix equation
thomas(a, b, c, u)
```

The function `transport` expects a conservative property (argument `u`) (e.g., the concentration (mol/m3) of j-th element in the fluid given by `bfluid[j]`), the time step (`dt`), the mesh cell length (`dx`), the fluid velocity (`v`), the diffusion coefficient (`D`), and the boundary condition of the conservative property (`g`) (e.g., the concentration of the j-th element in the fluid on the left boundary).

The transport equations are solved with a finite volume method. Its discretization in space and time (implicit) results in the constants `alpha` and `beta`. These correspond to the diffusion and advection terms in the equation: `D*dt/dx**2` and `v*dt/dx`, respectively.

Arrays `a`, `b`, `c` are the diagonals in the tridiagonal matrix that results by writing all discretized equations in a matrix equation. This system of linear equations is solved by the tridiagonal matrix algorithm, also known as the Thomas algorithm:

```def thomas(a, b, c, d):
n = len(d)
c /= b
for i in range(1, n - 1):
c[i] /= b[i] - a[i]*c[i - 1]
d /= b
for i in range(1, n):
d[i] = (d[i] - a[i]*d[i - 1])/(b[i] - a[i]*c[i - 1])
x = d
for i in reversed(range(0, n - 1)):
x[i] -= c[i]*x[i + 1]
return x
```

## Plotting of the results¶

The last block of the main routine is plotting of the results.

```def plot():
# Plot all result files
files = sorted(os.listdir('results'))
Parallel(n_jobs=16)(delayed(plotfile)(file) for file in files)
# Create videos for the figures
ffmpegstr = 'ffmpeg -y -r 30 -i figures/{0}/%03d.png -codec:v mpeg4 -flags:v +qscale -global_quality:v 0 videos/{0}.mp4'
os.system(ffmpegstr.format('calcite-dolomite'))
os.system(ffmpegstr.format('aqueous-species'))
os.system(ffmpegstr.format('ph'))
```

The full script for the reactive transport calculation is shown below.

```from reaktoro import *
from numpy import *
import matplotlib.pyplot as plt
from joblib import Parallel, delayed
import os, time

# Step 2: Auxiliary time related constants
second = 1
minute = 60
hour = 60 * minute
day = 24 * hour
year = 365 * day

# Step 3: Parameters for the reactive transport simulation
xl = 0.0          # the x-coordinate of the left boundary
xr = 1.0          # the x-coordinate of the right boundary
nsteps = 100      # the number of steps in the reactive transport simulation
ncells = 100      # the number of cells in the discretization

D  = 1.0e-9       # the diffusion coefficient (in units of m2/s)
v  = 1.0/day      # the fluid pore velocity (in units of m/s)
dt = 10*minute    # the time step (in units of s)
T = 60.0 + 273.15 # the temperature (in units of K)
P = 100 * 1e5     # the pressure (in units of Pa)

dirichlet = False  # the parameter that determines whether Dirichlet BC must be used

# Step 4: The list of quantities to be output for each mesh cell, each time step
output_quantities = """
pH
speciesMolality(H+)
speciesMolality(Ca++)
speciesMolality(Mg++)
speciesMolality(HCO3-)
speciesMolality(CO2(aq))
phaseVolume(Calcite)
phaseVolume(Dolomite)
""".split()

# Step 7: Perform the reactive transport simulation
def simulate():

# Step 7.1: Construct the chemical system with its phases and species
db = Database('supcrt98.xml')

editor = ChemicalEditor(db)

editor.addAqueousPhase([
'H2O(l)',
'H+',
'OH-',
'Na+',
'Cl-',
'Ca++',
'Mg++',
'HCO3-',
'CO2(aq)',
'CO3--'])  # aqueous species are individually selected for performance reasons
editor.addMineralPhase('Quartz')
editor.addMineralPhase('Calcite')
editor.addMineralPhase('Dolomite')

# Step 7.2: Create the ChemicalSystem object using the configured editor
system = ChemicalSystem(editor)

# Step 7.3: Define the initial condition of the reactive transport modeling problem
problem_ic = EquilibriumProblem(system)
problem_ic.setTemperature(T)
problem_ic.setPressure(P)
problem_ic.add('H2O', 1.0, 'kg')
problem_ic.add('NaCl', 0.7, 'mol')
problem_ic.add('CaCO3', 10, 'mol')
problem_ic.add('SiO2', 10, 'mol')

# Step 7.4: Define the boundary condition of the reactive transport modeling problem
problem_bc = EquilibriumProblem(system)
problem_bc.setTemperature(T)
problem_bc.setPressure(P)
problem_bc.add('H2O', 1.0, 'kg')
problem_bc.add('NaCl', 0.90, 'mol')
problem_bc.add('MgCl2', 0.05, 'mol')
problem_bc.add('CaCl2', 0.01, 'mol')
problem_bc.add('CO2', 0.75, 'mol')

# Step 7.5: Calculate the equilibrium states for the initial and boundary conditions
state_ic = equilibrate(problem_ic)
state_bc = equilibrate(problem_bc)

# Step 7.6: Scale the volumes of the phases in the initial condition
state_ic.scalePhaseVolume('Aqueous', 0.1, 'm3')
state_ic.scalePhaseVolume('Quartz', 0.882, 'm3')
state_ic.scalePhaseVolume('Calcite', 0.018, 'm3')

# Scale the boundary condition state to 1 m3
state_bc.scaleVolume(1.0, 'm3')

# Step 7.7: Partitioning fluid and solid species

# The number of elements in the chemical system
nelems = system.numElements()

# The indices of the fluid and solid species
ifluid_species = system.indicesFluidSpecies()
isolid_species = system.indicesSolidSpecies()

# The concentrations of each element in each mesh cell (in the current time step)
b = zeros((ncells, nelems))

# The concentrations (mol/m3) of each element in the fluid partition, in each mesh cell
bfluid = zeros((ncells, nelems))

# The concentrations (mol/m3) of each element in the solid partition, in each mesh cell
bsolid = zeros((ncells, nelems))

# Initialize the concentrations (mol/m3) of the elements in each mesh cell
b[:] = state_ic.elementAmounts()

# Initialize the concentrations (mol/m3) of each element on the boundary
b_bc = state_bc.elementAmounts()

# Step 7.8: Create a list of chemical states for the mesh cells

# The list of chemical states, one for each cell, initialized to state_ic
states = [state_ic.clone() for _ in range(ncells)]

# Step 7.9: Create the equilibrium solver object for the repeated equilibrium calculation

# The interval [xl, xr] split into ncells
x = linspace(xl, xr, ncells + 1)

# The length of each mesh cell (in units of m)
dx = (xr - xl)/ncells

# Step 7.10: Create the equilibrium solver object for the repeated equilibrium calculation
solver = EquilibriumSolver(system)

# Step 7.11: The auxiliary function to create an output file each time step
def outputstate():
# Create the instance of ChemicalOutput class
output = ChemicalOutput(system)

# The number of digits in number of steps (e.g., 100 has 3 digits)
ndigits = len(str(nsteps))

# Provide the output file name, which will correspond
output.filename('results/{}.txt'.format(str(step).zfill(ndigits)))

# We define the columns' tags filled with the name of the quantities
# The first column has a tag 'x' (which corresponds to the center coordinates of the cells )
output.add('tag', 'x') # The value of the center coordinates of the cells

# The rest of the columns correspond to the requested properties
for quantity in output_quantities:
output.add(quantity)

# We update the file with states that correspond to the cells' coordinates stored in x
output.open()
for state, tag in zip(states, x):
output.update(state, tag)
output.close()

# Step 7.12: Running the reactive transport simulation loop
step = 0  # the current step number
t = 0.0  # the current time (in seconds)

while step <= nsteps:
# Print the progress of the simulation
print("Progress: {}/{} steps, {} min".format(step, nsteps, t/minute))

# Output the current state of the reactive transport calculation
outputstate()

# Collect the amounts of elements from fluid and solid partitions
for icell in range(ncells):
bfluid[icell] = states[icell].elementAmountsInSpecies(ifluid_species)
bsolid[icell] = states[icell].elementAmountsInSpecies(isolid_species)

# Transport each element in the fluid phase
for j in range(nelems):
transport(bfluid[:, j], dt, dx, v, D, b_bc[j])

# Update the amounts of elements in both fluid and solid partitions
b[:] = bsolid + bfluid

# Equilibrating all cells with the updated element amounts
for icell in range(ncells):
solver.solve(states[icell], T, P, b[icell])

# Increment time step and number of time steps
t += dt
step += 1

print("Finished!")

# Step 10: Return a string for the title of a figure in the format Time: #h##m
def titlestr(t):
t = t / minute   # Convert from seconds to minutes
h = int(t) / 60  # The number of hours
m = int(t) % 60  # The number of remaining minutes
return 'Time: {:>3}h{:>2}m'.format(h, str(m).zfill(2))

# Step 9: Generate figures for a result file
def plotfile(file):
step = int(file.split('.'))

print('Plotting figure', step, '...')

t = step * dt
filearray = loadtxt('results/' + file, skiprows=1)
data = filearray.T

ndigits = len(str(nsteps))

plt.figure()
plt.xlim(left=-0.02, right=0.52)
plt.ylim(bottom=2.5, top=10.5)
plt.title(titlestr(t))
plt.xlabel('Distance [m]')
plt.ylabel('pH')
plt.plot(data, data)
plt.tight_layout()
plt.savefig('figures/ph/{}.png'.format(str(step).zfill(ndigits)))

plt.figure()
plt.xlim(left=-0.02, right=0.52)
plt.ylim(bottom=-0.1, top=2.1)
plt.ticklabel_format(style='sci', axis='y', scilimits=(0,0))
plt.title(titlestr(t))
plt.xlabel('Distance [m]')
plt.ylabel('Mineral Volume [%\$_{\mathsf{vol}}\$]')
plt.plot(data, data * 100, label='Calcite')
plt.plot(data, data * 100, label='Dolomite')
plt.legend(loc='center right')
plt.tight_layout()
plt.savefig('figures/calcite-dolomite/{}.png'.format(str(step).zfill(ndigits)))

plt.figure()
plt.yscale('log')
plt.xlim(left=-0.02, right=0.52)
plt.ylim(bottom=0.5e-5, top=2)
plt.title(titlestr(t))
plt.xlabel('Distance [m]')
plt.ylabel('Concentration [molal]')
plt.plot(data, data, label='Ca++')
plt.plot(data, data, label='Mg++')
plt.plot(data, data, label='HCO3-')
plt.plot(data, data, label='CO2(aq)')
plt.plot(data, data, label='H+')
plt.legend(loc='lower right')
plt.tight_layout()
plt.savefig('figures/aqueous-species/{}.png'.format(str(step).zfill(ndigits)))

plt.close('all')

# Step 8: Plot all result files and generate a video
def plot():
# Plot all result files
files = sorted(os.listdir('results'))
Parallel(n_jobs=16)(delayed(plotfile)(file) for file in files)
# Create videos for the figures
ffmpegstr = 'ffmpeg -y -r 30 -i figures/{0}/%03d.png -codec:v mpeg4 -flags:v +qscale -global_quality:v 0 videos/{0}.mp4'
os.system(ffmpegstr.format('calcite-dolomite'))
os.system(ffmpegstr.format('aqueous-species'))
os.system(ffmpegstr.format('ph'))

# Step 7.10.2: Solve a tridiagonal matrix equation using Thomas algorithm (or TriDiagonal Matrix Algorithm (TDMA))
def thomas(a, b, c, d):
n = len(d)
c /= b
for i in range(1, n - 1):
c[i] /= b[i] - a[i]*c[i - 1]
d /= b
for i in range(1, n):
d[i] = (d[i] - a[i]*d[i - 1])/(b[i] - a[i]*c[i - 1])
x = d
for i in reversed(range(0, n - 1)):
x[i] -= c[i]*x[i + 1]
return x

# Step 7.10.1: Perform a transport step
def transport(u, dt, dx, v, D, g):
# Number of DOFs
n = len(u)
alpha = D*dt/dx**2
beta = v*dt/dx
a = full(n, -beta - alpha)
b = full(n, 1 + beta + 2*alpha)
c = full(n, -alpha)

# Set the boundary condition
if dirichlet:
# Use Dirichlet BC boundary conditions
b = 1.0
c = 0.0
u = g
else:
# Use flux boundary conditions
u += beta*g
b = 1 + beta + alpha
b[-1] = 1 + beta + alpha

# Solve a tridiagonal matrix equation
thomas(a, b, c, u)

# Step 6: Creating folders for the results' output
def make_results_folders():
os.system('mkdir -p results')
os.system('mkdir -p figures/ph')
os.system('mkdir -p figures/aqueous-species')
os.system('mkdir -p figures/calcite-dolomite')
os.system('mkdir -p videos')

# Step 5: Define the main function
if __name__ == '__main__':

# Create folders for the results
make_results_folders()

# Run the reactive transport simulations
simulate()

# Plotting the result
plot()
```

Todo

Add figures and videos here in this tutorial.

## 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!