{ "cells": [ { "cell_type": "markdown", "source": [ "# Tutorial 7: Darcy equation (with RT)" ], "metadata": {} }, { "cell_type": "markdown", "source": [ "In this tutorial, we will learn\n", " - How to implement multi-field PDEs\n", " - How to build div-conforming FE spaces\n", " - How to impose boundary conditions in multi-field problems\n", "\n", "## Problem statement\n", "\n", "In this tutorial, we show how to solve a multi-field PDE in Gridap. As a model problem, we consider the Darcy equations with Dirichlet and Neumann boundary conditions. The PDE we want to solve is: find the flux vector $u$, and the pressure $p$ such that\n", "\n", "$$\n", " \\left\\lbrace\n", " \\begin{aligned}\n", " \\kappa^{-1} u + \\nabla p = 0 \\ &\\text{in} \\ \\Omega,\\\\\n", " \\nabla \\cdot u = f \\ &\\text{in} \\ \\Omega,\\\\\n", " u \\cdot n = g \\ &\\text{on}\\ \\Gamma_{\\rm D},\\\\\n", " p = h \\ &\\text{on}\\ \\Gamma_{\\rm N},\\\\\n", " \\end{aligned}\n", " \\right.\n", "$$\n", "\n", "being $n$ the outwards unit normal vector to the boundary $\\partial\\Omega$. In this particular tutorial, we consider the unit square $\\Omega \\doteq (0,1)^2$ as the computational domain, the Neumann boundary $\\Gamma_{\\rm N}$ is the right and left sides of $\\Omega$, and $\\Gamma_{\\rm D}$ is the bottom and top sides of $\\Omega$. We consider $f = g \\doteq 0$ and $h(x) \\doteq x_1$, i.e., $h$ equal to 0 on the left side and 1 on the right side. The inverse of the permeability tensor, namely $\\kappa^{-1}(x)$, is chosen equal to\n", "\n", "$$\n", "\\begin{pmatrix}\n", " 100 & 90 \\\\\n", " 90 & 100\n", "\\end{pmatrix}\n", "\\text{ for } \\ x \\in [0.4,0.6]^2, \\text{ and }\n", "\\begin{pmatrix}\n", " 1 & 0 \\\\\n", " 0 & 1\n", "\\end{pmatrix}\n", "\\ \\text\t{otherwise.}\n", "$$\n", "\n", "In order to state this problem in weak form, we introduce the following Sobolev spaces. $H(\\mathrm{div};\\Omega)$ is the space of vector fields in $\\Omega$, whose components and divergence are in $L^2(\\Omega)$. On the other hand, $H_g(\\mathrm{div};\\Omega)$ and $H_0(\\mathrm{div};\\Omega)$ are the subspaces of functions in $H(\\mathrm{div};\\Omega)$ such that their normal traces are equal to $g$ and $0$ respectively almost everywhere in $\\Gamma_{\\rm D}$. With these notations, the weak form reads: find $(u,p)\\in H_g(\\mathrm{div};\\Omega)\\times L^2(\\Omega)$ such that $a((u,q),(v,q)) = b(v,q)$ for all $(v,q)\\in H_0(\\mathrm{div};\\Omega)\\times L^2(\\Omega)$, where\n", "\n", "$$\n", "\\begin{aligned}\n", "a((u,q),(v,q)) &\\doteq \\int_{\\Omega} v \\cdot \\left(\\kappa^{-1} u\\right) \\ {\\rm d}\\Omega - \\int_{\\Omega} (\\nabla \\cdot v)\\ p \\ {\\rm d}\\Omega + \\int_{\\Omega} q\\ (\\nabla \\cdot u) \\ {\\rm d}\\Omega,\\\\\n", "b(v,q) &\\doteq \\int_{\\Omega} q\\ f \\ {\\rm d}\\Omega - \\int_{\\Gamma_{\\rm N}} (v\\cdot n)\\ h \\ {\\rm d}\\Gamma.\n", "\\end{aligned}\n", "$$\n", "\n", "\n", " ## Numerical scheme\n", "\n", "In this tutorial, we use the Raviart-Thomas (RT) space for the flux approximation [1]. On a reference square with sides aligned with the Cartesian axes, the \\ac{rt} space of order $k$ is represented as $Q_{(k+1,k)} \\times Q_{(k,k+1)}$, being the polynomial space defined as follows. The component $w_\\alpha$ of a vector field $w$ in $Q_{(k+1,k)} \\times Q_{(k,k+1)}$ is obtained as the tensor product of univariate polynomials of order $k+1$ in direction $\\alpha$ times univariate polynomials of order $k$ on the other directions. That is, $\\nabla\\cdot w \\in Q_k$, where $Q_k$ is the multivariate polynomial space of degree at most $k$ in each of the spatial coordinates. Note that the definition of the \\ac{rt} space also applies to arbitrary dimensions. The global FE space for the flux $V$ is obtained by mapping the cell-wise RT space into the physical space using the Piola transformation and enforcing continuity of normal traces across cells (see [1] for specific details).\n", "\n", " We consider the subspace $V_0$ of functions in $V$ with zero normal trace on $\\Gamma_{\\rm D}$, and the subspace $V_g$ of functions in $V$ with normal trace equal to the projection of $g$ onto the space of traces of $V$ on $\\Gamma_{\\rm D}$. With regard to the pressure, we consider the discontinuous space of cell-wise polynomials in $Q_k$.\n", "\n", "## Discrete model\n", "\n", "We start the driver loading the Gridap package and constructing the geometrical model. We generate a $100\\times100$ structured mesh for the domain $(0,1)^2$." ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "using Gridap\n", "domain = (0,1,0,1)\n", "partition = (100,100)\n", "model = CartesianDiscreteModel(domain,partition)" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "## Multi-field FE spaces\n", "\n", "Next, we build the FE spaces. We consider the first order RT space for the flux and the discontinuous pressure space as described above. This mixed FE pair satisfies the inf-sup condition and, thus, it is stable." ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "order = 1\n", "\n", "V = TestFESpace(\n", " reffe=:RaviartThomas, order=order, valuetype=VectorValue{2,Float64},\n", " conformity=:HDiv, model=model, dirichlet_tags=[5,6])\n", "\n", "Q = TestFESpace(\n", " reffe=:QLagrangian, order=order, valuetype=Float64,\n", " conformity=:L2, model=model)" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "Note that the Dirichlet boundary for the flux are the bottom and top sides of the squared domain (identified with the boundary tags 5, and 6 respectively), whereas no Dirichlet data can be imposed on the pressure space. We select `conformity=:HDiv` for the flux (i.e., shape functions with $H^1(\\mathrm{div};\\Omega)$ regularity) and `conformity=:L2` for the pressure (i.e. discontinuous shape functions).\n", "\n", "From these objects, we construct the trial spaces. Note that we impose homogeneous boundary conditions for the flux." ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "uD = VectorValue(0.0,0.0)\n", "U = TrialFESpace(V,uD)\n", "P = TrialFESpace(Q)" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "When the singe-field spaces have been designed, the multi-field test and trial spaces are expressed as arrays of single-field ones in a natural way." ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "Y = MultiFieldFESpace([V, Q])\n", "X = MultiFieldFESpace([U, P])" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "## Numerical integration\n", "\n", "In this example we need to integrate in the interior of $\\Omega$ and on the Neumann boundary $\\Gamma_{\\rm N}$. For the volume integrals, we extract the triangulation from the geometrical model and define the corresponding cell-wise quadrature of degree of exactness at least 2 as follows." ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "trian = Triangulation(model)\n", "degree = 2\n", "quad = CellQuadrature(trian,degree)" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "In order to integrate the Neumann boundary condition, we only need to build an integration mesh for the right side of the domain (which is the only part of $\\Gamma_{\\rm N}$, where the Neumann function $h$ is different from zero). Within the model, the right side of $\\Omega$ is identified with the boundary tag 8. Using this identifier, we extract the corresponding surface triangulation and create a quadrature with the desired degree of exactness." ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "neumanntags = [8,]\n", "btrian = BoundaryTriangulation(model,neumanntags)\n", "bquad = CellQuadrature(btrian,degree)" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "## Weak form\n", "\n", "We start by defining the permeability tensors inverses commented above using the `@law` macro." ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "const kinv1 = TensorValue(1.0,0.0,0.0,1.0)\n", "const kinv2 = TensorValue(100.0,90.0,90.0,100.0)\n", "@law function σ(x,u)\n", " if ((abs(x[1]-0.5) <= 0.1) && (abs(x[2]-0.5) <= 0.1))\n", " return kinv2⋅u\n", " else\n", " return kinv1⋅u\n", " end\n", "end" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "With this definition, we can express the integrand of the bilinear form as follows." ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "px = get_physical_coordinate(trian)\n", "\n", "function a(x,y)\n", " v, q = y\n", " u, p = x\n", " v⋅σ(px,u) - (∇⋅v)*p + q*(∇⋅u)\n", "end" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "The arguments `x` and `y` of previous function represent a trial and a test function in the multi-field test and trial spaces `X` and `Y` respectively. In the first lines in the function definition, we unpack the single-field test and trial functions from the multi-field ones. E.g., `v` represents a test function for the flux and `q` for the pressure. These quantities can also be written as `y[1]` and `y[2]` respectively. From the single-field functions, we write the different terms of the bilinear form as we have done in previous tutorials.\n", "\n", "In a similar way, we can define the forcing term related to the Neumann boundary condition." ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "nb = get_normal_vector(btrian)\n", "h = -1.0\n", "function b_ΓN(y)\n", " v, q = y\n", " (v⋅nb)*h\n", "end" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "## Multi-field FE problem\n", "\n", "Finally, we can assemble the FE problem and solve it. Note that we build the `AffineFEOperator` object using the multi-field trial and test spaces `Y` and `X`." ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "t_Ω = LinearFETerm(a,trian,quad)\n", "t_ΓN = FESource(b_ΓN,btrian,bquad)\n", "op = AffineFEOperator(X,Y,t_Ω,t_ΓN)\n", "xh = solve(op)\n", "uh, ph = xh" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "Since this is a multi-field example, the `solve` function returns a multi-field solution `xh`, which can be unpacked in order to finally recover each field of the problem. The resulting single-field objects can be visualized as in previous tutorials (see next figure)." ], "metadata": {} }, { "outputs": [], "cell_type": "code", "source": [ "writevtk(trian,\"darcyresults\",cellfields=[\"uh\"=>uh,\"ph\"=>ph])" ], "metadata": {}, "execution_count": null }, { "cell_type": "markdown", "source": [ "![](../assets/darcy/darcy_results.png)\n", "\n", "## References\n", "\n", "[1] F. Brezzi and M. Fortin. *Mixed and hybrid finite element methods*. Springer-Verlag, 1991." ], "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.3.1" }, "kernelspec": { "name": "julia-1.3", "display_name": "Julia 1.3.1", "language": "julia" } }, "nbformat": 4 }