The strength of a sandstone: crushing grains and sliding contacts
Before this page
Friction of a granular medium from its contacts, the same polycrystal with rigid grains, and Layered sphere §7 for the strain carried by the opening of an interface.
A sandstone is a sand whose grains have been cemented together. Its strength involves two mechanisms: the grains can yield, and the cement between them can slide. This page reproduces the micromechanical model of Dormieux et al. (2010), in which the solid of the grains obeys a von Mises criterion,
The normalization of this page
This page keeps the normalization of Dormieux et al. (2010), with
The model follows the modified secant method: each nonlinear behavior is replaced by a linear one whose stiffness depends on the average strain it undergoes, and the strength is reached when these stiffnesses vanish. The linear problem behind it is therefore the heart of the model: an incompressible grain of shear modulus
1. The grain and its interface
using MeanFieldHomogenization, TensND, ForwardDiff, Printf, Plots
gr() # headless backend; GKSwstype is set to "100" in make.jl
brick(μs, kt) = LayeredSphere(
(1.0,), (iso_stiffness(Inf, μs),); # R = 1, incompressible grain
interfaces = (SpringInterface(Inf, kt),), # rigid normally, kₜ tangentially
)The average strain of the grain is read on the outer lip of its interface (their eq. 43): it includes the sliding of the contacts, which is what the concentration tensor of a LayeredSphere counts by default. With
2. The self-consistent moduli in closed form
The self-consistent estimate
using SymPyPythonCall
@syms k::positive μ::positive ks::positive μs::positive kn::positive kt::positive
@syms R::positive φ::positive x::positive κ::positive ϵ::positive
C = iso_stiffness(k, μ)
S_sym = LayeredSphere((R,), (iso_stiffness(ks, μs),); interfaces = (SpringInterface(kn, kt),))
αS, βS = get_data(gradient_gradient_concentration(S_sym, C, C))
aS, bS = get_data(flux_gradient_concentration(S_sym, C, C))
αP, βP = get_data(gradient_gradient_concentration(Ellipsoid(1.0), iso_stiffness(0, 0), C))
k_sc = simplify(solve(3k * ((1 - φ) * αS + φ * αP) - (1 - φ) * aS, k)[1])which is eq. (49) of the article. Substituted in the deviatoric equation, it leaves a product of positive factors and of the cubic of eq. (50), the only factor of degree three in
shear = 2μ * ((1 - φ) * βS + φ * βP) - (1 - φ) * bS
num, _ = sympy.fraction(sympy.together(subs(shear, k => k_sc)))
[sympy.degree(f, μ) for (f, _) in sympy.factor_list(num)[2]]The article then lets the grain and the normal contact become incompressible,
function lowest_order(e)
n, _ = sympy.fraction(sympy.together(e))
c = sympy.Poly(n, ϵ).all_coeffs()
return c[findlast(c -> c != 0, c)]
end
eq52 = lowest_order(subs(shear, k => k_sc, ks => 1 / ϵ, kn => 1 / ϵ))
eq52 = subs(eq52, μ => x * μs, kt => κ * μs / R)
q52 = 16 * (5 + κ) * (3 - φ) * x^2 + ((9 + 77φ) * κ + 114 * (3φ - 1)) * x + 57κ * (2φ - 1)
factor(eq52 / q52)Its positive root is
simplify(subs(sympy.Poly(q52, κ).coeff_monomial(κ), x => 3 * (1 - 2φ) / (3 - φ)))3. The numerical estimate
The self-consistent iteration keeps every modulus above
function moduli(μs, kt, φ)
rve = RVE()
add_phase!(rve, :S, brick(μs, kt), Dict(:C => iso_stiffness(1.0, 1.0)); fraction = 1 - φ)
add_phase!(rve, :P, Ellipsoid(1.0), Dict(:C => iso_stiffness(0.0, 0.0)); fraction = φ)
start = ForwardDiff.value(min(μs, kt)) # the scale of the moduli, also under a Dual
sc = SelfConsistent(init = iso_stiffness(start, start), abstol = 1.0e-15, reltol = 1.0e-13, maxiters = 2000)
return collect(k_mu(homogenize(rve, sc, :C)))
end
M52(κ, φ) = (a = 16 * (5 + κ) * (3 - φ); b = (9 + 77φ) * κ + 114 * (3φ - 1); c = 57κ * (2φ - 1);
(-b + sqrt(b^2 - 4a * c)) / (2a))
for (κv, φv) in ((0.1, 0.35), (1.0, 0.25), (10.0, 0.30))
kv, μv = moduli(1.0, κv, φv)
@printf("κ = %4.1f, φ = %.2f: μ/μs = %.8f (eq. 52: %.8f), k/μ = %.6f (eq. 51: %.6f)\n",
κv, φv, μv, M52(κv, φv), kv / μv, 4 * (1 - φv) / (3φv))
endκ = 0.1, φ = 0.35: μ/μs = 0.06999429 (eq. 52: 0.06999429), k/μ = 2.476190 (eq. 51: 2.476190)
κ = 1.0, φ = 0.25: μ/μs = 0.32903820 (eq. 52: 0.32903820), k/μ = 4.000000 (eq. 51: 4.000000)
κ = 10.0, φ = 0.30: μ/μs = 0.40057945 (eq. 52: 0.40057945), k/μ = 3.111111 (eq. 51: 3.111111)4. From the averages to the strength
Two averages drive the criteria, and both come from derivatives of the effective moduli at fixed load (their eqs. 23 and 33):
the quadratic mean
of the deviatoric strain in the grains, with and ;the quadratic mean
of the tangential traction on the contacts, with and .
The mean normal traction on the contacts is
function coefficients(κv, φv)
k, μ = moduli(1.0, κv, φv)
J = ForwardDiff.jacobian(p -> moduli(p[1], p[2], φv), [1.0, κv])
return J[1, 1] / k^2, J[2, 1] / μ^2, κv^2 * J[1, 2] / k^2, κv^2 * J[2, 2] / μ^2
endGrains alone. When the grains yield, their secant shear modulus behaves as
Grains and contacts. When the contacts slide too, the Mohr–Coulomb law holds on the averages,
for
κs = 10.0 .^ range(-8, 6; length = 141)
H²(c, φv, αv) = (3αv^2 / ((1 - 2φv) * (1 - φv)^2) - c[3]) / c[4]
function point(c, φv, αv)
h² = max(H²(c, φv, αv), 0.0)
Sm = sqrt((1 - φv) / (c[1] + c[2] * h²))
return (Sm, Sm * sqrt(h²))
end
function envelope(φv, αv, coefs)
pts = φv > 1 / 3 ? [(0.0, 0.0)] : Tuple{Float64, Float64}[] # contacts alone
for i in eachindex(κs)
if H²(coefs[i], φv, αv) ≥ 0
push!(pts, point(coefs[i], φv, αv))
elseif i > 1 # H² vanishes between κs[i-1] and κs[i]
lo, hi = log10(κs[i - 1]), log10(κs[i])
for _ in 1:40
mid = (lo + hi) / 2
H²(coefficients(10.0^mid, φv), φv, αv) ≥ 0 ? (lo = mid) : (hi = mid)
end
push!(pts, point(coefficients(10.0^lo, φv), φv, αv))
break
end
end
return pts
end
ellipse_radii(φv) = (c = coefficients(1.0e8, φv); (sqrt((1 - φv) / c[1]), sqrt((1 - φv) / c[2])))
plt = plot(; aspect_ratio = :equal, xlabel = "−Σm / K", ylabel = "Σd / K", legend = :topright,
title = "Strength of a sandstone (K = 1)")
for (i, φv) in enumerate((0.35, 0.25))
a, b = ellipse_radii(φv)
t = range(0, π / 2; length = 100)
plot!(plt, a .* cos.(t), b .* sin.(t); color = :red, lw = 2,
label = i == 1 ? "grains alone (eq. 75)" : "")
coefs = [coefficients(κv, φv) for κv in κs]
for (j, αv) in enumerate((0.16, 0.20, 0.25, 0.30))
pts = envelope(φv, αv, coefs)
plot!(plt, first.(pts), last.(pts); color = :blue, lw = 1.5, marker = :cross, ms = 2,
label = (i == 1 && j == 1) ? "grains and contacts (eq. 85)" : "")
end
end
annotate!(plt, [(0.35, 0.05, text("φ = 0.35", 9)), (1.0, 0.05, text("φ = 0.25", 9))])
plt
For each porosity, the red ellipse is the strength of the porous solid when only the grains yield; the blue curves, for
The friction coefficient enters only through
include(joinpath(pkgdir(MeanFieldHomogenization), "scripts", "common", "docviz.jl"))
φv = 0.25
coef = [coefficients(κv, φv) for κv in κs]
## n points evenly spaced along the polyline pts.
function resample(pts, n)
s = vcat(0.0, cumsum([hypot((pts[k] .- pts[k - 1])...) for k in 2:length(pts)]))
return map(range(0, s[end]; length = n)) do t
k = clamp(searchsortedlast(s, t), 1, length(pts) - 1)
w = s[k + 1] > s[k] ? (t - s[k]) / (s[k + 1] - s[k]) : 0.0
(1 - w) .* pts[k] .+ w .* pts[k + 1]
end
end
αs = range(0.12, 0.34; length = 23)
envs = [envelope(φv, αv, coef) for αv in αs]
pts = resample.(envs, 61)
grid = [pts[i][j] for i in eachindex(pts), j in 1:61]
Xs, Zs = first.(grid), last.(grid)
Ys = [αv for αv in αs, _ in 1:61]
surface = surface_trace(Xs, Ys, Zs; color = "#2c7fb8", opacity = 0.9)
rim = line_trace(
hcat(permutedims(hcat(Xs[1, :], Ys[1, :], Zs[1, :])), permutedims(hcat(Xs[:, end], Ys[:, end], Zs[:, end])),
permutedims(hcat(reverse(Xs[end, :]), reverse(Ys[end, :]), reverse(Zs[end, :]))),
permutedims(hcat(reverse(Xs[:, 1]), reverse(Ys[:, 1]), reverse(Zs[:, 1]))));
color = FIG_VECTOR, width = 4)Two families of planes cut it. A plane
xm, zm = maximum(Xs), maximum(Zs)
## Plane α = const: the envelope itself.
αc = 0.12:0.02:0.34
envc = [envelope(φv, αv, coef) for αv in αc]
friction = CutFamily(
"Plane α = const", "α", string.(αc),
[plane_patch([xm / 2, αv, zm / 2], [1, 0, 0], [0, 0, 1], 0.55xm, 0.55zm) for αv in αc],
[vcat(first.(e)', fill(αv, 1, length(e)), last.(e)') for (αv, e) in zip(αc, envc)],
[vcat(first.(e)', last.(e)') for e in envc],
("−Σm / K", "Σd / K"); active = 5,
)
## Plane −Σm = c: Σd where each envelope crosses that mean stress.
function crossing(e, c)
for k in 1:(length(e) - 1)
(x0, z0), (x1, z1) = e[k], e[k + 1]
(x0 - c) * (x1 - c) ≤ 0 && x1 != x0 && return z0 + (c - x0) / (x1 - x0) * (z1 - z0)
end
return NaN
end
cs = 0.1:0.1:floor(xm; digits = 1)
shear = [[crossing(e, c) for e in envs] for c in cs]
confinement = CutFamily(
"Plane Σm = const", "−Σm / K", string.(cs),
[plane_patch([c, (first(αs) + last(αs)) / 2, zm / 2], [0, 1, 0], [0, 0, 1], 0.55 * (last(αs) - first(αs)), 0.55zm) for c in cs],
[vcat(fill(c, 1, length(αs)), collect(αs)', d') for (c, d) in zip(cs, shear)],
[vcat(collect(αs)', d') for d in shear],
("friction α", "Σd / K"); active = 5, equal = false,
)
section_figure([surface, rim], [friction, confinement];
uid = "sandstone-envelopes-3d", height = 620, aspectmode = "cube",
xlabel = "−Σm / K", ylabel = "friction α", zlabel = "Σd / K",
title = "Strength envelopes of a sandstone, φ = 0.25")The buttons above the surface choose a family of planes, or none, and the slider below moves the plane; on the right, the section is drawn in coordinates of its plane.
Where to go next
The rigid-grain limit of this model, with frictional contacts only, is Friction of a granular medium from its contacts. The secant reasoning on the energy derivatives is the same as in Quasi-brittle strength of cement paste and mortar, where it is applied to a brittle solid. The next application, Viscoelastic complex modulus of a bituminous mixture, turns to time-dependent behavior.