Linear Elasticity Fracture Mechanics#

Authors:

This notebook serves as a tutorial for linear elastic fracture mechanics

import sys
sys.path.append("../utils")

# Import required libraries
import matplotlib.pyplot as plt
import numpy as np

import dolfinx.fem as fem
import dolfinx.mesh as mesh
import dolfinx.io as io
import dolfinx.plot as plot
import ufl

from mpi4py import MPI
from petsc4py import PETSc
from petsc4py.PETSc import ScalarType


plt.rcParams["figure.figsize"] = (6,3)

outdir = "output"
from pathlib import Path
Path(outdir).mkdir(parents=True, exist_ok=True)

Asymptotic field and SIF (\(K_I\))#

Let us first get the elastic solution for a given crack length

from elastic_solver import solve_elasticity

Lx = 1.
Ly = 0.5
Lcrack = 0.3
lc =.05
dist_min = .1
dist_max = .3
uh, energy, sigma_ufl = solve_elasticity(Lx=Lx,
                                         Ly=Ly,
                                         Lcrack=Lcrack,
                                         lc=lc,
                                         refinement_ratio=20,
                                         dist_min=dist_min,
                                         dist_max=dist_max,
                                         verbosity=1)

from plots import warp_plot_2d
import pyvista
pyvista.set_jupyter_backend("static")
pyvista.start_xvfb()

import ufl
sigma_iso = 1./3*ufl.tr(sigma_ufl)*ufl.Identity(len(uh))
sigma_dev =  sigma_ufl - sigma_iso
von_Mises = ufl.sqrt(3./2*ufl.inner(sigma_dev, sigma_dev))
V_dg = fem.FunctionSpace(uh.function_space.mesh, ("DG", 0))
stress_expr = fem.Expression(von_Mises, V_dg.element.interpolation_points())
vm_stress = fem.Function(V_dg)
vm_stress.interpolate(stress_expr)

plotter = warp_plot_2d(uh,cell_field=vm_stress,field_name="Von Mises stress", factor=.1,show_edges=True,clim=[0.0, 1.0],show_scalar_bar=True)
if not pyvista.OFF_SCREEN:
    plotter.show()
else:
    figure = plotter.screenshot(f"{outdir}/VonMises.png")
The potential energy for Lcrack=3.000e-01 is -4.114e-01
../../_images/e41fcf4c5d1786081f05fd9d00222351e5300f1635f0084f060d72c1f5d1025c.png

Crack opening displacement (COD)#

Let us get the vertical displacement at the crack lip

from evaluate_on_points import evaluate_on_points
xs = np.linspace(0,Lcrack,100)
ys = 0.0 * np.ones_like(xs)
zs = 0.0 * np.ones_like(xs)
points = np.array([xs,ys,zs])
u_values = evaluate_on_points(uh,points)
us = u_values[1][:,1]
plt.plot(xs,us,".")
plt.xlim([0.,Lcrack])
plt.xlabel("x - coordinate")
plt.ylabel(r"$u_y$")
plt.title("Crack opening displacement")
plt.savefig(f"{outdir}/COD.png")
../../_images/26e6674240b9ccad311d5fce7d2772793e582ffe7b8df94d045fb1cd191c5849.png

As detailed in the lectures notes, we can estimate the value of the stress intensity factor \(K_I\) by extrapolating \(u \sqrt{2\pi/ r}\)

r = (Lcrack-xs)
#r_ = r[np.where(xs<Lcrack)]
#us_ = us[np.where(xs<Lcrack)]

nu = 0.3
E = 1.0
mu = E / (2.0 * (1.0 + nu))
kappa = (3 - nu) / (1 + nu)
factor = 2 * mu / (kappa + 1)

plt.semilogx(r,us * np.sqrt(2*np.pi/r)*factor,".")
plt.xlabel("r")
plt.ylabel(r"${u_y} \,\frac{2\mu}{k+1} \,\sqrt{2\pi/r}$")
plt.title("Crack opening displacement")
plt.savefig(f"{outdir}/KI-COD.png")
WARNING:py.warnings:/tmp/ipykernel_1992/2985875175.py:11: RuntimeWarning: divide by zero encountered in divide
  plt.semilogx(r,us * np.sqrt(2*np.pi/r)*factor,".")
../../_images/800a69a4bc495b3e1d271baa7ef90e5513455f2cc0648f597224066a18e03870.png

We estimate \(K_I\simeq 1.8\pm0.1\)

Stress at the crack tip#

Let us get the stress around the crack tip

