where \(\Psi_\text{elastic}\) is the elastic energy, \(\Psi_\text{external}\) is the work done by the external forces, and \(\Psi_\text{fracture}\) is the fracture energy that is dissipated in creating new fracture surfaces.
In cohesive zone modelling, we estimate the fracture energy is given by:
where \(\psi(\delta)\) is the work done per unit area in separating the two surfaces of the cohesive zone by an amount \(\delta\). In this exercise, we will make use of different Traction-Separation Laws (TSL) to understand how different TSLs affect the response of the cohesive element.
Lets start with importing the necessary libraries and modules.
Code: Importing the essential libraries
import jaxjax.config.update("jax_enable_x64", True) # use double-precisionjax.config.update("jax_persistent_cache_min_compile_time_secs", 0)jax.config.update("jax_platforms", "cpu")import jax.numpy as jnpfrom jax import Arrayfrom tatva import Mesh, Operator, elementfrom tatva.plotting import STYLE_PATH, colors, plot_nodal_valuesfrom jax_autovmap import autovmapimport numpy as npfrom typing import NamedTupleimport matplotlib.pyplot as plt
Model Setup
For understanding the cohesive zone approach, we will use a simple setting where fracture happens along a known plane. The setup comprises of two blocks of the same material. The fracture plane is the common interface between the two blocks. We use cohesive elements along this interface/plane to model the fracture or separation of the two blocks.
We discretize the two blocks using triangular elements. To keep things simple, we discretize each block into 2 elements. The two blocks are connected by cohesive element along the interface. Actually, the term cohesive element here is a misnomer, as we do not have any actual finite element along the interface. Rather, we use constraint technique to enforce how the two interfaces should separate. Since the constraints are applied to the displacements of nodes of the two interfaces of the blocks, we can think of the cohesive element as a 2D element with 4 nodes.
Note
For visualization purposes, we have separated the two 1D elements by a small \(\epsilon\). Ideally, the gap should be zero or extremely small (\(\approx 10^{-8}\) or \(10^{-10}\)).
Figure 16.1: Code: Function to plot two blocks with fracture interface modelled using cohesive approach.
Defining material parameters
Let us define the material parameters mainly the shear modulus and the first Lamé parameter. The Youngs modulus and the Poisson’s ratio are related for the material are:
\[
E = 100.0 ~\text{N/m}^2, \quad \nu = 0.35
\]
From the above, we can compute the shear modulus and the first Lamé parameter as:
In order to define the fracture energy, we need two things:
the traction-separation potential \(\psi(\delta(\![\boldsymbol{u}]\!]))\)
a way to integrate the traction-separation potential over the cohesive zone \(\Gamma_\text{coh}\)
Defining the operator for line integration
We will begin by defining the operator for line integration similar to Operator that we have used for the triangular elements. To do so, we will have to first define the line elements along the potential fracture surface. We will use the Line2 element to define the line elements. This element has 2 nodes. Below, we use the function extract_interface_mesh to generate the mesh that consists of the line elements along the potential fracture surface. Since are two blocks have two surfaces along which the fracture can propagate (upper and lower), we can use any one of the two surfaces to define the cohesive zone. Here, we use the lower surface. We will define a new mesh that contains the line elements from one of the two interfaces. Since the two interfaces are discretized using the same number of elements, and occupy the same spatial domain, we can use any one of the two to perform the integration.
def extract_interface_mesh(mesh, line_elements):"""Generate a mesh for the interface between two materials."""# --- Interface mesh --- line_element_nodes = jnp.unique(line_elements.flatten()) interface_coords = mesh.coords[line_element_nodes] interface_elements = jnp.array([[index, index +1] for index inrange(len(line_elements))]) interface_mesh = Mesh(interface_coords, interface_elements)return interface_mesh
We will use the line_op to integrate the traction-separation potential \(\psi(\delta)\) over the cohesive zone \(\Gamma_\text{coh}\).
Defining the Traction-Separation potential
In order to define the traction-separation potential, we need
the critical stress \(\sigma_c\)
the fracture energy \(\Gamma\)
the penalty parameter \(\kappa\)
and a functional form of the traction-separation potential \(\psi(\delta)\)
Gamma =5e-1# J/m^2sigma_c =1.# N/m^2penalty =1e8
Below define the functional form of the traction-separation potential \(\psi(\delta)\) for an exponential traction-separation law. The \(\psi(\delta)\) for an exponential traction-separation law is given by:
where \(\delta_c = \frac{\Gamma e^{-1}}{\sigma_c}\) is the critical separation, \(\Gamma\) is the fracture energy, \(\sigma_c\) is the critical stress, and \(e\) is the base of the natural logarithm.
Note
If you notice, the potential is also defined for \(\delta < 0\). When \(\delta < 0\), it means that the two surfaces are actually penetrating each other. In this case, we use a penalty like term to penalize the interpenetration. This is exactly similar to the penalty method used in the contact mechanics.
Remember at this point we do not introduce the irreversibility in the cohesive potential. This will be done in the next lectures.
In order to compute the \(\psi(\delta)\) for an exponential traction-separation law we need to perform the following steps:
Compute the opening \(\delta\) along the cohesive interface is defined as:
where \(\boldsymbol{t}\) is the tangential direction (which for this problem is the \(x\)-direction) and \(\boldsymbol{n}\) is the normal direction to the cohesive interface (which for this problem is the \(y\)-direction).
Compute the cohesive energy density \(\psi(\delta)\) using the exponential traction-separation potential.
The function to compute the opening of the cohesive element will look like this:
@autovmap(jump=1)def compute_opening(jump: jnp.ndarray) ->float:""" Compute the opening of the cohesive element. Args: jump: The jump in the displacement field, which is the difference between the displacement of the upper and lower bodies across the crack. Returns: The opening of the cohesive element. """ opening = ... # TODO: Compute the opening of the cohesive elementreturn opening
The function to compute the cohesive energy density will look like this:
@autovmap(jump=1)def exponential_cohesive_energy( jump: jnp.ndarray, Gamma: float, sigma_c: float, penalty: float, delta_threshold: float=0.0,) ->float:""" Compute the cohesive energy for a given jump. Args: jump: The jump in the displacement field. Gamma: Fracture energy of the material. sigma_c: The critical strength of the material. penalty: The penalty parameter for penalizing the interpenetration. """ delta = ... # TODO: Compute the opening of the cohesive element ... # TODO: check if delta is greater than the threshold ... # TODO: if delta is greater than the threshold, compute the cohesive energy density using the exponential traction-separation potential ... # TODO: if delta is less than the threshold, return 0.5 * penalty * delta**2return cohesive_energy_density
In order to check if the delta is greater than the threshold, use the jax.lax.cond function.
Moreover, use the below defined safe_sqrt function to compute the square root of the delta. We redefine the square root function because default square root function is non-differentiable at zero because of the limit definition of the square root. Here we use the jnp.where function to return zero when the input is less than or equal to zero. This makes the square root function differentiable at zero.
@jax.jitdef safe_sqrt(x):return jnp.sqrt(jnp.where(x >0., x, 0.))@autovmap(jump=1)def compute_opening(jump: jnp.ndarray) ->float:""" Compute the opening of the cohesive element. Args: jump: The jump in the displacement field. Returns: The opening of the cohesive element. """ opening = safe_sqrt(jump[0] **2+ jump[1] **2)return opening@autovmap(jump=1)def exponential_cohesive_energy( jump: jnp.ndarray, Gamma: float, sigma_c: float, penalty: float, delta_threshold: float=0.0,) ->float:""" Compute the cohesive energy for a given jump. Args: jump: The jump in the displacement field. Gamma: Fracture energy of the material. sigma_c: The critical strength of the material. penalty: The penalty parameter for penalizing the interpenetration. delta_threshold: The threshold for the delta parameter. Returns: The cohesive energy. """ delta = compute_opening(jump) delta_c = (Gamma * jnp.exp(-1)) / sigma_cdef true_fun(delta):return Gamma * (1- (1+ (delta / delta_c)) * (jnp.exp(-delta / delta_c)))def false_fun(delta):return0.5* penalty * delta**2return jax.lax.cond(delta > delta_threshold, true_fun, false_fun, delta)cohesive_traction = jax.jacrev(exponential_cohesive_energy)cohesive_stiffness = jax.jacfwd(cohesive_traction)
Now we can use the line_op to integrate the traction-separation potential \(\psi(\delta)\) over the cohesive zone \(\Gamma_\text{coh}\).
The function to compute the cohesive energy will look like this:
def total_fracture_energy(u_flat: Array, upper_cohesive_nodes: Array, lower_cohesive_nodes: Array) ->float:""" Compute the total fracture energy of the system. Args: u_flat: The flat displacement vector upper_cohesive_nodes: The nodes associated with the upper cohesive surface. lower_cohesive_nodes: The nodes associated with the lower cohesive surface. Returns: The total cohesive energy of the system. """# TODO: reshape the flat displacement vector to the displacement field u = ... # TODO: compute the jump in the displacement field between the upper and lower bodies across the crack, # make use of the `upper_cohesive_nodes` and `lower_cohesive_nodes` jump = ... # TODO: evaluate the jump at the quad points using the `line_op.eval` function jump_quad = ... # TODO: compute the cohesive energy density using the `exponential_cohesive_energy` function cohesive_energy_density = ... # TODO: integrate the cohesive energy density over the cohesive zone using the `line_op.integrate` function cohesive_energy = ... return cohesive_energy
How is cohesive approach similar to Penalty constrained approach?
As we mentioned in the last chapter, the cohesive approach is similar to the penalty constrained approach. This is because we are tying two surfaces together using a penalty like parameter. The only difference is that in the cohesive approach, we are using a traction-separation potential to tie the two surfaces together, while in the penalty constrained approach, we are using a penalty function.
We can actually see that the cohesive approach is similar to the penalty constrained approach by plotting the stiffness matrix of the above problem. Below we plot the stiffness matrix of the cohesive approach for the case when we do not consider the cohesive zone and when we consider the cohesive zone. As we can see the stiffness matrix without the cohesive zone is a block diagonal matrix with the two blocks corresponding to the upper and lower blocks.
The stiffness matrix with the cohesive zone is a block diagonal matrix with the two blocks corresponding to the upper and lower blocks and the cohesive zone and the two blocks are connected by the cohesive zone.
Stiffness matrix of the cohesive approach. The stiffness matrix on the left is when we do not consider the cohesive zone, while the stiffness matrix on the right is when we consider the cohesive zone.
Let us now break the two blocks
We now break the two blocks along the interface. To this end, we apply a displacement to the upper block along the interface. We will apply a Mode I loading to the upper block.
Now, we define a few functions to implement the matrix-free solvers. We will use the conjugate gradient method to solve the linear system of equations. The Dirichlet boundary conditions are applied using projection method. As we use the matrix-free solver, we need to define the gradient of the total potential energy, which is also the residual function.
where \(\boldsymbol{f}_{ext}\) is the external force vector and \(\boldsymbol{f}_{int}\) is the internal force vector given by the gradient of the total potential energy.
Below, we define the functions to compute the gradient and the Jacobian-vector product (JVP). Within the Jacoboian-Vector product, we apply Projection approach by first “zeroing” out the displacements and then “zeroing” out the residual at the fixed DOFs. See Matrix-free solvers and In-class activity: Node-to-Surface Contact for more details on how to compute the Jacobian-vector product using jax.jvp.
Hint
Below define the function to compute the \(\boldsymbol{f}_{int}\) using jax.jacrev and then use this function to compute the Jacobian-vector product using jax.jvp. Remember to apply the Projection approach to enforce the Dirichlet boundary conditions.
# creating functions to compute the gradient andgradient = jax.jacrev(total_energy)# create a function to compute the JVP product@jax.jitdef compute_tangent(du, u_prev): du_projected = du.at[fixed_dofs].set(0) tangent = jax.jvp(gradient, (u_prev,), (du_projected,))[1] tangent = tangent.at[fixed_dofs].set(0)return tangent
Below we define the Conjugate-gradient method and the Newton-Rahpson method to solve the nonlinear problem of contact. See Matrix-free solvers and In-class activity: Node-to-Surface Contact for more details on how to implement Conjugate-gradient and Newton-Raphson methods for matrix-free solvers.
Code: Functions to implement the conjugate gradient method and Netwon-Raphson method
from functools import partialimport equinox as eqx#@partial(jax.jit, static_argnames=["A", "atol", "max_iter"])@eqx.filter_jitdef conjugate_gradient(A, b, atol=1e-8, max_iter=100): iiter =0def body_fun(state): b, p, r, rsold, x, iiter = state Ap = A(p) alpha = rsold / jnp.vdot(p, Ap) x = x + jnp.dot(alpha, p) r = r - jnp.dot(alpha, Ap) rsnew = jnp.vdot(r, r) p = r + (rsnew / rsold) * p rsold = rsnew iiter = iiter +1return (b, p, r, rsold, x, iiter)def cond_fun(state): b, p, r, rsold, x, iiter = statereturn jnp.logical_and(jnp.sqrt(rsold) > atol, iiter < max_iter) x = jnp.full_like(b, fill_value=0.0) r = b - A(x) p = r rsold = jnp.vdot(r, p) b, p, r, rsold, x, iiter = jax.lax.while_loop( cond_fun, body_fun, (b, p, r, rsold, x, iiter) )return x, iiterdef newton_krylov_solver( u, fext, gradient, fixed_dofs,): fint = gradient(u) du = jnp.zeros_like(u) iiter =0 norm_res =1.0 tol =1e-8 max_iter =10while norm_res > tol and iiter < max_iter: residual = fext - fint residual = residual.at[fixed_dofs].set(0) A = eqx.Partial(compute_tangent, u_prev=u) du, cg_iiter = conjugate_gradient(A=A, b=residual, atol=1e-8, max_iter=100) u = u.at[:].add(du) fint = gradient(u) residual = fext - fint residual = residual.at[fixed_dofs].set(0) norm_res = jnp.linalg.norm(residual)print(f" Residual: {norm_res:.2e}") iiter +=1return u, norm_res
Implementing the Incremental Loading Loop
Now that we have the mesh, the energy functions, and the newton_krylov_solver, it’s time to set up the main simulation loop. We will apply the displacement in small increments and solve for equilibrium at each step.
We divide the total displacement into 5 steps and solve the system of equations for each step.
Within each step, we will also store the applied displacement on the top surface and the reaction traction on the top surface. Later we will use this data to plot the traction-displacement curve.
Hint
The loop will look like this:
for step inrange(n_steps):print(f"Step {step+1}/{n_steps}")# ... Your code for each step will go inside this loop ...
Snapshots of the deformed body at different stages of loading showing the decohesion process. The figure on the right shows how the traction on the top surface changes with the displacement on the top surface.