{ "cells": [ { "cell_type": "markdown", "source": [ "# Tutorial 3: Linear elasticity" ], "metadata": {} }, { "cell_type": "markdown", "source": [ "In this tutorial, we will learn\n", "\n", " - How to approximate vector-valued problems\n", " - How to solve problems with complex constitutive laws\n", " - How to impose Dirichlet boundary conditions only in selected components\n", " - How to impose Dirichlet boundary conditions described by more than one function\n", "\n", "## Problem statement\n", "\n", "In this tutorial, we detail how to solve a linear elasticity problem defined on the 3D domain depicted in next figure.\n", "\n", "![](../assets/elasticity/solid.png)\n", "\n", "We impose the following boundary conditions. All components of the displacement vector are constrained to zero on the surface $\\Gamma_{\\rm G}$, which is marked in green in the figure. On the other hand, the first component of the displacement vector is prescribed to the value $\\delta\\doteq 5$mm on the surface $\\Gamma_{\\rm B}$, which is marked in blue. No body or surface forces are included in this example. Formally, the PDE to solve is\n", "\n", "$$\n", "\\left\\lbrace\n", "\\begin{aligned}\n", "-∇\\cdot\\sigma(u) = 0 \\ &\\text{in} \\ \\Omega,\\\\\n", "u = 0 \\ &\\text{on}\\ \\Gamma_{\\rm G},\\\\\n", "u_1 = \\delta \\ &\\text{on}\\ \\Gamma_{\\rm B},\\\\\n", "\\sigma(u)\\cdot n = 0 \\ &\\text{on}\\ \\Gamma_{\\rm N}.\\\\\n", "\\end{aligned}\n", "\\right.\n", "$$\n", "\n", "The variable $u$ stands for the unknown displacement vector, the vector $n$ is the unit outward normal to the Neumann boundary $\\Gamma_{\\rm N}\\doteq\\partial\\Omega\\setminus\\left(\\Gamma_{\\rm B}\\cup\\Gamma_{\\rm G}\\right)$ and $\\sigma(u)$ is the stress tensor defined as\n", "$$\n", "\\sigma(u) \\doteq \\lambda\\ {\\rm tr}(\\varepsilon(u)) \\ I +2 \\mu \\ \\varepsilon(u),\n", "$$\n", "where $I$ is the 2nd order identity tensor, and $\\lambda$ and $\\mu$ are the *Lamé parameters* of the material. The operator $\\varepsilon(u)\\doteq\\frac{1}{2}\\left(\\nabla u + (\\nabla u)^t \\right)$ is the symmetric gradient operator (i.e., the strain tensor). Here, we consider material parameters corresponding to aluminum with Young's modulus $E=70\\cdot 10^9$ Pa and Poisson's ratio $\\nu=0.33$. From these values, the Lamé parameters are obtained as $\\lambda = (E\\nu)/((1+\\nu)(1-2\\nu))$ and $\\mu=E/(2(1+\\nu))$.\n", "\n", "\n", "## Numerical scheme\n", "\n", "As in previous tutorial, we use a conventional Galerkin FE method with conforming Lagrangian FE spaces. For this formulation, the weak form is: find $u\\in U$ such that $ a(u,v) = 0 $ for all $v\\in V_0$, where $U$ is the subset of functions in $V\\doteq[H^1(\\Omega)]^3$ that fulfill the Dirichlet boundary conditions of the problem, whereas $V_0$ are functions in $V$ fulfilling $v=0$ on $\\Gamma_{\\rm G}$ and $v_1=0$ on $\\Gamma_{\\rm B}$. The bilinear form of the problem is\n", "$$\n", "a(u,v)\\doteq \\int_{\\Omega} \\varepsilon(v) : \\sigma(u) \\ {\\rm d}\\Omega.\n", "$$\n", "\n", "The main differences with respect to previous tutorial is that we need to deal with a vector-valued problem, we need to impose different prescribed values on the Dirichlet boundary, and the integrand of the bilinear form $a(\\cdot,\\cdot)$ is more complex as it involves the symmetric gradient operator and the stress tensor. However, the implementation of this numerical scheme is still done in a user-friendly way since all these features can be easily accounted for with the abstractions in the library.\n", "\n", "## Discrete model\n", "\n", "We start by loading the discrete model from a file" ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "using Gridap\n", "model = DiscreteModelFromFile(\"../models/solid.json\")" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "In order to inspect it, write the model to vtk" ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "writevtk(model,\"model\")" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "and open the resulting files with Paraview. The boundaries $\\Gamma_{\\rm B}$ and $\\Gamma_{\\rm G}$ are identified with the names `\"surface_1\"` and `\"surface_2\"` respectively. For instance, if you visualize the faces of the model and color them by the field `\"surface_2\"` (see next figure), you will see that only the faces on $\\Gamma_{\\rm G}$ have a value different from zero.\n", "\n", "![](../assets/elasticity/solid-surf2.png)\n", "\n", "## Vector-valued FE space\n", "\n", "The next step is the construction of the FE space. Here, we need to build a vector-valued FE space, which is done as follows:" ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "order = 1\n", "\n", "reffe = ReferenceFE(lagrangian,VectorValue{3,Float64},order)\n", "V0 = TestFESpace(model,reffe;\n", " conformity=:H1,\n", " dirichlet_tags=[\"surface_1\",\"surface_2\"],\n", " dirichlet_masks=[(true,false,false), (true,true,true)])" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "As in previous tutorial, we construct a continuous Lagrangian interpolation of order 1. The vector-valued interpolation is selected via the option `valuetype=VectorValue{3,Float64}`, where we use the type `VectorValue{3,Float64}`, which is the way Gridap represents vectors of three `Float64` components. We mark as Dirichlet the objects identified with the tags `\"surface_1\"` and `\"surface_2\"` using the `dirichlet_tags` argument. Finally, we chose which components of the displacement are actually constrained on the Dirichlet boundary via the `dirichlet_masks` argument. Note that we constrain only the first component on the boundary $\\Gamma_{\\rm B}$ (identified as `\"surface_1\"`), whereas we constrain all components on $\\Gamma_{\\rm G}$ (identified as `\"surface_2\"`).\n", "\n", "The construction of the trial space is slightly different in this case. The Dirichlet boundary conditions are described with two different functions, one for boundary $\\Gamma_{\\rm B}$ and another one for $\\Gamma_{\\rm G}$. These functions can be defined as" ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "g1(x) = VectorValue(0.005,0.0,0.0)\n", "g2(x) = VectorValue(0.0,0.0,0.0)" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "From functions `g1` and `g2`, we define the trial space as follows:" ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "U = TrialFESpace(V0,[g1,g2])" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "Note that the functions `g1` and `g2` are passed to the `TrialFESpace` constructor in the same order as the boundary identifiers are passed previously in the `dirichlet_tags` argument of the `TestFESpace` constructor.\n", "\n", "## Constitutive law\n", "\n", "Once the FE spaces are defined, the next step is to define the weak form. In this example, the construction of the weak form requires more work than in previous tutorial since we need to account for the constitutive law that relates strain and stress. The symmetric gradient operator is represented by the function `ε` provided by Gridap (also available as `symmetric_gradient`). However, function `σ` representing the stress tensor is not predefined in the library and it has to be defined ad-hoc by the user, namely" ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "const E = 70.0e9\n", "const ν = 0.33\n", "const λ = (E*ν)/((1+ν)*(1-2*ν))\n", "const μ = E/(2*(1+ν))\n", "σ(ε) = λ*tr(ε)*one(ε) + 2*μ*ε" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "Function `σ` takes a strain tensor `ε`(one can interpret this strain as the strain at an arbitrary integration point) and computes the associated stress tensor using the Lamé operator. Note that the implementation of function `σ` is very close to its mathematical definition.\n", "\n", " ## Weak form\n", "\n", " As seen in previous tutorials, in order to define the weak form we need to build the integration mesh and the corresponding measure" ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "degree = 2*order\n", "Ω = Triangulation(model)\n", "dΩ = Measure(Ω,degree)" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ " From these objects and the constitutive law previously defined, we can write the weak form as follows" ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "a(u,v) = ∫( ε(v) ⊙ (σ∘ε(u)) )*dΩ\n", "l(v) = 0" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "Note that we have composed function `σ` with the strain field `ε(u)` in order to compute the stress field associated with the trial function `u`. The linear form is simply `l(v) = 0` since there are not external forces in this example.\n", "\n", "## Solution of the FE problem\n", "\n", "The remaining steps for solving the FE problem are essentially the same as in previous tutorial." ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "op = AffineFEOperator(a,l,U,V0)\n", "uh = solve(op)" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "Note that we do not have explicitly constructed a `LinearFESolver` in order to solve the FE problem. If a `LinearFESolver` is not passed to the `solve` function, a default solver (LU factorization) is created and used internally.\n", "\n", "Finally, we write the results to a file. Note that we also include the strain and stress tensors into the results file." ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "writevtk(Ω,\"results\",cellfields=[\"uh\"=>uh,\"epsi\"=>ε(uh),\"sigma\"=>σ∘ε(uh)])" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "It can be clearly observed (see next figure) that the surface $\\Gamma_{\\rm B}$ is pulled in $x_1$-direction and that the solid deforms accordingly.\n", "\n", "![](../assets/elasticity/disp_ux_40.png)" ], "metadata": {} }, { "cell_type": "markdown", "source": [ "## Multi-material problems\n", "\n", "We end this tutorial by extending previous code to deal with multi-material problems. Let us assume that the piece simulated before is now made of 2 different materials (see next figure). In particular, we assume that the volume depicted in dark green is made of aluminum, whereas the volume marked in purple is made of steel.\n", "\n", "![](../models/solid-mat.png)\n", "\n", "The two different material volumes are properly identified in the model we have previously loaded. To check this, inspect the model with Paraview (by writing it to vtk format as done before). Note that the volume made of aluminum is identified as `\"material_1\"`, whereas the volume made of steel is identified as `\"material_2\"`.\n", "\n", "In order to build the constitutive law for the bi-material problem, we need a vector that contains information about the material each cell in the model is composed. This is achieved by these lines" ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "using Gridap.Geometry\n", "labels = get_face_labeling(model)\n", "dimension = 3\n", "tags = get_face_tag(labels,dimension)" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "Previous lines generate a vector, namely `tags`, whose length is the number of cells in the model and for each cell contains an integer that identifies the material of the cell. This is almost what we need. We also need to know which is the integer value associated with each material. E.g., the integer value associated with `\"material_1\"` (i.e. aluminum) is retrieved as" ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "const alu_tag = get_tag_from_name(labels,\"material_1\")" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "Now, we know that cells whose corresponding value in the `tags` vector is `alu_tag` are made of aluminum, otherwise they are made of steel (since there are only two materials in this example).\n", "\n", "At this point, we are ready to define the multi-material constitutive law. First, we define the material parameters for aluminum and steel respectively:" ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "function lame_parameters(E,ν)\n", " λ = (E*ν)/((1+ν)*(1-2*ν))\n", " μ = E/(2*(1+ν))\n", " (λ, μ)\n", "end\n", "\n", "const E_alu = 70.0e9\n", "const ν_alu = 0.33\n", "const (λ_alu,μ_alu) = lame_parameters(E_alu,ν_alu)\n", "\n", "const E_steel = 200.0e9\n", "const ν_steel = 0.33\n", "const (λ_steel,μ_steel) = lame_parameters(E_steel,ν_steel)" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "Then, we define the function containing the constitutive law:" ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "function σ_bimat(ε,tag)\n", " if tag == alu_tag\n", " return λ_alu*tr(ε)*one(ε) + 2*μ_alu*ε\n", " else\n", " return λ_steel*tr(ε)*one(ε) + 2*μ_steel*ε\n", " end\n", "end" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "Note that in this new version of the constitutive law, we have included a third argument that represents the integer value associated with a certain material. If the value corresponds to the one for aluminum (i.e., `tag == alu_tag`), then, we use the constitutive law for this material, otherwise, we use the law for steel.\n", "\n", "Since we have constructed a new constitutive law, we need to re-define the bilinear form of the problem:" ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "a(u,v) = ∫( ε(v) ⊙ (σ_bimat∘(ε(u),tags)) )*dΩ" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "In previous line, pay attention in the usage of the new constitutive law `σ_bimat`. Note that we have passed the vector `tags` containing the material identifiers in the last argument of the function`.\n", "\n", "At this point, we can build the FE problem again and solve it" ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "op = AffineFEOperator(a,l,U,V0)\n", "uh = solve(op)" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "Once the solution is computed, we can store the results in a file for visualization. Note that, we are including the stress tensor in the file (computed with the bi-material law)." ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "writevtk(Ω,\"results_bimat\",cellfields=\n", " [\"uh\"=>uh,\"epsi\"=>ε(uh),\"sigma\"=>σ_bimat∘(ε(uh),tags)])" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ " Tutorial done!" ], "metadata": {} }, { "cell_type": "markdown", "source": [ "---\n", "\n", "*This notebook was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).*" ], "metadata": {} } ], "nbformat_minor": 3, "metadata": { "language_info": { "file_extension": ".jl", "mimetype": "application/julia", "name": "julia", "version": "1.6.7" }, "kernelspec": { "name": "julia-1.6", "display_name": "Julia 1.6.7", "language": "julia" } }, "nbformat": 4 }