The effect of limestone addition on a hydrated cement mix#

Written jointly by Svetlana Kyas (ETH Zurich) and Dan Miron (PSI) on April 4th, 2022.
Last revised on January 18th, 2024 by Allan Leal.


Always make sure you are using the latest version of Reaktoro. Otherwise, some new features documented on this website will not work on your machine and you may receive unintuitive errors. Follow these update instructions to get the latest version of Reaktoro!

This tutorial shows how Reaktoro can be used for modeling cementitious systems by using the thermodynamic data for cement hydrates from cemdata18 database. The example shows the effects of addition of limestone addition to the cement formulation.


If your main interest is in calculating thermodynamic properties of cement phases rather than modeling chemical equilibria and kinetics, check out ThermoFun, an excellent project dedicated to this task.

The model considered in this tutorial a thermofun database cemdata18 based on Cemdata, containing thermodynamic data for hydrated cement phases in the system (CaO-Al2O3-SiO2-CaSO4-CaCO3-Fe2O3-MgO-H2O). We start with the initialization of the chemical system by defining the elements of the aqueous and gas phase. The activity model of the aqueous phase is set to Debye-Hückel, where the parameters å and b are for the KOH background electrolyte (typical for cement systems where KOH is the most abundant electrolyte in the pore solution).

from reaktoro import *

# Define the Thermofun database
db = ThermoFunDatabase("cemdata18")

# Define an aqueous solution phase
solution = AqueousPhase(speciate("H O K Na S Si Ca Mg Al C Cl"))

# Set up a and b parameters for the ionic species (KOH, b = 0.123, a = 3.67) and the Debye-Huckel activity model
params = ActivityModelDebyeHuckelParams()
params.aiondefault = 3.67
params.biondefault = 0.123
params.bneutraldefault = 0.123

We continue with the definition of solid phases as either pure or solid solutions. Some phases included in the cemdata18 database should be added to the chemical system as solid solutions (more details in Lothenbach et al. (2019)). A solid solution contains two or more end-members (species) and can be ideal or non-ideal. In our case, the defined solid solutions are modeled as ideal solid solutions (the activity coefficient of the end-members is equal to 1).

We finish the creation of the chemical system by initializing it with the database cemdata18 (included in thermofun) and the phases we defined.

# Define pure minerals phases
minerals = MineralPhases("Cal hydrotalcite Portlandite hemicarbonate monocarbonate Amor-Sl FeOOHmic Gbs Mag")

# Define the hydrogarnet solid solution
ss_C3AFS084H  = SolidPhase("C3FS0.84H4.32 C3AFS0.84H4.32")

# Define the ettrignite solid solution
ss_ettringite = SolidPhase("ettringite ettringite30")

# Define the monosulfate solid solution
ss_OH_SO4_AFm = SolidPhase("C4AH13 monosulphate12")

# Define the CSHQ solid solution
ss_CSHQ = SolidPhase("CSHQ-TobD CSHQ-TobH CSHQ-JenH CSHQ-JenD KSiOH NaSiOH")

# Define the chemical system by providing database, aqueous phase, minerals, and solid solutions
system = ChemicalSystem(db, solution, minerals, ss_C3AFS084H, ss_ettringite, ss_OH_SO4_AFm, ss_CSHQ)

Next, we set up the equilibrium specifications, the equilibrium conditions, and the equilibrium solver, all of which are used for the equilibrium calculations:

# Specify conditions that need to be satisfied at chemical equilibrium
specs = EquilibriumSpecs(system)

# Define the value of the conditions that need to be satisfied at chemical equilibrium
conditions = EquilibriumConditions(specs)
conditions.temperature(20.0, "celsius")
conditions.pressure(1.0, "bar")

# Define chemical and aqueous properties
props = ChemicalProps(system)
aprops = AqueousProps(system)

# Define the equilibrium solver
solver = EquilibriumSolver(specs)

Below, we compile the chemical substances corresponding to:

  • 100 g of cement clinker (defined by the oxide composition, usually determined by XRF analysis),

  • 1000 g of water, and

  • 1 g calcite.

# We define the materials for our equilibrium recipe
# Cement clinker composition from XRF as given in Lothenbach et al.(2008) recalculated for 100g
cement_clinker = Material(system)
cement_clinker.add("SiO2" , 20.47, "g")
cement_clinker.add("CaO"  , 65.70, "g")
cement_clinker.add("Al2O3",  4.90, "g")
cement_clinker.add("Fe2O3",  3.20, "g")
cement_clinker.add("K2O"  ,  0.79, "g")
cement_clinker.add("Na2O" ,  0.42, "g")
cement_clinker.add("MgO"  ,  1.80, "g")
cement_clinker.add("SO3"  ,  2.29, "g")
cement_clinker.add("CO2"  ,  0.26, "g")
cement_clinker.add("O2"   ,  5.00, "g")

# Define water
water = Material(system)
water.add("H2O", 1000, "g")

# Define calcite
calcite = Material(system)
calcite.add("CaCO3", 1, "g")

Next, we specify the list of phases whose volume we want to track. This list of phases is used to define the columns of the pandas.DataFrame instance where the volume of the phases is stored as a percentage of the total volume of the system.

