Cohesive Fracture with Matrix-Free Newton Solver
This tutorial demonstrates quasi-static fracture simulation using FEAX's matrix-free Newton solver and cohesive zone model. We solve a 3D Mode I fracture problem where the total energy is composed of bulk elastic and cohesive interface contributions — all expressed as pure JAX functions and differentiated automatically.
Overview
Traditional FE fracture solvers assemble the tangent stiffness matrix explicitly. FEAX takes a different approach:
- Define the total potential energy as a sum of bulk and cohesive terms
- The residual is obtained automatically:
- The tangent operator is computed via JVP (Jacobian-vector product) of the residual — no sparse matrix assembly
- An iterative Krylov solver (CG) solves the Newton linear system using only matvec operations
This is the matrix-free Newton path in FEAX, activated by passing MatrixFreeOptions as the solver options.
Energy-Based Formulation
Bulk Elastic Energy
The bulk elastic strain energy density for linear elasticity is:
where is the infinitesimal strain tensor, and , are the Lamé constants:
The total bulk energy is the volume integral:
In FEAX, this is obtained via get_energy_density() and create_energy_fn():
class Elasticity3D(fe.problem.Problem):
def get_energy_density(self):
def psi(u_grad):
eps = 0.5 * (u_grad + u_grad.T)
return 0.5 * lmbda * np.trace(eps)**2 + mu * np.sum(eps * eps)
return psi
elastic_energy = create_energy_fn(problem) # ∫ ψ(∇u) dΩ
Note that get_energy_density() returns the scalar energy density , not the stress tensor. FEAX integrates this over quadrature points to compute the total energy. The stress and tangent stiffness are never assembled explicitly — they are computed on-the-fly via JAX's automatic differentiation.
Cohesive Interface Energy
The cohesive zone model introduces an energy contribution along the fracture interface . For a node pair across the interface, the displacement jump is:
This jump is decomposed into normal and tangential components:
The effective opening combines both modes:
where is the Macaulay bracket (no energy in compression) and is the mode-mixity ratio.
Xu–Needleman Exponential Potential
The cohesive potential per node follows the Xu–Needleman law:
where is the characteristic opening, is the fracture energy, and is the critical cohesive traction. The traction-separation relation is:
with peak traction at .
Irreversibility
Unloading follows a secant path to prevent energy recovery:
where is the historical maximum opening. This requires tracking as a state variable across load steps.
Total Cohesive Energy
The total cohesive energy is a weighted sum over interface nodes:
where are the integration weights (lumped area per node).
interface = CohesiveInterface.from_axis(
top_nodes=coh_top, bottom_nodes=coh_bottom,
weights=weights, normal_axis=1, vec=3, beta=0.0,
)
cohesive_energy = interface.create_energy_fn(
exponential_potential, Gamma=Gamma, sigma_c=sigma_c,
)
Total Energy and Matrix-Free Newton
The total potential energy is the sum:
def total_energy(u_flat, delta_max):
return elastic_energy(u_flat) + cohesive_energy(u_flat, delta_max)
The Newton solver finds such that . At each Newton iteration :
- Residual: — computed via
jax.grad - Newton direction: solve where
- Update:
The key: step 2 never forms explicitly. Instead, the CG solver only needs the matrix-vector product , which is computed via JVP:
This is exactly what jax.jvp computes in forward-mode AD, at roughly the cost of one residual evaluation.
Problem Setup
Material and Geometry
import jax.numpy as np
import numpy as onp
import feax as fe
from feax.mechanics.cohesive import (
CohesiveInterface, compute_lumped_area_weights, exponential_potential,
)
from feax.solvers.matrix_free import (
LinearSolverOptions, MatrixFreeOptions, create_energy_fn,
)
# Material parameters
E = 106e3 # Young's modulus [Pa]
nu = 0.35 # Poisson's ratio
Gamma = 15.0 # Fracture energy [J/m²]
sigma_c = 20e3 # Critical cohesive traction [Pa]
mu = E / (2 * (1 + nu))
lmbda = E * nu / ((1 + nu) * (1 - 2 * nu))
The geometry is scaled by the Griffith length:
where is the far-field stress. The specimen is with an initial crack of length .
Mesh with Split Interface
The mesh consists of two half-blocks (top/bottom) separated at . Nodes on the interface are duplicated to allow displacement discontinuity. A pre-crack extends from to (free surfaces with no cohesive traction).
mesh = fe.mesh.Mesh(points=np.array(coords), cells=np.array(elements))
problem = Elasticity3D(mesh, vec=3, dim=3, ele_type='HEX8')
Cohesive Interface Setup
Integration weights are computed from the quad elements on the interface surface using lumped area weighting — each quad contributes 1/4 of its area to each of its 4 nodes:
weights = compute_lumped_area_weights(coh_bottom, coords, active_quads_bottom)
interface = CohesiveInterface.from_axis(
top_nodes=coh_top, bottom_nodes=coh_bottom,
weights=weights, normal_axis=1, vec=3, beta=0.0,
)
beta=0.0 gives pure Mode I (only normal opening contributes to the effective opening).
Boundary Conditions
Mode I loading via prescribed displacement on top/bottom faces:
def make_bc(disp):
specs = [
fe.DCboundary.DirichletBCSpec(top_face, 'x', 0.0),
fe.DCboundary.DirichletBCSpec(top_face, 'y', disp / 2),
fe.DCboundary.DirichletBCSpec(top_face, 'z', 0.0),
fe.DCboundary.DirichletBCSpec(bottom_face, 'x', 0.0),
fe.DCboundary.DirichletBCSpec(bottom_face, 'y', -disp / 2),
fe.DCboundary.DirichletBCSpec(bottom_face, 'z', 0.0),
fe.DCboundary.DirichletBCSpec(left_face, 'x', 0.0),
fe.DCboundary.DirichletBCSpec(front_face, 'z', 0.0),
fe.DCboundary.DirichletBCSpec(back_face, 'z', 0.0),
]
return fe.DCboundary.DirichletBCConfig(specs).create_bc(problem)
Solver
Create the matrix-free solver by passing MatrixFreeOptions and the custom total_energy function:
solver_options = MatrixFreeOptions(
newton_tol=1e-8,
newton_max_iter=200,
linear_solver=LinearSolverOptions(solver='cg', atol=1e-8, maxiter=200),
verbose=True,
)
bc0 = make_bc(0.0)
solver = fe.create_solver(
problem, bc0,
solver_options=solver_options,
energy_fn=total_energy,
)
Key points:
MatrixFreeOptionsactivates the JVP-based Newton pathenergy_fn=total_energyprovides the custom energy (bulk + cohesive)- The inner CG solver uses only matvec operations — no sparse matrix stored
verbose=Trueprints Newton convergence info viajax.debug.print(JIT-compatible)
Quasi-Static Loading
Each load step increments the prescribed displacement. The solver uses the previous solution as initial guess, and is updated after convergence:
u_flat = np.zeros(problem.num_total_dofs_all_vars)
delta_max = np.zeros(interface.n_nodes)
for step in range(1, n_steps + 1):
disp = applied_disp * step / n_steps
# Apply BC values to initial guess
bc = make_bc(disp)
u_flat = u_flat.at[bc.bc_rows].set(bc.bc_vals)
# Solve: delta_max is passed as extra argument
u_flat = solver(delta_max, u_flat)
# Update irreversibility state
delta_current = interface.get_opening(u_flat)
delta_max = np.maximum(delta_max, delta_current)
Note: solver(delta_max, u_flat) — the first argument corresponds to the extra arguments of total_energy, and the second is the initial guess.
Post-Processing
Reaction Force via Energy Gradient
The reaction force is extracted from the internal force vector, which is the gradient of the total energy:
gradient_fn = jax.grad(total_energy)
fint = gradient_fn(u_flat, delta_max)
reaction_force = float(np.sum(fint[upper_y_dofs]))
Energy Decomposition
Track elastic and cohesive energy separately to monitor fracture progression:
e_elastic = elastic_energy(u_flat)
e_cohesive = cohesive_energy(u_flat, delta_max)
When the cohesive energy reaches the total fracture work (where is the ligament area), complete separation has occurred.
Visualization
Save VTK files with displacement and fields:
fe.utils.save_sol(
mesh=mesh, sol_file='fracture3d.vtu',
point_infos=[("displacement", u), ("delta_max", d_max_full[:, None])]
)
Summary
Key concepts:
- Energy-based formulation — define as a scalar function; stress and tangent stiffness are derived automatically via JAX AD
get_energy_density()— returns for the bulk;create_energy_fn()integrates it over the domainCohesiveInterface— encodes interface geometry (node pairs, normals, weights) and creates the cohesive energy function- Matrix-free Newton — JVP-based tangent operator avoids sparse matrix assembly; inner CG solver uses only matvec
- Irreversibility — state variable tracks maximum opening; updated outside the solver between load steps
Why matrix-free?
- Cohesive contributions couple arbitrary node pairs — hard to fit into standard FE sparse assembly
- The energy formulation naturally combines bulk and interface terms as pure JAX functions
- JVP cost ≈ 1 residual evaluation, regardless of problem size or sparsity pattern
Further Reading
examples/advance/cohesive_fracture_2d.py— 2D version with QUAD4 elementsexamples/advance/cohesive_fracture_3d.py— Complete 3D working example- API: feax.mechanics.cohesive — Cohesive zone models
- API: feax.solvers.matrix_free — Matrix-free Newton solver