Skip to content

Simplified Clinker Dissolution

This example illustrates ChemistryLab's equilibrium workflow applied to cement chemistry: computing the hydration state of a simplified Portland clinker at a given water-to-cement ratio. ChemistryLab computes thermodynamic equilibrium by minimizing the Gibbs free energy of the system subject to element-conservation constraints. The workflow always follows the same four steps:

  1. Build a ChemicalSystem (species + stoichiometric matrix).

  2. Create an initial ChemicalState (temperature, pressure, initial amounts).

  3. Call equilibrate (or use EquilibriumSolver explicitly).

  4. Inspect the resulting ChemicalState.


Minimal workflow

The convenience function equilibrate handles everything with sensible defaults. The example below reproduces a simplified clinker dissolution calculation.

julia
using ChemistryLab
using DynamicQuantities

substances = build_species(datapath("cemdata18-thermofun.json"))
input_species = split("C3S C2S C3A C4AF Gp Anh Portlandite Jennite H2O@ ettringite monosulphate12 C3AH6 C3FH6 C4FH13")
species = speciation(substances, input_species; aggregate_state=[AS_AQUEOUS])

cs = ChemicalSystem(species, CEMDATA_PRIMARIES)
julia
state = ChemicalState(cs)

# Clinker + gypsum composition (mass fractions, total = 1)
compo = ["C3S" => 0.678, "C2S" => 0.166, "C3A" => 0.04, "C4AF" => 0.072, "Gp" => 0.028]
c     = sum(last.(compo))
wc    = 0.4          # water-to-cement ratio
w     = wc * c
mtot  = c + w

for x in compo
    set_quantity!(state, x.first, x.second / mtot * u"kg")
end
set_quantity!(state, "H2O@", w / mtot * u"kg")

# pH-neutral seed for H⁺ and OH⁻
V = volume(state)
set_quantity!(state, "H+",  1e-7u"mol/L" * V.liquid)
set_quantity!(state, "OH-", 1e-7u"mol/L" * V.liquid)

