Laminates — periodic multilayer cells
A Laminate is a periodic unit cell of parallel layers of common normal n: no matrix, no reference medium, and an exact effective behavior rather than an estimate. It is an AbstractHomogenizationCell alongside RVE, solved by the Laminated scheme.
The theory, including the closed forms and the corrected pseudo-inverse argument, is on the laminate theory page.
Building a cell
Construction mirrors an RVE: an empty cell, then layers in stacking order.
using MeanFieldHomogenization, TensND
C_A = TensISO{3}(3 * 2.0, 2 * 0.8) # 3κ, 2μ
C_B = TensISO{3}(3 * 0.5, 2 * 0.2)
lam = Laminate(; normal = (0, 0, 1))
add_layer!(lam, :A, Dict(:C => C_A); fraction = 0.3)
add_layer!(lam, :B, Dict(:C => C_B); fraction = 0.7)
C_eff = homogenize(lam, Laminated(), :C)homogenize(lam, :laminated, :C) and the aliases :lam, :multilayer work too, as for every other scheme.
Thickness or fraction
Each layer takes exactly one of thickness (an absolute height) or fraction (a share of the period). Thicknesses are what is stored; layer_volume_fraction derives f_i = h_i / L from them.
The distinction matters as soon as an interface is imperfect: interfaces enter with the weight 1/L, an interface density, so the absolute period sets their size effect. With perfect bonding the result depends on the fractions alone and L is irrelevant.
lam = Laminate(; normal = (0, 0, 1))
add_layer!(lam, :A, Dict(:C => C_A); thickness = 30.0e-6) # 30 µm
add_layer!(lam, :B, Dict(:C => C_B); thickness = 70.0e-6)
laminate_period(lam) # 1.0e-4
layer_volume_fraction(lam, :A) # 0.3The frame
Give at most one of:
normal = (nx, ny, nz)— completed into an orthonormal(ℓ, m, n̂);euler_angles = (θ, ϕ, ψ)— ZYZ angles, as everywhere else in the package;basis = …— an explicitTensNDbasis whose third axis is the normal.
The default is the canonical frame n = e₃, for which the kernel skips the frame rotation entirely.
All three routes accept symbolic components, and so does the element type of the frame itself: Laminate(; normal = (0, sin(θ), cos(θ))) and Laminate(; euler_angles = (θ, 0, 0)) are ordinary laminates.
The result is invariant under rotation about n, so the choice of the in-plane pair is physically immaterial and never leaks into a gradient. It still has to be made, and it has to be non-degenerate. A numeric normal picks whichever canonical axis is least aligned with n̂ — a comparison, which can never degenerate. A symbolic normal cannot answer that comparison, so it falls back to e₁; when the normal may itself lie along e₁, name another reference:
lam = Laminate(; normal = (cos(θ), 0, sin(θ)), in_plane = (0, 1, 0))in_plane goes with normal only — euler_angles and basis already fix the whole frame.
Elasticity and transport
The physics is selected by the order of the stored property, exactly as for the mean-field schemes — the property key is only a dictionary key.
lam = Laminate(; normal = (0, 0, 1))
add_layer!(lam, :A, Dict(:C => C_A, :K => TensISO{3}(2.0)); fraction = 0.3)
add_layer!(lam, :B, Dict(:C => C_B, :K => TensISO{3}(0.3)); fraction = 0.7)
homogenize(lam, Laminated(), :C) # 4th order → elasticity
homogenize(lam, Laminated(), :K) # 2nd order → transportThe returned type
The symmetry of the result is decided structurally, from the declared classes of the layers:
one layer with perfect interfaces → the layer property object itself, unchanged;
every layer isotropic, or major-symmetric TI about
nitself (TensISO,TensTI{4,T,5}/TensTI{2,T,2}of axisn), and every interface in-plane isotropic → an exactTensTIaboutn;anything else → a generic
Tensin the laminate basis.
The middle case is worth more than tidiness: a TensTI fed back into a multiscale chain reaches the analytic TI-coaxial Hill branch instead of a cubature.
The last case is the general one, and it covers more than it may look. A laminate of orthotropic layers is not transversely isotropic even when their axes coincide with the laminate frame, a TI layer whose axis is not n breaks it too, and so does any anisotropic interface however isotropic the layers. The non-major-symmetric TensTI{4,T,8} — what the exact rotation-group average produces — also falls here deliberately: the five-coefficient Walpole read-off would discard its ℓ₃ ≠ ℓ₄ and antisymmetric content, so the generic wrapper, which is lossless, is used instead.
Bounds
Voigt and Reuss need no matrix phase, so they apply to a laminate. They bracket the exact answer, and two of the bracketings are equalities: the in-plane shear is exactly Voigt, the out-of-plane response exactly Reuss.
homogenize(lam, Voigt(), :C)
homogenize(lam, Reuss(), :C)Imperfect interfaces
The four models of the layered sphere are reused unchanged — a planar interface is the curvature-free case. interface is the condition on top of the layer; the last one closes the cell onto the first by periodicity.
| primal (field jump) | dual (surface stiffness) | |
|---|---|---|
| elasticity | SpringInterface(kn, kt) | MembraneInterface(κs, μs) |
| transport | KapitzaInterface(ρ) | SurfaceConductiveInterface(ks) |
lam = Laminate(; normal = (0, 0, 1))
add_layer!(lam, :A, Dict(:C => C_A); thickness = 0.3,
interface = SpringInterface(1.0e3, 5.0e2)) # stiffnesses
add_layer!(lam, :B, Dict(:C => C_B); thickness = 0.7,
interface = MembraneInterface(0.07, 0.04))Stiffnesses in, compliances stored
SpringInterface(kn, kt) takes stiffnesses — traction per unit opening — matching Echoes' PRIMALDISC, so kn, kt → ∞ is perfect bonding and k → 0 decouples the layers. The type stores the compliances itf.sn, itf.st and settable with SpringInterface(; sn, st), because a perfect interface is then the exact zero sn = st = 0 rather than an infinity — the near-perfect regime stays differentiable. interface_param(i, :kn) and interface_param(i, :sn) both work and give reciprocal sensitivities.
Anisotropic interfaces
The four types above are isotropic in the plane — that is what the spherical recurrence requires, since the jump conditions must share the symmetry of the geometry. A plane imposes no such restriction: it has a well-defined normal and an arbitrary in-plane texture. A laminate therefore also accepts a full tensor:
| scalar (shared with the sphere) | tensor (laminate only) |
|---|---|
SpringInterface(kn, kt) | AnisotropicSpringInterface(𝒦) — any symmetric 3×3 compliance |
MembraneInterface(κs, μs) | AnisotropicMembraneInterface(ℂˢ) — any in-plane surface stiffness (6 coefficients) |
SurfaceConductiveInterface(ks) | AnisotropicSurfaceConductiveInterface(𝐤ˢ) — any in-plane surface conductivity |
KapitzaInterface(ρ) | — already general: [T] = ρ qₙ relates two scalars |
# a spring with different normal and tangential compliances, and a coupling
𝒦 = [3.0e-3 5.0e-4 0.0; 5.0e-4 8.0e-3 0.0; 0.0 0.0 1.0e-3]
add_layer!(lam, :A, Dict(:C => C_A); thickness = 0.3,
interface = AnisotropicSpringInterface(𝒦))
# an orthotropic membrane: in-plane Kelvin-Mandel block (ℓ⊗ℓ, m⊗m, √2 ℓ⊗ˢm),
# so the [3,3] entry is 2 Cˢ₁₂₁₂
ℂˢ = [0.20 0.05 0.0; 0.05 0.09 0.0; 0.0 0.0 0.06]
add_layer!(lam, :B, Dict(:C => C_B); thickness = 0.7,
interface = AnisotropicMembraneInterface(ℂˢ))A tensor field is read as components in the layer frame (ℓ, m, n) when given as a plain matrix, or converted from its own basis when given as a TensND tensor. Feeding the tensor form the isotropic values reproduces the scalar form exactly.
Both oracles stay exact with a full tensor — the compliance simply adds to the out-of-plane series law, the surface stiffness to the in-plane one. What does change is the symmetry of the result: an anisotropic interface breaks transverse isotropy just as an anisotropic layer does, so such a cell returns a generic Tens even when every layer is isotropic.
The two families act on complementary halves of the answer: a primal interface changes the out-of-plane response and leaves the in-plane one untouched, a dual one does the reverse (and, the interfaces being planar, produces no traction jump at all).
The displacement jump itself is available:
interface_jump(lam, 1, E) # [u] across interface 1 under macroscopic strain EPer-layer fields
layer_strain_localization(lam, :A) # 𝔸_A, ε_A = 𝔸_A : E, Σ f 𝔸 = 𝕀
layer_stress_localization(lam, :A) # 𝔹_A, σ_A = 𝔹_A : Σ
layer_gradient_localization(lam, :A) # transport counterparts
layer_flux_localization(lam, :A)
laminate_hill(lam, :A) # (ℙ_A, ℚ_A), the two Hill tensorsA layer also answers the package-wide localization generics, under the same names used for every inclusion — with the layer name in place of the (ℂ₁, ℂ₀) pair, a laminate having neither a matrix nor a reference medium:
strain_strain_loc(lam, :A) # 𝔸_A — same object as above
stress_strain_loc(lam, :A) # ℂ_A : 𝔸_A, Σ f · = ℂ^hom
strain_stress_loc(lam, :A) # 𝔸_A : 𝕊^hom, Σ f · = 𝕊^hom
stress_stress_loc(lam, :A) # 𝔹_A
gradient_gradient_loc(lam, :A) # and the four transport twins
flux_gradient_loc(lam, :A)
gradient_flux_loc(lam, :A)
flux_flux_loc(lam, :A)The two mixed tensors ℂ_i : 𝔸_i and 𝔸_i : 𝕊^hom have no layer_* name; they are what a Levin-type post-processing of a laminate needs. All eight go through the same cofactor block algebra as the effective property, so they are exact under ForwardDiff.Dual and evaluable symbolically.
Primal interfaces break the strain-side sum rules
Σ_i f_i 𝔸_i = 𝕀 and Σ_i f_i ℂ_i:𝔸_i = ℂ^hom hold for perfect and dual (membrane) interfaces. With a primal one (spring, Kapitza) part of the macroscopic strain is carried by the displacement jumps, so the layer strains no longer average to E — see interface_jump.
Sensitivities
Two lenses are specific to a laminate, on top of the shared PropertyParameter:
thickness(:A) # ThicknessParameter — a layer thickness
interface_param(1, :kn) # InterfaceParameter — an interface scalar
property(:A, :C, :shear) # shared with the RVE; `phase` names a LAYER here
derivative(lam, Laminated(), thickness(:A); indexer = C -> k_mu(C)[1])
gradient(lam, Laminated(), [thickness(:A), interface_param(1, :kn)];
indexer = C -> k_mu(C)[2])Differentiating with respect to a thickness is not the same as with respect to a volume fraction: it also moves the period, hence the interface size effect. AmountParameter therefore raises on a laminate, pointing at ThicknessParameter, rather than silently reinterpreting itself.
Symbolic and autodiff
The kernel is generic in the number type: the pseudo-inverse is a cofactor inverse (never an SVD) and every intermediate is an SMatrix (never an MMatrix, which cannot even be constructed for a symbolic element type). A laminate of symbolic layers therefore produces the closed forms directly — scripts/38_laminate_symbolic.jl derives Backus (1962) that way — and ForwardDiff traverses moduli, thicknesses, interface compliances and nested scales alike.
Nothing has to be declared for this: symbolic moduli, thicknesses and frames are all carried by ordinary construction. T remains available as an element-type floor, and it is what a canonical frame takes its own element type from, so Laminate(; T = Sym) and Laminate(; T = Sym, normal = (0, 0, 1)) agree — but neither is required to obtain an exact symbolic answer.
using SymPy
@syms k_A::positive mu_A::positive k_B::positive mu_B::positive f::positive
lam = Laminate(; normal = (0, 0, 1))
add_layer!(lam, :A, Dict(:C => iso_stiffness(k_A, mu_A)); fraction = f)
add_layer!(lam, :B, Dict(:C => iso_stiffness(k_B, mu_B)); fraction = 1 - f)
C = get_array(homogenize(lam, Laminated(), :C))
simplify(C[2, 3, 2, 3]) # mu_A*mu_B/(f*mu_B + mu_A*(1 - f))Why the frame's element type matters
A TensTI converts its axis to the element type of its data, and its components are rebuilt from the Walpole basis of that axis. An axis read off a Float64 frame as (0.0, 0.0, 1.0) therefore reappears as a symbolic 1.0 multiplying every coefficient of the result. A canonical frame is consequently read exactly — (0, 0, 1) — whatever its own element type. An obliquely oriented numeric frame still contributes floating-point axis components, which is correct: the geometry itself is floating point.
Ageing viscoelasticity is the one part that stays numerical: laminate_alv discretizes Volterra operators on a grid of times, so it rejects a symbolic frame rather than pretending otherwise.
Ageing viscoelasticity
Store a ViscoLaw per layer and call homogenize_alv:
lam = Laminate(; normal = (0, 0, 1))
add_layer!(lam, :A, Dict(:C => maxwell_relaxation(C_A, [C_A], [3.0])); thickness = 0.4)
add_layer!(lam, :B, Dict(:C => heaviside_law(C_B)); thickness = 0.6)
homogenize_alv(lam, Laminated(), :C; times = 0.0:2.0:10.0)Voigt and Reuss are available in ALV too. Interfaces stay elastic in this version.
In a multiscale chain
A laminate is a cell like any other: chain it explicitly, or nest it declaratively with Homogenized — in either direction. See Multiscale models.
rve = RVE()
add_phase!(rve, :M, Ellipsoid(1.0), Dict(:C => C_matrix); fraction = :rest)
add_phase!(rve, :agg, Ellipsoid(1.0),
Dict(:C => Homogenized(lam, Laminated())); fraction = 0.3)
homogenize(rve, MoriTanaka(), :C)A cell, not an inclusion
A laminate is homogenized, not embedded. Putting a laminated inclusion inside a matrix would require its Hill tensor, which is a different problem and is not provided.