Skip to content

Scale transition

What the FE code needs

At each quadrature point a finite-element code solving a nonlinear problem needs two things from the material, per step:

the stress and the consistent tangent, together with the updated internal state . The residual uses the first; the Newton Jacobian uses the second. A tangent that is merely close costs quadratic convergence — nothing else, which is why a wrong one is easy to ship unnoticed.

Where homogenization enters

The material response is a homogenization: the strain drives an RVE, the scheme returns its effective stiffness, and

All the nonlinearity sits in — the crack apertures, the open/closed set, a damage variable. Between two events that change , the law is linear, so

is exact, not an approximation. No numerical differentiation is needed, and no algorithmic tangent has to be derived: the scheme already returns it.

This is what makes the coupling affordable

Because depends on the state only through a discrete configuration (which families are open), the expensive scheme solve is shared by every quadrature point in the same configuration — see MaterialCache.

Several gradients, several fluxes

A poroelastic material takes a strain and a pore pressure, and returns a stress and a variation of fluid content:

The contract is therefore written with named gradients and fluxes, and the tangent as a set of blocks keyed flux then gradient:

blockvaluekey
:σε
:σp
:φε
:φp

A purely mechanical law declares only :σε and never sees the rest. This is the same shape MGIS uses for MFront's generic behaviors.

The one rule that bites

A Tensors.jl tensor — what Ferrite and every Julia FE code speaks — has no basis: its components are always global.

A TensND tensor returns its components in its own basis, and a homogenized stiffness whose RVE carries tilted crack families comes back in a rotated basis. Handing its raw array to an FE assembler rotates the material silently. Cross the boundary with to_tensors and from_tensors, never with get_array.

julia
using MeanFieldHomogenization, TensND

C₀ = TensISO{3}(3 * 30.0, 2 * 18.0)
rve = RVE()
add_phase!(rve, :M, Ellipsoid(1.0), Dict(:C => C₀); fraction = :rest)
add_phase!(rve, :F, PennyCrack(1.0; euler_angles = (π / 4, 0.0)), Dict(:C => C₀);
           density = 0.08)

C_hom = homogenize(rve, MoriTanaka())
raw    = get_array(C_hom)[1, 1, 1, 1]          # components in ITS OWN basis
global_ = to_tensors(C_hom)[1, 1, 1, 1]        # components an FE code expects
(raw, global_)
(52.054054054054056, 43.433607520564046)

The two differ: the first is expressed in the crack frame, the second in the global one.