xs = np.linspace(Lcrack,2*Lcrack,1000)
ys = 0.0 * np.ones_like(xs)
zs = 0.0 * np.ones_like(xs)
points = np.array([xs,ys,zs])
r = (xs-Lcrack)
sigma_xx_expr = fem.Expression(sigma_ufl[0,0], V_dg.element.interpolation_points())
sigma_xx = fem.Function(V_dg)
sigma_xx.interpolate(sigma_xx_expr)
sigma_xx_values = evaluate_on_points(sigma_xx,points)[1]
plt.plot(r,sigma_xx_values[:,0],"o")
plt.xlabel("r")
plt.ylabel(r"$\sigma_{rr}$")
plt.title("Stress at the crack tip")
plt.savefig(f"{outdir}/stress.png")
../../_images/2570ae36ca2a3156869d22308359000cf6e89560a12c1e8a381de8b3ebaa99ec.png

As detailed in the lectures notes, we can estimate the value of the stress intensity factor \(K_I\) by extrapolating \(\sigma_{rr} \sqrt{2\pi r}\)

plt.semilogx(r,sigma_xx_values[:,0]*np.sqrt(2*np.pi*r),"o")
plt.xlabel("r")
plt.ylabel(r"$\sigma_{rr}*\sqrt{2\pi\,r}$")
plt.title("Stress at the crack tip")
plt.savefig(f"{outdir}/KI-stress.png")
../../_images/2ea50c71201abe637512c70da997a2a746e28c857183ff5a85fe8fec8312cb60.png

We can say that \(K_I\simeq 1.5\pm .5\) as from the COD, but this estimate is not precise and reliable.

From Irwin’s formula in plane-stress, we get the energy release rate (ERR)

KI_estimate = 1.8 
G_estimate = KI_estimate ** 2 / E # Irwin's formula in plane stress
print(f"ERR estimate is {G_estimate}")
ERR estimate is 3.24

The elastic energy release rate#

Naïf method: finite difference of the potential energy#

