$$ \newcommand{\C}{{\mathbb{{C}}}} \newcommand{\R}{{\mathbb{{R}}}} \newcommand{\Q}{{\mathbb{{Q}}}} \newcommand{\Z}{{\mathbb{{Z}}}} \newcommand{\N}{{\mathbb{{N}}}} \newcommand{\uu}[1]{{\boldsymbol{{#1}}}} \newcommand{\uuuu}[1]{{\symbb{{#1}}}} \newcommand{\uv}[1]{{\underline{{#1}}}} \newcommand{\ve}[1]{{\uv{{e}}_{{#1}}}} \newcommand{\x}{{\uv{{x}}}} \newcommand{\n}{{\uv{{n}}}} \newcommand{\eps}{{\uu{{\varepsilon}}}} \newcommand{\E}{{\uu{{E}}}} \newcommand{\sig}{{\uu{{\sigma}}}} \newcommand{\Sig}{{\uu{{\Sigma}}}} \newcommand{\cod}{{\uv{{\symscr{b}}}}} \newcommand{\trans}[1]{{{}^{t}{#1}}} \newcommand{\sotimes}{{\stackrel{s}{\otimes}}} \newcommand{\sboxtimes}{\stackrel{s}{\boxtimes}} \newcommand{\norm}[1]{{\lVert{{#1}}\rVert}} \newcommand{\ud}{{\,\mathrm{d}}} \newcommand{\mat}{\mathsf} \DeclareMathOperator{\arcosh}{arcosh} \DeclareMathOperator{\divz}{div} \DeclareMathOperator{\divu}{\uv{div}} \DeclareMathOperator{\hess}{hess} \DeclareMathOperator{\gradu}{\uv{grad}} \DeclareMathOperator{\graduu}{\uu{grad}} \DeclareMathOperator{\Mat}{Mat} \DeclareMathOperator{\tr}{tr} \DeclareMathOperator{\ISO}{ISO} \newcommand{\volt}[1]{{#1}^{-1\circ}} \newcommand{\dcirc}{\overset{\circ}{:}} \newcommand{\jump}[1]{\mathopen{[\![}\,#1\,\mathclose{]\!]}} $$

21  Multiscale elasticity of a hydrating cement paste

Important Objectives

Following Sanahuja et al. (2007), this tutorial builds a two-scale micromechanical model of Portland cement paste that predicts the evolution of the effective (drained and undrained) Young’s modulus from the water-to-cement ratio \(w/c\) and the hydration degree \(\alpha\).

The key features of the model are:

  • A self-consistent scheme at each scale to handle the percolation of the outer C-S-H phase at setting.
  • A generalized Mori-Tanaka scheme (via sphere_nlayers) at the paste scale to account for the inner/outer core-shell morphology.
  • A poromechanics correction (Biot theory) to convert drained into undrained moduli for comparison with ultra-sonic measurements.
import numpy as np
from echoes import *
import matplotlib.pyplot as plt

np.set_printoptions(precision=5, suppress=True)

Microstructural model

Sanahuja et al. (2007) distinguish two C-S-H types at the paste scale:

  • Inner (high-density) hydrates — a porous polycrystal of oblate solid bricks (60×30×5 nm, aspect ratio \(\omega_i^s \approx 0.12\)) surrounding each anhydrous grain. Their porosity \(\varphi_i = 0.30\) is fixed throughout hydration.
  • Outer (low-density) hydrates — oblate solid platelets (\(\omega_o^s = 0.033\), 4–8 bricks stacked) precipitating in the water-filled space. Their porosity \(\varphi_o\) evolves with hydration.

At the paste scale, composite inclusions (anhydrous core + inner layer) are embedded in the outer matrix via a generalized Mori-Tanaka scheme implemented with sphere_nlayers.

Powers hydration model and volume fractions

The Powers model (Powers and Brownyard, 1946) provides \(f_a\), \(f_h\) and the total porosity \(f_p\):

\[ f_a = \frac{0.32(1-\alpha)}{w/c+0.32},\quad f_h = \frac{0.68\,\alpha}{w/c+0.32},\quad f_p = \frac{w/c-0.17\,\alpha}{w/c+0.32} \]

Hydration stops when water (\(f_w = (w/c-0.4175\,\alpha)/(w/c+0.32)\)) or anhydrous is depleted: \(\alpha_{\max}=\min(1,\,w/c/0.4175)\).

The Tennis–Jennings model (Tennis and Jennings, 2000) gives the mass fraction \(m_{LD}\) of LD C-S-H: \[ m_{LD} = 3.017\,\alpha\,w/c - 1.347\,\alpha + 0.538 \]

Using \(\varphi_i = 0.30\) (fixed HD porosity), the five phase volumes are:

\[ f_i^s = (1-m_{LD})(1-f_a-f_p),\quad f_o^s = m_{LD}(1-f_a-f_p) \] \[ f_i^p = \frac{\varphi_i}{1-\varphi_i}\,f_i^s,\quad f_o^p = f_p - f_i^p \] \[ f_i = f_i^s+f_i^p,\quad \varphi_o = \frac{f_o^p}{f_o^p+f_o^s} \]

Volume fractions — Powers + Tennis-Jennings model
rho_a = 3.13 ; kappa_h = 2.13 ; kappa_w = 1.31
phi_i = 0.30   # inner (HD) fixed porosity

fa   = lambda wc, a: 0.32 * (1.-a) / (wc + 0.32)
fh   = lambda wc, a: 0.68 * a      / (wc + 0.32)
fw   = lambda wc, a: (wc - 0.4175*a) / (wc + 0.32)
fp   = lambda wc, a: (wc - 0.17 *a) / (wc + 0.32)   # total porosity
amax = lambda wc:    min(1., wc / 0.4175)

mLD  = lambda wc, a: np.clip(3.017*a*wc - 1.347*a + 0.538, 0., 1.)

def vf5(wc, alpha):
    """Returns fis, fos, fip, fop, fi, phi_o."""
    _fa = fa(wc, alpha); _fp = fp(wc, alpha)
    fhs = 1. - _fa - _fp            # total solid hydrates
    fis = (1. - mLD(wc, alpha)) * fhs
    fos = mLD(wc, alpha)            * fhs
    fip = phi_i / (1. - phi_i) * fis
    fop = _fp - fip
    fi  = fis + fip
    phi_o = fop / (fop + fos) if (fop + fos) > 1.e-12 else 0.
    return fis, fos, fip, fop, fi, phi_o

Scale 0: self-consistent estimates of inner and outer

Inner hydrates (fixed, SC)

The inner porosity \(\varphi_i=0.30\) is independent of \(w/c\) and \(\alpha\); \(C_\text{inner}\) is computed once:

omega_i = 0.12    # aspect ratio of HD solid bricks
omega_o = 0.033   # aspect ratio of LD solid platelets
C_sol   = stiff_Enu(71.6, 0.27)   # solid C-S-H (bricks and platelets)
C_anhyd = stiff_Enu(135.,  0.3)   # anhydrous clinker

ver_in = rve()
ver_in["SOLID"] = ellipsoid(shape=spheroidal(omega_i), symmetrize=[ISO],
                             fraction=1. - phi_i, prop={"C": C_sol})
ver_in["PORE"]  = ellipsoid(shape=spherical, fraction=phi_i, prop={"C": tZ4})

C_inner = homogenize(prop="C", rve=ver_in, scheme=SC,
                     verbose=False, epsrel=1.e-10, maxnb=300)
print(f"Inner: E = {C_inner.E:.2f} GPa,  k = {C_inner.k:.2f} GPa,  "
      f"mu = {C_inner.mu:.2f} GPa")
Inner: E = 31.00 GPa,  k = 19.93 GPa,  mu = 12.49 GPa

Outer hydrates (evolving \(\varphi_o\), SC)

The outer porosity \(\varphi_o(w/c,\alpha)\) varies with hydration. Setting occurs when \(\varphi_o\) reaches the SC percolation threshold (controlled by \(\omega_o^s\)):

Figure — Young’s modulus of inner and outer C-S-H (SC)
ver_out = rve()
ver_out["SOLID"] = ellipsoid(shape=spheroidal(omega_o), symmetrize=[ISO],
                              prop={"C": C_sol})
ver_out["PORE"]  = ellipsoid(shape=spherical, prop={"C": tZ4})

def C_outer(phi_o):
    ver_out["SOLID"].fraction = 1. - phi_o
    ver_out["PORE"].fraction  = phi_o
    C = homogenize(prop="C", rve=ver_out, scheme=SC,
                   verbose=False, epsrel=1.e-10, maxnb=300)
    if np.isnan(C.k): return tZ4
    return C

# Illustration: outer stiffness vs porosity
lphi = np.linspace(0., 1., 60)
lE_in  = [C_inner.E] * len(lphi)
lE_out = []
for phi in lphi:
    Co = C_outer(phi)
    lE_out.append(Co.E if not np.isnan(Co.E) else 0.)

fig, ax = plt.subplots(figsize=(7, 4.5))
ax.plot(lphi, lE_out, 'b-',  label=r"outer ($\omega_o^s=0.033$)")
ax.axhline(C_inner.E, color='r', ls='--', label=r"inner ($\omega_i^s=0.12$, fixed)")
ax.set_xlabel(r"Porosity $\varphi$")
ax.set_ylabel(r"$E^{hom}$ (GPa)")
ax.set_title("SC estimates of inner and outer C-S-H")
ax.grid(True); ax.legend()
plt.tight_layout(); plt.show()

Scale 1: cement paste (generalized MT)

At the paste scale, composite spheres (anhydrous core of radius \(R_a\) coated by an inner layer up to radius \(R_\text{ref}=1\)) are embedded in the outer matrix via the Mori-Tanaka scheme. The radius ratio is fixed by the volume fraction constraint:

\[ \frac{R_a}{R_\text{ref}} = \left(\frac{f_a}{f_a+f_i}\right)^{1/3} \]

Function C_paste() — paste-scale homogenization (MT)
def C_paste(wc, alpha=-1.):
    if alpha < 0.: alpha = amax(wc)
    if alpha < 1.e-6: return None
    _fa = fa(wc, alpha)
    fis, fos, fip, fop, fi, phi_o = vf5(wc, alpha)
    if fop < 0. or phi_o >= 1.: return None

    Cout = C_outer(phi_o)
    if Cout is tZ4 or np.isnan(Cout.k): return None

    f_outer = 1. - _fa - fi       # outer fraction in paste
    f_inc   = _fa + fi             # sphere_nlayers fraction
    Ra      = ((_fa / f_inc)**(1./3.) if f_inc > 1.e-12 else 0.)

    ver = rve(matrix="OUTER")
    ver["OUTER"] = ellipsoid(shape=spherical, fraction=f_outer,
                             prop={"C": Cout})
    ver["INC"]   = sphere_nlayers(radii=[Ra, 1.],
                                  fraction=f_inc,
                                  prop={"C": [C_anhyd, C_inner]})
    return homogenize(prop="C", rve=ver, scheme=MT, verbose=False)
Figure — drained Young’s modulus of cement paste
wc_list = [0.25, 0.35, 0.45, 0.55]
fig, ax = plt.subplots(figsize=(7, 4.5))

for wc in wc_list:
    la = np.linspace(0., 0.999 * amax(wc), 80)
    lE = []
    for alpha in la:
        C = C_paste(wc, alpha)
        lE.append(C.E if C is not None and not np.isnan(C.E) else 0.)
    ax.plot(la, lE, label=fr"$w/c={wc}$")

ax.set_xlabel(r"$\alpha$"); ax.set_ylabel(r"$E^{hom}$ (GPa)")
ax.set_title("Drained Young's modulus of cement paste")
ax.set_xlim(0., 1.); ax.set_ylim(bottom=0.)
ax.grid(True); ax.legend()
plt.tight_layout(); plt.show()

The model correctly predicts:

  • a setting threshold (onset of stiffness) at \(\alpha \approx 0.1\)\(0.25\) depending on \(w/c\), controlled by the SC percolation of the outer phase;
  • a monotonic increase of \(E^{hom}\) up to \(\alpha_{\max}\);
  • lower asymptotic stiffness for higher \(w/c\) (more residual porosity at full hydration).

Undrained moduli

Ultra-sonic measurements probe the undrained elastic moduli of the saturated paste. The conversion uses Biot poromechanics (see Section 12.2.1). For a porous medium with homogeneous isotropic solid (moduli \(k_s\), \(\mu_s\)):

\[ b = 1 - \frac{k^{hom}}{k_s}, \qquad M = \frac{k_s}{b - \varphi}, \qquad k^u = k^{hom} + M b^2, \qquad \mu^u = \mu^{hom} \]

Applied scale by scale (inner, then outer with \(k_s = k_\text{solid CSH}\); paste with the undrained inner/outer as inputs):

Function C_paste_undrained() — drained and undrained moduli
ks_sol, mus_sol = C_sol.kmu   # solid C-S-H

def undrained_kmu(k_hom, mu_hom, k_s, phi):
    """Biot undrained bulk modulus."""
    if k_hom < 1.e-12 or phi <= 0.:
        return k_hom, mu_hom
    b = 1. - k_hom / k_s
    M = k_s / max(b - phi, 1.e-12)
    return k_hom + M * b**2, mu_hom

# Undrained inner (fixed)
k_iu, mu_iu = undrained_kmu(C_inner.k, C_inner.mu, ks_sol, phi_i)
C_inner_u = stiff_kmu(k_iu, mu_iu)

def C_paste_undrained(wc, alpha=-1.):
    if alpha < 0.: alpha = amax(wc)
    if alpha < 1.e-6: return None
    _fa = fa(wc, alpha)
    fis, fos, fip, fop, fi, phi_o = vf5(wc, alpha)
    if fop < 0. or phi_o >= 1.: return None

    Cout = C_outer(phi_o)
    if Cout is tZ4 or np.isnan(Cout.k): return None

    # Undrained outer
    k_ou, mu_ou = undrained_kmu(Cout.k, Cout.mu, ks_sol, phi_o)
    Cout_u = stiff_kmu(k_ou, mu_ou)

    f_outer = 1. - _fa - fi
    f_inc   = _fa + fi
    Ra      = (_fa / f_inc)**(1./3.) if f_inc > 1.e-12 else 0.

    ver = rve(matrix="OUTER")
    ver["OUTER"] = ellipsoid(shape=spherical, fraction=f_outer,
                             prop={"C": Cout_u})
    ver["INC"]   = sphere_nlayers(radii=[Ra, 1.],
                                  fraction=f_inc,
                                  prop={"C": [C_anhyd, C_inner_u]})
    return homogenize(prop="C", rve=ver, scheme=MT, verbose=False)
Figure — drained vs undrained moduli
fig, axes = plt.subplots(1, 2, figsize=(9, 4))

for wc in wc_list:
    la = np.linspace(0., 0.999 * amax(wc), 80)
    lEd, lEu = [], []
    for alpha in la:
        Cd = C_paste(wc, alpha)
        Cu = C_paste_undrained(wc, alpha)
        lEd.append(Cd.E if Cd is not None and not np.isnan(Cd.E) else 0.)
        lEu.append(Cu.E if Cu is not None and not np.isnan(Cu.E) else 0.)
    axes[0].plot(la, lEd, label=fr"$w/c={wc}$")
    axes[1].plot(la, lEu, label=fr"$w/c={wc}$")

for ax, title in zip(axes, ["Drained $E^{hom}$", "Undrained $E^{hom,u}$"]):
    ax.set_xlabel(r"$\alpha$"); ax.set_ylabel(r"$E$ (GPa)")
    ax.set_title(title); ax.set_xlim(0., 1.); ax.set_ylim(bottom=0.)
    ax.grid(True); ax.legend()

plt.tight_layout(); plt.show()

The undrained moduli are systematically higher than the drained ones, the difference being largest at low hydration degrees where the pore network is still well connected. At full hydration the difference vanishes as \(\varphi \to 0\) and \(b \to 0\).

\(\,\)