# Create list of species and phases names, list of Species objects, and auxiliary amounts array
import numpy as np
phases_list_str = "ss_C3AFS084H ss_Ettrignite ss_Monosulfate ss_CSHQ " \
                  "Cal hydrotalcite Portlandite hemicarbonate monocarbonate Amor-Sl FeOOHmic Gbs Mag".split()
volume = np.zeros(len(phases_list_str))

# Define dataframe to collect amount of the selected species
import pandas as pd
columns = ["CaCO3"] + phases_list_str
df = pd.DataFrame(columns=columns)

In the following loop, we simulate the addition of calcite at the expense of clinker in the cement mixture (starting from 0 g and incrementing 0.5 g at each step to reach 10 g at the end). In these sequential calculations, the phase volume (cm3) of the selected phases is collected.

# Number of steps
steps_num = 101
step_size = 0.10


for i in range(steps_num):

    # Define a cement mix of 0.5 water/binder at each step calcite is added at the expense of clinker
    cement_mix = Material(system)
    cement_mix = cement_clinker(100.0-i*step_size, "g") + calcite(i*step_size + 1e-16, "g") + water(50.0, "g")

    # Equilibrate cement mix
    state = cement_mix.equilibrate(20.0, "celsius", 1.0, "bar")

    res = cement_mix.result()

    if res.failed(): continue

    # Update chemical and aqueous properties

    for j in range(0, len(phases_list_str)):
        # Collecting the volume of specified phase
        volume[j] = float(props.phaseProps(phases_list_str[j]).volume())

    # Update dataframe with obtained values
    df.loc[len(df)] = np.concatenate([[i*step_size], volume*1e6])

To inspect the content of the pandas.DataFrame, we can just output it in the code cell:

CaCO3 ss_C3AFS084H ss_Ettrignite ss_Monosulfate ss_CSHQ Cal hydrotalcite Portlandite hemicarbonate monocarbonate Amor-Sl FeOOHmic Gbs Mag
0 0.0 5.546534 5.869657 6.990195e-01 26.124561 3.693400e-15 2.345264 15.335895 2.902948e-14 1.476130 2.900000e-15 3.430550e-15 3.195600e-15 4.452400e-15
1 0.1 5.540962 6.214742 2.348576e-01 26.096718 3.693400e-15 2.342919 15.322307 2.881867e-14 1.736383 2.900000e-15 3.430550e-15 3.195600e-15 4.452400e-15
2 0.2 5.470486 6.361796 5.845700e-14 26.100733 8.696092e-14 2.340574 15.292004 2.845150e-14 1.995990 2.900000e-15 3.430550e-15 3.195600e-15 4.452400e-15
3 0.3 5.461302 6.354780 5.845700e-14 26.077237 3.597668e-02 2.338228 15.274939 2.845150e-14 2.001041 2.900000e-15 3.430550e-15 3.195600e-15 4.452400e-15
4 0.4 5.455379 6.348574 5.845700e-14 26.052154 7.284647e-02 2.335883 15.258716 2.845150e-14 1.999791 2.900000e-15 3.430550e-15 3.195600e-15 4.452400e-15
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
96 9.6 4.913008 5.772954 5.845700e-14 23.727492 3.465303e+00 2.120119 13.779004 2.845150e-14 1.881727 2.900000e-15 3.430550e-15 3.195600e-15 4.452400e-15
97 9.7 4.907146 5.766659 5.845700e-14 23.702064 3.502184e+00 2.117774 13.763041 2.845150e-14 1.880397 2.900000e-15 3.430550e-15 3.195600e-15 4.452400e-15
98 9.8 4.901284 5.760364 5.845700e-14 23.676634 3.539065e+00 2.115428 13.747081 2.845150e-14 1.879066 2.900000e-15 3.430550e-15 3.195600e-15 4.452400e-15
99 9.9 4.895423 5.754068 5.845700e-14 23.651200 3.575947e+00 2.113083 13.731122 2.845150e-14 1.877734 2.900000e-15 3.430550e-15 3.195600e-15 4.452400e-15
100 10.0 4.889563 5.747772 5.845700e-14 23.625764 3.612828e+00 2.110738 13.715166 2.845150e-14 1.876401 2.900000e-15 3.430550e-15 3.195600e-15 4.452400e-15

101 rows × 14 columns

To visualize the distribution of different minerals in the cement recipe while the limestone addition, we us bokeh plotting library.

from bokeh.plotting import figure, show
from bokeh.palettes import brewer
from import output_notebook


p = figure(
    x_axis_label='CaCO3 [%]',
    y_axis_label='PHASE VOLUME [cm3]',

volume_names = ["Cal", "hydrotalcite", "Portlandite", "ss_CSHQ", "ss_C3AFS084H", "ss_Ettrignite", "monocarbonate"]
p.varea_stack(stackers=volume_names, x='CaCO3', color=brewer['Spectral'][len(volume_names)], legend_label=volume_names, source=df)

Loading BokehJS ...

In this stacked area plot, we see that the volume of calcite is increasing (we add CaCO3 to the cement mix) and the overall volume of the solids in our hydrated cement mix is decreasing by about 2 cm3. This is because the molar volume of calcite is smaller than that of the hydrates it replaces.