state_eq = equilibrate(state)
┌ Warning: Verbosity toggle: missing_second_order_ad
│  The selected optimization algorithm requires second order derivatives, but `SecondOrder` ADtype was not provided. So a `SecondOrder` with AutoForwardDiff() for both inner and outer will be created, this can be suboptimal and not work in some cases so an explicit `SecondOrder` ADtype is recommended.
└ @ OptimizationBase ~/.julia/packages/OptimizationBase/TvAnm/src/cache.jl:116
The solved state in full — every species, with its amount
julia
state_eq
ChemicalState{Species, AbstractReaction, DynamicQuantities.Quantity{Float64, DynamicQuantities.SymbolicDimensions{DynamicQuantities.FRInt32}}, Float64}
┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│             T : 298.15 K                                                                         │
│             P : 1.0 bar                                                                          │
╞══════════════════════════════════════════════════════════════════════════════════════════════════╡
│    # liquid #│             n [mol]│               m [g]│             V [cm³]│           c [mol/L]│
├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┤
│   tot. liquid│             4.56714│             82.3078│             82.4392│                    │
├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┤
│          H2O@│              4.5639│             82.2186│             82.4623│             55.3608│
│           OH-│          0.00203441│           0.0345992│         -0.00957768│           0.0246777│
│          Ca+2│         0.000846972│            0.033945│          -0.0156171│           0.0102739│
│         CaOH+│         0.000347807│           0.0198546│          0.00200423│          0.00421896│
│         AlO2-│           7.2335e-6│         0.000426628│           6.8482e-5│          8.77434e-5│
│       CaSiO3@│           3.0664e-6│         0.000356192│                 0.0│          3.71958e-5│
│        HSiO3-│           6.4551e-8│          4.97624e-6│          2.92149e-7│          7.83014e-7│
│      Ca(SO4)@│          3.03929e-8│          4.13751e-6│          1.42866e-7│          3.68671e-7│
│         SO4-2│          1.47856e-8│          1.42025e-6│          1.90996e-7│          1.79352e-7│
│    Ca(HSiO3)+│          1.05399e-8│          1.23493e-6│         -7.10022e-8│           1.2785e-7│
│         FeO2-│          9.10608e-9│          7.99905e-7│          4.11187e-9│          1.10458e-7│
│        SiO3-2│          7.47692e-9│          5.68859e-7│                 0.0│          9.06962e-8│
│         SiO2@│         1.68406e-10│          1.01184e-8│          2.70516e-9│           2.0428e-9│
│        AlO2H@│         8.16864e-12│         4.90017e-10│         1.06269e-10│         9.90869e-11│
│        FeO2H@│         4.03028e-12│         3.58095e-10│         2.90556e-11│         4.88879e-11│
│      AlSiO5-3│         2.22885e-12│         3.01033e-10│        -7.60746e-11│         2.70363e-11│
│            H+│         3.31947e-14│         3.34602e-14│                 0.0│         4.02656e-13│
│      Si4O10-4│9.859679999999999e-305│        2.68509e-302│                 0.0│        1.19599e-303│
│     Al(SO4)2-│9.859679999999999e-305│        2.16019e-302│        3.06753e-303│        1.19599e-303│
│      Fe(SO4)@│9.859679999999999e-305│        1.49769e-302│        1.64841e-304│        1.19599e-303│
│      Fe(SO4)+│9.859679999999999e-305│        1.49769e-302│-2.5984500000000003e-304│        1.19599e-303│
│     Fe(SO4)2-│9.859679999999999e-305│        2.44478e-302│3.0057499999999996e-303│        1.19599e-303│
│      Al(SO4)+│9.859679999999999e-305│        1.21311e-302│       -5.93457e-304│        1.19599e-303│
│    Fe(HSO4)+2│9.859679999999999e-305│        1.50763e-302│2.2873100000000004e-304│        1.19599e-303│
│     Fe(HSO4)+│9.859679999999999e-305│        1.50763e-302│        1.85425e-303│        1.19599e-303│
│          Al+3│9.859679999999999e-305│        2.66029e-303│        -4.4608e-303│        1.19599e-303│
│          Fe+2│9.859679999999999e-305│        5.50614e-303│       -2.23227e-303│        1.19599e-303│
│           HS-│9.859679999999999e-305│         3.2604e-303│1.9925899999999998e-303│        1.19599e-303│
│        AlOH+2│9.859679999999999e-305│        4.33713e-303│       -2.68924e-304│        1.19599e-303│
│          AlO+│9.859679999999999e-305│        4.23774e-303│3.0302300000000003e-305│        1.19599e-303│
│        FeOH+2│9.859679999999999e-305│        7.18297e-303│       -2.49849e-303│        1.19599e-303│
│          FeO+│9.859679999999999e-305│        7.08359e-303│-4.1431099999999996e-303│        1.19599e-303│
│         FeOH+│9.859679999999999e-305│        7.18297e-303│       -1.64784e-303│        1.19599e-303│
│         HSO3-│9.859679999999999e-305│        7.99275e-303│        3.24943e-303│        1.19599e-303│
│         HSO4-│9.859679999999999e-305│         9.5702e-303│        3.43522e-303│        1.19599e-303│
│        S2O3-2│9.859679999999999e-305│        1.10544e-302│        2.72051e-303│        1.19599e-303│
│         SO3-2│9.859679999999999e-305│        7.89336e-303│-4.058230000000001e-304│        1.19599e-303│
│          Fe+3│9.859679999999999e-305│        5.50614e-303│       -3.72599e-303│        1.19599e-303│
│          H2S@│9.859679999999999e-305│        3.35978e-303│        3.44608e-303│        1.19599e-303│
│           H2@│9.859679999999999e-305│1.9877100000000003e-304│        2.49098e-303│        1.19599e-303│
│           O2@│9.859679999999999e-305│         3.1549e-303│        3.00729e-303│        1.19599e-303│
╞══════════════════════════════════════════════════════════════════════════════════════════════════╡
│     # solid #│             n [mol]│               m [g]│             V [cm³]│                    │
├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┤
│    tot. solid│              6.0689│             917.692│             387.255│                    │
├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┤
│   Portlandite│             2.89109│             214.207│             95.5796│                    │
│       Jennite│             2.85524│             546.425│             223.851│                    │
│monosulphate12│            0.118057│             73.4912│             36.6095│                    │
│         C3FH6│            0.107551│              46.893│             16.7012│                    │
│         C3AH6│           0.0969546│             36.6761│             14.5143│                    │
│        C4FH13│                 0.0│                 0.0│                 0.0│                    │
│           C3S│                 0.0│                 0.0│                 0.0│                    │
│           C2S│                 0.0│                 0.0│                 0.0│                    │
│           C3A│                 0.0│                 0.0│                 0.0│                    │
│          C4AF│                 0.0│                 0.0│                 0.0│                    │
│    ettringite│                 0.0│                 0.0│                 0.0│                    │
│            Gp│                 0.0│                 0.0│                 0.0│                    │
│           Anh│                 0.0│                 0.0│                 0.0│                    │
╞══════════════════════════════════════════════════════════════════════════════════════════════════╡
│     # TOTAL #│             n [mol]│               m [g]│             V [cm³]│                    │
├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┤
│              │              10.636│              1000.0│             469.695│                    │
╞══════════════════════════════════════════════════════════════════════════════════════════════════╡
│            pH : 12.3924                                                                          │
│           pOH : 1.6077                                                                           │
│      porosity : 0.175517                                                                         │
│    saturation : 1.0                                                                              │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘

