In this exercise, we will see an application of constraint handling in FEM and we will apply Dirichlet boundary conditions as constraints. The problem is the same as in the previous exercises.
We have a unit square domain \(\Omega = [0, 1] \times [0, 1]\) which fixed in space in both \(x\) and \(y\) directions on the left edge. We apply a displacement along \(x-\)direction on the right edge. The applied displacement is \(0.3\). Furthermore, the domain is discretized using triangular elements.
The assumptions that we make in this exercise are:
linear elastic material
infinitesimal strains under plain strain conditions
In this exercise, we explore the constrained problem with:
equality constraint i.e\(g_i(u) = 0\)
active set is known and does not change i.e we always know which degrees of freedom are in \(\mathcal{A}\)
The objective of this exercise is to use the penalty method to enforce the Dirichlet boundary conditions. We will use the penalty method to enforce the Dirichlet boundary conditions on the right edge as well as the left edge of the domain. Later, we will see how the value of the penalty parameter \(k_\text{pen}\) affects the solution.
Below is the expected workflow for solving this exercise.
Workflow
As we have mentioned, our way of solving any problem in this course is to define a python function that computes the total potential energy \(\Psi\).
For this specific problem, the total potential energy \(\Psi\) is given as
\(\Psi_\text{pen}\) is the penalty energy due to the Dirichlet boundary conditions
\(\boldsymbol{f}_\text{ext}\) is the external force vector which is zero in this case
Our task is to define the functions to compute the elastic strain energy, the penalty energy and the total potential energy.
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, element, sparsefrom tatva.plotting import STYLE_PATH, plot_element_values, plot_nodal_valuesfrom jax_autovmap import autovmapfrom typing import NamedTuplefrom functools import partialimport numpy as npimport matplotlib.pyplot as plt
Model setup
Similar to previous exercises, we consider a unit square domain \(\Omega = [0, 1] \times [0, 1]\) which fixed in space in both \(x\) and \(y\) directions on the right edge. We apply a displacement along \(x-\)direction on the left edge. Furthermore, the domain is discretized using triangular elements.
Let us define the material parameters mainly the shear modulus and the first Lamé parameter.
\[
\mu = 0.5, \quad \lambda = 1.0
\]
class Material(NamedTuple):"""Material properties for the elasticity operator.""" mu: float# Shear modulus lmbda: float# First Lamé parametermat = Material(mu=0.5, lmbda=1.0)
Defining the total elastic energy
We would now define a function that computes the total energy of the system.
Define the Operator object from tatva.operator which takes the mesh and the element as input.
Define the function that computes the total energy by performing following operations:
Compute the gradient of the displacement, uses Operator.grad.
Compute the strain energy density, use the function defined above
Integrate the strain energy density over the domain, use Operator.integrate.
Code: Defining the material properties and the strain energy density
tri = element.Tri3()op = Operator(mesh, tri)@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, mat.mu, mat.lmbda)return0.5* jnp.einsum("ij,ij->", sig, eps)@jax.jitdef total_elastic_energy(u_flat: Array) ->float:"""Compute the total energy of the system.""" u = u_flat.reshape(-1, n_dofs_per_node) u_grad = op.grad(u) energy_density = strain_energy(u_grad, mat.mu, mat.lmbda)return op.integrate(energy_density)
Defining Dirichlet boundary conditions as constraints
We now define the degrees of freedom that for the left edge of the domain and the right edge of the domain. These are the degrees of freedom that are associated with the nodes on the left and right edge of the domain.
Remember that on the left edge, we have both \(x\) and \(y\) degrees of freedom fixed (\(u_x=u_y=0\)) and on the right edge, we have apply a displacement of 0.3 on \(x\) degree of freedom (\(u_x=0.3\)).
Hint
It might be best to store all the degrees of freedom where displacement is applied (either 0 or 0.3) as vector named constrained_dofs.
We now define a constraints that enforces the Dirichlet boundary conditions on the degrees of freedom associated with the nodes on the right edge of the domain. Let us denote the applied displacement as \(u_d\). The constraint then is defined as
\[
u_x^{i} = u_d \quad \forall i \in \mathcal{A}
\]
where \(\mathcal{A}\) is the set of degrees of freedom associated with the nodes on the right edge and the left edge of the domain.
We can define the constraint as a function of the degrees of freedom as follows:
\[
g_i(u) = u_{x}^{i} - u_d
\]
Below define a function that computes the equality constraints as defined above.
Hint
The function should take the displacement vector \(u\) and the constraints as input and return the difference between the displacement vector and the constraints. The difference should be computed only for the degrees of freedom that are associated with the nodes on the left and right edge of the domain.
The function definition will be something like this:
def equality_constraints(u, constraints):"""Compute the constraint vector g(u) Args: u: the displacement vector, a vector of size (n_dofs + nb_cons) constraints: the applied displacement at the constrained_dofs, a vector of size (nb_cons) Returns: the constraint vector, a vector of size (nb_cons) """# get the displacement values at constrained_dofs from u ...# compute the g(u) i.e. difference between the above displacement values and the applied displacement ...# return the constraint vectorreturn ...
where \(\Psi_\text{e}\) is the elastic strain energy and \(\Psi_\text{pen}\) is the penalty energy. We have already defined \(\Psi_\text{e}\) in the previous section. We now define \(\Psi_\text{pen}\) as
For starting we choose a small value of \(k_\text{pen}=10^{-1}\) to see if the constraints are satisfied.
Later, we will modify this value to see the effect of the penalty parameter on the solution.
k_pen =1e-1
Write a function that computes the penalty energy term as defined above (using the k_pen) and the total potential energy as defined above.
Hint
The function definition will be something like this:
def penalty_energy(u, constraints):"""Compute the penalty energy term Args: u: the displacement vector, a vector of size (n_dofs) constraints: the applied displacement at the constrained_dofs, a vector of size (nb_cons) Returns: the penalty energy term, a scalar """# compute the constraint vector from u and constraints using the equality_constraints function ...# compute the penalty energy term ...# return the penalty energy termreturn ...def total_potential_energy(u, constraints):"""Compute the total potential energy Args: u: the displacement vector, a vector of size (n_dofs) constraints: the applied displacement at the constrained_dofs, a vector of size (nb_cons) Returns: the total potential energy, a scalar """# compute the elastic energy using the total_elastic_energy function ...# compute the penalty energy term using the penalty_energy function ...# compute the total potential energy ...# return the total potential energyreturn ...
Use JAX to differentiate the above defined total potential energy function to compute the gradient and the Hessian. Be careful with the use of jax.jacrev and jax.jacfwd. Also, we will use the sparse stiffness matrix and therefore, you will need to create the sparsity pattern first.
Use jax.jacrev to compute the gradient
Create the sparsity pattern using sparse.create_sparsity_pattern
Below define a newton solver that uses a sparse linear solver. We use the sparse solver from scipy.sparse.linalg.spsolve. Note that we need to convert the sparse matrix to a CSR matrix before passing it to the sparse solver.
Hint
We no longer need lifting approach to enforce the boundary conditions. This is taken care by the constraint enforcement through penalty approach. So if you are using the newton_sparse_solver from Week 2, remove the lifting of the stiffness matrix and the residual vector.
import scipy.sparse as spdef newton_sparse_solver(z, gradient, hessian, linear_solver=sp.linalg.spsolve):""" Newton solver with sparse linear solver. """ iiter =0 norm_res =1.0 tol =1e-8 max_iter =10while norm_res > tol and iiter < max_iter: residual =-gradient(z) K_sparse = hessian(z) K_csr = sp.csr_matrix( (K_sparse.data, (K_sparse.indices[:, 0], K_sparse.indices[:, 1])) ) dz = linear_solver(K_csr, residual) z = z.at[:].add(dz) residual =-gradient(z) norm_res = jnp.linalg.norm(residual)print(f" Residual: {norm_res:.2e}") iiter +=1return z, norm_res
Now we will use the above defined newton solver to solve the problem. We will solve the problem for 10 steps.
We now check if the \(k_\text{pen}\) used is good enough to enforce the constraints. We do this by checking the value of the displacement at the right edge of the domain and the left edge of the domain.
The displacement (in \(x-\)direction) at the right edge of the domain should be \(0.3\)i.e.
\[
u_x = 0.3
\]
and the displacement at the left edge of the domain should be \(0\)i.e.
\[
u_x = 0
\]
Please implement the above checks and print the values of the displacements at the right and left edges of the domain and see how close they are to the exact values.
What does \(k_\text{pen}\) represent as a physical quantity? What is its unit?
What is the effect of \(k_\text{pen}\) on the solution?
What happens when you increase \(k_\text{pen}\) from a small value like \(10^{-1}\) to a very large value like \(10^{12}\)? Do we still get a good solution? Are the applied displacements at the right edges (0.3) and the left edges (0) exact? Pay attention to the residual norm.