Skip to content

Cement chemist notation

Cement chemistry writes its phases in a shorthand that is opaque on first sight and indispensable once learned: alite is C3S, portlandite is CH, ettringite is C6AS̄3H32. This page is the key to it, and to how ChemistryLab reads it.

The idea is simple. A cement phase is an assembly of oxides, so each oxide gets a one-letter symbol and a phase is written as the oxides it contains, with their molar multiplicities. C3S is three parts lime to one of silica —    — which is written the way a cement chemist thinks about it: not as an arbitrary silicate, but as this much lime combined with that much silica.

The alphabet

SymbolOxideNameMolar mass
Clime56.08 g/mol
Ssilica60.08 g/mol
Aalumina101.96 g/mol
Fferric oxide159.69 g/mol
Mmagnesia40.30 g/mol
Kpotassium oxide94.20 g/mol
Nsodium oxide61.98 g/mol
Ttitania79.86 g/mol
Pphosphorus pentoxide141.94 g/mol
Hwater18.01 g/mol
carbon dioxide44.01 g/mol
sulfur trioxide80.06 g/mol
nitrate62.00 g/mol

The first four are the oxides a clinker is made of, and the four a datasheet always reports; the next five are the minor oxides; the last three are the acidic ones that take a bar.

Two conventions decide everything, and they are where a newcomer stumbles:

  • A letter stands for an oxide, not an element. C is lime , not carbon; S is silica , not sulfur; N is , not nitrogen.

  • The bar marks the acidic oxides whose letter is already taken. Carbon dioxide is because C is lime, and sulfur trioxide is because S is silica. The bar is a combining macron (U+0304): type the letter, then that character.

The table above is the mapping the parser itself uses, CEMENT_TO_MENDELEEV. The block below prints it from the code and checks the two agree, so the table cannot quietly drift from what ChemistryLab accepts:

julia
using ChemistryLab
using DynamicQuantities

# (formula, molar mass) exactly as the table above states them
documented = Dict(
    :C => ("CaO", 56.08), :S => ("SiO2", 60.08), :A => ("Al2O3", 101.96),
    :F => ("Fe2O3", 159.69), :M => ("MgO", 40.30), :K => ("K2O", 94.20),
    :N => ("Na2O", 61.98), :T => ("TiO2", 79.86), :P => ("P2O5", 141.94),
    :H => ("H2O", 18.01), :C̄ => ("CO2", 44.01), :S̄ => ("SO3", 80.06),
    :N̄ => ("NO3", 62.00),
)

for (letter, oxide) in CEMENT_TO_MENDELEEV
    sp = Species(oxide)
    formula, mass = documented[letter]
    @assert atoms(sp) == atoms(Species(formula)) "table disagrees on $letter"
    computed = round(ustrip(us"g/mol", sp.M), digits = 2)
    @assert abs(computed - mass) < 0.005 "table gives $mass for $letter, the data give $computed"
    println(rpad(string(letter), 3), " = ", rpad(unicode(sp), 8),
            lpad(computed, 8), " g/mol")
end
C   = CaO        56.08 g/mol
M   = MgO         40.3 g/mol
S   = SiO₂       60.08 g/mol
A   = Al₂O₃     101.96 g/mol
F   = Fe₂O₃     159.69 g/mol
K   = K₂O         94.2 g/mol
N   = Na₂O       61.98 g/mol
P   = O₅P₂      141.94 g/mol
T   = TiO₂       79.86 g/mol
C̄   = CO₂        44.01 g/mol
S̄   = SO₃        80.06 g/mol
N̄   = NO₃         62.0 g/mol
H   = H₂O        18.01 g/mol

H is water, which is why hydrates carry a large H count: C4AH13 is     .

Reading and building a phase

CemSpecies parses the shorthand. Ordinary ASCII digits and Unicode subscripts are both accepted, and non-integer multiplicities are allowed — which matters for C-S-H, whose lime-to-silica ratio is a composition, not a constant:

julia
for name in ["C3S", "C2S", "C3A", "C4AF", "CH", "C₄AH₁₃", "C1.7SH4"]
    sp = CemSpecies(name)
    println(rpad(name, 9), " -> ", rpad(unicode(sp), 12),
            "  M = ", round(ustrip(us"g/mol", sp.M), digits = 2), " g/mol")