Quick shortcut

If your ChemicalState was constructed from a ChemicalSystem built with CEMDATA_PRIMARIES, calling equilibrate(state) with no other arguments is usually sufficient for cement-chemistry problems.


Inspecting the equilibrium state

The returned ChemicalState carries all derived thermodynamic quantities:

julia
println("pH      = ", pH(state_eq))
println("pOH     = ", pOH(state_eq))
println("porosity   = ", porosity(state_eq))
println("saturation = ", saturation(state_eq))

Phase volumes and mole amounts are accessible via named tuples:

julia
v = volume(state_eq)
println("V liquid = ", v.liquid)
println("V solid  = ", v.solid)
println("V total  = ", v.total)

m = moles(state_eq)
println("n liquid = ", m.liquid)
println("n solid  = ", m.solid)

Individual species amounts (in mol):

julia
cs_eq = state_eq.system
for (i, sp) in enumerate(cs_eq.species)
    n_i = state_eq.n[i]
    println(rpad(symbol(sp), 20), ustrip(n_i), " mol")
end

Controlling the solver

Variable space: :linear vs :log

equilibrate accepts a variable_space keyword that selects the optimization variable space:

variable_spaceVariablesRecommended when
Val(:linear)mole amounts nᵢ ≥ 0most systems, default
Val(:log)log nᵢsystems spanning many orders of magnitude
julia
state_eq_log = equilibrate(state; variable_space=Val(:log))

Convergence

Solving a system of equations in chemistry can be a difficult undertaking. The orders of magnitude can vary greatly, and convergence is not guaranteed.


Tolerances

Tighter tolerances are passed directly as keyword arguments and forwarded to the underlying Ipopt solver:

julia
state_eq_tight = equilibrate(state; abstol=1e-12, reltol=1e-12)

Using EquilibriumSolver explicitly

For batch calculations where many different initial states share the same system and activity model, construct an EquilibriumSolver once and reuse it:

julia
using Optimization, OptimizationIpopt

opt = IpoptOptimizer(
    acceptable_tol        = 1e-12,
    dual_inf_tol          = 1e-12,
    acceptable_iter       = 1000,
    constr_viol_tol       = 1e-12,
    warm_start_init_point = "no",
)

solver = EquilibriumSolver(
    cs,
    DiluteSolutionModel(),
    opt;
    variable_space = Val(:linear),
    abstol  = 1e-10,
    reltol  = 1e-10,
)

Once built, solver is called with any compatible ChemicalState:

julia
using Optimization, OptimizationIpopt #hide

opt = IpoptOptimizer( #hide
    acceptable_tol        = 1e-12, #hide
    dual_inf_tol          = 1e-12, #hide
    acceptable_iter       = 1000, #hide
    constr_viol_tol       = 1e-12, #hide
    warm_start_init_point = "no", #hide
) #hide

solver = EquilibriumSolver( #hide
    cs, #hide
    DiluteSolutionModel(), #hide
    opt; #hide
    variable_space = Val(:linear), #hide
    abstol  = 1e-10, #hide
    reltol  = 1e-10, #hide
) #hide
state_eq2 = solve(solver, state)

Performance

The potential function μ(n, p) is compiled once during EquilibriumSolver construction. Repeated calls to solve(solver, ...) with different states reuse it, avoiding redundant compilation overhead.


Activity models

All activity models inherit from AbstractActivityModel. There are three built-in ones — DiluteSolutionModel, HKFActivityModel and DaviesActivityModel; see Activity models for the other two and for Reading the aqueous properties back. This page uses the first, which implements:

PhaseLawExpression
Solvent (H₂O)Raoultln a = ln xₛ
Aqueous solutesHenryln a = ln(cᵢ / c°), c° = 1 mol/L
CrystalsPure solidln a = 0
GasIdeal mixtureln a = ln xᵢ

To implement a custom activity model, define a new subtype and extend activity_model:

julia
struct MyModel <: AbstractActivityModel
    # model parameters
end

function ChemistryLab.activity_model(::ChemicalSystem, ::MyModel)
    # return a closure lna(n, p) -> Vector{Float64}
    return (_, _) -> begin
        # compute and return log-activities
    end
end

Pass your model to equilibrate or EquilibriumSolver:

julia
state_eq = equilibrate(state; model=MyModel(...))