{ "cells": [ { "cell_type": "markdown", "id": "legitimate-texas", "metadata": {}, "source": [ "# Finite Elasticity Part I\n", "\n", "*Authors: Jack S. Hale (Univ. Luxembourg), Corrado Maurini (Sorbonnes Université)*\n", "\n", "Uses elements from https://jorgensd.github.io/dolfinx-tutorial/chapter2/hyperelasticity.html by Dokken and Wells under CC-BY 4.0\n", "\n", "## Summary\n", "\n", "In this notebook we will give an example of solving for the displacement field of a geometrically non-linear bar sagging under its own weight.\n", "\n", "## Motivation\n", "\n", "\"drawing\"\n", "\n", "The above image shows image shows the *deformed configuration* of an initially straight silicone beam with cylindrical cross-section under its own weight. This material is very soft and quite dense. The resulting rotations and strains are large, so the assumptions made in a geometrically linear elastic model are no longer valid. For sensible predictions we must use the theory of finite elasticity. Source: Mazier et al. https://arxiv.org/abs/2102.13455\n", "\n", "## Learning objectives\n", "\n", "1. Briefly revisit the equations of non-linear elasticity.\n", "1. Be able to express the Lagrangian functional of a geometrically non-linear elastic body in the Unified Form Language (UFL) of the FEniCS Project.\n", "2. Use the automatic differentiation capabilities to derive symbolic expressions for the residual and Jacobian.\n", "2. Understand and implement basic methods for solving non-linear problems that are available in DOLFINx.\n", "3. See the difference in results between a geometrically linear and non-linear analysis.\n", "4. Be aware of the possible effects and solutions to the problem of numerical volumetric locking.\n", "5. Derive a stress measure automatically and output stresses.\n", "\n", "## Possible extensions\n", "\n", "1. Change boundary conditions to vertical beam under compression.\n", "1. To a three-dimensional analysis.\n", "2. Displacement-pressure (mixed) formulation to cure numerical locking." ] }, { "cell_type": "markdown", "id": "electric-blocking", "metadata": {}, "source": [ "## Implementation\n", "\n", "We first import the various modules that we require. So that it is clear where each piece of functionality comes from, we use fully qualified names rather than bringing functions into the local namespace (e.g. `ufl.grad` vs `from ufl import grad; grad(u);`). You may \n", "\n", "In FEniCSx, in contrast with the old FEniCS, the use of `from dolfinx import *` or `from ufl import *` is strongly discouraged." ] }, { "cell_type": "code", "execution_count": null, "id": "extreme-thirty", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "\n", "from mpi4py import MPI\n", "from petsc4py import PETSc\n", "\n", "import dolfinx\n", "import dolfinx.io\n", "\n", "import ufl\n", "\n", "import sys\n", "sys.path.append(\"../python/\")\n", "from nonlinear_pde_problem import NonlinearPDEProblem" ] }, { "cell_type": "markdown", "id": "transsexual-graphic", "metadata": {}, "source": [ "We begin by defining a rectangular mesh and writing it out to an XDMF file.\n", "\n", "We will model a clamped beam deformed under its own weight in two dimensions. The beam is a rectangular with length $L$ and square cross section of height $H$." ] }, { "cell_type": "code", "execution_count": null, "id": "nuclear-grenada", "metadata": {}, "outputs": [], "source": [ "L = 1.0\n", "H = 0.05\n", "\n", "mesh = dolfinx.RectangleMesh(MPI.COMM_WORLD, [(0.0, 0.0, 0.0), (L, H, 0.0)], [100, 15])\n", "\n", "from pathlib import Path\n", "Path(\"output\").mkdir(parents=True, exist_ok=True)\n", "\n", "with dolfinx.io.XDMFFile(MPI.COMM_WORLD, \"output/mesh.xdmf\", \"w\") as f:\n", " f.write_mesh(mesh)\n", "\n", "V = dolfinx.VectorFunctionSpace(mesh, (\"CG\", 1))" ] }, { "cell_type": "markdown", "id": "editorial-latter", "metadata": {}, "source": [ "On the clamped end $x = 0$ we will prescribe Dirichlet boundary conditions $u = u_D = (0, 0)$. The rest of the boundary is traction free." ] }, { "cell_type": "code", "execution_count": null, "id": "documentary-resident", "metadata": {}, "outputs": [], "source": [ "def left(x):\n", " #print(x.shape)\n", " #print(x)\n", " is_close = np.isclose(x[0], 0.0)\n", " #print(is_close)\n", " return is_close\n", "\n", "left_facets = dolfinx.mesh.locate_entities_boundary(mesh, mesh.topology.dim - 1, left)\n", "left_dofs = dolfinx.fem.locate_dofs_topological(V, mesh.topology.dim - 1, left_facets)\n", "\n", "u_bc = dolfinx.Function(V)\n", "with u_bc.vector.localForm() as loc:\n", " loc.set(0.0)\n", " \n", "bcs = [dolfinx.DirichletBC(u_bc, left_dofs)]" ] }, { "cell_type": "markdown", "id": "referenced-mouse", "metadata": {}, "source": [ "## Governing equations\n", "\n", "In elasticity problems, it is often more natural to specify the Lagrangian energy functional of the system, rather than the residual equation. This is also a natural way to work within the FEniCS Project because the automatic differentiation capabilities can derive the residual and Jacobian symbolically for us.\n", "\n", "Consider a hyperelastic body $\\Omega$. Our task is to find the displacement field $u: \\Omega \\to \\mathbb{R}^2$ that minimises the total potential energy of the system\n", "\n", "$$\\min_{u \\in V} \\Pi(u), $$\n", "\n", "where $V$ is a function space that satifies boundary conditions on $\\partial \\Omega$.\n", "\n", "For a hyperelastic system\n", "\n", "$$ \\Pi(u) = \\int_{\\Omega} \\psi(\\varepsilon) \\; \\mathrm{d}x - \\int_{\\Omega} b \\cdot u \\; \\mathrm{d}x, $$\n", "\n", "where $\\psi$ is the *elastic stored energy density* of the system, $\\varepsilon$ is a general strain measure (possibly more than one), and $b$ is the body force per unit volume.\n", "\n", "The Neo-Hookean model is suitable for modelling elastic bodies made from cross-chained polymers e.g. plastics and rubbers. It can be defined by the following set of equations\n", "\n", "$$\n", "F(u) = I + \\nabla u, \\\\\n", "C = F^T F, \\\\\n", "\\mathrm{I}_C = \\mathrm{trace}(C), \\\\\n", "J = \\mathrm{det}(C), \\\\\n", "\\psi(J, \\mathrm{I}_C) = \\frac{\\mu}{2}(\\mathrm{I}_C - 2) - \\mu \\ln(J) + \\frac{\\lambda}{2} \\ln(J)^2.\n", "$$\n", "\n", "The weight can be modelled by setting:\n", "\n", "$$ b = (0, 0, -\\rho g),$$\n", "\n", "where $\\rho$ is the density of the beam and $g$ the acceleration due to gravity." ] }, { "cell_type": "code", "execution_count": null, "id": "retired-guess", "metadata": {}, "outputs": [], "source": [ "u = dolfinx.Function(V)\n", "\n", "# Identity tensor\n", "d = len(u) # Spatial dimension\n", "I = ufl.variable(ufl.Identity(d))\n", "\n", "# Deformation gradient\n", "F = ufl.variable(ufl.grad(u) + I)\n", "\n", "# Right Cauchy-Green tensor\n", "C = ufl.variable(F.T * F)\n", "\n", "# Invariants of deformation tensors\n", "Ic = ufl.variable(ufl.tr(C))\n", "J = ufl.variable(ufl.det(F))\n", "\n", "# Elasticity parameters\n", "# nu is to be adjusted as exercise to observe locking.\n", "E, nu = 1.0E4, 0.3\n", "mu = dolfinx.Constant(mesh, E/(2.0*(1.0 + nu)))\n", "lmbda = dolfinx.Constant(mesh, E*nu/((1.0 + nu)*(1 - 2.0*nu)))\n", "\n", "# Body load in undeformed configuration\n", "g, rho = -9.81, 1.0\n", "b = dolfinx.Constant(mesh, [0.0, rho*g])\n", "\n", "# Stored strain energy density (compressible neo-Hookean model)\n", "psi = ufl.variable((mu / 2.0) * (Ic - 2) - mu * ufl.ln(J) + (lmbda / 2.0) * (ufl.ln(J))**2)\n", "\n", "dx = ufl.Measure(\"dx\")\n", "\n", "Pi = psi*dx - ufl.inner(b, u)*dx" ] }, { "cell_type": "markdown", "id": "informative-outside", "metadata": {}, "source": [ "## Minimisation using Newton's algorithm\n", "\n", "Variants of Newton's algorithm is the *de facto* algorithm for solving minimisation problems when we have first and second-order derivatives available. With FEniCS we can calculate these derivatives automatically.\n", "\n", "We can write the directional derivative (2) of a functional $\\Pi$ at a point $u$ in a direction $v$ as\n", "\n", "$$\n", "D_u[\\Pi(u)][v] = \\frac{\\mathrm{d}}{\\mathrm{d}\\tau} \\Pi(u + \\tau v)|_{\\tau=0}.\n", "$$\n", "\n", "We can find one possible minimum $u^{*}$ (1) when the gradient of the functional $\\Pi$ is zero.\n", "\n", "$$\n", "F(u^{*}; v) = D_u[\\Pi(u^{*})][v] = 0 \\quad \\forall v \\in V.\n", "$$\n", "\n", "$F(u; v)$ is often called the *residual equation*. Physically it represents the balance of internal and external forces on the elastic body (i.e. Newton's second law) in weak form. The colon $;$ in $F(u; v)$ splits the arguments that are non-linear (here $u$), and the arguments that are linear (here $v$). Because of this splitting $F(u; v)$ is called a *semi-linear form*.\n", "\n", "(1) There is no guarantee of a unique (global) minimiser of $F$ for a general hyperelasticity problem.\n", "\n", "(2) Gateaux derivative https://en.wikipedia.org/wiki/Gateaux_derivative\n", "\n", "Let's calculate the residual using the FEniCS `derivative` function:" ] }, { "cell_type": "code", "execution_count": null, "id": "positive-picture", "metadata": {}, "outputs": [], "source": [ "v = ufl.TestFunction(V)\n", "F_res = ufl.derivative(Pi, u, v)" ] }, { "cell_type": "markdown", "id": "seeing-israeli", "metadata": {}, "source": [ "We can get the second derivatives (Jacobian or Hessian (3)) at a point $u$ in a direction $\\delta u$ by repeating the derivative procedure\n", "\n", "$$\n", "J(u; \\delta u, v) := D_{u}[F(u; v)][\\delta u]\n", "$$\n", "\n", "(3) Whether to call $J$ the Jacobian or Hessian depends on the problem; in some problems it is more natural to specify the residual $F(u; v)$ directly. Then $J(u, v, \\delta u)$ is the first derivatives (Jacobian) of the residual $F(u; v)$. In our elasticity problem, $J(u, v, \\delta u)$ is the second derivative of the functional $\\Pi$, hence Hessian is probably more accurate. In FEniCS-land people usually refer to it as the Jacobian in all cases (hence the variable name `J`).\n", "\n", "Let's calculate the symbolic Jacobian using the FEniCS derivative function:" ] }, { "cell_type": "code", "execution_count": null, "id": "another-subcommittee", "metadata": {}, "outputs": [], "source": [ "J = ufl.derivative(F_res, u)" ] }, { "cell_type": "markdown", "id": "objective-panama", "metadata": {}, "source": [ "As a quick reminder, Newton's method consists of the following steps:\n", "\n", "Let $u^0$ be an initial guess to our problem. We seek an improved approximation $u^{K+1}$ through a sequence of $K$ *Newton steps*:\n", "\n", "$$u^{k+1} = u^{k} + \\delta u^k \\quad 0, \\ldots, K.$$\n", "\n", "where the *Newton increments* $\\delta u^k$ can be found by solving the following equation:\n", "\n", "$$\n", "J(u^k; \\delta u^k, v) = -F(u^k; v) \\quad \\forall v \\in V\n", "$$\n", "\n", "$K$ is typically chosen online using a convergence criterion e.g.\n", "\n", "$$||F(u^K, v)||_{V^*} < \\epsilon$$\n", "\n", "where $\\epsilon$ is a small parameter. This condition defines when 'we have reached equilbrium'.\n", "\n", "DOLFINx has various Newton solvers already built-in, so we don't need to code our own solver. Here we will use the basic `dolfinx.cpp.nls.NewtonSolver`. In the next session we will see how to use `PETSc.SNES` which has far more advanced options available.\n", "\n", "The built-in `NewtonSolver` class works by calling user-defined functions to compute the Jacobian and residual. We have implemented `NonlinearPDE` class in the file [../python/nonlinear_pde_problem.py](https://gitlab.com/newfrac/newfrac-fenicsx-training/-/blob/main/notebooks/python/nonlinear_pde_problem.py) that contains these functions." ] }, { "cell_type": "code", "execution_count": null, "id": "bored-oakland", "metadata": {}, "outputs": [], "source": [ "problem = NonlinearPDEProblem(F_res, J, u, bcs)\n", "solver = dolfinx.cpp.nls.NewtonSolver(MPI.COMM_WORLD)\n", "\n", "# Set Newton solver options\n", "solver.atol = 1e-8\n", "solver.rtol = 1e-8\n", "solver.convergence_criterion = \"incremental\"\n", "\n", "# Set non-linear problem for Newton solver\n", "solver.setF(problem.F, problem.b)\n", "solver.setJ(problem.J, problem.A)\n", "solver.set_form(problem.form)\n", "\n", "num_its, converged = solver.solve(u.vector)\n", "if converged:\n", " print(f\"Converged in {num_its} iterations.\")\n", "else:\n", " print(f\"Not converged.\")\n", "\n", "with dolfinx.io.XDMFFile(MPI.COMM_WORLD, \"output/displacement.xdmf\", \"w\") as f:\n", " f.write_mesh(mesh)\n", " f.write_function(u)" ] }, { "cell_type": "markdown", "id": "aging-harvest", "metadata": {}, "source": [ "## Exercise 1: Computing stresses\n", "\n", "The conjugate stress measure $P$ (first Piola-Kirchoff stress) can be computed by differentiating the elastic energy density function with respect to the strain measure $F$.\n", "\n", "$$P = \\frac{\\partial \\psi}{\\partial F}$$\n", "\n", "UFL can compute the for $S$ symbolically using `ufl.diff`. Note that to use `ufl.diff` in this way you must have defined the first argument (`psi`) in terms of a second (`F`) wrapped with `ufl.variable`. Without this, you can only differentiate with respect to `u`." ] }, { "cell_type": "code", "execution_count": null, "id": "natural-robertson", "metadata": {}, "outputs": [], "source": [ "# First Piola-Kirchhoff Stress\n", "P = ufl.diff(psi, F)" ] }, { "cell_type": "markdown", "id": "patent-transcript", "metadata": {}, "source": [ "`P` above is a UFL symbolic expression. We need to evaluate it to actually obtain the stresses. One easy way to do this in FEniCS is via projection, which requires the solution of a linear system.\n", "\n", "Find $P_h \\in T$ such that\n", "\n", "$$(P_h, v) = (P(u), v) \\quad \\forall v \\in T$$\n", "\n", "where $P(u)$ is the known as it is a function of the computed displacement $u$, and $(\\cdot, \\cdot)$ is the $L^2$ inner product. Mathematically $P_h$ is the 'best' approximation to $P(u)$ in the space $T$." ] }, { "cell_type": "code", "execution_count": null, "id": "cutting-backing", "metadata": { "tags": [] }, "outputs": [], "source": [ "# Solve for stresses\n", "T = dolfinx.TensorFunctionSpace(mesh, (\"DG\", 0))\n", "\n", "s = ufl.TrialFunction(T)\n", "t = ufl.TestFunction(T)\n", "\n", "a = ufl.inner(s, t)*dx\n", "L = ufl.inner(P, t)*dx\n", "\n", "problem = dolfinx.fem.LinearProblem(a, L)\n", "P_h = problem.solve()\n", "\n", "with dolfinx.io.XDMFFile(MPI.COMM_WORLD, \"output/stress.xdmf\", \"w\") as f:\n", " f.write_mesh(mesh)\n", " f.write_function(P_h)" ] }, { "cell_type": "markdown", "id": "representative-damages", "metadata": { "tags": [] }, "source": [ "## Exercise 2: Compare the solutions from a linear strain measure vs non-linear strain measure.\n", "\n", "1. Make a duplicate of this notebook and open it.\n", "2. Implement a compressible linear Hookean model using the following equations for the strain measure, energy density and conjugate stress measure (Cauchy stress):\n", "\n", "$$\n", "\\varepsilon = \\tfrac{1}{2} (\\nabla u + (\\nabla u)^T)\\\\\n", "\\psi = \\mu \\mathrm{tr} (\\varepsilon^2) + \\frac{\\lambda}{2} [\\mathrm{tr}(\\varepsilon)]^2 \\\\\n", "\\sigma = \\frac{\\partial \\psi}{\\partial \\varepsilon}\n", "$$\n", "\n", "3. Modify the output filenames `displacement.xdmf` to e.g. `displacement_linear.xdmf`.\n", "4. Run the original and duplicate notebooks.\n", "5. Compare the deformed configuration from both models in Paraview. What do you observe?" ] }, { "cell_type": "markdown", "id": "timely-plate", "metadata": {}, "source": [ "## Exercise 3: Change the boundary conditions so the beam is clamped at both ends.\n", "\n", "1. Make a duplicate of this notebook and open it.\n", "2. Clamp the other end.\n", "3. Execute the code and view in Paraview." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.5" } }, "nbformat": 4, "nbformat_minor": 5 }