In this exercise, we solve a linear elastic problem. To this end, we define a 2D square domain which is fixed in space (both in \(x\) and \(y\) directions) on the left edge. On the right edge, we apply a displacement in \(x\) direction.
Objective:
Define a mesh for a unit square with linear triangular elements.
Define material property (here we use a linear elastic material with first Lame parameter \(\lambda=1.0\) and shear modulus \(\mu=0.5\)).
Define a function to compute the total internal energy of the system.
Use Operator.grad to compute the gradient of the displacement field for computing strain tensor.
Use Operator.integrate to integrate the strain energy density over the domain.
Compute the internal force vector and the stiffness matrix using automatic differentiation.
Use jax.jacrev to compute the internal force vector from the total energy. \[
\boldsymbol{f}_{\text{int}} = \frac{\partial \Psi_{\text{int}}}{\partial \boldsymbol{u}}
\]
Use jax.jacfwd to compute the stiffness matrix from the internal force vector. \[
\mathbf{K} = \frac{\partial \boldsymbol{f}_{\text{int}}}{ \partial \boldsymbol{u}}
\]
Solve the system of equations to get the displacement field. \[
\boldsymbol{u} = \mathbf{K}^{-1} ( \boldsymbol{f}_{\text{ext}} - \boldsymbol{f}_{\text{int}})
\]
Plot the von-mises stress and the displacement field.
Lets start with necessary modules and libraries we will use to solve the problem.
First, we start with creating a mesh for a unit square. To this end, we use the Mesh.unit_square function which creates a mesh for a unit square with a given number of elements in the \(x\) and \(y\) directions.
where \(\psi(x)\) is the linear elastic energy density which is a function of the stress and strain tensors. The linear elastic energy density can be written as,
where \(\boldsymbol{\sigma}\) is the stress tensor and \(\boldsymbol{\varepsilon}\) is the strain tensor.
Since we assume a linear elastic material, the stress can be expressed as a function of the strain.
\[
\boldsymbol{\sigma} = \lambda \text{tr}(\boldsymbol{\varepsilon}) \mathbf{I} + 2\mu \boldsymbol{\varepsilon}
\] where \(\lambda\) and \(\mu\) are the Lamé parameters which are material properties. Assuming that the displacement field is small, the strain can be approximated as,
To ease the handling of the material paramters and later on ease the integration of the material parameters for computing energy density, we define a class Material that can be used to define the material parameters.
from typing import NamedTupleclass Material(NamedTuple):"""Material properties for the elasticity operator.""" mu: float# Shear modulus lmbda: float# First Lamé parametermat = Material(mu=0.5, lmbda=1.0)
Now, we define three functions to compute the strain, stress, and strain energy.
@autovmap(grad_u=2)def compute_strain(grad_u: Array) -> Array:"""Compute the strain tensor from the gradient of the displacement."""return0.5* (grad_u + grad_u.T)@autovmap(eps=2, mu=0, lmbda=0)def compute_stress(eps: Array, mu: float, lmbda: float) -> Array:"""Compute the stress tensor from the strain tensor.""" I = jnp.eye(2)return2* mu * eps + lmbda * jnp.trace(eps) * I@autovmap(grad_u=2, mu=0, lmbda=0)def strain_energy(grad_u: Array, mu: float, lmbda: float) -> Array:"""Compute the strain energy density.""" eps = compute_strain(grad_u) sig = compute_stress(eps, mu, lmbda)return0.5* jnp.einsum("ij,ij->", sig, eps)
Computing the total internal energy
In order to compute the total internal energy, we need to perform the following operations:
Compute the strain tensor from the displacements
Integrate the strain energy density over the domain
In tatva, we define a operator class that is capable of performing various mathematical operations on the mesh. To perform these operations, the operator class requires the following information:
The mesh (nodes and elements)
The element type (e.g. triangle)
Below, we define such an operator that can perform the above operations over triangular elements.
tri = element.Tri3()op = Operator(mesh, tri)
Now we can define a function that takes the nodal displacements and uses the above defined op to compute the gradient of the displacement field (using op.grad), compute the energy density based on the function strain_energy defined and finally integrate the energy density over the domain to get the total internal energy (using op.integrate). Please see the Appendix A — Basics of tatva for more details on the Operator class.
Computing the internal force vector and the stiffness matrix
We can use the JAX library to compute the gradient and Hessian of the total energy (see Appendix B — Basics of JAX for more details). The gradient is the internal force vector and the Hessian is the stiffness matrix. Therefore,
Now we locate the nodes that are associated with the Dirichlet boundary conditions. We will need the nodes to get the corresponding degrees of freedom. Since in this problem we prescribe the displacement on the left and right edges, we find the nodes at these two edges.
Taking initial guess for the displacement field as the prescribed values, i.e.\(\boldsymbol{u}^{0} + d\boldsymbol{u}\), we can write the above equation as,
where \(\boldsymbol{r}\) is the residual vector, \(\frac{\partial \boldsymbol{f}_{\text{int}}}{ \partial \boldsymbol{u}}\big |_{u=u^{0}}\) is the stiffness matrix (\(\mathbf{K}\)) evaluated at the initial guess and \(\Delta\boldsymbol{u}\) is the unknown displacement vector. We need to solve the above equation such that the \(\Delta\boldsymbol{u}\) at fixed degrees of freedom is zero. This is because we know the values of the displacement at the fixed degrees of freedom and that is already included in the initial guess. So, the linear system of equations is,
such that \(\Delta\boldsymbol{u}|_\text{fixed\_dofs} = \boldsymbol{0}\).
Lifting approach
Note
We apply the condition \(\Delta\boldsymbol{u}|_\text{fixed\_dofs} = \boldsymbol{0}\) using matrix lifting approach.
We arrange the system such that the unknown vector \(\Delta \boldsymbol{u}\) is flattened. Once we find the solution, we will reshape it back to the original shape.
create the unknown vector \(\Delta \boldsymbol{u}\) with the shape of the number of nodes times the number of degrees of freedom per node
create the system matrix \(\mathbf{K}\) and the right-hand side vector \(\boldsymbol{r}\).
solve the system \(\mathbf{K} \Delta \boldsymbol{u} = \boldsymbol{r}\) for \(\Delta \boldsymbol{u}\)
In the lifting approach (or partitioning approach), we separate the unknown vector \(\boldsymbol{u}\) into two parts:
\(\Delta \boldsymbol{u}_f\) is the vector of unknown displacements at the free nodes
\(\Delta \boldsymbol{u}_c\) is the vector of known displacements at the constrained nodes
As a result, the system matrix \(\mathbf{K}\) is partitioned into four blocks:
where \(\mathbf{K}_{ff}\) is the stiffness matrix of the free nodes, \(\mathbf{K}_{fc}\) is the stiffness matrix of the free nodes to the constrained nodes, \(\mathbf{K}_{cf}\) is the stiffness matrix of the constrained nodes to the free nodes, and \(\mathbf{K}_{cc}\) is the stiffness matrix of the constrained nodes.
where \(\boldsymbol{r}_f\) is the residual vector of the free nodes, and \(\boldsymbol{r}_c\) is the residual vector of the constrained nodes.
In order to have \(\boldsymbol{u}_c\) to be equal to the prescribed values \(\boldsymbol{u}^{0}\), we need to replace \(\mathbf{K}_{cc}\) with \(\mathbf{I}\), \(\mathbf{K}_{cf}\) with \(\mathbf{0}\), and \(\boldsymbol{r}_c\) with the prescribed values. This gives us the following system of equations:
In the lifting approach, we make all the columns and rows related to Dirichlet boundary conditions to be identity row/column. This converts the system of equations into a block diagonal system of equations.
Alternatively, we could have also solved only the subproblem for the free degrees of freedom i.e.\(\mathbf{K}_{ff} \Delta \boldsymbol{u}_f = \boldsymbol{r}_f\).
# define the initial guess for the displacement field such that the fixed degrees of freedom are set to the prescribed valuesu0 = jnp.zeros((n_dofs))u0 = u0.at[fixed_dofs].set(prescribed_values.at[fixed_dofs].get())# compute the hessian which is the stiffness matrix and the gradient which is the internal force vectorf_int = gradient(u0)K = hessian(u0)f_ext = jnp.zeros(n_dofs)
Figure 3.2: Stiffness matrix structure for original and lifted system. Notice the absence of values at the left top corner which corresponds to the fixed degrees of freedom.
We also lift the residual vector to account for the zero displacement conditions at fixed degrees of freedom.
r = f_ext - f_intr_lifted = r.at[fixed_dofs].set(0.0)
Solving the problem using a direct linear solver.
du = jnp.linalg.solve(K_lifted, r_lifted)u_solution = u0.at[:].add(du)u_solution = u_solution.reshape(n_nodes, n_dofs_per_node)
Post-processing
Now we plot the von-mises stress on the deformed mesh. The von-mises stress is computed as