end
C3S       -> C₃S           M = 228.31 g/mol
C2S       -> C₂S           M = 172.24 g/mol
C3A       -> C₃A           M = 270.19 g/mol
C4AF      -> C₄AF          M = 485.96 g/mol
CH        -> CH            M = 74.09 g/mol
C₄AH₁₃    -> C₄AH₁₃        M = 560.46 g/mol
C1.7SH4   -> C₁.₇SH₄       M = 227.47 g/mol

The composition in ordinary elements is always available, so the shorthand is a way of writing a species and never a different kind of object:

julia
atoms(CemSpecies("C3S"))
OrderedCollections.OrderedDict{Symbol, Int64} with 3 entries:
  :Ca => 3
  :O  => 5
  :Si => 1

The trap: Species and CemSpecies read the same string differently

This is the single most expensive mistake to make, and it is silent:

julia
cem = CemSpecies("C3S")
ord = Species("C3S")

println("CemSpecies(\"C3S\") = ", unicode(cem), "   M = ",
        round(ustrip(us"g/mol", cem.M), digits = 2), " g/mol   (3 CaO + SiO2)")
println("Species(\"C3S\")    = ", unicode(ord), "   M = ",
        round(ustrip(us"g/mol", ord.M), digits = 2), " g/mol   (3 carbons + 1 sulfur)")
CemSpecies("C3S") = C₃S   M = 228.31 g/mol   (3 CaO + SiO2)
Species("C3S")    = C₃S   M = 68.09 g/mol   (3 carbons + 1 sulfur)

Both display as C₃S, and they differ by more than a factor of three in molar mass. Species applies the ordinary rules of chemical formulas, in which C is carbon and S is sulfur; CemSpecies applies the cement convention. Neither is wrong — they answer different questions — but a recipe that reaches for the wrong one is wrong everywhere downstream and raises no error.

Read a database phase by name, never by re-parsing its symbol

A phase read from a thermodynamic database already carries its composition and its molar mass. Look it up by name — byname["C3S"] — rather than rebuilding it from its symbol with Species, which would read C3S as three carbons and a sulfur. This is the reason the worked examples in this documentation never write a molar mass by hand.

The common phases, and what they weigh

Nothing below is typed from a table: each mass is computed from the formula and the element data.

julia
phases = [
    ("C3S",       "alite"),
    ("C2S",       "belite"),
    ("C3A",       "aluminate"),
    ("C4AF",      "ferrite"),
    ("CS̄H2",      "gypsum"),
    ("CH",        "portlandite"),
    ("CC̄",        "calcite"),
    ("C6AS̄3H32",  "ettringite (AFt)"),
    ("C4AS̄H12",   "monosulfoaluminate (AFm)"),
    ("C4AH13",    "hydroxy-AFm"),
    ("C2ASH8",    "strätlingite"),
    ("C3AH6",     "hydrogarnet"),
    ("M5AH13",    "hydrotalcite"),
]

for (name, english) in phases
    sp = CemSpecies(name)
    println(rpad(unicode(sp), 13), rpad(english, 28),
            lpad(round(ustrip(us"g/mol", sp.M), digits = 2), 8), " g/mol")
end
C₃S          alite                         228.31 g/mol
C₂S          belite                        172.24 g/mol
C₃A          aluminate                     270.19 g/mol
C₄AF         ferrite                       485.96 g/mol
CS̄H₂         gypsum                        172.16 g/mol
CH           portlandite                    74.09 g/mol
CC̄           calcite                       100.09 g/mol
C₆AS̄₃H₃₂     ettringite (AFt)             1255.07 g/mol
C₄AS̄H₁₂      monosulfoaluminate (AFm)      622.51 g/mol
C₄AH₁₃       hydroxy-AFm                   560.46 g/mol
C₂ASH₈       strätlingite                  418.32 g/mol
C₃AH₆        hydrogarnet                   378.28 g/mol
M₅AH₁₃       hydrotalcite                  537.68 g/mol

Ettringite carries alumina, and the shorthand must say so

Ettringite is — six lime, one alumina, three sulfate, thirty-two water — at 1255 g/mol. Dropping the A gives C6S̄3H32, which parses without complaint, weighs 1153 g/mol and is not a cement phase. The shorthand is compact enough that an omission looks like a typo and behaves like a different substance.

Where the notation is used

  • CemSpecies for the species themselves;

  • the oxide components of a stoichiometric decomposition — see Stoichiometric matrices, where a clinker phase is expressed over C, S, A, F;

  • the Bogue Calculation, which is that decomposition inverted and converted to mass.