Poroelastic coefficients from a microstructure
Where the homogenisation stops and this package starts.
MeanFieldHomogenization.jl owns the estimate: given a microstructure and a scheme it returns a drained stiffness BiotPoroelastic consumes a Biot coefficient b and a storage modulus N, and knows nothing about where they came from. The division is deliberate — a microstructure is not a boundary value problem, and the two questions have different right answers about what to be general in.
This page checks that the two halves meet. Nothing here couples the packages at run time; the point is that the numbers agree on the one case where both have a closed form, so that handing one to the other is a defensible thing to do.
What this is not
Not a validation of the homogenisation, which is that package's business and is validated there. This checks the interface: the same coefficient, computed twice from different directions, with the conventions each package writes it in.
using MeanFieldHomogenization
using PoroMechanics
using PrintfA porous solid
Spherical pores in a uniform solid, Mori-Tanaka. Deliberately the simplest microstructure that has a textbook answer: the pore space is isotropic, so
E_s, ν_s, φ = 60.0e9, 0.25, 0.20 # solid grain, and the porosity
C_s = iso_stiffness_E_nu(E_s, ν_s)
k_s = E_s / (3 * (1 - 2ν_s)) # bulk modulus of the solid phase
rve = RVE(:solid)
add_matrix!(rve, Ellipsoid(1.0), Dict(:C => C_s))
# A void is an inclusion with no stiffness. Exactly zero would be singular for the scheme,
# so it is given 1 Pa against the solid's 60 GPa — eleven orders down, and the result is
# unchanged at the printed precision by making it smaller.
add_phase!(
rve, :pore, Ellipsoid(1.0), Dict(:C => iso_stiffness_E_nu(1.0, 0.0)); fraction = φ
)
C_hom = homogenize(rve, MoriTanaka())
par = poroelastic_parameters(C_hom, C_s, φ)(B = (0.3599999999946) 𝟏, inverse_modulus = 3.999999999864999e-12, modulus = 2.5000000000843756e11)The drained constants the field problem needs, read off the homogenised stiffness:
k_hom = C_hom[1, 1, 1, 1] - 4 / 3 * C_hom[1, 2, 1, 2]
G_hom = C_hom[1, 2, 1, 2]
E_hom = 9k_hom * G_hom / (3k_hom + G_hom)
ν_hom = (3k_hom - 2G_hom) / (2 * (3k_hom + G_hom))
@printf("drained: E = %.4f GPa ν = %.6f K = %.4f GPa\n", E_hom / 1.0e9, ν_hom, k_hom / 1.0e9)drained: E = 39.9638 GPa ν = 0.239819 K = 25.6000 GPaThe coefficient, computed twice
MeanFieldHomogenization forms
which for an isotropic pore space collapses to the two scalars every poromechanics text writes down. Computing those independently is the check:
b_closed = 1 - k_hom / k_s
N_closed = (b_closed - φ) / k_s
@printf(" MeanFieldHomogenization: b = %.12f 1/M = %.6e Pa⁻¹\n", par.B[1, 1], par.inverse_modulus)
@printf(" closed form: b = %.12f 1/M = %.6e Pa⁻¹\n", b_closed, N_closed)
@printf(" off-diagonal of B: %.3e (isotropic pore space ⇒ zero)\n", par.B[1, 2]) MeanFieldHomogenization: b = 0.359999999995 1/M = 4.000000e-12 Pa⁻¹
closed form: b = 0.359999999995 1/M = 4.000000e-12 Pa⁻¹
off-diagonal of B: 0.000e+00 (isotropic pore space ⇒ zero)Identical. The interface is sound in the direction that matters: par.B[1,1] is this package's b, and par.inverse_modulus is its N, with no conversion in between.
What the field problem then does with them
The microstructure now sets the consolidation, rather than a deck doing it:
m = BiotPoroelastic(;
E = E_hom, nu = ν_hom, k = 1.0e-16, mu_l = 1.0e-3,
b = par.B[1, 1], N = par.inverse_modulus
)
@printf("c_v = %.4e m²/s Skempton = %.6f ν_u = %.6f\n",
consolidation_coefficient(m), skempton(m), undrained_poisson(m))c_v = 1.4810e-02 m²/s Skempton = 1.551724 ν_u = 0.372840The Skempton coefficient comes out above one, which is not a bug in either package and is worth pausing on. MeanFieldHomogenization's own documentation warns of it: its skempton(m) = b / (N·K + b²) — was written from Biot's relations with no knowledge of that discussion, and reproduces it. Two independent routes to the same consequence is the strongest corroboration this page has to offer.
It also means the pairing carries an assumption that has to travel with it: a compressible fluid adds N above nor BiotPoroelastic will add it for you.
Where the bridge stops
rve_c = RVE(:solid)
add_matrix!(rve_c, Ellipsoid(1.0), Dict(:C => C_s))
add_phase!(rve_c, :crack, PennyCrack(1.0), Dict(:C => C_s); density = 0.08)
B_c = biot_tensor(homogenize(rve_c, MoriTanaka()), C_s)
@printf("cracked: B₁₁ = %.6f B₃₃ = %.6f ratio = %.4f\n",
B_c[1, 1], B_c[3, 3], B_c[3, 3] / B_c[1, 1])cracked: B₁₁ = 0.108108 B₃₃ = 0.324324 ratio = 3.0000A factor of three between the crack normal and the crack plane. BiotPoroelastic holds a scalar b and cannot carry that: the bridge built above closes only for an isotropic pore space, and a cracked medium needs a poroelastic material with a tensorial Biot coefficient before any of this transfers. That material does not exist here yet, and saying which microstructures the current one may legitimately be fed is the useful half of this page.