Let us first calculate the potential energy for several crack lengths. We multiply the result by `2`` to account for the symmetry when comparing with the \(K_I\) estimate above.

Ls = np.linspace(Lcrack*.7,Lcrack*1.3,10)
energies = np.zeros_like(Ls)
Gs = np.zeros_like(Ls)
for (i, L) in enumerate(Ls):
    uh, energies[i], _ = solve_elasticity(Lx=Lx,
                                          Ly=Ly
                                          ,Lcrack=L,
                                          lc=.05,
                                          refinement_ratio=10,
                                          dist_min=.1,
                                          dist_max=1.,
                                          verbosity=1)
    
energies = energies * 2
The potential energy for Lcrack=2.100e-01 is -3.138e-01
The potential energy for Lcrack=2.300e-01 is -3.305e-01
The potential energy for Lcrack=2.500e-01 is -3.501e-01
The potential energy for Lcrack=2.700e-01 is -3.731e-01
The potential energy for Lcrack=2.900e-01 is -3.998e-01
The potential energy for Lcrack=3.100e-01 is -4.305e-01
The potential energy for Lcrack=3.300e-01 is -4.660e-01
The potential energy for Lcrack=3.500e-01 is -5.061e-01
The potential energy for Lcrack=3.700e-01 is -5.521e-01
The potential energy for Lcrack=3.900e-01 is -6.035e-01

We can estimate the ERR by taking the finite-difference approximation of the derivative

ERR_naif = -np.diff(energies)/np.diff(Ls)

plt.figure()
plt.plot(Ls, energies,"*")
plt.xlabel("L_crack")
plt.ylabel("Potential energy")
plt.figure()
plt.plot(Ls[0:-1], ERR_naif,"o")
plt.ylabel("ERR")
plt.xlabel("L_crack")
#plt.axhline(G_estimate,linestyle='--',color="gray")
#plt.axvline(Lcrack,linestyle='--',color="gray") 
Text(0.5, 0, 'L_crack')
../../_images/ebd6eda63fde39c31bac3effb01e78124ec8e0bb5cf1509cdb260eb218acd883.png ../../_images/f303b80d26c4cdc3e28d06156f65ce3a085cae6ace4c2886a20bd4fe75f68650.png

G-theta method: domain derivative#

This function implement the G-theta method to compte the ERR as described in the lecture notes (see newfrac/CORE-school/newfrac-core-numerics/-/blob/master/Core_School_numerical_NOTES.pdf?ref_type=heads).

We first create by an auxiliary computation a suitable theta-field.

To this end, we solve an auxiliary problem for finding a \(\theta\)-field which is equal to \(1\) in a disk around the crack tip and vanishing on the boundary. This field defines the “direction” for the domain derivative, which should change the crack length, but not the outer boundary.

Here we determine the \(\theta\) field by solving the following problem

\[ \Delta \theta = 0\quad \text{for}\quad x\in\Omega, \quad \theta=1\quad \text{for} \quad x\in \mathrm{D}\equiv\{\Vert x-x_\mathrm{tip}\Vert<R_{\mathrm{int}}\}, \quad \theta=0\quad \text{for} \quad x\in\partial\Omega, \;\Vert x-x_\mathrm{tip}\Vert>R_{\mathrm{ext}} \]

This is implemented in the function below

def create_theta_field(domain,crack_tip,R_int,R_ext):
    
    def tip_distance(x):
          return np.sqrt((x[0]-crack_tip[0])**2 + (x[1]-crack_tip[1])**2) 
    
    V_theta = fem.FunctionSpace(domain,("Lagrange",1))
    
   
    # Define variational problem to define the theta-field. 
    # We solve a simple laplacian
    theta, theta_ = ufl.TrialFunction(V_theta), ufl.TestFunction(V_theta)
    a = ufl.dot(ufl.grad(theta), ufl.grad(theta_)) * ufl.dx
    L = fem.Constant(domain,ScalarType(0.)) * theta_ * ufl.dx(domain=domain) 

    # Set the BCs
    # Imposing 1 in the inner circle and zero in the outer circle
    dofs_inner = fem.locate_dofs_geometrical(V_theta,lambda x : tip_distance(x) < R_int)
    dofs_out = fem.locate_dofs_geometrical(V_theta,lambda x : tip_distance(x) > R_ext)
    bc_inner = fem.dirichletbc(ScalarType(1.),dofs_inner,V_theta)
    bc_out = fem.dirichletbc(ScalarType(0.),dofs_out,V_theta)
    bcs = [bc_out, bc_inner]

    # solve the problem
    problem = fem.petsc.LinearProblem(a, L, bcs=bcs, petsc_options={"ksp_type": "gmres", "pc_type": "gamg"})
    thetah = problem.solve()
    return thetah

crack_tip = np.array([Lcrack,0])
crack_tangent = np.array([1,0])
crack_tip = np.array([Lcrack,0])
R_int = Lcrack/4.
R_ext = Lcrack
domain = uh.function_space.mesh
thetah = create_theta_field(domain,crack_tip,R_int,R_ext)


# Plot theta
topology, cell_types, geometry = plot.create_vtk_mesh(thetah.function_space)
grid = pyvista.UnstructuredGrid(topology, cell_types, geometry)
grid.point_data["theta"] = thetah.x.array.real
grid.set_active_scalars("theta")
plotter = pyvista.Plotter()
plotter.add_mesh(grid, show_edges=False)
plotter.add_title("Theta-field")
plotter.view_xy()
if not pyvista.OFF_SCREEN:
    plotter.show()
../../_images/a2d8263ffbfca337a04d276c3ebf7d6bad09da201f3d1019a032592dc4c66ca2.png

From the scalar field, we define a vector field by multiplying by the tangent vector to the crack: t=[1,0]

Hence, we can compute the ERR with the formula $\( G = \int_\Omega \left(\sigma(\varepsilon(u))\cdot(\nabla u\nabla\theta)-\dfrac{1}{2}\sigma(\varepsilon(u))\cdot \varepsilon(u) \mathrm{div}(\theta)\,\right)\mathrm{dx}\)$

Lcrack = 0.1
Lx = 1.
Ly = 1.25
crack_tip = np.array([Lcrack,0])
crack_tangent = np.array([1,0])
crack_tip = np.array([Lcrack,0])
R_int = Lcrack/4
R_ext = Lcrack
uh, energy, sigma_ufl = solve_elasticity(Lx=Lx,Ly=Ly,Lcrack=Lcrack,lc=.02,refinement_ratio=30,dist_min=.1,dist_max=1.0)

thetah = create_theta_field(uh.function_space.mesh,crack_tip,R_int,R_ext)
eps_ufl = ufl.sym(ufl.grad(uh))
theta_vector = ufl.as_vector([1.,0.]) * thetah
dx = ufl.dx(domain=uh.function_space.mesh)
first_term = ufl.inner(sigma_ufl,ufl.grad(uh) * ufl.grad(theta_vector)) * dx
second_term = - 0.5 * ufl.inner(sigma_ufl,eps_ufl) * ufl.div(theta_vector) * dx

G_theta = 2 * fem.assemble_scalar(fem.form(first_term + second_term))
print(f'The ERR computed with the G-theta method is {G_theta:2.4f}' )
Info    : Meshing 1D...
Info    : [  0%] Meshing curve 1 (Line)
Info    : [ 20%] Meshing curve 2 (Line)
Info    : [ 40%] Meshing curve 3 (Line)
Info    : [ 60%] Meshing curve 4 (Line)
Info    : [ 80%] Meshing curve 5 (Line)
Info    : Done meshing 1D (Wall 0.0153336s, CPU 0.015812s)
Info    : Meshing 2D...
Info    : Meshing surface 1 (Plane, Frontal-Delaunay)
Info    : Done meshing 2D (Wall 2.42484s, CPU 2.40152s)
Info    : 75084 nodes 150171 elements
The potential energy for Lcrack=1.000e-01 is -6.350e-01
The ERR computed with the G-theta method is 0.3985